Skip to content

Commit 7c9484f

Browse files
committed
fix: wpscan URL format, searchsploit query and msf fallback
1 parent 4cd9d4d commit 7c9484f

3 files changed

Lines changed: 87 additions & 18 deletions

File tree

exploitation/metasploit_engine.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import logging
2+
import shutil
3+
import subprocess
24
import time
3-
from typing import Any, Dict
5+
from typing import Any, Dict, Optional
46

57
logger = logging.getLogger("pentest-agent.msf")
68

7-
# Mapping service → recommended auxiliary/exploit modules
89
SERVICE_MODULES = {
910
"smb": [
1011
"auxiliary/scanner/smb/smb_ms17_010",
@@ -30,6 +31,50 @@
3031
}
3132

3233

34+
def _try_connect(password: str, port: int):
35+
"""Attempt a single connection to MSF RPC. Returns client or None."""
36+
try:
37+
from pymetasploit3.msfrpc import MsfRpcClient
38+
return MsfRpcClient(password=password, port=port, ssl=False)
39+
except Exception:
40+
return None
41+
42+
43+
def start_msfrpcd(password: str = "msfrpc", port: int = 55553):
44+
"""
45+
Try to connect to MSF RPC. If unavailable, start msfrpcd automatically
46+
and wait up to 30 seconds for it to become ready.
47+
Returns a connected client or None.
48+
"""
49+
# Already running?
50+
client = _try_connect(password, port)
51+
if client:
52+
logger.info(f"Metasploit RPC already running on port {port}")
53+
return client
54+
55+
if not shutil.which("msfrpcd"):
56+
logger.warning("msfrpcd not found in PATH — skipping MSF modules")
57+
return None
58+
59+
logger.info("Starting Metasploit RPC daemon...")
60+
subprocess.Popen(
61+
["msfrpcd", "-P", password, "-p", str(port), "-S", "-f"],
62+
stdout=subprocess.DEVNULL,
63+
stderr=subprocess.DEVNULL,
64+
)
65+
66+
for i in range(30):
67+
time.sleep(1)
68+
client = _try_connect(password, port)
69+
if client:
70+
logger.info(f"Metasploit RPC daemon started successfully (took {i+1}s)")
71+
return client
72+
logger.debug(f"Waiting for MSF RPC... ({i+1}/30)")
73+
74+
logger.warning("Failed to start Metasploit RPC after 30s — skipping MSF modules")
75+
return None
76+
77+
3378
class MetasploitEngine:
3479
def __init__(self, password: str = "msfrpc", port: int = 55553, timeout: int = 120):
3580
self.password = password
@@ -38,29 +83,23 @@ def __init__(self, password: str = "msfrpc", port: int = 55553, timeout: int = 1
3883
self._client = None
3984

4085
def _connect(self) -> bool:
41-
try:
42-
from pymetasploit3.msfrpc import MsfRpcClient
43-
self._client = MsfRpcClient(
44-
password=self.password,
45-
port=self.port,
46-
ssl=False,
47-
)
86+
self._client = start_msfrpcd(self.password, self.port)
87+
if self._client:
4888
logger.info(f"Connected to Metasploit RPC on port {self.port}")
4989
return True
50-
except Exception as exc:
51-
logger.warning(f"Metasploit RPC unavailable: {exc}")
52-
return False
90+
logger.warning("Metasploit RPC unavailable — skipping MSF modules")
91+
return False
5392

5493
def run_module(self, target: str, port: str, module_path: str) -> Dict[str, Any]:
5594
if not self._connect():
5695
return {
5796
"status": "skipped",
58-
"output": "Metasploit RPC not available — start with: msfconsole -x 'load msgrpc Pass=msfrpc'",
97+
"output": "Metasploit RPC unavailable - skipping MSF modules",
5998
}
6099

61100
try:
62101
client = self._client
63-
module_type = module_path.split("/")[0] # auxiliary or exploit
102+
module_type = module_path.split("/")[0]
64103

65104
if module_type == "auxiliary":
66105
mod = client.modules.use("auxiliary", "/".join(module_path.split("/")[1:]))
@@ -83,11 +122,9 @@ def run_module(self, target: str, port: str, module_path: str) -> Dict[str, Any]
83122

84123
logger.info(f"MSF module launched: {module_path} (job {job_id})")
85124

86-
# Wait for completion
87125
elapsed = 0
88126
while elapsed < self.timeout:
89-
jobs = client.jobs.list
90-
if str(job_id) not in jobs:
127+
if str(job_id) not in client.jobs.list:
91128
break
92129
time.sleep(2)
93130
elapsed += 2

exploitation/searchsploit_engine.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,40 @@
77
logger = logging.getLogger("pentest-agent.searchsploit")
88

99

10+
def _build_query(service: str, version: str) -> str:
11+
"""
12+
Strip protocol/transport names, port numbers and generic daemon suffixes.
13+
Keep only the product name and version for searchsploit.
14+
e.g. "http apache httpd 2.2.8" → "apache 2.2.8"
15+
"netbios-ssn Samba smbd 3.0.20" → "samba 3.0.20"
16+
"ftp ProFTPD 1.3.1" → "proftpd 1.3.1"
17+
"ssh OpenSSH 7.4" → "openssh 7.4"
18+
"smb microsoft-ds" → (empty → skipped)
19+
"""
20+
# Protocol/transport names and generic daemon suffixes to remove
21+
noise = {
22+
"http", "https", "ftp", "ssh", "smtp", "smb", "rdp", "ldap",
23+
"netbios", "netbios-ssn", "netbios-ns", "microsoft-ds",
24+
"ssl", "tcp", "udp", "rpc", "msrpc", "domain", "sunrpc",
25+
"httpd", "smbd", "sshd", "ftpd", "telnet", "pop3", "imap",
26+
}
27+
combined = f"{service} {version}".lower()
28+
tokens = combined.split()
29+
filtered = [
30+
t for t in tokens
31+
if t not in noise
32+
and not t.startswith("-")
33+
and not t.isdigit() # Remove bare port numbers (445, 80, 22...)
34+
]
35+
return " ".join(filtered).strip()
36+
37+
1038
class SearchsploitEngine:
1139
def run(self, service: str, version: str) -> Dict[str, Any]:
1240
if not shutil.which("searchsploit"):
1341
return {"status": "skipped", "output": "searchsploit not installed (apt install exploitdb)"}
1442

15-
query = f"{service} {version}".strip()
43+
query = _build_query(service, version)
1644
if not query:
1745
return {"status": "skipped", "output": "Empty query"}
1846

exploitation/web/wpscan_engine.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ def run(self, target_url: str) -> Dict[str, Any]:
1818
if not shutil.which("wpscan"):
1919
return {"status": "skipped", "output": "wpscan not installed (gem install wpscan)"}
2020

21+
# Ensure URL is properly formatted (http://IP:PORT)
22+
if not target_url.startswith(("http://", "https://")):
23+
target_url = f"http://{target_url}"
24+
2125
os.makedirs(self.output_dir, exist_ok=True)
2226
out_file = str(self.output_dir / "wpscan_output.json")
2327

0 commit comments

Comments
 (0)