CURL这个组件大家都熟悉,是一个用来发送HTTP请求的命令行工具,同时也有libcurl的库,可以方便其他软件集成自己的能力。
10.13(次日)更新
好基友发来微信消息,发现我俩的分析逻辑很多地方对不上。经过之后,发现是我俩的CURL版本不同,不同的CURL版本,buffer的逻辑也不同。
- CURL v7.85.0 之前,目标buffer是个hardcode的值,大小为600byte (SOCKS_REQUEST_BUFSIZE)。
- CURL v7.85.0 之后,目标buffer是个变动的值,在cmd tool里默认是10kb,通过增加 –limit-rate 参数可以影响这个buffer大小。
关于状态机的描述存在不准确,CURL作者的描述是需要一个slow enough的socks5代理,实际测试任意代理均可,但是如果curl以一个极缓慢速度运行时候(例如通过debug step by step运行),则不会触发crash,这个与作者通过socks5实现的状态机有关,鉴于精力未再继续分析。
漏洞背景
最近,CURL的作者在讨论区提到了近期会发布一个新版本,其中会修复两个漏洞,其中一个漏洞为高危漏洞,并且提到:“所有使用liburl的软件都会被这个漏洞影响”。
之后又有热心网友提前发现了补丁和更多的漏洞细节
随着更多的热心网友的参与和漏洞细节被公开,长舒了一口气,还好还好,影响面有限,不是个啥大事。
漏洞Re-Search
太长不看不看版:
根据SOCKS协议,一个目的地的最大长度为 255 个字节(byte),如果是小于255个字节的域名并且使用socks5h协议的话,curl会给服务器解析,对于超过这个长度的域名,curl的做法是在本地解析,解析完之后再通过socks服务拉数据。
本来是没啥问题的,但是在具体的实现中有问题,某些特定场景下两种模式混乱了,导致超过长度的域名也会被memcopy到给socks服务器的buffer里, 于是就overflow了。
漏洞要被触发,还有四个前置条件,锦旭搂了搂代码,分析了一下原因,和大家分享一下子,如果你也在分析的话,希望对你有帮助 ^_^
条件1: 请求使用socks5h
socks5h是啥嘞,socks协议主要是管传输的,根据这个协议的设计,目的地址可以是个IP,也可以是个域名,如果是域名的话CURL就把这个叫做 socks5h(带hostname),就是下面这个BND.ADDR👇
+----+-----+-------+------+----------+----------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
- 用到socks5h == CURL DNS解析这个活交给了代理服务器,传一个域名+Port,和HTTP REQ的内容。
- 用到socks5 == CURL 本地解析好域名对应的IP,传一个IP+Port,和HTTP REQ的内容。
如果不是socks5h,则不影响。
条件2: 本地的buffer设置小于65kb
这个主要是CURL对URL的限制,最长只处理65kb的URL,过长就不干活了,走不到漏洞触发点。
条件3: SOCKS服务器没有及时回复HELLO请求
这个是酱紫的,有一个关键值是socks5_resolve_local
,这玩意是个全局变量,只在CONNECT_SOCKS_INIT
阶段里会改动这个,可以关注 lib/socks.c这个文件中的 do_SOCKS5 方法。
正常的逻辑:
1. socks5_resolve_local 默认为false
2. socks5初始化阶段 CONNECT_SOCKS_INIT 阶段设置为true,写入协商头,状态机到CONNECT_SOCKS_READ阶段,跳转到 CONNECT_SOCKS_READ_INIT(读返回阶段)
一次读取完协商包,到 CONNECT_REQ_INIT 阶段
3. CONNECT_SOCKS_INIT检查发现socks5_resolve_local为True,调用CONNECT_RESOLVED,将DNS地址填充到协议的指定字段。
4. 和SOCKS5协议服务器进行正常数据交互
黑客利用
1. 调用do_SOCKS5方法, socks5_resolve_local默认为false
2. socks5初始化阶段 CONNECT_SOCKS_INIT 阶段设置为true,写入协商头,状态机到CONNECT_SOCKS_READ阶段,跳转到 CONNECT_SOCKS_READ_INIT(读返回阶段)
3. CONNECT_SOCKS_READ_INIT读数据的时候,发现没有读到server的回包数据或者没读到指定数量(2个字节)的回包数据,返回CURLPX_OK
4. 上层继续调用 do_SOCKS5方法, socks5_resolve_local默认为false,此时状态机状态为 CONNECT_SOCKS_READ,继续从socks代理中读取数据(假设读取成果)
5. 走到 CONNECT_REQ_INIT,检查发现 socks5_resolve_local为false,认为应该远程解析,走到CONNECT_RESOLVE_REMOTE,把hostname整个memory-copy过去,于是产生堆溢出。
两个流程里1、2阶段一样,区别就在于3这个阶段,正常逻辑是一次读完了,流程退出。漏洞逻辑是一次没读完,方法退出导致走了 CONNECT_RESOLVE_REMOTE
逻辑后导致了堆溢出。
限制4: 可控的比buffer长的Host的输入
这个不用多解释,要溢出,当然要源长度要比目的长度大,不然没法覆盖堆外的数据了。
稳定触发
在github上有人公开利用方法,但其实是不能稳定触发的,主要原因在于从tcp里读两个字节的握手包,还要求这两个字节通过两次recv才能收到,这个需要自己做一个恶意的socks5服务器才能稳定触发。构造一socks5服务器,请移步 https://gist.github.com/xen0bit/0dccb11605abbeb6021963e2b1a811d3?permalink_comment_id=4722774#gistcomment-4722774 查看
关键代码:
...
wrapper.sendall(VER)
time.sleep(5)
wrapper.sendall(method)
...
漏洞证明
漏洞检测
根据漏洞原理,漏洞的特点是会在本地进行一次DNS请求,请求的对象是一个长度超长的域名,可以以此为特征,在DNS日志中添加对应规则以检测此威胁。
对于超域名规范限制的超长域名,UDP包不一定成功,上级的dns resolver也不一定会有记录,正在研究,研究后更新。
域名RFC限制:最大不超过253char,每个.中的内容不超过63个字符。
引用
- 漏洞作者的文章,很多内容援引于此:https://hackerone.com/reports/2187833
- CURL作者的文章:https://daniel.haxx.se/blog/2023/10/11/how-i-made-a-heap-overflow-in-curl/
- 网友讨论:https://news.ycombinator.com/item?id=37840581
- 一个热心网友的 gist: https://gist.github.com/xen0bit/0dccb11605abbeb6021963e2b1a811d3
复现的代码 https://github.com/imfht/CVE-2023-38545
牛逼