Skip to content

Commit 259ad9c

Browse files
authored
Merge pull request #1 from JJK96/pr-1
- Update to the latest Certipy version - Use Certipy for interpreting results and assigning vulnerability classes - Since we now use Certipy for outputting, we might as well make this configurable by copying the output options (text, json, csv) from Certipy. - Allow supplying user SIDs, which is necessary for finding templates where the user can enroll - Allow using BloodHound to resolve SIDs dynamically and for obtaining the SIDs of owned users - Allow parsing output of Outflank C2's reg query command
2 parents fa22f1b + cca2527 commit 259ad9c

9 files changed

Lines changed: 738 additions & 461 deletions

File tree

README.md

Lines changed: 106 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,140 @@
11
# regcertipy
2-
Parses cached certificate templates from a Windows Registry `.reg` file and
3-
displays them in the same style as
4-
[Certipy](https://github.com/ly4k/Certipy) does.
2+
3+
Parses cached certificate templates from a Windows Registry `.reg` file and displays them in the same style as [Certipy](https://github.com/ly4k/Certipy) does.
54

65
## Getting started
7-
We prefer using the [uv package manager](https://docs.astral.sh/uv/), as it
8-
will automatically create a virtual environment for you. Alternatively, you
9-
can use `pip install regcertipy` within any other Python environment that
10-
you manage.
6+
7+
We prefer using the [uv package manager](https://docs.astral.sh/uv/), as it will automatically create a virtual environment for you. Alternatively, you can use `pip install regcertipy` within any other Python environment that you manage.
118

129
```
1310
$ uv venv
1411
$ source .venv/bin/activate
1512
$ uv pip install regcertipy
1613
$ regcertipy -h
17-
usage: regcertipy [-h] regfile
14+
usage: regcertipy [-h] [-s SID_FILE] [-f {.reg,reg_bof}] [-text] [-stdout]
15+
[-json] [-csv] [-output prefix] [--neo4j-user NEO4J_USER]
16+
[--neo4j-pass NEO4J_PASS] [--neo4j-host NEO4J_HOST]
17+
[--neo4j-port NEO4J_PORT] [--use-owned-sids]
18+
regfile
1819
1920
Regfile ingestor for Certipy
2021
2122
positional arguments:
22-
regfile Path to the .reg file.
23+
regfile Path to the .reg file.
2324
2425
options:
25-
-h, --help show this help message and exit
26+
-h, --help show this help message and exit
27+
-s SID_FILE, --sid-file SID_FILE
28+
File containing the user's SIDs
29+
-f {.reg,reg_bof}, --input-format {.reg,reg_bof}
30+
Format of input file
31+
32+
output options:
33+
-text Output result as formatted text file
34+
-stdout Output result as text directly to console
35+
-json Output result as JSON
36+
-csv Output result as CSV
37+
-output prefix Filename prefix for writing results to
38+
39+
BloodHound:
40+
--neo4j-user NEO4J_USER
41+
Username for neo4j
42+
--neo4j-pass NEO4J_PASS
43+
Password for neo4j
44+
--neo4j-host NEO4J_HOST
45+
Host for neo4j
46+
--neo4j-port NEO4J_PORT
47+
Port for neo4j
48+
--use-owned-sids Use the SIDs of all owned principals as the user SIDs
2649
```
2750

28-
Use regedit.exe to export the keys under
29-
`HKEY_USERS\.DEFAULT\Software\ Microsoft\Cryptography\CertificateTemplateCache\`.
30-
Then, the .reg file can be fed into regcertipy with: regcertipy <regfile>.
51+
Use regedit.exe to export the keys under `HKEY_USERS\.DEFAULT\Software\Microsoft\Cryptography\CertificateTemplateCache\`. Then, the .reg file can be fed into regcertipy with: regcertipy <regfile>.
3152

3253
![Example of how to export a .reg file](resources/regedit.png)
3354

55+
Alternatively, it is possible to parse output the Outflank C2 `reg query` command by specifying the `-f reg_bof` flag. This parses the following (truncated) output.
56+
57+
```
58+
[01/01/1970 12:34:56 PM] (finished) Outflank > reg query -r HKEY_USERS\.DEFAULT\Software\Microsoft\Cryptography\CertificateTemplateCache
59+
60+
Reg Key: HKEY_USERS\.DEFAULT\Software\Microsoft\Cryptography\CertificateTemplateCache
61+
62+
Reg Value: TimestampAfter
63+
Reg Type: REG_BINARY
64+
Reg Data: 86F63B1D13E7DB01
65+
66+
Reg Value: Timestamp
67+
Reg Type: REG_BINARY
68+
Reg Data: 86F63B1D13E7DB01
69+
70+
Reg Key: HKEY_USERS\.DEFAULT\Software\Microsoft\Cryptography\CertificateTemplateCache\Administrator
71+
72+
Reg Value: DisplayName
73+
Reg Type: REG_SZ
74+
Reg Data: Administrator
75+
76+
Reg Value: SupportedCSPs
77+
Reg Type: REG_MULTI_SZ
78+
Reg Data: Microsoft Enhanced Cryptographic Provider v1.0 Microsoft Base Cryptographic Provider v1.0
79+
80+
Reg Value: ExtKeyUsageSyntax
81+
Reg Type: REG_MULTI_SZ
82+
Reg Data: 1.3.6.1.4.1.311.10.3.1 1.3.6.1.4.1.311.10.3.4 1.3.6.1.5.5.7.3.4 1.3.6.1.5.5.7.3.2
83+
84+
[...]
85+
```
86+
87+
### SIDs
88+
89+
Because `regcertipy` is intended for offline usage, SIDs cannot be dynamically resolved. Therefore, `regcertipy` includes a couple of options that can be used for offline SID information.
90+
91+
Firstly, the `--sid-file` flag can be used to provide a list of SIDs that the user is a member of. This list can be obtained from BloodHound or other tools.
92+
93+
Secondly, `regcertipy` can use a `neo4j` connection to dynamically resolve SIDs using BloodHound's database. This, combined with the `--use-owned-sids` command can help you find vulnerable templates exploitable by objects marked as owned in BloodHound.
94+
3495
## Development
35-
Note that we use the [Black code formatter](https://black.readthedocs.io/en/stable/)
36-
for code formatting. Moreover, we use the Git Flow branching model, meaning
37-
that we actively develop on the "develop" branch, and merge to the "main"
38-
branch (& tag it) when a new release is made, making the "main" branch the
39-
production branch.
96+
97+
Note that we use the [Black code formatter](https://black.readthedocs.io/en/stable/) for code formatting. Moreover, we use the Git Flow branching model, meaning that we actively develop on the "develop" branch, and merge to the "main" branch (& tag it) when a new release is made, making the "main" branch the production branch.
4098

4199
```
42100
$ uv sync --dev # Also installs the Black code formatter.
43101
$ uv run black . # To format the current code base.
44102
$ uv run regcertipy -h
45-
usage: regcertipy [-h] regfile
103+
usage: regcertipy [-h] [-s SID_FILE] [-f {.reg,reg_bof}] [-text] [-stdout]
104+
[-json] [-csv] [-output prefix] [--neo4j-user NEO4J_USER]
105+
[--neo4j-pass NEO4J_PASS] [--neo4j-host NEO4J_HOST]
106+
[--neo4j-port NEO4J_PORT] [--use-owned-sids]
107+
regfile
46108
47109
Regfile ingestor for Certipy
48110
49111
positional arguments:
50-
regfile Path to the .reg file.
112+
regfile Path to the .reg file.
51113
52114
options:
53-
-h, --help show this help message and exit
115+
-h, --help show this help message and exit
116+
-s SID_FILE, --sid-file SID_FILE
117+
File containing the user's SIDs
118+
-f {.reg,reg_bof}, --input-format {.reg,reg_bof}
119+
Format of input file
120+
121+
output options:
122+
-text Output result as formatted text file
123+
-stdout Output result as text directly to console
124+
-json Output result as JSON
125+
-csv Output result as CSV
126+
-output prefix Filename prefix for writing results to
127+
128+
BloodHound:
129+
--neo4j-user NEO4J_USER
130+
Username for neo4j
131+
--neo4j-pass NEO4J_PASS
132+
Password for neo4j
133+
--neo4j-host NEO4J_HOST
134+
Host for neo4j
135+
--neo4j-port NEO4J_PORT
136+
Port for neo4j
137+
--use-owned-sids Use the SIDs of all owned principals as the user SIDs
54138
```
55139

56-
You can also run the `__init__.py` or `__main.py__` Python file in your
57-
favourite debugger.
140+
You can also run the `__init__.py` or `__main.py__` Python file in your favourite debugger.

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "regcertipy"
3-
version = "1.0.0"
3+
version = "1.1.0"
44
description = """
55
Parses cached certificate templates from a Windows Registry file and \
66
displays them in the same style as Certipy does.
@@ -12,7 +12,8 @@ authors = [
1212
]
1313
requires-python = ">=3.12"
1414
dependencies = [
15-
"certipy-ad==4.8.2",
15+
"certipy-ad==5.0.3",
16+
"neo4j>=5.28.1",
1617
]
1718

1819
[project.scripts]

src/regcertipy/__init__.py

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,64 @@
11
import argparse
22

3-
from certipy.lib.formatting import pretty_print
3+
from certipy.commands.find import Find
44
from regcertipy.models import CertTemplate
5-
from regcertipy.parsers import RegfileParser
5+
from regcertipy.parsers import RegfileParser, RegBofParser
6+
from datetime import datetime
7+
from .utils import sid_to_name
8+
import functools
9+
10+
11+
class MockTarget:
12+
username = None
13+
14+
15+
class MockLDAPConnection:
16+
user_sids = []
17+
18+
def __init__(self, sid_file, neo4j_driver=None, use_owned_sids=False):
19+
self.neo4j_driver = neo4j_driver
20+
if sid_file:
21+
with open(sid_file) as f:
22+
for line in f:
23+
self.user_sids.append(line[:-1])
24+
if use_owned_sids and self.neo4j_driver:
25+
self.get_owned_sids()
26+
27+
def get_user_sids(self, *args, **kwargs):
28+
return self.user_sids
29+
30+
def get_owned_sids(self):
31+
records, _, _ = self.neo4j_driver.execute_query(
32+
"MATCH (u:User)-[:MemberOf*1..]->(g:Group) WHERE COALESCE(u.system_tags, '') CONTAINS 'owned' return g.objectid"
33+
)
34+
for record in records:
35+
self.user_sids.append(record["g.objectid"])
36+
37+
@functools.cache
38+
def lookup_sid(self, sid, **kwargs):
39+
name = sid_to_name(sid)
40+
if name != sid:
41+
return {"name": name}
42+
if self.neo4j_driver:
43+
records, _, _ = self.neo4j_driver.execute_query(
44+
"MATCH (g {objectid:'%s'}) return g.name" % (sid,)
45+
)
46+
if records:
47+
return {"name": records[0]["g.name"]}
48+
return {"name": name}
49+
50+
51+
class MyFind(Find):
52+
def get_template_properties(self, template, template_properties):
53+
template_properties = super().get_template_properties(
54+
template, template_properties
55+
)
56+
for key in ["Template OIDs"]:
57+
template_oids = template.get(key)
58+
if template_oids:
59+
template_properties[key] = template_oids
60+
61+
return template_properties
662

763

864
def main():
@@ -12,9 +68,68 @@ def main():
1268
formatter_class=argparse.RawDescriptionHelpFormatter,
1369
)
1470
parser.add_argument("regfile", help="Path to the .reg file.")
71+
parser.add_argument("-s", "--sid-file", help="File containing the user's SIDs")
72+
parser.add_argument(
73+
"-f",
74+
"--input-format",
75+
choices=[".reg", "reg_bof"],
76+
help="Format of input file",
77+
default=".reg",
78+
)
79+
output_group = parser.add_argument_group("output options")
80+
output_group.add_argument(
81+
"-text",
82+
action="store_true",
83+
help="Output result as formatted text file",
84+
)
85+
output_group.add_argument(
86+
"-stdout",
87+
action="store_true",
88+
help="Output result as text directly to console",
89+
)
90+
output_group.add_argument(
91+
"-json",
92+
action="store_true",
93+
help="Output result as JSON",
94+
)
95+
output_group.add_argument(
96+
"-csv",
97+
action="store_true",
98+
help="Output result as CSV",
99+
)
100+
output_group.add_argument(
101+
"-output",
102+
action="store",
103+
metavar="prefix",
104+
help="Filename prefix for writing results to",
105+
)
106+
107+
bloodhound = parser.add_argument_group("BloodHound")
108+
bloodhound.add_argument("--neo4j-user", help="Username for neo4j")
109+
bloodhound.add_argument("--neo4j-pass", help="Password for neo4j")
110+
bloodhound.add_argument("--neo4j-host", help="Host for neo4j", default="localhost")
111+
bloodhound.add_argument("--neo4j-port", help="Port for neo4j", default=7687)
112+
bloodhound.add_argument(
113+
"--use-owned-sids",
114+
help="Use the SIDs of all owned principals as the user SIDs",
115+
action="store_true",
116+
)
15117
args = parser.parse_args()
16118

17-
parser = RegfileParser(args.regfile)
119+
if args.neo4j_user and args.neo4j_pass:
120+
from neo4j import GraphDatabase
121+
122+
neo4j_driver = GraphDatabase.driver(
123+
f"neo4j://{args.neo4j_host}:{args.neo4j_port}",
124+
auth=(args.neo4j_user, args.neo4j_pass),
125+
)
126+
else:
127+
neo4j_driver = None
128+
129+
if args.input_format == ".reg":
130+
parser = RegfileParser(args.regfile)
131+
elif args.input_format == "reg_bof":
132+
parser = RegBofParser(args.regfile)
18133

19134
templates = []
20135

@@ -32,8 +147,25 @@ def main():
32147

33148
print(f"[*] Found {len(templates)} templates in the registry")
34149

150+
templates = [template.to_dict() for template in templates]
151+
152+
find = MyFind(
153+
target=MockTarget(),
154+
connection=MockLDAPConnection(
155+
args.sid_file, neo4j_driver=neo4j_driver, use_owned_sids=args.use_owned_sids
156+
),
157+
stdout=args.stdout,
158+
text=args.text,
159+
json=args.json,
160+
)
161+
35162
for template in templates:
36-
pretty_print(template.to_dict())
163+
user_can_enroll, enrollable_sids = find.can_user_enroll_in_template(template)
164+
template.set("Can Enroll", user_can_enroll)
165+
template.set("Enrollable SIDs", [sid_to_name(sid) for sid in enrollable_sids])
166+
167+
prefix = datetime.now().strftime("%Y%m%d%H%M%S") if not args.output else args.output
168+
find._save_output(templates=templates, cas=[], oids=[], prefix=prefix)
37169

38170

39171
if __name__ == "__main__":

0 commit comments

Comments
 (0)