Created
July 16, 2025 08:32
-
-
Save thib-ack/d56c4245fa746e20bd64734e26972185 to your computer and use it in GitHub Desktop.
Grafana : Extract an encrypted datasource password from database
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| /* | |
| Inspired by https://github.com/jas502n/Grafana-CVE-2021-43798/blob/main/AESDecrypt.go | |
| to work with new Grafana key format | |
| */ | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "crypto/aes" | |
| "crypto/cipher" | |
| "crypto/sha256" | |
| "encoding/base64" | |
| "errors" | |
| "fmt" | |
| "golang.org/x/crypto/pbkdf2" | |
| "os" | |
| ) | |
| const ( | |
| saltLength = 8 | |
| aesCfb = "aes-cfb" | |
| aesGcm = "aes-gcm" | |
| encryptionAlgorithmDelimiter = '*' | |
| encryptionKeynameDelimiter = '#' | |
| ) | |
| func deriveByDelimiter(payload []byte, delimiter byte) (string, []byte, error) { | |
| if len(payload) == 0 { | |
| return "", nil, fmt.Errorf("unable to derive empty payload") | |
| } | |
| if payload[0] != delimiter { | |
| return "", nil, fmt.Errorf("old format not supported") | |
| } | |
| payload = payload[1:] | |
| delim := bytes.Index(payload, []byte{delimiter}) | |
| if delim == -1 { | |
| return "", nil, fmt.Errorf("old format not supported") | |
| } | |
| dataB64 := payload[:delim] | |
| payload = payload[delim+1:] | |
| data := make([]byte, base64.RawStdEncoding.DecodedLen(len(dataB64))) | |
| _, err := base64.RawStdEncoding.Decode(data, dataB64) | |
| if err != nil { | |
| return "", nil, err | |
| } | |
| return string(data), payload, nil | |
| } | |
| func decryptGCM(block cipher.Block, payload []byte) ([]byte, error) { | |
| gcm, err := cipher.NewGCM(block) | |
| if err != nil { | |
| return nil, err | |
| } | |
| nonce := payload[saltLength : saltLength+gcm.NonceSize()] | |
| ciphertext := payload[saltLength+gcm.NonceSize():] | |
| return gcm.Open(nil, nonce, ciphertext, nil) | |
| } | |
| // Key needs to be 32bytes | |
| func encryptionKeyToBytes(secret, salt string) ([]byte, error) { | |
| return pbkdf2.Key([]byte(secret), []byte(salt), 10000, 32, sha256.New), nil | |
| } | |
| func decryptCFB(block cipher.Block, payload []byte) ([]byte, error) { | |
| // The IV needs to be unique, but not secure. Therefore it's common to | |
| // include it at the beginning of the ciphertext. | |
| if len(payload) < aes.BlockSize { | |
| return nil, errors.New("payload too short") | |
| } | |
| iv := payload[saltLength : saltLength+aes.BlockSize] | |
| payload = payload[saltLength+aes.BlockSize:] | |
| payloadDst := make([]byte, len(payload)) | |
| stream := cipher.NewCFBDecrypter(block, iv) | |
| // XORKeyStream can work in-place if the two arguments are the same. | |
| stream.XORKeyStream(payloadDst, payload) | |
| return payloadDst, nil | |
| } | |
| func Decrypt(payload []byte, secret string) ([]byte, error) { | |
| alg, payload, err := deriveByDelimiter(payload, encryptionAlgorithmDelimiter) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if len(payload) < saltLength { | |
| return nil, fmt.Errorf("unable to compute salt") | |
| } | |
| salt := payload[:saltLength] | |
| key, err := encryptionKeyToBytes(secret, string(salt)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| block, err := aes.NewCipher(key) | |
| if err != nil { | |
| return nil, err | |
| } | |
| switch alg { | |
| case aesGcm: | |
| return decryptGCM(block, payload) | |
| case aesCfb: | |
| return decryptCFB(block, payload) | |
| default: | |
| return nil, fmt.Errorf("unknown algorithm %v", alg) | |
| } | |
| } | |
| func readString(msg string) string { | |
| reader := bufio.NewReader(os.Stdin) | |
| fmt.Print(msg) | |
| text, _ := reader.ReadString('\n') | |
| return text[:len(text)-1] | |
| } | |
| func Process() error { | |
| secretKey := readString("Enter secret_key (grep secret_key /etc/grafana/grafana.ini): ") | |
| dataSourcePasswordB64 := readString("Enter datasource password (SELECT secure_json_data FROM data_source WHERE name = '<xxx>';): ") | |
| dataSourcePassword, err := base64.StdEncoding.DecodeString(dataSourcePasswordB64) | |
| if err != nil { | |
| return fmt.Errorf("unable to base64 decode datasource password. %v", err) | |
| } | |
| // format is #<key_name>#*<algo>*<data> | |
| keyName, payload, err := deriveByDelimiter(dataSourcePassword, encryptionKeynameDelimiter) | |
| if err != nil { | |
| return err | |
| } | |
| dataKeyB64 := readString(fmt.Sprintf("Enter data_key (SELECT TO_BASE64(encrypted_data) FROM data_keys WHERE name = '%v';): ", keyName)) | |
| // Now decrypt the data_key using the global secret_key | |
| dataKeyEncrypted, err := base64.StdEncoding.DecodeString(dataKeyB64) | |
| if err != nil { | |
| return fmt.Errorf("unable to base64 decode data_key. %v", err) | |
| } | |
| dataKey, err := Decrypt(dataKeyEncrypted, secretKey) | |
| if err != nil { | |
| return fmt.Errorf("unable to decrypt data_key. %v", err) | |
| } | |
| // and then the datasource_password with its dedicated data_key | |
| password, err := Decrypt(payload, string(dataKey)) | |
| if err != nil { | |
| return fmt.Errorf("unable to decrypt datasource password. %v", err) | |
| } | |
| fmt.Println("password = " + string(password)) | |
| return nil | |
| } | |
| func main() { | |
| if err := Process(); err != nil { | |
| fmt.Println(err) | |
| os.Exit(1) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment