diff --git a/README.md b/README.md index b19ade7..d4cb086 100644 --- a/README.md +++ b/README.md @@ -619,6 +619,49 @@ print(cam.get_upgrade_info()) cam.upgrade("General_HZXM_IPC_HI3516CV300_50H20L_AE_S38_V4.03.R12.Nat.OnvifS.HIK.20181126_ALL.bin") ``` +## Enable telnet & ipctool backup + +`telnet_opener.py` uses the `OPSystemUpgrade` / `InstallDesc` exploit on +Xiongmai cameras to run shell commands as root, with no firmware change. + +```sh +# 1. Enable telnet on port 23 (camera reboots once). +python3 telnet_opener.py 10.0.0.10 + +# 2. Open a non-persistent shell on port 4321 (no reboot). +python3 telnet_opener.py 10.0.0.10 -t + +# 3. Make a full hardware backup with ipctool, written to your NFS share. +python3 telnet_opener.py 10.0.0.10 -b --nfs 10.0.0.1:/srv/ipctool +``` + +Default telnet credentials on Xiongmai stock firmware are `root` / `xmhdipc`. + +`-b/--backup` requires that telnet is already enabled (step 1) and that you +export an NFS share containing the [`ipctool`][ipctool] ARM32 binary. The +script telnets in, mounts your share at `/utils`, runs +`ipctool backup /utils/backup-`, and unmounts. The resulting +`backup-` file lives on your NFS server — nothing is uploaded to a +third party. + +Minimal NFS server setup on the host machine: + +```sh +# /etc/exports +/srv/ipctool 10.0.0.0/8(rw,no_root_squash,no_subtree_check,insecure) +``` + +```sh +sudo cp ipctool /srv/ipctool/ +sudo exportfs -arv +``` + +This replaces the previous cloud-upload flow that was disabled per +[OpenIPC/ipctool#78][issue78] — backups now stay on your network. + +[ipctool]: https://github.com/OpenIPC/ipctool +[issue78]: https://github.com/OpenIPC/ipctool/issues/78 + ## Monitor Script This script will persistently attempt to connect to camera at `CAMERA_IP`, will create a directory named `CAMERA_NAME` in `FILE_PATH` and start writing separate video and audio streams in files chunked in 10-minute clips, arranged in folders structured as `%Y/%m/%d`. It will also log what it does. diff --git a/telnet_opener.py b/telnet_opener.py index 0eff74f..1ac3788 100755 --- a/telnet_opener.py +++ b/telnet_opener.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 from dvrip import DVRIPCam -from telnetlib import Telnet import argparse import datetime import json import os +import re import socket import time import requests @@ -63,14 +63,8 @@ conf = { def add_flashes(desc, swver): - board = conf.get(swver) - if board is None: - return - - fls = [] - for i in board["flashes"]: - fls.append({"FlashID": i}) - desc["SupportFlashType"] = fls + board = conf.get(swver, XMV4) + desc["SupportFlashType"] = [{"FlashID": fid} for fid in board["flashes"]] def get_envtool(swver): @@ -112,14 +106,63 @@ def cmd_telnetd(port): } -def cmd_backup(): - return [ - { - "Command": "Shell", - "Script": "mount -o nolock 95.217.179.189:/srv/ro /utils/", - }, - {"Command": "Shell", "Script": "/utils/ipctool -w"}, - ] +def _read_until(sock, token, timeout): + deadline = time.monotonic() + timeout + buf = bytearray() + sock.settimeout(0.5) + while time.monotonic() < deadline: + try: + chunk = sock.recv(4096) + except socket.timeout: + if token in buf: + break + continue + if not chunk: + break + buf.extend(chunk) + if token in buf: + break + return bytes(buf) + + +def do_backup_via_telnet(host_ip, nfs_share, mount_point="/utils"): + if not check_port(host_ip, 23): + print(f"Telnet (port 23) is not open on {host_ip}.") + print("Enable it first by running: " + f"python3 telnet_opener.py {host_ip}") + return + + print(f"Connecting to {host_ip}:23 as root/xmhdipc") + s = socket.create_connection((host_ip, 23), timeout=10) + _read_until(s, b"login:", 5) + s.sendall(b"root\n") + _read_until(s, b"assword:", 5) + s.sendall(b"xmhdipc\n") + _read_until(s, b"# ", 5) + + s.sendall(f"mkdir -p {mount_point}\n".encode()) + _read_until(s, b"# ", 5) + s.sendall(f"mount -o nolock {nfs_share} {mount_point}\n".encode()) + out = _read_until(s, b"# ", 10).decode(errors="replace") + print(out.strip()) + + s.sendall(b"cat /sys/class/net/eth0/address\n") + out = _read_until(s, b"# ", 5).decode(errors="replace") + m = re.search(r"([0-9a-f]{2}(?::[0-9a-f]{2}){5})", out.lower()) + mac = m.group(1) if m else "unknown" + + backup_path = f"{mount_point}/backup-{mac}" + print(f"Running ipctool backup -> {backup_path}") + s.sendall(f"{mount_point}/ipctool backup {backup_path}\n".encode()) + out = _read_until(s, b"# ", 120).decode(errors="replace") + print(out.strip()) + + s.sendall(f"umount {mount_point}\n".encode()) + _read_until(s, b"# ", 5) + s.sendall(b"exit\n") + s.close() + print(f"Done. Backup file is at {nfs_share.rstrip('/')}/backup-{mac} " + "on your NFS server.") def downgrade_old_version(cam, buildtime, swver): @@ -158,9 +201,18 @@ def downgrade_old_version(cam, buildtime, swver): def open_telnet(host_ip, port, **kwargs): make_telnet = kwargs.get("telnet", False) make_backup = kwargs.get("backup", False) + nfs_share = kwargs.get("nfs") user = kwargs.get("username", "admin") password = kwargs.get("password", "") + if make_backup: + if not nfs_share: + print("--backup requires --nfs HOST:/exported/path " + "(NFS share with ipctool, where the backup will be written)") + return + do_backup_via_telnet(host_ip, nfs_share) + return + cam = DVRIPCam(host_ip, user=user, password=password) if not cam.login(): print(f"Cannot connect {host_ip}") @@ -186,8 +238,6 @@ def open_telnet(host_ip, port, **kwargs): upcmd = [] if make_telnet: upcmd.append(cmd_telnetd(port)) - elif make_backup: - upcmd = cmd_backup() else: upcmd.append(cmd_armebenv(swver)) desc["UpgradeCommand"] = upcmd @@ -199,10 +249,6 @@ def open_telnet(host_ip, port, **kwargs): cam.close() os.remove(zipfname) - if make_backup: - print("Check backup") - return - if not make_telnet: port = 23 print("Waiting for camera is rebooting...") @@ -228,7 +274,11 @@ def main(): "-p", "--password", default="", help="Password for camera login" ) parser.add_argument( - "-b", "--backup", action="store_true", help="Make backup to the cloud" + "-b", + "--backup", + action="store_true", + help="Telnet in (root/xmhdipc), mount NFS, run ipctool backup " + "(requires --nfs and telnet already enabled on the camera)", ) parser.add_argument( "-t", @@ -236,6 +286,11 @@ def main(): action="store_true", help="Open telnet port without rebooting camera", ) + parser.add_argument( + "--nfs", + help="NFS share for --backup, e.g. 10.0.0.1:/srv/ipctool. " + "Must contain the ipctool binary; backup- is written here.", + ) args = parser.parse_args() open_telnet(args.hostname, TELNET_PORT, **vars(args))