Skip to content

Commit 3ca0a4b

Browse files
committed
release v1
1 parent b05bca8 commit 3ca0a4b

File tree

2 files changed

+54
-17
lines changed

2 files changed

+54
-17
lines changed

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,34 @@ We were on an Internal pen test where the client had unauthenticated access to a
99

1010
Also Bastion on Hack The Box is a thing.
1111

12+
#### No, why isn't this a PR to foremost or binwalk?
13+
...good question
14+
1215
## Notes
1316
If the haystack is a true backup of a Windows computer, it is very likely there will be multiple copies of the registry hive on disk due to Windows keeping a copy for recovery purposes. If local or LSA secrets is output multiple times with the same data, this is likely the reasoning.
1417

1518
## Usage
1619
```
17-
python3 needle.py /path/to/haystack
20+
usage: needle.py [-h] [-c] [-n] [-o OUTPUT] [haystack [haystack ...]]
21+
22+
Process a large haystack looking for high value files from Windows. Specifically SAM, SECURITY, and SYSTEM hives.
23+
24+
positional arguments:
25+
haystack Haystack to parse
26+
27+
optional arguments:
28+
-h, --help show this help message and exit
29+
-c, --clean Clean dirty on disk registry keys in a very hacky way
30+
that somehow works (usually needed for vhd)
31+
-n, --no-auto-dump Try to automatically use secretsdump if SAM and SYSTEM
32+
or SYSTEM and SECURITY are found
33+
-o OUTPUT, --output OUTPUT
34+
Output Directory for registry hives, default: current
35+
directory
36+
37+
Examples:
38+
python3 needle.py /mnt/HTB/Bastion/file.vhd --clean
39+
python3 needle.py /mnt/VeritasNetbackup/dc.tar
1840
```
1941

2042
## Expected Output
@@ -56,7 +78,7 @@ NL$KM:6e0e6b09c158fa85e3ad464f21944dda6a1e237b67bbd302f96cccabe3dc158eeef2bb6536
5678
- [ ] Refactor patterns into a list for easier expandability
5779
- [ ] Add ability to skip to certain chunks if ran before
5880
- [ ] Add flag to only look for system, sam, security, etc
59-
- [ ] Add flag to change output directory
81+
- [x] Add flag to change output directory
6082
- [ ] Add flag to only search misaligned (debug)
6183
- [x] Add in ability to find multiple copies within given chunk
6284
- [x] Find ESE DBs if haystack is from DC

needle.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
found = [False, False, False, False]
1717

1818

19-
def init(haystack, clean, no_auto_dump):
19+
def init(haystack, clean, no_auto_dump, output):
2020

2121
#if (len(sys.argv) < 2):
2222
# print("Please provide a filename to run on")
@@ -28,7 +28,7 @@ def init(haystack, clean, no_auto_dump):
2828

2929
f = open(haystack, 'rb')
3030
f_size = os.stat(haystack).st_size
31-
main(f, f_size, clean, no_auto_dump)
31+
main(f, f_size, clean, no_auto_dump, output)
3232

3333
# https://stackoverflow.com/questions/4664850/how-to-find-all-occurrences-of-a-substring
3434
def cust_findall(string, substring):
@@ -86,17 +86,25 @@ def autodump(sam, system, security, ntds):
8686
except:
8787
pass
8888

89-
def search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, misaligned, clean):
89+
def search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, misaligned, clean, output):
9090
_temp_SAM = cust_findall(chunk, SAM_pattern)
9191
_temp_SYSTEM = cust_findall(chunk, SYSTEM_pattern)
9292
_temp_SECURITY = cust_findall(chunk, SECURITY_pattern)
9393
# _temp_NTDS = cust_findall(chunk, NTDS_pattern)
9494

95+
# https://github.com/knavesec/Max/blob/24ea388bf004557ab4662294f11a2e9e75756f6b/max.py#L1236
96+
if output:
97+
if not os.path.exists(args.output):
98+
os.makedirs(args.output)
99+
95100
for temp_SAM in _temp_SAM:
96101
# potential SAM found
97102
# print(hexdump.hexdump(chunk[temp_SAM - 0x30 : temp_SAM + len(SAM_pattern)]))
98103
if (b"regf" in chunk[temp_SAM - 0x30 : temp_SAM - 0x30 + 0x4]):
99-
tmp_name = str(uuid.uuid4()) + "_SAM"
104+
if (output != None):
105+
tmp_name = os.path.join(os.path.curdir, output, str(uuid.uuid4()) + "_SAM")
106+
else:
107+
tmp_name = str(uuid.uuid4()) + "_SAM"
100108
SAM_filenames.append(tmp_name)
101109
print("Potentially found SAM at offset {} within searched chunk {}. Writing to {}".format(temp_SAM, chunk_num, tmp_name))
102110
with open(tmp_name, "wb") as SAM:
@@ -113,7 +121,10 @@ def search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, misaligned, cle
113121
# potential SYSTEM found; 0x2F since we search by \x00\x00\x00S to rule out false positives
114122
# print(hexdump.hexdump(chunk[temp_SYSTEM - 0x2F : temp_SYSTEM + len(SYSTEM_pattern)]))
115123
if (b"regf" in chunk[temp_SYSTEM - 0x2D : temp_SYSTEM - 0x2D + 0x4 ]):
116-
tmp_name = str(uuid.uuid4()) + "_SYSTEM"
124+
if (output != None):
125+
tmp_name = os.path.join(os.path.curdir, output, str(uuid.uuid4()) + "_SYSTEM")
126+
else:
127+
tmp_name = str(uuid.uuid4()) + "_SYSTEM"
117128
SYSTEM_filenames.append(tmp_name)
118129
print("Potentially found SYSTEM at offset {} within searched chunk {}. Writing to {}".format(temp_SYSTEM, chunk_num, tmp_name))
119130
#print(hexdump.hexdump(chunk[temp_SYSTEM - 0x2F : temp_SYSTEM + len(SYSTEM_pattern)]))
@@ -145,7 +156,10 @@ def search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, misaligned, cle
145156
# potential SECURITY found
146157
# print(hexdump.hexdump(chunk[temp_SECURITY - 0x30 : temp_SECURITY + len(SECURITY_pattern)]))
147158
if (b"regf" in chunk[temp_SECURITY - 0x30 : temp_SECURITY - 0x30 + 0x4]):
148-
tmp_name = str(uuid.uuid4()) + "_SECURITY"
159+
if (output != None):
160+
tmp_name = os.path.join(os.path.curdir, output, str(uuid.uuid4()) + "_SECURITY")
161+
else:
162+
tmp_name = str(uuid.uuid4()) + "_SECURITY"
149163
SECURITY_filenames.append(tmp_name)
150164
print("Potentially found SECURITY at offset {} within searched chunk {}. Writing to {}".format(temp_SECURITY, chunk_num, tmp_name))
151165
with open(tmp_name, "wb") as SECURITY:
@@ -183,7 +197,7 @@ def check(no_auto_dump):
183197
autodump(found[0], found[1], found[2], found[3])
184198

185199

186-
def main(f, f_size, clean, no_auto_dump):
200+
def main(f, f_size, clean, no_auto_dump, output):
187201
# reading in chunks and scanning through the chunks, if we don't find anything, maybe our chunks were too small and the pattern was at the boundry of chunks so we need to seek by chunk / 2 and scan again
188202

189203
chunk_size = 4 * 1024 * 1024 # 4MiB
@@ -195,15 +209,15 @@ def main(f, f_size, clean, no_auto_dump):
195209
f.seek(start)
196210
chunk = f.read(chunk_size)
197211
chunk_num += 1
198-
if (search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, False, clean) == True):
212+
if (search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, False, clean, output) == True):
199213
break
200214
start = end
201215
end += chunk_size
202216

203217
# finish last partial chunk just in case
204218
f.seek(start)
205219
chunk = f.read(f_size - start)
206-
search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, False, clean)
220+
search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, False, clean, output)
207221

208222
check(no_auto_dump)
209223

@@ -216,7 +230,7 @@ def main(f, f_size, clean, no_auto_dump):
216230
f.seek(start)
217231
chunk = f.read(chunk_size)
218232
chunk_num += 1
219-
if (search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, False, clean) == True):
233+
if (search_chunk(chunk, chunk_num, chunk_size, haystack, f_size, False, clean, output) == True):
220234
break
221235
start = end
222236
end += chunk_size
@@ -227,18 +241,19 @@ def main(f, f_size, clean, no_auto_dump):
227241
parser = argparse.ArgumentParser(
228242
formatter_class=argparse.RawDescriptionHelpFormatter,
229243
description='Process a large haystack looking for high value files from Windows. Specifically SAM, SECURITY, and SYSTEM hives.',
230-
epilog=textwrap.dedent('''Examples:\npython3 needle.py /mnt/HTB/Bastion/file.vhd --hacky-clean\npython3 needle.py /mnt/VeritasNetbackup/dc.tar''')
244+
epilog=textwrap.dedent('''Examples:\npython3 needle.py /mnt/HTB/Bastion/file.vhd --clean\npython3 needle.py /mnt/VeritasNetbackup/dc.tar''')
231245
)
232246
# https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
233-
parser.add_argument('--clean', action='store_true', default=False, help="Clean dirty on disk registry keys in a very hacky way that somehow works (usually needed for vhd)")
234-
parser.add_argument('--no-auto-dump', action='store_true', default=False, help="Try to automatically use secretsdump if SAM and SYSTEM or SYSTEM and SECURITY are found")
247+
parser.add_argument('-c','--clean', action='store_true', default=False, help="Clean dirty on disk registry keys in a very hacky way that somehow works (usually needed for vhd)")
248+
parser.add_argument('-n','--no-auto-dump', action='store_true', default=False, help="Try to automatically use secretsdump if SAM and SYSTEM or SYSTEM and SECURITY are found")
249+
parser.add_argument('-o','--output', dest="output", default=None, required=False, help='Output Directory for registry hives, default: current directory')
235250
parser.add_argument('haystack', metavar='haystack', type=str, nargs='*', help='Haystack to parse')
236251

237252
args = parser.parse_args()
238253

239-
if (args.haystack != None):
254+
if (args.haystack != []):
240255
#do things
241256
for haystack in args.haystack:
242-
init(haystack, args.clean, args.no_auto_dump)
257+
init(haystack, args.clean, args.no_auto_dump, args.output)
243258
else:
244259
parser.print_help()

0 commit comments

Comments
 (0)