I mean, where the fuck should I really even start?

I got hoes that I’m keepin’ in the dark

Okay, but really though, where do I start? Right now we know that the Meraki MR18 is cloud-locked by Cisco. Without an active Meraki dashboard license there are many niceties that I don’t have access to such as:

  • There is no local web UI
  • There is no TFTP recovery mode accessible without the Meraki bootloader password (TFTP allows you to flash a new version of firmware)
  • There is no serial console shell—the Meraki NAND loader drops us straight into a locked-down Linux environment
  • The bootloader disables JTAG probing within ~2 seconds of powering the device on

Now though, how do we know all of those things? Well, I struggled a lot trying all of these, I found that when we booted the device the AP was trying to connect home to Cisco’s cloud, but since we don’t have an account nothing happens. We know about the bootloader disabling the JTAG through trial and error (also the OpenWrt Wiki mentioned something similar). I don’t recall if the OpenWrt Wiki specifically said ~2 seconds, but that is what I found while debugging and testing.

All this to say that JTAG is the only way in. This project exploits a narrow timing window during the NAND loader’s early boot to halt the CPU via EJTAG—EJTAG is a MIPS extension of JTAG read more here—to load an OpenWrt initramfs kernel directly into RAM, boot it, trigger failsafe mode, and flash a sysupgrade image to the NAND. This entire long process is automated with Python.

From that long technical paragraph above, I said some things without giving context. This AP has a SoC—a System on Chip, basically everything crammed onto one die: the CPU, WiFi radio, ethernet switch, USB, the works—and that SoC is an Atheros AR9344 running a MIPS M4K core at 560MHz. Since this is MIPS we can use the EJTAG extension. EJTAG is essentially JTAG but with superpowers specific to MIPS—it gives you a debug interface that lets you halt the CPU dead in its tracks, read and write registers, and most importantly, read and write physical memory directly through the debug unit. That last part is what makes it so powerful here: we can shove an entire kernel image straight into RAM over JTAG without the bootloader’s permission or involvement. What we want to do is catch that ~2 second window right after power-on—before Linux boots and the Meraki bootloader disables JTAG probing—halt the CPU, DMA the OpenWrt initramfs kernel into RAM, and then resume execution at the kernel entry point. At that point the MR18 boots our kernel, not Meraki’s, and we own the box.

How do we rip this apart and start the JTAG process

Before you MAYBE read the following section do understand, I don’t expect you to read this. Its long and boring and only is relevant if you care how I wired things up—but hey! There are images.

On the MR18 after you disassemble it, there are 2 headers: 1 is for UART and the other is for JTAG. So, first I had to solder a 01x05 to the back 2nd row of the header labeled J3. The J3 pinout was determined empirically from the working wire connections (confirmed by signal matching with the ESP-Prog). Pins 1-9 (odd) follow a standard MIPS EJTAG layout. Pins 11-14 are standard EJTAG assignments but were not used or verified.

              MR18 J3 JTAG Header
         (pin 1 marked on PCB silkscreen)

        +-------+------+------+------+------+-------+------+
  Pin:  |  1    |  3   |  5   |  7   |  9   | 11    | 13   |
Signal: |nTRST  | TDI  | TDO  | TMS  | TCK  |nSRST? | VREF?|
        +-------+------+------+------+------+-------+------+
  Pin:  |  2    |  4   |  6   |  8   | 10   | 12    | 14   |
Signal: | GND   | GND  | GND  | GND  | GND  | GND   | GND  |
        +-------+------+------+------+------+-------+------+

Four pads in a vertical row (top to bottom when the board is oriented with the Ethernet port at the bottom):

  MR18 J1 UART
  +-----+
  | GND |  (top)
  +-----+
  | TXD |  MR18 transmit -> ESP-Prog RXD0
  +-----+
  | RXD |  MR18 receive <- ESP-Prog TXD0
  +-----+
  | NC  |  (bottom, not connected -- possibly VCC)
  +-----+

I used this shitty ahh ESP-Prog since my friend @CesMag had my actual good JTAG. What was pretty cool though about this ESP-Prog was the fact it had both JTAG and UART on it where its connection pinouts are:

             ESP-Prog JTAG
        +-----+-----+-----+-----+-----+
  Pin:  |  1  |  3  |  5  |  7  |  9  |
Signal: | NC  | TDI | TDO | TCK | TMS |
        +-----+-----+-----+-----+-----+
  Pin:  |  2  |  4  |  6  |  8  | 10  |
Signal: | GND | GND | GND | GND |VJTAG|
        +-----+-----+-----+-----+-----+
  ESP-Prog UART
  +-----+-------+------+
  |  1  |   2   |  3   |
  | IO0 | RXD0  | GND  |
  +-----+-------+------+
  |  4  |   5   |  6   |
  | TXD0| VProg |  EN  |
  +-----+-------+------+

Note: ESP-Prog and MR18 pin numbers do NOT match for TMS and TCK—connect by signal name, not by pin number.

Both operate at 3.3V logic. No level shifter needed. Do NOT connect ESP-Prog VJTAG directly to any MR18 pin—it only feeds the 4.7k pull-up resistor.

4.7k pull-up purpose: nTRST is active-low. Pulling it HIGH through 4.7k keeps the JTAG TAP controller out of reset and available for probing. Without this pull-up, nTRST may float LOW, holding the TAP in reset and preventing OpenOCD from scanning the chain.

The EN pin is wired directly to the reset button pad—no series resistor. The ESP-Prog UART connector exposes an EN (enable) pin. This pin is driven by the FT2232H’s RTS line through an NPN transistor—the same auto-reset circuit used by esptool.py for ESP32 boards.

ser.rts = True   --> NPN base driven HIGH --> transistor conducts
                 --> EN pin (collector) pulled to GND
                 --> GPIO17 net pulled LOW (= reset button pressed)

ser.rts = False  --> NPN base driven LOW --> transistor off
                 --> EN pin released
                 --> reset supervisor pull-up returns GPIO17 HIGH (= button released)

Okay last technical thing:

Serial Parameters:

  • Baud rate: 115200
  • Data bits: 8
  • Parity: None
  • Stop bits: 1
  • Logic level: 3.3V TTL

Bench Power Supply

A SCPI-capable bench PSU allows the mr18_flash.py script to automate power cycling during the JTAG timing attack. The script uses scpi-repl to send SCPI commands via a named pipe. What’s really sick about these drivers are the fact I cooked them up with Claude for my ESET 453 course and got paid to work on them. For this project, I did ask Claude questions, but I didn’t rely on it to get this working—unlike those drivers which are essentially 100% slop AI coded.

The power supply I used was a Matrix MPS6010H and with our REPL code which allowed us to be simple and say psu set 12 1.5 for 12V 1.5A and then psu chan on or psu chan off

Enough of that

That was a bunch of technical yap that I don’t expect anyone to read, for that reason, I will go more into the software side of things in the next blog post.

Bench Layout

graph LR subgraph Host PC USB1["USB Port 1
(ESP-Prog)"] USB2["USB Port 2
(Bench PSU)"] ETH["Ethernet NIC
192.168.1.2/24"] end subgraph ESP-Prog ["ESP-Prog (FT2232H)"] JTAG_CONN["JTAG Connector
(Interface A, 10-pin)
ttyUSB0/ttyUSB1"] UART_CONN["UART Connector
(Interface B, 6-pin)
ttyUSB4"] end subgraph Breadboard PULLUP["4.7k nTRST
Pull-up"] end subgraph MR18 ["Cisco Meraki MR18"] J3["J3 JTAG Header"] J1["J1 UART Pads"] RST["Reset Button Pad
(GPIO17 net)"] GBE["GbE Ethernet Port"] BARREL["12V Barrel Jack"] end subgraph PSU ["Bench PSU"] SCPI["SCPI USB"] DC["12V DC Output"] end USB1 --- ESP-Prog USB2 --- SCPI ETH -- "CAT5 Direct" --- GBE JTAG_CONN -- "TDI/TDO/TMS/TCK/GND" --- J3 JTAG_CONN -.- PULLUP PULLUP -.- J3 UART_CONN -- "TXD0->MR18_RX
RXD0<-MR18_TX" --- J1 UART_CONN -- "EN -> GPIO17 net" --- RST DC -- "12V center-positive" --- BARREL

SCPI connection

Connect the PSU’s USB control port to the host PC. The scpi-repl utility discovers the instrument automatically. Commands are injected through a named pipe at /tmp/scpi_pipe.

Device Enumeration

After plugging the ESP-Prog into the host USB port, verify the devices appear:

ls /dev/ttyUSB*

Expected output (device numbers may vary if other USB serial devices are connected):

Device FT2232H Channel Purpose
/dev/ttyUSB0 Interface A JTAG (used by OpenOCD)
/dev/ttyUSB1 Interface A JTAG (secondary, unused)
/dev/ttyUSB4 Interface B UART console + EN pin

The gap between ttyUSB1 and ttyUSB4 occurs when other USB serial devices (such as the bench PSU) occupy ttyUSB2 and ttyUSB3. If your numbering differs, update the ESPPROG_UART constant in mr18_flash.py and the UART constant in send_binary.py and uart_transfer.py.

If no ttyUSB devices appear, check:

  • ESP-Prog USB cable (must be a data cable, not charge-only)
  • lsusb should show 0403:6010 Future Technology Devices International, Ltd FT2232C/D/H
  • The ftdi_sio kernel module must be loaded (sudo modprobe ftdi_sio)

Host Ethernet

Connect a CAT5 Ethernet cable directly from the host PC’s NIC to the MR18 GbE Ethernet port. No switch or router is needed.

Configure the host NIC with a static IP on the same subnet as the MR18’s failsafe address:

sudo ip addr flush dev <your-nic>
sudo ip addr add 192.168.1.2/24 dev <your-nic>
sudo ip link set <your-nic> up

Replace <your-nic> with your Ethernet adapter’s interface name (find it with ip link). The mr18_flash.py script does this automatically using the HOST_NIC and HOST_IP constants.

The MR18 in OpenWrt failsafe mode comes up at 192.168.1.1 with a static IP. The host must be at 192.168.1.2/24 (or any other address on the 192.168.1.0/24 subnet) to communicate.