Compare commits

...
12 Commits
13 changed files with 537 additions and 355 deletions

No files matched your search

+6 -5
View File
@@ -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
+139 -7
View File
@@ -1,5 +1,7 @@
# 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:
@@ -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.
![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..."}
```
## Help
A full list of commands can be found by using the `--help` flag:
![Help Command](img/sj-help.gif)
```bash
$ sj --help
The process of reviewing and testing exposed API definition files is often tedious and requires a large investment of time for a thorough review.
sj (swaggerjacker) is a CLI tool that can be used to perform an initial check of API endpoints identified through exposed Swagger/OpenAPI definition files.
Once you determine what endpoints require authentication and which do not, you can use the "prepare" command to generate command templates for further (manual) testing.
Example usage:
Perform a quick check of endpoints which require authentication:
$ sj automate -u https://petstore.swagger.io/v2/swagger.json
Generate a list of commands to use for manual testing:
$ sj prepare -u https://petstore.swagger.io/v2/swagger.json
Generate a list of raw API routes for use with custom scripts:
$ sj endpoints -u https://petstore.swagger.io/v2/swagger.json
Perform a brute-force attack against the target to identify hidden definition files:
$ sj brute -u https://petstore.swagger.io
Usage:
sj [flags]
sj [command]
Available Commands:
automate Sends a series of automated requests to the discovered endpoints.
brute Sends a series of automated requests to discover hidden API operation definitions.
endpoints Prints a list of endpoints from the target.
help Help about any command
prepare Prepares a set of commands for manual testing of each endpoint.
Flags:
-a, --agent string Set the User-Agent string. (default "Swagger Jacker (github.com/BishopFox/sj)")
-b, --base-path string Set the API base path if not defined in the definition file (i.e. /V2/).
-f, --format string Declare the format of the definition file (json/yaml/yml/js). (default "json")
-H, --headers stringArray Add custom headers, separated by a colon ("Name: Value"). Multiple flags are accepted.
-h, --help help for sj
-i, --insecure Ignores server certificate validation.
-l, --local-file string Loads the documentation from a local file.
-o, --outfile string Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.
-p, --proxy string Proxy host and port. Example: http://127.0.0.1:8080 (default "NOPROXY")
-q, --quiet Do not prompt for user input - uses default values for all requests.
-r, --randomize-user-agent Randomizes the user agent string. Default is 'false'.
-s, --safe-word stringArray Avoids 'dangerous word' check for the specified word(s). Multiple flags are accepted.
-T, --target string Manually set a target for the requests to be made if separate from the host the documentation resides on.
-t, --timeout int Set the request timeout period. (default 30)
-u, --url string Loads the documentation file from a URL
-v, --version version for sj
Use "sj [command] --help" for more information about a command.
```
+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)
}
}
}
}
+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.")
}
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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

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