Open source platforms, although extremely useful and popular, can sometimes be prone to vulnerabilities. By allowing users to access the code that creates the platforms, these free software resources open themselves up for easier vulnerability testing by cyber professionals and hobbyists, as well as malicious actors.
Immersive Labs’ Director of Cyber Threat Research, Kev Breen, recently uncovered a weakness in a tool dubbed the Open Source Social Network. This vulnerability would enable “arbitrary file read” – allowing an attacker to read any file in the system, whether permitted or not. There were a couple of obstacles to overcome during this process, which eventually involved writing a custom crypto cracking tool… but more on that later.
What is the Open Source Social Network?
The Open Source Social Network (OSSN) is pretty self-explanatory: it’s an open source social media platform that a user hosts themself. Reasonably easy to download and get running, it’s written in PHP, uses a MySQL backend, and has just shy of half a million downloads listed on its main site.
The scanner
The source code for OSSN is free to download either from the main site or from the OSSN GitHub. As it’s a PHP application, it’s relatively easy to read and keep track of what’s happening. That being said, there is a lot of code here. We ran a quick pass over it using an open source static code analyser to see if we could find any bugs.
The results
A progpilot (static PHP analyzer) scan only takes a couple of minutes to complete, so we immediately had some pretty interesting results. Check it out.
{
“source_name”: [
“$file_get_contents_return”
],
“source_line”: [
316
],
“source_column”: [
10492
],
“source_file”: [
“\/tmp\/ossn\/components\/OssnComments\/ossn_com.php”
],
“sink_name”: “echo”,
“sink_line”: 316,
“sink_column”: 10492 ,
“sink_file”: “\/tmp\/ossn\/components\/OssnComments\/ossn_com.php”,
“vuln_name”: “xss”,
“vuln_cwe”: “CWE_79”,
“vuln_id”: “bd6478779f63431a516932bc0fe193e97ff73a84e6710cb644d88ba036f5bbbe”,
“vuln_type”: “taint-style”
},
The scan threw up a potential XSS (cross-site) vulnerability in a component that seemed to be related to comments in the network. It tells us it’s on line 316 of the source code, so we headed there to take a closer look. What was going on?
case ‘staticimage’:
$image = base64_decode(input(‘image’));
if(!empty($image)) {
$file = ossn_string_decrypt(base64_decode($image));
header(‘content-type: image/jpeg‘);
$file = rtrim(ossn_validate_filepath($file), ‘/’);
if(is_file($file)) {
echo file_get_contents($file);
} else {
ossn_error_page();
}
} else {
ossn_error_page();
}
break;
From the source code we discovered that there wasn’t in fact an XSS vulnerability, but an echo that was reading data from a file. It was setting image content types and seemed to be validating file paths. For the keen-eyed amongst you, there’s also an encryption function here, which we’ll get to later.
First, we needed to see if it was possible to control the file path $file. If that could be done then we were looking at local file inclusion or, at the very least, file read. From reading the source code, there is a case statement with different options. We identified that the case option ‘staticimage’ needs to evaluate to TRUE for the vulnerable code to be reached. Let’s dig deeper.
‘staticimage’
Using a docker-compose template, we started a new instance with a clean database and some generic accounts. Once the application got up and running, we needed to find where this function and case were called in the code. The above script gave us a few clues to work with.
- It’s in a comment
- It’s image related
- The path contains ‘staticimage’
- There is a Base64 string
Burp felt a little heavy-handed for this fairly straightforward task, so we used Dev Tools on Chrome instead, setting a filter for ‘staticimage’.
At this stage, there was just one demo post on the social network. We knew the bug was something to do with comments, so we took a look at the comments section on the post.
There’s an option to add an image to a comment. We selected an image to upload to see what would happen, and before posting it…
Before even hitting send on our comment, we hit all the requirements on the above list. This is just a preview that has sent a request to the server, stored the image, and rendered the preview in the front end.
Once it’s been previewed, the image is stored on the server and can be accessed directly without being authenticated to the OSSN platform. Bingo.
From here, we knew we could control the Base64 string from the attacker’s side and that it was somehow being used to construct a file path that was then being echoed out to the page.
The crypto
So what was that ‘ossn_string_decrypt’ we referenced right at the outset actually doing? The application took the Base64 string from the URL image parameter and Base64 decoded it twice. The resulting output was then passed to this function:
function ossn_string_decrypt($string = ‘ ‘, $key = ‘ ‘) {
if (empty($string)) {
return false;
}
if (empty($key)) {
$key = ossn_site_settings(‘site_key‘);
}
$key = ossn_string_encrypt_key_cycled($key);
$size = openssl_cipher_iv_length(‘bf-ecb‘);
$mcgetvi = openssl_random_pseudo_bytes($size);
//note mcrypt and now this acting mcrpyt adds the spaces to make 16 bytes if its less then 16 bytes
//you can use trim() to get orignal data without spaces
return openssl_decrypt($string, “bf-ecb“, $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $mcgetvi);
}
That all looked reasonably standard, so we now needed to grab the ‘site_key’ and find out what was in that Base64 encoded value. Getting the site key was easy from our side, as it was stored in the database.
mysql> select value from ossn_site_settings where name = “site_key”;
+———-+
| value |
+———-+
| 94bf7ac1 |
+———-+
1 row in set (0.00 sec)
mysql>
That’s not a very long key! However, at this moment, we were only interested in seeing what was in that BLOB (binary large object: these can hold a variable amount of data) to make it a little easier for us. We copied these functions out to a standalone PHP script to play around with all the values and put some debug lines in there.
Once we had enough information, we also created a couple of CyberChef recipes to replicate the functions. All that was needed now was to replace the placeholder site key that we were examining with our own, repeated to a length of 20 characters.
OSSN Blowfish Decryption
OSSN Blowfish Encryption
Arbitrary file read
Now that we knew how to generate the Base64 encoded value, it was time to find out whether we could read any files from the server that we shouldn’t have been able to.
There was another function that was doing some file path validation too. It was trying to replace directory traversal attempts by replacing any ../ in the strings. This was easy to ignore, as we specified the full path instead of using a relative path.
To make things a bit more portable, we created a proof of concept (PoC) Python script that took a ‘site_key’, a ‘file_path’, and a target URL, and attempted to read the file.
You can get the full code and the docker-compose images on GitHub.
Getting the site key
It was pretty easy to get to this point with knowledge of the site key – but was there a way for an attacker to get hold of it? At this point, Alex Seymour, Content Engineer, helped to trace all of the crypto bits and pieces, jumping with both feet into the deep end of C and PHP Blowfish implementations.
We knew the site key used for the encryption was a mere eight characters in length. We could predict some of the plaintext, so was there a way to brute force the key from the outside in a reasonable timeframe?
The key seemed to be in lowercase hex, which gives us 16 possible options per character. At eight characters, that gave us around 281, 474, 976, 710, 656 possible site keys. Certainly that wouldn’t be easy, but we weren’t deterred. We decided to take a look at the function that actually created the site key to see if there was anything that could be used. Turns out, there was.
Here’s the function that created the unique site key:
function ossn_generate_site_secret() {
return substr(md5(‘ossn‘ . rand()), 3, 8);
This function:
- Starts with the string ‘ossn’
- Calculates a random number with ‘PHP rand()’
- Appends the number to the first string
- Calculates the md5 hash of this new string
- Takes characters 3-11 as the ‘site_key’
The use of ‘rand()’ here is interesting, as the rand function is not cryptographically secure, and warns you as such in the PHP docs.
Using ‘rand()’ in PHP7 or higher, the maximum possible value is 2, 147, 483, 647. In terms of cracking the key, we’ve reduced the number of possible keys from around 282 trillion to a measly two billion.
Instead of trying every possible permutation of the eight character key, we calculated every md5 sum for each of the two billion possible values of ‘rand’.
That’s ossn1 to ossn2147483647.
Breaking the crypto
Having identified the weak key generation routine, we needed to start writing code to recover the site key using that encrypted BLOB containing an image’s file path.
First, we had to identify a successful decryption attempt. We examined the source code and plaintext file path, which revealed the value tmp/photos to be a safe, known plaintext value as it was hard-coded into the source code.
The initial PoC was a quick Python script that spawned a handful of threads to distribute the generation of all the possible keys. The generation threads, in turn, spawned yet more threads to handle each decryption attempt. Each of these threads then simply executed a PHP subprocess and checked the output for the known value tmp/photos. Unfortunately, the performance of this approach wasn’t anywhere near good enough. We saw average speeds of around 2000 attempts per second, which would have taken around two weeks to work through all the possibilities.
The next iteration of the PoC was written in C, which unsurprisingly provided significant performance improvements. The C version follows pretty much the same structure as the Python PoC: a fixed number of generator threads were spawned and the available keyspace was split across them. In this case, we ran four generator threads, so split 2,147,483,647 into quarters, and passed each to a different generator thread.
void start_generators(long max_value, int thread_count) {
pthread_t threads[thread_count];
struct generator_args args[thread_count];
while (max_value % thread_count != 0) {
max_value++;
}
for (int i = 0; i < thread_count; i++) {
args[i].max = (max_value / thread_count) * (i + 1);
args[i].min = (max_value / thread_count) * i;
if (args[i].min > 0) {
args[i].min++;
}
pthread_create(&threads[i], NULL, attempt_generator, &args[i]);
}
for (int i = 0; i < thread_count; i++) {
pthread_join(threads[i], );
}
}
Each generator thread enumerated its assigned value range in order to construct the key for each decryption attempt, according to the OSSN source code. Each value was appended to the string ossn, and the result was then md5 hashed. Characters 3-11 of the resulting hash are extracted, and passed to a new thread to handle further key preparation and the actual decryption attempt.
char *string = malloc(256);
unsigned char hash[MD5_DIGEST_LENGTH];
char *attempt = malloc(SUBSTR_LENGTH + 1);
char *hash_hex = malloc(32);
memset(string, 0, 256);
memset(attempt, 0, SUBSTR_LENGTH + 1);
memset(hash_hex, 0, 32);
sprintf(string, “ossn%ld”, i);
MD5(string, strlen(string), hash);
for (int j = 0; j < MD5_DIGEST_LENGTH; j++) { hash_hex += sprintf(hash_hex, “%02x”, hash[j]);
}
hash_hex -= 32;
strncpy(attempt, hash_hex + SUBSTR_START, SUBSTR_LENGTH);
pthread_create(&threads[thread_index], NULL, test_attempt, attempt);
During this process we made an interesting discovery; we realised that Blowfish’s key expansion is not correctly implemented in PHP’s OpenSSL extension. As detailed in this bug report, keys that are made up of less than 128 bits (16 bytes) are zero-padded but should use key cycling according to the algorithm’s inventor. The PHP developers added a new constant, OPENSSL_DONT_ZERO_PAD_KEY, which instructs calls to openssl_encrypt() to use key cycling instead of zero-padding. The default implementation, however, still uses zero-padding and, at the time of writing, the constant is undocumented. OSSN contains its own key cycling function that is used to cycle a key up to 20 characters in length (so test1234 would become test1234test1234test).
During this process we made an interesting discovery; we realised that Blowfish’s key expansion is not correctly implemented in PHP’s OpenSSL extension. As detailed in this bug report, keys that are made up of less than 128 bits (16 bytes) are zero-padded but should use key cycling according to the algorithm’s inventor. The PHP developers added a new constant, OPENSSL_DONT_ZERO_PAD_KEY, which instructs calls to openssl_encrypt() to use key cycling instead of zero-padding. The default implementation, however, still uses zero-padding and, at the time of writing, the constant is undocumented. OSSN contains its own key cycling function that is used to cycle a key up to 20 characters in length (so test1234 would become test1234test1234test).
Now, all that was left was to implement key cycling, start decrypting the ciphertext block by block, and checking the output for the known plaintext tmp/photos.
The C implementation performed an average of around 45,000 attempts per second, meaning it would take just over 13 hours to try every possible key. Cursory testing showed even faster speeds on higher-spec machines.
During the creation of this PoC, the OSSN developers released an update that changed their encryption process to use AES instead of Blowfish. We modified our PoC to create an AES version as well. Both can be found on GitHub.
Disclosure
Disclosing the vulnerability to Soft Lab 24, the company that develops OSSN, was a fairly smooth process. We reached out to their team via email, and they were quick to respond and push updates. After a few days of back and forth, we were unable to read any more files.
If you’d like to get hands on with this vulnerability in a safe, secure environment, log in to your Immersive Labs account and head over to Cyber Threat Intelligence.