You are thrusted into a digital crisis at Forela - a crucial Dev server has fallen victim to a cyber attack. This incident unfolds at a critical time for the company as it is expanding its business in Pakistan, under the leadership of senior dev Abdullah. Despite assurances of server hardening, the server was mistakenly exposed to the internet, creating a doorway for the threat actor. The task involves swift analysis & reporting to prevent catastrophic damage to the company’s operations. Equipped with a packet capture from the time of the incident, players must sift through a sea of data, with a 1-2 days margin of uncertainty. This cybersecurity whodunit will put your forensic skills to the test, seeking answers to how the attacker breached their defenses in the first place.
Warning: This Sherlock will require an element of OSINT to complete fully.
An external host scanned the exposed Forela dev server, found FTP among a handful of open services, and sprayed one predictable password across several usernames until an account logged in. That account’s FTP home held a hidden backup file with a port knocking configuration and credentials. The attacker replayed the knock sequence, which opened a hidden second FTP service, and logged in there with a developer’s credentials. The service exposed a database dump with cloud credentials, planning documents and a way out of the chroot to read system files, and a reminder note pointed to a public GitHub repository whose history leaked the SSH password. The attacker then logged in over SSH and pulled a ransomware archive from a separate server over HTTP.
Capture.pcap → open with Wireshark or tshark. FTP carries credentials and files in clear text, so --export-objects ftp-data recovers the stolen filesTimestamp note: Time answers use
DD/MM/YYYY HH:mm:ssin UTC. ForceTZ=UTContsharkbefore readingframe.time.
Note: The hidden FTP service is not dissected as FTP by default. Use
-d tcp.port==<port>,ftp(Wireshark: Decode As) to see its commands.
Triage the capture by conversation first. One external host sends bare SYNs across a wide port range, which is the scanner. Open ports are the ones that answer with a SYN-ACK, so filter on tcp.flags.syn==1 && tcp.flags.ack==1 towards the scanner and list the distinct source ports.
tshark -r Capture.pcap -q -z conv,tcp
tshark -r Capture.pcap -Y 'ip.dst==$SCANNER && tcp.flags.syn==1 && tcp.flags.ack==1' -T fields -e tcp.srcport | sort -nu
Tip: Count only SYN-ACKs. Closed ports answer with RST and pad the list.
The first packet from the scanner marks the start. Force UTC on frame.time and reformat to the day-first mask.
TZ=UTC tshark -r Capture.pcap -Y 'ip.src==$SCANNER' -T fields -e frame.time -c 1
The scanner from Q1 is also the host that authenticates to the FTP service. One external address owns both the scan and the FTP session.
tshark -r Capture.pcap -Y 'ftp' -T fields -e ip.src -e ip.dst -e ftp.request.command -e ftp.request.arg
Port 21 is the way in. The attacker tries a short list of seasonal passwords against three different usernames, so the pattern is one password pattern across many accounts, not many passwords against one account. That is password spraying.
tshark -r Capture.pcap -Y 'ftp.request.command=="USER" || ftp.request.command=="PASS"' -T fields -e tcp.stream -e ftp.request.command -e ftp.request.arg
Gotcha:
T1110.001(Password Guessing) is the obvious first pick, and HTB rejected it. The traffic is a spray across three users, which is the.003sub-technique.
Most sprayed pairs fail with 530. Credentials only count if the server answers 230 in the same TCP stream, so list the streams that got a 230 and read the PASS command from each.
tshark -r Capture.pcap -Y 'ftp.response.code==230' -T fields -e tcp.stream
tshark -r Capture.pcap -Y 'ftp.request.command=="PASS" && tcp.stream==<stream>' -T fields -e ftp.request.arg
Gotcha: Another user appears in the spray with the same password but never reaches a
230, and submitting that pair was wrong. Confirm the success code in the stream rather than taking the first username.
After login the attacker lists the home directory and issues RETR for a hidden file. The data channel carries a port knocking configuration plus credentials used for the rest of the intrusion.
tshark -r Capture.pcap -Y 'ftp.request.command=="RETR"' -T fields -e tcp.stream -e ftp.request.arg
tshark -r Capture.pcap -q --export-objects ftp-data,work/ftpobj
The exported hidden file holds a knockd-style config. It names the protected service port that stays closed until the knock sequence completes.
strings work/ftpobj/.backup
The service only opens after connection attempts hit a fixed sequence of closed ports in order. That is port knocking, MITRE T1205.001 (Traffic Signaling: Port Knocking).
Take the knock sequence from the Q7 config. In the capture, the attacker’s bare SYNs hit those three ports in order, and the service port answers afterwards.
tshark -r Capture.pcap -Y 'ip.src==$ATTACKER && tcp.flags.syn==1 && tcp.flags.ack==0 && tcp.dstport in {<knock ports from .backup>}' -T fields -e frame.time -e tcp.dstport
Same filter as Q9 with UTC forced. The timestamp of the last knock SYN is when the interaction ended.
TZ=UTC tshark -r Capture.pcap -Y 'ip.src==$ATTACKER && tcp.flags.syn==1 && tcp.flags.ack==0 && tcp.dstport in {<knock ports from .backup>}' -T fields -e frame.time
The hidden service is a second FTP server. Force the FTP dissector on its port, then read the USER and PASS commands from the stream that ends in 230.
tshark -r Capture.pcap -d tcp.port==24456,ftp -Y 'tcp.port==24456 && (ftp.request.command=="USER" || ftp.request.command=="PASS")' -T fields -e tcp.stream -e ftp.request.command -e ftp.request.arg
Tip: Without the decode-as, the traffic is raw TCP and no FTP commands match the filter.
The time of the 230 login response from the hidden service, after the knock completed.
TZ=UTC tshark -r Capture.pcap -d tcp.port==24456,ftp -Y 'ftp.response.code==230 && tcp.srcport==24456' -T fields -e frame.time
Export the FTP data objects from the second service and read them. A hidden SQL dump holds the developer’s AWS account ID and password.
tshark -r Capture.pcap -d tcp.port==24456,ftp -q --export-objects ftp-data,work/ftpobj2
strings work/ftpobj2/.archived.sql | grep -i abdullah
An exported Tasks archive contains Done.docx, which embeds an Excel sheet. The deadline is stored as an Excel serial number in sheet1.xml (45168), which converts to a calendar date counting from 1899-12-30.
python3 -I -c "import zipfile,sys;print(zipfile.ZipFile(sys.argv[1]).namelist())" work/ftpobj2/Tasks
python3 -I -c "import datetime;print(datetime.date(1899,12,30)+datetime.timedelta(days=45168))"
reminder.txt is among the exported objects and states the CEO’s travel date.
cat work/ftpobj2/reminder.txt
Once out of the chroot, the attacker retrieves /etc/passwd over FTP. Filter the exported copy for /bin/bash shells; root is expected and one other account matches.
grep '/bin/bash' work/ftpobj2/passwd
The attacker also retrieves a hidden reminder file from outside the chroot. Its contents are a note pointing at a public GitHub repository, which is where the SSH password comes from. Read the CWD/RETR arguments for the full path.
tshark -r Capture.pcap -d tcp.port==24456,ftp -Y 'ftp.request.command=="RETR" || ftp.request.command=="CWD"' -T fields -e frame.time -e ftp.request.arg
cat work/ftpobj2/.reminder
SSH is encrypted, so the password never appears in the capture. The note from Q17 points at a public GitHub source and the scenario warns about OSINT. Searching users for names seen in the capture goes nowhere, but a plain repository search for the company name finds a Forela repository created the day before the intrusion. The current tree is clean. The history is not: the initial commit contains a config file with an SSH password entry for the account found in Q16, and a later commit removes it. Read the patch of the first commit and take the value from the + line. This answer was found on public GitHub, not in the pcap.
curl -s 'https://api.github.com/search/repositories?q=forela'
curl -s https://api.github.com/repos/<owner>/<repo>/commits
curl -s -H 'Accept: application/vnd.github.patch' https://api.github.com/repos/<owner>/<repo>/commits/<first-commit>
Gotcha: The file looks sanitised in the latest commit. The secret survives only in the first commit’s diff, so walk the full history of a leaked repo.
After SSH access, the capture shows an HTTP request from the victim to a second external host. http.request.full_uri gives the URL, a zip under a campaign path named after the target.
tshark -r Capture.pcap -Y 'http.request' -T fields -e ip.dst -e http.request.full_uri
The User-Agent of the same request identifies the client.
tshark -r Capture.pcap -Y 'http.request' -T fields -e http.user_agent
--export-objects http missed the large transfer, so follow the TCP stream as raw bytes, strip the HTTP headers and carve the zip. The archive listing names the ransomware.
tshark -r Capture.pcap -q -z follow,tcp,raw,78238 > work/ransom_raw.bin
unzip -l work/Ransomware2_Server.zip
Tip: Carve with
follow,tcp,rawwhen--export-objects httpreturns nothing for a large download.
| Phase | Action |
|---|---|
| Discovery | Port scan finds 21, 22, 3306, 6379 and 8086 open |
| Initial Access | FTP password spray across three users; tony.shephard logs in |
| Collection | Hidden .backup file retrieved from FTP |
| Defense Evasion | Knock sequence sent to three closed ports, opening the hidden service on 24456 |
| Lateral Movement | Login to the second FTP service as abdullah.yasin |
| Collection | Database dump, task document and reminder file retrieved |
| Discovery | Chroot escape and /etc/passwd read; cyberjunkie identified |
| Credential Access | .reminder points to a leaked repository; the SSH password sits in its first commit |
| Impact | Ransomware archive downloaded over HTTP from a second server |
| Type | Value |
|---|---|
| IP (scanner, FTP, SSH) | 3.109.209[.]43 |
| IP (ransomware host) | 13.233.179[.]35 |
| URL | hxxp[://]13[.]233[.]179[.]35/PKCampaign/Targets/Forela/Ransomware2_Server.zip |
| Account (FTP) | tony.shephard |
| Account (hidden FTP) | abdullah.yasin |
| File | .backup, .archived.sql, /opt/reminders/.reminder |
| Port knock sequence | 29999, 45087, 50234 (service on 24456) |
| User-Agent | Wget/1.21.2 |
| Malware | GonnaCry |
| Repository | github[.]com/forela-finance/forela-dev (commit 182da42) |
| Technique | ID | Description |
|---|---|---|
| Network Service Discovery | T1046 | SYN scan of the server |
| Brute Force: Password Spraying | T1110.003 | Seasonal password sprayed across FTP users |
| Valid Accounts | T1078 | Logins to both FTP services and SSH |
| Traffic Signaling: Port Knocking | T1205.001 | Knock sequence opened the hidden FTP service |
| File and Directory Discovery | T1083 | Chroot escape and filesystem browsing |
| Data from Local System | T1005 | Backup, SQL dump, documents and /etc/passwd retrieved |
| Ingress Tool Transfer | T1105 | Ransomware archive fetched with wget |
| Data Encrypted for Impact | T1486 | GonnaCry ransomware archive downloaded to the host |
Plain FTP exposed to the internet with seasonal passwords is an open door. The spray used one predictable pattern across three accounts. Drop FTP for SFTP, ban password patterns tied to the year, and alert on a burst of 530 responses from one source followed by a 230.
A backup file inside the FTP root leaked the defence. .backup held the knock sequence and credentials, so the one control meant to hide the critical service was given away by the first foothold. Keep configs and secrets out of any directory reachable by a low-privilege service account.
Port knocking is obscurity, not authentication. The service on 24456 was still plain FTP with a developer login. Anyone who reads the sequence or watches the traffic can replay it. Put the service behind a VPN or key-based access instead.
A chroot that can be traversed gives no isolation. The attacker read /etc/passwd and /opt/reminders, and the note there led to the SSH password. Fix the FTP server’s jail and keep credentials and hints out of readable reminder files.
Plaintext credentials in shared dumps and public repositories travel far. A database dump held cloud account credentials, and the SSH password sat in the first commit of a public repository, still readable after a later commit removed it. Rotate anything found in dumps, scan public repos and their full history for company secrets, and treat a removed secret as burned.
The attacker went from a scan to a ransomware download on the server using a weak FTP password, a leaked config, and credentials that had already been exposed on GitHub. Port knocking hid the critical service but guarded nothing once the sequence was read from the first stolen file.