Skip to content

fix(yara): detect multiline socket reverse shells - #594

Open
deepujain wants to merge 4 commits into
NVIDIA:mainfrom
deepujain:fix/592-multiline-reverse-shell
Open

deepujain wants to merge 4 commits into
NVIDIA:mainfrom
deepujain:fix/592-multiline-reverse-shell

Conversation

@deepujain

@deepujain deepujain commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Multiline Python and Perl socket reverse shells can evade the built-in YARA rule because its original expressions stop at line breaks. Match those bounded multiline sequences while requiring shell-specific behavior, so ordinary socket clients that run date or write a log file do not become CRITICAL findings.

Python detection now requires socket-descriptor redirection followed by a shell invocation, including the existing subprocess, pty.spawn and os.execl forms. Perl detection requires standard-stream duplication from the socket and shell execution. The packaged rules keep their original CRLF line endings and 76-column base64 wrapping.

Fixes #592. Both current review findings are addressed in a892688, with replies on their individual threads.

Validation

The three benign-client regressions failed on the previous rule and pass with the repair. All 95 focused YARA tests pass, including the complete Python/Perl shell fixtures and additional pty/exec compatibility controls. Final make lint, make format-check and make test-ci pass: 5,983 passed, 14 skipped, 39 deselected and 4 expected failures. Independent agent review checked the decoded rules and reran the focused tests.

Risk

These remain lexical heuristics with bounded 400-character spans, not a Python or Perl execution model. Tests scan fixture text only; they do not execute the shell examples. No provider-backed integration result is claimed.

Signed-off-by: Deepak Jain <deepujain@gmail.com>
The bounded-gap patterns used '.' which does not match newlines in
YARA regexes, so socket creation and .connect() on separate lines
never matched. Add the 's' (dotall) modifier to the python_socket
and perl_socket patterns so multiline forms are detected.

Fixes the two failing parametrized cases in
test_builtin_reverse_shell_matches_multiline_socket_forms.

Signed-off-by: Deepak Jain <deepujain@gmail.com>

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SkillSpector Review]

Reviewed exact head 8f7cb94dfda2a09b6cdf18b140282b3ad60edf9c. The bounded dot-all syntax is valid and closes the newline gap, but both widened signatures remain generic socket-client detectors rather than reverse-shell detectors. The Python case needs only stream-socket construction plus connect; the Perl case needs only use Socket plus socket creation. With any of them, benign clients become CRITICAL reverse_shell/YR1 findings.

Require bounded shell-specific evidence and add paired positive real-shell and negative ordinary-client regressions for both languages. Keep signature-triggering fixtures encoded per the test module's antivirus-safety convention. Also preserve the packaged rule's existing CRLF bytes: this branch converts all 125 decoded lines to LF, turning a two-expression fix into full-file base64 churn and increasing conflict/audit risk. PRs #593, #594, and #599 are alternatives; only one corrected implementation should land.

All six hosted checks pass and GitHub reports mergeable/clean, but the detector false positives and unnecessary resource rewrite require changes.

[
(
"import socket\ns = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n"
's.connect(("10.0.0.1", 4444))\n',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P0] This positive fixture has no reverse-shell action—only normal socket creation and connection—so the test pins a CRITICAL false positive. Tighten the rule to require bounded shell/descriptor-redirection evidence, make this a negative regression, and use an encoded complete reverse-shell fixture for the positive path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in c20dec8 (pushed to fix/592-multiline-reverse-shell).

Both signatures now require bounded shell-specific evidence instead of matching any socket client: the Python pattern requires os.dup2 or a subprocess call within 400 chars after connect; the Perl pattern requires STD(IN|OUT|ERR) redirection or exec of /bin/sh within 400 chars after socket creation.

Paired regressions added: encoded complete reverse-shell fixtures are the positive path for both languages, and the former positive ordinary-client fixtures are now negative regressions, per the test module's AV-safety convention. The packaged base64 rule was re-encoded from the CRLF original, so the decoded diff is exactly two lines with no full-file churn. tests/nodes/analyzers/test_static_yara.py: 90 passed; ruff lint and format clean.

Please re-review.

The widened Python and Perl signatures matched any socket client: the
Python pattern needed only stream-socket construction plus connect, and
the Perl pattern only use Socket plus socket creation, so ordinary TCP
clients raised CRITICAL reverse_shell/YR1 findings. Both signatures now
require bounded shell-specific evidence (dup2/subprocess after connect
for Python, STD descriptor redirection or /bin/sh exec for Perl).

Paired regressions: encoded complete-shell fixtures are the positive
path for both languages; the former positive ordinary-client fixtures
are now negative regressions. The packaged base64 rule is re-encoded
from the CRLF original, so the decoded diff is exactly two lines with
no full-file churn.

Signed-off-by: Deepak Jain <deepujain@gmail.com>
@deepujain

Copy link
Copy Markdown
Contributor Author

rng1995: the false-positive and churn findings are addressed on head c20dec8. Both widened signatures now require bounded shell-specific evidence: dup2/subprocess after connect for Python, STD descriptor redirection or /bin/sh exec for Perl. The former positive ordinary-client fixtures are now negative regressions, and encoded complete-shell fixtures are the positive path for both languages. The packaged base64 rule was re-encoded from the CRLF original, so the decoded diff is exactly two lines with no full-file churn. All six hosted checks pass on this head. Please re-review when convenient. (On #593/#599 as alternatives: leaving that choice to maintainers.)

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed head c20dec868bcb3a3c7d8ee4d4f1469062c4213557 against the previous review.

The CRLF/resource-churn issue is fixed: all 125 decoded lines retain CRLF, and only the two socket expressions change. Both languages now have encoded complete-shell positive fixtures and plain-client negative regressions. Validation: 90 YARA tests passed; Ruff lint and format checks passed; all six hosted checks passed.

The central false-positive concern remains partially unresolved. I reproduced two benign multiline clients that still produce CRITICAL reverse_shell / YR1 findings through the full analyzer: Python invoking a normal subprocess, and Perl opening a local log file on STDOUT. Neither matches the base rule. The added alternatives do not yet establish shell execution or socket-descriptor redirection; details and reproductions are inline.

Please tighten both signatures and add negative regressions for these ordinary operations before approval.

XCguKlNPQ0tfU1RSRUFNLipcLmNvbm5lY3RcKC8NCiAgICAgICAgJHBlcmxfc29ja2V0ICAgICAg
PSAvdXNlXHMrU29ja2V0Oy4qc29ja2V0XHMqXChccypTT0NLLw0KICAgICAgICAkcGhwX2Zzb2Nr
XCguezAsNDAwfVNPQ0tfU1RSRUFNLnswLDQwMH1cLmNvbm5lY3RcKC57MCw0MDB9KG9zXC5kdXAy
XHMqXCh8c3VicHJvY2Vzc1wuKGNhbGx8UG9wZW58cnVuKVxzKlwoKS9zICAgICAgICAgICAgIA0K

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Require shell-specific evidence in the Python alternative

The decoded $python_socket expression accepts any nearby subprocess.call, Popen, or run invocation, regardless of its arguments; the os.dup2 alternative similarly does not establish socket-to-stdio redirection. This benign multiline client produces CRITICAL reverse_shell / YR1 on this head through static_yara.node, while the base rule does not match:

import socket, subprocess
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 8080))
subprocess.run(["date"], check=True)
s.sendall(b"healthcheck\n")
s.close()

No shell is launched or attached to the socket. Please require actual shell-specific invocation or socket-descriptor redirection evidence and add this kind of ordinary subprocess client as an encoded negative regression. The current plain-client negative has neither operation, so it does not catch this remaining false positive.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a892688. The Python rule now requires bounded socket-file-descriptor redirection plus an actual shell invocation, instead of matching a generic subprocess call. Regression controls retain subprocess, pty.spawn and os.execl shell variants and reject the ordinary date subprocess and logfile-redirection cases. All95 focused YARA tests and5983 CI-suite tests pass; lint and formatting pass. The encoded resource retains76-column wrapping and decoded CRLF.

XCguezAsNDAwfVNPQ0tfU1RSRUFNLnswLDQwMH1cLmNvbm5lY3RcKC57MCw0MDB9KG9zXC5kdXAy
XHMqXCh8c3VicHJvY2Vzc1wuKGNhbGx8UG9wZW58cnVuKVxzKlwoKS9zICAgICAgICAgICAgIA0K
ICAgICAgICAkcGVybF9zb2NrZXQgICAgICA9IC91c2VccytTb2NrZXQ7LnswLDQwMH1zb2NrZXRc
cypcKFxzKlNPQ0suezAsNDAwfShvcGVuXHMqXChccypTVEQoSU58T1VUfEVSUil8ZXhlY1xzKlwo

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Distinguish Perl file logging from socket descriptor redirection

The decoded $perl_socket expression stops the open alternative at STD(IN|OUT|ERR), so merely opening a local log file on STDOUT satisfies the supposed reverse-shell evidence. This benign multiline client produces CRITICAL reverse_shell / YR1 through static_yara.node on this head, while the base rule does not match:

use Socket;
socket(SOCKET, PF_INET, SOCK_STREAM, getprotobyname("tcp"));
connect(SOCKET, sockaddr_in(8080, inet_aton("127.0.0.1")));
open(STDOUT, ">", "client.log") or die $!;
print SOCKET "healthcheck\n";
close(SOCKET);

STDOUT is a local file, and there is no shell execution. Please require socket descriptor duplication/redirection or sufficient additional shell evidence, and add an encoded negative regression covering ordinary file logging.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a892688. The Perl rule now requires STDIN/STDOUT/STDERR duplication from SOCK plus shell execution; opening STDOUT to a client.log file no longer matches. The complete reverse-shell positive fixture remains detected. All95 focused YARA tests and5983 CI-suite tests pass.

Signed-off-by: Deepak Jain <deepujain@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Built-in reverse_shell YARA rule misses multi-line Python/Perl socket reverse shells

2 participants