HackThisSite - Forensic Mission 1

Challenge

From: stacy.melroy@tritech.org

Thank you for agreeing to help me. We recently had a problem with a former employee and though I cannot prove it, I believe he is the one that erased the files off my thumbdrive. I did some research and I made an image of the drive for you but that is as far as I got. I can replace most of what was lost but there is one file in particular that holds a very important account password. If you could recover that file, I would be extremely grateful.

Also, this is my personal thumbdrive. Keep that in mind please. I'm not sure what you might recover, but I would rather it not get spread around.

委托人(stacy.melroy@tritech.org)说自己的 U 盘被一名已离职的员工清空,附件 image.tar.gz 是整盘镜像,要求把其中保存账号密码的那个文件恢复出来。附件自带 md5 校验值 4e7af965caed9b8d29c40f549fdb7d28

Solution

Step 1: 固定镜像与文件系统

1
2
3
4
5
$ md5sum image.tar.gz
4e7af965caed9b8d29c40f549fdb7d28 image.tar.gz

$ tar xzf image.tar.gz && file image.dd
image.dd: DOS/MBR boot sector, code offset 0x52+2, OEM-ID "NTFS ", sectors/cluster 8, Media descriptor 0xf8, sectors/track 32, heads 64, hidden sectors 1464320, dos < 4.0 BootSector (0x80), FAT (1Y bit by descriptor); NTFS, sectors/track 32, sectors 47103, $MFT start cluster 4, $MFTMirror start cluster 2943, bytes/RecordSegment 2^(-1*246), clusters/index block 1, serial number 038037c7c7f42f96e; contains bootstrap BOOTMGR

filefsstat 都直接把它识别成 NTFS 卷,没有分区表,mmls 无输出,也就是说 image.dd 本身就是卷设备,取证工具按裸卷解析即可:

1
2
3
4
5
6
7
8
$ fsstat image.dd
FILE SYSTEM INFORMATION
--------------------------------------------
File System Type: NTFS
Volume Serial Number: 38037C7C7F42F96E
OEM Name: NTFS
Volume Name: stacy
Version: Windows XP

卷标 stacy 和委托人对得上,确认这就是目标 U 盘。

Step 2: 列出被删除的 inode

fls -r -d 递归列出被标记为删除的目录项,-p 输出完整路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
$ fls -r -d -p image.dd
-/d * 64-144-2: .Trash-1000/expunged/2026288587
r/r * 85-128-2: .Trash-1000/expunged/2026288587/Termination - Allen Smith.docx
r/- * 0: .Trash-1000/expunged/2026288587/Voicemail 1.wav
r/r * 81-128-2: .Trash-1000/expunged/2026288587/Voicemail 1.wav
r/r * 81-128-4: .Trash-1000/expunged/2026288587/Voicemail 1.wav:Zone.Identifier
r/- * 0: .Trash-1000/expunged/2026288587/XuN4Olv.jpg
r/- * 0: .Trash-1000/expunged/2026288587/Zr5FTg3.jpg
r/r * 76-128-2: .Trash-1000/expunged/2026288587/Zr5FTg3.jpg
-/d * 65-144-2: .Trash-1000/expunged/2026288587/private
-/r * 66-128-2: .Trash-1000/expunged/2026288587/private/Cxm5Xlgh.jpg
-/r * 66-128-4: .Trash-1000/expunged/2026288587/private/Cxm5Xlgh.jpg:Zone.Identifier
-/r * 86-128-2: .Trash-1000/expunged/2026288587/private/Your new password is.rar
-/r * 67-128-2: .Trash-1000/expunged/2026288587/5.jpg
-/r * 68-128-2: .Trash-1000/expunged/2026288587/hkjvXEH.jpg
-/r * 69-128-2: .Trash-1000/expunged/2026288587/bvCvfQz.jpg
-/r * 70-128-2: .Trash-1000/expunged/2026288587/BQrr07W.jpg
-/r * 71-128-2: .Trash-1000/expunged/2026288587/IMGP2027_8_9_tonemapped_2-X3.jpg
-/r * 72-128-2: .Trash-1000/expunged/2026288587/kygf687rf.jpg
-/r * 73-128-2: .Trash-1000/expunged/2026288587/11738542606_4157a9cb04_b.jpg
-/r * 74-128-2: .Trash-1000/expunged/2026288587/24155.pdf
-/r * 75-128-2: .Trash-1000/expunged/2026288587/yIeVjcQ.jpg
-/r * 77-128-2: .Trash-1000/expunged/2026288587/46L3tK0.jpg
-/r * 78-128-2: .Trash-1000/expunged/2026288587/XuN4Olv.jpg
-/r * 78-128-4: .Trash-1000/expunged/2026288587/XuN4Olv.jpg:Zone.Identifier
-/r * 79-128-2: .Trash-1000/expunged/2026288587/armls_mwr_2013-q1.pdf
-/r * 79-128-4: .Trash-1000/expunged/2026288587/armls_mwr_2013-q1.pdf:Zone.Identifier
-/r * 80-128-2: .Trash-1000/expunged/2026288587/TrueCrypt Setup 7.1a(1).rar
-/r * 82-128-2: .Trash-1000/expunged/2026288587/jgA1wpZ.jpg
-/r * 82-128-4: .Trash-1000/expunged/2026288587/jgA1wpZ.jpg:Zone.Identifier
-/r * 83-128-2: .Trash-1000/expunged/2026288587/logins.txt
-/r * 84-128-2: .Trash-1000/expunged/2026288587/License.txt
-/r * 91-128-2: .Trash-1000/info/Current.trashinfo
d/- * 0: Current
-/r * 16: $OrphanFiles/OrphanFile-16
-/r * 17: $OrphanFiles/OrphanFile-17
-/r * 18: $OrphanFiles/OrphanFile-18
-/r * 19: $OrphanFiles/OrphanFile-19
-/r * 20: $OrphanFiles/OrphanFile-20
-/r * 21: $OrphanFiles/OrphanFile-21
-/r * 22: $OrphanFiles/OrphanFile-22
-/r * 23: $OrphanFiles/OrphanFile-23

几个观察:

  • 所有被删内容都在 .Trash-1000(Linux 桌面环境的回收站目录)里,Current.trashinfo 记录了删除时间 2014-01-05T01:11:31,路径是 Current,这是整个目录被一次性移入回收站再清空的痕迹。
  • inode-attrtype-attrid 三元组里,r/r * 85-128-2 表示该名字在 $MFT 里仍有 FILE_NAME 属性残留(128 = 0x80,即默认 $DATA 流所在的属性记录),可以按 inode 直接 icatr/- * 0 那几条(Voicemail 1.wavXuN4Olv.jpgZr5FTg3.jpg)只剩索引项,inode 已被清零,同名文件在别处还有一条带真实 inode 的记录,对应关系靠文件名匹配。
  • 和题面相关的四个文件:Termination - Allen Smith.docx(85)、private/Your new password is.rar(86)、logins.txt(83)、Voicemail 1.wav(81)。

Step 3: 按 inode 恢复文件

1
2
3
4
5
6
7
8
9
10
11
$ mkdir -p recovered
$ icat image.dd 85 > recovered/Termination.docx
$ icat image.dd 86 > 'recovered/Your new password is.rar'
$ icat image.dd 81 > 'recovered/Voicemail 1.wav'
$ icat image.dd 83 > recovered/logins.txt

$ file recovered/*
recovered/Termination.docx: Microsoft Word 2007+
recovered/Your new password is.rar: RAR archive data, v4, os: Win32
recovered/Voicemail 1.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, mono 44100 Hz
recovered/logins.txt: ASCII text, with CRLF line terminators

删除只清了 $MFT 里的分配位和索引项,数据运行(data runs)指向的簇没有被复用,所以 icat 出来仍然是完好文件。被删名字还带着 NTFS 备用数据流,Voicemail 1.wavZone.Identifier 里是 ZoneId=3,说明这批文件是从网上下载后拷进来的:

1
2
3
$ icat image.dd 81-128-4
[ZoneTransfer]
ZoneId=3

先看那个名字最直白的凭证文件:

1
2
$ cat recovered/logins.txt
stacy.melroy@tritech.org - LittleSister92

Step 4: 从辞退信找解压密码

Termination.docx 是一封辞退通知,落款人正是委托人 Stacy Melroy,正文里留着她的联系电话。docx 是 zip 容器,正文在 word/document.xml,剥掉 XML 标签就能看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ unzip -p recovered/Termination.docx word/document.xml | sed -e 's/<\/w:p>/\n/g' -e 's/<[^>]*>//g' | grep -n .
1:
2:Stacy Melroy
3:Marketing Day Shift Manager
4:TriTech Inc.
5:519-555-4783
7:January 1st, 2014
9:Allen Smith
10:Marketing Analyst
11:TriTech Inc.
13:
14:Allen Smith,
16:This is an official notification of termination.
17:Effective as of January 3rd, 2014, your position at TriTech Inc. as a Marketing Analyst will be terminated as of the end of your shift.
19:After numerous complaints of harassment from multiple female employees of TriTech Inc. we have no other alternative than to terminate your employment with us.
21:Verbal warning on November 1st, 2013
22:First official write up on November 22nd, 2013
23:Second official write up on December 2nd, 2013 and two days of unpaid vacation required.
24:Final write up on December 20th, 2013 and informed that continued employment will be evaluated by Human Resources.
26:At the end of your shift on January 3rd, 2014, you are to turn in your access badge and any other company issued equipment to the front security desk. Your exit interview will be conducted with HR at 3:00pm to discuss any company benefits that you chose to utilize; health and retirement.
28:Due to the nature of your termination we will not consider you for future employment with TriTech Inc.
34:Respectfully,
39:Stacy Melroy
40:Marketing Day Shift Manager
42:

519-555-4783 就是后来用来加密压缩包的号码。

Step 5: 电话号码解开 RAR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ 7z x -p5195554783 -orecovered 'recovered/Your new password is.rar'
Extracting archive: recovered/Your new password is.rar
--
Path = recovered/Your new password is.rar
Type = Rar
Physical Size = 7393
Solid = -
Blocks = 1
Multivolume = -
Volumes = 1

Everything is Ok

Size: 9940
Compressed: 7393

$ unzip -p 'recovered/Your new password is.docx' word/document.xml | sed -e 's/<\/w:p>/\n/g' -e 's/<[^>]*>//g' | grep -n .
1:
2:Your new password is 'qPYgbs0w5&amp;?i{8a'.

压缩包内是一个同名的 docx,正文一句话就是任务密码(&amp; 是 XML 转义的 &)。

另外恢复出来的 Voicemail 1.wav 是一段 8.54 秒的 16 bit/44.1 kHz 单声道 PCM 录音(753710 字节),属于同一个事件背景,不参与上面的解压链路。

Step 6: Submit

1
2
3
4
5
6
7
8
$ curl -s -b "$HTS_COOKIE" -H 'Referer: https://www.hackthissite.org/missions/forensic/1/' \
--data-urlencode 'forensic1=qPYgbs0w5&?i{8a' \
https://www.hackthissite.org/missions/forensic/1/ \
| sed -e "s/&#039;/'/g" -e 's/<[^>]*>//g' | grep -aoE "Congratulations! You've successfully completed Forensic 1!"
Congratulations! You've successfully completed Forensic 1!

$ curl -s -b "$HTS_COOKIE" 'https://www.hackthissite.org/user/view/***/' | grep -aoE 'missions/forensic/[0-9]+/'
missions/forensic/1/

Step 7: Script

把上面每一步串起来,从镜像直接跑到任务密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#!/usr/bin/env python3
"""HackThisSite Forensic 1 - recover the deleted password file from the NTFS image.

Chain:
fls -r -d lists deleted inodes -> icat extracts them -> the termination
letter leaks the phone number -> that number is the RAR password -> the
recovered .docx holds the mission password.

Requires: sleuthkit (fls, icat) and 7z on PATH.
"""
import html
import os
import re
import subprocess
import sys
import zipfile

IMAGE = "image.dd"
OUT = "recovered"
os.chdir(os.path.dirname(os.path.abspath(__file__)))


def run(*args, **kw):
return subprocess.run(args, capture_output=True, **kw)


def deleted_inodes(image):
"""Return {basename: inode} for every deleted inode in the image."""
out = run("fls", "-r", "-d", "-p", image).stdout.decode("utf-8", "replace")
found = {}
for line in out.splitlines():
m = re.match(r"^[-rd]/[rd-] \* (\d+)-\d+-\d+:\t(.*)$", line)
if not m:
continue
inode, path = m.group(1), m.group(2)
found[os.path.basename(path)] = int(inode)
return found


def icat(image, inode, dest):
"""Extract the default $DATA stream of an inode to dest."""
with open(dest, "wb") as fh:
subprocess.run(["icat", image, str(inode)], stdout=fh, check=True)
return os.path.getsize(dest)


def docx_text(path):
"""Return the plain text of a .docx (word/document.xml, tags stripped)."""
with zipfile.ZipFile(path) as z:
xml = z.read("word/document.xml").decode("utf-8", "replace")
xml = xml.replace("</w:p>", "\n")
text = re.sub(r"<[^>]+>", "", xml)
return html.unescape(text)


def main():
os.makedirs(OUT, exist_ok=True)
inodes = deleted_inodes(IMAGE)
print("[*] deleted inodes: %d" % len(inodes))
for name in ("Termination - Allen Smith.docx",
"Your new password is.rar",
"Voicemail 1.wav",
"logins.txt"):
if name in inodes:
print(" %-34s inode %d" % (name, inodes[name]))

# 1. termination letter -> manager phone number
letter = os.path.join(OUT, "Termination.docx")
icat(IMAGE, inodes["Termination - Allen Smith.docx"], letter)
text = docx_text(letter)
phone = re.search(r"(\d{3})[-.\s]?(\d{3})[-.\s]?(\d{4})", text)
if not phone:
sys.exit("no phone number in the termination letter")
rar_pw = "".join(phone.groups())
print("[*] phone number : %s" % phone.group(0))
print("[*] rar password : %s" % rar_pw)

# 2. deleted logins.txt (the other credential artifact on the stick)
logins = os.path.join(OUT, "logins.txt")
icat(IMAGE, inodes["logins.txt"], logins)
print("[*] logins.txt : %s" % open(logins, encoding="utf-8",
errors="replace").read().strip())

# 3. the deleted voicemail recording
wav = os.path.join(OUT, "Voicemail 1.wav")
size = icat(IMAGE, inodes["Voicemail 1.wav"], wav)
import wave
with wave.open(wav) as w:
dur = round(w.getnframes() / w.getframerate(), 2)
print("[*] voicemail : %d bytes, %s s PCM" % (size, dur))

# 4. unlock the RAR with the phone number and read the new password
rar = os.path.join(OUT, "Your new password is.rar")
icat(IMAGE, inodes["Your new password is.rar"], rar)
subprocess.run(["7z", "x", "-y", "-p" + rar_pw, "-o" + OUT, rar],
check=True, stdout=subprocess.DEVNULL)
inner = os.path.join(OUT, "Your new password is.docx")
match = re.search(r"'([^']+)'", docx_text(inner))
print("[*] mission password: %s" % match.group(1))


if __name__ == "__main__":
main()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
$ cd <hts-workspace> && uv run python challenges/hts-forensic/1/solve.py
[*] deleted inodes: 29
Termination - Allen Smith.docx inode 85
Your new password is.rar inode 86
Voicemail 1.wav inode 81
logins.txt inode 83
[*] phone number : 519-555-4783
[*] rar password : 5195554783
[*] logins.txt : stacy.melroy@tritech.org - LittleSister92
[*] voicemail : 753710 bytes, 8.54 s PCM
[*] mission password: qPYgbs0w5&?i{8a

状态:verified(live 通关并已计入 profile)。

Vulnerabilities

NTFS 的删除只做两件事:把 $MFT 记录标成未分配、撤掉父目录索引项,文件内容所在的簇原地不动,直到被新数据覆盖。fls + icat 之所以能整盘还原,就是因为字节还在,元数据也还留着 inode、时间戳和备用数据流。这盘镜像里同时残留两处删除痕迹:一张写着登录邮箱和明文口令的 logins.txt,和一个只用一个电话号码当口令的压缩包,而那个号码印在另一封同样被删除的辞退信签名栏里。也就是说,把文件移入回收站再清空,对取证者而言等同于没有删除;真正需要保密的内容必须用整盘加密或可信的擦除工具处理,否则删除动作本身只是把线索排好队。口令复用把这两处痕迹连成一条链:同一个 5195554783 既出现在文档里又当压缩包密码,攻击者只要恢复出任何一个文件就能顺着链条把所有东西打开。

qPYgbs0w5&?i{8a