mirror of
https://github.com/BishopFox/sj.git
synced 2026-11-09 17:09:35 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ef8abdb8e | ||
|
|
f1f9e19422 | ||
|
|
0d6776b758 | ||
|
|
8f4e4a5d9f | ||
|
|
55baa79785 | ||
|
|
5bdafb64d8 | ||
|
|
27b59354fc | ||
|
|
bee5088fcf | ||
|
|
1eda960182 |
No files matched your search
@@ -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
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+32
-13
@@ -44,7 +44,7 @@ var bruteCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
client, _ := CheckAndConfigureProxy()
|
||||
|
||||
var allURLs []string
|
||||
u, err := url.Parse(swaggerURL)
|
||||
@@ -52,12 +52,13 @@ var bruteCmd = &cobra.Command{
|
||||
printWarn("Error parsing URL: %s", err)
|
||||
}
|
||||
target := u.Scheme + "://" + u.Host
|
||||
normalizedBasePath := normalizeBasePath(basePath)
|
||||
if endpointWordlist == "" {
|
||||
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)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, priorityURLs, "", true)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, "", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, javascriptEndpoints, ".js", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, ".json", false)...)
|
||||
allURLs = append(allURLs, makeURLs(target, normalizedBasePath, jsonEndpoints, "/", false)...)
|
||||
} else {
|
||||
endpointList, err := os.Open(endpointWordlist)
|
||||
if err != nil {
|
||||
@@ -68,7 +69,7 @@ var bruteCmd = &cobra.Command{
|
||||
scanner := bufio.NewScanner(endpointList)
|
||||
for scanner.Scan() {
|
||||
endpoint := scanner.Text()
|
||||
fullURL := target + endpoint
|
||||
fullURL := target + normalizedBasePath + endpoint
|
||||
allURLs = append(allURLs, fullURL)
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ var bruteCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func makeURLs(target string, endpoints []string, fileExtension string, skipPrefix bool) []string {
|
||||
func makeURLs(target string, basePath string, endpoints []string, fileExtension string, skipPrefix bool) []string {
|
||||
urls := []string{}
|
||||
if !skipPrefix {
|
||||
for _, dir := range prefixDirs {
|
||||
@@ -127,7 +128,7 @@ func makeURLs(target string, endpoints []string, fileExtension string, skipPrefi
|
||||
if dir == "" && endpoint == "" {
|
||||
continue
|
||||
}
|
||||
targetURL := target + dir + endpoint + fileExtension
|
||||
targetURL := target + basePath + dir + endpoint + fileExtension
|
||||
urls = append(urls, targetURL)
|
||||
|
||||
}
|
||||
@@ -137,7 +138,7 @@ func makeURLs(target string, endpoints []string, fileExtension string, skipPrefi
|
||||
if endpoint == "" {
|
||||
continue
|
||||
}
|
||||
targetURL := target + endpoint + fileExtension
|
||||
targetURL := target + basePath + endpoint + fileExtension
|
||||
urls = append(urls, targetURL)
|
||||
}
|
||||
}
|
||||
@@ -161,18 +162,36 @@ func findDefinitionFile(urls []string, client http.Client) (bool, *openapi3.T) {
|
||||
} else if strings.Contains(ct, "application/javascript") {
|
||||
bodyBytes, bodyString, _ := MakeRequest(client, "GET", url, timeout, nil)
|
||||
if bodyBytes != nil {
|
||||
regexPattern := regexp.MustCompile(`(?s)let\s+(\w+)\s*=\s*({.*?});`)
|
||||
regexPattern := regexp.MustCompile(`(?s)(?:let|const|var)\s+(\w+)\s*=\s*({.*?});`)
|
||||
matches := regexPattern.FindAllStringSubmatch(bodyString, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 3 {
|
||||
continue
|
||||
}
|
||||
jsonContent := match[2]
|
||||
checkSpec := UnmarshalSpec([]byte(jsonContent))
|
||||
jsonContent := []byte(match[2])
|
||||
checkSpec := UnmarshalSpec(jsonContent)
|
||||
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
|
||||
printInfo("\nFound operation definitions embedded in JavaScript file at %s\n", url)
|
||||
return true, checkSpec
|
||||
}
|
||||
// Swagger UI commonly nests the spec one level deep under
|
||||
// keys like swaggerDoc (swagger-ui-express / NestJS),
|
||||
// spec (Swagger UI Bundle config), or openapi.
|
||||
var wrapper map[string]json.RawMessage
|
||||
if err := json.Unmarshal(jsonContent, &wrapper); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, key := range []string{"swaggerDoc", "spec", "openapi"} {
|
||||
inner, ok := wrapper[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
checkSpec := UnmarshalSpec(inner)
|
||||
if strings.HasPrefix(checkSpec.OpenAPI, "2") || strings.HasPrefix(checkSpec.OpenAPI, "3") {
|
||||
printInfo("\nFound operation definitions embedded in JavaScript file at %s\n", url)
|
||||
return true, checkSpec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ var convertCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
client := CheckAndConfigureProxy()
|
||||
client, _ := CheckAndConfigureProxy()
|
||||
|
||||
if strings.ToLower(outputFormat) != "json" {
|
||||
fmt.Printf("\n")
|
||||
|
||||
+3
-3
@@ -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
@@ -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
@@ -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
@@ -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.2",
|
||||
}
|
||||
|
||||
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
@@ -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{} {
|
||||
|
||||
Reference in new issue
Block a user