├── .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 |
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 |
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 | $str";
62 | }
63 | ?>
64 |
65 |
66 |
67 |
68 | = $title ?>
69 |
70 |
71 | = $title ?>
72 | = $desc ?>
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | = disappear() ?>
94 |
95 |
96 |
97 | = reflect() ?>
98 |
99 |
100 |
101 | = randomness() ?>
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------