GitLab的文件讀取問題導致的RCE漏洞
漏洞概述
GitLab CE/EE的8.9、8.10、8.11、8.12以及8.13版本中存在任意文件讀取漏洞,攻擊者或可利用這個漏洞來獲取應用程序中敏感文件的訪問權。在獲取到這些機密數(shù)據(jù)之后,攻擊者將可以通過執(zhí)行惡意命令來訪問應用程序服務器。
8.9、8.10、8.11和8.12版本中漏洞的CVSS(通用漏洞評分系統(tǒng))評分為8.4分(CVSS:3.0/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:H/A:H)。而8.13版本中相同漏洞的CVSS評分為9.0分,因為在該版本中攻擊者完全可以在無需獲得管理員權限的情況下利用這個漏洞(CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H)。在所有版本的攻擊場景中,GitLab實例需要導入并啟用GitLab的輸出文件功能。
漏洞分析
GitLab的輸出上傳功能中存在一個漏洞,這個漏洞將允許攻擊者讀取GitLab實例中的任意文件,這個漏洞主要是由JSON.parse中的錯誤操作而導致的,因為JSON.parse中有可能會包含或引用GitLab導出文件的符號鏈接。當我準備開始對這個功能進行分析之前,我創(chuàng)建了一個演示倉庫,并且還通過項目的管理員面板導出了這個GitLab實例。當我們創(chuàng)建了一個新的項目之后,我們就可以直接導入這些GitLab文件了。演示站點為https://gitlab.com/projects/new(點擊”GitLab導出”)。通常情況下,一個簡單的GitLab導出文件一般包含下列文件:
- export $ ls -lash
- total 48
- 8 -rw-r--r--@ 1 jobert staff 5B Oct 25 19:52 VERSION
- 8 -rw-r--r--@ 1 jobert staff 341B Oct 25 19:53 project.bundle
- 8 lrwxr-xr-x 1 jobert staff 11B Oct 25 20:43 project.json
當我們再次加載之前導出的GitLab文件時,將會發(fā)生以下幾件事情。首先,系統(tǒng)會等待文件寫入磁盤(針對大型倉庫而言);其次,系統(tǒng)會根據(jù)VERSION文件來檢測導入項目的版本;最后,GitLab會根據(jù)project.json文件來創(chuàng)建一個新的Project實例。
這里的第一步其實并不重要,所以我們現(xiàn)在直接來看一看第二步中系統(tǒng)所執(zhí)行的相關代碼(Gitlab::ImportExport::VersionChecker,第12-18行):
- def check!
- version = File.open(version_file, &:readline)
- verify_version!(version)
- rescue => e
- shared.error(e)
- false
- end
請各位注意第13行的代碼,它將會打開文件并調用readline方法,而這個方法將會返回穩(wěn)健的第一行數(shù)據(jù)。第16行代碼會捕獲系統(tǒng)運行過程中的所有異常,并將異常信息壓入errors棧。所有的這些錯誤都將被發(fā)送至前端。接下來,讓我們看一看該文件中的第27-31行代碼:
- if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
- raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
- else
- true
- end
這也就意味著,如果文件版本不正確的話,系統(tǒng)會返回一個異常,異常信息中會包含這份GitLab導出文件的版本信息。我們解壓GitLab的導出文件,用一個符號鏈接替換其中的VERSION文件,然后再重新進行壓縮。tar文件的結構如下所示:
- export $ ls -lash
- 8 lrwxr-xr-x 1 jobert staff 11B Oct 25 20:43 VERSION -> /etc/passwd
- 8 -rw-r--r--@ 1 jobert staff 341B Oct 25 19:53 project.bundle
- 8 lrwxr-xr-x 1 jobert staff 11B Oct 25 20:43 project.json
在創(chuàng)建好了新的GitLab導出文件之后(在導出目錄中執(zhí)行tar -czvf test.tar.gz),我們就可以加載這份新的GitLab文件了。加載成功之后,因為文件存在版本錯誤,所以系統(tǒng)將會拋出一個異常,而GitLab實例將會返回第一行錯誤信息:
但是,這種方法只能讀取某個文件的第一行數(shù)據(jù)。這的確很有意思,但這肯定不是我們想要的,我們想要讀取文件的完整內容。于是我們繼續(xù)分析,看看是否能找到讀取完整文件的方法。正如我之前所提到的,導入過程中的第三步就是創(chuàng)建一個新的Project實例。此時,下列代碼將會被執(zhí)行(Gitlab::ImportExport::ProjectTreeRestorer,第11-22行):
- def restore
- json = IO.read(@path)
- tree_hash = ActiveSupport::JSON.decode(json)
- project_members = tree_hash.delete('project_members')
- ActiveRecord::Base.no_touching do
- create_relations
- end
- rescue => e
- shared.error(e)
- false
- end
這段代碼采用的結構與負責進行版本檢測的代碼結構非常相似,第13-18行代碼可以捕獲異常,然后將錯誤信息壓入errors棧。ActiveSupport會使用JSON.parse來解碼JSON數(shù)據(jù),如果解碼失敗的話,系統(tǒng)會將待解碼的字符串包含在錯誤信息中一起返回。這也就意味著,如果我們可以讓解碼器拋出一個異常的話,我們就可以讀取穩(wěn)健的內容了。其實也并不難,先來看看下面給出的這個文件結構:
- export $ ls -lash
- 8 -rw-r--r--@ 1 jobert staff 11B Oct 25 20:43 VERSION
- 8 -rw-r--r--@ 1 jobert staff 341B Oct 25 19:53 project.bundle
- 8 lrwxr-xr-x 1 jobert staff 11B Oct 25 20:43 project.json -> /etc/passwd
在這個例子中,project.json文件是一個指向/etc/passwd的符號鏈接。第14行代碼可以調用IO.read方法來讀取文件內容。很明顯,/etc/passwd文件中并不包含有效的JSON數(shù)據(jù)。因此,系統(tǒng)肯定會拋出一個異常,而異常信息中將會包含/etc/passwd文件的內容。使用tar來對文件進行壓縮,然后準備上傳[演示文件下載-test.tar.gz(F130233)]。文件導入成功之后,我們就可以從錯誤信息中獲取鏈接文件的內容了:
需要聲明的是,這并不是我自己的/etc/passwd文件。下面給出的是gitlab.com中/etc/passwd文件的最后五行數(shù)據(jù):
- alejandro:x:1117:1117::/home/alejandro:/bin/bash
- prometheus:x:999:999::/opt/prometheus:/bin/false
- gitlab-monitor:x:998:998::/opt/gitlab-monitor:/bin/false
- postgres:x:116:121:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
- brian:x:1118:1118::/home/brian:/bin/bash
因此,攻擊者同樣可以利用這種方法來讀取GitLab中Rails項目的機密文件。需要注意的是,這個問題也將導致RCE漏洞。除此之外,攻擊者甚至還可以通過這個漏洞拿到GitLab的shell,并訪問所有的代碼倉庫。
附件下載
1. F130233: test.tar.gz
2. F130234: Screen_Shot_2016-10-25_at_20.55.36.png
3. F130235: Screen_Shot_2016-10-25_at_19.28.51.png
漏洞修復情況
下面給出的是我們所采用的漏洞修復代碼,感興趣的用戶可以自己動手實現(xiàn)一下:
- diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
- index 113895b..ffd1711 100644
- --- a/lib/gitlab/import_export/file_importer.rb
- +++ b/lib/gitlab/import_export/file_importer.rb
- @@ -43,6 +43,14 @@ module Gitlab
- raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result
- + remove_symlinks!
- + end
- +
- + def remove_symlinks!
- + Dir["#{@shared.export_path}/**/*"].each do |path|
- + FileUtils.rm(path) if File.lstat(path).symlink?
- + end
- +
- true
- end
- end
- diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
- index 7cdba88..c551321 100644
- --- a/lib/gitlab/import_export/project_tree_restorer.rb
- +++ b/lib/gitlab/import_export/project_tree_restorer.rb
- @@ -9,8 +9,14 @@ module Gitlab
- end
- def restore
- - json = IO.read(@path)
- - [@tree_hash](/tree_hash) = ActiveSupport::JSON.decode(json)
- + begin
- + json = IO.read(@path)
- + [@tree_hash](/tree_hash) = ActiveSupport::JSON.decode(json)
- + rescue => e
- + Rails.logger.error("Import/Export error: #{e.message}")
- + raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
- + end
- +
- [@project_members](/project_members) = [@tree_hash](/tree_hash).delete('project_members')
- ActiveRecord::Base.no_touching do
- diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
- index fc08082..bd3c3ee 100644
- --- a/lib/gitlab/import_export/version_checker.rb
- +++ b/lib/gitlab/import_export/version_checker.rb
- @@ -24,12 +24,19 @@ module Gitlab
- end
- def verify_version!(version)
- - if Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
- + if different_version?(version)
- raise Gitlab::ImportExport::Error.new("Import version mismatch: Required #{Gitlab::ImportExport.version} but was #{version}")
- else
- true
- end
- end
- +
- + def different_version?(version)
- + Gem::Version.new(version) != Gem::Version.new(Gitlab::ImportExport.version)
- + rescue => e
- + Rails.logger.error("Import/Export error: #{e.message}")
- + raise Gitlab::ImportExport::Error.new('Incorrect VERSION format')
- + end
- end
- end
- end
我們已經(jīng)發(fā)布了針對該漏洞的修復補丁和安全公告,感興趣的用戶可以訪問并了解詳情。地址:https://about.gitlab.com/2016/11/02/cve-2016-9086-patches/
我們強烈建議使用了上述版本GitLab的用戶盡快安裝更新補丁。請注意,GitLab 8.9.x版本目前還沒有可用的更新補丁。使用了8.9.0-8.9.11版本的用戶雖然沒有可用的更新補丁,但是可以通過下面給出的解決方案來緩解這個漏洞的影響。
漏洞緩解方案
禁用項目的導入/導出功能
使用管理員賬號登錄GitLab,然后執(zhí)行下列操作:
1. 選擇“Admin Area”;
2. 點擊“Settings”
3. 在“Import Sources”面板中禁用“GitLab export”選項
4. 點擊“保存”
驗證操作是否成功:
1. 使用瀏覽器以普通用戶身份登錄GitLab;
2. 點擊“Projects”;
3. 點擊“New Project”
4. 輸入項目名稱;
5. 確保界面中沒有顯示“GitLab export”選項