Compare commits

...
8 Commits
5 changed files with 237 additions and 158 deletions

No files matched your search

+2
View File
@@ -14,6 +14,7 @@ import (
var getAccessibleEndpoints bool
var outputFormat string
var responsePreviewLength int
var testString string
var verbose bool
var automateCmd = &cobra.Command{
@@ -74,6 +75,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().StringVar(&testString, "test-string", "bishopfox", "The string to use when testing endpoints with string values.")
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.")
+22 -10
View File
@@ -26,6 +26,7 @@ var endpointWordlist string
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 priorityURLs []string = []string{"/swagger.json", "/openapi.json", "/api-docs", "/swagger", "/docs", "/api/swagger.json", "/api/openapi.json", "/api-docs/swagger.json", "/api/schema/", "/webjars/swagger-ui/index.html", "/API/swagger/ui/index", "/swagger/ui/index", "/v2/swagger.json", "/v2/openapi.json", "/v2/api-docs", "/v3/api-docs", "/v3/openapi.json", "/public/api-merged.json", "/analytics/v1/swagger", "/api.json", "/api/4.0/swagger.json", "/api/api-doc/openapi.json", "/api/api-doc/openapi.yaml", "/api/doc.json", "/api/docs.json", "/api/swagger", "/api/swagger/ui/index", "/api/v1/swagger", "/api/v2/api-docs", "/api/v2/openapi.json", "/api/v2/swagger.json", "/api/v3/api-docs", "/api/v3/apispec", "/api/workorder/openapi.json", "/apidocs", "/audiences/v1/swagger", "/audittrail/v1/swagger", "/certification/v1/swagger", "/citrixapi/store/swagger.json", "/conferencetool/v1/swagger", "/course/v1/swagger", "/dcl_swagger.yaml", "/doc/doc.json", "/doc/swagger.json", "/docs/swagger.json", "/docs/v1/swagger.json", "/ecommerce/v1/swagger", "/enrollment/v1/swagger", "/externalids/v1/swagger", "/impact/v1/swagger", "/learn/v1/swagger", "/learningplan/v1/swagger", "/manage/v1/swagger", "/management/info", "/marketplace/v1/swagger", "/messenger/v1/swagger", "/notifications/v1/swagger", "/openapi", "/openapi/spec.json", "/otj/v1/swagger", "/pages/v1/swagger", "/poweruser/v1/swagger", "/proctoring/v1/swagger", "/report/v1/swagger", "/swagger-ui/index.html", "/swagger-ui/openapi.json", "/swagger.yaml", "/swagger/0.1.0/swagger.json", "/swagger/doc.json", "/swagger/latest/swagger.json", "/swagger/swagger.json", "/swagger/test/swagger.json", "/swagger/ui/index.html", "/swagger/v1/openapiv2.json", "/swagger/v1/swagger.json", "/swagger/v2/swagger.json", "/swagger/v4/swagger.json", "/v1/openapi.json", "/v1/swagger", "/v1/swagger.json", "/swagger/docs/v1", "/swagger/docs/v1.json", "/Api/swagger/docs/v1", "/swagger/v1/swagger.json", "/api/api-docs/swagger.json", "/api/docs/", "/api/docs", "/swagger-ui"}
var bruteCmd = &cobra.Command{
Use: "brute",
@@ -54,10 +55,11 @@ var bruteCmd = &cobra.Command{
}
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, "/")...)
allURLs = append(allURLs, makeURLs(target, priorityURLs, "", true)...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "", false)...)
allURLs = append(allURLs, makeURLs(target, javascriptEndpoints, ".js", false)...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, ".json", false)...)
allURLs = append(allURLs, makeURLs(target, jsonEndpoints, "/", false)...)
} else {
endpointList, err := os.Open(endpointWordlist)
if err != nil {
@@ -114,21 +116,31 @@ var bruteCmd = &cobra.Command{
}
// TODO: Check if (future implementation) automate flag is true and if so than call the 'sj automate' command with the discovered definition file.
} else {
log.Errorf("No definition file found for:\t%s\n", swaggerURL)
log.Errorf("\nNo definition file found for:\t%s\n", swaggerURL)
}
},
}
func makeURLs(target string, endpoints []string, fileExtension string) []string {
func makeURLs(target string, endpoints []string, fileExtension string, skipPrefix bool) []string {
urls := []string{}
for _, dir := range prefixDirs {
if !skipPrefix {
for _, dir := range prefixDirs {
for _, endpoint := range endpoints {
if dir == "" && endpoint == "" {
continue
}
targetURL := target + dir + endpoint + fileExtension
urls = append(urls, targetURL)
}
}
} else {
for _, endpoint := range endpoints {
if dir == "" && endpoint == "" {
if endpoint == "" {
continue
}
targetURL := target + dir + endpoint + fileExtension
targetURL := target + endpoint + fileExtension
urls = append(urls, targetURL)
}
}
return urls
+11 -14
View File
@@ -98,22 +98,19 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
continue
}
key := Headers[i][:delimIndex]
value := Headers[i][delimIndex+1:]
key := strings.TrimSpace(Headers[i][:delimIndex])
value := strings.TrimSpace(Headers[i][delimIndex+1:])
if len(Headers) == 2 {
if key == "User-Agent" {
UserAgent = value
}
if key == "Content-Type" {
contentType = value
}
if key == "Accept" {
accept = value
}
} else {
req.Header.Set(key, value)
if key == "User-Agent" {
UserAgent = value
}
if key == "Content-Type" {
contentType = value
}
if key == "Accept" {
accept = value
}
req.Header.Set(key, value)
}
// User-Agent handling
+1 -1
View File
@@ -49,7 +49,7 @@ $ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
log.Error("Command not specified. See the --help flag for usage.")
}
},
Version: "2.0.3",
Version: "2.2.1",
}
func Execute() {
+201 -133
View File
@@ -3,7 +3,6 @@ package cmd
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"net/http"
"net/url"
@@ -22,29 +21,113 @@ var jsonVerboseResultArray []VerboseResult
var specTitle string
var specDescription string
func PrintSpecInfo(spec map[string]interface{}) {
info, ok := spec["info"].(map[string]interface{})
if !ok || info == nil {
log.Info("No information defined in the documentation.")
} else {
title, ok := info["title"].(string)
if ok && title != "" {
if outputFormat == "json" {
specTitle = title
} else {
fmt.Printf("Title: %s\n", title)
}
}
type SchemaNode struct {
Type string
Properties map[string]*SchemaNode
Items *SchemaNode
Required map[string]bool
Enum []interface{}
Example interface{}
Ref string
}
description, ok := info["description"].(string)
if ok && description != "" {
if outputFormat == "json" {
specDescription = description
} else {
fmt.Printf("Description: %s\n", description)
func EnforceSingleContentType(newContentType string) {
newContentType = strings.TrimSpace(newContentType)
// Remove old 'Content-Type' header
Headers = slices.DeleteFunc(Headers, func(h string) bool {
return strings.HasPrefix(strings.ToLower(h), "content-type:")
})
Headers = append(Headers, "Content-Type: "+newContentType)
// Remove empty elements to avoid repetitions of "-H ''"
Headers = slices.DeleteFunc(Headers, func(h string) bool {
return strings.TrimSpace(h) == ""
})
}
func ExpandSchema(
spec map[string]interface{},
schema map[string]interface{},
visited map[string]bool,
) *SchemaNode {
if schema == nil {
return &SchemaNode{Type: "object"}
}
if ref, ok := schema["$ref"].(string); ok {
if visited[ref] {
return &SchemaNode{Type: "object"} // break cycle
}
visited[ref] = true
resolved := ResolveRef(spec, ref)
if resolved == nil {
return &SchemaNode{Type: "object"}
}
return ExpandSchema(spec, resolved, visited)
}
node := &SchemaNode{
Properties: map[string]*SchemaNode{},
Required: map[string]bool{},
}
if t, ok := schema["type"].(string); ok {
node.Type = t
}
if props, ok := schema["properties"].(map[string]interface{}); ok {
for name, raw := range props {
if m, ok := raw.(map[string]interface{}); ok {
node.Properties[name] = ExpandSchema(spec, m, visited)
}
}
}
if items, ok := schema["items"].(map[string]interface{}); ok {
node.Items = ExpandSchema(spec, items, visited)
}
if allOf, ok := schema["allOf"].([]interface{}); ok {
merged := &SchemaNode{Type: "object", Properties: map[string]*SchemaNode{}}
for _, entry := range allOf {
if m, ok := entry.(map[string]interface{}); ok {
sub := ExpandSchema(spec, m, visited)
for k, v := range sub.Properties {
merged.Properties[k] = v
}
}
}
return merged
}
return node
}
func GenerateExample(node *SchemaNode) interface{} {
switch node.Type {
case "object", "":
obj := map[string]interface{}{}
for k, v := range node.Properties {
obj[k] = GenerateExample(v)
}
return obj
case "array":
if node.Items != nil {
return []interface{}{GenerateExample(node.Items)}
}
return []interface{}{}
case "string":
return testString
case "integer", "number":
return 1
case "boolean":
return true
default:
return nil
}
}
func GenerateRequests(bodyBytes []byte, client http.Client) {
@@ -62,7 +145,9 @@ func GenerateRequests(bodyBytes []byte, client http.Client) {
// Swagger (v2)
host, _ := spec["host"].(string)
bp, _ := spec["basePath"].(string)
if bp != "" {
if bp == "/" {
basePath = ""
} else if bp != "" {
basePath = bp
}
@@ -96,6 +181,8 @@ func GenerateRequests(bodyBytes []byte, client http.Client) {
if serverURL, ok := srv["url"].(string); ok {
if strings.Contains(serverURL, "://") {
apiTarget = serverURL
} else if serverURL == "/" {
basePath = ""
} else {
basePath = serverURL
apiTarget = u.Scheme + "://" + u.Host
@@ -167,7 +254,7 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
pType = schema["type"].(string)
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
if pType == "string" && pMap["name"] != "version" {
pValue = "bishopfox"
pValue = testString
} else {
pValue = "1"
}
@@ -198,7 +285,7 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
} else if propertyType, ok := propertyName["type"].(string); ok {
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
if propertyType == "string" && propertyName["name"] != "version" {
pValue = "bishopfox"
pValue = testString
} else {
pValue = "1"
}
@@ -223,7 +310,7 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
} else if pType, ok := pMap["type"].(string); ok {
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
if pType == "string" && pMap["name"] != "version" {
pValue = "bishopfox"
pValue = testString
} else {
pValue = "1"
}
@@ -267,94 +354,19 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
if ct, ok := contentTypes[cType].(map[string]interface{}); ok {
if schema, ok := ct["schema"].(map[string]interface{}); ok {
if ref, ok := schema["$ref"].(string); ok {
if components, ok := spec["components"].(map[string]interface{}); ok {
if schemas, ok := components["schemas"].(map[string]interface{}); ok {
schemaName := strings.TrimPrefix(ref, "#/components/schemas/")
if s, ok := schemas[schemaName].(map[string]interface{}); ok {
if properties, ok := s["properties"].(map[string]interface{}); ok {
expanded := ExpandSchema(spec, schema, map[string]bool{})
example := GenerateExample(expanded)
for propertyItem := range properties {
var pValue string
if propertyName, ok := properties[propertyItem].(map[string]interface{}); ok {
if propertyType, ok := propertyName["type"].(string); ok {
switch propertyType {
case "string":
// Attempt to prevent version strings from being improperly supplied (i.e. v1)
if propertyName["name"] != "version" {
pValue = "bishopfox"
} else {
pValue = "1"
}
case "int":
pValue = "1"
default:
pValue = "1"
}
switch cType {
case "application/json":
if strings.Contains(curl, "-d") && strings.Contains(curl, "application/json") {
bodyData = ""
curl = strings.TrimSuffix(curl, "}'")
if propertyType == "string" {
bodyData += fmt.Sprintf(",\"%s\":\"%s\"}'", propertyItem, pValue)
} else {
bodyData += fmt.Sprintf(",\"%s\":%s}'", propertyItem, pValue)
}
curl += bodyData
} else if !strings.Contains(curl, "Content-Type") {
if propertyType == "string" {
bodyData += fmt.Sprintf("{\"%s\":\"%s\"}", propertyItem, pValue)
} else {
bodyData += fmt.Sprintf("{\"%s\":%s}", propertyItem, pValue)
}
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", contentType, bodyData)
}
case "application/xml", "text/xml":
bodyData = ""
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
elements = append(elements, Element{
XMLName: xml.Name{Local: propertyItem},
Content: pValue,
})
root := Root{
Elements: elements,
}
xmlData, err := xml.Marshal(root)
if err != nil {
log.Warn("Error marshalling XML data.")
}
if strings.Contains(curl, "-d") && strings.Contains(curl, "xml") {
curl = strings.TrimSuffix(curl, "</root>'")
bodyData = strings.TrimSuffix(bodyData, "</root>'")
bodyData += strings.TrimPrefix(string(xmlData), "<root>")
curl += bodyData + "'"
} else if !strings.Contains(curl, "Content-Type") {
bodyData = string(xmlData)
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", contentType, bodyData)
}
}
}
}
}
}
}
}
if cType == "application/json" {
bodyBytes, err := json.Marshal(example)
if err == nil {
curl += fmt.Sprintf(" -H \"Content-Type: application/json\" -d '%s'\"", bodyBytes)
}
}
if cType == "application/xml" || cType == "text/xml" {
if obj, ok := example.(map[string]interface{}); ok {
xml := XmlFromObject(obj)
curl += fmt.Sprintf(" -H \"Content-Type: %s\" -d '%s'", cType, xml)
}
}
}
@@ -454,6 +466,51 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
}
}
func ResolveRef(spec map[string]interface{}, ref string) map[string]interface{} {
if !strings.HasPrefix(ref, "#") {
return nil
}
parts := strings.Split(ref[2:], "/")
var cur interface{} = spec
for _, p := range parts {
m, ok := cur.(map[string]interface{})
if !ok {
return nil
}
cur = m[p]
}
resolved, _ := cur.(map[string]interface{})
return resolved
}
func PrintSpecInfo(spec map[string]interface{}) {
info, ok := spec["info"].(map[string]interface{})
if !ok || info == nil {
log.Info("No information defined in the documentation.")
} else {
title, ok := info["title"].(string)
if ok && title != "" {
if outputFormat == "json" {
specTitle = title
} else {
fmt.Printf("Title: %s\n", title)
}
}
description, ok := info["description"].(string)
if ok && description != "" {
if outputFormat == "json" {
specDescription = description
} else {
fmt.Printf("Description: %s\n", description)
}
}
}
}
func SetScheme(swaggerURL string) (scheme string) {
if strings.HasPrefix(swaggerURL, "http://") {
scheme = "http"
@@ -465,6 +522,17 @@ func SetScheme(swaggerURL string) (scheme string) {
return scheme
}
func SafelyUnmarshalSpec(data []byte) map[string]interface{} {
var doc map[string]interface{}
if err := yaml.Unmarshal(data, &doc); err != nil {
fmt.Printf("Failed to unmarshal API documentation: %v\n", err)
os.Exit(1)
}
return doc
}
/*
TrimHostScheme trims the scheme from the provided URL if the '-T' flag is supplied to sj.
*/
@@ -483,29 +551,29 @@ func TrimHostScheme(apiTarget, fullUrlHost string) (host string) {
return host
}
func SafelyUnmarshalSpec(data []byte) map[string]interface{} {
func XmlFromObject(obj map[string]interface{}) string {
var b strings.Builder
var doc map[string]interface{}
if err := yaml.Unmarshal(data, &doc); err != nil {
fmt.Printf("Failed to unmarshal API documentation: %v\n", err)
os.Exit(1)
for k, v := range obj {
switch val := v.(type) {
case map[string]interface{}:
b.WriteString("<" + k + ">")
b.WriteString(XmlFromObject(val))
b.WriteString("</" + k + ">")
case []interface{}:
for _, item := range val {
b.WriteString("<" + k + ">")
if m, ok := item.(map[string]interface{}); ok {
b.WriteString(XmlFromObject(m))
} else {
b.WriteString(XmlFromObject(m))
}
b.WriteString("</" + k + ">")
}
default:
b.WriteString(fmt.Sprintf("<%s>%v</%s>", k, val, k))
}
}
return doc
}
func EnforceSingleContentType(newContentType string) {
newContentType = strings.TrimSpace(newContentType)
// Remove old 'Content-Type' header
Headers = slices.DeleteFunc(Headers, func(h string) bool {
return strings.HasPrefix(strings.ToLower(h), "content-type:")
})
Headers = append(Headers, "Content-Type: "+newContentType)
// Remove empty elements to avoid repetitions of "-H ''"
Headers = slices.DeleteFunc(Headers, func(h string) bool {
return strings.TrimSpace(h) == ""
})
return b.String()
}