flaghunter 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jake Burre
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: flaghunter
3
+ Version: 0.1.0
4
+ Summary: CTF crypto autosolver and pcap hunter. Point it at a challenge file or packet capture and it runs a full attack battery for you.
5
+ Author: Jake Burre
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/burrejak22/flaghunter
8
+ Project-URL: Repository, https://github.com/burrejak22/flaghunter
9
+ Keywords: ctf,crypto,pcap,forensics,security,codebreakers
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Topic :: Security
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: cryptsmith>=0.1.0
19
+ Dynamic: license-file
20
+
21
+ # flaghunter
22
+
23
+ Point it at a CTF crypto challenge or a packet capture and it runs a full attack battery for you, then writes up what it found.
24
+
25
+ Built on [cryptsmith](https://github.com/burrejak22/cryptsmith), which does all the crypto and pcap heavy lifting. flaghunter is the analyst on top.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install flaghunter
31
+ ```
32
+
33
+ ## Use
34
+
35
+ ```bash
36
+ # throw a crypto challenge file at it
37
+ flaghunter solve challenge.txt
38
+
39
+ # hunt a pcap for enrollment keys, flags, and obfuscated blobs
40
+ flaghunter pcap capture.pcap
41
+
42
+ # custom token pattern, json output
43
+ flaghunter pcap capture.pcap --pattern "FLAG\{[^}]+\}" --format json
44
+ ```
45
+
46
+ ## What the pcap hunter does
47
+
48
+ 1. Reassembles TCP streams from the capture
49
+ 2. Carves printable strings out of every stream
50
+ 3. Hunts tokens matching your pattern (default: 12 char alpha strings, like enrollment keys)
51
+ 4. Scores every stream for entropy, then runs single byte xor, repeating key xor, caesar, and layered base64/hex decoding on anything suspicious
52
+ 5. Writes a markdown report with stream labels, findings, and decoded candidates
53
+
54
+ ## What the solver does
55
+
56
+ Reads a challenge file and runs the battery: encoding layers, xor attacks, classical ciphers, hash identification. Ranks everything by english score so the real answer floats to the top.
57
+
58
+ ## Example
59
+
60
+ Say a capture has a server checking a 12 character alpha enrollment key, and the key is xor obfuscated in the traffic. flaghunter finds the stream, breaks the xor, and hands you the key.
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,44 @@
1
+ # flaghunter
2
+
3
+ Point it at a CTF crypto challenge or a packet capture and it runs a full attack battery for you, then writes up what it found.
4
+
5
+ Built on [cryptsmith](https://github.com/burrejak22/cryptsmith), which does all the crypto and pcap heavy lifting. flaghunter is the analyst on top.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install flaghunter
11
+ ```
12
+
13
+ ## Use
14
+
15
+ ```bash
16
+ # throw a crypto challenge file at it
17
+ flaghunter solve challenge.txt
18
+
19
+ # hunt a pcap for enrollment keys, flags, and obfuscated blobs
20
+ flaghunter pcap capture.pcap
21
+
22
+ # custom token pattern, json output
23
+ flaghunter pcap capture.pcap --pattern "FLAG\{[^}]+\}" --format json
24
+ ```
25
+
26
+ ## What the pcap hunter does
27
+
28
+ 1. Reassembles TCP streams from the capture
29
+ 2. Carves printable strings out of every stream
30
+ 3. Hunts tokens matching your pattern (default: 12 char alpha strings, like enrollment keys)
31
+ 4. Scores every stream for entropy, then runs single byte xor, repeating key xor, caesar, and layered base64/hex decoding on anything suspicious
32
+ 5. Writes a markdown report with stream labels, findings, and decoded candidates
33
+
34
+ ## What the solver does
35
+
36
+ Reads a challenge file and runs the battery: encoding layers, xor attacks, classical ciphers, hash identification. Ranks everything by english score so the real answer floats to the top.
37
+
38
+ ## Example
39
+
40
+ Say a capture has a server checking a 12 character alpha enrollment key, and the key is xor obfuscated in the traffic. flaghunter finds the stream, breaks the xor, and hands you the key.
41
+
42
+ ## License
43
+
44
+ MIT
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "flaghunter"
7
+ version = "0.1.0"
8
+ description = "CTF crypto autosolver and pcap hunter. Point it at a challenge file or packet capture and it runs a full attack battery for you."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Jake Burre" }]
13
+ keywords = ["ctf", "crypto", "pcap", "forensics", "security", "codebreakers"]
14
+ dependencies = ["cryptsmith>=0.1.0"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Information Technology",
18
+ "Topic :: Security",
19
+ "Programming Language :: Python :: 3",
20
+ "License :: OSI Approved :: MIT License",
21
+ ]
22
+
23
+ [project.scripts]
24
+ flaghunter = "flaghunter.cli:main"
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/burrejak22/flaghunter"
28
+ Repository = "https://github.com/burrejak22/flaghunter"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """flaghunter: CTF crypto autosolver and pcap hunter."""
2
+
3
+ from . import cli, pcaphunt, solver
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["cli", "pcaphunt", "solver"]
@@ -0,0 +1,61 @@
1
+ """flaghunter command line interface."""
2
+ import argparse
3
+ import sys
4
+
5
+ from .pcaphunt import hunt_pcap, render_json, render_markdown
6
+ from .solver import render_markdown as solve_markdown
7
+ from .solver import solve_file
8
+
9
+
10
+ def _cmd_solve(args: argparse.Namespace) -> int:
11
+ report = solve_file(args.file)
12
+ print(solve_markdown(report))
13
+ return 0
14
+
15
+
16
+ def _cmd_pcap(args: argparse.Namespace) -> int:
17
+ try:
18
+ report = hunt_pcap(args.file, key_len=args.key_len, pattern=args.pattern)
19
+ except FileNotFoundError:
20
+ print(f"error: file not found: {args.file}", file=sys.stderr)
21
+ return 1
22
+ except ValueError as e:
23
+ print(f"error: {e}", file=sys.stderr)
24
+ return 1
25
+ if args.format == "json":
26
+ print(render_json(report))
27
+ else:
28
+ print(render_markdown(report))
29
+ return 0
30
+
31
+
32
+ def build_parser() -> argparse.ArgumentParser:
33
+ parser = argparse.ArgumentParser(
34
+ prog="flaghunter",
35
+ description="CTF crypto autosolver and pcap hunter. Runs the attack battery so you don't have to.")
36
+ sub = parser.add_subparsers(dest="command", required=True)
37
+
38
+ solve = sub.add_parser("solve", help="run the attack battery against a challenge file")
39
+ solve.add_argument("file", help="challenge file to analyze")
40
+ solve.set_defaults(func=_cmd_solve)
41
+
42
+ pcap_cmd = sub.add_parser("pcap", help="hunt a packet capture for tokens and obfuscated blobs")
43
+ pcap_cmd.add_argument("file", help="pcap file to analyze")
44
+ pcap_cmd.add_argument("--key-len", type=int, default=12,
45
+ help="length of alpha tokens to hunt (default 12)")
46
+ pcap_cmd.add_argument("--pattern", default=None,
47
+ help="custom regex for token hunting, overrides --key-len")
48
+ pcap_cmd.add_argument("--format", choices=["md", "json"], default="md",
49
+ help="report format (default md)")
50
+ pcap_cmd.set_defaults(func=_cmd_pcap)
51
+ return parser
52
+
53
+
54
+ def main(argv: list[str] | None = None) -> int:
55
+ parser = build_parser()
56
+ args = parser.parse_args(argv)
57
+ return args.func(args)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ sys.exit(main())
@@ -0,0 +1,178 @@
1
+ """Pcap hunting: reassemble streams, find tokens, break obfuscated blobs.
2
+
3
+ The main event for captures with custom protocols and hidden keys, like a
4
+ distribution server checking a 12 character alpha enrollment key.
5
+ """
6
+ import json
7
+ from dataclasses import dataclass, field
8
+
9
+ from cryptsmith import analysis, pcap, xor
10
+ from cryptsmith import encoding as enc
11
+
12
+ from .solver import analyze_blob
13
+
14
+
15
+ @dataclass
16
+ class StreamFinding:
17
+ stream: str
18
+ kind: str
19
+ detail: str
20
+ value: str
21
+ score: float = 0.0
22
+
23
+
24
+ @dataclass
25
+ class PcapReport:
26
+ source: str
27
+ streams: int
28
+ packets: int
29
+ findings: list[StreamFinding] = field(default_factory=list)
30
+
31
+
32
+ def _summarize_stream(s: pcap.TCPStream) -> dict:
33
+ return {
34
+ "a_to_b_bytes": len(s.a_to_b),
35
+ "b_to_a_bytes": len(s.b_to_a),
36
+ "a_to_b_entropy": round(analysis.shannon_entropy(s.a_to_b), 2) if s.a_to_b else 0.0,
37
+ "b_to_a_entropy": round(analysis.shannon_entropy(s.b_to_a), 2) if s.b_to_a else 0.0,
38
+ }
39
+
40
+
41
+ # words that suggest the decode is real protocol or challenge text, not luck
42
+ _CONTEXT_KEYWORDS = {
43
+ "enroll", "key", "flag", "auth", "login", "password", "passwd", "token",
44
+ "secret", "challenge", "session", "crypto", "decrypt", "encrypt", "admin",
45
+ "server", "client", "hello", "welcome", "granted", "denied", "payload",
46
+ }
47
+
48
+
49
+ def _context_score(data: bytes) -> float:
50
+ """Score a decode context, tolerant of binary framing.
51
+
52
+ Plain english scoring plus a bonus when the context contains security
53
+ flavored words, which is a strong hint the xor key is right.
54
+ """
55
+ text = "".join(chr(b) for b in data if 32 <= b < 127)
56
+ if len(text) < 8:
57
+ return float("-inf")
58
+ score = analysis.english_score(text)
59
+ low = text.lower()
60
+ if any(w in low for w in _CONTEXT_KEYWORDS):
61
+ score += 50.0
62
+ return score
63
+
64
+
65
+ def hunt_pcap(path: str, key_len: int = 12, pattern: str | None = None,
66
+ min_blob: int = 16) -> PcapReport:
67
+ """Analyze a pcap: token hunt plus attack battery on suspicious blobs."""
68
+ reader = pcap.PcapReader(path)
69
+ packets = sum(1 for _ in reader.packets())
70
+ streams = pcap.reassemble_streams(pcap.PcapReader(path))
71
+ report = PcapReport(source=path, streams=len(streams), packets=packets)
72
+
73
+ token_hunter = (lambda d: pcap.hunt_pattern(d, pattern)) if pattern else (
74
+ lambda d: pcap.hunt_alpha_tokens(d, length=key_len))
75
+
76
+ for s in streams:
77
+ payload = s.payload
78
+
79
+ # 1. token hunt on the raw payload
80
+ for token in token_hunter(payload):
81
+ report.findings.append(StreamFinding(
82
+ stream=s.label, kind="token",
83
+ detail=f"candidate {'custom pattern' if pattern else f'{key_len} char alpha'} token",
84
+ value=token, score=100.0))
85
+
86
+ # 2. interesting strings
87
+ strings = pcap.carve_strings(payload, min_len=6)
88
+ for st in strings:
89
+ for token in token_hunter(st.encode("latin1")):
90
+ if not any(f.value == token for f in report.findings):
91
+ report.findings.append(StreamFinding(
92
+ stream=s.label, kind="token",
93
+ detail="token inside carved string", value=token, score=90.0))
94
+
95
+ # 3. attack battery per direction: encodings on strings, xor on opaque runs
96
+ for direction, blob in (("a->b", s.a_to_b), ("b->a", s.b_to_a)):
97
+ if len(blob) < min_blob:
98
+ continue
99
+ for st in pcap.carve_strings(blob, min_len=12):
100
+ for chain, decoded in enc.smart_decode(st.encode("latin1")):
101
+ try:
102
+ text = decoded.decode("utf-8", errors="strict").strip()
103
+ except Exception:
104
+ continue
105
+ if len(text) >= 4 and all(32 <= ord(c) < 127 or c in " \t" for c in text):
106
+ for token in token_hunter(text.encode("latin1")):
107
+ report.findings.append(StreamFinding(
108
+ stream=s.label, kind="decoded-token",
109
+ detail=f"token after {chain} decode ({direction})",
110
+ value=token, score=95.0))
111
+ # xor obfuscated token scan: finds 12+ alpha runs under any single
112
+ # byte xor key, even buried in mixed binary. O(n) bitmask scan.
113
+ # near miss keys cluster at the same offset, so group them into
114
+ # one finding per offset and let the analyst pick.
115
+ groups: dict[int, list[tuple[float, int, str]]] = {}
116
+ for offset, key, token in pcap.xor_token_scan(blob, min_length=key_len):
117
+ ctx = blob[max(0, offset - 24):offset + len(token) + 24]
118
+ ctx_pt = xor.xor_bytes(ctx, bytes([key]))
119
+ score = _context_score(ctx_pt)
120
+ groups.setdefault(offset, []).append((score, key, token))
121
+ for offset, cands in groups.items():
122
+ cands.sort(key=lambda c: -c[0])
123
+ lines = [f"0x{k:02x}: {t}" for _, k, t in cands]
124
+ best = cands[0][0]
125
+ report.findings.append(StreamFinding(
126
+ stream=s.label, kind="xor-token",
127
+ detail=(f"{len(cands)} xor keys yield {key_len}+ alpha runs "
128
+ f"at offset {offset} ({direction})"),
129
+ value="\n".join(lines), score=max(best, 1.0)))
130
+ # full solver battery as a last resort, strict threshold
131
+ if analysis.shannon_entropy(blob) > 5.5 and len(blob) >= 32:
132
+ for f in analyze_blob(blob, max_findings=3):
133
+ if f.score > 40 and f.plaintext:
134
+ report.findings.append(StreamFinding(
135
+ stream=s.label, kind=f"auto-{f.method}",
136
+ detail=f"{f.detail} ({direction})",
137
+ value=f.plaintext[:200], score=min(f.score, 80.0)))
138
+
139
+ # dedupe, keep highest score per (stream, kind, value)
140
+ seen: dict[tuple, StreamFinding] = {}
141
+ for f in report.findings:
142
+ k = (f.stream, f.kind, f.value)
143
+ if k not in seen or f.score > seen[k].score:
144
+ seen[k] = f
145
+ report.findings = sorted(seen.values(), key=lambda f: -f.score)
146
+ return report
147
+
148
+
149
+ def render_markdown(report: PcapReport) -> str:
150
+ lines = ["# flaghunter pcap report", "",
151
+ f"source: `{report.source}`",
152
+ f"packets: {report.packets}, tcp streams: {report.streams}", ""]
153
+ if not report.findings:
154
+ lines.append("no tokens or decodable blobs found.")
155
+ return "\n".join(lines)
156
+ lines.append(f"## findings ({len(report.findings)})")
157
+ lines.append("")
158
+ for i, f in enumerate(report.findings, 1):
159
+ lines.append(f"### {i}. [{f.kind}] {f.detail}")
160
+ lines.append(f"stream: `{f.stream}`")
161
+ lines.append("```")
162
+ lines.append(f.value)
163
+ lines.append("```")
164
+ lines.append("")
165
+ return "\n".join(lines)
166
+
167
+
168
+ def render_json(report: PcapReport) -> str:
169
+ return json.dumps({
170
+ "source": report.source,
171
+ "packets": report.packets,
172
+ "streams": report.streams,
173
+ "findings": [
174
+ {"stream": f.stream, "kind": f.kind, "detail": f.detail,
175
+ "value": f.value, "score": round(f.score, 1)}
176
+ for f in report.findings
177
+ ],
178
+ }, indent=2)
@@ -0,0 +1,123 @@
1
+ """Attack battery for crypto challenge files.
2
+
3
+ Reads a challenge blob and runs every cheap attack, ranking results so the
4
+ real answer floats to the top.
5
+ """
6
+ from dataclasses import dataclass, field
7
+
8
+ from cryptsmith import analysis, classical, encoding, hashid, xor
9
+
10
+
11
+ @dataclass
12
+ class Finding:
13
+ method: str
14
+ detail: str
15
+ plaintext: str
16
+ score: float
17
+
18
+
19
+ @dataclass
20
+ class SolveReport:
21
+ source: str
22
+ findings: list[Finding] = field(default_factory=list)
23
+
24
+ def top(self, n: int = 10) -> list[Finding]:
25
+ return sorted(self.findings, key=lambda f: -f.score)[:n]
26
+
27
+
28
+ def _printable(data: bytes) -> str | None:
29
+ try:
30
+ text = data.decode("utf-8")
31
+ except Exception:
32
+ try:
33
+ text = data.decode("latin1")
34
+ except Exception:
35
+ return None
36
+ if not text or sum(32 <= ord(c) < 127 or c in "\n\r\t" for c in text) / len(text) < 0.85:
37
+ return None
38
+ return text
39
+
40
+
41
+ def analyze_blob(data: bytes, max_findings: int = 25) -> list[Finding]:
42
+ """Run the full battery against one blob of bytes."""
43
+ findings: list[Finding] = []
44
+
45
+ def add(method: str, detail: str, raw: bytes):
46
+ text = _printable(raw)
47
+ if text is None:
48
+ return
49
+ score = analysis.english_score(text)
50
+ if score == float("-inf"):
51
+ return
52
+ findings.append(Finding(method, detail, text.strip()[:500], score))
53
+
54
+ # 1. layered encodings
55
+ for chain, decoded in encoding.smart_decode(data):
56
+ add("encoding", f"decoded via {chain}", decoded)
57
+
58
+ text = _printable(data)
59
+
60
+ # 2. hash id on standalone hex digests
61
+ if text:
62
+ for token in text.split():
63
+ names = hashid.identify(token.strip())
64
+ if names:
65
+ findings.append(Finding("hashid", f"{token.strip()[:64]} looks like {', '.join(names)}",
66
+ token.strip()[:200], 0.0))
67
+
68
+ # 3. single byte xor (worth trying on almost anything)
69
+ if len(data) >= 8:
70
+ for key, pt, score in xor.single_byte_xor_break(data, top=2):
71
+ if score > 5:
72
+ findings.append(Finding("xor", f"single byte key 0x{key:02x}", pt.decode("latin1").strip()[:500], score))
73
+
74
+ # 4. repeating key xor on longer blobs
75
+ if len(data) >= 60:
76
+ try:
77
+ for key, pt, score in xor.repeating_key_xor_break(data)[:2]:
78
+ if score > 20:
79
+ findings.append(Finding("xor", f"repeating key {key!r}", pt.decode("latin1").strip()[:500], score))
80
+ except Exception:
81
+ pass
82
+
83
+ # 5. classical ciphers on texty input
84
+ if text and sum(c.isalpha() for c in text) / max(len(text), 1) > 0.6 and len(text) >= 20:
85
+ for shift, pt, score in classical.caesar_bruteforce(text, top=1):
86
+ if score > 5:
87
+ findings.append(Finding("caesar", f"shift {shift}", pt.strip()[:500], score))
88
+ for a, b, pt, score in classical.affine_bruteforce(text, top=1):
89
+ if score > 10:
90
+ findings.append(Finding("affine", f"a={a} b={b}", pt.strip()[:500], score))
91
+
92
+ # 6. notes on randomness
93
+ entropy = analysis.shannon_entropy(data)
94
+ if entropy > 7.5 and len(data) >= 32:
95
+ findings.append(Finding("note", f"high entropy ({entropy:.2f} bits/byte), likely encrypted or compressed",
96
+ "", -100.0))
97
+
98
+ findings.sort(key=lambda f: -f.score)
99
+ return findings[:max_findings]
100
+
101
+
102
+ def solve_file(path: str) -> SolveReport:
103
+ with open(path, "rb") as f:
104
+ data = f.read()
105
+ return SolveReport(source=path, findings=analyze_blob(data))
106
+
107
+
108
+ def render_markdown(report: SolveReport) -> str:
109
+ lines = [f"# flaghunter solve report", "", f"source: `{report.source}`", ""]
110
+ if not report.findings:
111
+ lines.append("no promising findings. the blob resisted the battery.")
112
+ return "\n".join(lines)
113
+ lines.append(f"## top findings ({len(report.findings)})")
114
+ lines.append("")
115
+ for i, f in enumerate(report.top(), 1):
116
+ lines.append(f"### {i}. {f.method} (score {f.score:.1f})")
117
+ lines.append(f"*{f.detail}*")
118
+ if f.plaintext:
119
+ lines.append("```")
120
+ lines.append(f.plaintext)
121
+ lines.append("```")
122
+ lines.append("")
123
+ return "\n".join(lines)
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: flaghunter
3
+ Version: 0.1.0
4
+ Summary: CTF crypto autosolver and pcap hunter. Point it at a challenge file or packet capture and it runs a full attack battery for you.
5
+ Author: Jake Burre
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/burrejak22/flaghunter
8
+ Project-URL: Repository, https://github.com/burrejak22/flaghunter
9
+ Keywords: ctf,crypto,pcap,forensics,security,codebreakers
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Topic :: Security
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: cryptsmith>=0.1.0
19
+ Dynamic: license-file
20
+
21
+ # flaghunter
22
+
23
+ Point it at a CTF crypto challenge or a packet capture and it runs a full attack battery for you, then writes up what it found.
24
+
25
+ Built on [cryptsmith](https://github.com/burrejak22/cryptsmith), which does all the crypto and pcap heavy lifting. flaghunter is the analyst on top.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install flaghunter
31
+ ```
32
+
33
+ ## Use
34
+
35
+ ```bash
36
+ # throw a crypto challenge file at it
37
+ flaghunter solve challenge.txt
38
+
39
+ # hunt a pcap for enrollment keys, flags, and obfuscated blobs
40
+ flaghunter pcap capture.pcap
41
+
42
+ # custom token pattern, json output
43
+ flaghunter pcap capture.pcap --pattern "FLAG\{[^}]+\}" --format json
44
+ ```
45
+
46
+ ## What the pcap hunter does
47
+
48
+ 1. Reassembles TCP streams from the capture
49
+ 2. Carves printable strings out of every stream
50
+ 3. Hunts tokens matching your pattern (default: 12 char alpha strings, like enrollment keys)
51
+ 4. Scores every stream for entropy, then runs single byte xor, repeating key xor, caesar, and layered base64/hex decoding on anything suspicious
52
+ 5. Writes a markdown report with stream labels, findings, and decoded candidates
53
+
54
+ ## What the solver does
55
+
56
+ Reads a challenge file and runs the battery: encoding layers, xor attacks, classical ciphers, hash identification. Ranks everything by english score so the real answer floats to the top.
57
+
58
+ ## Example
59
+
60
+ Say a capture has a server checking a 12 character alpha enrollment key, and the key is xor obfuscated in the traffic. flaghunter finds the stream, breaks the xor, and hands you the key.
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/flaghunter/__init__.py
5
+ src/flaghunter/cli.py
6
+ src/flaghunter/pcaphunt.py
7
+ src/flaghunter/solver.py
8
+ src/flaghunter.egg-info/PKG-INFO
9
+ src/flaghunter.egg-info/SOURCES.txt
10
+ src/flaghunter.egg-info/dependency_links.txt
11
+ src/flaghunter.egg-info/entry_points.txt
12
+ src/flaghunter.egg-info/requires.txt
13
+ src/flaghunter.egg-info/top_level.txt
14
+ tests/test_flaghunter.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flaghunter = flaghunter.cli:main
@@ -0,0 +1 @@
1
+ cryptsmith>=0.1.0
@@ -0,0 +1 @@
1
+ flaghunter
@@ -0,0 +1,106 @@
1
+ import base64
2
+ import os
3
+ import struct
4
+ import tempfile
5
+
6
+ from cryptsmith import xor
7
+
8
+ from flaghunter import pcaphunt, solver
9
+
10
+
11
+ def _write(path, data: bytes):
12
+ with open(path, "wb") as f:
13
+ f.write(data)
14
+
15
+
16
+ def _build_pcap(path, packets):
17
+ def frame(src_ip, dst_ip, sport, dport, seq, payload):
18
+ eth = b"\x00" * 12 + struct.pack("!H", 0x0800)
19
+ total = 20 + 20 + len(payload)
20
+ ip = struct.pack("!BBHHHBBH4s4s", 0x45, 0, total, 0, 0, 64, 6, 0,
21
+ bytes(map(int, src_ip.split("."))), bytes(map(int, dst_ip.split("."))))
22
+ tcp = struct.pack("!HHIIHHHH", sport, dport, seq, 0, 0x5000, 0, 0, 0)
23
+ return eth + ip + tcp + payload
24
+
25
+ with open(path, "wb") as f:
26
+ f.write(struct.pack("<IHHIIII", 0xA1B2C3D4, 2, 4, 0, 0, 65535, 1))
27
+ for (s, sp, d, dp, seq, payload) in packets:
28
+ p = frame(s, d, sp, dp, seq, payload)
29
+ f.write(struct.pack("<IIII", 0, 0, len(p), len(p)) + p)
30
+
31
+
32
+ def test_solver_breaks_single_byte_xor():
33
+ pt = b"the secret enrollment key is hidden right here in this message"
34
+ ct = xor.xor_bytes(pt, b"\x99")
35
+ with tempfile.NamedTemporaryFile(delete=False) as t:
36
+ _write(t.name, ct)
37
+ path = t.name
38
+ try:
39
+ report = solver.solve_file(path)
40
+ assert any(f.method == "xor" and "hidden right here" in f.plaintext
41
+ for f in report.findings)
42
+ finally:
43
+ os.unlink(path)
44
+
45
+
46
+ def test_solver_peels_base64_layers():
47
+ inner = base64.b64encode(b"flag{layered_encoding_win}").decode()
48
+ outer = base64.b64encode(inner.encode()).decode()
49
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as t:
50
+ _write(t.name, outer.encode())
51
+ path = t.name
52
+ try:
53
+ report = solver.solve_file(path)
54
+ assert any(f.method == "encoding" and "flag{layered_encoding_win}" in f.plaintext
55
+ for f in report.findings)
56
+ finally:
57
+ os.unlink(path)
58
+
59
+
60
+ def test_pcap_hunt_finds_plaintext_enrollment_key():
61
+ with tempfile.NamedTemporaryFile(suffix=".pcap", delete=False) as t:
62
+ path = t.name
63
+ try:
64
+ _build_pcap(path, [
65
+ ("10.0.0.5", 1337, "10.0.0.9", 9000, 1, b"HELLO distsrv v1"),
66
+ ("10.0.0.5", 1337, "10.0.0.9", 9000, 17, b"ENROLL ZxCvBnMkLjHg"),
67
+ ("10.0.0.9", 9000, "10.0.0.5", 1337, 1, b"OK enrolled"),
68
+ ])
69
+ report = pcaphunt.hunt_pcap(path)
70
+ assert report.streams == 1
71
+ tokens = [f.value for f in report.findings if f.kind == "token"]
72
+ assert "ZxCvBnMkLjHg" in tokens
73
+ finally:
74
+ os.unlink(path)
75
+
76
+
77
+ def test_pcap_hunt_breaks_xor_obfuscated_key():
78
+ key = b"QwErTyUiOpAs"
79
+ blob = xor.xor_bytes(b"enrollment key follows: " + key + b" end", b"\x42")
80
+ with tempfile.NamedTemporaryFile(suffix=".pcap", delete=False) as t:
81
+ path = t.name
82
+ try:
83
+ _build_pcap(path, [
84
+ ("10.0.0.5", 1337, "10.0.0.9", 9000, 1, b"\x01\x02hello"),
85
+ ("10.0.0.9", 9000, "10.0.0.5", 1337, 1, blob),
86
+ ])
87
+ report = pcaphunt.hunt_pcap(path)
88
+ values = [f.value for f in report.findings]
89
+ assert any("QwErTyUiOpAs" in v for v in values), values
90
+ kinds = [f.kind for f in report.findings if "QwErTyUiOpAs" in f.value]
91
+ assert "xor-token" in kinds
92
+ finally:
93
+ os.unlink(path)
94
+
95
+
96
+ def test_pcap_hunt_custom_pattern():
97
+ with tempfile.NamedTemporaryFile(suffix=".pcap", delete=False) as t:
98
+ path = t.name
99
+ try:
100
+ _build_pcap(path, [
101
+ ("10.0.0.5", 1337, "10.0.0.9", 9000, 1, b"here is FLAG{you_found_me} bye"),
102
+ ])
103
+ report = pcaphunt.hunt_pcap(path, pattern=r"FLAG\{[^}]+\}")
104
+ assert any(f.value == "FLAG{you_found_me}" for f in report.findings)
105
+ finally:
106
+ os.unlink(path)