當(dāng)git遇上ssh——CVE-2017-1000117漏洞淺析
Git是一個開源的分布式版本控制系統(tǒng),主要用于項目管理。
而SSH是一種應(yīng)用層的安全通信協(xié)議,最常用的就是為通信雙方在在不安全網(wǎng)絡(luò)上提供安全的遠(yuǎn)程登錄。
當(dāng)他們二者相遇會發(fā)生什么有趣的事呢?這里以CVE-2017-1000117漏洞為例,簡要剖析該漏洞的成因及防護方法。
漏洞相關(guān)信息:
版本控制軟件爆出遠(yuǎn)程命令執(zhí)行漏洞 涉及Git、SVN、Mercurial、CVS版本控制
簡述:幾個流行的版本控制系統(tǒng)受到可能嚴(yán)重的遠(yuǎn)程命令執(zhí)行漏洞的影響。受影響產(chǎn)品的開發(fā)人員本周發(fā)布了更新補丁來修補安全漏洞。該缺陷影響版本控制軟件, 如 Git (CVE-2017-1000117)、Apache Subversion (CVE-2017-9800)、Mercurial (CVE-2017-1000116) 和 CVS。由于CVS 系統(tǒng)上次更新已經(jīng)是9年前的事情了, 因此沒有為它分配 CVE 標(biāo)識符。
背景知識
ssh客戶端登錄時,有一個ProxyCommand選項,該選項的指定鏈接服務(wù)器時執(zhí)行的命令。
- 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.
該選項常用的場景是通過代理服務(wù)器與目標(biāo)機器相連,因此被稱作ProxyCommand,如下圖。
本地的機器(Local)無法直接與目標(biāo)機器(Target)相連,必須通過一個代理機器(Proxy)才能和目標(biāo)機器建立連接。此場景多見于企業(yè)或有較強訪問控制的需求的地方。
因此在這種情況下,ssh客戶端可以采用ProxyCommand選項,通過下面命令最終和目標(biāo)機器建立連接。
- ssh -o ProxyCommand=’ssh user@proxy nc %h 22′ user@Target
加上ProxyCommand選項后。ssh客戶端會先用當(dāng)前用戶的shell執(zhí)行ProxyCommand中的內(nèi)容。
例如下面的命令,在Linux桌面環(huán)境中執(zhí)行,就會彈出gedit文本編輯器。
- ssh -oProxyCommand=gedit user@Target
即便最后的user@hostname不合法,也不會影響ProxyCommand中先執(zhí)行的命令,照樣可以彈出gedit。
好了介紹完了ProxyCommand,可以理解為這個選項如處理不當(dāng),是可以進行命令注入的!
CVE-2017-1000117漏洞
CVE-2017-1000117這個漏洞就是沒有正確處理ssh鏈接的請求,導(dǎo)致受害人通過Git版本控制系統(tǒng),訪問惡意鏈接時,存在安全隱患,一旦黑客攻擊成功,可在受害人機器上執(zhí)行任意命令。
git clone是Git版本控制系統(tǒng)中常用的將遠(yuǎn)程倉庫克隆到本地的命令。當(dāng)使用git clone訪問下面的惡意ssh鏈接時,會在本地執(zhí)行命令,彈出gedit。
- git clone ssh://-oProxyCommand=”gedit /tmp/xxx”
下面我們來詳細(xì)看一看其中的過程,當(dāng)git遇上ssh后,最終是如何觸發(fā)這個漏洞執(zhí)行的。
git客戶端在執(zhí)行上面的命令后,通過一系列的參數(shù)解析后,進入git_connect函數(shù),向git的服務(wù)端建立連接。
- 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函數(shù)的第二個參數(shù)url,即為傳入的ssh鏈接,在此例中為 “ssh://-oProxyCommand=gedit /tmp/xxx”。
在git_connect函數(shù)中通過parse_connect_url函數(shù)將待連接的url解析出來,返回url的主機名、相對路徑及url采用的協(xié)議。
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/,經(jīng)parse_connect_url解析后,其返回的ret_host和ret_path的值應(yīng)該為 user@host.xzy 和 /path/to/repo.git/ 。
但由于沒有對ssh做正確過濾及識別,對于惡意的ssh鏈接,返回的ret_host和ret_path的值則是 -oProxyCommand=gedit 和 /tmp/xxx ,誤將 -oProxyCommand=gedit 作為了主機名ret_host。
在后續(xù)處理中,git_connect得到本地ssh的路徑,將上面獲取的ssh host和path填充到struct child_process *conn中,再通過start_command調(diào)用本地ssh執(zhí)行。
在start_command函數(shù)中,最終調(diào)用exec系列函數(shù)執(zhí)行ssh,由于錯誤的把 -oProxyCommand=gedit 作為遠(yuǎn)程待連接的host,最終引發(fā)了命令執(zhí)行。
但像上面ssh://-oProxyCommand=”gedit /tmp/xxx”的鏈接比較暴露,直接在鏈接中就出現(xiàn)命令。比較隱蔽的方法是,在正常倉庫的目錄下建立一個子模塊submodule,而將惡意的ssh鏈接藏在.gitmodule文件中。
修復(fù)防護方法
看完上面漏洞發(fā)生的成因,其實可以發(fā)現(xiàn)這個過程就是git處理ssh這類智能協(xié)議的傳輸過程:ssh遠(yuǎn)程登錄git服務(wù)器后,通過執(zhí)行g(shù)it-upload-pack處理下載的數(shù)據(jù),這種處理方式較http啞協(xié)議傳輸更高效。
但是在這過程中,對一些惡意的ssh鏈接,沒有正確識別,在解析時誤將 -oProxyCommand 這類參數(shù)當(dāng)做了遠(yuǎn)程主機名host,從而產(chǎn)生了漏洞。
在新版本中,我們看到增加了對host和path的識別過濾。
對包含疑似命令的host和path及時進行了阻止,阻斷了漏洞的發(fā)生。
建議用戶及時排查,更新系統(tǒng)存在漏洞的Git版本,在日常通過Git進行項目管理時,仔細(xì)檢查項目中是否存在一些惡意ssh鏈接來預(yù)防安全問題。
原文鏈接:http://blog.nsfocus.net/git-ssh-cve-2017-1000117/
【本文是51CTO專欄作者“綠盟科技博客”的原創(chuàng)稿件,轉(zhuǎn)載請通過51CTO聯(lián)系原作者獲取授權(quán)】