Compare commits

..
38 Commits
Author SHA1 Message Date
Tony West 5bbf0453a5 Fixed missing preview response for JSON output when the verbose flag is applied 2024-08-21 19:16:15 -04:00
Tony West a39028b463 Fixed bug in handling variations of schema references 2024-08-19 15:58:51 -04:00
Tony West e22c11c118 Added verbose output option for the automate command 2024-08-14 23:03:13 -04:00
Tony West 528a771c87 Fixed improper handling of defined XML content 2024-08-12 18:21:17 -04:00
Tony West c85512dfe0 Improved handling of nested schema references 2024-08-07 22:20:15 -04:00
Tony West 6a01afb367 Fixed bug that occurred when referencing non-existent schema values 2024-08-02 10:57:41 -04:00
Tony West 7ac01dc2a1 Fixed issue with fatal errors when encountering nested references 2024-08-01 18:29:49 -04:00
Tony West 9b48aab4fb Added warning when localhost (and equivalent) values are defined. 2024-07-30 18:47:59 -04:00
Tony West 8ce3ca7aa5 Fixed handling of x509 errors 2024-07-30 16:08:26 -04:00
Tony West 62be170a97 Update issue templates 2024-07-30 10:06:38 -04:00
Tony West a290716c76 Update README.md 2024-07-29 17:36:04 -04:00
Tony West 927b2adf53 Refactored schema reference handling code 2024-07-29 17:11:02 -04:00
Tony West cecaccc07a Added support for basic parsing of schema references 2024-07-29 13:28:24 -04:00
Tony West 54413d32e0 Updated README 2024-07-29 13:27:11 -04:00
Tony West c399ca1ff8 Fixed bug where parameter values did not populate correctly 2024-07-23 18:33:42 -04:00
Tony West 45dd989ef3 Fixed breaking change in 1.5.1 2024-07-23 12:11:54 -04:00
Tony West 1474d660e4 Fixed improper handling of empty server object 2024-07-19 14:07:23 -04:00
Tony West fe597147e6 Fixed improper parsing of the host object 2024-07-19 01:03:19 -04:00
Tony West 15cc756a03 Fixed output when target is set manually 2024-07-18 22:56:27 -04:00
Tony West 2b83f36c9d Fixed handling of TCP 'no such host' errors 2024-07-18 22:44:00 -04:00
Tony West 8261a1c154 Fixed bug in response handling when a definition file initiates a redirect 2024-07-18 16:43:43 -04:00
Tony West 17743c8c8a Merge pull request #8 from 0x303/feature_1-brute_custom_wordlists
Feature: brute custom wordlists
2024-07-16 17:52:30 -04:00
Tony West 03ac3f1a3d Updated version string, code cleanup 2024-07-16 17:51:21 -04:00
Hank 3f02c53d94 add a param to pass in a wordlist to the brute subcommand 2024-07-16 14:41:03 -04:00
Hank 246d6f1241 add updated path to the prefix paths 2024-07-16 13:07:51 -04:00
Tony West eef14078b4 Merge pull request #7 from affix/fix_basic_auth
Fix for issue #6 - Handle Bearer tokens correctly
2024-07-15 10:50:38 -04:00
Tony West 20c02e4fa6 Updated version string 2024-07-15 10:49:24 -04:00
Keiran Smith 1fc2b657da Fix for issue #6 - Handle Bearer tokens correctly 2024-07-13 22:14:24 +01:00
Tony West 3d0e81ffd4 Fixes bug due to improper handling of definition files with no servers defined 2024-07-12 13:42:36 -04:00
Tony West 44141deba9 Fixed issue where automate command could only test one defined server. 2024-07-08 23:14:55 -04:00
Tony West 91c8ffd56f Bug fixes for improper handling of the server object to populate base paths. Also added additional paths to the brute command. 2024-07-03 14:16:59 -04:00
Tony West 75da0224d3 Fixed improper base path handling in the prepare command 2024-06-17 14:40:20 -04:00
Tony West 540d6064dc Fixed missing base path in prepare command 2024-06-17 14:30:50 -04:00
Tony West 686022c827 Added sqlmap support to the 'prepare' command. 2024-06-17 14:03:51 -04:00
Tony West df162f7c35 Fixes improper content check in brute command 2024-06-11 15:18:52 -04:00
Tony West 4d7814c74e Updated release config 2024-06-10 22:58:26 -04:00
Tony West 4bde10ad1b Updated release config 2024-06-10 22:53:44 -04:00
Tony West 4d05f51fbe fixes multiple output and error handling bugs 2024-06-10 22:46:36 -04:00
19 changed files with 1241 additions and 587 deletions

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+17
View File
@@ -1,3 +1,5 @@
version: 2
before:
hooks:
- go mod tidy
@@ -9,6 +11,11 @@ builds:
- linux
- windows
- darwin
flags:
- -trimpath
ldflags:
- -s -w -X main.build={{.Version}}
archives:
- format: tar.gz
@@ -24,8 +31,18 @@ archives:
format: zip
changelog:
disable: false
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: Features
regexp: "^(?i).*add.*$"
order: 0
- title: "Bug fixes"
regexp: "^(?i).*fix.*$"
order: 1
- title: Others
order: 999
+144 -9
View File
@@ -1,8 +1,10 @@
# sj (Swagger Jacker)
![](img/sj-logo.png)
sj is a command line tool designed to assist with auditing of exposed Swagger/OpenAPI definition files by checking the associated API endpoints for weak authentication. It also provides command templates for manual vulnerability testing.
It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of three commands:
It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of four commands:
- `automate` - Crafts a series of requests and analyzes the status code of the response.
- `prepare` - Generates a list of commands to use for manual testing.
- `endpoints` - Generates a list of raw API routes. *Path values will not be replaced with test data*.
@@ -24,32 +26,165 @@ To install the latest version of the tool, run:
```bash
$ go install github.com/BishopFox/sj@latest
# Note: you may also need to place the path to your Go binaries within your PATH environment variable:
$ export PATH=$PATH:~/go/bin
```
## Usage
> Use the `automate` command to send a series of requests to each defined endpoint and analyze the status code of each response.
![Automate Command](img/sj-automate.gif)
```bash
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -q
> Use the `prepare` command to prepare a list of curl commands for manual testing. You will likely have to modify these slightly.
INFO[0000] Gathering API details.
![Prepare Command](img/sj-prepare.gif)
Title: Swagger Petstore
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
INFO[0000] Available authentication mechanisms:
- api_key
- petstore_auth
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByTags?tags=1"
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/store/order"
WARN[0000] Manual testing may be required. Method=POST Status=500 Target="https://petstore.swagger.io/v2/user/createWithList"
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/user"
ERRO[0000] User not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/user/test"
WARN[0000] Manual testing may be required. Method=PUT Status=415 Target="https://petstore.swagger.io/v2/user/test"
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/user/login?password=test&username=test"
WARN[0000] Manual testing may be required. Method=POST Status=500 Target="https://petstore.swagger.io/v2/user/createWithArray"
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/user/logout"
WARN[0000] Manual testing may be required. Method=POST Status=415 Target="https://petstore.swagger.io/v2/pet/test"
ERRO[0000] Pet not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/pet/test"
WARN[0000] Manual testing may be required. Method=POST Status=415 Target="https://petstore.swagger.io/v2/pet/test/uploadImage"
ERRO[0000] Order not found Method=GET Status=404 Target="https://petstore.swagger.io/v2/store/order/test"
INFO[0000] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/store/inventory"
INFO[0000] Endpoint accessible! Method=POST Status=200 Target="https://petstore.swagger.io/v2/pet"
WARN[0000] Manual testing may be required. Method=PUT Status=415 Target="https://petstore.swagger.io/v2/pet"
INFO[0001] Endpoint accessible! Method=GET Status=200 Target="https://petstore.swagger.io/v2/pet/findByStatus?status=1"
```
> Use the `prepare` command to prepare a list of commands for manual testing. Currently supports both `curl` and `sqlmap`. You will likely have to modify these slightly.
```bash
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json -q
INFO[0000] Gathering API details.
Title: Swagger Petstore
Description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.
INFO[0000] Available authentication mechanisms:
- api_key
- petstore_auth
curl -sk -X PUT 'https://petstore.swagger.io/v2/user/test' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/test'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/test'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/test' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/store/order' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet' -d '{"test":"test"}'
curl -sk -X PUT 'https://petstore.swagger.io/v2/pet' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/store/order/test'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByTags?tags=1'
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithList' -d '{"test":"test"}'
curl -sk -X POST 'https://petstore.swagger.io/v2/user' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/logout'
curl -sk -X GET 'https://petstore.swagger.io/v2/user/login?password=test&username=test'
curl -sk -X POST 'https://petstore.swagger.io/v2/pet/test/uploadImage' -d '{"test":"test"}'
curl -sk -X GET 'https://petstore.swagger.io/v2/pet/findByStatus?status=1'
curl -sk -X GET 'https://petstore.swagger.io/v2/store/inventory'
curl -sk -X POST 'https://petstore.swagger.io/v2/user/createWithArray' -d '{"test":"test"}'
```
> Use the `endpoints` command to generate a list of raw endpoints from the provided definition file.
![Endpoints Command](img/sj-endpoints.gif)
```bash
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json
INFO[0000] Gathering endpoints.
/v2/pet
/v2/pet/findByStatus
/v2/pet/findByTags
/v2/pet/{petId}
/v2/pet/{petId}/uploadImage
/v2/store/inventory
/v2/store/order
/v2/store/order/{orderId}
/v2/user
/v2/user/createWithArray
/v2/user/createWithList
/v2/user/login
/v2/user/logout
/v2/user/{username}
```
> Use the `brute` command to send a series of requests in an attempt to find a definition file on the target.
```bash
$ sj brute -u https://<TARGET>
INFO[0000] Sending 2045 requests. This could take a while...
INFO[0111] Found operation definitions embedded in JavaScript file at https://<TARGET>/example.js
$ sj brute -u https://petstore.swagger.io/v2/swagger.json
INFO[0000] Sending 2045 requests. This could take a while...
Request: 343
INFO[0015] Definition file found: https://petstore.swagger.io/v2/swagger
{"...SNIP..."}
```
## Help
A full list of commands can be found by using the `--help` flag:
![Help Command](img/sj-help.gif)
```bash
$ sj --help
The process of reviewing and testing exposed API definition files is often tedious and requires a large investment of time for a thorough review.
sj (swaggerjacker) is a CLI tool that can be used to perform an initial check of API endpoints identified through exposed Swagger/OpenAPI definition files.
Once you determine what endpoints require authentication and which do not, you can use the "prepare" command to generate command templates for further (manual) testing.
Example usage:
Perform a quick check of endpoints which require authentication:
$ sj automate -u https://petstore.swagger.io/v2/swagger.json
Generate a list of commands to use for manual testing:
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json
Generate a list of raw API routes for use with custom scripts:
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json
Perform a brute-force attack against the target to identify hidden definition files:
$ sj brute -u https://petstore.swagger.io
Usage:
sj [flags]
sj [command]
Available Commands:
automate Sends a series of automated requests to the discovered endpoints.
brute Sends a series of automated requests to discover hidden API operation definitions.
endpoints Prints a list of endpoints from the target.
help Help about any command
prepare Prepares a set of commands for manual testing of each endpoint.
Flags:
-a, --agent string Set the User-Agent string. (default "Swagger Jacker (github.com/BishopFox/sj)")
-b, --base-path string Set the API base path if not defined in the definition file (i.e. /V2/).
-f, --format string Declare the format of the definition file (json/yaml/yml/js). (default "json")
-H, --headers stringArray Add custom headers, separated by a colon ("Name: Value"). Multiple flags are accepted.
-h, --help help for sj
-i, --insecure Ignores server certificate validation.
-l, --local-file string Loads the documentation from a local file.
-o, --outfile string Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.
-p, --proxy string Proxy host and port. Example: http://127.0.0.1:8080 (default "NOPROXY")
-q, --quiet Do not prompt for user input - uses default values for all requests.
-r, --randomize-user-agent Randomizes the user agent string. Default is 'false'.
-s, --safe-word stringArray Avoids 'dangerous word' check for the specified word(s). Multiple flags are accepted.
-T, --target string Manually set a target for the requests to be made if separate from the host the documentation resides on.
-t, --timeout int Set the request timeout period. (default 30)
-u, --url string Loads the documentation file from a URL
-v, --version version for sj
Use "sj [command] --help" for more information about a command.
```
+75 -50
View File
@@ -13,69 +13,94 @@ import (
var (
autoApplyAPIKey string = "n"
autoApplyBasicAuth string = "n"
autoApplyBearer string = "n"
basicAuthUser string
basicAuthPass string
basicAuth []byte
basicAuthString string
bearerToken string
)
func CheckSecDefs(doc3 openapi3.T) (apiInQuery bool, apiKey string, apiKeyName string) {
if len(doc3.Components.SecuritySchemes) > 0 {
log.Info("Available authentication mechanisms: ")
}
for mechanism := range doc3.Components.SecuritySchemes {
if doc3.Components.SecuritySchemes[mechanism].Value == nil {
log.Error("Authorization schema structure unsupported. Manual testing is required.")
return false, "", ""
if outputFormat == "json" && !quiet {
if len(doc3.Components.SecuritySchemes) != 0 {
log.Warnf("The following authentication mechanisms are supported. If necessary, supply these manually when using the JSON output format:\n")
}
if doc3.Components.SecuritySchemes[mechanism].Value.Scheme != "" {
fmt.Printf(" - %s (%s)\n", mechanism, doc3.Components.SecuritySchemes[mechanism].Value.Scheme)
} else {
fmt.Printf(" - %s\n", mechanism)
for mechanism := range doc3.Components.SecuritySchemes {
log.Infof(`Type: %s, Scheme: %s`, doc3.Components.SecuritySchemes[mechanism].Value.Type, doc3.Components.SecuritySchemes[mechanism].Value.Scheme)
}
log.Warnln("Note: To avoid these messages, pass the quiet (-q) flag.")
} else if !quiet {
if len(doc3.Components.SecuritySchemes) > 0 {
log.Info("Available authentication mechanisms: ")
}
if doc3.Components.SecuritySchemes[mechanism].Value.Type == "http" && !quiet {
if doc3.Components.SecuritySchemes[mechanism].Value.Scheme == "basic" {
log.Infof("Basic Authentication is accepted. Supply a username and password? (y/N)")
fmt.Scanln(&autoApplyBasicAuth)
autoApplyBasicAuth = strings.ToLower(autoApplyBasicAuth)
if autoApplyBasicAuth == "y" {
fmt.Printf("Enter a username.")
fmt.Scanln(&basicAuthUser)
fmt.Printf("Enter a password.")
fmt.Scanln(&basicAuthPass)
basicAuth = []byte(basicAuthUser + ":" + basicAuthPass)
basicAuthString = base64.StdEncoding.EncodeToString(basicAuth)
log.Infof("Using %s as the Basic Auth value.", basicAuthString)
Headers = append(Headers, "Authorization: Basic "+basicAuthString)
for mechanism := range doc3.Components.SecuritySchemes {
if doc3.Components.SecuritySchemes[mechanism].Value == nil {
log.Error("Authorization schema structure unsupported. Manual testing is required.")
return false, "", ""
}
if doc3.Components.SecuritySchemes[mechanism].Value.Scheme != "" {
fmt.Printf(" - %s (%s)\n", mechanism, doc3.Components.SecuritySchemes[mechanism].Value.Scheme)
} else if outputFormat != "json" {
fmt.Printf(" - %s\n", mechanism)
}
if doc3.Components.SecuritySchemes[mechanism].Value.Type == "http" {
if doc3.Components.SecuritySchemes[mechanism].Value.Scheme == "basic" {
log.Infof("Basic Authentication is accepted. Supply a username and password? (y/N)")
fmt.Scanln(&autoApplyBasicAuth)
autoApplyBasicAuth = strings.ToLower(autoApplyBasicAuth)
if autoApplyBasicAuth == "y" {
fmt.Printf("Enter a username.")
fmt.Scanln(&basicAuthUser)
fmt.Printf("Enter a password.")
fmt.Scanln(&basicAuthPass)
basicAuth = []byte(basicAuthUser + ":" + basicAuthPass)
basicAuthString = base64.StdEncoding.EncodeToString(basicAuth)
log.Infof("Using %s as the Basic Auth value.", basicAuthString)
Headers = append(Headers, "Authorization: Basic "+basicAuthString)
} else {
log.Warn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
}
} else if strings.ToLower(doc3.Components.SecuritySchemes[mechanism].Value.Scheme) == "bearer" {
log.Warn("A bearer token is accepted. Review the spec and craft a token manually using the -H flag.")
}
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "query" {
apiInQuery = true
log.Infof("An API key can be provided via a parameter string. Would you like to apply one? (y/N)")
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
if autoApplyAPIKey == "y" {
apiKeyName = doc3.Components.SecuritySchemes[mechanism].Value.Name
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
fmt.Scanln(&apiKey)
log.Infof("Using %s=%s as the API key in all requests.", apiKeyName, apiKey)
}
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "header" {
if mechanism == "bearer" {
log.Infof("A bearer token is accepted. Would you like to provide one? (y/N)")
fmt.Scanln(&autoApplyBearer)
autoApplyBearer = strings.ToLower(autoApplyBearer)
if autoApplyBearer == "y" {
fmt.Printf("What value would you like to use for the Bearer Token? ")
fmt.Scanln(&bearerToken)
Headers = append(Headers, "Authorization: Bearer "+bearerToken)
} else {
log.Warn("A bearer token is accepted. Review the spec and craft a header manually using the -H flag.")
}
} else {
log.Warn("A basic authentication header is accepted. Review the spec and craft a header manually using the -H flag.")
log.Infof("An API key can be provided via the header %s. Would you like to apply one? (y/N)", doc3.Components.SecuritySchemes[mechanism].Value.Name)
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
if autoApplyAPIKey == "y" {
apiKeyName = doc3.Components.SecuritySchemes[mechanism].Value.Name
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
fmt.Scanln(&apiKey)
Headers = append(Headers, doc3.Components.SecuritySchemes[mechanism].Value.Name+": "+apiKey)
}
}
} else if strings.ToLower(doc3.Components.SecuritySchemes[mechanism].Value.Scheme) == "bearer" {
log.Warn("A bearer token is accepted. Review the spec and craft a token manually using the -H flag.")
}
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "query" && !quiet {
apiInQuery = true
log.Infof("An API key can be provided via a parameter string. Would you like to apply one? (y/N)")
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
if autoApplyAPIKey == "y" {
apiKeyName = doc3.Components.SecuritySchemes[mechanism].Value.Name
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
fmt.Scanln(&apiKey)
log.Infof("Using %s=%s as the API key in all requests.", apiKeyName, apiKey)
}
} else if doc3.Components.SecuritySchemes[mechanism].Value.Type == "apiKey" && doc3.Components.SecuritySchemes[mechanism].Value.In == "header" && !quiet {
log.Infof("An API key can be provided via the header %s. Would you like to apply one? (y/N)", doc3.Components.SecuritySchemes[mechanism].Value.Name)
fmt.Scanln(&autoApplyAPIKey)
autoApplyAPIKey = strings.ToLower(autoApplyAPIKey)
if autoApplyAPIKey == "y" {
apiKeyName = doc3.Components.SecuritySchemes[mechanism].Value.Name
fmt.Printf("What value would you like to use for the API key (%s)?", apiKeyName)
fmt.Scanln(&apiKey)
Headers = append(Headers, doc3.Components.SecuritySchemes[mechanism].Value.Name+": "+apiKey)
}
}
}
+10 -2
View File
@@ -13,6 +13,9 @@ import (
var getAccessibleEndpoints bool
var outputFormat string
var responsePreviewLength int
var verbose bool
var automateCmd = &cobra.Command{
Use: "automate",
Short: "Sends a series of automated requests to the discovered endpoints.",
@@ -24,6 +27,8 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
if outfile != "" && strings.ToLower(outputFormat) != "" {
if !strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) != "json" {
log.Fatal("Only the JSON output format is supported at the moment.")
} else if strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) == "console" {
outputFormat = "json"
}
}
@@ -37,7 +42,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
}
if swaggerURL != "" {
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil, "automate")
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil)
} else {
specFile, err := os.Open(localFile)
if err != nil {
@@ -46,11 +51,14 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
bodyBytes, _ = io.ReadAll(specFile)
}
GenerateRequests(bodyBytes, client, "automate")
GenerateRequests(bodyBytes, client)
},
}
func init() {
automateCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "console", "The output format. Only 'console' (default) and 'json' are supported at the moment.")
automateCmd.PersistentFlags().BoolVar(&getAccessibleEndpoints, "get-accessible-endpoints", false, "Only output the accessible endpoints (those that return a 200 status code).")
automateCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose mode, which shows a preview of each response.")
automateCmd.PersistentFlags().IntVar(&responsePreviewLength, "response-preview-length", 50, "sets the response preview length when using verbose output.")
}
+81 -123
View File
@@ -1,6 +1,7 @@
package cmd
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
@@ -16,126 +17,11 @@ import (
log "github.com/sirupsen/logrus"
)
var (
prefixDirs = []string{
"/docs",
"",
"/swagger",
"/swagger/docs",
"/swagger/v1",
"/swagger/v2",
"/swagger/v3",
"/swagger/static",
"/swagger/ui",
"/swagger-ui",
"/api-docs",
"/api-docs/v1",
"/api-docs/v2",
"/apidocs",
"/api",
"/api/v1",
"/api/v2",
"/api/v3",
"/v1",
"/v2",
"/v3",
"/doc",
"/docs/swagger",
"/docs/swagger/v1",
"/docs/swagger/v2",
"/docs/swagger-ui",
"/docs/swagger-ui/v1",
"/docs/swagger-ui/v2",
"/docs/v1",
"/docs/v2",
"/docs/v3",
"/public",
}
jsonEndpoints = []string{
"",
"/index",
"/swagger",
"/swagger-ui",
"/swagger-resources",
"/swagger-config",
"/openapi",
"/api",
"/api-docs",
"/apidocs",
"/v1",
"/v2",
"/v3",
"/doc",
"/docs",
"/apispec",
"/apispec_1",
"/api-merged",
}
javascriptEndpoints = []string{
"/swagger-ui-init",
"/swagger-ui-bundle",
"/swagger-ui-standalone-preset",
"/swagger-ui",
"/swagger-ui.min",
"/swagger-ui-es-bundle-core",
"/swagger-ui-es-bundle",
"/swagger-ui-standalone-preset",
"/swagger-ui-layout",
"/swagger-ui-plugins",
}
)
var endpointWordlist string
func makeURLs(target string, endpoints []string, fileExtension string) []string {
urls := []string{}
for _, dir := range prefixDirs {
for _, endpoint := range endpoints {
if dir == "" && endpoint == "" {
continue
}
targetURL := target + dir + endpoint + fileExtension
urls = append(urls, targetURL)
}
}
return urls
}
func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
for i, url := range urls {
ct := CheckContentType(client, url)
if strings.Contains(ct, "application/json") {
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil, "brute")
if bodyBytes != nil {
checkSpec := UnmarshalSpec(bodyBytes)
if (strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3")) && checkSpec.Paths != nil {
log.Infof("\nDefinition file found: %s\n", url)
return true, checkSpec
}
}
} else if strings.Contains(ct, "application/javascript") {
bodyBytes, bodyString, _ := MakeRequest(client, "GET", url, timeout, nil, "brute")
if bodyBytes != nil {
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
for _, match := range matches {
jsonContent := match[2]
checkSpec := UnmarshalSpec([]byte(jsonContent))
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
log.Infof("\nFound operation definitions embedded in JavaScript file at %s\n", url)
return true, checkSpec
}
}
}
}
if i+1 == len(urls) {
fmt.Printf("\033[2K\r%s%d\n", "Request: ", i+1)
} else {
fmt.Printf("\033[2K\r%s%d", "Request: ", i+1)
}
}
return false, nil
}
var prefixDirs []string = []string{"", "/swagger", "/swagger/docs", "/swagger/latest", "/swagger/v1", "/swagger/v2", "/swagger/v3", "/swagger/static", "/swagger/ui", "/swagger-ui", "/api-docs", "/api-docs/v1", "/api-docs/v2", "/apidocs", "/api", "/api/v1", "/api/v2", "/api/v3", "/v1", "/v2", "/v3", "/doc", "/docs", "/docs/swagger", "/docs/swagger/v1", "/docs/swagger/v2", "/docs/swagger-ui", "/docs/swagger-ui/v1", "/docs/swagger-ui/v2", "/docs/v1", "/docs/v2", "/docs/v3", "/public", "/redoc"}
var jsonEndpoints []string = []string{"", "/index", "/swagger", "/swagger-ui", "/swagger-resources", "/swagger-config", "/openapi", "/api", "/api-docs", "/apidocs", "/v1", "/v2", "/v3", "/doc", "/docs", "/apispec", "/apispec_1", "/api-merged"}
var javascriptEndpoints []string = []string{"/swagger-ui-init", "/swagger-ui-bundle", "/swagger-ui-standalone-preset", "/swagger-ui", "/swagger-ui.min", "/swagger-ui-es-bundle-core", "/swagger-ui-es-bundle", "/swagger-ui-standalone-preset", "/swagger-ui-layout", "/swagger-ui-plugins"}
var bruteCmd = &cobra.Command{
Use: "brute",
@@ -151,11 +37,29 @@ var bruteCmd = &cobra.Command{
log.Warnf("Error parsing URL:%s\n", err)
}
target := u.Scheme + "://" + u.Host
if endpointWordlist == "" {
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "")...)
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js")...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json")...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/")...)
} else {
endpointList, err := os.Open(endpointWordlist)
if err != nil {
log.Fatalf("failed to open file: %s", err)
}
defer endpointList.Close()
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "")...)
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js")...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json")...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/")...)
scanner := bufio.NewScanner(endpointList)
for scanner.Scan() {
endpoint := scanner.Text()
fullURL := target + endpoint
allURLs = append(allURLs, fullURL)
}
if err := scanner.Err(); err != nil {
log.Fatalf("failed to read words from file: %s", err)
}
}
log.Infof("Sending %d requests. This could take a while...\n", len(allURLs))
specFound, definitionFile := findDefinitionFile(allURLs, client)
@@ -191,6 +95,60 @@ var bruteCmd = &cobra.Command{
},
}
func makeURLs(target string, endpoints []string, fileExtension string) []string {
urls := []string{}
for _, dir := range prefixDirs {
for _, endpoint := range endpoints {
if dir == "" && endpoint == "" {
continue
}
targetURL := target + dir + endpoint + fileExtension
urls = append(urls, targetURL)
}
}
return urls
}
func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
for i, url := range urls {
ct := CheckContentType(client, url)
if strings.Contains(ct, "application/json") {
bodyBytes, _, _ := MakeRequest(client, "GET", url, timeout, nil)
if bodyBytes != nil {
checkSpec := UnmarshalSpec(bodyBytes)
if (strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3")) && checkSpec.Paths != nil {
fmt.Println("")
log.Infof("Definition file found: %s\n", url)
return true, checkSpec
}
}
} else if strings.Contains(ct, "application/javascript") {
bodyBytes, bodyString, _ := MakeRequest(client, "GET", url, timeout, nil)
if bodyBytes != nil {
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
for _, match := range matches {
jsonContent := match[2]
checkSpec := UnmarshalSpec([]byte(jsonContent))
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
log.Infof("\nFound operation definitions embedded in JavaScript file at %s\n", url)
return true, checkSpec
}
}
}
}
if i == len(urls) {
fmt.Printf("\033[2K\r%s%d\n", "Request: ", i+1)
} else {
fmt.Printf("\033[2K\r%s%d", "Request: ", i+1)
}
}
return false, nil
}
func init() {
// TODO: Add a flag here (boolean) that defaults to false that will cause the program to execute 'sj automate' on the discovered definition file automatically.
bruteCmd.PersistentFlags().StringVarP(&endpointWordlist, "wordlist", "w", "", "The file containing a list of paths to brute force for discovery.")
}
+3 -3
View File
@@ -25,15 +25,15 @@ This list contains the raw endpoints (parameter values will not be appended or m
log.Infof("Gathering endpoints.\n\n")
if swaggerURL != "" {
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil, "endpoints")
paths = GenerateRequests(bodyBytes, client, "endpoints")
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
paths = GenerateRequests(bodyBytes, client)
} else {
specFile, err := os.Open(localFile)
if err != nil {
log.Fatal("Error opening file:", err)
}
bodyBytes, _ = io.ReadAll(specFile)
paths = GenerateRequests(bodyBytes, client, "endpoints")
paths = GenerateRequests(bodyBytes, client)
}
unique.Sort(unique.StringSlice{&paths})
for _, v := range paths {
+129 -14
View File
@@ -1,25 +1,81 @@
package cmd
import (
"fmt"
"os"
"strings"
log "github.com/sirupsen/logrus"
)
func writeLog(sc int, target, method string, errorMsg string) {
if sc != 200 {
if sc == 401 || sc == 403 {
logUnauth(sc, target, method, errorMsg)
} else if sc == 301 || sc == 302 {
logRedirect(sc, target, method)
} else if sc == 0 {
logSkipped(sc, target, method)
} else if sc == 404 {
logNotFound(sc, target, method, errorMsg)
func writeLog(sc int, target, method, errorMsg, response string) {
tempResponsePreviewLength := responsePreviewLength
if len(response) < responsePreviewLength {
responsePreviewLength = len(response)
}
if outfile != "" {
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
fmt.Println("Output file does not exist or cannot be created")
os.Exit(1)
}
defer file.Close()
log.SetOutput(file)
}
if outputFormat == "console" {
if strings.Contains(response, "\"") {
log.SetFormatter(&log.TextFormatter{DisableQuote: true, DisableTimestamp: true})
} else {
logManual(sc, target, method, errorMsg)
log.SetFormatter(&log.TextFormatter{DisableTimestamp: true})
}
} else {
logAccessible(sc, target, method)
log.SetFormatter(&log.JSONFormatter{DisableHTMLEscape: true, DisableTimestamp: true})
}
if verbose {
if sc != 200 {
if sc == 401 || sc == 403 {
logVerboseUnauth(sc, target, method, errorMsg, response)
} else if sc == 301 || sc == 302 {
logVerboseRedirect(sc, target, method, response)
} else if sc == 0 {
logVerboseBad(sc, target, method, response)
} else if sc == 404 {
logNotFound(sc, target, method, errorMsg)
} else if sc == 1 {
logDangerous(target, method)
} else {
logVerboseManual(sc, target, method, errorMsg, response)
}
} else {
logVerboseAccessible(sc, target, method, response)
}
} else {
if sc != 200 {
if sc == 401 || sc == 403 {
logUnauth(sc, target, method, errorMsg)
} else if sc == 301 || sc == 302 {
logRedirect(sc, target, method)
} else if sc == 0 {
logBad(sc, target, method)
} else if sc == 404 {
logNotFound(sc, target, method, errorMsg)
} else if sc == 1 {
logDangerous(target, method)
} else {
logManual(sc, target, method, errorMsg)
}
} else {
logAccessible(sc, target, method)
}
}
responsePreviewLength = tempResponsePreviewLength
}
func logAccessible(status int, target, method string) {
@@ -30,6 +86,23 @@ func logAccessible(status int, target, method string) {
}).Print("Endpoint accessible!")
}
func logVerboseAccessible(status int, target, method, response string) {
log.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Print("Endpoint accessible!")
}
func logDangerous(target, method string) {
log.WithFields(log.Fields{
"Status": "skipped",
"Target": target,
"Method": method,
}).Warn("Endpoint skipped due to dangerous keyword.")
}
func logManual(status int, target, method, errorMsg string) {
if errorMsg == "" {
errorMsg = "Manual testing may be required."
@@ -41,6 +114,18 @@ func logManual(status int, target, method, errorMsg string) {
}).Warn(errorMsg)
}
func logVerboseManual(status int, target, method, errorMsg, response string) {
if errorMsg == "" {
errorMsg = "Manual testing may be required."
}
log.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Warn(errorMsg)
}
func logNotFound(status int, target, method, errorMsg string) {
if errorMsg == "" {
errorMsg = "Endpoint not found."
@@ -60,12 +145,30 @@ func logRedirect(status int, target, method string) {
}).Error("Redirect detected. This likely requires authentication.")
}
func logSkipped(status int, target, method string) {
func logVerboseRedirect(status int, target, method, response string) {
log.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[0:responsePreviewLength],
}).Error("Redirect detected. This likely requires authentication.")
}
func logBad(status int, target, method string) {
log.WithFields(log.Fields{
"Status": "N/A",
"Target": target,
"Method": method,
}).Warn("Request skipped (dangerous keyword found).")
}).Warn("Bad request (could not reach the target).")
}
func logVerboseBad(status int, target, method, response string) {
log.WithFields(log.Fields{
"Status": "N/A",
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Warn("Bad request (could not reach the target).")
}
func logUnauth(status int, target, method, errorMsg string) {
@@ -78,3 +181,15 @@ func logUnauth(status int, target, method, errorMsg string) {
"Method": method,
}).Error(errorMsg)
}
func logVerboseUnauth(status int, target, method, errorMsg, response string) {
if errorMsg == "" {
errorMsg = "Unauthorized."
}
log.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Error(errorMsg)
}
+9 -4
View File
@@ -12,7 +12,7 @@ import (
var prepareCmd = &cobra.Command{
Use: "prepare",
Short: "Prepares a set of commands for manual testing of each endpoint.",
Long: `The prepare command prepares a set of curl commands for manual testing of each endpoint.
Long: `The prepare command prepares a set of commands for manual testing of each endpoint.
This enables you to test specific API functions for common vulnerabilities or misconfigurations.`,
Run: func(cmd *cobra.Command, args []string) {
client := CheckAndConfigureProxy()
@@ -20,8 +20,8 @@ This enables you to test specific API functions for common vulnerabilities or mi
fmt.Printf("\n")
log.Infof("Gathering API details.\n\n")
if swaggerURL != "" {
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil, "prepare")
GenerateRequests(bodyBytes, client, "prepare")
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
GenerateRequests(bodyBytes, client)
} else {
specFile, err := os.Open(localFile)
if err != nil {
@@ -29,7 +29,12 @@ This enables you to test specific API functions for common vulnerabilities or mi
}
specBytes, _ := io.ReadAll(specFile)
GenerateRequests(specBytes, client, "prepare")
GenerateRequests(specBytes, client)
}
},
}
var prepareFor string
func init() {
prepareCmd.PersistentFlags().StringVarP(&prepareFor, "external-tool", "e", "curl", "The external tool to prepare commands for. Generates syntax for 'curl' by default.")
}
+32 -12
View File
@@ -7,6 +7,7 @@ import (
"math/rand"
"net/http"
"net/url"
"os"
"strings"
"time"
@@ -51,25 +52,26 @@ var (
userChoice string
)
func MakeRequest(client http.Client, method, target string, timeout int64, reqData io.Reader, command string) ([]byte, string, int) {
func MakeRequest(client http.Client, method, target string, timeout int64, reqData io.Reader) ([]byte, string, int) {
if quiet {
avoidDangerousRequests = "y"
}
// Handling of dangerous keywords
u, _ := url.Parse(target)
endpoint := u.RawPath + "?" + u.RawQuery
for _, v := range dangerousStrings {
if command == "automate" && strings.Contains(endpoint, v) && !strings.Contains(strings.Join(safeWords, ","), v) {
if os.Args[1] == "automate" && strings.Contains(endpoint, v) && !strings.Contains(strings.Join(safeWords, ","), v) {
userChoice = ""
if avoidDangerousRequests == "y" {
return nil, "", 0
} else {
fmt.Printf("[!] Dangerous keyword '%s' detected in URL (%s). Do you still want to test this endpoint? (y/N)", v, target)
log.Warnf("Dangerous keyword '%s' detected in URL (%s). Do you still want to test this endpoint? (y/N)", v, target)
fmt.Scanln(&userChoice)
if strings.ToLower(userChoice) != "y" {
if !riskSurveyed {
avoidDangerousRequests = "y"
fmt.Printf("[!] Do you want to avoid all dangerous requests? (Y/n)")
log.Warnf("Do you want to avoid all dangerous requests? (Y/n)")
fmt.Scanln(&avoidDangerousRequests)
avoidDangerousRequests = strings.ToLower(avoidDangerousRequests)
riskSurveyed = true
@@ -102,10 +104,11 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
}
req.Header.Set(h[0], h[1])
} else {
log.Fatal("Custom header provided cannot be used.")
log.Warnf("Header provided (%s) cannot be used. Headers must be in 'Key: Value' format (this may be caused by a header declared within the definition file).\n", Headers[i])
}
}
// User-Agent handling
if randomUserAgent {
if UserAgent != "Swagger Jacker (github.com/BishopFox/sj)" {
log.Fatalf("Cannot set a User Agent while supplying the 'random-user-agent' flag.")
@@ -137,25 +140,39 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
log.Printf("Error: %s - skipping request.", err)
return nil, "", 0
} else if err != nil && err != context.Canceled && err != io.EOF {
log.Error("Error: response not received.\n", err)
if strings.Contains(fmt.Sprint(err), "tls") && !strings.Contains(fmt.Sprint(err), "user canceled") {
if (strings.Contains(fmt.Sprint(err), "tls") || strings.Contains(fmt.Sprint(err), "x509")) && !strings.Contains(fmt.Sprint(err), "user canceled") {
log.Fatal("Try supplying the --insecure flag.")
} else if strings.Contains(fmt.Sprint(err), "tcp") && strings.Contains(fmt.Sprint(err), "no such host") {
log.Fatalf("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
} else if strings.Contains(fmt.Sprint(err), "user canceled") {
return nil, "skipped", 1
} else {
log.Error("Error: response not received.\n", err)
}
return nil, "", 0
}
bodyBytes, _ := io.ReadAll(resp.Body)
bodyString := string(bodyBytes)
if (resp.StatusCode == 301 || resp.StatusCode == 302) && strings.Contains(bodyString, "<html>") {
redirect, _ := resp.Location()
bodyBytes, bodyString, requestStatus = MakeRequest(client, method, redirect.Scheme+"://"+redirect.Host+redirect.Path, timeout, reqData)
return bodyBytes, bodyString, requestStatus
}
requestStatus = resp.StatusCode
return bodyBytes, bodyString, requestStatus
}
func CheckContentType(client http.Client, url string) string {
func CheckContentType(client http.Client, target string) string {
u, _ := url.Parse(target)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
req, err := http.NewRequest("GET", url, nil)
req, err := http.NewRequest("GET", target, nil)
if err != nil && err != context.Canceled && err != io.EOF {
log.Fatal("Error: could not create HTTP request - ", err)
}
@@ -165,9 +182,12 @@ func CheckContentType(client http.Client, url string) string {
log.Printf("Error: %s - skipping request.", err)
return ""
} else if err != nil && err != context.Canceled && err != io.EOF {
log.Error("Error: response not received.\n", err)
if strings.Contains(fmt.Sprint(err), "tls") && !strings.Contains(fmt.Sprint(err), "user canceled") {
fmt.Println("Try supplying the --insecure flag.")
if (strings.Contains(fmt.Sprint(err), "tls") || strings.Contains(fmt.Sprint(err), "x509")) && !strings.Contains(fmt.Sprint(err), "user canceled") {
log.Fatal("Try supplying the --insecure flag.")
} else if strings.Contains(fmt.Sprint(err), "tcp") && strings.Contains(fmt.Sprint(err), "no such host") {
log.Fatalf("The target '%s' is not reachable. Check the declared host(s) and supply a target manually using -T if needed.", u.Scheme+"://"+u.Host)
} else {
log.Error("Error: response not received.\n", err)
}
return ""
}
+4 -4
View File
@@ -32,7 +32,7 @@ Example usage:
Perform a quick check of endpoints which require authentication:
$ sj automate -u https://petstore.swagger.io/v2/swagger.json
Generate a list of curl commands to use for manual testing:
Generate a list of commands to use for manual testing:
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json
Generate a list of raw API routes for use with custom scripts:
@@ -46,7 +46,7 @@ $ sj brute -u https://petstore.swagger.io`,
log.Error("Command not specified. See the --help flag for usage.")
}
},
Version: "1.3.4", // Also change in .goreleaser.yaml !
Version: "1.7.2",
}
func Execute() {
@@ -59,8 +59,8 @@ func init() {
rootCmd.AddCommand(prepareCmd)
rootCmd.AddCommand(bruteCmd)
rootCmd.PersistentFlags().StringVarP(&UserAgent, "agent", "a", "Swagger Jacker (github.com/BishopFox/sj)", "Set the User-Agent string.")
rootCmd.PersistentFlags().StringVarP(&basePath, "base-path", "b", "", "Set the API base path if not defined in the spec (i.e. /V2/)")
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "json", "Declare the format of the documentation (json/yaml/yml/js).")
rootCmd.PersistentFlags().StringVarP(&basePath, "base-path", "b", "", "Set the API base path if not defined in the definition file (i.e. /V2/).")
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "json", "Declare the format of the definition file (json/yaml/yml/js).")
rootCmd.PersistentFlags().StringArrayVarP(&Headers, "headers", "H", nil, "Add custom headers, separated by a colon (\"Name: Value\"). Multiple flags are accepted.")
rootCmd.PersistentFlags().BoolVarP(&insecure, "insecure", "i", false, "Ignores server certificate validation.")
rootCmd.PersistentFlags().StringVarP(&localFile, "local-file", "l", "", "Loads the documentation from a local file.")
+106 -18
View File
@@ -2,30 +2,118 @@ package cmd
import (
"fmt"
"math/rand"
"strings"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
)
func BuildObjectsFromSchemaDefinitions(doc openapi3.T) {
if doc.Components.Schemas != nil {
log.Infof("Building test objects from schema definitions...\n\n")
}
if doc.Components.Schemas != nil {
for definition := range doc.Components.Schemas {
log.Infof("%s accepts %d parameters:\n", definition, len(doc.Components.Schemas[definition].Value.Properties))
for property := range doc.Components.Schemas[definition].Value.Properties {
schemaDef := doc.Components.Schemas[definition]
definitionProperty := schemaDef.Value.Properties[property]
if definitionProperty.Value.Type == "string" {
log.Infof("%s=%s\n", property, "test")
} else if definitionProperty.Value.Type == "integer" {
log.Infof("%s=%d\n", property, 1)
} else if definitionProperty.Ref != "" {
fmt.Println("[!] [!] [!] " + definitionProperty.Ref) // Item $ref within a property
}
type SchemaReference struct {
Required []string
Type string
Properties struct {
Property struct {
Type string
Format string
Example string
Reference string
XML struct {
Name string
Namespace string
Prefix string
Attribute bool
Wrapped bool
}
}
}
}
func HandleSchemaReference(schema *openapi3.SchemaRef) {
}
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef, counter int) SwaggerRequest {
if param != nil {
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
if s.Def.Components.Schemas[name] != nil {
schema := s.Def.Components.Schemas[name]
if schema.Value.Properties != nil {
for property := range schema.Value.Properties {
if schema.Value.Properties[property].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
break
}
} else if location == "path" {
if schema.Value.Properties[property].Value.Type == "string" {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
}
}
} else if location == "query" {
if schema.Value.Properties[property].Value.Type == "string" {
s.Query.Add(param.Value.Name, "test")
} else {
s.Query.Add(param.Value.Name, "1")
}
} else if location == "body" {
if schema.Value.Properties[property].Value.Type == "string" {
s.Body[param.Value.Name] = "test"
} else {
s.Body[param.Value.Name] = 1
}
}
}
} else if schema.Value.Enum != nil {
if location == "path" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", fmt.Sprint(value))
} else if location == "query" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Query.Add(param.Value.Name, fmt.Sprint(value))
} else if location == "body" {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Body[param.Value.Name] = value
}
}
}
} else {
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
if s.Def.Components.Schemas[name] != nil {
schema := s.Def.Components.Schemas[name]
if schema.Value.Properties != nil {
for property := range schema.Value.Properties {
if schema.Value.Properties[property].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
break
}
} else {
if schema.Value.Properties[property].Value.Type == "string" {
s.Body[property] = "test"
} else {
s.Body[property] = 1
}
}
}
} else if schema.Value.Enum != nil {
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
s.Body[name] = value
}
}
}
return s
}
+573 -348
View File
@@ -1,16 +1,15 @@
package cmd
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/json"
"encoding/xml"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
@@ -19,30 +18,556 @@ import (
"gopkg.in/yaml.v3"
)
type SwaggerRequest struct {
ApiKey string
ApiKeyName string
ApiInQuery bool
BasePath string
Body map[string]any
BodyData []byte
Def *openapi3.T
Path string
Paths []string
Query url.Values
RawQuery string
ResultsJSON []string
URL url.URL
}
var accessibleEndpoints []string
var specJSON string
func CheckAndConfigureProxy() (client http.Client) {
var proxyUrl *url.URL
func GenerateRequests(bodyBytes []byte, client http.Client) []string {
transport := &http.Transport{}
var s SwaggerRequest
s.Def = UnmarshalSpec(bodyBytes)
s.ApiInQuery, s.ApiKey, s.ApiKeyName = CheckSecDefs(*s.Def)
u, _ := url.Parse(swaggerURL)
s.URL = *u
if insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
if os.Args[1] != "endpoints" {
// Prints Title/Description values if they exist
PrintSpecInfo(*s.Def.Info)
}
if proxy != "NOPROXY" {
proxyUrl, _ = url.Parse(proxy)
transport.Proxy = http.ProxyURL(proxyUrl)
if s.Def.Paths == nil {
log.Fatalf("Could not find any defined operations. Review the file manually.")
}
client = http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
if len(s.Def.Servers) > 1 {
if !quiet && (os.Args[1] != "endpoints") {
if apiTarget == "" {
log.Warn("Multiple servers detected in documentation. You can manually set a server to test with the -T flag.\nThe detected servers are as follows:")
for i, server := range s.Def.Servers {
fmt.Printf("Server %d: %s\n", i+1, server.URL)
}
fmt.Println()
}
}
if apiTarget == "" {
for _, server := range s.Def.Servers {
if outputFormat == "console" && os.Args[1] == "automate" {
log.Warnf("Results for %s:\n", server.URL)
}
if server.URL == "/" {
s.Path = ""
}
if strings.Contains(server.URL, "localhost") || strings.Contains(server.URL, "127.0.0.1") || strings.Contains(server.URL, "::1") {
log.Warn("The server(s) documented in the definition file contain(s) a local host value and may result in errors. Supply a target manually using the '-T' flag.")
}
u, _ := url.Parse(server.URL)
s.URL = *u
s = s.IterateOverPaths(client)
fmt.Println()
}
} else {
if apiTarget != "" {
u, _ = url.Parse(apiTarget)
}
s.URL = *u
s = s.IterateOverPaths(client)
}
} else {
if apiTarget != "" {
u, _ = url.Parse(apiTarget)
} else {
if swaggerURL == "" {
u, _ = url.Parse(s.Def.Servers[0].URL)
if strings.Contains(s.Def.Servers[0].URL, "localhost") || strings.Contains(s.Def.Servers[0].URL, "127.0.0.1") || strings.Contains(s.Def.Servers[0].URL, "::1") {
log.Warn("The server documented in the definition file contains a local host value and may result in errors. Supply a target manually using the '-T' flag.")
}
}
}
s.URL = *u
s = s.IterateOverPaths(client)
}
if os.Args[1] == "automate" {
if outfile == "" && getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
var isDuplicateEndpoint bool
var printedEndpoints []string
if accessibleEndpoints != nil {
log.Infof("Accessible endpoints:\n")
for i := range accessibleEndpoints {
for j := range printedEndpoints {
if accessibleEndpoints[i] == printedEndpoints[j] {
isDuplicateEndpoint = true
}
}
if !isDuplicateEndpoint {
fmt.Printf(" %s\n", accessibleEndpoints[i])
printedEndpoints = append(printedEndpoints, accessibleEndpoints[i])
}
isDuplicateEndpoint = false
}
}
}
}
return s.Paths
}
func (s SwaggerRequest) IterateOverPaths(client http.Client) SwaggerRequest {
for path, pathItem := range s.Def.Paths {
operations := map[string]*openapi3.Operation{
"CONNECT": pathItem.Connect,
"GET": pathItem.Get,
"HEAD": pathItem.Head,
"OPTIONS": pathItem.Options,
"PATCH": pathItem.Patch,
"POST": pathItem.Post,
"PUT": pathItem.Put,
"TRACE": pathItem.Trace,
}
for method, op := range operations {
// Do all the things here :D
s.BodyData = nil
if op != nil {
s.URL.Path = s.Path + path
s = s.BuildDefinedRequests(client, method, pathItem, op)
}
}
}
return s
}
func (s SwaggerRequest) BuildDefinedRequests(client http.Client, method string, pathItem *openapi3.PathItem, op *openapi3.Operation) SwaggerRequest {
s.ApiInQuery = false
b := make(map[string]any)
s.Body = b
s.Query = url.Values{}
var pathMap = make(map[string]bool)
basePathResult := s.GetBasePath()
s.URL.Path = basePathResult + s.URL.Path
s = s.AddParametersToRequest(op)
var errorDescriptions = make(map[any]string)
for status := range op.Responses {
if op.Responses[status].Ref == "" {
if op.Responses[status].Value == nil {
continue
} else {
errorDescriptions[status] = *op.Responses[status].Value.Description
}
} else {
continue
}
}
s.URL.RawQuery = s.Query.Encode()
if os.Args[1] == "automate" {
_, resp, sc := MakeRequest(client, method, s.URL.String(), timeout, bytes.NewReader(s.BodyData))
if sc == 200 {
accessibleEndpointFound = true
accessibleEndpoints = append(accessibleEndpoints, s.URL.String())
}
if strings.ToLower(outputFormat) != "console" {
if getAccessibleEndpoints {
if sc == 200 {
writeLog(sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)], resp)
}
} else {
writeLog(sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)], resp)
}
} else {
writeLog(sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)], resp)
}
//}
} else if os.Args[1] == "prepare" {
s.PrintPreparedCommands(method)
} else if os.Args[1] == "endpoints" {
for k := range s.Def.Paths {
if !pathMap[k] {
s.Paths = append(s.Paths, basePathResult+k)
pathMap[k] = true
}
}
}
return s
}
// This whole function needs to be refactored/cleaned up a bit
func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRequest {
for _, param := range op.Parameters {
if param.Value == nil && param.Value.Schema.Ref == "" {
continue
} else if param.Value.In == "path" {
if param.Value.Schema != nil {
if param.Value.Schema.Ref != "" {
s = s.SetParametersFromSchema(param, "path", param.Value.Schema.Ref, nil, 0)
} else if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
}
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
}
}
} else if param.Value.In == "query" {
if param.Value.Schema != nil {
if param.Value.Schema.Ref != "" {
s = s.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil, 0)
} else {
if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
s.Query.Add(param.Value.Name, "test")
} else {
s.Query.Add(param.Value.Name, "1")
}
}
} else {
s.Query.Add(param.Value.Name, "test")
}
} else if param.Value.In == "header" && param.Value.Required && strings.ToLower(param.Value.Name) != "content-type" {
Headers = append(Headers, fmt.Sprintf("%s: %s", param.Value.Name, "1"))
} else if param.Value.In == "body" {
if param.Value.Schema.Ref != "" {
s = s.SetParametersFromSchema(param, "body", param.Value.Schema.Ref, nil, 0)
}
if param.Value.Schema.Value.Type == "string" {
s.Body[param.Value.Name] = "test"
} else {
s.Body[param.Value.Name] = 1
}
var data []string
for k, v := range s.Body {
data = append(data, fmt.Sprintf("%s=%s", k, v))
}
s.BodyData = []byte(strings.Join(data, "&"))
} else {
continue
}
}
if op.RequestBody != nil {
if op.RequestBody.Value.Content != nil {
for i := range op.RequestBody.Value.Content {
if contentType == "" {
EnforceSingleContentType(i)
} else {
EnforceSingleContentType(contentType)
}
if op.RequestBody.Value.Content.Get(i).Schema != nil {
if op.RequestBody.Value.Content.Get(i).Schema.Value == nil {
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Ref, op.RequestBody, 0)
if strings.Contains(i, "json") {
s.BodyData, _ = json.Marshal(s.Body)
} else if strings.Contains(i, "x-www-form-urlencoded") {
var formData []string
for j := range s.Body {
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
}
s.BodyData = []byte(strings.Join(formData, "&"))
} else if strings.Contains(i, "xml") {
type Element struct {
XMLName xml.Name
Content any `xml:",chardata"`
}
type Root struct {
XMLName xml.Name `xml:"root"`
Elements []Element `xml:",any"`
}
var elements []Element
for key, value := range s.Body {
elements = append(elements, Element{
XMLName: xml.Name{Local: key},
Content: value,
})
}
root := Root{
Elements: elements,
}
xmlData, err := xml.Marshal(root)
if err != nil {
log.Warn("Error marshalling XML data.")
}
s.BodyData = xmlData
} else {
log.Warnf("Content type not supported. Test this path manually: %s (Content type: %s)\n", s.URL.Path, i)
}
} else {
var formData []string
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Ref != "" {
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Ref, op.RequestBody, 0)
} else {
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
if valueType == "string" {
s.Body[j] = "test"
} else if valueType == "boolean" {
s.Body[j] = false
} else if valueType == "integer" || valueType == "number" {
s.Body[j] = 1
} else {
s.Body[j] = "unknown_type_populate_manually"
}
if i == "application/x-www-form-urlencoded" {
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
}
}
if i == "application/x-www-form-urlencoded" {
s.BodyData = []byte(strings.Join(formData, "&"))
} else if strings.Contains(i, "json") || i == "*/*" {
s.BodyData, _ = json.Marshal(s.Body)
} else if strings.Contains(i, "xml") {
//
type Element struct {
XMLName xml.Name
Content any `xml:",chardata"`
}
type Root struct {
XMLName xml.Name `xml:"root"`
Elements []Element `xml:",any"`
}
var elements []Element
for key, value := range s.Body {
elements = append(elements, Element{
XMLName: xml.Name{Local: key},
Content: value,
})
}
root := Root{
Elements: elements,
}
xmlData, err := xml.Marshal(root)
if err != nil {
log.Warn("Error marshalling XML data.")
}
s.BodyData = xmlData
} else {
s.Body["test"] = "test"
s.BodyData = []byte("test=test")
}
}
}
}
}
}
}
}
if s.ApiInQuery && s.ApiKey != "" {
s.Query.Add(s.ApiKeyName, s.ApiKey)
}
return s
}
func (s SwaggerRequest) PrintPreparedCommands(method string) {
var preparedCommand string
if strings.ToLower(prepareFor) == "curl" {
preparedCommand = fmt.Sprintf("curl -sk -X %s '%s'", method, s.URL.String())
} else if strings.ToLower(prepareFor) == "sqlmap" {
preparedCommand = fmt.Sprintf("sqlmap --method=%s -u %s", method, s.URL.String())
} else if strings.ToLower(prepareFor) == "ffuf" {
log.Fatal("ffuf is not supported yet :(")
// TODO
} else {
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
}
if s.BodyData != nil {
if strings.ToLower(prepareFor) == "sqlmap" {
preparedCommand = preparedCommand + fmt.Sprintf(" --data='%s'", s.BodyData)
} else {
preparedCommand = preparedCommand + fmt.Sprintf(" -d '%s'", s.BodyData)
}
}
if len(Headers) != 0 {
preparedCommand = preparedCommand + fmt.Sprintf(" -H '%s'", strings.Join(Headers, "' -H '"))
}
fmt.Println(preparedCommand)
}
func (s SwaggerRequest) GetBasePath() string {
if basePath == "" {
if s.Def.Servers != nil {
if strings.Contains(s.Def.Servers[0].URL, ":") {
var schemeIndex int
if strings.Contains(s.Def.Servers[0].URL, "://") {
schemeIndex = strings.Index(s.Def.Servers[0].URL, "://") + 3
} else {
schemeIndex = 0
}
s.URL.Host = s.Def.Servers[0].URL[schemeIndex:]
}
if s.Def.Servers[0].URL == "/" {
basePath = "/"
} else if strings.Contains(s.Def.Servers[0].URL, "http") && !strings.Contains(s.Def.Servers[0].URL, s.URL.Host) { // Check to see if the server object being used for the base path contains a different host than the target
basePath = s.Def.Servers[0].URL
basePath = strings.ReplaceAll(basePath, "http://", "")
basePath = strings.ReplaceAll(basePath, "https://", "")
indexSubdomain := strings.Index(basePath, "/")
basePath = basePath[indexSubdomain:]
if !strings.HasSuffix(basePath, "/") {
basePath = basePath + "/"
}
} else {
basePath = s.Def.Servers[0].URL
if strings.Contains(basePath, s.URL.Host) || strings.Contains(basePath, "http") {
basePath = strings.ReplaceAll(basePath, s.URL.Host, "")
basePath = strings.ReplaceAll(basePath, "http://", "")
basePath = strings.ReplaceAll(basePath, "https://", "")
}
}
}
}
basePath = strings.TrimSuffix(basePath, "/")
return basePath
}
func PrintSpecInfo(i openapi3.Info) {
if strings.ToLower(outputFormat) == "console" {
if i.Title != "" {
fmt.Println("Title:", i.Title)
}
if i.Description != "" {
fmt.Printf("Description: %s\n\n", i.Description)
}
if i.Title == "" && i.Description == "" {
log.Warnf("Detected possible error in parsing the definition file. Title and description values are empty.\n\n")
}
} else if strings.ToLower(outputFormat) == "json" {
titleDescriptionLogger := log.New()
titleDescriptionLogger.SetFormatter(&log.JSONFormatter{DisableTimestamp: true, DisableHTMLEscape: true})
if outfile != "" {
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
fmt.Println("Output file does not exist or cannot be created")
os.Exit(1)
}
defer file.Close()
titleDescriptionLogger.SetOutput(file)
}
if i.Title != "" {
titleDescriptionLogger.WithField("Title:", i.Title).Print("N/A")
}
if i.Description != "" {
titleDescriptionLogger.WithField("Description:", i.Description).Print("N/A")
}
if i.Title == "" && i.Description == "" {
log.Warn("Detected possible error in parsing the definition file. Title and description values are empty.")
}
}
}
func SetScheme(swaggerURL string) (scheme string) {
if strings.HasPrefix(swaggerURL, "http://") {
scheme = "http"
} else if strings.HasPrefix(swaggerURL, "https://") {
scheme = "https"
} else {
scheme = "https"
}
return scheme
}
/*
TrimHostScheme trims the scheme from the provided URL if the '-T' flag is supplied to sj.
*/
func TrimHostScheme(apiTarget, fullUrlHost string) (host string) {
if apiTarget != "" {
if strings.HasPrefix(apiTarget, "http://") {
host = strings.TrimPrefix(apiTarget, "http://")
} else if strings.HasPrefix(apiTarget, "https://") {
host = strings.TrimPrefix(apiTarget, "https://")
} else {
host = apiTarget
}
} else {
host = fullUrlHost
}
return host
}
func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
var doc openapi2.T
var doc3 openapi3.T
format = strings.ToLower(format)
if format == "js" || strings.HasSuffix(swaggerURL, ".js") {
bodyBytes = ExtractSpecFromJS(bodyBytes)
} else if format == "yaml" || format == "yml" || strings.HasSuffix(swaggerURL, ".yaml") || strings.HasSuffix(swaggerURL, ".yml") {
_ = yaml.Unmarshal(bodyBytes, &doc)
_ = yaml.Unmarshal(bodyBytes, &doc3)
}
_ = json.Unmarshal(bodyBytes, &doc)
_ = json.Unmarshal(bodyBytes, &doc3)
if strings.HasPrefix(doc3.OpenAPI, "3") {
newDoc := &doc3
return newDoc
} else if strings.HasPrefix(doc.Swagger, "2") {
newDoc, err := openapi2conv.ToV3(&doc)
if err != nil {
fmt.Printf("Error converting v2 document to v3: %s\n", err)
}
return newDoc
} else if os.Args[1] == "brute" {
var noDoc openapi3.T
return &noDoc
} else {
log.Fatal("Error parsing definition file.")
return nil
}
return client
}
func ExtractSpecFromJS(bodyBytes []byte) []byte {
@@ -93,347 +618,47 @@ func ExtractSpecFromJS(bodyBytes []byte) []byte {
return bodyBytes
}
func GenerateRequests(bodyBytes []byte, client http.Client, command string) []string {
var apiKey string
var apiKeyName string
var apiInQuery bool = false
var resultsJSON []string
var pathMap = make(map[string]bool)
var paths []string
func CheckAndConfigureProxy() (client http.Client) {
var proxyUrl *url.URL
fullUrl, _ := url.Parse(swaggerURL)
newDoc := UnmarshalSpec(bodyBytes)
basePathResult := GetBasePath(newDoc.Servers, TrimHostScheme(apiTarget, fullUrl.Host))
transport := &http.Transport{}
// BuildObjectsFromSchemaDefinitions(*newDoc) TODO
if command != "endpoints" {
// Prints Title/Description values if they exist
PrintSpecInfo(*newDoc.Info)
apiInQuery, apiKey, apiKeyName = CheckSecDefs(*newDoc)
if insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if command == "automate" && outfile != "" {
WriteJSONFile(`"results":[`, false)
if proxy != "NOPROXY" {
proxyUrl, _ = url.Parse(proxy)
transport.Proxy = http.ProxyURL(proxyUrl)
}
for path, pathItem := range newDoc.Paths {
operations := map[string]*openapi3.Operation{
"CONNECT": pathItem.Connect,
"GET": pathItem.Get,
"HEAD": pathItem.Head,
"OPTIONS": pathItem.Options,
"PATCH": pathItem.Patch,
"POST": pathItem.Post,
"PUT": pathItem.Put,
"TRACE": pathItem.Trace,
}
for method, op := range operations {
// Do all the things here :D
if op != nil {
var newPath string
newPath = path
u := url.URL{
Scheme: SetScheme(swaggerURL),
Host: TrimHostScheme(apiTarget, fullUrl.Host),
Path: basePathResult,
}
if u.Path == "/" {
u.Path = ""
}
query := url.Values{}
var bodyData []byte
body := make(map[string]any)
for _, param := range op.Parameters {
if param.Ref != "" || param.Value == nil {
continue
} else if param.Value.In == "path" {
newPath = strings.ReplaceAll(newPath, "{"+param.Value.Name+"}", "test")
} else if param.Value.In == "query" {
if param.Value.Schema.Ref != "" {
query.Add(param.Value.Name, "test") // TODO: Implement actual definition/schema
} else {
if param.Value.Schema.Value.Type == "string" {
query.Add(param.Value.Name, "test")
} else {
query.Add(param.Value.Name, "1")
}
}
} else if param.Value.In == "header" && param.Value.Required {
Headers = append(Headers, fmt.Sprintf("%s: %s", param.Value.Name, "1"))
} else if param.Value.In == "body" {
if param.Value.Schema.Value.Type == "string" {
body[param.Value.Name] = "test"
} else {
body[param.Value.Name] = 1
}
bodyData, _ = json.Marshal(body)
} else {
continue
}
}
if op.RequestBody != nil {
body["test"] = "test"
bodyData, _ = json.Marshal(body)
}
if apiInQuery && apiKey != "" {
query.Add(apiKeyName, apiKey)
}
var errorDescriptions = make(map[any]string)
for status := range op.Responses {
if op.Responses[status].Ref == "" {
if op.Responses[status].Value == nil {
continue
} else {
errorDescriptions[status] = *op.Responses[status].Value.Description
}
} else {
continue
}
}
u.Path = u.Path + newPath
u.RawQuery = query.Encode()
if command == "automate" {
_, _, sc := MakeRequest(client, method, u.String(), timeout, bytes.NewReader([]byte(bodyData)), command)
if sc == 200 {
accessibleEndpointFound = true
accessibleEndpoints = append(accessibleEndpoints, u.String())
}
if outfile != "" {
if getAccessibleEndpoints {
if sc == 200 {
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)])
WriteJSONFile(result, false)
}
} else {
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)])
WriteJSONFile(result, false)
}
time.Sleep(1 * time.Second)
} else if outfile == "" {
if strings.ToLower(outputFormat) != "console" {
if getAccessibleEndpoints {
if sc == 200 {
resultsJSON = append(resultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)]))
}
} else {
resultsJSON = append(resultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)]))
}
} else if !getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
writeLog(sc, u.String(), method, errorDescriptions[fmt.Sprint(sc)])
}
}
} else if command == "prepare" {
if bodyData == nil {
if len(Headers) == 0 {
fmt.Printf("curl -sk -X %s '%s'\n", method, u.String())
} else {
fmt.Printf("curl -sk -X %s '%s' -H '%s'\n", method, u.String(), strings.Join(Headers, "' -H '"))
}
} else {
if len(Headers) == 0 {
fmt.Printf("curl -sk -X %s '%s' -d '%s'\n", method, u.String(), bodyData)
} else {
fmt.Printf("curl -sk -X %s '%s' -d '%s' -H '%s'\n", method, u.String(), bodyData, strings.Join(Headers, "' -H '"))
}
}
} else if command == "endpoints" {
for k := range newDoc.Paths {
if !pathMap[k] {
paths = append(paths, k)
pathMap[k] = true
}
}
}
}
}
client = http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
if command == "automate" {
if outfile == "" && getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
var isDuplicateEndpoint bool
var printedEndpoints []string
if accessibleEndpoints != nil {
log.Infof("Accessible endpoints:\n")
for i := range accessibleEndpoints {
for j := range printedEndpoints {
if accessibleEndpoints[i] == printedEndpoints[j] {
isDuplicateEndpoint = true
}
}
if !isDuplicateEndpoint {
fmt.Printf(" %s\n", accessibleEndpoints[i])
printedEndpoints = append(printedEndpoints, accessibleEndpoints[i])
}
isDuplicateEndpoint = false
}
}
} else if outfile == "" && strings.ToLower(outputFormat) == "json" {
results := strings.Join(resultsJSON, ",")
fmt.Printf(`%s"results":[%s]}%s`, specJSON, results, "\n")
} else if outfile != "" {
if accessibleEndpoints == nil {
WriteJSONFile("]}\n", false)
} else {
WriteJSONFile(",", true)
}
log.Infof("Results written to %s", outfile)
}
}
return paths
return client
}
func GetBasePath(servers openapi3.Servers, host string) (bp string) {
if basePath == "" {
if servers != nil {
s1, _ := url.Parse(servers[0].URL)
if len(servers) >= 2 {
s2, _ := url.Parse(servers[1].URL)
if s1.Host != s2.Host || len(servers) > 2 {
log.Warn("Multiple servers detected in documentation. You can manually set a server to test with the -T flag.\nThe detected servers are as follows:")
for i := range servers {
fmt.Printf("Server %d: %s\n", i+1, servers[i].URL)
}
}
}
if servers[0].URL == "/" {
basePath = "/"
} else {
basePath = servers[0].URL
if strings.Contains(basePath, host) || strings.Contains(basePath, "http") {
basePath = strings.ReplaceAll(basePath, host, "")
basePath = strings.ReplaceAll(basePath, "http://", "")
basePath = strings.ReplaceAll(basePath, "https://", "")
}
}
func EnforceSingleContentType(newContentType string) {
newContentType = strings.TrimSpace(newContentType)
if Headers != nil {
headerString := strings.Join(Headers, ",")
Headers = nil
ctIndex := strings.Index(strings.ToLower(headerString), "content-type:") + 14
headerString = headerString[ctIndex:]
if strings.Contains(headerString, ",") {
headerString = strings.TrimPrefix(headerString, ",")
ctEndIndex := strings.Index(headerString[ctIndex:], ",") + 1
headerString = headerString[:ctEndIndex]
} else if !strings.Contains(headerString, ":") {
headerString = ""
}
if headerString != "" {
Headers = append(Headers, strings.Split(headerString, ",")...)
}
}
basePath = strings.TrimSuffix(basePath, "/")
return basePath
}
func PrintSpecInfo(i openapi3.Info) {
specJSON = fmt.Sprintf(`{"title":"%s","description":"%s",`, i.Title, i.Description)
if outfile != "" {
WriteJSONFile(specJSON, false)
} else if strings.ToLower(outputFormat) == "console" {
if i.Title != "" {
fmt.Println("Title:", i.Title)
}
if i.Description != "" {
fmt.Printf("Description: %s\n\n", i.Description)
}
}
}
func SetScheme(swaggerURL string) (scheme string) {
if strings.HasPrefix(swaggerURL, "http://") {
scheme = "http"
} else if strings.HasPrefix(swaggerURL, "https://") {
scheme = "https"
} else {
scheme = "https"
}
return scheme
}
func TrimHostScheme(apiTarget, fullUrlHost string) (host string) {
if apiTarget != "" {
if strings.HasPrefix(apiTarget, "http://") {
host = strings.TrimPrefix(apiTarget, "http://")
} else if strings.HasPrefix(apiTarget, "https://") {
host = strings.TrimPrefix(apiTarget, "https://")
} else {
host = apiTarget
}
} else {
host = fullUrlHost
}
return host
}
func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
var doc openapi2.T
var doc3 openapi3.T
format = strings.ToLower(format)
if format == "js" || strings.HasSuffix(swaggerURL, ".js") {
bodyBytes = ExtractSpecFromJS(bodyBytes)
} else if format == "yaml" || format == "yml" || strings.HasSuffix(swaggerURL, ".yaml") || strings.HasSuffix(swaggerURL, ".yml") {
_ = yaml.Unmarshal(bodyBytes, &doc)
_ = yaml.Unmarshal(bodyBytes, &doc3)
}
_ = json.Unmarshal(bodyBytes, &doc)
_ = json.Unmarshal(bodyBytes, &doc3)
if strings.HasPrefix(doc3.OpenAPI, "3") {
newDoc := &doc3
return newDoc
} else {
newDoc, err := openapi2conv.ToV3(&doc)
if err != nil {
fmt.Printf("Error converting v2 document to v3: %s\n", err)
}
return newDoc
}
}
func WriteJSONFile(result string, end bool) {
if end {
file, err := os.OpenFile(outfile, os.O_RDWR, 0644)
if err != nil {
fmt.Println("File does not exist or cannot be created")
os.Exit(1)
}
defer file.Close()
w := bufio.NewWriter(file)
fileInfo, err := file.Stat()
if err != nil {
log.Error("Failed to get file info:", err)
}
fileSize := fileInfo.Size()
if fileSize != 0 {
lastChar := fileSize - 1
_, err = file.Seek(lastChar, 0)
if err != nil {
log.Error("Error seeking to last character", err)
}
fmt.Fprintf(w, "]}\n")
}
w.Flush()
} else {
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
fmt.Println("File does not exist or cannot be created")
os.Exit(1)
}
defer file.Close()
w := bufio.NewWriter(file)
fmt.Fprintf(w, "%s", result)
w.Flush()
}
Headers = append(Headers, "Content-Type: "+newContentType)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB