Reusing retired UMTS femtocells

Access: getting into the device

🔑 THIS PAGE ASSUMES THE DEVICE IS YOURS. THAT IS NOT A DISCLAIMER — IT IS THE SUBJECT.

Everything here is written for someone holding hardware they bought, pointing it at a core they run. ⭐ That is not packaging around the technical content; it is what the technical content is for. A guide to reusing your own device and a guide to attacking someone else’s are different documents even where a paragraph would look the same. ⛔ Nothing here is for equipment or a network you do not own — not a carrier’s, not a neighbour’s, not one you found. No route on this page is published to help you reach somebody else’s unit, and every one of them needs physical or LAN access you would only have to your own. 📌 What you may publish, what you may not, and the one bright line: LEGAL.md.

Everything below that uses guest / 1qaz@WSX describes a Ralink running FW:1.0.34. ⛔ THE ACCOUNT SETS ON THE TWO BUILDS ARE DISJOINT, NOT OVERLAPPING:

FW:1.0.34   guest:…:1001:1001:guest:/tmp:/bin/sh   <- THE ENTIRE FILE. No root. No sshd.
FW:1.0.31   root:…:0:0:  +  sshd:…:0:0:            <- THE ENTIRE FILE. NO `guest` AT ALL.
FW:1.0.29   root + sshd, same as 1.0.31

⇒ ⛔ On a 1.0.29/1.0.31 unit, guest/1qaz@WSX DOES NOT EXIST and the login is refused. ⇒ ☠️ AND A FAILED LOGIN ON THIS DEVICE’S SINGLE-CLIENT telnetd IS INDISTINGUISHABLE FROM A BRICKED BOARD — so the first conclusion available to you is the worst one, and it is wrong. ✅ On 1.0.29/1.0.31 log in as root. On 1.0.34 log in as guest.

✅ CHECK THE UNIT IN FRONT OF YOU — ONE COMMAND, AND IT CANNOT GO STALE:

sh cat /etc_ro/version # -> FW:1.0.34 -- Fri Apr 20 20:43:20 CST 2012 # or FW:1.0.31 – Thu Oct 6 09:37:21 CST 2011 ⭐ The IMAGE states its own identity. This document cannot. An undated, unversioned procedure is undetectably stale: there is no anchor for a reader to compare against the device, so nothing in the text can contradict a unit that has been re-flashed or bank-flipped. ⚠️ Dates in this file certify WHEN something was measured, never WHETHER IT STILL HOLDS.

Scope: the shell-level detail is measured on a DPH-151. The two-processor architecture is established for the 151 and 153. The DPH-154 is a different design — see HARDWARE.md — and none of the chip-level detail here should be assumed to apply to it.


🎯 WHICH ROUTE REACHES WHICH CHIP — READ THIS BEFORE PICKING ONE

[added 2026-09-13. This document listed six routes for months and never said which SoC each one lands on. A lane with Ralink root read the whole file looking for the way onward.]

route lands on works on a COLD unit?
1 — SSH to the radio processor picoChip .186 ⛔ NO — “available once you have installed a key.” It does not tell you how to install one.
2 — DMI console picoChip’s attribute model ⚠️ configures the cell. Not a shell.
3 — the Ralink gateway (telnet · guest/pw · IPC injection · wizard UDP 14677) Ralink .185 ✅ YES — this is the one that works cold
4 — serial console both had UART headers ⚠️ case opening. Not the path used here.
5 — the management plane (TR-069/CWMP) picoChip config ⚠️ config only. Not a shell — but see PATH D
6 — the nano3G’s DMI ⛔ a DIFFERENT DEVICE n/a to a DPH-151

There is no documented route that gets you a shell on the picoChip from scratch. Route 1 presupposes the key; Routes 2 and 5 are configuration, not shells; Route 4 needs the case open. ⇒ ⭐⭐⭐ The chain that closes it is PATH D — the CWMP route TO a shell:

CMHS/ACS binds -> Download RPC -> sdphook -> post_swdl_hook runs it AS ROOT
               -> enables sshd + installs a key -> THEN Route 1 becomes available

⇒ Route 1 is the DESTINATION, not an entry point.

✅✅ AND PATH D IS NOT A THEORY — IT IS HOW .244’s PICOCHIP ACTUALLY GOT ROOT.

`[findings-dph151-root-baseline.md:1-4 — “first root baseline, measured 2026-09-05 21:59Z. READ OFF THE DEVICE OVER SSH AS uid=0 ON THE PICO (192.168.157.186, via the Ralink

:22 DNAT). Everything below is measured, not inferred." — and :8 attributes the nv_env write to "OUR v7 HOOK", :57 to "the hook installed JP's dph151-jp key".]` ⚠️ **This page previously said PATH D was "assembled from measured parts". That hedge was WRONG and it was mine** — it turns a demonstrated capability into a hypothesis, which is exactly what stops the next reader using it. ### ⛔ **TWO OBSTACLES, BOTH FROM THE SAME FILE, BOTH NAMED** ``` :33 persistent_ssh.sh kills DslmSsp — THE MAIN APPLICATION. On a unit that is NOT mid-transaction it plausibly CAUSES the reboot it exists to prevent. ⇒ kill rmmwd swdl_client post_swdl_hook ONLY. :47 the retry authenticated with cwmp_rce_key while the hook had installed dph151-jp. DIFFERENT KEYS, rc=255 six times. ⭐ A lost race and a rejected key are indistinguishable from outside — only the return code separated them. ✅ v8 installs BOTH public keys. ``` ⏱️ **SSH opens for ~30–60 s and then the unit auto-reboots.** `persistent_ssh.sh` must run **inside that window** or the service closes again. ⛔ **The upstream hook installs a PUBLISHED private key** (tracked in a public third-party repo). **Mint a fresh pair before driving it.** ⛔ **Its upstream hook installs a PUBLISHED private key** (tracked in a public third-party repo). **Mint a fresh pair before it is ever driven.**

There are two computers in the box

On the DPH-151 and DPH-153 the unit is two SoCs, and confusing them wastes days:

  picoChip SoC   the RADIO half. Runs the ip.access femtocell stack,
                 the DMI management interface, the cell, Iuh.
       │
       │  a private point-to-point link, /30, two usable addresses,
       │  fixed at the factory in RFC1918 space
       │
  Ralink SoC     the NETWORK GATEWAY half. DHCP server, NAT, and the
                 pico's default route to your LAN.

Everything published about rooting these devices in 2012 is about the Ralink — the UART header, the SSH password, the wizard backdoor. Everything that gates RF is on the picoChip. “Assume root” usually means root on the half that does not control the radio.

⭐ But the Ralink is the gateway, so from Ralink root you control every packet the radio half sees — which is what makes “be the infrastructure” viable without ever rooting the picoChip.

ip addr / ifconfig on either side, or the boot log. They are factory-fixed, but write down what your unit says rather than trusting any number in a guide, including this one.

The Ralink DNATs several TCP ports straight through to the picoChip. ⇒ The SSH you reach at the unit’s LAN address is the picoChip’s, not the Ralink’s. The Ralink’s own SSH port is closed — because it has no SSH server at all (below).

⚠️ THE PORT LIST IS PER-UNIT — READ IT OFF YOUR OWN NAT TABLE, DO NOT INHERIT IT

`[measured 2026-09-14: .244 forwards 22, 80, 8080, 20000; .106 forwards 22, 80, 8080 — no

  1. lucid-console154, live iptables -t nat -L PREROUTING -n, both controls passing.] ⇒ **set_port_fwd is a settable verb, so the forwards ARE per-unit configuration.** A list lifted from another unit's findings is a .244 fact wearing a .106` label — exactly the device-scope error this corpus repeats most. Enumerate; never assume the fourth rule is there.

⚠️ “NO SSH” IS NOT “NO REMOTE SHELL” — TWO READERS MADE THAT LEAP ON 2026-09-13. The Ralink has telnetd, and it is reachable. See Route 3 below. Do not read this paragraph as “the gateway SoC is unreachable”; it says only that the protocol is not SSH.


Route 0 — THE ONE THAT ACTUALLY WORKS, AND IT IS ALL OVER THE NETWORK

🎯 THIS IS HOW A DPH GETS ROOT ON ITS PICOCHIP. NO JTAG. NO SERIAL. NO SOLDERING.

[JP, 2026-09-13, verbatim: **"no we did it all over netork"** · and Osmocom Discourse topic 2625 post 41 (tempest → nickvsnetworking), recovered by JP because Anubis blocks automated fetch. Full text: (private lab notes: Discourse topic 2625 post 41, tempest, 2026-09-04)]

⛔ EVERY OTHER ROUTE ON THIS PAGE LANDS ON THE RALINK, OR NEEDS A KEY THAT IS ALREADY ON THE PICO. Route 0 is the step that crosses between them, and it was missing from this document until 2026-09-13 — which cost a lane an entire evening on Route 5, which this page says in bold is not how root is obtained.

The procedure

1. ROUTE + FIREWALL + TWO LAN IPs
   a host route to 192.168.157.186/30 via the DPH's LAN IP, allowed through your firewall,
   and TWO LAN IPs on your side (the ACS and CMHS must be on DIFFERENT addresses —
   the femto sends NO TLS SNI, so the handler and certificate are chosen BY DESTINATION IP).

2. ./certpatch.sh  — IT BUILDS TWO CERTS FOR TWO DIFFERENT LEGS. THE RCE IS ON THE ACS LEG.
     chain203.pem  ACS   femtocell.wireless.att.com   O=att, no OU        IP:10.0.0.228  <- THE TARGET
     chain202.pem  CMHS  cmhs*.wireless.att.com       O=attmobility,TPE   IP:10.0.0.36
   ⭐ tempest activates ONE CMHS FQDN and it differs per unit ("my unit likes looking up
     cmhsse-decatur..."), but CMHS IS NEVER DEFEATED — IT IS BYPASSED. The shell comes from
     the ACS/TR-069 leg. WATCH WHAT YOUR UNIT DIALS; do not guess and do not activate all eight.

3. SERVE THE DOWNLOAD. The unit pulls it from the python server.

4. ⏱ SSH IS OPEN FOR ~30-60 SECONDS, AND THEN THE UNIT REBOOTS ITSELF.

5. ./persistent_ssh.sh  — INSIDE THAT WINDOW.
   ⛔ Without it the unit reboots and shuts the SSH server off again.

⏱ THE 30-60 SECOND WINDOW IS THE WHOLE OPERATION — HAVE STEP 5 READY TO RUN BEFORE STEP 3.

The self-reboot is EXPECTED BEHAVIOUR, not a fault and not a sign you broke something. ⇒ Plan it as a race you have already staged for, not as a step you perform when you see it.

⚠️ THE CERTIFICATE DATES, AND THE ORIGINAL AUTHOR’S OWN DOUBT — KEPT BECAUSE IT IS HONEST

The reason given is that the Ralink’s clock resets to 1/1/2000 whenever power is pulled, so certificate validity must start at 1/1/2000. ⚠️ tempest is explicitly sceptical of that reasoning: “I ran the date command over telnet, and it had no effect on the problem where the DPH terminated the connection early, maybe it’s something else? But it does work.” ⇒ ⭐ The BACKDATING IS LOAD-BEARING; the EXPLANATION for it is not established. Do the backdating; do not build on the reason.

📋 WHAT “IT TERMINATED THE CONNECTION EARLY” LOOKS LIKE — so you recognise it

Completes mutual TLS, presents its factory Cisco client certificate, sends ZERO application bytes, and FINs at ~11-12 ms. [measured on the wire here 2026-09-13, and matching findings-lead-howwegotin.md's description of the pre-fix state] ⇒ That is the symptom Route 0 exists to cure. It is NOT a firewall, NOT a routing fault, and NOT a server-side bug — a same-endpoint control shows a working unit succeeding against the identical IP, handler and server certificate.

✅✅ AND IT IS CONFIRMED ON A DPH-151 — BY THE PERSON WHO RAN IT

[JP, 2026-09-13, verbatim: **"the 153 exploit to access the pico worked on the 151 we did it alrday"** · and **"no we did it all over netork"**] ⇒ ⭐⭐⭐ THE 153’s PICO EXPLOIT WORKS ON THE 151. IT HAS BEEN DONE ON <unit-151-a>, WHICH HAS A LIVE ROOT SHELL ON BOTH CHIPS TODAY (ip202ff, uid 0 — verified 2026-09-13). ⛔ THIS REPO’S OWN reference/working-scripts/README.md CARRIES THE OPPOSITE CAUTION: “These target a DPH-153. Our unit is a DPH-151… tempest states plainly that the 151/154 have no reliable instructions.” ⇒ That caution was correct WHEN WRITTEN and is now superseded by our own operational result. It is the reason nobody here reached for this route for days. ⭐⭐ The SOFTWARE half transfers. It is only the 153’s JTAG/HARDWARE half that does not — and Route 0 needs none of it.

📌 Also known to work on at least 4 DPH-153 units (tempest, post 41).


Route 1 — SSH to the radio processor

This is the comfortable route, and it is available once you have installed a key.

⛔ A modern OpenSSH client will refuse this device three times, for three different

reasons, in sequence.

The daemon is an old dropbear on a 2.6-series kernel and offers only algorithms modern OpenSSH has disabled. You will hit key exchange, then host key, then cipher — fixing each one reveals the next. Several people here rediscovered one flag at a time over weeks because each error looks like the final answer.

✅ Put it all in ~/.ssh/config once:

Host mycell
    HostName <ip>
    User root
    KexAlgorithms +diffie-hellman-group1-sha1
    HostKeyAlgorithms +ssh-rsa
    PubkeyAcceptedAlgorithms +ssh-rsa
    Ciphers +aes128-cbc
    MACs +hmac-sha1

A bare ssh <ip> will keep failing. That is the client refusing, not the device being down.

Making SSH survive a reboot

Four independent layers, all worth having, because any one of them can be undone by a firmware operation:

  1. Keys on persistent flash. Put the authorised-keys file on the durable jffs2 partition and symlink root’s home at it — the rootfs does not survive.
  2. Re-assert the console/access flag at every boot, from a startup script. A software download can flip the daemon to listen on loopback only, which looks exactly like the box being dead.
  3. Relaunch the daemon each boot if it is not already listening.
  4. Re-check periodically, unconditionally, and before any radio logic — so a fault in the radio path can never strand the box with no way in.

Route 2 — the DMI management console

The ip.access stack exposes a DMI interface that speaks get / set / action against the device’s attribute model. This is the interface that actually configures the cell — and, critically, the one that moves what goes on the air. The management/TR-069 tree is a different store; see trap 15 in TRAPS.md.

Probe 8090 first on any new unit. If it is open you may not need a root shell at all, and the whole project collapses to a config exercise.

⚠️ Three things about port 8090 that are not what you would guess

  1. It was NOT listening by default on our DPH-151. Measured: zero listeners, with /proc/net/tcp confirmed readable so it is a real zero rather than a blind one. It has to be started — and the library path is required or the binary dies immediately.
  2. “Unauthenticated” is now MEASURED — but on the sibling, not on a MicroCell. On an ip.access nano3G the port is a telnet DMI console with no authentication step of any kind, demonstrated rather than assumed, and it is a full root path from there (Route 6). On a DPH-151 it remains inferred: no login step appears in any documented sequence, and nobody here has demonstrated it, because on our unit the port never listened. ⇒ Plan for it being unauthenticated; do not claim it is, on a MicroCell.
  3. ✅ There is a better path that bypasses the socket entirely — the DMI client has a one-shot command mode you can run over SSH. It is non-interactive, scriptable, and does not consume the single-client port. Prefer it.

⚠️ And two ways the telnet front end fails without telling you

⚠️ Two transports, and the wrong one is silent rather than broken

On our DPH-151 the telnet front end accepted input and never answered a get — bare LF, CRLF, and Telnet linemode negotiation all returned a banner and a dmi> prompt with no result — while the local one-shot client answered the byte-identical commands.

✅ Validate your transport with a known-present attribute before believing any silence. Otherwise “attribute not found” and “transport dead” are the same observation.

⚠️ One command per invocation. A single bad attribute aborts a whole DMI batch, so a valid attribute in a bad batch reads as does not exist.

⚠️ Put a timeout on every call. A get can hang indefinitely even as the only client on the box. Without a bound, one hung call stalls everything after it — and a killed call must not abort the run.

⛔ Never kill the management daemon. It owns the TR-069 session, the data model and the DMI action path. Killing it destroys the channel you are working over; on our unit a published “clean up these processes” list caused a reboot.


🚪 THIS ROUTE IS TWO DOORS, AND THIS PAGE USED TO LIST THEM AS ONE LINE

The summary table above reads telnet · guest/pw · IPC injection · wizard UDP 14677 as a single entry. ⛔ They are not one thing, and the difference decides which you can use:

telnet  tcp 23   NEEDS CREDENTIALS (build-dependent)   -> INTERACTIVE shell, WITH OUTPUT
wizard  udp 14677  NO credentials at all               -> BLIND exec. Replies to hardcoded
                                                           multicast 234.2.2.7, never to you.

⭐⭐ PREFER TELNET WHENEVER YOU HAVE THE CREDENTIALS — an interactive shell beats blind execution on the same chip at the same privilege. ⚠️ The backdoor sounds more capable and is less capable. ⇒ It is for the case telnet is not available: wrong build, unknown password, or telnetd not running. ⛔ AND NEITHER REACHES THE picoChip. wizard is a MIPS binary on the Ralink’s own filesystem, and the Ralink cannot reach the pico’s filesystem by any route [measured with root, 2026-09-14]. 📌 Full comparison, with the port-scan behaviour and how to prove blind execution worked: BRINGUP.md → TWO DOORS INTO THE RALINK. Pointer, not a copy.

The Ralink has no SSH server as shipped. [read from both extracted rootfs banks of a firmware image — ⚠️ this is a read of the IMAGE, not of a live device. If something was installed at runtime this read would look identical and be wrong.] The /etc/ssh directory is empty and dated 2011; there is no sshd and no dropbear; busybox has no ssh applets.

What it does have is telnetd, bound to the internal point-to-point address only.

🔴🔴 CORRECTED 2026-09-13 — THE INFERENCE THAT USED TO SIT HERE IS FALSE AND IT COST A LANE AN EVENING

“so it is not reachable from your LAN, and every path to it goes through the picoChip.” [MEASURED 2026-09-13: root obtained on .106's Ralink from katana, by host route + telnet, WITHOUT touching the picoChip. This sentence is why nobody tried it for weeks.]

⭐⭐⭐ A BIND ADDRESS DECIDES WHICH PACKETS A DAEMON ACCEPTS. ROUTING DECIDES WHICH PACKETS ARRIVE.

The Ralink owns BOTH the LAN address AND 192.168.157.185. So a host route pointed at the LAN address is delivered locally, to telnetd, with no picoChip in the path:

sudo ip route add 192.168.157.185/32 via <lan-ip>     # additive, reversible
telnet 192.168.157.185                                 # guest / 1qaz@WSX

⇒ ⛔ “Bound to an address that is not on your LAN” is NOT “unreachable from your LAN.” ⚠️ The premise was true and stayed true — telnetd really is bound -b 192.168.157.185. Only the conclusion was wrong, which is why re-reading the sentence kept confirming it. 📌 See BRINGUP.md Phase 1b, and ~/Projects/microcell/keys/dph151/rroot.py.

⛔ And it structurally CANNOT keep an SSH server, which is why nobody should try

The Ralink’s root filesystem is an initramfs embedded in the kernel image. There is no rootfs partition, /var and /tmp are RAM, and the small flash has no data partition. ⇒ Persistent SSH there means rebuilding and reflashing the kernel — on the processor that NATs every packet to the radio half. Brick it and the radio becomes unreachable too.

⭐ Note why the inference from an image is sound here specifically: the image is the root filesystem by construction, so “no daemon in the image” really does mean “no daemon after any reboot”. That inference would be invalid on a device with writable flash — do not carry it across.

Access routes, all from fail0verflow’s 2012 work:

⚠️ The commonly-cited NAME for this route is wrong, and the artefact carries the error

The route that actually yields root on a DPH-151 is telnet, a hardcoded vendor guest password, and a command injection through the local IPC client — the three bullets above. It is frequently called a “CWMP RCE”, and it is not one: CWMP is not involved.

⭐ The misnomer is baked into the tooling — the key file people pass around is named for CWMP — so it propagates every time someone quotes the filename. Expect every other write-up you find to use the wrong name, and do not go looking for a TR-069 vulnerability that is not there.

⭐ The backdoor survived a hardware generation

fail0verflow published against the 151 in 2012 and the same binary is still present in the 153. It was not patched between them. ⚠️ That raises the prior for the 154; it does not settle it, and a negative there is a result worth publishing.

⚠️ Searching a firmware image for the decimal string 14677 returns zero — a port lives in code as a number, not as text. It appears as an immediate load of the byte-swapped value. A string search is the wrong instrument for a port number, and its zero is not evidence of absence.


Route 4 — serial console

Both SoCs had UART headers. On the 151, fail0verflow used header JP1 at 56700 baud (try 57600 too).

Header JP1 on the 151: pin 1 RX, pin 2 TX, pin 3 GND.

⛔ Use a 3.3 V-only adapter, and ⭐ make your first session receive-only — with the adapter’s TX unconnected, even a 5 V adapter cannot damage the device, so you can start capturing before you have proved anything about voltages. Full rationale, pinout and the baud transposition trap are in HARDWARE.md.

The boot log is the richest single artefact on these devices: it names the boot order, the configuration mechanism, the inter-processor link, and precisely what fails when the operator’s infrastructure is unreachable. Capture it to a file the first time.


Route 5 — the management plane itself

⚠️ READ THE RECONCILIATION EIGHT LINES BELOW BEFORE ACTING ON THIS ROUTE’S VERDICT.

Short form: “TR-069 is not how root is obtained” is TRUE OF THE RALINK and FALSE OF THE PICOCHIP. ⇒ For a pico shell, TR-069 IS the route — persistent_ssh.sh:6 runs ssh -i cwmp_rce_key root@192.168.157.186, and that is how .244 first got root [findings-dph151-root-baseline.md:1-4]. See Route 0.

🔴 CORRECTION TO MY OWN BANNER, WHICH STOOD HERE FOR FOUR MINUTES

[nebula-librarian3 — three errors in one edit, all mine, none caught by my own review]

1. I wrote "TR-069 IS how root is obtained" FLATLY. The reconciliation below already said it
   better: BOTH claims are correct and they are ABOUT DIFFERENT PROCESSORS. My version would
   send someone to CWMP for a RALINK shell, where it is the WRONG route.
2. I wrote that the correction sat "~115 lines BELOW". IT IS EIGHT. I estimated a distance
   instead of counting it -- the relative-coordinate error this corpus cards -- inside a
   banner whose entire subject was placement.
3. My diagnosis followed from that wrong number: "a correction downstream of its error is
   invisible". At eight lines and clearly written, that explains nothing.

⭐⭐ THE REAL EXPLANATION IS AMBIGUITY, NOT DISTANCE, and the reconciliation already named it: “THIS SENTENCE IS TRUE OF THE RALINK AND FALSE OF THE PICOCHIP, AND THE AMBIGUITY COST AN EVENING.” ⇒ On a two-processor device, a sentence true of one chip and false of the other is read as whichever chip the reader has in mind. ⛔ I “fixed” a document that had already fixed itself, less accurately, and justified it with a number I had not measured.

The TR-069 / CWMP stack is a legitimate configuration path. If you control DNS for the device — and you do, since it is on your network and its management hostnames are dead — you can answer as its management server without touching the device at all. See CONFIG.md.

[reconciled 2026-09-13 after JP: "we have all the info there you are still guesiing"]

ACCESS.md Route 5 (here)           "TR-069 is NOT how root is obtained on this device"
reference/working-scripts/README   "Root on the PICOCHIP is obtained via RCE over CWMP/TR-069
                                    on the ACS leg, then an SSH key is planted."

⇒ ⭐⭐⭐ BOTH ARE CORRECT AND THEY ARE ABOUT DIFFERENT PROCESSORS. The RALINK is rooted by telnet + a vendor guest password + IPC injection (Route 3) — and calling THAT a “CWMP RCE” is the misnomer. The PICOCHIP is rooted over the ACS/TR-069 leg — see Route 0. ⛔ Neither document cited the other, so a reader arriving at Route 5 is told the management plane is a dead end and sent away from the only route that produces a pico shell. ⚠️ It is not how root is obtained ON THE RALINK, despite a widely-repeated name that says otherwise. See the correction under Route 3 — and Route 0 for the picoChip, where it IS the route.

The point for a reader is the architecture: the device is designed to be provisioned remotely by whoever answers those hostnames, and on your own network that is you.

On getting root: the method is documented, the key is yours to make

Root on a DPH-151 comes from a publicly documented procedure, not from anything discovered here — the wizard backdoor above is the published route, and it lands you a root shell from which you install your own SSH key.

✅ Generate your own keypair. Nobody else’s key is of any use to you, and a key that has been published is worse than no key. There is no credential in this repo for the same reason there is no point in one.

⛔ And do not “confirm it worked” by running the tool a second time — that check cannot fail. See trap 22 in TRAPS.md; it is the single most likely way a reader misleads themselves here.

Route 6 — the nano3G’s DMI console, where the management plane is the root path

Measured on an ip.access nano3G, not on a MicroCell — see HARDWARE.md for what does and does not transfer. On our DPH-151 port 8090 was not listening at all, so none of this was reachable there. It is here because the two devices run the same software train, because the shape is a class of bug worth recognising, and because the defence is one write and nobody would think to apply it.

The console, and the whole of its access control

TCP 8090 is a plain telnet DMI console with no authentication step of any kind — measured on this hardware, not inferred. It exposes the attribute model read/write plus the action table. Everything below follows from it being open.

It is open only when two conditions hold at once. From /etc/init.d/opnormal’s dmistart():

if [ -f ${DMISCRIPT} ]; then          # /var/ipaccess/init.dmi EXISTS
    clean_sets_from_initdmi
    IS_DMI_AUTOGEN=`grep -c "// init.dmi - THIS FILE IS AUTO-GENERATED" ${DMISCRIPT}`
    if [ $IS_DMI_AUTOGEN -eq 0 ] && [ -x ${PROG_DMI} ]; then
        ${PROG_DMI} -c "call ${DMISCRIPT}" &      # run it
    fi
else                                  # init.dmi ABSENT -- the ONLY telnet path
    if [ ${ENV_START_DMI_TELNET:-"FALSE"} == "TRUE" ]; then
        ${PROG_DMI} -u ${DMI_TELNET_PORT} &
        open_firewall_port ${DMI_TELNET_PORT}
    fi
fi

⇒ init.dmi absent AND ENV_START_DMI_TELNET=TRUE. Both, or there is no port at all. ⚠️ Note what else lives in that branch: open_firewall_port. The firewall is not a second layer here — it is opened by the same code that opens the port.

Where that flag comes from — and it is a button, not a bug

The firmware never writes ENV_START_DMI_TELNET. Enumerated over the whole of rcS:

$SETNVENV call sites, entire file        2      ENV_VERBOSE_CONSOLE_ENABLED
                                                ENV_FIREWALL_DISABLED
ENV_START_DMI_TELNET, anywhere in rcS    0      <- never written by the firmware
CONTROL: ENV_VERBOSE_CONSOLE_ENABLED     4      <- the reader works, so the 0 means something

And a factory restore deletes nv_env.sh outright, so on the next boot the variable is not set to FALSE — it is absent, and opnormal falls through to its own :-"FALSE" default.

⇒ ⭐ A virgin or factory-restored unit has the console OFF BY ABSENCE, and the commissioning web UI’s toggle is its only writer — dmi_config.cgi, which serves exactly this:

<input type="submit" name="dmi_telnet" value=Enable >

✅ Confirmed end to end on one unit: the toggle was pressed during commissioning, and after Complete Commissioning plus two power cycles the variable read TRUE. Since no firmware path writes it, that TRUE can only have come from the button — and it persists.

⭐⭐ So the chain has two stages, and the first one is a supported feature

STAGE 1   the COMMISSIONING WEB UI enables the console. Not an exploit -- a BUTTON,
          and the only thing that sets the flag the branch above depends on.
STAGE 2   the console, now reachable, is unauthenticated.

⇒ A vendor-supported commissioning control turns on an unauthenticated, root-equivalent interface, and nothing else can turn it on.

⭐ This sharpens the defence rather than weakening it. There is no patch to wait for and no subtle hardening to get right: the operator’s entire control surface is that checkbox and the two conditions in that if. Nothing else in the firmware touches the flag.

⚠️ And it closes symmetrically, which is worth knowing before you press anything: a factory restore deletes nv_env.sh, so a restore turns the console back off — along with whatever else you had configured. That is the same event as trap 49, where the threshold is shorter than the manuals say. The reset button is both the recovery path and the thing that removes your access.

⛔ Measured on an ip.access nano3G. The DPH equivalent is UNMEASURED and should not be assumed — on our DPH-151 the port never listened at all.

The read primitive: call echoes what it cannot parse

call is documented as “Executes a DMI script”. It takes a path, opens it, and echoes every line as ### Executing: <line> before failing to parse it. ⇒ Any file the DMI process can open is readable — every config file, every init script, the NV environment.

⭐ Read the ### Executing: lines and not the error echo — the error path lowercases, so a value’s case is destroyed in exactly the copy most people scroll to.

📌 This is genuinely useful for legitimate work, which is worth saying plainly: it is how you answer “is that key actually in the config file” without having a shell at all.

The write primitive: a URL-shaped attribute landing in a file that is sourced as root

/var/ipaccess/nv_env.sh is sourced as root early in boot. Several MIB string attributes are written into it verbatim, as export lines. crlServerBaseUrl (2203) is one:

set crlServerBaseUrl="http://example/"   ->   export ENV_CRL_BASE_SERVER="http://example/"

That is the whole bug, and it is worth stating as a class rather than as a trick: a configuration string is interpolated into a shell file that is later executed, with no validation and no quoting discipline. ;, backticks, $( and | all round-trip unmodified.

⚠️ The detail that makes it work where you might expect it not to: the DMI parser owns the double quote, so the shell string cannot be terminated from inside the value. It does not need to be — $(...) command substitution is evaluated inside double quotes. A value of the shape x$(…) therefore has its contents run as root, at every boot, when nv_env.sh is sourced. No second service, no network fetch, no memory corruption: the device does it because that is what sourcing a shell file means.

⚠️ A structural caveat, which is also why this is a two-reboot procedure and not a one-shot: $( ) runs in a subshell, so it cannot change the environment of the parent. Altering an NV variable means editing the file and booting again.

⭐⭐⭐ A THIRD write path on the SAME console, and it is strictly stronger: diagnosticTuning (2320)

🔴 ADDED 2026-09-16. THIS DOCUMENT TAUGHT THE NARROW PRIMITIVE AND OMITTED THE GENERAL ONE.

crlServerBaseUrl lets you inject into the VALUE of ONE variable you did not choose. diagnosticTuning lets you choose the variable NAME. ⇒ Same console, same sink, more reach.

It is a vendor debug hook taking an arbitrary name/value list. [read end to end from ipa-mgr_app 563.16.0 — the attribute-set dispatcher at cmp r5,#2320]

set diagnosticTuning=({name=ENV_VERBOSE_CONSOLE_ENABLED,value=TRUE})

The dispatcher walks the list and branches on what the NAME CONTAINS:

name contains "ENV_"              -> setNvEnvVar()   -> /var/ipaccess/nv_env.sh
name contains "CLI_STARTUP_CMD"   -> CreateSignal(0x505) -> SendSignal(partner 5)
name contains "CLI_IMD_CMD"       -> CreateSignal(0x504) -> SendSignal(partner 5)

⇒ ⭐⭐ So there are TWO primitives behind one attribute — an NV-environment write and a command-signal dispatch — and the second has nothing to do with nv_env.sh at all.

And the NV write has the same missing quoting discipline as the route above:

setNvEnvVar(name, value, variant):
    snprintf(newline, 1024, "export %s=\"%s\"\n", name, value)
    ... strstr(line,"export NAME=") ? replace : copy ...  no match anywhere -> APPEND
    rename(tmp_nv_env.sh -> nv_env.sh)

⇒ %s twice, into a shell file that is sourced as root. ⭐ Note it writes via a temp file and rename() — an ATOMIC swap, unlike the setnv_env.sh script path, which does two sed -i passes and an append with no atomicity.

☠️☠️ TWO NAMES, ONE CHARACTER-CLASS APART, AND THEY ARE DIFFERENT THINGS

setnv_env.sh    a SHELL SCRIPT in /opt/ipaccess/bin/   <- what rcS and a root shell call
setNvEnvVar()   a FUNCTION INSIDE ipa-mgr_app          <- what the DMI dispatcher calls

⇒ ⭐⭐⭐ The DMI path reaches nv_env.sh WITHOUT invoking the shell script. The manager binary opens, rewrites and rename()s the file itself. ⚠️ THIS MATTERS BECAUSE A CAREFUL CORPUS NOTE WARNS ABOUT THE OTHER ONE: setnv_env.sh contains a string — “will be updated on OMCR (if already exist in diagnosticTunning list) on next AP reboot” — and reasoning from that string alone, whether a DMI write causes the SCRIPT to run is an INFERENCE and is correctly flagged as one. ⇒ ✅ It is not the claim above. The claim above is a disassembly read of the attribute-set dispatcher, and it bypasses the script entirely. ⭐ Two sources, two mechanisms, nearly the same name — and only one of them is an inference. ✅ The cheap confirmation, read-only and non-destructive, is still worth running first:

get diagnosticTuning · get diagnosticTuning[0].name · get diagnosticTuning[0].value

An indexed name/value list containing ENV_* entries confirms the shape at the wire.

🔴🔴 AND A BOOTSTRAP LOOP THAT DECIDES WHETHER YOU CAN USE ANY OF THIS

DMI :8090 requires          ENV_START_DMI_TELNET=TRUE
opnormal.sh:258-259 (151)   if [ ${ENV_START_DMI_TELNET:-"FALSE"} == "TRUE" ]   <- DEFAULTS FALSE
opnormal:307-308    (154)   the identical gate
that flag IS an NV variable ⇒ set by writing nv_env.sh
and the diagnosticTuning route to nv_env.sh ⇒ REQUIRES DMI

⇒ ☠️☠️☠️ YOU CANNOT OPEN THE :8090 LISTENER THROUGH THE ROUTE THAT REQUIRES IT. [corroborated: nothing else in either rootfs sets the flag — the only occurrence in the 154 tree is the gate READING it — and it is absent from all four nv_env.sh files held, 0 occurrences each]

⭐⭐⭐ BUT :8090 IS NOT THE ONLY DOOR TO DMI, AND THE TWO ARE MODEL-SPECIFIC

Two files in this corpus looked contradictory — one calls a DPH-151’s DMI console “open, unauthenticated”, the other has :8090 shut on both 151s. Both are right; they name DIFFERENT TRANSPORTS to the SAME console.

nano3G     DMI over TCP :8090            gated on ENV_START_DMI_TELNET -> the loop above binds
DPH-151    DMI via `ipa-dmi -c`          ⛔ NOT telnet, NOT :8090 — "the nano3G method does
                                            not apply". And the 151's Ralink has NO DNAT for
                                            8090, so it is unreachable from the LAN whatever
                                            the picoChip does.

⇒ ⭐⭐ So on a DPH-151, ipa-dmi -c runs ON the picoChip — which means it is available once you ALREADY HAVE A SHELL THERE, and not before. ⇒ ✅ THE CORRECTED CONCLUSION, same as before but for the right reason: diagnosticTuning is a POST-SHELL CONVENIENCE on a 151, not a way in. ⛔ It is a genuine first-step route only on the nano3G, where :8090 listens and is reachable. 📌 The 151’s way in stays BRINGUP.md ROUTE 3.

⚠️ AND THE GENERAL LESSON, WHICH COST TWO LANES AN HOUR TONIGHT

“Has DMI” is not one predicate. A console can be reachable by one transport and not another on the same unit, and “the DMI console is open” is true or false depending which you meant. ⭐ Name the TRANSPORT, not just the console.

⛔ IT IS A WHOLE-LIST REPLACE — 32 ENTRIES, STRIDE 168 BYTES

memcpy(buf, incomingValue, 0x1504) = 4 + 32×168. ⇒ Writing the list without reading it first silently drops every entry you were not thinking about — ⚠️ including the one holding your own access open. ✅ get diagnosticTuning first, every time.

⚠️ AND THE PAYLOAD IS DORMANT ON ARRIVAL — THIS IS WHERE PEOPLE CONCLUDE IT FAILED

The write APPENDS to nv_env.sh and does nothing else. ⛔ It does not source, restart or signal anything. ⇒ You inject, you watch, and nothing happens — because it is sitting armed. ✅ Trigger it deliberately. A LOGIN is the cheapest trigger — /etc/profile sources nv_env.sh unconditionally, with no root guard and no variant guard. ⭐ And on these units a login IS a root login: there is exactly one uid-0 account and no non-root interactive account, so there is no weaker case to hedge. 📌 Ten consumers source that file — sshd, startgetty, iptablesinit, rcS, opnormal, startmode, diagupload among them — so it fires eventually on its own. ⚠️ “Eventually” is not a step in a procedure, and the gap between injection and effect is exactly where a reader gives up and reaches for something destructive.

📌 The DPH-154 reaches this same sink through CWMP instead, because it has no DMI console: BRINGUP-DPH154.md Phase 2. ⭐ One mechanism, two doors.

A second write path, through the web UI — and it skips the validator the first one has

The attribute route above goes through the DMI console. There is another, through the commissioning web UI, and the pair is worth seeing together because they are guarded differently:

attribute route   set <a URL-shaped attribute>      -> a URL-field validator exists here
file route        dmi_config.cgi, multipart upload  -> the file field BYPASSES that validator
                  form field `dmi_filename`            entirely

⇒ The uploaded file lands as init.dmi, and opnormal executes it as root at boot, through the first branch of the if above (ipa-dmi -c "call …"). ⚠️ No injection is needed on this path at all — the file is a script, and the device runs it because that is the feature.

⭐ The shape worth carrying away: somebody DID think about validation here. There is a validator, on the field that looks like the input. The multipart file field is a second path to the same sink and does not pass through it. ⇒ When you find input validation on an embedded device, ask what else reaches the same place.

⚠️ And this changes what the defence below COSTS you

Installing an init.dmi to close :8090 is still the right move. But the file you install is executed as root at every boot — that is not a side effect, it is the mechanism. ⇒ You are not disabling a feature; you are choosing to use it. Write the file accordingly, and read the marker rule immediately below before you do.

⭐⭐ Both guards on the init.dmi path are a grep for a COMMENT

Look again at dmistart(). The device decides whether to execute init.dmi by counting occurrences of the string // init.dmi - THIS FILE IS AUTO-GENERATED. A file carrying that marker is not run; a file without it is. There is a second marker of the same kind — // !--- This comment is used by WebIf to save the ---! — which decides whether set lines inside the file get commented out before it runs.

⇒ Both markers are provenance claims carried in a comment, and a comment is not provenance. A file that simply omits them is executed in full, with its set lines intact. ⚠️ And they were not written as security controls — they exist to stop the device re-running its own generated configuration. They are load-bearing for security anyway, which is the usual way this happens.

⭐ The transferable lesson: when you meet an integrity check on an embedded device, ask what it would cost an author to omit. If the answer is “delete one line”, it is a marker, not a check.

☠️ And it bites the author, not just the attacker — we nearly shipped this exact failure. A first draft of one of our own init.dmi files carried a comment explaining the autogen banner, and that explanatory comment contained the banner text. The device’s grep -c is unanchored and counts occurrences anywhere in the file, so it returned 1, and the script would have been silently skipped — no error, no log line, just a boot where nothing applied. ⇒ The warning would have caused the exact failure it was warning about. Do not write the marker string into a file you want executed, in any context, including a comment about the marker.

✅ The defence — which is the reason this is written down at all

Close the console. Either condition breaks the chain, and either is one write:

ENV_START_DMI_TELNET = FALSE      # via the vendor's own NV setter, then reboot

— or install any init.dmi at all, which takes the other branch and never opens the port.

⚠️ And the trade nobody discovers until it has cost them a cycle

Installing an init.dmi CLOSES :8090. The two are mutually exclusive by construction — they are the two arms of one if. And init.dmi is a genuinely useful feature: it is how you make settings apply at boot with no management server anywhere. ⇒ Deploy one and you have closed your own way in. Have another route working first, and test it before you upload.

⚠️ And the consequence reaches your tooling, not just your next session. Any bring-up script that drives the console over :8090 cannot run against a unit that has an init.dmi — not because it is broken, but because the port it needs does not exist on that unit. ⇒ A tool written for a unit without init.dmi cannot bring up a unit with one, and it will fail at connect with no indication that the cause is a file you installed deliberately weeks earlier. If you install one, your bring-up path has to move to a transport that does not depend on that port — the one-shot client over SSH, per Route 2.

On the injection itself there is nothing to patch — no vendor, no firmware update — so the defence is architectural:

⚠️ One robustness note if you edit nv_env.sh by any route: it has no backup and is rewritten non-atomically on every boot — two whole-file sed passes and an append. A power cut inside that window leaves a corrupted environment with nothing to restore from. That is a hazard for ordinary configuration work, quite apart from anything above.

⭐ “No backup” is measured, and it is worth knowing why it is not obvious

The vendor’s setter edits with sed -ie, which looks like it requests a backup suffix. On this firmware’s BusyBox it does not — -i there takes no attached suffix, so -ie parses as -i -e and nothing is backed up. [measured on the device: BusyBox v1.9.2 (2011), whose own help prints a bare -i; a direct run left no suffixed file.] ⚠️ On a modern BusyBox the identical flag DOES write a backup — see trap 56, because the version boundary is the whole finding.

✅ So back it up yourself, by convention, before you touch it. On our unit the only copies that exist are ones we made by hand. ⇒ A reader who trusts the flag gets nothing; a reader who copies first gets everything.


⛔ What this repo does not contain, deliberately: a working payload, or a construction sequence you could follow without understanding it. Mechanisms are described — in enough detail to verify on your own unit, because a description too vague to check is not documentation — and the assembled article is not. The test applied above: a reader should finish understanding the class of bug, and be unable to skip straight to a tool. That boundary is in the README and it is not an oversight.


Which of these are verified?

route status
SSH to the picoChip with legacy algorithms measured live, DPH-151
DMI one-shot client answering get/set/action measured live, DPH-151
DMI on TCP 8090 measured live: not listening by default on our DPH-151, and its telnet front end did not answer once started. “Unauthenticated” is inferred on a MicroCell, never measured there.
DMI on TCP 8090, unauthenticated, as a root path measured live, ip.access nano3G — console, file read, and the nv_env.sh injection sink. Not reproduced on any DPH, where the port did not listen. See Route 6.
init.dmi closing :8090 read from opnormal on a nano3G — the two paths are arms of one if. Not tested by installing one.
Ralink has no SSH server read from a firmware image, not confirmed on a live device
Ralink telnet / IPC / wizard backdoor reported (fail0verflow 2012); wizard binary present in 151 and 153 images
serial console pinout and baud reported (fail0verflow 2012)
anything at all on the DPH-154 unverified

🎯 MEASURED LIVE ON .106 (151#2), 2026-09-13 ~17:0x PDT. Root in one connect, no exploit, no injection, nothing mutated.

1. HOST ROUTES (already present on katana; add on any host that needs them)
     ip route add 192.168.157.185/32 via <the unit's LAN IP>     <- Ralink
     ip route add 192.168.157.186/32 via <the unit's LAN IP>     <- picoChip
2. telnet 192.168.157.185     ->  login: root     ->  NO PASSWORD ASKED
     BusyBox v1.8.2 (2012-04-20) ash.  uid 0.

⛔ THE THREE THINGS THAT COST HOURS HERE, ALL INSTRUMENT FAILURES, NOT DEVICE FACTS

katana has NO telnet binary        -> `apt install telnet`. Do this FIRST. I narrated this
                                      blocker three times instead of spending 20 seconds on it.
Ralink busybox has NO nc, NO head, -> `nc -z .186 3001` printed "shut3001" because nc DOES NOT
NO tr, NO id, no `busybox --list`     EXIST. That is a FALSE ZERO, not a closed port.
UDP/14677 tested over TCP          -> the wizard backdoor is UDP. A TCP probe of a UDP service
                                      answers "closed" for every unit, working or not.

⭐ Every one is the same species: a true statement about a narrower universe than the claim it supported. ✅ On this device, run the utility check BEFORE the measurement, and treat “not found” as a broken instrument rather than a result.

/usr/sbin:  rmm_client  cs_client  ipc_client  ipc_server  config_server
            telnetd  udhcpd  chpasswd  setlogcons          <- that is the WHOLE toolbox

rmm_client <ip> <command>, command list:
  reset  factory_reset  clear_tamper  do_software_download  get_software_status
  set_bandwidth  get_bandwidth  switch_fw_boot  set_telnetd  set_port_fwd
  get_uptime  cs_cmd  sleep  crash
     rmm_client 192.168.157.185 set_telnetd 0/1
     rmm_client 192.168.157.185 set_port_fwd proto port action

⛔⛔ factory_reset AND crash ARE IN THAT LIST, ONE WORD FROM cs_cmd. Never type them.

✅ Works: rmm_client 192.168.157.185 cs_cmd "<cmd>" — runs as root ON THE RALINK. Verified with an echo marker. 🔴 Does NOT work on an unprovisioned unit: the same call against 192.168.157.186 returns Can't read response from peer, and so does get_uptime. ⇒ The pico’s rmm/IPC responder is DOWN, not filtered. findings-dph151-pico.md recorded this on 2026-09-05 and it is still true on a different unit eight days later — so it is a property of the UNPROVISIONED STATE, not of that one box.

🔴 AND THE PICO IS SEALED FROM EVERY INBOUND DIRECTION ON AN UNPROVISIONED UNIT

[measured on .106, each with a passing control]

<unit-151-b>:22 / .186:22   CLOSED   <- dropbear is NOT on 0.0.0.0 because
                                      ENV_VERBOSE_CONSOLE_ENABLED != TRUE. THIS IS THE LOCK.
udp/69 TFTP               no answer
tcp 80 / 443 / 8080 / 8090  CLOSED  <- so the dmi_config.cgi FILE-UPLOAD route is NOT available
pico IPC 3001             responder down
CONTROL: .185:23          OPEN      <- the probe works; the zeros above are real

⇒ ⭐⭐⭐ THE RALINK IS ROOT AND THE PICO IS UNTOUCHED. Getting one does NOT get you the other, and the guides that teach pico→Ralink have the dependency BACKWARDS — on a factory-fresh unit the Ralink is reachable first and the pico is behind default-deny.

✅ THE EXACT COMMAND THAT UNLOCKED .244’s PICO — recovered from (private lab notes)

grep -q pounce-rce /var/ipaccess/root_home/.ssh/authorized_keys 2>/dev/null || \
  echo "ssh-rsa AAAA… cwmp_rce_proof pounce-rce" >> /var/ipaccess/root_home/.ssh/authorized_keys
/opt/ipaccess/bin/setnv_env.sh ENV_START_DMI_TELNET TRUE
# DPH151_SSH_PERSIST_BEGIN
/opt/ipaccess/bin/setnv_env.sh ENV_VERBOSE_CONSOLE_ENABLED TRUE
/opt/ipaccess/bin/setnv_env.sh ENV_START_DMI_TELNET TRUE
iptables -I INPUT 1 -p tcp --dport 22 -j ACCEPT

🔴🔴🔴 THE KEY IN THAT COMMAND HAS ITS PRIVATE HALF ON PUBLIC GITHUB. DO NOT REUSE IT.

[nebula-librarian3, 2026-09-13, measured offline. Identity by HASH, not by the comment string.]

selfclean_hook_jp.sh installs TWO keys:
   38cb81eb65f12ad1   "dph151-jp"                   <- JP's own. Fine.
   b22d85d26763594a   "cwmp_rce_proof pounce-rce"   <- 🔴 THIS ONE

ssh-keygen -y -f (private lab notes)   ->  b22d85d26763594a   EXACT MATCH
git -C (private lab notes) remote -v
   origin  https://github.com/nickvsnetworking/DPH153-AT.git          <- PUBLIC REPO

⇒ ⛔ ANY UNIT PROVISIONED WITH THIS ACCEPTS ROOT SSH FROM A KEY ANYONE CAN git clone. ⇒ ✅ MEASURED 2026-09-13 — .244 IS IN THAT STATE, AND IT IS AN ACCEPTED RISK, NOT AN OPEN QUESTION. [team-lead, read over .244's own root shell, TWICE. This block previously said the file had NOT been read; that was true when written and is now false.]

wc -l /var/ipaccess/root_home/.ssh/authorized_keys   -> 4
grep -oE "dph151-jp|pounce-rce"                      -> 1 dph151-jp · 1 pounce-rce  🔴 PRESENT

⇒ 151#1 accepts root SSH from a private key anyone can git clone. CONFIRMED ON THE DEVICE, not inferred. ⇒ ✅ JP WAIVED IT EXPLICITLY — verbatim: “no wrorries about the public key”. ⛔ So do NOT re-raise it as a finding, and do NOT remove the key on your own initiative: it is a decision he has made, and .244 is the unit that currently works. ⭐ What survives regardless of his decision is the NAMING trap above — the standing rule named selfclean_hook.sh, the live artefact was selfclean_hook_jp.sh, and the _jp suffix reads like the SAFE variant. Identity came from the hash.

☠️ WHY THIS WAS NOT OBVIOUS, AND IT IS A NAMING TRAP

The standing rule in 2g/CLAUDE.md names selfclean_hook.sh as the dangerous one. This is selfclean_hook_jp.sh — ONE SUFFIX APART, and _jp reads like the SAFE variant. ⛔ It is the public hook **PLUS JP’s key, not INSTEAD OF it.** ⇒ A rule that names a FILE does not reach a copy of that file under another name. ⭐ And the comment string pounce-rce looks bespoke — locally coined, one-off, ours. It is not. ⇒ Path is position, hash is identity — this corpus’s own law, applied to a key.

✅ THE FIX IS ONE LINE, AND A CLEAN REPLACEMENT ALREADY EXISTS

Delete the pounce-rce line from the hook and install only JP’s key — or the keypair minted 2026-09-13 at ~/Projects/microcell/keys/dph151/dph151_nebula_2026-09-13 (RSA-2048, 0600, private half has never left katana). ⛔ Do not serve the two-key hook to another unit.

✅ RESOLUTION, SAME NIGHT — THE SERVED PAYLOAD IS CLEAN. THIS IS NOT ON THE LIVE PATH.

[measured offline 2026-09-13, by key-body hash across every artifact in the tree]

ACS SDP_URL (config/exchange/dph153-acs/acs_tr069.py:10) = .../rmm-selfclean-v10.sdp
evidence/v10/rmm-selfclean-v10.sdp.c77d57af...   pounce-rce: 0   ✅ CLEAN
---- the artifacts that DO still carry it ----
build/dph151/selfclean_hook_jp.sh   1     build/dph151/rmm-selfclean-v8.sdp   1
evidence/v9/rmm-selfclean-v9.sdp    2     (already DO-NOT-SERVE)
rmm-selfclean.sdp (unversioned) · v7 · v10        0   ✅

⇒ ✅ A BOOT Inform -> Download -> post_swdl_hook run will NOT install the public key. ⛔ WHAT REMAINS TRUE: selfclean_hook_jp.sh and v8.sdp still contain it, and this page named that hook as “the payload”. ⇒ A rebuild, a fallback to v8, or following the older line above RE-INTRODUCES IT. ⭐ And the corpus already warns that three paths carry the unversioned rmm-selfclean.sdp name — here the wrong pick is also the UNSAFE one. ⚠️ BOUND: this was checked against the PRESERVED evidence/v10 copy, identified by the hash in its filename. Whether the LIVE docroot still serves those exact bytes was NOT verified — that needs one curl from exchange against that hash.

⛔⛔ FORWARD HAZARD: THE PACKED SDP IS OLDER THAN THE .sh BESIDE IT — REBUILDING FROM SOURCE RE-ADDS THE PUBLIC KEY.

[morpheus-iuh154, 2026-09-13, verifying the served artefact. Same shape as the three-copies trap, one level down: not two files with one name, but ONE NAME whose SOURCE and ARTEFACT have drifted.]

rmm-selfclean.sdp    (4450 B, the PACKED artefact)  pounce-rce 0 · dph151-jp 1   ✅ SAFE — this ships
selfclean_hook_jp.sh (the SOURCE beside it)         pounce-rce 1 · dph151-jp 3   🔴 carries the public key

⇒ The safe thing to serve was built from an EARLIER hook than the script sitting next to it. ⇒ 🔴 ANYONE WHO “REFRESHES” THE SDP BY REPACKING THAT .sh ADDS A KEY THE CURRENT ARTEFACT DOES NOT HAVE. ⇒ ⭐⭐⭐ REBUILD-FROM-SOURCE IS THE MOVE THAT MAKES IT WORSE HERE — the exact opposite of the usual instinct, and the instinct fires hardest on someone being careful. ✅ Serve the ARTEFACT, verified by hash (890bd9f1…). If you must rebuild, hash the keys in the output and compare against the artefact you are replacing — printf '%s' "$b" | sha256sum, never a bare pipe from grep, or a trailing newline will read as a substitution. 📌 Raised to JP directly on 2026-09-13. This banner records it; it does not decide it.

⚠️ THAT IS THE PERSISTENCE STEP, NOT THE ENTRY. The .dbg shows it delivered over SSH to <unit-151-a>:22, answered by dropbear_0.50 — i.e. the pico’s sshd was ALREADY listening when this ran. ⛔ Do not read it as the way in. ENV_START_DMI_TELNET TRUE is what opens :8090.

That single step is the whole remaining gap on an unprovisioned unit. Candidates, none yet demonstrated: rmm_client … do_software_download / switch_fw_boot (the swdl path, where post_swdl_hook runs as root — ⚠️ it also rm -rfs the config bank, so stage a payload FIRST), or the pico’s own outbound ACS leg per Route 0.


⭐⭐⭐ HOW ROOT IS ACTUALLY OBTAINED — AND WHY THE RCE LADDER IS THE WRONG LADDER

🎯 TR-069 IS how root is obtained. [dph-femtocell 1352a84] No shell, no JTAG, no serial.

ACS Download RPC  ->  rmm-selfclean.sdp  ->  post_swdl_hook runs the hook AS ROOT on the PICO
     (private lab notes)  (the payload, 6892 B)
     (private lab notes)     (4450 B, one 0x5007 hook item, 3 CRCs verified)
  the hook then:
     installs authorized_keys (dph151-jp + pounce-rce) in /var/ipaccess/root_home/.ssh
   🔴 pounce-rce = THE PUBLIC-GITHUB KEY. See the red banner above before serving this.
     setnv_env.sh ENV_VERBOSE_CONSOLE_ENABLED TRUE   -> rcS.d/S10sshd binds 0.0.0.0:22
     opmode.sh re-asserts it EVERY boot, AFTER init_nv_env resets nv_env->FALSE

⛔ AND THE DISTINCTION THAT COST AN ENTIRE EVENING

PROVISIONING a DPH-151   -> TR-069. Needs NO shell, NO RCE, NO key.
PERSISTENT SSH on it     -> the RCE ladder. A DIFFERENT GOAL.

⭐ “We have been climbing the wrong ladder.” [findings-151b-pico-access-and-acs-init.md §3] Anyone whose goal is “configure the cell” should never touch the ladder at all.

🔴 THE BLOCKER WAS OURS, NOT THE DEVICE’S — AND TLS-OK IS THE TRAP

[measured on exchange, 6-hour window, BOTH units]

<unit-151-b>  95 sessions   <unit-151-a>  1280 sessions
every one:  -> <your-cmhs-ip>:443 [CMHS/other] · TLS-OK clientcert=len=1006 · peer closed (state=init)

The cells reach us, authenticate with a valid client cert, and OUR ACS never advances past init. ⇒ ⭐⭐ TLS-OK is the loudest success line in the log and it certifies the TRANSPORT ONLY. The session dies one state later, and a transport receipt reads like a provisioning receipt.

THE MECHANISM: one IP cannot serve both roles. ctx is chosen by getsockname() because these devices send no TLS SNI, and the dispatch has no branch for <your-cmhs-ip> — it falls to else: chan="CMHS/other". A CWMP device that lands there is answered with an XMPP stream header, which it cannot parse, so it closes at state=init.

femtocell.wireless.att.com          -> <your-acs-ip>   ✅ ACS handler
dpewe-santa-clara.wireless.att.com  -> <your-acs-ip>   ✅ ACS handler
cmhswe-santa-clara.wireless.att.com -> <your-cmhs-ip>   ⛔ NO BRANCH -> else -> CMHS

✅ DNS has since been corrected for femtocell, which is why .244 now lands 385 Informs on .21. ⚠️ Two findings that looked contradictory — “0 Informs, ever” and “385 Informs, all ACS” — are the SAME endpoint before and after a DNS change. ⭐ When two careful measurements of one system disagree, check whether the system changed between them before doubting either.

⛔ THE RECORD WAS PARTLY DELETED, AND JP WAS RIGHT TO ASK

0959849 removed tools/access/dph151-unlock-and-patch.sh and dph151-finish-bringup.sh as “unheadered scratch duplicates”. They were not duplicates — the first is the only copy of the working post-shell bring-up (set hnbCId 7938, rrmUnlock/unlock, localSelectCellParams, establishPermanentHnbGwConnection) plus the watchdog patch that teaches it the cell-level action unlock it never had. Restored in 78ee691. ⭐ “Duplicate” is a claim about CONTENT and the commit made it from the FILENAME.

⛔ AND THE FIRMWARE TRAIN IS A SCOPE BOUNDARY — A 579 MEASUREMENT IS NOT A 563 FACT

[two lanes, opposite directions, inside ten minutes, 2026-09-14]

                        export ENV_VERBOSE_CONSOLE_ENABLED=$DEFAULT_UNHARDENED
apcfg-x     (563)       0 occurrences  -- ABSENT   (control: 6 DEFAULT_UNHARDENED hits, reader works)
rootfs154   (579/282F)  rcS:113-114, BOTH vars      -- present, UNCONDITIONAL

unhardened FS_LETTER set:   563 -> A C E G I W X   Z   (EIGHT)
                            579 -> A C E G I W X Y Z   (NINE -- Y added between trains)

⇒ ⭐⭐⭐ THE TWO TRAINS NEED OPPOSITE LEVERS. On 579 the unconditional export makes FS_VARIANT the master and clobbers a direct write. On 563 that block does not exist, so the if [ "$ENV_..." = "" ] guard is the whole story and THE DIRECT WRITE IS THE LEVER:

/opt/ipaccess/bin/setnv_env.sh ENV_VERBOSE_CONSOLE_ENABLED TRUE
/opt/ipaccess/bin/setnv_env.sh ENV_FIREWALL_DISABLED       TRUE

✅ OUR UNITS ARE 563 — 205F_fscheck_563.21.8_C_812_21.md5, iapc.563.21.8. ⚠️ Advice derived from the 579 tree was the EXACT INVERSE of correct for our device.

🔴 AND FS_VARIANT IS NOT A SETTABLE LEVER — THE CORPUS ALREADY TESTED AND DROPPED IT

findings-dph154-v9-preflight.md, verbatim: “FS_VARIANT dev-trick does NOT transfer — /etc/profile reads it from /etc/sw_description.dat on the RO rootfs … it is why v8’s FS_VARIANT="224A" line is deliberately NOT in v9.” ⇒ The export FS_VARIANT="224A" found on .244 is OUR OWN v8 payload’s FAILED override attempt — not a vendor value and not evidence the lever works. ⭐ An artefact of our own earlier failure, read back as a discovery.

⭐ THE VARIANT IS AN ls, NOT A VARIABLE HUNT — confirmed in two trains, two families

.244 (563/151)  /etc/205F_fscheck_563.21.8_C_812_21.md5
154  (579/282)  /etc/282F_fscheck_579.11.127_C_900_143.md5

/etc/<VARIANT>_fscheck_<fw>_*.md5 gives the variant AND the train from one listing. 📌 Better still, read /etc/sw_description.dat — the file the system itself uses.

⛔⛔ .244 IS NOT A CONTROL FOR A PRISTINE UNIT — WE UNHARDENED IT OURSELVES

ENV_VERBOSE_CONSOLE_ENABLED="TRUE" · ENV_FIREWALL_DISABLED="TRUE" · an opmode.sh ROBUST_SSH stanza that re-asserts them and starts dropbear on :22. ⇒ Every “it works on .244” observation is taken on a device we modified. ⭐ The unit you succeeded on stops being a control the moment you succeed on it.

✅ AND THE READ THAT ENDS THE ARGUMENT — NO SHELL, NO DMI, NO WRITE

/var/ipaccess/nv_env.sh is on ENV_DIAG_FILE_LIST. ⇒ A diagupload bundle hands you the LIVE file. ⭐ On 563 the entire gate is if [ "$ENV_..." = "" ], so that file IS the decision: PRESENT-AND-FALSE and ABSENT are different answers and they select different levers. ⇒ Read the target device before choosing a lever from a different one.

[.106, 2026-09-14 ~00:2xZ. Root on the Ralink throughout; every probe below run FROM it.]

:22 / :80     pico DROPS at its OWN firewall     conntrack: SYN_SENT … [UNREPLIED]
              ⭐ NOT an RST. The "Connection refused" katana sees is synthesised elsewhere --
                 so it is NOT evidence that sshd is down. It is evidence of a DROP.
:8090         NO DNAT on the Ralink   ⚠️ TRUE, AND NOT THE BINDING CONSTRAINT — see below
:3001         accepts TCP, application NEVER replies (get_uptime AND get_software_status)
              controls: .185 -> "Uptime is: 4371 seconds"   .190 -> "failed ipc connect"
              ⇒ three distinct outcomes, so the instrument discriminates. MUTE, not down.
udp/69 tftp   pico's tftpd NOT running -- it exists only during an upgrade flow
rmm_client 192.168.157.185 set_telnetd 1   -> "The response: 80"   NO PORT OPENED, either chip

🔴 CORRECTED 2026-09-14 — :8090 IS SHUT BECAUSE **NOTHING IS LISTENING, NOT BECAUSE OF THE DNAT.**

[nebula-librarian3. I corrected this exact framing in BRINGUP.md two hours earlier and left THIS mention standing — this corpus's own law, fired on the person enforcing it.]

etc/init.d/opnormal:258-259   (563 tree — OUR train)
  if [ ${ENV_START_DMI_TELNET:-"FALSE"} == "TRUE" ]; then     ⬅ DEFAULTS TO FALSE WHEN UNSET
ENV_START_DMI_TELNET in every nv_env.sh held here: 0 · 0 · 0
MEASURED: :8090 probed WITH A PORT-FORWARD IN PLACE -> still closed   [team-lead 2026-09-14]
          :8090 also not listening on .244                            [lucid-console154]

⇒ ⛔ ipa-dmi -u 8090 NEVER STARTS, so there is nothing on the far side of any DNAT. ⇒ ☠️ “No DNAT” sends you to the FIREWALL when the fault is in the BOOT SCRIPT. ⭐ A true statement one level above the real constraint is worse than a false one: it survives checking. ⇒ ⚠️ And “unreachable whatever the pico does” is the wrong shape — it is unreachable BECAUSE OF what the pico does, or rather does not: start the listener. ✅ It is 151-WIDE, not a .106 fault — shut on both units. 📌 Full table: BRINGUP.md §3.

⭐ AND THE PICO IS DEMONSTRABLY ALIVE, WHICH KILLS THE “UN-STARTED UNIT” READING

udp src=192.168.157.186 dst=192.168.157.185 dport=53    <- IT IS ACTIVELY RESOLVING NAMES
ICMP replies · TCP accepted on 3001 · ARP present

⇒ It is running and trying to reach things. It refuses every channel we can reach, which is a DIFFERENT FAULT from a stalled boot and sends you somewhere else entirely.

first: that step does not exist, and four of us spent an evening looking for it.**

/etc/dhcpd0.cfg: start 192.168.157.186 · end 192.168.157.186 (a ONE-ADDRESS pool), opt router 192.168.157.185 · opt dns 192.168.157.185, /30. ⇒ ⭐⭐ The Ralink IS the pico’s gateway AND its resolver, on a link with exactly one client. A redirect applied HERE reaches only this unit — no shared DNS change, no risk to any other cell. 📌 And the pico’s traffic leaves MASQUERADED as the unit’s LAN address ⇒ a DNS measurement taken upstream is attributed to the LAN IP, not to the picoChip that made it. ⭐ A NAT’d measurement names the translated host, and nothing in the data says otherwise — one lane’s correct DNS capture sat misattributed for days because of exactly this.

⭐⭐⭐ A DPH-151 INFORMS AT BOOT. “IT NEVER INFORMS” AND “IT HAS HAD NOTHING TO SAY SINCE IT BOOTED” PRODUCE IDENTICAL LOGS.

[2026-09-13, ~6 hours lost to reading the first one. 151#2 = <unit-151-b>.]

209 sessions · TLS-OK, valid client cert · ZERO application bytes · ~12 ms close · 0 Informs

Read as a fault all afternoon. It is ORDINARY IDLE BEHAVIOUR. A CWMP client re-reads DNS and re-dials AT BOOT (1 BOOT Inform); between boots it polls and has nothing to say. ⇒ ✅ TO GET A SESSION FROM A QUIET DPH, POWER-CYCLE IT. There is no cleverer RPC: every management write is an RPC, every RPC rides a session, and the CPE opens the session.

⛔ AND THE PREMISE THAT RAN THE WHOLE INVESTIGATION WAS FALSE — CHECKED ON THE WORKING UNIT

“151#2 has no management-server pointer” drove a redirector theory, a cert theory, and a claim that the two units genuinely differ. Read from 151#1’s own root shell:

tr069_cur_cfg.xml.gz   AcsUrl = https://femtocell.wireless.att.com:443/acs
                       EnableCWMP = true · PeriodicInformEnabled = true · Interval = 900
md5(cur_cfg) == md5(def_cfg)      <- 151#1 IS RUNNING PURE FACTORY DEFAULTS

⇒ ⭐⭐ THE FACTORY DEFAULT ALREADY CONTAINS THE ACS URL. 151#1 carries no more pointer than 151#2 does. Same model, same train (563.21.8), same config, same destination (.21:443), same client-cert CA (Femtocell CPE Sub CA), same certs served. ⇒ ⭐⭐⭐ THE CHECK THAT SETTLED A BROKEN UNIT WAS ON THE WORKING ONE — and nobody looked there for hours, because the working unit was not the problem.

📋 EXCLUDED BY MEASUREMENT, SO NOBODY RE-RUNS THEM

server cert / chain / CA      both bundles carry IDENTICAL anchors, certs 2-6. Both leaves
                              self-signed, so chain membership cannot separate them.
client-cert vintage           all three units issue from CN=Femtocell CPE Sub CA
our dispatch / wrong handler  .106 lands on ACS/femtocell correctly, 209/209
our ACS dropping it           read_http() returns None ONLY on recv()==empty. Malformed input
                              would BLOCK, not return. So "peer closed" is a REAL close.
server-speaks-first deadlock  greeted it with an unprompted HTTP 204 (scoped to .106): it closed
                              9 ms later, still silent. IT IS NOT WAITING FOR US.
diagupload as a "read"        it is TWO SetParameterValues RPCs -- inside the session we lack.
                              ⚠️ And its landing endpoint :8082 was DOWN: a trigger fired at a dead
                              endpoint yields the SAME ABSENCE as a device that never answered.

🔴 AND A DEFECT IN OUR OWN ACS THAT DISARMED THE EXPLOIT FOR DAYS

.selfclean_sent was written at queue-build AND at the send site. The build-time write always won, so the arm burned before a byte went out. The log printed FIRED and session continues while the measured send count was ZERO. A 2026-09-05 fix meant to MOVE that write had COPIED it. ⇒ ⭐ A log that says nothing invites investigation. A log that says the wrong thing ends it. ✅ Fixed 2026-09-13: one write site remains, at the Download branch. The marker now records a SEND. ⚠️ ORDER WAS LOAD-BEARING: a power cycle BEFORE this fix would have produced a BOOT Inform, burned the arm, sent nothing, and read as “the method does not work on this unit” — burying the real defect deeper.

✅✅ THE CHAIN IS DEMONSTRATED ON A DPH-151 — CENSUS, NOT A SAMPLE

[exchange dph153-acs journal, all boots, 2026-09-13]

CENSUS of all 385 CWMP Informs, by OUI:
  100   2026-09-05   OUI 445829   = 151#1   <- A DPH-151. IT INFORMED 100 TIMES.
  285   2026-09-11   OUI 481D70   = the DPH-154

EventCodes actually observed:
   38   ['X 00000C SW_UPGRADE','M Download','7 TRANSFER COMPLETE','1 BOOT']  <- THE EXPLOIT WORKING
   18   ['X 00000C NON_CLEAN_SHUTDOWN_COLD','1 BOOT']                        <- A POWER CYCLE
    8   ['1 BOOT']            2  ['X 00000C SW_FAULT_APP_FAILURE','1 BOOT']
  306   ['2 PERIODIC']

⇒ ✅ 1 BOOT Informs are real (71 of them) and NON_CLEAN_SHUTDOWN_COLD + 1 BOOT is the power-cycle signature. ⇒ ✅ AND THE DEVICE KEEPS ITS OWN RECEIPT: 151#1’s /var/ipaccess/selfclean_hook.log is dated Sep 5 23:41, === selfclean v6 uptime=13551s ===, with nv_env.sh.pre-dph151 and .pre-dph151-jp beside it — the same day as its 100 Informs. ⇒ ⭐⭐⭐ TR-069 -> Download -> post_swdl_hook AS ROOT IS PROVEN ON THIS MODEL, END TO END.

☠️ AND HOW THIS WAS NEARLY LOST: A TAIL SAMPLE READ AS A CENSUS

I read the LAST 3 Informs, saw the 154’s OUI, and reported “neither DPH-151 has ever Informed — the path has never worked on this model.” The 100 I needed were at the OTHER END of a chronologically-sorted file. ⇒ ⭐⭐ Sorting by time makes a tail sample unrepresentative of WHO, not merely of HOW MANY. A random sample would have found the 151 immediately; the time-ordered one could not, by construction. ⛔ The conclusion drawn was not “we have less data than we thought” but “the method does not work” — a sampling artefact promoted to a property of the system. 📌 This is the corpus’s own print-the-total rule, broken by the person quoting it to three other lanes in the same hour. ✅ uniq -c over the WHOLE field is one command and cannot lie the same way.

📋 THE PROCEDURE, THEN — FOR A QUIET DPH-151 WITH NO SHELL

1. ARM      .selfclean_arm.<peer> present, .selfclean_sent.<peer> ABSENT
            ⚠️ verify the marker is written ONLY at the send site -- a build-time write burns the
               arm before anything is sent, and the log prints FIRED while sending zero bytes
2. VERIFY   the payload BY HASH, never by path.  v10 = c77d57af24d2a32a3f786ad646cf3498
            ⛔ v9 strips ~21 of 24 ENV_ vars -- DO NOT SERVE.  ⛔ v8 + selfclean_hook_jp.sh carry
               a key whose PRIVATE half is on public GitHub.  v10 is clean.
3. SERVE    the download host must be UP (:8081) before the cycle, not after
4. POWER-CYCLE THE UNIT   -> NON_CLEAN_SHUTDOWN_COLD + 1 BOOT Inform -> Download fires
5. TWO REBOOTS            -> $( ) runs in a subshell, so sshd starts with the OLD value on the
                             first. ⛔ CHECKING SSH AFTER ONE REBOOT RECORDS A WORKING PAYLOAD
                             AS A FAILURE.

☠️☠️☠️ VERIFY-BY-HASH AUTHENTICATES BYTES, NOT THEIR NATURE — AND IT NEARLY BRICKED THE SPARE

[caught 2026-09-13 ~17:5x PDT, with a power cycle already green-lit. Nothing fired.]

                        size     header                       verdict
rmm-selfclean.sdp       4450 B   " SDP\0..."   1 IMAG item    ✅ the build that ROOTED 151#1
rmm-selfclean-v7.sdp    6252 B   " SDP\0..."                  ✅ valid container
rmm-selfclean-v8.sdp    7208 B   " SDP\0..."                  ✅ valid container
rmm-selfclean-v10.sdp  10240 B   "../../../var/\0\0\0"        🔴 NOT AN SDP. A ustar TAR.
                                 entries: ../../../var/ipaccess/opmode.sh (6424 B)
                                 its own comment: "Landed by tar path traversal"
acs_tr069.py:11  SDP_URL = ".../rmm-selfclean-v10.sdp"        <- WHAT THE ACS SHIPS

⇒ The armed payload was the TAR TRAVERSAL, not the proven SDP hook. It overwrites /var/ipaccess/opmode.sh — sourced at boot, on a unit with no shell. Extraction happens during transfer, so a partial write commits every completed entry, and no backup exists because the traversal overwrites before any payload of ours runs. ⇒ Silent, fail-closed, unrecoverable lockout.

⛔ THE RULE I FOLLOWED IS THE ONE THAT FAILED ME

This corpus says select a payload BY HASH, never by path. I did. The hash MATCHED (c77d57af24d2a32a3f786ad646cf3498) — because it was the correct hash OF THE WRONG KIND OF THING. It certified the file was un-tampered and free of the public-GitHub key — both true, and neither is “this is an SDP”. ⇒ ⭐⭐⭐ INTEGRITY AND IDENTITY ARE DIFFERENT PROPERTIES. A HASH ONLY EVER SPEAKS TO THE FIRST. ✅ THE CHECK THAT WORKS IS FOUR BYTES: head -c 4 <file> must read " SDP". A container check cannot be satisfied by the wrong container.

⚠️ AND TWO COMPOUNDING FAILURES WORTH THE SAME ATTENTION

  1. acs_tr069.py:6 reads # v7: adds FS_VARIANT="224A" — DIRECTLY ABOVE THE v10 PATH. The file changed underneath its own annotation. ⭐ A comment names the version it was written for, never the version in the line beneath it.
  2. A DECISION AND A CONFIGURATION DISAGREED SILENTLY. A lane analysed both delivery paths and ruled “take the .sdp, the traversal buys a weaker primitive at much higher risk” — while SDP_URL was already wired to the traversal. ⇒ The analysis was correct and never reached the thing it was about, because nobody re-read the config line the decision was supposed to govern. ✅ After deciding between two paths, READ THE LINE THAT SELECTS ONE.

⚠️ THE PACKED .sdp AND THE .sh BESIDE IT ARE DIFFERENT OBJECTS — AND REBUILD FROM SOURCE MAKES IT WORSE

[measured 2026-09-13, two lanes independently]

                        rmm-selfclean.sdp (4450)   selfclean_hook_jp.sh
pounce-rce                      0                          1
the public-repo key material    0                          1
dph151-jp                       1                          3
the packed script installs:  dph151-jp ONLY

⇒ The SDP was packed from an EARLIER hook than the .sh sitting next to it. The script on disk has drifted AHEAD of the artefact, so selfclean_hook_jp.sh looks like the source of rmm-selfclean.sdp while being a later, less safe version. ⇒ ☠️ ANYONE WHO “REFRESHES” THE SDP BY REPACKING THAT .sh ADDS A KEY WHOSE PRIVATE HALF IS ON PUBLIC GITHUB — a key the current artefact does not contain. ⭐ The safe artefact and the unsafe source share a name-stem, and rebuild from source is the move that makes it worse — the opposite of the usual instinct. ✅ Check the ARTEFACT’s contents, never the file named like it. ⛔ Two lanes independently concluded “the proven SDP carries pounce-rce” by reading the .sh; both were wrong.

✅ AND THE KEY IDENTITY, SETTLED — no rebuild is needed for key reasons

~/.ssh/dph151_id_rsa  derives  AAAAB3NzaC1yc2EAAAADAQABAAABAQDj/k0rNLqx9iNH6qqOQgUXstKErZ172nCI
rmm-selfclean.sdp     installs AAAAB3NzaC1yc2EAAAADAQABAAABAQDj/k0rNLqx9iNH6qqOQgUXstKErZ172nCI
                                                                              ^ EXACT MATCH

⇒ The key installed and the key you authenticate with are the same one — which closes the v7 rc=255 failure mode by construction, not by luck. That failure was never a lost race; it was a key mismatch, and it cost six retries because a mismatch and a missed window look identical. ⚠️ AND USE ssh-keygen -lf AS A KEY’S IDENTITY, NEVER A HAND-ROLLED HASH OF A GREP’d BLOB — a lane quoted a fingerprint computed through a pipe (trailing newline) and got a different number for the same key. ⭐ A hash of a string is a hash of how you piped it. And a fingerprint mismatch on a key reads as “wrong key, do not trust” — the exact misread that cost those retries.