Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog

CVE-2026-119977: bấm Update Now cũng có thể là lúc WordPress bị chiếm

Tung Bui Thanh (TungDaDev)16 mins read

Red padlock on black computer keyboard

Sáng thứ Tư, 28/10/2026, bạn nhận email "Your site has been updated to WordPress 7.1.4". Bạn không làm gì cả. Auto-update chạy lúc rạng sáng, site vẫn lên bình thường, khách vẫn mua hàng. Một việc tốt, ai cũng khuyên bật.

Có điều trong khoảng vài giây của lần cập nhật đó, một đống file của bản WordPress mới đang nằm ngoài hiên, trong một thư mục ai gõ đúng địa chỉ cũng chạm tới được. Và cùng đêm hôm ấy đã có người đứng sẵn gõ cửa.

Đó là cốt truyện của CVE-2026-119977. Cái lỗi nằm ở chính nút "cập nhật", thứ mà lâu nay mình vẫn coi là phần an toàn của hệ thống.

# dòng thời gian

Mình gom các mốc từ nguồn gốc (WordPress, GitHub advisory, CISA) và đánh dấu chỗ nào chỉ có nguồn thứ cấp.

ngàysự kiện
28/10/2026WordPress phát hành 7.1.4, backport về mọi nhánh còn hỗ trợ tới 4.7. Mục số 6 trong 18 mục bảo mật: "race condition during core updates that could lead to remote code execution", người báo cáo là Umut Bayram
28/10/2026Wordfence tung rule firewall cho khách trả phí, request khớp đầu tiên xuất hiện khoảng 9 phút sau (theo bài tổng hợp của rescana và webhosting.today, mình không mở được bài gốc của Wordfence)
29/10/2026WordPress công bố advisory GHSA-4w3m-c66w-69mg, chấm mức Low, ghi "No known CVE"
31/10/2026CVE-2026-119977 được công bố trên cve.org, Wordfence làm CNA cấp số (theo một nguồn thứ cấp)
3/11/2026CISA đưa vào KEV, hạn xử lý cho cơ quan liên bang 6/11/2026, kèm yêu cầu forensic triage theo BOD 26-04
4/11/2026Báo chí trích Wordfence: 12.632 lượt tấn công bị chặn trong 24 giờ

Có hai điều lạ trong bảng. Thứ nhất, bản vá ra trước khi có CVE, nên suốt mấy ngày đầu ai chỉ theo dõi CVE sẽ không thấy gì. Thứ hai, hai bên chấm điểm lệch nhau hẳn: WordPress gọi là Low, còn CVSS 3.1 do Wordfence chấm là 8.1 (High), vector AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H. Chữ AC:H đáng để ý: điều kiện khai thác khó, phải trúng một cửa sổ thời gian ngắn. Nhưng một khi trúng thì không cần tài khoản, và hậu quả là chiếm site. Theo mình thì cả hai cách chấm đều có lý của nó, chỉ là đứng ở hai góc nhìn khác nhau: WordPress nhìn xác suất, CISA nhìn hậu quả cộng với bằng chứng bị khai thác thật.

# race condition và TOCTOU, nói bằng chuyện đi xem phim

TOCTOU là viết tắt của time-of-check to time-of-use: kiểm tra lúc này, dùng lúc khác, và giữa hai lúc đó thế giới có thể đã đổi.

Cứ tưởng tượng bạn gọi điện cho rạp: "Ghế A5 còn trống không?". Chị bán vé nói còn. Bạn nói "ok chị giữ giúp em, em chạy qua liền". Trong lúc bạn đang chạy xe, một người khác đứng ngay quầy mua mất A5. Lời "còn trống" lúc nãy vẫn đúng tại thời điểm chị trả lời, nhưng đã hết đúng khi bạn dùng nó. Lỗi không nằm ở chị bán vé, cũng không nằm ở bạn. Nó nằm ở khoảng hở giữa bước hỏi và bước mua mà không ai khoá lại.

Trong lập trình, khoảng hở này hay gặp ở file: kiểm tra file tồn tại rồi mới mở, kiểm tra quyền rồi mới ghi, và tới nay vẫn là một nguồn lỗi bảo mật đều đặn (CWE-367). Với CVE này, khoảng hở không phải nằm giữa hai dòng code, mà nằm giữa hai pha của việc nâng cấp: giải nén bản mới ra, rồi chép nó đè lên site thật.

# cơ chế: vì sao nâng cấp lại mở cửa

Theo bài phân tích của chính người tìm ra lỗi (Umut Bayram, Picus Security), luồng nâng cấp core bằng gói đầy đủ (full package) đại khái như sau:

  1. Tải gói WordPress về.
  2. Giải nén vào wp-content/upgrade/.
  3. Kiểm tra các file đã biết.
  4. Chép từng file vào site đang chạy.
  5. Xoá thư mục tạm.

Đoạn mô phỏng dưới đây là minh hoạ đơn giản hoá của mình, không phải code gốc của WordPress, chỉ để thấy chỗ hở nằm ở đâu:

// MINH HOẠ ĐƠN GIẢN HOÁ, không phải code gốc
$staging = WP_CONTENT_DIR . '/upgrade/' . $package_name;   // tên đoán được
extract_package( $zip, $staging );                          // (A) file mới nằm sẵn trên web
verify_known_files( $staging );
// ... khoảng hở: staging vẫn truy cập được qua HTTP ...
copy_over_live_site( $staging, ABSPATH, $skip_list );       // (B) chép đè site thật
delete_dir( $staging );

Giữa (A) và (B), thư mục staging chứa đầy đủ ruột của WordPress mới, kể cả bộ cài đặt wp-admin/setup-config.php, thứ vốn chỉ dành cho site chưa có wp-config.php. Có ba điều kiện cùng tụ lại làm lỗi này có thật:

  • Thư mục nằm trong wp-content/, tức là nằm trong thư mục web phục vụ ra ngoài, và PHP chạy được ở đó.
  • Tên thư mục đoán được. Tuỳ đời WordPress mà cách đặt tên khác nhau: theo bài của Picus, từ 2.8 đến 4.3 và từ 5.9 đến 7.1.3 tên suy ra từ tên file gói, chỉ khoảng 4.4 đến 5.8 có thêm hậu tố ngẫu nhiên 6 ký tự (đây là số liệu từ một nguồn, mình chưa kiểm lại bằng code).
  • Trước 7.1.4, bước dọn dẹp xoá luôn cả index.php chặn liệt kê thư mục, nên ở một số cấu hình server còn "tự khai" luôn tên thư mục (theo threatdefence).

Quay lại setup-config.php: bản này tìm wp-config.php ở hai chỗ, trong thư mục staging và một cấp phía trên nó. Nó không biết site thật đang có sẵn file cấu hình ở chỗ khác, nên coi như site chưa cài và chạy luồng thiết lập bình thường. Kẻ tấn công đưa vào tham số database của riêng họ, bộ cài ghi ra một wp-config.php mới ngay trong thư mục staging. Đến bước (B), danh sách bỏ qua khi chép không loại wp-config.php, nên file của kẻ tấn công đè lên file thật. Lần tải trang kế tiếp, site kết nối tới database do họ kiểm soát. Nếu đó là database trống, WordPress sẽ mở luồng cài đặt, họ tạo tài khoản admin, rồi cài một plugin độc hại để chạy PHP. Từ đó là chiếm hoàn toàn.

Mình cố ý dừng ở mức mô tả này. Quan trọng là hiểu vì sao nó chạy, không cần biết gõ gì.

Có hai chi tiết khiến mình thấy lỗi này tinh tế hơn bề ngoài. Một là kẻ tấn công không biết lúc nào site của bạn nâng cấp, nên họ poll: Wordfence mô tả một số nguồn đứng gõ vào vài đường dẫn đúng chuẩn liên tục khoảng 14,5 giờ, và thêm nhiều nguồn khác dò rộng nhiều tên thư mục. Hai là cửa sổ chỉ kéo dài vài giây trong phòng lab, nên đây là cuộc đua thật sự, không phải lỗi cứ gọi là trúng. Nhưng auto-update trên hàng triệu site thì cuộc đua nào cũng có người thắng.

Về maintenance mode mà đề bài nhắc: các nguồn mình đọc (WordPress, Wordfence qua tổng hợp, Picus) không nhắc tới nó trong cơ chế. Theo suy luận của mình, trang "đang bảo trì" do WordPress dựng ra không ngăn được web server phục vụ trực tiếp một file PHP nằm trong thư mục upgrade, nhưng đây là suy luận, không phải kết luận từ nguồn nào.

# ai bị ảnh hưởng, bản nào đã vá

Advisory của WordPress ghi lỗi ảnh hưởng mọi nhánh từ 4.7 tới 7.1.3 và vá bằng 7.1.4 cùng bản backport. Picus nói phần updater cũ hơn nhiều (từ 2.7, năm 2008) nhưng các nhánh dưới 4.7 không có bản vá nào.

nhánhbị ảnh hưởngbản vá
7.17.1.0 đến 7.1.37.1.4
7.07.0.0 đến 7.0.77.0.8
6.96.9.0 đến 6.9.106.9.11
6.86.8.0 đến 6.8.116.8.12
6.0 đến 6.7tới bản kế trước bản vá6.7.11, 6.6.11, 6.5.14, 6.4.14, 6.3.14, 6.2.15, 6.1.16, 6.0.18
4.7 đến 5.9tới bản kế trước bản vá5.9.20 ... 5.0.31, 4.9.35, 4.8.34, 4.7.39
4.6 trở xuốngkhông còn hỗ trợkhông có bản vá, cần lên nhánh còn hỗ trợ

Bảng đầy đủ từng nhánh nằm trong advisory trên GitHub. Điều kiện để dính: cập nhật core bằng gói đầy đủ, thư mục wp-content/upgrade/ truy cập được từ ngoài và chạy được PHP, và xảy ra nâng cấp trong lúc kẻ tấn công đang đứng chờ. Site nào đã cập nhật xong và web server chặn thư mục này thì rủi ro còn lại chủ yếu là chuyện "đã bị lúc trước chưa".

Chú ý thêm bản nâng cấp lên 7.1.4 cũng chưa an toàn trọn vẹn. Phần giải nén vẫn do updater cũ chạy, nên hậu tố ngẫu nhiên và index.php mới chưa có tác dụng cho đúng lần cập nhật đó. Theo Picus chỉ phần loại wp-config.php khỏi bước chép là có hiệu lực ngay, vì nó nằm ở update-core.php mới đi kèm gói.

# bản vá làm gì

Ba thay đổi trong 7.1.4, cũng theo Picus:

  1. Tên thư mục staging có thêm hậu tố ngẫu nhiên 16 ký tự, không còn đoán được.
  2. upgrade/index.php được giữ lại, không còn bị xoá khi dọn dẹp.
  3. wp-config.php được thêm vào danh sách bỏ qua trong update_core().

Mình thích hướng sửa này vì nó vá ở nhiều lớp thay vì chỉ một chỗ. Cả ba lớp đều là phòng thủ chiều sâu, chỉ cần một lớp trụ được thì cuộc đua kia vô nghĩa. Lớp số 3 theo mình là quan trọng nhất: kể cả có kẻ nào đặt được file vào staging, nó cũng không đè được cấu hình thật. Còn lớp số 1 chỉ là che giấu, chống đoán chứ không chống lộ.

# kiểm tra mình có dính không

Tất cả lệnh dưới đây chỉ đọc, không sửa gì. Chạy trong thư mục gốc của site, bằng user của web.

wp core version              # phải >= bản vá của nhánh bạn đang dùng
wp core check-update         # còn bản mới nào chưa lên không
wp core verify-checksums     # so file core với checksum chính thức

Nếu site cập nhật bằng pipeline hoặc deploy thủ công, đừng tin là mọi môi trường đã lên: chạy wp core version trên từng cái (staging, production, các site phụ).

Kiểm tra thư mục tạm có đang bị phơi ra không, bằng request tới chính site của bạn:

curl -s -o /dev/null -w "%{http_code}\n" https://site-cua-ban.example/wp-content/upgrade/
# mong muốn: 403 hoặc 404, không phải 200
ls -la wp-content/upgrade/
# ngoài lúc đang nâng cấp, chỉ nên có index.php

# biện pháp tạm thời nếu chưa kịp vá

Chặn truy cập web vào wp-content/upgrade/ ở web server. WordPress đọc và ghi thư mục này qua hệ thống file chứ không qua HTTP, nên chặn như vậy không làm hỏng nâng cấp (theo Picus và threatdefence). Với nginx:

location ^~ /wp-content/upgrade/ { deny all; }

Với Apache dùng Require all denied trong khối <Directory>, hoặc RedirectMatch 403 ^/wp-content/upgrade/ trong .htaccess ở thư mục gốc. Đừng đặt .htaccess ngay trong wp-content/upgrade/, vì updater bản cũ sẽ xoá nó. Sau khi sửa thì dùng lệnh curl ở trên để xác nhận ra 403.

Một lưu ý nữa từ Picus: wp core update của WP-CLI đi qua đúng đường code dễ bị lỗi này, nên không thể xem là lối thoát. Nếu muốn thận trọng, có thể nâng cấp thủ công ngoài web root, giải nén bản mới ở nơi khác rồi rsync sang và loại trừ wp-content với wp-config.php. Cách này vất vả hơn nhiều, chỉ nên làm khi không thể chặn thư mục.

Về WAF: Wordfence có rule riêng cho bản trả phí từ 28/10, bản miễn phí nhận rule muộn hơn (theo threatdefence là 27/11/2026, một nguồn). Rule WAF chỉ giảm rủi ro, không thay thế việc cập nhật.

# dấu hiệu đã bị xâm nhập

Câu hỏi thực tế hơn là: nếu site của bạn đã tự cập nhật từ 28/10 tới nay, có ai chen vào lúc đó không? Advisory của WordPress, CISA và Wordfence không công bố IoC dạng hash hay IP. Những gì dưới đây là dấu hiệu theo cơ chế, tổng hợp từ Picus và threatdefence, nên hãy xem như gợi ý săn tìm chứ không phải danh sách đầy đủ.

Trong access log, tìm bất kỳ request .php nào tới /wp-content/upgrade/. Người dùng thật không bao giờ gọi vào đó. Response 200 là ưu tiên số một, đặc biệt là POST tới setup-config.php:

zgrep -hE '/wp-content/upgrade/.*\.php' /var/log/nginx/access.log* | head -50

Trên host, những thứ nên rà:

  • wp-config.php bị sửa quanh thời điểm nâng cấp, hoặc DB_HOST, DB_NAME, DB_USER, tiền tố bảng lạ so với trước. Nhớ đây là file bị nhắm tới, mà wp core verify-checksums chỉ so các file của core, không bắt được một wp-config.php bị thay, nên phải xem tay.
  • Tài khoản admin lạ, plugin hoặc theme mới cài gần thời điểm nâng cấp.
  • File PHP lạ trong wp-content/uploads, wp-content/mu-plugins, hoặc thư mục upgrade có thứ gì khác ngoài index.php khi không có nâng cấp nào chạy.
  • Web server mở kết nối ra ngoài tới cổng 3306 của một địa chỉ công cộng (database của kẻ tấn công), hoặc PHP-FPM sinh ra sh, curl, wget.

Nếu thấy dấu hiệu, hướng xử lý chung: đưa site offline, giữ nguyên bản sao file, database và log trước khi đụng vào, rồi dựng lại từ bản core đã vá và plugin/theme tải từ nguồn chính thức, khôi phục nội dung từ backup trước thời điểm nâng cấp, và đổi hết mật khẩu database, salts, tài khoản admin, khoá API. CISA yêu cầu cả bước forensic triage cho cơ quan liên bang, nghĩa là cập nhật xong chưa đủ, phải kiểm tra xem trước khi vá có bị chiếm chưa.

# còn những điểm chưa chắc

Mình nói thẳng chỗ nguồn chưa khớp để bạn khỏi bị dẫn sai:

  • Số tấn công. Con số 12.632 trong 24 giờ là Wordfence qua các bài tin ngày 4/11. BleepingComputer trích một con số khác, 13.095 trong 24 giờ, và Wordfence từng nêu hơn 18.500 request nhắm vào đường dẫn staging trong 21 giờ đầu. Đây đều là lượt tấn công bị chặn hoặc request ghi nhận, không phải số site bị chiếm. Chưa nguồn nào xác nhận số site bị xâm nhập.
  • PoC công khai. Một nguồn nói có PoC xuất hiện ngày 4/11, trong khi một bản tổng hợp khác nói chưa thấy repo nào. Nếu bạn gặp repo "bypass bản vá", hãy coi chừng, nguồn đó cảnh báo có repo là mồi cài malware.
  • Mức nghiêm trọng. WordPress ghi Low, Wordfence/NVD ghi 8.1. Hai con số cùng đúng với định nghĩa của riêng chúng.
  • Chi tiết cơ chế như tên thư mục theo từng đời WordPress, hay việc ghi đè wp-config.php, chủ yếu đến từ bài của Picus (tác giả lỗi) và mình chưa đối chiếu với diff code.

# bài học

Thứ nhất, thư mục tạm không phải chỗ riêng tư. Ở đây sai lầm không nằm ở chuyện dùng thư mục tạm, mà ở chỗ nó nằm dưới web root, với cái tên đoán được và có file thực thi bên trong. Đặt staging ngoài web root, hoặc ít nhất đặt tên ngẫu nhiên và chặn ở lớp server, là quy tắc rẻ mà hiệu quả. Nhiều ứng dụng khác ngoài WordPress cũng có đường "giải nén rồi mới kiểm tra" y hệt thế này.

Thứ hai, auto-update vẫn nên bật. Mình biết câu chuyện hơi trớ trêu, lỗi nằm trong chính lúc nâng cấp. Nhưng nếu không bật, site lại chạy bản có lỗi lâu hơn, và bản vá thì có mặt từ 28/10, WordPress nói thẳng site bật cập nhật nền sẽ tự lên. Rủi ro của cửa sổ vài giây nhỏ hơn rủi ro của việc ngồi trên bản cũ nhiều tuần.

Thứ ba, đừng chỉ nhìn điểm CVSS. Cả advisory đánh Low lẫn "AC:High" đều dễ làm người ta hoãn lại. Chính khả năng bị khai thác thật, thể hiện qua KEV, mới là tín hiệu để đẩy việc lên đầu hàng đợi. Và khi bản vá ra trước CVE, hãy theo dõi release note chứ đừng chờ số CVE.

Thứ tư, một lớp phòng thủ chiều sâu như mấy dòng cấu hình nginx ở trên có giá trị ngang với bản vá trong lúc chờ. Cho vào template server của cả team để lần sau không phải làm vội.

Việc nên làm ngay hôm nay: chạy wp core version trên mọi site, kiểm tra /wp-content/upgrade/ trả về 403, và rà log từ 28/10 xem có request .php nào tới thư mục đó không. Nếu bạn còn site ở nhánh dưới 4.7 thì lên kế hoạch nâng cấp, vì nhánh đó sẽ không bao giờ có bản vá.

Tài liệu tham khảo:


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.