Chào mọi người, Zeno đây! Hôm nay mình sẽ chính thức khởi động series OWASP Top 10 nha.
Dành cho những ai chưa biết thì đây giống như là bảng phong thần xếp hạng 10 rủi ro bảo mật web “khét” nhất thế giới vậy. Và để mở bát cho chuỗi bài này, chúng ta sẽ lôi một “bố già” của làng bảo mật ra ánh sáng: Injection (Tiêm mã).
Thường thì nhắc tới Security, mọi người hay bị ngợp bởi mớ lý thuyết khô khan. Yên tâm, bài này mình sẽ đi thẳng vào thực tế: lỗi nó sờ sờ ra sao, dùng Golang fix thế nào cho chuẩn bài, và cách ép máy móc tự bắt lỗi cho mình luôn.
Pha ấm trà rồi chúng ta bắt đầu thôi!
1. Bản chất của Injection là gì?
Thật ra lỗi bảo mật phần lớn đến từ việc các pháp sư code… lười kiểm tra dữ liệu đầu vào. :D
Injection xảy ra khi chúng ta lấy dữ liệu (do người dùng nhập) và ném thẳng vào hệ thống (database, shell,…) mà quên bọc nó lại cẩn thận. Hệ thống ngây thơ đọc dòng dữ liệu đó và tưởng nhầm nó là một câu lệnh. Boom! Kẻ tấn công vừa có quyền điều khiển hệ thống của bạn luôn!
Lưu ý nhỏ từ Zeno: Injection thực ra có rất nhiều họ hàng hang hốc khác như NoSQL Injection, SSTI (Template Injection), LDAP Injection, hay thậm chí cả XSS (HTML/JS Injection trên trình duyệt). Tuy nhiên để bài viết không bị “bội thực”, Zeno sẽ tập trung bóc tách 2 “hung thần” kinh điển và nguy hiểm nhất ở Server-Side trong Golang: SQL Injection và OS Command Injection nhé!
2. SQL Injection (SQLi) - Ác mộng của Database
Đây là lỗi xảy ra khi kẻ tấn công chèn lệnh SQL độc hại vào input để “dắt mũi” Database của chúng ta. Lỗi này sinh ra từ đâu? Đơn giản là do thói quen nối chuỗi (String Concatenation) huyền thoại của anh em dev nhà mình :v
Phân loại sương sương
- In-band SQLi (Classic): Kẻ tấn công vừa tiêm mã vừa nhận kết quả trả về ngay trên màn hình. Rất trực quan!
- Error-based: Ép Database văng ra lỗi để moi thông tin (như tên bảng, phiên bản).
- UNION-based: Dùng lệnh
UNIONđể gộp kết quả của mình vào kết quả của code gốc.
- Inferential SQLi (Blind): Loại này “mù” do kết quả không hiện ra màn hình. Hacker phải đoán mò bằng cách gửi các câu hỏi Đúng/Sai.
- Boolean-based: Nhìn xem trang web có thay đổi gì không (ví dụ mất cái nút hay mất dòng chữ) để đoán đúng/sai.
- Time-based: Chèn lệnh bắt Database phải “ngủ” (vd:
WAITFOR DELAY '0:0:5'). Nếu server mất 5 giây mới rep thì hacker biết là lệnh đã chạy thành công!
Ví dụ minh họa siêu kinh điển
Nhiều tài liệu hay lấy cái ví dụ ' OR '1'='1 tiêm vào ô username. Nhưng Zeno thấy thế này mới chuẩn bài nè:
| |
Khi tiêm chữ admin' -- vào ô username, ký hiệu -- sẽ biến toàn bộ đoạn check password phía sau thành comment (ghi chú). Câu lệnh giờ chỉ còn là SELECT * FROM users WHERE username = 'admin', và thế là đăng nhập thẳng vào admin luôn mà khỏi cần password! :v
Mình sẽ giải thích thêm một chút về cái lỗi học thuật phổ biến nha: Nếu bạn chỉ tiêm
' OR '1'='1vào username mà không có dấu chú thích--, thì câu lệnh sẽ là:username = '' OR '1'='1' AND password = ''. Khổ nỗi trong SQL, toán tửANDcó quyền ưu tiên cao hơnOR. Nên nó sẽ gom lại thànhusername = '' OR ('1'='1' AND password = ''). Nếu password trống thì vế phải tịt ngòi (False), vế trái cũng False nốt. Thế là không hack được đâu nhé! EHE! Muốn xài chiêu này thì phải ném vào ô password cơ!
3. OS Command Injection - Thủng luôn hệ điều hành
Khác với SQLi chỉ quậy trong Database, cái này là kẻ tấn công đánh thẳng vào Hệ điều hành (Linux/Windows) luôn!
Ví dụ thực tế nha
Làm tính năng ping mạng bằng PHP chẳng hạn:
| |
Giờ thay vì nhập IP, hacker nhập cái này: 8.8.8.8 ; cat /etc/passwd.
Và bùm, hệ điều hành sẽ chạy 2 lệnh liên tiếp:
- Ping Google :D
- Đọc luôn file chứa thông tin user của Linux (
cat /etc/passwd). Xong phim!
4. Tóm tắt nhanh bằng bảng cho xịn
| Tiêu chí | SQLi | Command Injection |
|---|---|---|
| Mục tiêu | Cơ sở dữ liệu (MySQL, Postgres…) | Hệ điều hành (Linux, Windows) |
| Hậu quả | Mất/sửa/xóa dữ liệu | Mất quyền điều khiển cả cái server |
| Hàm hay gây họa | Nối chuỗi SQL | system(), exec(), shell_exec() |
| Cách phòng chống | Dùng Parameter / Prepared Statements | Dùng API chuẩn hoặc làm Whitelist |
5. Thực hành chống Hack bằng Golang!
Go thì mặc định khá an toàn, nhưng dùng sai API thì vẫn “toang” như thường. Cùng Zeno xem code nha.
5.1. Phòng SQL Injection
[Cảnh báo] Code lỗi nè (Vulnerable): Cái tội nối chuỗi bằng fmt.Sprintf.
| |
[Khuyến nghị] Code chuẩn bài (Secure): Dùng Placeholder nha các bạn! Thư viện database/sql nó tự lo hết. (Lưu ý: Postgres dùng $1, $2 còn MySQL thì dùng ? nhé).
| |
5.2. Phòng Command Injection
Nhớ 2 quy tắc vàng của Zeno:
- Không bao giờ gọi qua Shell kiểu
sh -chaycmd /c. - Truyền tham số theo dạng từng mảng cắt rời.
[Cảnh báo] Code này là toang luôn nha:
| |
[Khuyến nghị] Code an toàn 100 điểm:
| |
6. GORM có thực sự an toàn?
Anh em hay bảo nhau “dùng ORM như GORM là auto chống được SQLi”. Sai lầm chết người nha! :v
Cách viết để GORM “đỡ đạn” cho bạn
| |
Các Cạm bẫy GORM hay gặp (Rất dễ sập hầm!)
[Cảnh báo] Bẫy số 1: Tự tin nối chuỗi trong Where()
| |
[Cảnh báo] Bẫy số 2: Viết Query thuần sai cách
| |
[Cảnh báo] Bẫy số 3: Lỗi chết người ở Order()
Cái này nhiều anh em dính nè! Cái dấu ? KHÔNG BAO GIỜ xài được cho tên cột hay tên bảng. Thế là anh em toàn… nối chuỗi :D
| |
Cách fix cực gắt: Làm quả Whitelist (danh sách trắng) kiểm tra tận răng luôn.
| |
7. Tự Động Hóa Quét Lỗi (Đẳng Cấp DevSecOps)
Làm dev xịn là phải cho máy móc tự làm nha! Setup thẳng vào CI/CD cho nó oai.
7.1. Gắn Linter quét tự động
Xài gosec để bắt sống mấy cái lỗi Sprintf. Vào file .golangci.yml quất dòng này vô:
| |
7.2. Tự chế Unit Test quét luôn cả Code
Cái này là đỉnh của chóp luôn! Dùng package go/ast để bới tung Abstract Syntax Tree của Go lên, thấy ông nào dùng dấu + trong hàm Where là đánh trượt (FAIL) test liền.
| |
Tóm lại
Injection nghe thì cũ nhưng chưa bao giờ hết hot. Anh em cứ nhớ thần chú: Tách biệt Mã lệnh ra khỏi Dữ liệu là sống khỏe. Zeno chúc anh em code an toàn, không bao giờ bị dính Injection nha! Hẹn gặp lại ở các bài OWASP tiếp theo! :D