Synaktiv ended up investigating the Western Digital Pro PR4100 when looking at the target list for pwn2own tokyo 2020. When looking at this device, they took particular interest in the webserver, and reversed the cgi-bin that implemented it. One of the functions they looked at was the
wd_login() function, which would take a user-provided username and base64 encoded password (which later gets compared against the
/etc/shadow file). This function ended up containing 2 bugs which could be chained together to gain code execution.
Bug 1 - Base64 decoding across two buffers in
What was strange was the fact that
wd_login() would take a 256 byte base64 password, which would allow 192 bytes of the decoded password. This was a red flag, because the buffer for the decoded password was only 64 bytes on the stack. This in itself was a bug, because it lead to the decode routine writing into the next adjacent buffer, being the base64 password buffer itself. This bug was not immediately useful, but was helpful for exploiting another more critical bug.
Bug 2 - Heap overflow in
When copying the decoded password into a local 120 byte stack buffer,
do_auth_with_shadow() would use
strcpy(), and did no bounds checking on the length of the string. Because of the first bug which allowed the decoded string to flow across two buffers, it allowed for more than 120 of continuous bytes without a null terminator, leading to stack overflow. Because there’s no canaries built into this binary, this easily allows arbitrary code execution via hijacking the return address.
While triggering code execution was fairly straightforward, actually abusing it turned out to be tricky. While the login_mgr binary was not position independent and was a 32-bit binary, libraries like libc are 64-bit and are position independent. They were also restricted by not being able to place any null bytes in the first 120 bytes of the payload, since that would fail to trigger the bug. The upper 32-bits of the RIP overwrite also needed to be null for the address to be a valid address. They needed a one-shot exploit using gadgets found in the login_mgr binary. Ultimately it came down to abusing functions that called
system(), and getting control over the RDI register to specify arbitrary commands to it.
They discovered a range of bytes that could be completely user controlled on the stack, being
$rsp+0x0150. This range is after the return address overwrite, so the no-null byte restraint wasn’t required. It could also be completely controlled by the attacker with the ability to write null bytes, because the data was written there via base64 decode. They used two stack pivots to exploit the vulnerability. The first one shifted the stack pointer to point into the completely controlled range. The second pivot was used to call the
WD’s Alleged Bad Faith Synaktiv briefly notes at the end that this vulnerability was patched 2 days before the end of registration for pwn2own, hinting that they think Western Digital intentionally patched this so close to the deadline. They further note that Western Digital’s bad faith toward security researchers is “notorious”, and some researchers don’t bother reporting to them anymore for that reason. Finally, they state some interesting observations with how similar the code in WD NAS devices and D-Link NAS devices are, stating someone else found a very similar issue to this one in a D-Link product. They suspect this is because D-Link offloaded their firmware code to Western Digital after they winded down their NAS product division.