
ParseExcel लाइब्रेरी में RCE भेद्यता के लिए POC, और एक निर्भर लाइब्रेरी के रूप में ParseXLSX भी
TL;DR: फ़ॉर्मेट स्ट्रिंग्स को पार्स करने के लॉजिक से RCE।
एक्सप्लॉइटेशन का मूल कारण Utility.pm में बिना सत्यापित उपयोगकर्ता इनपुट पर eval को कॉल करना है।
# 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 का उपयोग इस मामले में बहुत अधिक ("over kill") है। इस कारण से, ParseExcel::parse और ParseXLSX::parse दोनों (जो Excel फ़ाइलों से डेटा पढ़ने के लिए उपयोग होते हैं) RCE के प्रति संवेदनशील हैं।
$format_str कहाँ है?ValFmt ही ExcelFmt का सबसे संभावित कॉलर है, इसलिए मैं इस मेथड के बारे में और विस्तार से बताऊँगा
https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel/FmtDefault.pm#L141-L161
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
https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel/FmtDefault.pm#L101-L136
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 की भी जाँच करेंगे
https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel/FmtDefault.pm#L87-L96
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 कमांड तक कैसे पहुँचाया। ParseExcel का उपयोग करके .xls फ़ाइल को पार्स करने और ParseXLSX का उपयोग करके .xlsx फ़ाइल को पार्स करने के लिए 2 अनुभाग होंगे।
प्रदर्शन के लिए, नीचे हमारी तैयार की गई दुर्भावनापूर्ण 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 नामक हेडर (little-endian में) से शुरू होता है, फिर रिकॉर्ड की लंबाई और उसका वास्तविक डेटा होता है।
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 रिकॉर्ड डेटा को निकालने के लिए रिकॉर्ड प्रकार का एक संगत हैंडलर उपयोग किया जाता है। https://github.com/jmcnamara/spreadsheet-parseexcel/blob/19ea68d2ebf640e06df4f6937fcb43d76a5ec96b/lib/Spreadsheet/ParseExcel.pm#L576-L580
if ( defined $self->{FuncTbl}->{$record} && !$workbook->{_skip_chart} )
{
$self->{FuncTbl}->{$record}
->( $workbook, $record, $record_length, $record_header );
}
फ़ॉर्मेट स्ट्रिंग को _subFormat द्वारा संभाला जाता है, जिसमें opCode = 0x41E होता है।
https://github.com/jmcnamara/spreadsheet-parseexcel/blob/19ea68d2ebf640e06df4f6937fcb43d76a5ec96b/lib/Spreadsheet/ParseExcel.pm#L1563-L1585
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;
}
मुझे यकीन नहीं था कि मेरी .xls फ़ाइल में कौन सा BIFF संस्करण उपयोग हो रहा है, लेकिन बाइनरी फ़ाइल के डेटा के अनुसार, यह else केस (> verBIFF5) से मेल खाना चाहिए।
नए BIFF संस्करणों में फ़ॉर्मेट स्ट्रिंग रिकॉर्ड की संरचना इस प्रकार होनी चाहिए 1E 04 [रिकॉर्ड लंबाई - 2 बाइट्स] [फ़ॉर्मेट स्ट्रिंग इंडेक्स - 2 बाइट्स] [फ़ॉर्मेट स्ट्रिंग लंबाई - 1 बाइट] [स्ट्रिंग फ़्लैग्स - 2 बाइट्स] [फ़ॉर्मेट स्ट्रिंग सामग्री]
सही संरचना का पालन करके, मैं .xls फ़ाइल में कोई भी फ़ॉर्मेट स्ट्रिंग इंजेक्ट कर सकता हूँ।