Skip to content
Draft
392 changes: 392 additions & 0 deletions Tools/hg8145v5_ntp_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,392 @@
#!/usr/bin/env python3
"""
Huawei HG8145V5 password management and recovery tool.

The ISP (Megacable) changes admin passwords daily via TR-069.
This tool provides three methods to regain access:

Method 1 - set-password (RECOMMENDED, no NTP needed):
Patches the config XML to set a permanent custom password
and disables TR-069 so the ISP cannot overwrite it.

Method 2 - login (use cracked/known password):
The original admin password is 'admintelecom' (cracked from config).
Works if TR-069 hasn't changed it yet.

Method 3 - ntp-server (fallback):
Runs a fake NTP server so the router uses a date whose
daily password you already know.

Requirements:
pip install pycryptodome

Usage:
# Set a permanent admin password (recommended)
python3 hg8145v5_ntp_password.py set-password \\
--password MySecurePass123 \\
--input configs/hw_ctree_optimized.xml \\
--output configs/hw_ctree_custom.xml

# Show credentials
python3 hg8145v5_ntp_password.py login --date 2025-03-16

# Fake NTP server (fallback, needs root)
sudo python3 hg8145v5_ntp_password.py ntp-server --date 2025-03-16
"""
import argparse
import datetime
import hashlib
import os
import socket
import struct
import sys

# Known daily credentials (captured from the device)
KNOWN_CREDENTIALS = {
"2025-03-16": {
"Mega_gpon": "eef90b1496430707",
"Meg4_root": "eb52daf690f49e85",
},
}

# CRACKED: Original admin password stored in config before TR-069 changes it.
# Mega_gpon: "admintelecom" verified via PBKDF2-SHA256
ORIGINAL_PASSWORD = "admintelecom"


def _generate_password_hash(password, salt, iterations=5000):
"""Generate PBKDF2-SHA256 hash matching the router's format."""
dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'),
salt.encode('utf-8'), iterations, dklen=32)
return dk.hex()


def _generate_salt(length=24):
"""Generate a random Base64-like salt matching the router's format."""
charset = ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs'
'tuvwxyz0123456789+/')
return ''.join(charset[b % len(charset)] for b in os.urandom(length))


def get_credentials(date_str):
"""Get known credentials for a specific date."""
if date_str in KNOWN_CREDENTIALS:
return KNOWN_CREDENTIALS[date_str]
normalized = date_str.replace("-", "")
for k, v in KNOWN_CREDENTIALS.items():
if k.replace("-", "") == normalized:
return v
return None


def set_password_in_config(input_path, output_path, web_password,
ssh_password=None, disable_tr069=True):
"""Patch the config XML to set custom admin passwords.

Sets permanent passwords for Mega_gpon (web) and Meg4_root (SSH)
by generating new PBKDF2-SHA256 hashes and encrypting them.
Optionally disables TR-069 to prevent the ISP from overwriting.
"""
try:
import xml.etree.ElementTree as ET
except ImportError:
print("ERROR: xml.etree.ElementTree not available")
sys.exit(1)

# Import the encryption toolkit
toolkit_dir = os.path.dirname(os.path.abspath(__file__))
if toolkit_dir not in sys.path:
sys.path.insert(0, toolkit_dir)
from huawei_fw_toolkit import value_encrypt

if ssh_password is None:
ssh_password = web_password

tree = ET.parse(input_path)
root = tree.getroot()
changes = 0

# Set Mega_gpon (web admin) password
for user in root.iter('X_HW_WebUserInfoInstance'):
username = user.get('UserName', '')
if username == 'Mega_gpon':
salt = user.get('Salt', '')
if not salt:
salt = _generate_salt()
user.set('Salt', salt)
new_hash = _generate_password_hash(web_password, salt, 5000)
encrypted_hash = value_encrypt(new_hash)
user.set('Password', encrypted_hash)
user.set('PassMode', '3')
changes += 1
print(" [OK] Mega_gpon password set (web admin)")

# Set Meg4_root (SSH/CLI) password
for user in root.iter('X_HW_CLIUserInfoInstance'):
username = user.get('Username', '')
if username == 'Meg4_root':
salt = user.get('Salt', '')
if not salt:
salt = _generate_salt()
user.set('Salt', salt)
new_hash = _generate_password_hash(ssh_password, salt, 5000)
encrypted_hash = value_encrypt(new_hash)
user.set('Userpassword', encrypted_hash)
user.set('EncryptMode', '2')
changes += 1
print(" [OK] Meg4_root password set (SSH/CLI)")

# Disable TR-069 to prevent ISP from overwriting passwords
if disable_tr069:
for ms in root.iter('ManagementServer'):
ms.set('EnableCWMP', '0')
ms.set('X_HW_EnableCWMP', '0')
ms.set('PeriodicInformEnable', '0')
changes += 1
print(" [OK] TR-069 (CWMP) disabled")

if changes == 0:
print(" WARNING: No user accounts found to update")
return False

tree.write(output_path, encoding='unicode', xml_declaration=False)

print(f"\n Config saved: {output_path}")
print("\n Credentials after uploading config:")
print(" Web (https://192.168.100.1):")
print(" User: Mega_gpon")
print(f" Pass: {web_password}")
print(" SSH/Telnet:")
print(" User: Meg4_root")
print(f" Pass: {ssh_password}")
if disable_tr069:
print("\n TR-069 disabled: ISP cannot change the password.")
print(" WARNING: Disabling TR-069 may prevent ISP firmware updates")
print(" and remote management. Re-enable if needed.")
return True


def show_credentials(date_str):
"""Show known credentials for a date."""
creds = get_credentials(date_str)

print()
print('=' * 58)
print(" HG8145V5 Password Recovery")
print('=' * 58)

print()
print(" METHOD 1: Original password (cracked from config)")
print('─' * 58)
print(f" User: Mega_gpon Pass: {ORIGINAL_PASSWORD}")
print(" (Works if TR-069 hasn't changed it)")

print()
print(" METHOD 2: Set a permanent custom password")
print('─' * 58)
print(f" python3 {sys.argv[0]} set-password \\")
print(" --password YOUR_PASSWORD \\")
print(" --input configs/hw_ctree_optimized.xml \\")
print(" --output configs/hw_ctree_custom.xml")
print(" Then upload hw_ctree_custom.xml to the router.")
print(" TR-069 will be disabled so the ISP can't change it.")

if creds:
print()
print(f" METHOD 3: Daily credentials for {date_str}")
print('─' * 58)
if "Mega_gpon" in creds:
print(f" User: Mega_gpon Pass: {creds['Mega_gpon']}")
if "Meg4_root" in creds:
print(f" User: Meg4_root Pass: {creds['Meg4_root']}")
print(f" (Use NTP trick to set router date to {date_str})")
else:
print()
print(f" No known daily credentials for {date_str}")
print(" Available dates:")
for d in sorted(KNOWN_CREDENTIALS.keys()):
print(f" {d}")

print()
print('=' * 58)


def ntp_timestamp(dt):
"""Convert datetime to NTP timestamp (seconds since 1900-01-01)."""
ntp_epoch = datetime.datetime(1900, 1, 1)
delta = dt - ntp_epoch
return int(delta.total_seconds())


def build_ntp_response(recv_data, target_date):
"""Build an NTP response packet with the target date/time."""
li_vn_mode = (0 << 6) | (4 << 3) | 4
stratum = 1
poll = 6
precision = -20

root_delay = 0
root_dispersion = 0
ref_id = b'LOCL'

target_dt = datetime.datetime.strptime(target_date, "%Y-%m-%d")
target_dt = target_dt.replace(hour=12, minute=0, second=0)

ts = ntp_timestamp(target_dt)
ts_frac = 0

if len(recv_data) >= 48:
orig_ts = recv_data[40:48]
else:
orig_ts = b'\x00' * 8

packet = struct.pack('!BBBb', li_vn_mode, stratum, poll, precision)
packet += struct.pack('!II', root_delay, root_dispersion)
packet += ref_id
packet += struct.pack('!II', ts, ts_frac)
packet += orig_ts
packet += struct.pack('!II', ts, ts_frac)
packet += struct.pack('!II', ts, ts_frac)

return packet


def run_ntp_server(target_date, bind_addr="0.0.0.0", port=123):
"""Run a fake NTP server that returns a fixed date."""
creds = get_credentials(target_date)

print("╔══════════════════════════════════════════════════════╗")
print("║ HG8145V5 NTP Password Recovery Server ║")
print("╠══════════════════════════════════════════════════════╣")
print(f"║ Target Date: {target_date:<39s} ║")
print(f"║ Listening: {bind_addr}:{port:<33} ║")
print("╠══════════════════════════════════════════════════════╣")
if creds:
print(f"║ Credentials for {target_date}: ║")
for user, pwd in creds.items():
print(f"║ User: {user:<15s} Pass: {pwd:<16s} ║")
else:
print(f"║ No known credentials for {target_date} ║")
print("║ Capture them after NTP sync and add to script ║")
print("╠══════════════════════════════════════════════════════╣")
print("║ Steps: ║")
print("║ 1. Set router NTP to this PC's IP ║")
print("║ 2. Reboot router or wait for NTP sync ║")
print("║ 3. Login with credentials above ║")
print("║ Press Ctrl+C to stop ║")
print("╚══════════════════════════════════════════════════════╝")
print()

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

try:
sock.bind((bind_addr, port))
except PermissionError:
print(f"ERROR: Port {port} requires root/admin privileges.")
print(f" sudo python3 {sys.argv[0]} ntp-server --date {target_date}")
sys.exit(1)
except OSError as e:
print(f"ERROR: Cannot bind to {bind_addr}:{port}: {e}")
sys.exit(1)

print(f"[*] NTP server running on {bind_addr}:{port}")
print(f"[*] Serving date: {target_date}")
print()

try:
while True:
data, addr = sock.recvfrom(1024)
response = build_ntp_response(data, target_date)
sock.sendto(response, addr)
print(f"[NTP] {addr[0]}:{addr[1]} -> Served date {target_date}")
except KeyboardInterrupt:
print("\n[*] NTP server stopped.")
finally:
sock.close()


def main():
parser = argparse.ArgumentParser(
description='HG8145V5 password management and recovery tool',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Set a permanent custom password (RECOMMENDED, no NTP needed)
%(prog)s set-password --password MyPass123 \\
--input configs/hw_ctree_optimized.xml \\
--output configs/hw_ctree_custom.xml

# Show all recovery methods
%(prog)s login --date 2025-03-16

# NTP server fallback (requires root)
sudo %(prog)s ntp-server --date 2025-03-16

# List known credential dates
%(prog)s list
""")

subparsers = parser.add_subparsers(dest='command', help='Command')

# set-password command (MAIN FEATURE)
sp = subparsers.add_parser('set-password',
help='Set permanent admin password in config')
sp.add_argument('--password', '-p', required=True,
help='New admin password for web and SSH')
sp.add_argument('--ssh-password',
help='Separate SSH password (default: same as --password)')
sp.add_argument('--input', '-i', required=True,
help='Input config XML file')
sp.add_argument('--output', '-o', required=True,
help='Output config XML file')
sp.add_argument('--keep-tr069', action='store_true',
help='Do NOT disable TR-069 (ISP can still change pwd)')

# login command
lp = subparsers.add_parser('login',
help='Show credentials / recovery methods')
lp.add_argument('--date', default='2025-03-16',
help='Date for daily credentials (YYYY-MM-DD)')

# ntp-server command
np = subparsers.add_parser('ntp-server',
help='Run fake NTP server (fallback)')
np.add_argument('--date', required=True,
help='Target date (YYYY-MM-DD)')
np.add_argument('--bind', default='0.0.0.0',
help='Bind address (default: 0.0.0.0)')
np.add_argument('--port', type=int, default=123,
help='Port (default: 123)')

# list command
subparsers.add_parser('list', help='List known credential dates')

args = parser.parse_args()

if args.command == 'set-password':
print("\n Setting password in config...")
set_password_in_config(
args.input, args.output,
web_password=args.password,
ssh_password=args.ssh_password,
disable_tr069=not args.keep_tr069,
)
elif args.command == 'login':
show_credentials(args.date)
elif args.command == 'ntp-server':
run_ntp_server(args.date, args.bind, args.port)
elif args.command == 'list':
print("\nKnown daily credential dates:")
for d in sorted(KNOWN_CREDENTIALS.keys()):
creds = KNOWN_CREDENTIALS[d]
print(f" {d}: {', '.join(creds.keys())}")
if not KNOWN_CREDENTIALS:
print(" (none)")
print(f"\nOriginal password (cracked): {ORIGINAL_PASSWORD}")
else:
parser.print_help()


if __name__ == '__main__':
main()
Loading