├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config.yml ├── knoxnl ├── __init__.py ├── images │ ├── discord.png │ ├── example1.png │ ├── example2.png │ └── title.png └── knoxnl.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | knoxnl.egg-info 2 | dist/ 3 | build/ 4 | __pycache__ 5 | *.todo -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | - 5.3 4 | 5 | - New 6 | 7 | - If the API returns a response of `Invalid or expired API key.` then stop the program with an error `KNOXSS ERR: Invalid or expired API key. Go to knoxss.pro and (re)validate your key.` 8 | 9 | - 5.2 10 | 11 | - Changed 12 | - If a file of URLs is passed as input, those URLs will be processed in a random order instead of the order in the original file. This is to help with the "flying under the radar" methodology that KNOXSS are trying to employ in the next versions. 13 | 14 | - 5.1 15 | 16 | - Changed 17 | 18 | - The error that includes `can't test it (forbidden)` has now changed to `got another 403` so code changed to reflect this. This is only relevant if the `-sb`/`--skip-blocked` argument is used. 19 | 20 | - 5.0 21 | 22 | - New 23 | 24 | - When using the `-rl`/`--runtime-log` or `-v`/`--verbose` argument, the KNOXSS Runtime Logs will be streamed to the terminal. 25 | - Add argument `-st`/`--stall-timeout`. This will be the amount of time that we will wait between steps in the scan (as shown in the runtime logs) before aborting. It must be at least 60 seconds, and will default to 300 seconds. 26 | - Clarify that the `-fn`/`--force-new` option will make checks slower because it is doing checks from scratch instead of looking at the cache. Using the option all the time will defeat the purpose of the cache system. 27 | - When `--runtime-log` or `--verbose` is used, prefix the output with a Thread ID, e.g. `[T2]` so that it is easier to track what log refers to what URL. This will only be displayed if a file was passed as input and there is more than one process (i.e. `args.processes` > 1). 28 | - Add argument `-do`/`--debug-output` to specify a file to save all terminal output to, used for debugging. Using this option will also show the JSON response. 29 | - Reset the `retryAttempt` back to 0 after 24 hours. 30 | - Add a description to the `README` that clarifies what Content Types of URl responses KNOXSS will be able to search for XSS. 31 | 32 | - Changed 33 | 34 | - Change how to determine if a result from the API is classed and an error `[ ERR! ]` or `[ NONE ]` to make it clearer. If the error `KNOXSS engine is failing at some point` is returned, but `API Call` is not `0`, then just set as `[ NONE ]`. If there is an error `Content type of target page can't lead to XSS` then display that after the `[ NONE ]` line to make it clear there was an error, but not with the API. 35 | - Argument `-v`/`--verbose` will also show the streaming runtime logs aswell as the JSON response. 36 | - Change the README to mention that KNOXSS searches for Open Redirects aswell as XSS. 37 | 38 | - 4.13 39 | 40 | - New 41 | 42 | - If for any reason the API response says there is a `PoC` but neither the `XSS` or `Redir` values are `true` in the API response (this shouldn't happen, but has) then just assume the `XSS` should be `true` and report success. 43 | 44 | - Change 45 | 46 | - BUG FIX: The check for whether an error occurred but has a PoC wasn't always working, so sometimes there was a genuine error and no PoC, but being reported as `[ NONE ]`. 47 | - BUG FIX: If the API times out before getting a response, the correct error message wasn't always displayed. 48 | - BUG FIX: If sending a notification to Discord failed, it did not display a warning because of incorrect logic. 49 | - BUG FIX: In the unlikely event that many XSS were found and therefore many Discord notifications sent in a short space of time, it was possible that Discord would respond with a 429 and fail. This has been change to retry a number of times based on the retry value Discord respond with. 50 | - Use `ThreadPoolExecutor` instead of `multiprocessing.Pool` when processing more that one URL at a time. Using `ThreadPoolExecutor` is better for web requests, uses less memory and faster. 51 | - Prevent the error `You need to provide an input with -i argument or through .` being displayed when using the `-up`/`--update` option, and also confirm when on the latest version. 52 | - When showing the `KNOXSS API timed out getting the response (consider changing -t value)` message if the API times out, also show the current value of `-t`. 53 | - Use `sys.exit(0)` instead of `quit()` because it plays more nicely with threaded code. 54 | 55 | - 4.12 56 | 57 | - New 58 | 59 | - After the KNOXSS API upgrade to 4.1.1, the response now has a `Redir` value. If `true` then it means that the PoC is also for an Open Redirect. If an XSS is found, that will just be reported, but if `XSS` is `false` then an Open Redirect will be reported instead. 60 | - If for any reason the API response has a value for `PoC` AND `Error`, then ignore the error and just report the successful `PoC`. 61 | 62 | - 4.11 63 | 64 | - Changed 65 | 66 | - BUG FIX: When processing a file and it's complete, a discord message was only sent when finished but incomplete. It should also be sent when finished and the whole file was completed. 67 | - Make discord notifications prettier. 68 | 69 | - 4.10 70 | 71 | - New 72 | 73 | - Allow the `-r`/`--retries` to be set to zero which will mean it will not try to sleep or retry any links if there are problems with the API. 74 | 75 | - Changed 76 | 77 | - BUG FIX: When the program sleeps for 10 seconds, the KNOXSS error wasn't shown for other thread completing before pausing. 78 | 79 | - v4.9 80 | 81 | - New 82 | - Add `DISCORD_WEBHOOK_COMPLETE` to `config.yml` to specify Discord webhook URL for completion notifications (only if the input was a file). If a webhook has been given, details of a completion (whether finished completely or stopped in error) will be sent to Discord. This can obviously be the same value as `DISCORD_WEBHOOK` if required. 83 | - Add `-dwc`/`--discord-webhook-complete` argument. This can be passed in the command to specify a Discord webhook Completion webhook and will override the value in the `config.yml` file. 84 | - BUG FIX: Add the `-dw`/`--discord-webhook` argument to the `README`, which was missing. 85 | 86 | - v4.8 87 | 88 | - New 89 | 90 | - Add argument `-fn`/`--force-new`. The Force New feature of the KNOXSS API is new in v4. Passing the argument forces KNOXSS to do a new scan instead of getting cached results. 91 | - Add argument `-rl`/`--runtime-log`. The Runtime Log feature of the KNOXSS API is new in v4. Passing the argument provides a live runtime log of the KNOXSS scan. 92 | - Add argument `-nt`/`--no-todo` to not create a `.todo` file if the input file is not completed because of errors. 93 | - If the "Error" in the KNOXSS response included the text "please retry" then it will be retried, and therefore added to the .todo file if stopped. 94 | 95 | - Changed 96 | 97 | - Change references of `https://brutelogic.com.br/xss.php` to `https://x55.is/brutelogic/xss.php` in the README. 98 | 99 | - v4.7 100 | 101 | - Changed 102 | 103 | - Change all references of knoxss.me to knoxss.pro 104 | 105 | - v4.6 106 | 107 | - Changed 108 | 109 | - Changed the response of `[ SAFE ]` to `[ NONE ]` because just because the service doesn't find XSS, it doesn't necessarily mean XSS is impossible on that URL. 110 | - Remove the `-afb` argument because this is no longer used in the API and is done automatically. 111 | 112 | - v4.5 113 | 114 | - New 115 | 116 | - In the output `API calls made so far today`, also add the API limit reset time, if known. 117 | 118 | - Changed 119 | 120 | - Fix the bug that shows `:( There was a problem calling KNOXSS API: local variable 'resp' referenced before assignment` in certain situations where the KNOXSS API has initially timed out. 121 | - Remove `argparse` from `setup.py` because it is a Python standard module. 122 | 123 | - v4.4 124 | 125 | - Changed 126 | 127 | - Fix a stupid bug I left in the last update while trying to test! 128 | 129 | - v4.3 130 | 131 | - New 132 | 133 | - Add new argument `-up`/`--update` to easily update the program to the latest version. 134 | - Add new argument `-sb`/`--skip-blocked` to determine whether any URLs wil be skipped if they have resulted in that many 403 responses from the target. This was previously done all the time for more than 5 blocks for a scheme+(sub)domain, bit will only be done if this argument is passed with a value greater than zero. This is useful if you know there is a WAF in place. 135 | - If there is a problem with the `session` object before a call is even made to the KNOXSS API, catch the error, display to the user, and set the `knoxssResponse.Error` to `Some kind of network error occurred before calling KNOXSS`. 136 | - Save a new file `.apireset` to the default config directory (e.g. `~/.config/knoxnl/`) if a request is returned that has and `API Call` value starting with `1/`. The file will contain the `Timestamp` from the response, converted to the users timezone and increased by 24 hours and 5 minutes. This will be the rough time the API limit will be reset. 137 | - Add new argument `-pur`/`--pause-until-reset`. If passed, and the `.apireset` file exists, then when the API limit is reached, it will pause until 24 hours after the first request (when the limit is reset) and then continue again. 138 | - Display the API Limit Reset time from the `.apireset` file if it exists. The file will be deleted if the timestamp in the file is over 24 hours ago. 139 | - If the `-o`/`--output` value includes a directory, then caused error `[Errno 2] No such file or directory:`. The directory will now be created if it doesn't exist. The `.todo` file will also be created in that same directory. 140 | - Add Timestamp to the KNOXSS API response object and retrieve from the KNOXSS JSON response. 141 | - Add a Disclaimer to the README and the tool banner. 142 | - URL encode any `+` characters in the data for a POST request too. 143 | - Show stats when the program ends. This will show the number of requests made to the API, the number of successful, safe, error and skipped. 144 | 145 | - Changed 146 | 147 | - Only add the method+scheme+domain/domain to the blocked list and start skipping if there have been more than the number of occurrences specified by `-skip`/`--skip-blocked` (only if greater than zero). 148 | - Change the error message `Target is blocking KNOXSS IP` to `Target returned a "403 Forbidden". There could be WAF in place.`. 149 | - When getting the response, and there is no JSON, set the `knoxssResponse.Error` to `knoxssResponseError` instead of `none`. When the KNOXSS returns a response for a non-vulnerable URL, the default value of `knoxssResponse.Error` will be `none`. It needs to be different so isn't accidentally shown as `SAFE`. 150 | 151 | - v4.2 152 | 153 | - Changed 154 | 155 | - BUG FIX: `&` were not being encoded since the version 4.1 156 | 157 | - v4.1 158 | 159 | - New 160 | 161 | - Add arg `-r`/`--retries` for the number of times to retry when having issues connecting to the KNOXSS API (default: 3) 162 | - Add arg `ri`/`--retry-interval` for how many seconds to wait before retrying when having issues connecting to the KNOXSS API (default: 30 seconds) 163 | - Add arg `rb`/`--rety-backoff` for the backoff factor used when retrying when having issues connecting to the KNOXSS API (default: 1.5). For example, with defaults, first time will wait for 30 seconds, 2nd time will be 45 (30 x 1.5) seconds, etc. 164 | - Check for the runtime error `Response ended prematurely` when sending to the API. This can happen if the user is using a VPN, which the KNOXSS servers don't seem to like. 165 | - If a scheme and domain have been flagged as blocked already, skip other URLs with the same. Include `from urllib.parse import urlparse` and add `urlparse3` to `setup.py` to achieve this. 166 | - URL encode any `+` characters in the target URL so they don't get changed to spaces. 167 | 168 | - Changed 169 | 170 | - Change the error `The target website timed out` to `The KNOXSS API timed out getting the response (consider changing -t value)` 171 | - Change the error `The target dropped the connection.` to `The KNOXSS API dropped the connection.` 172 | - Set the default timeout limit for requests to the KNOXSS API to 600 seconds. The previous default was 180, but this has been resulting in many timeouts as the server response can take a lot longer for some URLs. 173 | - If you set `-t`/`--timeout` to 0, it will not request a timeout at all when calling the KNOXSS API. 174 | - When adding a blocked domain to the set, include the scheme too because there have been examples where a target blocks KNOXSS for `https://target.com`, but not `http://target.com`. 175 | 176 | - v4.0 177 | 178 | - New 179 | 180 | - Add `long_description_content_type` to `setup.py` to upload to PyPi 181 | - Add `knxonl` to `PyPi` so can be installed with `pip install knoxnl` 182 | - Include a NOTE in the `README` to put a URL in quotes when passing as input because the shell can interpret the `&` character as an instruction to run a background task. 183 | - If a `Read timed out` error happens then the target timed out, but could work again later. The target URL will be added back to the end of the list to try again later (or be written to the `.todo` file). 184 | 185 | - Changed 186 | 187 | - If the input file ends with `.YYYYMMDD_HHMMSS.todo` then remove that part before adding it for the new `.todo` file. 188 | - When an input URL contains unicode characters, it can cause an error from the API like `'latin-1' codec can't encode characters in position 41-41: Body ('�') is not valid Latin-1`. When posting to the API, use `data.encode('utf-8')` to send it encoded in UTF-8. 189 | - Ensure that the current URL is removed from the list written to the `.todo` file if it is an error with the target. 190 | 191 | - v3.4 192 | 193 | - Changed 194 | 195 | - Fix a bug that causes the error `ERROR showOutput 1: '_io.TextIOWrapper' object has no attribute 'print'` when writing to the output file. 196 | 197 | - v3.3 198 | 199 | - Changed 200 | 201 | - If input from a file is a blank line, just ignore instead of raising an error. 202 | - Fix a bug when using `knoxnl` from Burps Piper. Only try writing the `.todo` file if a file was passed. 203 | 204 | - v3.2 205 | 206 | - Fix bug that was stopping `--version` argument working 207 | 208 | - v3.1 209 | 210 | - New 211 | 212 | - When installing knoxnl, if the config.yml already exists then it will keep that one and create `config.yml.NEW` in case you need to replace the old config. 213 | 214 | - v3.0 215 | 216 | - New 217 | 218 | - The `.todo` file will also be written if `Ctrl-C` is used to exit. 219 | - Show the current version of the tool in the banner, and whether it is the latest, or outdated. 220 | - Check for `urllib3` error mentioning `Temporary failure in name resolution`. This implies the users internet connection has been lost so we will stop processing. 221 | - Check for `urllib3` error mentioning `Failed to establish a new connection`. This implies the machine is running low on memory. 222 | - Add `Config file path` to data shown when `-v` is passed. 223 | - Sometimes when you call KNOXSS API, you will get the error `Expiration time reset, please try again.`. f this happens, the same request will be made again one more time. 224 | - Add a HTTPAdapter to retry if the request to the API returns status code 429, 500, 502, 503 or 504 225 | - Add `TOOO` section to README.md 226 | - If a file is passed as input, show how many targets knoxnl is running for. 227 | - If a message from the KNOXSS API indicated that the target is blocking KNOXSS, then a list of domains that are blocking will be displayed at the end. 228 | 229 | - Changed 230 | 231 | - If the `API_KEY` value is blank in `config.yml`, make sure the error is displayed correctly. Also add the following message to the error message displayed: `Don't forget to generate and SAVE your API key before using it here!` 232 | - Check for `Invalid or expired API key.` as-well as `Incorrect API key.` and add the following text to the error message displayed: `Check if your subscription is still active, or if you forgot to save your current API key.` 233 | 234 | - v2.10 235 | 236 | - New 237 | 238 | - If a URL is provided without a scheme, then add `https://` as default and warn the user. 239 | - Add `*.todo` to `.gitignore` file. 240 | 241 | - Changed 242 | 243 | - The `.todo` file will not just be written if the `-o` option is used. If an input file is passed then when the APi Rate Limit is hit, or the Service Unavailable message is given, the remaining URLs will be written to a `.todo` file. 244 | - The `.todo` file will be named with the name of the input file plus a timestamp, e.g. `inputfile.YYYMMDD_HHMMSS.todo`. It was previously the same as the output file name plus `.todo`. 245 | - Limit the number of successful API calls made per minute (requested by @KN0X55). 246 | - Fix a bug that sometimes prevented the `API calls made so far today` being displayed. 247 | - If the message `service unavailable` is returned from the API, the process will stop, and the `.todo` file will be written. 248 | - Show more specific error messages. 249 | 250 | - v2.9 251 | 252 | - New 253 | 254 | - Add `DISCORD_WEBHOOK` to `config.yml` to specify Discord webhook URL for alerts. If a webhook has been given, details of a successful XSS will be sent to Discord. 255 | - Add `-dw`/`--discord-webhook` argument. This can be passed in the command to specify a Discord webhook and will override the value in the `config.yml` file. 256 | 257 | - v2.8 258 | 259 | - New 260 | 261 | - Add `--version` argument to see the current version of knoxnl. 262 | 263 | - v2.7 264 | 265 | - Changed 266 | 267 | - Back out changes from v2.3 that changed the processing of a file of URLs by running a batch of 1-5 (determines by the `-p` argument) at a time per minute. This has caused the messages to be incorrect when running for a file or URLs. 268 | 269 | - v2.6 270 | 271 | - Changed 272 | 273 | - Changes to prevent `SyntaxWarning: invalid escape sequence` errors when Python 3.12 is used. 274 | 275 | - v2.5 276 | 277 | - Changed 278 | 279 | - Fix a bug from [Issue #5](https://github.com/xnl-h4ck3r/knoxnl/issues/5) that prevented output being written. 280 | 281 | - v2.4 282 | 283 | - Changed 284 | 285 | - Change the default KNOXSS API URL to the new version after the upgrade. 286 | 287 | - v2.3 288 | 289 | - Changes 290 | 291 | - Change the processing of a file of URLs by running a batch of 1-5 (determines by the `-p` argument) at a time per minute. This is required by the KNOXSS API, and will result is getting blocked by their WAF if not followed. 292 | - Change the number of the daily limit of KNOXSS API calls from 3335 to 5000 in README. This changed after a KNOXSS server upgrade. 293 | - Show a specific error if the API rate limit is reached, and also if the user gets 403 from the API 294 | 295 | - v2.2 296 | 297 | - Changed 298 | 299 | - Sometimes the console can't display unicode characters. When displaying strings with the emoji 🤘, if an error occurs, try writing without. 300 | - Change description for setting up with Piper to clarify the Linux way is also for Windows if NOT using WSL. 301 | 302 | - v2.1 303 | 304 | - Changed 305 | 306 | - Remove code that encodes the whole URL as this prevents successful XSS being found in some cases. 307 | 308 | - v2.0 309 | 310 | - New 311 | 312 | - Improve installation method to allow `pip` and `pipx`. 313 | 314 | - Changed 315 | 316 | - Only output HTML with `Something went wrong:` message if the error is from `knoxss``. Don't output messages like when there is no internet connection. 317 | 318 | - v1.5 319 | 320 | - New 321 | 322 | - Added argument `-bp`/`--burp-piper` which can be used if calling from the Burp Piper extension. 323 | 324 | - v1.4 325 | 326 | - New 327 | 328 | - Add argument `-pd`/`--post-data`. If a POST request is made, this is the POST data passed. It must be in the format `'param1=value¶m2=value¶m3=value'`. If this isn't passed and query string parameters are used, then these will be used as POST data if POST Method is requested. 329 | 330 | - Changed 331 | 332 | - Fix a bug that was incorrectly formatting POST requests when using the URL query string as Post data. 333 | - When showing Error results, remove the query string from the URL and show post data in `[]` after. 334 | - When `-v` is used and settings are displayed, clarify when a file is used for input, e.g. add ` (FILE)` to the name. 335 | 336 | - v1.3 337 | 338 | - New 339 | 340 | - If a file is passed as input and the `-o` \ `--output` argument is passed then if the KNOXSS API rate limit is reached before checking all URLs, all unchecked URLs will be written to a file with the same location and name as the output file, but with a `.todo` suffix. This file can then be renamed and used as input when you are allowed to make API requests again. 341 | 342 | - v1.2 343 | 344 | - New 345 | 346 | - Add argument `-oa`/`--output-all`. If passed, all results will be output to the file, not just successful one's. 347 | - Show the current API calls and limit in the CLI at the end of each line, e.g. `[1337/3335]`. 348 | - Try to close the output file before ending when someone presses Ctrl-C. 349 | 350 | - Changed 351 | 352 | - Include `pyaml` in the `setup.py` file as this is required. 353 | 354 | - v1.1 355 | 356 | - Changed 357 | 358 | - Small improvements to display some errors in a better way 359 | 360 | - v1.0 361 | 362 | - Changed 363 | 364 | - When URL is passed to the API in POST data, after encoding & to %26 characters, URL encode the whole URL. This still works as before, but also resolves some issues with URLs that caused an InvalidChunkLength error. 365 | 366 | - v0.1 367 | 368 | - Initial release 369 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 /XNL-h4ck3r 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ## About - v5.3 4 | 5 | This is a python wrapper around the amazing [KNOXSS API](https://knoxss.pro/?page_id=2729) by Brute Logic. 6 | To use this tool (and the underlying API), you must have a valid KNOXSS API key. Don't have one? Go visit https://knoxss.pro and subscribe! 7 | This was inspired by the ["knoxssme" tool](https://github.com/edoardottt/lit-bb-hack-tools/tree/main/knoxssme) by @edoardottt2, but developed to allow for greater options. 8 | 9 | **DISCLAIMER: We are not responsible for any use, and especially misuse, of this tool or the KNOXSS API** 10 | 11 | ## Installation 12 | 13 | **NOTE: If you already have a `config.yml` file, it will not be overwritten. The file `config.yml.NEW` will be created in the same directory. If you need the new config, remove `config.yml` and rename `config.yml.NEW` back to `config.yml`.** 14 | 15 | `knoxnl` supports **Python 3**. 16 | 17 | Install `knoxnl` in default (global) python environment. 18 | 19 | ```bash 20 | pip install knoxnl 21 | ``` 22 | 23 | OR 24 | 25 | ```bash 26 | pip install git+https://github.com/xnl-h4ck3r/knoxnl.git -v 27 | ``` 28 | 29 | You can upgrade with 30 | 31 | ```bash 32 | knoxnl -up 33 | ``` 34 | 35 | OR 36 | 37 | ```bash 38 | pip install --upgrade knoxnl 39 | ``` 40 | 41 | ### pipx 42 | 43 | Quick setup in isolated python environment using [pipx](https://pypa.github.io/pipx/) 44 | 45 | ```bash 46 | pipx install git+https://github.com/xnl-h4ck3r/knoxnl.git 47 | ``` 48 | 49 | ## Usage 50 | 51 | | Arg | Long Arg | Description | 52 | | ---- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 53 | | -i | --input | Input to send to KNOXSS API: a single URL, or file of URLs. **NOTE: If you pass a URL, put it in quotes otherwise the shell can interpret `&` characters as instruction to run a background task.** | 54 | | -o | --output | The file to save the successful XSS and payloads to. If the file already exist it will just be appended to unless option `-ow` is passed. If the full path doesn't exist, then any necessary directories will be created. | 55 | | -ow | --output-overwrite | If the output file already exists, it will be overwritten instead of being appended to. | 56 | | -oa | --output-all | Write all results to the output file, not just successful one's. | 57 | | -X | --http-method | Which HTTP method to use, values `GET`, `POST` or `BOTH` (default: `GET`). If `BOTH` is chosen, then a `GET` call will be made, followed by a `POST`. | 58 | | -pd | --post-data | If a POST request is made, this is the POST data passed. It must be in the format `'param1=value¶m2=value¶m3=value'`. If this isn't passed and query string parameters are used, then these will be used as POST data if POST Method is requested. | 59 | | -H | --headers | Add custom headers to pass with HTTP requests. Pass in the format `'Header1:value1;\|Header2:value2'` (e.g. separate different headers with a pipe \| character). | 60 | | -A | --api-key | The KNOXSS API Key to use. This will be used instead of the value in `config.yml` | 61 | | -s | --success-only | Only show successful XSS and Open Redirect payloads in the CLI output. | 62 | | -p | --processes | Basic multithreading is done when getting requests for a file of URLs. This argument determines the number of processes/threads (one per URL to check) are run per minute (default: 3). This is due to the rate limit of the KNOXSS API. | 63 | | -t | --timeout | How many seconds to wait for the KNOXSS API to respond before giving up (default: 600). If set to 0, then timeout will be used. | 64 | | -st | --stall-timeout | How many seconds to wait for the KNOXSS API scan to take between steps before aborting (default: 1200 seconds). | 65 | | -bp | --burp-piper | Use if **knoxnl** is called from the Burp Piper extension, so that a request in **Burp Suite** proxy can be tested. See the [Using in Burp Suite Proxy](#using-in-burp-suite-proxy) section below. | 66 | | -dw | --discord-webhook | The Discord Webhook to send successful XSS and Open Redirect notifications to. This will be used instead of the value in `config.yml`. | 67 | | -dwc | --discord-webhook-complete | The Discord Webhook to send completion notifications to when a file has been used as input (whether finished completely or stopped in error). This will be used instead of the value in `config.yml`. | 68 | | -r | --retries | The number of times to retry when having issues connecting to the KNOXSS API (default: 3). If set to 0 then then it will not sleep or try to retry any URLs. The number of retries will also be reset every 24 hours when running for a file. | 69 | | -ri | --retry-interval | How many seconds to wait before retrying when having issues connecting to the KNOXSS API (default: 30) | 70 | | -rb | --retry-backoff | The backoff factor used when retrying when having issues connecting to the KNOXSS API (default: 1.5). For example, with defaults, first time will wait for 30 seconds, 2nd time will be 45 (30 x 1.5) seconds, etc. | 71 | | -pur | --pause-until-reset | If the API Limit reset time is known and the API limit is reached, wait the required time until the limit is reset and continue again. The reset time is only known if knoxnl has run for request number 1 previously. The API rate limit is reset 24 hours after request 1. | 72 | | -sb | --skip-blocked | The number of 403 Forbidden responses from a target (for a given HTTP method + scheme + (sub)domain) before skipping. This is useful if you know the target has a WAF. The default is zero, which means no blocking is done. | 73 | | -fn | --force-new | Forces KNOXSS to do a new scan instead of getting cached results. **NOTE: Using this option will make checks SLOWER because it won't check the cache. Using the option all the time will defeat the purpose of the cache system.** | 74 | | -rl | --runtime-log | Provides a live runtime log of the KNOXSS scan that will be streamed. This will be prefixed with the number of the thread running, e.g. `[T2]` so the output can be tracked easier (if `-p`/`--processes` > 1). | 75 | | -nt | --no-todo | Do not create the .todo file if the input file is not completed because of issues with API, connection, etc. | 76 | | -up | --update | Update knoxnl to the latest version. | 77 | | -v | --verbose | Verbose output | 78 | | -do | --debug-output | VerThe file to save all terminal output to a file, used for debugging. Using this option will also show the JSON response. | 79 | | | --version | Show current version number. | 80 | | -h | --help | show the help message and exit | 81 | 82 | ## config.yml 83 | 84 | The `config.yml` file (in the global location based on the OS, e.g. `~/.config/knoxnl/config.yml`) has the keys which can be updated to suit your needs: 85 | 86 | - `API_URL` - This can be set to the KNOXSS API endpoint, if and when it is changed 87 | - `API_KEY` - Your KNOXSS API key that you will have generated on https://knoxss.pro/ 88 | - `DISCORD_WEBHOOK` - Your discord webhook URL if you want to be notified of successful XSS and Open Redirect 89 | - `DISCORD_WEBHOOK_COMPLETE` - Your discord webhook URL if you want to be notified of completion when a file has been used as input (whether finished completely or stopped in error). 90 | 91 | ## Important Notes from KNOXSS API Guidelines 92 | 93 | - Unlike other APIs that just retrieve data from a database, KNOXSS API returns the results like the web interface, actually performing a comprehensive vulnerability scan for XSS and Open Redirects. Since scan results are not stored on our system, they need to be generated on the fly taking several JavaScript-evaluated live tests to return them. So it's natural the data returned takes much more time to get delivered since there's a long process involved at server side. To have a better idea of what the APi is actually doing, you can stream the KNOXSS runtime logs with the `-rl`/`--runtime-log` argument (this also shows using the `-v`/`--verbose` argument). 94 | - The API standard rate limit is 5000 requests over a 24 hours period. That means an average of **2.3 requests per minute** so please try to keep this pace **to not overload the system**. Due to this rate limit, if the input is a file or URLs, then only a batch (determined by argument `-p`/`--processes`) will be run per minute. 95 | - **Generating or Regenerating your API Key** - The API key is in your profile. If you have never generated it you need to hit the button at least once to generate it and save. Any time you need a new API key for security reasons, you can simply hit the button and regenerate it. 96 | - **Flash Mode Mark - [XSS]** - Provide the `[XSS]` mark in any place of the target's data values to enable Flash Mode which enables KNOXSS to perform a single quick XSS Polyglot based test. 97 | 98 | ## Important Notes for knoxnl 99 | 100 | - At the time of writing this, the daily limit of KNOXSS API calls is **5000**. If you are testing a large file of URLs, it is advisable that you use the `-o` / `--output` option to specify a file where output will be written. If you do reach the API limit, it resets 24 hours after the first API call was made. If you are processing a file of URLs, you can use the `-pur`/`--pause-until-reset` to wait until the reset happens and then continue (this is only possible if the first request was run by `knoxnl` so it could save the response timestamp). 101 | - If you pass an input file and the API limit is reached, or the Service is Unavailable, part way through the input, all unchecked URLs will be output to an file in the same location, and with the same name as the input file, but with a `.YYYYMMDD_HHMMSS.todo` suffix. You can then rename this file and use this as input at another time. The `.todo` file will be created in the current directory unless a path is specified in the `-o`/`--output` directory, and then the `.todo` file will be created in the same directory. 102 | - By default, only successful results are written to the output file. 103 | - Passing argument `-oa` / `--output-all` will write **ALL** results to the output file, not just successful one's. 104 | - The KNOXSS API has a rate limit of no more than 5 URLs processed per minute. If the rate limit is exceeded then you might end up getting blocked by their WAF, and you will not get the results you want. This rate limit is taken into account when passing a file of URLs as input. However, if you keep running for a single URL more than this per minute you wil run into problems. Please respect the rules of their API. 105 | - When creating a file of URLs to pass as input, bear in mind that KNOXSS only looks for mainly is XSS in HTML and XML responses. However, JS, JSON and Text are not blocked from scanning due to header injection auxiliary vulnerabilities, so it can be worth passing URLs with these response types too. KNOXSS also can't search for XSS on responses that give a 403 response. When curating a file of URLs, a tool such as [urless](https://github.com/xnl-h4ck3r/urless) can also help. 106 | - The KNOXSS only deals with POST requests with basic post data in the format `'param1=value¶m2=value¶m3=value'`, e.g. `Content-Type: application/x-www-form-urlencoded`. 107 | - If the `-pd`/`--post-data` argument is not passed and a POST request is made, it will use the query string from the URL as post data if it has one. 108 | - If a file is passed as input and POST method is required, then the post data parameters need to be provided as a query string for the URL in the file, e.g. `https://example.com?postParam1=value&postParam2-value`. If you use the `-pd`/`--post-data` with an input file then ALL URLs will use that post data. 109 | - These are required based on the way the KNOXSS API works. 110 | 111 | ## Examples 112 | 113 | ### Basic 114 | 115 | Pass a single URL: 116 | 117 | **NOTE: If you pass a URL, put it in quotes otherwise the shell can interpret `&` characters as instruction to run a background task.** 118 | 119 | ``` 120 | knoxnl -i "https://x55.is/brutelogic/xss.php" 121 | ``` 122 | 123 | Or a file of URLs: 124 | 125 | ``` 126 | knoxnl -i ~/urls.txt 127 | ``` 128 | 129 | ### Detailed 130 | 131 | Test a single URL for both GET and POST. if it is successful, the payload will be output to `output.txt`. In this case, an API key is provided, overriding any in `config.yml` if it exists. Also, the parameter value has been passed as `[XSS]` which will request the KNOXSS API to enable Flash Mode which performs a single quick XSS Polyglot based test: 132 | 133 | ``` 134 | knoxnl -i "https://x55.is/brutelogic/xss.php?b3=[XSS]" -X BOTH -o output.txt -A 93c864f5-af3a-4f6a-8b25-8662bc8b5ab6 135 | ``` 136 | 137 | Test a single URL for POST and pass post body data: 138 | 139 | ``` 140 | knoxnl -i "https://x55.is/brutelogic/xss.php" -X POST -pd user=xnl -o output.txt 141 | ``` 142 | 143 | Pass cookies and an auth header for a single URL: 144 | 145 | ``` 146 | knoxnl -i "https://bugbountytarget.com?a=one&b=2" -H "Cookie: sessionId=9d7127ca-8966-4ae9-b20a-c2892a2f1167; lang=en;|Authorization: Basic eyJZb3UgZGlkbid0IHRoaW5rIHRoaXMgYSBnZW51aW5lIHRva2VuIGRpZCB5b3U/ISA7KSJ9" 147 | ``` 148 | 149 | ## Using in Burp Suite Proxy 150 | 151 | To be able to use **knoxnl** to test a request in Burp Suite Proxy, we can use it in conjunction with the amazing `Piper` extension by András Veres-Szentkirályi. Follow the steps below to set it up: 152 | 153 | 1. Go to the **BApp Store** in Burp and install the **Piper** extension. 154 | 2. Go to the **Piper** tab and click the **Context menu items** sub tab, then click the **Add** button. 155 | 3. In the **Add menu item** dialog box, enter the **Name** as `knoxnl` and change the **Can handle...** drop down to `HTTP requests only`. 156 | 4. Change both the **Minimum required number of selected items** and **Maximum allowed number of selected items** values to `1`. 157 | 5. Click the **Edit...** button for **Command** and the **Command invocation editor** dialog box should be displayed. 158 | 6. Check the **Pass HTTP headers to command** check-box. 159 | 7. If you are on a Linux machine, or Windows without WSL, do the following: 160 | - In the **Command line parameters** box you enter the command and arguments one line at a time. 161 | - You want to enter a command of `/my/path/to/python3 /my/path/to/knoxnl.py --burp-piper -X BOTH` for example, providing the full path of the `knoxnl` binary file. 162 | - So in the **Command line parameters** input field it would look like this: 163 | ``` 164 | /my/path/to/knoxnl 165 | --burp-piper 166 | -X 167 | BOTH 168 | ``` 169 | - You may want to add other **knoxnl** arguments too, such as `-A your_knoxss_api_key`, `-t 60`, etc. Remember to put the argument and the value on separate lines. 170 | 8. If you are on a Windows machine using WSL, do the following: 171 | - In the **Command line parameters** box you enter the command and arguments one line at a time. 172 | - You want to enter a command of `wsl -e /my/path/to/knoxnl --burp-piper -X BOTH` for example, providing the full path of the `knoxnl.py` binary file. 173 | - So in the **Command line parameters** input field it would look like this: 174 | ``` 175 | wsl 176 | -e 177 | /my/path/to/knoxnl 178 | --burp-piper 179 | -X 180 | BOTH 181 | ``` 182 | - You may want to add other **knoxnl** arguments too, such as `-A your_knoxss_api_key`, `-t 60`, etc. Remember to put the argument and the value on separate lines. 183 | 9. Click the **OK** button on the **Command invocation editor** dialog box. 184 | 10. Click the **OK** button on the **Edit menu item** dialog box. 185 | 186 | Piper is now set up to be able to call **knoxnl**. 187 | 188 | To call **knoxnl** for a particular request, follow these steps: 189 | 190 | 1. Right click on a Request and select **Extensions -> Piper -> Process 1 request -> knoxnl**. 191 | 2. A window should open with the title **Piper - knoxnl**. 192 | 3. **IMPORTANT NOTE:** This **Piper** window stays blank until the command is complete (which could be up to 180 seconds - the default value of `-t`/`--timeout`). 193 | 4. When complete, it should show the **knoxnl** output in the same way as on the command line version. Just close the window when you have finished. 194 | 195 | With **Piper** you can also send the **knoxnl** request to a queue by selecting **Extensions -> Piper -> Add to queue**. You can then go to the **Queue** sub tab under **Piper** and see the request. Right click the request to send to **knoxnl**. 196 | 197 | ## Issues 198 | 199 | If you come across any problems at all, or have ideas for improvements, please feel free to raise an issue on Github. If there is a problem, it will be useful if you can provide the exact command you ran and a detailed description of the problem. If possible, run with `-v` to reproduce the problem and let me know about any error messages that are given, and the KNOXSS API request/response. 200 | 201 | ## TODO 202 | 203 | - Allow input to be piped into `knoxnl`. 204 | - Allow a large file to be passed, and if the API limit is reached, wait until the API limit is refreshed and continue. 205 | - Deal with downgrading HTTPS to HTTP if required. 206 | - If a target is blocking KNOXSS, then try a few times, and if no success then skip all links for that domain, and write to a `.blocked` file. 207 | 208 | ## Example output 209 | 210 | Single URL: 211 | 212 |
213 | 214 | File of URLs checked with GET and POST: 215 | 216 |
217 | 218 | Example Discord notification: 219 | 220 |
221 | 222 | Good luck and good hunting! 223 | If you really love the tool (or any others), or they helped you find an awesome bounty, consider [BUYING ME A COFFEE!](https://ko-fi.com/xnlh4ck3r) ☕ (I could use the caffeine!) 224 | 225 | 🤘 /XNL-h4ck3r 226 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | API_URL: https://api.knoxss.pro 2 | API_KEY: YOUR_API_KEY 3 | DISCORD_WEBHOOK: YOUR_WEBHOOK 4 | DISCORD_WEBHOOK_COMPLETE: YOUR_WEBHOOK 5 | -------------------------------------------------------------------------------- /knoxnl/__init__.py: -------------------------------------------------------------------------------- 1 | __version__="5.3" -------------------------------------------------------------------------------- /knoxnl/images/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnl-h4ck3r/knoxnl/52fa1e7b683d5de963ee8a8130e0676d94ed8882/knoxnl/images/discord.png -------------------------------------------------------------------------------- /knoxnl/images/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnl-h4ck3r/knoxnl/52fa1e7b683d5de963ee8a8130e0676d94ed8882/knoxnl/images/example1.png -------------------------------------------------------------------------------- /knoxnl/images/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnl-h4ck3r/knoxnl/52fa1e7b683d5de963ee8a8130e0676d94ed8882/knoxnl/images/example2.png -------------------------------------------------------------------------------- /knoxnl/images/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xnl-h4ck3r/knoxnl/52fa1e7b683d5de963ee8a8130e0676d94ed8882/knoxnl/images/title.png -------------------------------------------------------------------------------- /knoxnl/knoxnl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Python 3 3 | # A wrapper around the amazing KNOXSS API (https://knoxss.pro/?page_id=2729) by Brute Logic (@brutelogic) 4 | # Inspired by "knoxssme" by @edoardottt2 5 | # Full help here: https://github.com/xnl-h4ck3r/knoxnl#readme 6 | # Good luck and good hunting! If you really love the tool (or any others), or they helped you find an awesome bounty, consider BUYING ME A COFFEE! (https://ko-fi.com/xnlh4ck3r) ☕ (I could use the caffeine!) 7 | 8 | import requests 9 | import argparse 10 | from signal import SIGINT, signal 11 | import time 12 | from termcolor import colored 13 | import yaml 14 | import json 15 | import os 16 | import sys 17 | from pathlib import Path 18 | try: 19 | from . import __version__ 20 | except: 21 | pass 22 | from datetime import datetime, timedelta 23 | from requests.adapters import HTTPAdapter, Retry 24 | import re 25 | import time 26 | from urllib.parse import urlparse 27 | from dateutil import tz 28 | import subprocess 29 | from concurrent.futures import ThreadPoolExecutor 30 | from threading import Event 31 | import queue 32 | import threading 33 | import random 34 | 35 | # Global variables 36 | stopProgram = False 37 | latestApiCalls = "Unknown" 38 | urlPassed = True 39 | rateLimitExceeded = False 40 | needToStop = False 41 | needToRetry = False 42 | dontDisplay = False 43 | successCountXSS = 0 44 | successCountOR = 0 45 | safeCount = 0 46 | errorCount = 0 47 | requestCount = 0 48 | skipCount = 0 49 | outFile = None 50 | fileIsOpen = False 51 | debugOutFile = None 52 | debugFileIsOpen = False 53 | todoFileName = '' 54 | currentCount = {} 55 | configPath = '' 56 | inputValues = set() 57 | blockedDomains = {} 58 | HTTP_ADAPTER = None 59 | HTTP_ADAPTER_DISCORD = None 60 | retryAttempt = 0 61 | apiResetPath = '' 62 | timeAPIReset = None 63 | forbiddenResponseCount = 0 64 | latestVersion = '' 65 | runtimeLog = "" 66 | 67 | pauseEvent = Event() 68 | 69 | DEFAULT_API_URL = 'https://api.knoxss.pro' 70 | API_KEY_SECRET = "aHR0cHM6Ly95b3V0dS5iZS9kUXc0dzlXZ1hjUQ==" 71 | 72 | # The default timeout for KNOXSS API to respond in seconds 73 | DEFAULT_TIMEOUT = 1200 # 20 minutes 74 | DEFAULT_STALL_TIMEOUT = 300 # 5 minutes 75 | 76 | # The default number of times to retry when having issues connecting to the KNOXSS API 77 | DEFAULT_RETRIES = 3 78 | 79 | # The default number of seconds to wait when having issues connecting to the KNOXSS API before retrying 80 | DEFAULT_RETRY_INTERVAL = 30 81 | 82 | # The default backoff factor to use when retrying after having issues connecting to the KNOXSS API 83 | DEFAULT_RETRY_BACKOFF_FACTOR = 1.5 84 | 85 | # Yaml config values 86 | API_URL = '' 87 | API_KEY = '' 88 | DISCORD_WEBHOOK = '' 89 | DISCORD_WEBHOOK_COMPLETE = '' 90 | 91 | # Object for an KNOXSS API response 92 | class knoxss: 93 | Code = '' 94 | XSS = '' 95 | Redir = '' 96 | PoC = '' 97 | Calls = '' 98 | Error = '' 99 | POSTData = '' 100 | Timestamp = '' 101 | 102 | # Shared map for readable IDs 103 | thread_id_map = {} 104 | thread_id_lock = threading.Lock() 105 | thread_id_counter = [0] # list so it's mutable in closure 106 | 107 | # Use this to reset the number of retries every 24 hours 108 | lastRetryResetTime = datetime.now() 109 | 110 | def showVersion(): 111 | global latestVersion 112 | try: 113 | if latestVersion == '': 114 | tprint('Current knoxnl version '+__version__+' (unable to check if latest)') 115 | elif __version__ == latestVersion: 116 | tprint('Current knoxnl version '+__version__+' ('+colored('latest','green')+')\n') 117 | else: 118 | tprint('Current knoxnl version '+__version__+' ('+colored('outdated','red')+')\n') 119 | except: 120 | pass 121 | 122 | def showBanner(): 123 | tprint() 124 | tprint(" _ "+colored("_ ___ ","red")+colored("__","yellow")+colored(" _","cyan")) 125 | tprint("| | ___ __ "+colored("V","red")+"_"+colored(r"V\ \ ","red")+colored("/ /","yellow")+colored("_ __","green")+colored(" | | ","cyan")) 126 | tprint(r"| |/ / '_ \ / _ \ "[:-1]+colored(r"\ \ "[:-1],"red")+colored("/ /","yellow")+colored(r"| '_ \ "[:-1],"green")+colored("| | ","cyan")) 127 | tprint("| <| | | | (_) "+colored("/ /","red")+colored(r"\ \ "[:-1],"yellow")+colored("| | | |","green")+colored(" | ","cyan")) 128 | tprint(r"|_|\_\_| |_|\___"+colored("/_/ ","red")+colored(r"\_\ "[:-1],"yellow")+colored("_| |_|","green")+colored("_| ","cyan")) 129 | tprint(colored(" by @Xnl-h4ck3r ","magenta")) 130 | tprint() 131 | try: 132 | currentDate = datetime.now().date() 133 | if currentDate.month == 12 and currentDate.day in (24,25): 134 | tprint(colored(" *** 🎅 HAPPY CHRISTMAS! 🎅 ***","green",attrs=["blink"])) 135 | elif currentDate.month == 10 and currentDate.day == 31: 136 | tprint(colored(" *** 🎃 HAPPY HALLOWEEN! 🎃 ***","red",attrs=["blink"])) 137 | elif currentDate.month == 1 and currentDate.day in (1,2,3,4,5): 138 | tprint(colored(" *** 🥳 HAPPY NEW YEAR!! 🥳 ***","yellow",attrs=["blink"])) 139 | elif currentDate.month == 10 and currentDate.day == 10: 140 | tprint(colored(" *** 🧠 HAPPY WORLD MENTAL HEALTH DAY!! 💚 ***","yellow",attrs=["blink"])) 141 | tprint() 142 | except: 143 | pass 144 | tprint(colored('DISCLAIMER: We are not responsible for any use, and especially misuse, of this tool or the KNOXSS API','yellow')) 145 | tprint() 146 | showVersion() 147 | 148 | # Functions used when printing messages dependant on verbose options 149 | def verbose(): 150 | return args.verbose 151 | 152 | def showBlocked(): 153 | global blockedDomains 154 | try: 155 | # Accumulate domains with a count more than the specified limit into a list 156 | domainsBlockedLimit = [domain for domain, count in blockedDomains.items() if count > args.skip_blocked-1] 157 | if domainsBlockedLimit: 158 | # Join the domains into a comma-separated string 159 | domainList = ', '.join(domainsBlockedLimit) 160 | 161 | tprint(colored('The following domains seem to be blocking KNOXSS and might be worth excluding for now:','yellow'),colored(domainList,'white')) 162 | except: 163 | pass 164 | 165 | # Handle the user pressing Ctrl-C and programatic interupts 166 | def handler(signal_received, frame): 167 | """ 168 | This function is called if Ctrl-C is called by the user 169 | An attempt will be made to try and clean up properly 170 | """ 171 | global stopProgram, needToStop, inputValues, blockedDomains, todoFileName, fileIsOpen, debugFileIsOpen 172 | stopProgram = True 173 | pauseEvent.clear() 174 | if not needToStop: 175 | tprint(colored('\n>>> "Oh my God, they killed Kenny... and knoXnl!" - Kyle','red')) 176 | # If there are any input values not checked, write them to a .todo file, unless the -nt/-no-todo arg was passed 177 | try: 178 | if len(inputValues) > 0 and not args.no_todo: 179 | try: 180 | tprint(colored('\n>>> Just trying to save outstanding input to .todo file before ending...','yellow')) 181 | with open(todoFileName, 'w') as file: 182 | for inp in inputValues: 183 | file.write(inp+'\n') 184 | tprint(colored('All unchecked URLs have been written to','cyan'),colored(todoFileName+'\n', 'white')) 185 | except Exception as e: 186 | tprint(colored('Error saving file ','cyan'),colored(todoFileName+'\n', 'white'),colored(':'+str(e),'red')) 187 | except: 188 | pass 189 | 190 | # If there were any domains that might be blocking KNOXSS, let the user know 191 | if args.skip_blocked > 0: 192 | showBlocked() 193 | 194 | # Try to close the output files before ending 195 | try: 196 | fileIsOpen = False 197 | outFile.close() 198 | debugFileIsOpen = False 199 | debugOutFile.close() 200 | except: 201 | pass 202 | sys.exit(0) 203 | 204 | # Show the chosen options and config settings 205 | def showOptions(): 206 | 207 | global urlPassed, fileIsOpen, debugFileIsOpen, API_URL, timeAPIReset 208 | 209 | try: 210 | print(colored('Selected config and settings:', 'cyan')) 211 | 212 | print(colored('Config file path:', 'magenta'), configPath) 213 | print(colored('KNOXSS API Url:', 'magenta'), API_URL) 214 | print(colored('KNOXSS API Key:', 'magenta'), API_KEY) 215 | print(colored('Discord Webhook:', 'magenta'), DISCORD_WEBHOOK) 216 | 217 | if args.burp_piper: 218 | print(colored('-i: ' + args.input, 'magenta'), 'Request passed from Burp Piper Extension') 219 | else: 220 | if urlPassed: 221 | print(colored('-i: ' + args.input, 'magenta'), 'The URL to check with KNOXSS API.') 222 | else: 223 | print(colored('Discord Webhook Complete:', 'magenta'), DISCORD_WEBHOOK_COMPLETE) 224 | print(colored('-i: ' + args.input + ' (FILE)', 'magenta'), 'All URLs will be passed to KNOXSS API.') 225 | 226 | if fileIsOpen: 227 | print(colored('-o: ' + args.output, 'magenta'), 'The output file where successful XSS payloads will be saved.') 228 | print(colored('-ow: ' + str(args.output_overwrite), 'magenta'), 'Whether the output will be overwritten if it already exists.') 229 | print(colored('-oa: ' + str(args.output_all), 'magenta'), 'Whether the output all results to the output file, not just successful one\'s.') 230 | 231 | if debugFileIsOpen: 232 | print(colored('-do: ' + args.debug_output, 'magenta'), 'The output file where all debug information will be saved.') 233 | 234 | if not urlPassed: 235 | print(colored('-p: ' + str(args.processes), 'magenta'), 'The number of parallel requests made, i.e. number or processes/threads.') 236 | 237 | print(colored('-X: ' + args.http_method, 'magenta'), 'The HTTP method checked by KNOXSS API.') 238 | 239 | if args.http_method in ('POST','BOTH'): 240 | if args.post_data != '': 241 | print(colored('-pd: ' + args.post_data, 'magenta'), 'Data passed with a POST request.') 242 | else: 243 | if urlPassed: 244 | try: 245 | postData = args.input.split('?')[1] 246 | except: 247 | postData = '' 248 | print(colored('-pd: ' + postData, 'magenta'), 'Data passed with a POST request.') 249 | else: 250 | print(colored('-pd: {the URL query string}', 'magenta'), 'Data passed with a POST request.') 251 | 252 | if args.headers != '': 253 | print(colored('-H: ' + args.headers, 'magenta'), 'HTTP Headers passed with requests.') 254 | 255 | print(colored('-t: ' + str(args.timeout), 'magenta'), 'The number of seconds to wait for KNOXSS API to respond.') 256 | print(colored('-st: ' + str(args.stall_timeout), 'magenta'), 'The number of seconds to wait for the KNOXSS API scan to take between steps before aborting.') 257 | 258 | if args.retries == 0: 259 | print(colored('-r: ' + str(args.retries), 'magenta'), 'If having issues connecting to the KNOXSS API, the program will not sleep and not try to retry any URLs. ') 260 | else: 261 | print(colored('-r: ' + str(args.retries), 'magenta'), 'The number of times to retry when having issues connecting to the KNOXSS API. The number of retries will also be reset every 24 hours when running for a file.') 262 | if args.retries > 0: 263 | print(colored('-ri: ' + str(args.retry_interval), 'magenta'), 'How many seconds to wait before retrying when having issues connecting to the KNOXSS API.') 264 | print(colored('-rb: ' + str(args.retry_backoff), 'magenta'), 'The backoff factor used when retrying when having issues connecting to the KNOXSS API.') 265 | 266 | if args.skip_blocked > 0: 267 | print(colored('-sb: ' + str(args.skip_blocked), 'magenta'), 'The number of 403 Forbidden responses from a target (for a given HTTP method + scheme + (sub)domain) before skipping.') 268 | 269 | if args.force_new: 270 | print(colored('-fn: True', 'magenta'), 'Forces KNOXSS to do a new scan instead of getting cached results.') 271 | 272 | if args.runtime_log: 273 | print(colored('-rl: True', 'magenta'), 'Provides a live runtime log of the KNOXSS scan.') 274 | 275 | if args.no_todo and not urlPassed: 276 | print(colored('-nt: True', 'magenta'), 'If the input file is not completed because of issues with API, connection, etc. the .todo file will not be created.') 277 | 278 | if timeAPIReset is not None: 279 | print(colored('KNOXSS API Limit Reset Time:', 'magenta'), str(timeAPIReset.strftime("%Y-%m-%d %H:%M"))) 280 | if args.pause_until_reset: 281 | print(colored('-pur: True', 'magenta'), 'If the API limit is reached, the program will pause and then continue again when it has been reset.') 282 | else: 283 | if args.pause_until_reset: 284 | print(colored('-pur: True', 'magenta'), 'NOT POSSIBLE: Unfortunately the API reset time is currently unknown, so the program cannot be paused and continue when the API limit is reached.') 285 | print() 286 | 287 | except Exception as e: 288 | print(colored('ERROR showOptions: ' + str(e), 'red')) 289 | 290 | # If an API key wasn't supplied, or was invalid, then point the user to https://knoxss.pro 291 | def needApiKey(): 292 | # If the console can't display 🤘 then an error will be raised to try without 293 | try: 294 | tprint(colored('Haven\'t got an API key? Why not head over to https://knoxss.pro and subscribe?\nDon\'t forget to generate and SAVE your API key before using it here! 🤘\n', 'green')) 295 | except: 296 | tprint(colored('Haven\'t got an API key? Why not head over to https://knoxss.pro and subscribe?\nDon\'t forget to generate and SAVE your API key before using it here!\n', 'green')) 297 | 298 | def getConfig(): 299 | # Try to get the values from the config file, otherwise use the defaults 300 | global API_URL, API_KEY, DISCORD_WEBHOOK, DISCORD_WEBHOOK_COMPLETE, configPath, HTTP_ADAPTER, HTTP_ADAPTER_DISCORD, apiResetPath 301 | try: 302 | 303 | # Put config in global location based on the OS. 304 | configPath = ( 305 | Path(os.path.join(os.getenv('APPDATA', ''), 'knoxnl')) if os.name == 'nt' 306 | else Path(os.path.join(os.path.expanduser("~"), ".config", "knoxnl")) if os.name == 'posix' 307 | else Path(os.path.join(os.path.expanduser("~"), "Library", "Application Support", "knoxnl")) if os.name == 'darwin' 308 | else None 309 | ) 310 | 311 | # Set up an HTTPAdaptor for retry strategy when making requests 312 | try: 313 | retry= Retry( 314 | total=2, 315 | backoff_factor=0.1, 316 | status_forcelist=[429, 500, 502, 503, 504], 317 | raise_on_status=False, 318 | respect_retry_after_header=False 319 | ) 320 | HTTP_ADAPTER = HTTPAdapter(max_retries=retry) 321 | except Exception as e: 322 | tprint(colored('ERROR getConfig 2: ' + str(e), 'red')) 323 | 324 | # Set up an HTTPAdaptor for retry strategy when sending Discord notifications 325 | try: 326 | retry= Retry( 327 | total=3, 328 | backoff_factor=1, 329 | status_forcelist=[429, 500, 502, 503, 504], 330 | raise_on_status=False, 331 | respect_retry_after_header=True 332 | ) 333 | HTTP_ADAPTER_DISCORD = HTTPAdapter(max_retries=retry) 334 | except Exception as e: 335 | tprint(colored('ERROR getConfig 2: ' + str(e), 'red')) 336 | 337 | 338 | configPath.absolute 339 | # Set config file path and apireset file path 340 | if configPath == '': 341 | apiResetPath = '.apireset' 342 | configPath = 'config.yml' 343 | else: 344 | apiResetPath = Path(configPath / '.apireset') 345 | configPath = Path(configPath / 'config.yml') 346 | config = yaml.safe_load(open(configPath)) 347 | 348 | try: 349 | API_URL = config.get('API_URL') 350 | except Exception as e: 351 | tprint(colored('Unable to read "API_URL" from config.yml; defaults set', 'red')) 352 | API_KEY = DEFAULT_API_URL 353 | try: 354 | API_KEY = config.get('API_KEY') 355 | if args.api_key != '': 356 | API_KEY = args.api_key 357 | tprint(colored('NOTE: Overriding "API_KEY" from config.yml with passed API Key', 'cyan'), API_KEY + '\n') 358 | else: 359 | if API_KEY is None or API_KEY == 'YOUR_API_KEY': 360 | tprint(colored('ERROR: You need to add your "API_KEY" to config.yml or pass it with the -A option.', 'red')) 361 | needApiKey() 362 | sys.exit(0) 363 | except Exception as e: 364 | tprint(colored('Unable to read "API_KEY" from config.yml; We need an API key! - ' + str(e), 'red')) 365 | needApiKey() 366 | sys.exit(0) 367 | 368 | # Set the Discord webhook. If passed with argument -dw / --discord-webhook then this will override the config value 369 | if args.discord_webhook != '': 370 | DISCORD_WEBHOOK = args.discord_webhook 371 | else: 372 | try: 373 | DISCORD_WEBHOOK = config.get('DISCORD_WEBHOOK').replace('YOUR_WEBHOOK','') 374 | except Exception as e: 375 | DISCORD_WEBHOOK = '' 376 | 377 | # Set the Discord webhook Complete. If passed with argument -dwc / --discord-webhook-complete then this will override the config value 378 | if args.discord_webhook_complete != '': 379 | DISCORD_WEBHOOK_COMPLETE = args.discord_webhook_complete 380 | else: 381 | try: 382 | DISCORD_WEBHOOK_COMPLETE = config.get('DISCORD_WEBHOOK_COMPLETE').replace('YOUR_WEBHOOK','') 383 | except Exception as e: 384 | DISCORD_WEBHOOK_COMPLETE = '' 385 | 386 | except Exception as e: 387 | try: 388 | if args.api_key == '': 389 | tprint(colored('Unable to read config.yml and API Key not passed with -A; Unable to use KNOXSS API! - ' + str(e), 'red')) 390 | needApiKey() 391 | sys.exit(0) 392 | else: 393 | API_URL = DEFAULT_API_URL 394 | API_KEY = args.api_key 395 | tprint(colored('Unable to read config.yml; using default API URL and passed API Key', 'cyan')) 396 | DISCORD_WEBHOOK = args.discord_webhook 397 | DISCORD_WEBHOOK_COMPLETE = args.discord_webhook_complete 398 | except Exception as e: 399 | tprint(colored('ERROR getConfig 1: ' + str(e), 'red')) 400 | 401 | # Get a short ID for the thread 402 | def short_thread_id(): 403 | ident = threading.get_ident() 404 | with thread_id_lock: 405 | if ident not in thread_id_map: 406 | thread_id_map[ident] = thread_id_counter[0] 407 | thread_id_counter[0] += 1 408 | return thread_id_map[ident] 409 | 410 | # Prefix CLI output with a Thread number so the runtime logs and responses can be tracked effectively. 411 | # Only prefix if the number of threads is more than 1 and a file was passed as input 412 | def tprint(*arguments, sep=' ', end='\n'): 413 | global urlPassed, debugOutFile, debugFileIsOpen 414 | try: 415 | message = sep.join(str(arg) for arg in arguments) 416 | tid = colored('[T'+str(short_thread_id()+1)+']','magenta') 417 | if (verbose() or args.runtime_log) and (args.processes > 1 and not urlPassed): 418 | print(f"{tid} {message}", end=end) 419 | else: 420 | if not re.match(r'^\[\d{2}:\d{2}:\d{2}\]: ', message) or (verbose() or args.runtime_log): 421 | print(message, end=end) 422 | 423 | # If there is a debug file then write it all to the file 424 | if debugFileIsOpen: 425 | if (args.processes > 1 and not urlPassed): 426 | debugOutFile.write(f"{tid} {message}{end}") 427 | else: 428 | debugOutFile.write(message + end) 429 | except Exception as e: 430 | print(colored('ERROR tprint 1: ' + str(e), 'red')) 431 | 432 | # Call the knoxss API 433 | def knoxssApi(targetUrl, headers, method, knoxssResponse): 434 | global latestApiCalls, rateLimitExceeded, needToStop, dontDisplay, stopProgram 435 | global HTTP_ADAPTER, inputValues, needToRetry, requestCount, runtimeLog, debugFileIsOpen 436 | 437 | try: 438 | if stopProgram: return 439 | try: 440 | apiHeaders = { 441 | 'X-API-KEY': API_KEY, 442 | 'Content-Type': 'application/x-www-form-urlencoded', 443 | 'User-Agent': 'knoXnl tool by @xnl-h4ck3r' 444 | } 445 | 446 | targetData = targetUrl.replace('&', '%26').replace('+', '%2B') 447 | postData = '' 448 | if method == 'POST' and args.http_method in ('POST', 'BOTH'): 449 | if args.post_data != '': 450 | postData = args.post_data.replace('&', '%26').replace('+', '%2B') 451 | if '?' in targetUrl: 452 | targetData = targetData.split('?')[0] 453 | else: 454 | if '?' in targetUrl: 455 | postData = targetData.split('?')[1] 456 | targetData = targetData.split('?')[0] 457 | 458 | data = f'target={targetData}' 459 | if method == 'POST': 460 | data += f'&post={postData}' 461 | if args.force_new: 462 | data += '&new=1' 463 | if args.runtime_log or verbose() or debugFileIsOpen: 464 | data += '&log=1' 465 | if headers != '': 466 | encHeaders = headers.replace(' ', '%20').replace('|', '%0D%0A') 467 | data += f'&auth={encHeaders}' 468 | except Exception as e: 469 | tprint(colored('ERROR knoxssApi 2: ' + str(e), 'red')) 470 | 471 | tryAgain = True 472 | while tryAgain: 473 | tryAgain = False 474 | session = requests.Session() 475 | session.mount('https://', HTTP_ADAPTER) 476 | requestCount += 1 477 | 478 | fullResponse = "" 479 | runtimeLog = "" 480 | q = queue.Queue() 481 | shared = {'status_code': None} 482 | reader_finished = threading.Event() 483 | 484 | def reader(): 485 | global stopProgram 486 | try: 487 | if stopProgram: 488 | return 489 | with session.post( 490 | url=API_URL, 491 | headers=apiHeaders, 492 | data=data.encode('utf-8'), 493 | timeout=None, 494 | stream=True 495 | ) as r: 496 | shared['status_code'] = r.status_code 497 | for line in r.iter_lines(decode_unicode=True): 498 | if line: 499 | q.put(line) 500 | except Exception as e: 501 | nonlocal connectionError 502 | connectionError = True 503 | q.put(f"[Reader error] {e}") 504 | finally: 505 | reader_finished.set() 506 | q.put(None) 507 | 508 | connectionError = False 509 | thread = threading.Thread(target=reader, daemon=True) 510 | thread.start() 511 | 512 | start_time = time.time() 513 | last_line_time = start_time 514 | stalled = False 515 | timed_out = False 516 | 517 | if stopProgram: return 518 | try: 519 | while True: 520 | if stopProgram: break 521 | if time.time() - start_time > args.timeout: 522 | timed_out = True 523 | break 524 | 525 | try: 526 | line = q.get(timeout=0.5) 527 | if line is None: 528 | break # End of stream 529 | if not line.startswith("["): 530 | fullResponse += line + "\n" 531 | 532 | if (args.runtime_log or verbose() or debugFileIsOpen) and line.startswith("["): 533 | tprint(line) 534 | 535 | last_line_time = time.time() 536 | 537 | except queue.Empty: 538 | if time.time() - last_line_time > args.stall_timeout: 539 | stalled = True 540 | break 541 | except Exception as e: 542 | tprint(colored('ERROR knoxssApi 3: ' + str(e), 'red')) 543 | 544 | if stopProgram: return 545 | try: 546 | knoxssResponse.Code = str(shared['status_code']) if shared['status_code'] else "Unknown" 547 | if connectionError: 548 | knoxssResponse.Error = 'There was a problem connecting to the KNOXSS API. Check your internet connection.' 549 | knoxssResponse.PoC = 'none' 550 | knoxssResponse.Calls = 'Unknown' 551 | if args.retries > 0: 552 | needToRetry = True 553 | elif stalled: 554 | knoxssResponse.Error = f"The scan stalled for more than {args.stall_timeout} seconds, so aborting." 555 | knoxssResponse.PoC = 'none' 556 | knoxssResponse.Calls = 'Unknown' 557 | elif timed_out: 558 | knoxssResponse.Error = f"The scan exceeded the timeout of {args.timeout} seconds." 559 | knoxssResponse.PoC = 'none' 560 | knoxssResponse.Calls = 'Unknown' 561 | elif fullResponse.strip() == 'Invalid or expired API key.': 562 | knoxssResponse.Error = 'Invalid or expired API key. Go to knoxss.pro and (re)validate your key.' 563 | knoxssResponse.PoC = 'none' 564 | knoxssResponse.Calls = 'Unknown' 565 | needToStop = True 566 | else: 567 | if verbose() or debugFileIsOpen: 568 | tprint('KNOXSS API request:') 569 | tprint(' Data: ' + data) 570 | tprint('KNOXSS API response:') 571 | tprint(fullResponse.strip()) 572 | 573 | jsonPart = fullResponse.strip() 574 | if not jsonPart: 575 | raise ValueError("Empty response received from KNOXSS API") 576 | jsonResponse = json.loads(jsonPart) 577 | knoxssResponse.XSS = str(jsonResponse.get('XSS')) 578 | knoxssResponse.Redir = str(jsonResponse.get('Redir')) 579 | knoxssResponse.PoC = str(jsonResponse.get('PoC')) 580 | knoxssResponse.Calls = str(jsonResponse.get('API Call', 'Unknown')) 581 | if knoxssResponse.Calls == '0': 582 | knoxssResponse.Calls = 'Unknown' 583 | knoxssResponse.Error = str(jsonResponse.get('Error')) 584 | knoxssResponse.POSTData = str(jsonResponse.get('POST Data')) 585 | knoxssResponse.Timestamp = str(jsonResponse.get('Timestamp')) 586 | 587 | if knoxssResponse.PoC != 'none': 588 | if 'service unavailable' in knoxssResponse.Error.lower() or "please retry" in knoxssResponse.Error.lower(): 589 | if args.retries > 0: 590 | needToRetry = True 591 | elif knoxssResponse.Error == 'API rate limit exceeded.': 592 | rateLimitExceeded = True 593 | knoxssResponse.Calls = 'API rate limit exceeded!' 594 | if not (timeAPIReset is not None and args.pause_until_reset): 595 | needToStop = True 596 | else: 597 | inputValues.discard(targetUrl) 598 | else: 599 | inputValues.discard(targetUrl) 600 | 601 | if knoxssResponse.Calls not in ('Unknown', ''): 602 | latestApiCalls = knoxssResponse.Calls 603 | except Exception as e: 604 | knoxssResponse.Error = str(e) 605 | knoxssResponse.PoC = 'none' 606 | knoxssResponse.Calls = 'Unknown' 607 | tprint(colored('ERROR knoxssApi 4: ' + str(e), 'red')) 608 | 609 | except Exception as e: 610 | tprint(colored('ERROR knoxssApi 1: ' + str(e), 'red')) 611 | 612 | def checkForAlteredParams(url): 613 | # Show a warning if it looks like the user has tampered with the parameter values before sending to knoxnl. Some indications of this are using FUZZ and also Gxss. 614 | # Show a warning if any XSS payloads appear to be included in the URL already 615 | try: 616 | if '=FUZZ' in url or '=Gxss' in url: 617 | tprint(colored('WARNING: It appears the URL may have been manually changed by yourself (or another tool) first. KNOXSS might not work as expected without the default values of parameters (some parameters might be value sensitive). Just pass original URLs to knoxnl.', 'yellow')) 618 | regexCheck = r'<[A-Z]+|alert([^\}]*)|javascript:' 619 | regexCheckCompiled = re.compile(regexCheck, re.IGNORECASE) 620 | if regexCheckCompiled.search(url): 621 | tprint(colored('WARNING: It appears the URL may already include some XSS payload. If that\'s correct, KNOXSS won\'t work as expected since it\'s not meant to receive XSS payloads, but to provide them.', 'yellow')) 622 | except Exception as e: 623 | tprint(colored('ERROR checkForAlteredParams 1: ' + str(e), 'red')) 624 | 625 | def processInput(): 626 | global urlPassed, latestApiCalls, stopProgram, inputValues, todoFileName 627 | try: 628 | latestApiCalls = 'Unknown' 629 | 630 | # if the Burp Piper Extension argument was passed, assume the input is passed by stdin 631 | if args.burp_piper: 632 | try: 633 | # Get URL and 634 | firstLine = sys.stdin.readline() 635 | # Get HOST header 636 | secondLine = sys.stdin.readline() 637 | args.input = 'https://'+secondLine.split(' ')[1].strip()+firstLine.split(' ')[1].strip() 638 | # Get first header after HOST 639 | headers = '' 640 | header = sys.stdin.readline() 641 | # Get all headers 642 | while header.strip() != '': 643 | if header.lower().find('cookie') >= 0 or header.lower().find('api') >= 0 or header.lower().find('auth') >= 0: 644 | headers = headers + header.strip()+'|' 645 | header = sys.stdin.readline() 646 | args.headers = headers.rstrip('|') 647 | # Get the POST body 648 | postBody = '' 649 | postBodyLine = sys.stdin.readline() 650 | while postBodyLine.strip() != '': 651 | postBody = postBody + postBodyLine.strip() 652 | postBodyLine = sys.stdin.readline() 653 | postBody = postBody.replace('&', '%26') 654 | args.post_data = postBody 655 | 656 | except Exception as e: 657 | print(colored('ERROR: Burp Piper Extension input expected: ' + str(e), 'red')) 658 | exit() 659 | else: 660 | # Check that the -i argument was passed 661 | if not args.input: 662 | print(colored('ERROR: The -i / --input argument must be passed (unless calling from Burp Piper extension with -bp / --burp-piper). The input can be a single URL or a file or URLs.', 'red')) 663 | exit() 664 | 665 | # Set .todo file name in case we need later 666 | # If the existing filename already has ".YYYYMMDD_HHMMSS.todo" at the end, then remove it before creating the new name 667 | originalFileName = args.input 668 | pattern = r'\.\d{8}_\d{6}\.todo$' 669 | if re.search(pattern, originalFileName): 670 | originalFileName = re.sub(pattern, '', originalFileName) 671 | todoFileName = originalFileName+'.'+datetime.now().strftime("%Y%m%d_%H%M%S")+'.todo' 672 | 673 | # If the -i (--input) can be a standard file (text file with URLs per line), 674 | # if the value passed is not a valid file, then assume it is an individual URL 675 | urlPassed = False 676 | try: 677 | inputFile = open(args.input, 'r') 678 | firstLine = inputFile.readline() 679 | except IOError: 680 | urlPassed = True 681 | except Exception as e: 682 | print(colored('ERROR processInput 2: ' + str(e), 'red')) 683 | 684 | if verbose(): 685 | showOptions() 686 | 687 | inputArg = args.input 688 | if urlPassed: 689 | print(colored('Calling KNOXSS API...\n', 'cyan')) 690 | # Check if input has scheme. If not, then add https:// 691 | if '://' not in inputArg: 692 | print(colored('WARNING: Input "'+inputArg+'" should include a scheme. Using https by default...', 'yellow')) 693 | inputArg = 'https://'+inputArg 694 | # Check for non default values of parameters 695 | checkForAlteredParams(inputArg) 696 | 697 | processUrl(inputArg) 698 | else: # It's a file of URLs 699 | try: 700 | # Open file and put all values in input set, but in random order 701 | with open(inputArg, 'r') as inputFile: 702 | lines = [line.strip() for line in inputFile if line.strip() != ''] 703 | 704 | # Randomize the order before adding to the set. This is to help "fly under the radar" of WAFs on the target systems 705 | random.shuffle(lines) 706 | inputValues = set(lines) 707 | 708 | print(colored('Calling KNOXSS API for '+str(len(inputValues))+' targets (with '+str(args.processes)+' processes/threads)...\n', 'cyan')) 709 | if not stopProgram: 710 | with ThreadPoolExecutor(max_workers=args.processes) as executor: 711 | executor.map(processUrl, inputValues) 712 | 713 | except Exception as e: 714 | print(colored('ERROR processInput 3: ' + str(e), 'red')) 715 | 716 | except Exception as e: 717 | print(colored('ERROR processInput 1: ' + str(e), 'red')) 718 | 719 | def discordNotify(target, poc, vulnType): 720 | global DISCORD_WEBHOOK, HTTP_ADAPTER_DISCORD 721 | try: 722 | embed = { 723 | "description": "```\n" + poc + "\n```", 724 | "title": "KNOXSS POC for " + target, 725 | "color": 42320, 726 | "fields": [ 727 | { 728 | "name": "", 729 | "value": "[☕ Buy me a coffee, Thanks!](https://ko-fi.com/xnlh4ck3r)", 730 | "inline": False 731 | } 732 | ] 733 | } 734 | data = { 735 | "content": vulnType + " found by knoxnl! 🤘", 736 | "username": "knoxnl", 737 | "embeds": [embed], 738 | "avatar_url": "https://avatars.githubusercontent.com/u/84544946" 739 | } 740 | 741 | session = requests.Session() 742 | session.mount('https://', HTTP_ADAPTER_DISCORD) 743 | 744 | max_attempts = 5 745 | attempt = 0 746 | 747 | while attempt < max_attempts: 748 | attempt += 1 749 | result = session.post(DISCORD_WEBHOOK, json=data) 750 | 751 | if result.status_code == 429: 752 | try: 753 | retry_after = result.json().get("retry_after", 1) 754 | time.sleep(retry_after) 755 | continue 756 | except Exception as e: 757 | time.sleep(1) 758 | continue 759 | elif result.status_code >= 300: 760 | tprint(colored('ERROR: Failed to send notification to Discord - ' + result.text, 'yellow')) 761 | break 762 | 763 | break 764 | 765 | # If we used all attempts and it never broke out early, print final warning: 766 | if attempt == max_attempts and (result.status_code < 200 or result.status_code >= 300): 767 | tprint(colored('ERROR: Failed to send notification to Discord after '+str(max_attempts)+' attempts.', 'red')) 768 | 769 | except Exception as e: 770 | tprint(colored('ERROR discordNotify: ' + str(e), 'red')) 771 | 772 | def discordNotifyComplete(input, description, incomplete): 773 | global DISCORD_WEBHOOK_COMPLETE, HTTP_ADAPTER_DISCORD 774 | try: 775 | if incomplete: 776 | title = "INCOMPLETE FOR FILE `"+input+"`" 777 | embed_color = 16711722 # Red 778 | else: 779 | title = "COMPLETE FOR FILE `"+input+"`" 780 | embed_color = 42320 # Green 781 | 782 | embed = { 783 | "description": description, 784 | "title": title, 785 | "color": embed_color, 786 | "fields": [ 787 | { 788 | "name": "", 789 | "value": "[☕ Buy me a coffee, Thanks!](https://ko-fi.com/xnlh4ck3r)", 790 | "inline": False 791 | } 792 | ] 793 | } 794 | 795 | data = { 796 | "content": "knoxnl finished!", 797 | "username": "knoxnl", 798 | "avatar_url": "https://avatars.githubusercontent.com/u/84544946", 799 | "embeds": [embed], 800 | } 801 | 802 | session = requests.Session() 803 | session.mount('https://', HTTP_ADAPTER_DISCORD) 804 | 805 | max_attempts = 5 806 | attempt = 0 807 | 808 | while attempt < max_attempts: 809 | attempt += 1 810 | result = session.post(DISCORD_WEBHOOK_COMPLETE, json=data) 811 | 812 | if result.status_code == 429: 813 | try: 814 | retry_after = result.json().get("retry_after", 1) 815 | time.sleep(retry_after) 816 | continue 817 | except Exception as e: 818 | time.sleep(1) 819 | continue 820 | elif result.status_code >= 300: 821 | tprint(colored('ERROR: Failed to send Complete notification to Discord - ' + result.text, 'yellow')) 822 | break 823 | 824 | break 825 | 826 | # If we used all attempts and it never broke out early, print final warning: 827 | if attempt == max_attempts and (result.status_code < 200 or result.status_code >= 300): 828 | tprint(colored('ERROR: Failed to send Complete notification to Discord after '+str(max_attempts)+' attempts.', 'red')) 829 | 830 | except Exception as e: 831 | tprint(colored('ERROR discordNotifyComplete 1: ' + str(e), 'red')) 832 | 833 | def getAPILimitReset(): 834 | global apiResetPath, timeAPIReset 835 | try: 836 | # If the .apireset file exists then get the API reset time 837 | if os.path.exists(apiResetPath): 838 | # Read the timestamp from the file 839 | with open(apiResetPath, 'r') as file: 840 | timeAPIReset = datetime.strptime(file.read().strip(), '%Y-%m-%d %H:%M') 841 | 842 | # If the timestamp is more than 24 hours ago, then delete the file and set the timeAPIReset back to None 843 | if timeAPIReset is not None and (datetime.now() - timeAPIReset) > timedelta(hours=24): 844 | timeAPIReset = None 845 | # If the .apireset file already exists then delete it 846 | if os.path.exists(apiResetPath): 847 | os.remove(apiResetPath) 848 | 849 | except Exception as e: 850 | tprint(colored('ERROR getAPILimitReset 1: ' + str(e), 'red')) 851 | 852 | def setAPILimitReset(timestamp): 853 | global apiResetPath, latestApiCalls, timeAPIReset 854 | try: 855 | # Convert the timestamp to the local timezone and add 24 hours and 5 minutes 856 | timestamp = datetime.strptime(timestamp, '%a, %d %b %Y %H:%M:%S %z') 857 | localTimezone = tz.tzlocal() 858 | localTimestamp = timestamp.astimezone(localTimezone) 859 | timeAPIReset = localTimestamp + timedelta(hours=24, minutes=5) 860 | 861 | # If the .apireset file already exists then delete it 862 | if os.path.exists(apiResetPath): 863 | os.remove(apiResetPath) 864 | 865 | # Write the new API limit reset time to the .apireset file, and set the global variable 866 | with open(apiResetPath, 'w') as file: 867 | file.write(timeAPIReset.strftime('%Y-%m-%d %H:%M')) 868 | 869 | except Exception as e: 870 | tprint(colored('ERROR setAPILimitReset 1: ' + str(e), 'red')) 871 | 872 | def processOutput(target, method, knoxssResponse): 873 | global latestApiCalls, successCountXSS, successCountOR, outFile, debugOutFile, debugFileIsOpen, currentCount, rateLimitExceeded, urlPassed, needToStop, dontDisplay, blockedDomains, needToRetry, forbiddenResponseCount, errorCount, safeCount, requestCount, skipCount, runtimeLog, stopProgram 874 | try: 875 | if stopProgram: return 876 | knoxssResponseError = knoxssResponse.Error 877 | 878 | if knoxssResponse.Calls == 'Unknown' and all(s not in knoxssResponseError.lower() for s in ["content type", "can't test it (forbidden)"]): 879 | if not args.success_only: 880 | 881 | # If there is a 403, it maybe because the users IP is blocked on the KNOXSS firewall 882 | if knoxssResponse.Code == "403": 883 | knoxssResponseError = '403 Forbidden - Check http://knoxss.pro manually and if you are blocked, contact Twitter/X @KN0X55 or brutelogic@null.net' 884 | needToStop = True 885 | # If there is "InvalidChunkLength" in the error returned, it means the KNOXSS API returned an empty response 886 | elif 'InvalidChunkLength' in knoxssResponseError: 887 | knoxssResponseError = 'The API Timed Out' 888 | if args.retries > 0: 889 | needToRetry = True 890 | 891 | # If method is POST, remove the query string from the target and show the post data in [ ] 892 | if method == 'POST': 893 | try: 894 | querystring = target.split('?')[1] 895 | except: 896 | querystring = '' 897 | target = target.split('?')[0] 898 | if args.post_data: 899 | target = target + ' ['+args.post_data+']' 900 | else: 901 | if querystring != '': 902 | target = target + ' [' + querystring + ']' 903 | 904 | if not dontDisplay: 905 | xssText = '[ ERR! ] - (' + method + ') ' + target + ' KNOXSS ERR: ' + knoxssResponseError 906 | errorCount = errorCount + 1 907 | if urlPassed: 908 | tprint(colored(xssText, 'red')) 909 | else: 910 | tprint(colored(xssText, 'red'), colored('['+latestApiCalls+']','white')) 911 | if args.output_all and fileIsOpen: 912 | outFile.write(xssText + '\n') 913 | else: 914 | # If it is a new reset time then replace the .apireset file 915 | if knoxssResponse.Timestamp != '' and latestApiCalls.startswith('1/'): 916 | setAPILimitReset(knoxssResponse.Timestamp) 917 | 918 | # If the error has "got another 403" it means the a 403 was returned by the target 919 | if "got another 403" in knoxssResponseError.lower(): 920 | knoxssResponseError = 'Target returned a "403 Forbidden". There could be WAF in place.' 921 | # If requested to skip blocked domains after a limit, then save them 922 | if args.skip_blocked > 0: 923 | try: 924 | parsedTarget = urlparse(target) 925 | domain = '(' + method + ') ' + str(parsedTarget.scheme + '://' + parsedTarget.netloc) 926 | pauseEvent.set() 927 | blockedDomains[domain] = blockedDomains.get(domain, 0) + 1 928 | pauseEvent.clear() 929 | except: 930 | pass 931 | 932 | # If it is the generic error "KNOXSS engine is failing at some point" then we will not display that because it will be reported as NONE 933 | if 'failing at some point' in knoxssResponseError: 934 | knoxssResponseError = 'none' 935 | 936 | # If for any reason neither of the XSS and Redir flags are "true" (not intended) then assume it the PoC is XSS 937 | if knoxssResponse.PoC != 'none' and knoxssResponse.XSS != 'true' and knoxssResponse.Redir != 'true': 938 | knoxssResponse.XSS = 'true' 939 | 940 | # If no PoC then report as [ NONE ] 941 | if knoxssResponse.PoC == 'none': 942 | if not args.success_only: 943 | xssText = '[ NONE ] - (' + method + ') ' + target 944 | if knoxssResponseError != 'none': 945 | errorText = ' KNOXSS ERR: ' + knoxssResponseError 946 | else: 947 | errorText = '' 948 | safeCount = safeCount + 1 949 | if urlPassed: 950 | tprint(colored(xssText, 'yellow'), colored(errorText,'red')) 951 | else: 952 | tprint(colored(xssText, 'yellow'), colored(errorText,'red'), colored('['+latestApiCalls+']','white')) 953 | if args.output_all and fileIsOpen: 954 | outFile.write(xssText + '\n') 955 | else: 956 | if knoxssResponse.XSS == 'true': 957 | xssText = '[ XSS! ] - (' + method + ') ' + knoxssResponse.PoC 958 | if urlPassed: 959 | tprint(colored(xssText, 'green')) 960 | else: 961 | tprint(colored(xssText, 'green'), colored('['+latestApiCalls+']','white')) 962 | successCountXSS = successCountXSS + 1 963 | 964 | # If there was an Open Redirect too, then increment that count 965 | if knoxssResponse.Redir == 'true': 966 | vulnType = 'XSS (and OR)' 967 | else: 968 | vulnType = 'XSS' 969 | 970 | # Send a notification to discord if a webhook was provided 971 | if DISCORD_WEBHOOK != '': 972 | discordNotify(target,knoxssResponse.PoC,vulnType) 973 | 974 | # Write the successful XSS details to file 975 | if fileIsOpen: 976 | outFile.write(xssText + '\n') 977 | 978 | elif knoxssResponse.Redir == 'true': 979 | orText = '[ OR ! ] - (' + method + ') ' + knoxssResponse.PoC 980 | if urlPassed: 981 | tprint(colored(orText, 'green')) 982 | else: 983 | tprint(colored(orText, 'green'), colored('['+latestApiCalls+']','white')) 984 | successCountOR = successCountOR + 1 985 | 986 | # Send a notification to discord if a webhook was provided 987 | if DISCORD_WEBHOOK != '': 988 | discordNotify(target,knoxssResponse.PoC, 'Open Redirect') 989 | 990 | # Write the successful OR details to file 991 | if fileIsOpen: 992 | outFile.write(orText + '\n') 993 | 994 | # This shouldn't happen, but just in case, output an error if verbose was chosen 995 | else: 996 | if verbose() or debugFileIsOpen: 997 | tprint(colored('ERROR: There was a PoC, but didn\'t seem to be an XSS or OR. Check the response for details:', 'red')) 998 | tprint(knoxssResponse) 999 | 1000 | except Exception as e: 1001 | tprint(colored('ERROR showOutput 1: ' + str(e), 'red')) 1002 | 1003 | # Process one URL 1004 | def processUrl(target): 1005 | 1006 | global stopProgram, latestApiCalls, urlPassed, needToStop, needToRetry, retryAttempt, rateLimitExceeded, timeAPIReset, skipCount, apiResetPath, lastRetryResetTime 1007 | try: 1008 | # If the event is set, pause for a while until its unset again 1009 | while pauseEvent.is_set() and not stopProgram and not needToStop: 1010 | time.sleep(1) 1011 | 1012 | if not stopProgram and not needToStop: 1013 | 1014 | # Reset retryAttempt every 24 hours 1015 | if (datetime.now() - lastRetryResetTime).total_seconds() >= 86400: 1016 | retryAttempt = 0 1017 | lastRetryResetTime = datetime.now() 1018 | 1019 | # If the API Limit was exceeded, and we want to wait until the limit is reset pause all processes until that time 1020 | if rateLimitExceeded and timeAPIReset is not None and args.pause_until_reset: 1021 | # Set the event to pause all processes 1022 | pauseEvent.set() 1023 | tprint(colored(f'WAITING UNTIL {str(timeAPIReset.strftime("%Y-%m-%d %H:%M"))} WHEN THEN API LIMIT HAS BEEN RESET...','yellow')) 1024 | time_difference = (timeAPIReset - datetime.now()).total_seconds() 1025 | timeAPIReset = None 1026 | os.remove(apiResetPath) 1027 | time.sleep(time_difference) 1028 | tprint(colored('API LIMIT HAS BEEN RESET. RESUMING...','yellow')) 1029 | # Reset the event for to unpause all processes 1030 | pauseEvent.clear() 1031 | 1032 | # If we need to try again because of an KNOXSS error, then delay 1033 | if needToRetry and args.retries > 0: 1034 | if retryAttempt < args.retries: 1035 | # Set the event to pause all processes 1036 | pauseEvent.set() 1037 | needToRetry = False 1038 | if retryAttempt == 0: 1039 | delay = args.retry_interval * 1.0 1040 | else: 1041 | delay = args.retry_interval * (retryAttempt * args.retry_backoff) 1042 | if retryAttempt == args.retries: 1043 | tprint(colored('WARNING: There are issues with KNOXSS API. Sleeping for ' + str(delay) + ' seconds before trying again. Last retry.', 'yellow')) 1044 | else: 1045 | tprint(colored('WARNING: There are issues with KNOXSS API. Sleeping for ' + str(delay) + ' seconds before trying again.', 'yellow')) 1046 | retryAttempt += 1 1047 | time.sleep(delay) 1048 | # Reset the event for the next iteration 1049 | pauseEvent.clear() 1050 | else: 1051 | needToStop = True 1052 | 1053 | target = target.strip() 1054 | # Check if target has scheme. If not, then add https:// 1055 | if '://' not in target: 1056 | tprint(colored('WARNING: Input "'+target+'" should include a scheme. Using https by default...', 'yellow')) 1057 | target = 'https://'+target 1058 | 1059 | # If the domain has already been flagged as blocked, then skip it and remove from the input values so not written to the .todo file 1060 | parsedTarget = urlparse(target) 1061 | 1062 | headers = args.headers.strip() 1063 | knoxssResponse=knoxss() 1064 | 1065 | if args.http_method in ('GET','BOTH'): 1066 | method = 'GET' 1067 | domain = '(' + method + ') ' + str(parsedTarget.scheme + '://' + parsedTarget.netloc) 1068 | 1069 | # If skipping blocked domains was requested, check if the domain is in blockedDomains, if not, add it with count 0 1070 | if args.skip_blocked > 0: 1071 | while pauseEvent.is_set(): 1072 | time.sleep(1) 1073 | pauseEvent.set() 1074 | if domain not in blockedDomains: 1075 | blockedDomains[domain] = 0 1076 | pauseEvent.clear() 1077 | 1078 | # If skipping blocked domains was requested and the domain has been blocked more than the requested number of times, then skip, otherwise process 1079 | if args.skip_blocked > 0 and blockedDomains[domain] > args.skip_blocked-1: 1080 | tprint(colored('[ SKIP ] - ' + domain + ' has already been flagged as blocked, so skipping ' + target, 'yellow', attrs=['dark'])) 1081 | skipCount = skipCount + 1 1082 | inputValues.discard(target) 1083 | else: 1084 | knoxssApi(target, headers, method, knoxssResponse) 1085 | processOutput(target, method, knoxssResponse) 1086 | 1087 | if args.http_method in ('POST','BOTH'): 1088 | method = 'POST' 1089 | domain = '(' + method + ') ' + str(parsedTarget.scheme + '://' + parsedTarget.netloc) 1090 | 1091 | # If skipping blocked domains was requested, check if the domain is in blockedDomains, if not, add it with count 0 1092 | if args.skip_blocked > 0: 1093 | while pauseEvent.is_set(): 1094 | time.sleep(1) 1095 | pauseEvent.set() 1096 | if domain not in blockedDomains: 1097 | blockedDomains[domain] = 0 1098 | pauseEvent.clear() 1099 | 1100 | # If skipping blocked domains was requested and the domain has been blocked more than the requested number of times, then skip, otherwise process 1101 | if args.skip_blocked > 0 and blockedDomains[domain] > args.skip_blocked-1: 1102 | tprint(colored('[ SKIP ] - ' + domain + ' has already been flagged as blocked, so skipping ' + target, 'yellow', attrs=['dark'])) 1103 | skipCount = skipCount + 1 1104 | inputValues.discard(target) 1105 | else: 1106 | knoxssApi(target, headers, method, knoxssResponse) 1107 | processOutput(target, method, knoxssResponse) 1108 | 1109 | except Exception as e: 1110 | pauseEvent.clear() 1111 | tprint(colored('ERROR processUrl 1: ' + str(e), 'red')) 1112 | 1113 | # Validate the -p argument 1114 | def processes_type(x): 1115 | x = int(x) 1116 | if x < 1 or x > 5: 1117 | raise argparse.ArgumentTypeError('The number of processes must be between 1 and 5') 1118 | return x 1119 | 1120 | def updateProgram(): 1121 | try: 1122 | # Execute pip install --upgrade knoxnl 1123 | subprocess.run(['pip', 'install', '--upgrade', 'knoxnl'], check=True) 1124 | print(colored(f'knoxnl successfully updated {__version__} -> {latestVersion} (latest) 🤘', 'green')) 1125 | except subprocess.CalledProcessError as e: 1126 | print(colored(f'Unable to update knoxnl to version {latestVersion}: {str(e)}', 'red')) 1127 | 1128 | def argcheckStallTimeout(value): 1129 | ivalue = int(value) 1130 | if ivalue < 60: 1131 | raise argparse.ArgumentTypeError("stall-timeout must be at least 60 seconds.") 1132 | return ivalue 1133 | 1134 | # Run knoXnl 1135 | def main(): 1136 | global args, latestApiCalls, urlPassed, successCountXSS, successCountOR, fileIsOpen, outFile, debugOutFile, debugFileIsOpen, needToStop, todoFileName, blockedDomains, latestVersion, safeCount, errorCount, requestCount, skipCount, DISCORD_WEBHOOK_COMPLETE 1137 | 1138 | # Tell Python to run the handler() function when SIGINT is received 1139 | signal(SIGINT, handler) 1140 | 1141 | # Parse command line arguments 1142 | parser = argparse.ArgumentParser( 1143 | description='knoXnl - by @Xnl-h4ck3r: A wrapper around the KNOXSS API by Brute Logic (requires an API key)' 1144 | ) 1145 | parser.add_argument( 1146 | '-i', 1147 | '--input', 1148 | action='store', 1149 | help='Input to send to KNOXSS API: a single URL, or file of URLs.', 1150 | ) 1151 | parser.add_argument( 1152 | '-o', 1153 | '--output', 1154 | action='store', 1155 | help='The file to save the successful XSS and payloads to. If the file already exist it will just be appended to unless option -ow is passed.', 1156 | default='', 1157 | ) 1158 | parser.add_argument( 1159 | '-ow', 1160 | '--output-overwrite', 1161 | action='store_true', 1162 | help='If the output file already exists, it will be overwritten instead of being appended to.', 1163 | ) 1164 | parser.add_argument( 1165 | '-oa', 1166 | '--output-all', 1167 | action='store_true', 1168 | help='Output all results to file, not just successful one\'s.', 1169 | ) 1170 | parser.add_argument( 1171 | '-X', 1172 | '--http-method', 1173 | action='store', 1174 | help='Which HTTP method to use, values GET, POST or BOTH (default: GET). If BOTH is chosen, then a GET call will be made, followed by a POST.', 1175 | default='GET', 1176 | choices=['GET','POST','BOTH'] 1177 | ) 1178 | parser.add_argument( 1179 | '-pd', 1180 | '--post-data', 1181 | help='If a POST request is made, this is the POST data passed. It must be in the format \'param1=value¶m2=value¶m3=value\'. If this isn\'t passed and query string parameters are used, then these will be used as POST data if POST Method is requested.', 1182 | action='store', 1183 | default='', 1184 | ) 1185 | parser.add_argument( 1186 | '-H', 1187 | '--headers', 1188 | help='Add custom headers to pass with HTTP requests. Pass in the format \'Header1:value1|Header2:value2\' (e.g. separate different headers with a pipe | character).', 1189 | action='store', 1190 | default='', 1191 | ) 1192 | parser.add_argument( 1193 | '-A', 1194 | '--api-key', 1195 | help='The KNOXSS API Key to use. This will be used instead of the value in config.yml', 1196 | action='store', 1197 | default='', 1198 | ) 1199 | parser.add_argument( 1200 | '-s', 1201 | '--success-only', 1202 | action='store_true', 1203 | help='Only show successful XSS payloads in the CLI output.', 1204 | ) 1205 | parser.add_argument( 1206 | '-p', 1207 | '--processes', 1208 | help='Basic multithreading is done when getting requests for a file of URLs. This argument determines the number of processes/threads used (default: 3). ', 1209 | action='store', 1210 | type=processes_type, 1211 | default=3, 1212 | metavar="", 1213 | ) 1214 | parser.add_argument( 1215 | '-t', 1216 | '--timeout', 1217 | help='How many seconds to wait for the KNOXSS API to respond before giving up (default: '+str(DEFAULT_TIMEOUT)+' seconds). If set to 0, then timeout will be used.', 1218 | default=DEFAULT_TIMEOUT, 1219 | type=int, 1220 | metavar="", 1221 | ) 1222 | parser.add_argument( 1223 | '-st', 1224 | '--stall-timeout', 1225 | help='How many seconds to wait for the KNOXSS API scan to take between steps before aborting (default: '+str(DEFAULT_STALL_TIMEOUT)+' seconds). If set to 0, then timeout will be used.', 1226 | default=DEFAULT_STALL_TIMEOUT, 1227 | type=argcheckStallTimeout, 1228 | metavar="", 1229 | ) 1230 | parser.add_argument( 1231 | '-bp', 1232 | '--burp-piper', 1233 | action='store_true', 1234 | help='Set if called from the Burp Piper extension.', 1235 | ) 1236 | parser.add_argument( 1237 | '-dw', 1238 | '--discord-webhook', 1239 | help='The Discord Webhook to send successful XSS notifications to. This will be used instead of the value in config.yml', 1240 | action='store', 1241 | default='', 1242 | ) 1243 | parser.add_argument( 1244 | '-dwc', 1245 | '--discord-webhook-complete', 1246 | help='The Discord Webhook to send completion notifications to when a file has been used as input (whether finished completely or stopped in error). This will be used instead of the value in config.yml.', 1247 | action='store', 1248 | default='', 1249 | ) 1250 | parser.add_argument( 1251 | '-r', 1252 | '--retries', 1253 | help='The number of times to retry when having issues connecting to the KNOXSS API (default: '+str(DEFAULT_RETRIES)+'). If set to 0 then then it will not sleep or try to retry any URLs.', 1254 | default=DEFAULT_RETRIES, 1255 | type=int, 1256 | ) 1257 | parser.add_argument( 1258 | '-ri', 1259 | '--retry-interval', 1260 | help='How many seconds to wait before retrying when having issues connecting to the KNOXSS API (default: '+str(DEFAULT_RETRY_INTERVAL)+' seconds)', 1261 | default=DEFAULT_RETRY_INTERVAL, 1262 | type=int, 1263 | metavar="", 1264 | ) 1265 | parser.add_argument( 1266 | '-rb', 1267 | '--retry-backoff', 1268 | help='The backoff factor used when retrying when having issues connecting to the KNOXSS API (default: '+str(DEFAULT_RETRY_BACKOFF_FACTOR)+')', 1269 | default=DEFAULT_RETRY_BACKOFF_FACTOR, 1270 | type=float, 1271 | ) 1272 | parser.add_argument( 1273 | '-pur', 1274 | '--pause-until-reset', 1275 | action='store_true', 1276 | help='If the API Limit reset time is known and the API limit is reached, wait the required time until the limit is reset and continue again. The reset time is only known if knoxnl has run for request number 1 previously. The API rate limit is reset 24 hours after request 1.', 1277 | ) 1278 | parser.add_argument( 1279 | '-sb', 1280 | '--skip-blocked', 1281 | help='The number of 403 Forbidden responses from a target (for a given HTTP method + scheme + (sub)domain) before skipping. This is useful if you know the target has a WAF. The default is zero, which means no blocking is done.', 1282 | default=0, 1283 | type=int, 1284 | ) 1285 | parser.add_argument( 1286 | '-fn', 1287 | '--force-new', 1288 | action='store_true', 1289 | help='Forces KNOXSS to run a new scan instead of getting cached results. NOTE: Using this option will make checks SLOWER because it won\'t check the cache. Using the option all the time will defeat the purpose of the cache system.', 1290 | ) 1291 | parser.add_argument( 1292 | '-rl', 1293 | '--runtime-log', 1294 | action='store_true', 1295 | help='Provides a live runtime log of the KNOXSS scan that will be streamed. This will be prefixed with the number of the thread running, e.g. `[T2]` so the output can be tracked easier (if -p/--processes > 1).', 1296 | ) 1297 | parser.add_argument( 1298 | '-nt', 1299 | '--no-todo', 1300 | action='store_true', 1301 | help='Do not create the .todo file if the input file is not completed because of issues with API, connection, etc.', 1302 | ) 1303 | parser.add_argument( 1304 | '-up', 1305 | '--update', 1306 | action='store_true', 1307 | help='Update knoxnl to the latest version.', 1308 | ) 1309 | parser.add_argument( 1310 | '-do', 1311 | '--debug-output', 1312 | action='store', 1313 | help='The file to save all terminal output to a file, used for debugging. Using this option will also show the JSON response.', 1314 | default='', 1315 | ) 1316 | parser.add_argument('-v', '--verbose', action='store_true', help="Verbose output") 1317 | parser.add_argument('--version', action='store_true', help="Show version number") 1318 | args = parser.parse_args() 1319 | 1320 | # If --version was passed, display version and exit 1321 | if args.version: 1322 | print(colored('knoxnl - v' + __version__,'cyan')) 1323 | sys.exit() 1324 | 1325 | # Get the latest version 1326 | try: 1327 | resp = requests.get('https://raw.githubusercontent.com/xnl-h4ck3r/knoxnl/main/knoxnl/__init__.py',timeout=3) 1328 | latestVersion = resp.text.split('=')[1].replace('"','') 1329 | except: 1330 | pass 1331 | 1332 | showBanner() 1333 | 1334 | # If --update was passed, update to the latest version 1335 | if args.update: 1336 | try: 1337 | if latestVersion == '': 1338 | print(colored('Unable to check the latest version. Check your internet connection.', 'red')) 1339 | elif __version__ != latestVersion: 1340 | updateProgram() 1341 | else: 1342 | print(colored('You are already running the latest version of knoxnl.Thanks for using!', 'green')) 1343 | print() 1344 | print(colored('✅ Want to buy me a coffee? ☕ https://ko-fi.com/xnlh4ck3r 🤘', 'green')) 1345 | sys.exit() 1346 | except Exception as e: 1347 | print(colored(f'ERROR: Unable to update - {str(e)}','red')) 1348 | 1349 | # If no input was given, raise an error 1350 | if sys.stdin.isatty(): 1351 | if args.input is None: 1352 | print(colored('You need to provide an input with -i argument or through .', 'red')) 1353 | sys.exit() 1354 | 1355 | # Get the config settings from the config.yml file 1356 | getConfig() 1357 | 1358 | # Get the API reset time from the .apireset file 1359 | getAPILimitReset() 1360 | 1361 | # If -o (--output) argument was passed then open the output file 1362 | if args.output != "": 1363 | try: 1364 | # If the filename has any "/" in it, remove the contents after the last one to just get the path and create the directories if necessary 1365 | try: 1366 | output_path = os.path.abspath(os.path.expanduser(args.output)) 1367 | output_dir = os.path.dirname(output_path) 1368 | if not os.path.exists(output_dir): 1369 | os.makedirs(output_dir) 1370 | except Exception as e: 1371 | pass 1372 | # If argument -ow was passed and the file exists, overwrite it, otherwise append to it 1373 | if args.output_overwrite: 1374 | outFile = open(os.path.expanduser(args.output), "w") 1375 | else: 1376 | outFile = open(os.path.expanduser(args.output), "a") 1377 | fileIsOpen = True 1378 | except Exception as e: 1379 | print(colored('WARNING: Output won\'t be saved to file - ' + str(e) + '\n', 'red')) 1380 | 1381 | # If -do (--debug-output) argument was passed then open the debug output file 1382 | if args.debug_output != "": 1383 | try: 1384 | # If the filename has any "/" in it, remove the contents after the last one to just get the path and create the directories if necessary 1385 | try: 1386 | output_path = os.path.abspath(os.path.expanduser(args.debug_output)) 1387 | output_dir = os.path.dirname(output_path) 1388 | if not os.path.exists(output_dir): 1389 | os.makedirs(output_dir) 1390 | except Exception as e: 1391 | pass 1392 | # Append to the debug file if it exists already 1393 | debugOutFile = open(os.path.expanduser(args.debug_output), "a") 1394 | debugFileIsOpen = True 1395 | except Exception as e: 1396 | print(colored('WARNING: Debug output won\'t be saved to file - ' + str(e) + '\n', 'red')) 1397 | 1398 | try: 1399 | 1400 | processInput() 1401 | 1402 | completeDescription = "" 1403 | 1404 | # Show the user the latest API quota 1405 | if latestApiCalls is None or latestApiCalls == '': 1406 | latestApiCalls = 'Unknown' 1407 | if timeAPIReset is not None: 1408 | message = '\nAPI calls made so far today - ' + latestApiCalls + ' (API Limit Reset Time: ' +str(timeAPIReset.strftime("%Y-%m-%d %H:%M")) + ')\n' 1409 | print(colored(message, 'cyan')) 1410 | completeDescription = message 1411 | else: 1412 | message = '\nAPI calls made so far today - ' + latestApiCalls + '\n' 1413 | print(colored(message, 'cyan')) 1414 | completeDescription = message 1415 | 1416 | # If a file was passed, there is a reason to stop, write the .todo file and let the user know about it (unless -nt/--no-todo was passed) 1417 | if needToStop and not urlPassed and not args.burp_piper and not args.no_todo: 1418 | try: 1419 | with open(todoFileName, 'w') as file: 1420 | for inp in inputValues: 1421 | file.write(inp+'\n') 1422 | print(colored('Had to stop due to errors. All unchecked URLs have been written to','cyan'),colored(todoFileName+'\n', 'white')) 1423 | completeDescription = completeDescription + 'Had to stop due to errors. All unchecked URLs have been written to `'+todoFileName+'`\n' 1424 | except Exception as e: 1425 | message = 'Was unable to write .todo file: '+str(e)+'\n' 1426 | print(colored(message,'red')) 1427 | completeDescription = completeDescription + message 1428 | 1429 | if args.skip_blocked > 0: 1430 | showBlocked() 1431 | 1432 | # Report number of None, Error and Skipped results 1433 | if args.skip_blocked > 0: 1434 | message = f'Requests made to KNOXSS API: {str(requestCount)} (XSS!: {str(successCountXSS)}, OR!: {str(successCountOR)}, NONE: {str(safeCount)}, ERR!: {str(errorCount)}, SKIP: {str(skipCount)})' 1435 | print(colored(message,'cyan')) 1436 | else: 1437 | message = f'Requests made to KNOXSS API: {str(requestCount)} (XSS!: {str(successCountXSS)}, OR!: {str(successCountOR)}, NONE: {str(safeCount)}, ERR!: {str(errorCount)})' 1438 | print(colored(message,'cyan')) 1439 | completeDescription = completeDescription + message + '\n' 1440 | 1441 | # Report if any successful XSS or Open Redirect was found this time. 1442 | # If the console can't display 🤘 then an error will be raised to try without 1443 | try: 1444 | message = "" 1445 | if successCountOR == 1: 1446 | openRedirectTerm = 'Open Redirect' 1447 | else: 1448 | openRedirectTerm = 'Open Redirects' 1449 | if successCountXSS > 0 and successCountOR > 0: 1450 | message = '🤘 '+str(successCountXSS)+' successful XSS found and '+str(successCountOR)+' successful '+openRedirectTerm+' found! 🤘\n' 1451 | print(colored(message,'green')) 1452 | else: 1453 | if successCountXSS > 0: 1454 | message = '🤘 '+str(successCountXSS)+' successful XSS found! 🤘\n' 1455 | print(colored(message,'green')) 1456 | elif successCountOR > 0: 1457 | message = '🤘 No successful XSS, but '+str(successCountOR)+' successful '+openRedirectTerm+' found! 🤘\n' 1458 | print(colored(message,'green')) 1459 | else: 1460 | message = 'No successful XSS or '+openRedirectTerm+' found... better luck next time! 🤘\n' 1461 | print(colored(message,'cyan')) 1462 | except: 1463 | print(colored('ERROR: ' + str(e), 'red')) 1464 | completeDescription = completeDescription + message 1465 | 1466 | # If the output was sent to a file, close the file 1467 | if fileIsOpen: 1468 | try: 1469 | fileIsOpen = False 1470 | outFile.close() 1471 | except Exception as e: 1472 | print(colored("ERROR: Unable to close output file: " + str(e), "red")) 1473 | 1474 | # If the debug output was sent to a file, close the file 1475 | if debugFileIsOpen: 1476 | try: 1477 | debugFileIsOpen = False 1478 | debugOutFile.close() 1479 | except Exception as e: 1480 | print(colored("ERROR: Unable to close debug output file: " + str(e), "red")) 1481 | 1482 | # If a file was passed as input and the Discord webhook for Completion was given, then send an appropriate notification 1483 | if not urlPassed and not args.burp_piper and DISCORD_WEBHOOK_COMPLETE != '': 1484 | discordNotifyComplete(args.input,completeDescription,needToStop) 1485 | 1486 | except Exception as e: 1487 | print(colored('ERROR main 1: ' + str(e), 'red')) 1488 | 1489 | if __name__ == '__main__': 1490 | main() 1491 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import shutil 4 | from setuptools import setup, find_packages 5 | 6 | # Define the target directory for the config.yml file 7 | # target_directory = os.path.join(os.path.expanduser("~"), ".config", "urless") if os.path.expanduser("~") else None 8 | 9 | target_directory = ( 10 | os.path.join(os.getenv('APPDATA', ''), 'knoxnl') if os.name == 'nt' 11 | else os.path.join(os.path.expanduser("~"), ".config", "knoxnl") if os.name == 'posix' 12 | else os.path.join(os.path.expanduser("~"), "Library", "Application Support", "knoxnl") if os.name == 'darwin' 13 | else None 14 | ) 15 | 16 | # Copy the config.yml file to the target directory if it exists 17 | configNew = False 18 | if target_directory and os.path.isfile("config.yml"): 19 | os.makedirs(target_directory, exist_ok=True) 20 | # If file already exists, create a new one 21 | if os.path.isfile(target_directory+'/config.yml'): 22 | configNew = True 23 | os.rename(target_directory+'/config.yml',target_directory+'/config.yml.OLD') 24 | shutil.copy("config.yml", target_directory) 25 | os.rename(target_directory+'/config.yml',target_directory+'/config.yml.NEW') 26 | os.rename(target_directory+'/config.yml.OLD',target_directory+'/config.yml') 27 | else: 28 | shutil.copy("config.yml", target_directory) 29 | 30 | setup( 31 | name="knoxnl", 32 | packages=find_packages(), 33 | version=__import__('knoxnl').__version__, 34 | description="A python wrapper around the amazing KNOXSS API by Brute Logic (requires an API Key)", 35 | long_description=open("README.md").read(), 36 | long_description_content_type='text/markdown', 37 | author="@xnl-h4ck3r", 38 | url="https://github.com/xnl-h4ck3r/knoxnl", 39 | py_modules=["knoxnl"], 40 | zip_safe=False, 41 | install_requires=["requests","termcolor","pyaml","urlparse3","python-dateutil"], 42 | entry_points={ 43 | 'console_scripts': [ 44 | 'knoxnl = knoxnl.knoxnl:main', 45 | ], 46 | }, 47 | ) 48 | 49 | if configNew: 50 | print('\n\033[33mIMPORTANT: The file '+target_directory+'/config.yml already exists.\nCreating config.yml.NEW but leaving existing config.\nIf you need the new file, then remove the current one and rename config.yml.NEW to config.yml\n\033[0m') 51 | else: 52 | print('\n\033[92mThe file '+target_directory+'/config.yml has been created.\n\033[0m') --------------------------------------------------------------------------------