THM Write-up: Vulnerable Codes
Security is often learned the hard way: by finding, exploiting, and understanding bugs in real-world code. In this article, I’ll walk you through five intentionally vulnerable web security tasks I created for TryHackMe, ranging from CSRF bypasses to race conditions in Python and PHP. Each task demonstrates a common security pitfall, explains why it’s dangerous, and shows how it can be exploited — helping you sharpen your penetration testing skills.
https://tryhackme.com/room/vulnerablecodes
Task 1: JavaScript — DOC Export
const csrfShield = require('csurf')({ cookie: true });
const session = require('express-session');
const path = require('path');
const uuid = require('uuid');
const fs = require('fs/promises');
const fileUpload = require('express-fileupload');
const express = require('express');
const app = express();
app.use(fileUpload());
app.use(session({
secret: process.env.SECRET,
cookie: {
secure: true,
sameSite: 'none',
},
}));
// Upload endpoint
app.post('/uploadDoc', csrfShield, async (req, res) => {
const f = req.files.document;
if (path.extname(f.name) !== '.txt') {
return res.status(400).send('Only .txt files allowed');
}
const id = uuid.v4();
await f.mv(`public/uploads/${id}`);
return res.json({ id });
});
// Export endpoint
app.get('/exportDOC', csrfShield, async (req, res) => {
if (!req.query.id) {
return res.status(400).send('Missing id');
}
const id = path.basename(req.query.id);
const dst = `public/export/${id}.doc`;
const formData = buildTemplate(`public/uploads/${id}`).replaceAll('{csrf_token}', req.csrfToken());
await fs.writeFile(dst, formData);
return res.send(`<a href="${escape(dst)}">Your DOC file!<a>`);
});In this challenge, we explore a subtle yet critical security flaw in a web application handling file uploads and exports. The application allows users to upload .txt files and later export them as .doc documents, dynamically replacing placeholders with the user’s CSRF token.
The vulnerability arises from the misuse of HTTP methods and CSRF protection. While the application attempts to safeguard sensitive tokens using standard middleware, it fails to account for GET requests, which are not protected. This oversight creates a scenario where an attacker can manipulate file exports, potentially obtaining another user’s CSRF token.
This task highlights the importance of understanding middleware limitations, carefully controlling the exposure of sensitive data, and ensuring that all endpoints — regardless of method — are appropriately secured. By analyzing this challenge, readers gain practical insights into CSRF exploitation and the nuances of protecting user sessions in web applications.
Task 2: JavaScript — OAuth Popup
<!-- oauth-google-popup.html -->
<script>
const commands = Object.assign(Object.create(null), {
getLoginCode(sender) {
sender.postMessage({
type: 'login-code',
code: new URL(location).searchParams.get('code'),
}, '*');
},
startLoginFlow(sender, clientId) {
location.href = 'https://accounts.google.com/o/oauth2/v2/auth'
+ '?client_id=' + encodeURIComponent(clientId)
+ '&redirect_uri=' + encodeURIComponent(location.href)
+ '&response_type=code';
},
});
window.addEventListener('message', ({ source, origin, data }) => {
if (source !== window.opener) return;
if (origin !== window.origin) return;
commands[data.cmd](source, ...data.args);
});
window.opener.postMessage({ type: 'popup-ready' }, '*');
</script>This challenge demonstrates a common security pitfall in OAuth implementations using popups. The popup is designed to facilitate Google OAuth authentication, communicating with its opener via the postMessage() API. Commands such as startLoginFlow and getLoginCode allow the opener to initiate authentication and retrieve the authorization code.
The vulnerability stems from a flawed origin validation. The popup checks that incoming messages match window.origin, intending to prevent arbitrary sites from sending malicious commands. However, when the popup is opened from a sandboxed iframe without the allow-same-origin flag, its origin is set to "null". This behavior can be exploited by an attacker, as both the iframe and the popup share the "null" origin, allowing unauthorized commands to be executed.
Through this task, readers learn the risks of improper origin checks and the importance of secure inter-window communication. Corrective measures include using location.origin for validation instead of window.origin and specifying a strict target origin in postMessage() calls rather than relying on the wildcard '*'.
Task 3: C — URL Parsing Differential
#include <stdio.h>
#include <string.h>
#include <curl/curl.h>
int check_host(const char *url) {
char host[128];
int port = 443;
// naive parsing: assumes simple format
sscanf(url, "https://%127[^:/]:%d", host, &port);
return strcmp(host, "secure.internal") == 0;
}
int main(int argc, char **argv) {
if (argc < 2) {
printf("Usage: %s <url>\n", argv[0]);
return 1;
}
const char *target = argv[1];
CURL *h;
CURLcode r;
if (!check_host(target)) {
printf("host validation failed\n");
return 1;
}
h = curl_easy_init();
if (h) {
curl_easy_setopt(h, CURLOPT_URL, target);
r = curl_easy_perform(h);
if (r == CURLE_OK) {
printf("Request successful\n");
} else {
printf("Request error\n");
}
curl_easy_cleanup(h);
}
return 0;
}This challenge highlights a subtle but impactful vulnerability in custom URL validation. The program attempts to restrict HTTP requests to a specific internal host, secure.internal, by using a naive parser that extracts the domain from a given URL.
The problem arises because URL parsing is a complex task governed by standards such as RFC2396 and RFC3986. Libraries like libcurl implement comprehensive parsing, which often interprets URLs differently from simplified, custom parsers. This discrepancy creates a URL parsing differential, where the validator sees one hostname, but the underlying HTTP client connects to another.
Attackers can exploit this differential by crafting URLs such as https://secure.internal:@attacker.com/or https://secure.internal:@evil.com/.While the validator believes the request targets secure.internal, libcurl actually connects to attacker.com, allowing requests to unintended hosts.
This task illustrates the dangers of writing custom URL parsers, the importance of relying on well-tested libraries, and the security risks associated with parsing discrepancies.
Task 4: PHP — HTTP Header Exploit
<?php
session_start();
function get_client_ip() {
return !empty($_SERVER['HTTP_X_FORWARDED_FOR'])
? $_SERVER['HTTP_X_FORWARDED_FOR']
: $_SERVER['REMOTE_ADDR'];
}
$client = get_client_ip();
if (!filter_var($client, FILTER_VALIDATE_IP) || !in_array($client, ['127.0.0.1', '::1'])) {
die("Access denied\n");
}
if (!isset($_SESSION['login'])) {
header("Location: denied.php");
}
echo call_user_func($_GET['action'], $_GET['param']);This challenge explores how improper handling of HTTP headers and session management can lead to critical vulnerabilities in PHP applications. The script is intended to restrict access to internal users by validating the client’s IP address and ensuring a valid session exists.
However, the implementation contains multiple issues. First, the IP check relies on the X-Forwarded-For header, which attackers can spoof to impersonate a trusted internal address. Second, the session validation uses header("Location: ...") for redirection but does not terminate execution with exit(). As a result, even unauthorized users may reach subsequent code.
Finally, the script uses call_user_func() with user-provided parameters, enabling arbitrary function execution. When combined with the bypassable IP and session checks, this results in a Remote Code Execution (RCE) vulnerability.
This task emphasizes the need for careful validation of client-supplied headers, proper control flow after redirection, and secure handling of function calls to prevent exploitation.
Task 5: Python — shutil Race Condition & Path Traversal
import os, shutil
from django.http import HttpResponse
def upload(request):
f = request.FILES["data"]
with open(f'/tmp/files/{f.name}', 'wb+') as out:
for chunk in f.chunks():
out.write(chunk)
return HttpResponse("Uploaded!")
def install(request):
lang = request.GET['lang']
if '..' in lang:
return HttpResponse("Not allowed!")
src = os.path.join('addons', 'langs', lang)
dst = os.path.join('/tmp/extract', lang)
shutil.copy(src, dst)
shutil.unpack_archive(dst, extract_dir='/tmp/extract')
return HttpResponse("Installed!")
def clean(request):
target = os.path.basename(request.GET['file'])
safe_path = f'/tmp/files/{target}'
os.unlink(safe_path)
return HttpResponse("Removed!")
This challenge demonstrates a critical security risk in file handling within web applications. The Django-style application provides three endpoints: one for uploading files, one for installing language packages, and one for cleaning up uploaded files.
The vulnerability arises in the /install endpoint, which attempts to prevent path traversal by rejecting inputs containing ... However, the use of os.path.join can be manipulated to reference files outside the intended directory.
Moreover, the combination of shutil.copy and shutil.unpack_archive introduces a race condition. Attackers can exploit the timing between copying and deleting files via the /clean endpoint to force extraction of their uploaded archive. This sequence effectively provides an arbitrary file write primitive, which can lead to full system compromise.
This task illustrates the importance of secure path validation, atomic file operations, and careful handling of user-supplied filenames to prevent race conditions and path traversal vulnerabilities.
