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:

  1. The filename must start with one or two dots (the regexp is .*/\.{1,2} and / is not a legitimate character in a filename)
  2. 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.

  1. Creating a symbolic link gitlab-rails/uploads/mygroup/myrepo/.\nevil -> /var/opt/gitlab by importing a crafted tarball.
  2. Delete the project.
  3. 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.
  4. ssh git@GITLAB-INSTANCE

Timeline

  1. Jul 6th: Report the issue via Gitlab’s Hackerone program
  2. Jul 10th: First response from Gitlab
  3. Jul 11th: Gitlab asked for more details
  4. Jul 12th: Validated
  5. Jul 18th: Fix released. https://about.gitlab.com/2018/07/17/critical-security-release-gitlab-11-dot-0-dot-4-released/
  6. Jul 19th: Resolved

Categories:

Updated: