133 lines
6.4 KiB
Python
133 lines
6.4 KiB
Python
"""Regression cases based on upstream checker output, without network access."""
|
|
|
|
import unittest
|
|
|
|
from rpcscope.checks import classify_msf, classify_nxc
|
|
|
|
|
|
HOST = "10.20.30.41"
|
|
OTHER_HOST = "10.20.30.42"
|
|
|
|
|
|
def nxc_line(module, message, host=HOST):
|
|
return f"{module:16} {host} 445 HOST1 {message}\n"
|
|
|
|
|
|
class CheckerOutputTests(unittest.TestCase):
|
|
def test_ms17_indeterminate_message_is_never_positive(self):
|
|
result = classify_nxc(
|
|
"ms17-010", HOST,
|
|
nxc_line("MS17-010", f"[-] {HOST} Unable to detect if this host is vulnerable"),
|
|
)
|
|
self.assertEqual(result.status, "inconclusive")
|
|
|
|
def test_ms17_upstream_positive_and_negative_markers(self):
|
|
cases = [
|
|
(f"[+] {HOST} is likely VULNERABLE to MS17-010! (Windows 7)", "vulnerable"),
|
|
("[+] VULNERABLE", "vulnerable"),
|
|
(f"[-] {HOST} does NOT appear vulnerable", "not-vulnerable"),
|
|
]
|
|
for message, expected in cases:
|
|
with self.subTest(message=message):
|
|
self.assertEqual(
|
|
classify_nxc("ms17-010", HOST, nxc_line("MS17-010", message)).status,
|
|
expected,
|
|
)
|
|
|
|
def test_markers_must_match_target_and_module(self):
|
|
text = (
|
|
nxc_line("MS17-010", "[+] VULNERABLE", OTHER_HOST)
|
|
+ nxc_line("ZEROLOGON", "[+] VULNERABLE")
|
|
+ nxc_line("SMB", r"[+] LAB\auditor:VULNERABLE")
|
|
+ "description: target may be VULNERABLE\n"
|
|
)
|
|
self.assertEqual(classify_nxc("ms17-010", HOST, text).status, "inconclusive")
|
|
|
|
def test_ansi_and_truncated_module_name_are_attributed(self):
|
|
text = "\x1b[32m" + nxc_line(
|
|
"PRINTNIGH...",
|
|
"[+] Vulnerable, next step https://github.com/ly4k/PrintNightmare",
|
|
) + "\x1b[0m"
|
|
self.assertEqual(classify_nxc("printnightmare", HOST, text).status, "suspected")
|
|
|
|
def test_smbghost_capability_indicator_is_not_a_cve_finding(self):
|
|
text = nxc_line(
|
|
"SMBGHOST", "[+] Potentially vulnerable to SMBGhost (CVE-2020-0796)",
|
|
)
|
|
self.assertEqual(classify_nxc("smbghost", HOST, text).status, "exposed")
|
|
|
|
def test_zerologon_exhausted_attempts_are_a_scoped_negative(self):
|
|
result = classify_nxc("zerologon", HOST, nxc_line("ZEROLOGON", "[+] Attack failed. Target is probably patched."))
|
|
self.assertEqual(result.status, "not-vulnerable")
|
|
self.assertIn("probabilistic", result.evidence)
|
|
|
|
def test_denial_blocks_probe_instead_of_implying_patched(self):
|
|
result = classify_nxc("printnightmare", HOST, nxc_line("PRINTNIGHTMARE", "[-] Failed to bind: STATUS_ACCESS_DENIED"))
|
|
self.assertEqual(result.status, "skipped-access-denied")
|
|
self.assertEqual(classify_nxc("ms17-010", HOST, nxc_line("SMB", "[-] LAB\\: STATUS_LOGON_FAILURE")).status, "skipped-auth-denied")
|
|
self.assertEqual(classify_nxc("ms17-010", HOST, nxc_line("SMB", "[+] LAB\\user:STATUS_LOGON_FAILURE")).status, "inconclusive")
|
|
|
|
def test_mixed_rpc_methods_do_not_hide_positive_binds_behind_another_denial(self):
|
|
text = nxc_line("COERCE_PLUS", "[-] Failed to bind: STATUS_ACCESS_DENIED") + nxc_line("COERCE_PLUS", "[+] VULNERABLE, PetitPotam")
|
|
self.assertEqual(classify_nxc("petitpotam", HOST, text).status, "exposed")
|
|
text = nxc_line("PRINTNIGHTMARE", "[-] Failed to bind: STATUS_ACCESS_DENIED") + nxc_line(
|
|
"PRINTNIGHTMARE", "[+] Vulnerable, next step https://github.com/ly4k/PrintNightmare")
|
|
self.assertEqual(classify_nxc("printnightmare", HOST, text).status, "inconclusive")
|
|
|
|
def test_zerologon_mixed_positive_negative_is_unknown_not_patched(self):
|
|
text = nxc_line("ZEROLOGON", "[+] VULNERABLE") + nxc_line("ZEROLOGON", "[+] Attack failed. Target is probably patched.")
|
|
self.assertEqual(classify_nxc("zerologon", HOST, text).status, "inconclusive")
|
|
|
|
def test_nonzero_exit_fails_closed_even_with_result_markers(self):
|
|
for message in ("[+] VULNERABLE", f"[-] {HOST} does NOT appear vulnerable"):
|
|
with self.subTest(message=message):
|
|
result = classify_nxc(
|
|
"ms17-010", HOST, nxc_line("MS17-010", message), returncode=124,
|
|
)
|
|
self.assertEqual(result.status, "inconclusive")
|
|
self.assertIn("124", result.evidence)
|
|
self.assertEqual(classify_msf(
|
|
HOST, f"[+] {HOST}:445 - Host is likely VULNERABLE to MS17-010!\n", 1,
|
|
).status, "inconclusive")
|
|
|
|
def test_conflicting_nxc_markers_require_review(self):
|
|
text = (
|
|
nxc_line("MS17-010", "[+] VULNERABLE")
|
|
+ nxc_line("MS17-010", f"[-] {HOST} does NOT appear vulnerable")
|
|
)
|
|
result = classify_nxc("ms17-010", HOST, text)
|
|
self.assertEqual(result.status, "inconclusive")
|
|
self.assertIn("conflicting", result.evidence)
|
|
|
|
def test_silence_and_upstream_errors_remain_inconclusive(self):
|
|
cases = [
|
|
("ms17-010", "MS17-010", ""),
|
|
("zerologon", "ZEROLOGON", "[-] Error while connecting to host: DCERPCException, which means this is probably not a DC!"),
|
|
("smbghost", "SMBGHOST", "[-] Error while connecting to host: timed out"),
|
|
]
|
|
for key, module, message in cases:
|
|
with self.subTest(key=key):
|
|
text = nxc_line(module, message) if message else ""
|
|
self.assertEqual(classify_nxc(key, HOST, text).status, "inconclusive")
|
|
|
|
def test_removed_module_is_explicitly_unsupported(self):
|
|
text = nxc_line(
|
|
"NTLM_REFLECTION",
|
|
"[-] [REMOVED] This module has been integrated into the enum_cve module.",
|
|
)
|
|
self.assertEqual(classify_nxc("ntlm-reflection", HOST, text).status, "skipped-unsupported")
|
|
|
|
def test_msf_results_require_target_and_conflicts_are_inconclusive(self):
|
|
positive = f"[+] {HOST}:445 - Host is likely VULNERABLE to MS17-010! (Windows 7)\n"
|
|
negative = f"[-] {HOST}:445 - Host does NOT appear vulnerable.\n"
|
|
self.assertEqual(classify_msf(HOST, positive).status, "vulnerable")
|
|
self.assertEqual(classify_msf(HOST, negative).status, "not-vulnerable")
|
|
self.assertEqual(classify_msf(HOST, positive.replace(HOST, OTHER_HOST)).status, "inconclusive")
|
|
result = classify_msf(HOST, positive + negative)
|
|
self.assertEqual(result.status, "inconclusive")
|
|
self.assertIn("conflicting", result.evidence)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|