Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
wp2shell-lab — Non-destructive detector + Docker lab for wp2shell (CVE-2026-63030 REST /batch/v1 route confusion + CVE-2026-60137 author__not_in SQLi) in WordPress core 6.9.0-6.9.4 / 7.0.0-7.0.1 | Kitploit
工具/GitHubGitHub/dinosn/wp2shell-lab
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & EducationLabs & Practice
GitHubdinosn/wp2shell-lab

wp2shell-lab

Non-destructive detector + Docker lab for wp2shell (CVE-2026-63030 REST /batch/v1 route confusion + CVE-2026-60137 author__not_in SQLi) in WordPress core 6.9.0-6.9.4 / 7.0.0-7.0.1

查看仓库
5417212个月前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
内容在请求的语言中不可用。显示英文版本。

wp2shell lab & detector + pre-auth RCE PoC

A self-contained lab, non-destructive detector, and full pre-auth RCE proof-of-concept for wp2shell — the pre-authentication vulnerability chain in WordPress core:

CVEComponentClassCVSS
CVE-2026-60137WP_Query::author__not_inSQL injection (CWE-89)9.1
CVE-2026-63030REST /batch/v1 route confusioninterpretation conflict (CWE-436) → chains to RCE7.5

Affected: WordPress core 6.9.0–6.9.4 and 7.0.0–7.0.1 (the SQLi sink alone also affects 6.8.0–6.8.5). Fixed in 6.8.6 / 6.9.5 / 7.0.2. Reported by Adam Kues (Assetnote / Searchlight Cyber); SQLi also credited to TF1T, dtro, haongo. Stock-default RCE chain (oEmbed → changeset → re-entry) by Mustafa Can İPEKÇİ (nukedx).

Update first. WordPress shipped forced auto-updates for this. This repo exists to help you verify your own estate is patched and to understand the bug — not to attack anyone. See SECURITY.md.


What it actually is

The always-true primitive is an unauthenticated, no-plugin, stock-core SQL injection giving full database read (admin password hashes, everything in wp_options/wp_users). That alone earns the 9.1 and immediate patching.

The RCE is real and works on stock-default WordPress — no FILE privilege, no persistent object cache, no plugins, no misconfigurations required. The chain uses the read-only SQLi as a row-forgery primitive (UNION ALL SELECT injects fake wp_posts rows), then leverages WordPress's own content-rendering pipeline to convert those forged rows into real database writes via oEmbed caching. From there, changeset elevation and re-entrant parse_request run in admin context, creating a new administrator account — all from a single unauthenticated HTTP request.

The full chain (no credentials, single entry point POST /?rest_route=/batch/v1)

1. Route confusion    — double-nested batch desyncs $matches/$validation so a GET
                        /wp/v2/widgets runs under posts::get_items() (public), reaching
                        WP_Query's author__not_in with attacker-controlled input.

2. Row forgery        — author__not_in is string-concatenated into SQL;
                        "1) AND 1=0 UNION ALL SELECT <23 cols> -- -" injects fake
                        WP_Post rows. per_page=-1 bypasses split_the_query (WP_Query
                        treats -1 as "no limit" → empty $limits → split=false →
                        full SELECT wp_posts.* → UNION columns match).

3. oEmbed write       — forged posts carry [embed]<self-url>[/embed]; rendering via
                        context=view makes WordPress cache real oembed_cache posts in
                        the DB (turns read-only SQLi into writes with predictable IDs).

4. Elevation+re-entry — a forged customize_changeset (user_id = real admin) plus a
                        forged post_type=request row with parent loops drives an
                        in-process re-entrant parse_request in admin context.

5. Admin creation     — POST /wp/v2/users in the same batch passes
                        current_user_can('create_users') → new administrator.

6. RCE                — login → plugin webshell upload → command execution → cleanup.

Load-bearing gadgets (why the chain holds)

The novelty is the composition, not any single bug — the individual gadgets are legitimate WordPress behaviours. Naming them (per Adam Kues's writeup) makes the forged-row graph in exploit() readable and flags what to re-audit after the entry points were patched:

  • Cache/DB reconciliation — when an in-memory cached post disagrees with its DB row, WordPress reconciles via wp_update_post() and prefers the in-memory post_type/post_status, letting an oembed_cache row be re-typed into a real post/customize_changeset.
  • Cycle-detection preserving post_content — the wp_insert_post_parent filter walks the parent chain; on a detected cycle it calls a second wp_update_post() that fixes the parent without overriding post_content. That is the critical link: it lets the forged changeset's malicious post_content survive. (This is why the forged rows use self-/mutual-parent loops.)
  • Hook replay via parse_request — publishing a post fires do_action("{$status}_{$type}"); a forged row with post_status=parse / post_type=request triggers parse_request, re-running the batch pipeline while the changeset's assumed-admin identity (wp_set_current_user) still holds.

These gadgets remain present in patched WordPress — only the two entry points (batch desync + author__not_in scalar bypass) were closed. Any new primitive that forges in-memory post cache or writes an oembed_cache row would re-enable the identical admin-takeover tail.

Render-time write primitives

All four render-time write primitives confirmed live against 7.0.1 — each forged as an unauthenticated post, rendered via the batch confusion, and the resulting DB write verified by blind SQLi:

PrimitiveTrigger markupSinkPredicted identifierVerified
oembed[embed]<url>[/embed]wp_posts row (oembed_cache)post_name = md5(url+attrs)post ID created
rsswp:rss {feedURL}wp_options site-transient_site_transient_feed_<md5(url)>option_id, 5192 B cached
navigationwp:navigationwp_posts row (wp_navigation)post_name = 'navigation' (fixed)ID created, slug navigation
calendarwp:calendarwp_optionswp_calendar_block_has_published_postsoption_id, value '1'

What each result proves:

  • oembed — the reference primitive: a fresh wp_posts row with an attacker-predicted slug (md5(url+serialize(attrs))) and a real auto-increment ID. This is the only one that gives you multiple, on-demand, attacker-named post rows — which is why the RCE chain uses it to back the forged changeset/request graph.
  • rss — the strongest general write: the key _site_transient_feed_<md5(url)> is fully predictable, and the stored bytes are the feed body the attacker's URL serves — i.e., attacker controls both key and value. It's an wp_options write (no post ID), so it's an option-poisoning primitive rather than a drop-in for the changeset backing.
  • navigation — the only other unauth "render creates a real post row" path. It's single-shot (skips if any published wp_navigation exists) with a fixed slug, so it can back at most one forged object, unlike oEmbed's N rows.
  • calendar — confirms the render→update_option path fires unauthenticated, but the option name and its '1'/'0' value are fixed/DB-derived, so it's a "write happens" demonstration with no attacker control over key or value.

Earlier conditional chains (superseded by the stock-default chain above)

  • INTO OUTFILE webshell: requires the WordPress DB user to hold global FILE privilege + a web-served secure_file_priv + the drop readable by the web user. On normal/managed hosts none of that holds.
  • SimplePie → WP_HTML_Token POP: call_user_func('wp_insert_user', user_data_array) — requires gc_enabled()=false + valid HMAC (wp_hash of exact serialized bytes, needs wp-config.php secrets).

Quick start

Requirements: Docker + Docker Compose v2, Python 3.8+ (stdlib only), make, curl.

下载工具