Browse Source

telnet_opener: replace dead cloud backup with local NFS+ipctool flow

The old `-b/--backup` mounted a now-defunct NFS share at 95.217.179.189
and ran `ipctool -w` (the silent S3-upload mode disabled per
OpenIPC/ipctool#78). Both pieces are gone. The new flow telnets in as
root/xmhdipc, mounts a user-supplied NFS share, and runs
`ipctool backup /utils/backup-<MAC>` so the dump stays on the user's
network.

Also fall back to the XMV4 SupportFlashType list when the camera's
SWVER is unknown — without this, older firmware (e.g. 50H20L /
00031520) rejects the InstallDesc payload entirely.
pull/4/head
Dmitry Ilyin 1 month ago
parent
commit
ec4b973acf
  1. 43
      README.md
  2. 103
      telnet_opener.py

43
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-<MAC>`, and unmounts. The resulting
`backup-<MAC>` 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.

103
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-<MAC> is written here.",
)
args = parser.parse_args()
open_telnet(args.hostname, TELNET_PORT, **vars(args))

Loading…
Cancel
Save