
POC pour une vulnérabilité RCE dans la bibliothèque ParseExcel, et ParseXLSX également, en tant que bibliothèque dépendante.
TL;DR : RCE à partir de la logique d'analyse des chaînes de format.
La cause racine de l'exploitation vient de l'appel à eval sur une entrée utilisateur non validée dans 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;
}
#...
}
D'après ce que j'ai inspecté, l'implémentation actuelle de ce flux manque de validation appropriée, alors que l'utilisation de eval pour gérer les logiques de comparaison est excessive (« over kill ») dans ce cas. De ce fait, ParseExcel::parse et ParseXLSX::parse (utilisés pour lire les données des fichiers Excel) sont tous deux vulnérables aux RCE.
$format_str ?ValFmt est l'appelant le plus probable de ExcelFmt, je vais donc expliquer plus en détail cette méthode
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} );
}
}
Si $oCell->{Type} est Date ou Number, ExcelFmt sera appelé.
La valeur $format_str est celle retournée par une autre méthode : 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;
}
Une autre fonction est appelée, nous allons donc examiner également 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
#...
}
Toutes les variables sont claires, nous pouvons conclure le vecteur d'attaque comme suit :
$iFmtIdx$oBook->{Format}[$cellFmtIdx] correspond à $iFmtIdx$oCell->{FormatNo} = $cellFmtIdx)
![[flow 1.png]]Dans les sections ci-dessous, je vais détailler comment la charge utile a propagé le code shell jusqu'à la commande eval. Il y aura 2 sections : l'analyse des fichiers .xls avec ParseExcel et l'analyse des fichiers .xlsx avec ParseXLSX.
Pour démontrer, voici le lien vers nos fichiers Excel malveillants conçus (en .xls et .xlsx) qui exécutent whoami et stockent le résultat dans le fichier /tmp/inject.txt.
https://gist.github.com/haile01/0f4f19e4441895ef33ff27385080478b
Prenons un simple programme Perl pour analyser un fichier xls comme ci-dessous, qui utilise ParseExcel::parse. La RCE se produira pendant l'analyse, avant même que des données ne soient récupérées.
use strict;
use Spreadsheet::ParseExcel;
my $parser = Spreadsheet::ParseExcel->new();
# file.xls is malicious file from end user
my $workbook = $parser->parse("test.xls");
Les fichiers binaires Excel 97 sont structurés en blocs de données binaires appelés enregistrements BIFF. Chaque enregistrement commence par un en-tête appelé opCode (en little-endian), suivi de la longueur de l'enregistrement et de ses données réelles.
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;
}
Ensuite, un gestionnaire correspondant au type d'enregistrement est utilisé pour extraire les données de cet enregistrement 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 );
}
La chaîne de format est gérée par _subFormat, avec 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;
}
Je n'étais pas sûr de la version BIFF utilisée dans mon fichier .xls, mais d'après les données du fichier binaire, elle devrait correspondre au cas else (> verBIFF5).
La structure de l'enregistrement de chaîne de format dans les versions BIFF plus récentes devrait être 1E 04 [longueur d'enregistrement - 2 octets] [index de chaîne de format - 2 octets] [longueur de chaîne de format - 1 octet] [drapeaux de chaîne - 2 octets] [contenu de chaîne de format]
En suivant la structure correcte, je peux injecter n'importe quelle chaîne de format dans le fichier .xls.