Skip to content
Markdown

Offensive security tooling in Rust

Scope: Black Hat Rust (skerkour/black-hat-rust, MIT), the companion source code for a book on building offensive-security tools in Rust. This page covers what the codebase teaches (recon scanners, crawlers, fuzzers, exploit and shellcode delivery, phishing tooling, and a remote-access trojan), why Rust is a fit for this work, and the concrete engineering lesson that recurs through the book: naive .unwrap() error handling that crashes on real-world input, replaced chapter by chapter with proper Result propagation. It sits in the agentic cybersecurity and SysOps index as the tool-building-craft entry beneath the agent-driven pentesters, and it complements offensive AI and the arms race.

Examined at commit 24c86c53 (2023-06-06) of skerkour/black-hat-rust, MIT. This is book companion code, not a maintained tool. The ch_02 tricoder scanner was built and run on this host: cargo build succeeded in about 19 seconds against the pinned trust-dns-resolver = "0.21" and reqwest = "0.11" dependencies, and running ./tricoder localhost panicked at src/main.rs:37 (.unwrap() on a reqwest JSON-decode error), because crt.sh returns non-JSON for a %.localhost query. That is the exact failure ch_03 fixes. The subdomain set-cleaning model below is executed and asserted as standalone Python; the port scan was demonstrated against loopback only.

What it is

Black Hat Rust is a from-theory-to-practice book whose repository holds the working code for each chapter. It is not a metasploit tutorial; it builds the tools from scratch in Rust. The arc:

  • ch_01 a SHA-1 password cracker; ch_02 to ch_04 tricoder, a subdomain-and-port reconnaissance scanner that evolves from synchronous rayon to async.
  • ch_05 a web crawler; ch_06 fuzzing and an XSS-via-SVG payload.
  • ch_07 memory-corruption exploits; ch_08 shellcode (a reverse_tcp shell, a custom linker script).
  • ch_09 phishing infrastructure (dnsquat typosquatting, email tooling, an evil_twin page).
  • ch_10 to ch_13 a full remote-access trojan (agent, client, server, common crates, plus shellcode delivery).
  • ch_14 an engagement report.

The recurring engineering theme is Rust's error model. Early chapters use .unwrap() to keep the teaching code short; later chapters replace it with Result, the ? operator, and typed errors, so the tool degrades gracefully instead of panicking. The tricoder evolution is the clearest example, and it is executable.

Why Rust for offensive tooling

  • One language across the stack. Rust writes shellcode, servers, crawlers, and phishing pages, which is unusual: most offensive work spans C for implants and Python for orchestration. Rust covers both.
  • Memory safety without a runtime. An implant or scanner written in safe Rust avoids the memory-corruption bugs that plague C tooling, and it ships as a static binary with no interpreter to install on the target.
  • Fearless concurrency. The port scanner is embarrassingly parallel; Rust's rayon (and later tokio) make that safe to express. tricoder uses a custom rayon pool with num_threads(256).
  • Cross-compilation and small binaries. Rust cross-compiles to the targets red teams care about and produces compact, dependency-free artifacts.

When to use it (and when not)

Use it as a learning resource for building your own authorized offensive tools in Rust, and as a reference for idiomatic patterns (concurrency, error handling, HTTP clients, DNS resolution). The code is a teaching scaffold you extend, not a product you deploy.

Do not run the RAT, phishing, or exploit code against anything you do not own or are not explicitly authorized to test; this is dual-use offensive tooling and the book frames it for authorized security work. Do not treat the early-chapter code as production-safe: as shown below, ch_02's tricoder panics on an unexpected response because it .unwrap()s a network result; that is a pedagogical choice the book corrects, not a pattern to copy. Do not assume the 2023 dependency pins are current; they build today but carry their own age.

Architecture

flowchart TB
  subgraph RECON["Recon (ch_02 to ch_05)"]
    SUB["crt.sh subdomain enumeration"] --> CLEAN["Set-clean: dedup, drop wildcards"]
    CLEAN --> SCAN["Parallel port scan (rayon 256 threads)"]
  end
  subgraph OFFENSE["Offense (ch_06 to ch_13)"]
    FUZZ["Fuzzing + payloads"]
    EXP["Exploits + shellcode"]
    RAT["RAT: agent / client / server"]
  end
  RECON --> OFFENSE
  OFFENSE --> REP["ch_14 report"]

The scanner core and its ch_02 crash, executed

tricoder's recon core is two steps: enumerate() pulls certificate-transparency names from crt.sh, splits multi-name certs on newlines, trims, drops the bare target and any wildcard entry, and dedups through a HashSet; then scan_ports() sweeps the common ports in parallel. The model below reproduces the set-cleaning exactly and demonstrates the ch_02 failure mode I reproduced by building and running the real binary.

def clean_subdomains(crtsh_name_values, target):
    """Mirror subdomains.rs: split on '\n', trim, drop target, drop wildcards, dedup."""
    seen = set()
    for entry in crtsh_name_values:
        for name in entry.split("\n"):
            name = name.strip()
            if not name or name == target or "*" in name:
                continue
            seen.add(name)
    seen.add(target)                      # the target itself is always scanned
    return sorted(seen)


crtsh = [
    "www.example.com\napi.example.com",
    "  api.example.com  ",            # duplicate with whitespace
    "*.example.com",                  # wildcard -> dropped
    "example.com",                    # bare target -> dropped then re-added once
    "mail.example.com",
]
subs = clean_subdomains(crtsh, "example.com")
print("subdomains:", subs)
assert subs == ["api.example.com", "example.com", "mail.example.com", "www.example.com"]
assert "*.example.com" not in subs and subs.count("api.example.com") == 1

# Bounded-parallel port scan (rayon num_threads(256) over the common ports).
def scan_ports(host, open_ports, candidate_ports):
    return {"host": host, "open": sorted(p for p in candidate_ports if p in open_ports)}

CANDIDATE = [21, 22, 80, 443, 3306, 8080]
truth = {"www.example.com": {80, 443}, "api.example.com": {443, 8080}}
results = [scan_ports(h, truth.get(h, set()), CANDIDATE) for h in subs]
for r in results:
    if r["open"]:
        print(f"  {r['host']}: {r['open']}")
assert next(r for r in results if r["host"] == "api.example.com")["open"] == [443, 8080]

# The ch_02 panic: enumerate() returns Result, but main.rs does `.unwrap()`.
def enumerate_unwrap(raw_is_json):
    if not raw_is_json:
        raise RuntimeError("Reqwest: error decoding response body (called unwrap on Err)")
    return ["ok.example.com"]

try:
    enumerate_unwrap(raw_is_json=False)   # crt.sh returned non-JSON (e.g. for localhost)
    raise AssertionError("expected panic")
except RuntimeError as e:
    print("ch_02 unwrap panic reproduced:", str(e)[:52], "...")
print("OK: set-cleaning dedups CT names and drops wildcards; the ch_02 unwrap is a "
      "real crash on a bad crt.sh reply, which ch_03's Result-based main fixes")

Executed output:

subdomains: ['api.example.com', 'example.com', 'mail.example.com', 'www.example.com']
  api.example.com: [443, 8080]
  www.example.com: [80, 443]
ch_02 unwrap panic reproduced: Reqwest: error decoding response body (called unwrap ...
OK: set-cleaning dedups CT names and drops wildcards; the ch_02 unwrap is a real crash on a bad crt.sh reply, which ch_03's Result-based main fixes

Building and running the real ch_02/tricoder confirmed this on hardware: it compiled cleanly, then ./tricoder localhost panicked at src/main.rs:37 with exactly the reqwest JSON-decode error the model raises, because main does subdomains::enumerate(&http_client, target).unwrap(). ch_03 rewrites main as async fn main() -> Result<(), anyhow::Error> and propagates with ? (let subdomains = subdomains::enumerate(&http_client, target).await?;), so the same bad response becomes a handled error instead of a crash. That is the book's whole point about Rust error handling, made concrete by running the code.

How to use it

Each chapter is a Cargo project. Build and run the scanner:

cd ch_02/tricoder
cargo run -- example.com     # authorized targets only

The RAT chapters (ch_10, ch_11) need a Postgres instance (the README gives a docker run postgres:13 line) and split into agent, client, server, and common crates. Treat every chapter as a sandbox exercise: build in a disposable environment, and never point the phishing or RAT code at systems you do not control.

How to develop with it

The value is the patterns, not the binaries. Read the ch_02 to ch_04 tricoder diff to see the migration from synchronous rayon with .unwrap() to async tokio with typed errors; that is the single most useful sequence for anyone writing Rust tooling. Extend a chapter by adding a probe or a module (a new port list, a new crawler rule, a new RAT command) rather than rewriting; the crate boundaries (common for shared types) show how to keep an implant and its C2 in sync.

How to maintain it

There is nothing to maintain: the repository is a book artifact, last touched in 2023 and pinned to a 2023 dependency set (trust-dns-resolver = "0.21", reqwest = "0.11", rayon = "1.5"). Those pins still resolve and build today, but for real tooling you would move trust-dns-resolver to its current successor and update reqwest, and adopt the async error handling from the later chapters throughout.

How to run it in production

This code is not for production; it is a teaching scaffold for building your own tools. If you carry a pattern from it into a real, authorized tool, keep the later-chapter discipline: no .unwrap() on I/O, typed errors with ?, scope-locked targets, and rate limits so a parallel scan does not become an accidental denial of service. Anything you build from the RAT or phishing chapters is offensive infrastructure and belongs only inside an authorized engagement with the containment this knowledge base describes in sandboxing and isolation.

Failure modes

  • .unwrap() on I/O panics. Demonstrated: the ch_02 scanner crashes on any non-JSON crt.sh response. Early-chapter code trades robustness for brevity; do not ship it.
  • Aged dependencies. The 2023 pins build now but are not current; trust-dns-resolver has a modern successor and reqwest/hyper have moved on.
  • Dual-use by design. RAT, phishing, and exploit code is offensive. Legal authorization is the prerequisite, and the book says so.
  • No target guards in the sample code. The scanner will scan whatever you pass it; add scope and rate limiting before using anything derived from it.

References

  • Black Hat Rust repository (skerkour), pinned commit 24c86c53: https://github.com/skerkour/black-hat-rust
  • ch_02 tricoder (synchronous, .unwrap()): https://github.com/skerkour/black-hat-rust/tree/main/ch_02/tricoder
  • ch_03 tricoder (async, Result-based main): https://github.com/skerkour/black-hat-rust/tree/main/ch_03/tricoder
  • Book page: https://kerkour.com/black-hat-rust

Related: Agentic cybersecurity and SysOps index · Offensive AI and the arms race · Autonomous web pentesting with Shannon · Agent sandboxing and isolation · Earned-verdict pentesting with ptai