From 5539ff24f47443128d1a4b291ed247f73642c1d1 Mon Sep 17 00:00:00 2001 From: Stefano Amorelli Date: Sun, 6 Sep 2026 13:55:48 +0300 Subject: [PATCH 1/2] fix(yara): replace prose-colliding webshell strings with family markers `php_webshell_known` matched the bare substrings "behinder" and "WSO " under `any of them`, so ordinary prose produced a CRITICAL YR2 finding at 0.9 confidence with a remediation telling the reader to remove a webshell. The German words "behindert" and "Behinderung" contain the first string, and the product name "WSO 2 Micro Integrator" contains the second, as reported in #487. Behinder is now identified by its hardcoded AES key, md5("rebeyond") truncated to 16 characters, which its PHP, ASP and JSP shells share and which signature-base [1] uses for the same purpose. The key is written as a YARA hex string so the packaged rule file does not carry the indicator in plaintext, in the spirit of the encoded malware rules. WSO is identified by the helper names oRb introduced in 2.x: `wsoEx(`, `WSO_VERSION` and `wsoSecParam`. I checked a WSO 2.5 source against the old and new rule: the old one hit only the banner and the bare "WSO " string, the new one hits the banner and all three helpers, so no known sample is lost. I preferred narrowing the strings over a `fullword` modifier because a document that names the Behinder family would still have scored CRITICAL, and "WSO 2" would still have matched. Fixes #487 [1]: https://github.com/Neo23x0/signature-base Signed-off-by: Stefano Amorelli --- src/skillspector/yara_rules/webshells.yar | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/skillspector/yara_rules/webshells.yar b/src/skillspector/yara_rules/webshells.yar index 0b4817f18..830d36cc4 100644 --- a/src/skillspector/yara_rules/webshells.yar +++ b/src/skillspector/yara_rules/webshells.yar @@ -56,13 +56,15 @@ rule php_webshell_known $c99v2 = "c99_sess_put" nocase $r57 = "r57shell" nocase $wso = "Web Shell by oRb" nocase - $wso2 = "WSO " nocase + $wso_ex = "wsoEx(" ascii + $wso_ver = "WSO_VERSION" ascii + $wso_sec = "wsoSecParam" 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] $godzilla = "GodzillaShell" nocase $china_chopper = "China Chopper" nocase condition: From f465db48b6bda9ccb9dab20a7301acb8a82c9413 Mon Sep 17 00:00:00 2001 From: Stefano Amorelli Date: Sun, 6 Sep 2026 13:55:48 +0300 Subject: [PATCH 2/2] test(yara): pin php_webshell_known against prose and family samples The benign cases are the two reproductions from #487 plus a document that names the Behinder and WSO families without shipping them. The malicious cases are a Behinder PHP shell, a Behinder JSP shell and a WSO fragment carrying `WSO_VERSION` and `wsoEx(`. All six fail against the previous rule: the prose cases fired and the samples were missed, so the tests guard both directions. The samples are base64-encoded like the existing reverse shell fixture so the indicators do not sit in the repository in plaintext. Signed-off-by: Stefano Amorelli --- tests/nodes/analyzers/test_static_yara.py | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/nodes/analyzers/test_static_yara.py b/tests/nodes/analyzers/test_static_yara.py index ccee96429..c20d16698 100644 --- a/tests/nodes/analyzers/test_static_yara.py +++ b/tests/nodes/analyzers/test_static_yara.py @@ -87,6 +87,17 @@ def _reverse_shell_fixture() -> str: return base64.b64decode("YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQ0NCAwPiYx").decode() +_WEBSHELL_FIXTURES = { + "behinder_php": "PD9waHAgQGVycm9yX3JlcG9ydGluZygwKTsgc2Vzc2lvbl9zdGFydCgpOyAka2V5PSJlNDVlMzI5ZmViNWQ5MjViIjsKJF9TRVNTSU9OWydrJ109JGtleTsgJHBvc3Q9ZmlsZV9nZXRfY29udGVudHMoInBocDovL2lucHV0Iik7CiRwb3N0PW9wZW5zc2xfZGVjcnlwdCgkcG9zdCwgIkFFUzEyOCIsICRrZXkpOyBldmFsKCRwb3N0KTsgPz4K", + "behinder_jsp": "PCVAcGFnZSBpbXBvcnQ9ImphdmEudXRpbC4qLGphdmF4LmNyeXB0by4qIiU+CjwlIFN0cmluZyBrPSJlNDVlMzI5ZmViNWQ5MjViIjsgc2Vzc2lvbi5wdXRWYWx1ZSgidSIsayk7CkNpcGhlciBjPUNpcGhlci5nZXRJbnN0YW5jZSgiQUVTIik7ICU+Cg==", + "wso_php": "PD9waHAgZGVmaW5lKCdXU09fVkVSU0lPTicsICcyLjUnKTsKZnVuY3Rpb24gd3NvRXgoJGluKSB7ICRvdXQ9Jyc7IGlmKGZ1bmN0aW9uX2V4aXN0cygnZXhlYycpKSB7IEBleGVjKCRpbiwkb3V0KTsgfQpyZXR1cm4gJG91dDsgfQo=", +} + + +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) @@ -553,6 +564,32 @@ 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( + ("fixture", "filename"), + [ + ("behinder_php", "shell.php"), + ("behinder_jsp", "shell.jsp"), + ("wso_php", "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 ──────────────────────────────────────────────────────