Compare commits

...
55 Commits
Author SHA1 Message Date
Tony West a9f907e2f1 Modified convert command to enable saved output even when definition file is already v3 2024-10-16 14:27:48 -04:00
Tony West 3b3fed2c99 Merge branch 'main' of https://github.com/BishopFox/sj 2024-10-09 18:07:14 -04:00
Tony West 1571b14873 Updated version string 2024-10-09 18:06:38 -04:00
Tony West 94e65fd96f Fixed missing check for 'formData' in the 'in' parameter object value 2024-10-09 18:06:26 -04:00
Tony West 5274b1b0ab Fixed null pointer bug 2024-10-09 13:06:50 -04:00
Tony West 49d6867ebf Now requires Go >= 1.21 2024-10-04 18:44:51 -04:00
Tony West 65d424c6a7 Added support for 'oneOf', 'anyOf', and 'allOf' schema keywords. 2024-10-03 22:47:27 -04:00
Tony West d7b34598d2 Fixed bug in verbose JSON output 2024-10-03 14:52:47 -04:00
Tony West 4526ddbf23 Fixed improper JSON output structure 2024-10-01 13:37:23 -04:00
Tony West 31e286235b Merge pull request #27 from mr-pmillz/main
Resolved nil pointer dereference
2024-09-20 15:27:05 -04:00
Tony West c84e679ac4 Merge branch 'main' into main 2024-09-20 15:26:18 -04:00
Tony West 41ba000755 Updated the version string. 2024-09-20 15:24:30 -04:00
Tony West cc99aef9ae Adds automatic sorting of results when output to the console 2024-09-20 15:13:14 -04:00
mr-pmillz b98e8c7d25 resolved nil pointer dereference 2 2024-09-16 18:23:45 -04:00
mr-pmillz 2879da6f87 resolved nil pointer dereference 2024-09-16 18:06:07 -04:00
Tony West 530143b4ee Fixed bug that caused improperly formatted JSON output 2024-09-13 18:29:02 -04:00
Tony West 69de3abbf1 Merge pull request #26 from idanya/fix-param-condition-check
Fix parameter handling bug in AddParametersToRequest() function
2024-09-13 11:49:58 -04:00
Tony West 9fd1b78081 Modified version number to reflect bug fix 2024-09-13 11:46:03 -04:00
Idan Yael c5a46d35bd Refactor parameter handling in SwaggerRequest.AddParametersToRequest() 2024-09-11 23:23:57 +03:00
Tony West 2877384d54 Updated issue templates 2024-08-26 22:03:33 -04:00
Tony West cb3e0bb16e Updated issue templates 2024-08-26 22:02:39 -04:00
Tony West 291edfe60c Added the 'convert' subcommand 2024-08-26 14:08:07 -04:00
Tony West 69b316e7bc Fixed bug in the '--get-accessible-endpoints' flag 2024-08-23 19:07:41 -04:00
Tony West 615f0c7a26 Fixed bug where output file would close pre-emptively 2024-08-23 13:27:38 -04:00
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
19 changed files with 1491 additions and 611 deletions

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
---
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**
A code block containing the command executed with output:
```bash
$ sj automate -u https://petstore.swagger.io/v2/swagger.json
[SOME OUTPUT AND/OR ERROR HERE]
#Note: To maintain privacy, please omit any IP address, URL, organizational information contained within the definition file, etc.
```
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
*Note: To maintain privacy, please omit any IP address, URL, organizational information contained within the definition file, etc.*
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+24
View File
@@ -0,0 +1,24 @@
---
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.
+11 -5
View File
@@ -11,6 +11,11 @@ builds:
- linux
- windows
- darwin
flags:
- -trimpath
ldflags:
- -s -w -X main.build={{.Version}}
archives:
- format: tar.gz
@@ -26,17 +31,18 @@ archives:
format: zip
changelog:
disable: false
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: "New Features"
regexp: '^(Add|add)(s|ed|ing)?'
- title: Features
regexp: "^(?i).*add.*$"
order: 0
- title: "Bug Fixes"
regexp: '^(Fix|fix)(es|ed|ing)?'
- title: "Bug fixes"
regexp: "^(?i).*fix.*$"
order: 1
- title: "Others"
- title: Others
order: 999
+159 -9
View File
@@ -1,16 +1,19 @@
# 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 five sub-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*.
- `brute` - Sends a series of requests to a target to find operation definitions based on commonly used file paths.
- `convert` - Converts a definition file from v2 to v3.
## Build
To compile from source, ensure you have Go version `>= 1.20` installed and run `go build` from within the repository:
To compile from source, ensure you have Go version `>= 1.21` installed and run `go build` from within the repository:
```bash
$ git clone https://github.com/BishopFox/sj.git
@@ -24,32 +27,179 @@ 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
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
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.
![Prepare Command](img/sj-prepare.gif)
```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..."}
```
> Use the `convert` command to convert a definition file from version 2 to version 3.
```bash
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json
INFO[0000] Gathering API details.
INFO[0000] Wrote file to /current/directory/openapi.json
```
## 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
Convert a Swagger (v2) definition file to an OpenAPI (v3) definition file:
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json
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.
convert Converts a Swagger definition file to an OpenAPI v3 definition file.
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.
```
+29 -13
View File
@@ -13,22 +13,25 @@ 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 outputFormat == "json" {
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")
}
for mechanism := range doc3.Components.SecuritySchemes {
log.Infof(`Type: %s, Scheme: %s`, doc3.Components.SecuritySchemes[mechanism].Value.Type, doc3.Components.SecuritySchemes[mechanism].Value.Scheme)
}
} else {
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: ")
}
@@ -44,7 +47,7 @@ func CheckSecDefs(doc3 openapi3.T) (apiInQuery bool, apiKey string, apiKeyName s
fmt.Printf(" - %s\n", mechanism)
}
if doc3.Components.SecuritySchemes[mechanism].Value.Type == "http" && !quiet {
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)
@@ -64,7 +67,7 @@ func CheckSecDefs(doc3 openapi3.T) (apiInQuery bool, apiKey string, apiKeyName s
} 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 {
} 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)
@@ -75,15 +78,28 @@ func CheckSecDefs(doc3 openapi3.T) (apiInQuery bool, apiKey string, apiKeyName s
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)
} 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.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)
}
}
}
}
+8
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"
}
}
@@ -53,4 +58,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
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)
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)
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.")
}
+121
View File
@@ -0,0 +1,121 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
var convertCmd = &cobra.Command{
Use: "convert",
Short: "Converts a Swagger definition file to an OpenAPI v3 definition file.",
Long: `The convert command converts a provided definition file from the Swagger specification (v2) to the OpenAPI specification (v3) and stores it into an output file.`,
Run: func(cmd *cobra.Command, args []string) {
var bodyBytes []byte
client := CheckAndConfigureProxy()
if strings.ToLower(outputFormat) != "json" {
fmt.Printf("\n")
log.Infof("Gathering API details.\n\n")
}
if swaggerURL != "" {
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil)
} else {
specFile, err := os.Open(localFile)
if err != nil {
log.Fatal("Error opening definition file:", err)
}
bodyBytes, _ = io.ReadAll(specFile)
}
var doc openapi2.T
var doc3 openapi3.T
format = strings.ToLower(format)
if format == "yaml" || format == "yml" || strings.HasSuffix(swaggerURL, ".yaml") || strings.HasSuffix(swaggerURL, ".yml") {
_ = yaml.Unmarshal(bodyBytes, &doc)
_ = yaml.Unmarshal(bodyBytes, &doc3)
} else {
_ = json.Unmarshal(bodyBytes, &doc)
_ = json.Unmarshal(bodyBytes, &doc3)
}
if strings.HasPrefix(doc3.OpenAPI, "3") {
log.Warnln("Definition file is already version 3.")
WriteConvertedDefinitionFile(bodyBytes)
} 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)
}
if format == "json" {
if strings.HasSuffix(outfile, "yaml") || strings.HasSuffix(outfile, "yml") {
log.Warn("It looks like you're trying to save the file in YAML format. Supply the '-f yaml' option to do so (default: json).")
}
converted, err := json.Marshal(newDoc)
if err != nil {
log.Fatal("Error converting definition file to v3:", err)
}
if !strings.HasSuffix(string(converted), "}") {
if outfile == "" {
fmt.Println(string(converted))
} else {
WriteConvertedDefinitionFile(converted)
}
} else {
endOfJSON := strings.LastIndex(string(converted), "}") + 1
if outfile == "" {
fmt.Println(string(converted)[:endOfJSON])
} else {
WriteConvertedDefinitionFile(converted)
}
}
} else if format == "yaml" || format == "yml" {
converted, err := yaml.Marshal(newDoc)
if err != nil {
log.Fatal("Error converting definition file to v3:", err)
}
if outfile == "" {
fmt.Println(string(converted))
} else {
WriteConvertedDefinitionFile(converted)
}
}
} else {
log.Fatal("Error parsing definition file.")
}
},
}
func WriteConvertedDefinitionFile(data []byte) {
file, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Errorf("Error opening file: %s\n", err)
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
log.Errorf("Error writing file: %s\n", err)
} else {
f, _ := filepath.Abs(outfile)
log.Infof("Wrote file to %s\n", f)
}
}
+178 -34
View File
@@ -1,90 +1,234 @@
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)
} else if sc == 1 {
logDangerous(target, method)
} else {
logManual(sc, target, method, errorMsg)
}
} else {
logAccessible(sc, target, method)
}
type Result struct {
Method string `json:"method"`
Status int `json:"status"`
Target string `json:"target"`
}
func logAccessible(status int, target, method string) {
log.WithFields(log.Fields{
type VerboseResult struct {
Method string `json:"method"`
Preview string `json:"preview"`
Status int `json:"status"`
Target string `json:"target"`
}
var tempLogger *log.Logger
func writeLog(sc int, target, method, errorMsg, response string) {
var file *os.File
tempLogger = log.New()
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)
}
tempLogger.SetOutput(file)
}
if outputFormat == "console" {
if strings.Contains(response, "\"") {
tempLogger.SetFormatter(&log.TextFormatter{DisableQuote: true, DisableTimestamp: true})
} else {
tempLogger.SetFormatter(&log.TextFormatter{DisableTimestamp: true})
}
} else {
tempLogger.SetFormatter(&log.JSONFormatter{DisableHTMLEscape: true, DisableTimestamp: true, PrettyPrint: true})
}
if verbose {
if sc != 200 {
if sc == 401 || sc == 403 {
logVerboseUnauth(sc, target, method, errorMsg, response, tempLogger)
} else if sc == 301 || sc == 302 {
logVerboseRedirect(sc, target, method, response, tempLogger)
} else if sc == 0 {
logVerboseBad(sc, target, method, response, tempLogger)
} else if sc == 404 {
logNotFound(sc, target, method, errorMsg, tempLogger)
} else if sc == 1 {
logDangerous(target, method, tempLogger)
} else if sc == 8899 {
logVerboseJSON(target, method)
} else {
logVerboseManual(sc, target, method, errorMsg, response, tempLogger)
}
} else {
logVerboseAccessible(sc, target, method, response, tempLogger)
}
} else {
if sc != 200 {
if sc == 401 || sc == 403 {
logUnauth(sc, target, method, errorMsg, tempLogger)
} else if sc == 301 || sc == 302 {
logRedirect(sc, target, method, tempLogger)
} else if sc == 0 {
logBad(sc, target, method, tempLogger)
} else if sc == 404 {
logNotFound(sc, target, method, errorMsg, tempLogger)
} else if sc == 1 {
logDangerous(target, method, tempLogger)
} else if sc == 8899 {
logJSON(target, method)
} else {
logManual(sc, target, method, errorMsg, tempLogger)
}
} else {
if sc == 8899 {
logJSON(target, method)
} else {
logAccessible(sc, target, method, tempLogger)
}
}
}
responsePreviewLength = tempResponsePreviewLength
file.Close()
}
func logAccessible(status int, target, method string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Print("Endpoint accessible!")
}
func logDangerous(target, method string) {
log.WithFields(log.Fields{
func logVerboseAccessible(status int, target, method, response string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Print("Endpoint accessible!")
}
func logDangerous(target, method string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": "skipped",
"Target": target,
"Method": method,
}).Warn("Endpoint skipped due to dangerous keyword.")
}).Warn("Endpoint skipped due to dangerous keyword (or request cancelled due to timeout).")
}
func logManual(status int, target, method, errorMsg string) {
func logManual(status int, target, method, errorMsg string, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Manual testing may be required."
}
log.WithFields(log.Fields{
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Warn(errorMsg)
}
func logNotFound(status int, target, method, errorMsg string) {
func logVerboseManual(status int, target, method, errorMsg, response string, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Manual testing may be required."
}
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Warn(errorMsg)
}
func logNotFound(status int, target, method, errorMsg string, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Endpoint not found."
}
log.WithFields(log.Fields{
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Error(errorMsg)
}
func logRedirect(status int, target, method string) {
log.WithFields(log.Fields{
func logRedirect(status int, target, method string, logger *log.Logger) {
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Error("Redirect detected. This likely requires authentication.")
}
func logSkipped(status int, target, method string) {
log.WithFields(log.Fields{
func logVerboseRedirect(status int, target, method, response string, logger *log.Logger) {
logger.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, logger *log.Logger) {
logger.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 logUnauth(status int, target, method, errorMsg string) {
func logVerboseBad(status int, target, method, response string, logger *log.Logger) {
logger.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, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Unauthorized."
}
log.WithFields(log.Fields{
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
}).Error(errorMsg)
}
func logVerboseUnauth(status int, target, method, errorMsg, response string, logger *log.Logger) {
if errorMsg == "" {
errorMsg = "Unauthorized."
}
logger.WithFields(log.Fields{
"Status": status,
"Target": target,
"Method": method,
"Preview": response[:responsePreviewLength],
}).Error(errorMsg)
}
func logJSON(title, description string) {
tempLogger.WithFields(log.Fields{
"apiTitle": title,
"description": description,
"results": jsonResultArray,
}).Println("Done")
}
func logVerboseJSON(title, description string) {
tempLogger.WithFields(log.Fields{
"apiTitle": title,
"description": description,
"results": jsonVerboseResultArray,
}).Println("Done")
}
+28
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"os"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -35,6 +36,33 @@ This enables you to test specific API functions for common vulnerabilities or mi
}
var prepareFor string
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 init() {
prepareCmd.PersistentFlags().StringVarP(&prepareFor, "external-tool", "e", "curl", "The external tool to prepare commands for. Generates syntax for 'curl' by default.")
}
+45 -7
View File
@@ -2,6 +2,7 @@ package cmd
import (
"context"
"crypto/tls"
"fmt"
"io"
"math/rand"
@@ -104,7 +105,7 @@ 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])
}
}
@@ -140,8 +141,10 @@ 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 {
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 {
@@ -152,16 +155,25 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
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)
}
@@ -171,11 +183,37 @@ 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 ""
}
return resp.Header.Get("Content-Type")
}
func CheckAndConfigureProxy() (client http.Client) {
var proxyUrl *url.URL
transport := &http.Transport{}
if insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if proxy != "NOPROXY" {
proxyUrl, _ = url.Parse(proxy)
transport.Proxy = http.ProxyURL(proxyUrl)
}
client = http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return client
}
+6 -3
View File
@@ -5,7 +5,6 @@ import (
"github.com/spf13/cobra"
)
var accessibleEndpointFound bool
var apiTarget string
var basePath string
var format string
@@ -39,14 +38,17 @@ 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`,
$ sj brute -u https://petstore.swagger.io
Convert a Swagger (v2) definition file to an OpenAPI (v3) definition file:
$ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
log.Error("Command not specified. See the --help flag for usage.")
}
},
Version: "1.4.2",
Version: "1.9.2",
}
func Execute() {
@@ -58,6 +60,7 @@ func init() {
rootCmd.AddCommand(endpointsCmd)
rootCmd.AddCommand(prepareCmd)
rootCmd.AddCommand(bruteCmd)
rootCmd.AddCommand(convertCmd)
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 definition file (i.e. /V2/).")
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "json", "Declare the format of the definition file (json/yaml/yml/js).")
+162 -18
View File
@@ -2,30 +2,174 @@ 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.Example != "" && schema.Value.Properties[property].Value.Example != nil {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", schema.Value.Properties[property].Value.Example.(string))
} else 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.Example != "" && schema.Value.Properties[property].Value.Example != nil {
s.Query.Add(param.Value.Name, (schema.Value.Properties[property].Value.Example).(string))
} else 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.Example != "" && schema.Value.Properties[property].Value.Example != nil {
s.Body[param.Value.Name] = schema.Value.Properties[property].Value.Example
} else 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 if schema.Value.AllOf != nil {
for i := range schema.Value.AllOf {
if schema.Value.AllOf[i].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.AllOf[i].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
break
}
}
}
} else if schema.Value.OneOf != nil && schema.Value.OneOf[0].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.OneOf[0].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
}
} else if schema.Value.AnyOf != nil && schema.Value.AnyOf[0].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.AnyOf[0].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
}
}
}
} 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.Example != "" {
s.Body[property] = schema.Value.Properties[property].Value.Example
} 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
} else {
if schema.Value.AllOf != nil {
for i := range schema.Value.AllOf {
if schema.Value.AllOf[i].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.AllOf[i].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
break
}
}
}
} else if schema.Value.OneOf != nil && schema.Value.OneOf[0].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.OneOf[0].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
}
} else if schema.Value.AnyOf != nil && schema.Value.AnyOf[0].Ref != "" {
if counter < 3 {
s = s.SetParametersFromSchema(param, location, schema.Value.AnyOf[0].Ref, req, counter+1)
} else {
log.Warnf("Nested reference encountered for %s. Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path)
}
}
}
}
}
return s
}
+597 -399
View File
File diff suppressed because it is too large. Load diff
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