Compare commits

..
11 Commits
7 changed files with 336 additions and 132 deletions

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+4 -1
View File
@@ -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
+2 -2
View File
@@ -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
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.5.2",
Version: "1.6.2",
}
func Execute() {
-31
View File
@@ -1,31 +0,0 @@
package cmd
import (
"fmt"
"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
}
}
}
}
}
+271 -97
View File
@@ -6,6 +6,7 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"net/url"
"os"
@@ -38,77 +39,6 @@ type SwaggerRequest struct {
var accessibleEndpoints []string
var specJSON string
func CheckAndConfigureProxy() (client http.Client) {
var proxyUrl *url.URL
transport := &http.Transport{}
if insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if proxy != "NOPROXY" {
proxyUrl, _ = url.Parse(proxy)
transport.Proxy = http.ProxyURL(proxyUrl)
}
client = http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return client
}
func ExtractSpecFromJS(bodyBytes []byte) []byte {
var openApiIndex int
var specClose int
var bodyString, spec string
bodyString = string(bodyBytes)
spec = strings.ReplaceAll(bodyString, "\n", "")
spec = strings.ReplaceAll(spec, "\t", "")
spec = strings.ReplaceAll(spec, " ", "")
if strings.Contains(strings.ReplaceAll(bodyString, " ", ""), `"swagger":"2.0"`) {
openApiIndex = strings.Index(spec, `"swagger":`) - 1
specClose = strings.LastIndex(spec, "]}") + 2
var doc2 openapi2.T
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc2)
if !strings.Contains(doc2.Swagger, "2") {
specClose = strings.LastIndex(spec, "}") + 1
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc2)
if !strings.Contains(doc2.Swagger, "2") {
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
}
}
} else if strings.Contains(strings.ReplaceAll(bodyString, " ", ""), `"openapi":"3`) {
openApiIndex = strings.Index(spec, `"openapi":`) - 1
specClose = strings.LastIndex(spec, "]}") + 2
var doc3 openapi3.T
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc3)
if !strings.Contains(doc3.OpenAPI, "3") {
specClose = strings.LastIndex(spec, "}") + 1
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc3)
if !strings.Contains(doc3.OpenAPI, "3") {
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
}
}
} else {
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
}
return bodyBytes
}
func GenerateRequests(bodyBytes []byte, client http.Client) []string {
var s SwaggerRequest
@@ -117,8 +47,6 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
u, _ := url.Parse(swaggerURL)
s.URL = *u
// BuildObjectsFromSchemaDefinitions(*newDoc) TODO
if os.Args[1] != "endpoints" {
// Prints Title/Description values if they exist
PrintSpecInfo(*s.Def.Info)
@@ -134,11 +62,13 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
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)
if apiTarget == "" {
log.Warn("Multiple servers detected in documentation. You can manually set a server to test with the -T flag.\nThe detected servers are as follows:")
for i, server := range s.Def.Servers {
fmt.Printf("Server %d: %s\n", i+1, server.URL)
}
fmt.Println()
}
fmt.Println()
}
if len(s.Def.Servers) > 1 && apiTarget == "" {
for _, server := range s.Def.Servers {
@@ -218,6 +148,7 @@ func (s SwaggerRequest) IterateOverPaths(client http.Client) SwaggerRequest {
for method, op := range operations {
// Do all the things here :D
s.BodyData = nil
if op != nil {
s.URL.Path = s.Path + path
s = s.BuildDefinedRequests(client, method, pathItem, op)
@@ -335,17 +266,32 @@ func (s SwaggerRequest) BuildDefinedRequests(client http.Client, method string,
return s
}
// This whole function needs to be refactored/cleaned up a bit
func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRequest {
for _, param := range op.Parameters {
if param.Ref != "" || param.Value == nil {
if param.Value == nil && param.Value.Schema.Ref == "" {
continue
} else if param.Value.In == "path" {
s.Path = strings.ReplaceAll(s.Path, "{"+param.Value.Name+"}", "test")
if param.Value.Schema.Ref != "" {
s.SetParametersFromSchema(param, "path", param.Value.Schema.Ref, nil)
} else if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "test")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "test")
}
} else {
if strings.Contains(s.URL.Path, param.Value.Name) {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+param.Value.Name+"}", "1")
} else {
s.URL.Path = strings.ReplaceAll(s.URL.Path, "{"+strings.ToLower(param.Value.Name)+"}", "1")
}
}
} else if param.Value.In == "query" {
if param.Value.Schema.Ref != "" {
s.Query.Add(param.Value.Name, "test") // TODO: Implement actual definition/schema
s.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil)
} else {
if param.Value.Schema.Value.Type == "string" {
if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
s.Query.Add(param.Value.Name, "test")
} else {
s.Query.Add(param.Value.Name, "1")
@@ -354,20 +300,104 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
} 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.Ref != "" {
s.SetParametersFromSchema(param, "body", param.Value.Schema.Ref, nil)
}
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)
var data []string
for k, v := range s.Body {
data = append(data, fmt.Sprintf("%s=%s", k, v))
}
s.BodyData = []byte(strings.Join(data, "&"))
} else {
continue
}
}
if op.RequestBody != nil {
s.Body["test"] = "test"
s.BodyData, _ = json.Marshal(s.Body)
if op.RequestBody.Value.Content != nil {
for i := range op.RequestBody.Value.Content {
if contentType == "" && Headers != nil && strings.Contains(strings.ToLower(strings.Join(Headers, ",")), "content-type") {
headerString := strings.Join(Headers, ",")
Headers = nil
ctIndex := strings.Index(strings.ToLower(headerString), "content-type:") + 14
headerString = headerString[ctIndex:]
if strings.Contains(headerString, ",") {
headerString = strings.TrimPrefix(headerString, ",")
ctEndIndex := strings.Index(headerString[ctIndex:], ",") + 1
headerString = headerString[:ctEndIndex]
} else if !strings.Contains(strings.ToLower(headerString), "content-type") && (strings.Contains(strings.ToLower(headerString), "application/x-www-form-urlencoded") || strings.Contains(strings.ToLower(headerString), "application/json")) {
headerString = ""
}
if headerString != "" {
Headers = append(Headers, strings.Split(headerString, ",")...)
}
}
if op.RequestBody.Value.Content.Get(i).Schema.Value == nil {
s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Ref, op.RequestBody)
if strings.Contains(i, "json") {
s.BodyData, _ = json.Marshal(s.Body)
}
} else if i == "application/x-www-form-urlencoded" {
if contentType == "" && !strings.Contains(strings.ToLower(strings.Join(Headers, "")), "content-type") {
Headers = append(Headers, "Content-Type: application/x-www-form-urlencoded")
}
var formData []string
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
if valueType == "string" {
s.Body[j] = "test"
} else if valueType == "boolean" {
s.Body[j] = false
} else if valueType == "integer" || valueType == "number" {
s.Body[j] = 1
} else {
s.Body[j] = "unknown_type_populate_manually"
}
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
}
}
s.BodyData = []byte(strings.Join(formData, "&"))
} else if strings.Contains(i, "json") {
if contentType == "" && !strings.Contains(strings.ToLower(strings.Join(Headers, "")), "content-type") {
Headers = append(Headers, "Content-Type: application/json")
}
for j := range op.RequestBody.Value.Content.Get(i).Schema.Value.Properties {
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
if valueType == "string" {
s.Body[j] = "test"
} else if valueType == "boolean" {
s.Body[j] = false
} else if valueType == "integer" || valueType == "number" {
s.Body[j] = 1
} else {
s.Body[j] = "unknown_type_populate_manually"
}
}
}
s.BodyData, _ = json.Marshal(s.Body)
} else if strings.Contains(i, "multipart/form-data") {
//TODO
s.Body["MULTIPART_FORM_DATA"] = "TEST_MANUALLY"
s.BodyData = []byte("MULTIPART_FORM_DATA=TEST_MANUALLY")
} else if strings.Contains(i, "xml") {
//TODO
s.Body["XML"] = "TEST_MANUALLY"
s.BodyData = []byte("XML=TEST_MANUALLY")
} else {
fmt.Println(i)
s.Body["test"] = "test"
s.BodyData = []byte("test=test")
}
}
}
}
if s.ApiInQuery && s.ApiKey != "" {
@@ -376,13 +406,91 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
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]
func (s SwaggerRequest) SetParametersFromSchema(param *openapi3.ParameterRef, location, schemaRef string, req *openapi3.RequestBodyRef) {
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].Value.Type == "string" {
if location == "path" {
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 != "" {
s.SetParametersFromSchema(nil, "body", schema.Value.Properties[property].Ref, req)
} 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[schema.Value.Title] = value
}
}
}
}
func (s SwaggerRequest) GetBasePath() string {
if basePath == "" {
if s.Def.Servers != nil {
if strings.Contains(s.Def.Servers[0].URL, ":") {
var schemeIndex int
if strings.Contains(s.Def.Servers[0].URL, "://") {
schemeIndex = strings.Index(s.Def.Servers[0].URL, "://") + 3
} else {
schemeIndex = 0
}
s.URL.Host = s.Def.Servers[0].URL[schemeIndex:]
}
if s.Def.Servers[0].URL == "/" {
basePath = "/"
} else if strings.Contains(s.Def.Servers[0].URL, "http") && !strings.Contains(s.Def.Servers[0].URL, s.URL.Host) { // Check to see if the server object being used for the base path contains a different host than the target
@@ -469,14 +577,9 @@ func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
_ = yaml.Unmarshal(bodyBytes, &doc)
_ = yaml.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)
_ = json.Unmarshal(bodyBytes, &doc)
_ = json.Unmarshal(bodyBytes, &doc3)
if strings.HasPrefix(doc3.OpenAPI, "3") {
newDoc := &doc3
@@ -491,7 +594,7 @@ func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
var noDoc openapi3.T
return &noDoc
} else {
log.Fatalf("Error parsing definition file: %s\n", doc3.OpenAPI)
log.Fatal("Error parsing definition file.")
return nil
}
}
@@ -540,3 +643,74 @@ func WriteJSONFile(result string, end bool) {
}
}
func ExtractSpecFromJS(bodyBytes []byte) []byte {
var openApiIndex int
var specClose int
var bodyString, spec string
bodyString = string(bodyBytes)
spec = strings.ReplaceAll(bodyString, "\n", "")
spec = strings.ReplaceAll(spec, "\t", "")
spec = strings.ReplaceAll(spec, " ", "")
if strings.Contains(strings.ReplaceAll(bodyString, " ", ""), `"swagger":"2.0"`) {
openApiIndex = strings.Index(spec, `"swagger":`) - 1
specClose = strings.LastIndex(spec, "]}") + 2
var doc2 openapi2.T
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc2)
if !strings.Contains(doc2.Swagger, "2") {
specClose = strings.LastIndex(spec, "}") + 1
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc2)
if !strings.Contains(doc2.Swagger, "2") {
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
}
}
} else if strings.Contains(strings.ReplaceAll(bodyString, " ", ""), `"openapi":"3`) {
openApiIndex = strings.Index(spec, `"openapi":`) - 1
specClose = strings.LastIndex(spec, "]}") + 2
var doc3 openapi3.T
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc3)
if !strings.Contains(doc3.OpenAPI, "3") {
specClose = strings.LastIndex(spec, "}") + 1
bodyBytes = []byte(spec[openApiIndex:specClose])
_ = json.Unmarshal(bodyBytes, &doc3)
if !strings.Contains(doc3.OpenAPI, "3") {
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
}
}
} else {
log.Error("Error parsing JavaScript file for spec. Try saving the object as a JSON file and reference it locally.")
}
return bodyBytes
}
func CheckAndConfigureProxy() (client http.Client) {
var proxyUrl *url.URL
transport := &http.Transport{}
if insecure {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if proxy != "NOPROXY" {
proxyUrl, _ = url.Parse(proxy)
transport.Proxy = http.ProxyURL(proxyUrl)
}
client = http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
return client
}