├── .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 | 
2 |
3 |
4 | A system administration or post-exploitation script to automatically extract the bitlocker recovery keys from a domain.
5 |
6 |
7 |
8 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------