Front Door 1
Introduction
Hacktheon Sejong 2025
Category: Web
Write-up date: 07/05/2025
This is a server monitoring page.
Find the vulnerability.
Flag format: FLAG{_}
Point: easy
Source code analysis
When first access to the website, we receive a list of api request

let app = Router::new()
.route("/api", get(handlers::get_root_handler))
.route("/api/health-check", get(handlers::get_health_check_handler))
.route("/api/logs", get(handlers::get_logs_handler))
.route("/api/monitor/{info}", get(handlers::get_monitor_handler))
.route("/api/signin", post(handlers::post_signin_handler))
.route(
"/api/flag",
get(handlers::get_flag_handler).layer(middleware::from_fn(middlewares::authorize)),
)
.layer(middleware::from_fn(middlewares::tracing_session_id))
.layer(middleware::from_fn(middlewares::set_session_expiry))
.layer(session_layer);
Inside source code, only route /api/monitor/{info} and route /api/signin give us attack surface to deal with, route
/api/flag when access requires us to log in so we pass the function for now.
pub async fn get_monitor_handler(Path(info): Path<String>) -> impl IntoResponse {
let file_path = match PathBuf::from("/proc")
.join(alias(&info).unwrap_or(info.clone()))
.canonicalize()
{
Ok(path) => path,
Err(e) => {
tracing::error!("Error canonicalizing path: {}", e);
return (StatusCode::BAD_REQUEST, "Invalid parameter").into_response();
}
};
let is_in_proc = file_path.starts_with("/proc");
let is_file = file_path.is_file();
tracing::error!("{}, {}", is_file, is_in_proc);
if !is_in_proc || !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();
}
if comps.len() == 4
&& comps.get(2) != Some(&Component::Normal(OsStr::new(&process::id().to_string())))
{
return (StatusCode::BAD_REQUEST, "Invalid argument").into_response();
}
let msg = match fs::read_to_string(&file_path).await {
Ok(content) => parse_content(&info, &content).await,
Err(err) => {
tracing::error!("Failed to read file '{}': {}", file_path.display(), err);
return (StatusCode::BAD_REQUEST, "Invalid argument").into_response();
}
};
(StatusCode::OK, msg).into_response()
In the get_monitor_handler function, we can read any file in /proc as long as it is a file and start with /proc.
Using this path traversal exploit, we can read all the environment var by accessing /proc/self/environ including
GUEST_ID and GUEST_PWD, the username and password of guest user.
pub async fn post_signin_handler(
session: Session,
Json(body): Json<PostSigninBody>,
) -> impl IntoResponse {
let guest_id = env::var("GUEST_ID")
.ok()
.unwrap_or(consts::DEFAULT_GUEST_ID.to_string());
let guest_pwd = env::var("GUEST_PWD")
.ok()
.unwrap_or(consts::DEFAULT_GUEST_PW.to_string());
let is_authed = guest_id == body.username && guest_pwd == body.password;
if !is_authed {
return (StatusCode::BAD_REQUEST, "Invalid username or password").into_response();
}
if let Err(err) = session.insert("auth", body.username).await {
tracing::error!("Failed to insert session data: {}", err);
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to signin").into_response();
}
let session_dir = match tempfile::tempdir() {
Ok(tempdir) => tempdir.into_path(),
Err(err) => {
tracing::error!("Failed to create session directory: {}", err);
let _ = session.remove::<String>("auth").await;
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to signin").into_response();
}
};
let session_dir = match session_dir.canonicalize() {
Ok(path) => path,
Err(err) => {
tracing::error!("Failed to canonicalize session directory: {}", err);
let _ = session.remove::<String>("auth").await;
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to signin").into_response();
}
};
if let Err(err) = session.insert("dir", session_dir).await {
tracing::error!("Failed to insert session data: {}", err);
let _ = session.remove::<String>("auth").await;
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to store session data",
)
.into_response();
}
StatusCode::OK.into_response()
}

But wait, how can we read normal cpu or uptime file but when accessed self/environ, it's return nothing. Well
let's look at the function parse_content
async 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()
}
}
}
It's only show the content of uptime, idle-time, cpu, mem but not other, so to read the content we have to rely
on the log itself.
#[derive(Deserialize, PartialEq, PartialOrd)]
#[serde(rename_all = "lowercase")]
enum LogLevel {
Error,
Warn,
Info,
Debug,
}
pub async fn get_logs_handler(
session: Session,
Query(query): Query<GetLogsQuery>,
) -> impl IntoResponse {
let session_id = session.id().unwrap_or_default().to_string();
let level = query.level.unwrap_or(LogLevel::Error);
match get_logs(&session_id, level).await {
Ok(logs) => (StatusCode::OK, logs.join("\n")).into_response(),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to get logs".to_string(),
)
.into_response(),
}
}
The log handler function take 2 parameter, one is session_id, the other is Query<GetLogsQuery>. By default, the log
only split out Error. That why we can't see the Warn log from the function parse_content. But when we add
the paramlevel=warnnow the log will split out content ofself/environ`

Authentication
Now using the environment leaked from the self/enviroment we can use it to log in the website.

After login successfully, we extract the flag using the /api/flag endpoint.

FLAG: FLAG{Me7Hod_Ch4iN1nG_1s_5o_COoo0Oo00oO0ol}