Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Test

on:
push:
branches: ["**"]
pull_request:
branches: ["**"]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: go test ./...
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
go.sum
vendor/
108 changes: 78 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,72 +57,120 @@ cfuzz -w [wordlist]
Or if you prefer in one line:
```Shell
# example for subdomain enum
cfuzz -w [wordlist] -t 5 ping -c 4 FUZZ.domain.net
cfuzz -w [wordlist] ping -c 4 FUZZ.domain.net
```

Additionnaly it is possible to:
* **[Filter results](#filter-results)**
* **[Custom displayed field](#displayed-field)**
* **[Configure `cfuzz` run](#cfuzz-run-configuration)**
* **[Generate wordlists with AI](#ai-features)**
* **[Use cfuzz as an MCP server](#mcp-server)**

### Filter results

Additionaly, it is possible to filter displayed results:

**stdout filters:**
```shell
-omin, --stdout-min filter to only display if stdout characters number is lesser than n
-omax, --stdout-max filter to only display if stdout characters number is greater than n
-oeq, --stdout-equal filter to only display if stdout characters number is equal to n
-ow, --stdout-word filter to only display if stdout cointains specific word
--stdout-min n show only if stdout character count >= n
--stdout-max n show only if stdout character count <= n
--stdout-eq n show only if stdout character count == n
--stdout-word w show only if stdout contains word w (repeatable)
```

**stderr filters:**
```shell
-emin, --stderr-min filter to only display if stderr characters number is lesser than n
-emax, --stderr-max filter to only display if stderr characters number is greater than n
-eeq, --stderr-equal filter to only display if stderr characters number is equal to n
-ew, --stderr-word filter to only display if stderr cointains specific word
--stderr-min n show only if stderr character count >= n
--stderr-max n show only if stderr character count <= n
--stderr-eq n show only if stderr character count == n
--stderr-word w show only if stderr contains word w (repeatable)
```

**execution time filters:**
```shell
-tmin, --time-min filter to only display if exectuion time is shorter than n seconds
-tmax, --time-max filter to only display if exectuion time is longer than n seconds
-teq, --time-equal filter to only display if exectuion time is shorter than n seconds
--time-min n show only if execution time >= n seconds
--time-max n show only if execution time <= n seconds
--time-eq n show only if execution time == n seconds
```

**command exit code filters:**
```shell
--success filter to only display if execution return a zero exit code
--failure filter to only display if execution return a non-zero exit code
--success show only if execution returns exit code 0
--failure show only if execution returns a non-zero exit code
```

To only display results that don't pass the filter use `-H` or `--hide` flag.

### `cfuzz` run configuration
To make cfuzz more flexible and adapt to different constraints, many options are possible:
```shell
-w, --wordlist wordlist used by fuzzer
-d, --delay delay in ms between each thread launching. A thread executes one command. (default: 0)
-k, --keyword keyword used to determine which zone to fuzz (default: FUZZ)
-s, --shell shell to use for execution (default: /bin/bash)
-to, --timeout command execution timeout in s. After reaching it the command is killed. (default: 30)
-i, --input provide command stdin
-if, --stdin-fuzzing fuzz sdtin instead of command line
-m, --spider fuzz multiple keyword places. You must provide as many wordlists as keywords. Provide them in order you want them to be applied
-sw, --stdin-wordlist provide wordlist in cfuzz stdin
-w, --wordlist wordlist file(s) for fuzzing (repeatable with --spider)
-d, --delay delay in ms between goroutine launches (default: 0)
-j, --threads max concurrent workers (default: 50)
-k, --keyword keyword to replace in command (default: FUZZ)
-s, --shell shell to use for execution (default: /bin/bash)
--timeout command execution timeout in seconds (default: 30)
-i, --input provide command stdin
--stdin-fuzzing fuzz stdin instead of command line
-m, --spider fuzz multiple keyword positions (requires multiple -w)
--stdin-wordlist read wordlist from cfuzz stdin
```

### Displayed field

It is also possible to choose which result field is displayed in `cfuzz` output (also possible to use several):
```shell
-oc, --stdout display stdout number of characters
-ec, --stderr display stderr number of characters
-t, --time display execution time
-c, --code display exit code
-Hb, --no-banner do not display banner
-r, --only-word only display words
-f, --full-output display full command execution output (can't be combined with others display mode)
--stdout-chars display stdout character count
--stderr-chars display stderr character count
-t, --time display execution time
-c, --code display exit code
--no-banner hide banner
-r, --only-word print only matched words (no metadata columns)
-f, --full-output display full command execution output (can't be combined with other display modes)
```

### AI features

`cfuzz` integrates with Claude (via the Anthropic API) for two AI-powered workflows. Both require the `ANTHROPIC_API_KEY` environment variable to be set.

**AI filter** — describe what an interesting result looks like in plain English; `cfuzz` will ask Claude to evaluate each execution result and only show the ones that match:

```shell
cfuzz -w wordlist.txt --ai-filter "output contains an error about invalid credentials" \
curl -s http://target/login -d "user=admin&pass=FUZZ"
```

**AI wordlist generation** — generate a context-aware wordlist by describing what you need:

```shell
cfuzz wordlist "default credentials for network switches"
cfuzz wordlist "common web admin paths" -n 50
```

Output is printed to stdout, one entry per line, making it easy to pipe directly into cfuzz:

```shell
cfuzz wordlist "linux privilege escalation binaries" | \
cfuzz --stdin-wordlist "sudo -l FUZZ 2>/dev/null | grep -v 'not allowed'"
```

### MCP server

`cfuzz` can run as a [Model Context Protocol](https://modelcontextprotocol.io) server, exposing a `fuzz` tool that any MCP-compatible AI assistant can call:

```shell
cfuzz mcp
```

To register with Claude Desktop, add to `~/.claude/claude_desktop_config.json`:

```json
{
"mcpServers": {
"cfuzz": { "command": "cfuzz", "args": ["mcp"] }
}
}
```

The `fuzz` tool accepts: `command` (string), `wordlist` (array of strings), and optional `threads`, `timeout`, `success_only`, `stdout_word`, and `ai_filter` parameters.
27 changes: 0 additions & 27 deletions cmd/cfuzz/cfuzz.go

This file was deleted.

31 changes: 31 additions & 0 deletions cmd/cfuzz/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import (
"log"
"os"

"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "cfuzz [flags] [command]",
Short: "Fuzz any command line execution",
Long: `cfuzz fuzzes any shell command by replacing a keyword with wordlist entries.

Put FUZZ in your command, provide a wordlist, and cfuzz runs every substitution concurrently.

Examples:
cfuzz -w passwords.txt echo FUZZ
cfuzz -w users.txt --success ssh FUZZ@host id
cfuzz wordlist "SSH usernames" | cfuzz --stdin-wordlist "ssh FUZZ@host id"`,
SilenceUsage: true,
SilenceErrors: true,
}

func main() {
log.SetFlags(0)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
os.Exit(1)
}
}
100 changes: 100 additions & 0 deletions cmd/cfuzz/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package main

import (
"fmt"
"log"
"os"
"strings"

"github.com/ariary/cfuzz/pkg/ai"
"github.com/ariary/cfuzz/pkg/fuzz"
"github.com/ariary/cfuzz/pkg/mcp"
"github.com/spf13/cobra"
)

var mcpCmd = &cobra.Command{
Use: "mcp",
Short: "Start cfuzz as an MCP server over stdio",
Long: `Start cfuzz as a Model Context Protocol (MCP) server.

Communicates over stdio using JSON-RPC 2.0 and exposes a "fuzz" tool.

To register with Claude Desktop, add to ~/.claude/claude_desktop_config.json:
{
"mcpServers": {
"cfuzz": { "command": "cfuzz", "args": ["mcp"] }
}
}`,
SilenceUsage: true,
SilenceErrors: true,
Run: runMCP,
}

func init() {
rootCmd.AddCommand(mcpCmd)
}

func runMCP(_ *cobra.Command, _ []string) {
fmt.Fprintln(os.Stderr, "cfuzz MCP server started (stdio)")
mcp.Serve(handleFuzzCall)
}

func handleFuzzCall(args map[string]any) (string, error) {
cfg := fuzz.DefaultConfig()
cfg.HideBanner = true
cfg.OnlyWord = false

command, ok := args["command"].(string)
if !ok || command == "" {
return "", fmt.Errorf("command is required")
}
cfg.Command = command

rawList, ok := args["wordlist"].([]any)
if !ok || len(rawList) == 0 {
return "", fmt.Errorf("wordlist is required and must be a non-empty array")
}

tmp, err := os.CreateTemp("", "cfuzz-mcp-*.txt")
if err != nil {
return "", fmt.Errorf("failed to create temp wordlist: %w", err)
}
defer os.Remove(tmp.Name())
for _, entry := range rawList {
if s, ok := entry.(string); ok {
fmt.Fprintln(tmp, s)
}
}
tmp.Close()
cfg.Wordlists = []string{tmp.Name()}

if v, ok := args["threads"].(float64); ok {
cfg.Threads = int(v)
}
if v, ok := args["timeout"].(float64); ok {
cfg.Timeout = int64(v)
}
if v, ok := args["success_only"].(bool); ok && v {
cfg.Filters = append(cfg.Filters, fuzz.CodeSuccessFilter{Zero: true})
}
if v, ok := args["stdout_word"].(string); ok && v != "" {
cfg.Filters = append(cfg.Filters, fuzz.StdoutWordFilter{TargetWord: v})
}
if v, ok := args["ai_filter"].(string); ok && v != "" {
client, err := ai.NewClient()
if err != nil {
return "", fmt.Errorf("ai_filter requires ANTHROPIC_API_KEY: %w", err)
}
criterion := v
cfg.AIFilterFn = func(word, stdout, stderr, code string) bool {
return ai.IsInteresting(client, criterion, word, stdout, stderr, code)
}
}

var buf strings.Builder
cfg.ResultLogger = log.New(&buf, "", 0)
cfg.DisplayModes = fuzz.BuildDisplayModes(false, false, false, false, false)

fuzz.PerformFuzzing(cfg)
return buf.String(), nil
}
Loading
Loading