Skip to content

Commit fcaf5cd

Browse files
committed
feat: evolutionary guided fuzzer, banner, comment injection
1 parent 2eb328a commit fcaf5cd

17 files changed

+820
-18
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@
2222

2323

2424
## [Unreleased]
25+
### Added
26+
- Iterative evolutionary search for `GuidedRandomSqlFuzzer`
27+
- `PayloadPool` — priority queue for ranked payloads with deduplication
28+
- `RandomCommentInjectionTransformer` — inject `/**/` at random token boundaries
29+
- `--max-rounds`, `--round-size`, `--timeout` CLI options and builder methods

README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# pîrebok (from Kurdish "witch") - a guided adversarial fuzzer
1+
# pîrebok (from Kurdish "witch") - a guided adversarial fuzzer with evolutionary search
22

33

44
[![pypi](https://img.shields.io/pypi/v/pirebok.svg)](https://pypi.org/project/pirebok/)
@@ -13,11 +13,46 @@
1313
* PyPI: <https://pypi.org/project/pirebok/>
1414
* Free software: MIT
1515

16+
## How it works
17+
18+
Give it a payload. It mutates it until it bypasses the classifier.
19+
20+
```bash
21+
pirebok -f GuidedRandomSqlFuzzer -p "admin' OR 1=1#" -s 5 -q
22+
```
23+
24+
```
25+
"admin' OR%001313<>1314#"
26+
'admin\'/*%%0x9*/|| 1=1 || "iD" NOT LIKE "iD"#'
27+
"ADmIn'/*>SQN*//**//*h[xI*/or/*p*//**/0X1=0X1#s<"
28+
'AdMin\'\x0c|| "b"<>"bV"#;YR\x0b'
29+
'aDMin\'||"Mce"%%231BF7%%0ALiKE%00"McE"#>wgpxX'
30+
```
31+
32+
The original `admin' OR 1=1#` is classified as **sqli with 100% confidence**. After evolutionary mutations, the classifier misclassifies them as xss with confidence dropping to **0.48** - below the detection threshold.
33+
34+
| Confidence | Classification | Payload |
35+
|---|---|---|
36+
| 1.0000 | sqli | `admin' OR 1=1#` |
37+
| 0.7808 | xss | `admin'/*PCvp<a*/\|\|%000x1=0x1#i>` |
38+
| 0.4785 | xss | `ADmiN'%%0x39441%%0aOr/**/'Te'<>'TeD'#-HLa.` |
39+
40+
## Install
41+
42+
```bash
43+
pip install pirebok
44+
45+
# for guided mode (requires metamaska)
46+
pip install pirebok[guided]
47+
```
1648

1749
## Features
1850
- Random generic fuzzer w/ multiple transformers
1951
- Random sql fuzzer w/ multiple transformers
20-
- Guided random sql fuzzer w/ multiple transformers and [metamaska](https://github.com/HappyHackingSpace/metamaska)
52+
- Guided random sql fuzzer w/ iterative evolutionary search and [metamaska](https://github.com/HappyHackingSpace/metamaska)
53+
- Priority-queue-based payload pool ranked by confidence
54+
- Configurable `max_rounds`, `round_size`, and `timeout`
55+
- Random comment injection transformer (`/**/` at token boundaries)
2156

2257
## Credits
2358
- [Cookiecutter](https://github.com/audreyr/cookiecutter)

docs/usage.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,53 @@
22

33
To use pirebok in a project
44

5-
```
5+
```python
66
from pirebok.fuzzers import FuzzerBuilder
77
fuzzer_builder = FuzzerBuilder()
88
fuzzer = fuzzer_builder.choice("RandomGenericFuzzer").build()
99
fuzzer.fuzz("<script> ")
1010
```
1111

12+
For the guided fuzzer with evolutionary search parameters:
13+
14+
```python
15+
from pirebok.fuzzers import FuzzerBuilder
16+
fuzzer = (
17+
FuzzerBuilder()
18+
.choice("GuidedRandomSqlFuzzer")
19+
.threshold(0.5)
20+
.max_rounds(100)
21+
.round_size(20)
22+
.timeout(30)
23+
.build()
24+
)
25+
fuzzer.fuzz("admin' OR 1=1#")
26+
```
27+
1228
To use from CLI
1329

1430
```
1531
pirebok --help
1632
Usage: pirebok [OPTIONS]
1733
1834
Options:
19-
-f, --fuzzer [RandomGenericFuzzer|GuidedRandomSqlFuzzer|RandomSqlFuzzer]
35+
-f, --fuzzer [randomgenericfuzzer|guidedrandomsqlfuzzer|randomsqlfuzzer]
2036
choose fuzzer [required]
21-
-s, --steps INTEGER Number of iteration
22-
-p, --payload TEXT payload to fuzz [required]
37+
-s, --steps INTEGER Number of iteration [default: 10]
38+
-t, --threshold FLOAT Threshold for the guided fuzzers [default: 0.5]
39+
--max-rounds INTEGER Maximum mutation rounds for guided fuzzers [default: 100]
40+
--round-size INTEGER Mutations per round for guided fuzzers [default: 20]
41+
--timeout INTEGER Timeout in seconds, 0=unlimited [default: 0]
42+
-p, --payload TEXT Payload to fuzz [required]
2343
--help Show this message and exit.
2444
```
45+
46+
### Guided fuzzer options
47+
48+
| Option | Default | Description |
49+
|--------|---------|-------------|
50+
| `--max-rounds` | 100 | Maximum number of evolutionary mutation rounds per `fuzz()` call |
51+
| `--round-size` | 20 | Number of mutations generated per round |
52+
| `--timeout` | 0 | Timeout in seconds (0 = unlimited) |
53+
| `-t, --threshold` | 0.5 | WAF confidence threshold — stop when confidence drops below this |
54+
| `-s, --steps` | 10 | Number of independent `fuzz()` calls. For deep search use `--steps 1` with high `--max-rounds` |

pirebok/banner.py

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.

pirebok/cli.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
import sys
12
from functools import reduce
23
from operator import iconcat
34

45
import click
5-
from tqdm.auto import trange
6-
6+
from pirebok.banner import banner
77
from pirebok.fuzzers import Fuzzer, FuzzerBuilder
88

99

10-
@click.command(no_args_is_help=True, context_settings={'show_default': True})
10+
class BannerCommand(click.Command):
11+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
12+
if not ctx.params.get("silent"):
13+
print(banner(), file=sys.stderr)
14+
super().format_help(ctx, formatter)
15+
16+
17+
@click.command(cls=BannerCommand, no_args_is_help=True, context_settings={'show_default': True})
1118
@click.option(
1219
"-f",
1320
"--fuzzer",
@@ -20,11 +27,33 @@
2027
)
2128
@click.option("-s", "--steps", default=10, help="Number of iteration")
2229
@click.option("-t", "--threshold", default=0.5, help="Threshold for the guided fuzzers")
30+
@click.option("--max-rounds", default=100, help="Maximum mutation rounds for guided fuzzers")
31+
@click.option("--round-size", default=20, help="Mutations per round for guided fuzzers")
32+
@click.option("--timeout", default=0, help="Timeout in seconds, 0=unlimited")
2333
@click.option("-p", "--payload", required=True, help="Payload to fuzz")
24-
def main(fuzzer: str, steps: int, threshold: float, payload: str) -> None:
34+
@click.option("-q", "--silent", is_flag=True, default=False, help="Suppress banner")
35+
def main(
36+
fuzzer: str,
37+
steps: int,
38+
threshold: float,
39+
max_rounds: int,
40+
round_size: int,
41+
timeout: int,
42+
payload: str,
43+
silent: bool,
44+
) -> None:
45+
if not silent:
46+
print(banner(), file=sys.stderr)
2547
fuzzer_builder = FuzzerBuilder()
26-
fzzer = fuzzer_builder.choice(fuzzer).threshold(threshold).build()
27-
print("\n".join(map(repr, set(map(lambda _: fzzer.fuzz(payload), trange(steps))))))
48+
fzzer = (
49+
fuzzer_builder.choice(fuzzer)
50+
.threshold(threshold)
51+
.max_rounds(max_rounds)
52+
.round_size(round_size)
53+
.timeout(timeout)
54+
.build()
55+
)
56+
print("\n".join(map(repr, set(map(lambda _: fzzer.fuzz(payload), range(steps))))))
2857

2958

3059
if __name__ == "__main__":

pirebok/fuzzers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .fuzzer_visitor import FuzzerVisitor
55
from .generic.random_generic_fuzzer import RandomGenericFuzzer
66
from .generic_fuzzer import GenericFuzzer
7+
from .payload_pool import PayloadPool
78
from .sql.guided_random_sql_fuzzer import GuidedRandomSqlFuzzer
89
from .sql.random_sql_fuzzer import RandomSqlFuzzer
910
from .sql_fuzzer import SqlFuzzer

pirebok/fuzzers/fuzzer_builder.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,17 @@ def threshold(self, threshold: float) -> FuzzerBuilder:
3131
self.fuzzer.threshold = threshold # type: ignore
3232
return self
3333

34+
def max_rounds(self, max_rounds: int) -> FuzzerBuilder:
35+
self.fuzzer.max_rounds = max_rounds # type: ignore
36+
return self
37+
38+
def round_size(self, round_size: int) -> FuzzerBuilder:
39+
self.fuzzer.round_size = round_size # type: ignore
40+
return self
41+
42+
def timeout(self, timeout: int) -> FuzzerBuilder:
43+
self.fuzzer.timeout = timeout # type: ignore
44+
return self
45+
3446
def build(self) -> Fuzzer:
3547
return self.fuzzer

pirebok/fuzzers/payload_pool.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import heapq
2+
3+
4+
class PayloadPool:
5+
def __init__(self) -> None:
6+
self._heap: list[tuple[float, str]] = []
7+
self._seen: set[str] = set()
8+
9+
def push(self, confidence: float, payload: str) -> None:
10+
if payload in self._seen:
11+
return
12+
self._seen.add(payload)
13+
heapq.heappush(self._heap, (confidence, payload))
14+
15+
def pop(self) -> tuple[float, str]:
16+
return heapq.heappop(self._heap)
17+
18+
def peek(self) -> tuple[float, str]:
19+
return self._heap[0]
20+
21+
def __len__(self) -> int:
22+
return len(self._heap)
23+
24+
def __bool__(self) -> bool:
25+
return len(self._heap) > 0
Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import random
2+
import time
23
from typing import Sequence
34

45
from pirebok.fuzzers.fuzzer_visitor import FuzzerVisitor
6+
from pirebok.fuzzers.payload_pool import PayloadPool
57
from pirebok.fuzzers.sql_fuzzer import SqlFuzzer
68
from pirebok.transformers import Transformer
79

@@ -10,16 +12,51 @@ class GuidedRandomSqlFuzzer(SqlFuzzer):
1012
def __init__(self, transformers: Sequence[Transformer]) -> None:
1113
super().__init__(transformers)
1214
self.threshold: float
15+
self.max_rounds: int = 100
16+
self.round_size: int = 20
17+
self.timeout: int = 0
18+
19+
def _mutation_round(self, payload: str, round_size: int) -> set[str]:
20+
return {random.choice(self.transformers).transform(payload) for _ in range(round_size)}
1321

1422
def fuzz(self, payload: str) -> str:
1523
from metamaska.metamaska import Metamaska
1624

1725
metamask = Metamaska()
18-
payload_buff = random.choice(self.transformers).transform(payload)
19-
cls, proba = metamask.form(payload_buff, True)
20-
if cls == "sqli" and proba < self.threshold:
21-
return payload_buff
22-
return payload
26+
pool = PayloadPool()
27+
28+
cls, proba = metamask.form(payload, True)
29+
confidence = proba if cls == "sqli" else 0.0
30+
pool.push(confidence, payload)
31+
32+
best_confidence = confidence
33+
best_payload = payload
34+
35+
deadline = (time.monotonic() + self.timeout) if self.timeout > 0 else 0.0
36+
37+
for _ in range(self.max_rounds):
38+
if deadline and time.monotonic() >= deadline:
39+
break
40+
if best_confidence < self.threshold:
41+
break
42+
if not pool:
43+
break
44+
45+
candidate_confidence, candidate = pool.pop()
46+
mutations = self._mutation_round(candidate, self.round_size)
47+
48+
for mutated in mutations:
49+
cls, proba = metamask.form(mutated, True)
50+
mutation_confidence = proba if cls == "sqli" else 0.0
51+
pool.push(mutation_confidence, mutated)
52+
53+
if mutation_confidence < best_confidence:
54+
best_confidence = mutation_confidence
55+
best_payload = mutated
56+
57+
pool.push(candidate_confidence, candidate)
58+
59+
return best_payload
2360

2461
def accept(self, visitor: FuzzerVisitor) -> None:
2562
visitor.visit_sql(self)

pirebok/transformers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .generic.random_case_swapping_transformer import RandomCaseSwappingTransformer
2+
from .generic.random_comment_injection_transformer import RandomCommentInjectionTransformer
23
from .generic.random_comment_removing_transformer import RandomCommentRemovingTransformer
34
from .generic.random_comment_rewriting_transformer import RandomCommentRewritingTransformer
45
from .generic.random_number_encoding_transformer import RandomNumberEncodingTransformer

0 commit comments

Comments
 (0)