├── .github ├── FUNDING.yml ├── banner.png ├── example_powershell.png └── example_python.png ├── .gitignore ├── README.md ├── go ├── Dockerfile ├── Makefile └── src │ ├── ExtractBitlockerKeys.go │ ├── go.mod │ └── go.sum ├── powershell └── ExtractBitlockerKeys.ps1 ├── python └── ExtractBitlockerKeys.py └── requirements.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: p0dalirius 4 | patreon: Podalirius -------------------------------------------------------------------------------- /.github/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/ExtractBitlockerKeys/bb10be27dae2497f137e55b7898186df81010dc2/.github/banner.png -------------------------------------------------------------------------------- /.github/example_powershell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/ExtractBitlockerKeys/bb10be27dae2497f137e55b7898186df81010dc2/.github/example_powershell.png -------------------------------------------------------------------------------- /.github/example_python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/ExtractBitlockerKeys/bb10be27dae2497f137e55b7898186df81010dc2/.github/example_python.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./.github/banner.png) 2 | 3 |

4 | A system administration or post-exploitation script to automatically extract the bitlocker recovery keys from a domain. 5 |
6 | GitHub release (latest by date) 7 | 8 | YouTube Channel Subscribers 9 |
10 |

11 | 12 | ## Features 13 | 14 | - [x] Automatically gets the list of all computers from the domain controller's LDAP. 15 | - [x] Multithreaded connections to extract Bitlocker keys from LDAP. 16 | - [x] Iterate on LDAP result pages to get every computer of the domain, no matter the size. 17 | 18 | > [!WARNING] 19 | > Please do not store this backup in an online SMB share of the domain. You should prefer to print it and store it physically in a locked safe. 20 | - [x] Export results in JSON with Computer FQDN, Domain, Recovery Key, Volume GUID, Created At and Organizational Units. 21 | - [x] Export results in XLSX with Computer FQDN, Domain, Recovery Key, Volume GUID, Created At and Organizational Units. 22 | - [x] Export results in SQLITE3 with Computer FQDN, Domain, Recovery Key, Volume GUID, Created At and Organizational Units. 23 | 24 | --- 25 | 26 | ## Demonstration from Linux in Python 27 | 28 | To extract Bitlocker recovery keys from all the computers of the domain `domain.local` you can use this command: 29 | 30 | ``` 31 | ./ExtractBitlockerKeys.py -d 'domain.local' -u 'Administrator' -p 'Podalirius123!' --dc-ip 192.168.1.101 32 | ``` 33 | 34 | You will get the following output: 35 | 36 | ![](./.github/example_python.png) 37 | 38 | --- 39 | 40 | ## Demonstration from Windows in Powershell 41 | 42 | To extract Bitlocker recovery keys from all the computers of the domain `domain.local` you can use this command: 43 | 44 | ``` 45 | .\ExtractBitlockerKeys.ps1 -dcip 192.168.1.101 -ExportToCSV ./keys.csv -ExportToJSON ./keys.json 46 | ``` 47 | 48 | You will get the following output: 49 | 50 | ![](./.github/example_powershell.png) 51 | 52 | --- 53 | 54 | ## Usage 55 | 56 | ``` 57 | $ ./ExtractBitlockerKeys.py -h 58 | ExtractBitlockerKeys.py v1.1 - by Remi GASCOU (Podalirius) 59 | 60 | usage: ExtractBitlockerKeys.py [-h] [-v] [-q] [-t THREADS] [--export-xlsx EXPORT_XLSX] [--export-json EXPORT_JSON] [--export-sqlite EXPORT_SQLITE] --dc-ip ip address [-d DOMAIN] [-u USER] 61 | [--no-pass | -p PASSWORD | -H [LMHASH:]NTHASH | --aes-key hex key] [-k] 62 | 63 | options: 64 | -h, --help show this help message and exit 65 | -v, --verbose Verbose mode. (default: False) 66 | -q, --quiet Show no information at all. 67 | -t THREADS, --threads THREADS 68 | Number of threads (default: 4). 69 | 70 | Output files: 71 | --export-xlsx EXPORT_XLSX 72 | Output XLSX file to store the results in. 73 | --export-json EXPORT_JSON 74 | Output JSON file to store the results in. 75 | --export-sqlite EXPORT_SQLITE 76 | Output SQLITE3 file to store the results in. 77 | 78 | Authentication & connection: 79 | --dc-ip ip address IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter 80 | -d DOMAIN, --domain DOMAIN 81 | (FQDN) domain to authenticate to 82 | -u USER, --user USER user to authenticate with 83 | 84 | Credentials: 85 | --no-pass Don't ask for password (useful for -k) 86 | -p PASSWORD, --password PASSWORD 87 | Password to authenticate with 88 | -H [LMHASH:]NTHASH, --hashes [LMHASH:]NTHASH 89 | NT/LM hashes, format is LMhash:NThash 90 | --aes-key hex key AES key to use for Kerberos Authentication (128 or 256 bits) 91 | -k, --kerberos Use Kerberos authentication. Grabs credentials from .ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line. 92 | ``` 93 | 94 | ## Contributing 95 | 96 | Pull requests are welcome. Feel free to open an issue if you want to add other features. 97 | 98 | ## References 99 | 100 | - [https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-keypackage](https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-keypackage?wt.mc_id=SEC-MVP-5005286) 101 | - [https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoveryguid](https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoveryguid?wt.mc_id=SEC-MVP-5005286) 102 | - [https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoverypassword](https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoverypassword?wt.mc_id=SEC-MVP-5005286) 103 | - [https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-volumeguid](https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-volumeguid?wt.mc_id=SEC-MVP-5005286) 104 | -------------------------------------------------------------------------------- /go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | 3 | RUN apt-get -y -q update \ 4 | && apt-get -y -q install nano git wget build-essential librust-gobject-sys-dev libnss3 libnss3-dev 5 | 6 | RUN wget https://go.dev/dl/go1.22.1.linux-amd64.tar.gz -O /tmp/go.tar.gz \ 7 | && rm -rf /usr/local/go \ 8 | && tar -C /usr/local -xzf /tmp/go.tar.gz \ 9 | && echo 'export PATH=$PATH:/usr/local/go/bin' >> /root/.bashrc \ 10 | && echo 'export PATH=$PATH:/root/go/bin' >> /root/.bashrc 11 | 12 | RUN echo "go clean; go build -v" >> /root/.bash_history 13 | 14 | RUN echo '#!/bin/bash' > /entrypoint.sh \ 15 | && echo 'mkdir -p /workspace/bin/' >> /entrypoint.sh \ 16 | && echo 'cd /workspace/src/' >> /entrypoint.sh \ 17 | && echo '/usr/local/go/bin/go clean' >> /entrypoint.sh \ 18 | && echo 'echo "[+] Building"' >> /entrypoint.sh \ 19 | && echo 'echo " ├──[>] Building for linux i386"' >> /entrypoint.sh \ 20 | && echo 'mkdir -p /workspace/bin/linux/x86/' >> /entrypoint.sh >> /entrypoint.sh \ 21 | && echo 'GOOS=linux GOARCH=386 /usr/local/go/bin/go build -o /workspace/bin/linux/x86/ ExtractBitlockerKeys.go' >> /entrypoint.sh \ 22 | && echo 'echo " ├──[>] Building for linux amd64"' >> /entrypoint.sh \ 23 | && echo 'mkdir -p /workspace/bin/linux/x64/' >> /entrypoint.sh >> /entrypoint.sh \ 24 | && echo 'GOOS=linux GOARCH=amd64 /usr/local/go/bin/go build -o /workspace/bin/linux/x64/ ExtractBitlockerKeys.go' >> /entrypoint.sh \ 25 | && echo 'echo " ├──[>] Building for Windows i386"' >> /entrypoint.sh \ 26 | && echo 'mkdir -p /workspace/bin/windows/x86/' >> /entrypoint.sh >> /entrypoint.sh \ 27 | && echo 'GOOS=windows GOARCH=386 /usr/local/go/bin/go build -o /workspace/bin/windows/x86/ ExtractBitlockerKeys.go' >> /entrypoint.sh \ 28 | && echo 'echo " └──[>] Building for Windows amd64"' >> /entrypoint.sh \ 29 | && echo 'mkdir -p /workspace/bin/windows/x64/' >> /entrypoint.sh >> /entrypoint.sh \ 30 | && echo 'GOOS=windows GOARCH=amd64 /usr/local/go/bin/go build -o /workspace/bin/windows/x64/ ExtractBitlockerKeys.go' >> /entrypoint.sh \ 31 | && chmod +x /entrypoint.sh 32 | 33 | # Prepare workspace volume 34 | RUN mkdir -p /workspace/ 35 | VOLUME /workspace/ 36 | WORKDIR /workspace/ 37 | 38 | CMD ["/bin/bash", "/entrypoint.sh"] 39 | -------------------------------------------------------------------------------- /go/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build_docker compile 2 | 3 | IMGNAME := build_go 4 | 5 | all: build_docker compile 6 | 7 | build_docker: 8 | docker build -t $(IMGNAME):latest -f Dockerfile . 9 | 10 | compile: build_docker 11 | docker run --rm -v "$(shell pwd):/workspace" -it $(IMGNAME) 12 | -------------------------------------------------------------------------------- /go/src/ExtractBitlockerKeys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/go-ldap/ldap/v3" 12 | "github.com/xuri/excelize/v2" 13 | ) 14 | 15 | func banner() { 16 | fmt.Printf("ExtractBitlockerKeys v%s - by Remi GASCOU (Podalirius)\n", "1.3") 17 | fmt.Println("") 18 | } 19 | 20 | func ldap_init_connection(host string, port int, username string, domain string, password string) (*ldap.Conn, error) { 21 | // Check if TCP port is valid 22 | if port < 1 || port > 65535 { 23 | fmt.Println("[!] Invalid port number. Port must be in the range 1-65535.") 24 | return nil, errors.New("invalid port number") 25 | } 26 | 27 | // Set up LDAP connection 28 | ldapSession, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) 29 | if err != nil { 30 | fmt.Println("[!] Error connecting to LDAP server:", err) 31 | return nil, nil 32 | } 33 | 34 | // Bind with credentials if provided 35 | bindDN := "" 36 | if username != "" { 37 | bindDN = fmt.Sprintf("%s@%s", username, domain) 38 | } 39 | if bindDN != "" && password != "" { 40 | err = ldapSession.Bind(bindDN, password) 41 | if err != nil { 42 | fmt.Println("[!] Error binding:", err) 43 | return nil, nil 44 | } 45 | } 46 | 47 | return ldapSession, nil 48 | } 49 | 50 | func ldap_get_rootdse(ldapSession *ldap.Conn) *ldap.Entry { 51 | // Specify LDAP search parameters 52 | // https://pkg.go.dev/gopkg.in/ldap.v3#NewSearchRequest 53 | searchRequest := ldap.NewSearchRequest( 54 | // Base DN blank 55 | "", 56 | // Scope Base 57 | ldap.ScopeBaseObject, 58 | // DerefAliases 59 | ldap.NeverDerefAliases, 60 | // SizeLimit 61 | 1, 62 | // TimeLimit 63 | 0, 64 | // TypesOnly 65 | false, 66 | // Search filter 67 | "(objectClass=*)", 68 | // Attributes to retrieve 69 | []string{"*"}, 70 | // Controls 71 | nil, 72 | ) 73 | 74 | // Perform LDAP search 75 | searchResult, err := ldapSession.Search(searchRequest) 76 | if err != nil { 77 | fmt.Println("[!] Error searching LDAP:", err) 78 | return nil 79 | } 80 | 81 | return searchResult.Entries[0] 82 | } 83 | 84 | func getDomainFromDistinguishedName(distinguishedName string) string { 85 | domain := "" 86 | if strings.Contains(strings.ToLower(distinguishedName), "dc=") { 87 | dnParts := strings.Split(strings.ToLower(distinguishedName), ",") 88 | for _, part := range dnParts { 89 | if strings.HasPrefix(part, "dc=") { 90 | dcValue := strings.SplitN(part, "=", 2)[1] 91 | if domain == "" { 92 | domain = dcValue 93 | } else { 94 | domain = domain + "." + dcValue 95 | } 96 | } 97 | } 98 | } 99 | return domain 100 | } 101 | 102 | func getOUPathFromDistinguishedName(distinguishedName string) string { 103 | ouPath := "" 104 | if strings.Contains(strings.ToLower(distinguishedName), "ou=") { 105 | dnParts := strings.Split(strings.ToLower(distinguishedName), ",") 106 | // Reverse dnParts slice 107 | for i, j := 0, len(dnParts)-1; i < j; i, j = i+1, j-1 { 108 | dnParts[i], dnParts[j] = dnParts[j], dnParts[i] 109 | } 110 | 111 | // Skip domain 112 | for len(dnParts) > 0 && strings.HasPrefix(dnParts[0], "dc=") { 113 | dnParts = dnParts[1:] 114 | } 115 | 116 | for len(dnParts) > 0 && strings.HasPrefix(dnParts[0], "ou=") { 117 | ouValue := strings.SplitN(dnParts[0], "=", 2)[1] 118 | if ouPath == "" { 119 | ouPath = ouValue 120 | } else { 121 | ouPath = ouPath + " --> " + ouValue 122 | } 123 | dnParts = dnParts[1:] 124 | } 125 | } 126 | return ouPath 127 | } 128 | 129 | func parseFVE(distinguishedName string, ldapEntry *ldap.Entry) map[string]string { 130 | entry := make(map[string]string) 131 | entry["distinguishedName"] = distinguishedName 132 | entry["domain"] = getDomainFromDistinguishedName(distinguishedName) 133 | entry["organizationalUnits"] = getOUPathFromDistinguishedName(distinguishedName) 134 | entry["createdAt"] = "" 135 | entry["volumeGuid"] = "" 136 | 137 | // Parse CN of key 138 | re := regexp.MustCompile(`^(CN=)([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2})({[0-9A-F\-]+}),`) 139 | matched := re.FindStringSubmatch(distinguishedName) 140 | if matched != nil { 141 | createdAt, guid := matched[2], matched[3] 142 | entry["createdAt"] = createdAt 143 | entry["volumeGuid"] = strings.Trim(guid, "{}") 144 | } 145 | 146 | // Parse computer name 147 | entry["computerName"] = "" 148 | if strings.Contains(distinguishedName, ",") { 149 | splitDN := strings.Split(distinguishedName, ",") 150 | if strings.ToUpper(splitDN[1][:3]) == "CN=" { 151 | entry["computerName"] = strings.SplitN(splitDN[1], "=", 2)[1] 152 | } 153 | } 154 | 155 | // Add recovery key 156 | entry["recoveryKey"] = ldapEntry.GetAttributeValue("msFVE-RecoveryPassword") 157 | 158 | return entry 159 | } 160 | 161 | 162 | var ( 163 | useLdaps bool 164 | quiet bool 165 | debug bool 166 | ldapHost string 167 | ldapPort int 168 | authDomain string 169 | authUsername string 170 | // noPass bool 171 | authPassword string 172 | authHashes string 173 | // authKey string 174 | // useKerberos bool 175 | xlsx string 176 | ) 177 | 178 | func parseArgs() { 179 | flag.BoolVar(&useLdaps, "use-ldaps", false, "Use LDAPS instead of LDAP.") 180 | flag.BoolVar(&quiet, "quiet", false, "Show no information at all.") 181 | flag.BoolVar(&debug, "debug", false, "Debug mode") 182 | 183 | flag.StringVar(&ldapHost, "host", "", "IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter.") 184 | flag.IntVar(&ldapPort, "port", 0, "Port number to connect to LDAP server.") 185 | 186 | flag.StringVar(&authDomain, "domain", "", "(FQDN) domain to authenticate to.") 187 | flag.StringVar(&authUsername, "username", "", "User to authenticate as.") 188 | //flag.BoolVar(&noPass, "no-pass", false, "don't ask for password (useful for -k)") 189 | flag.StringVar(&authPassword, "password", "", "password to authenticate with.") 190 | flag.StringVar(&authHashes, "hashes", "", "NT/LM hashes, format is LMhash:NThash.") 191 | //flag.StringVar(&authKey, "aes-key", "", "AES key to use for Kerberos Authentication (128 or 256 bits)") 192 | //flag.BoolVar(&useKerberos, "k", false, "Use Kerberos authentication. Grabs credentials from .ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line") 193 | 194 | flag.StringVar(&xlsx, "xlsx", "", "Output results in a XLSX Excel file.") 195 | 196 | flag.Parse() 197 | 198 | if ldapHost == "" { 199 | fmt.Println("[!] Option -host is required.") 200 | flag.Usage() 201 | os.Exit(1) 202 | } 203 | 204 | if ldapPort == 0 { 205 | if useLdaps { 206 | ldapPort = 636 207 | } else { 208 | ldapPort = 389 209 | } 210 | } 211 | } 212 | 213 | func main() { 214 | banner() 215 | parseArgs() 216 | 217 | if debug { 218 | if !useLdaps { 219 | fmt.Printf("[debug] Connecting to remote ldap://%s:%d ...\n", ldapHost, ldapPort) 220 | } else { 221 | fmt.Printf("[debug] Connecting to remote ldaps://%s:%d ...\n", ldapHost, ldapPort) 222 | } 223 | } 224 | 225 | // Init the LDAP connection 226 | ldapSession, err := ldap_init_connection(ldapHost, ldapPort, authUsername, authDomain, authPassword) 227 | if err != nil { 228 | fmt.Println("[!] Error searching LDAP:", err) 229 | return 230 | } 231 | 232 | rootDSE := ldap_get_rootdse(ldapSession) 233 | if debug { 234 | fmt.Printf("[debug] Using defaultNamingContext %s ...\n", rootDSE.GetAttributeValue("defaultNamingContext")) 235 | } 236 | 237 | // Specify LDAP search parameters 238 | // https://pkg.go.dev/gopkg.in/ldap.v3#NewSearchRequest 239 | searchRequest := ldap.NewSearchRequest( 240 | // Base DN 241 | rootDSE.GetAttributeValue("defaultNamingContext"), 242 | // Scope 243 | ldap.ScopeWholeSubtree, 244 | // DerefAliases 245 | ldap.NeverDerefAliases, 246 | // SizeLimit 247 | 0, 248 | // TimeLimit 249 | 0, 250 | // TypesOnly 251 | false, 252 | // Search filter 253 | "(objectClass=msFVE-RecoveryInformation)", 254 | // Attributes to retrieve 255 | []string{ 256 | "msFVE-KeyPackage", // https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-keypackage 257 | "msFVE-RecoveryGuid", // https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoveryguid 258 | "msFVE-RecoveryPassword", // https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoverypassword 259 | "msFVE-VolumeGuid", // https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-volumeguid 260 | "distinguishedName", 261 | }, 262 | // Controls 263 | nil, 264 | ) 265 | 266 | // Perform LDAP search 267 | fmt.Println("[+] Extracting LAPS passwords of all computers ... ") 268 | searchResult, err := ldapSession.Search(searchRequest) 269 | if err != nil { 270 | fmt.Println("[!] Error searching LDAP:", err) 271 | return 272 | } 273 | 274 | // Print search results 275 | var resultsList []map[string]string 276 | for _, entry := range searchResult.Entries { 277 | result := parseFVE(entry.GetAttributeValue("distinguishedName"), entry) 278 | resultsList = append(resultsList, result) 279 | } 280 | fmt.Printf("[+] Total BitLocker recovery keys found: %d\n", len(resultsList)) 281 | 282 | // Export BitLocker Recovery Keys to an Excel 283 | if xlsx != "" { 284 | f := excelize.NewFile() 285 | // Create a new sheet. 286 | index, err := f.NewSheet("Sheet1") 287 | // Set value of a cell. 288 | f.SetCellValue("Sheet1", "A1", "Domain") 289 | f.SetCellValue("Sheet1", "B1", "Computer Name") 290 | f.SetCellValue("Sheet1", "C1", "BitLocker Recovery Key") 291 | for i, result := range resultsList { 292 | f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), result["domain"]) 293 | f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), result["computerName"]) 294 | f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), result["recoveryKey"]) 295 | } 296 | // Set active sheet of the workbook. 297 | f.SetActiveSheet(index) 298 | // Save xlsx file by the given path. 299 | if err := f.SaveAs(xlsx); err != nil { 300 | fmt.Println(err) 301 | } 302 | fmt.Printf("[+] Exported BitLocker recovery keys to: %s\n", xlsx) 303 | } else { 304 | // Print the keys in the console 305 | for _, result := range resultsList { 306 | fmt.Printf("| %-20s | %-20s | %s |\n", result["domain"], result["computerName"], result["recoveryKey"]) 307 | } 308 | } 309 | 310 | fmt.Println("[+] All done!") 311 | } 312 | -------------------------------------------------------------------------------- /go/src/go.mod: -------------------------------------------------------------------------------- 1 | module ExtractBitlockerKeys 2 | 3 | go 1.22.1 4 | 5 | require github.com/go-ldap/ldap/v3 v3.4.6 6 | 7 | require ( 8 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 9 | github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect 10 | github.com/google/uuid v1.3.1 // indirect 11 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 12 | github.com/richardlehane/mscfb v1.0.4 // indirect 13 | github.com/richardlehane/msoleps v1.0.3 // indirect 14 | github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect 15 | github.com/xuri/excelize/v2 v2.8.1 // indirect 16 | github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect 17 | golang.org/x/crypto v0.19.0 // indirect 18 | golang.org/x/net v0.21.0 // indirect 19 | golang.org/x/text v0.14.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go/src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 2 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 3 | github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= 7 | github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 8 | github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= 9 | github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= 10 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 11 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 13 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= 16 | github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= 17 | github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 18 | github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= 19 | github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 22 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0= 25 | github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= 26 | github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ= 27 | github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE= 28 | github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= 29 | github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= 30 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 31 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 32 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 33 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= 34 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 35 | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= 36 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 37 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 38 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 39 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 40 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 41 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 42 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 43 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 44 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 45 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 46 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 58 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 59 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 60 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 61 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 62 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 63 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 64 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 65 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 66 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 67 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 68 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 69 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 70 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 71 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 72 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 73 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 74 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 75 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 76 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 77 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 78 | -------------------------------------------------------------------------------- /powershell/ExtractBitlockerKeys.ps1: -------------------------------------------------------------------------------- 1 | # File name : ExtractBitLockerKeys.ps1 2 | # Author : Podalirius (@podalirius_) 3 | # Date created : 21 September 2023 4 | 5 | Param ( 6 | [parameter(Mandatory=$true)][string]$dcip = $null, 7 | [parameter(Mandatory=$false,ParameterSetName="Credentials")][System.Management.Automation.PSCredential]$Credentials, 8 | [parameter(Mandatory=$false,ParameterSetName="Credentials")][Switch]$UseCredentials, 9 | [parameter(Mandatory=$false)][string]$LogFile = $null, 10 | [parameter(Mandatory=$false)][switch]$Quiet, 11 | [parameter(Mandatory=$false)][string]$ExportToCSV = $null, 12 | [parameter(Mandatory=$false)][string]$ExportToJSON = $null, 13 | [parameter(Mandatory=$false)][int]$PageSize = 5000, 14 | [parameter(Mandatory=$false)][string]$SearchBase = $null, 15 | [parameter(Mandatory=$false)][switch]$LDAPS, 16 | [parameter(Mandatory=$false)][switch]$Help 17 | ) 18 | 19 | If ($Help) { 20 | Write-Host "[+]========================================================" 21 | Write-Host "[+] Powershell ExtractBitLockerKeys v1.3 @podalirius_ " 22 | Write-Host "[+]========================================================" 23 | Write-Host "" 24 | 25 | Write-Host "Required arguments:" 26 | Write-Host " -dcip : LDAP host to target, most likely the domain controller." 27 | Write-Host "" 28 | Write-Host "Optional arguments:" 29 | Write-Host " -Help : Displays this help message" 30 | Write-Host " -Quiet : Do not print keys, only export them." 31 | Write-Host " -UseCredentials : Flag for asking for credentials to authentication" 32 | Write-Host " -Credentials : Providing PSCredentialObject for authentication" 33 | Write-Host " -PageSize : Sets the LDAP page size to use in queries (default: 5000)." 34 | Write-Host " -LDAPS : Use LDAPS instead of LDAP." 35 | Write-Host " -LogFile : Log file to save output to." 36 | Write-Host " -ExportToCSV : Export Bitlocker Keys in a CSV file." 37 | Write-Host " -ExportToJSON : Export Bitlocker Keys in a JSON file." 38 | exit 0 39 | } 40 | 41 | If ($LogFile.Length -ne 0) { 42 | # Init log file 43 | $Stream = [System.IO.StreamWriter]::new($LogFile) 44 | $Stream.Close() 45 | } 46 | 47 | if($UseCredentials -and ([string]::IsNullOrEmpty($Credentials))){ 48 | $Credentials = Get-Credential 49 | } 50 | 51 | 52 | 53 | Function Write-Logger { 54 | [CmdletBinding()] 55 | [OutputType([Nullable])] 56 | Param 57 | ( 58 | [Parameter(Mandatory=$true)] $Logfile, 59 | [Parameter(Mandatory=$true)] $Message 60 | ) 61 | Begin 62 | { 63 | Write-Host $Message 64 | If ($LogFile.Length -ne 0) { 65 | $Stream = [System.IO.StreamWriter]::new($LogFile, $true) 66 | $Stream.WriteLine($Message) 67 | $Stream.Close() 68 | } 69 | } 70 | } 71 | 72 | Function Get-ComputerNameFromLDAPDN { 73 | [CmdletBinding()] 74 | [OutputType([Nullable])] 75 | param ([string]$ldapDN) 76 | 77 | Begin 78 | { 79 | $ldapDNParts = $ldapDN -split ',' 80 | if ($ldapDNParts[1] -match '^CN=([^,]+)$') { 81 | return $matches[1] 82 | } else { 83 | return "" 84 | } 85 | } 86 | } 87 | 88 | Function Get-DomainFromLDAPDN { 89 | [CmdletBinding()] 90 | [OutputType([Nullable])] 91 | param ([string]$ldapDN) 92 | 93 | Begin 94 | { 95 | $domain = "" 96 | 97 | [System.Collections.ArrayList]$ldapDNParts = @(); 98 | foreach ($part in ($ldapDN -split ',')) { $ldapDNParts.Add($part) | Out-Null } 99 | $ldapDNParts.Reverse() | Out-Null 100 | 101 | foreach ($part in $ldapDNParts) { 102 | if ($part -match '^DC=([^,]+)$') { 103 | $domain = $matches[1] + "." + $domain 104 | } else { 105 | # Check if the domain ends with a period 106 | if ($domain.EndsWith('.')) { 107 | # Remove the trailing period 108 | $domain = $domain.TrimEnd('.') 109 | } 110 | 111 | return $domain 112 | } 113 | } 114 | return $domain 115 | } 116 | } 117 | 118 | 119 | Function Get-VolumeGuidFromLDAPDN { 120 | [CmdletBinding()] 121 | [OutputType([Nullable])] 122 | param ([string]$ldapDN) 123 | 124 | Begin 125 | { 126 | $ldapDNParts = $ldapDN -split '/' 127 | $ldapDNParts = $ldapDNParts[3] -split ',' 128 | if ($ldapDNParts[0] -match '^CN=([^,]+){([^,]+)}$') { 129 | return $matches[2] 130 | } else { 131 | return "" 132 | } 133 | } 134 | } 135 | 136 | 137 | Function Get-CreatedAtFromLDAPDN { 138 | [CmdletBinding()] 139 | [OutputType([Nullable])] 140 | param ([string]$ldapDN) 141 | 142 | Begin 143 | { 144 | $ldapDNParts = $ldapDN -split '/' 145 | $ldapDNParts = $ldapDNParts[3] -split ',' 146 | if ($ldapDNParts[0] -match '^CN=([^,]+)$') { 147 | $createdAt = ($matches[1] -split '{') 148 | return $createdAt[0] 149 | } else { 150 | return "" 151 | } 152 | } 153 | } 154 | 155 | 156 | Function Invoke-LDAPQuery { 157 | [CmdletBinding()] 158 | [OutputType([Nullable])] 159 | Param 160 | ( 161 | [Parameter(Mandatory=$true)] $connectionString, 162 | [parameter(Mandatory=$false,ParameterSetName="Credentials")][System.Management.Automation.PSCredential] $Credentials, 163 | [Parameter(Mandatory=$false)] $PageSize 164 | ) 165 | Begin 166 | { 167 | $rootDSE = New-Object System.DirectoryServices.DirectoryEntry("{0}/RootDSE" -f $connectionString); 168 | $defaultNamingContext = $rootDSE.Properties["defaultNamingContext"].ToString(); 169 | Write-Logger -Logfile $Logfile -Message "[+] Authentication successful!"; 170 | Write-Logger -Logfile $Logfile -Message "[+] Targeting defaultNamingContext: $defaultNamingContext"; 171 | $ldapSearcher = New-Object System.DirectoryServices.DirectorySearcher 172 | if ($Credentials.UserName) { 173 | # Connect to Domain with credentials 174 | Write-Logger -Logfile $Logfile -Message ("[+] Connecting to {0}/{1} with specified account" -f $connectionString, $defaultNamingContext) 175 | $ldapSearcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry(("{0}/{1}" -f $connectionString, $defaultNamingContext), $Credentials.UserName, $($Credentials.Password | ConvertFrom-Securestring -AsPlaintext)) 176 | } else { 177 | # Connect to Domain with current session 178 | Write-Logger -Logfile $Logfile -Message ("[+] Connecting to {0}/{1} using current session" -f $connectionString, $defaultNamingContext) 179 | $ldapSearcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry(("{0}/{1}" -f $connectionString, $defaultNamingContext)) 180 | } 181 | $ldapSearcher.SearchScope = "Subtree" 182 | if ($PageSize) { 183 | $ldapSearcher.PageSize = $PageSize 184 | } else { 185 | Write-Logger -Logfile $Logfile -Message "[+] Setting PageSize to $PageSize"; 186 | $ldapSearcher.PageSize = 5000 187 | } 188 | 189 | Write-Logger -Logfile $Logfile -Message "[+] Extracting BitLocker recovery keys ..."; 190 | $ldapSearcher.Filter = "(objectClass=msFVE-RecoveryInformation)" 191 | $ldapSearcher.PropertiesToLoad.Add("msFVE-KeyPackage") | Out-Null ; # https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-keypackage 192 | $ldapSearcher.PropertiesToLoad.Add("msFVE-RecoveryGuid") | Out-Null ; # https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoveryguid 193 | $ldapSearcher.PropertiesToLoad.Add("msFVE-RecoveryPassword") | Out-Null ; # https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoverypassword 194 | $ldapSearcher.PropertiesToLoad.Add("msFVE-VolumeGuid") | Out-Null ; # https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-volumeguid 195 | $results = [ordered]@{}; 196 | Foreach ($item in $ldapSearcher.FindAll()) { 197 | if (!($results.Keys -contains $item.Path)) { 198 | $results[$item.Path] = $item.Properties; 199 | } else { 200 | Write-Logger -Logfile $Logfile -Message "[debug] key already exists: $key (this shouldn't be possible)" 201 | } 202 | } 203 | 204 | $bitlocker_keys = Foreach ($distinguishedName in $results.Keys) { 205 | Foreach ($recoveryKey in $results[$distinguishedName]["msFVE-RecoveryPassword"]) { 206 | $domainName = (Get-DomainFromLDAPDN $distinguishedName) 207 | $createdAt = (Get-CreatedAtFromLDAPDN $distinguishedName) 208 | $volumeGuid = (Get-VolumeGuidFromLDAPDN $distinguishedName) 209 | $computerName = (Get-ComputerNameFromLDAPDN $distinguishedName) 210 | [PSCustomObject]@{ 211 | domainName = $domainName 212 | computerName = $computerName 213 | recoveryKey = $recoveryKey 214 | volumeGuid = $volumeGuid 215 | createdAt = $createdAt 216 | distinguishedName = $distinguishedName 217 | } 218 | } 219 | } 220 | return $bitlocker_keys 221 | } 222 | } 223 | 224 | #=============================================================================== 225 | 226 | Write-Logger -Logfile $Logfile -Message "[+]========================================================" 227 | Write-Logger -Logfile $Logfile -Message "[+] Powershell ExtractBitLockerKeys v1.3 @podalirius_ " 228 | Write-Logger -Logfile $Logfile -Message "[+]========================================================" 229 | Write-Logger -Logfile $Logfile -Message "" 230 | 231 | # Handle LDAPS connection 232 | $connectionString = "LDAP://{0}:{1}"; 233 | If ($LDAPS) { 234 | $connectionString = ($connectionString -f $dcip, "636"); 235 | } else { 236 | $connectionString = ($connectionString -f $dcip, "389"); 237 | } 238 | Write-Verbose "Using connectionString: $connectionString" 239 | 240 | # Connect to LDAP 241 | try { 242 | $bitlocker_keys = Invoke-LDAPQuery -connectionString $connectionString -Credentials $Credentials -PageSize $PageSize 243 | 244 | If (!($Quiet)) { 245 | Foreach ($entry in $bitlocker_keys) { 246 | $domainName = $entry.domainName.PadRight(20," ") 247 | $computerName = $entry.computerName.PadRight(20," ") 248 | $recoveryKey = $entry.recoveryKey.PadRight(20," ") 249 | $createdAt = $entry.createdAt 250 | Write-Logger -Logfile $Logfile -Message ("| {0} | {1} | {2} | {3} " -f $domainName, $computerName, $recoveryKey, $createdAt) 251 | } 252 | } 253 | Write-Logger -Logfile $Logfile -Message ("[>] Extracted {0} BitLocker recovery keys!" -f $bitlocker_keys.Length) 254 | If ($ExportToCSV) { 255 | Write-Logger -Logfile $Logfile -Message "[>] Exporting Bitlocker recovery keys in CSV in $ExportToCSV ..." 256 | $bitlocker_keys | Export-CSV -Path $ExportToCSV -NoTypeInformation 257 | } 258 | If ($ExportToJSON) { 259 | Write-Logger -Logfile $Logfile -Message "[>] Exporting Bitlocker recovery keys in JSON in $ExportToJSON ..." 260 | $bitlocker_keys | ConvertTo-Json -Depth 100 -Compress | Out-File -FilePath $ExportToJSON 261 | } 262 | 263 | } catch { 264 | Write-Verbose $_.Exception 265 | Write-Logger -Logfile $Logfile -Message -Logfile $Logfile -Message ("[!] (0x{0:X8}) {1}" -f $_.Exception.HResult, $_.Exception.InnerException.Message) 266 | exit -1 267 | } -------------------------------------------------------------------------------- /python/ExtractBitlockerKeys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : ExtractBitlockerKeys.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 19 Sep 2023 6 | 7 | 8 | import argparse 9 | from sectools.windows.ldap import raw_ldap_query, init_ldap_session 10 | from sectools.windows.crypto import nt_hash, parse_lm_nt_hashes 11 | import os 12 | import sys 13 | import sqlite3 14 | import json 15 | import xlsxwriter 16 | import re 17 | 18 | 19 | VERSION = "1.3" 20 | 21 | 22 | def export_json(options, results): 23 | print("[>] Exporting results to %s ... " % options.export_json, end="") 24 | sys.stdout.flush() 25 | basepath = os.path.dirname(options.export_json) 26 | filename = os.path.basename(options.export_json) 27 | if basepath not in [".", ""]: 28 | if not os.path.exists(basepath): 29 | os.makedirs(basepath) 30 | path_to_file = basepath + os.path.sep + filename 31 | else: 32 | path_to_file = filename 33 | f = open(path_to_file, "w") 34 | f.write(json.dumps(results, indent=4) + "\n") 35 | f.close() 36 | print("done.") 37 | 38 | 39 | def export_xlsx(options, results): 40 | print("[>] Exporting results to %s ... " % options.export_xlsx, end="") 41 | sys.stdout.flush() 42 | basepath = os.path.dirname(options.export_xlsx) 43 | filename = os.path.basename(options.export_xlsx) 44 | if basepath not in [".", ""]: 45 | if not os.path.exists(basepath): 46 | os.makedirs(basepath) 47 | path_to_file = basepath + os.path.sep + filename 48 | else: 49 | path_to_file = filename 50 | workbook = xlsxwriter.Workbook(path_to_file) 51 | worksheet = workbook.add_worksheet() 52 | 53 | header_format = workbook.add_format({'bold': 1}) 54 | header_fields = ["Computer FQDN", "Domain", "Recovery Key", "Volume GUID", "Created At", "Organizational Units"] 55 | for k in range(len(header_fields)): 56 | worksheet.set_column(k, k + 1, len(header_fields[k]) + 3) 57 | worksheet.set_row(0, 20, header_format) 58 | worksheet.write_row(0, 0, header_fields) 59 | 60 | row_id = 1 61 | for computerfqdn in results.keys(): 62 | data = [ 63 | computerfqdn, 64 | results[computerfqdn]["domain"], 65 | results[computerfqdn]["recoveryKey"], 66 | results[computerfqdn]["volumeGuid"], 67 | results[computerfqdn]["createdAt"], 68 | results[computerfqdn]["organizationalUnits"], 69 | ] 70 | worksheet.write_row(row_id, 0, data) 71 | row_id += 1 72 | worksheet.autofilter(0, 0, row_id, len(header_fields) - 1) 73 | workbook.close() 74 | print("done.") 75 | 76 | 77 | def export_sqlite(options, results): 78 | print("[>] Exporting results to %s ... " % options.export_sqlite, end="") 79 | sys.stdout.flush() 80 | basepath = os.path.dirname(options.export_sqlite) 81 | filename = os.path.basename(options.export_sqlite) 82 | if basepath not in [".", ""]: 83 | if not os.path.exists(basepath): 84 | os.makedirs(basepath) 85 | path_to_file = basepath + os.path.sep + filename 86 | else: 87 | path_to_file = filename 88 | 89 | conn = sqlite3.connect(path_to_file) 90 | cursor = conn.cursor() 91 | cursor.execute("CREATE TABLE IF NOT EXISTS bitlocker_keys(fqdn VARCHAR(255), domain VARCHAR(255), recoveryKey VARCHAR(255), volumeGuid VARCHAR(255), createdAt VARCHAR(255), organizationalUnits VARCHAR(1024));") 92 | for computerfqdn in results.keys(): 93 | cursor.execute("INSERT INTO shares VALUES (?, ?, ?, ?, ?, ?)", ( 94 | computerfqdn, 95 | results[computerfqdn][0]["domain"], 96 | results[computerfqdn][0]["recoveryKey"], 97 | results[computerfqdn][0]["volumeGuid"], 98 | results[computerfqdn][0]["createdAt"], 99 | results[computerfqdn][0]["organizationalUnits"], 100 | ) 101 | ) 102 | conn.commit() 103 | conn.close() 104 | print("done.") 105 | 106 | 107 | def get_domain_from_distinguished_name(distinguishedName): 108 | domain = None 109 | if "dc=" in distinguishedName.lower(): 110 | distinguishedName = distinguishedName.lower().split(',')[::-1] 111 | 112 | while distinguishedName[0].startswith("dc="): 113 | if domain is None: 114 | domain = distinguishedName[0].split('=',1)[1] 115 | else: 116 | domain = distinguishedName[0].split('=', 1)[1] + "." + domain 117 | distinguishedName = distinguishedName[1:] 118 | 119 | return domain 120 | 121 | 122 | def get_ou_path_from_distinguished_name(distinguishedName): 123 | ou_path = None 124 | if "ou=" in distinguishedName.lower(): 125 | distinguishedName = distinguishedName.lower().split(',')[::-1] 126 | 127 | # Skip domain 128 | while distinguishedName[0].startswith("dc="): 129 | distinguishedName = distinguishedName[1:] 130 | 131 | while distinguishedName[0].startswith("ou="): 132 | if ou_path is None: 133 | ou_path = distinguishedName[0].split('=',1)[1] 134 | else: 135 | ou_path = ou_path + " --> " + distinguishedName[0].split('=',1)[1] 136 | distinguishedName = distinguishedName[1:] 137 | 138 | return ou_path 139 | else: 140 | return ou_path 141 | 142 | 143 | def parse_fve(distinguishedName, bitlocker_keys): 144 | entry = { 145 | "distinguishedName": distinguishedName, 146 | "domain": get_domain_from_distinguished_name(distinguishedName), 147 | "organizationalUnits": get_ou_path_from_distinguished_name(distinguishedName), 148 | "createdAt": None, 149 | "volumeGuid": None 150 | } 151 | # Parse CN of key 152 | matched = re.match(r"^(CN=)([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]-[0-9][0-9]:[0-9][0-9])({[0-9A-F\-]+}),", distinguishedName, re.IGNORECASE) 153 | if matched is not None: 154 | _, created_at, guid = matched.groups() 155 | entry["createdAt"] = created_at 156 | entry["volumeGuid"] = guid.strip('{}').lower() 157 | # Parse computer name 158 | entry["computerName"] = None 159 | if ',' in distinguishedName: 160 | if distinguishedName.split(',')[1].upper().startswith("CN="): 161 | entry["computerName"] = distinguishedName.split(',')[1].split('=',1)[1] 162 | # Add recovery key 163 | entry["recoveryKey"] = bitlocker_keys["msFVE-RecoveryPassword"] 164 | 165 | return entry 166 | 167 | 168 | def parseArgs(): 169 | print("ExtractBitlockerKeys.py v%s - by Remi GASCOU (Podalirius)\n" % VERSION) 170 | 171 | parser = argparse.ArgumentParser(description="") 172 | 173 | parser.add_argument("-v", "--verbose", default=False, action="store_true", help='Verbose mode. (default: False)') 174 | parser.add_argument("-q", "--quiet", dest="quiet", action="store_true", default=False, help="Show no information at all.") 175 | parser.add_argument("-t", "--threads", dest="threads", action="store", type=int, default=4, required=False, help="Number of threads (default: 4).") 176 | 177 | output = parser.add_argument_group('Output files') 178 | output.add_argument("--export-xlsx", dest="export_xlsx", type=str, default=None, required=False, help="Output XLSX file to store the results in.") 179 | output.add_argument("--export-json", dest="export_json", type=str, default=None, required=False, help="Output JSON file to store the results in.") 180 | output.add_argument("--export-sqlite", dest="export_sqlite", type=str, default=None, required=False, help="Output SQLITE3 file to store the results in.") 181 | 182 | authconn = parser.add_argument_group('Authentication & connection') 183 | authconn.add_argument('--dc-ip', required=True, action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter') 184 | authconn.add_argument('--kdcHost', dest="kdcHost", action='store', metavar="FQDN KDC", help='FQDN of KDC for Kerberos.') 185 | authconn.add_argument("-d", "--domain", dest="auth_domain", metavar="DOMAIN", action="store", default="", help="(FQDN) domain to authenticate to") 186 | authconn.add_argument("-u", "--user", dest="auth_username", metavar="USER", action="store", default="", help="user to authenticate with") 187 | 188 | secret = parser.add_argument_group("Credentials") 189 | cred = secret.add_mutually_exclusive_group() 190 | cred.add_argument("--no-pass", default=False, action="store_true", help="Don't ask for password (useful for -k)") 191 | cred.add_argument("-p", "--password", dest="auth_password", metavar="PASSWORD", action="store", default=None, help="Password to authenticate with") 192 | cred.add_argument("-H", "--hashes", dest="auth_hashes", action="store", metavar="[LMHASH:]NTHASH", help='NT/LM hashes, format is LMhash:NThash') 193 | cred.add_argument("--aes-key", dest="auth_key", action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') 194 | secret.add_argument("-k", "--kerberos", dest="use_kerberos", action="store_true", help='Use Kerberos authentication. Grabs credentials from .ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line') 195 | 196 | return parser.parse_args() 197 | 198 | 199 | if __name__ == '__main__': 200 | options = parseArgs() 201 | if options.auth_hashes is not None: 202 | if ":" not in options.auth_hashes: 203 | options.auth_hashes = ":" + options.auth_hashes 204 | auth_lm_hash, auth_nt_hash = parse_lm_nt_hashes(options.auth_hashes) 205 | 206 | if options.auth_key is not None: 207 | options.use_kerberos = True 208 | 209 | if options.use_kerberos is True and options.kdcHost is None: 210 | print("[!] Specify KDC's Hostname of FQDN using the argument --kdcHost") 211 | exit() 212 | 213 | if not options.quiet: 214 | print("[>] Extracting BitLocker recovery keys of all computers ...") 215 | 216 | computer_keys = raw_ldap_query( 217 | auth_domain=options.auth_domain, 218 | auth_dc_ip=options.dc_ip, 219 | auth_username=options.auth_username, 220 | auth_password=options.auth_password, 221 | auth_hashes=options.auth_hashes, 222 | auth_key=options.auth_key, 223 | query="(objectClass=msFVE-RecoveryInformation)", 224 | attributes=[ 225 | "msFVE-KeyPackage", # https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-keypackage 226 | "msFVE-RecoveryGuid", # https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoveryguid 227 | "msFVE-RecoveryPassword", # https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-recoverypassword 228 | "msFVE-VolumeGuid" # https://learn.microsoft.com/en-us/windows/win32/adschema/a-msfve-volumeguid 229 | ], 230 | use_kerberos=options.use_kerberos, 231 | kdcHost=options.kdcHost 232 | ) 233 | 234 | if not options.quiet: 235 | print("[>] Found %d BitLocker recovery keys!" % len(computer_keys.keys())) 236 | 237 | results = {} 238 | 239 | if len(computer_keys.keys()) != 0: 240 | for dn, fve_entry in computer_keys.items(): 241 | if len(fve_entry.keys()) != 0: 242 | if dn not in results.keys(): 243 | results[dn] = [] 244 | result = parse_fve(dn, fve_entry) 245 | print("| %-20s | %-20s | %s |" % (result["domain"], result["computerName"], result["recoveryKey"])) 246 | results[dn].append(result) 247 | 248 | print("[>] Extracted %d BitLocker recovery keys!" % len(computer_keys.keys())) 249 | 250 | # Export results 251 | if options.export_json is not None: 252 | export_json(options, results) 253 | 254 | if options.export_xlsx is not None: 255 | export_xlsx(options, results) 256 | 257 | if options.export_sqlite is not None: 258 | export_sqlite(options, results) 259 | else: 260 | print("[!] No computers in the domain found matching filter (objectClass=msFVE-RecoveryInformation)") 261 | 262 | 263 | print("[+] Bye Bye!") 264 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sectools>=1.4.1 2 | xlsxwriter 3 | --------------------------------------------------------------------------------