SecMap - 非常见协议大礼包

本文最后更新于:1 个月前

一份非常用协议大礼包,SSRF 篇的前置知识

SecMap - 非常用协议大礼包

知识点介绍

这里说的 “非常用” 是相对于 HTTP 这种非常知名的协议来说的。对于同是搞安全的橘友来说,像 gopher 这种协议其实也挺常用的,各位无需纠结这个,意思传达到了就好。

协议

gopher://

协议详情:https://zh.wikipedia.org/wiki/Gopher_%28%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%29

gopher 协议,人送外号“万金油”,因为利用此协议可以攻击内网的 Web 应用、Redis、MySQL、FTP、Memcache 等等。本质上是因为 gopher 携带的数据可以插入 \r\n%0d%0a),而很多组件就是利用 \r\n 来分隔连续的两条命令,所以它可以与很多组件进行连续交互,那么也就可以很方便地进行攻击了。

gopher 协议的格式:gopher://+ip:端口+/+_ + TCP/IP 数据

需要注意的是:

  1. gopher 协议的默认端口为 70
  2. 这里面的 _,这是分隔符,其实可以用任意 ASCII 字符
  3. 分隔符后面的 TCP/IP 数据就是发出去的数据,数据部分如果有特殊字符必须要进行 url 编码,这样 gopher 协议才能正确解析
  4. 大部分 PHP 并不会开启 fopen 的 gopher wrapper
  5. PHP file_get_contents 的 gopher 协议不能 URLencode
  6. PHP file_get_contents 的 gopher 协议的 302 跳转有 bug,导致利用失败
  7. PHP 的 curl 默认不跟随 302 跳转
  8. curl/libcurl v7.43 上 gopher 协议存在 %00 截断,v7.49 可用

下面举几个比较典型的例子,其实原理都是一样的,很容易拓展到攻击其他组件。

发出 HTTP 请求

例如,我们打算通过 gopher 协议,来完成一次 http 请求,那么我们最少需要发出这些数据:

1
2
GET / HTTP/1.1
Host: baidu.com

那么首先,把这些数据进行 url 编码:

1
2
3
4
In [1]: from urllib import parse

In [2]: parse.quote('GET / HTTP/1.1\r\nHost: baidu.com\r\n\r\n')
Out[2]: 'GET%20/%20HTTP/1.1%0D%0AHost%3A%20baidu.com%0D%0A%0D%0A'

然后用 curl 即可通过 gopher 协议完成一次 http 请求:

当然 POST 也是可以的。

攻击 MySQL

同理,也可以访问其他服务,这里演示一下 MySQL 的。

注意,mysql 的实验,需要配置 skip_ssl 来禁用 ssl,因为 gopher 无法处理 ssl 握手。

按照我们实际想要达成的目的去使用 mysql,我称之为“录像”,期间用 wireshark 抓包,点击跟踪 TCP 流:

简单起见,我这里仅仅是连上去之后执行了 select 'tr0y{Th1s_is_fl4g}'; 就退出了(注意,这里一定要退出,原因见下面)。那么可以得到下面的数据:

注意我们只需要发出数据,所以选择客户端发送到 MySQL 服务器的数据包,然后导出原始数据:

wireshark 所谓的“原始数据”其实就是十六进制的数据,而我们小学二年级就知道,url 编码就是字符的十六进制数据表示,前面加个 %,故可得:

1
ba00000185a6ff0900000001ff00000000000000000000000000000000000000000000005472307900010063616368696e675f736861325f70617373776f7264007c035f6f73086f737831302e3136095f706c6174666f726d067838365f36340f5f636c69656e745f76657273696f6e06382e302e32330c5f636c69656e745f6e616d65086c69626d7973716c045f706964053238303337076f735f757365720a6d6163723070686167330c70726f6772616d5f6e616d65056d7973716c2300000003000173656c65637420404076657273696f6e5f636f6d6d656e74206c696d697420311e00000003000173656c6563742027747230797b546831735f69735f666c34677d270100000001

(这里的操作实际上会产生 4 个请求数据包,直接拼接在一起即可)

发出:

整个过程可以看做是先录制与 MySQL 交互的过程,然后利用 gopher 回放,所以录像中不能有随机因素,比如 ssl 握手,否则就无法回放。

最后总结一下需要注意的事情:

  1. 目标 MySQL 必须关闭 ssl(配置 skip_ssl
  2. 录制的时候,需要用 -h 指定 host,否则如果是搭建在本地的 mysql,会直接通过 sock 文件(默认是 /tmp/mysql.sock)进行进程通信,不走网卡所以抓不到包。
  3. 目标 MySQL 必须配置无需认证。如果是用 caching_sha2_password 进行认证(版本 >8.0 默认就是这个认证模式)的用户,那么 mysql 服务端会要求客户端必须启用 ssl;如果是用 mysql_native_password 进行认证的用户,那么在认证过程中会有随机的字符出现(应该是用于 challenge)。以上两种认证模式均无法通过 gopher 利用。
  4. 录制完毕之后,没有手动退出 mysql cli,那么就需要在最后加上 %01%00%00%00%01,否则 curl 会一直卡着(虽然这时候抓包已经可以看到回显了),原因是 curl 没收到服务端发来的结束符,认为还有剩余的数据在管道中,于是继续等待,表现出来就是 curl 一直卡着。

攻击 Redis

没啥特别的,简直就是一个 redis-cli。Redis 有密码无密码都可,可以利用 \r\n 拼接来一次性执行多条命令:

1
2
# 即 auth 123456\r\nPING\r\nquit
curl gopher://127.1:6001//auth%20123456%0d%0aPING%0d%0aquit

当然,也可以用来爆破密码。

dict://

协议详情:http://dict.org/rfc2229.txt

dict 协议是一个字典服务器协议,就是用来查单词的那种字典。字典服务器本来是为了让客户端使用过程中能够访问更多的字典源。

dict 协议的格式:dict://+ip:端口+/ + TCP/IP 数据

与 gopher 相比,dict 携带的数据无法插入 \r\n(只能插入 \\r\\n),所以对于大部分组件来说,只能执行一条命令,所以如果一个组件可以一步一步操作(比如 redis),那么才可以利用,那么很明显,需要认证的 redis 是无法通过 dict 攻击的(即使你知道密码也没用),但是可以用来爆破密码。

这里举个 Redis 的例子,由于 dict:// 相对比较鸡肋一些,所以算是一个低配的 redis-cli。

首先演示一下 set/get:

再来个写 webshell 的:

需要注意的点:

  1. 若最后的 payload 需要用空格,那么在 dict 里需要用 : 表示,即 dict://127.0.0.1:80/set:key:1 等于 set key 1
  2. curl 在通过 dict 执行的时候,会在前后自动加上客户端版本和 QUIT:

    这也是为什么上面那个 redis 的例子中,第一行永远是错误,最后一行永远是 +OK 的原因
  3. 由于 dict:// 协议在出现 ? 的时候会截断后面的数据:

    这里的思路就是先把数据传给组件,然后让组件去完成转义,比如 set key "1\x3f1" 对于 redis 来说等于 set key "1?1",所以 dict:// 就可以这样:curl 'dict://127.1:6379/set:key:"1\x3f1"'。上面那个写 webshell 的例子就是因为这个原因所以进行了转义。

file://

协议详情:https://docs.microsoft.com/en-us/previous-versions//aa767731(v=vs.85)

读取本地文件,这个没啥好说的:curl file:///etc/passwd

比如浏览器读取本地文件的时候,用的也是这个协议:

对于 PHP 的文件包含有个骚操作就是,可以创建一个名为 <?php phpinfo(); ?>.txt 的文件,然后压缩上传,接下来直接包含即可(需要绝对路径):include("file://wwwhtml/upload/test.zip");,当然,.tar.zip 都可以。至于原理,贴个图大家就都懂啦:

data://

  • 作用:Data URI scheme 是在 RFC2397 中定义的,目的是将一些小的数据,直接嵌入到网页中,从而不用再从外部文件载入
  • 条件:
    • allow_url_include: On
    • PHP >= 5.2.0

格式为 data:(//)[class](/)[type](;base64),payload

  1. // 可以省略
  2. / 可以省略,特殊情况在下面
  3. [class][type]: 可以省略,任意字符;如果要写其中一个,那么 / 不可省略,另一个为可空。
  4. ;base64 可以省略;如果要加的话,payload 必须经过 base64 编码

这些在 PHP 里都是可以的:

1
2
3
4
5
6
data://tr0y/tr0y;base64,PD9waHAgcGhwaW5mbygpOz8+
data:tr0y/tr0y;base64,PD9waHAgcGhwaW5mbygpOz8+
data:tr0y/;base64,PD9waHAgcGhwaW5mbygpOz8+
data:/;base64,PD9waHAgcGhwaW5mbygpOz8+
data:;base64,PD9waHAgcGhwaW5mbygpOz8+
data:,<?php phpinfo();?>

该协议类似于 php://input,区别在于 data:// 是直接获取后面跟着的内容。浏览器一般都支持这个协议,最常用的莫过于展示小图片了;在 CTF 中常用于执行任意 PHP 代码。

每个组件支持的会稍微有点区别,例如 data:;,<?php phpinfo();?> 在 Chrome 可以,但是在 PHP 不行。

php://

php://:访问各个输入/输出流(I/O streams)。PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符;内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。

可以看官方文档的介绍:https://www.php.net/manual/zh/wrappers.php.php

属于 PHP 中的伪协议,一般受限于 php.ini 中的配置,所以下文会一并列出使用条件。

php://input

  • 作用:访问请求的原始数据的只读流
  • 条件:
    • allow_url_include: On
    • form 表单里的 enctype 不为 multipart/form-data(默认为 application/x-www-form-urlencoded)。体现在请求头里就是 Content-Type 不能为 multipart/form-data

作用有点拗口,其实 php://input 可以理解为“文件路径”,但是 PHP 在遇到这个伪协议的时候,不会真去本地文件找这个路径然后读取文件内容,而是直接将 POST 的数据当做文件内容。

比如 readfile、file_get_contents、include,都支持此伪协议。

在 CTF 中常用于执行任意 PHP 代码。

php://filter

  • 作用:用于数据流打开时,对数据进行筛选、过滤。
  • 条件:无

格式:
php://filter/[0]=[1]/resource=[2]

其中:

  • [0]: 可选 read/write
  • [1]: 就是过滤器,可以设定一个或多个过滤器名称,以管道符(|)分隔即可。比如:convertstring,更多可参考官方文档:https://www.php.net/manual/zh/filters.php
  • [2]: 必选的值,为要筛选过滤的数据流,通常是本地文件的路径,绝对路径和相对路径均可以使用。

示例:php://filter/read=convert.base64-encode/resource=flag.php

表示读取本地文件 flag.php,并进行 base64 编码。

在 CTF 中常用于读取 PHP 的源码。

phar://

  • 作用:属于 PHP 伪协议,phar(PHP Archive) 是 PHP 里类似于 JAR 的一种打包文件。
  • 条件:
    • PHP >= 5.3.0

phar:// 的利用场景示例:

1
2
3
4
<?php
$files = $_GET['file'];
include($files);
?>

对于这样的例子,先上传一个 zip 压缩包,里面是一个 txt 文件,内容是:<?php phpinfo(); ?>,在知道绝对路径(/www/upload/test.zip)之后,可以利用 phar:// 来执行这段代码,payload: phar:///www/upload/test.zip/test.txt

phar:// 还有反序列化的利用方式,见:

https://www.tr0y.wang/2021/06/01/SecMap-unserialize-php/#%E5%88%A9%E7%94%A8-phar-%E4%BC%AA%E5%8D%8F%E8%AE%AE

zip://

  • 作用:属于 PHP 伪协议,用于访问 zip 压缩流

格式:zip://[压缩文件绝对路径]#[压缩文件内的路径以及文件名]

compress.*://

  • 作用:属于 PHP 伪协议,用于访问压缩流

compress.zlib://

  • 作用:访问 .gz 压缩流

示例:

  1. 创建 .gz: tar -zcvf test.gz ./test.txt
  2. payload: compress.zlib://test.gz

compress.bzip2://

  • 作用:访问 .bz2 压缩流

示例:

  1. 创建 .bz2: tar -jcvf test.bz2 ./test.txt
  2. payload: compress.bzip2://test.bz2

ldap://

ldap 主要有三种协议:

  1. ldap://: 基础的 LDAP 协议
  2. ldaps://: SSL/TLS + ldap
  3. ldapi://: IPC + LDAP

对于 ldaps:// 来说,由于存在 SSL/TLS 握手过程,所以没啥用;而 ldapi 属于 IPC,也没法使用;所以只剩下 ldap://,默认是 389 端口,主要有两种姿势。

第一种与上面这些协议类似,同样它支持用 \r\n 分割 payload,但是由于 ldap 协议在一开始的时候,会尝试先建立 cleartext 连接(establish cleartext connection),所以它相比 gopher 来说,开头多了一些杂乱的字符。这里给个攻击 redis 的示例:

1
2
3
4
import ldap  # pip install python-ldap

conn = ldap.initialize("ldap://127.1:6379")
conn.simple_bind_s("\r\n", "\r\nPING\r\n")

它会把建立 cleartext 连接的过程与传输用户名密码的过程合并到一个数据包里,而在用户名和密码可以插入 \r\n 并且还不会有其他字符干扰,所以这样可以很方便地利用:

个人感觉,大多数组件实现的 ldap 协议,应该默认都是这样合并的。有不合并的例子吗?有,比如 curl,如果你直接执行:curl -v ldap://127.1:6379,它会先发送建立cleartext 连接的请求:

并且等待服务端返回这样的响应:


大家可以对比一下客户端的两种行为,上面是没合并的,下面是合并的:

那么对于我们攻击 redis 来说,肯定是要用合并的,因为 redis 不可能会按照 ldap 协议返回响应。

那么如果我们非要用 curl 来做实验呢?可以用这个命令:

1
2
3
4
5
6
curl --negotiate -v --user "
":"

PING

" 'ldap://127.0.0.1:6379/'

结果


第二种稍有些复杂,需要 JNDI 等前置知识,等讲反序列化的时候再补上好了。

rmi://

需要 JNDI 等前置知识,等讲反序列化的时候再补上好了。

总结

最近本来在写 SSRF 的,快写完了,发现先写一下这些协议作为前置知识比较合适,当然它们可以造成的安全问题不仅仅有 SSRF,还有文件包含等。从个人经验来看,这些协议还是在 CTF 中用的多一些,各大 CTF 中出现过非常多精彩的题目,比如 hxpCTF 的一道通过 file_put_contents 来完成 SSRF:

https://rmb122.com/2020/12/30/hxp-CTF-resonator-Writeup-SSRF-via-file-put-contents/

最后多说一句,我感觉很多 CTF 题目可以用来做后门,应该很难检测出来吧…


下一篇是 SSRF
(日常拖更)


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!