Skip to content

Instantly share code, notes, and snippets.

@ak4zh
Forked from ankurpandeyvns/zyxel_backup_restore.py
Created November 20, 2025 12:32
Show Gist options
  • Select an option

  • Save ak4zh/7f753810904c2916c82418abffe6cfe0 to your computer and use it in GitHub Desktop.

Select an option

Save ak4zh/7f753810904c2916c82418abffe6cfe0 to your computer and use it in GitHub Desktop.
AOT-5221ZY Backup/Restore Decryption+Encryption
#!/usr/bin/env python3
"""
================================================================================
Zyxel Router Configuration Backup/Restore Tool
================================================================================
DESCRIPTION:
This tool provides comprehensive functionality for working with Zyxel router
configuration backups. It can download, decrypt, encrypt, and restore
configuration files, as well as extract and analyze firmware images.
FEATURES:
- Download encrypted backups from Zyxel routers via HTTP
- Decrypt configuration files encrypted with AES-256-CBC
- Encrypt configuration files for restoration
- Restore configurations to router (triggers reboot)
- Extract and analyze Zyxel firmware images
- Automatic discovery of encryption password from firmware
ENCRYPTION DETAILS:
- Algorithm: AES-256-CBC
- Key Derivation: MD5 (OpenSSL EVP_BytesToKey)
- Password: EwhJaD44DfprDOs7OXx9jzAtLg5PKtD8 (hardcoded in firmware)
- Format: OpenSSL salted encryption (begins with "Salted__")
AUTHENTICATION:
The router uses MD5-based authentication:
1. Login page provides a Session ID (SID)
2. Client calculates: MD5(password:sid)
3. Submit hash with username
4. Server validates and creates session cookie (COOKIE_SESSION_KEY)
FILE FORMAT:
Encrypted: Salted OpenSSL format (starts with "Salted__")
Decrypted: XML following TR-069 InternetGatewayDevice schema
ROUTER WEB INTERFACE:
- Login Page: /cgi-bin/login_advance.cgi
- Backup/Restore: /cgi-bin/backupRestore.cgi
- Backup File: /romfile.cfg
USAGE EXAMPLES:
# Download and decrypt backup
python3 zyxel_backup_tool.py full-backup -u admin -p password -o config.xml
# Decrypt existing backup
python3 zyxel_backup_tool.py decrypt -i backup.cfg -o config.xml
# Encrypt configuration
python3 zyxel_backup_tool.py encrypt -i config.xml -o backup.cfg
# Restore configuration (WARNING: reboots router)
python3 zyxel_backup_tool.py full-restore -i config.xml -u admin -p password
# Extract firmware
python3 zyxel_backup_tool.py extract -i firmware.bin -o ./extracted
# Use custom router IP
python3 zyxel_backup_tool.py full-backup -u admin -p password -o config.xml -r 10.0.0.1
REQUIREMENTS:
Python:
- Python 3.6 or higher
- Standard library only (no external packages)
System Tools:
- openssl: For encryption/decryption operations
- binwalk: For firmware extraction (optional)
- unsquashfs: For SquashFS extraction (optional)
- strings: For binary analysis (optional)
INSTALLATION (System Tools):
macOS:
brew install openssl binwalk squashfs
Ubuntu/Debian:
sudo apt-get install openssl binwalk squashfs-tools
RHEL/CentOS:
sudo yum install openssl binwalk squashfs-tools
SECURITY NOTES:
WARNING: This tool exposes several security vulnerabilities in the router:
1. Hardcoded Encryption Key: Same password across all devices of same model
2. Weak Key Derivation: MD5 is deprecated and vulnerable to attacks
3. No Authentication: CBC mode without HMAC allows tampering attacks
4. Plaintext Credentials: All passwords visible in decrypted config
5. MD5 Password Hashing: Weak hashing algorithm for authentication
RECOMMENDATIONS:
- Only use on devices you own or have permission to access
- Change default admin passwords immediately
- Restrict web interface access to LAN only
- Keep firmware updated
- Don't share backup files publicly
LEGAL DISCLAIMER:
This tool is provided for educational and research purposes only.
Users should:
- Only analyze devices they own or have explicit permission to examine
- Comply with all applicable laws and regulations
- Use this information responsibly and ethically
- Respect manufacturer intellectual property
Unauthorized access to network devices may be illegal in your jurisdiction.
TECHNICAL IMPLEMENTATION DETAILS:
LOGIN PROCESS:
1. GET /cgi-bin/login_advance.cgi
2. Extract SID from JavaScript: var sid = 'XXXXXXXX'
3. Calculate hash = MD5(password:sid)
4. POST with Loginuser, LoginPasswordValue (hash), LoginSidValue, submitValue=1
5. Receive COOKIE_SESSION_KEY in response
6. Use cookie for subsequent requests
BACKUP DOWNLOAD:
1. POST /cgi-bin/backupRestore.cgi with configFilter=1
2. Wait 3 seconds for backup generation
3. GET /romfile.cfg
4. File is encrypted with OpenSSL AES-256-CBC
BACKUP RESTORE:
1. Encrypt XML file first (must be encrypted!)
2. POST /cgi-bin/backupRestore.cgi as multipart/form-data
3. Field: tools_FW_UploadFile (file data)
4. Field: postflag = "1"
5. Router validates, applies, and reboots
ENCRYPTION COMMAND (used by router):
openssl aes-256-cbc -md MD5 -k EwhJaD44DfprDOs7OXx9jzAtLg5PKtD8 \\
-e -in /var/config.cfg -out /var/romfile.cfg
DECRYPTION COMMAND (used by router):
openssl aes-256-cbc -md MD5 -k EwhJaD44DfprDOs7OXx9jzAtLg5PKtD8 \\
-d -in /var/tmp/upload_config.cfg -out /var/tmp/decrypt_config.cfg
FIRMWARE STRUCTURE:
- Format: UBI images within GPT partitions
- Root FS: SquashFS compressed (ubi_r0.ubifs)
- Encryption password found in: /lib/MSTC/libCmd.so
- Functions: EncodeRomfile, doSysBackupCfg, make_backup_config
CONFIGURATION FILE STRUCTURE:
<?xml version="1.0" encoding="UTF-8"?>
<InternetGatewayDevice>
<X_5067F0_Ext>
<!-- Vendor-specific extensions -->
<WebHttp>...</WebHttp>
<WlanScheduler>...</WlanScheduler>
</X_5067F0_Ext>
<ManagementServer>
<!-- TR-069 management settings -->
</ManagementServer>
<LANDevice>
<!-- LAN configuration -->
</LANDevice>
<WANDevice>
<!-- WAN configuration -->
</WANDevice>
</InternetGatewayDevice>
TROUBLESHOOTING:
Login Fails:
- Verify username and password are correct
- Check router IP address is accessible
- Ensure web interface is enabled
- Try accessing manually via browser first
Decryption Fails:
- Verify file starts with "Salted__" (check with hexdump)
- Ensure OpenSSL is installed
- Check if firmware version uses different encryption password
- Extract firmware to find correct password
Download Returns HTML:
- Session may have expired, try logging in again
- Check if backup creation is supported on this firmware
- Verify router isn't in restricted mode
Restore Fails:
- Ensure file is encrypted before upload
- Verify XML structure is valid
- Check that configuration is compatible with firmware version
- Router may need factory reset if it becomes unresponsive
VERSION HISTORY:
v1.0 (2024-11-18): Initial release
- Full backup/restore functionality
- Firmware extraction support
- Automated encryption/decryption
AUTHOR: Security Research Team
LICENSE: Educational and Research Use Only
================================================================================
"""
import sys
import os
import hashlib
import subprocess
import argparse
import time
import re
from pathlib import Path
from typing import Optional
import urllib.request
import urllib.parse
import http.cookiejar
# ============================================================================
# CONFIGURATION CONSTANTS
# ============================================================================
# Encryption password found in firmware /lib/MSTC/libCmd.so
ENCRYPTION_PASSWORD = "EwhJaD44DfprDOs7OXx9jzAtLg5PKtD8"
# Default router configuration
DEFAULT_ROUTER_IP = "192.168.1.1"
DEFAULT_USERNAME = "admin"
# Working directory for temporary files
WORK_DIR = "./zyxel_work"
# ANSI color codes for terminal output
class Colors:
"""ANSI color codes for colored terminal output"""
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m' # No Color
# ============================================================================
# LOGGER CLASS
# ============================================================================
class Logger:
"""
Simple logger with colored output for different message types
Methods:
info(msg): Blue info messages
success(msg): Green success messages
warning(msg): Yellow warning messages
error(msg): Red error messages
header(msg): Green section headers
"""
@staticmethod
def info(msg: str):
"""Print informational message in blue"""
print(f"{Colors.BLUE}[INFO]{Colors.NC} {msg}")
@staticmethod
def success(msg: str):
"""Print success message in green"""
print(f"{Colors.GREEN}[SUCCESS]{Colors.NC} {msg}")
@staticmethod
def warning(msg: str):
"""Print warning message in yellow"""
print(f"{Colors.YELLOW}[WARNING]{Colors.NC} {msg}")
@staticmethod
def error(msg: str):
"""Print error message in red to stderr"""
print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}", file=sys.stderr)
@staticmethod
def header(msg: str):
"""Print section header in green"""
print(f"\n{Colors.GREEN}{'=' * 50}{Colors.NC}")
print(f"{Colors.GREEN}{msg}{Colors.NC}")
print(f"{Colors.GREEN}{'=' * 50}{Colors.NC}")
# ============================================================================
# MAIN TOOL CLASS
# ============================================================================
class ZyxelBackupTool:
"""
Main class for Zyxel router backup/restore operations
This class handles all interactions with the Zyxel router including:
- Authentication and session management
- Downloading encrypted backups
- Encrypting and decrypting configuration files
- Restoring configurations
- Extracting and analyzing firmware
Attributes:
router_ip (str): IP address of the router
work_dir (Path): Working directory for temporary files
cookie_jar (CookieJar): HTTP cookie storage
opener (OpenerDirector): URL opener with cookie handling
Example:
tool = ZyxelBackupTool(router_ip="192.168.1.1")
if tool.login("admin", "password"):
tool.download_backup("backup.cfg")
tool.decrypt_backup("backup.cfg", "config.xml")
"""
def __init__(self, router_ip: str = DEFAULT_ROUTER_IP):
"""
Initialize the Zyxel Backup Tool
Args:
router_ip: IP address of the router (default: 192.168.1.1)
"""
self.router_ip = router_ip
self.work_dir = Path(WORK_DIR)
self.work_dir.mkdir(exist_ok=True)
self.cookie_jar = http.cookiejar.CookieJar()
self.opener = urllib.request.build_opener(
urllib.request.HTTPCookieProcessor(self.cookie_jar)
)
def _calculate_md5(self, data: str) -> str:
"""
Calculate MD5 hash of string
Used for password authentication: hash = MD5(password:sid)
Args:
data: String to hash
Returns:
MD5 hash as hexadecimal string
"""
return hashlib.md5(data.encode()).hexdigest()
def _make_request(self, url: str, data: Optional[dict] = None,
headers: Optional[dict] = None) -> bytes:
"""
Make HTTP request with cookie handling
Args:
url: URL to request
data: Optional POST data dictionary
headers: Optional HTTP headers dictionary
Returns:
Response body as bytes
Raises:
urllib.error.URLError: If request fails
"""
if headers is None:
headers = {}
if data:
data = urllib.parse.urlencode(data).encode()
request = urllib.request.Request(url, data=data, headers=headers)
response = self.opener.open(request)
return response.read()
def login(self, username: str, password: str) -> bool:
"""
Login to router and establish session
Authentication process:
1. GET login page to obtain Session ID (SID)
2. Calculate MD5 hash: hash = MD5(password:sid)
3. POST with username, hash, SID, and submit flag
4. Verify COOKIE_SESSION_KEY is set in response
Args:
username: Router admin username (usually "admin")
password: Router admin password
Returns:
True if login successful, False otherwise
Example:
tool = ZyxelBackupTool()
if tool.login("admin", "mypassword"):
print("Login successful!")
"""
Logger.header("Logging into Router")
Logger.info(f"Router: {self.router_ip}")
Logger.info(f"Username: {username}")
try:
# Get login page and extract SID
Logger.info("Fetching login page...")
login_url = f"http://{self.router_ip}/cgi-bin/login_advance.cgi"
login_page = self._make_request(login_url).decode('utf-8', errors='ignore')
# Extract SID from JavaScript: var sid = 'XXXXXXXX'
sid_match = re.search(r"var sid = '([^']+)'", login_page)
if not sid_match:
Logger.error("Failed to extract SID from login page")
return False
sid = sid_match.group(1)
Logger.info(f"Session ID: {sid}")
# Calculate MD5 hash: MD5(password:sid)
hash_input = f"{password}:{sid}"
password_hash = self._calculate_md5(hash_input)
Logger.info(f"Password hash: {password_hash}")
# Perform login
Logger.info("Authenticating...")
login_data = {
"Loginuser": username,
"LoginPasswordValue": password_hash,
"LoginSidValue": sid,
"submitValue": "1"
}
self._make_request(login_url, data=login_data)
# Verify login by checking for session cookie
session_cookie = None
for cookie in self.cookie_jar:
if cookie.name == "COOKIE_SESSION_KEY":
session_cookie = cookie.value
break
if session_cookie:
Logger.success("Login successful!")
return True
else:
Logger.error("Login failed!")
return False
except Exception as e:
Logger.error(f"Login error: {e}")
return False
def download_backup(self, output_file: str) -> bool:
"""
Download encrypted backup from router
Process:
1. POST to /cgi-bin/backupRestore.cgi with configFilter=1
2. Wait 3 seconds for backup generation
3. GET /romfile.cfg (encrypted backup file)
4. Verify file starts with "Salted__" header
Args:
output_file: Path to save encrypted backup file
Returns:
True if download successful, False otherwise
Note:
Requires active session (call login() first)
Example:
tool.login("admin", "password")
tool.download_backup("backup.cfg")
"""
Logger.header("Downloading Backup from Router")
try:
# Trigger backup creation
Logger.info("Triggering backup creation...")
backup_url = f"http://{self.router_ip}/cgi-bin/backupRestore.cgi"
headers = {"Referer": backup_url}
self._make_request(backup_url, data={"configFilter": "1"}, headers=headers)
# Wait for backup to be created
Logger.info("Waiting for backup file to be generated...")
time.sleep(3)
# Download backup
Logger.info("Downloading backup file...")
romfile_url = f"http://{self.router_ip}/romfile.cfg"
backup_data = self._make_request(romfile_url, headers=headers)
# Save to file
with open(output_file, 'wb') as f:
f.write(backup_data)
# Verify download
if os.path.exists(output_file) and os.path.getsize(output_file) > 0:
# Check if it's actually encrypted (should start with "Salted__")
with open(output_file, 'rb') as f:
header = f.read(8)
if header == b'Salted__':
size = os.path.getsize(output_file)
Logger.success(f"Backup downloaded successfully: {output_file} ({size} bytes)")
return True
else:
Logger.error("Downloaded file is not encrypted (may be an error page)")
with open(output_file, 'r') as f:
print(f.read())
return False
else:
Logger.error("Download failed!")
return False
except Exception as e:
Logger.error(f"Download error: {e}")
return False
def decrypt_backup(self, input_file: str, output_file: str,
password: str = ENCRYPTION_PASSWORD) -> bool:
"""
Decrypt backup file using OpenSSL AES-256-CBC
Decryption command equivalent:
openssl aes-256-cbc -md MD5 -k <password> -d -in <input> -out <output>
File format:
Bytes 0-7: "Salted__" (ASCII header)
Bytes 8-15: 8-byte random salt
Bytes 16+: AES-256-CBC encrypted data
Args:
input_file: Path to encrypted backup file
output_file: Path to save decrypted XML file
password: Encryption password (default: hardcoded password from firmware)
Returns:
True if decryption successful, False otherwise
Example:
tool.decrypt_backup("backup.cfg", "config.xml")
"""
Logger.header("Decrypting Backup File")
Logger.info(f"Input: {input_file}")
Logger.info(f"Output: {output_file}")
# Verify input file exists
if not os.path.exists(input_file):
Logger.error(f"Input file not found: {input_file}")
return False
# Check if file is encrypted
with open(input_file, 'rb') as f:
header = f.read(8)
if header != b'Salted__':
Logger.warning("File does not appear to be OpenSSL encrypted (no 'Salted__' header)")
try:
# Decrypt using OpenSSL
Logger.info("Decrypting...")
cmd = [
'openssl', 'aes-256-cbc',
'-md', 'MD5',
'-k', password,
'-d',
'-in', input_file,
'-out', output_file
]
result = subprocess.run(cmd, capture_output=True, text=True)
# Check for errors (ignore deprecation warnings)
if result.returncode != 0 and 'bad decrypt' in result.stderr:
Logger.error("Decryption failed! Wrong password or corrupted file.")
return False
# Verify output is valid XML
if os.path.exists(output_file) and os.path.getsize(output_file) > 0:
with open(output_file, 'r') as f:
first_line = f.readline().strip()
if first_line.startswith('<?xml'):
size = os.path.getsize(output_file)
with open(output_file, 'r') as f:
lines = len(f.readlines())
Logger.success("Decryption successful!")
Logger.info(f"Output file: {output_file} ({size} bytes, {lines} lines)")
return True
else:
Logger.error("Decrypted file is not XML (decryption may have failed)")
return False
else:
Logger.error("Decryption failed!")
return False
except FileNotFoundError:
Logger.error("OpenSSL not found! Please install OpenSSL.")
return False
except Exception as e:
Logger.error(f"Decryption error: {e}")
return False
def encrypt_config(self, input_file: str, output_file: str,
password: str = ENCRYPTION_PASSWORD) -> bool:
"""
Encrypt configuration file using OpenSSL AES-256-CBC
Encryption command equivalent:
openssl aes-256-cbc -md MD5 -k <password> -e -in <input> -out <output>
Output format:
Bytes 0-7: "Salted__" (ASCII header)
Bytes 8-15: 8-byte random salt
Bytes 16+: AES-256-CBC encrypted data
Args:
input_file: Path to XML configuration file
output_file: Path to save encrypted backup file
password: Encryption password (default: hardcoded password from firmware)
Returns:
True if encryption successful, False otherwise
Note:
Input file should be valid XML in TR-069 format
Example:
tool.encrypt_config("modified.xml", "backup.cfg")
"""
Logger.header("Encrypting Configuration File")
Logger.info(f"Input: {input_file}")
Logger.info(f"Output: {output_file}")
# Verify input file exists
if not os.path.exists(input_file):
Logger.error(f"Input file not found: {input_file}")
return False
# Check if file is XML
with open(input_file, 'r') as f:
first_line = f.readline().strip()
if not first_line.startswith('<?xml'):
Logger.warning("Input file does not appear to be XML")
try:
# Encrypt using OpenSSL
Logger.info("Encrypting...")
cmd = [
'openssl', 'aes-256-cbc',
'-md', 'MD5',
'-k', password,
'-e',
'-in', input_file,
'-out', output_file
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
Logger.error(f"Encryption failed: {result.stderr}")
return False
# Verify output has correct format
if os.path.exists(output_file) and os.path.getsize(output_file) > 0:
with open(output_file, 'rb') as f:
header = f.read(8)
if header == b'Salted__':
size = os.path.getsize(output_file)
Logger.success("Encryption successful!")
Logger.info(f"Output file: {output_file} ({size} bytes)")
return True
else:
Logger.error("Encrypted file format is incorrect")
return False
else:
Logger.error("Encryption failed!")
return False
except FileNotFoundError:
Logger.error("OpenSSL not found! Please install OpenSSL.")
return False
except Exception as e:
Logger.error(f"Encryption error: {e}")
return False
def restore_backup(self, config_file: str) -> bool:
"""
Upload and restore configuration to router
WARNING: This will reboot the router!
Process:
1. Verify file is encrypted (must start with "Salted__")
2. Create multipart/form-data request
3. POST to /cgi-bin/backupRestore.cgi with:
- tools_FW_UploadFile: encrypted file data
- postflag: "1"
4. Router applies configuration and reboots
Form structure from router JavaScript:
<form name="uiPostUpdateForm" method="post"
action="/cgi-bin/backupRestore.cgi"
enctype="multipart/form-data">
<input type="file" name="tools_FW_UploadFile" />
<input type="hidden" name="postflag" value="1" />
</form>
Args:
config_file: Path to encrypted configuration file
Returns:
True if upload successful, False otherwise
Note:
- Configuration file MUST be encrypted first
- Router will reboot after successful restore
- Will need to login again after reboot
Example:
tool.encrypt_config("modified.xml", "backup.cfg")
tool.login("admin", "password")
tool.restore_backup("backup.cfg")
"""
Logger.header("Restoring Configuration to Router")
Logger.warning("This will overwrite router configuration and reboot the device!")
# Verify file is encrypted
with open(config_file, 'rb') as f:
header = f.read(8)
if header != b'Salted__':
Logger.error("Configuration file must be encrypted first!")
Logger.info(f"Use: {sys.argv[0]} encrypt -i <xml_file> -o <encrypted_file>")
return False
try:
Logger.info("Uploading configuration...")
backup_url = f"http://{self.router_ip}/cgi-bin/backupRestore.cgi"
# Read file data
with open(config_file, 'rb') as f:
file_data = f.read()
# Create multipart form data
# This mimics the HTML form submission from the web interface
boundary = '----WebKitFormBoundary' + os.urandom(16).hex()
headers = {
'Content-Type': f'multipart/form-data; boundary={boundary}',
'Referer': backup_url
}
# Build multipart body
# Format follows RFC 2388 (multipart/form-data)
body = []
body.append(f'--{boundary}'.encode())
body.append(b'Content-Disposition: form-data; name="tools_FW_UploadFile"; filename="config.cfg"')
body.append(b'Content-Type: application/octet-stream')
body.append(b'')
body.append(file_data)
body.append(f'--{boundary}'.encode())
body.append(b'Content-Disposition: form-data; name="postflag"')
body.append(b'')
body.append(b'1')
body.append(f'--{boundary}--'.encode())
body.append(b'')
body_bytes = b'\r\n'.join(body)
# Make request
request = urllib.request.Request(
backup_url,
data=body_bytes,
headers=headers
)
self.opener.open(request)
Logger.success("Configuration uploaded!")
Logger.warning("Router is rebooting... This may take 1-2 minutes.")
Logger.info("You will need to log in again after reboot.")
return True
except Exception as e:
Logger.error(f"Restore error: {e}")
return False
def extract_firmware(self, firmware_file: str, output_dir: str) -> bool:
"""
Extract and analyze Zyxel firmware image
Process:
1. Use binwalk to extract UBI images from firmware
2. Locate SquashFS root filesystem (ubi_r0.ubifs)
3. Extract SquashFS using unsquashfs
4. Search for encryption password in libCmd.so
Firmware structure:
- Format: UBI images within GPT partitions
- Root FS: ubi_r0.ubifs (SquashFS compressed)
- Config: ubi_Config.ubifs
- Kernel: ubi_k0.ubifs
- Encryption password location: /lib/MSTC/libCmd.so
Args:
firmware_file: Path to firmware binary file
output_dir: Directory to extract firmware to
Returns:
True if extraction successful, False otherwise
Requires:
- binwalk: Firmware analysis and extraction
- unsquashfs: SquashFS filesystem extraction
- strings: Binary string extraction (optional)
Example:
tool.extract_firmware("firmware.bin", "./extracted")
"""
Logger.header("Extracting Firmware")
Logger.info(f"Firmware: {firmware_file}")
Logger.info(f"Output directory: {output_dir}")
# Check for required tools
try:
subprocess.run(['binwalk', '--version'], capture_output=True, check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
Logger.error("binwalk is required for firmware extraction")
Logger.info("Install with: brew install binwalk (macOS) or apt-get install binwalk (Linux)")
return False
try:
subprocess.run(['unsquashfs', '-v'], capture_output=True, check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
Logger.error("unsquashfs is required for filesystem extraction")
Logger.info("Install with: brew install squashfs (macOS) or apt-get install squashfs-tools (Linux)")
return False
try:
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Extract firmware using binwalk
Logger.info("Running binwalk extraction...")
subprocess.run(
['binwalk', '-e', firmware_file],
cwd=output_dir,
check=True
)
# Find SquashFS filesystem
Logger.info("Locating root filesystem...")
squashfs_files = list(Path(output_dir).rglob('*ubi_r0.ubifs'))
if not squashfs_files:
Logger.error("Could not find root filesystem")
return False
squashfs_file = str(squashfs_files[0])
Logger.info(f"Found: {squashfs_file}")
# Extract SquashFS
Logger.info("Extracting SquashFS...")
rootfs_dir = os.path.join(output_dir, 'rootfs')
subprocess.run(
['unsquashfs', '-d', rootfs_dir, squashfs_file],
capture_output=True
)
if os.path.exists(rootfs_dir):
Logger.success("Firmware extracted successfully!")
Logger.info(f"Root filesystem: {rootfs_dir}")
# Search for encryption password in libCmd.so
Logger.info("Searching for encryption password in firmware...")
libcmd_path = os.path.join(rootfs_dir, 'lib', 'MSTC', 'libCmd.so')
if os.path.exists(libcmd_path):
try:
result = subprocess.run(
['strings', libcmd_path],
capture_output=True,
text=True
)
if ENCRYPTION_PASSWORD in result.stdout:
Logger.success(f"Found encryption password in libCmd.so: {ENCRYPTION_PASSWORD}")
except Exception:
pass
return True
else:
Logger.error("Extraction failed!")
return False
except Exception as e:
Logger.error(f"Extraction error: {e}")
return False
# ============================================================================
# COMMAND LINE INTERFACE
# ============================================================================
def main():
"""
Main entry point for command-line interface
Parses command-line arguments and executes the requested operation.
"""
parser = argparse.ArgumentParser(
description='Zyxel Router Backup/Restore Tool v1.0',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
EXAMPLES:
Download and decrypt backup:
%(prog)s full-backup -u admin -p password -o config.xml
Encrypt and restore configuration:
%(prog)s full-restore -i modified.xml -u admin -p password
Decrypt existing backup file:
%(prog)s decrypt -i backup.cfg -o config.xml
Encrypt configuration file:
%(prog)s encrypt -i config.xml -o backup.cfg
Extract firmware:
%(prog)s extract -i firmware.bin -o ./extracted
Use custom router IP:
%(prog)s full-backup -u admin -p password -o config.xml -r 10.0.0.1
COMMANDS:
download Download encrypted backup from router
decrypt Decrypt an encrypted backup file
encrypt Encrypt a configuration file
restore Restore configuration to router (reboots!)
full-backup Download and decrypt in one step
full-restore Encrypt and restore in one step
extract Extract and analyze firmware image
For detailed documentation, see the header comments in this script.
"""
)
parser.add_argument('command', choices=[
'download', 'decrypt', 'encrypt', 'restore',
'full-backup', 'full-restore', 'extract'
], help='Command to execute')
parser.add_argument('-r', '--router', default=DEFAULT_ROUTER_IP,
help=f'Router IP address (default: {DEFAULT_ROUTER_IP})')
parser.add_argument('-u', '--username', default=DEFAULT_USERNAME,
help=f'Router username (default: {DEFAULT_USERNAME})')
parser.add_argument('-p', '--password', help='Router password')
parser.add_argument('-i', '--input', help='Input file path')
parser.add_argument('-o', '--output', help='Output file path')
parser.add_argument('-k', '--key', default=ENCRYPTION_PASSWORD,
help='Encryption password (default: hardcoded password from firmware)')
args = parser.parse_args()
# Create tool instance
tool = ZyxelBackupTool(router_ip=args.router)
# Execute command
if args.command == 'download':
if not args.password or not args.output:
Logger.error("download requires --password and --output")
sys.exit(1)
if tool.login(args.username, args.password):
success = tool.download_backup(args.output)
sys.exit(0 if success else 1)
sys.exit(1)
elif args.command == 'decrypt':
if not args.input or not args.output:
Logger.error("decrypt requires --input and --output")
sys.exit(1)
success = tool.decrypt_backup(args.input, args.output, args.key)
sys.exit(0 if success else 1)
elif args.command == 'encrypt':
if not args.input or not args.output:
Logger.error("encrypt requires --input and --output")
sys.exit(1)
success = tool.encrypt_config(args.input, args.output, args.key)
sys.exit(0 if success else 1)
elif args.command == 'restore':
if not args.password or not args.input:
Logger.error("restore requires --password and --input")
sys.exit(1)
confirm = input("Are you sure you want to restore configuration? This will reboot the router (yes/no): ")
if confirm.lower() != 'yes':
Logger.info("Restore cancelled")
sys.exit(0)
if tool.login(args.username, args.password):
success = tool.restore_backup(args.input)
sys.exit(0 if success else 1)
sys.exit(1)
elif args.command == 'full-backup':
if not args.password or not args.output:
Logger.error("full-backup requires --password and --output")
sys.exit(1)
temp_encrypted = os.path.join(WORK_DIR, 'temp_backup.cfg')
if tool.login(args.username, args.password):
if tool.download_backup(temp_encrypted):
if tool.decrypt_backup(temp_encrypted, args.output, args.key):
os.remove(temp_encrypted)
sys.exit(0)
sys.exit(1)
elif args.command == 'full-restore':
if not args.password or not args.input:
Logger.error("full-restore requires --password and --input")
sys.exit(1)
confirm = input("Are you sure you want to restore configuration? This will reboot the router (yes/no): ")
if confirm.lower() != 'yes':
Logger.info("Restore cancelled")
sys.exit(0)
temp_encrypted = os.path.join(WORK_DIR, 'temp_restore.cfg')
if tool.encrypt_config(args.input, temp_encrypted, args.key):
if tool.login(args.username, args.password):
if tool.restore_backup(temp_encrypted):
sys.exit(0)
sys.exit(1)
elif args.command == 'extract':
if not args.input or not args.output:
Logger.error("extract requires --input and --output")
sys.exit(1)
success = tool.extract_firmware(args.input, args.output)
sys.exit(0 if success else 1)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
Logger.warning("\nOperation cancelled by user")
sys.exit(130)
except Exception as e:
Logger.error(f"Unexpected error: {e}")
sys.exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment