mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-10 09:51:26 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b83f36c9d | ||
|
|
8261a1c154 | ||
|
|
17743c8c8a | ||
|
|
03ac3f1a3d | ||
|
|
3f02c53d94 | ||
|
|
246d6f1241 | ||
|
|
eef14078b4 | ||
|
|
20c02e4fa6 | ||
|
|
1fc2b657da | ||
|
|
3d0e81ffd4 | ||
|
|
44141deba9 | ||
|
|
91c8ffd56f |
No files matched your search
+6
-5
@@ -26,17 +26,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
|
||||
@@ -1,5 +1,7 @@
|
||||
# sj (Swagger Jacker)
|
||||
|
||||

|
||||
|
||||
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:
|
||||
@@ -30,26 +32,156 @@ $ go install github.com/BishopFox/sj@latest
|
||||
|
||||
> Use the `automate` command to send a series of requests to each defined endpoint and analyze the status code of each response.
|
||||
|
||||

|
||||
```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.
|
||||
|
||||

|
||||
```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.
|
||||
|
||||

|
||||
```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:
|
||||
|
||||

|
||||
```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.
|
||||
```
|
||||
+29
-13
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+81
-123
@@ -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.")
|
||||
}
|
||||
+3
-3
@@ -11,7 +11,7 @@ func writeLog(sc int, target, method string, errorMsg string) {
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logRedirect(sc, target, method)
|
||||
} else if sc == 0 {
|
||||
logSkipped(sc, target, method)
|
||||
logBad(sc, target, method)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg)
|
||||
} else if sc == 1 {
|
||||
@@ -70,12 +70,12 @@ func logRedirect(status int, target, method string) {
|
||||
}).Error("Redirect detected. This likely requires authentication.")
|
||||
}
|
||||
|
||||
func logSkipped(status int, target, method string) {
|
||||
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 logUnauth(status int, target, method, errorMsg string) {
|
||||
|
||||
+18
-4
@@ -142,6 +142,8 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
|
||||
} else if err != nil && err != context.Canceled && err != io.EOF {
|
||||
if strings.Contains(fmt.Sprint(err), "tls") && !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 +154,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,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.")
|
||||
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 ""
|
||||
}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ $ sj brute -u https://petstore.swagger.io`,
|
||||
log.Error("Command not specified. See the --help flag for usage.")
|
||||
}
|
||||
},
|
||||
Version: "1.4.2",
|
||||
Version: "1.5.2",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
|
||||
+260
-199
@@ -19,6 +19,22 @@ 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
|
||||
|
||||
@@ -94,197 +110,63 @@ func ExtractSpecFromJS(bodyBytes []byte) []byte {
|
||||
}
|
||||
|
||||
func GenerateRequests(bodyBytes []byte, client http.Client) []string {
|
||||
var apiKey string
|
||||
var apiKeyName string
|
||||
var apiInQuery bool = false
|
||||
var resultsJSON []string
|
||||
var pathMap = make(map[string]bool)
|
||||
var paths []string
|
||||
|
||||
fullUrl, _ := url.Parse(swaggerURL)
|
||||
newDoc := UnmarshalSpec(bodyBytes)
|
||||
basePathResult := GetBasePath(newDoc.Servers, TrimHostScheme(apiTarget, fullUrl.Host))
|
||||
var s SwaggerRequest
|
||||
s.Def = UnmarshalSpec(bodyBytes)
|
||||
s.ApiInQuery, s.ApiKey, s.ApiKeyName = CheckSecDefs(*s.Def)
|
||||
u, _ := url.Parse(swaggerURL)
|
||||
s.URL = *u
|
||||
|
||||
// BuildObjectsFromSchemaDefinitions(*newDoc) TODO
|
||||
|
||||
if os.Args[1] != "endpoints" {
|
||||
// Prints Title/Description values if they exist
|
||||
PrintSpecInfo(*newDoc.Info)
|
||||
apiInQuery, apiKey, apiKeyName = CheckSecDefs(*newDoc)
|
||||
PrintSpecInfo(*s.Def.Info)
|
||||
}
|
||||
|
||||
if os.Args[1] == "automate" && outfile != "" {
|
||||
WriteJSONFile(`"results":[`, false)
|
||||
}
|
||||
|
||||
if newDoc.Paths == nil {
|
||||
if s.Def.Paths == nil {
|
||||
log.Fatalf("Could not find any defined operations. Review the file manually.")
|
||||
}
|
||||
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 os.Args[1] == "automate" {
|
||||
_, _, sc := MakeRequest(client, method, u.String(), timeout, bytes.NewReader([]byte(bodyData)))
|
||||
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 {
|
||||
if sc == 1 {
|
||||
resultsJSON = append(resultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"Skipped due to dangerous keyword in request"}`, sc, u.String(), method))
|
||||
} 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 os.Args[1] == "prepare" {
|
||||
if strings.ToLower(prepareFor) == "curl" {
|
||||
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 strings.ToLower(prepareFor) == "sqlmap" {
|
||||
if bodyData == nil {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("sqlmap -u %s\n", u.String())
|
||||
} else {
|
||||
fmt.Printf("sqlmap -u %s -H '%s'\n", u.String(), strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
} else {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("sqlmap -u %s --data='%s'\n", u.String(), bodyData)
|
||||
} else {
|
||||
fmt.Printf("sqlmap -u %s --data='%s' -H '%s'\n", u.String(), bodyData, strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
}
|
||||
} else if strings.ToLower(prepareFor) == "ffuf" {
|
||||
// TODO
|
||||
} else {
|
||||
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
|
||||
}
|
||||
|
||||
} else if os.Args[1] == "endpoints" {
|
||||
|
||||
for k := range newDoc.Paths {
|
||||
if !pathMap[k] {
|
||||
paths = append(paths, basePathResult+k)
|
||||
pathMap[k] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.Def.Servers) > 1 {
|
||||
if !quiet && (os.Args[1] != "endpoints") {
|
||||
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 len(s.Def.Servers) > 1 && 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 = ""
|
||||
}
|
||||
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)
|
||||
}
|
||||
s.URL = *u
|
||||
s = s.IterateOverPaths(client)
|
||||
}
|
||||
|
||||
if os.Args[1] == "automate" {
|
||||
@@ -307,7 +189,7 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
|
||||
}
|
||||
}
|
||||
} else if outfile == "" && strings.ToLower(outputFormat) == "json" {
|
||||
results := strings.Join(resultsJSON, ",")
|
||||
results := strings.Join(s.ResultsJSON, ",")
|
||||
fmt.Printf(`%s"results":[%s]}%s`, specJSON, results, "\n")
|
||||
} else if outfile != "" {
|
||||
if accessibleEndpoints == nil {
|
||||
@@ -318,34 +200,204 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
|
||||
log.Infof("Results written to %s", outfile)
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
return s.Paths
|
||||
}
|
||||
|
||||
func GetBasePath(servers openapi3.Servers, host string) (bp string) {
|
||||
if basePath == "" {
|
||||
if servers != nil {
|
||||
s1, _ := url.Parse(servers[0].URL)
|
||||
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,
|
||||
}
|
||||
|
||||
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)
|
||||
for method, op := range operations {
|
||||
// Do all the things here :D
|
||||
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" {
|
||||
_, _, sc := MakeRequest(client, method, s.URL.String(), timeout, bytes.NewReader(s.BodyData))
|
||||
if sc == 200 {
|
||||
accessibleEndpointFound = true
|
||||
accessibleEndpoints = append(accessibleEndpoints, s.URL.String())
|
||||
}
|
||||
if outfile != "" {
|
||||
if getAccessibleEndpoints {
|
||||
if sc == 200 {
|
||||
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)])
|
||||
WriteJSONFile(result, false)
|
||||
}
|
||||
} else {
|
||||
result := fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"},`, sc, s.URL.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 {
|
||||
s.ResultsJSON = append(s.ResultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)]))
|
||||
}
|
||||
} else {
|
||||
if sc == 1 {
|
||||
s.ResultsJSON = append(s.ResultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"Skipped due to dangerous keyword in request"}`, sc, s.URL.String(), method))
|
||||
} else {
|
||||
s.ResultsJSON = append(s.ResultsJSON, fmt.Sprintf(`{"status_code":"%d","url":"%s","method":"%s","details":"%s"}`, sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)]))
|
||||
}
|
||||
}
|
||||
} else if !getAccessibleEndpoints && strings.ToLower(outputFormat) == "console" {
|
||||
writeLog(sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)])
|
||||
}
|
||||
if servers[0].URL == "/" {
|
||||
basePath = "/"
|
||||
} else {
|
||||
basePath = servers[0].URL
|
||||
if strings.Contains(host, ":") {
|
||||
hostPortStart := strings.Index(host, ":")
|
||||
host = host[0:hostPortStart]
|
||||
}
|
||||
} else if os.Args[1] == "prepare" {
|
||||
if strings.ToLower(prepareFor) == "curl" {
|
||||
if s.BodyData == nil {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("curl -sk -X %s '%s'\n", method, s.URL.String())
|
||||
} else {
|
||||
fmt.Printf("curl -sk -X %s '%s' -H '%s'\n", method, s.URL.String(), strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
if strings.Contains(basePath, host) || strings.Contains(basePath, "http") {
|
||||
basePath = strings.ReplaceAll(basePath, host, "")
|
||||
} else {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("curl -sk -X %s '%s' -d '%s'\n", method, s.URL.String(), s.BodyData)
|
||||
} else {
|
||||
fmt.Printf("curl -sk -X %s '%s' -d '%s' -H '%s'\n", method, s.URL.String(), s.BodyData, strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
}
|
||||
} else if strings.ToLower(prepareFor) == "sqlmap" {
|
||||
if s.BodyData == nil {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("sqlmap -u %s\n", s.URL.String())
|
||||
} else {
|
||||
fmt.Printf("sqlmap -u %s -H '%s'\n", s.URL.String(), strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
} else {
|
||||
if len(Headers) == 0 {
|
||||
fmt.Printf("sqlmap -u %s --data='%s'\n", s.URL.String(), s.BodyData)
|
||||
} else {
|
||||
fmt.Printf("sqlmap -u %s --data='%s' -H '%s'\n", s.URL.String(), s.BodyData, strings.Join(Headers, "' -H '"))
|
||||
}
|
||||
}
|
||||
} else if strings.ToLower(prepareFor) == "ffuf" {
|
||||
// TODO
|
||||
} else {
|
||||
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
|
||||
}
|
||||
|
||||
} 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
|
||||
}
|
||||
|
||||
func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRequest {
|
||||
for _, param := range op.Parameters {
|
||||
if param.Ref != "" || param.Value == nil {
|
||||
continue
|
||||
} else if param.Value.In == "path" {
|
||||
s.Path = strings.ReplaceAll(s.Path, "{"+param.Value.Name+"}", "test")
|
||||
} else if param.Value.In == "query" {
|
||||
if param.Value.Schema.Ref != "" {
|
||||
s.Query.Add(param.Value.Name, "test") // TODO: Implement actual definition/schema
|
||||
} else {
|
||||
if param.Value.Schema.Value.Type == "string" {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
} else {
|
||||
s.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" {
|
||||
s.Body[param.Value.Name] = "test"
|
||||
} else {
|
||||
s.Body[param.Value.Name] = 1
|
||||
}
|
||||
s.BodyData, _ = json.Marshal(s.Body)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if op.RequestBody != nil {
|
||||
s.Body["test"] = "test"
|
||||
s.BodyData, _ = json.Marshal(s.Body)
|
||||
}
|
||||
|
||||
if s.ApiInQuery && s.ApiKey != "" {
|
||||
s.Query.Add(s.ApiKeyName, s.ApiKey)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SwaggerRequest) GetBasePath() string {
|
||||
if strings.Contains(s.URL.Host, ":") {
|
||||
hostPortStart := strings.Index(s.URL.Host, ":")
|
||||
s.URL.Host = s.URL.Host[0:hostPortStart]
|
||||
}
|
||||
if basePath == "" {
|
||||
if s.Def.Servers != nil {
|
||||
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://", "")
|
||||
}
|
||||
@@ -388,6 +440,9 @@ func SetScheme(swaggerURL string) (scheme string) {
|
||||
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://") {
|
||||
@@ -414,8 +469,14 @@ func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
|
||||
_ = yaml.Unmarshal(bodyBytes, &doc)
|
||||
_ = yaml.Unmarshal(bodyBytes, &doc3)
|
||||
}
|
||||
_ = json.Unmarshal(bodyBytes, &doc)
|
||||
_ = json.Unmarshal(bodyBytes, &doc3)
|
||||
err := json.Unmarshal(bodyBytes, &doc)
|
||||
if err != nil {
|
||||
err = json.Unmarshal(bodyBytes, &doc3)
|
||||
if err != nil {
|
||||
log.Fatalf("Error unmarshalling API definitions: %s\n", err)
|
||||
}
|
||||
}
|
||||
err = json.Unmarshal(bodyBytes, &doc3)
|
||||
|
||||
if strings.HasPrefix(doc3.OpenAPI, "3") {
|
||||
newDoc := &doc3
|
||||
@@ -430,7 +491,7 @@ func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
|
||||
var noDoc openapi3.T
|
||||
return &noDoc
|
||||
} else {
|
||||
log.Fatal("Error parsing definition file.\n")
|
||||
log.Fatalf("Error parsing definition file: %s\n", doc3.OpenAPI)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 498 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 186 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 480 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 123 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 386 KiB |
Reference in new issue
Block a user