Git是一个开源的分布式版本控制系统,主要用于项目管理。
而SSH是一种应用层的安全通信协议,最常用的就是为通信双方在在不安全网络上提供安全的远程登录。
当他们二者相遇会发生什么有趣的事呢?这里以CVE-2017-1000117漏洞为例,简要剖析该漏洞的成因及防护方法。
漏洞相关信息:
版本控制软件爆出远程命令执行漏洞 涉及Git、SVN、Mercurial、CVS版本控制
简述:几个流行的版本控制系统受到可能严重的远程命令执行漏洞的影响。受影响产品的开发人员本周发布了更新补丁来修补安全漏洞。该缺陷影响版本控制软件, 如 Git (CVE-2017-1000117)、Apache Subversion (CVE-2017-9800)、Mercurial (CVE-2017-1000116) 和 CVS。由于CVS 系统上次更新已经是9年前的事情了, 因此没有为它分配 CVE 标识符。
背景知识
ssh客户端登录时,有一个ProxyCommand选项,该选项的指定链接服务器时执行的命令。
- ProxyCommand
- Specifies the command to use to connect to the server. The
- command string extends to the end of the line, and is executed
- with the user’s shell. In the command string, any occurrence
- of ‘%h’ will be substituted by the host name to connect,‘%p’
- by the port, and ‘%r’ by the remote user name.
该选项常用的场景是通过代理服务器与目标机器相连,因此被称作ProxyCommand,如下图。
本地的机器(Local)无法直接与目标机器(Target)相连,必须通过一个代理机器(Proxy)才能和目标机器建立连接。此场景多见于企业或有较强访问控制的需求的地方。
因此在这种情况下,ssh客户端可以采用ProxyCommand选项,通过下面命令最终和目标机器建立连接。
- ssh -o ProxyCommand=’ssh user@proxy nc %h 22′ user@Target
加上ProxyCommand选项后。ssh客户端会先用当前用户的shell执行ProxyCommand中的内容。
例如下面的命令,在Linux桌面环境中执行,就会弹出gedit文本编辑器。
- ssh -oProxyCommand=gedit user@Target
即便最后的user@hostname不合法,也不会影响ProxyCommand中先执行的命令,照样可以弹出gedit。
好了介绍完了ProxyCommand,可以理解为这个选项如处理不当,是可以进行命令注入的!
CVE-2017-1000117漏洞
CVE-2017-1000117这个漏洞就是没有正确处理ssh链接的请求,导致受害人通过Git版本控制系统,访问恶意链接时,存在安全隐患,一旦黑客攻击成功,可在受害人机器上执行任意命令。
git clone是Git版本控制系统中常用的将远程仓库克隆到本地的命令。当使用git clone访问下面的恶意ssh链接时,会在本地执行命令,弹出gedit。
- git clone ssh://-oProxyCommand=”gedit /tmp/xxx”
下面我们来详细看一看其中的过程,当git遇上ssh后,最终是如何触发这个漏洞执行的。
git客户端在执行上面的命令后,通过一系列的参数解析后,进入git_connect函数,向git的服务端建立连接。
- struct child_process *git_connect(int fd[2], const char *url,
- const char *prog, int flags)
- {
- char *hostandport, *path;
- struct child_process *conn = &no_fork;
- enum protocol protocol;
- struct strbuf cmd = STRBUF_INIT;
- /* Without this we cannot rely on waitpid() to tell
- * what happened to our children.
- */
- signal(SIGCHLD, SIG_DFL);
- protocol = parse_connect_url(url, &hostandport, &path);
- if ((flags & CONNECT_DIAG_URL) && (protocol != PROTO_SSH)) {
- printf(“Diag: url=%s\n”, url ? url : “NULL”);
- printf(“Diag: protocol=%s\n”, prot_name(protocol));
- printf(“Diag: hostandport=%s\n”, hostandport ? hostandport : “NULL”);
- printf(“Diag: path=%s\n”, path ? path : “NULL”);
- conn = NULL;
- } else if (protocol == PROTO_GIT) {
- …..
- } else {
- conn = xmalloc(sizeof(*conn));
- child_process_init(conn);
- strbuf_addstr(&cmd, prog);
- strbuf_addch(&cmd, ‘ ‘);
- sq_quote_buf(&cmd, path);
- /* remove repo-local variables from the environment */
- conn->env = local_repo_env;
- conn->use_shell = 1;
- conn->in = conn->out = -1;
- if (protocol == PROTO_SSH) {
- const char *ssh;
- int putty = 0, tortoiseplink = 0;
- char *ssh_host = hostandport;
- const char *port = NULL;
- transport_check_allowed(“ssh”);
- get_host_and_port(&ssh_host, &port);
- if (!port)
- port = get_port(ssh_host);
- ssh = getenv(“GIT_SSH_COMMAND”);
- if (!ssh) {
- const char *base;
- char *ssh_dup;
- /*
- * GIT_SSH is the no-shell version of
- * GIT_SSH_COMMAND (and must remain so for
- * historical compatibility).
- */
- conn->use_shell = 0;
- ssh = getenv(“GIT_SSH”);
- if (!ssh)
- ssh = “ssh”;
- ssh_dup = xstrdup(ssh);
- base = basename(ssh_dup);
- free(ssh_dup);
- }
- argv_array_push(&conn->args, ssh);
- if (port) {
- /* P is for PuTTY, p is for OpenSSH */
- argv_array_push(&conn->args, putty ? “-P” : “-p”);
- argv_array_push(&conn->args, port);
- }
- argv_array_push(&conn->args, ssh_host);
- } else {
- transport_check_allowed(“file”);
- }
- argv_array_push(&conn->args, cmd.buf);
- if (start_command(conn))
- die(“unable to fork”);
- …..
- }
- }
git_connect函数的第二个参数url,即为传入的ssh链接,在此例中为 “ssh://-oProxyCommand=gedit /tmp/xxx”。
在git_connect函数中通过parse_connect_url函数将待连接的url解析出来,返回url的主机名、相对路径及url采用的协议。
https://github.com/git/git/blob/master/connect.c#L620
- /*
- * Extract protocol and relevant parts from the specified connection URL.
- * The caller must free() the returned strings.
- */
- static enum protocol parse_connect_url(const char *url_orig, char **ret_host, char **ret_path)
对于正常的ssh链接,如 ssh://user@host.xzy/path/to/repo.git/,经parse_connect_url解析后,其返回的ret_host和ret_path的值应该为 user@host.xzy 和 /path/to/repo.git/ 。
但由于没有对ssh做正确过滤及识别,对于恶意的ssh链接,返回的ret_host和ret_path的值则是 -oProxyCommand=gedit 和 /tmp/xxx ,误将 -oProxyCommand=gedit 作为了主机名ret_host。
在后续处理中,git_connect得到本地ssh的路径,将上面获取的ssh host和path填充到struct child_process *conn中,再通过start_command调用本地ssh执行。
在start_command函数中,最终调用exec系列函数执行ssh,由于错误的把 -oProxyCommand=gedit 作为远程待连接的host,最终引发了命令执行。
但像上面ssh://-oProxyCommand=”gedit /tmp/xxx”的链接比较暴露,直接在链接中就出现命令。比较隐蔽的方法是,在正常仓库的目录下建立一个子模块submodule,而将恶意的ssh链接藏在.gitmodule文件中。
修复防护方法
看完上面漏洞发生的成因,其实可以发现这个过程就是git处理ssh这类智能协议的传输过程:ssh远程登录git服务器后,通过执行git-upload-pack处理下载的数据,这种处理方式较http哑协议传输更高效。
但是在这过程中,对一些恶意的ssh链接,没有正确识别,在解析时误将 -oProxyCommand 这类参数当做了远程主机名host,从而产生了漏洞。
在新版本中,我们看到增加了对host和path的识别过滤。
对包含疑似命令的host和path及时进行了阻止,阻断了漏洞的发生。
建议用户及时排查,更新系统存在漏洞的Git版本,在日常通过Git进行项目管理时,仔细检查项目中是否存在一些恶意ssh链接来预防安全问题。
原文链接:http://blog.nsfocus.net/git-ssh-cve-2017-1000117/
【本文是51CTO专栏作者“绿盟科技博客”的原创稿件,转载请通过51CTO联系原作者获取授权】