Skip to content

Commit

Permalink
feat: use seclist for discoverable endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
emmanuelgautier committed Mar 22, 2024
1 parent 88b782d commit 6d89704
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 34 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ The VulnAPI may support additional options for customizing scans or output forma

This scanner is provided for educational and informational purposes only. It should not be used for malicious purposes or to attack any system without proper authorization. Always respect the security and privacy of others.

## Thanks

This project used the following open-source libraries:
* [SecLists](https://github.com/danielmiessler/SecLists)

## License

This repository is licensed under the [MIT License](https://github.com/cerberauth/vulnapi/blob/main/LICENSE) @ [CerberAuth](https://www.cerberauth.com/). You are free to use, modify, and distribute the contents of this repository for educational and testing purposes.
6 changes: 6 additions & 0 deletions report/vuln.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ type VulnerabilityReport struct {
Operation *request.Operation
}

func (vr *VulnerabilityReport) WithOperation(operation *request.Operation) *VulnerabilityReport {
vr.Operation = operation

return vr
}

func (vr *VulnerabilityReport) IsLowRiskSeverity() bool {
return vr.SeverityLevel < 4
}
Expand Down
15 changes: 15 additions & 0 deletions report/vuln_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ import (
"github.com/stretchr/testify/assert"
)

func TestVulnerabilityReport_WithOperation(t *testing.T) {
vr := &report.VulnerabilityReport{}
operation := &request.Operation{
Request: &http.Request{
Method: "POST",
URL: &url.URL{Scheme: "https", Host: "example.com", Path: "/vulnerability"},
},
}

vr.WithOperation(operation)

assert.Equal(t, operation, vr.Operation)
}

func TestVulnerabilityReport_IsLowRiskSeverity(t *testing.T) {
vr := &report.VulnerabilityReport{SeverityLevel: 3.5}
assert.True(t, vr.IsLowRiskSeverity())
Expand Down Expand Up @@ -41,6 +55,7 @@ func TestVulnerabilityReport_String(t *testing.T) {
expected := "[High][Test Vulnerability] GET https://example.com/vulnerability: This is a test vulnerability"
assert.Equal(t, expected, vr.String())
}

func TestVulnerabilityReport_SeverityLevelString(t *testing.T) {
vr := &report.VulnerabilityReport{}

Expand Down
6 changes: 5 additions & 1 deletion scan/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ func (s *Scan) WithDiscoverableOpenAPIScan() *Scan {
return s.AddScanHandler(discover.DiscoverableOpenAPIScanHandler)
}

func (s *Scan) WithDiscoverableGraphQLPathScan() *Scan {
return s.AddScanHandler(discover.DiscoverableGraphQLPathScanHandler)

Check warning on line 14 in scan/discover.go

View check run for this annotation

Codecov / codecov/patch

scan/discover.go#L13-L14

Added lines #L13 - L14 were not covered by tests
}

func (s *Scan) WithGraphQLIntrospectionScan() *Scan {
return s.AddScanHandler(discover.GraphqlIntrospectionScanHandler)
}

func (s *Scan) WithAllDiscoverScans() *Scan {
return s.WithServerSignatureScan().WithDiscoverableOpenAPIScan().WithGraphQLIntrospectionScan()
return s.WithServerSignatureScan().WithDiscoverableOpenAPIScan().WithDiscoverableGraphQLPathScan().WithGraphQLIntrospectionScan()

Check warning on line 22 in scan/discover.go

View check run for this annotation

Codecov / codecov/patch

scan/discover.go#L22

Added line #L22 was not covered by tests
}
40 changes: 7 additions & 33 deletions scan/discover/discoverable_openapi.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package discover

import (
"net/http"
"net/url"

"github.com/cerberauth/vulnapi/internal/auth"
"github.com/cerberauth/vulnapi/internal/request"
"github.com/cerberauth/vulnapi/internal/scan"
"github.com/cerberauth/vulnapi/report"
)

Expand All @@ -29,37 +25,15 @@ var potentialOpenAPIPaths = []string{
"/v1/api-docs",
"/v2/api-docs",
"/v3/api-docs",
".well-known/openapi.json",
".well-known/openapi.yaml",
}
var openapiSeclistUrl = "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/swagger.txt"

func DiscoverableOpenAPIScanHandler(operation *request.Operation, securityScheme auth.SecurityScheme) (*report.ScanReport, error) {
r := report.NewScanReport()

securityScheme.SetAttackValue(securityScheme.GetValidValue())

base := ExtractBaseURL(operation.Request.URL)
for _, path := range potentialOpenAPIPaths {
newRequest, _ := http.NewRequest(http.MethodGet, base.ResolveReference(&url.URL{Path: path}).String(), nil)
newOperation := request.NewOperationFromRequest(newRequest, []auth.SecurityScheme{securityScheme})

attempt, err := scan.ScanURL(newOperation, &securityScheme)
r.AddScanAttempt(attempt).End()
if err != nil {
return r, err
}

if attempt.Response.StatusCode < 300 {
r.AddVulnerabilityReport(&report.VulnerabilityReport{
SeverityLevel: DiscoverableOpenAPISeverityLevel,
Name: DiscoverableOpenAPIVulnerabilityName,
Description: DiscoverableOpenAPIVulnerabilityDescription,
Operation: newOperation,
})

return r, nil
}
}
handler := CreateURLScanHandler("OpenAPI", openapiSeclistUrl, potentialOpenAPIPaths, &report.VulnerabilityReport{
SeverityLevel: DiscoverableOpenAPISeverityLevel,
Name: DiscoverableOpenAPIVulnerabilityName,
Description: DiscoverableOpenAPIVulnerabilityDescription,
})

return r, nil
return handler(operation, securityScheme)
}
15 changes: 15 additions & 0 deletions scan/discover/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (
)

const (
DiscoverableGraphQLPathSeverityLevel = 0
DiscoverableGraphQLPathVulnerabilityName = "Discoverable GraphQL Path"
DiscoverableGraphQLPathVulnerabilityDescription = "GraphQL path seems discoverable and can lead to information disclosure and security issues"

GraphqlIntrospectionEnabledSeverityLevel = 0
GraphqlIntrospectionEnabledVulnerabilityName = "GraphQL Introspection enabled"
GraphqlIntrospectionEnabledVulnerabilityDescription = "GraphQL Introspection seems enabled and can lead to information disclosure and security issues"
Expand All @@ -26,6 +30,7 @@ var potentialGraphQLEndpoints = []string{
"/v1/graphiql",
"/v1/explorer",
}
var graphqlSeclistUrl = "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/graphql.txt"

func newGraphqlIntrospectionRequest(endpoint *url.URL) (*http.Request, error) {
return http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewReader([]byte(`{
Expand Down Expand Up @@ -68,3 +73,13 @@ func GraphqlIntrospectionScanHandler(operation *request.Operation, securitySchem

return r, nil
}

func DiscoverableGraphQLPathScanHandler(operation *request.Operation, securityScheme auth.SecurityScheme) (*report.ScanReport, error) {
handler := CreateURLScanHandler("GraphQL", graphqlSeclistUrl, potentialGraphQLEndpoints, &report.VulnerabilityReport{
SeverityLevel: DiscoverableGraphQLPathSeverityLevel,
Name: DiscoverableGraphQLPathVulnerabilityName,
Description: DiscoverableGraphQLPathVulnerabilityDescription,
})

return handler(operation, securityScheme)
}
46 changes: 46 additions & 0 deletions scan/discover/graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,49 @@ func TestGraphqlIntrospectionScanHandlerWithKnownGraphQLIntrospectionEndpoint(t
assert.Equal(t, report.GetVulnerabilityReports()[0].Name, expectedReport.Name)
assert.Equal(t, report.GetVulnerabilityReports()[0].Operation.Request.URL.String(), expectedReport.Operation.Request.URL.String())
}

func TestDiscoverableScannerWithNoDiscoverableGraphqlPath(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

securityScheme := auth.NewNoAuthSecurityScheme()
operation := request.NewOperation("http://localhost:8080/", "GET", nil, nil, nil)

httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil).HeaderAdd(http.Header{"Server": []string{"Apache/2.4.29 (Ubuntu)"}}))
httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) {
return httpmock.NewStringResponse(404, "Not Found"), nil
})

report, err := discover.DiscoverableGraphQLPathScanHandler(operation, securityScheme)

require.NoError(t, err)
assert.Greater(t, httpmock.GetTotalCallCount(), 7)
assert.False(t, report.HasVulnerabilityReport())
}

func TestDiscoverableScannerWithOneDiscoverableGraphQLPath(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

securityScheme := auth.NewNoAuthSecurityScheme()
operation := request.NewOperation("http://localhost:8080/graphql", "GET", nil, nil, nil)
httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil).HeaderAdd(http.Header{"Server": []string{"Apache/2.4.29 (Ubuntu)"}}))
httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) {
return httpmock.NewStringResponse(404, "Not Found"), nil
})

expectedReport := report.VulnerabilityReport{
SeverityLevel: discover.DiscoverableGraphQLPathSeverityLevel,
Name: discover.DiscoverableGraphQLPathVulnerabilityName,
Description: discover.DiscoverableGraphQLPathVulnerabilityDescription,
Operation: operation,
}

report, err := discover.DiscoverableGraphQLPathScanHandler(operation, securityScheme)

require.NoError(t, err)
assert.Greater(t, httpmock.GetTotalCallCount(), 0)
assert.True(t, report.HasVulnerabilityReport())
assert.Equal(t, report.GetVulnerabilityReports()[0].Name, expectedReport.Name)
assert.Equal(t, report.GetVulnerabilityReports()[0].Operation.Request.URL.String(), expectedReport.Operation.Request.URL.String())
}
39 changes: 39 additions & 0 deletions scan/discover/utils.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package discover

import (
"net/http"
"net/url"

"github.com/cerberauth/vulnapi/internal/auth"
"github.com/cerberauth/vulnapi/internal/request"
"github.com/cerberauth/vulnapi/internal/scan"
"github.com/cerberauth/vulnapi/report"
"github.com/cerberauth/vulnapi/seclist"
)

func ExtractBaseURL(inputURL *url.URL) *url.URL {
Expand All @@ -12,3 +19,35 @@ func ExtractBaseURL(inputURL *url.URL) *url.URL {

return baseURL
}

func CreateURLScanHandler(name string, seclistUrl string, defaultUrls []string, vulnReport *report.VulnerabilityReport) func(operation *request.Operation, securityScheme auth.SecurityScheme) (*report.ScanReport, error) {
scanUrls := defaultUrls
if urlsFromSeclist, err := seclist.NewSecListFromURL(name, seclistUrl); err == nil {
scanUrls = urlsFromSeclist.Items

Check warning on line 26 in scan/discover/utils.go

View check run for this annotation

Codecov / codecov/patch

scan/discover/utils.go#L26

Added line #L26 was not covered by tests
}

return func(operation *request.Operation, securityScheme auth.SecurityScheme) (*report.ScanReport, error) {
r := report.NewScanReport()
securityScheme.SetAttackValue(securityScheme.GetValidValue())

base := ExtractBaseURL(operation.Request.URL)
for _, path := range scanUrls {
newRequest, _ := http.NewRequest(http.MethodGet, base.ResolveReference(&url.URL{Path: path}).String(), nil)
newOperation := request.NewOperationFromRequest(newRequest, []auth.SecurityScheme{securityScheme})

attempt, err := scan.ScanURL(newOperation, &securityScheme)
r.AddScanAttempt(attempt).End()
if err != nil {
return r, err

Check warning on line 41 in scan/discover/utils.go

View check run for this annotation

Codecov / codecov/patch

scan/discover/utils.go#L41

Added line #L41 was not covered by tests
}

if attempt.Response.StatusCode < 300 {
r.AddVulnerabilityReport(vulnReport.WithOperation(newOperation))

return r, nil
}
}

return r, nil
}
}
95 changes: 95 additions & 0 deletions seclist/seclist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package seclist

import (
"bufio"
"errors"
"io"
"net/http"
"os"
)

type SecList struct {
Name string
Items []string
}

func NewSecList(name string) *SecList {
return &SecList{
Name: name,
Items: []string{},
}
}

func NewSecListFromFile(name, filepath string) (*SecList, error) {
s := NewSecList(name)
err := s.ImportFromFile(filepath)
if err != nil {
return nil, err

Check warning on line 27 in seclist/seclist.go

View check run for this annotation

Codecov / codecov/patch

seclist/seclist.go#L27

Added line #L27 was not covered by tests
}
return s, nil
}

func NewSecListFromURL(name, url string) (*SecList, error) {
s := NewSecList(name)
err := s.DownloadFromURL(url)
if err != nil {
return nil, err
}
return s, nil
}

func (s *SecList) ImportFromFile(filepath string) error {
file, err := os.Open(filepath)
if err != nil {
return err

Check warning on line 44 in seclist/seclist.go

View check run for this annotation

Codecov / codecov/patch

seclist/seclist.go#L44

Added line #L44 was not covered by tests
}
defer file.Close()

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
s.Items = append(s.Items, line)
}

if err := scanner.Err(); err != nil {
return err

Check warning on line 55 in seclist/seclist.go

View check run for this annotation

Codecov / codecov/patch

seclist/seclist.go#L55

Added line #L55 was not covered by tests
}

return nil
}

func (s *SecList) DownloadFromURL(url string) error {
resp, err := http.Get(url)
if err != nil {
return err

Check warning on line 64 in seclist/seclist.go

View check run for this annotation

Codecov / codecov/patch

seclist/seclist.go#L64

Added line #L64 was not covered by tests
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return errors.New("sec list download failed")
}

tempFile, err := os.CreateTemp("", "seclist")
if err != nil {
return err

Check warning on line 74 in seclist/seclist.go

View check run for this annotation

Codecov / codecov/patch

seclist/seclist.go#L74

Added line #L74 was not covered by tests
}
defer tempFile.Close()

_, err = io.Copy(tempFile, resp.Body)
if err != nil {
return err

Check warning on line 80 in seclist/seclist.go

View check run for this annotation

Codecov / codecov/patch

seclist/seclist.go#L80

Added line #L80 was not covered by tests
}

filepath := tempFile.Name()
err = s.ImportFromFile(filepath)
if err != nil {
return err

Check warning on line 86 in seclist/seclist.go

View check run for this annotation

Codecov / codecov/patch

seclist/seclist.go#L86

Added line #L86 was not covered by tests
}

err = os.Remove(filepath)
if err != nil {
return err

Check warning on line 91 in seclist/seclist.go

View check run for this annotation

Codecov / codecov/patch

seclist/seclist.go#L91

Added line #L91 was not covered by tests
}

return nil
}
Loading

0 comments on commit 6d89704

Please sign in to comment.