Files
Doğan Can Bakır 81461d35e8 feat: add passive CPE and WordPress detection
Add support for passive detection of CPE (Common Platform Enumeration)
identifiers and WordPress plugins/themes using awesome-search-queries.

CPE Detection (-cpe flag):
- Matches response title, body, and favicon hash against patterns
- Extracts product, vendor, and generates CPE 2.3 identifiers
- Uses patterns from Shodan, FOFA, Google dorks

WordPress Detection (-wp flag):
- Detects plugins via /wp-content/plugins/[name]/ patterns
- Detects themes via /wp-content/themes/[name]/ patterns
- Validates against known plugins/themes list

New CLI flags in PROBES group:
- -cpe: display CPE based on awesome-search-queries
- -wp, -wordpress: display WordPress plugins and themes

Both are automatically included in JSON/CSV output.

Closes #1975
2026-01-06 13:22:43 +03:00

226 lines
5.0 KiB
Go

package runner
import (
"encoding/json"
"fmt"
"strings"
awesomesearchqueries "github.com/projectdiscovery/awesome-search-queries"
)
type CPEInfo struct {
Product string `json:"product,omitempty"`
Vendor string `json:"vendor,omitempty"`
CPE string `json:"cpe,omitempty"`
}
type CPEDetector struct {
titlePatterns map[string][]CPEInfo
bodyPatterns map[string][]CPEInfo
faviconPatterns map[string][]CPEInfo
}
type rawQuery struct {
Name string `json:"name"`
Vendor json.RawMessage `json:"vendor"`
Type string `json:"type"`
Engines []rawEngine `json:"engines"`
}
type rawEngine struct {
Platform string `json:"platform"`
Queries []string `json:"queries"`
}
func NewCPEDetector() (*CPEDetector, error) {
data, err := awesomesearchqueries.GetQueries()
if err != nil {
return nil, fmt.Errorf("failed to load queries: %w", err)
}
var queries []rawQuery
if err := json.Unmarshal(data, &queries); err != nil {
return nil, fmt.Errorf("failed to parse queries: %w", err)
}
detector := &CPEDetector{
titlePatterns: make(map[string][]CPEInfo),
bodyPatterns: make(map[string][]CPEInfo),
faviconPatterns: make(map[string][]CPEInfo),
}
for _, q := range queries {
vendor := parseVendor(q.Vendor)
info := CPEInfo{
Product: q.Name,
Vendor: vendor,
CPE: generateCPE(vendor, q.Name),
}
for _, engine := range q.Engines {
for _, query := range engine.Queries {
detector.extractPattern(query, info)
}
}
}
return detector, nil
}
func parseVendor(raw json.RawMessage) string {
var vendorStr string
if err := json.Unmarshal(raw, &vendorStr); err == nil {
return vendorStr
}
var vendorSlice []string
if err := json.Unmarshal(raw, &vendorSlice); err == nil && len(vendorSlice) > 0 {
return vendorSlice[0]
}
return ""
}
func generateCPE(vendor, product string) string {
if vendor == "" || product == "" {
return ""
}
return fmt.Sprintf("cpe:2.3:a:%s:%s:*:*:*:*:*:*:*:*",
strings.ToLower(strings.ReplaceAll(vendor, " ", "_")),
strings.ToLower(strings.ReplaceAll(product, " ", "_")))
}
func (d *CPEDetector) extractPattern(query string, info CPEInfo) {
query = strings.TrimSpace(query)
titlePrefixes := []string{
"http.title:",
"title=",
"title==",
"intitle:",
"title:",
"title='",
`title="`,
}
for _, prefix := range titlePrefixes {
if strings.HasPrefix(strings.ToLower(query), strings.ToLower(prefix)) {
pattern := extractQuotedValue(strings.TrimPrefix(query, prefix))
pattern = strings.TrimPrefix(pattern, prefix[:len(prefix)-1])
if pattern != "" {
pattern = strings.ToLower(pattern)
d.titlePatterns[pattern] = appendUnique(d.titlePatterns[pattern], info)
}
return
}
}
bodyPrefixes := []string{
"http.html:",
"body=",
"body==",
"intext:",
}
for _, prefix := range bodyPrefixes {
if strings.HasPrefix(strings.ToLower(query), strings.ToLower(prefix)) {
pattern := extractQuotedValue(strings.TrimPrefix(query, prefix))
if pattern != "" {
pattern = strings.ToLower(pattern)
d.bodyPatterns[pattern] = appendUnique(d.bodyPatterns[pattern], info)
}
return
}
}
faviconPrefixes := []string{
"http.favicon.hash:",
"icon_hash=",
"icon_hash==",
}
for _, prefix := range faviconPrefixes {
if strings.HasPrefix(strings.ToLower(query), strings.ToLower(prefix)) {
pattern := extractQuotedValue(strings.TrimPrefix(query, prefix))
if pattern != "" {
d.faviconPatterns[pattern] = appendUnique(d.faviconPatterns[pattern], info)
}
return
}
}
}
func extractQuotedValue(s string) string {
s = strings.TrimSpace(s)
if len(s) >= 2 {
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
s = s[1 : len(s)-1]
}
}
if idx := strings.Index(s, "\" ||"); idx > 0 {
s = s[:idx]
}
if idx := strings.Index(s, "' ||"); idx > 0 {
s = s[:idx]
}
return strings.TrimSpace(s)
}
func appendUnique(slice []CPEInfo, info CPEInfo) []CPEInfo {
for _, existing := range slice {
if existing.Product == info.Product && existing.Vendor == info.Vendor {
return slice
}
}
return append(slice, info)
}
func (d *CPEDetector) Detect(title, body, faviconHash string) []CPEInfo {
seen := make(map[string]bool)
var results []CPEInfo
titleLower := strings.ToLower(title)
bodyLower := strings.ToLower(body)
for pattern, infos := range d.titlePatterns {
if strings.Contains(titleLower, pattern) {
for _, info := range infos {
key := info.Product + "|" + info.Vendor
if !seen[key] {
seen[key] = true
results = append(results, info)
}
}
}
}
for pattern, infos := range d.bodyPatterns {
if strings.Contains(bodyLower, pattern) {
for _, info := range infos {
key := info.Product + "|" + info.Vendor
if !seen[key] {
seen[key] = true
results = append(results, info)
}
}
}
}
if faviconHash != "" {
if infos, ok := d.faviconPatterns[faviconHash]; ok {
for _, info := range infos {
key := info.Product + "|" + info.Vendor
if !seen[key] {
seen[key] = true
results = append(results, info)
}
}
}
}
return results
}