Skip to content
KitploitKITPLOIT
HerramientasBlog
Log in
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
1869hace 2 añosAú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

# 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 eval para manejar la lógica de comparación es excesivo ("over kill") en este caso. Debido a esto, tanto ParseExcel::parse como ParseXLSX::parse (usados para leer datos de archivos Excel) son vulnerables a RCE.

¿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

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

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

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.

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

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

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

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)

Descargar herramienta