Safe Penetration Testing Labs · beginner · ~10 min
- Navigate the 23-module C-for-Kali track and choose a sensible starting point for your goals. - Explain what a **defensive parser** is and why every module in this track is one. - State the difference between *reading/inspecting* data on static fixtures and *attacking* a live system. - Recognise the standard Kali tool categories (recon, password attacks, sniffing, web, forensics) and the C skill each one exercises. - Write a small, safe C helper that validates a tool name against a fixed allow-list, and prove it rejects bad input. - Apply an authorization checklist before you run anything in a lab.
Security objective of this page: the asset we protect is you and your learning environment. The threat is a beginner accidentally pointing a real tool at a system they do not own, or building code that trusts untrusted input. This tour teaches you to detect that risk early (Is this exercise defensive? Is this data a static fixture? Am I authorized?) and to prevent it by staying inside the lab.
This is a map lesson. It orients you across the 23 paired modules of the C-for-Kali track. Each module pairs a short lesson (explaining an on-wire or on-disk format) with an exercise (write the matching parser in C). The track's core idea: to understand a Kali tool, you first understand the data format it reads. So instead of driving nmap at a target, you write count_open_ports() over a fixed sample of nmap's XML output.
This builds directly on your prereqs. From C strings you already know that a C string is a char array terminated by a '\0' byte, and that reading past that terminator is undefined behaviour — the single most important habit for any parser. From pointers you know how to walk a buffer with a moving pointer and compare bytes without copying. Every module here is those two skills applied to a real format: a Wi-Fi header byte, a pcap record, an ELF symbol table, an auth-log line.
Everything runs on static fixtures: fixed sample data baked into the exercise's test harness. No module opens a socket, captures a live packet, or executes a Kali binary. That is a deliberate safety property, not a limitation.
In authorized professional work, the person who understands the format is far more valuable than the person who only knows which button to click. When a scanner produces 40,000 lines of XML, someone has to write the parser that turns it into a triaged report. When an incident responder pulls a disk image, someone has to read the filesystem records that a GUI tool hides. When a fuzzing harness feeds a million malformed inputs into a parser, someone had to write a parser that does not crash or corrupt memory on hostile bytes.
Those parsers are exactly what this track teaches. Getting them right is also a security control in itself: a parser that mishandles attacker-controlled input is a classic source of buffer overflows, out-of-bounds reads, and denial-of-service bugs. Learning to write bounds-checked, allow-list-driven C on static fixtures is the safest possible way to build that muscle — you get real formats without the risk of touching a live system.
A map matters too. Without it, a 23-module track is just a wall of exercise names. With it, you can see the categories at a glance, respect the prerequisite order, and pick the row that matches the job in front of you.
Definition: code that reads and validates input to understand or reject it, never to attack a remote system. Plain explanation: a parser takes bytes in and produces a structured answer out — "3 ports are open", "this frame is a beacon", "this filename was deleted". A defensive parser assumes the input is hostile: it checks lengths before reading, never trusts a claimed size, and fails safely. How it works: you walk the buffer with a pointer, comparing bytes against a known format, always staying inside the buffer's length. When to use: any time you consume data you did not create — files, network captures, logs, tool output. When not to: never as a stand-in for a real attack tool. These parsers describe formats; they do not exploit anyone. Pitfall: trusting a length field from the data itself. A malicious file can claim "my name is 9000 bytes long"; if you copy 9000 bytes into a 32-byte buffer you have a classic overflow. Always clamp to your own buffer size.
Definition: fixed sample data baked into the test harness, not read from a live source.
Plain explanation: instead of capturing a real packet, the exercise hands you a const unsigned char[] that looks like a packet. Your code processes it; the test checks your answer.
How it works: the bytes never change and never come from a network, so running the code is completely safe and repeatable.
When to use: all learning and unit testing.
When not to: production tools eventually read real data — but only on systems you are authorized to touch, which is a later, supervised step.
Pitfall: assuming a fixture is "clean". Good fixtures include malformed cases on purpose so your parser learns to reject them.
Definition: Kali groups tools by engagement phase: information gathering / recon, vulnerability analysis, password attacks, sniffing & spoofing, web application, forensics, reporting. Plain explanation: each phase reads a characteristic data format, and each track module teaches the C behind one of them. How it works: e.g. recon → nmap XML; password attacks → auth-log analysis and policy checks; sniffing → 802.11 and pcap; web → HTTP request lines and SQLi markers; forensics → ELF, MFT, FAT; reporting → Markdown findings and USTAR bundles. When to use: to decide where to start based on the skill you want. Pitfall: thinking category names imply you should run those tools live. Here you only build the parsers behind them.
Definition: you may only test systems you own or have explicit written permission to test. Plain explanation: the categories in this track map to activities that are illegal against systems you do not control. The track sidesteps this by using fixtures — but the habit of checking authorization must start now. Pitfall: "it's just a scan" — an unauthorized scan can still be a crime and can still break a fragile system.
THREAT MODEL — the C-for-Kali learning environment
TRUSTED SIDE (the lab) | UNTRUSTED SIDE
|
+-----------------------------+ | (NOT USED IN THIS TRACK)
| Your machine / container | | - live networks
| | | - third-party hosts
| ASSET: your learning env | | - real production data
| ASSET: memory safety of | |
| your parser | |
| | |
| ENTRY POINT: static | |
| fixture bytes ============|===X=====|== no path to the outside
| | | TRUST |
| v | BOUNDARY|
| [ defensive parser ] | |
| | | |
| v | |
| structured answer + tests | |
+-----------------------------+ |
The X marks the boundary the track never crosses: fixture bytes are
treated as untrusted INPUT, but nothing ever leaves to a live target.
Knowledge check
The recurring shape across the whole track is: take a const view of input plus its length, walk it with bounds checks, return a small structured result. A minimal signature and safe loop:
#include <stddef.h> /* size_t */
/* const input + explicit length: never trust a NUL to end binary data */
int count_markers(const unsigned char *buf, size_t len, unsigned char marker)
{
if (buf == NULL) return -1; /* validate the pointer */
int count = 0;
for (size_t i = 0; i < len; i++) /* i < len is the bounds check */
if (buf[i] == marker)
count++;
return count;
}
Key points annotated:
const unsigned char * — you promise not to modify the input; unsigned char is the correct type for raw bytes.len explicitly — binary formats contain 0x00 bytes, so you cannot rely on strlen.i < len — the guard that keeps every read inside the buffer. This one comparison prevents out-of-bounds reads.The C-for-Kali track teaches the C-programming foundations behind each Kali Linux tool category.
Every module is written as a defensive parser — code that reads and inspects data rather than attacking anything. Each parser runs on static fixtures: fixed sample data baked into the exercise's test harness.
No exercise touches a live network. None of them open a raw socket, capture a real packet, or run a Kali binary.
This page is a tour. Pick the row that matches what you want to understand, then click through to that module.
count_open_ports).detect_brute_force).pw_check).has_sqli_markers).render_finding).parse_request_line).read_pcap_header).parse_sip_method).classify_frame).extract_local_name)..symtab (count_global_symbols).detect_firmware_type).read_mft_name).recover_8_3).parse_iq_header).count_unique_domains).count_high_severity).phishy_score).format_uid).has_canary_pattern).LLVMFuzzerTestOneInput).ct_memcmp).write_ustar_header).Everything in this track is defensive, lab-only, and works on static fixtures.
The modules teach the parsers that sit behind real tools. They never teach you how to drive a tool against a live target.
If you want to go deeper on any one row, the linked lesson explains the on-wire or on-disk format in detail. The linked exercise then asks you to write the matching parser.
Below is a small, self-contained helper in the spirit of the track: recognise whether a tool name is part of a known allow-list. It shows the INSECURE → SECURE → VERIFY shape.
/* WARNING: intentionally vulnerable — use only in a local, isolated,
authorized lab. Do not deploy. */
#include <stdio.h>
#include <string.h>
/* INSECURE: copies a caller-controlled name into a fixed buffer with no
bounds check, then "matches" with a naive prefix compare. A long name
overflows `local`; a partial name like "nm" wrongly matches "nmap". */
int is_kali_tool_bad(const char *name)
{
char local[8];
strcpy(local, name); /* BUG: unbounded copy -> overflow */
if (strncmp(local, "nmap", 2) == 0) /* BUG: prefix, not exact match */
return 1;
return 0;
}
/* SECURE version: fixed allow-list, exact comparison, no copying,
NULL-checked. This is the pattern the exercises expect. */
#include <stdio.h>
#include <string.h>
#include <stddef.h>
int is_kali_tool(const char *name)
{
static const char *const tools[] = {
"nmap", "metasploit", "wireshark", "hydra", "john",
"aircrack-ng", "sqlmap", "nikto", "burpsuite", "gobuster"
};
const size_t n = sizeof(tools) / sizeof(tools[0]);
if (name == NULL) return 0; /* reject NULL safely */
for (size_t i = 0; i < n; i++)
if (strcmp(name, tools[i]) == 0) /* exact, full match */
return 1;
return 0;
}
/* VERIFY: prove the fix REJECTS bad input and ACCEPTS good input. */
int main(void)
{
struct { const char *in; int want; } cases[] = {
{ "nmap", 1 }, /* good: exact known tool */
{ "aircrack-ng", 1 }, /* good: exact known tool */
{ "nm", 0 }, /* bad: partial name must NOT match*/
{ "NMAP", 0 }, /* bad: case-sensitive, no match */
{ "rm -rf /", 0 }, /* bad: junk input rejected */
{ NULL, 0 }, /* bad: NULL handled, no crash */
};
const size_t n = sizeof(cases) / sizeof(cases[0]);
int failures = 0;
for (size_t i = 0; i < n; i++) {
int got = is_kali_tool(cases[i].in);
const char *label = cases[i].in ? cases[i].in : "(NULL)";
printf("%-12s -> %d (want %d) %s\n",
label, got, cases[i].want,
got == cases[i].want ? "ok" : "FAIL");
if (got != cases[i].want) failures++;
}
printf("%s\n", failures == 0 ? "ALL PASS" : "SOME FAILED");
return failures == 0 ? 0 : 1;
}
Expected output:
nmap -> 1 (want 1) ok
aircrack-ng -> 1 (want 1) ok
nm -> 0 (want 0) ok
NMAP -> 0 (want 0) ok
rm -rf / -> 0 (want 0) ok
(NULL) -> 0 (want 0) ok
ALL PASS
The insecure version is here only to name the two bugs you must avoid (unbounded copy, partial match). The secure version never copies the input, compares whole strings, and handles NULL — the exact discipline every parser exercise in this track rewards.
Walking the SECURE is_kali_tool and its test:
static const char *const tools[] = {...} — a fixed allow-list. static gives it program lifetime; const char *const means neither the pointers nor the strings change. An allow-list (only these are valid) is safer than a deny-list (block bad ones), because you can never forget an entry.const size_t n = sizeof(tools)/sizeof(tools[0]) — computes the element count from the array itself, so adding a tool never requires touching the loop bound.if (name == NULL) return 0; — the input is untrusted; a NULL pointer would crash strcmp. Reject it before use.for (size_t i = 0; i < n; i++) — bounded loop over the allow-list; i < n can never read past the array.if (strcmp(name, tools[i]) == 0) return 1; — strcmp returns 0 only on a full match. This is why "nm" fails: strcmp("nm", "nmap") is non-zero.return 0; — default deny: unknown input is not a tool.Trace of the test cases:
| input | NULL? | first exact match | result |
|---|---|---|---|
nmap |
no | index 0 | 1 |
aircrack-ng |
no | index 5 | 1 |
nm |
no | none (prefix ≠ full) | 0 |
NMAP |
no | none (case differs) | 0 |
rm -rf / |
no | none | 0 |
NULL |
yes | — (returns early) | 0 |
Every row lands on the expected value, so failures stays 0 and main prints ALL PASS and returns 0.
1. Using strcpy / unbounded copies.
Wrong: strcpy(local, name) into a fixed buffer.
Why wrong: a name longer than the buffer overflows the stack — a classic, exploitable memory-safety bug.
Corrected: don't copy at all when you only need to compare; if you must copy, use a bounded copy and always NUL-terminate, checking the source length first.
Recognise/prevent: compile with -Wall -Wextra; run under AddressSanitizer (-fsanitize=address); grep your code for strcpy, strcat, sprintf, gets.
2. Prefix or substring matching where you need exact.
Wrong: strncmp(name, "nmap", 2) treats "nm", "nmapX" as matches.
Why wrong: it accepts inputs you meant to reject, weakening any allow-list.
Corrected: use strcmp(...) == 0 for full-string equality.
Recognise/prevent: write a test with a near-miss input ("nm") and confirm it returns 0.
3. Trusting a length field from the data.
Wrong: reading n bytes where n came from the input, into a smaller buffer.
Why wrong: hostile input sets n huge → out-of-bounds read/write.
Corrected: clamp: size_t take = n < cap ? n : cap; and never read past the buffer's own len.
Recognise/prevent: every read guarded by i < len; fuzz the parser.
4. Confusing this track with running live tools. Wrong: "the recon module means I should scan a network." Why wrong: unauthorized scanning is unethical and often illegal; the module only parses a fixture. Corrected: keep to fixtures; run real tools only on systems you own or are authorized to test. Recognise/prevent: if an activity leaves your machine and touches someone else's system, stop and check authorization.
gcc -g -fsanitize=address,undefined and rerun — ASan prints the exact line and access.strncmp with a short length) or memcmp with the wrong size. Switch to strcmp for whole strings, and print both operands to confirm.strlen on data containing 0x00. Pass an explicit len and loop i < len.Memory safety (C). Every module here reads attacker-shaped bytes, so the same rules apply throughout:
i < len.const and don't write through it.-Wall -Wextra -fsanitize=address,undefined; these catch overflows and UB the compiler otherwise ignores.Security & safety — detection and logging. Even a parser deserves an audit trail when it rejects input, because rejections can signal an attack (malformed packets, oversized length fields, junk tool names). For each significant decision, log: a UTC timestamp, the source (file name, fixture id, or connection id — not raw payload), the resource being parsed, the result (accepted/rejected + reason code), the security decision (e.g. "length clamped", "NULL rejected"), and a correlation id to tie related events together. Never log passwords, tokens, session cookies, private keys, full PANs, or raw payloads that may contain secrets or PII. Events that signal abuse: a burst of malformed inputs from one source, repeated oversized-length rejections, or a spike in parse failures. Remember false positives: a legitimate new firmware version or a benign truncated capture can trip a strict parser — tune thresholds and review, do not auto-block on a single event.
Authorized real-world use. A blue-team analyst writes a small C tool that reads nmap's XML export from an authorized internal scan and produces a triage list of newly-opened ports. The scan itself was scoped and approved in writing; the parser runs offline on the saved output. That is exactly count_open_ports scaled up — and the safe way to apply what the track teaches.
Best-practice habits (all tracks):
Beginner vs advanced.
| Beginner | Advanced | |
|---|---|---|
| Input | one static fixture | fuzzed + real authorized captures |
| Goal | correct answer | correct and crash-free on hostile input |
| Tooling | -Wall -Wextra |
ASan/UBSan, libFuzzer, CI gates |
| Logging | print result | structured audit log with correlation ids |
| Scope | localhost only | scoped, written authorization for any live data |
Beginner 1 — Read the map. Objective: choose your path. Requirements: from the four categories (networking/web, binary/forensic, text/score, closing patterns), pick one module whose format you want to learn and write two sentences on what data it parses and what its function returns. Constraints: no code; use only the module descriptions on this page. Hints: match the category to a real job (e.g. forensics → filesystem records). Concepts: tool categories, defensive parser.
Beginner 2 — Safe allow-list check.
Objective: implement int is_known_category(const char *name) returning 1 for exactly "recon", "web", "forensics", else 0.
Requirements: fixed allow-list, exact strcmp, NULL-safe.
Input/output: "web"→1, "we"→0, NULL→0.
Constraints: no copying, no dynamic memory.
Hints: reuse the is_kali_tool shape.
Concepts: allow-list, default deny, exact match.
Intermediate 1 — Bounded byte counter.
Objective: int count_byte(const unsigned char *buf, size_t len, unsigned char target) returns how many times target appears, or -1 if buf is NULL.
Requirements: loop guarded by i < len; handle len == 0.
Input/output: {0x01,0x02,0x02}, 3, 0x02 → 2; NULL,5,0x00 → -1.
Constraints: no strlen (binary-safe).
Hints: unsigned char for raw bytes.
Concepts: bounds checking, explicit length.
Intermediate 2 — Reject the near-miss.
Objective: extend Beginner 2 with a test suite that includes at least one prefix near-miss and one case-mismatch, and prove both are rejected.
Requirements: a main that prints each case and a final PASS/FAIL, exit code non-zero on any failure.
Input/output: table like the lesson's VERIFY block.
Constraints: deterministic, no external input.
Hints: model it on the cases[] array shown above.
Concepts: test-driven verification, exact match.
Challenge — Length-field hardening (lab-only, defensive).
Objective: given a mock record [len:1 byte][name:len bytes] inside a fixed buffer, write int read_name(const unsigned char *buf, size_t buflen, char *out, size_t outcap) that copies the name safely and returns 0 on success, -1 on any inconsistency.
Requirements: reject buflen == 0; clamp the declared len to both buflen-1 and outcap-1; always NUL-terminate out; never read or write out of bounds.
Input/output: a record claiming len=200 inside an 8-byte buffer must return -1, not overflow.
Constraints: lab-only, static fixtures, no network.
Defensive conclusion: after coding, remediate and verify — build with -fsanitize=address,undefined, feed an oversized-length fixture, and confirm the sanitizer stays silent and the function returns -1. Add a log line recording the rejection (timestamp, fixture id, reason oversized-length).
Hints: compute take = min(declared_len, buflen-1, outcap-1) before copying.
Concepts: never trust a length from the data, clamping, NUL-termination, mitigation verification.
const and untrusted, pass an explicit len, guard every read with i < len, and never trust a length field that came from the data.strcmp for names; bounded loops for bytes; clamp sizes before copying; NUL-terminate manually.strcpy/unbounded copies, prefix matches where you need exact, trusting attacker-supplied lengths, and confusing "a recon parser" with "run a scan". Build with -Wall -Wextra -fsanitize=address,undefined.