Expand offline review checks and fix relative output finalization
This commit is contained in:
1 parent
83f53adb29
commit
b4fd5fb6ff
12 files changed
+571
-6
No files matched your search
+17
-1
@@ -32,5 +32,21 @@ Prüfkontext, nicht automatisch eine veröffentlichte kampagnenspezifische IOC.
|
|||||||
Die neuen Regeln ersetzen keine vollständige Datenflussanalyse einer Webshell.
|
Die neuen Regeln ersetzen keine vollständige Datenflussanalyse einer Webshell.
|
||||||
404 und fehlende temporäre Dateien sind keine sicheren Misserfolgsbeweise.
|
404 und fehlende temporäre Dateien sind keine sicheren Misserfolgsbeweise.
|
||||||
|
|
||||||
149 automatisierte Tests bestanden. Dies bestätigt die dokumentierte Funktion,
|
152 automatisierte Tests bestanden. Dies bestätigt die dokumentierte Funktion,
|
||||||
keine universelle Erkennungsrate oder vollständige Unversehrtheit.
|
keine universelle Erkennungsrate oder vollständige Unversehrtheit.
|
||||||
|
|
||||||
|
|
||||||
|
Ergänzung: Zehn weitere Sigma-Prüfungen und elf Inhaltsprüfungen im
|
||||||
|
Dateisystemmodus ergänzen die bisherige Abdeckung. Neu geprüft werden unter
|
||||||
|
anderem kommentierte Apache-Schutzdirektiven, Startup-Python, Core-Löschbefehle,
|
||||||
|
ungewöhnliche ACS-/WS-Federation-Felder sowie erfasste Prozess-/Open-File-Listen.
|
||||||
|
PHP/XHTML in Webverzeichnissen und SIG/DEB-Kandidaten werden als Namenshinweis
|
||||||
|
markiert. Bekannte admin_ui-Verzeichnisse und PHP unter gui/websocketd sind von
|
||||||
|
der pauschalen Namensprüfung ausgenommen, nicht von Inhalts- oder Hashprüfungen.
|
||||||
|
BLX-Konfiguration /etc/blx/nshttpd.conf wird ebenfalls erfasst.
|
||||||
|
|
||||||
|
Byte-Anomalien werden in Originalbytes geprüft; UTF-8-Ersatzzeichen in Logs
|
||||||
|
sind nur ein zusätzlicher Suchhinweis. HTTP-Fehlermuster gelten im Dateisystemmodus
|
||||||
|
nur für httperror-Dateien, nicht für Binärpakete mit ähnlichen Textfragmenten.
|
||||||
|
Private Suchindikatoren bleiben separat. Die Existenz eines Indikators in einer
|
||||||
|
zugelieferten Liste ist kein unabhängig bestätigter Malware- oder C2-Nachweis.
|
||||||
@@ -302,3 +302,36 @@ der oben beschriebenen CLI-Berichtsstruktur.
|
|||||||
Der Programmcode steht unter der [MIT-Lizenz](LICENSE.txt). Für Abhängigkeiten und
|
Der Programmcode steht unter der [MIT-Lizenz](LICENSE.txt). Für Abhängigkeiten und
|
||||||
zitierte Materialien gelten deren eigene Lizenzen. Quellenangaben bedeuten keine
|
zitierte Materialien gelten deren eigene Lizenzen. Quellenangaben bedeuten keine
|
||||||
Zugehörigkeit zu Citrix oder den genannten Forschenden.
|
Zugehörigkeit zu Citrix oder den genannten Forschenden.
|
||||||
|
|
||||||
|
|
||||||
|
## Ergänzende Prüfungen und ihre Grenzen
|
||||||
|
|
||||||
|
Die zusätzlichen Sigma-Regeln prüfen ungewöhnliche ACS-/WS-Federation-Felder,
|
||||||
|
Core-Datei-Löschbefehle, PHP-Handler, kommentierte Schutzdirektiven und auffällige
|
||||||
|
Webartefakt-Antworten. Sie gelten auch für sicher dekodierte Textansichten.
|
||||||
|
Erfasste Prozess- und Open-File-Listen können nach Prozessen unter Webkonten und
|
||||||
|
NSPPE-Dateireferenzen durchsucht werden. Das Werkzeug erhebt diese Listen nicht
|
||||||
|
live: Ohne entsprechende Sicherung ist dieser Zustand nicht prüfbar.
|
||||||
|
|
||||||
|
Im Modus `filesystem` kommen Pfadprüfungen für PHP-/XHTML-Dateien in Webverzeichnissen,
|
||||||
|
SIG-/DEB-Kandidaten, Startup-Python und BLX-Konfiguration hinzu. Die bestehenden
|
||||||
|
Hash- und SUID/SGID-Prüfungen bleiben aktiv. Ursprüngliche Rechte und Eigentümer
|
||||||
|
sind nur mit verlässlichen Archivmetadaten bewertbar; lokale Kopien können diese
|
||||||
|
verändern. Logregeln werden im Dateisystemmodus auf passende Logpfade begrenzt,
|
||||||
|
damit Zeichenketten in Herstellerpaketen nicht als Logereignisse erscheinen.
|
||||||
|
|
||||||
|
Alle ergänzenden Verhaltensprüfungen liefern Prüfkandidaten. Ein HTTP-404, ein
|
||||||
|
PHP-Dateiname, eine auskommentierte Direktive oder nicht-ASCII-Text ist allein kein
|
||||||
|
Angriffsnachweis. Mit einem vertrauenswürdigen Stand desselben Builds vergleichen.
|
||||||
|
Fehlende Logs, Prozesslisten oder Dateien bedeuten fehlende Prüfbarkeit.
|
||||||
|
|
||||||
|
Zusätzliche private IOCs als CSV mit `type,value,status,description` laden:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
netscaler-triage ./logs --ioc-file ./private/indicators.csv --output ./new-log-review
|
||||||
|
netscaler-triage filesystem ./export-root --ioc-file ./private/indicators.csv --output ./new-file-review
|
||||||
|
```
|
||||||
|
|
||||||
|
Nicht unabhängig bestätigte Indikatoren als `reference_only` kennzeichnen.
|
||||||
|
Sie werden gesucht, aber nicht automatisch als bösartig bewertet oder als
|
||||||
|
öffentliche Threat Intelligence weitergegeben.
|
||||||
@@ -188,3 +188,44 @@ Die Erweiterung verarbeitet Archive ohne Ausführung enthaltener Programme.
|
|||||||
Unvollständige gzip-Streams werden als Verarbeitungsfehler ausgewiesen.
|
Unvollständige gzip-Streams werden als Verarbeitungsfehler ausgewiesen.
|
||||||
Zusätzliche strukturierte Logkorrelation ist optional; der Batch-Helfer aktiviert sie nicht.
|
Zusätzliche strukturierte Logkorrelation ist optional; der Batch-Helfer aktiviert sie nicht.
|
||||||
Erkennungsgrenzen: FILESYSTEM_COVERAGE.txt. Validierung ist keine Clean-Garantie.
|
Erkennungsgrenzen: FILESYSTEM_COVERAGE.txt. Validierung ist keine Clean-Garantie.
|
||||||
|
|
||||||
|
|
||||||
|
SUPPLEMENTAL REVIEW CHECKS — 2026-10-05
|
||||||
|
|
||||||
|
Version 1.0.2 adds ten low-severity Sigma review rules and eleven filesystem
|
||||||
|
content checks, plus candidate web-tree file names and BLX configuration paths.
|
||||||
|
The default Sigma catalog validates with 57 rules; public indicators remain
|
||||||
|
246. Unverified additional indicators are kept in an optional private pack.
|
||||||
|
|
||||||
|
All 151 tests passed with warnings treated as errors. Ruff lint, formatting and
|
||||||
|
strict mypy passed. Positive and negative controls exercise each new Sigma rule
|
||||||
|
on raw and decoded views. Filesystem tests cover original anomalous bytes,
|
||||||
|
configuration path restrictions and suppression of an error-message lookalike
|
||||||
|
inside a binary package. Eleven filesystem tests passed after increasing the
|
||||||
|
stream overlap to 8 KiB for bounded patterns spanning a block boundary.
|
||||||
|
|
||||||
|
An isolated installed package also validated all 57 rules. All 25 supplemental
|
||||||
|
private indicator values matched raw and Base64-decoded text in a separate
|
||||||
|
private check. This demonstrates matching behavior, not malicious attribution.
|
||||||
|
|
||||||
|
Two existing configuration/program exports were rescanned: 5,361 and 5,319
|
||||||
|
regular files, with no read errors and no hits from the added checks. Existing
|
||||||
|
heuristic candidates remain separate from newly added checks. A retained larger
|
||||||
|
snapshot inventory was compared against all 12 supplemental SHA-256 values:
|
||||||
|
53,696 file hashes, no match. This inventory comparison did not reread the
|
||||||
|
original larger archive and does not prove current system integrity.
|
||||||
|
|
||||||
|
|
||||||
|
RELATIVE OUTPUT REGRESSION
|
||||||
|
|
||||||
|
A real-data run completed analysis but failed while finalizing a relative output
|
||||||
|
path. Preparation resolves paths internally; the CLI now resolves its preparation
|
||||||
|
root before constructing relative provenance references. A regression verifies
|
||||||
|
SUMMARY.txt and each manifest input root when --output is relative. All 152 tests
|
||||||
|
then passed with warnings treated as errors; Ruff and strict mypy passed.
|
||||||
|
|
||||||
|
The completed run's output was recovered only after every one of its 851 input
|
||||||
|
inventory entries matched a freshly prepared copy. It processed 1,117,532 records:
|
||||||
|
two missing-file review events, no supplemental IOC or file-hash hit. Status
|
||||||
|
remains partial: ten binary-file read stops and 106,842 decoding failures, with
|
||||||
|
no matching or inventory errors. These counts are not compromise indicators.
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "netscaler-triage"
|
name = "netscaler-triage"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
description = "Offline NetScaler evidence analysis with inspectable decoding and provenance"
|
description = "Offline NetScaler evidence analysis with inspectable decoding and provenance"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Offline analysis; no discovered payload is ever executed."""
|
"""Offline analysis; no discovered payload is ever executed."""
|
||||||
|
|
||||||
__version__ = "1.0.1"
|
__version__ = "1.0.2"
|
||||||
@@ -369,7 +369,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
if args.output.exists():
|
if args.output.exists():
|
||||||
raise FileExistsError("Output already exists")
|
raise FileExistsError("Output already exists")
|
||||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
prepared = fresh_destination(args.output.parent, ".triage-preparation")
|
prepared = fresh_destination(args.output.parent, ".triage-preparation").resolve()
|
||||||
inputs, preparation_manifest = prepare_inputs(
|
inputs, preparation_manifest = prepare_inputs(
|
||||||
[path for _, path in inputs],
|
[path for _, path in inputs],
|
||||||
prepared,
|
prepared,
|
||||||
|
|||||||
@@ -97,6 +97,118 @@
|
|||||||
"required_any_literal": [
|
"required_any_literal": [
|
||||||
"nsconfig"
|
"nsconfig"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "httpd_commented_security_directive",
|
||||||
|
"regex": "^[ \\t]*#[ \\t]*(?:Require[ \\t]+all[ \\t]+denied|php_flag[ \\t]+engine[ \\t]+off)\\b",
|
||||||
|
"description": "An Apache restriction is commented out. Compare with a trusted configuration of the same build; comments can be vendor defaults.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"#"
|
||||||
|
],
|
||||||
|
"path_regex": "(?:^|/)(?:httpd|nshttpd)\\.conf$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "httpd_php_handler_review",
|
||||||
|
"regex": "^[ \\t]*AddHandler[ \\t]+application/x-httpd-php\\b",
|
||||||
|
"description": "PHP handler declaration. Check the mapped extensions and vendor baseline; a handler alone is not malicious.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"addhandler"
|
||||||
|
],
|
||||||
|
"path_regex": "(?:^|/)(?:httpd|nshttpd)\\.conf$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "startup_python_reference",
|
||||||
|
"regex": "(?:/flash)?/nsconfig/rc\\.netscaler[^\\r\\n]{0,300}\\bpython(?:[23](?:\\.\\d+)?)?\\b",
|
||||||
|
"description": "Python is referenced alongside the startup configuration path. Verify actual file contents and administrative intent.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"rc.netscaler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core_file_removal",
|
||||||
|
"regex": "\\brm[ \\t]+(?:-[a-z]+[ \\t]+){0,3}[\\x22\\x27]?/var/core/\\*",
|
||||||
|
"description": "A command references removal of core files. This can be maintenance or evidence deletion; a logged command alone does not prove execution.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"/var/core/"
|
||||||
|
],
|
||||||
|
"path_regex": "(?:^|/)(?:notice|sh|bash)\\.log(?:\\.|$)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wsfed_request_id_anomaly",
|
||||||
|
"regex": "WSFed:[ \\t]*request_id[^\\r\\n]{0,2048}(?:[\\x80-\\xff\\xfe]|\\^[^\\r\\n]{0,256}\\^)",
|
||||||
|
"description": "WS-Federation request ID contains unusual characters. Encoding, corruption and malformed input are alternatives; this does not prove memory disclosure.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"wsfed:"
|
||||||
|
],
|
||||||
|
"path_regex": "(?:^|/)ns\\.log(?:\\.|$)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "acs_control_marker",
|
||||||
|
"regex": "acs=<[^\\r\\n]{0,2048}\\xf0\\^M[^\\r\\n]{0,2048}>,[ \\t]*for",
|
||||||
|
"description": "ACS field contains an unusual byte/control representation. Inspect original bytes and authentication context; not proof of memory disclosure.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"acs=<"
|
||||||
|
],
|
||||||
|
"path_regex": "(?:^|/)ns\\.log(?:\\.|$)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "web_artifact_response_review",
|
||||||
|
"regex": "(?:nsginstaller\\.deb|nsgclient[^\\s\\x22?]*|[0-9a-f]{6}\\.ico)[^\\r\\n\\x22]{0,512}HTTP/1\\.[01]\\x22[ \\t]+(?:404|202)[ \\t]+[0-9]{4}\\b",
|
||||||
|
"description": "A candidate artifact returned 202 or 404 with a four-digit response size. Status and size do not establish a web shell; inspect response content and filesystem evidence.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"nsginstaller",
|
||||||
|
"nsgclient",
|
||||||
|
".ico"
|
||||||
|
],
|
||||||
|
"path_regex": "(?:^|/)httpaccess[^/]*$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "web_artifact_error_review",
|
||||||
|
"regex": "(?:File does not exist|not found|\\[error\\])[^\\r\\n]{0,1024}(?:nsginstaller\\.deb|nsgclient|[0-9a-f]{6}/\\.sig)",
|
||||||
|
"description": "Web server error mentions a candidate artifact. A missing-file error usually supports absence at that moment, not successful installation.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"nsginstaller",
|
||||||
|
"nsgclient",
|
||||||
|
"/.sig"
|
||||||
|
],
|
||||||
|
"path_regex": "(?:^|/)httperror[^/]*$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "packet_engine_open_file_review",
|
||||||
|
"regex": "^NSPPE\\S*[ \\t]+\\d+[^\\r\\n]{0,1024}(?:/var/netscaler/|callhome)",
|
||||||
|
"description": "A captured open-file listing associates NSPPE with a candidate path. Inspect file type and process context; legitimate open files can match.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"nsppe"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "web_user_process_review",
|
||||||
|
"regex": "^(?:nobody|daemon)[ \\t]+\\d+[ \\t]+(?!(?:[^\\r\\n]*)(?:/bin/httpd|/usr/sbin/nshttpd)\\b)[^\\r\\n]{1,2048}$",
|
||||||
|
"description": "A captured process listing contains a non-httpd process under a web-service account. Validate the executable and parent; legitimate helper processes can match.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"nobody",
|
||||||
|
"daemon"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "startup_python_content",
|
||||||
|
"regex": "^[ \\t]*(?!#)[^\\r\\n]{0,512}\\bpython(?:[23](?:\\.\\d+)?)?\\b",
|
||||||
|
"path_regex": "(?:^|/)(?:flash/)?nsconfig/rc\\.netscaler$",
|
||||||
|
"description": "Python in startup configuration; compare with authorized configuration.",
|
||||||
|
"source": "",
|
||||||
|
"required_any_literal": [
|
||||||
|
"python"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -13162,3 +13162,206 @@ indicators:
|
|||||||
A match does not establish compromise.
|
A match does not establish compromise.
|
||||||
matching: exact
|
matching: exact
|
||||||
context_only: false
|
context_only: false
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Httpd Commented Security Directive
|
||||||
|
id: 0da9523e-6cf7-560d-b6ba-11169fbb53bc
|
||||||
|
name: httpd_commented_security_directive
|
||||||
|
status: experimental
|
||||||
|
description: An Apache restriction is commented out. Compare with a trusted configuration
|
||||||
|
of the same build; comments can be vendor defaults.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?im)^[ \t]*#[ \t]*(?:Require[ \t]+all[ \t]+denied|php_flag[ \t]+engine[
|
||||||
|
\t]+off)\b
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Httpd Php Handler Review
|
||||||
|
id: 060a060d-71cf-5632-aaab-61cf04128ab2
|
||||||
|
name: httpd_php_handler_review
|
||||||
|
status: experimental
|
||||||
|
description: PHP handler declaration. Check the mapped extensions and vendor baseline;
|
||||||
|
a handler alone is not malicious.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?im)^[ \t]*AddHandler[ \t]+application/x-httpd-php\b
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Startup Python Reference
|
||||||
|
id: 1b9bc1d2-914e-5ca0-8989-911f0860e0ec
|
||||||
|
name: startup_python_reference
|
||||||
|
status: experimental
|
||||||
|
description: Python is referenced alongside the startup configuration path. Verify
|
||||||
|
actual file contents and administrative intent.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?i)(?:/flash)?/nsconfig/rc\.netscaler[^\r\n]{0,300}\bpython(?:[23](?:\.\d+)?)?\b
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Core File Removal
|
||||||
|
id: 6affc1b7-cc98-5d60-b822-5ca306a0f5ac
|
||||||
|
name: core_file_removal
|
||||||
|
status: experimental
|
||||||
|
description: A command references removal of core files. This can be maintenance or
|
||||||
|
evidence deletion; a logged command alone does not prove execution.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?i)\brm[ \t]+(?:-[a-z]+[ \t]+){0,3}[\x22\x27]?/var/core/\*
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Wsfed Request Id Anomaly
|
||||||
|
id: 04997de6-7489-5045-9d89-ca58c6a6cda2
|
||||||
|
name: wsfed_request_id_anomaly
|
||||||
|
status: experimental
|
||||||
|
description: WS-Federation request ID contains unusual characters. Encoding, corruption
|
||||||
|
and malformed input are alternatives; this does not prove memory disclosure.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?i)WSFed:[ \t]*request_id[^\r\n]{0,2048}(?:[\x80-\xff\ufffd]|\^[^\r\n]{0,256}\^)
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Acs Control Marker
|
||||||
|
id: 1fd8ae59-7fa2-5996-8fb7-c22a175e4b59
|
||||||
|
name: acs_control_marker
|
||||||
|
status: experimental
|
||||||
|
description: ACS field contains an unusual byte/control representation. Inspect original
|
||||||
|
bytes and authentication context; not proof of memory disclosure.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?i)acs=<[^\r\n]{0,2048}(?:\xf0|\ufffd)\^M[^\r\n]{0,2048}>,[ \t]*for
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Web Artifact Response Review
|
||||||
|
id: 7e7dbc21-2824-5f7d-a230-849fc84eeede
|
||||||
|
name: web_artifact_response_review
|
||||||
|
status: experimental
|
||||||
|
description: A candidate artifact returned 202 or 404 with a four-digit response size.
|
||||||
|
Status and size do not establish a web shell; inspect response content and filesystem
|
||||||
|
evidence.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?i)(?:nsginstaller\.deb|nsgclient[^\s\x22?]*|[0-9a-f]{6}\.ico)[^\r\n\x22]{0,512}HTTP/1\.[01]\x22[
|
||||||
|
\t]+(?:404|202)[ \t]+[0-9]{4}\b
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Web Artifact Error Review
|
||||||
|
id: 2a24ad72-0b49-5717-98cf-400d0b0e164c
|
||||||
|
name: web_artifact_error_review
|
||||||
|
status: experimental
|
||||||
|
description: Web server error mentions a candidate artifact. A missing-file error
|
||||||
|
usually supports absence at that moment, not successful installation.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?i)(?:File does not exist|not found|\[error\])[^\r\n]{0,1024}(?:nsginstaller\.deb|nsgclient|[0-9a-f]{6}/\.sig)
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Packet Engine Open File Review
|
||||||
|
id: 89dd38c0-6b27-5931-ac71-fa8be5da8e61
|
||||||
|
name: packet_engine_open_file_review
|
||||||
|
status: experimental
|
||||||
|
description: A captured open-file listing associates NSPPE with a candidate path.
|
||||||
|
Inspect file type and process context; legitimate open files can match.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?im)^NSPPE\S*[ \t]+\d+[^\r\n]{0,1024}(?:/var/netscaler/|callhome)
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
|
|
||||||
|
---
|
||||||
|
title: NetScaler Web User Process Review
|
||||||
|
id: 2843041d-c85b-59e1-a8c5-b60fd87233d0
|
||||||
|
name: web_user_process_review
|
||||||
|
status: experimental
|
||||||
|
description: A captured process listing contains a non-httpd process under a web-service
|
||||||
|
account. Validate the executable and parent; legitimate helper processes can match.
|
||||||
|
author: NetScaler Triage contributors
|
||||||
|
logsource:
|
||||||
|
product: netscaler
|
||||||
|
detection:
|
||||||
|
selection:
|
||||||
|
raw|re: (?im)^(?:nobody|daemon)[ \t]+\d+[ \t]+(?!(?:[^\r\n]*)(?:/bin/httpd|/usr/sbin/nshttpd)\b)[^\r\n]{1,2048}$
|
||||||
|
condition: selection
|
||||||
|
level: low
|
||||||
|
falsepositives:
|
||||||
|
- Legitimate administration, vendor defaults, malformed input or collection artifacts.
|
||||||
|
indicator_status: behavior_pattern
|
||||||
|
references: []
|
||||||
@@ -22,7 +22,7 @@ from pathlib import Path, PurePosixPath
|
|||||||
from typing import Any, BinaryIO
|
from typing import Any, BinaryIO
|
||||||
|
|
||||||
BLOCK = 1024 * 1024
|
BLOCK = 1024 * 1024
|
||||||
OVERLAP = 4096
|
OVERLAP = 8192
|
||||||
RARE = re.compile(
|
RARE = re.compile(
|
||||||
rb"\.ctxs\.receiver|\.local_journal|\.uxd(?:port|lock)|nsmon\.pl|sec_monitor|http_nsc_ldap|http_nsc_clienttype|http_x_ux|uxd_idle_exit|nsc_tass|rhfajaf1h992|7489a0f93c67fa5cdaeb4b921d90594d|update_c08937\.pl|update_result_3567cs\.tgz|id009\.txt|ww009\.php|wbnsisqmfj\.png"
|
rb"\.ctxs\.receiver|\.local_journal|\.uxd(?:port|lock)|nsmon\.pl|sec_monitor|http_nsc_ldap|http_nsc_clienttype|http_x_ux|uxd_idle_exit|nsc_tass|rhfajaf1h992|7489a0f93c67fa5cdaeb4b921d90594d|update_c08937\.pl|update_result_3567cs\.tgz|id009\.txt|ww009\.php|wbnsisqmfj\.png"
|
||||||
)
|
)
|
||||||
@@ -69,6 +69,8 @@ EXACT = {
|
|||||||
}
|
}
|
||||||
CAPTURE = {
|
CAPTURE = {
|
||||||
"etc/httpd.conf",
|
"etc/httpd.conf",
|
||||||
|
"etc/blx/nshttpd.conf",
|
||||||
|
"usr/local/bin/blx/nshttpd/conf/httpd.conf",
|
||||||
"nsconfig/httpd.conf",
|
"nsconfig/httpd.conf",
|
||||||
"flash/nsconfig/httpd.conf",
|
"flash/nsconfig/httpd.conf",
|
||||||
"nsconfig/crontab",
|
"nsconfig/crontab",
|
||||||
@@ -264,6 +266,11 @@ def scan_filesystem(
|
|||||||
r["id"]: tuple(t.encode() for t in r.get("required_any_literal", []))
|
r["id"]: tuple(t.encode() for t in r.get("required_any_literal", []))
|
||||||
for r in rules_doc["rules"]
|
for r in rules_doc["rules"]
|
||||||
}
|
}
|
||||||
|
rule_paths = {
|
||||||
|
r["id"]: re.compile(r["path_regex"], re.I)
|
||||||
|
for r in rules_doc["rules"]
|
||||||
|
if r.get("path_regex")
|
||||||
|
}
|
||||||
known = {x["value"].lower(): x for x in intel["indicators"] if x["type"] == "sha256"}
|
known = {x["value"].lower(): x for x in intel["indicators"] if x["type"] == "sha256"}
|
||||||
known_paths = {x["value"].lstrip("/") for x in intel["indicators"] if x["type"] == "path"}
|
known_paths = {x["value"].lstrip("/") for x in intel["indicators"] if x["type"] == "path"}
|
||||||
network_values = sorted(
|
network_values = sorted(
|
||||||
@@ -345,6 +352,8 @@ def scan_filesystem(
|
|||||||
if context(self.path) == "log":
|
if context(self.path) == "log":
|
||||||
patterns.append(("script_or_crash_log", LOG))
|
patterns.append(("script_or_crash_log", LOG))
|
||||||
for rule, pattern in patterns:
|
for rule, pattern in patterns:
|
||||||
|
if rule in rule_paths and not rule_paths[rule].search(self.path):
|
||||||
|
continue
|
||||||
gates = rule_gates.get(rule, ())
|
gates = rule_gates.get(rule, ())
|
||||||
if gates and not any(token in lower for token in gates):
|
if gates and not any(token in lower for token in gates):
|
||||||
continue
|
continue
|
||||||
@@ -482,6 +491,39 @@ def scan_filesystem(
|
|||||||
note="Vendor binaries can legitimately have these bits",
|
note="Vendor binaries can legitimately have these bits",
|
||||||
)
|
)
|
||||||
if member.isfile():
|
if member.isfile():
|
||||||
|
web_path = path.lower()
|
||||||
|
web_roots = (
|
||||||
|
"var/netscaler/logon/",
|
||||||
|
"var/vpn/",
|
||||||
|
"netscaler/ns_gui/",
|
||||||
|
"var/netscaler/gui/",
|
||||||
|
"netscaler/portal/",
|
||||||
|
)
|
||||||
|
excluded = any(
|
||||||
|
web_path.startswith(prefix + "admin_ui/")
|
||||||
|
for prefix in ("netscaler/ns_gui/", "var/netscaler/gui/")
|
||||||
|
) or (
|
||||||
|
web_path.endswith(".php")
|
||||||
|
and web_path.startswith("var/netscaler/gui/websocketd/")
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
web_path.startswith(web_roots)
|
||||||
|
and web_path.endswith((".php", ".xhtml"))
|
||||||
|
and not excluded
|
||||||
|
) or (
|
||||||
|
web_path.startswith("var/netscaler/gui/")
|
||||||
|
and (
|
||||||
|
web_path.endswith(".sig")
|
||||||
|
or PurePosixPath(web_path).name
|
||||||
|
in {"nsgclient18.deb", "nsginstaller.deb"}
|
||||||
|
)
|
||||||
|
):
|
||||||
|
finding(
|
||||||
|
path,
|
||||||
|
"web_tree_file_review",
|
||||||
|
metadata=row,
|
||||||
|
note="Candidate name/location only; compare with a vendor baseline",
|
||||||
|
)
|
||||||
counts["regular_files"] += 1
|
counts["regular_files"] += 1
|
||||||
view = View(path, "raw")
|
view = View(path, "raw")
|
||||||
unpacked: View | None = None
|
unpacked: View | None = None
|
||||||
|
|||||||
@@ -95,6 +95,18 @@ class EasyCliTests(unittest.TestCase):
|
|||||||
self.invoke(["scan", str(folder)])
|
self.invoke(["scan", str(folder)])
|
||||||
self.assertEqual(len(self.events()), 1)
|
self.assertEqual(len(self.events()), 1)
|
||||||
|
|
||||||
|
def test_relative_output_preserves_preparation_manifest(self) -> None:
|
||||||
|
folder = self.case / "logs"
|
||||||
|
folder.mkdir()
|
||||||
|
(folder / "httpaccess.log").write_text(LOG, encoding="utf-8")
|
||||||
|
self.invoke(["scan", "logs", "--output", "results/new"])
|
||||||
|
output = self.case / "results/new"
|
||||||
|
manifest = json.loads((output / "details/preparation_manifest.json").read_text())
|
||||||
|
self.assertTrue((output / "SUMMARY.txt").is_file())
|
||||||
|
for root in manifest["roots"]:
|
||||||
|
self.assertTrue((output / root["path"]).is_dir())
|
||||||
|
self.assertEqual(len(self.events()), 1)
|
||||||
|
|
||||||
def test_missing_common_drive_uses_current_output_parent(self) -> None:
|
def test_missing_common_drive_uses_current_output_parent(self) -> None:
|
||||||
folder = self.case / "logs"
|
folder = self.case / "logs"
|
||||||
folder.mkdir()
|
folder.mkdir()
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""Review heuristics must retain context and never turn a candidate into proof."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
from netscaler_triage.filesystem import scan_filesystem
|
||||||
|
from netscaler_triage.matching import Matcher
|
||||||
|
|
||||||
|
|
||||||
|
class SupplementalChecksTests(unittest.TestCase):
|
||||||
|
matcher: ClassVar[Matcher]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
cls.matcher = Matcher([])
|
||||||
|
|
||||||
|
def test_review_rules_raw_decoded_and_negative_controls(self) -> None:
|
||||||
|
cases = {
|
||||||
|
"httpd_commented_security_directive": ("# Require all denied", "Require all denied"),
|
||||||
|
"httpd_php_handler_review": (
|
||||||
|
"AddHandler application/x-httpd-php .php",
|
||||||
|
"# AddHandler application/x-httpd-php .php",
|
||||||
|
),
|
||||||
|
"startup_python_reference": (
|
||||||
|
"/flash/nsconfig/rc.netscaler: python3 script.py",
|
||||||
|
"python3 script.py",
|
||||||
|
),
|
||||||
|
"core_file_removal": ("rm -f /var/core/*", "ls /var/core/*"),
|
||||||
|
"wsfed_request_id_anomaly": ("WSFed: request_id bad^M^M", "WSFed: request_id abc-123"),
|
||||||
|
"acs_control_marker": (
|
||||||
|
"acs=<bad\ufffd^M>, for login",
|
||||||
|
"acs=<https://example.invalid/>, for login",
|
||||||
|
),
|
||||||
|
"web_artifact_response_review": (
|
||||||
|
'GET /vpn/abcdef.ico HTTP/1.1" 404 1234',
|
||||||
|
'GET /vpn/abcdef.ico HTTP/1.1" 404 123',
|
||||||
|
),
|
||||||
|
"web_artifact_error_review": (
|
||||||
|
"File does not exist: /vpn/abcdef/.sig",
|
||||||
|
"File does not exist: /favicon.ico",
|
||||||
|
),
|
||||||
|
"packet_engine_open_file_review": (
|
||||||
|
"NSPPE 42 root 3r /var/netscaler/sample",
|
||||||
|
"other 42 root 3r /var/netscaler/sample",
|
||||||
|
),
|
||||||
|
"web_user_process_review": (
|
||||||
|
"nobody 42 0.0 0.0 /bin/helper",
|
||||||
|
"nobody 42 0.0 0.0 /bin/httpd -k start",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for name, (positive, negative) in cases.items():
|
||||||
|
with self.subTest(name=name):
|
||||||
|
hits = self.matcher.search_sigma({"raw": positive})
|
||||||
|
self.assertTrue(any(h["name"] == name and h["level"] == "low" for h in hits))
|
||||||
|
decoded = self.matcher.search_sigma({"raw": "opaque"}, [positive])
|
||||||
|
self.assertTrue(
|
||||||
|
any(h["name"] == name and "decoded:0" in h["basis"] for h in decoded)
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
any(h["name"] == name for h in self.matcher.search_sigma({"raw": negative}))
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_snapshot_paths_original_bytes_and_config_scope(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
source = root / "export"
|
||||||
|
entries = {
|
||||||
|
"etc/blx/nshttpd.conf": (
|
||||||
|
b"# Require all denied\nAddHandler application/x-httpd-php .php\n"
|
||||||
|
),
|
||||||
|
"flash/nsconfig/rc.netscaler": b"python3 /opt/example.py\n",
|
||||||
|
"var/log/ns.log": b"WSFed: request_id \x81\nacs=<abc\xf0^M>, for login\n",
|
||||||
|
"var/vpn/test.PHP": b"inert",
|
||||||
|
"var/netscaler/gui/package.bin": b"not found\x00/opt/Citrix/NSGClient/a",
|
||||||
|
"var/netscaler/gui/test.sig": b"inert",
|
||||||
|
"var/netscaler/gui/admin_ui/vendor.php": b"inert",
|
||||||
|
"var/log/notes.log": b"# Require all denied\n",
|
||||||
|
}
|
||||||
|
for name, content in entries.items():
|
||||||
|
path = source / name
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(content)
|
||||||
|
result = scan_filesystem(source, root / "results")
|
||||||
|
self.assertTrue(result["complete_scan"])
|
||||||
|
rows = [
|
||||||
|
json.loads(line)
|
||||||
|
for line in (root / "results/findings.jsonl").read_text().splitlines()
|
||||||
|
]
|
||||||
|
found = {(r["path"], r["rule"]) for r in rows}
|
||||||
|
for source_path, rule in [
|
||||||
|
("etc/blx/nshttpd.conf", "httpd_commented_security_directive"),
|
||||||
|
("flash/nsconfig/rc.netscaler", "startup_python_content"),
|
||||||
|
("var/log/ns.log", "wsfed_request_id_anomaly"),
|
||||||
|
("var/log/ns.log", "acs_control_marker"),
|
||||||
|
("var/vpn/test.PHP", "web_tree_file_review"),
|
||||||
|
("var/netscaler/gui/test.sig", "web_tree_file_review"),
|
||||||
|
]:
|
||||||
|
self.assertIn((source_path, rule), found)
|
||||||
|
self.assertNotIn(
|
||||||
|
("var/netscaler/gui/admin_ui/vendor.php", "web_tree_file_review"), found
|
||||||
|
)
|
||||||
|
self.assertNotIn(("var/log/notes.log", "httpd_commented_security_directive"), found)
|
||||||
|
self.assertNotIn(("var/netscaler/gui/package.bin", "web_artifact_error_review"), found)
|
||||||
@@ -546,7 +546,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "netscaler-triage"
|
name = "netscaler-triage"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "py7zr" },
|
{ name = "py7zr" },
|
||||||
|
|||||||
Reference in new issue
Block a user