Jump to content

Title: jumpserver remote execution vulnerability analysis and reproduction

Featured Replies

Posted

0 Introduction

JumpServer is an open source bastion machine, an operation and maintenance security audit system that complies with the 4A specifications. In layman's terms, it is a springboard machine.

On January 15, 2021, JumpServer released a security update, fixing a remote command execution vulnerability. Since some JumpServer interfaces do not have authorization restrictions, an attacker can construct a malicious request to obtain sensitive information, or perform related operations to control all machines and execute arbitrary commands.

Affect version:

JumpServer v2.6.2JumpServer v2.5.4JumpServer v2.4.5JumpServer=v1.5.9

1. Vulnerability Analysis

See the commit record of the repair code: https://github.com/jumpserver/jumpserver/commit/f04e2fa0905a7cd439d7f6118bc810894eed3f3e

It was found that the connection of the CeleryLogWebsocket class was added with identity authentication.

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 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()

Check out the http interface of this class:

oz4bb4wuk1p18987.png

Through this class, we can know that the access chain of this interface is:

Visit ws/ops/tasks/log/

-- Enter the receive function of TaskLogWebsocket class

-- Enter the handle_task function of TaskLogWebsocket class

-- Enter the read_log_file function of the TaskLogWebsocket class

-- Enter the wait_util_log_path_exist function of the TaskLogWebsocket class

-- Enter the read_log_file function of the TaskLogWebsocket class

-- Enter the get_task_log_path function in app/ops/utls.py d5xoohgzac218988.png

taskid is parsed from the text_data we sent, so it is controllable. Through the following method, we can read the log file /opt/jumpserver/logs/jumpserver.log.

Send to ws://10.10.10.10:8080/ws/ops/tasks/log/

{'task':'/opt/jumpserver/logs/jumpserver'}The above is the principle of file reading. There are the following restrictions on reading log files:

Files can only be read using absolute paths. Only files ending in log are read. The following analysis is how to implement remote code execution.

By reading /opt/jumpserver/logs/gunicorn.log, if you are lucky, you can read the user uid, system user uid, and asset id:

user idasset idsystem user id The above three information needs to be found that the user is logging in to the web terminal to get it from the log. After getting it. Through the /api/v1/authentication/connection-token/interface, you can enter the /apps/authentication/api/UserConnectionTokenApi

ba0a03tnuwn18989.png

r0ysmxpkvm218990.png

The token with only 20s validity period can be obtained through user_id asset_id system_user_id. This token can be used to create a koko component's 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 The specific code is as follows:

xr3kkmfwxfb18991.png

aep4o4hcepw18992.png

The complete RCE utilization steps are summarized as:

The websocket connection can be established without authorization. The log file can be read through the websocket to get the system user, user, and asset fields in the log file. Through the fields in 3, you can get a token of 20 seconds through the token and enter the koko tty. Execute the command

2 Vulnerability recurrence

2.1 Environment construction

Local environment: xubuntu20.04jumpserver version: 2.6.1 version installation steps:

# download

git clone https://github.com/jumpserver/installer.git

cd installer

# Domestic docker source acceleration

export DOCKER_IMAGE_PREFIX=docker.mirrors.ustc.edu.cn

# Install the dev version and then switch to 2.6.1 (You should be able to install 2.6.1 directly. At the beginning, it was installed as the default dev version, but it doesn't matter)

sudo su

./jmsctl.sh install

./jmsctl.sh upgrade v2.6.1

# start up

./jmsctl.sh restart full log

# yanq @ yanq-desk in ~/gitrepo [22:18:53] C:127

$ git clone https://github.com/jumpserver/installer.git

Cloning to '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, completed.

Process : 100% (305/305), done.

# 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

1. Configure JumpServer

1. Check the configuration file

Each component uses environment variable configuration files instead of yaml format, and the configuration name is consistent with the previous one

Configuration file location : /opt/jumpserver/config/config.txt

Finish

2. Configure Nginx certificates

The certificate location is : /opt/jumpserver/config/nginx/cert

Finish

3. Backup configuration files

Backup to /opt/jumpserver/config/backup/config.txt.2021-01-17_22-03-52

Finish

4. Configure the network

Need to support IPv6? (y/n) (default is n): n

Finish

5. Automatically generate encryption keys

Finish

6. Configure the persistence directory

Modify persistent directories such as log recording, find the largest disk, and create directories, such as /opt/jumpserver

Note that : cannot be changed after installation, otherwise the database may be lost.

File System Capacity Used Available Used % Mount Point

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

Set the persistent volume storage directory (default is /opt/jumpserver) :

Finish

7. Configure MySQL

Whether to use external mysql (y/n) (default is n): n

Finish

8. Configure Redis

Whether to use external redis (y/n) (default is n): n

Finish

2. Install and configure Docker

1. Install Docker

Start downloading the Docker program.

--2021-01-17 22:04:12-- https://mirrors.aliyun.com/docker-ce/linux/static/stable/x86_64/docker-18.06.2-ce.tgz

Resolving host mirrors.aliyun.com (mirrors.aliyun.com). 180.97.148.110, 101.89.125.248, 58.216.16.38,

Connecting mirrors.aliyun.com (mirrors.aliyun.com)|180.97.148.110|:443. Connected.

HTTP request has been issued, waiting for a response. 200 OK

Length: 43834194 (42M) [application/x-tar]

Saving to : "/tmp/docker.tar.gz"

/tmp/docker.tar.gz 100%[==============================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================

2021-01-17 22:04:16 (13.8 MB/s) - Saved "/tmp/docker.tar.gz" [43834194/43834194])

Start downloading the Docker compose program.

--2021-01-17 22:04:17-- https://get.daocloud.io/docker/compose/releases/download/1.27.4/docker-compose-Linux-x86_64

Resolving the host get.daocloud.io (get.daocloud.io). 106.75.86.15

Connecting get.daocloud.io (get.daocloud.io)|106.75.86.15|:443. Connected.

HTTP request has been issued, waiting for a response. 302 FOUND

Location: https://dn-dao-github-mirror.daocloud.io/docker/compose/releases/download/1.27.4/docker-compose-Linux-x86_64 [Follow to new 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

Resolving host dn-dao-github-mirror.daocloud.io (dn-dao-github-mirror.daocloud.io). 240e:ff:a024:200:33603fe, 240e:964:100:302:33603fe, 61.160.204.242,

Connecting dn-dao-github-mirror.daocloud.io (dn-dao-github-mirror.daocloud.io)|240e:ff:a024:200:3fe|:443. Connected.

HTTP request has been issued, waiting for a response. 200 OK

Length: 12218968 (12M) [application/x-executable]

Saving to : "/tmp/docker-compose"

/tmp/docker-compose

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

Important Information

HackTeam Cookie PolicyWe have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.