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

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

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

订阅源联系隐私© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
php2shell-deep-dive — 深入解析 PHP2Shell 的技术细节:将两个 PHP 核心 CVE(内存耗尽崩溃与释放后使用)串联起来,进而实现堆布局操纵、ASLR 信息泄露以及远程代码执行。 | Kitploit
工具/GitHubGitHub/djad04/php2shell-deep-dive
内存取证漏洞分析漏洞利用逆向工程Web应用程序漏洞利用CTF论文与研究学习与教育二进制利用
GitHubdjad04/php2shell-deep-dive

php2shell-deep-dive

深入解析 PHP2Shell 的技术细节:将两个 PHP 核心 CVE(内存耗尽崩溃与释放后使用)串联起来,进而实现堆布局操纵、ASLR 信息泄露以及远程代码执行。

634天前尚未审核

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
查看仓库网站

我在 2026 年 10 月 26 日开始撰写这项研究,目前还不完全确定何时能够完成。

在撰写本文时,关于 PHP2Shell 的信息仍然非常少,尤其是涉及漏洞和利用过程的详细技术分析方面。

说实话,我已经厌倦了那些只触及表面、却没有真正展示内部发生了什么情况的解释。我想做一些不同的事情:一份恰当、深入的技术讲解,探索整个过程,审视每一个重要细节,不仅解释发生了什么,还要解释为什么发生以及如何发生。

这项研究基于我对 PHP 源代码的理解、我的实验,以及我在试图理解这些漏洞时所进行的调查。我想把所有这些碎片拼凑起来,分享我的发现,并希望能让其中一些底层概念对其他人来说更容易理解。

我的主要目标之一是在不牺牲技术深度的前提下让内容易于理解。我希望具备基本 C 语言知识和一般编程经验的人能够跟上,即使他们之前没有任何底层内存利用方面的经验。

我也希望这项研究能帮助澄清一些目前解释不清或被误解的内容。不过,我想坦诚说明:这是我自己对这些漏洞的技术理解,并非官方分析,也不是绝对可靠的参考。我自己仍在学习、调查并串联线索。过程中可能存在错误、不准确之处,或者我误解的地方。

如果你发现任何不正确的地方、有不同的解读,或者认为某些内容值得进一步调查,请不要犹豫与我联系。我会真诚地感谢纠正、讨论和贡献,这些都有助于让这项研究更加准确,对所有人更有用。

毕竟,分享这一切的意义就在于共同学习,帮助更多人理解底层实际发生的事情。


快速导航

已经熟悉基础知识了?直接跳转到你关心的部分:

  • 开始之前:栈与堆、内存分配基础
  • 这为什么危险?:陈旧指针的故事、UAF 读取与写入
  • 陈旧指针:它从何而来:PHP 会话与 OOM 交互中的 UAF 起源故事
  • 构建攻击请求:真正触发 OOM 的 HTTP 请求
  • 目标:我们需要泄露什么以及为什么(堆指针 + 代码指针)
  • 目标结构体:zend_string:PHP 在构建会话 cookie 时读取的结构体
  • 回收内存:请求 #2 与目标 zend_string 结构体
  • 堆布局深度剖析:我们如何将伪造数据精确放置到正确的内存槽位
  • multipart_buffer_read_body 之谜:那个差点让我崩溃的隐藏分配
  • 两次泄露而非一次:为什么堆泄露和代码泄露需要不同的校准
  • 代码泄露(阶段 3):泄露函数指针以绕过 ASLR
  • 指纹识别:我们如何知道拿到了正确的指针
  • 解析堆泄露:在 256 字节的混乱中找到 chunk 基址
  • 1+8 流水线:两次泄露实际上是如何被传递的
  • 复用分配器伪造结构体:pDestructor 劫持思路
  • 触发器:PHP 在这里干脏活,真实的 zend_hash_destroy 代码
  • 三合一结构体:在 384 字节内构造 ForgedArray + FakeExecuteData + FakeString
  • 最终载荷(阶段 4):伪造 HashTable 以劫持 pDestructor 并实现 RCE
  • 引子机制:被释放的引子在 bucket[2] 中的幽灵如何触发销毁链
  • 命令输出技巧:在 /proc 中找到你自己的 TCP socket
  • 完整流水线:全部四个阶段端到端
  • 针对不同构建进行校准:每个数字如何来自特定 PHP 构建的堆分析
  • 现实世界的摩擦:为什么这种攻击在真实生产服务器上要困难得多
  • 偷走我的比特币 CTF:引发这一切的挑战
  • 致谢:鸣谢与参考资料

如果你对堆利用还不熟悉,或者想要完整体验,继续往下读就好。我写这篇文章就是为了让人从头读到尾。


开始之前

在深入之前,有几件事大家都应该记住。随着研究的推进,它们会变得特别有用。正如我之前提到的,我希望这篇讲解对所有人都易于理解,所以我会花时间解释哪怕是最基础的概念。如果你已经熟悉它们,尽管跳过!

栈与堆

在典型的程序中,内存被组织成不同的区域,包括栈和堆。

例如,在 C++ 中:

int main() {
    int a = 30;       // Typically stored on the stack
    int* b = new int(30);  // b is typically on the stack,
                           // but the integer it points to is on the heap
}

这里,a 是一个局部变量,包含值 30。指针 b 也是一个局部变量,但它包含的是在堆上分配的某个整数的地址。

内存分配的工作原理

PHP 主要用 C 编写,因此我们必须理解内存分配的工作原理。

操作系统为进程提供其自己的虚拟地址空间。随后,进程的内存分配器在可供其使用的内存范围内管理分配。

例如,在 C++ 中,我们有 new、malloc() 以及其他分配内存的方式。

分配器通常会尝试复用已释放的内存,而不是每次都向操作系统请求新的内存。

此外,当对象的生命周期结束时,其内存中存储的数据不一定会立即被擦除。那块内存可以简单地被标记为可供复用。


为了更好地理解这一点,让我们简要看一下以下代码:

#include <cstdlib>
#include <iostream>

int main() {
    void* a = malloc(64);

    free(a);

    void* b = malloc(64);

    std::cout << a << "\n" << b << std::endl;

    free(b);
    return 0;
}

在这里,当我们打印 a 和 b 时,可能会发现它们包含相同的数值内存地址。

为什么?因为在我们调用 free(a) 之后,malloc() 可能会复用之前分配给 a 的内存,从而避免再获取另一块内存。

这并不保证每次都会发生,但这是一种常见行为。


这为什么危险?悬空指针的故事

这就引出了问题的核心。我们知道内存会被回收,但那些持有这些内存地址的变量会怎样呢?

当你调用 free(a) 时,内存分配系统只是更新其内部记录,将那块内存标记为可复用。然而,该内存中的数据并不会被清除,指针变量本身也不会神奇地重置为 NULL。除非程序员显式清除它,否则指针会继续持有与之前完全相同的内存地址。

这个残留的变量就是我们所说的悬空指针(stale pointer,或称 dangling pointer)。

悬空指针可能在多个方面造成危险。其中最关键的两个是:读取不再属于原对象的数据,以及通过旧指针写入从而破坏新对象。

读取另一个对象的数据(释放后使用)

想象我们为一个用户账户分配内存:

struct User {
    char username[24];
    int is_admin;
};

int main() {
    // 1. Allocate memory for a user
    User* a = (User*)malloc(sizeof(User));

    // Initialize the original user
    strcpy(a->username, "alice");
    a->is_admin = 0;

    // We keep a copy of the pointer to demonstrate the bug
    User* a_stale = a; 

    // 2. Free the memory. The object is gone, but a_stale still holds the address!
    free(a);

现在,就在这之后,假设程序在别处又进行了一次分配,并且分配器复用了同一个地址:

User* b = (User*)malloc(sizeof(User));

strcpy(b->username, "jack");
b->is_admin = 1;

因此,b 现在正好位于 a 原先所在的位置。但 a_stale 仍然指向那里,因为我们释放了 a,而没有任何东西更新 a_stale。

因此,仍在使用 a_stale 的代码以为自己在查看 Alice 的数据,但实际上它可能正在访问 Jack 的数据:

if (a_stale->is_admin) {
    // This may run, even though Alice was never an admin
    printf("Welcome admin %s\n", a_stale->username);
}

这样,我们可能访问到本不该看到的数据,例如 Jack 的用户名,而程序可能会做出错误的授权决策。

从技术上讲,访问已释放的内存是 C/C++ 中的“未定义行为”,因此确切结果无法保证,取决于编译器和系统。但这说明了根本问题。

然而在现实中,事情并没有这么简单。你可能会想:“是啊,这挺好,但是……”

要让这生效,我们首先需要一个悬空指针。在真实攻击中,我们并不是在编写受害者的程序,所以不能像处理 a_stale 那样故意保存一个指向变量的指针。这个漏洞必须已经存在于代码内部……

攻击者只能影响最终落入那个已释放内存槽的内容。

目前,只需确保你深入理解了这一点,因为这个问题的答案将是我们正在讨论的 PHP CVE 中最重要的部分。

修改另一个对象的数据(内存破坏)

现在想象相反的情况:程序在内存被释放后,仍然使用旧指针向该内存写入数据。

#include <cstdlib>
#include <cstdio>

int main() {
    int* a = (int*)malloc(sizeof(int));
    *a = 42;

    int* stale = a;  // Keep another pointer to the allocation

    free(a);

    // Another allocation may reuse the same address
    int* b = (int*)malloc(sizeof(int));

    if (b != nullptr) {
        *b = 100;

        // BUG: stale still holds the old address.
        // Writing through it is undefined behavior.
        *stale = 999;

        printf("%d\n", *b);  // Might appear to print 999
    }

    free(b);
    return 0;
}

如果分配器复用了该地址,stale 可能与 b 具有相同的数值地址。因此,通过 stale 进行的写入可能看起来落在了 b 的内存中,并将其值从 100 改为 999,即使代码从未显式地将 999 写入 b。

现在想象一下,b 不是一个简单的整数,而是某种重要的东西,比如 is_admin 标志、长度或函数指针。

这就是它成为潜在安全漏洞而不仅仅是 bug 的地方。

那么 PHP 从哪里获得自己的悬空指针?

前面我说过:“这个 bug 必须已经存在于程序内部:它释放了某个东西,而代码的另一部分仍然持有指向它的指针。”

这正是我们需要为 PHP2Shell 回答的问题。PHP 不会故意把悬空指针交给攻击者……不,PHP 没那么慷慨。

那个指针是某个合法 PHP 特性与 Zend 引擎(PHP 的核心)的错误处理机制交互方式中的一个微妙缺陷所导致的结果。

PHP worker 不是“一个程序,一次运行”

如果你习惯于编写一个运行一次就退出的 main(),那么首先容易忽略的一点是:PHP Web 服务器不会为每个 HTTP 请求启动一个新进程。一小池 worker 进程 会保持存活,处理一个又一个请求,复用同一块内存空间。

One worker process, over time:
┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│ Request #1   │   │ Request #2   │   │ Request #3   │  ...
└──────────────┘   └──────────────┘   └──────────────┘
   same heap           same heap          same heap
   same globals         same globals       same globals

将其与一个标准的 C 程序相比:它在函数的每次结束时分配、释放并干净地退出。而 PHP worker 是一个持续运行的循环。尽管它有一个内存管理器(Zend MM)会在请求之间清理堆,但 C 代码中的全局变量除非被显式清零,否则会保留其值。在第 1 次迭代中遗留的任何悬空内容,在第 2 次迭代中仍然存在。

session.upload_progress(一切问题的主要根源)

谁不想要一个显示大文件上传状态的进度条呢?PHP 开发者提出了同样的问题,并构建了一个功能:在文件上传期间,一个名为 PHP_SESSION_UPLOAD_PROGRESS 的隐藏表单字段会让 PHP 自动跟踪上传进度。

下载工具