Skip to content

Commit ca2e1fb

Browse files
Merge pull request #240 from cerberauth/discover-well-known-paths
Discover Well-Known paths and leaked files
2 parents 2862a49 + 75115d5 commit ca2e1fb

File tree

17 files changed

+1360
-101
lines changed

17 files changed

+1360
-101
lines changed

.github/workflows/scans.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
- name: VulnAPI
4646
id: vulnapi
4747
run: |
48-
go run main.go discover api http://localhost:8080 --sqa-opt-out
48+
go run main.go discover api http://localhost:8080 --rate-limit 500 --sqa-opt-out
4949
5050
- name: Stop Server
5151
if: ${{ always() }}

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ The Vulnerability Scanner CLI offers two methods for scanning APIs:
3232

3333
### Discover Command
3434

35-
To discover target API useful information, execute the following command:
35+
To discover target API useful information, leaked files and well-known path execute the following command:
3636

3737
```bash
3838
vulnapi discover api [API_URL]
@@ -41,11 +41,12 @@ vulnapi discover api [API_URL]
4141
Example output:
4242

4343
```bash
44-
| WELL-KNOWN PATHS | URL |
45-
|------------------|------------------------------------|
46-
| OpenAPI | http://localhost:5000/openapi.json |
47-
| GraphQL | N/A |
48-
44+
| TYPE | URL |
45+
|---------------|---------------------------------------------|
46+
| OpenAPI | http://localhost:5000/openapi.json |
47+
| GraphQL | http://localhost:5000/graphql |
48+
| Well-Known | http://localhost:8080/.well-known/jwks.json |
49+
| Exposed Files | http://localhost:8080/.env.dev |
4950

5051
| TECHNOLOGIE/SERVICE | VALUE |
5152
|---------------------|---------------|

internal/cmd/printtable/wellknown_paths_table.go

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,57 @@ import (
44
"fmt"
55

66
"github.com/cerberauth/vulnapi/report"
7+
"github.com/cerberauth/vulnapi/scan/discover"
78
discoverablegraphql "github.com/cerberauth/vulnapi/scan/discover/discoverable_graphql"
89
discoverableopenapi "github.com/cerberauth/vulnapi/scan/discover/discoverable_openapi"
10+
exposedfiles "github.com/cerberauth/vulnapi/scan/discover/exposed_files"
11+
wellknown "github.com/cerberauth/vulnapi/scan/discover/well-known"
912
"github.com/olekukonko/tablewriter"
1013
)
1114

15+
func wellKnownPathsFromReport(r *report.ScanReport, header string) [][]string {
16+
rows := [][]string{}
17+
if r == nil || !r.HasData() {
18+
return rows
19+
}
20+
21+
data, ok := r.Data.(discover.DiscoverData)
22+
if ok && len(data) > 0 {
23+
rows = append(rows, []string{header, data[0].URL})
24+
}
25+
26+
return rows
27+
}
28+
1229
func WellKnownPathsScanReport(reporter *report.Reporter) {
13-
openapiURL := ""
30+
rows := [][]string{}
31+
1432
openapiReport := reporter.GetScanReportByID(discoverableopenapi.DiscoverableOpenAPIScanID)
15-
if openapiReport != nil && openapiReport.HasData() {
16-
openapiData, ok := openapiReport.Data.(discoverableopenapi.DiscoverableOpenAPIData)
17-
if ok {
18-
openapiURL = openapiData.URL
19-
}
20-
}
33+
rows = append(rows, wellKnownPathsFromReport(openapiReport, "OpenAPI")...)
2134

22-
graphqlURL := ""
2335
graphqlReport := reporter.GetScanReportByID(discoverablegraphql.DiscoverableGraphQLPathScanID)
24-
if graphqlReport != nil && graphqlReport.HasData() {
25-
graphqlData, ok := graphqlReport.Data.(discoverablegraphql.DiscoverableGraphQLPathData)
26-
if ok {
27-
graphqlURL = graphqlData.URL
28-
}
29-
}
36+
rows = append(rows, wellKnownPathsFromReport(graphqlReport, "GraphQL")...)
3037

31-
if openapiURL == "" && graphqlURL == "" {
38+
wellKnownReport := reporter.GetScanReportByID(wellknown.DiscoverableWellKnownScanID)
39+
rows = append(rows, wellKnownPathsFromReport(wellKnownReport, "Well-Known")...)
40+
41+
exposedFiles := reporter.GetScanReportByID(exposedfiles.DiscoverableFilesScanID)
42+
rows = append(rows, wellKnownPathsFromReport(exposedFiles, "Exposed Files")...)
43+
44+
if len(rows) == 0 {
3245
return
3346
}
3447

3548
fmt.Println()
36-
headers := []string{"Well-Known Paths", "URL"}
49+
headers := []string{"Type", "URL"}
3750
table := CreateTable(headers)
3851

3952
tableColors := make([]tablewriter.Colors, len(headers))
4053
tableColors[0] = tablewriter.Colors{tablewriter.Bold}
4154
tableColors[1] = tablewriter.Colors{tablewriter.Bold}
4255

43-
if openapiURL != "" {
44-
table.Rich([]string{"OpenAPI", openapiURL}, tableColors)
45-
}
46-
if graphqlURL != "" {
47-
table.Rich([]string{"GraphQL", graphqlURL}, tableColors)
56+
for _, row := range rows {
57+
table.Rich(row, tableColors)
4858
}
4959

5060
table.Render()

scan/broken_authentication/jwt/weak_secret/weak_secret.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ func ShouldBeScanned(securityScheme *auth.SecurityScheme) bool {
4949

5050
var defaultJwtSecretDictionary = []string{"secret", "password", "123456", "changeme", "admin", "token"}
5151

52-
const jwtSecretDictionarySeclistUrl = "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/scraped-JWT-secrets.txt"
52+
// From https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/scraped-JWT-secrets.txt
53+
const jwtSecretDictionarySeclistUrl = "https://raw.githubusercontent.com/cerberauth/vulnapi/main/seclist/lists/jwt-secrets.txt"
5354

5455
func ScanHandler(op *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) {
5556
vulnReport := report.NewIssueReport(issue).WithOperation(op).WithSecurityScheme(securityScheme)

scan/discover/discoverable_graphql/discoverable_graphql.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,10 @@ var issue = report.Issue{
2929
},
3030
}
3131

32-
var graphqlSeclistUrl = "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/graphql.txt"
33-
var potentialGraphQLEndpoints = []string{
34-
"/graphql",
35-
"/graph",
36-
"/api/graphql",
37-
"/graphql/console",
38-
"/v1/graphql",
39-
"/v1/graphiql",
40-
}
32+
var graphqlSeclistUrl = "https://raw.githubusercontent.com/cerberauth/vulnapi/main/seclist/lists/graphql.txt"
4133

4234
func ScanHandler(op *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) {
4335
vulnReport := report.NewIssueReport(issue).WithOperation(op).WithSecurityScheme(securityScheme)
4436
r := report.NewScanReport(DiscoverableGraphQLPathScanID, DiscoverableGraphQLPathScanName, op)
45-
handler := discover.CreateURLScanHandler("GraphQL", graphqlSeclistUrl, potentialGraphQLEndpoints, r, vulnReport)
46-
47-
return handler(op, securityScheme)
37+
return discover.DownloadAndScanURLs("GraphQL", graphqlSeclistUrl, r, vulnReport, op, securityScheme)
4838
}

scan/discover/discoverable_openapi/discoverable_openapi.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,10 @@ var issue = report.Issue{
2929
},
3030
}
3131

32-
var openapiSeclistUrl = "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/swagger.txt"
33-
var potentialOpenAPIPaths = []string{
34-
"/openapi",
35-
"/api-docs.json",
36-
"/api-docs.yaml",
37-
"/api-docs.yml",
38-
"/.well-known/openapi.yml",
39-
}
32+
var openapiSeclistUrl = "https://raw.githubusercontent.com/cerberauth/vulnapi/main/seclist/lists/swagger.txt"
4033

4134
func ScanHandler(op *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) {
4235
vulnReport := report.NewIssueReport(issue).WithOperation(op).WithSecurityScheme(securityScheme)
4336
r := report.NewScanReport(DiscoverableOpenAPIScanID, DiscoverableOpenAPIScanName, op)
44-
handler := discover.CreateURLScanHandler("OpenAPI", openapiSeclistUrl, potentialOpenAPIPaths, r, vulnReport)
45-
46-
return handler(op, securityScheme)
37+
return discover.DownloadAndScanURLs("OpenAPI", openapiSeclistUrl, r, vulnReport, op, securityScheme)
4738
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package exposedfiles
2+
3+
import (
4+
"github.com/cerberauth/vulnapi/internal/auth"
5+
"github.com/cerberauth/vulnapi/internal/operation"
6+
"github.com/cerberauth/vulnapi/report"
7+
"github.com/cerberauth/vulnapi/scan/discover"
8+
)
9+
10+
const (
11+
DiscoverableFilesScanID = "discover.exposed_files"
12+
DiscoverableFilesScanName = "Discoverable exposed files"
13+
)
14+
15+
type DiscoverableFilesData = discover.DiscoverData
16+
17+
var issue = report.Issue{
18+
ID: "discover.exposed_files",
19+
Name: "Discoverable exposed files",
20+
21+
Classifications: &report.Classifications{
22+
OWASP: report.OWASP_2023_SSRF,
23+
},
24+
25+
CVSS: report.CVSS{
26+
Version: 4.0,
27+
Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N",
28+
Score: 0,
29+
},
30+
}
31+
32+
var discoverableFilesSeclistUrl = "https://raw.githubusercontent.com/cerberauth/vulnapi/main/seclist/lists/exposed-paths.txt"
33+
34+
func ScanHandler(op *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) {
35+
vulnReport := report.NewIssueReport(issue).WithOperation(op).WithSecurityScheme(securityScheme)
36+
r := report.NewScanReport(DiscoverableFilesScanID, DiscoverableFilesScanName, op)
37+
return discover.DownloadAndScanURLs("Exposed Files", discoverableFilesSeclistUrl, r, vulnReport, op, securityScheme)
38+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package exposedfiles_test
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/cerberauth/vulnapi/internal/auth"
8+
"github.com/cerberauth/vulnapi/internal/operation"
9+
"github.com/cerberauth/vulnapi/internal/request"
10+
exposedfiles "github.com/cerberauth/vulnapi/scan/discover/exposed_files"
11+
"github.com/jarcoal/httpmock"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestDiscoverableScanner_Passed_WhenNoDiscoverableGraphqlPathFound(t *testing.T) {
17+
client := request.NewClient(request.NewClientOptions{
18+
RateLimit: 500,
19+
})
20+
httpmock.ActivateNonDefault(client.Client)
21+
defer httpmock.DeactivateAndReset()
22+
23+
op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, client)
24+
httpmock.RegisterResponder(op.Method, op.URL.String(), httpmock.NewBytesResponder(http.StatusNoContent, nil))
25+
httpmock.RegisterNoResponder(httpmock.NewBytesResponder(http.StatusNotFound, nil))
26+
27+
report, err := exposedfiles.ScanHandler(op, auth.MustNewNoAuthSecurityScheme())
28+
29+
require.NoError(t, err)
30+
assert.Greater(t, httpmock.GetTotalCallCount(), 7)
31+
assert.True(t, report.Issues[0].HasPassed())
32+
}
33+
34+
func TestDiscoverableScanner_Failed_WhenOneGraphQLPathFound(t *testing.T) {
35+
client := request.NewClient(request.NewClientOptions{
36+
RateLimit: 500,
37+
})
38+
httpmock.ActivateNonDefault(client.Client)
39+
defer httpmock.DeactivateAndReset()
40+
41+
operation := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/.aws/credentials", nil, client)
42+
httpmock.RegisterResponder(operation.Method, operation.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil))
43+
httpmock.RegisterNoResponder(httpmock.NewBytesResponder(http.StatusNotFound, nil))
44+
45+
report, err := exposedfiles.ScanHandler(operation, auth.MustNewNoAuthSecurityScheme())
46+
47+
require.NoError(t, err)
48+
assert.Greater(t, httpmock.GetTotalCallCount(), 0)
49+
assert.True(t, report.Issues[0].HasFailed())
50+
}

scan/discover/utils.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package discover
22

33
import (
4+
"log"
45
"net/http"
56
"net/url"
67

@@ -11,7 +12,7 @@ import (
1112
"github.com/cerberauth/vulnapi/seclist"
1213
)
1314

14-
type DiscoverData struct {
15+
type DiscoverData []struct {
1516
URL string
1617
}
1718

@@ -56,32 +57,35 @@ func ScanURLs(scanUrls []string, op *operation.Operation, securityScheme *auth.S
5657
}(chunk)
5758
}
5859

60+
data := DiscoverData{}
5961
for i := 0; i < len(scanUrls); i++ {
6062
select {
6163
case attempt := <-results:
6264
r.AddScanAttempt(attempt)
6365
if attempt.Response.GetStatusCode() == http.StatusOK { // TODO: check if the response contains the expected content
64-
r.WithData(DiscoverData{
65-
URL: attempt.Request.GetURL(),
66-
}).AddIssueReport(vulnReport.Fail()).End()
67-
return r, nil
66+
data = append(data, struct{ URL string }{URL: attempt.Request.GetURL()})
6867
}
6968
case err := <-errors:
70-
return r, err
69+
log.Printf("Error scanning URL: %v", err)
70+
continue
7171
}
7272
}
7373

74+
if len(data) > 0 {
75+
r.WithData(data).AddIssueReport(vulnReport.Fail()).End()
76+
return r, nil
77+
}
78+
7479
r.AddIssueReport(vulnReport.Pass()).End()
7580
return r, nil
7681
}
7782

78-
func CreateURLScanHandler(name string, seclistUrl string, defaultUrls []string, r *report.ScanReport, vulnReport *report.IssueReport) func(operation *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) {
79-
scanUrls := defaultUrls
80-
if urlsFromSeclist, err := seclist.NewSecListFromURL(name, seclistUrl); err == nil && urlsFromSeclist != nil {
81-
scanUrls = urlsFromSeclist.Items
83+
func DownloadAndScanURLs(name string, seclistUrl string, r *report.ScanReport, vulnReport *report.IssueReport, op *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) {
84+
urlsFromSeclist, err := seclist.NewSecListFromURL(name, seclistUrl)
85+
if err != nil {
86+
return nil, err
8287
}
88+
scanUrls := urlsFromSeclist.Items
8389

84-
return func(op *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) {
85-
return ScanURLs(scanUrls, op, securityScheme, r, vulnReport)
86-
}
90+
return ScanURLs(scanUrls, op, securityScheme, r, vulnReport)
8791
}

0 commit comments

Comments
 (0)