安全研究者撰写 PHP2Shell 漏洞深度技术分析
来源标题:PHP2Shell Deep Dive
一名安全研究者正在撰写关于 PHP2Shell 漏洞的深度技术分析,计划完整剖析漏洞原理与利用过程。该研究者称,目前关于 PHP2Shell 的公开信息仍然很少,尤其缺乏详细的技术分析。文章基于其对 PHP 源码的理解和实验,目标是让具备基础 C 语言知识的人也能理解,同时不牺牲技术深度。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
我在2026年10月26日开始撰写这篇研究,目前还不完全确定什么时候能完成。写作之时,关于PHP2Shell的公开信息仍然非常少,尤其是针对这些漏洞及其利用过程的详细技术分析。
说实话,我已经厌倦了那些只停留在表面的解释——它们并没有真正展示内部到底发生了什么。我想做点不一样的事情:一份真正深入的技术讲解,完整地梳理整个过程,审视每一个重要细节,不仅解释“发生了什么”,还要解释“为什么会发生”以及“是如何发生的”。
这份研究基于我对PHP源码的理解、我的实验,以及我在试图理解这些漏洞过程中所做的调查。我想把这些碎片拼在一起,分享我的发现,并希望能让某些底层概念对其他人来说更容易理解。
我的主要目标之一是让内容易于理解,同时不牺牲技术深度。我希望具备基本C语言知识和一般编程经验的人都能跟上,即使之前没有任何底层内存利用的经验。
我也希望这项研究能澄清一些目前解释不清或被误解的内容。但我要坦诚:这是我自己对这些漏洞的技术理解,不是官方分析,也不是绝对可靠的参考。我自己仍在学习、调查、梳理线索。过程中可能存在错误、不准确之处,或者我理解有误的地方。
如果你发现有问题、有不同解读,或者认为某些地方值得进一步研究,请随时联系我。我真心欢迎纠正、讨论和贡献,这些都能帮助这份研究变得更准确、对所有人更有用。
毕竟,分享这一切的意义就在于共同学习,帮助更多人理解底层到底发生了什么。
已经了解基础了?直接跳到你关心的部分:
如果你是堆利用的新手,或者想要完整体验,那就继续往下读。我是按从头到尾的顺序写的。
在我们深入之前,有几件事需要大家记住。随着研究的推进,它们会变得特别有用。正如我之前提到的,我希望这份讲解对所有人都友好,所以我会花时间解释哪怕是最基础的概念。如果你已经熟悉这些内容,尽管跳过!
在典型的程序中,内存被组织成不同的区域,包括栈和堆。
int main() { int a = 30; // 通常存储在栈上 int* b = new int(30); // b 通常位于栈上, // 但它指向的整数位于堆上
这里,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. 为用户分配内存 User* a = (User*)malloc(sizeof(User));
// 初始化原始用户 strcpy(a->username, "alice"); a->is_admin = 0;
// 我们保留一份指针副本,用来演示这个 bug User* a_stale = a;
// 2. 释放内存。对象已消失,但 a_stale 仍持有该地址! 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) { // 这里可能会执行,尽管 Alice 从来都不是管理员
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。