CVE-2018-14364: How did I find a bug in Gitlab project import and got shell access
Brief
We use Gitlab-CE as our version control tool and part of the auto dev-ops pipeline. As one of the several sys admins in this company, I think that I might also be the one who knows Gitlab the most.
So I’m going to tell a story about how did I find a bug in Gitlab project import and elaborated it to get shell access of the Gitlab instance.
Curiosity
One day when I was exploring in the source code of Gitlab. I saw a function in
./lib/gitlab/import_export/file_importer.rb
def extracted_files
Dir.glob("#{@shared.export_path}/**/*", File::FNM_DOTMATCH).reject { |f| f =~ %r{.*/\.{1,2}$} }
end
The objective of this function is obvious, to list every file in target
directory (including hidden files) and exclude all .
and ..
which
means current directory and the up level directory. However, there’s a
miss-use of regular expression here. According to Ruby Doc
, $ - Matches end of line
while \z - Matches end of string
. Many
programmers remembers “$ for the end” but neglects that the input could
be a multi-line string, for example, according to Comparison of file systems - Wikipedia, any byte except NUL, /
is legal
to form a filename, which means a string like .\nevil
is a fully legitimate
filename and able to pass the regular expression here.
So we got a flaw here now, the next question is how to elaborate it. I searched
the function name extracted_files
and found that this function only occurs in
the same file:
def remove_symlinks!
extracted_files.each do |path|
FileUtils.rm(path) if File.lstat(path).symlink?
end
true
end
and remove_symlinks!
is called in
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
Now we know we have a way to smuggle a symbolic link to the upload directory by importing a tarball, with some limits to the filename:
- The filename must start with one or two dots (the regexp is
.*/\.{1,2}
and/
is not a legitimate character in a filename) - The filename will have a linefeed character right after the dots.
Say we have successfully uploaded a symbolic link, like .\nevil
, to the
Gitlab instance’s file system. How could it be used then?
Luck
At first I was expecting that I could point the symlink to sensitive files and have a arbitrary file read. However, after several hours of try and inspection. It looks not that easy due to the limits in Gitlab’s routes definition and the existence of CarrierWave.
Then I decided to grab a cup of tea. Before I left my seat, I deleted
several projects I used for testing on Gitlab web interface to keep
the workspace clean. After the tea time, I came back to the seat and
unconsciously executed ls
in the terminal (this might a common habit
for many sys admins). I noticed that the uploaded .\nevil
was still there
even if the project itself was already deleted. It makes sense to keep
the uploaded files, since if a link is referred in another page, it’s not
affected by the project deletion. This is a matter of design so I won’t go
too deep. But with the existence of this feature, an attack procedure finally
comes to my mind.
- Creating a symbolic link
gitlab-rails/uploads/mygroup/myrepo/.\nevil -> /var/opt/gitlab
by importing a crafted tarball. - Delete the project.
- Import another specially crafted tarball, but this time,
.\nevil
is a real directory instead of a symlink, and a file at.\nevil/authorized_keys
which contains my public key. ssh git@GITLAB-INSTANCE
Timeline
- Jul 6th: Report the issue via Gitlab’s Hackerone program
- Jul 10th: First response from Gitlab
- Jul 11th: Gitlab asked for more details
- Jul 12th: Validated
- Jul 18th: Fix released. https://about.gitlab.com/2018/07/17/critical-security-release-gitlab-11-dot-0-dot-4-released/
- Jul 19th: Resolved