PHPCVE-2026-109618深度分析
原创
YMsora
YMsora
YMs0ra的安全漫路
2026年10月22日 12:11
上海
标题已修改
去阅读
后续会对109619也分析,非常深入zend的两个洞,很有意思,ai还是太恐怖了
依旧波及范围极广,我让ai调取了php更新的补丁,并进行了审计 在于清理机制以及回调,header链表等等多个问题
heap->overflow = 1;
zend_try {
zend_error_noreturn(E_ERROR, format, ...);
} zend_catch {
} zend_end_try();
heap->overflow = 0;
zend_bailout(); /* ← 走 longjmp,跳过清理 */
exit(1);
在内存耗尽的时候会设置为 heap->overflow = 1;
也就是应急模式,开启了配额缓冲,并且对于zend报错进行格式化输出,也就是zend\_try的内容 当应急退出后便进入了zend\_bailout(),他的宏实现是longjmp(EG(bailout), 1)
继续跟进zend.c
CG(unclean_shutdown) = 1;
CG(active_class_entry) = NULL;
CG(in_compilation) = 0;
CG(memoize_mode) = 0;
EG(current_execute_data) = NULL;
LONGJMP(*EG(bailout), FAILURE); /* ← 非局部跳转 */
CG和EG是编译期和执行期的全局结构体,在heap->overflow = 0;之后, EG current\_execute\_data的状态也就被滞空了, 紧接着是LONGJMP(\*EG(bailout), FAILURE);跳转回预设 也就是之前的zend\_bailout();目的点
#define zend_try
{
JMP_BUF *__orig_bailout = EG(bailout);
JMP_BUF __bailout;
EG(bailout) = &__bailout;
if (SETJMP(__bailout)==0) {
#define zend_catch
} else { //跳转位置
EG(bailout) = __orig_bailout;
#define zend_end_try()
}
EG(bailout) = __orig_bailout;
}
在跳转else的catch分支后并没有执行清理,中间清理的代码是被略过的 略过之后
zend_result php_request_startup(void)
{
zend_result retval = SUCCESS;
...
zend_try {
...
sapi_activate();
...
php_hash_environment();
zend_activate_modules();
PG(modules_activated)=1;
} zend_catch {
retval = FAILURE;
} zend_end_try();
SG(sapi_started) = 1;
return retval; // ← 返回 FAILURE
}
在跳转的时候,EG,SG,BG都是半成品,下面retval返回FAILURE,并且状态并没有回滚 这时,有两个处理逻辑。 FPM / LSAPI 会直接杀掉并重启worker 但是 Apache mod\_php 他会执行 php\_request\_shutdown(NULL); 与此同时
void php_request_shutdown(void *dummy)
{
bool report_memleaks;
EG(flags) |= EG_FLAGS_IN_SHUTDOWN;
report_memleaks = PG(report_memleaks);
/* EG(current_execute_data) points into nirvana and therefore cannot be safely accessed
* inside zend_executor callback functions.
*/
EG(current_execute_data) = NULL;
php_deactivate_ticks();
/* 0. Call any open observer end handlers that are still open after a zend_bailout */
if (ZEND_OBSERVER_ENABLED) {
zend_observer_fcall_end_all();
}
/* 1. Call all possible shutdown functions registered with register_shutdown_function() */
if (PG(modules_activated)) {
php_call_shutdown_functions();
}
/* 2. Call all possible __destruct() functions */
zend_try {
zend_call_destructors();
} zend_end_try();
/* 3. Flush all output buffers */
zend_try {
php_output_end_all();
} zend_end_try();
/* 4. Reset max_execution_time (no longer executing php code after response sent) */
zend_try {
zend_unset_timeout();
} zend_end_try();
/* 5. Call all extensions RSHUTDOWN functions */
if (PG(modules_activated)) {
zend_deactivate_modules();
}
/* 6. Shutdown output layer (send the set HTTP headers, cleanup output handlers, etc.) */
zend_try {
php_output_deactivate();
} zend_end_try();
/* 7. Free shutdown functions */
if (PG(modules_activated)) {
php_free_shutdown_functions();
}
/* 8. Destroy super-globals */
zend_try {
int i;
for (i=0; i<NUM_TRACK_VARS; i++) {
zval_ptr_dtor(&PG(http_globals)[i]);
}
} zend_end_try();
/* 9. Shutdown scanner/executor/compiler and restore ini entries */
zend_deactivate();
/* 10. free request-bound globals */
php_free_request_globals();
/* 11. Call all extensions post-RSHUTDOWN functions */
zend_try {
zend_post_deactivate_modules();
} zend_end_try();
/* 12. SAPI related shutdown*/
zend_try {
sapi_deactivate_module();
} zend_end_try();
/* free SAPI stuff */
sapi_deactivate_destroy();
/* 13. free virtual CWD memory */
virtual_cwd_deactivate();
/* 14. Destroy stream hashes */
zend_try {
php_shutdown_stream_hashes();
} zend_end_try();
/* 15. Free Willy (here be crashes) */
zend_arena_destroy(CG(arena));
zend_interned_strings_deactivate();
zend_try {
shutdown_memory_manager(CG(unclean_shutdown) || !report_memleaks, 0);
} zend_end_try();
/* Reset memory limit, as the reset during INI_STAGE_DEACTIVATE may have failed.
* At this point, no memory beyond a single chunk should be in use. */
zend_set_memory_limit(PG(memory_limit));
/* 16. Deactivate Zend signals */
#ifdef ZEND_SIGNALS
zend_signal_deactivate();
#endif
#ifdef PHP_WIN32
if (PG(com_initialized)) {
CoUninitialize();
PG(com_initialized) = 0;
}
#endif
#ifdef HAVE_DTRACE
DTRACE_REQUEST_SHUTDOWN(SAFE_FILENAME(SG(request_info).path_translated), SAFE_FILENAME(SG(request_info).request_uri), (char *)SAFE_FILENAME(SG(request_info).request_method));
#endif /* HAVE_DTRACE */
}
因为EG(current\execute\_data)至null所以 任何 executor callback 里都不能安全访问它 , 上游默认是unclean的状态, 于是在这之中是可控制的,zend\_call\_destructors() 调用了\\_destruct() zend\_deactivate\_modules();扩展了扩展 RSHUTDOWN, 些步骤会去遍历、析构、调用那些被 longjmp 跳过清理指向无效内存的全局状态 比如读坏指针或者调坏函数指针,在 shutdown 阶段被执行 而这几部分坏指针是攻击者可控的也就可RCE 并且可知还有个问题,就是攻击可以小limit造成大链表的内存破坏 看看以下的结构体以及处理逻辑
typedef struct { char *key; char *value; } mime_header_entry;
multipart_buffer *self;
zend_llist *header;
key = estrdup(line);
smart_string_appends(&buf_value, value);
zend_llist_add_element(header, &entry);
key的value是每一个都会被添加进链表的结点,然后复制到堆上
#define SMART_STRING_OVERHEAD (ZEND_MM_OVERHEAD + 1)
#define SMART_STRING_START_SIZE 256
#define SMART_STRING_START_LEN (SMART_STRING_START_SIZE - SMART_STRING_OVERHEAD)
#define SMART_STRING_PAGE 4096
首次分配固定按 256 字节起步;之后每次增长按 4096 对齐,用 erealloc2 扩容。也就是说 value 缓冲总是保留一个 4KB 量级的容量,而不是刚好等于内容长度。 所以对于 X-N: v 这种很短的 header,在smart\_string value 累积 后是可以共享缓冲,4096字节对齐增长的 还有几种放大方法,也就不做赘述了 以上,bye
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。






评论