Compare commits

..
8 Commits
7 changed files with 483 additions and 298 deletions

No files matched your search

+5
View File
@@ -11,6 +11,11 @@ builds:
- linux
- windows
- darwin
flags:
- -trimpath
ldflags:
- -s -w -X main.build={{.Version}}
archives:
- format: tar.gz
+8
View File
@@ -13,6 +13,9 @@ import (
var getAccessibleEndpoints bool
var outputFormat string
var responsePreviewLength int
var verbose bool
var automateCmd = &cobra.Command{
Use: "automate",
Short: "Sends a series of automated requests to the discovered endpoints.",
@@ -24,6 +27,8 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
if outfile != "" && strings.ToLower(outputFormat) != "" {
if !strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) != "json" {
log.Fatal("Only the JSON output format is supported at the moment.")
} else if strings.HasSuffix(strings.ToLower(outfile), "json") && strings.ToLower(outputFormat) == "console" {
outputFormat = "json"
}
}
@@ -53,4 +58,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
func init() {
automateCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "console", "The output format. Only 'console' (default) and 'json' are supported at the moment.")
automateCmd.PersistentFlags().BoolVar(&getAccessibleEndpoints, "get-accessible-endpoints", false, "Only output the accessible endpoints (those that return a 200 status code).")
automateCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose mode, which shows a preview of each response.")
automateCmd.PersistentFlags().IntVar(&responsePreviewLength, "response-preview-length", 50, "sets the response preview length when using verbose output.")
}
+119 -14
View File
@@ -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)
}
+1 -1
View File
@@ -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])
}
}
+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.6.2",
Version: "1.7.2",
}
func Execute() {
+119
View File
@@ -0,0 +1,119 @@
package cmd
import (
"fmt"
"math/rand"
"strings"
"github.com/getkin/kin-openapi/openapi3"
log "github.com/sirupsen/logrus"
)
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
}
+230 -282
View File
@@ -1,17 +1,15 @@
package cmd
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/json"
"encoding/xml"
"fmt"
"math/rand"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
@@ -37,7 +35,6 @@ type SwaggerRequest struct {
}
var accessibleEndpoints []string
var specJSON string
func GenerateRequests(bodyBytes []byte, client http.Client) []string {
@@ -52,10 +49,6 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
PrintSpecInfo(*s.Def.Info)
}
if os.Args[1] == "automate" && outfile != "" {
WriteJSONFile(`"results":[`, false)
}
if s.Def.Paths == nil {
log.Fatalf("Could not find any defined operations. Review the file manually.")
}
@@ -70,7 +63,7 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
fmt.Println()
}
}
if len(s.Def.Servers) > 1 && apiTarget == "" {
if apiTarget == "" {
for _, server := range s.Def.Servers {
if outputFormat == "console" && os.Args[1] == "automate" {
log.Warnf("Results for %s:\n", server.URL)
@@ -79,6 +72,11 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
if server.URL == "/" {
s.Path = ""
}
if strings.Contains(server.URL, "localhost") || strings.Contains(server.URL, "127.0.0.1") || strings.Contains(server.URL, "::1") {
log.Warn("The server(s) documented in the definition file contain(s) a local host value and may result in errors. Supply a target manually using the '-T' flag.")
}
u, _ := url.Parse(server.URL)
s.URL = *u
s = s.IterateOverPaths(client)
@@ -94,6 +92,13 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
} else {
if apiTarget != "" {
u, _ = url.Parse(apiTarget)
} else {
if swaggerURL == "" {
u, _ = url.Parse(s.Def.Servers[0].URL)
if strings.Contains(s.Def.Servers[0].URL, "localhost") || strings.Contains(s.Def.Servers[0].URL, "127.0.0.1") || strings.Contains(s.Def.Servers[0].URL, "::1") {
log.Warn("The server documented in the definition file contains a local host value and may result in errors. Supply a target manually using the '-T' flag.")
}
}
}
s.URL = *u
s = s.IterateOverPaths(client)
@@ -118,16 +123,6 @@ func GenerateRequests(bodyBytes []byte, client http.Client) []string {
isDuplicateEndpoint = false
}
}
} else if outfile == "" && strings.ToLower(outputFormat) == "json" {
results := strings.Join(s.ResultsJSON, ",")
fmt.Printf(`%s"results":[%s]}%s`, specJSON, results, "\n")
} else if outfile != "" {
if accessibleEndpoints == nil {
WriteJSONFile("]}\n", false)
} else {
WriteJSONFile(",", true)
}
log.Infof("Results written to %s", outfile)
}
}
return s.Paths
@@ -186,74 +181,25 @@ func (s SwaggerRequest) BuildDefinedRequests(client http.Client, method string,
s.URL.RawQuery = s.Query.Encode()
if os.Args[1] == "automate" {
_, _, sc := MakeRequest(client, method, s.URL.String(), timeout, bytes.NewReader(s.BodyData))
_, resp, 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 strings.ToLower(outputFormat) != "console" {
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)
writeLog(sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)], resp)
}
} 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)
writeLog(sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)], resp)
}
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)])
}
}
} 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 '"))
}
} 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.")
writeLog(sc, s.URL.String(), method, errorDescriptions[fmt.Sprint(sc)], resp)
}
//}
} else if os.Args[1] == "prepare" {
s.PrintPreparedCommands(method)
} else if os.Args[1] == "endpoints" {
for k := range s.Def.Paths {
@@ -272,36 +218,49 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
if param.Value == nil && param.Value.Schema.Ref == "" {
continue
} else if param.Value.In == "path" {
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 param.Value.Schema != nil {
if param.Value.Schema.Ref != "" {
s = s.SetParametersFromSchema(param, "path", param.Value.Schema.Ref, nil, 0)
} 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 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.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil)
} else {
if param.Value.Schema.Value.Type != "" && param.Value.Schema.Value.Type == "string" {
s.Query.Add(param.Value.Name, "test")
if param.Value.Schema != nil {
if param.Value.Schema.Ref != "" {
s = s.SetParametersFromSchema(param, "query", param.Value.Schema.Ref, nil, 0)
} else {
s.Query.Add(param.Value.Name, "1")
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")
}
}
} else {
s.Query.Add(param.Value.Name, "test")
}
} else if param.Value.In == "header" && param.Value.Required {
} else if param.Value.In == "header" && param.Value.Required && strings.ToLower(param.Value.Name) != "content-type" {
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)
s = s.SetParametersFromSchema(param, "body", param.Value.Schema.Ref, nil, 0)
}
if param.Value.Schema.Value.Type == "string" {
s.Body[param.Value.Name] = "test"
@@ -321,80 +280,116 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
if op.RequestBody != nil {
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")
if contentType == "" {
EnforceSingleContentType(i)
} else {
fmt.Println(i)
s.Body["test"] = "test"
s.BodyData = []byte("test=test")
EnforceSingleContentType(contentType)
}
if op.RequestBody.Value.Content.Get(i).Schema != nil {
if op.RequestBody.Value.Content.Get(i).Schema.Value == nil {
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Ref, op.RequestBody, 0)
if strings.Contains(i, "json") {
s.BodyData, _ = json.Marshal(s.Body)
} else if strings.Contains(i, "x-www-form-urlencoded") {
var formData []string
for j := range s.Body {
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
}
s.BodyData = []byte(strings.Join(formData, "&"))
} else if strings.Contains(i, "xml") {
type Element struct {
XMLName xml.Name
Content any `xml:",chardata"`
}
type Root struct {
XMLName xml.Name `xml:"root"`
Elements []Element `xml:",any"`
}
var elements []Element
for key, value := range s.Body {
elements = append(elements, Element{
XMLName: xml.Name{Local: key},
Content: value,
})
}
root := Root{
Elements: elements,
}
xmlData, err := xml.Marshal(root)
if err != nil {
log.Warn("Error marshalling XML data.")
}
s.BodyData = xmlData
} else {
log.Warnf("Content type not supported. Test this path manually: %s (Content type: %s)\n", s.URL.Path, i)
}
} else {
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].Ref != "" {
s = s.SetParametersFromSchema(nil, "body", op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Ref, op.RequestBody, 0)
} else {
var valueType string = op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value.Type
if op.RequestBody.Value.Content.Get(i).Schema.Value.Properties[j].Value != nil {
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"
}
if i == "application/x-www-form-urlencoded" {
formData = append(formData, fmt.Sprintf("%s=%s", j, fmt.Sprint(s.Body[j])))
}
}
if i == "application/x-www-form-urlencoded" {
s.BodyData = []byte(strings.Join(formData, "&"))
} else if strings.Contains(i, "json") || i == "*/*" {
s.BodyData, _ = json.Marshal(s.Body)
} else if strings.Contains(i, "xml") {
//
type Element struct {
XMLName xml.Name
Content any `xml:",chardata"`
}
type Root struct {
XMLName xml.Name `xml:"root"`
Elements []Element `xml:",any"`
}
var elements []Element
for key, value := range s.Body {
elements = append(elements, Element{
XMLName: xml.Name{Local: key},
Content: value,
})
}
root := Root{
Elements: elements,
}
xmlData, err := xml.Marshal(root)
if err != nil {
log.Warn("Error marshalling XML data.")
}
s.BodyData = xmlData
} else {
s.Body["test"] = "test"
s.BodyData = []byte("test=test")
}
}
}
}
}
}
}
@@ -406,76 +401,31 @@ func (s SwaggerRequest) AddParametersToRequest(op *openapi3.Operation) SwaggerRe
return s
}
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
}
}
}
func (s SwaggerRequest) PrintPreparedCommands(method string) {
var preparedCommand string
if strings.ToLower(prepareFor) == "curl" {
preparedCommand = fmt.Sprintf("curl -sk -X %s '%s'", method, s.URL.String())
} else if strings.ToLower(prepareFor) == "sqlmap" {
preparedCommand = fmt.Sprintf("sqlmap --method=%s -u %s", method, s.URL.String())
} else if strings.ToLower(prepareFor) == "ffuf" {
log.Fatal("ffuf is not supported yet :(")
// TODO
} else {
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
}
log.Fatal("External tool not supported. Only 'curl' and 'sqlmap' are supported options for the '-e' flag at this time.")
}
if s.BodyData != nil {
if strings.ToLower(prepareFor) == "sqlmap" {
preparedCommand = preparedCommand + fmt.Sprintf(" --data='%s'", s.BodyData)
} else {
preparedCommand = preparedCommand + fmt.Sprintf(" -d '%s'", s.BodyData)
}
}
if len(Headers) != 0 {
preparedCommand = preparedCommand + fmt.Sprintf(" -H '%s'", strings.Join(Headers, "' -H '"))
}
fmt.Println(preparedCommand)
}
func (s SwaggerRequest) GetBasePath() string {
@@ -519,10 +469,7 @@ func (s SwaggerRequest) GetBasePath() string {
}
func PrintSpecInfo(i openapi3.Info) {
specJSON = fmt.Sprintf(`{"title":"%s","description":"%s",`, i.Title, i.Description)
if outfile != "" {
WriteJSONFile(specJSON, false)
} else if strings.ToLower(outputFormat) == "console" {
if strings.ToLower(outputFormat) == "console" {
if i.Title != "" {
fmt.Println("Title:", i.Title)
}
@@ -534,6 +481,30 @@ func PrintSpecInfo(i openapi3.Info) {
if i.Title == "" && i.Description == "" {
log.Warnf("Detected possible error in parsing the definition file. Title and description values are empty.\n\n")
}
} else if strings.ToLower(outputFormat) == "json" {
titleDescriptionLogger := log.New()
titleDescriptionLogger.SetFormatter(&log.JSONFormatter{DisableTimestamp: true, DisableHTMLEscape: true})
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()
titleDescriptionLogger.SetOutput(file)
}
if i.Title != "" {
titleDescriptionLogger.WithField("Title:", i.Title).Print("N/A")
}
if i.Description != "" {
titleDescriptionLogger.WithField("Description:", i.Description).Print("N/A")
}
if i.Title == "" && i.Description == "" {
log.Warn("Detected possible error in parsing the definition file. Title and description values are empty.")
}
}
}
@@ -599,51 +570,6 @@ func UnmarshalSpec(bodyBytes []byte) (newDoc *openapi3.T) {
}
}
func WriteJSONFile(result string, end bool) {
if end {
file, err := os.OpenFile(outfile, os.O_RDWR, 0644)
if err != nil {
fmt.Println("File does not exist or cannot be created")
os.Exit(1)
}
defer file.Close()
w := bufio.NewWriter(file)
fileInfo, err := file.Stat()
if err != nil {
log.Error("Failed to get file info:", err)
}
fileSize := fileInfo.Size()
if fileSize != 0 {
lastChar := fileSize - 1
_, err = file.Seek(lastChar, 0)
if err != nil {
log.Error("Error seeking to last character", err)
}
fmt.Fprintf(w, "]}\n")
}
w.Flush()
} else {
file, err := os.OpenFile(outfile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644)
if err != nil {
fmt.Println("File does not exist or cannot be created")
os.Exit(1)
}
defer file.Close()
w := bufio.NewWriter(file)
fmt.Fprintf(w, "%s", result)
w.Flush()
}
}
func ExtractSpecFromJS(bodyBytes []byte) []byte {
var openApiIndex int
var specClose int
@@ -714,3 +640,25 @@ func CheckAndConfigureProxy() (client http.Client) {
}
return client
}
func EnforceSingleContentType(newContentType string) {
newContentType = strings.TrimSpace(newContentType)
if Headers != nil {
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(headerString, ":") {
headerString = ""
}
if headerString != "" {
Headers = append(Headers, strings.Split(headerString, ",")...)
}
}
Headers = append(Headers, "Content-Type: "+newContentType)
}