Minh họa: xác minh bản vá, bốn lớp log và một vòng quay log không hề chạy

Báo cáo này đã được ẩn danh. Nó mô tả một sự cố thật mà không nêu tên máy chủ, địa chỉ hay dữ liệu khách hàng. Trọng tâm không phải lỗ hổng, mà là chuỗi chứng cứ: cần kiểm tra gì khi phải quyết định trong vài giờ liệu một hệ thống đã bị xâm nhập hay chưa — và việc đọc sai kết quả dễ đến mức nào?

Xuất phát điểm

Trong vòng một ngày, dự án PHP công bố hai lỗ hổng nghiêm trọng có thể khai thác mà không cần xác thực (CVE-2026-109618 và CVE-2026-109619, mỗi lỗi CVSS 9,2, được gọi chung là “PHP2Shell”). Đối tượng ảnh hưởng là các cài đặt PHP phục vụ yêu cầu web. Máy chủ liên quan chạy một reverse proxy, một phiên bản Nextcloud trên PHP-FPM và một số trang tĩnh. Câu hỏi của người vận hành rất đơn giản:

Chúng ta có bị ảnh hưởng không? Bản vá có đang hoạt động không? Và log có cho thấy ai đó đã khai thác lỗ hổng không?

Ba câu hỏi, ba bài toán chứng cứ rất khác nhau. Khó khăn nằm ở đó.

Bước 1 — chứng minh phạm vi ảnh hưởng thay vì phỏng đoán

Phản xạ đầu tiên (“chúng ta dùng PHP nên bị ảnh hưởng”) là quá thô. Điều quyết định là SAPI: chỉ những cài đặt xử lý yêu cầu web mới bị phơi bày. Việc chứng minh vì thế rất cụ thể:

Bước 2 — chứng minh bản vá: gói và thời gian chạy

Một bản cập nhật bảo mật đã cài chưa phải là bản vá đang hoạt động. Giữa “gói nằm trên đĩa” và “tiến trình đang chạy dùng nó” là một lần khởi động lại FPM master. Vì vậy đã kiểm tra hai lần:

Kiểm traKết quảĐiều được chứng minh
Phiên bản gói8.5.4-0ubuntu1.5 (bản vá Ubuntu, USN-8948-1/-2)Bản vá đã được cài
Thời điểm cài2026-10-22 06:12 (cập nhật tự động)Khi nào bản vá đến
Khởi động lại FPM2026-10-22 06:13, master hoạt động từ đóBản vá đang chạy
Toàn vẹn tệpkiểm tra gói không sai lệchKhông có binary bị sửa

Bốn dòng này là chứng cứ thực sự của bản vá. Mọi thứ khác là diễn giải log.

Bước 3 — bốn lớp log, bốn câu hỏi khác nhau

Một tệp log không thể chứng minh một sự cố. Mỗi lớp trả lời một câu hỏi khác — và có điểm mù riêng:

LớpTrả lờiĐiểm mù
Log truy cập proxyYêu cầu nào đã đến, với mã trạng thái và kích thước thân yêu cầu nào?Không có nội dung; vòng quay log giới hạn khả năng nhìn lại
Log ứng dụng (Nextcloud)Ứng dụng đã làm gì bên trong — lỗi, cảnh báo, đăng nhập?Ở mức info chỉ có sự kiện, không có truy cập
Log FPMWorker sụp, cạn kiệt bộ nhớ, lỗi nghiêm trọngChỉ mức PHP, không có ngữ cảnh HTTP
Journald / kernelOOM kill, segfault, core dumpKhông có ngữ nghĩa ứng dụng

Bổ sung: Fail2ban (dò mật khẩu SSH) và trình quản lý gói (thời điểm vá). Chỉ sự kết hợp mới mang lại một khẳng định — và chỉ khi các điểm mù được nêu rõ.

Phát hiện sai — “ứng dụng không ghi log”

Việc tìm log ứng dụng đã quét các thư mục thông thường (/srv, /var, /home, độ sâu giới hạn) và không thấy gì. Kết luận hiển nhiên: không có log. Kết luận đó sai.

Hai điều đã bị bỏ sót:

  1. Thư mục dữ liệu nằm ngoài phạm vi tìm kiếm. Phiên bản không lưu dữ liệu trong thư mục chương trình mà trên một volume riêng. Việc tìm kiếm rất kỹ — chỉ là không tìm ở đó.
  2. Logger có cơ chế dự phòng. Nếu tệp log đã cấu hình không tồn tại và thư mục của nó không ghi được, ứng dụng ghi vào tệp thay thế trong thư mục dữ liệu. Đó là hành vi đúng — nhưng nó che giấu một cấu hình sai.

Lỗi thực sự nằm ở tầng sâu hơn: tác vụ quay log đọc đường dẫn log từ cấu hình. Vì đường dẫn đó không tồn tại, nó so sánh kích thước của một tệp không có — và không bao giờ quay log. Kết quả: một tệp duy nhất 438 MB thay vì quay theo giờ.

flowchart TD C["Cấu hình:
logfile = thư-mục-chương-trình/data"] --> L{"Tệp có tồn tại?"} L -->|không| F["Logger dùng dự phòng:
thư-mục-dữ-liệu/nextcloud.log"] L -->|có| W["Logger ghi tệp đã cấu hình"] C --> R["Tác vụ quay log đọc
cùng đường dẫn đã cấu hình"] R --> N{"Tệp có ở đó?"} N -->|không| X["Không quay log
(tệp tăng không giới hạn)"] F --> X

Bài học này không dễ chịu vì nó liên quan đến chính công việc của chúng ta: “không tìm thấy” không phải là một phát hiện chừng nào phạm vi tìm kiếm chưa được ghi lại. Một phát hiện phủ định cần sự cẩn trọng như một phát hiện khẳng định — bao gồm việc nêu rõ nơi không tìm kiếm.

Phát hiện sai thứ hai — tường lửa

Một cổng quản trị được cho là “chỉ truy cập được trong LAN”. Việc kiểm tra không thấy quy tắc cho phép riêng cho cổng đó và ghi nhận: không có quy tắc cho phép, vậy là đóng từ bên ngoài. Điều đó cũng quá vội.

Thực tế tồn tại một quy tắc chung cho toàn bộ subnet LAN, cho phép mọi cổng. Vậy cổng đó mở trong LAN — sự bảo vệ đến từ cấu trúc mạng, không từ quy tắc cổng. Kết quả (“không truy cập được từ internet”) đúng, nhưng lý do thì không. Và một lý do sai là khởi đầu của quyết định sai tiếp theo.

Nguyên tắc: đọc tường lửa như một tổng thể — chính sách mặc định, các quy tắc cho phép chung, thứ tự. Chỉ nhìn “thiếu quy tắc cho cổng này?” là không đủ.

Log thực sự cho thấy gì

Sau khi sửa đường dẫn log, việc đánh giá mới có thể thực hiện — và bức tranh khá bình thường:

Kết quả “nóng” duy nhất là một máy quét gửi bốn yêu cầu khoảng 1 MB tới trang Adminer bị bỏ rơi — đúng mẫu mà một nỗ lực khai thác tạo ra. Cả bốn đều kết thúc bằng 502 vì socket PHP không tồn tại. Cuộc tấn công chạy vào một backend đã chết. Đó cũng là một kết quả: phát hiện nguy hiểm nhất lại chứng minh hiệu quả của một biện pháp gia cố (ngoài ý muốn).

Giới hạn của chứng cứ — và vì sao phải bảo toàn bằng chứng

Hai quy tắc lưu trữ giới hạn khẳng định:

  1. Log proxy: quay 5 × 10 MB — khoảng mười ngày nhìn lại. Kỳ quan sát được phủ đầy đủ, không hơn.
  2. Log ứng dụng: vòng quay chỉ giữ đúng một tệp trước đó. Ở lần quay kế tiếp, kỳ đã đánh giá sẽ bị ghi đè.

Vì vậy kỳ đánh giá đã được bảo toàn trước lần quay kế tiếp dưới dạng một kho lưu riêng: nén, chủ sở hữu root, quyền 640 (tệp chứa tên người dùng và địa chỉ IP), kèm tổng kiểm tra được ghi lại. Một chứng cứ không thể tái tạo có rất ít giá trị trong kiểm toán — tổng kiểm tra biến một tệp thành một khẳng định có thể kiểm chứng.

flowchart LR A["Log đang chạy
(một tệp đã quay)"] --> B["Trích xuất kỳ
2026-10-15 … 2026-10-25"] B --> C["Kho nén gzip
root:root 640"] C --> D["SHA-256 trong báo cáo"] D --> E["Tái tạo:
gunzip + wc -l"]

Điều đã làm tốt

Lần sau chúng ta sẽ làm khác

  1. Ghi lại phạm vi tìm kiếm trước khi tìm. Chỉ viết “không tìm thấy” khi đã nêu đường dẫn và độ sâu đã quét.
  2. Mỗi khẳng định về cấu hình cần một phép đối chứng. Đường dẫn log: không chỉ đọc, mà kiểm tra tệp có tồn tại, có tăng và có quay hay không.
  3. Kiểm tra vòng quay log một cách chủ động (vượt ngưỡng kích thước có chủ đích, chứng minh trong log) — không giả định là nó chạy.
  4. Đọc tường lửa như một tổng thể (chính sách mặc định, quy tắc subnet chung, thứ tự).
  5. Dỡ bỏ ngay các trang bị bỏ rơi. Một trang PHP chết không phải rủi ro, nhưng là nguồn nhiễu vĩnh viễn trong log và là điểm mù trong đánh giá.
  6. Bảo toàn bằng chứng trước khi quay log — kèm chủ sở hữu, quyền và tổng kiểm tra.
  7. Sửa các phát hiện sai, không làm mượt chúng. Báo cáo chứa phần đính chính kèm ngày và nguyên nhân. Một phát hiện sai được sửa âm thầm phá hoại niềm tin nhiều hơn chính lỗi đó.

Khả năng chuyển giao

Bản thân sự cố không kịch tính: đã vá, không bị khai thác, không cần thông báo. Điều có giá trị là bộ máy chứng cứ: phạm vi ảnh hưởng qua kiểm kê, bản vá qua gói và thời gian chạy, khai thác qua nhiều lớp log, đánh giá với giới hạn chứng cứ nêu rõ, bằng chứng kèm tổng kiểm tra — và một báo cáo có lịch sử thay đổi, trong đó có cả sai sót của chính chúng ta. Cấu trúc này có thể được quản lý phiên bản trong Git và vẫn kiểm chứng được sau nhiều năm.

Nguồn