mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-10 17:09:13 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bbf0453a5 | ||
|
|
a39028b463 | ||
|
|
e22c11c118 | ||
|
|
528a771c87 | ||
|
|
c85512dfe0 | ||
|
|
6a01afb367 | ||
|
|
7ac01dc2a1 | ||
|
|
9b48aab4fb | ||
|
|
8ce3ca7aa5 | ||
|
|
62be170a97 | ||
|
|
a290716c76 | ||
|
|
927b2adf53 | ||
|
|
cecaccc07a | ||
|
|
54413d32e0 | ||
|
|
c399ca1ff8 | ||
|
|
45dd989ef3 | ||
|
|
1474d660e4 | ||
|
|
fe597147e6 | ||
|
|
15cc756a03 |
No files matched your search
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -11,6 +11,11 @@ builds:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X main.build={{.Version}}
|
||||
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
sj is a command line tool designed to assist with auditing of exposed Swagger/OpenAPI definition files by checking the associated API endpoints for weak authentication. It also provides command templates for manual vulnerability testing.
|
||||
|
||||
It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of three commands:
|
||||
It does this by parsing the definition file for paths, parameters, and accepted methods before using the results with one of four commands:
|
||||
- `automate` - Crafts a series of requests and analyzes the status code of the response.
|
||||
- `prepare` - Generates a list of commands to use for manual testing.
|
||||
- `endpoints` - Generates a list of raw API routes. *Path values will not be replaced with test data*.
|
||||
@@ -26,6 +26,9 @@ 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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
}
|
||||
+119
-14
@@ -1,27 +1,81 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func writeLog(sc int, target, method string, errorMsg string) {
|
||||
if sc != 200 {
|
||||
if sc == 401 || sc == 403 {
|
||||
logUnauth(sc, target, method, errorMsg)
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logRedirect(sc, target, method)
|
||||
} else if sc == 0 {
|
||||
logBad(sc, target, method)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg)
|
||||
} else if sc == 1 {
|
||||
logDangerous(target, method)
|
||||
func writeLog(sc int, target, method, errorMsg, response string) {
|
||||
|
||||
tempResponsePreviewLength := responsePreviewLength
|
||||
|
||||
if len(response) < responsePreviewLength {
|
||||
responsePreviewLength = len(response)
|
||||
}
|
||||
|
||||
if outfile != "" {
|
||||
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
fmt.Println("Output file does not exist or cannot be created")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
log.SetOutput(file)
|
||||
}
|
||||
|
||||
if outputFormat == "console" {
|
||||
if strings.Contains(response, "\"") {
|
||||
log.SetFormatter(&log.TextFormatter{DisableQuote: true, DisableTimestamp: true})
|
||||
} else {
|
||||
logManual(sc, target, method, errorMsg)
|
||||
log.SetFormatter(&log.TextFormatter{DisableTimestamp: true})
|
||||
}
|
||||
} else {
|
||||
logAccessible(sc, target, method)
|
||||
log.SetFormatter(&log.JSONFormatter{DisableHTMLEscape: true, DisableTimestamp: true})
|
||||
}
|
||||
|
||||
if verbose {
|
||||
if sc != 200 {
|
||||
if sc == 401 || sc == 403 {
|
||||
logVerboseUnauth(sc, target, method, errorMsg, response)
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logVerboseRedirect(sc, target, method, response)
|
||||
} else if sc == 0 {
|
||||
logVerboseBad(sc, target, method, response)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg)
|
||||
} else if sc == 1 {
|
||||
logDangerous(target, method)
|
||||
} else {
|
||||
logVerboseManual(sc, target, method, errorMsg, response)
|
||||
}
|
||||
} else {
|
||||
logVerboseAccessible(sc, target, method, response)
|
||||
}
|
||||
} else {
|
||||
if sc != 200 {
|
||||
if sc == 401 || sc == 403 {
|
||||
logUnauth(sc, target, method, errorMsg)
|
||||
} else if sc == 301 || sc == 302 {
|
||||
logRedirect(sc, target, method)
|
||||
} else if sc == 0 {
|
||||
logBad(sc, target, method)
|
||||
} else if sc == 404 {
|
||||
logNotFound(sc, target, method, errorMsg)
|
||||
} else if sc == 1 {
|
||||
logDangerous(target, method)
|
||||
} else {
|
||||
logManual(sc, target, method, errorMsg)
|
||||
}
|
||||
} else {
|
||||
logAccessible(sc, target, method)
|
||||
}
|
||||
}
|
||||
responsePreviewLength = tempResponsePreviewLength
|
||||
}
|
||||
|
||||
func logAccessible(status int, target, method string) {
|
||||
@@ -32,6 +86,15 @@ func logAccessible(status int, target, method string) {
|
||||
}).Print("Endpoint accessible!")
|
||||
}
|
||||
|
||||
func logVerboseAccessible(status int, target, method, response string) {
|
||||
log.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Print("Endpoint accessible!")
|
||||
}
|
||||
|
||||
func logDangerous(target, method string) {
|
||||
log.WithFields(log.Fields{
|
||||
"Status": "skipped",
|
||||
@@ -51,6 +114,18 @@ func logManual(status int, target, method, errorMsg string) {
|
||||
}).Warn(errorMsg)
|
||||
}
|
||||
|
||||
func logVerboseManual(status int, target, method, errorMsg, response string) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Manual testing may be required."
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Warn(errorMsg)
|
||||
}
|
||||
|
||||
func logNotFound(status int, target, method, errorMsg string) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Endpoint not found."
|
||||
@@ -70,6 +145,15 @@ func logRedirect(status int, target, method string) {
|
||||
}).Error("Redirect detected. This likely requires authentication.")
|
||||
}
|
||||
|
||||
func logVerboseRedirect(status int, target, method, response string) {
|
||||
log.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[0:responsePreviewLength],
|
||||
}).Error("Redirect detected. This likely requires authentication.")
|
||||
}
|
||||
|
||||
func logBad(status int, target, method string) {
|
||||
log.WithFields(log.Fields{
|
||||
"Status": "N/A",
|
||||
@@ -78,6 +162,15 @@ func logBad(status int, target, method string) {
|
||||
}).Warn("Bad request (could not reach the target).")
|
||||
}
|
||||
|
||||
func logVerboseBad(status int, target, method, response string) {
|
||||
log.WithFields(log.Fields{
|
||||
"Status": "N/A",
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Warn("Bad request (could not reach the target).")
|
||||
}
|
||||
|
||||
func logUnauth(status int, target, method, errorMsg string) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Unauthorized."
|
||||
@@ -88,3 +181,15 @@ func logUnauth(status int, target, method, errorMsg string) {
|
||||
"Method": method,
|
||||
}).Error(errorMsg)
|
||||
}
|
||||
|
||||
func logVerboseUnauth(status int, target, method, errorMsg, response string) {
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Unauthorized."
|
||||
}
|
||||
log.WithFields(log.Fields{
|
||||
"Status": status,
|
||||
"Target": target,
|
||||
"Method": method,
|
||||
"Preview": response[:responsePreviewLength],
|
||||
}).Error(errorMsg)
|
||||
}
|
||||
+3
-3
@@ -104,7 +104,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,7 +140,7 @@ 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)
|
||||
@@ -182,7 +182,7 @@ func CheckContentType(client http.Client, target string) string {
|
||||
log.Printf("Error: %s - skipping request.", err)
|
||||
return ""
|
||||
} 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)
|
||||
|
||||
+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.5.2",
|
||||
Version: "1.7.2",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
|
||||
+106
-18
@@ -2,30 +2,118 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func BuildObjectsFromSchemaDefinitions(doc openapi3.T) {
|
||||
if doc.Components.Schemas != nil {
|
||||
log.Infof("Building test objects from schema definitions...\n\n")
|
||||
}
|
||||
|
||||
if doc.Components.Schemas != nil {
|
||||
for definition := range doc.Components.Schemas {
|
||||
log.Infof("%s accepts %d parameters:\n", definition, len(doc.Components.Schemas[definition].Value.Properties))
|
||||
for property := range doc.Components.Schemas[definition].Value.Properties {
|
||||
schemaDef := doc.Components.Schemas[definition]
|
||||
definitionProperty := schemaDef.Value.Properties[property]
|
||||
if definitionProperty.Value.Type == "string" {
|
||||
log.Infof("%s=%s\n", property, "test")
|
||||
} else if definitionProperty.Value.Type == "integer" {
|
||||
log.Infof("%s=%d\n", property, 1)
|
||||
} else if definitionProperty.Ref != "" {
|
||||
fmt.Println("[!] [!] [!] " + definitionProperty.Ref) // Item $ref within a property
|
||||
}
|
||||
type SchemaReference struct {
|
||||
Required []string
|
||||
Type string
|
||||
Properties struct {
|
||||
Property struct {
|
||||
Type string
|
||||
Format string
|
||||
Example string
|
||||
Reference string
|
||||
XML struct {
|
||||
Name string
|
||||
Namespace string
|
||||
Prefix string
|
||||
Attribute bool
|
||||
Wrapped bool
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSchemaReference(schema *openapi3.SchemaRef) {
|
||||
|
||||
}
|
||||
|
||||
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef, counter int) SwaggerRequest {
|
||||
if param != nil {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
|
||||
break
|
||||
}
|
||||
} else if location == "path" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(s.URL.Path, param.Value.Name) {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
|
||||
} else {
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
|
||||
}
|
||||
}
|
||||
} else if location == "query" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Query.Add(param.Value.Name, "test")
|
||||
} else {
|
||||
s.Query.Add(param.Value.Name, "1")
|
||||
}
|
||||
} else if location == "body" {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[param.Value.Name] = "test"
|
||||
} else {
|
||||
s.Body[param.Value.Name] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
if location == "path" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", fmt.Sprint(value))
|
||||
} else if location == "query" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Query.Add(param.Value.Name, fmt.Sprint(value))
|
||||
} else if location == "body" {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[param.Value.Name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
name := strings.TrimPrefix(schemaRef, "#/components/schemas/")
|
||||
if s.Def.Components.Schemas[name] != nil {
|
||||
schema := s.Def.Components.Schemas[name]
|
||||
if schema.Value.Properties != nil {
|
||||
for property := range schema.Value.Properties {
|
||||
if schema.Value.Properties[property].Ref != "" {
|
||||
if counter < 3 {
|
||||
s = s.SetParametersFromSchema(param, location, schema.Value.Properties[property].Ref, req, counter+1)
|
||||
} else {
|
||||
log.Warnf("Nested reference encountered for %s (Property: %s). Test this endpoint manually.\n", s.URL.Scheme+"://"+s.URL.Host+s.URL.Path, property)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if schema.Value.Properties[property].Value.Type == "string" {
|
||||
s.Body[property] = "test"
|
||||
} else {
|
||||
s.Body[property] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if schema.Value.Enum != nil {
|
||||
value := schema.Value.Enum[rand.Intn(len(schema.Value.Enum))]
|
||||
s.Body[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
+556
-434
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user