Skip to content

Commit

Permalink
Add wpscan for wordpress (#54)
Browse files Browse the repository at this point in the history
* add wpscan recommendations
* add wpscan facts

---------

Signed-off-by: Patrick Double <pat@patdouble.com>
  • Loading branch information
double16 authored May 28, 2024
1 parent eac3a5d commit 940363b
Show file tree
Hide file tree
Showing 35 changed files with 1,215 additions and 78 deletions.
2 changes: 1 addition & 1 deletion shadycompass.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def shadycompass_cli(args: list[str]) -> int:
parser = argparse.ArgumentParser(
prog='shadycompass',
description='Tool to help ethical hackers cover enumeration steps.')
parser.add_argument('directories', metavar='D', type=str, nargs='+',
parser.add_argument('directories', metavar='dir', type=str, nargs='+',
help='directory to search for tool output')
parsed = parser.parse_args(args)

Expand Down
1 change: 1 addition & 0 deletions shadycompass/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ToolCategory(object):
asrep_roaster = 'asrep_roaster'
kerberoaster = 'kerberoaster'
timeroaster = 'timeroaster'
wordpress_scanner = 'wordpress_scanner'
etc_hosts = 'hosts'
docs = 'docs'

Expand Down
15 changes: 11 additions & 4 deletions shadycompass/facts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from shadycompass.rules.library import METHOD_POP, METHOD_IMAP, METHOD_SMTP, METHOD_DNS

HTTP_PATTERN = re.compile(r'https?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(%[0-9a-fA-F][0-9a-fA-F]))+')
PRODUCT_PATTERN = re.compile(r'([A-Za-z0-9.-]+)/([0-9]+[.][A-Za-z0-9.]+)')
PRODUCTS_PATTERN = re.compile(r'([A-Za-z0-9.-]+)/([0-9]+[.][A-Za-z0-9.]+)')
PRODUCT_PATTERN = re.compile(r'([A-Za-z0-9.-]+)[/ ]([0-9]+[.][A-Za-z0-9.-]+)')
IPV4_PATTERN = re.compile(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
IPV6_PATTERN = re.compile(
r'(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:)|([0-9A-Fa-f]{1,4}:){1,6}:([0-9A-Fa-f]{1,4}|:){1,5}|([0-9A-Fa-f]{1,4}:){1,5}(:[0-9A-Fa-f]{1,4}){1,2}|([0-9A-Fa-f]{1,4}:){1,4}(:[0-9A-Fa-f]{1,4}){1,3}|([0-9A-Fa-f]{1,4}:){1,3}(:[0-9A-Fa-f]{1,4}){1,4}|([0-9A-Fa-f]{1,4}:){1,2}(:[0-9A-Fa-f]{1,4}){1,5}|([0-9A-Fa-f]{1,4}:)((:[0-9A-Fa-f]{1,4}){1,6}|:)|:((:[0-9A-Fa-f]{1,4}){1,7}|:))')
Expand Down Expand Up @@ -984,12 +985,18 @@ def get_product_spec(self):
return self.get_product()


def parse_products(value: str, **kwargs) -> list[Product]:
def parse_products(value: str, multiple=True, **kwargs) -> list[Product]:
if not value:
return []
result = set()
for match in re.findall(PRODUCT_PATTERN, value):
result.add(Product(product=match[0].lower(), version=match[1].lower(), **kwargs))
if multiple:
for match in re.findall(PRODUCTS_PATTERN, value):
result.add(Product(product=match[0].lower(), version=match[1].lower(), **kwargs))
else:
match = re.search(PRODUCT_PATTERN, value)
if match:
result.add(Product(product=match[1].lower(), version=match[2].lower(), **kwargs))

return list(result)


Expand Down
1 change: 1 addition & 0 deletions shadycompass/facts/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
import shadycompass.facts.smb_scanner.enum4linux_ng # noqa: F401
import shadycompass.facts.smtp_scanner.smtp_user_enum # noqa: F401
import shadycompass.facts.vuln_scanner.nuclei # noqa: F401
import shadycompass.facts.wordpress_scanner.wpscan # noqa: F401
10 changes: 7 additions & 3 deletions shadycompass/facts/http_buster/gobuster.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
GOBUSTER_DIR_PATTERN = re.compile(r'(/\S+)\s+.*Status:\s+\d+.*Size:\s+\d+', re.IGNORECASE)

GOBUSTER_VHOST_FILENAME_PATTERN = re.compile(r'gobuster-vhost-(\d+)-([^/\\]+[.][a-z]{2,6})(?:-[\w-]+?)?[.]\w{3,5}$')
GOBUSTER_VHOST_PATTERN = re.compile(r'Found:\s+(\S+)\s+Status:\s+(\d+)', re.IGNORECASE)
GOBUSTER_VHOST_PATTERN = re.compile(r'Found:\s+(\S+?)(:\d+)?\s+Status:\s+(\d+)', re.IGNORECASE)


class GobusterReader(FactReader):
Expand Down Expand Up @@ -51,9 +51,13 @@ def _read_vhost(self, file_path: str) -> list[Fact]:
for line in remove_terminal_escapes(file.readlines()):
m = GOBUSTER_VHOST_PATTERN.search(line)
if m:
status = int(m.group(2))
status = int(m.group(3))
if status < 400:
result.append(VirtualHostname(hostname=m.group(1), port=port, secure=secure))
if m.group(2):
my_port = int(m.group(2)[1:])
else:
my_port = port
result.append(VirtualHostname(hostname=m.group(1), port=my_port, secure=secure))
return result


Expand Down
28 changes: 20 additions & 8 deletions shadycompass/facts/port_scanner/nmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,22 @@ def _parse_ports(self, addrs: Iterable[str], hostnames: set[str], windows_domain
if len(subjects) > 0:
result.append(TlsCertificate(subjects=subjects, issuer=issuer))

product_kwargs = {}
if os_type:
product_kwargs['os_type'] = os_type
product_kwargs['port'] = port
product_kwargs['secure'] = secure

for port_detail_el in port_el:
if port_detail_el.tag == 'state':
state = port_detail_el.attrib.get('state', 'unknown')
elif port_detail_el.tag == 'script' and port_detail_el.attrib.get('id', None) == 'http-generator':
for parsed in parse_products(port_detail_el.get('output', None), multiple=False):
my_kwargs = product_kwargs.copy()
if parsed.get_version():
my_kwargs['version'] = parsed.get_version()
products.extend(
spread_addrs(Product, addrs, hostnames, product=parsed.get_product(), **my_kwargs))
elif port_detail_el.tag == 'service':
service_name = port_detail_el.attrib.get('name', None)
confidence = int(port_detail_el.attrib.get('conf', '0')) # 0-10
Expand All @@ -139,23 +152,22 @@ def _parse_ports(self, addrs: Iterable[str], hostnames: set[str], windows_domain
product_version_els = port_el.findall(".//elem[@key='Product_Version']")
if product_version_els:
product_version = product_version_els[0].text
product_kwargs = {}
if hostname:
product_kwargs['hostname'] = hostname
if os_type:
product_kwargs['os_type'] = os_type
product_kwargs['port'] = port
if product:
my_kwargs = product_kwargs.copy()
if hostname:
my_kwargs['hostname'] = hostname
if product_version:
my_kwargs['version'] = product_version
products.extend(spread_addrs(Product, addrs, product=product, **my_kwargs))
products.extend(spread_addrs(Product, addrs, hostnames, product=product, **my_kwargs))
if extra_info:
for parsed in parse_products(extra_info):
my_kwargs = product_kwargs.copy()
if hostname:
my_kwargs['hostname'] = hostname
if parsed.get_version():
my_kwargs['version'] = parsed.get_version()
products.extend(spread_addrs(Product, addrs, product=parsed.get_product(), **my_kwargs))
products.extend(
spread_addrs(Product, addrs, hostnames, product=parsed.get_product(), **my_kwargs))

if product and 'Active Directory' in product:
ad_kwargs = dict(hostname=hostname, netbios_computer_name=hostname)
Expand Down
2 changes: 1 addition & 1 deletion shadycompass/facts/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def create_service_facts(addrs: Iterable[str], os_type, port, protocol, result,
def spread_addrs(fact_type, addrs: Iterable[str], hostnames: Iterable[str] = None, **kwargs) -> list[Fact]:
result = []
for addr in addrs:
if hostnames:
if hostnames and 'hostname' not in kwargs:
for hostname in hostnames:
result.append(fact_type(addr=addr, hostname=hostname, **kwargs))
else:
Expand Down
Empty file.
81 changes: 81 additions & 0 deletions shadycompass/facts/wordpress_scanner/wpscan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import json
import urllib.parse

from experta import Fact

from shadycompass.config import ToolCategory
from shadycompass.facts import FactReader, check_file_signature, fact_reader_registry, ScanPresent, Product, \
guess_target, HttpService, \
Username


class WpscanReader(FactReader):
def read_facts(self, file_path: str) -> list[Fact]:
if not check_file_signature(file_path, '"description": "WordPress Security Scanner'):
return []
result = []
try:
with open(file_path, 'rt') as f:
data = json.load(f)
except ValueError:
return result
if not isinstance(data, dict):
return result
print(f"[*] Reading wpscan findings from {file_path}")

scan_present_kwargs = {}

target_url = data.get('target_url')
if not target_url:
return result
target_parsed = urllib.parse.urlparse(target_url)
target_fact = guess_target(target_parsed.hostname)
scan_present_kwargs['url'] = target_url
if 'hostname' in target_fact:
scan_present_kwargs['hostname'] = target_fact.get('hostname')
if 'addr' in target_fact:
scan_present_kwargs['addr'] = target_fact.get('addr')
if target_parsed.port:
scan_present_kwargs['port'] = target_parsed.port
else:
scan_present_kwargs['port'] = 80
if target_parsed.scheme.endswith('s'):
secure = True
else:
secure = False

target_ip = data.get('target_ip')
if target_ip:
scan_present_kwargs['addr'] = target_ip
result.append(guess_target(target_ip))

result.append(ScanPresent(category=ToolCategory.wordpress_scanner, name='wpscan', **scan_present_kwargs))

if 'addr' in scan_present_kwargs and 'port' in scan_present_kwargs:
result.append(
HttpService(addr=scan_present_kwargs['addr'], port=scan_present_kwargs['port'], secure=secure))

product_kwargs = {
'product': 'wordpress',
'addr': scan_present_kwargs['addr'],
'port': scan_present_kwargs['port'],
'secure': secure,
}
if data.get('version', {}).get('number', ''):
product_kwargs['version'] = data.get('version', {}).get('number', '')
if 'hostname' in scan_present_kwargs:
product_kwargs['hostname'] = scan_present_kwargs['hostname']
result.append(Product(**product_kwargs))

user_kwargs = {
'addr': scan_present_kwargs['addr'],
}
if 'hostname' in scan_present_kwargs:
user_kwargs['hostname'] = scan_present_kwargs['hostname']
for user in data.get('users', {}).keys():
result.append(Username(username=user, **user_kwargs))

return result


fact_reader_registry.append(WpscanReader())
6 changes: 5 additions & 1 deletion shadycompass/rules/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
import shadycompass.rules.smb_scanner.all as smb_scanner
import shadycompass.rules.smtp_scanner.all as smtp_scanner
import shadycompass.rules.vuln_scanner.all as vuln_scanner
import shadycompass.rules.wordpress_scanner.all as wordpress_scanner
from shadycompass.config import ConfigRules
from shadycompass.rules.asrep_roaster import AsRepRoaster
from shadycompass.rules.dns_scanner import DnsScan
from shadycompass.rules.etc_hosts import EtcHostsRules
from shadycompass.rules.http_busting import HttpBusting
from shadycompass.rules.http_buster import HttpBusting
from shadycompass.rules.imap_scanner import ImapScan
from shadycompass.rules.ldap_scanner import LdapScan
from shadycompass.rules.misc import ProductionTargetRules, RateLimitRules, PublicAddrRules, MiscRules
Expand All @@ -21,6 +22,7 @@
from shadycompass.rules.smtp_scanner import SmtpScan
from shadycompass.rules.virtualhost_scanner import VirtualHostScan
from shadycompass.rules.vuln_scanner import VulnScan
from shadycompass.rules.wordpress_scanner import WordpressScan


class AllRules(
Expand All @@ -32,6 +34,7 @@ class AllRules(
dns_scanner.AllRules,
kerberoaster.AllRules,
ldap_scanner.AllRules,
wordpress_scanner.AllRules,
HttpBusting,
ScanRules,
PortScan,
Expand All @@ -44,6 +47,7 @@ class AllRules(
AsRepRoaster,
LdapScan,
VirtualHostScan,
WordpressScan,
ConfigRules,
EtcHostsRules,
ProductionTargetRules,
Expand Down
43 changes: 43 additions & 0 deletions shadycompass/rules/http_buster/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from abc import ABC

from experta import Rule, OR, AS, MATCH, NOT

from shadycompass.facts import HttpService, HttpUrl, HttpBustingNeeded, TargetIPv4Address, TargetIPv6Address, \
HostnameIPv4Resolution, HostnameIPv6Resolution, VirtualHostname
from shadycompass.rules.irules import IRules

"""
Rules to decide if we need to bust HTTP servers.
"""


class HttpBusting(IRules, ABC):
@Rule(
HttpService(addr=MATCH.addr, port=MATCH.port, secure=MATCH.secure),
OR(TargetIPv4Address(addr=MATCH.addr), TargetIPv6Address(addr=MATCH.addr)),
OR(HostnameIPv4Resolution(hostname=MATCH.hostname, addr=MATCH.addr),
HostnameIPv6Resolution(hostname=MATCH.hostname, addr=MATCH.addr)),
NOT(VirtualHostname(hostname=MATCH.hostname, port=MATCH.port)),
)
def virtualhostname_from_httpservice(self, port, hostname, secure):
self.declare(VirtualHostname(hostname=hostname, port=port, secure=secure))

@Rule(
AS.f1 << HttpService(addr=MATCH.addr, port=MATCH.port),
VirtualHostname(hostname=MATCH.hostname, domain=MATCH.domain, port=MATCH.port),
OR(TargetIPv4Address(addr=MATCH.addr), TargetIPv6Address(addr=MATCH.addr)),
OR(
HostnameIPv4Resolution(addr=MATCH.addr, hostname=MATCH.hostname | MATCH.domain),
HostnameIPv6Resolution(addr=MATCH.addr, hostname=MATCH.hostname | MATCH.domain),
),
NOT(HttpUrl(port=MATCH.port, vhost=MATCH.hostname)),
)
def need_http_busting(self, f1: HttpService, addr, port, hostname):
self.declare(HttpBustingNeeded(secure=f1.is_secure(), addr=addr, port=port, vhost=hostname))

@Rule(
AS.f1 << HttpBustingNeeded(secure=MATCH.secure, addr=MATCH.addr, port=MATCH.port, vhost=MATCH.hostname),
HttpUrl(secure=MATCH.secure, port=MATCH.port, vhost=MATCH.hostname),
)
def do_not_need_http_busting(self, f1: HttpBustingNeeded):
self.retract(f1)
43 changes: 0 additions & 43 deletions shadycompass/rules/http_busting.py

This file was deleted.

4 changes: 4 additions & 0 deletions shadycompass/rules/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@
METHOD_LDAP = [
'https://book.hacktricks.xyz/network-services-pentesting/pentesting-ldap',
]

METHOD_WORDPRESS = [
'https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/wordpress',
]
2 changes: 1 addition & 1 deletion shadycompass/rules/virtualhost_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ def do_not_need_virtualhost_scan(self, f1: ScanNeeded):
ScanPresent(category=ToolCategory.virtualhost_scanner, addr=MATCH.addr, port=MATCH.port,
hostname=MATCH.hostname),
)
def retract_smtp_tool(self, f1: ToolRecommended):
def retract_virtualhost_scan_tool(self, f1: ToolRecommended):
self.retract(f1)
Loading

0 comments on commit 940363b

Please sign in to comment.