Compare commits

...
5 Commits
9 changed files with 119 additions and 17 deletions

No files matched your search

+13
View File
@@ -61,6 +61,18 @@ Description: This is a sample server Petstore server. You can find out more abo
✓ GET 200 /v2/user/login
```
You can use the `--replay-proxy` flag to replay matched requests through a separate proxy (e.g., Burp Suite). This lets you route all traffic through one proxy (or direct) while only sending interesting results to your interception proxy:
```bash
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi --replay-proxy http://127.0.0.1:8080
```
You can also combine it with `--proxy` to route scanning traffic through a different proxy while replaying matches to Burp:
```bash
$ sj automate -u https://petstore.swagger.io/v2/swagger.json -qi -p http://proxy:9090 --replay-proxy http://127.0.0.1:8080
```
You can also request verbose output to see the partial (or full) response:
```bash
@@ -229,6 +241,7 @@ Flags:
-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.
--replay-proxy string Replay matched requests using this proxy.
--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.
+2 -2
View File
@@ -52,7 +52,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
var bodyBytes []byte
client := CheckAndConfigureProxy()
client, replayClient := CheckAndConfigureProxy()
if strings.ToLower(outputFormat) != "json" {
fmt.Printf("\n")
@@ -81,7 +81,7 @@ responds in an abnormal way, manual testing should be conducted (prepare manual
log.Info("Sending requests at a rate of ", rateLimit, " requests per second.")
}
*/
GenerateRequests(bodyBytes, client)
GenerateRequests(bodyBytes, client, replayClient)
},
}
+1 -1
View File
@@ -44,7 +44,7 @@ var bruteCmd = &cobra.Command{
}
}
client := CheckAndConfigureProxy()
client, _ := CheckAndConfigureProxy()
var allURLs []string
u, err := url.Parse(swaggerURL)
+1 -1
View File
@@ -29,7 +29,7 @@ var convertCmd = &cobra.Command{
}
}
client := CheckAndConfigureProxy()
client, _ := CheckAndConfigureProxy()
if strings.ToLower(outputFormat) != "json" {
fmt.Printf("\n")
+3 -3
View File
@@ -21,7 +21,7 @@ This list contains the raw endpoints (parameter values will not be appended or m
}
}
client := CheckAndConfigureProxy()
client, _ := CheckAndConfigureProxy()
var bodyBytes []byte
@@ -30,7 +30,7 @@ This list contains the raw endpoints (parameter values will not be appended or m
if swaggerURL != "" {
bodyBytes, _, _ = MakeRequest(client, "GET", swaggerURL, timeout, nil)
GenerateRequests(bodyBytes, client)
GenerateRequests(bodyBytes, client, nil)
} else {
specFile, err := os.Open(localFile)
if err != nil {
@@ -44,7 +44,7 @@ This list contains the raw endpoints (parameter values will not be appended or m
}
}
bodyBytes, _ = io.ReadAll(specFile)
GenerateRequests(bodyBytes, client)
GenerateRequests(bodyBytes, client, nil)
}
},
}
+3 -3
View File
@@ -29,13 +29,13 @@ This enables you to test specific API functions for common vulnerabilities or mi
os.Exit(1)
}
client := CheckAndConfigureProxy()
client, _ := CheckAndConfigureProxy()
fmt.Printf("\n")
printInfo("Gathering API details.\n\n")
if swaggerURL != "" {
bodyBytes, _, _ := MakeRequest(client, "GET", swaggerURL, timeout, nil)
GenerateRequests(bodyBytes, client)
GenerateRequests(bodyBytes, client, nil)
} else {
specFile, err := os.Open(localFile)
if err != nil {
@@ -50,7 +50,7 @@ This enables you to test specific API functions for common vulnerabilities or mi
}
specBytes, _ := io.ReadAll(specFile)
GenerateRequests(specBytes, client)
GenerateRequests(specBytes, client, nil)
}
},
}
+82 -3
View File
@@ -65,7 +65,7 @@ func MakeRequest(client http.Client, method, target string, timeout int64, reqDa
}
endpoint := u.RawPath + "?" + u.RawQuery
for _, v := range dangerousStrings {
if os.Args[1] == "automate" && strings.Contains(endpoint, v) && !strings.Contains(strings.Join(safeWords, ","), v) {
if os.Args[1] == "automate" && !force && strings.Contains(endpoint, v) && !strings.Contains(strings.Join(safeWords, ","), v) {
userChoice = ""
if avoidDangerousRequests == "y" {
return nil, "", 0
@@ -193,6 +193,19 @@ func CheckContentType(client http.Client, target string) string {
return ""
}
for i := range Headers {
delimIndex := strings.Index(Headers[i], ":")
if delimIndex == -1 {
continue
}
key := strings.TrimSpace(Headers[i][:delimIndex])
value := strings.TrimSpace(Headers[i][delimIndex+1:])
if key == "User-Agent" {
UserAgent = value
}
req.Header.Set(key, value)
}
// User-Agent handling
if randomUserAgent {
rand.New(rand.NewSource(time.Now().UnixNano()))
@@ -219,7 +232,7 @@ func CheckContentType(client http.Client, target string) string {
return resp.Header.Get("Content-Type")
}
func CheckAndConfigureProxy() (client http.Client) {
func CheckAndConfigureProxy() (client http.Client, replayClient *http.Client) {
var proxyUrl *url.URL
transport := &http.Transport{}
@@ -239,5 +252,71 @@ func CheckAndConfigureProxy() (client http.Client) {
return http.ErrUseLastResponse
},
}
return client
if replayProxy != "" {
replayTransport := &http.Transport{}
if insecure {
replayTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
replayProxyUrl, err := url.Parse(replayProxy)
if err != nil {
die("Error parsing replay proxy URL: %v", err)
}
replayTransport.Proxy = http.ProxyURL(replayProxyUrl)
replayClient = &http.Client{
Transport: replayTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
return client, replayClient
}
func ReplayRequest(replayClient *http.Client, method, target string, timeout int64, reqData io.Reader) {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
defer cancel()
req, err := http.NewRequest(method, target, reqData)
if err != nil {
printWarn("Replay: error creating request for %s - %v", target, err)
return
}
for i := range Headers {
delimIndex := strings.Index(Headers[i], ":")
if delimIndex == -1 {
continue
}
key := strings.TrimSpace(Headers[i][:delimIndex])
value := strings.TrimSpace(Headers[i][delimIndex+1:])
req.Header.Set(key, value)
}
req.Header.Set("User-Agent", UserAgent)
if accept == "" {
req.Header.Set("Accept", "application/json, text/html, */*")
} else {
req.Header.Set("Accept", accept)
}
if method == "POST" {
if contentType == "" {
req.Header.Set("Content-Type", "application/json")
} else {
req.Header.Set("Content-Type", contentType)
}
}
resp, err := replayClient.Do(req.WithContext(ctx))
if err != nil {
printWarn("Replay: error sending request to %s - %v", target, err)
return
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
fmt.Fprintf(os.Stderr, "[Replay] %s %s -> %d\n", method, target, resp.StatusCode)
}
+5 -1
View File
@@ -9,11 +9,13 @@ var basePath string
var customDate string
var customEmail string
var customURL string
var force bool
var format string
var insecure bool
var localFile string
var outfile string
var proxy string
var replayProxy string
var quiet bool
var randomUserAgent bool
var rateLimit int
@@ -51,7 +53,7 @@ $ sj convert -u https://petstore.swagger.io/v2/swagger.json -o openapi.json`,
printErr("Command not specified. See the --help flag for usage.")
}
},
Version: "2.5.0",
Version: "2.6.0",
}
func Execute() {
@@ -69,12 +71,14 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&customURL, "custom-url", "c", "https://bishopfox.com", "Set a custom URL to test discovered URL parameters.")
rootCmd.PersistentFlags().StringVarP(&customDate, "custom-date", "d", "1990-01-01", "A custom date to test discovered date parameters.")
rootCmd.PersistentFlags().StringVar(&customEmail, "custom-email", "[email protected]", "A custom email address to test discovered email parameters.")
rootCmd.PersistentFlags().BoolVar(&force, "force", false, "Send requests without prompting, even if dangerous keywords are detected.")
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "json", "Declare the format of the definition file (json/yaml/yml/js).")
rootCmd.PersistentFlags().StringArrayVarP(&Headers, "headers", "H", nil, "Add custom headers, separated by a colon (\"Name: Value\"). Multiple flags are accepted.")
rootCmd.PersistentFlags().BoolVarP(&insecure, "insecure", "i", false, "Ignores server certificate validation.")
rootCmd.PersistentFlags().StringVarP(&localFile, "local-file", "l", "", "Loads the documentation from a local file.")
rootCmd.PersistentFlags().StringVarP(&outfile, "outfile", "o", "", "Output the results to a file. Only supported for the 'automate' and 'brute' commands at this time.")
rootCmd.PersistentFlags().StringVarP(&proxy, "proxy", "p", "NOPROXY", "Proxy host and port. Example: http://127.0.0.1:8080")
rootCmd.PersistentFlags().StringVar(&replayProxy, "replay-proxy", "", "Replay matched requests using this proxy.")
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Do not prompt for user input - uses default values for all requests.")
rootCmd.PersistentFlags().BoolVar(&randomUserAgent, "randomize-user-agent", false, "Randomizes the user agent string. Default is 'false'.")
// rootCmd.PersistentFlags().IntVarP(&rateLimit, "rate", "r", 15, "Limit the number of requests per second.") // NEED TO RE-IMPLEMENT RATE LIMIT
+9 -3
View File
@@ -37,7 +37,7 @@ type SchemaNode struct {
AdditionalProperties *SchemaNode
}
func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client, replayClient *http.Client) {
paths, ok := spec["paths"].(map[string]interface{})
if !ok || paths == nil {
die("Could not find any defined operations. Review the file manually.")
@@ -296,6 +296,9 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
if outputFormat == "console" {
writeLog(sc, logURL.Path, strings.ToUpper(method), errorDescriptions[sc], resp[:tempResponsePreviewLength])
}
if replayClient != nil {
ReplayRequest(replayClient, strings.ToUpper(method), targetURL, timeout, bytes.NewReader([]byte(postBodyData)))
}
}
} else {
if jsonResultsStringArray == nil {
@@ -306,6 +309,9 @@ func BuildRequestsFromPaths(spec map[string]interface{}, client http.Client) {
if outputFormat == "console" {
writeLog(sc, logURL.Path, strings.ToUpper(method), errorDescriptions[sc], resp[:tempResponsePreviewLength])
}
if replayClient != nil {
ReplayRequest(replayClient, strings.ToUpper(method), targetURL, timeout, bytes.NewReader([]byte(postBodyData)))
}
}
case "endpoints":
@@ -533,7 +539,7 @@ func GenerateExample(node *SchemaNode) interface{} {
}
}
func GenerateRequests(bodyBytes []byte, client http.Client) {
func GenerateRequests(bodyBytes []byte, client http.Client, replayClient *http.Client) {
// Ingests the specification file
spec := SafelyUnmarshalSpec(bodyBytes)
@@ -650,7 +656,7 @@ func GenerateRequests(bodyBytes []byte, client http.Client) {
}
// Reviews all defined API routes and builds requests as defined
BuildRequestsFromPaths(spec, client)
BuildRequestsFromPaths(spec, client, replayClient)
}
func ResolveRef(spec map[string]interface{}, ref string) map[string]interface{} {