Python Web内存马多框架植入技术详解
2024-7-10 14:11:15 Author: govuln.com(查看原文) 阅读量:8 收藏

前  言

内存马作为一种常见的攻击与权限维持手段,往往多见于Java Web应用中,然而在Python Web场景下却并不多见这种攻击。

本文将针对Flask、Tornado与Django三个在日常开发中使用频率较高的框架,探寻在Python Web场景下的内存马种植方法,文中所有场景均为抽象出的理想场景,仅做可行性讨论。

Flask
01

老版本Flask内存马种植方法

在网上针对Flask内存马的探讨,均在SSTI场景下,并且payload都相同,格式化后payload如下:

在Flask中所有定义的路由都会使用一个装饰器 app.route而这个装饰器就是调用了 add_url_rule

所以在payload中,直接调用了Flask中的 add_url_rule 函数来动态增加一条路由实现内存马。然而在最新版本的Flask中,如果直接使用这个payload会发现将抛出一个异常。

02

AssertionError出现原因

在回溯这个异常的时候会发现,这个异常出现在 setupmethod 这个装饰器中的一个校验函数。

跟踪一下 _got_first_request 会发现在 full_dispatch_request 中会强制赋值成 True

这样一来就会导致在任何请求中,都无法再调用到使用了 setupmethod 装饰器的函数。

03

add_url_rule实现

既然无法绕过这个校验,那么回头重新看一下 add_url_rule 中的实现。

省略掉开头的处理代码,会发现在函数末尾的处理中,将 rule_obj 对象添加到了 url_map 中,之后将 view_func 作为了 view_functions 字典中 endpoint 键的值,所以理论上来讲,可以通过直接操作这两个变量来完成一次手动的 add_url_rule

url_mapview_functions 的定义如下:

04

种植方法

一个任意代码执行的理想场景如下:

在 eval 场景下,没有办法执行多条代码,所以这里需要发送两条请求来完成操作,当前上下文中可以直接使用 app 对象,构造第一条请求向 url_map 中新增一条 UrlRule

这个时候已经可以访问 /flask-shell 路由,但是由于 view_functions 中并不存在路由指定的 endpoint 所以会报错。

之后再构造第二条请求,向 view_functions 中增加对应 endpoint 的实现:

为了灵活性这里需要http传参来控制执行的命令,但是这里会发现上下文中并不存在 request_context,当前 app 对象中的 request_context 是一个函数。

那么可以通过函数的 __globals__ 属性来获取当前的全局变量字典,在这其中就有需要的 RequestContext 对象。

所以修改一下第二条payload如下:

05

结果

将两条payload发送完成后,即可新增一条任意命令执行的路由 /flask-shell

Tornado
01

场景代码

02

种植方法

Tornado 的路由一般情况下都会在实例化 tornado.web.Application 的时候传入,最初想到的办法和 flask 相同,考虑是否能找到其中存放路由的列表来直接操作,在阅读 Application 的代码时确实发现在构造函数中存在这样的列表。

而在继续向下阅读的时候,发现在 Application 类中存在一个类似flaskadd_url_rule 的函数 add_handlers,这个函数用来支持配置虚拟主机,并且在之后会将指定的路由加入当前的路由表中。

既然有一个现成的函数可供调用,那么就可以放弃去直接操作列表,转而考虑怎么来构造这个函数的参数。

03

参数构造

首先看到这个函数声明接受两个参数 host_patternhost_handlers,其中 host_pattern 是一个字符串没有什么需要多考虑的,这个场景下直接构造 .* 匹配所有域名即可,而第二个参数 host_handlers 较为复杂一点,类型为 _RuleList,查看一下这个类型定义。

add_rules 中,整个传入的值都会被作为构造参数来实例化一个 Rule 对象,构造函数如下:

第一个参数类型为 Matcher,如果自己来构造的话会比较麻烦,但是看到 add_rules 中的处理,会判断一次传入值,如果是 tuple 或者 list 并且第一个值是字符串,那么就会调用一次 PathMatches 返回一个 Matcher 对象,所以这里考虑直接传入路由字符串,让系统来做一次自动转换。

接下来考虑路由对应的 handler, 这往往需要是一个 tornado.web.RequestHandler 的子类,那么这里可以直接使用 type 函数来创建一个对应基类的对象,当 type 函数接受三个参数时,第一个参数为类名,第二个参数为基类元组,第三个参数为类属性/方法的字典,函数原型如下:

所以使用下面的payload即可创建一个合法 RequestHandler

04

结果

将所有分析结合起来,即可在当前场景下构造出下面的请求payload。

在发出这条请求之后,应用正常返回。

之后便可以访问 /tornado-shell 来执行任意系统命令。

Django
01

场景代码

虽然 Django 的代码结构不太相同,但由于所有路由都定义在 app/urls.py#urlpatterns 中,所以大体思路没有什么差别,首先考虑如何获取到这个列表,然后再进行操作。

02

获取app.urlpatterns

Django 中,root app下会有一个 settings.py 文件用于定义应用配置,其中 ROOT_URLCONF 指定了当前应用路由入口,在当前场景下的 ROOT_URLCONF 为:

首先考虑如何获取到 settings 这个对象,得益于当前场景下可以使用 request,所以使用其中函数的 __globals__ 属性来获取到当前的全局变量字典,其中就可以找到。

那么直接将其导入,就可以获得当前应用的入口app。

在这个基础上,就可以通过访问 urls.urlpatterns 来操作路由列表了。

03

Django.urls.path

在路由定义中,每一条路由都会调用 path 函数来进行定义,传入的参数相对也比较简单,就是路由:函数的对应,第一个路由参数不需要考虑,传入字符串即可,需要考虑的是如何构造第二个参数,查看 path 函数定义。

其中会发现 view 参数除了判断是否为 View(list, tuple) 之外,还判断了是否是一个可调用对象,那么这里就比较简单了,直接构造一个 lambda 函数即可。

04

请求构造

根据之前的分析结果,可以得到下面的构造流程:

1. 获取app.urlpatterns

2. 调用path函数,返回一条新路由

当前场景下需要返回一个 http.HttpResponse,所以需要额外引入 django 来进行调用

3. 将新路由append到app.urlpatterns中实现内存马

05

结果

将构造好的payload发送后,便可通过访问 /shell 来实现任意命令执行。

总  结

在刚开始寻找Python Web内存马资料的时候,发现除了Flask SSTI之外,找不到除了这个场景、框架之外的其他资料,或许确实在这个条件下的利用场景过于稀少,并且在有RCE的情况下更多的考虑也并不是获取一个webshell。

本文仅在最理想的场景下,简单探讨了一下在三个常见Python Web框架中种植内存马的方法,除了文中提到的思路还有一些其他的方法,比如可以在 Flask 中操作各种reqeust hook函数来实现,或者在 Tornado 直接操作 wildcard_router等,原理上来说都大同小异。

【版权说明】

本作品著作权归4uuu Nya所有

未经作者同意,不得转载

4uuu Nya

天工实验室安全研究员,Nu1L Team,Web安全与代码审计。

往期回顾
01
以CVE-2024-26229为例分析Windows RDBSS机制
02
Apple操作系统-XNU内核下FlowDivert网络协议漏洞分析
03
阿里云WebShell伏魔挑战赛新思路挖掘
04
macOS中四类TCC BYPASS案例分析
每周三更新一篇技术文章  点击关注我们吧!

文章来源: https://govuln.com/news/url/0PRd
如有侵权请联系:admin#unsafe.sh