IoT: Hacking on a Budget (part 1) - CenturyLink PK5001A Router
Table of Contents
IoT Series: Hacking on a Budget - Part 1
Cheap Fun with a CenturyLink PK5001A Router

Intro
Welcome to a new series I will be starting as I practice and learn the craft of IoT hacking. It’s been a long time coming for me. I always entertained the idea of getting into hardware hacking as I love the tactile feel of it all. Ever since I was a kid I used to love taking apart old electronics. I remember I had a drawer full of “garbage” electronics that I would salvage from breaking down old printers and mobos. Had a large collection of “barrels” (as I used to call them), nowadays commonly known as capacitors, which I sheered off with pliers. It was a great activity for an impatient, ADHD young me.
Anyway, I’ve taken one course on hardware hacking, the SANS SEC556 (which was OK), but most of my newbfoo came from watching YouTube channels, such as the legendary Matt Brown. This was enough to get me familiar with the basics. It was certainly enough to get me started and confident enough to take apart a piece of hardware and find something fun to do with it. As such, my journey begins.
I semi-recently found myself in a local thrift shop looking for old hardware to practice on. The Goodwills around me tend to be pretty picked through; however, there will occasionally be an older router for $20-30. To me, that’s a price of a brand new piece of hardware that I can pick up from Amazon, I needed something cheaper. The local thirft shop turned out to be a gold mine. They had 4 routers, with power cables, which I walked out with for a whopping…. $8. YEP. And so, here’s my journey with the first router, which was quite a learning experience. While things seemed to fall into place at the beginning, I ended up hitting a wall more than once. Ultimatley, the try harder mentality got me what I was looking for: root shellz. Let’s dive in!
OSINT
Before I even cracked the case open… (who am I kidding, the case was already open and I already lost two screws, but for the sake of the story, stick with me) I started with conducting OSINT like a good pentester should. The sticker on the bottom of the router revealed its FCCID, I88PK5001Z
, which I searched up on fccid.io. The FCC filing for the device came with detailed images of the board, which let me peep at the device without having to open it up:

The pictures were a littler grainy, but upon a closer look, I noticed the potential UART headers in the right upper corner of the board. The FCC filing did not have any other useful information, so I jumped straight into cracking this thing open.
Cracking the Case
Having unscrewed a handful of screws, the case popped open with ease thanks to my pry tools. After removing the top cover, sure enough, there was the UART as pictured in the FCC filing:

The only thing left was to probe the pins with my multimeter to determine: the ground pin, transmit pin (TX), and receive pin (RX). If you are unfamiliar with the UART protocol, I recommend you ask your favorite AI or read through this article: UART basics. In essence, you can find the ground pin by probing around pads and pins on the board while having your multimeter set to continuity testing. If you have a multimeter that beeps when continuity is detected, that’ll tell you when you found your ground pin. Most of the time, you can find ground contact on certain shielding for chips, or clearly designated “GND” contacts on the board.
Through an educated guess, I found the ground pin to be the pin that was separated from the other 3 pins (the top pin as pictured). To find the transmit pin, I read the voltage of the remaining three pins and looked for one that flactuated between 3.3V and a lower voltage, jumping back and forth. This effectively told me that there’s data being sent on the pin, thus making it the TX pin. The TX pin ended up being the second pin from the bottom as pictured above. For the RX pin, I made an educated guess and figured it would be the third pin from the bottom, as the VCC 3v pin usually resides on the edges of t he pinout, and I was correct.
UART Access
Having identified my UART pins, I plugged them into my Flipper’s GPIO pins (reversing TX and RX with RX and TX on the Flipper), and I used the flipper as my serial bridge. I assumed a baud rate of 112500 and sure enough, I saw data. At this point, I powercycled the router to catch the whole boot sequence of the router. I won’t include the full dump here, but I’ll refrence to it when pertinent to explain what I’m talking about.
I noticed that as soon as the router booted up, there was an option to stop the boot process of u-boot by pressing d
. Doing so landed me in a u-boot limited shell where I could only execute the following commands:
? - alias for 'help'
askenv - get environment variables from stdin
bootm - boot application image from memory
cp - memory copy
echo - echo args to console
go - start application at address 'addr'
help - print online help
imls - list all images found in flash
loadb - load binary file over serial line (kermit mode)
loady - load binary file over serial line (ymodem mode)
md - memory display
mm - memory modify (auto-incrementing)
mtest - simple RAM test
mw - memory write (fill)
nand - NAND sub-system
printenv- print environment variables
reset - Perform RESET of the CPU
run - run commands in an environment variable
setenv - set environment variables
switchup - initialize switch
tftpboot- boot image via network using TFTP protocol
upgrade - forward/backward copy memory to pre-defined flash location
The goal at this point was to gain access to the firmware by dumping it. I haven’t done many firmware dumps through UART at this point, but based on my assessment of these commands, it appeard that the key command to dump data was md
. The printenv
command also came in handy as it gave me information about the commands being executed at boot. I tried using the md
command several times to see if I can extract any data, but every time I did so, the device would simply reboot.
At first I thought there may have been something corrupted on the device, but after playing around with it more, I found that the device reboot seemed to coincide with exceptions caused by the commands I was running. For instance, executing the md
command on a small section of RAM, the data would print with no problem. But if I tried reading larger chunks of data, or trying to read from unavailable RAM addresses, the device would reboot.
Memory Dumping
Having identified the issue for the device reboots, I proceeded to dump data. The boot log presented the memory addresses of each parition:
Creating 8 MTD partitions on "ifx_nand":
0x000e0000-0x01ee0000 : "rootfs,kernel1"
0x01ee0000-0x03ce0000 : "rootfs,kernel2"
0x03ce0000-0x04760000 : "reserve"
0x04760000-0x07420000 : "firmware"
0x07420000-0x07ec0000 : "config"
0x07ec0000-0x07f00000 : "mrd_romd"
0x07f00000-0x07f40000 : "mrd_cert1"
0x07f40000-0x07f80000 : "mrd_cert2"
Given that I only had access to nand
and md
commands for dumping firmware, I had to calculate the size of data I wish to load into RAM and then dump it with md
. It is worth noting that when investigating this shell, I found a boot command within the environment variables which loaded u-boot(?) into memory:
AMAZON_S # printenv
bootcmd=nand read 0x81a00000 0x0001B800 10000; go 0x81a00000
bootdelay=0
baudrate=115200
preboot=echo;echo set voltage;echo;mw.b 0xbf106a0b 7f
<...snip...>
The nand
command usage (based on my research) worked as follows:
read
- tells the command to read data from NAND into RAM0x81a00000
- starting RAM address at which the data will be written to0x0001B800
- starting NAND address from which the data will be read from10000
- amount of bytes to read from NAND
So technically, the data will exist within RAM from 0x81a00000
to 0x81a02710
.
Within the boot sequence, I also noticed several outputs that suggested a lower RAM address of 0x80800000
may be used:
## Booting image at 80800000 ...
Due to this, I assumed I would be good to start loading data starting from the RAM address 0x80000000
. I tested this to be the case by modifying the command from bootcmd
to have it write starting from 0x80000000
and it worked fine.
Having put together the first piece of my RAM write command (nand read 0x80000000
) I now needed to calculate the size of the first rootfs,kernel1
partition so I can load it up. Python to the rescue in this case:
# python3
>>> hex(0x01ee0000-0x000e0000)
'0x1e00000'
The size of the partition was 0x1e00000
, or 31,457,280, bytes (~31 MB). Now I had the full command: nand read 0x80000000 0x000e0000 0x1e00000
. However, after executing this command, the device rebooted once again. So I tried to halve the data I was reading into RAM to see if that made a difference and it did! Here is the final command I ended up using to read the first half of rootfs,kernel1
partition into RAM: nand read 0x80000000 0x000e0000 0xf00000
with 0xf00000
being half of 0x1e00000
:
AMAZON_S # nand read 0x80000000 0x000e0000 0xf00000
NAND read: device 0 offset 917504, size 15728640 ... 15728640 bytes read: OK
As you can see, the device spit out read: OK
confirming successful load. Now, all I had to do is dump out the data, and by dump out I mean print it to screen… one byte at a time… And with 15,728,640 bytes to print (or 15 MB) it naturally took a while, about 2 hours. To do so, I ran the md
command in the following manner: md.b 0x80000000 0xf00000
. To catch all this output, I stood up my UART connection through picocom
and the --log rootfs1-1.dump
argument to save all the console output to the file.
I then repeated the process for the other half of the partition with nand read 0x80000000 0x00fe0000 0xfe0000
and the same md
command. The nand
command is different as I now wanted to start reading the partition from its half point, at 0x00fe0000
(which is its start memory address 0x000e0000
+ the half size offset 0xf00000
).
With both halves dumped, I cleaned up the beginning and end of the files so that only the hex dump remained:
# head -n 5 rootfs1-1.dump
80000000: 00 00 00 03 00 00 00 01 ff ff 00 00 00 00 00 00 ................
80000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
80000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
80000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
80000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
# tail -n 5 rootfs1-1.dump
80efffb0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
80efffc0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
80efffd0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
80efffe0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
80effff0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ................
Then I combined them into rootfs1.dump
.
Hex to Bin
To transform the hext into binary data, I tried using Matt Brown’s parse-uboot-dump.py
tool (found here) but the tool did not work as expected. I’ll spare you the details, but in essence, my dump was slightly corrupted where not all lines were in the same md.b
format. Picocom must have glitched out a few times during the dump process.
To remedy this, I set off to write a new tool, which will hopefully help you in the future as well: uboot-md2bin. I took inspiration from Matt’s tool in terms of what it did (cleaned up the hex output, stripped it to just hex bytes, and translated the hex into bin), but I added several error checks that are performed prior to processing the file. This allowed me to ensure my large dumps (hehe…) were reliable for further processing. After running uboot-md2bin
on the rootfs1 dump, I found that there were several places where the dump was missing data.

Line 578142 was indeed broken:

Not only that, when I compared the memory offset values, it seemed that there were 64 bytes of data missing from offset 0x808d25c0
-> 0x808d2600
. Quick Python math helped out here:
# python3
>>> hex(0x808d2600-0x808d25c0)
'0x40'
>>> 0x40
64
To fix this gap I read into RAM the broken portion of the dump and dumped it again:
nand read 0x80000000 0x9b25c0 0x80
md.b 0x80000000 0x80
0x9b25b0
in the above command is the starting point of the partiton + the address where the broken offset occurs. I chose 808d25c0
(which would be 0x8d25c0
after stripping off the RAM address 0x80000000
) and read 128 (0x80
) bytes to make sure I get the section I need.

This wasn’t the only place where the dump was bad. So, I decided to go back to my two half dumps as it would be easier to math out the memory locations I needed to restore (as I knew the starting addresses for each half).
Here’s the fixed dump section:

I saved the dump, reran the tool, found the next broken portion, fixed it, and so on until it exported successfully. (I have since re-wrote the logic of the tool to find all broken sections at once instead of having to go through saving, then re-running, fixing…). Some of the missing sections were larger, so naturally I had to accommodate for the larger gaps in both of the commands I was using. Note: when I got to processing the second half of the dump, rootfs1-2.dump
, I realized the dump is just filled with ff
, meaning it was empty. Therefore, I proceeded further with just the first half as my full dump.
Eventually, running uboot-md2bin
completed successfully without any issues:
# uboot-md2bin rootfs1-1.dump
╻━━ ┳┳ ┳┓ ┓┏┓┓ • ━━╻
┃━━━━ ┃┃━━┣┫┏┓┏┓╋ ┏┳┓┏┫┏┛┣┓┓┏┓ ━━━━┃
╹━━ ┗┛ ┻┛┗┛┗┛┗ ┛┗┗┗┻┗━┗┛┗┛┗ ━━╹
- By enzym3 (github.com/e-nzym3) -
[*] Reading rootfs1-1.dump file for processing...
[*] Validating contents...
[*] Processing complete! Resulting file is: rootfs1-1_output.bin
Now it was time to binwalk!
Rootfs1 Analysis
Binwalk detected a whole bunch of things within the dump, so I slapped on -e
to see what’s inside:
# binwalk -e rootfs1-1_output.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 YAFFS filesystem, big endian
393216 0x60000 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
475136 0x74000 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
849664 0xCF700 Unix path: /var/run/utmp
860068 0xD1FA4 Unix path: /etc/config/resolv.conf
937984 0xE5000 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
1007616 0xF6000 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
1012112 0xF7190 Base64 standard index table
1038336 0xFD800 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
1130496 0x114000 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
1179648 0x120000 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
1253376 0x132000 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
1273856 0x137000 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
1282048 0x139000 ELF, 32-bit MSB MIPS64 shared object, MIPS, version 1 (SYSV)
1298442 0x13D00A Unix path: /etc/init.d/rcS
1304576 0x13E800 Executable script, shebang: "/bin/sh"
1307104 0x13F1E0 Unix path: /etc/init.d/rcS1 ]; then
1307141 0x13F205 Unix path: /etc/init.d/rcS1
1699840 0x19F000 ELF, 32-bit MSB MIPS64 executable, MIPS, version 1 (SYSV)
2653950 0x287EFE Intel x86 or x64 microcode, sig 0x000020a4, pf_mask 0xc2af0000, 2000-13-08, rev 0x-3d510000, size 8192
3045854 0x2E79DE bzip2 compressed data, block size = 100k
3068828 0x2ED39C Copyright string: "Copyright (C) 1998-2007 Erik Andersen, Rob Landley, Denys Vlasenko"
3071244 0x2EDD0C Unix path: /var/run/utmp
3073616 0x2EE650 Unix path: /var/spool/cron/crontabs
3074444 0x2EE98C Unix path: /var/spool/cron/crontabs
3085308 0x2F13FC Unix path: /var/lock/LCK..%s
3088152 0x2F1F18 Unix path: /lib/modules/modules.dep
3091252 0x2F2B34 HTML document header
3091323 0x2F2B7B HTML document footer
3094240 0x2F36E0 Unix path: /var/run/udhcpc.%iface%.pid -i %iface%[[ -H %hostname%]][[ -c %clientid%]][[ -s %script%]
<...snip...>
Unfortunately, most of the items binwalk found dumped out as “data” files, and the yaffs image did not successfully extract. I tried using yaffshiv
manually with some brute force arguments, to no avail. I was able to go through the extracted data by simply grepping through strings with strings * | grep -i "<search_string>"
but I did not end up finding anything that would assist me in finding the firmware image or any credentials that could be further leveraged.
As such, I went back to my UART console and repeated the memory dumping process for the rootfs,kernel2
partition.
Rootfs2 Analysis
The binwalk extract of rootfs2 resembled that of rootfs1 and also did not extract any individual files; however, when grepping through the extracted data, I found several mentions of admin_404Ao3Tel
user, in what appeared to be a function to update its password to 1234
:
# strings * | grep -i "admin_404"
/bin/adduser -s /bin/sh -h /root -G root -D -S admin_404A03Tel && echo admin_404A03Tel:1234 | /sbin/chpasswd
admin_404A03Tel
/bin/deluser admin_404A03Tel 2> /dev/null
/bin/adduser -s /bin/sh -h /root -G root -D -S admin_404A03Tel && echo admin_404A03Tel:1234 | /sbin/chpasswd
admin_404A03Tel
/bin/deluser admin_404A03Tel 2> /dev/null
/bin/adduser -s /bin/sh -h /root -G root -D -S admin_404A03Tel && echo admin_404A03Tel:1234 | /sbin/chpasswd
admin_404A03Tel
This user also appeared during the router’s boot sequence with a prompt indicating an update to its password, likely as a result of the commands above:
<...snip...>
adduser: /root: File exists
Password for 'admin_404A03Tel' changed
Mar 16 04:20:24 ccc_be: cccRdmSearchObjNodeByName(): cannot find
<...snip...>
[
I attempted to log in with these credentials to the router, to no avail. Furthermore, the router’s boot sequence also mentions updates to passwords of root
and admin_404A03SSH
:
<...snip...>
socket: Address family not supported by protocol
socket: Address family not supported by protocol
Password for 'root' changed
error reading input file
adduser: /root: File exists
Password for 'admin_404A03SSH' changed
DSL[00]: ERROR - Function is only available in the SHOWTIME!
DSL[00]: ERROR - Function is on
<...snip...>
While the rootfs dumps did not contain any mentions of such actions against these two accounts, it led me to believe there may be a way to reveal the actual passwords being set for both of these accounts with a little bit of extra digging. Ultimately, I came out empty handed. After hours of digging through the dumps, I started to lose hope, but I remembered something that I should have checked in the first place: THE WEB PORTAL!
Web Portal
Every router comes with a configuration portal accessible through its private IP address (http://192.168.0.1/ in my case). I plugged my laptop into the router, obtained an IP address on the local subnet, and went ahead to log into the web portal using the credentials provided by the router’s label.
After logging in, under the “Advanced Setup” menu, I found an option to enable SSH remote console access to the router. I enabled it and changed the admin account’s password to admin
.

While doing so, I had my UART serial console up, within which I noticed a previously detected prompt from the initial boot sequence, Password for 'admin_404A03SSH' changed
. BINGO! This web potal feature let me control one of the users I was after! I hopped back into the serial console and tried to login as the admin_404A03SSH
user using the newly configured password. Lo and behold, I was in!!
<..snip...>
deluser: can't find admin_404A03SSH in /etc/group
deluser: /etc/gshadow: No such file or directory
adduser: /root: File exists
Password for 'admin_404A03SSH' changed
PK5001Z login: admin_404A03SSH
Password:
$ whoami
admin_404A03SSH
$ pwd
/root
$ ls
fw voip_fw
Shell Access
Having attained shell access, I tried to first list out the contents of the shadow
file to see what is there and if there are any other user’s whose password hashes I can steal and potentially crack. However, upon trying to do so, I got a permission denied error.
$ cat /etc/shadow
cat: can't open '/etc/shadow': Permission denied
Hmmm… so I then listed out the passwd
file, which worked (naturally), and here I learned there’s a few other users I might be able to attack, such as admin
and obviously root
:
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/false
nobody:x:99:99:nobody:/nonexistent:/bin/false
admin:x:500:500:admin:/home/admin:/bin/sh
user:x:501:501:user:/home/user:/bin/sh
admin_404A03Tel:x:1:1:Linux User,,,:/root:/bin/sh
admin_404A03SSH:x:2:2:Linux User,,,:/root:/bin/sh
I tried su
to other users but unfortunately this failed. I searched through the files in other user directories, some of which contained config.rom
files but these did not contain any interesting details.
Further enumeration led me to the /mnt
directory where I found the NAND
folder.
$ ls /mnt
Config Data NAND firmware preNAND
$ ls /mnt/NAND
bin e-data i-data lib mnt sbin sys var
com etc include linuxrc proc script tmp
dev home info man root share usr
Score! This looked like a copy of the file system stored in NAND. With that being the case, the same permissions would not apply there as in the live system, and I was right. Nothing stopped me from accessing the etc
directory there:
$ pwd
/mnt/NAND/etc
$ ls
Wireless dropbear mdev.conf services
ZLD_Config.sh dsl_api mini_httpd.conf shadow
Zy_Private fstab mini_httpd.pem shadow.default
applist fw_env.config mycert shells
crontab group nsswitch.conf ssl
dhcp.bound iface.map passwd sysconfig
dhcp.deconfig igmpproxy.conf ppp syslog-ng
dhcp.leasefail init.d profile upnpd.conf
dhcp.renew inittab pure-ftpd wide-dhcpv6
dhcp.script l7-protocols radvd.conf zyfwupgrade
dnsmasq linuxigd resolv.conf
dnsmasq.leases logrotate.d rip
Including the shadow
file which gave me access to the user’s password hashes:
$ cat shadow
root:$1$lCu1EmHM$dGWmwYC6TyY9hebi0UjZk1:13013:0:99999:7:::
lp:*:13013:0:99999:7:::
nobody:*:13013:0:99999:7:::
admin:$1$$iC.dUsGpxNNJGeOm1dFio/:13013:0:99999:7:::
user:$1$$iC.dUsGpxNNJGeOm1dFio/:13013:0:99999:7:::
First thing I tried was to search these hashes on the internet in case they represented some default values that have already been cracked by someone. Turns out $1$$iC.dUsGpxNNJGeOm1dFio
stood for 1234
. Which is something I’ve already found when analyzing the rootfs2
parition dump! So I tried to su
as admin
and sure enough, it worked:
$ su admin
Password:
$ whoami
admin
But admin
still did not have sufficient permissions to access the live system’s /etc
directory, and root access is what I am ultimately after. The next thing left to try was to crack the root’s password. Shortly after launching hashcat
, the password cracked to 1234
as well. Hmm… But that password didn’t work with su
. At this point I remembered that there was a prompt during boot that indicated the root’s password was changed:
<...snip...>
socket: Address family not supported by protocol
Password for 'root' changed
error reading input file
<...snip...>
I decided to reboot the router and observe the boot process again. However, this time I noticed something very, very interesting when pressing enter the whole way through boot. There was a login prompt that appeared prior to the system finishing its boot that displayed a different login prompt:
<...snip...>
br0: port 1(eth0.10) entering disabled state
(none) login:
(none) login:
(none) login:
=======================================
resetAllPPPDUnitNumberStatus():
pppdUnitNumberInUsed[0]=0
pppdUnitNumberInUsed[1]=0
pppdUnitNumberInUsed[2]=0
pppdUnitNumberInUsed[3]=0
pppdUnitNumberInUsed[4]=0
pppdUnitNumberInUsed[5]=0
pppdUnitNumberInUsed[6]=0
pppdUnitNumberInUsed[7]=0
=======================================
<...snip...>
The (none) login:
suggested to me that this prompt was pre-router’s config. Which meant, whatever was originally in the firmware, should still be there. WHICH meant, the root password could still be 1234
prior to being reset during the later portion of the boot, as I’ve shown earlier. As such, I rebooted the router once more, and this time tried to authenticate as root when the (none)
login prompt popped up. The result was….. ROOOOOOOT:
(none) login: root
<...snip...>
Password:
# whoami
root
# ls /mnt/Config
WLAN lost+found tr069
HA! I caught the router with its pants down and could now reset the root password to whatever I wished. Great success!
Closing Thoughts
I learned quite a few lessons here, some that I probably should have already known, such as checking the router’s portal first. I was too eager to go in and dump the firmware through UART that I forgot that basic step. But nonetheless, doing so led me on a path to learn some very interesting memory interaction methods and to develop the uboot-md2bin tool. Can’t complain. As the famous Bob Ross always said, “a happy accident”.
As I’ve mentioned before, I’ll be posting more of my IoT adventures, especially with the remaining three routers I snagged from the thirft shop. After all, this was a LOT of value for a $1:

See you around!