mirror of
https://github.com/lunchcat/sif.git
synced 2026-01-12 21:13:50 -08:00
feat: add sql reconnaissance module (#48)
adds a new --sql flag that performs sql reconnaissance on target urls: - detects common database admin panels (phpmyadmin, adminer, pgadmin, etc.) - identifies database error disclosure (mysql, postgresql, mssql, oracle, sqlite) - scans common paths for sql injection indicators closes #3
This commit is contained in:
committed by
GitHub
parent
44842dd659
commit
3ba18a956a
@@ -42,6 +42,7 @@ type Settings struct {
|
||||
CloudStorage bool
|
||||
SubdomainTakeover bool
|
||||
Shodan bool
|
||||
SQL bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -85,6 +86,7 @@ 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.CreateGroup("runtime", "Runtime",
|
||||
|
||||
323
pkg/scan/sql.go
Normal file
323
pkg/scan/sql.go
Normal file
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (Celeste Hickenlooper), 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
|
||||
}
|
||||
}
|
||||
}
|
||||
280
pkg/scan/sql_test.go
Normal file
280
pkg/scan/sql_test.go
Normal file
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (Celeste Hickenlooper), 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)
|
||||
}
|
||||
}
|
||||
10
sif.go
10
sif.go
@@ -257,6 +257,16 @@ func (app *App) Run() error {
|
||||
}
|
||||
}
|
||||
|
||||
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 SQL reconnaissance: %s", err)
|
||||
} else if result != nil {
|
||||
moduleResults = append(moduleResults, ModuleResult{"sql", result})
|
||||
scansRun = append(scansRun, "SQL Recon")
|
||||
}
|
||||
}
|
||||
|
||||
if app.settings.ApiMode {
|
||||
result := UrlResult{
|
||||
Url: url,
|
||||
|
||||
Reference in New Issue
Block a user