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.
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
zitierte Materialien gelten deren eigene Lizenzen. Quellenangaben bedeuten keine
|
||||
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.
|
||||
Zusätzliche strukturierte Logkorrelation ist optional; der Batch-Helfer aktiviert sie nicht.
|
||||
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]
|
||||
name = "netscaler-triage"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
description = "Offline NetScaler evidence analysis with inspectable decoding and provenance"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""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():
|
||||
raise FileExistsError("Output already exists")
|
||||
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(
|
||||
[path for _, path in inputs],
|
||||
prepared,
|
||||
|
||||
@@ -97,6 +97,118 @@
|
||||
"required_any_literal": [
|
||||
"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.
|
||||
matching: exact
|
||||
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
|
||||
|
||||
BLOCK = 1024 * 1024
|
||||
OVERLAP = 4096
|
||||
OVERLAP = 8192
|
||||
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"
|
||||
)
|
||||
@@ -69,6 +69,8 @@ EXACT = {
|
||||
}
|
||||
CAPTURE = {
|
||||
"etc/httpd.conf",
|
||||
"etc/blx/nshttpd.conf",
|
||||
"usr/local/bin/blx/nshttpd/conf/httpd.conf",
|
||||
"nsconfig/httpd.conf",
|
||||
"flash/nsconfig/httpd.conf",
|
||||
"nsconfig/crontab",
|
||||
@@ -264,6 +266,11 @@ def scan_filesystem(
|
||||
r["id"]: tuple(t.encode() for t in r.get("required_any_literal", []))
|
||||
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_paths = {x["value"].lstrip("/") for x in intel["indicators"] if x["type"] == "path"}
|
||||
network_values = sorted(
|
||||
@@ -345,6 +352,8 @@ def scan_filesystem(
|
||||
if context(self.path) == "log":
|
||||
patterns.append(("script_or_crash_log", LOG))
|
||||
for rule, pattern in patterns:
|
||||
if rule in rule_paths and not rule_paths[rule].search(self.path):
|
||||
continue
|
||||
gates = rule_gates.get(rule, ())
|
||||
if gates and not any(token in lower for token in gates):
|
||||
continue
|
||||
@@ -482,6 +491,39 @@ def scan_filesystem(
|
||||
note="Vendor binaries can legitimately have these bits",
|
||||
)
|
||||
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
|
||||
view = View(path, "raw")
|
||||
unpacked: View | None = None
|
||||
|
||||
@@ -95,6 +95,18 @@ class EasyCliTests(unittest.TestCase):
|
||||
self.invoke(["scan", str(folder)])
|
||||
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:
|
||||
folder = self.case / "logs"
|
||||
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)
|
||||
Reference in new issue
Block a user