├── .gitignore ├── README.md ├── assets ├── fireflyLogo.png └── fireflyOptions.png ├── cmd └── firefly │ └── firefly.go ├── go.mod ├── go.sum ├── internal ├── banner │ └── banner.go ├── config │ └── config.go ├── fail │ └── fail.go ├── global │ └── global.go ├── knowledge │ └── knowledge.go ├── option │ ├── configure.go │ ├── helpmenu.go │ └── options.go ├── output │ ├── output.go │ ├── result.go │ └── stout.go ├── runner │ └── runner.go ├── scan │ ├── behavior.go │ ├── handler.go │ └── scan.go ├── setup │ └── setup.go ├── ui │ ├── buffer.go │ ├── help.go │ ├── program.go │ ├── progressbar.go │ └── ui.go ├── verbose │ └── verbose.go └── version │ └── version.go ├── pkg ├── design │ ├── design.go │ └── style.go ├── encode │ └── encode.go ├── extract │ └── extract.go ├── files │ └── files.go ├── functions │ └── slices.go ├── httpdiff │ └── httpdiff.go ├── httpfilter │ └── httpfilter.go ├── httpprepare │ ├── header.go │ └── htmlnode.go ├── httpreflect │ └── httpreflect.go ├── insertpoint │ └── insertpoint.go ├── parameter │ └── parameter.go ├── payloads │ ├── cwe.go │ ├── payload.go │ ├── relation.go │ └── wordlist.go ├── random │ └── random.go ├── randomness │ └── randomness.go ├── request │ ├── handler.go │ └── request.go ├── statistics │ └── statistics.go ├── transformation │ └── transformation.go └── waitgroup │ └── waitgroup.go ├── tests ├── httpfilter_test.go ├── randomness_test.go └── wordlist.txt └── testserver ├── Dockerfile ├── config ├── apache.conf └── supervisord.conf ├── docker-compose.yml ├── run.sh └── server └── server.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | # Go Debug files 21 | **/*__debug* 22 | 23 | # Visual studio code launch json debug file: 24 | launch.json 25 | 26 | # General 27 | **/*logs 28 | **/*.log 29 | **/*cache 30 | **/*tmp 31 | **/*temp 32 | **/*bak 33 | **/*bakup 34 | **/*.bak 35 | **/*.bakup 36 | **/*_bakup 37 | **/*_bak 38 | #**/*test 39 | #**/*tests 40 | **/*vscode 41 | **/*svn 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | firefly 4 |
5 |

6 | 7 |

</ 8 | Advantages | 9 | Features | 10 | Installation | 11 | Usage | 12 | Community > 13 |

14 | 15 | Firefly is an advanced black-box fuzzer and not just a standard asset discovery tool. Firefly provides the advantage of testing a target with a large number of built-in checks to detect behaviors in the target. 16 | 17 | # Advantages 18 | - [x] Hevy use of gorutines and internal hardware for great preformance 19 | - [x] Built-in engine that handles each task for "x" response results inductively 20 | - [x] Highly cusomized to handle more complex fuzzing 21 | - [x] Filter options and request verifications to avoid junk results 22 | - [x] Friendly error and debug output 23 | - [x] Build in payloads (default list are mixed with the wordlist from [seclists](https://github.com/danielmiessler/SecLists)) 24 | - [x] Payload tampering and encoding functionality 25 | 26 | # Features 27 |

28 | fireflyOptions 29 |
30 |

31 | 32 | # Installation 33 | 34 | ``` 35 | go install -v github.com/Brum3ns/firefly/cmd/firefly@latest 36 | ``` 37 | or 38 | ``` 39 | go get -v github.com/Brum3ns/firefly/cmd/firefly 40 | ``` 41 | 42 | 51 | 52 | 53 | # Usage 54 | 55 | ### Simple 56 | 57 | ```bash 58 | firefly -h 59 | ``` 60 | 61 | ```bash 62 | firefly -u 'http://example.com/?query=FUZZ' 63 | ``` 64 | 65 | --- 66 | 67 | ## Advanced usage 68 | 69 | ### Request 70 | Different types of request input that can be used 71 | 72 | Basic 73 | ```bash 74 | firefly -u 'http://example.com/?query=FUZZ' --timeout 7000 75 | ``` 76 | 77 | Request with different methods and protocols 78 | ```bash 79 | firefly -u 'http://example.com/?query=FUZZ' -m GET,POST,PUT -p https,http,ws 80 | ``` 81 | 82 | #### Pipeline 83 | ```bash 84 | echo 'http://example.com/?query=FUZZ' | firefly 85 | ``` 86 | 87 | #### HTTP Raw 88 | ```bash 89 | firefly -r ' 90 | GET /?query=FUZZ HTTP/1.1 91 | Host: example.com 92 | User-Agent: FireFly' 93 | ``` 94 | 95 | This will send the HTTP Raw and auto detect all GET and/or POST parameters to fuzz. 96 | ```bash 97 | firefly -r ' 98 | POST /?A=1 HTTP/1.1 99 | Host: example.com 100 | User-Agent: Firefly 101 | X-Host: FUZZ 102 | 103 | B=2&C=3' -au replace 104 | ``` 105 | 106 | ### Request Verifier 107 | Request verifier is the most important part. This feature let Firefly know the core behavior of the target your fuzz. It's important to do quality over quantity. More verfiy requests will lead to better quality at the cost of internal hardware preformance (*depending on your hardware*) 108 | 109 | ```bash 110 | firefly -u 'http://example.com/?query=FUZZ' -e 111 | ``` 112 | 113 | ### Payloads 114 | Payload can be highly customized and with a good core wordlist it's possible to be able to fully adapt the payload wordlist within Firefly itself. 115 | 116 | #### Payload debug 117 | > Display the format of all payloads and exit 118 | ```bash 119 | firefly -show-payload 120 | ``` 121 | 122 | #### Tampers 123 | > List of all Tampers avalible 124 | ```bash 125 | firefly -list-tamper 126 | ``` 127 | 128 | Tamper all paylodas with given type (*More than one can be used separated by comma*) 129 | ```bash 130 | firefly -u 'http://example.com/?query=FUZZ' -e s2c 131 | ``` 132 | 133 | #### Encode 134 | ```bash 135 | firefly -u 'http://example.com/?query=FUZZ' -e hex 136 | ``` 137 | Hex then URL encode all payloads 138 | ```bash 139 | firefly -u 'http://example.com/?query=FUZZ' -e hex,url 140 | ``` 141 | 142 | #### Payload regex replace 143 | ```bash 144 | firefly -u 'http://example.com/?query=FUZZ' -pr '\([0-9]+=[0-9]+\) => (13=(37-24))' 145 | ``` 146 | >The Payloads: `' or (1=1)-- -` and `" or(20=20)or "` 147 | > Will result in: `' or (13=(37-24))-- -` and `" or(13=(37-24))or "` 148 | > Where the ` => ` (with spaces) inducate the "*replace to*". 149 | 150 | 151 | ### Filters 152 | > Filter options to filter/match requests that include a given rule. 153 | 154 | Filter response to **ignore** (filter) `status code 302` and `line count 0` 155 | ```bash 156 | firefly -u 'http://example.com/?query=FUZZ' -fc 302 -fl 0 157 | ``` 158 | 159 | Filter responses to **include** (match) `regex`, and `status code 200` 160 | ```bash 161 | firefly -u 'http://example.com/?query=FUZZ' -mr '[Ee]rror (at|on) line \d' -mc 200 162 | ``` 163 | 164 | ```bash 165 | firefly -u 'http://example.com/?query=FUZZ' -mr 'MySQL' -mc 200 166 | ``` 167 | 168 | 169 | ### Preformance 170 | > Preformance and time delays to use for the request process 171 | 172 | Threads / Concurrency 173 | ```bash 174 | firefly -u 'http://example.com/?query=FUZZ' -t 35 175 | ``` 176 | 177 | Time Delay in millisecounds (ms) for each Concurrency 178 | ```bash 179 | FireFly -u 'http://example.com/?query=FUZZ' -t 35 -dl 2000 180 | ``` 181 | 182 | ### Wordlists 183 | > Wordlist that contains the paylaods can be added separatly or extracted from a given folder 184 | 185 | Single Wordlist with its attack type 186 | ```bash 187 | firefly -u 'http://example.com/?query=FUZZ' -w wordlist.txt:fuzz 188 | ``` 189 | 190 | Extract all wordlists inside a folder. Attack type is depended on the suffix `_wordlist.txt` 191 | ```bash 192 | firefly -u 'http://example.com/?query=FUZZ' -w wl/ 193 | ``` 194 | Example 195 | > Wordlists names inside folder `wl` : 196 | > 1. fuzz_wordlist.txt 197 | > 2. time_wordlist.txt 198 | 199 | 200 | ### Output 201 | > JSON output is **strongly recommended**. This is because you can benefit from the `jq` tool to navigate throw the result and compare it. 202 | 203 | (*If Firefly is pipeline chained with other tools, standard plaintext may be a better choice.*) 204 | 205 | Simple plaintext output format 206 | ```bash 207 | firefly -u 'http://example.com/?query=FUZZ' -o file.txt 208 | ``` 209 | 210 | JSON output format (*recommended*) 211 | ```bash 212 | firefly -u 'http://example.com/?query=FUZZ' -oJ file.json 213 | ``` 214 | 215 | # Community 216 | 217 | Everyone in the community are allowed to suggest new features, improvements and/or add new payloads to Firefly just make a pull request or add a comment with your suggestions! 218 | -------------------------------------------------------------------------------- /assets/fireflyLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brum3ns/firefly/de3f84d0f0034b553e83b81716f7742622fc33e2/assets/fireflyLogo.png -------------------------------------------------------------------------------- /assets/fireflyOptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brum3ns/firefly/de3f84d0f0034b553e83b81716f7742622fc33e2/assets/fireflyOptions.png -------------------------------------------------------------------------------- /cmd/firefly/firefly.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "time" 9 | 10 | "github.com/Brum3ns/firefly/internal/banner" 11 | "github.com/Brum3ns/firefly/internal/config" 12 | "github.com/Brum3ns/firefly/internal/global" 13 | "github.com/Brum3ns/firefly/internal/option" 14 | "github.com/Brum3ns/firefly/internal/runner" 15 | "github.com/Brum3ns/firefly/internal/setup" 16 | "github.com/Brum3ns/firefly/pkg/design" 17 | ) 18 | 19 | func main() { 20 | //Check resources before starting (first time use): 21 | if _, err := setup.Setup(); err != nil { 22 | log.Println(err) 23 | os.Exit(1) 24 | } 25 | 26 | //Setup user arguments (options) 27 | opt := option.NewOptions() 28 | 29 | // Configure user arguments (options) 30 | conf, err := config.NewConfigure(opt) 31 | if err != nil { 32 | log.Fatal(design.STATUS.ERROR, err) 33 | } 34 | 35 | if !conf.Option.TerminalUI { 36 | banner.Banner() 37 | banner.Disclaimer() 38 | } 39 | 40 | //Listen for user keypress (CTRL + C): 41 | c := make(chan os.Signal, 1) 42 | signal.Notify(c, os.Interrupt) 43 | go func() { 44 | for range c { 45 | fmt.Println("\n\r"+design.STATUS.WARNING, "CTRL+C pressed - Exiting") 46 | os.Exit(130) 47 | } 48 | }() 49 | 50 | timer := time.Now() 51 | 52 | //Run the runner in verifyication process mode to detect normal behavior and patterns within the target: 53 | VerifyRunner := runner.NewRunner(conf, nil) 54 | KnowledgeStorage, _, err := VerifyRunner.Run() 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | //Run the black-box enumiration process: 60 | AttackRunner := runner.NewRunner(conf, KnowledgeStorage) 61 | _, Statistic, err := AttackRunner.Run() 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | //Display summary of the process: 67 | fmt.Printf( 68 | "%s\033[1;32m\u2713\033[0m Process finished: Requests/Responses:[%d/%d], Scanned:[\033[1;32m%d\033[0m], Behavior:[\033[1;33m%d\033[0m], Filtered:[\033[1;36m%d\033[0m], Error:[\033[31m%d\033[0m], Time:[%v]\n", 69 | global.TERMINAL_CLEAR, 70 | Statistic.Request.GetCount(), 71 | Statistic.Response.GetCount(), 72 | Statistic.Scanner.GetCount(), 73 | Statistic.Behavior.GetCount(), 74 | Statistic.Request.GetFilterCount(), 75 | Statistic.Request.GetErrorCount(), 76 | time.Since(timer), 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Brum3ns/firefly 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.1 6 | 7 | require ( 8 | github.com/charmbracelet/bubbles v0.18.0 9 | github.com/charmbracelet/bubbletea v0.25.0 10 | github.com/charmbracelet/lipgloss v0.10.0 11 | golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 12 | golang.org/x/net v0.26.0 13 | golang.org/x/term v0.21.0 14 | gopkg.in/yaml.v2 v2.4.0 15 | ) 16 | 17 | require ( 18 | github.com/atotto/clipboard v0.1.4 // indirect 19 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 20 | github.com/containerd/console v1.0.4 // indirect 21 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 22 | github.com/mattn/go-isatty v0.0.20 // indirect 23 | github.com/mattn/go-localereader v0.0.1 // indirect 24 | github.com/mattn/go-runewidth v0.0.15 // indirect 25 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 26 | github.com/muesli/cancelreader v0.2.2 // indirect 27 | github.com/muesli/reflow v0.3.0 // indirect 28 | github.com/muesli/termenv v0.15.2 // indirect 29 | github.com/rivo/uniseg v0.4.7 // indirect 30 | github.com/sahilm/fuzzy v0.1.1 // indirect 31 | golang.org/x/sync v0.7.0 // indirect 32 | golang.org/x/sys v0.21.0 // indirect 33 | golang.org/x/text v0.16.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= 6 | github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= 7 | github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= 8 | github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= 9 | github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= 10 | github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= 11 | github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= 12 | github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= 13 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 14 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 15 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 16 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 17 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 18 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 19 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 20 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 21 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 22 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 23 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 24 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 25 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 26 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 27 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 28 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 29 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 30 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 31 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 32 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 33 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 34 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 35 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 36 | github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= 37 | github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 38 | golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc= 39 | golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= 40 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 41 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 42 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 43 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 44 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 47 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 48 | golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= 49 | golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 50 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 51 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 55 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 56 | -------------------------------------------------------------------------------- /internal/banner/banner.go: -------------------------------------------------------------------------------- 1 | package banner 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Brum3ns/firefly/internal/version" 7 | "github.com/Brum3ns/firefly/pkg/design" 8 | ) 9 | 10 | func Banner() { 11 | fmt.Printf(` 12 | / __7 o / _7/¯7 13 | / _7 /¯7 /¯_7¯-_)/ _7/ /\¯\/7 14 | /_/ /_/ /_/ \__7/_/ /_/ ) / 15 | /_/ 16 | (%s) 17 | By: @yeswehack : Brumens 18 | 19 | `, (design.COLOR.GREY + version.VERSION + design.COLOR.WHITE)) 20 | } 21 | 22 | func Disclaimer() { 23 | fmt.Println(design.ICON.AWARE + " Stay ethical. The creator of the tool is not responsible for any misuse or damage.") 24 | } 25 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/Brum3ns/firefly/internal/global" 7 | "github.com/Brum3ns/firefly/internal/option" 8 | "github.com/Brum3ns/firefly/pkg/extract" 9 | "github.com/Brum3ns/firefly/pkg/functions" 10 | "github.com/Brum3ns/firefly/pkg/httpdiff" 11 | "github.com/Brum3ns/firefly/pkg/httpfilter" 12 | "github.com/Brum3ns/firefly/pkg/httpprepare" 13 | "github.com/Brum3ns/firefly/pkg/payloads" 14 | "github.com/Brum3ns/firefly/pkg/randomness" 15 | "github.com/Brum3ns/firefly/pkg/request" 16 | "github.com/Brum3ns/firefly/pkg/transformation" 17 | ) 18 | 19 | type Configure struct { 20 | Httpfilter httpfilter.Filter 21 | HttpMatch httpfilter.Filter 22 | Option *option.Options 23 | Wordlist *payloads.Wordlist 24 | Scanner *Scanner 25 | } 26 | 27 | // Scanner properties (static storage) 28 | // Note : (This structure should not be modified once it's defined). 29 | type Scanner struct { 30 | OK_Extract bool 31 | OK_Diff bool 32 | OK_Transformation bool 33 | DisablesTechniques bool 34 | Extract extract.Extract 35 | Transformation transformation.Transformation 36 | Randomness randomness.Randomness 37 | HttpDiffFilter httpdiff.Filter 38 | } 39 | 40 | func NewConfigure(opt *option.Options) (*Configure, error) { 41 | wl_transformation, err := transformation.GetWordlist(opt.TransformationYAMLFile) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | // Configure HTTP filter 47 | filter, err := httpfilter.NewFilter(httpfilter.Config{ 48 | Mode: opt.FilterMode, 49 | HeaderRegex: opt.FilterHeaderRegex, 50 | BodyRegex: opt.FilterBodyRegex, 51 | StatusCodes: functions.SplitEscape(opt.FilterCode, ','), 52 | WordCounts: functions.SplitEscape(opt.FilterWord, ','), 53 | LineCounts: functions.SplitEscape(opt.FilterLine, ','), 54 | ResponseSizes: functions.SplitEscape(opt.FilterSize, ','), 55 | ResponseTimesMillisec: functions.SplitEscape(opt.FilterTime, ','), 56 | Header: request.LstToHeaders(LstToKeyMap(functions.SplitEscape(opt.FilterHeader, ','))), 57 | }) 58 | if err != nil { 59 | return &Configure{}, err 60 | } 61 | 62 | // Configure HTTP match filter 63 | match, err := httpfilter.NewFilter(httpfilter.Config{ 64 | Mode: opt.MatchMode, 65 | HeaderRegex: opt.MatchHeaderRegex, 66 | BodyRegex: opt.MatchBodyRegex, 67 | StatusCodes: functions.SplitEscape(opt.MatchCode, ','), 68 | WordCounts: functions.SplitEscape(opt.MatchWord, ','), 69 | LineCounts: functions.SplitEscape(opt.MatchLine, ','), 70 | ResponseSizes: functions.SplitEscape(opt.MatchSize, ','), 71 | ResponseTimesMillisec: functions.SplitEscape(opt.MatchTime, ','), 72 | Header: request.LstToHeaders(LstToKeyMap(functions.SplitEscape(opt.MatchHeader, ','))), 73 | }) 74 | if err != nil { 75 | return &Configure{}, err 76 | } 77 | 78 | conf := &Configure{ 79 | Option: opt, 80 | Httpfilter: filter, 81 | HttpMatch: match, 82 | Wordlist: payloads.NewWordlist( 83 | &payloads.Wordlist{ 84 | Files: opt.WordlistPaths, 85 | TransformationList: wl_transformation, 86 | Verify: payloads.Verify{ 87 | Payload: opt.VerifyPayload, 88 | Amount: opt.VerifyAmount, 89 | }, 90 | PayloadProperties: payloads.PayloadProperties{ 91 | Tamper: opt.Tamper, 92 | Encode: opt.Encode, 93 | PayloadPattern: opt.PayloadPattern, 94 | PayloadPrefix: opt.PayloadPrefix, 95 | PayloadSuffix: opt.PayloadSuffix, 96 | PayloadReplace: opt.PayloadReplace, 97 | }, 98 | }, 99 | ), 100 | } 101 | 102 | //Return a *pointer* of the "Scanner" [struct]ure: 103 | conf.Scanner, err = conf.newScanner() 104 | if err != nil { 105 | return &Configure{}, err 106 | } 107 | 108 | return conf, nil 109 | 110 | } 111 | 112 | func (conf *Configure) newScanner() (*Scanner, error) { 113 | //Setup scanner technique resources: 114 | wlPtn, wlRegex := extract.MakeWordlists(global.DIR_DETECTION) 115 | wlPatternPrefix, wlPatterns := extract.CreatePrefixMap(wlPtn) 116 | 117 | rand, err := randomness.NewRandomness(randomness.DefaultConfig()) 118 | if err != nil { 119 | return &Scanner{}, err 120 | } 121 | 122 | // Configure the transformation structure 123 | transform, err := transformation.NewTransformation(conf.Option.TransformationYAMLFile) 124 | if err != nil { 125 | return &Scanner{}, err 126 | } 127 | 128 | return &Scanner{ 129 | OK_Extract: conf.Option.Techniques["E"], 130 | OK_Diff: conf.Option.Techniques["D"], 131 | OK_Transformation: conf.Option.Techniques["T"], 132 | DisablesTechniques: conf.Option.Techniques["X"], 133 | 134 | Randomness: rand, 135 | Transformation: transform, 136 | Extract: extract.NewExtract(extract.Properties{ 137 | Threads: conf.Option.ThreadsExtract, 138 | PrefixPatterns: wlPatternPrefix, 139 | WordlistPattern: wlPatterns, 140 | WordlistRegex: map[string][]string{extract.WILDCARD: wlRegex}, 141 | }), 142 | HttpDiffFilter: httpdiff.Filter{ 143 | HeaderFilter: httpdiff.HeaderFilter{ 144 | Header: httpprepare.GetHeaderNode(request.LstToHeaders(LstToKeyMap(conf.Option.FilterDiffHeader))), 145 | }, 146 | }, 147 | }, nil 148 | } 149 | 150 | func LstToKeyMap(lst []string) map[string]string { 151 | var m = make(map[string]string) 152 | for _, i := range lst { 153 | m[i] = "" 154 | } 155 | return m 156 | } 157 | -------------------------------------------------------------------------------- /internal/fail/fail.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/Brum3ns/firefly/internal/global" 8 | "github.com/Brum3ns/firefly/pkg/design" 9 | ) 10 | 11 | // Failed messages: 12 | var ERRORCODE_MESSAGES = map[int]string{ 13 | 13001: design.STATUS.FAIL + " The verify responses are less than 50% in success (" + design.COLOR.RED + "There was to many request/response errors." + design.COLOR.WHITE + ")", 14 | 1011: design.STATUS.FAIL + " The filter syntax is invalid. The valid for each filter separeted by a comma (if any) are as following (not combined): \".\",\"-\",\"--\",\"++\"", 15 | 1008: design.STATUS.FAIL + " No input was detected (" + design.COLOR.ORANGE + "-u" + design.COLOR.WHITE + "," + design.COLOR.ORANGE + "-f" + design.COLOR.WHITE + ") or STDIN pipeline", 16 | 1001: design.STATUS.FAIL + " Invalid HTTP Raw data" + design.COLOR.ORANGE + "-r" + design.COLOR.WHITE + ")", 17 | 10005: design.STATUS.FAIL + " No insert points detected (" + design.COLOR.ORANGE + "-i" + design.COLOR.WHITE + ")", 18 | 8001: design.STATUS.FAIL + " The argument \"payload-replace\" (" + design.COLOR.ORANGE + "-pr" + design.COLOR.WHITE + ") do not contain the \" => \" (spaces included). Firefly dosen't know what to replace the regex/string with.", 19 | 1006: design.STATUS.FAIL + " Can't use a threads lower or equal to zero (" + design.COLOR.ORANGE + "-t" + design.COLOR.WHITE + ")", 20 | 1005: design.STATUS.WARNING + " This file already exist. If you want to overwrite it. Use option (" + design.COLOR.ORANGE + "-ov" + design.COLOR.WHITE + ")", 21 | 10009: design.STATUS.FAIL + " Invalid input for \"auto-detect\" (" + design.COLOR.ORANGE + "-au" + design.COLOR.WHITE + ")", 22 | 100014: design.STATUS.FAIL + " Invalid random value Example usage: s8 (string with length as 8) or 's4,n8' to use both string and number(" + design.COLOR.RED + "Random: Invalid usage" + design.COLOR.WHITE + ")", 23 | 13003: design.STATUS.FAIL + " Can't setup the payload given (" + design.COLOR.ORANGE + "-verify-char" + design.COLOR.WHITE + ")", 24 | 2001: design.STATUS.FAIL + " The level has to be between 1-3 (" + design.COLOR.ORANGE + "-lv" + design.COLOR.WHITE + ")", 25 | 3001: design.STATUS.FAIL + " The match mode is invalid (" + design.COLOR.ORANGE + "-mmode" + design.COLOR.WHITE + "). Valid input: and, or", 26 | 3010: design.STATUS.FAIL + " The filter mode is invalid (" + design.COLOR.ORANGE + "-fmode" + design.COLOR.WHITE + "). Valid input: and, or", 27 | 9003: design.STATUS.FAIL + " Invalid transformation input", 28 | 10001: design.STATUS.FAIL + " Invalid URL(s) given (" + design.COLOR.ORANGE + "-u" + design.COLOR.WHITE + ")", 29 | 10002: design.STATUS.FAIL + " Invalid method(s) given (" + design.COLOR.ORANGE + "-X" + design.COLOR.WHITE + ")", 30 | 10003: design.STATUS.FAIL + " Invalid scheme(s) input (" + design.COLOR.ORANGE + "-scheme" + design.COLOR.WHITE + ")", 31 | 9001: design.STATUS.FAIL + " Invalid wordlist given. Make sure that the wordlist is not empty (" + design.COLOR.ORANGE + "-w" + design.COLOR.WHITE + ").", 32 | 9002: design.STATUS.FAIL + " Invalid wordlist folder given. Firefly coulen't find atleast one valid file (wordlist to use) in the folder. Make sure that the files in the folder are correct set and not empty (" + design.COLOR.ORANGE + "-wf" + design.COLOR.WHITE + ").", 33 | 10012: design.STATUS.FAIL + " Cannot use a timeout lower than zero", 34 | 4001: design.STATUS.FAIL + " The specified output file already exists. Use the overwrite option to overwrite it (be careful).", 35 | } 36 | 37 | // Check the failed type 38 | // Return a desciption message on why it failed 39 | func IFFail(code int) { 40 | if code <= 0 { 41 | return 42 | } else if msg, ok := ERRORCODE_MESSAGES[code]; ok { 43 | log.Fatal(msg) 44 | } else { 45 | log.Fatal(design.STATUS.FAIL + " Unkown failure!") 46 | } 47 | } 48 | 49 | // If there is an error then check what type to provide 50 | func IFError(typ string, err error) (bool, string) { 51 | if err != nil { 52 | //Verbose error message 53 | var ErrMsg = fmt.Sprintf("%s %s", design.STATUS.FAIL, err) 54 | 55 | switch typ { 56 | case "f": 57 | log.Fatal(ErrMsg) 58 | 59 | case "p": 60 | log.Panic(ErrMsg) 61 | } 62 | 63 | if global.VERBOSE { 64 | fmt.Println(design.STATUS.FAIL, typ, err) 65 | } 66 | return true, typ 67 | } 68 | 69 | //No error was detected 70 | return false, typ 71 | } 72 | -------------------------------------------------------------------------------- /internal/global/global.go: -------------------------------------------------------------------------------- 1 | // Global variables - (no dynamic variables) 2 | package global 3 | 4 | import ( 5 | "os" 6 | ) 7 | 8 | // Terminal based variables 9 | var ( 10 | TERMINAL_CLEAR = "\r\x1b[2K" 11 | ) 12 | 13 | // File directory global variables (Never ever reassign any of these): 14 | var ( 15 | //Root directories: 16 | DIR_HOME, _ = os.UserHomeDir() 17 | DIR_CONFIG = (DIR_HOME + "/.config/firefly/") 18 | DIR_DB = (DIR_CONFIG + "/db/") 19 | 20 | //Resource directories: 21 | DIR_TAMPERS = (DIR_DB + "/tampers/") 22 | DIR_RESOURCE = (DIR_DB + "/resources/") 23 | DIR_DETECTION = (DIR_DB + "/resources/detection/") 24 | DIR_WORDLIST = (DIR_DB + "/wordlists/") 25 | 26 | //Resource files: 27 | FILE_RANDOMAGENT = (DIR_RESOURCE + "/randomUserAgent.txt") 28 | FILE_SKIP_HEADERS = (DIR_RESOURCE + "/skipheaders.txt") 29 | FILE_TRANSFORMATION = (DIR_RESOURCE + "/transformation.yml") 30 | ) 31 | 32 | // Major of the variables are declared in the validation process in 'options.go'. 33 | var ( 34 | 35 | //Request 36 | VERIFY int 37 | INSERT string 38 | PAYLOAD_PATTERN string 39 | PayloadChars = make(map[rune]string) 40 | DefaultProto = "http" 41 | 42 | RANDOMNESS_WHITELIST = lettersDigits() 43 | CONSONANTS_DIGITS = constantsDigits() 44 | 45 | //Random 46 | RANDOM_INSERT = make(map[string]int) 47 | RANDOM_OPTION string 48 | 49 | //Input and output: 50 | Pipe bool 51 | 52 | //Payload: 53 | PayloadMark = "__FIREFLY_PAYLOAD__" 54 | 55 | //Verbose: 56 | DEBUG bool 57 | VERBOSE bool 58 | 59 | //Input: 60 | Lst_rawData map[string][]string 61 | 62 | //Output: 63 | OutputType string 64 | OutputFile string 65 | OutputFileOS *os.File 66 | 67 | Total int //DELETE to save RAM memory 68 | 69 | CHECK_HEADERS []string 70 | RANDOM_AGENTS []string 71 | 72 | //Amount 73 | AMOUNT_ITEM int 74 | 75 | //Verification tags 76 | TAG_VERIFYPAYLOAD = "verifypayload" 77 | TAG_VERIFYCHAR = "verifychar" 78 | ) 79 | 80 | // Make and return a map containing rune and string of character [a-zA-Z0-9] 81 | func lettersDigits() map[rune]string { 82 | var m = make(map[rune]string) 83 | for az, AZ, O9 := 'a', 'A', 48; az <= 'z' && AZ <= 'Z'; az, AZ, O9 = (az + 1), (AZ + 1), (O9 + 1) { 84 | if O9 <= 57 { 85 | v := rune(O9) 86 | m[v] = string(v) 87 | } 88 | m[az], m[AZ] = string(az), string(AZ) 89 | } 90 | //Add some special characters that is common in tokens and or hashes (URL encoded and or separeted for 'x' length etc...) 91 | //( _, -, :, ;, %, . =) 92 | for _, rn := range []rune{95, 45, 58, 59, 37, 46, 61} { 93 | m[rn] = string(rn) 94 | } 95 | return m 96 | } 97 | 98 | func constantsDigits() map[rune]string { 99 | var ( 100 | m = make(map[rune]string) //Return 101 | l = []rune{ 102 | 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z', 103 | 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 104 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 105 | } 106 | ) 107 | for _, rn := range l { 108 | m[rn] = string(rn) 109 | } 110 | return m 111 | } 112 | -------------------------------------------------------------------------------- /internal/knowledge/knowledge.go: -------------------------------------------------------------------------------- 1 | package knowledge 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/Brum3ns/firefly/internal/output" 7 | "github.com/Brum3ns/firefly/pkg/extract" 8 | "github.com/Brum3ns/firefly/pkg/httpprepare" 9 | ) 10 | 11 | type Knowledge struct { 12 | PayloadVerify string 13 | Responses []output.Response 14 | Requests []output.Request 15 | Combine Combine 16 | } 17 | 18 | type Combine struct { 19 | Extract extract.ResultCombine 20 | HTMLNode httpprepare.HTMLNodeCombine 21 | HeaderNode httpprepare.Header 22 | } 23 | 24 | type Learnt struct { 25 | Payload string 26 | HTMLNode httpprepare.HTMLNode 27 | Extract extract.Result 28 | Response output.Response 29 | Request output.Request 30 | } 31 | 32 | func NewKnowledge() *Knowledge { 33 | return &Knowledge{} 34 | } 35 | 36 | func NewCombine() Combine { 37 | return Combine{ 38 | Extract: extract.NewCombine(), 39 | HTMLNode: httpprepare.NewCombineHTMLNode(), 40 | HeaderNode: httpprepare.NewHeader(), 41 | } 42 | } 43 | 44 | func GetKnowledge(learnt map[string][]Learnt) map[string]Knowledge { 45 | var storedKnowledge = make(map[string]Knowledge) 46 | c := NewCombine() 47 | 48 | for hashId, data := range learnt { 49 | k := Knowledge{} 50 | for _, d := range data { 51 | k.PayloadVerify = d.Payload 52 | k.Requests = append(k.Requests, d.Request) 53 | k.Responses = append(k.Responses, d.Response) 54 | 55 | k.Combine.HeaderNode = c.HeaderNode.Merge(d.Response.Headers) 56 | k.Combine.Extract = combineAppendMaps(reflect.ValueOf(&c.Extract), d.Extract).(extract.ResultCombine) 57 | k.Combine.HTMLNode = combineAppendMaps(reflect.ValueOf(&c.HTMLNode), d.HTMLNode).(httpprepare.HTMLNodeCombine) 58 | } 59 | storedKnowledge[hashId] = k 60 | } 61 | 62 | return storedKnowledge 63 | } 64 | 65 | // Take a structure and combine all "map[string]int" into a map[string][]int and return the combined map: 66 | func combineAppendMaps(combineData reflect.Value, data any) interface{} { 67 | combineData = combineData.Elem() 68 | dataValue := reflect.ValueOf(data) 69 | t := dataValue.Type() 70 | 71 | //Extract all field from the given "data": 72 | for i := 0; i < dataValue.NumField(); i++ { 73 | data_field := dataValue.Field(i) 74 | data_name := t.Field(i).Name 75 | 76 | //In case the field is a correct map that can be combined, then procceed: 77 | if data_map, ok := data_field.Interface().(map[string]int); ok { 78 | 79 | //Extract the same field (by name) from "combineData" that was recently extracted from "data": 80 | combineData_field := combineData.FieldByName(data_name) 81 | 82 | //Make sure the "cData" field is a correct map that can be used to compare the original map from "data": 83 | if combineData_map, ok := combineData_field.Interface().(map[string][]int); ok { 84 | 85 | //Extract the key value and the key's value. Then add only the unique items from "newData" to "combineData" 86 | for k, v := range data_map { 87 | combineData_map[k] = appendUniqueInt(combineData_map[k], v) 88 | } 89 | } 90 | } 91 | } 92 | return combineData.Interface() 93 | } 94 | 95 | // Append a string to a list Works similar as append but do not append duplicates or empty strings 96 | func appendUniqueInt(l []int, i int) []int { 97 | if len(l) == 0 { 98 | return append(l, i) 99 | } 100 | for _, item := range l { 101 | if item == i { 102 | return l 103 | } 104 | } 105 | return append(l, i) 106 | } 107 | -------------------------------------------------------------------------------- /internal/option/configure.go: -------------------------------------------------------------------------------- 1 | package option 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/Brum3ns/firefly/internal/global" 10 | "github.com/Brum3ns/firefly/pkg/files" 11 | ) 12 | 13 | // The structure configure is a alias for *Options in it's current state but holds all the validation/configuration functions. 14 | // !IMPORTANT : The reciver functions *MUST* have the same name as the variable name (*if the variable is needed to be validated*). This makes it possible to loop over variables and easy configure them 15 | // Note : (The reason why each option has its own function is for easier troubleshooting and also for easier development in the future) 16 | type configure struct { 17 | opt *Options 18 | reflectValue reflect.Value 19 | interfaceValue reflect.Value 20 | typ reflect.Type 21 | } 22 | 23 | // validate the user input to be correct before starting any future processes 24 | func Configure(opt *Options) (*Options, int) { 25 | conf := &configure{opt: opt} 26 | conf.reflectValue = reflect.ValueOf(conf.opt) 27 | conf.interfaceValue = conf.reflectValue.Elem() 28 | conf.typ = conf.interfaceValue.Type() 29 | 30 | for i := 0; i < conf.interfaceValue.NumField(); i++ { 31 | fValue := conf.interfaceValue.FieldByName(conf.typ.Field(i).Name) 32 | fTyp := fValue.Type() 33 | 34 | //Extract the option group variables, then extract all (options/flags) within the group: 35 | if fTyp.Kind() == reflect.Struct { 36 | for i := 0; i < fValue.NumField(); i++ { 37 | item := fTyp.Field(i) 38 | 39 | //Validation error detected for user input, return error to the user screen: 40 | if exist, ok := conf.MethodCall(item.Name); exist && !ok { 41 | if errcode, ok := strconv.Atoi(item.Tag.Get("errorcode")); ok == nil { 42 | return nil, errcode 43 | } else { 44 | log.Panicf("can't convert errorcode value \"%v\" for flag \"%s\".\n", errcode, item.Name) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | //Define global variables (only none sensitive) 52 | conf.setGlobal() 53 | 54 | return conf.opt, 0 55 | } 56 | 57 | // Declare static values to be global: 58 | // !Note : (This variable should NEVER be changed once defined) 59 | func (conf *configure) setGlobal() bool { 60 | global.RANDOM_INSERT = conf.opt.Random 61 | global.VERBOSE = conf.opt.Verbose 62 | global.PAYLOAD_PATTERN = conf.opt.PayloadPattern 63 | return true 64 | } 65 | 66 | // Call a method within the validate struct. 67 | // Return if the method exist and if the method execution status, otherwise if the method wasen't found return double false values 68 | func (conf *configure) MethodCall(name string) (bool, bool) { 69 | if v := reflect.ValueOf(conf).MethodByName(name); v.IsValid() { 70 | return true, v.Call(nil)[0].Interface() == true 71 | } 72 | return false, false 73 | } 74 | 75 | // //////////Configure Options////////// // 76 | //Configure each option in it's own method for easy code read and development in the feature 77 | 78 | func (conf *configure) ReqRaw() bool { 79 | return true 80 | } 81 | 82 | func (conf *configure) Encode() bool { 83 | return true 84 | } 85 | func (conf *configure) Tamper() bool { 86 | return true 87 | } 88 | 89 | func (conf *configure) Technique() bool { 90 | return true 91 | } 92 | 93 | func (conf *configure) Output() bool { 94 | return !files.FileExist(conf.opt.Output) || conf.opt.Overwrite 95 | } 96 | 97 | func (conf *configure) MaxIdleConns() bool { 98 | return conf.opt.MaxIdleConns > 0 99 | } 100 | func (conf *configure) MaxIdleConnsPerHost() bool { 101 | return conf.opt.MaxIdleConnsPerHost > 0 102 | } 103 | func (conf *configure) MaxConnsPerHost() bool { 104 | return conf.opt.MaxConnsPerHost > 0 105 | } 106 | 107 | /* func (conf *configure) AutoDetectParams() bool { //Can be ignored ATM 108 | return len(conf.opt.AutoParamRules) == 0 || conf.opt.AutoParamRules == "append" || conf.opt.AutoParamRules == "replace" 109 | } */ 110 | 111 | func (conf *configure) PayloadReplace() bool { 112 | return len(conf.opt.PayloadReplace) == 0 || (len(conf.opt.PayloadReplace) > 0 && strings.Contains(conf.opt.PayloadReplace, " => ")) 113 | } 114 | func (conf *configure) URLs() bool { 115 | return len(conf.opt.URLs) > 0 116 | } 117 | func (conf *configure) MatchMode() bool { 118 | mode := strings.ToLower(conf.opt.MatchMode) 119 | return mode == "or" || mode == "and" 120 | } 121 | func (conf *configure) FilterMode() bool { 122 | mode := strings.ToLower(conf.opt.FilterMode) 123 | return mode == "or" || mode == "and" 124 | } 125 | func (conf *configure) Scheme() bool { 126 | return len(conf.opt.Scheme) > 0 127 | } 128 | func (conf *configure) Methods() bool { 129 | return len(conf.opt.Methods) > 0 130 | } 131 | func (conf *configure) Scanner() bool { 132 | return conf.opt.ThreadsScanner > 0 133 | } 134 | func (conf *configure) Threads() bool { 135 | return conf.opt.Threads > 0 136 | } 137 | func (conf *configure) Insert() bool { 138 | return len(conf.opt.InsertKeyword) > 0 139 | } 140 | func (conf *configure) Delay() bool { 141 | return conf.opt.Delay >= 0 142 | } 143 | func (conf *configure) Timeout() bool { 144 | return conf.opt.Timeout >= 0 145 | } 146 | 147 | func (conf *configure) ThreadsExtract() bool { 148 | return conf.opt.ThreadsExtract >= 0 149 | } 150 | func (conf *configure) VerifyAmount() bool { 151 | return conf.opt.VerifyAmount > 0 152 | } 153 | func (conf *configure) WordlistPaths() bool { 154 | return len(conf.opt.wordlistPath) > 0 && len(conf.opt.WordlistPaths) > 0 155 | } 156 | -------------------------------------------------------------------------------- /internal/option/helpmenu.go: -------------------------------------------------------------------------------- 1 | package option 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type groupField struct { 12 | name string 13 | defValue string 14 | usage string 15 | } 16 | 17 | func (opt *Options) customUsage() { 18 | m := make(map[string]string) 19 | lst_groupOrder := []string{} 20 | v := reflect.ValueOf(opt).Elem() 21 | t := v.Type() 22 | 23 | //Extract nested struct from the core struct (Aka: option group): 24 | for i := 0; i < v.NumField(); i++ { 25 | groupValue := v.FieldByName(t.Field(i).Name) 26 | groupType := groupValue.Type() 27 | 28 | lst_groupOrder = append(lst_groupOrder, groupType.Name()) 29 | 30 | //Extract the variables (options/flags) inside the struct (options) found in the option struct (option group) 31 | for i := 0; i < groupValue.NumField(); i++ { 32 | if tag := string(groupType.Field(i).Tag.Get("flag")); len(tag) > 0 { 33 | m[tag] = groupType.Name() 34 | } 35 | } 36 | } 37 | 38 | //Add the groups options to the map "groupOption": 39 | groupOption := map[string][]groupField{} 40 | flag.VisitAll(func(f *flag.Flag) { 41 | groupName := m[f.Name] 42 | groupOption[groupName] = append(groupOption[groupName], groupField{ 43 | name: f.Name, 44 | usage: f.Usage, 45 | defValue: f.DefValue, 46 | }) 47 | }) 48 | 49 | //Make help menu by adding the "group name", "option", "usage" and default value (if any): 50 | menu := make(map[string]string) 51 | space_width := "\t" 52 | for group_name, strt := range groupOption { 53 | for _, field := range strt { 54 | var defaultValue string 55 | 56 | if len(field.defValue) > 0 { 57 | 58 | defaultValue = fmt.Sprintf("(Default: %s%v\033[0m)", colorDefaultValue(field.defValue), field.defValue) 59 | } 60 | if len(field.name) <= 4 { 61 | space_width = " \t" 62 | } 63 | menu[group_name] += fmt.Sprintf(" -%s%s %s\n", field.name, (space_width + field.usage), defaultValue) 64 | } 65 | } 66 | 67 | //Print the help menu: 68 | fmt.Println("Usage: firefly -u 'target.com/query=FUZZ' [OPTION] ...") 69 | for _, k := range lst_groupOrder { 70 | fmt.Printf("%s:\n%s\n", strings.ToUpper(k), menu[k]) 71 | } 72 | //exampleUsage() //TODO - Update examples 73 | } 74 | 75 | func colorDefaultValue(s string) string { 76 | if s == "false" || s == "true" { 77 | return "\033[1;34m" 78 | } else if _, err := strconv.Atoi(s); err == nil { 79 | return "\033[1:32m" 80 | } 81 | return "\033[33m" 82 | } 83 | 84 | func exampleUsage() { 85 | fmt.Println(`[ Basic Examples ] 86 | firefly -u 'target.com/?query=FUZZ' 87 | firefly -u 'target.com/?query=hoodie&sort=DESC' -au replace -H 'Host:localhost' 88 | firefly -u 'target.com/?query=FUZZ&cachebuster=#RANDOM#' -e url -w wordlist.txt -pr '( ) => (/**/)' 89 | `) 90 | } 91 | -------------------------------------------------------------------------------- /internal/output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "sync" 8 | ) 9 | 10 | var mutex sync.Mutex 11 | 12 | var ( 13 | prefix = []byte("[\r\n") 14 | suffix = []byte("\r\n]") 15 | filesize = 0 16 | ) 17 | 18 | func WriteJSON(count int, f *os.File, result ResultFinal) error { 19 | if !result.OK { 20 | return nil 21 | } 22 | if count == 0 { 23 | f.Write(prefix) 24 | filesize += len(prefix) 25 | } 26 | 27 | dataJSON, err := json.MarshalIndent(result, "", " ") 28 | if err != nil { 29 | return err 30 | } 31 | 32 | f.Write(dataJSON) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | //Replace last binary characters and append the new JSON data to output file: 38 | f.Write(suffix) 39 | 40 | filesize += len(dataJSON) 41 | filesize += len(suffix) 42 | 43 | info, _ := f.Stat() 44 | fmt.Println("size:", info.Size(), "|", filesize) 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/output/result.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/Brum3ns/firefly/pkg/extract" 7 | "github.com/Brum3ns/firefly/pkg/httpdiff" 8 | "github.com/Brum3ns/firefly/pkg/transformation" 9 | ) 10 | 11 | // The output result is the final result that is generated that stores all the result from all processes done by the runner 12 | // Note : (The final result only stores the result details that are of int `json:""`erest to the user and not the properties that were used during the runner process. The variable may be reformulated for better readability) 13 | type ResultFinal struct { 14 | RequestId int `json:"RequestId"` 15 | TargetHashId string `json:"TargetId"` 16 | Tag string `json:"Tag"` 17 | Date string `json:"Date"` 18 | Payload string `json:"Payload"` 19 | Request Request `json:"Request"` 20 | Response Response `json:"Response"` 21 | Scanner Scanner `json:"Scanner"` 22 | Error error `json:"Error"` 23 | OK bool `json:"-"` 24 | UnkownBehavior bool 25 | //Origin string `json:"Origin"` 26 | //Behavior Behavior `json:"Behavior"` 27 | } 28 | 29 | //TODO 30 | // Contains the collected Behavior from the target 31 | /* type Behavior struct { 32 | CWE string `json:"CWE"` 33 | Component string `json:"Component"` 34 | Confidence string `json:"Confidence"` 35 | Description string `json:"Desc"` 36 | } */ 37 | 38 | // Refer to the results of the Request/response process 39 | type Request struct { 40 | URL string `json:"URL"` 41 | URLOriginal string `json:"URL-Original"` 42 | Host string `json:"Host"` 43 | Scheme string `json:"Scheme"` 44 | Method string `json:"Method"` 45 | PostBody string `json:"PostBody"` 46 | Proto string `json:"HTTP"` 47 | Headers [][2]string `json:"Headers"` 48 | } 49 | 50 | // Refer to the results of the request/Response process 51 | type Response struct { 52 | StatusCode int `json:"Status-Code"` 53 | WordCount int `json:"WordCount"` 54 | LineCount int `json:"LineCount"` 55 | HeaderAmount int `json:"Header-Amount"` 56 | ContentLength int `json:"Content-Length"` 57 | ContentType string `json:"Contnet-Type"` 58 | Host string `json:"Host"` 59 | Body string `json:"-"` 60 | Title string `json:"Title"` 61 | Proto string `json:"HTTP"` 62 | IPAddress []string `json:"IPAddress"` 63 | Time float64 `json:"Response-Time"` 64 | Headers http.Header `json:"Headers"` 65 | } 66 | 67 | // Refer to the results of the scanning process 68 | type Scanner struct { 69 | Extract extract.Result `json:"Extract"` 70 | Diff httpdiff.Result `json:"Diff"` 71 | Transformation transformation.Result `json:"Transformation"` 72 | } 73 | -------------------------------------------------------------------------------- /internal/output/stout.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/Brum3ns/firefly/pkg/design" 9 | "github.com/Brum3ns/firefly/pkg/httpprepare" 10 | "github.com/charmbracelet/lipgloss" 11 | ) 12 | 13 | var ( 14 | TERMINAL_CLEAR = "\r\x1b[2K" 15 | 16 | COLOR_BLACK = lipgloss.Color("#000000") 17 | COLOR_WHITE = lipgloss.Color("#D9DCCF") 18 | COLOR_GREY = lipgloss.Color("#383838") 19 | COLOR_GREEN = lipgloss.Color("#3AF191") 20 | COLOR_ORANGE = lipgloss.Color("#D98D00") 21 | COLOR_YELLOW = lipgloss.Color("#FFDF00") 22 | COLOR_RED = lipgloss.Color("#EB2D3A") 23 | ) 24 | 25 | type Display struct { 26 | ResultFinal 27 | detailed bool 28 | design *design.Design 29 | 30 | style 31 | } 32 | 33 | type style struct { 34 | detail lipgloss.Style 35 | } 36 | 37 | func NewDisplay(detailed bool, design *design.Design) *Display { 38 | return &Display{ 39 | detailed: detailed, 40 | design: design, 41 | } 42 | } 43 | 44 | func (d Display) MakeStyles() { 45 | d.style = style{ 46 | detail: lipgloss.NewStyle().Foreground(COLOR_GREY), 47 | } 48 | } 49 | 50 | // Display the information to the screen from a given structure (result data) to the command line interface (CLI) [show: on/off]) and any struct that *include JSON supported tags*. 51 | // The function use color highlighting in the CLI by using a mixture of stderr and stout. The output values will be in stout 52 | // version which makes it possible to support pipelining without including garbage in the values. 53 | func (d *Display) ToScreen(result ResultFinal) { 54 | d.ResultFinal = result 55 | //print("\033[?25l") 56 | 57 | boxChar := "╰╴" 58 | if d.detailed { 59 | boxChar = "├╴" 60 | } 61 | 62 | diff := d.Scanner.Diff 63 | 64 | stout := fmt.Sprintf("%s╭ \033[33m%s\033[0m Status:%s, Words:%s, Lines:%s, CL:%s, CT:%s, Time:%sms\n"+ 65 | "%sErrors:[Body:%s, Header:%s] Diff:[Tag:%s, Attr:%s, AttrVal:%s, Words:%s, Comments:%s, Header:%s] %s\n", 66 | TERMINAL_CLEAR, 67 | d.Payload, 68 | // Response information 69 | d.design.StatusCode(d.Response.StatusCode), 70 | d.design.WordCount(d.Response.WordCount), 71 | d.design.LineCount(d.Response.LineCount), 72 | d.design.ContentLength(d.Response.ContentLength), 73 | d.design.ContentType(d.Response.ContentType), 74 | d.design.ResponseTime(d.Response.Time), 75 | // Box Draw character 76 | boxChar, 77 | //Extract: 78 | //d.design.Diff(d.Scanner.Extract.TotalHits), 79 | d.design.Highlight(len(d.Scanner.Extract.PatternBody)), 80 | d.design.Highlight(len(d.Scanner.Extract.PatternHeaders)), 81 | //Difference - HTMLNode: 82 | d.design.Highlight( 83 | diff.HTMLResult.Appear.TagStartHits+ 84 | diff.HTMLResult.Appear.TagEndHits+ 85 | diff.HTMLResult.Appear.TagSelfCloseHits, 86 | ), 87 | d.design.Highlight(diff.HTMLResult.Appear.AttributeHits), 88 | d.design.Highlight(diff.HTMLResult.Appear.AttributeValueHits), 89 | d.design.Highlight(diff.HTMLResult.Appear.WordsHits), 90 | d.design.Highlight(diff.HTMLResult.Appear.CommentHits), 91 | //Difference - Headers: 92 | d.design.Highlight(diff.HeaderResult.HeaderHits), 93 | d.transformation(), 94 | ) 95 | 96 | if d.detailed { 97 | prefix := "|" 98 | stout += "\n├╴[Header]\n" + 99 | d.getDetailDiff("Appear", strings.Join(headerNodeToLst(prefix, diff.HeaderResult.Appear), "\n")) + 100 | d.getDetailDiff("Disapear", strings.Join(headerNodeToLst(prefix, diff.HeaderResult.Disappear), "\n")) + 101 | "\n├╴[HTML]\n" + 102 | d.getDetailDiff("Appear", strings.Join(htmlNodeToLst(prefix, diff.HTMLResult.Appear.HTMLNode), "\n")) + 103 | d.getDetailDiff("Disappear", strings.Join(htmlNodeToLst(prefix, diff.HTMLResult.Disappear.HTMLNode), "\n")) 104 | } 105 | fmt.Println(stout) 106 | } 107 | 108 | func (d *Display) getDetailDiff(title, s string) string { 109 | if len(s) > 0 { 110 | return fmt.Sprintf("├╴%s\n%s\n", title, (d.design.Color.GREY + s + d.design.Color.WHITE)) 111 | } 112 | return "" 113 | } 114 | 115 | func htmlNodeLst(htmlNode string) { 116 | 117 | } 118 | 119 | func headerNodeToLst(prefix string, headerNode httpprepare.Header) []string { 120 | var lst []string 121 | // Todo ... 122 | for header, headerInfo := range headerNode { 123 | s := "" 124 | // Check the amount of items in the lists of header info: 125 | if len(headerInfo.Amount) == 1 && len(headerInfo.Values) == 1 { 126 | s = fmt.Sprintf("%s(%d) : %s: %s", prefix, headerInfo.Amount[0], header, headerInfo.Values[0]) 127 | 128 | // This should not be possible, but just in case, output it if it happen to be 129 | } else { 130 | s = fmt.Sprintf("%s(%d) : %s:\n\t%s", prefix, headerInfo.Amount, header, strings.Join(headerInfo.Values, "\n")) 131 | } 132 | lst = append(lst, s) 133 | } 134 | 135 | return lst 136 | } 137 | 138 | func htmlNodeToLst(prefix string, htmlnode httpprepare.HTMLNode) []string { 139 | var lst []string 140 | 141 | if len(htmlnode.TagStart) > 0 { 142 | lst = append(lst, 143 | fmt.Sprintf("%sTag-start: %v", prefix, htmlnode.TagStart), 144 | ) 145 | } 146 | if len(htmlnode.TagEnd) > 0 { 147 | lst = append(lst, 148 | fmt.Sprintf("%sTag-end: %v", prefix, htmlnode.TagEnd), 149 | ) 150 | } 151 | if len(htmlnode.TagSelfClose) > 0 { 152 | lst = append(lst, 153 | fmt.Sprintf("%sTag-selfclose: %v", prefix, htmlnode.TagSelfClose), 154 | ) 155 | } 156 | if len(htmlnode.Attribute) > 0 { 157 | lst = append(lst, 158 | fmt.Sprintf("%sAttribute: %v", prefix, htmlnode.Attribute), 159 | ) 160 | } 161 | if len(htmlnode.AttributeValue) > 0 { 162 | lst = append(lst, 163 | fmt.Sprintf("%sAttributeValue: %v", prefix, htmlnode.AttributeValue), 164 | ) 165 | } 166 | if len(htmlnode.Comment) > 0 { 167 | lst = append(lst, 168 | fmt.Sprintf("%sComment: %v", prefix, htmlnode.Comment), 169 | ) 170 | } 171 | if len(htmlnode.Words) > 0 { 172 | lst = append(lst, 173 | fmt.Sprintf("%sWords: %v", prefix, htmlnode.Words), 174 | ) 175 | } 176 | 177 | return lst 178 | } 179 | 180 | // Display payload transformation: 181 | func (d Display) transformation() string { 182 | if len(d.Scanner.Transformation.Format) > 0 { 183 | return fmt.Sprintf(" Transformation: [\033[1;32m%s\033[0m => \033[1;32m%s\033[0m]", strconv.Quote(d.Scanner.Transformation.Payload), strconv.Quote(d.Scanner.Transformation.Format)) 184 | } 185 | return "" 186 | } 187 | -------------------------------------------------------------------------------- /internal/runner/runner.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/url" 8 | "os" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/Brum3ns/firefly/internal/config" 14 | "github.com/Brum3ns/firefly/internal/global" 15 | "github.com/Brum3ns/firefly/internal/knowledge" 16 | "github.com/Brum3ns/firefly/internal/output" 17 | "github.com/Brum3ns/firefly/internal/scan" 18 | "github.com/Brum3ns/firefly/internal/ui" 19 | "github.com/Brum3ns/firefly/internal/verbose" 20 | "github.com/Brum3ns/firefly/pkg/design" 21 | "github.com/Brum3ns/firefly/pkg/files" 22 | "github.com/Brum3ns/firefly/pkg/httpfilter" 23 | "github.com/Brum3ns/firefly/pkg/httpprepare" 24 | "github.com/Brum3ns/firefly/pkg/insertpoint" 25 | "github.com/Brum3ns/firefly/pkg/payloads" 26 | "github.com/Brum3ns/firefly/pkg/request" 27 | "github.com/Brum3ns/firefly/pkg/statistics" 28 | "github.com/Brum3ns/firefly/pkg/waitgroup" 29 | ) 30 | 31 | // The runner should contain the structures needed for all the processes. 32 | // It must NOT contain structures that need to be modified and/or dynamicly changed once the process is running. 33 | type Runner struct { 34 | Count int 35 | OutputOK bool 36 | VerifyMode bool 37 | TerminalUIMode bool 38 | Conf *config.Configure 39 | Design *design.Design 40 | RequestTasks *request.TaskStorage 41 | stats statistics.Statistic 42 | channel Channel 43 | handler Handler 44 | } 45 | 46 | type Handler struct { 47 | HTTP request.Handler 48 | Scanner scan.Handler 49 | } 50 | 51 | type Channel struct { 52 | ListenerScanner chan scan.Result 53 | ListenerHTTP chan request.Result 54 | Result chan output.ResultFinal 55 | Statistic chan bool 56 | } 57 | 58 | // Setup a new runner. The runner can run in a verification mode, in that case the argument "knowledgeStorage" MUST be set to "nil". 59 | // The other mode is the attack mode and need the "knowledgeStorage" to contain knowledge (data) about the target to attack to be run successfully. 60 | func NewRunner(conf *config.Configure, knowledgeStorage map[string]knowledge.Knowledge) *Runner { 61 | var verifyMode = (knowledgeStorage == nil) 62 | return &Runner{ 63 | Count: 0, 64 | Conf: conf, 65 | VerifyMode: verifyMode, 66 | TerminalUIMode: (!verifyMode && conf.Option.TerminalUI), 67 | OutputOK: (len(conf.Option.Output) > 0 && knowledgeStorage != nil), 68 | Design: design.NewDesign(), 69 | stats: statistics.NewStatistic(verifyMode), 70 | channel: Channel{ 71 | ListenerScanner: make(chan scan.Result), 72 | ListenerHTTP: make(chan request.Result), 73 | Result: make(chan output.ResultFinal), 74 | Statistic: make(chan bool), 75 | }, 76 | handler: Handler{ 77 | // Setup the HTTP handler: 78 | HTTP: request.NewHandler(request.HandlerSettings{ 79 | Delay: conf.Option.Delay, 80 | Threads: conf.Option.Threads, 81 | VerifyMode: verifyMode, 82 | Client: request.NewClient(request.ClientSettings{ 83 | Timeout: conf.Option.Timeout, 84 | Proxy: conf.Option.Proxy, 85 | HTTP2: conf.Option.HTTP2, 86 | }), 87 | RequestBase: request.RequestBase{ 88 | RandomUserAgent: conf.Option.RandomAgent, 89 | HeadersOriginalArray: conf.Option.Headers, 90 | PostBody: conf.Option.PostData, 91 | InsertPoint: conf.Option.InsertKeyword, 92 | }, 93 | }), 94 | 95 | // Setup the HTTP scanner handler: 96 | Scanner: scan.NewHandler(scan.Config{ 97 | Scanner: conf.Scanner, 98 | Threads: conf.Option.ThreadsScanner, 99 | PayloadVerify: conf.Option.VerifyPayload, 100 | Knowledge: knowledgeStorage, 101 | }), 102 | }, 103 | } 104 | } 105 | 106 | // Firefly verify/fuzz runner 107 | // The runner is the core process for all other child processes. It's preforming the requests and listen for HTTP results to be scanned analyzed. 108 | func (r *Runner) Run() (map[string]knowledge.Knowledge, statistics.Statistic, error) { 109 | var ( 110 | outputFileWriter = r.MustValidateOutput() 111 | learnt = make(map[string][]knowledge.Learnt) 112 | display = output.NewDisplay(r.Conf.Option.Detail, r.Design) 113 | terminalUI = ui.NewProgram() 114 | wg waitgroup.WaitGroup 115 | ) 116 | 117 | // Start terminal UI 118 | if r.TerminalUIMode { 119 | wg.Add(1) 120 | go func() { 121 | if _, err := terminalUI.Run(); err != nil { 122 | log.Fatalf("terminal UI - %s", err) 123 | } 124 | wg.Done() 125 | }() 126 | } 127 | 128 | // Start the request and scanner handlers 129 | go r.handler.HTTP.Run(r.channel.ListenerHTTP) 130 | go r.handler.Scanner.Run(r.channel.ListenerScanner) 131 | 132 | //Runner listener 133 | go func() { 134 | var ( 135 | progressbar = ui.NewProgressBar(100, &r.stats) 136 | //progressBar = statistics.NewProgressBar(100, &r.stats) 137 | mutex sync.Mutex 138 | ) 139 | for { 140 | select { 141 | case <-r.channel.Statistic: 142 | if !r.Conf.Option.NoDisplay && r.TerminalUIMode { 143 | terminalUI.Send(r.stats) 144 | } 145 | 146 | case result := <-r.channel.Result: 147 | r.stats.Count() 148 | 149 | if r.VerifyMode { 150 | mutex.Lock() 151 | learnt[result.TargetHashId] = append(learnt[result.TargetHashId], knowledge.Learnt{ 152 | Payload: result.Payload, 153 | Extract: result.Scanner.Extract, 154 | HTMLNode: httpprepare.GetHTMLNode(result.Response.Body), 155 | Response: result.Response, 156 | }) 157 | mutex.Unlock() 158 | } else if result.UnkownBehavior { 159 | r.stats.Behavior.Count() 160 | 161 | // Send the result to the output file specified by the user: 162 | if r.OutputOK { 163 | err := output.WriteJSON(r.stats.Output.GetCount(), outputFileWriter, result) 164 | if err != nil { 165 | log.Println(design.STATUS.ERROR, "Request ID:", result.RequestId, err) 166 | } 167 | r.stats.Output.Count() 168 | } 169 | 170 | // Display the final result to the screen (CLI) 171 | if !r.Conf.Option.NoDisplay { 172 | if r.TerminalUIMode { 173 | terminalUI.Send(r.stats) 174 | terminalUI.Send(result) 175 | } else { 176 | display.ToScreen(result) 177 | progressbar.Print() 178 | } 179 | } 180 | } 181 | } 182 | } 183 | }() 184 | 185 | //Listeners 186 | wg.Add(2) 187 | go r.listenerScanner() 188 | go r.listenerHTTP() 189 | 190 | // Give all the request jobs to the HTTP handler and wait until the handlers are completed with all the jobs: 191 | jobHandlerAmount := r.jobToHandler(&r.handler.HTTP) 192 | r.waitForHandlers(jobHandlerAmount) 193 | 194 | // Wait for the handlers to finish 195 | r.handler.HTTP.Wait() 196 | r.handler.Scanner.Wait() 197 | 198 | // Close the output file (if any output have been handled) 199 | if r.OutputOK { 200 | if err := outputFileWriter.Close(); err != nil { 201 | log.Fatal(err) 202 | } 203 | } 204 | 205 | if r.TerminalUIMode { 206 | terminalUI.Quit() 207 | wg.Wait() 208 | } 209 | 210 | return knowledge.GetKnowledge(learnt), r.stats, nil 211 | } 212 | 213 | // Listen for results from the HTTP handler and preform a scan for each intercepted HTTP result: 214 | func (r *Runner) listenerScanner() { 215 | for { 216 | scanResult := <-r.channel.ListenerScanner 217 | if scanResult.Error != nil { 218 | verbose.Show(scanResult.Error) 219 | } else { 220 | r.stats.Scanner.Count() 221 | r.channel.Result <- scanResult.Output 222 | } 223 | } 224 | } 225 | 226 | // Listen for HTTP request/response results from the request handler and add the response as a job to the scanner handler: 227 | func (r *Runner) listenerHTTP() { 228 | for { 229 | resultHTTP := <-r.channel.ListenerHTTP 230 | r.stats.Request.Count() 231 | 232 | //Check if we got a valid HTTP response from our requested target or if any error appeared: 233 | if resultHTTP.Error != nil { 234 | r.stats.Response.CountError() 235 | r.channel.Statistic <- true 236 | verbose.Show(resultHTTP.Error) 237 | continue 238 | } 239 | r.stats.Response.Count() 240 | r.stats.Response.UpdateTime(resultHTTP.Response.Time) 241 | 242 | //Filter the HTTP response (if set): 243 | filterResp := httpfilter.Response{ 244 | Body: []byte(resultHTTP.Response.Body), 245 | StatusCode: resultHTTP.Response.StatusCode, 246 | ResponseSize: resultHTTP.Response.ResponseBodySize, 247 | WordCount: resultHTTP.Response.WordCount, 248 | LineCount: resultHTTP.Response.LineCount, 249 | ResponseTime: resultHTTP.Response.Time, 250 | Headers: resultHTTP.Response.Header, 251 | } 252 | 253 | // HTTP Filter filter/match (if set) 254 | if r.Conf.Httpfilter.Run(filterResp) || (r.Conf.HttpMatch.IsSet() && !r.Conf.HttpMatch.Run(filterResp)) { 255 | r.stats.Response.CountFilter() 256 | r.channel.Statistic <- true 257 | continue 258 | } 259 | 260 | //Give the scanner handler job related to the Http result (request/response): 261 | r.handler.Scanner.AddJob(resultHTTP) 262 | } 263 | } 264 | 265 | // Validate and verify the output to store the result to (if set): 266 | // Note : (will panic in case an error is triggered) 267 | func (r *Runner) MustValidateOutput() *os.File { 268 | var ( 269 | fileWriter = &os.File{} 270 | err error 271 | ) 272 | //Create output file and create a file writer (*if output file set*): 273 | if r.OutputOK { 274 | if !files.FileExist(r.Conf.Option.Output) || r.Conf.Option.Overwrite { 275 | fileWriter, err = os.OpenFile(r.Conf.Option.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 276 | 277 | if err != nil { 278 | log.Panicln(err) 279 | 280 | } else if err = fileWriter.Truncate(0); err != nil { 281 | log.Panicln(err) 282 | 283 | } else if _, err = fileWriter.Seek(0, 0); err != nil { 284 | log.Panicln(err) 285 | } 286 | } else { 287 | err = fmt.Errorf("%s The specified output file already exists (\033[33m%s\033[0m), use the overwrite option to overwrite it", design.STATUS.FAIL, r.Conf.Option.Output) 288 | log.Panicln(err) 289 | } 290 | verbose.Show("Save result to output file: " + r.Conf.Option.Output) 291 | } 292 | return fileWriter 293 | } 294 | 295 | func (r *Runner) jobToHandler(requestHandler *request.Handler) int { 296 | var ( 297 | payloadWordlist = r.Conf.Wordlist.GetAll() 298 | headersArray = r.Conf.Option.Headers 299 | postbody = r.Conf.Option.PostData 300 | jobAmount = 0 301 | ) 302 | for hash, host := range r.Conf.Option.Hosts { 303 | param := r.Conf.Option.Params[hash] 304 | rawURL := host.URL 305 | 306 | for _, tag := range payloads.TAGS { 307 | // Check if we should adapt to "behavior verification mode": 308 | if (r.VerifyMode && tag != payloads.TAG_VERIFY) || (!r.VerifyMode && tag == payloads.TAG_VERIFY) { 309 | continue 310 | } 311 | 312 | wordlist := payloadWordlist[tag] 313 | for _, payload := range wordlist { 314 | // Prepare the request by inserting the current payload into the request: 315 | // !Note : (Some variables given will be modified) 316 | insert := insertpoint.NewInsert(r.Conf.Option.InsertKeyword, payload) 317 | 318 | URLStruct, _ := url.Parse(rawURL) 319 | 320 | if param.AutoQueryURL { 321 | URLStruct.RawQuery = param.URL.RawQueryInsertPoint 322 | rawURL = URLStruct.String() 323 | } 324 | 325 | if param.AutoQueryBody { 326 | postbody = param.Body.RawQueryInsertPoint 327 | } 328 | 329 | if param.AutoQueryCookie { 330 | headersArray = request.SetNewHeaderValue(headersArray, "cookie", param.Cookie.RawQueryInsertPoint) 331 | } 332 | 333 | randomUserAgents, err := getRandomUserAgent(global.FILE_RANDOMAGENT) 334 | if err != nil { 335 | log.Fatalf("Random User-Agent:", err) 336 | } 337 | 338 | requestSettings := request.RequestSettings{ 339 | UserAgents: randomUserAgents, 340 | TargetHashId: hash, 341 | Tag: tag, 342 | Payload: payload, 343 | URLOriginal: rawURL, 344 | Parameter: r.Conf.Option.Params[hash], 345 | URL: insert.SetURL(rawURL), 346 | Method: insert.SetMethod(host.Method), 347 | RequestBase: request.RequestBase{ 348 | Headers: insert.SetHeaders(headersArray), 349 | PostBody: insert.SetPostBody(postbody), 350 | RandomUserAgent: r.Conf.Option.RandomAgent, 351 | HeadersOriginalArray: r.Conf.Option.Headers, 352 | }, 353 | } 354 | jobAmount++ 355 | requestHandler.AddJob(requestSettings) 356 | } 357 | } 358 | } 359 | return jobAmount 360 | } 361 | 362 | // Take a file containing user agents 363 | func getRandomUserAgent(file string) ([]string, error) { 364 | content, err := ioutil.ReadFile(file) 365 | if err != nil { 366 | log.Fatalf("User-Agent file error :", err) 367 | } 368 | return strings.Split(string(content), "\n"), nil 369 | } 370 | 371 | func (r *Runner) waitForHandlers(jobHandlerAmount int) { 372 | for { 373 | time.Sleep(100 * time.Millisecond) 374 | if jobHandlerAmount > 0 && jobHandlerAmount == r.handler.HTTP.GetJobAmount() && r.handler.HTTP.GetInProcess() == 0 { 375 | return 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /internal/scan/behavior.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | type behavior struct { 4 | status bool 5 | } 6 | 7 | func NewBehavior() *behavior { 8 | return &behavior{} 9 | } 10 | 11 | // Quick detection for unkown behavior 12 | func (b *behavior) QuickDetect(job Job) bool { 13 | count := 0 14 | countMax := len(job.Knowledge.Responses) * 3 //Note : (Number represents the number of if statements in the loop) 15 | for _, resp := range job.Knowledge.Responses { 16 | if resp.StatusCode == job.Http.Response.StatusCode { 17 | count++ 18 | } 19 | if resp.Title == job.Http.Response.Title { 20 | count++ 21 | } 22 | if resp.ContentType == job.Http.Response.ContentType { 23 | count++ 24 | } 25 | } 26 | //If no test did hit and the count is the same length as the list of known data. No unexpected behavior was discovered: 27 | return count != countMax 28 | } 29 | -------------------------------------------------------------------------------- /internal/scan/handler.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Brum3ns/firefly/internal/config" 7 | "github.com/Brum3ns/firefly/internal/knowledge" 8 | "github.com/Brum3ns/firefly/internal/output" 9 | "github.com/Brum3ns/firefly/pkg/request" 10 | "github.com/Brum3ns/firefly/pkg/waitgroup" 11 | ) 12 | 13 | type Handler struct { 14 | Process scan 15 | WaitGroup waitgroup.WaitGroup 16 | JobQueue chan Job 17 | Pool chan chan Job 18 | quit chan bool 19 | Config 20 | } 21 | 22 | // Config given by user input to adapt the scanning process 23 | type Config struct { 24 | Threads int 25 | PayloadVerify string 26 | 27 | // The scanner contains the points to a base structure that contains the base structure 28 | // of all the scanner techniques the handler need. This save memory and gain better preformence in the overall preformance. 29 | // Note : (Static data stored. Read struct DESC) 30 | Scanner *config.Scanner 31 | 32 | // This map holds all the knowledge of all the targets 33 | // !Note : (This map *MUST* be static and not modifed) 34 | Knowledge map[string]knowledge.Knowledge 35 | } 36 | 37 | type Job struct { 38 | OK_knowledge bool 39 | Knowledge knowledge.Knowledge 40 | Encode []string 41 | Http request.Result 42 | } 43 | 44 | // Note : (Alias of structure "output.ResultFinal") 45 | type Result struct { 46 | Output output.ResultFinal 47 | Error error 48 | } 49 | 50 | // Start the handler for the workers by giving the tasks to preform and the amount of workers. 51 | func NewHandler(config Config) Handler { 52 | return Handler{ 53 | Config: config, 54 | JobQueue: make(chan Job), 55 | Pool: make(chan chan Job, config.Threads), 56 | } 57 | } 58 | 59 | // Start all the processes and assign tasks (jobs) to the scanners that are listening. Use the method "Stop()" to stop the scanner. 60 | // Note : (The scanner handler *MUST* run inside a [go]rutine. It can only stop from the method "Stop()" that do send a stop signal to the handler) 61 | func (e *Handler) Run(listener chan<- Result) { 62 | var pResult = make(chan scanResult) 63 | 64 | //Validate process amount: 65 | if e.Threads <= 0 { 66 | e.Threads = 1 67 | } 68 | 69 | // Start the amount of processes related to the amount of given threads: 70 | for i := 0; i < e.Threads; i++ { 71 | e.Process = newScan(e.Config.Scanner, e.Pool) 72 | e.Process.spawnScan(pResult) 73 | } 74 | 75 | // Listen for new jobs from the queue and send it to the job channel for the workers to handle it: 76 | go func() { 77 | for { 78 | select { 79 | case job := <-e.JobQueue: 80 | go func(job Job) { 81 | //Get an available job channel from any running process: 82 | jobChannel := <-e.Pool 83 | 84 | //Give the available process the job: 85 | jobChannel <- job 86 | }(job) 87 | 88 | //Listen for result from any process, if a result is recived, then send it to the listener [chan]nel: 89 | case r := <-pResult: 90 | listener <- makeResult(r) 91 | e.WaitGroup.Done() 92 | } 93 | } 94 | }() 95 | 96 | // Listen a stop signal then wait until all background processes are completed: 97 | if <-e.quit { 98 | e.WaitGroup.Wait() 99 | fmt.Println(":: Scanner handler stopped") 100 | return 101 | } 102 | } 103 | 104 | // Add new jobs (tasks) to be performed by the handler processes: 105 | func (e *Handler) AddJob(httpResult request.Result) { 106 | // Get knowledge for the specific target 107 | knowledge, ok := e.GetKnowledge(httpResult.TargetHashId) 108 | 109 | e.WaitGroup.Add(1) 110 | e.JobQueue <- Job{ 111 | Http: httpResult, 112 | Knowledge: knowledge, 113 | OK_knowledge: ok, 114 | } 115 | } 116 | 117 | func (e *Handler) GetKnowledge(hashid string) (knowledge.Knowledge, bool) { 118 | knowledge, ok := e.Knowledge[hashid] 119 | return knowledge, ok 120 | } 121 | 122 | // Get the amount of job that are active 123 | func (e *Handler) GetJobInProcess() int { 124 | return e.WaitGroup.GetCount() 125 | } 126 | 127 | // Wait until all jobs are done 128 | func (e *Handler) Wait() { 129 | e.WaitGroup.Wait() 130 | } 131 | 132 | func (e *Handler) Stop() { 133 | e.quit <- true 134 | } 135 | 136 | // Start the extract scanning process 137 | func makeResult(pResult scanResult) Result { 138 | req := pResult.Http.Request 139 | resp := pResult.Http.Response 140 | 141 | return Result{ 142 | Output: output.ResultFinal{ 143 | TargetHashId: pResult.Http.TargetHashId, 144 | RequestId: pResult.Http.RequestId, 145 | Tag: pResult.Http.Tag, 146 | Date: pResult.Http.Date, 147 | Payload: pResult.Http.Payload, 148 | UnkownBehavior: pResult.UnkownBehavior, 149 | OK: true, 150 | 151 | Request: output.Request{ 152 | URL: req.RequestURI, 153 | URLOriginal: req.URLOriginal, 154 | Host: req.URL.Host, 155 | Scheme: req.URL.Scheme, 156 | Method: req.Method, 157 | PostBody: req.Body, 158 | Proto: req.Proto, 159 | Headers: req.HeadersOriginal, 160 | }, 161 | Response: output.Response{ 162 | Time: resp.Time, 163 | Host: resp.Request.Host, 164 | Body: resp.Body, 165 | Title: resp.Title, 166 | Proto: resp.Proto, 167 | IPAddress: resp.IPAddress, 168 | StatusCode: resp.StatusCode, 169 | WordCount: resp.WordCount, 170 | LineCount: resp.LineCount, 171 | ContentType: resp.ContentType, 172 | ContentLength: resp.ResponseBodySize, 173 | HeaderAmount: resp.HeaderAmount, 174 | Headers: resp.Header, 175 | }, 176 | Scanner: output.Scanner{ 177 | Extract: pResult.Extract, 178 | Diff: pResult.Difference, 179 | Transformation: pResult.Transformation, 180 | //Data... 181 | }, 182 | 183 | Error: nil, 184 | }, 185 | Error: nil, 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /internal/scan/scan.go: -------------------------------------------------------------------------------- 1 | package scan 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/Brum3ns/firefly/internal/config" 7 | "github.com/Brum3ns/firefly/pkg/design" 8 | "github.com/Brum3ns/firefly/pkg/extract" 9 | "github.com/Brum3ns/firefly/pkg/httpdiff" 10 | "github.com/Brum3ns/firefly/pkg/httpprepare" 11 | "github.com/Brum3ns/firefly/pkg/request" 12 | "github.com/Brum3ns/firefly/pkg/transformation" 13 | ) 14 | 15 | // scan represents the scan that executes the job 16 | type scan struct { 17 | jobChannel chan Job 18 | pool chan chan Job 19 | 20 | Scanner *config.Scanner //!Note : (Static data stored. Read struct DESC) 21 | Result scanResult 22 | } 23 | 24 | type scanResult struct { 25 | UnkownBehavior bool 26 | Http request.Result 27 | Extract extract.Result 28 | Difference httpdiff.Result 29 | Transformation transformation.Result 30 | } 31 | 32 | // Create a new scan 33 | func newScan(scanner *config.Scanner, pool chan chan Job) scan { 34 | return scan{ 35 | pool: pool, 36 | Scanner: scanner, 37 | jobChannel: make(chan Job), 38 | } 39 | } 40 | 41 | // Spawn a new scan process 42 | func (s scan) spawnScan(result chan scanResult) { 43 | go func() { 44 | for { 45 | // Add the current spawned scan into the scanning queue: 46 | s.pool <- s.jobChannel 47 | 48 | //A job was given, start processing it 49 | select { 50 | case job := <-s.jobChannel: 51 | result <- s.scan(job) 52 | } 53 | } 54 | }() 55 | } 56 | 57 | // Start a new process 58 | func (s scan) scan(job Job) scanResult { 59 | var ( 60 | //Behavior contains the methods that check unknown behavior along with the behavioral status of the current job: 61 | behavior = NewBehavior() 62 | 63 | // Scanning techniques 64 | ResultExtract extract.Result 65 | ResultDifference httpdiff.Result 66 | ResultTransformation transformation.Result 67 | ) 68 | 69 | //Quick basic behavior checks: 70 | if job.OK_knowledge { 71 | behavior.status = behavior.QuickDetect(job) 72 | } 73 | 74 | //Check if we should preform scanner techniques or not: 75 | if !s.Scanner.DisablesTechniques { 76 | 77 | if s.Scanner.OK_Diff { 78 | ResultDifference = s.Difference(job) 79 | } 80 | if s.Scanner.OK_Extract { 81 | ResultExtract = s.Extract(job) 82 | } 83 | if s.Scanner.OK_Transformation { 84 | ResultTransformation = s.Transformation(job) 85 | } 86 | } 87 | 88 | //Confirm the unexpected behavior 89 | if (job.OK_knowledge && !behavior.status) && (ResultDifference.OK || ResultExtract.OK || ResultTransformation.OK) { 90 | behavior.status = true 91 | } 92 | 93 | return scanResult{ 94 | UnkownBehavior: behavior.status, 95 | Http: job.Http, 96 | Extract: ResultExtract, 97 | Difference: ResultDifference, 98 | Transformation: ResultTransformation, 99 | } 100 | } 101 | 102 | // Scan for errors, patterns in the response that have been triggered by the payload 103 | func (s scan) Extract(job Job) extract.Result { 104 | e := s.Scanner.Extract 105 | e.AddJob( 106 | job.Http.Response.Body, 107 | job.Http.Response.HeaderString, 108 | ) 109 | 110 | result := e.Run() 111 | 112 | //In case we do have knowledge of the target. 113 | if job.OK_knowledge { 114 | //Extract all the new unique regex and patterns discovered. 115 | //Note: Current map result MUST be first. Return the same order of "currentMaps" as the result. 116 | currentMaps := []map[string]int{ 117 | result.RegexBody, 118 | result.RegexHeaders, 119 | result.PatternBody, 120 | result.PatternHeaders, 121 | } 122 | knownMaps := []map[string][]int{ 123 | job.Knowledge.Combine.Extract.RegexBody, 124 | job.Knowledge.Combine.Extract.RegexHeaders, 125 | job.Knowledge.Combine.Extract.PatternBody, 126 | job.Knowledge.Combine.Extract.PatternHeaders, 127 | } 128 | ExtractMapDiff, totalhits := extract.GetMultiUnique(currentMaps, knownMaps, job.Http.Payload) 129 | 130 | //This shall not be happening, then it's a bug (critical) 131 | if len(ExtractMapDiff) != len(currentMaps) { 132 | log.Fatal(design.STATUS.CRITICAL, 133 | " The current maps used in the handler - extract process containing the list of maps did not match the diff list. Please report this to the official Firefly Github repository", 134 | ) 135 | } 136 | //Note : (The same order as in "compareMaps") 137 | result = extract.Result{ 138 | OK: (totalhits > 0), 139 | TotalHits: totalhits, 140 | RegexBody: ExtractMapDiff[0], 141 | RegexHeaders: ExtractMapDiff[1], 142 | PatternBody: ExtractMapDiff[2], 143 | PatternHeaders: ExtractMapDiff[3], 144 | } 145 | } 146 | return result 147 | } 148 | 149 | // Scan for differences in the current compare to the known HTTP responses 150 | func (s scan) Difference(job Job) httpdiff.Result { 151 | //Make a new difference instant and provided the current HTTP response body and headers: 152 | diff := httpdiff.NewDifference( 153 | httpdiff.Config{ 154 | Payload: job.Http.Payload, 155 | PayloadVerify: job.Knowledge.PayloadVerify, 156 | Compare: httpdiff.Compare{ 157 | HTMLMergeNode: job.Knowledge.Combine.HTMLNode, 158 | HeaderMergeNode: job.Knowledge.Combine.HeaderNode, 159 | }, 160 | Randomness: s.Scanner.Randomness, 161 | Filter: s.Scanner.HttpDiffFilter, 162 | }, 163 | ) 164 | 165 | headerResult := diff.GetHeadersDiff(httpprepare.GetHeaderNode(job.Http.Response.Header)) 166 | htmlResult := diff.GetHTMLNodeDiff(httpprepare.GetHTMLNode(job.Http.Response.Body)) 167 | 168 | return httpdiff.Result{ 169 | OK: (headerResult.OK || htmlResult.OK), 170 | HeaderResult: headerResult, 171 | HTMLResult: htmlResult, 172 | } 173 | } 174 | 175 | // Scan for transformations within the payload 176 | func (s scan) Transformation(job Job) transformation.Result { 177 | tfmt := s.Scanner.Transformation 178 | return tfmt.Detect(job.Http.Response.Body, job.Http.Payload) 179 | } 180 | -------------------------------------------------------------------------------- /internal/setup/setup.go: -------------------------------------------------------------------------------- 1 | package setup 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/Brum3ns/firefly/internal/banner" 10 | "github.com/Brum3ns/firefly/internal/global" 11 | "github.com/Brum3ns/firefly/pkg/design" 12 | ) 13 | 14 | // Struct only if new development in the future will happen (inc none used variables) 15 | type resource struct { 16 | folder_DB string 17 | folder_HOME string 18 | folder_CONFIG string 19 | } 20 | 21 | // Setup the .config folder and install needed resources (Only run first time the tool is executed) 22 | // If a new installation is being processed, exit when finished 23 | func Setup() (bool, error) { 24 | rsource := &resource{ 25 | folder_DB: global.DIR_DB, 26 | folder_HOME: global.DIR_HOME, 27 | folder_CONFIG: global.DIR_CONFIG, 28 | } 29 | 30 | // Create config folder and ignore error in case it already exists 31 | os.MkdirAll(rsource.folder_CONFIG, os.ModePerm) 32 | 33 | // Check if the db git repository is installed already, otherwise install it 34 | if _, err := os.Stat(rsource.folder_DB); err != nil && os.IsNotExist(err) { 35 | banner.Banner() 36 | if rsource.approve() { 37 | fmt.Println("Installing DB from the firefly-db Github repository") 38 | InstallDB() 39 | fmt.Printf("Firefly's database is installed and can be find in the folder: %s\n", rsource.folder_DB) 40 | os.Exit(0) 41 | 42 | } else { 43 | return false, errors.New("Firefly's needs to have its database (resources) installed to work properly") 44 | } 45 | } 46 | return true, nil 47 | } 48 | 49 | // Approve the firefly db download and installation 50 | func (rs *resource) approve() bool { 51 | var confirm string 52 | fmt.Println(design.STATUS.INFO+" The Github repository - \"https://github.com/Brum3ns/firefly-db\" contains all the resources that Firefly use, and is needed to be installed. It will be installed into the folder:"+design.COLOR.ORANGE, rs.folder_DB, design.COLOR.WHITE) 53 | fmt.Print(design.DEBUG.INPUT + " Write \"" + design.COLOR.ORANGE + "ok" + design.COLOR.WHITE + "\" to confirm: ") 54 | fmt.Scanln(&confirm) 55 | 56 | if confirm == "ok" { 57 | return true 58 | } else { 59 | fmt.Println(design.STATUS.FAIL + " Firefly needs its resources to run") 60 | rs.exit(0) 61 | } 62 | return false 63 | } 64 | 65 | // Exit the setup process 66 | func (rs *resource) exit(n int) { 67 | fmt.Println(design.STATUS.FAIL, "Setup process aborted") 68 | os.Exit(n) 69 | } 70 | 71 | // Update all resources in the ".config/firefly/db/*" from the firefly-db Github repository 72 | // Note : Repository - https://github.com/Brum3ns/firefly-db.git 73 | func InstallDB() (string, error) { 74 | const gitURL = "https://github.com/Brum3ns/firefly-db.git" 75 | 76 | cmd := exec.Command("git", "clone", gitURL, global.DIR_CONFIG) 77 | 78 | stdout, err := cmd.Output() 79 | if err != nil { 80 | return "", err 81 | } 82 | return string(stdout), nil 83 | } 84 | 85 | func UpdateDB() (string, error) { 86 | cmd := exec.Command("git", "-C", global.DIR_CONFIG, "pull") 87 | 88 | stdout, err := cmd.Output() 89 | if err != nil { 90 | return "", err 91 | } 92 | 93 | return string(stdout), nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/ui/buffer.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | ) 8 | 9 | type buffer struct { 10 | data [10]string 11 | head int 12 | tail int 13 | length int 14 | } 15 | 16 | func (b *buffer) Append(item string) { 17 | if b.length >= 10 { 18 | b.data[b.head] = item 19 | b.head = (b.head + 1) % 10 20 | b.tail = (b.tail + 1) % 10 21 | 22 | } else { 23 | b.data[b.head] = item 24 | b.head = (b.head + 1) % 10 25 | b.length++ 26 | } 27 | } 28 | 29 | func (b *buffer) Print(style ...lipgloss.Style) { 30 | for i := 0; i < b.length; i++ { 31 | index := (b.tail + 1) % 10 32 | 33 | if len(style) > 0 { 34 | //style[0] 35 | } else { 36 | fmt.Println("%s", b.data[index]) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /internal/ui/help.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/help" 5 | "github.com/charmbracelet/bubbles/key" 6 | "github.com/charmbracelet/lipgloss" 7 | ) 8 | 9 | type helpmenu struct { 10 | menu help.Model 11 | style lipgloss.Style 12 | } 13 | 14 | type keyMap struct { 15 | Up key.Binding 16 | Down key.Binding 17 | Left key.Binding 18 | Right key.Binding 19 | Tab key.Binding 20 | Help key.Binding 21 | Exit key.Binding 22 | } 23 | 24 | var keys = keyMap{ 25 | Tab: key.NewBinding( 26 | key.WithKeys("tab"), 27 | key.WithHelp("«»", "switch window"), 28 | ), 29 | Up: key.NewBinding( 30 | key.WithKeys("up"), 31 | key.WithHelp("↑", "move up"), 32 | ), 33 | Down: key.NewBinding( 34 | key.WithKeys("down"), 35 | key.WithHelp("↓", "move down"), 36 | ), 37 | Left: key.NewBinding( 38 | key.WithKeys("left"), 39 | key.WithHelp("←", "move left"), 40 | ), 41 | Right: key.NewBinding( 42 | key.WithKeys("right"), 43 | key.WithHelp("→", "move right"), 44 | ), 45 | Help: key.NewBinding( 46 | key.WithKeys("h"), 47 | key.WithHelp("h", "help"), 48 | ), 49 | Exit: key.NewBinding( 50 | key.WithKeys("ctrl+c"), 51 | key.WithHelp("ctrl+c", "exit"), 52 | ), 53 | } 54 | 55 | // ShortHelp returns keybindings to be shown in the mini help view. It's part 56 | // of the key.Map interface. 57 | func (k keyMap) ShortHelp() []key.Binding { 58 | return []key.Binding{k.Help, k.Exit} 59 | } 60 | 61 | // FullHelp returns keybindings for the expanded help view. It's part of the 62 | // key.Map interface. 63 | func (k keyMap) FullHelp() [][]key.Binding { 64 | return [][]key.Binding{ 65 | {k.Up, k.Down, k.Left, k.Right}, // first column 66 | {k.Help, k.Exit}, // second column 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/ui/program.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | // A simple example that shows how to send activity to Bubble Tea in real-time 4 | // through a channel. 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/Brum3ns/firefly/internal/output" 10 | "github.com/Brum3ns/firefly/pkg/statistics" 11 | "github.com/charmbracelet/bubbles/help" 12 | "github.com/charmbracelet/bubbles/key" 13 | "github.com/charmbracelet/bubbles/spinner" 14 | tea "github.com/charmbracelet/bubbletea" 15 | "github.com/charmbracelet/lipgloss" 16 | ) 17 | 18 | const ( 19 | WINDOW_PAYLOAD = 1 20 | WINDOW_TRANSFORMATION = 0 21 | ) 22 | 23 | type ProgramModel struct { 24 | keys keyMap 25 | quit bool 26 | Cancel bool 27 | // Holds the terminal user-interface (UI) design that is presented in the terminal stdin 28 | terminalUI *TerminalUI 29 | // How aften to check if the currecnt terminal width changes in milliseconds 30 | //TerminalWidthCheckDelay time.Duration 31 | // Listen if the terminal width change during the running process 32 | //channelScreenWidth chan int 33 | // Listen for new result to be deisplayed in the terminal user interface 34 | //channelResult chan Data 35 | // The data that contains all data that will be displayed during the process 36 | data Data 37 | // Represent the visual progress bar 38 | spinner spinner.Model 39 | 40 | // Contains the index of the current window 41 | window int 42 | 43 | help helpmenu 44 | } 45 | 46 | type Data struct { 47 | ResultFinal output.ResultFinal 48 | stats statistics.Statistic 49 | } 50 | 51 | func NewProgram() *tea.Program { 52 | return tea.NewProgram(ProgramModel{ 53 | terminalUI: NewTerminalUI(), 54 | spinner: spinner.New(spinner.WithSpinner(spinner.MiniDot)), 55 | //channelResult: make(chan Data), 56 | //channelScreenWidth: make(chan int), 57 | //TerminalWidthCheckDelay: 1, //Todo 58 | help: helpmenu{ 59 | menu: help.New(), 60 | style: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF75B7")), 61 | }, 62 | 63 | keys: keyMap{ 64 | Tab: key.NewBinding( 65 | key.WithKeys("tab"), 66 | key.WithHelp("«»", "switch window"), 67 | ), 68 | Up: key.NewBinding( 69 | key.WithKeys("up"), 70 | key.WithHelp("↑", "move up"), 71 | ), 72 | Down: key.NewBinding( 73 | key.WithKeys("down"), 74 | key.WithHelp("↓", "move down"), 75 | ), 76 | Left: key.NewBinding( 77 | key.WithKeys("left"), 78 | key.WithHelp("←", "move left"), 79 | ), 80 | Right: key.NewBinding( 81 | key.WithKeys("right"), 82 | key.WithHelp("→", "move right"), 83 | ), 84 | Exit: key.NewBinding( 85 | key.WithKeys("ctrl+c"), 86 | key.WithHelp("ctrl+c", "exit"), 87 | ), 88 | }, 89 | }) 90 | } 91 | 92 | // Processes that are running in the background during the program core process 93 | func (m ProgramModel) Init() tea.Cmd { 94 | return tea.Batch( 95 | m.spinner.Tick, 96 | ) 97 | } 98 | 99 | // Listen for changes during by intercepting commands from other processes. 100 | // Then change the needed data (Ex: the result of the runner) 101 | func (m ProgramModel) Update(message tea.Msg) (tea.Model, tea.Cmd) { 102 | switch msg := message.(type) { 103 | case tea.KeyMsg: 104 | switch { 105 | case key.Matches(msg, m.keys.Tab): 106 | m.changeWindow() 107 | case key.Matches(msg, m.keys.Up): 108 | fmt.Println("↑") 109 | case key.Matches(msg, m.keys.Down): 110 | fmt.Println("↓") 111 | case key.Matches(msg, m.keys.Left): 112 | fmt.Println("←") 113 | case key.Matches(msg, m.keys.Right): 114 | fmt.Println("→") 115 | 116 | case key.Matches(msg, m.keys.Exit): 117 | m.quit = true 118 | return m, tea.Quit 119 | } 120 | return m, nil 121 | 122 | case statistics.Statistic: 123 | m.data.stats = msg 124 | return m, func() tea.Msg { return m.data } 125 | 126 | case output.ResultFinal: 127 | m.data.ResultFinal = msg 128 | return m, func() tea.Msg { return m.data } 129 | 130 | case spinner.TickMsg: 131 | var cmd tea.Cmd 132 | m.spinner, cmd = m.spinner.Update(msg) 133 | return m, cmd 134 | 135 | // Updated the spinner (loadingbar) 136 | default: 137 | return m, nil 138 | } 139 | } 140 | 141 | // Output the terminal user interface (UI) to the terminal 142 | func (m ProgramModel) View() string { 143 | var view string 144 | 145 | m.prepareTerminalUI() 146 | view = m.terminalUI.Render(m.data) 147 | 148 | /* model := m.currentFocusedModel() 149 | if m.state == timerView { 150 | s += lipgloss.JoinHorizontal(lipgloss.Top, focusedModelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), modelStyle.Render(m.spinner.View())) 151 | } else { 152 | s += lipgloss.JoinHorizontal(lipgloss.Top, modelStyle.Render(fmt.Sprintf("%4s", m.timer.View())), focusedModelStyle.Render(m.spinner.View())) 153 | } 154 | s += helpStyle.Render(fmt.Sprintf("\ntab: focus next • n: new %s • q: exit\n", model)) 155 | */ 156 | view += "\n" + m.help.menu.View(m.keys) 157 | 158 | return view 159 | } 160 | 161 | func (m ProgramModel) prepareTerminalUI() { 162 | m.terminalUI.SetWindow(m.window) 163 | m.terminalUI.SetSpinner(m.spinner.View()) 164 | } 165 | 166 | func (m ProgramModel) changeWindow() { 167 | if m.window == WINDOW_PAYLOAD { 168 | m.window = WINDOW_TRANSFORMATION 169 | } else { 170 | m.window = WINDOW_PAYLOAD 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /internal/ui/progressbar.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/Brum3ns/firefly/internal/global" 9 | "github.com/Brum3ns/firefly/pkg/statistics" 10 | "github.com/charmbracelet/bubbles/spinner" 11 | ) 12 | 13 | type ProgressBar struct { 14 | Counter int 15 | time time.Time 16 | delay time.Duration 17 | Stats *statistics.Statistic 18 | SegmentedDigits []string 19 | spinner spinner.Model 20 | } 21 | 22 | func NewProgressBar(delayMS int, statistic *statistics.Statistic) ProgressBar { 23 | return ProgressBar{ 24 | Counter: 0, 25 | delay: time.Duration(delayMS) * time.Millisecond, 26 | time: time.Now(), 27 | Stats: statistic, 28 | spinner: spinner.New(spinner.WithSpinner(spinner.MiniDot)), 29 | } 30 | } 31 | 32 | // Display the progress the statistic structure 33 | func (p *ProgressBar) Print() { 34 | t := p.Stats.GetTime() 35 | p.spinner, _ = p.spinner.Update(spinner.Tick()) 36 | 37 | //fmt.Println(global.TERMINAL_CLEAR, "->", p.delay, x) 38 | fmt.Fprintf(os.Stderr, "%s%s Request:[%d], Scanned:[%d], Behavior:[%d], Filtered:[%d], Error:[%d], Time:[%d:%02d:%02d]", 39 | global.TERMINAL_CLEAR, 40 | p.spinner.View(), 41 | p.Stats.Request.GetCount(), 42 | p.Stats.Scanner.GetCount(), 43 | p.Stats.Behavior.GetCount(), 44 | p.Stats.Response.GetFilterCount(), 45 | p.Stats.Request.GetErrorCount(), 46 | t[0], t[1], t[2], 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /internal/ui/ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/charmbracelet/bubbles/list" 10 | "github.com/charmbracelet/lipgloss" 11 | "golang.org/x/term" 12 | ) 13 | 14 | const ( 15 | AUTHOR = "By: @yeswehack / Brumens" 16 | TOOL = "FireFly (v1.3.1)" 17 | MODE_DARK = "#383838" 18 | MODE_LIGHT = "#D9DCCF" 19 | BACKGROUND_PATTERN = "萤火虫" 20 | 21 | COLOR_BLACK = lipgloss.Color("#000000") 22 | COLOR_WHITE = lipgloss.Color("#D9DCCF") 23 | COLOR_GREY = lipgloss.Color("#383838") 24 | COLOR_GREEN = lipgloss.Color("#3AF191") 25 | COLOR_ORANGE = lipgloss.Color("#D98D00") 26 | COLOR_YELLOW = lipgloss.Color("#FFDF00") 27 | COLOR_RED = lipgloss.Color("#EB2D3A") 28 | ) 29 | 30 | type TerminalUI struct { 31 | Data 32 | Style *style 33 | listPayload list.Model 34 | listTransformation list.Model 35 | spinner string 36 | window int 37 | } 38 | 39 | type style struct { 40 | item lipgloss.Style 41 | core lipgloss.Style 42 | banner lipgloss.Style 43 | author lipgloss.Style 44 | column lipgloss.Style 45 | payload lipgloss.Style 46 | infobox lipgloss.Style 47 | bar lipgloss.Style 48 | done lipgloss.Style 49 | table lipgloss.Style 50 | spinner lipgloss.Style 51 | window lipgloss.Style 52 | windowActive lipgloss.Style 53 | adColor lipgloss.AdaptiveColor 54 | oversize func(s ...string) string 55 | header func(s ...string) string 56 | width 57 | } 58 | 59 | type width struct { 60 | columnRight int 61 | columnleft int 62 | columnMid int 63 | } 64 | 65 | type item struct { 66 | title, desc string 67 | } 68 | 69 | func (i item) Title() string { return i.title } 70 | func (i item) Description() string { return i.desc } 71 | func (i item) FilterValue() string { return i.title } 72 | 73 | func NewTerminalUI() *TerminalUI { 74 | demoList := []list.Item{ 75 | item{title: "Raspberry Pi’s", desc: "I have ’em all over my house"}, 76 | } 77 | 78 | return &TerminalUI{ 79 | Style: NewStyle(), 80 | listPayload: list.New(demoList, list.NewDefaultDelegate(), 0, 0), 81 | listTransformation: list.New(demoList, list.NewDefaultDelegate(), 0, 0), 82 | } 83 | } 84 | 85 | // Make style definitions for the terminal user-interface (UI) 86 | func NewStyle() *style { 87 | border := lipgloss.NormalBorder() 88 | adaptiveColor := lipgloss.AdaptiveColor{Light: MODE_LIGHT, Dark: MODE_DARK} 89 | s := &style{ 90 | adColor: adaptiveColor, 91 | bar: lipgloss.NewStyle(), 92 | spinner: lipgloss.NewStyle(), 93 | core: lipgloss.NewStyle().Padding(1, 2, 1, 2), 94 | item: lipgloss.NewStyle().Padding(0, 1, 0, 1), 95 | banner: lipgloss.NewStyle().Foreground(COLOR_YELLOW), 96 | payload: lipgloss.NewStyle().Foreground(COLOR_ORANGE), 97 | author: lipgloss.NewStyle().Foreground(COLOR_WHITE), 98 | 99 | done: lipgloss.NewStyle(). 100 | Padding(0, 1, 0, 1). 101 | SetString("✓"). 102 | Foreground(COLOR_GREEN), 103 | 104 | infobox: lipgloss.NewStyle(). 105 | Padding(0, 1, 0, 1). 106 | Background(COLOR_WHITE). 107 | Foreground(COLOR_BLACK), 108 | 109 | oversize: lipgloss.NewStyle(). 110 | Foreground(COLOR_GREY). 111 | Render, 112 | 113 | header: lipgloss.NewStyle(). 114 | Foreground(COLOR_GREY). 115 | Bold(true). 116 | Render, 117 | 118 | table: lipgloss.NewStyle(). 119 | Foreground(COLOR_GREY). 120 | Height(20), 121 | 122 | window: lipgloss.NewStyle(). 123 | Width(15). 124 | Height(5). 125 | Align(lipgloss.Center, lipgloss.Center). 126 | BorderStyle(lipgloss.HiddenBorder()), 127 | 128 | windowActive: lipgloss.NewStyle(). 129 | Width(15). 130 | Height(5). 131 | Align(lipgloss.Center, lipgloss.Center). 132 | BorderStyle(lipgloss.NormalBorder()). 133 | BorderForeground(lipgloss.Color("69")), 134 | 135 | column: lipgloss.NewStyle(). 136 | Align(lipgloss.Left). 137 | Border(border, false, true, true, false). 138 | BorderForeground(adaptiveColor). 139 | Height(20), 140 | } 141 | 142 | return s 143 | } 144 | 145 | func (t *TerminalUI) Render(r Data) string { 146 | s := t.Style 147 | screenWidth, _, _ := term.GetSize(int(os.Stdout.Fd())) 148 | s.width.setColumns(screenWidth) 149 | 150 | // Prepare banner 151 | banner := t.banner(screenWidth) 152 | 153 | //Get the process time 154 | timerDuration := r.stats.GetTime() 155 | timer := fmt.Sprintf("%d:%02d:%02d", timerDuration[0], timerDuration[1], timerDuration[2]) 156 | 157 | // columnWidth := screenWidth / 2 158 | stringUI := strings.Builder{} 159 | 160 | // Add the banner at the top 161 | stringUI.WriteString(banner + "\n") 162 | 163 | //Make all the columns 164 | { 165 | columnLeft := s.column.Render( 166 | s.HeaderRender(lipgloss.Center, "Analyzer", s.width.columnleft) + 167 | lipgloss.JoinVertical(lipgloss.Left, 168 | s.viewItem("Hits........", 0), 169 | s.viewItem("Chars.......", 0), 170 | s.viewItem("Patterns....", 0), 171 | s.viewItem("Mutations...", 0), 172 | s.viewItem("Reflections.", 0), 173 | ) + "\n" + 174 | s.HeaderRender(lipgloss.Center, "Scanner", s.width.columnleft) + 175 | lipgloss.JoinVertical(lipgloss.Left, 176 | s.viewItem("Behavior...", r.stats.Behavior.GetCount()), 177 | s.viewItem("Difference.", r.stats.Difference.GetCount()), 178 | s.viewItem("Transform..", r.stats.Transformation.GetCount()), 179 | ) + "\n" + 180 | s.HeaderRender(lipgloss.Center, "Request", s.width.columnleft) + 181 | lipgloss.JoinVertical(lipgloss.Left, 182 | s.viewItem("Requests.....", r.stats.Request.GetCount()), 183 | s.viewItem("Responses....", r.stats.Response.GetCount()), 184 | s.viewItem("Filtered.....", r.stats.Request.GetFilterCount()), 185 | s.viewItem("Forbidden....", r.stats.Request.GetCountForbidden()), 186 | s.viewItem("Request Err..", r.stats.Request.GetErrorCount()), 187 | s.viewItem("Response Err.", r.stats.Response.GetErrorCount()), 188 | s.viewItem("Average Time.", r.stats.Response.GetAverageTime()), 189 | ), 190 | ) 191 | columMid := s.column.Render( 192 | s.HeaderRender(lipgloss.Center, "Payload", s.width.columnMid) + 193 | lipgloss.JoinVertical(lipgloss.Left), 194 | //t.listPayload.View(), 195 | ) 196 | 197 | columnRight := s.column.Render( 198 | s.HeaderRender(lipgloss.Center, "Transformation", s.width.columnMid) + 199 | lipgloss.JoinVertical(lipgloss.Left), //t.listTransformation.View(), 200 | 201 | ) 202 | 203 | //Write all columns 204 | stringUI.WriteString( 205 | lipgloss.JoinHorizontal(lipgloss.Top, 206 | lipgloss.JoinHorizontal( 207 | lipgloss.Top, 208 | columnLeft, 209 | columMid, 210 | columnRight, 211 | ), 212 | ) + "\n", 213 | ) 214 | } 215 | 216 | // Status bar 217 | { 218 | statusBar := s.bar.Render( 219 | lipgloss.JoinHorizontal(lipgloss.Left, 220 | s.infobox.Background(COLOR_GREEN).Render(timer), 221 | s.viewItem("Current", r.ResultFinal.Payload, s.payload), 222 | ), 223 | ) 224 | stringUI.WriteString(statusBar + "\n") 225 | } 226 | 227 | if screenWidth > 0 { 228 | t.Style.core = t.Style.core.MaxWidth(screenWidth) 229 | } 230 | 231 | return t.Style.core.Render(stringUI.String()) 232 | } 233 | 234 | func (s *style) HeaderRender(position lipgloss.Position, header string, width int) string { 235 | return lipgloss.JoinVertical(position, 236 | lipgloss.Place(width, 1, 237 | lipgloss.Center, lipgloss.Center, 238 | s.header(header), 239 | lipgloss.WithWhitespaceChars("─"), 240 | lipgloss.WithWhitespaceForeground(s.adColor), 241 | ), 242 | ) + "\n" 243 | } 244 | 245 | /* func (s *style) TableRender(position lipgloss.Position, table string, width int) string { 246 | return lipgloss.JoinVertical(position, 247 | lipgloss.Place(width, 1, 248 | lipgloss.Center, lipgloss.Center, 249 | table, 250 | lipgloss.WithWhitespaceForeground(s.adColor), 251 | ), 252 | ) + "\n" 253 | } 254 | */ 255 | // Set the window that will be in focus 256 | func (m *TerminalUI) SetSpinner(spinner string) { 257 | m.spinner = spinner 258 | } 259 | 260 | func (m *TerminalUI) SetWindow(wid int) { 261 | m.window = wid 262 | } 263 | 264 | // Set column width and return the width used from the screen width 265 | func (w *width) setColumns(screenWidth int) int { 266 | sw := screenWidth / 3 267 | widthUsed := 0 268 | 269 | //Left column 270 | if sw >= 24 { 271 | w.columnleft = 24 272 | widthUsed += 24 273 | sw = (screenWidth - 24) 274 | } else { 275 | w.columnleft = sw 276 | widthUsed += sw 277 | } 278 | sw = (screenWidth - w.columnleft) / 2 279 | w.columnRight = sw 280 | w.columnMid = sw 281 | 282 | return widthUsed 283 | } 284 | 285 | // Show the banner in the terminal UI 286 | func (t *TerminalUI) banner(width int) string { 287 | banner := lipgloss.Place(width, 1, 288 | lipgloss.Center, lipgloss.Center, 289 | t.Style.banner.Render(TOOL), 290 | lipgloss.WithWhitespaceChars(BACKGROUND_PATTERN), 291 | lipgloss.WithWhitespaceForeground(t.Style.adColor), 292 | ) 293 | author := lipgloss.Place(width, 1, 294 | lipgloss.Center, lipgloss.Center, 295 | t.Style.author.Render(AUTHOR), 296 | lipgloss.WithWhitespaceChars(BACKGROUND_PATTERN), 297 | lipgloss.WithWhitespaceForeground(t.Style.adColor), 298 | ) 299 | return banner + "\n" + author 300 | } 301 | 302 | // Take a argument name and a value that represent an unkown value. 303 | // The value vill be escaped with the 'strconv.Quote()' function to avoid ASNI injections. 304 | // If the value given is longer than 20 characters it will be cutted. 305 | // !WARNING! The "name" argument MUST BE TRUSTED! 306 | func (s *style) viewItem(trustedNameValue string, value any, lipglosStyle ...lipgloss.Style) string { 307 | var ( 308 | maxLength = 24 309 | v string 310 | oversize string 311 | sep = ":" 312 | ) 313 | 314 | /** The reason why we not use the method: fmt.Sprintf("%v", value) first is because 315 | * we will have a better performance since we need to ASNI escape the string. 316 | * If we know that it is an int type, it can be handled faster. 317 | * Major of the items will be int based values 318 | */ 319 | switch val := value.(type) { 320 | case string: 321 | v = strconv.Quote(val) 322 | case int: 323 | v = strconv.Itoa(val) 324 | case float64, float32: 325 | if v = "0"; strings.Index(v, ".") != -1 { 326 | v = fmt.Sprintf("%3.f", val) 327 | } 328 | case nil: 329 | v = "-" 330 | default: 331 | v = strconv.Quote(fmt.Sprintf("%v", val)) 332 | } 333 | 334 | // Value are longer than expected then cut it: 335 | if len(v) > maxLength { 336 | oversize = "..." 337 | v = v[0:maxLength] 338 | } 339 | 340 | if len(lipglosStyle) > 0 { 341 | return s.item.Render(trustedNameValue+sep) + lipglosStyle[0].Render(v) + s.oversize(oversize) 342 | } 343 | return s.item.Render(trustedNameValue+sep, v, s.oversize(oversize)) 344 | } 345 | 346 | func (m *style) payloadView() string { 347 | return "test" //spin + info + gap + prog + pkgCount 348 | } 349 | 350 | // Update the process bar 351 | func (s *style) progressBar(header string, current, max int) string { 352 | v := header + strconv.Itoa(current/max) 353 | if current == max { 354 | return s.done.Render(v) 355 | } 356 | return s.spinner.Render() + v 357 | } 358 | -------------------------------------------------------------------------------- /internal/verbose/verbose.go: -------------------------------------------------------------------------------- 1 | package verbose 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/Brum3ns/firefly/internal/global" 7 | "github.com/Brum3ns/firefly/pkg/design" 8 | ) 9 | 10 | // Show verbose on screen (if verbose is enable by the user) 11 | func Show(msg any) { 12 | if global.VERBOSE { 13 | log.Printf(global.TERMINAL_CLEAR, "%v", msg) 14 | } 15 | } 16 | 17 | // Output the error messages using the 'log' package (Function used for easy customization and better error output) 18 | func Error(err error) { 19 | if err != nil { 20 | log.Println(global.TERMINAL_CLEAR, design.STATUS.FAIL, err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | const VERSION = "v1.4.3" 4 | -------------------------------------------------------------------------------- /pkg/design/design.go: -------------------------------------------------------------------------------- 1 | package design 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // static [Icons] and design variables: 9 | var ( 10 | // Colors: 11 | COLOR = &Color{ 12 | WHITE: "\033[0m", 13 | WHITEBG: "\033[47m", 14 | BLACK: "\033[30m", 15 | GREY: "\033[90m", 16 | GREYLIGHT: "\033[1;90m", 17 | GREYBG: "\033[1;40m", 18 | RED: "\033[31m", 19 | REDLIGHT: "\033[1:31m", 20 | REDBG: "\033[1;41m", 21 | ORANGE: "\033[33m", 22 | ORANGELIGHT: "\033[1;33m", 23 | ORANGEBG: "\033[43m", 24 | YELLOW: "\033[93m", 25 | GREEN: "\033[32m", 26 | GREENLIGHT: "\033[1;32m", 27 | BLUE: "\033[34m", 28 | BLUELIGHT: "\033[36m", 29 | PINK: "\033[1;35m", 30 | PURPEL: "\033[35m", 31 | } 32 | 33 | DETECT = &Detect{ 34 | Certain: (COLOR.REDBG + ("Certain") + COLOR.WHITE), 35 | Firm: (COLOR.ORANGEBG + ("Firm") + COLOR.WHITE), 36 | Tentative: (COLOR.GREYBG + ("Tentative") + COLOR.WHITE), 37 | } 38 | 39 | ICON = &Icons{ 40 | PLUS: ("[" + COLOR.GREENLIGHT + ("+") + COLOR.WHITE + "]"), 41 | AWARE: ("[" + COLOR.ORANGELIGHT + ("!") + COLOR.WHITE + "]"), 42 | NEGATIVE: ("[" + COLOR.REDLIGHT + ("-") + COLOR.WHITE + "]"), 43 | POSSIBLE: ("[" + COLOR.ORANGELIGHT + ("?") + COLOR.WHITE + "]"), 44 | } 45 | 46 | STATUS = &Status{ 47 | OK: ("[" + COLOR.GREEN + ("OK") + COLOR.WHITE + "]"), 48 | SUCCESS: ("[" + COLOR.GREENLIGHT + ("OK") + COLOR.WHITE + "]"), 49 | INFO: ("[" + COLOR.BLUE + ("INF") + COLOR.WHITE + "]"), 50 | FAIL: ("[" + COLOR.RED + ("FAI") + COLOR.WHITE + "]"), 51 | WARNING: ("[" + COLOR.ORANGE + ("WAR") + COLOR.WHITE + "]"), 52 | ERROR: (COLOR.REDBG + ("ERROR") + COLOR.WHITE), 53 | CRITICAL: (COLOR.REDBG + ("CRITICAL") + COLOR.WHITE), 54 | } 55 | 56 | DEBUG = &Debug{ 57 | DEBUG: ("[" + COLOR.BLUELIGHT + ("DEBUG") + COLOR.WHITE + "]"), 58 | PAYLOAD: ("[" + COLOR.GREEN + ("PAYLOAD") + COLOR.WHITE + "]"), 59 | INPUT: ("[" + COLOR.BLUELIGHT + ("INPUT") + COLOR.WHITE + "]"), 60 | EXAMPLE: (COLOR.ORANGE + ("Exemple") + COLOR.WHITE), 61 | } 62 | 63 | BEHAVIOR = &Behavior{ 64 | NONE: ("----"), 65 | TRANSFORMATION: (COLOR.REDBG + ("Tfmt") + COLOR.WHITE), 66 | DIFF: (COLOR.REDBG + ("Diff") + COLOR.WHITE), 67 | TIME: (COLOR.ORANGEBG + ("Time") + COLOR.WHITE), 68 | REFLECT: (COLOR.GREYBG + ("Reflect") + COLOR.WHITE), 69 | PATTERN: (COLOR.ORANGEBG + ("Pattern") + COLOR.WHITE), 70 | } 71 | 72 | // Colorized HTTP status codes: 73 | STATUSCODE_COLOR = map[int]string{ 74 | //(Default Status code colors) 75 | 1: (COLOR.GREYLIGHT + "{CODE}" + COLOR.WHITE), 76 | 2: (COLOR.GREENLIGHT + "{CODE}" + COLOR.WHITE), 77 | 3: (COLOR.BLUELIGHT + "{CODE}" + COLOR.WHITE), 78 | 4: (COLOR.PURPEL + "{CODE}" + COLOR.WHITE), 79 | 5: (COLOR.PINK + "{CODE}" + COLOR.WHITE), 80 | //(100) 81 | 100: (COLOR.GREY + "100" + COLOR.WHITE), 82 | //(200) 83 | 200: (COLOR.GREEN + "200" + COLOR.WHITE), 84 | //(300) 85 | 301: (COLOR.BLUELIGHT + "301" + COLOR.WHITE), 86 | 302: (COLOR.BLUE + "302" + COLOR.WHITE), 87 | //(404) 88 | 400: (COLOR.PURPEL + "400" + COLOR.WHITE), 89 | 404: (COLOR.GREY + "404" + COLOR.WHITE), 90 | 403: (COLOR.RED + "403" + COLOR.WHITE), 91 | 429: (COLOR.REDBG + "429" + COLOR.WHITE), 92 | //(500) 93 | 500: (COLOR.PINK + "500" + COLOR.WHITE), 94 | 501: (COLOR.PINK + "501" + COLOR.WHITE), 95 | 502: (COLOR.YELLOW + "502" + COLOR.WHITE), 96 | 503: (COLOR.ORANGELIGHT + "503" + COLOR.WHITE), 97 | } 98 | ) 99 | 100 | // Store all design values 101 | type Design struct { 102 | Color 103 | Icons 104 | Debug 105 | Detect 106 | Status 107 | Behavior 108 | } 109 | 110 | type Color struct { 111 | WHITE string 112 | WHITEBG string 113 | BLACK string 114 | GREY string 115 | GREYLIGHT string 116 | GREYBG string 117 | RED string 118 | REDLIGHT string 119 | REDBG string 120 | ORANGE string 121 | ORANGELIGHT string 122 | ORANGEBG string 123 | YELLOW string 124 | GREEN string 125 | GREENLIGHT string 126 | BLUE string 127 | BLUELIGHT string 128 | PINK string 129 | PURPEL string 130 | } 131 | 132 | type Icons struct { 133 | PLUS string 134 | AWARE string 135 | NEGATIVE string 136 | POSSIBLE string 137 | } 138 | 139 | type Status struct { 140 | OK string 141 | SUCCESS string 142 | INFO string 143 | FAIL string 144 | ERROR string 145 | WARNING string 146 | CRITICAL string 147 | } 148 | 149 | type Detect struct { 150 | Firm string 151 | Certain string 152 | Tentative string 153 | } 154 | 155 | type Debug struct { 156 | DEBUG string 157 | PAYLOAD string 158 | INPUT string 159 | EXAMPLE string 160 | } 161 | 162 | type Behavior struct { 163 | NONE string 164 | DIFF string 165 | TIME string 166 | REFLECT string 167 | TRANSFORMATION string 168 | PATTERN string 169 | } 170 | 171 | func NewDesign() *Design { 172 | return &Design{ 173 | Color: *COLOR, 174 | Debug: *DEBUG, 175 | Icons: *ICON, 176 | Detect: *DETECT, 177 | Status: *STATUS, 178 | Behavior: *BEHAVIOR, 179 | } 180 | } 181 | 182 | // Colorize the status code and return it as a string 183 | func (d *Design) StatusCode(code int) string { 184 | if v, ok := STATUSCODE_COLOR[code]; ok { 185 | return v 186 | } else if v, ok := STATUSCODE_COLOR[code/100]; ok { 187 | return v 188 | } 189 | return fmt.Sprintf("\033[31m%d\033[0m", code) 190 | } 191 | 192 | // Colorize the word count and return it as a string 193 | func (d *Design) WordCount(wordCount int) string { 194 | return d.Color.BLUELIGHT + strconv.Itoa(wordCount) + d.Color.WHITE 195 | } 196 | 197 | // Colorize the word line and return it as a string 198 | func (d *Design) LineCount(lineCount int) string { 199 | return d.Color.BLUE + strconv.Itoa(lineCount) + d.Color.WHITE 200 | } 201 | 202 | // Colorize the Content Type header value and return it as a string 203 | func (d *Design) ContentType(contentType string) string { 204 | return d.Color.PURPEL + contentType + d.Color.WHITE 205 | } 206 | 207 | // Colorize the Content Length and return it as a string 208 | func (d *Design) ContentLength(contentLength int) string { 209 | return d.Color.PINK + strconv.Itoa(contentLength) + d.Color.WHITE 210 | } 211 | 212 | // Colorize the response time and return it as a string 213 | func (d *Design) ResponseTime(time float64) string { 214 | //Check recived *response time* and add color to it if it's odd from the original responses: 215 | if time > 7 { 216 | return fmt.Sprintf("\033[31m%.3f\033[0m", time) 217 | } 218 | return fmt.Sprintf("\033[1:38m%.3f\033[0m", time) 219 | } 220 | 221 | // Colorize the Content Length and return it as a string 222 | func (d *Design) Highlight(value int) string { 223 | v := strconv.Itoa(value) 224 | if value != 0 { 225 | return d.Color.REDBG + v + d.Color.WHITE 226 | } 227 | return v 228 | } 229 | -------------------------------------------------------------------------------- /pkg/design/style.go: -------------------------------------------------------------------------------- 1 | package design 2 | 3 | import "github.com/charmbracelet/lipgloss" 4 | 5 | const ( 6 | COLOR_RED = lipgloss.Color("#EB2D3A") 7 | COLOR_BLUE = lipgloss.Color("#0069D9") 8 | COLOR_PINK = lipgloss.Color("#D900C7") 9 | COLOR_GREY = lipgloss.Color("#383838") 10 | COLOR_WHITE = lipgloss.Color("#D9DCCF") 11 | COLOR_BLACK = lipgloss.Color("#000000") 12 | COLOR_GREEN = lipgloss.Color("#3AF191") 13 | COLOR_PURPEL = lipgloss.Color("#7F00D9") 14 | COLOR_ORANGE = lipgloss.Color("#D98D00") 15 | COLOR_YELLOW = lipgloss.Color("#FFDF00") 16 | ) 17 | 18 | type Style struct { 19 | } 20 | 21 | func MakeStyle() { 22 | 23 | } 24 | 25 | func (s Style) StatusCode(v int) { 26 | 27 | } 28 | -------------------------------------------------------------------------------- /pkg/encode/encode.go: -------------------------------------------------------------------------------- 1 | // supported encode format for Firfly 2 | package encode 3 | 4 | import ( 5 | "encoding/base32" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "html" 11 | "net/url" 12 | "strings" 13 | ) 14 | 15 | var ( 16 | encodeTo = map[string]func(string) string{ 17 | "surl": func(s string) string { return Url(s) }, 18 | "sdurl": func(s string) string { return DoubleUrl(s) }, 19 | "url": func(s string) string { return UrlEsacpe(s) }, 20 | "durl": func(s string) string { return UrlDoubleEscape(s) }, 21 | "base64": func(s string) string { return Base64(s) }, 22 | "base32": func(s string) string { return Base32(s) }, 23 | "html": func(s string) string { return HTMLEsacpe(s) }, 24 | "htmle": func(s string) string { return HTMLEquivalent(s) }, 25 | "hex": func(s string) string { return Hex(s) }, 26 | "json": func(s string) string { sJson, _ := json.Marshal(s); return string(sJson) }, 27 | "binary": func(s string) string { 28 | var b string 29 | for _, r := range s { 30 | b = fmt.Sprintf("%s%.8b", b, r) 31 | } 32 | return b 33 | }, 34 | } 35 | ) 36 | 37 | func Encode(payload string, encodes []string) string { 38 | for _, encode := range encodes { 39 | if _, ok := encodeTo[strings.ToLower(encode)]; ok { 40 | payload = encodeTo[strings.ToLower(encode)](payload) 41 | } 42 | } 43 | return payload 44 | } 45 | 46 | func Url(s string) string { 47 | return ("%" + hex.EncodeToString([]byte(s))) 48 | } 49 | 50 | func DoubleUrl(s string) string { 51 | return strings.ReplaceAll(Url(s), "%", "%25") 52 | } 53 | 54 | // Alias of : url.QueryEscape() 55 | func UrlEsacpe(s string) string { 56 | return url.QueryEscape(s) 57 | } 58 | 59 | func UrlDoubleEscape(s string) string { 60 | return strings.ReplaceAll(url.QueryEscape(s), "%", "%25") 61 | } 62 | 63 | func HTMLEsacpe(s string) string { 64 | return html.EscapeString(s) 65 | } 66 | 67 | func HTMLEquivalent(s string) string { 68 | return strings.ReplaceAll(html.EscapeString(s), """, """) 69 | } 70 | 71 | func Base32(s string) string { 72 | return base32.StdEncoding.EncodeToString([]byte(s)) 73 | } 74 | 75 | func Base64(s string) string { 76 | return base64.StdEncoding.EncodeToString([]byte(s)) 77 | } 78 | 79 | func Hex(s string) string { 80 | return hex.EncodeToString([]byte(s)) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/extract/extract.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "bufio" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "regexp" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | var ( 14 | WILDCARD = "WILDCARD" 15 | ) 16 | 17 | type Extract struct { 18 | Properties 19 | jobAmount int 20 | sources map[string]string //(Body|Headers) 21 | fn_check map[string]func(item, stringToTest string) int //Note : (contains the pattern/regex "check" function) 22 | 23 | //Known Result //<- Known Patterns/Regex that have been discovered 24 | } 25 | 26 | type Properties struct { 27 | Threads int 28 | PrefixPatterns []string 29 | WordlistPattern map[string][]string //Inc: key="A shared prefix that all words in the list have has", value="wordlist itself" 30 | WordlistRegex map[string][]string // -/- 31 | } 32 | 33 | type Result struct { 34 | OK bool 35 | TotalHits int 36 | PatternBody map[string]int 37 | PatternHeaders map[string]int 38 | RegexBody map[string]int 39 | RegexHeaders map[string]int 40 | } 41 | 42 | // !Note : (MUST be the same name as the "Result") 43 | type ResultCombine struct { 44 | PatternBody map[string][]int `json:"PatternBody"` 45 | PatternHeaders map[string][]int `json:"PatternHeaders"` 46 | RegexBody map[string][]int `json:"RegexBody"` 47 | RegexHeaders map[string][]int `json:"RegexHeaders"` 48 | } 49 | 50 | type process struct { 51 | done bool 52 | ok bool 53 | hits int 54 | typ string 55 | method string 56 | foundItem string 57 | } 58 | 59 | type job struct { 60 | prefix string 61 | method string 62 | wordlist []string 63 | } 64 | 65 | func NewExtract(p Properties) Extract { 66 | return Extract{ 67 | fn_check: map[string]func(item, stringToTest string) int{ 68 | "pattern": checkPattern, 69 | "regex": checkRegex, 70 | }, 71 | jobAmount: (len(p.WordlistPattern) + len(p.WordlistRegex)) * 2, // Note : ("2" : Is used because we have two sources to check (body/headers) ) 72 | Properties: p, 73 | } 74 | } 75 | 76 | func NewCombine() ResultCombine { 77 | return ResultCombine{ 78 | PatternBody: make(map[string][]int), 79 | PatternHeaders: make(map[string][]int), 80 | RegexBody: make(map[string][]int), 81 | RegexHeaders: make(map[string][]int), 82 | } 83 | } 84 | 85 | func (e *Extract) AddJob(body, headers string) { 86 | //e.Known = known 87 | e.sources = map[string]string{ 88 | "body": body, 89 | "headers": headers, 90 | } 91 | } 92 | 93 | // Extract all patterns that was found within the response body and/or the response headers from the current target response. 94 | // Want: "Threads to use for pattern extracation", sources: "array of 2 {body, headers}", givenData: "Extract structure". 95 | // Return: status, amountTotal, amountType, headersPatterns, bodyPatterns. 96 | func (e Extract) Run() Result { 97 | result := Result{ 98 | TotalHits: 0, 99 | PatternBody: make(map[string]int), 100 | PatternHeaders: make(map[string]int), 101 | RegexBody: make(map[string]int), 102 | RegexHeaders: make(map[string]int), 103 | } 104 | 105 | //Check if the sources empty, if so, then end: 106 | if len(e.sources["body"]) == 0 || len(e.sources["headers"]) == 0 { 107 | return result 108 | } 109 | 110 | var ( 111 | wg sync.WaitGroup 112 | processChannel = make(chan process) 113 | JobQueue = make(chan job) 114 | ) 115 | 116 | for i := 0; i < e.Threads; i++ { 117 | go e.analyze(&wg, JobQueue, processChannel) 118 | } 119 | 120 | go func(wg *sync.WaitGroup) { 121 | var mutex sync.Mutex 122 | for { 123 | r := <-processChannel 124 | 125 | if r.ok { 126 | result.TotalHits += r.hits 127 | if r.typ == "body" { 128 | //Check method for body: 129 | if r.method == "pattern" { 130 | mutex.Lock() 131 | result.PatternBody[r.foundItem] = r.hits 132 | mutex.Unlock() 133 | 134 | } else if r.method == "regex" { 135 | mutex.Lock() 136 | result.RegexBody[r.foundItem] = r.hits 137 | mutex.Unlock() 138 | } 139 | } else if r.typ == "headers" { 140 | //Check method for headers: 141 | if r.method == "pattern" { 142 | mutex.Lock() 143 | result.PatternHeaders[r.foundItem] = r.hits 144 | mutex.Unlock() 145 | 146 | } else if r.method == "regex" { 147 | mutex.Lock() 148 | result.RegexHeaders[r.foundItem] = r.hits 149 | mutex.Unlock() 150 | } 151 | } 152 | 153 | } else if r.done { 154 | wg.Done() 155 | } 156 | } 157 | }(&wg) 158 | 159 | e.appendJobs(&wg, JobQueue) 160 | 161 | //Wait for all processes to end: 162 | wg.Wait() 163 | 164 | //Provide success status 165 | if result.TotalHits > 0 { 166 | result.OK = true 167 | } 168 | 169 | return result 170 | } 171 | 172 | // Give job to the extract process 173 | func (e *Extract) appendJobs(wg *sync.WaitGroup, j chan<- job) { 174 | m := map[string]map[string][]string{ 175 | "pattern": e.WordlistPattern, 176 | "regex": e.WordlistRegex, 177 | } 178 | for _, mt := range []string{"pattern", "regex"} { 179 | for pfx, wl := range m[mt] { //Note : (Just extracting the map - key/value) 180 | wg.Add(1) 181 | j <- job{ 182 | prefix: pfx, 183 | method: mt, 184 | wordlist: wl, 185 | } 186 | } 187 | } 188 | } 189 | 190 | // Take the current extracted map result and compare it with a known map result. 191 | // Return the "current" map and all the unique items with their unique values 192 | func GetUnique(current map[string]int, known map[string][]int, payload string) (map[string]int, int) { 193 | hit := 0 194 | for item, ValueCurrent := range current { 195 | //If the key exists and they share the same value, delete the key from the "current" map: 196 | uniuqe := true 197 | if lstValue, ok := known[item]; ok { 198 | if len(lstValue) == 1 && ValueCurrent == lstValue[0] { 199 | uniuqe = false 200 | 201 | } else { 202 | for _, value := range lstValue { 203 | if ValueCurrent == value { 204 | uniuqe = false 205 | break 206 | } 207 | } 208 | } 209 | } 210 | if uniuqe && !strings.Contains(payload, item) { 211 | hit += ValueCurrent 212 | } 213 | delete(current, item) 214 | } 215 | return current, hit 216 | } 217 | 218 | // Take a list that contains an array of two maps. The first map in the array is the *current* map and the secound map is a map that contains known items. 219 | // Compare the two maps in the array for all arrays in the list and delete all known items that was detected inside the *current* map. 220 | // Return a list of maps in the same order as given that only contains the unique items of the *current map*. 221 | // !Note : (The order for input is important since it effects the return order of the final list of maps) 222 | func GetMultiUnique(current []map[string]int, known []map[string][]int, payload string) ([]map[string]int, int) { 223 | if len(current) != len(known) { 224 | log.Fatal("length was different from \"current\" and \"known\" map list given in extract.") 225 | } 226 | 227 | //Extract the full list and take the two maps in each list to compare the differences within them: 228 | storageDiff := []map[string]int{} //<-List to store the differences (same order as it was given in) 229 | totalHits := 0 230 | 231 | for i := 0; i < len(current); i++ { 232 | uniqueItems, hit := GetUnique(current[i], known[i], payload) 233 | 234 | storageDiff = append(storageDiff, uniqueItems) 235 | totalHits += hit 236 | } 237 | 238 | return storageDiff, totalHits 239 | } 240 | 241 | // Check for pattern inside the response body & headers: 242 | // This function uses a prefix technique that takes advantage of patterns that share the same prefix to provide a faster and more lightweight analyze. 243 | func (e *Extract) analyze(wg *sync.WaitGroup, jobs <-chan job, result chan<- process) { 244 | for j := range jobs { 245 | for _, t := range []string{"body", "headers"} { 246 | stringToTest := e.sources[t] 247 | if strings.Contains(stringToTest, j.prefix) { 248 | //A new wordlist is in need of being analyzed. Add the wordlist length to the listener: 249 | for _, item := range j.wordlist { 250 | 251 | //Calculate how many times the item was within the content source. 252 | //Then check if the item is a common (known behavior), if not, then send it to the listener: 253 | if hits := e.fn_check[j.method](item, stringToTest); hits > 0 { 254 | result <- process{ 255 | ok: true, 256 | typ: t, 257 | hits: hits, 258 | foundItem: item, 259 | method: j.method, 260 | } 261 | } 262 | } 263 | } 264 | } 265 | result <- process{done: true} 266 | } 267 | } 268 | 269 | // Regex, string - check regex: 270 | func checkRegex(re, s string) int { 271 | if match, _ := regexp.MatchString(re, s); match { 272 | return 1 273 | } 274 | return 0 275 | } 276 | 277 | // Pattern, string - check amount of pattern in string: 278 | func checkPattern(ptn, s string) int { 279 | if s == "" { 280 | return 0 281 | } 282 | return strings.Count(s, ptn) 283 | } 284 | 285 | // Create a map from words within a list. 286 | // Return a list of all the shared prefix and a map. The key of the map is a prefix of all the words associated within the words added to the map list. 287 | // Note : (The list that only contain prefix is used for better preformance within loops.) 288 | func CreatePrefixMap(lst []string) ([]string, map[string][]string) { 289 | var ( 290 | m = make(map[string][]string) 291 | lst_pfx []string 292 | wildcard bool 293 | ) 294 | for _, i := range lst { 295 | k := i[:3] 296 | m[k] = append(m[k], i) 297 | } 298 | for k, l := range m { 299 | if len(l) == 1 { 300 | wildcard = true 301 | //Add all alone items into a wildcard key ("WILDCARD"): 302 | //Note : This words only if the max prefix for the other items are set to 3 in length. 303 | m[WILDCARD] = append(m[WILDCARD], k) 304 | 305 | //Delete the old list with only one item: 306 | delete(m, k) 307 | } else { 308 | lst_pfx = append(lst_pfx, k) 309 | } 310 | } 311 | //If the map contained wildcard prefix, then add it at the end: 312 | if wildcard { 313 | lst_pfx = append(lst_pfx, WILDCARD) 314 | } 315 | 316 | return lst_pfx, m 317 | } 318 | 319 | // Take a folder that have files (wordlists) with a prefix of: "ptn_" (pattern) OR "_re" (regex). 320 | // Return two wordlist : (Patterns|Regex) 321 | func MakeWordlists(folder string) ([]string, []string) { 322 | var lst_files []string 323 | //Read the files from the directory, If there is atleast one *file* found start adding the names to a list: 324 | filesFolders, _ := ioutil.ReadDir(folder) 325 | for _, f := range filesFolders { 326 | if !f.IsDir() { 327 | lst_files = append(lst_files, f.Name()) 328 | } 329 | } 330 | 331 | wordlists := make(map[string][]string) 332 | for _, f := range lst_files { 333 | fpath := (folder + f) 334 | typ := "" 335 | if strings.HasPrefix(f, "ptn_") { 336 | typ = "ptn" 337 | } else if strings.HasPrefix(f, "re_") { 338 | typ = "re" 339 | } else { //Simply ignore the other files that miss the prefix 340 | continue 341 | } 342 | //Read file and append each item to the map "wordlists": 343 | fcontent, _ := os.Open(fpath) 344 | scanner := bufio.NewScanner(fcontent) 345 | for scanner.Scan() { 346 | item := scanner.Text() 347 | if len(item) > 0 { 348 | wordlists[typ] = append(wordlists[typ], item) 349 | } 350 | } 351 | fcontent.Close() 352 | } 353 | 354 | return wordlists["ptn"], wordlists["re"] 355 | } 356 | -------------------------------------------------------------------------------- /pkg/files/files.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | ) 10 | 11 | // Check if the file(s) or folder(s) already exists. 12 | // Return true if it exist and false if it do not. 13 | func ExistAny(f ...string) bool { 14 | l := []string{} 15 | 16 | //Extract all the files/folders: 17 | for _, i := range f { 18 | if _, err := os.Stat(i); !os.IsNotExist(err) { 19 | l = append(l, i) 20 | } 21 | } 22 | return len(l) == len(f) 23 | } 24 | 25 | func FileExist(filename string) bool { 26 | //File dose not exist: 27 | if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) { 28 | return false 29 | } 30 | //File exists: 31 | return true 32 | } 33 | 34 | // Read all content in a directory. Return a list of: Files | Folders 35 | func InDir(folder string) ([]string, []string) { 36 | var ( 37 | l_files []string 38 | l_folders []string 39 | ) 40 | //Read the files from the directory: 41 | filesFolders, _ := ioutil.ReadDir(folder) 42 | 43 | //If there is atleast one *file* found start adding the names to a list: 44 | for _, f := range filesFolders { 45 | if !f.IsDir() { 46 | l_files = append(l_files, f.Name()) 47 | 48 | } else if f.IsDir() { 49 | l_folders = append(l_folders, f.Name()) 50 | } 51 | } 52 | return l_files, l_folders 53 | } 54 | 55 | // Create a folder 56 | func CreateFolder(name string) { 57 | if err := os.Mkdir(name, os.ModePerm); err != nil { 58 | log.Fatal(err) 59 | } 60 | } 61 | 62 | // Take a file and convert it's data to a given string list 63 | func FileToList(file string) ([]string, error) { 64 | var lst []string 65 | 66 | //Open the file and check for errors: 67 | f, err := os.Open(file) 68 | if err != nil { 69 | return lst, err 70 | } 71 | scanner := bufio.NewScanner(f) 72 | 73 | //Read the file items and add it to map: 74 | for scanner.Scan() { 75 | if item := scanner.Text(); item != "" { 76 | lst = append(lst, item) 77 | } 78 | } 79 | f.Close() 80 | return lst, nil 81 | } 82 | 83 | // Take a file and convert it's data to a given string map 84 | func FileToMap(file string) (map[string]int, error) { 85 | var m = make(map[string]int) 86 | 87 | //Open the file and check for errors: 88 | f, err := os.Open(file) 89 | if err != nil { 90 | return m, err 91 | } 92 | scanner := bufio.NewScanner(f) 93 | 94 | //Read the file items and add it to map: 95 | for scanner.Scan() { 96 | if item := scanner.Text(); item != "" { 97 | m[item] += 1 98 | } 99 | } 100 | f.Close() 101 | return m, nil 102 | } 103 | 104 | // Check file size 105 | func FileSize(f string) (int, error) { 106 | fInfo, err := os.Stat(f) 107 | if err != nil { 108 | return 0, err 109 | } 110 | return int(fInfo.Size()), nil 111 | } 112 | 113 | // Check if the given value is a valid file or folder 114 | // Return a string of "file" if it's a file and "folder" if it's a folder 115 | func FileOrFolder(f string) (string, error) { 116 | finfo, err := os.Stat(f) 117 | switch { 118 | case err != nil: 119 | return "", err 120 | case finfo.IsDir(): 121 | return "folder", nil 122 | default: 123 | return "file", nil 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pkg/functions/slices.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | // Split a string by comma but ignore escaped comma characters (\,) to be splitted. 4 | // Return a string based list of all the items. 5 | func SplitEscape(s string, sep rune) []string { 6 | var ( 7 | l []string 8 | str string 9 | ) 10 | for idx, r := range s { 11 | if len(s) >= 2 && r == sep { 12 | if s[idx-1] != '\\' { 13 | l = append(l, str) 14 | str = "" 15 | continue 16 | 17 | } else if s[idx-1] == '\\' { 18 | str = str[:len(str)-1] 19 | } 20 | } 21 | str += string(r) 22 | 23 | if idx == len(s)-1 { 24 | l = append(l, str) 25 | } 26 | } 27 | return l 28 | } 29 | -------------------------------------------------------------------------------- /pkg/httpdiff/httpdiff.go: -------------------------------------------------------------------------------- 1 | package httpdiff 2 | 3 | import ( 4 | "slices" 5 | 6 | "github.com/Brum3ns/firefly/pkg/httpprepare" 7 | "github.com/Brum3ns/firefly/pkg/randomness" 8 | ) 9 | 10 | type Difference struct { 11 | *Config 12 | } 13 | 14 | type Config struct { 15 | Payload string 16 | PayloadVerify string 17 | Randomness randomness.Randomness 18 | Filter 19 | Compare 20 | } 21 | 22 | type Compare struct { 23 | HeaderMergeNode httpprepare.Header 24 | HTMLMergeNode httpprepare.HTMLNodeCombine 25 | } 26 | 27 | type Result struct { 28 | OK bool 29 | HeaderResult 30 | HTMLResult 31 | } 32 | 33 | type HeaderResult struct { 34 | OK bool 35 | HeaderHits int 36 | Appear httpprepare.Header 37 | Disappear httpprepare.Header 38 | } 39 | 40 | type HTMLResult struct { 41 | OK bool 42 | Appear HTMLNodeDiff 43 | Disappear HTMLNodeDiff 44 | } 45 | 46 | type HTMLNodeDiff struct { 47 | TagStartHits int 48 | TagEndHits int 49 | TagSelfCloseHits int 50 | WordsHits int 51 | CommentHits int 52 | AttributeHits int 53 | AttributeValueHits int 54 | httpprepare.HTMLNode 55 | } 56 | type Filter struct { 57 | HeaderFilter 58 | //HTMLFilter 59 | } 60 | 61 | type HeaderFilter struct { 62 | Header httpprepare.Header 63 | } 64 | 65 | type diffNode struct { 66 | hit int 67 | checkRandomness bool 68 | data map[string]int 69 | } 70 | 71 | func NewDifference(config Config) *Difference { 72 | return &Difference{ 73 | Config: &config, 74 | } 75 | } 76 | 77 | func newDiffNode() diffNode { 78 | return diffNode{ 79 | data: make(map[string]int), 80 | } 81 | } 82 | 83 | // Run the [diff]erence enumiration process for the HTML node 84 | func (diff *Difference) GetHTMLNodeDiff(htmlNode httpprepare.HTMLNode) HTMLResult { 85 | totalHits := 0 86 | storage := struct { 87 | appearHits int 88 | disappearHits int 89 | appear []diffNode 90 | disappear []diffNode 91 | }{} 92 | 93 | //!Note : Order for "current" and "known" togther with the list length *MUST* be the same: 94 | current := [7]diffNode{ 95 | {data: htmlNode.TagStart}, 96 | {data: htmlNode.TagEnd}, 97 | {data: htmlNode.TagSelfClose}, 98 | {data: htmlNode.Words, checkRandomness: true}, 99 | {data: htmlNode.Comment, checkRandomness: true}, 100 | {data: htmlNode.Attribute}, 101 | {data: htmlNode.AttributeValue, checkRandomness: true}, 102 | } 103 | known := [7]map[string][]int{ 104 | diff.Config.Compare.HTMLMergeNode.TagStart, 105 | diff.Config.Compare.HTMLMergeNode.TagEnd, 106 | diff.Config.Compare.HTMLMergeNode.TagSelfClose, 107 | diff.Config.Compare.HTMLMergeNode.Words, 108 | diff.Config.Compare.HTMLMergeNode.Comment, 109 | diff.Config.Compare.HTMLMergeNode.Attribute, 110 | diff.Config.Compare.HTMLMergeNode.AttributeValue, 111 | } 112 | 113 | for i := 0; i < len(current); i++ { 114 | //Detect difference 115 | diffAppear, diffDisappear := diff.nodeDiff(current[i], known[i], diff.Payload) 116 | 117 | storage.appear = append(storage.appear, diffAppear) 118 | storage.appearHits += diffAppear.hit 119 | 120 | storage.disappear = append(storage.disappear, diffDisappear) 121 | storage.disappearHits += diffDisappear.hit 122 | 123 | totalHits += (diffAppear.hit + diffDisappear.hit) 124 | } 125 | 126 | return HTMLResult{ 127 | // !Note : The order for this *MUST* follow the same as "known" and "current" above 128 | OK: (totalHits > 0), 129 | Appear: HTMLNodeDiff{ 130 | TagStartHits: storage.appear[0].hit, 131 | TagEndHits: storage.appear[1].hit, 132 | TagSelfCloseHits: storage.appear[2].hit, 133 | WordsHits: storage.appear[3].hit, 134 | CommentHits: storage.appear[4].hit, 135 | AttributeHits: storage.appear[5].hit, 136 | AttributeValueHits: storage.appear[6].hit, 137 | HTMLNode: httpprepare.HTMLNode{ 138 | TagStart: storage.appear[0].data, 139 | TagEnd: storage.appear[1].data, 140 | TagSelfClose: storage.appear[2].data, 141 | Words: storage.appear[3].data, 142 | Comment: storage.appear[4].data, 143 | Attribute: storage.appear[5].data, 144 | AttributeValue: storage.appear[6].data, 145 | }, 146 | }, 147 | Disappear: HTMLNodeDiff{ 148 | TagStartHits: storage.disappear[0].hit, 149 | TagEndHits: storage.disappear[1].hit, 150 | TagSelfCloseHits: storage.disappear[2].hit, 151 | WordsHits: storage.disappear[3].hit, 152 | CommentHits: storage.disappear[4].hit, 153 | AttributeHits: storage.disappear[5].hit, 154 | AttributeValueHits: storage.disappear[6].hit, 155 | HTMLNode: httpprepare.HTMLNode{ 156 | TagStart: storage.disappear[0].data, 157 | TagEnd: storage.disappear[1].data, 158 | TagSelfClose: storage.disappear[2].data, 159 | Words: storage.disappear[3].data, 160 | Comment: storage.disappear[4].data, 161 | Attribute: storage.disappear[5].data, 162 | AttributeValue: storage.disappear[6].data, 163 | }, 164 | }, 165 | } 166 | } 167 | 168 | // Take two prepared header structures and compare their differences 169 | func (diff *Difference) GetHeadersDiff(HeaderNode httpprepare.Header) HeaderResult { 170 | var ( 171 | appear = httpprepare.NewHeader() 172 | disappear = httpprepare.NewHeader() 173 | testedItems = make(map[string]struct{}) 174 | totalHits = 0 175 | ) 176 | 177 | for currentHeader, currentHeaderData := range HeaderNode { 178 | // Diff Filter check 179 | if diff.FilterHeader(currentHeader) { 180 | continue 181 | } 182 | 183 | // Check if the information is unique in relation to the shared header names 184 | if knownHeaderData, ok := diff.Compare.HeaderMergeNode[currentHeader]; ok { 185 | 186 | // Add to tested header names 187 | testedItems[currentHeader] = struct{}{} 188 | 189 | // Search for unique values inside the current and known header data values 190 | if (!lstIntShareItem(knownHeaderData.Amount, currentHeaderData.Amount) || 191 | !lstStringShareItem(knownHeaderData.Values, currentHeaderData.Values)) && 192 | !slices.Contains(knownHeaderData.Values, diff.Payload) { 193 | 194 | appear[currentHeader] = currentHeaderData 195 | } 196 | } else { 197 | appear[currentHeader] = currentHeaderData 198 | } 199 | } 200 | 201 | // Add headers that disappear in the current response compare to the original 202 | for knownHeader, knownHeaderData := range diff.Compare.HeaderMergeNode { 203 | // Diff Filter check 204 | if diff.FilterHeader(knownHeader) { 205 | continue 206 | } 207 | 208 | if _, ok := testedItems[knownHeader]; !ok { 209 | // Extract the highest difference from the known values and add it 210 | disappear[knownHeader] = httpprepare.HeaderInfo{ 211 | Amount: []int{highestLstIntValue(knownHeaderData.Amount)}, 212 | Values: knownHeaderData.Values, 213 | } 214 | } 215 | 216 | } 217 | totalHits = len(disappear) + len(appear) 218 | return HeaderResult{ 219 | OK: (totalHits > 0), 220 | HeaderHits: totalHits, 221 | Appear: appear, 222 | Disappear: disappear, 223 | } 224 | } 225 | 226 | func (diff *Difference) nodeDiff(current diffNode, known map[string][]int, payload string) (diffNode, diffNode) { 227 | var ( 228 | appear = newDiffNode() 229 | disappear = newDiffNode() 230 | testedItems = make(map[string]struct{}) 231 | ) 232 | 233 | for currentItem, currentValue := range current.data { 234 | // Set the isDiff as true by default 235 | isDiff := true 236 | amountDiff := 0 237 | 238 | // Check if the current item exists in the known 239 | if knownValues, ok := known[currentItem]; ok { 240 | 241 | // Add the item to the tested map to be used to discover items that disappear in the response body 242 | testedItems[currentItem] = struct{}{} 243 | 244 | // Compare with the known values 245 | for _, knownValue := range knownValues { 246 | if currentValue == knownValue { 247 | isDiff = false 248 | break 249 | 250 | // If the current value isen't in the list check the amount diff and update to the highest amount diff 251 | } else if v := lengthMinMaxDiff(currentValue, knownValue); v > amountDiff { 252 | amountDiff = v 253 | } 254 | } 255 | } else if amountDiff == 0 { 256 | amountDiff = currentValue 257 | } 258 | 259 | if isDiff && currentItem != payload { 260 | // Check randomness (false positive) 261 | if !current.checkRandomness || (current.checkRandomness && !diff.Config.Randomness.IsRandom(currentItem)) { 262 | appear.data[currentItem] = amountDiff 263 | appear.hit += amountDiff 264 | } 265 | } 266 | } 267 | 268 | // Check known item and see if any of them where not included in the current response, then add them as a valid diff 269 | for knownItem, knownValues := range known { 270 | if _, ok := testedItems[knownItem]; !ok { 271 | // Check randomness (false positive) 272 | if !current.checkRandomness || (current.checkRandomness && !diff.Config.Randomness.IsRandom(knownItem)) { 273 | value := highestLstIntValue(knownValues) 274 | disappear.data[knownItem] = value 275 | disappear.hit += value 276 | } 277 | } 278 | } 279 | return appear, disappear 280 | } 281 | 282 | // Filter HTTP header name 283 | func (diff *Difference) FilterHeader(header string /*value string*/) bool { 284 | if len(diff.Filter.HeaderFilter.Header) == 0 { 285 | return false 286 | } 287 | _, ok := diff.Filter.HeaderFilter.Header[header] 288 | 289 | return ok 290 | } 291 | 292 | func highestLstIntValue(lst []int) int { 293 | v := 0 294 | for _, i := range lst { 295 | if i > v { 296 | v = i 297 | } 298 | } 299 | return v 300 | } 301 | 302 | // Return an int array of 2 that holds 0/1 (min/max) and length diff 303 | func lengthMinMaxDiff(x, y int) int { 304 | if x < y { 305 | return (y - x) 306 | } 307 | return x - y 308 | } 309 | 310 | // Compare two int type lists and return the differences presented in lstCurrent 311 | // Return a true if the lists has one item in common 312 | func lstIntShareItem(lstCompare, lstCurrent []int) bool { 313 | // Convert list1 into a map for quick lookups 314 | m := make(map[int]struct{}) 315 | for _, i := range lstCompare { 316 | m[i] = struct{}{} 317 | } 318 | 319 | // Iterate over list2 and check if any item exists in the map 320 | for _, i := range lstCurrent { 321 | // An item is shared between the two lists 322 | if _, ok := m[i]; ok { 323 | return true 324 | } 325 | } 326 | return false 327 | } 328 | 329 | // Compare two string type lists and return the differences presented in lstCurrent 330 | // Return a true if the lists has one item in common 331 | func lstStringShareItem(lstCompare, lstCurrent []string) bool { 332 | // Convert list1 into a map for quick lookups 333 | m := make(map[string]struct{}) 334 | for _, i := range lstCompare { 335 | m[i] = struct{}{} 336 | } 337 | 338 | // Iterate over list2 and check if any item exists in the map 339 | for _, i := range lstCurrent { 340 | // An item is shared between the two lists 341 | if _, ok := m[i]; ok { 342 | return true 343 | } 344 | } 345 | return false 346 | } 347 | -------------------------------------------------------------------------------- /pkg/httpfilter/httpfilter.go: -------------------------------------------------------------------------------- 1 | package httpfilter 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | var ( 14 | DEFAULT_MODE = "or" 15 | OPERATOR_RANGE = "-" 16 | OPERATOR_LESS = "--" 17 | OPERATOR_GREATER = "++" 18 | OPERATOR_EQUAL = "==" 19 | VALID_MODES = map[string]struct{}{ 20 | "and": {}, 21 | "or": {}, 22 | } 23 | ) 24 | 25 | type Filter struct { 26 | // Reperesent the amount of different filter types that has been set 27 | // Note : Not the amount of filter values for each section 28 | amount int 29 | // Contains all the configurations 30 | Config 31 | } 32 | 33 | // Response is lightway and adapted to the filter support. 34 | // This makes the filter process faster when a lot of HTTP responses are being analyzed. 35 | type Response struct { 36 | Body []byte 37 | StatusCode int 38 | ResponseSize int 39 | WordCount int 40 | LineCount int 41 | ResponseTime float64 42 | Headers http.Header 43 | } 44 | 45 | type Config struct { 46 | // Mode represent the mode when the filter is running 47 | // Valid modes: "or", "and", if no mode is set the "or" mode will be used 48 | Mode string 49 | HeaderRegex string 50 | BodyRegex string 51 | StatusCodes []string 52 | WordCounts []string 53 | LineCounts []string 54 | ResponseSizes []string 55 | ResponseTimesMillisec []string 56 | Header http.Header 57 | 58 | // Local configuration for faster lookup and greater preformance 59 | headerRegex *regexp.Regexp 60 | bodyRegex *regexp.Regexp 61 | statusCode map[string][]string 62 | wordCount map[string][]string 63 | lineCount map[string][]string 64 | responseSize map[string][]string 65 | responseTimeMillisec map[string][]string 66 | headers http.Header 67 | } 68 | 69 | func NewFilter(config Config) (Filter, error) { 70 | conf, err := makeConfig(config) 71 | return Filter{ 72 | Config: conf, 73 | amount: getFilterAmount(config), 74 | }, err 75 | } 76 | 77 | // Run the filter 78 | // The "typ" represent the filter type: Filter / Match 79 | func (f Filter) Run(resp Response) bool { 80 | if f.amount == 0 { 81 | return false 82 | } 83 | 84 | count := 0 85 | if f.Config.HeaderRegex != "" && f.HeaderRegex(makeHeaderToBytes(resp.Headers)) { 86 | count++ 87 | } 88 | if f.Config.BodyRegex != "" && f.BodyRegex(resp.Body) { 89 | count++ 90 | } 91 | if len(f.Config.StatusCodes) > 0 && f.StatusCode(resp.StatusCode) { 92 | count++ 93 | } 94 | if len(f.Config.ResponseSizes) > 0 && f.ResponseSize(resp.ResponseSize) { 95 | count++ 96 | } 97 | if len(f.Config.WordCounts) > 0 && f.WordCount(resp.WordCount) { 98 | count++ 99 | } 100 | if len(f.Config.LineCounts) > 0 && f.LineCount(resp.LineCount) { 101 | count++ 102 | } 103 | if len(f.Config.ResponseTimesMillisec) > 0 && f.ResponseTime(resp.ResponseTime) { 104 | count++ 105 | } 106 | if len(f.Config.Header) > 0 && f.Header(resp.Headers) { 107 | count++ 108 | } 109 | 110 | // Check the result in relation to the filter mode 111 | if f.Mode == "or" && count > 0 { 112 | return true 113 | 114 | } else if f.Mode == "and" && count == f.amount { 115 | return true 116 | } 117 | return false 118 | } 119 | 120 | func (f Filter) IsSet() bool { 121 | return f.amount > 0 122 | } 123 | 124 | // Set the filter mode 125 | func (f Filter) SetMode(mode string) { 126 | f.Mode = strings.ToLower(mode) 127 | } 128 | 129 | func (f Filter) doFilter(valueCompare float64, m map[string][]string) bool { 130 | mapLength := len(m) 131 | 132 | if mapLength == 0 { 133 | return false 134 | } 135 | 136 | countModeAnd := 0 137 | countValues := 0 138 | for operator, values := range m { 139 | countValues += len(values) 140 | for _, valueStr := range values { 141 | ok := filter(valueCompare, operator, valueStr) 142 | 143 | // If the mode is true and only a single filter is equal to true, return true on the whole filter process 144 | if f.Mode == "or" && ok { 145 | return true 146 | 147 | } else if f.Mode == "and" { 148 | if !ok { 149 | return false 150 | } 151 | countModeAnd++ 152 | } 153 | } 154 | } 155 | 156 | if f.Mode == "and" && countModeAnd == countValues { 157 | return true 158 | } 159 | return false 160 | } 161 | 162 | // Child function of: Filter.doFilter() 163 | func filter(valueCompare float64, operator string, value string) bool { 164 | // Make sure range operator is used first since it require the string value 165 | // To be splitted and converted to int values 166 | if operator == OPERATOR_RANGE { 167 | valueArry, _ := getIntRange(value) 168 | if valueCompare >= valueArry[0] && valueCompare <= valueArry[1] { 169 | return true 170 | } 171 | } else { 172 | // The value must be converted to an int and must work (makeConfig has responsibility for this) 173 | valueFloat64 := mustToFloat64(value) 174 | 175 | if (operator == OPERATOR_EQUAL) && (valueCompare == valueFloat64) { 176 | return true 177 | 178 | } else if (operator == OPERATOR_GREATER) && (valueCompare > valueFloat64) { 179 | return true 180 | 181 | } else if (operator == OPERATOR_LESS) && (valueCompare < valueFloat64) { 182 | return true 183 | } 184 | } 185 | return false 186 | } 187 | 188 | func (f Filter) Header(headers http.Header) bool { 189 | for headerName, _ := range f.headers { 190 | if _, ok := headers[headerName]; ok { 191 | return true 192 | } 193 | } 194 | return false 195 | } 196 | 197 | func (f Filter) HeaderRegex(headers []byte) bool { 198 | return len(headers) > 0 && f.headerRegex.Match(headers) 199 | } 200 | 201 | func (f Filter) BodyRegex(body []byte) bool { 202 | return len(body) > 0 && f.bodyRegex.Match(body) 203 | } 204 | 205 | func (f Filter) ResponseTime(time float64) bool { 206 | return f.doFilter(time, f.responseTimeMillisec) 207 | } 208 | 209 | func (f Filter) StatusCode(statuscode int) bool { 210 | return f.doFilter(float64(statuscode), f.statusCode) 211 | } 212 | 213 | func (f Filter) ResponseSize(size int) bool { 214 | return f.doFilter(float64(size), f.responseSize) 215 | } 216 | 217 | func (f Filter) WordCount(count int) bool { 218 | return f.doFilter(float64(count), f.wordCount) 219 | } 220 | 221 | func (f Filter) LineCount(count int) bool { 222 | return f.doFilter(float64(count), f.lineCount) 223 | } 224 | 225 | // Set the regex for the header 226 | func (config *Config) SetBodyRegex(regexStr string) error { 227 | var err error 228 | if regexStr != "" { 229 | config.bodyRegex, err = regexp.Compile(regexStr) 230 | if err != nil { 231 | return err 232 | } 233 | } 234 | return nil 235 | } 236 | 237 | // Set the regex for the body 238 | func (config *Config) SetHeaderRegex(regexStr string) error { 239 | var err error 240 | if regexStr != "" { 241 | config.headerRegex, err = regexp.Compile(regexStr) 242 | if err != nil { 243 | return err 244 | } 245 | } 246 | return nil 247 | } 248 | 249 | // Validate the configuration of the Filter structure 250 | func makeConfig(config Config) (Config, error) { 251 | var err error 252 | 253 | config.Mode = strings.ToLower(strings.TrimSpace(config.Mode)) 254 | 255 | // Validate the filter mode if it's set to an empty string, set it to its default value 256 | if config.Mode == "" { 257 | config.Mode = DEFAULT_MODE 258 | 259 | } else if _, ok := VALID_MODES[config.Mode]; !ok { 260 | return config, errors.New("invalid filter mode. Valid modes are 'or', 'and'") 261 | } 262 | // Validate the regex for the body 263 | if err = config.SetBodyRegex(config.BodyRegex); err != nil { 264 | return config, err 265 | } 266 | // Validate the regex for the header 267 | if err = config.SetHeaderRegex(config.HeaderRegex); err != nil { 268 | return config, err 269 | } 270 | 271 | // Make operator maps 272 | if config.statusCode, err = makeOperatorMap(config.StatusCodes); err != nil { 273 | return config, err 274 | } 275 | if config.lineCount, err = makeOperatorMap(config.LineCounts); err != nil { 276 | return config, err 277 | } 278 | if config.wordCount, err = makeOperatorMap(config.WordCounts); err != nil { 279 | return config, err 280 | } 281 | if config.responseSize, err = makeOperatorMap(config.ResponseSizes); err != nil { 282 | return config, err 283 | } 284 | if config.responseTimeMillisec, err = makeOperatorMap(config.ResponseTimesMillisec); err != nil { 285 | return config, err 286 | } 287 | // Data is copied only to keep the organization correct for future called methods (It will keep the same data as the original) 288 | config.headers = config.Header 289 | 290 | return config, nil 291 | } 292 | 293 | // Calculate the amount of filter categories that has been set 294 | func getFilterAmount(config Config) int { 295 | var count = 0 296 | if len(config.HeaderRegex) > 0 { 297 | count++ 298 | } 299 | if len(config.BodyRegex) > 0 { 300 | count++ 301 | } 302 | if len(config.StatusCodes) > 0 { 303 | count++ 304 | } 305 | if len(config.WordCounts) > 0 { 306 | count++ 307 | } 308 | if len(config.LineCounts) > 0 { 309 | count++ 310 | } 311 | if len(config.ResponseSizes) > 0 { 312 | count++ 313 | } 314 | if len(config.ResponseTimesMillisec) > 0 { 315 | count++ 316 | } 317 | if len(config.Header) > 0 { 318 | count++ 319 | } 320 | return count 321 | } 322 | 323 | func makeHeaderToBytes(headers http.Header) []byte { 324 | var headerString strings.Builder 325 | for key, values := range headers { 326 | for _, value := range values { 327 | headerString.WriteString(fmt.Sprintf("%s: %s\n", key, value)) 328 | } 329 | } 330 | return []byte(headerString.String()) 331 | } 332 | 333 | // Take a list of int and convert it into a lookup map. 334 | // In case there are any space characters as prefix and/or suffix, it will be trimmed with 'strings.TrimSpace'. 335 | func makeOperatorMap(lst []string) (map[string][]string, error) { 336 | var m = make(map[string][]string) 337 | 338 | for _, i := range lst { 339 | i = strings.TrimSpace(i) 340 | value, operator, err := getOperator(i) 341 | if err != nil { 342 | return m, err 343 | } 344 | 345 | switch operator { 346 | case OPERATOR_EQUAL: 347 | operator = OPERATOR_EQUAL 348 | case OPERATOR_RANGE: 349 | _, err := getIntRange(value) 350 | if err != nil { 351 | return m, err 352 | } 353 | } 354 | m[operator] = append(m[operator], value) 355 | } 356 | return m, nil 357 | } 358 | 359 | // Return the operator that was set in the string. 360 | // If an empty value is returned, The operator is invalid or none are set 361 | // !WARNING! : If the operator is the range operator (global variable: OPERATOR_RANGE) the argument string value is returned instead. 362 | func getOperator(s string) (string, string, error) { 363 | // Verify the given value 364 | ok_range, _ := regexp.MatchString(`^(-|)(\d+\.|)\d+-(-|)(\d+\.|)\d+$`, s) 365 | 366 | ok_digit, _ := regexp.MatchString(`^(\+\+|)(-{0,3}|)(\d+\.|)\d+$`, s) 367 | 368 | if ok_range && ok_digit || !ok_range && !ok_digit { 369 | return "", "", fmt.Errorf("httpfilter - invalid operator format given: %s", s) 370 | } 371 | 372 | if v, ok := strings.CutPrefix(s, OPERATOR_GREATER); ok { 373 | return v, OPERATOR_GREATER, nil 374 | 375 | } else if v, ok := strings.CutPrefix(s, OPERATOR_LESS); ok { 376 | return v, OPERATOR_LESS, nil 377 | 378 | } else if ok_range { 379 | return s, OPERATOR_RANGE, nil 380 | 381 | } else { 382 | return s, OPERATOR_EQUAL, nil 383 | } 384 | } 385 | 386 | // Get the range from a string value and return the two values within the range as an int array 387 | // If the range of the string value is invalid an error will be triggered 388 | func getIntRange(value string) ([2]float64, error) { 389 | var ( 390 | errMsg = "invalid range value when trying to get range between two values" 391 | arry [2]float64 392 | ) 393 | 394 | lastHyphenIndex := strings.LastIndex(value, "-") 395 | if lastHyphenIndex == -1 { 396 | return arry, errors.New("invalid range format, no hyphen (-) found") 397 | } 398 | 399 | // Handle edge cases like "-100--200", "-100-200", "100--200", "100-200" 400 | firstPart := value[:lastHyphenIndex] 401 | secondPart := value[lastHyphenIndex:] 402 | 403 | // Check if the values are negative then a modification is needed 404 | // Note : Special case when the second part starts with a double hyphen 405 | if (len(firstPart) > 1 && len(secondPart) > 1) && (strings.HasSuffix(firstPart, "-") && strings.HasPrefix(secondPart, "-")) { 406 | firstPart, _ = strings.CutSuffix(firstPart, "-") 407 | } else { 408 | secondPart, _ = strings.CutPrefix(secondPart, "-") 409 | } 410 | 411 | v1, err := strconv.ParseFloat(firstPart, 64) 412 | if err != nil { 413 | return arry, fmt.Errorf("%s - %s", errMsg, err) 414 | } 415 | v2, err := strconv.ParseFloat(secondPart, 64) 416 | if err != nil { 417 | return arry, fmt.Errorf("%s - %s", errMsg, err) 418 | } 419 | return [2]float64{v1, v2}, nil 420 | } 421 | 422 | // Take value of type string and convert it into a int type 423 | func mustToFloat64(s string) float64 { 424 | v, err := strconv.ParseFloat(s, 64) 425 | if err != nil { 426 | log.Panic("Invalid filter, can't be converted to float64:", s, err) 427 | } 428 | return v 429 | } 430 | -------------------------------------------------------------------------------- /pkg/httpprepare/header.go: -------------------------------------------------------------------------------- 1 | package httpprepare 2 | 3 | import ( 4 | "net/http" 5 | "slices" 6 | ) 7 | 8 | type Header map[string]HeaderInfo 9 | 10 | type HeaderInfo struct { 11 | Amount []int 12 | Values []string 13 | } 14 | 15 | func NewHeader() Header { 16 | return make(Header) 17 | } 18 | 19 | // Take HTTP headers and merge it within the prepared header 20 | func (header Header) Merge(httpheader http.Header) Header { 21 | for h, values := range httpheader { 22 | 23 | // Note : The amount of values in the list "http.Header" 24 | // represent the amount of times the header was repeated in the HTTP response. 25 | amount := len(values) 26 | 27 | // Check existing header if we have the header 28 | if hasHeader, ok := header[h]; !ok { 29 | header[h] = HeaderInfo{ 30 | Amount: []int{amount}, 31 | Values: values, 32 | } 33 | } else { 34 | // Add unique amount of header that repeated in the HTTP response 35 | if !slices.Contains(hasHeader.Amount, amount) { 36 | hasHeader.Amount = append(hasHeader.Amount, amount) 37 | } 38 | 39 | // Add unique values from the HTTP header 40 | // The list is usually very short and major of case only contain one item 41 | for _, value := range values { 42 | // The item is unique, then add it 43 | if !slices.Contains(hasHeader.Values, value) { 44 | hasHeader.Values = append(hasHeader.Values, value) 45 | } 46 | } 47 | 48 | // Update the values in the current header from the new HTTP header 49 | header[h] = hasHeader 50 | } 51 | } 52 | return header 53 | } 54 | 55 | // Take a http.header and return a prepared header node 56 | func GetHeaderNode(httpheader http.Header) Header { 57 | header := NewHeader() 58 | // Note : We do not need to check for duplicates since http.Header is a map in it's core. 59 | // We also do return a fresh new Header type. 60 | for h, values := range httpheader { 61 | 62 | // Note : The amount of values in the list "http.Header" 63 | // represent the amount of times the header was repeated in the HTTP response. 64 | amount := len(values) 65 | 66 | header[h] = HeaderInfo{ 67 | Amount: []int{amount}, 68 | Values: values, 69 | } 70 | } 71 | return header 72 | } 73 | -------------------------------------------------------------------------------- /pkg/httpprepare/htmlnode.go: -------------------------------------------------------------------------------- 1 | package httpprepare 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | 7 | "golang.org/x/net/html" 8 | ) 9 | 10 | type HTMLNode struct { 11 | TagStart map[string]int 12 | TagEnd map[string]int 13 | TagSelfClose map[string]int 14 | Words map[string]int 15 | Comment map[string]int 16 | Attribute map[string]int 17 | AttributeValue map[string]int 18 | //Text map[string]int 19 | } 20 | 21 | // !Note : (MUST be the same name as the "HTMLNode") 22 | type HTMLNodeCombine struct { 23 | TagStart map[string][]int `json:"TagStart"` 24 | TagEnd map[string][]int `json:"TagEnd"` 25 | TagSelfClose map[string][]int `json:"TagSelfClose"` 26 | Words map[string][]int `json:"Words"` 27 | Comment map[string][]int `json:"Comment"` 28 | Attribute map[string][]int `json:"Attribute"` 29 | AttributeValue map[string][]int `json:"AttributeValue"` 30 | //Text map[string][]int `json:"Text"` 31 | } 32 | 33 | func NewHTMLNode() HTMLNode { 34 | return HTMLNode{ 35 | TagStart: make(map[string]int), 36 | TagEnd: make(map[string]int), 37 | TagSelfClose: make(map[string]int), 38 | Comment: make(map[string]int), 39 | Attribute: make(map[string]int), 40 | AttributeValue: make(map[string]int), 41 | Words: make(map[string]int), 42 | //Text: make(map[string]int), 43 | } 44 | } 45 | 46 | func NewCombineHTMLNode() HTMLNodeCombine { 47 | return HTMLNodeCombine{ 48 | TagStart: make(map[string][]int), 49 | TagEnd: make(map[string][]int), 50 | TagSelfClose: make(map[string][]int), 51 | Words: make(map[string][]int), 52 | Comment: make(map[string][]int), 53 | Attribute: make(map[string][]int), 54 | AttributeValue: make(map[string][]int), 55 | //Text: make(map[string][]int), 56 | } 57 | } 58 | 59 | // Collect HTML elements 60 | // Note : (This function is used within the difference technique process) 61 | func GetHTMLNode(body string) HTMLNode { 62 | htmlNode := NewHTMLNode() 63 | 64 | // Create a tokenizer and parse the HTML content 65 | tokenizer := html.NewTokenizer(strings.NewReader(body)) 66 | 67 | for { 68 | typ := tokenizer.Next() 69 | 70 | // Reached the end of the response body: 71 | if typ == html.ErrorToken { 72 | if err := tokenizer.Err(); err == io.EOF { 73 | break 74 | } 75 | } 76 | 77 | //Check what type of token and sort it into the HTML [struct]ure: 78 | t := tokenizer.Token() 79 | switch typ { 80 | case html.StartTagToken: 81 | htmlNode.TagStart[t.Data]++ 82 | for _, attr := range t.Attr { 83 | htmlNode.Attribute[attr.Key]++ 84 | htmlNode.AttributeValue[attr.Val]++ 85 | } 86 | 87 | case html.EndTagToken: 88 | htmlNode.TagEnd[t.Data]++ 89 | for _, attr := range t.Attr { 90 | htmlNode.Attribute[attr.Key]++ 91 | htmlNode.AttributeValue[attr.Val]++ 92 | } 93 | 94 | case html.TextToken: 95 | for _, w := range strings.Fields(t.Data) { 96 | htmlNode.Words[w]++ 97 | } 98 | //htmlNode.Text[t.Data]++ 99 | 100 | case html.CommentToken: 101 | for _, w := range strings.Fields(t.Data) { 102 | htmlNode.Words[w]++ 103 | } 104 | htmlNode.Comment[t.Data]++ 105 | 106 | case html.SelfClosingTagToken: 107 | htmlNode.TagSelfClose[t.Data]++ 108 | for _, attr := range t.Attr { 109 | htmlNode.Attribute[attr.Key]++ 110 | htmlNode.AttributeValue[attr.Val]++ 111 | } 112 | } 113 | } 114 | return htmlNode 115 | } 116 | -------------------------------------------------------------------------------- /pkg/httpreflect/httpreflect.go: -------------------------------------------------------------------------------- 1 | package httpreflect 2 | 3 | type Reflect struct { 4 | Needle string 5 | } 6 | 7 | type HTMLReflect struct { 8 | } 9 | 10 | type HeaderReflect struct { 11 | Header string 12 | HeaderValue string 13 | } 14 | 15 | func NewReflect(needle string) Reflect { 16 | return Reflect{} 17 | } 18 | -------------------------------------------------------------------------------- /pkg/insertpoint/insertpoint.go: -------------------------------------------------------------------------------- 1 | package insertpoint 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/Brum3ns/firefly/pkg/random" 8 | ) 9 | 10 | type Insert struct { 11 | Keyword string 12 | Payload string 13 | } 14 | 15 | // Take the "Insert" structure and the "Request" structure 16 | // Return the "Request" structure with the insert points replaced by the current payload. 17 | func NewInsert(keyword, payload string) Insert { 18 | return Insert{ 19 | Keyword: keyword, 20 | Payload: payload, 21 | } 22 | } 23 | 24 | // Insert the payload based on the insert point (default=FUZZ) from user options to a string 25 | func (ist Insert) addKeyword(s string) string { 26 | return strings.ReplaceAll(random.RandomInsert(s), ist.Keyword, ist.Payload) 27 | } 28 | 29 | func (ist Insert) SetHeaders(sliceArry [][2]string) http.Header { 30 | var headers = http.Header{} 31 | for _, h := range sliceArry { 32 | hName := random.RandomInsert(h[0]) 33 | hValue := random.RandomInsert(h[1]) 34 | headers.Add(ist.addKeyword(hName), ist.addKeyword(hValue)) 35 | } 36 | return headers 37 | } 38 | 39 | func (ist Insert) SetURL(s string) string { 40 | return strings.ReplaceAll(random.RandomInsert(s), ist.Keyword, normalizeURLstring(ist.Payload)) 41 | } 42 | 43 | func (ist Insert) SetPostBody(s string) string { 44 | return ist.addKeyword(s) 45 | } 46 | 47 | func (ist Insert) SetMethod(s string) string { 48 | return ist.addKeyword(s) 49 | } 50 | 51 | // Normalize common characters in the URL into URL-encode: 52 | func normalizeURLstring(s string) string { 53 | var ( 54 | l_find = []string{" ", "\t", "\n", "#", "&", "?"} 55 | l_URLEncodeTo = []string{"%20", "%09", "%0a", "%23", "%26", "%3F"} 56 | ) 57 | for i := 0; i < len(l_URLEncodeTo); i++ { 58 | if strings.Contains(s, l_find[i]) { 59 | s = strings.ReplaceAll(s, l_find[i], l_URLEncodeTo[i]) 60 | } 61 | } 62 | return s 63 | } 64 | -------------------------------------------------------------------------------- /pkg/parameter/parameter.go: -------------------------------------------------------------------------------- 1 | package parameter 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | 10 | "golang.org/x/exp/slices" 11 | ) 12 | 13 | // Global parameter variables: 14 | var ( 15 | SUPPORTED_PARAM_POSITIONS = []string{"url", "body", "cookie"} 16 | SUPPORTED_PARAM_METHODS = []string{"replace", "append"} 17 | ) 18 | 19 | type Parameter struct { 20 | // InsertKeyword is a shortcut to access the global insert keyword used for all params in all positions 21 | // Keyword presented for example in: URL, Body, Cookie 22 | InsertKeyword string 23 | AutoQueryURL bool 24 | AutoQueryBody bool 25 | AutoQueryCookie bool 26 | URL query 27 | Body query 28 | Cookie query 29 | } 30 | 31 | type query struct { 32 | // Auto detect and insert InsertKeyword in the RawQuery located in "query" 33 | // Holds the original raw query 34 | RawQueryOriginal string 35 | // Holds the built raw query and adapted raw query based on the given rules 36 | RawQueryInsertPoint string 37 | // Holds all the params and it's settings / properties, including it's values 38 | Params []paramSettings 39 | // Position holds the name of the position the request parameter was placed. 40 | // Example: (url, body, cookie). 41 | Position string 42 | // The rules to be used for the param and it's settings 43 | Rule QueryRules 44 | } 45 | 46 | type QueryRules struct { 47 | // Separators is optional and if no separators are set, the default separators in relation to the method will be used instead. 48 | Separators []rune 49 | // The method that the param will used for the "InsertKeyword" variable value when building adapted raw queries 50 | // Ex: (replace, append) 51 | Method string 52 | // The insert keyword that will used to method to be modified after the users request 53 | InsertKeyword string 54 | } 55 | 56 | // Holds the settings of a parameter 57 | type paramSettings struct { 58 | // Name contains the full name of the parameter (not value). 59 | // In case the parameter name contains a name such as: "query[0]=1" the full name will be "query[0]". 60 | Name string 61 | Separator string 62 | //Holds all the parameter including values as a raw string 63 | Raw string 64 | // The values holds all the values and in case the param is used multiple times then all values are added to the list: 65 | // When a parameter do have an equal sign but there is no value that representate that the param was defined with an empty stirng. 66 | // However when the value is "nil" the param did not have any equal sign and represent the param to only be declared and not defined. 67 | Value any 68 | // Index representate the order the given parameter was place in 69 | Index int 70 | // Type representate the value types that the param holds. 71 | // Param types can be string, int, bool, array, list, object etc... 72 | // Note : (By default if no parameter type is set, the default will be "string") 73 | Type reflect.Kind 74 | // Comment ... 75 | TypeValue reflect.Kind 76 | } 77 | 78 | // Make a param object that contain the param settings and rules for each position. 79 | // The argument "paramRules" holds the supported *position* ("url", "cookie", "body") and the rules for the parameter placed in that position. 80 | func NewParameter(rule map[string]QueryRules, insertKeyword string) (Parameter, error) { 81 | parameter := Parameter{} 82 | 83 | // Verify before returning the parameter [struct]ure: 84 | // !Note : (Rules should already be validated in the [struct]ure "NewParamRules") 85 | for position, rules := range rule { 86 | if r, err := NewRules(insertKeyword, rules.Method, rules.Separators); err == nil { 87 | q := query{ 88 | Params: make([]paramSettings, 0), 89 | Rule: r, 90 | } 91 | switch position { 92 | case "url": 93 | parameter.URL = q 94 | parameter.AutoQueryURL = true 95 | 96 | case "body": 97 | parameter.Body = q 98 | parameter.AutoQueryBody = true 99 | 100 | case "cookie": 101 | parameter.Cookie = q 102 | parameter.AutoQueryCookie = true 103 | 104 | default: 105 | return Parameter{}, errors.New("invalid method used, only support: " + strings.Join(SUPPORTED_PARAM_POSITIONS, ",")) 106 | } 107 | } else { 108 | return parameter, err 109 | } 110 | } 111 | return parameter, nil 112 | } 113 | 114 | func NewRules(insertKeyword, method string, separators []rune) (QueryRules, error) { 115 | rules := QueryRules{ 116 | Method: method, 117 | Separators: separators, 118 | InsertKeyword: insertKeyword, 119 | } 120 | //Verify that the given method is within the supported list of methods: 121 | if !slices.Contains(SUPPORTED_PARAM_METHODS, rules.Method) { 122 | return rules, errors.New("invalid method used, only support: " + strings.Join(SUPPORTED_PARAM_METHODS, ",")) 123 | } 124 | return rules, nil 125 | } 126 | 127 | func (p *Parameter) GetURLParam(param string) []paramSettings { 128 | return getParam(param, p.URL.Params) 129 | } 130 | 131 | func (p *Parameter) GetBodyParam(param string) []paramSettings { 132 | return getParam(param, p.Body.Params) 133 | } 134 | 135 | func (p *Parameter) GetCookieParam(param string) []paramSettings { 136 | return getParam(param, p.Cookie.Params) 137 | } 138 | 139 | // Set the URL parameter from a rawQuery 140 | func (p *Parameter) SetURLparams(rawQuery string) { 141 | p.URL.Position = "url" 142 | p.URL.RawQueryOriginal = rawQuery 143 | p.URL.Params = setParam(p.URL) 144 | p.URL.RawQueryInsertPoint = buildRawQueryInsertPoint(p.URL) 145 | } 146 | 147 | // Set the body parameter from a rawQuery 148 | func (p *Parameter) SetBodyparams(rawQuery string) { 149 | p.Body.Position = "body" 150 | p.Body.RawQueryOriginal = rawQuery 151 | p.Body.Params = setParam(p.Body) 152 | p.Body.RawQueryInsertPoint = buildRawQueryInsertPoint(p.Body) 153 | } 154 | 155 | // Set the cookie parameter from a rawQuery 156 | func (p *Parameter) SetCookieparams(rawQuery string) { 157 | p.Cookie.Position = "cookie" 158 | p.Cookie.RawQueryOriginal = rawQuery 159 | p.Cookie.Params = setParam(p.Cookie) 160 | p.Cookie.RawQueryInsertPoint = buildRawQueryInsertPoint(p.Cookie) 161 | } 162 | 163 | func getParam(param string, params []paramSettings) []paramSettings { 164 | var l []paramSettings 165 | for _, p := range params { 166 | //Add the parameter that matched the given, continue to check in case multiple is included: 167 | if p.Name == param { 168 | l = append(l, p) 169 | } 170 | } 171 | return l 172 | } 173 | 174 | // set param settings from the given rawQuery. 175 | func setParam(q query) []paramSettings { 176 | q.Position = strings.ToLower(q.Position) 177 | params := []paramSettings{} 178 | 179 | if len(q.Rule.Separators) == 0 { 180 | q.Rule.Separators, _ = getSeparators(q.Position) 181 | } 182 | //Loop over all the discovered parameters: 183 | index := 0 184 | 185 | rawQuerySplit(q.Position, q.RawQueryOriginal, q.Rule.Separators) 186 | 187 | for _, lst := range rawQuerySplit(q.Position, q.RawQueryOriginal, q.Rule.Separators) { 188 | var ( 189 | sep = lst[0] 190 | rawParam = lst[1] 191 | name string 192 | value any //Note : (List in case the param is being found multiple times with different values) 193 | ) 194 | //Split the first discovered equal sign (=) and define the parameter name: 195 | l := strings.SplitN(rawParam, "=", 2) 196 | name = l[0] 197 | 198 | //In case the list has the length of two. A value was presented in the parameter, then add it: 199 | //Note: (In case no equal sign (=) is presented within the parameter: v = nil) 200 | if len(l) == 2 { 201 | value = l[1] 202 | } 203 | //Add the parameter to the final map containing all parameters and values: 204 | param := paramSettings{ 205 | Name: name, 206 | Separator: sep, 207 | Value: value, 208 | Raw: rawParam, 209 | Index: index, 210 | } 211 | param.setType() 212 | 213 | params = append(params, param) 214 | index++ 215 | } 216 | return params 217 | } 218 | 219 | // Set the parameter type: 220 | func (p *paramSettings) setType() { 221 | isArray := func(s string) bool { 222 | switch { 223 | case strings.Contains(s, "[") && strings.Contains(s, "]"): 224 | return true 225 | case strings.Contains(s, "%5B") && strings.Contains(s, "%5D"): 226 | return true 227 | default: 228 | return false 229 | } 230 | } 231 | 232 | switch p.Value.(type) { 233 | case string: 234 | v := p.Value.(string) 235 | 236 | if isArray(p.Name) { // List 237 | p.Type = reflect.Array 238 | 239 | } else if ok, err := strconv.ParseBool(v); ok && err == nil { //[Bool]ean 240 | p.Type = reflect.Bool 241 | 242 | } else if _, err = strconv.Atoi(v); err == nil { //[Int]eger 243 | p.Type = reflect.Int 244 | 245 | } else if _, err = strconv.ParseFloat(v, 64); err == nil { //Float (32/64) 246 | p.Type = reflect.Float64 247 | 248 | } else { //String 249 | p.Type = reflect.String 250 | } 251 | case nil: //Param value not defined = invalid / nil 252 | p.Type = reflect.Invalid 253 | } 254 | } 255 | 256 | // Build a raw query adapted to the given insertpoint keyword(s): 257 | // !Note : (The function DO NOT verify the method in the "query" [struct]ure) 258 | func buildRawQueryInsertPoint(q query) string { 259 | var rawQuery string 260 | 261 | // Loop over all params presented in the given query: 262 | for _, p := range q.Params { 263 | rawParam := (p.Separator + p.Name) 264 | 265 | // If the original value in the rawQuery was nil without an equal sign. 266 | // Then add directly and continue: 267 | if p.Value == nil { 268 | rawQuery += rawParam 269 | continue 270 | } 271 | 272 | // Check which method should be used to build the raw query together with the insertpoint keyword: 273 | var v string 274 | if q.Rule.Method == "replace" { 275 | v = q.Rule.InsertKeyword 276 | 277 | } else if q.Rule.Method == "append" { 278 | v = fmt.Sprintf("%v%s", p.Value, q.Rule.InsertKeyword) 279 | } 280 | 281 | rawQuery += (rawParam + "=" + v) 282 | } 283 | return rawQuery 284 | } 285 | 286 | // Get the default param separators based on the given param point (placed at) 287 | func getSeparators(point string) ([]rune, error) { 288 | var defaultSeparators = map[string][]rune{ 289 | "url": {'&'}, 290 | "body": {'&'}, 291 | "cookie": {';'}, 292 | } 293 | if separators, ok := defaultSeparators[point]; ok { 294 | return separators, nil 295 | } else { 296 | return []rune{}, errors.New("invalid param point. The valid are: ") 297 | } 298 | } 299 | 300 | // Modified alias of "strings.FieldsFunc": 301 | func rawQuerySplit(position, rawQuery string, separators []rune) [][2]string { // <----- BODY AND COOKIE DO NOT INCLUDE FIRST SEPARATOR 302 | var arryResult [][2]string 303 | 304 | f := func(r rune) bool { 305 | for _, i := range separators { 306 | if i == r { 307 | return true 308 | } 309 | } 310 | return false 311 | } 312 | 313 | // Add a value separator character at the end to make it possible to include all the raw parameters (name + value) and it's related separator. 314 | // The added separator will not be included and is only added to easily detect the last param properties at the end of the given raw query. 315 | if len(separators) > 0 { 316 | rawQuery = rawQuery + string(separators[0]) 317 | } 318 | 319 | start := -1 320 | for end, r := range rawQuery { 321 | if f(r) { 322 | if start >= 0 { 323 | sep := "" 324 | if start > 0 { 325 | sep = string(rawQuery[start-1]) 326 | } 327 | arryResult = append(arryResult, [2]string{sep, rawQuery[start:end]}) 328 | // Set start to a negative value. 329 | // Note: using -1 here consistently and reproducibly 330 | // slows down this code by a several percent on amd64. 331 | start = ^start 332 | } 333 | } else { 334 | if start < 0 { 335 | start = end 336 | } 337 | } 338 | } 339 | return arryResult 340 | } 341 | -------------------------------------------------------------------------------- /pkg/payloads/cwe.go: -------------------------------------------------------------------------------- 1 | package payloads 2 | 3 | type CWE struct { 4 | } 5 | -------------------------------------------------------------------------------- /pkg/payloads/payload.go: -------------------------------------------------------------------------------- 1 | package payloads 2 | 3 | var DEFAULT_CHARS = []rune{ 4 | '{', 5 | '}', 6 | '[', 7 | ']', 8 | '(', 9 | ')', 10 | '+', 11 | '-', 12 | '*', 13 | '/', 14 | '%', 15 | '=', 16 | '!', 17 | '>', 18 | '<', 19 | '&', 20 | '|', 21 | '^', 22 | '~', 23 | '.', 24 | ',', 25 | ':', 26 | ';', 27 | '"', 28 | '\'', 29 | '#', 30 | '@', 31 | '?', 32 | '_', 33 | '$', 34 | '\\', 35 | } 36 | 37 | type Mutation struct { 38 | Chars map[rune]payloadInfo 39 | Seed string 40 | cwe CWE 41 | feedback 42 | } 43 | 44 | type payloadInfo struct { 45 | level int 46 | // Relation contains the char that the current char have a relation with 47 | // and how high the relation is given by a int value. 48 | relation map[rune]int 49 | } 50 | 51 | type feedback struct { 52 | payloadsSuccess map[string]int 53 | payloadFail map[string]int 54 | cache []string 55 | } 56 | 57 | func NewMutation() Mutation { 58 | return Mutation{} 59 | } 60 | 61 | func (m Mutation) Run() feedback { 62 | return feedback{} 63 | } 64 | 65 | // Set custom chars to use for the mutation process. 66 | // True/False to also include the default characters (no duplicates) 67 | func (m Mutation) makeChars(chars []rune, includeDefault bool) { 68 | 69 | } 70 | 71 | // Set the seed, this will be the root payload for the mutation 72 | func (m Mutation) SetSeed(seed string) { 73 | m.Seed = seed 74 | } 75 | 76 | func (f Mutation) SetCWEFocus() { 77 | 78 | } 79 | 80 | // Set the chars to be used within the mutation process and the one to focus on (if any) 81 | func (m Mutation) SetChars(chars []rune, charsFocus ...rune) { 82 | 83 | } 84 | 85 | // Set a char relation (Ex: char '{' has a relation to char '}', '$' or ';') 86 | func (m Mutation) SetCharRelation(char rune, charRelation ...rune) { 87 | if charRelation == nil { 88 | return 89 | } 90 | // Code... 91 | } 92 | 93 | func (m Mutation) SetCharsIgnore(chars map[rune]int) { 94 | 95 | } 96 | 97 | func (f Mutation) AppendFeedback() { 98 | 99 | } 100 | 101 | func (f Mutation) GetPayload() { 102 | 103 | } 104 | -------------------------------------------------------------------------------- /pkg/payloads/relation.go: -------------------------------------------------------------------------------- 1 | package payloads 2 | 3 | type Relation struct { 4 | Payload map[string][]string 5 | Character map[rune][]string 6 | } 7 | 8 | func NewRelation() Relation { 9 | return Relation{} 10 | } 11 | 12 | // Find a related chars within the given string when compared to chars from other strings in the memory 13 | func (r *Relation) findChar(s string) { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /pkg/payloads/wordlist.go: -------------------------------------------------------------------------------- 1 | package payloads 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "log" 7 | "os" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/Brum3ns/firefly/pkg/encode" 12 | ) 13 | 14 | // Wordlist global tag names: 15 | var ( 16 | TAG_VERIFY = "Verify" 17 | TAG_FUZZ = "Fuzz" 18 | TAG_TRANSFORMATION = "Transformation" 19 | TAGS = []string{TAG_VERIFY, TAG_FUZZ, TAG_TRANSFORMATION} 20 | ) 21 | 22 | // Wordlist structure stores the wordlist and tags 23 | type Wordlist struct { 24 | Wordlist map[string][]string //(tag|wordlist) 25 | Files []string 26 | TransformationList []string 27 | Verify Verify 28 | PayloadProperties 29 | } 30 | 31 | type PayloadProperties struct { 32 | Tamper string 33 | Encode []string 34 | PayloadReplace string 35 | PayloadPattern string 36 | PayloadSuffix string 37 | PayloadPrefix string 38 | } 39 | 40 | type Verify struct { 41 | Payload string 42 | Amount int 43 | } 44 | 45 | // Create a new wordlist object 46 | func NewWordlist(wl *Wordlist) *Wordlist { 47 | wl.Wordlist = make(map[string][]string) 48 | 49 | //Create verify wordlist 50 | wl.Wordlist[TAG_VERIFY] = verifyWordlist(wl.Verify.Payload, wl.Verify.Amount) 51 | 52 | //Create fuzz wordlist by combining all wordlist files given (if multiple) 53 | for _, filename := range wl.Files { 54 | wl.Wordlist[TAG_FUZZ] = append(wl.Wordlist[TAG_FUZZ], wl.createPayloadWordlist(filename)...) 55 | } 56 | 57 | //Transformation wordlist: 58 | wl.Wordlist[TAG_TRANSFORMATION] = wl.TransformationList 59 | 60 | return wl 61 | } 62 | 63 | // Get a wordlist by tag 64 | func (wl *Wordlist) Get(tag string) ([]string, error) { 65 | if wl, exist := wl.Wordlist[tag]; !exist { 66 | return wl, errors.New("can't find the tag") 67 | } else { 68 | return wl, nil 69 | } 70 | } 71 | 72 | // Return a map containing all the wordlists and tags (tag as the key) 73 | func (wl *Wordlist) GetAll() map[string][]string { 74 | return wl.Wordlist 75 | } 76 | 77 | func verifyWordlist(verifyPayload string, amount int) []string { 78 | var lst []string 79 | for i := 0; i < amount; i++ { 80 | lst = append(lst, verifyPayload) 81 | } 82 | return lst 83 | } 84 | 85 | // Create a wordlist by a given file path 86 | func (wl Wordlist) createWordlist(filePath string) []string { 87 | file, err := os.Open(filePath) 88 | if err != nil { 89 | log.Println(err) 90 | } 91 | var ( 92 | lst []string 93 | scanner = bufio.NewScanner(file) 94 | ) 95 | for scanner.Scan() { 96 | item := scanner.Text() 97 | if len(item) > 0 { 98 | lst = append(lst, item) 99 | } 100 | } 101 | file.Close() 102 | return lst 103 | } 104 | 105 | // Take a filename and return a payload adapted wordlist by using given rules in the payloadProperties [struct]ure: 106 | func (wl Wordlist) createPayloadWordlist(filePath string) []string { 107 | file, err := os.Open(filePath) 108 | if err != nil { 109 | log.Println(err) 110 | } 111 | var ( 112 | lst []string 113 | scanner = bufio.NewScanner(file) 114 | ) 115 | for scanner.Scan() { 116 | payload := scanner.Text() 117 | if len(payload) > 0 { 118 | 119 | if len(wl.PayloadReplace) > 0 { 120 | payload = replaceRegex(payload, wl.PayloadReplace) 121 | } 122 | 123 | //Check if payload should be encoded: 124 | if len(wl.Encode) > 0 { 125 | payload = encode.Encode(payload, wl.Encode) 126 | } 127 | payload = (wl.PayloadPrefix + payload + wl.PayloadSuffix) 128 | lst = append(lst, payload) 129 | } 130 | } 131 | file.Close() 132 | return lst 133 | } 134 | 135 | func replaceRegex(p, regexReplace string) string { 136 | i := strings.Split(regexReplace, " => ") 137 | re := regexp.MustCompile(i[0]) 138 | return re.ReplaceAllString(p, i[1]) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/random/random.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "strings" 8 | "time" 9 | 10 | "github.com/Brum3ns/firefly/internal/global" 11 | ) 12 | 13 | var Rand = rand.New(rand.NewSource(time.Now().UnixNano())) 14 | 15 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 16 | 17 | // Insert the a random string/int based on rules: (Ex: s:7, n:3) { 18 | func RandomInsert(s string) string { 19 | var ( 20 | l = []string{"#RANDOM#", "#RANDOMNUM#"} 21 | r string 22 | ) 23 | for key, i := range global.RANDOM_INSERT { 24 | switch key { 25 | case "s": 26 | r = l[0] 27 | case "n": 28 | r = l[1] 29 | } 30 | 31 | if strings.Contains(s, r) { 32 | s = strings.ReplaceAll(s, r, RandomCreate(key, i)) 33 | } 34 | } 35 | return s 36 | } 37 | 38 | // Craft a random str/int value with "x" length - "[t]ype:[l]ength" Return the random value 39 | func RandomCreate(t string, l int) string { 40 | switch t { 41 | case "n": 42 | return RandNumber(l) 43 | default: //Default = [s]tring 44 | return RandString(l) 45 | } 46 | } 47 | 48 | // Return random number with given length as string 49 | func RandNumber(n int) string { 50 | rand.Seed(time.Now().UnixNano()) 51 | if n <= 0 { 52 | n = 1 53 | } 54 | randint := (rand.Float64() - 0.01) * (math.Pow(1*10, (float64)(n))) 55 | return fmt.Sprintf("%.0f", randint) 56 | } 57 | 58 | // Return random string with given length 59 | func RandString(n int) string { 60 | rand.Seed(time.Now().UnixNano()) 61 | 62 | b := make([]byte, n) 63 | for i := range b { 64 | b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] 65 | } 66 | return string(b) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/randomness/randomness.go: -------------------------------------------------------------------------------- 1 | package randomness 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | // Represent all default values 11 | DEFAULT_INROW = 3 12 | DEFAULT_VOCAL = false 13 | DEFAULT_DIGIT = true 14 | DEFAULT_CONSONANT = true 15 | DEFAULT_SPACES = []rune{' ', '-', '_'} 16 | DEFAULT_WHITELISTS = []string{} 17 | DEFAULT_BLACKLISTS = []string{} 18 | DEFAULT_WHITEREGEX = "" 19 | DEFAULT_BLACKREGEX = "(" + 20 | "[Jj]x|[Jj]z|[Kk]q|[Kk]x|[Qq]g|[Qq]j|[Qq]k|[Qq]x|[Qq]z" + 21 | "[Vv]x|[Vv]z|[Ww]x|[Ww]z|[Xx]j|[Xx]k|[Xx]q|[Zz]j|[Zz]q" + 22 | "[Zz]x|[Tt]z|[Ss]x|[Qq]d|[Dd]h|[Hh]c|[Cc]q|[Ll]k|[Uu]y" + 23 | ")" 24 | 25 | // All vocals 26 | VOCALS = map[rune]struct{}{ 27 | 'a': {}, 28 | 'e': {}, 29 | 'i': {}, 30 | 'o': {}, 31 | 'u': {}, 32 | 'y': {}, 33 | 'A': {}, 34 | 'E': {}, 35 | 'I': {}, 36 | 'O': {}, 37 | 'U': {}, 38 | 'Y': {}, 39 | } 40 | ) 41 | 42 | type Randomness struct { 43 | // Config represents the configuration that all methods within the Randomness structure will use 44 | Config 45 | } 46 | 47 | type Config struct { 48 | // When Vocals, Consonant, Digits etc... 49 | // have been found to reapet x amount of time in a row, it will be a random value. 50 | InRow int 51 | // Decide whether triggers should be triggered based on Case sensitive or not 52 | //CaseSensitive bool 53 | // If set to true, if a vocal character is discovered (togehte with any other "triggered" set to true). 54 | // It will trigger the "InRow" and if it reaches "x" of these triggers in a row. The full value will be threated as a random value. 55 | Vocal bool 56 | Digit bool 57 | Consonant bool 58 | // BlackRegex is used to find patterns in a string that will be triggered as blacklisted 59 | BlackRegex string 60 | // BlackRegex is used to find patterns in a string that will be triggered as whitelisted 61 | WhiteRegex string 62 | // Whitelist represents a list that if a certain subvalue (keyword) is found in the full value being tested, 63 | // the subvalue in the whitelist will not be affected by triggers 64 | // The whitelist can contain either a single [char]acter or a keyword. 65 | Whitelist []string 66 | // Blacklist represents a list that if a certain subvalue (keyword) is found in the full value being tested, 67 | // the subvalue in the blacklist will make the full value threated as a random value. 68 | // The whitelist can contain either a single [char]acter or a keyword. 69 | Blacklist []string 70 | // Spaces repesents a list of valid space characters (Ex: , , _, - etc...) 71 | Spaces []rune 72 | // Internal variables that are modified based on the given [Config]uration 73 | blackRegex *regexp.Regexp 74 | whiteRegex *regexp.Regexp 75 | spaces map[rune]struct{} 76 | } 77 | 78 | // Return the Randomness structure adjusted to the configurations given. 79 | // Note : if the config variable InRow is set to zero, the default value will be set 80 | func NewRandomness(config Config) (Randomness, error) { 81 | rand := Randomness{ 82 | Config: config, 83 | } 84 | 85 | if rand.Config.InRow == 0 { 86 | rand.Config.InRow = DEFAULT_INROW 87 | } 88 | 89 | // Generate lookup map for the given spaces 90 | rand.spaces = runeListToLookupMap(rand.Config.Spaces) 91 | 92 | // Set black/white-regex 93 | 94 | if err := rand.SetBlackRegex(rand.Config.BlackRegex); err != nil { 95 | return rand, errors.New("invalid regex set") 96 | } 97 | if err := rand.SetWhiteRegex(rand.Config.WhiteRegex); err != nil { 98 | return rand, errors.New("invalid regex set") 99 | } 100 | // Set black/white-list 101 | rand.SetBlacklist(rand.Config.Blacklist) 102 | rand.SetWhitelist(rand.Config.Whitelist) 103 | 104 | return rand, nil 105 | } 106 | 107 | // Set the config to its default values 108 | func DefaultConfig() Config { 109 | return Config{ 110 | InRow: DEFAULT_INROW, 111 | Vocal: DEFAULT_VOCAL, 112 | Digit: DEFAULT_DIGIT, 113 | Consonant: DEFAULT_CONSONANT, 114 | BlackRegex: DEFAULT_BLACKREGEX, 115 | WhiteRegex: DEFAULT_WHITEREGEX, 116 | Spaces: DEFAULT_SPACES, 117 | Whitelist: DEFAULT_WHITELISTS, 118 | Blacklist: DEFAULT_BLACKLISTS, 119 | } 120 | } 121 | 122 | // Set the whitelist. When a keyword in the whitelist is a sub-string of the tested value, the value will be treated as a non-random value 123 | func (r *Randomness) AppendWhitelist(lst []string) { 124 | r.Config.Blacklist = append(r.Config.Blacklist, lst...) 125 | } 126 | 127 | // Set the blacklist. When a keyword in the blacklist is a sub-string of the tested value, the value will be treated as a random value 128 | func (r *Randomness) AppendBlacklist(lst []string) { 129 | r.Config.Whitelist = append(r.Config.Whitelist, lst...) 130 | } 131 | 132 | // Set the whitelist. When a keyword in the whitelist is a sub-string of the tested value, the value will be treated as a non-random value 133 | func (r *Randomness) SetWhitelist(lst []string) { 134 | r.Config.Blacklist = lst 135 | } 136 | 137 | // Set the blacklist. When a keyword in the blacklist is a sub-string of the tested value, the value will be treated as a random value 138 | func (r *Randomness) SetBlacklist(lst []string) { 139 | r.Config.Whitelist = lst 140 | } 141 | 142 | // Set the blackregex that makes the string containg the keyword be treated as a *non-random value* 143 | // Note : This method must be used when setting a regex for the Randomness structure 144 | func (r *Randomness) SetWhiteRegex(regex string) error { 145 | if re, err := regexp.Compile(regex); err != nil { 146 | return err 147 | } else { 148 | r.whiteRegex = re 149 | return nil 150 | } 151 | } 152 | 153 | // Set the blackregex that makes the string containg the keyword be treated as a *random value* 154 | // Note : This method must be used when setting a regex for the Randomness structure 155 | func (r *Randomness) SetBlackRegex(regex string) error { 156 | if re, err := regexp.Compile(regex); err != nil { 157 | return err 158 | } else { 159 | r.Config.blackRegex = re 160 | return nil 161 | } 162 | } 163 | 164 | // Set trigger, this parameter will be seen as a possible start of a random value. 165 | // If the trigger(s) that are set meet the InRow parameter value, it is then treated as a random value 166 | // Note : Valid trigger(s) are: vocal, digit or consonant 167 | func (r *Randomness) SetTrigger(s string) error { 168 | switch strings.ToLower(s) { 169 | case "vocal": 170 | r.Config.Vocal = true 171 | case "digit": 172 | r.Config.Digit = true 173 | case "consonant": 174 | r.Config.Consonant = true 175 | default: 176 | return errors.New("invalid trigger set, the valid triggers are: \"vocal\", \"digit\" or \"consonant\"") 177 | } 178 | return nil 179 | } 180 | 181 | // unSet trigger, this parameter not be seen as a possible start of a random value. 182 | // Note : Valid trigger(s) are: vocal, digit or consonant 183 | func (r *Randomness) UnsetTrigger(s string) error { 184 | switch strings.ToLower(s) { 185 | case "vocal": 186 | r.Config.Vocal = false 187 | case "digit": 188 | r.Config.Digit = false 189 | case "consonant": 190 | r.Config.Consonant = false 191 | default: 192 | return errors.New("invalid trigger set, the valid triggers are: \"vocal\", \"digit\" or \"consonant\"") 193 | } 194 | return nil 195 | } 196 | 197 | // Check if the string given is likely to be random 198 | // Whitelist and regex will be prioritized if a match is found 199 | func (r *Randomness) IsRandom(s string) bool { 200 | hit := 0 201 | for _, char := range s { 202 | if !r.IsTrigger(char) { 203 | hit = 0 204 | } else { 205 | hit++ 206 | } 207 | //The value is random 208 | if hit == r.Config.InRow { 209 | return true 210 | } 211 | } 212 | 213 | if r.ContainsValidValue(s) { 214 | return false // => Not random 215 | } 216 | if r.ContainsInvalidValue(s) { 217 | return true // => Random 218 | } 219 | return false 220 | } 221 | 222 | // Check white-regex/list 223 | func (r *Randomness) ContainsValidValue(s string) bool { 224 | if s == "" { 225 | return false 226 | } 227 | if (len(r.Config.Whitelist) > 0 && r.IsWhitelist(s)) || 228 | (r.IsWhiteRegex(s)) { 229 | return true 230 | } 231 | return false 232 | } 233 | 234 | // Check black-regex/list 235 | func (r *Randomness) ContainsInvalidValue(s string) bool { 236 | if s == "" { 237 | return false 238 | } 239 | if (len(r.Config.Blacklist) > 0 && r.IsBlacklist(s)) || 240 | (r.IsBlackRegex(s)) { 241 | return true 242 | } 243 | return false 244 | } 245 | 246 | // Check if a given char will pass or trigger in relation to the configuration of the Randomness structure 247 | func (r *Randomness) IsTrigger(char rune) bool { 248 | switch { 249 | case r.Config.Consonant && r.IsConsonant(char): 250 | return true 251 | case r.Config.Digit && r.IsDigit(char): 252 | return true 253 | case r.Config.Vocal && r.IsVocal(char): 254 | return true 255 | default: 256 | return false 257 | } 258 | } 259 | 260 | // Check if the string match the "whitelisted" regex 261 | func (r *Randomness) IsWhiteRegex(s string) bool { 262 | if s == "" { 263 | return false 264 | } 265 | return issetRegex(r.whiteRegex) && r.whiteRegex.MatchString(s) 266 | } 267 | 268 | // Check if the string match the "blacklisted" regex 269 | func (r *Randomness) IsBlackRegex(s string) bool { 270 | if s == "" { 271 | return false 272 | } 273 | return issetRegex(r.blackRegex) && r.Config.blackRegex.MatchString(s) 274 | } 275 | 276 | // Check if a string is within the whitelist 277 | func (r *Randomness) IsWhitelist(s string) bool { 278 | return listItemContains(r.Config.Whitelist, s) 279 | } 280 | 281 | // Check if a string is within the blacklist 282 | func (r *Randomness) IsBlacklist(s string) bool { 283 | return listItemContains(r.Config.Blacklist, s) 284 | } 285 | 286 | // Check if the given string is a space in relation to the configuration of the Randomness structure 287 | func (r *Randomness) IsSpace(char rune) bool { 288 | _, ok := r.spaces[char] 289 | return ok 290 | } 291 | 292 | // Check if the given rune is a vocal (including y and Y) 293 | func (r *Randomness) IsVocal(char rune) bool { 294 | _, ok := VOCALS[char] 295 | return ok 296 | } 297 | 298 | // Check if the given rune is a number ([0-9]) 299 | func (r *Randomness) IsDigit(char rune) bool { 300 | return (char >= '0' && char <= '9') 301 | } 302 | 303 | // Check if the char is a consonant 304 | // Note : (Alias of : !r.IsVocal(char)) 305 | func (r *Randomness) IsConsonant(char rune) bool { 306 | return !r.IsVocal(char) 307 | } 308 | 309 | // Check if any item in thte list are a substring of the given string 310 | func listItemContains(lst []string, s string) bool { 311 | if s == "" { 312 | return false 313 | } 314 | for _, i := range lst { 315 | if strings.Contains(s, i) { 316 | return true 317 | } 318 | } 319 | return false 320 | } 321 | 322 | // Convert a rune list into a lookup map 323 | func runeListToLookupMap(lst []rune) map[rune]struct{} { 324 | m := make(map[rune]struct{}) 325 | for _, i := range lst { 326 | m[i] = struct{}{} 327 | } 328 | return m 329 | } 330 | 331 | // Make sure a compiled regexp is not empty 332 | func issetRegex(re *regexp.Regexp) bool { 333 | return re != nil && re.String() != "" 334 | } 335 | -------------------------------------------------------------------------------- /pkg/request/handler.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/Brum3ns/firefly/pkg/waitgroup" 8 | ) 9 | 10 | type Handler struct { 11 | jobAmount int 12 | Worker worker 13 | TaskStorage *TaskStorage 14 | WaitGroup waitgroup.WaitGroup 15 | 16 | stop chan bool 17 | JobReceived chan int 18 | JobQueue chan RequestSettings 19 | WorkerPool chan chan RequestSettings 20 | HandlerSettings 21 | } 22 | 23 | // HandlerSettings holds all the settings which will be used within the primary Handler structure 24 | type HandlerSettings struct { 25 | VerifyMode bool 26 | Delay int 27 | Threads int 28 | Client *http.Client 29 | RequestBase RequestBase 30 | } 31 | 32 | // worker represents the worker that executes the job 33 | type worker struct { 34 | Delay int 35 | client *http.Client 36 | jobChannel chan RequestSettings 37 | workerPool chan chan RequestSettings 38 | } 39 | 40 | type TaskStorage struct { 41 | URLs []string 42 | Methods []string 43 | Schemes []string 44 | Payloads map[string][]string //(tag|wordlist) 45 | PostData string 46 | InsertPoint string 47 | Headers [][2]string 48 | RandomUserAgent bool 49 | } 50 | 51 | // Start the handler for the workers by giving the tasks to preform and the amount of workers. 52 | func NewHandler(settings HandlerSettings) Handler { // httpclient *http.Client, task *TaskStorage, threads int, delay int, verifyMode bool) *Handler { 53 | return Handler{ 54 | HandlerSettings: settings, 55 | stop: make(chan bool), 56 | JobReceived: make(chan int), 57 | JobQueue: make(chan RequestSettings), 58 | WorkerPool: make(chan chan RequestSettings, settings.Threads), 59 | } 60 | } 61 | 62 | // Start all the workers and assign tasks (jobs) to the request workers 63 | // The process will start listen for job and stop once all job sent is done. 64 | // !Note : (To set the job amount to let the process know when to stop use the method "SetJobAmount") 65 | func (h *Handler) Run(listener chan<- Result) { 66 | var result = make(chan Result) 67 | 68 | //Start the amount of workers related to the amount of given threads: 69 | for i := 0; i < h.Threads; i++ { 70 | h.Worker = newRequestWorker(h.Client, h.WorkerPool, h.Delay) 71 | go h.Worker.spawnRequestWorker(result) 72 | } 73 | 74 | //Listen for new jobs from the queue and send it to the job channel for the workers to handle it: 75 | go func() { 76 | for { 77 | select { 78 | case job := <-h.JobQueue: 79 | go func(job RequestSettings) { 80 | //Get an available job channel from any worker: 81 | jobChannel := <-h.WorkerPool 82 | 83 | //Give the available worker the job: 84 | jobChannel <- job 85 | }(job) 86 | 87 | //Listen for result from any Worker, if a result is recived, then send it to the listener [chan]nel: 88 | case r := <-result: 89 | listener <- r 90 | h.WaitGroup.Done() 91 | } 92 | } 93 | }() 94 | //Wait until all workers have provided the result for each job given, then send a signal that the core process is done: 95 | <-h.stop 96 | } 97 | 98 | // Add a job process to the handler 99 | func (h *Handler) AddJob(job RequestSettings) { 100 | h.WaitGroup.Add(1) 101 | h.jobAmount++ 102 | job.RequestId = h.jobAmount 103 | h.JobQueue <- job 104 | } 105 | 106 | // Get the amount of job that are active 107 | func (e *Handler) GetJobInProcess() int { 108 | return e.WaitGroup.GetCount() 109 | } 110 | 111 | // Wait until all jobs are done 112 | func (e *Handler) Wait() { 113 | e.WaitGroup.Wait() 114 | } 115 | 116 | // Send a stop signal to the handler 117 | func (h *Handler) Stop() { 118 | h.stop <- true 119 | } 120 | 121 | // Get the amount of active processes that are within the process 122 | func (h *Handler) GetInProcess() int { 123 | return h.WaitGroup.GetCount() 124 | } 125 | 126 | // Get the amount of given jobs 127 | func (h *Handler) GetJobAmount() int { 128 | return h.jobAmount 129 | } 130 | 131 | // Create a new request worker 132 | func newRequestWorker(client *http.Client, workerPool chan chan RequestSettings, delay int) worker { 133 | return worker{ 134 | Delay: delay, 135 | client: client, 136 | workerPool: workerPool, 137 | jobChannel: make(chan RequestSettings), 138 | } 139 | } 140 | 141 | // start the request worker 142 | func (w worker) spawnRequestWorker(result chan Result) { 143 | for { 144 | // Add the current worker into the worker queue: 145 | w.workerPool <- w.jobChannel 146 | 147 | RequestJob := <-w.jobChannel 148 | time.Sleep(time.Duration(w.Delay) * time.Millisecond) 149 | result <- Request(w.client, RequestJob) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /pkg/request/request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "crypto/tls" 7 | "encoding/hex" 8 | "fmt" 9 | "io" 10 | "log" 11 | "math/rand" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "regexp" 16 | "sort" 17 | "strings" 18 | "time" 19 | 20 | "github.com/Brum3ns/firefly/pkg/parameter" 21 | ) 22 | 23 | var regex_HTMLTitle = regexp.MustCompile(`(.*?)<\/title>`) 24 | 25 | type Result struct { 26 | RequestId int 27 | TargetHashId string 28 | Tag string 29 | Date string 30 | Payload string 31 | Request HttpRequest 32 | Response Response 33 | Skip bool 34 | Error error 35 | } 36 | 37 | type Response struct { 38 | Time float64 39 | WordCount int 40 | LineCount int 41 | HeaderAmount int 42 | ResponseBodySize int 43 | ContentType string 44 | Title string 45 | Body string 46 | HeaderString string 47 | IPAddress []string 48 | HeadersOriginal [][2]string 49 | http.Response 50 | } 51 | 52 | // HttpRequest configuration (alias of the "http.HttpRequest" struct but with some extra variables added) 53 | type HttpRequest struct { 54 | Body string 55 | URLOriginal string 56 | HeadersOriginal [][2]string 57 | http.Request 58 | } 59 | 60 | // Request settings for each individuallyrequest 61 | type RequestSettings struct { 62 | RequestId int 63 | TargetHashId string 64 | Tag string 65 | URL string 66 | URLOriginal string 67 | Payload string 68 | Method string 69 | UserAgents []string 70 | Parameter parameter.Parameter 71 | RequestBase 72 | } 73 | 74 | // Stores the base (static) HTTP data that will be used within all requests 75 | type RequestBase struct { 76 | PostBody string 77 | InsertPoint string 78 | RandomUserAgent bool 79 | HeadersOriginalArray [][2]string 80 | Headers http.Header 81 | } 82 | 83 | type ClientSettings struct { 84 | Timeout int 85 | MaxIdleConns int 86 | MaxConnsPerHost int 87 | MaxIdleConnsPerHost int 88 | HTTP2 bool 89 | Proxy string 90 | } 91 | 92 | type Host struct { 93 | URL string 94 | Scheme string 95 | Method string 96 | } 97 | 98 | var ( 99 | regexScheme = regexp.MustCompile(`^(.*?)://`) 100 | ) 101 | 102 | // Reques module that send and add the response data to the "results" channel and use "Response" as struct for dynamic temp variables: 103 | func Request(client *http.Client, requestSettings RequestSettings) Result { 104 | httpRequest, err := http.NewRequest(requestSettings.Method, requestSettings.URL, SetPostbody(requestSettings.PostBody)) 105 | if err != nil { 106 | return Result{Error: err} 107 | } 108 | 109 | //Add headers: 110 | httpRequest.Header = requestSettings.Headers 111 | 112 | //Add random headers (if set): 113 | ruaLength := len(requestSettings.UserAgents) 114 | if ruaLength > 0 && requestSettings.RandomUserAgent { 115 | httpRequest.Header.Add("User-Agent", requestSettings.UserAgents[rand.Intn(ruaLength-1)]) 116 | } 117 | 118 | Timer := time.Now() 119 | response, err := client.Do(httpRequest) 120 | if err != nil { 121 | return Result{Error: err} 122 | } 123 | //The response was successful. Get the response time: 124 | var responseTime float64 125 | if len(response.Status) > 0 { 126 | responseTime = float64(time.Since(Timer).Seconds()) 127 | } 128 | 129 | buffer := new(bytes.Buffer) 130 | buffer.ReadFrom(httpRequest.Body) 131 | 132 | //Read the response body content: 133 | bodyBytes, err := io.ReadAll(response.Body) 134 | if err != nil { 135 | log.Println("Could not read the response body:", err) 136 | return Result{Error: err} 137 | } 138 | 139 | bodyString := string(bodyBytes[:]) 140 | response.Body.Close() 141 | 142 | //In case any normalization happens within the request post body it will be spotted for the userlater: 143 | return Result{ 144 | TargetHashId: requestSettings.TargetHashId, 145 | RequestId: requestSettings.RequestId, 146 | Tag: requestSettings.Tag, 147 | Payload: requestSettings.Payload, 148 | Date: time.Now().Format(time.UnixDate), 149 | Error: nil, 150 | Request: HttpRequest{ 151 | URLOriginal: requestSettings.URLOriginal, 152 | HeadersOriginal: requestSettings.HeadersOriginalArray, 153 | Request: *httpRequest, 154 | }, 155 | Response: Response{ 156 | IPAddress: GetIPAddresses(response.Request.URL.Hostname()), 157 | HeaderString: headersToStr(response.Header), 158 | Title: GetHTMLTitle(bodyString), 159 | ContentType: response.Header.Get("content-type"), 160 | ResponseBodySize: len(bodyString), 161 | HeaderAmount: len(response.Header), 162 | Time: responseTime, 163 | LineCount: len(strings.Split(bodyString, "\n")), 164 | WordCount: len(strings.Fields(bodyString)), 165 | Body: bodyString, 166 | Response: *response, 167 | }, 168 | } 169 | } 170 | 171 | // Get a list of IP addresses that the hostname resolves to 172 | func GetIPAddresses(hostname string) []string { 173 | var lst []string 174 | ips, _ := net.LookupIP(hostname) 175 | for _, i := range ips { 176 | lst = append(lst, i.String()) 177 | } 178 | return lst 179 | } 180 | 181 | // Client configure with custom parse *timeout*: 182 | func NewClient(p ClientSettings) *http.Client { 183 | var ( 184 | proxy = http.ProxyFromEnvironment 185 | timeout = time.Duration(time.Duration(p.Timeout) * time.Second) 186 | ) 187 | if len(p.Proxy) > 0 { 188 | if p, err := url.Parse(p.Proxy); err == nil { 189 | proxy = http.ProxyURL(p) 190 | } 191 | } 192 | client := &http.Client{ 193 | CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, 194 | Timeout: timeout, 195 | Transport: &http.Transport{ 196 | ForceAttemptHTTP2: p.HTTP2, 197 | Proxy: proxy, 198 | MaxIdleConns: p.MaxIdleConns, 199 | MaxIdleConnsPerHost: p.MaxIdleConnsPerHost, 200 | MaxConnsPerHost: p.MaxConnsPerHost, 201 | DialContext: (&net.Dialer{ 202 | Timeout: timeout, 203 | }).DialContext, 204 | TLSHandshakeTimeout: timeout, 205 | TLSClientConfig: &tls.Config{ 206 | InsecureSkipVerify: true, 207 | MinVersion: tls.VersionTLS10, 208 | Renegotiation: tls.RenegotiateOnceAsClient, 209 | }, 210 | }, 211 | } 212 | return client 213 | } 214 | 215 | // Setup the post data used within the request (if any) 216 | func SetPostbody(body string) *bytes.Buffer { 217 | return bytes.NewBuffer([]byte(body)) 218 | } 219 | 220 | // Check if a URL contains a scheme (any type) 221 | // Return the scheme or an empty string if no scheme was presented in the given URL. 222 | func ContainScheme(s string) string { 223 | if lst_scheme := regexScheme.FindStringSubmatch(s); len(lst_scheme) > 0 { 224 | return lst_scheme[1] 225 | } 226 | return "" 227 | } 228 | 229 | // Validate if the scheme is either http or https 230 | func ValidHTTPScheme(s string) bool { 231 | return s == "http" || s == "https" 232 | } 233 | 234 | func ValidURLOrIP(s string) bool { 235 | _, err := url.Parse(s) 236 | return err == nil || net.ParseIP(s) != nil 237 | } 238 | 239 | // Convert the *http.Header* to a string (type: "map[string][]string"). 240 | // The converted string version is sorted which makes it easier to compare with others. 241 | func headersToStr(headers http.Header) string { 242 | if headers == nil { 243 | return "" 244 | } 245 | 246 | arr := make([]string, 0, len(headers)) 247 | for k, v := range headers { 248 | arr = append(arr, fmt.Sprintf("%s: %s\n", k, strings.Join(v, " "))) 249 | } 250 | sort.Strings(arr) 251 | return strings.Join(arr, "") 252 | } 253 | 254 | // Set a new value for an existing header 255 | func SetNewHeaderValue(arr [][2]string, header string, value string) [][2]string { 256 | header = strings.ToLower(header) 257 | for idx, h := range arr { 258 | if strings.ToLower(h[0]) == header { 259 | arr[idx] = [2]string{h[0], value} 260 | break 261 | } 262 | } 263 | return arr 264 | } 265 | 266 | // Get a header and it's value from a header array list ([][2]string) 267 | // Note : (Requested header name is in-case sensitive) 268 | func GetHeader(arr [][2]string, header string) (string, string) { 269 | header = strings.ToLower(header) 270 | for _, h := range arr { 271 | if strings.ToLower(h[0]) == header { 272 | return h[0], h[1] //header, value 273 | } 274 | } 275 | return "", "" 276 | } 277 | 278 | // Use regexp to extract the HTML title and return it as a string 279 | func GetHTMLTitle(s string) string { 280 | var title string 281 | if ti := regex_HTMLTitle.FindString(s); ti != "" { 282 | title = ti[7 : len(ti)-8] //(Known size from re_title) 283 | } 284 | return title 285 | } 286 | 287 | // Make a unique md5 hash from the url and method: 288 | func MakeHash(Url, method string) string { 289 | hash := md5.Sum([]byte(method + Url)) 290 | return hex.EncodeToString(hash[:]) 291 | } 292 | 293 | // Take a full raw URL and return the raw query 294 | func GetRawQuery(Url string) (string, error) { 295 | u, err := url.Parse(Url) 296 | return u.RawQuery, err 297 | } 298 | 299 | // Take a string list or map and convert it the http.Header 300 | func LstToHeaders(m map[string]string) http.Header { 301 | var header = http.Header{} 302 | for k, v := range m { 303 | header.Add(k, v) 304 | } 305 | return header 306 | } 307 | -------------------------------------------------------------------------------- /pkg/statistics/statistics.go: -------------------------------------------------------------------------------- 1 | // Provides general statistics for the runner process and all its nested processes/tasks. 2 | package statistics 3 | 4 | import ( 5 | "time" 6 | ) 7 | 8 | type Statistic struct { 9 | base 10 | Response Http `json:"Responses"` 11 | Request Http `json:"Request"` 12 | Output Output `json:"Output"` 13 | Payload `json:"Payload"` 14 | Scanner `json:"Scanner"` 15 | Behavior `json:"Behavior"` 16 | Pattern `json:"Pattern"` 17 | Transformation `json:"Transformation"` 18 | Difference `json:"Difference"` 19 | } 20 | 21 | type Http struct { 22 | base 23 | timeTotal float64 24 | Timeout int `json:"TimeTotal"` 25 | Forbidden int `json:"Forbidden"` 26 | TimeAverage int `json:"TimeAverage"` 27 | } 28 | 29 | type Output struct{ base } 30 | type Scanner struct{ base } 31 | type Pattern struct{ base } 32 | type Payload struct{ base } 33 | type Behavior struct{ base } 34 | type Difference struct{ base } 35 | type Transformation struct{ base } 36 | 37 | type base struct { 38 | err int `json:"error"` 39 | count int `json:"Count"` 40 | filter int `json:"filter"` 41 | time time.Time `json:"time"` 42 | } 43 | 44 | func (b *base) Count() { b.count++ } 45 | func (b *base) CountFilter() { b.filter++ } 46 | func (b *base) CountError() { b.err++ } 47 | func (b *base) GetErrorCount() int { return b.err } 48 | func (b *base) GetCount() int { return b.count } 49 | func (b *base) GetFilterCount() int { return b.filter } 50 | 51 | func NewStatistic(verify bool) Statistic { 52 | return Statistic{ 53 | base: base{time: time.Now()}, 54 | } 55 | } 56 | 57 | // Set a timer 58 | func (b *base) SetTime() time.Time { 59 | return time.Now() 60 | } 61 | 62 | // Return the time duration for how long the process has been running 63 | func (b *base) GetTime() [3]time.Duration { 64 | t := time.Since(b.time) 65 | h := t / time.Hour 66 | t -= h * time.Hour 67 | m := t / time.Minute 68 | t -= m * time.Minute 69 | s := t / time.Second 70 | return [3]time.Duration{h, m, s} 71 | } 72 | 73 | func (h *Http) CountForbidden() { 74 | h.Forbidden++ 75 | } 76 | 77 | func (h *Http) GetCountForbidden() int { 78 | return h.Forbidden 79 | } 80 | 81 | func (h *Http) UpdateTime(t float64) { 82 | h.timeTotal += t 83 | } 84 | 85 | func (h *Http) GetAverageTime() float64 { 86 | if h.timeTotal <= 0 || h.base.count <= 0 { 87 | return 0 88 | } 89 | return h.timeTotal / float64(h.base.count) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/transformation/transformation.go: -------------------------------------------------------------------------------- 1 | package transformation 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "regexp" 7 | 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | var ( 12 | PREFIX = "1229" 13 | SUFFIX = "1345" 14 | ) 15 | 16 | type Transformation struct { 17 | Storage map[string][2]string //(Expected payload[Transformed payload|Description]) 18 | Regex *regexp.Regexp 19 | } 20 | 21 | type Properties struct { 22 | } 23 | 24 | type Result struct { 25 | OK bool 26 | Desc string 27 | Payload string 28 | Format string 29 | } 30 | 31 | // Create a new transformation 32 | func NewTransformation(yamlFile string) (Transformation, error) { 33 | storage, err := getYamlMap(yamlFile) 34 | if err != nil { 35 | return Transformation{}, err 36 | } 37 | return Transformation{ 38 | Storage: storage, 39 | Regex: regexp.MustCompile(PREFIX + `(.*?)` + SUFFIX), 40 | }, nil 41 | } 42 | 43 | func (t Transformation) Detect(body, payload string) Result { 44 | reflectedPayloads := t.Regex.FindAllString(body, -1) 45 | if reflectedPayloads == nil { 46 | return Result{OK: false} 47 | } 48 | 49 | //Search for a payload transformation:s 50 | payload = RmPrefixSuffix(payload) 51 | if arr, ok := t.Storage[payload]; ok { 52 | expectedPayload := arr[0] 53 | desc := arr[1] 54 | 55 | //Check if any valid transformation was discovered from all the reflected payload patterns: 56 | for _, i := range reflectedPayloads { 57 | transformationPayload := RmPrefixSuffix(i) 58 | if transformationPayload == expectedPayload { 59 | return Result{ 60 | OK: true, 61 | Desc: desc, 62 | Payload: payload, 63 | Format: transformationPayload, 64 | } 65 | } 66 | } 67 | } 68 | return Result{OK: false} 69 | } 70 | 71 | func RmPrefixSuffix(s string) string { 72 | return s[len(PREFIX):(len(s) - len(SUFFIX))] 73 | } 74 | func GetWordlist(yamlFile string) ([]string, error) { 75 | var wordlist []string 76 | 77 | m, err := getYamlMap(yamlFile) 78 | if err != nil { 79 | return wordlist, err 80 | } 81 | for payload, _ := range m { 82 | wordlist = append(wordlist, (PREFIX + payload + SUFFIX)) 83 | } 84 | return wordlist, nil 85 | } 86 | 87 | // Read the transformation yaml file and return the full payloads, payload transformation map or error (if any) 88 | func getYamlMap(yamlFile string) (map[string][2]string, error) { 89 | 90 | var storage = make(map[string][2]string) 91 | 92 | // Read the YAML file. 93 | data, err := ioutil.ReadFile(yamlFile) 94 | if err != nil { 95 | return storage, errors.New("error reading transformation yaml file that was given") 96 | } 97 | 98 | // Unmarshal the YAML data into a map. 99 | tmpMap := make(map[string][][2]string) 100 | if err := yaml.Unmarshal(data, &tmpMap); err != nil { 101 | return storage, errors.New("error when unmarshaling the given yaml file, make sure that the yaml file do not contain any syntax errors") 102 | } 103 | 104 | for expectedPayload, lst := range tmpMap { 105 | for _, arr := range lst { 106 | 107 | if len(arr) != 2 { 108 | return storage, errors.New("invalid yaml syntax. The list must contain a paylaod and a desciption.") 109 | } 110 | payload := arr[0] 111 | desc := arr[1] 112 | 113 | storage[payload] = [2]string{expectedPayload, desc} 114 | } 115 | } 116 | 117 | return storage, nil 118 | } 119 | -------------------------------------------------------------------------------- /pkg/waitgroup/waitgroup.go: -------------------------------------------------------------------------------- 1 | package waitgroup 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | type WaitGroup struct { 9 | sync.WaitGroup 10 | count int64 11 | } 12 | 13 | func (wg *WaitGroup) Add(delta int) { 14 | atomic.AddInt64(&wg.count, int64(delta)) 15 | wg.WaitGroup.Add(delta) 16 | } 17 | 18 | func (wg *WaitGroup) Done() { 19 | atomic.AddInt64(&wg.count, -1) 20 | wg.WaitGroup.Done() 21 | } 22 | 23 | func (wg *WaitGroup) GetCount() int { 24 | return int(atomic.LoadInt64(&wg.count)) 25 | } 26 | -------------------------------------------------------------------------------- /tests/httpfilter_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/Brum3ns/firefly/pkg/httpfilter" 14 | ) 15 | 16 | func config() (httpfilter.Filter, error) { 17 | headersIgnore := http.Header{} 18 | 19 | filter, err := httpfilter.NewFilter(httpfilter.Config{ 20 | Mode: "", //"and", // OK 21 | HeaderRegex: "", //"Da[Tt]e+", // OK 22 | BodyRegex: "", //"i[a-z] mi[s]{2}ing", // OK 23 | StatusCodes: []string{}, //[]string{" ++100", "200", " 100-300", "--201"}, // OK 24 | ResponseSizes: []string{}, //[]string{"100-500"}, // OK 25 | WordCounts: []string{"47"}, //[]string{"47 "}, // OK 26 | LineCounts: []string{}, //[]string{"--50", "100-500 "}, // OK 27 | ResponseTimesMillisec: []string{}, //[]string{" ++0.0015 "}, // OK 28 | Header: headersIgnore, // OK 29 | }) 30 | 31 | return filter, err 32 | } 33 | 34 | func Test_HttpFilter(t *testing.T) { 35 | filter, err := config() 36 | if err != nil { 37 | log.Println(err) 38 | os.Exit(1) 39 | } 40 | 41 | // Do basic requests: 42 | for i := 0; i < 10; i++ { 43 | url := "http://localhost:1337/" 44 | 45 | timer := time.Now() 46 | 47 | resp, _ := http.Get(url) 48 | bodyBytes, err := io.ReadAll(resp.Body) 49 | if err != nil { 50 | log.Println("Response body error", err) 51 | } 52 | respTimeSeconds := time.Since(timer).Seconds() 53 | bodyString := string(bodyBytes[:]) 54 | bodySize := len(bodyBytes) 55 | wordCount := len(strings.Fields(bodyString)) 56 | lineCount := len(strings.Split(bodyString, "\n")) 57 | 58 | filterRespone := httpfilter.Response{ 59 | Body: bodyBytes, 60 | StatusCode: resp.StatusCode, 61 | ResponseSize: bodySize, 62 | LineCount: lineCount, 63 | WordCount: wordCount, 64 | ResponseTime: respTimeSeconds, 65 | Headers: resp.Header, 66 | } 67 | 68 | // Filter requests 69 | fmt.Printf("[%d] %s - %d, Size:%d, LC:%d, WC:%d, Time:%f", 70 | i, 71 | resp.Request.URL, 72 | resp.StatusCode, 73 | bodySize, 74 | lineCount, 75 | wordCount, 76 | respTimeSeconds, 77 | ) 78 | // fmt.Printf("|- Headers: %+v", resp.Header) 79 | print("\n") 80 | 81 | if filter.Run(filterRespone) { 82 | fmt.Println("[\033[1;36m>\033[0m] Filtered ID:", i) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/randomness_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/Brum3ns/firefly/pkg/random" 9 | "github.com/Brum3ns/firefly/pkg/randomness" 10 | ) 11 | 12 | func Test_RandomnessAccuracy(t *testing.T) { 13 | // Config 14 | var ( 15 | amountToTest = 100 16 | lengthOfRandomString = 16 17 | ) 18 | 19 | //defaultConfig := randomness.DefaultConfig() 20 | config := randomness.Config{ 21 | InRow: randomness.DEFAULT_INROW, 22 | Vocal: randomness.DEFAULT_VOCAL, 23 | Digit: randomness.DEFAULT_DIGIT, 24 | Consonant: randomness.DEFAULT_CONSONANT, 25 | Blacklist: randomness.DEFAULT_BLACKLISTS, 26 | BlackRegex: randomness.DEFAULT_BLACKREGEX, 27 | Spaces: []rune{' ', '_', '-', '.'}, 28 | } 29 | 30 | // Setup randomness config 31 | r, err := randomness.NewRandomness(config) 32 | if err != nil { 33 | log.Println(err) 34 | } 35 | 36 | // Config random strings to test 37 | lst_random := getRandomStrings(lengthOfRandomString, amountToTest) 38 | // lst_valid := getValidStrings() 39 | 40 | // Check values if they are random 41 | hit := 0 42 | miss := 0 43 | for _, i := range lst_random { 44 | if r.IsRandom(i) { 45 | //fmt.Println("RANDOM:", i) 46 | hit++ 47 | } else { 48 | //fmt.Println("NORMAL:", i) 49 | miss++ 50 | } 51 | } 52 | 53 | // Show result 54 | fmt.Printf("\n===Result===\nHit:%d, Miss:%d (%f%%)\n============\n", hit, miss, float64(miss)/float64(hit)*10) 55 | } 56 | 57 | func getRandomStrings(nr, amount int) []string { 58 | var lst []string 59 | for i := 0; i < amount; i++ { 60 | // nr, _ := strconv.Atoi(random.RandNumber(2)) 61 | lst = append(lst, random.RandString(nr)) 62 | 63 | } 64 | return lst 65 | } 66 | 67 | func getValidStrings() []string { 68 | return []string{ 69 | "username", 70 | "master", 71 | "testThisstuff", 72 | "works", 73 | "cat", 74 | "PillarTown", 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/wordlist.txt: -------------------------------------------------------------------------------- 1 | START'"${{%<%=END 2 | \\" 3 | '-1-' 4 | -------------------------------------------------------------------------------- /testserver/Dockerfile: -------------------------------------------------------------------------------- 1 | #PHP Apache setup 2 | FROM php:8.2-apache 3 | 4 | #Install and update system dependencies 5 | RUN apt update -y; apt install -y supervisor apache2 6 | 7 | #Prepare and setup the working directory 8 | WORKDIR /var/www/html/ 9 | COPY server . 10 | 11 | #Copy configs 12 | COPY config/supervisord.conf /etc/supervisord.conf 13 | COPY config/apache.conf /etc/apache2/sites-enabled/apache.conf 14 | 15 | EXPOSE 1337 16 | 17 | ENTRYPOINT [ "/usr/bin/supervisord", "-c", "/etc/supervisord.conf" ] 18 | -------------------------------------------------------------------------------- /testserver/config/apache.conf: -------------------------------------------------------------------------------- 1 | DirectoryIndex server.php -------------------------------------------------------------------------------- /testserver/config/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | user=root 3 | nodaemon=true 4 | logfile=/dev/null 5 | logfile_maxbytes=0 6 | pidfile=/run/supervisord.pid 7 | 8 | [program:httpd] 9 | command=/bin/bash -c "source /etc/apache2/envvars && exec /usr/sbin/apache2 -DFOREGROUND" 10 | stdout_logfile=/dev/stdout 11 | stdout_logfile_maxbytes=0 12 | stderr_logfile=/dev/stderr 13 | stderr_logfile_maxbytes=0 14 | -------------------------------------------------------------------------------- /testserver/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | php-apache: 4 | container_name: firefly-testserver 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - 1337:80 10 | -------------------------------------------------------------------------------- /testserver/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | docker compose up --build 3 | -------------------------------------------------------------------------------- /testserver/server/server.php: -------------------------------------------------------------------------------- 1 | <?php 2 | $title = "Firefly testserver"; 3 | $desc = "dynamic black box testing"; 4 | 5 | function randomStr($length) { 6 | $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 7 | $charactersLength = strlen($characters); 8 | $randomString = ''; 9 | for ($i = 0; $i < $length; $i++) { 10 | $randomString .= $characters[random_int(0, $charactersLength - 1)]; 11 | } 12 | return $randomString; 13 | } 14 | 15 | // CRLF 16 | if ( isset($_GET['crlf']) ) { 17 | $crlf = $_GET['crlf']; 18 | 19 | if ( str_contains($crlf, '"') === false ) { 20 | header('X-Deleted: true'); 21 | header('X-Appear: APPEAR'); 22 | } else { 23 | header('X-Normal: normalResponse'); 24 | header('X-Appear: false'); 25 | } 26 | 27 | header('Content-Type: text/plain'); 28 | header("X-Custom: $crlf"); 29 | header("Cache-Control: no-cache, must-revalidate"); 30 | } 31 | 32 | function xss() {} 33 | 34 | function ssti() {} 35 | 36 | function reflect() { 37 | if ( isset($_GET['reflect']) ) { 38 | return "Reflect" . $_GET['reflect']; 39 | } 40 | return "reflect parameter is missing"; 41 | } 42 | 43 | function randomness() { 44 | /* 45 | CSRF ~ 16-32 bytes 46 | SESSIONS ~ 32 bytes 47 | */ 48 | 49 | if ( isset($_GET['randomness']) ) { 50 | return randomStr(16); 51 | } 52 | return ""; 53 | } 54 | 55 | function disappear() {} 56 | if ( isset($_GET['disappear']) ) { 57 | $str = "disappear result: "; 58 | if ( str_contains($_GET['disappear'], '"') === false) { 59 | $str .= "APPEAR"; 60 | } 61 | echo "<p>$str</p>"; 62 | } 63 | ?> 64 | 65 | 66 | <html> 67 | <head> 68 | <title><?= $title ?> 69 | 70 | 71 |

72 |

73 | 74 | 75 | 76 |
77 |
78 | 79 |
80 |
81 | 82 |
83 |
84 | 85 |
86 |
87 | 88 | 89 |
90 |
91 | 92 |
93 | 94 |
95 | 96 |
97 | 98 |
99 | 100 |
101 | 102 |
103 | 104 | 105 | --------------------------------------------------------------------------------