1、事情的起因
在和V&N的师傅打KalmarCTF2023时,碰到了一题叫做healthycalc的题目,这一题当时好像是没有任何思路的,因为整个程序就是一个计算器,但是后面在调试过程中发现了Pymemcache是怎么处理指令的运行和获取的
2、审计源码
题目源码如下:
1 | import asyncio, os |
程序的代码并不是很多,因此要看起来还是挺容易的,单纯看题目源码你是啥也不会发现的,因为就是一个很正常的运算,唯一有点可疑的估计也就是cache_lookup
对缓存的处理了,也就是这里存在着漏洞隐患,这一点是TEL师傅指出的,我则是沿着这里往下深挖。
先看题目吧,就是一个计算器
是通过路由变量进行获取的,然后将其进行一些运算,审计源码可以得知,计算的过程大概分为两步,首先判断是不是第一次进行运算,如果是第一次则存入memcache缓存当中,之后再取出相同的数据时,直接从缓存中拿去,实际上就是redis做的事情,但是这里用的是memcache
在set和get处下2个断点,跟踪调试分析一波:
首先进入的是get方法,跟进去看看:
这里的key是由一系列名称拼接起来的,k = f"{operation.name}_{lhs}_{rhs}"
随后是获取self.client,跟进看看是怎么处理的:
然后是拿到了client对象,随后直接在最后return返回:
退出该方法,回到刚刚的父类调用
上面是先拿到了client,然后再调用get方法,这下跟进get方法:
调用了self.get
传入的参数是get,也就是让memcache运行get指令,这是最后一个get方法,接下里是主要逻辑:
首先对key进行一次encode,这里的encode实际上就是utf-8编码一下:
然后获取server,然后根据一开始给的cmd=get
,它会拼接一段完整的命令给memcache运行:
这里完整的命令就是b'get __main__._mult_1270_960'
,也就是获取key对应的value,然后send_cmd执行该命令,到这里就是get命令执行的点
剩下的还有对应的set方法,流程也是一样的,这里直接跳到上面说的获取fullcmd的步骤看看运行了什么指令:
也是先调用client的set:
然后运行memcache的set指令:
fullcmd为b'set __main__._mult_1270_333 2 86400 6 \r\n422910'
,也就是在memcache服务器运行了该指令,那么对于该部分我们已经清楚了流程,如果我们可以控制运行指令,是不是就会有更多的思路了?
3、Memcached CRLF注入
有关memcached的指令在网上找一找就好了,和redis其实很像,以KV对的形式存储的
- set:
set key flags exptime length value
- get:
get key1 key2 key3
我们现在要说的是CRLF注入
所谓CRLF注入在SSRF的时候也很常见,其实算是请求走私的一种方法,而对于Memcached也存在这种风险,仔细观察上面的2个fullcmd:'get __main__._mult_1270_960'
'set __main__._mult_1270_333 2 86400 6 \r\n422910'
单看get指令可能看不出什么,可以看到set指令时有个回车符隔开,末尾的也就是运算结果V,memcached协议是以回车符进行分割的,可以参考这一篇文章:
我们可以用\r\n
来分割指令,达到运行多条指令的效果,最终也就等于memcached指令可控,如:[http://localhost:5000/calc/add/595/2628%0d%0aset](http://localhost:5000/calc/add/595/2628%0d%0aset) uwsgi_file__app_chall._add_1_1 0 3600 3%0d%0a222
这样我们可以覆盖1+1的值为222了
走私成功
这里关于uwsgi_file__app_chall
有点讲究,这里对应源代码中的f"{operation.name}"
,也就是函数的__name__,我们需要观察dockerfile:ENTRYPOINT ["uwsgi" "--http-socket" ":5000" "--master" "--processes" "8" "--threads" "4" "--uid" "kalmar" "--gid" "kalmar" "--wsgi-file" "/app/chall.py"]
他是用uwsgi运行的flask项目,因此主程序不会是__main__
在这一点也是卡了我很久,因为这个名字不对,因此导致我覆盖值不成功XD
现在我们访问一下/calc/add/1/1
看看值为多少:
成功覆盖为222,说明CRLF注入是成功的,那么现在思路又拓宽了,也就是我们可以执行memcached服务器上任意指令,那么能不能进行RCE呢?
4、Pymemcached RCE探究
我们都知道在进行存储和取出数据时,一般都会进行序列化和反序列化操作,pymemcached也不例外,查阅资料可以看到:
https://sendapatch.se/projects/pylibmc/reference.html
pymemcached需要依赖pylibmc库,而pylibmc库中就引入了pickle这个概念,pylibmc库是辅助缓存进行存储的:
在这里指出,在特定条件下会进行pickle序列化和反序列化,那么到这里思路就清晰了许多,也就是该怎么去触发pickle反序列化的问题了,想要触发就得看懂条件,那么这里就涉及到memcached指令中关于flag的解释了:
https://blog.csdn.net/xyz_dream/article/details/90453222
flag这个参数在memcached中实际上起到一个标记作用,就是标记你存入的值到底是什么类型,然后依据flag的值不同,进行不同种类的序列化和反序列化,那么啥时候才可以触发pickle呢?我们可以去查阅pylibmc的源码:
https://github.com/lericson/pylibmc/blob/master/src/_pylibmcmodule.h#L75
在这里可以清楚的看见,当flag为1时进行pickle相关操作,因此flag的值写死为1,其他的无所谓,然后是在啥时候进行pickle反序列化的呢?
在Discord中有一位大牛为我指出了调用栈:
可以看到在进行get方法去除时,会对存入的数据进行pickle反序列化,因此得以RCE
5、EXP的编写
这一点算是留给我的一个作业,因为目前网上并没有啥公布的Exp,因此打算自己试试,思路确定完就是写个py脚本,回顾侧重点,flag的值,payload的length,opcode编写
最后写出来的东西也是这样:
1 | #Author:Boogipop |
解释一下payload,第一个payload将恶意opcode存入,第二个是去除该数据时触发pickle反序列化,因此getshell:
至此算是基本结束了
6、其他的设想
Pymemcached会有这样的RCE漏洞,那么其他语言操作memcached服务器呢?首先CRLF注入肯定都会有的,但是RCE由于pypickle比较特殊,因此算特例,但是不代表别的就一定安全,既然有序列化和反序列化,那么就一定有安全隐患,因此这是留给我们自行去探索的,这里也可以拿来出题,会是一个不错的题目哟
注意事项和最后想说的话
首先说一下环境吧
- Windows Docker
- Pycharm
上面的调试有时候并不是一个程序,假如需要在Pycharm调试的话,请确保以下几点:
- Python!=3.7
- Dependency:python-memcached、pylibmc、flask[async](搭在windows)
- 虚拟机起个memcached
- 源码里的memcached地址改一下XD
另外强烈推荐使用windows Docker进行分析:
在win10 docker中外部的访问和内部执行的命令都会一五一十的记录在内,可以更加直观的去分析,这一点我表示VeryNice
那么就是最后想说的话了:
这一题写的时候是真的很开心,由一个正常的程序,根据调试发现了原理,再进行猜想与假设,然后去实践构造,最后RCE,是一个很令人兴奋的过程。这题是由3个师傅(包括我)一起解出的,也是感受到了战队里其他师傅的实力有多么强劲,希望自己这个菜鸡在后面不会拖后腿QWQ
About this Post
This post is written by Boogipop, licensed under CC BY-NC 4.0.