Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

订阅源联系隐私© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
StealMyBitcoin-PoC — Steal My Bitcoin 挑战的公开版本 | Kitploit
工具/GitHubGitHub/killaboi/stealmybitcoin-poc
内存取证漏洞分析漏洞利用Web应用程序漏洞利用CTFPayload 开发二进制利用实验室与实践
GitHubkillaboi/stealmybitcoin-poc

StealMyBitcoin-PoC

Steal My Bitcoin 挑战的公开版本

查看仓库
21641611天前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

由 ❤️ 赞助 hextree.io

Steal My Bitcoin CTF 概念验证

针对 Steal My Bitcoin 挑战的一次实时 PHP 回调,从开始到结束,全部包含在一个仅使用标准库的 Python 文件中。

Python Dependencies Target Lab

本 README 介绍 stealmybitcoin_poc.py,这是一个单文件 Python 脚本,复现了我在 Steal My Bitcoin CTF 中使用的请求序列。它仅使用 Python 标准库。该脚本自行构建请求,在继续之前检查每一次泄露,并记录一次新运行产生的 HTTP 字节。

这大概可以做得更简单,不需要所有这些额外的调试垃圾,而且正如我在关于 Steam 本地权限提升的 John Hammond 视频中发现的那样,你大概可以清理掉这个丑陋的 PoC。

[!NOTE] ⚠️ 说实话,请清理它,或者制作你自己的更好版本,一个不会被 Sol 揉皱的版本。 ⚠️

以下所有内容都是针对 Docker 实验环境编写的,而不是针对线上挑战。该实验环境是一个位于 nginx 之后的本地 Alpine PHP 8.4.26 CLI 服务器,与偏移量校准所依据的构建和 worker 模型相同。一分钟内启动它,整条链就能在你自己的机器上端到端运行。真正解决该挑战的线上运行则作为历史记录保留在底部。

  stage                 what goes out                        what comes back
  ─────────────────────────────────────────────────────────────────────────────
  0  health      GET /php-version.php                 ->  PHP 8.4.26 confirmed
  1  heap leak   249,042 B producer + 8 x   6,672 B   ->  256 B cookie
                 (OOM poisons a worker)                   └─ Zend chunk base
  2  code leak   251,557 B producer + 8 x  92,388 B   ->   16 B cookie
                 (OOM poisons a worker)                   └─ zval_ptr_dtor
  3  callback    368,812 B POST                       ->  forged HashTable runs
                 (fake HashTable + command)               └─ zif_system(cmd)
  ─────────────────────────────────────────────────────────────────────────────
  20 HTTP requests at most. No looping, no retries.

简要版本

  1. 一次小型 multipart 上传会让 PHP 消耗远超其网络传输大小的内存。
  2. 当它在会话上传进度解析过程中途耗尽内存时,PHP 会在随后被复用的 worker 中留下陈旧状态。
  3. 正确构造后续请求,字节就会通过会话 cookie 泄漏出来。我将这些字节解读为一个堆地址和一个 PHP 代码地址。
  4. 有了这两个地址,最后一个请求会将一个伪造的 PHP 结构放到清理逻辑能够找到的位置,随后清理逻辑会调用 zif_system,即 PHP 对 system 的处理函数。
  5. 该命令会通过仍然保持打开的连接返回一个新的验证标记和一个 print-flag 结果。

为什么需要两个地址? 操作系统会独立地随机化 PHP 代码和堆的落点。堆泄漏告诉脚本将伪造结构放在哪里,代码泄漏告诉脚本它想调用的函数实际位于何处。来自较早 worker 或较早运行的地址不能安全替代,而且每次我尝试时,该尝试都只是失败了。

目录

  • 实验环境
  • 从这里开始
  • 每个请求的作用
    • 1. 一个小型生产者如何达到内存限制
    • 2. 堆地址如何被恢复
    • 3. PHP 代码地址如何被恢复
    • 4. 最终请求如何到达 system
  • 解读结果
  • 它实际成功的频率
  • 成功完成它的实时运行
  • 术语简释
  • 致谢

实验环境

此 PoC 中的偏移量是针对 运行内置 CLI 服务器并启用四个 worker 的 Alpine PHP 8.4.26 校准的。实验环境在回环地址上、nginx 之后精确复现了该配置。

[!IMPORTANT] 构建版本比版本字符串更重要。Debian php:8.4.26-fpm 栈报告相同的 PHP/8.4.26,但二进制布局不同,因此代码指针指纹将永远无法匹配,运行会在代码泄漏阶段中止。请使用下面的 Alpine 镜像。

三个小文件。将它们放入一个空目录:

public/php-version.php(健康检查端点,也是 PoC 唯一需要的页面)

<?php
header('Content-Type: text/plain');
echo 'PHP ', PHP_VERSION, "\n";
echo 'PID: ', getmypid(), "\n";

print-flag(挑战二进制文件的替代品,必须可执行)

#!/bin/sh
printf '%s\n' 'LOCAL_DUMMY_WIF_NEVER_USE'

nginx.conf

events {}
http {
    server {
        listen 80;
        server_name _;
        location / {
            proxy_pass http://ctf-cross-php:8000;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
        }
    }
}

然后启动它:

docker network create ctf-cross-net

docker run -d --name ctf-cross-php --network ctf-cross-net \
  --read-only --tmpfs /tmp:rw,size=32m,mode=1777 --memory 1g \
  -v "$PWD/public:/app:ro" \
  -v "$PWD/print-flag:/usr/local/bin/print-flag:ro" \
  -e PHP_CLI_SERVER_WORKERS=4 \
  php:8.4.26-fpm-alpine3.23 php -S 0.0.0.0:8000 -t /app

docker run -d --name ctf-cross-nginx --network ctf-cross-net \
  -p 127.0.0.1:18166:80 \
  -v "$PWD/nginx.conf:/etc/nginx/nginx.conf:ro" \
  nginx:1.27-alpine

检查一下:

$ curl -s http://127.0.0.1:18166/php-version.php
PHP 8.4.26
PID: 1

PID 会随应答的 worker 不同而变化,这没关系。PoC 检查的是版本行。

用 docker rm -f ctf-cross-nginx ctf-cross-php && docker network rm ctf-cross-net 将其拆除。

[!TIP] 在 Windows 上,请从 PowerShell 运行那些 docker run 命令,或将 $PWD 替换为完整的驱动器路径,如 C:/lab。在 Git Bash 中,$PWD 会展开为 MSYS 路径,例如 /tmp/...,Docker Desktop 会在其自身的 VM 内解析该路径。此时绑定挂载会静默地在 nginx.conf 和 print-flag 本应所在的位置创建空目录,容器启动后便会处于损坏状态。

关于该配方有几点说明。镜像使用的是 fpm 标签,但容器运行的是 php -S,因此 SAPI 为 cli-server,这正是校准所针对的环境。PHP_CLI_SERVER_WORKERS=4 使泄漏阶段有多个 worker 可供竞争。我自己的实验环境还传入了 --cap-add SYS_PTRACE --security-opt seccomp=unconfined,以便能够将跟踪器附加到 worker 上;PoC 本身从不这样做,因此上述命令中省略了这些参数。


从这里开始

Python 3.11 或更新版本,仅使用标准库,无需安装任何东西:

python .\stealmybitcoin_poc.py --url http://127.0.0.1:18166/php-version.php

不带参数运行它,它会打印帮助信息并退出,不会访问网络。

一次正常的运行如下所示:

[health] PHP 8.4.26 confirmed
[heap] strict Zend base 0x78113e800000
[code] strict zval_ptr_dtor 0x615b1f512930
{"stage": "final", "nonce": "1ef0e73d8615157a", "execution_proof_verified": true, "response_path": "..."}

注意该 zval_ptr_dtor 值的低 21 位:0x112930,正是脚本期望从该 Alpine 构建中获取的指纹。这就是用于拒绝不匹配镜像的检查。

返回的响应体如下:

<br />
<b>Fatal error</b>:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 131072 bytes) in <b>Unknown</b> on line <b>0</b><br />
1ef0e73d8615157a
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
0ec066e8b91457c7c19b5612f3e2ded7f117df08f402693669c1ffc2cfc24942  /usr/local/bin/print-flag
LOCAL_DUMMY_WIF_NEVER_USE

PHP 致命错误、本次运行生成的 nonce、id、它解析到的二进制文件哈希,以及 dummy 标志。验证通过时退出状态为 0,其他情况为 1。

[!WARNING] 生产者会故意让 PHP 触及内存上限。只应在行为已获授权的环境中运行。脚本只会与挑战端点或回环实验 URL 通信,其他任何目标都会在发出数据包之前中止。

一次运行意味着一次健康检查 GET、最多两批泄漏请求,以及至多一次最终请求。不循环、不重试。每个 multipart 请求体都保持在 1 MiB 以下,不过每批泄漏请求都会在一条连接上流水线发送九个独立请求。

标志作用
--url实验端点或挑战地址。执行任何操作都必须提供。
--output-dir写入本次运行证据的位置。默认为 poc-run-<UTC timestamp>。

每个请求的作用

步骤发出什么、应返回什么原因
健康检查一个普通的 GET /php-version.php 必须返回 HTTP 200、PHP 8.4.26 和 X-Powered-By: PHP/8.4.26。避免将内存上限载荷发送到已变更或不健康的端点。
堆泄漏一个 249,042 字节的 multipart 生产者,随后是八个流水线发送的 6,672 字节消费者。生产者必须返回 PHP 128 MiB 内存上限致命错误。某个消费者可能返回一个 256 字节的 PHPSESSID cookie。该 cookie 中至少四个看似指针的值必须一致指向同一个对齐的 2 MiB Zend 堆基址。
代码泄漏一个 251,557 字节的生产者,随后是八个流水线发送的 92,388 字节消费者。某个消费者可能返回一个 16 字节的 PHPSESSID cookie。其前八个字节必须编码 16,后八个字节必须看起来像本地校准的 zval_ptr_dtor 指针。
最终回调一个 368,812 字节的 multipart POST。一个伪造的 PHP HashTable 将清理指向 zif_system 并提供命令参数。新的 nonce、id 输出和 print-flag 二进制哈希是回调确实执行过的证据。

即最多 20 个 HTTP 请求:一次健康检查、每批泄漏九个、一次最终请求。一批中的全部八个消费者都会在读取第一个响应之前发出,这有助于时序,但也意味着如果生产者失败,你无法撤回它们。一批失败或格式错误会中止下一阶段。

1. 小型生产者如何触及内存上限

生产者是携带 PHPSESSID 和 PHP_SESSION_UPLOAD_PROGRESS 字段的 multipart/form-data。PHP 在仍在解析请求时就会处理上传进度。数百个空文件控件获得类似 f[0][x][x]... 的名称,其中包含数十个嵌套的 [x] 段,因此请求体在网络上保持在约 250 KiB,而解析它会生成大量嵌套 PHP 数组及其伴随的所有分配。生产者还携带 1,001 个不同的空 cookie 名称,这是请求构建器当时被配置发送的内容。

你想要的致命错误会表明 PHP 耗尽了其 128 MiB 内存上限。你真正追求的是这种特定失败在工作进程中留下的陈旧状态。

[!IMPORTANT] 单独一个内存上限致命错误或 nginx 502 并不能证明存在地址泄漏或代码执行。

2. 如何恢复堆地址

下一个请求针对与该陈旧会话状态相关的内存。它重复一个数字表单键,然后插入一个精心构造的值,改变 PHP 写入 PHPSESSID 时使用的长度。命中正确的复用工作进程和正确的堆布局后,cookie 会返回 256 字节而不是通常的 32 字节,其中包含相邻的堆数据。

heap_candidate() 将该 cookie 按小端 64 位字遍历,只有当至少四个看似指针的值落在同一个对齐的 2 MiB 堆区域时才会接受一个基址。除此之外,运行器还要求 HTTP 200、预期的 PHP 版本、全部八个消费者返回有效的 cookie 长度,并且恰好只有一个一致的基址。任何一项不满足就会停止。

下载工具