module.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <?php namespace pineapple;
  2. abstract class EncryptionFields
  3. {
  4. const WPA = 0x01;
  5. const WPA2 = 0x02;
  6. const WEP = 0x04;
  7. const WPA_PAIRWISE_WEP40 = 0x08;
  8. const WPA_PAIRWISE_WEP104 = 0x10;
  9. const WPA_PAIRWISE_TKIP = 0x20;
  10. const WPA_PAIRWISE_CCMP = 0x40;
  11. const WPA2_PAIRWISE_WEP40 = 0x80;
  12. const WPA2_PAIRWISE_WEP104 = 0x100;
  13. const WPA2_PAIRWISE_TKIP = 0x200;
  14. const WPA2_PAIRWISE_CCMP = 0x400;
  15. const WPA_AKM_PSK = 0x800;
  16. const WPA_AKM_ENTERPRISE = 0x1000;
  17. const WPA_AKM_ENTERPRISE_FT = 0x2000;
  18. const WPA2_AKM_PSK = 0x4000;
  19. const WPA2_AKM_ENTERPRISE = 0x8000;
  20. const WPA2_AKM_ENTERPRISE_FT = 0x10000;
  21. const WPA_GROUP_WEP40 = 0x20000;
  22. const WPA_GROUP_WEP104 = 0x40000;
  23. const WPA_GROUP_TKIP = 0x80000;
  24. const WPA_GROUP_CCMP = 0x100000;
  25. const WPA2_GROUP_WEP40 = 0x200000;
  26. const WPA2_GROUP_WEP104 = 0x400000;
  27. const WPA2_GROUP_TKIP = 0x800000;
  28. const WPA2_GROUP_CCMP = 0x1000000;
  29. }
  30. class Recon extends SystemModule
  31. {
  32. private $scanID = null;
  33. private $dbConnection = null;
  34. const PATH_WS_SCRIPT = '/pineapple/modules/Recon/api/reconpp.py';
  35. const CLI_PINEAP = 'pineap /tmp/pineap.conf';
  36. public function __construct($request)
  37. {
  38. parent::__construct($request, __CLASS__);
  39. $this->dbConnection = false;
  40. $dbLocation = $this->uciGet("pineap.@config[0].recon_db_path");
  41. if (file_exists($dbLocation)) {
  42. $this->dbConnection = new DatabaseConnection($dbLocation);
  43. }
  44. }
  45. public function route()
  46. {
  47. switch ($this->request->action) {
  48. case 'startPineAPDaemon':
  49. $this->startPineAPDaemon();
  50. break;
  51. case 'checkPineAPDaemon':
  52. $this->checkPineAPDaemon();
  53. break;
  54. case 'startNormalScan':
  55. $this->startNormalScan();
  56. break;
  57. case 'startLiveScan':
  58. $this->startLiveScan();
  59. break;
  60. case 'startReconPP':
  61. $this->startReconPP();
  62. break;
  63. case 'stopScan':
  64. $this->stopScan();
  65. break;
  66. case 'getScans':
  67. $this->getScans();
  68. break;
  69. case 'getScanLocation':
  70. $this->getScanLocation();
  71. break;
  72. case 'setScanLocation':
  73. $this->setScanLocation();
  74. break;
  75. case 'checkScanStatus':
  76. $this->checkScanStatus();
  77. break;
  78. case 'loadResults':
  79. $this->loadResults($this->request->scanID);
  80. break;
  81. case 'downloadResults':
  82. $this->downloadResults();
  83. break;
  84. case 'removeScan':
  85. $this->removeScan();
  86. break;
  87. case 'getWSAuthToken':
  88. $this->getWSAuthToken();
  89. break;
  90. }
  91. }
  92. private function startPineAPDaemon()
  93. {
  94. if ($this->checkPineAPDaemon()) {
  95. $this->response = array("success" => true);
  96. return;
  97. }
  98. exec("/etc/init.d/pineapd start", $status_output);
  99. if ($status_output[0] === "Status: OK") {
  100. $this->response = array("success" => true);
  101. } else {
  102. $this->response = array("message" => implode("\n", $status_output));
  103. }
  104. }
  105. private function checkPineAPDaemon()
  106. {
  107. return (bool) $this->checkRunningFull("/usr/sbin/pineapd");
  108. }
  109. private function startNormalScan()
  110. {
  111. $scanDuration = $this->request->scanDuration;
  112. $scanType = $this->request->scanType;
  113. if ($this->checkPineAPDaemon()) {
  114. $this->startPineAPDaemon();
  115. exec(Recon::CLI_PINEAP . " run_scan {$scanDuration} {$scanType}");
  116. $scanID = $this->getCurrentScanID();
  117. $this->response = array("success" => true, "scanID" => $scanID);
  118. } else {
  119. $this->error = "The PineAP Daemon must be running.";
  120. }
  121. }
  122. private function startReconPP()
  123. {
  124. if ($this->checkRunningFull("python " . Recon::PATH_WS_SCRIPT)) {
  125. $this->response = array("success" => true);
  126. return;
  127. }
  128. $dbPath = $this->uciGet("pineap.@config[0].recon_db_path");
  129. $scanID = $this->getCurrentScanID();
  130. $this->execBackground("python " . Recon::PATH_WS_SCRIPT . " {$dbPath} {$scanID}");
  131. $this->response = array("success" => true);
  132. }
  133. private function startLiveScan()
  134. {
  135. if ($this->checkPineAPDaemon()) {
  136. $scanDuration = $this->request->scanDuration;
  137. $scanType = $this->request->scanType;
  138. $scanID = 0;
  139. $dbLocation = $this->uciGet("pineap.@config[0].recon_db_path");
  140. // Check if a scan is already in progress
  141. if (!is_numeric($this->getCurrentScanID())) {
  142. exec(Recon::CLI_PINEAP . " run_scan {$scanDuration} {$scanType}");
  143. $scanID = $this->getCurrentScanID();
  144. $this->execBackground("python " . Recon::PATH_WS_SCRIPT . " {$dbLocation} {$scanID}");
  145. }
  146. $this->startReconPP();
  147. $this->response = array("success" => true, "scanID" => $scanID);
  148. } else {
  149. $this->error = "The PineAP Daemon must be running.";
  150. }
  151. }
  152. private function stopScan()
  153. {
  154. $this->execBackground(Recon::CLI_PINEAP . " stop_scan");
  155. $this->execBackground("pkill -9 -f " . Recon::PATH_WS_SCRIPT);
  156. if (file_exists('/tmp/reconpp.lock')) {
  157. unlink('/tmp/reconpp.lock');
  158. }
  159. $this->response = array("success" => true);
  160. }
  161. private function getCurrentScanID()
  162. {
  163. exec(Recon::CLI_PINEAP . " get_status", $status_output);
  164. if ($status_output[0] === "PineAP is not running") {
  165. $this->stopScan();
  166. $this->response = array("completed" => true, "error" => "The PineAP Daemon must be running.");
  167. return null;
  168. }
  169. $status_output = json_decode(implode("\n", $status_output), true);
  170. return $status_output['scanID'];
  171. }
  172. private function wsRunning()
  173. {
  174. return $this->checkRunningFull("python " . Recon::PATH_WS_SCRIPT);
  175. }
  176. private function checkScanStatus()
  177. {
  178. exec(Recon::CLI_PINEAP . " get_status", $status_output);
  179. if ($status_output[0] === "PineAP is not running") {
  180. $this->stopScan();
  181. $this->response = array("completed" => true, "error" => "The PineAP Daemon must be running.");
  182. return;
  183. }
  184. $status_output = json_decode(implode("\n", $status_output), true);
  185. if ($status_output['scanRunning'] === false) {
  186. $this->stopScan();
  187. $this->response = array("completed" => true);
  188. } else if ($status_output['scanRunning'] === true) {
  189. $this->response = array(
  190. "completed" => false,
  191. "scanID" => $status_output['scanID'],
  192. "scanPercent" => $status_output['scanPercent'],
  193. "continuous" => $status_output['continuous'],
  194. "live" => $this->wsRunning(),
  195. "captureRunning" => $status_output['captureRunning'] === true
  196. );
  197. } else {
  198. $this->stopScan();
  199. $this->response = array("completed" => true, "debug" => $status_output);
  200. }
  201. }
  202. private function loadResults($scanID)
  203. {
  204. $accessPoints = array();
  205. $unassociatedClients = array();
  206. $outOfRangeClients = array();
  207. $rows = $this->dbConnection->query("SELECT ssid, bssid, encryption, channel, signal, last_seen, wps FROM aps WHERE scan_id = '%s';", $scanID);
  208. foreach ($rows as $row) {
  209. $accessPoints[ $row['bssid'] ] = array(
  210. 'ssid' => $row['ssid'],
  211. 'bssid' => $row['bssid'],
  212. 'encryption' => $this->printEncryption($row['encryption']),
  213. 'channel' => $row['channel'],
  214. 'power' => $row['signal'],
  215. 'lastSeen' => $row['last_seen'],
  216. 'wps' => $row['wps'],
  217. 'clients' => array()
  218. );
  219. }
  220. $rows = $this->dbConnection->query("SELECT bssid, mac, last_seen FROM clients WHERE scan_id = '%s';", $scanID);
  221. foreach ($rows as $row) {
  222. $bssid = $row['bssid'];
  223. $mac = $row['mac'];
  224. $lastSeen = $row['last_seen'];
  225. if ($bssid == "FF:FF:FF:FF:FF:FF") {
  226. $unassociatedClients[] = array('mac' => $mac, 'lastSeen' => $lastSeen);
  227. } else if ($accessPoints[ $bssid ] != null && in_array($bssid, $accessPoints[ $bssid ])) {
  228. $accessPoints[$bssid]['clients'][] = array('mac' => $mac, 'lastSeen' => $lastSeen);
  229. } else {
  230. $outOfRangeClients[$mac] = array('bssid' => $bssid, 'lastSeen' => $lastSeen);
  231. }
  232. }
  233. $realAPs = array();
  234. foreach ($accessPoints as $key => $value) {
  235. $realAPs[] = $value;
  236. }
  237. $returnArray = array(
  238. 'ap_list' => $realAPs,
  239. 'unassociated_clients' => $unassociatedClients,
  240. 'out_of_range_clients' => $outOfRangeClients,
  241. );
  242. $this->response = array("results" => $returnArray);
  243. return $returnArray;
  244. }
  245. private function printEncryption($encryptionType)
  246. {
  247. if ($encryptionType === 0) {
  248. return 'Open';
  249. }
  250. $retStr = '';
  251. if ($encryptionType & EncryptionFields::WEP) {
  252. return 'WEP';
  253. } else if (($encryptionType & EncryptionFields::WPA) && ($encryptionType & EncryptionFields::WPA2)) {
  254. $retStr .= 'WPA Mixed ';
  255. } else if ($encryptionType & EncryptionFields::WPA) {
  256. $retStr .= 'WPA ';
  257. } else if ($encryptionType & EncryptionFields::WPA2) {
  258. $retStr .= 'WPA2 ';
  259. }
  260. if (($encryptionType & EncryptionFields::WPA2_AKM_PSK) || ($encryptionType & EncryptionFields::WPA_AKM_PSK)) {
  261. $retStr .= 'PSK ';
  262. } else if (($encryptionType & EncryptionFields::WPA2_AKM_ENTERPRISE) || ($encryptionType & EncryptionFields::WPA_AKM_ENTERPRISE)) {
  263. $retStr .= 'Enterprise ';
  264. } else if (($encryptionType & EncryptionFields::WPA2_AKM_ENTERPRISE_FT) || ($encryptionType & EncryptionFields::WPA_AKM_ENTERPRISE_FT)) {
  265. $retStr .= 'Enterprise FT ';
  266. }
  267. $retStr .= '(';
  268. if (($encryptionType & EncryptionFields::WPA2_PAIRWISE_CCMP) || ($encryptionType & EncryptionFields::WPA_PAIRWISE_CCMP)) {
  269. $retStr .= 'CCMP ';
  270. }
  271. if (($encryptionType & EncryptionFields::WPA2_PAIRWISE_TKIP) || ($encryptionType & EncryptionFields::WPA_PAIRWISE_TKIP)) {
  272. $retStr .= 'TKIP ';
  273. }
  274. // Fix the code below - these never trigger. Make sure to set "return WEP" to retStr += WEP
  275. if (($encryptionType & EncryptionFields::WPA2_PAIRWISE_WEP40) || ($encryptionType & EncryptionFields::WPA_PAIRWISE_WEP40)) {
  276. $retStr .= 'WEP40 ';
  277. }
  278. if (($encryptionType & EncryptionFields::WPA2_PAIRWISE_WEP104) || ($encryptionType & EncryptionFields::WPA_PAIRWISE_WEP104)) {
  279. $retStr .= 'WEP104 ';
  280. }
  281. return substr($retStr, 0, -1) . ')';
  282. }
  283. private function utcToPineapple($timeStr) {
  284. $d = new \DateTime($timeStr . ' UTC');
  285. exec("date +%Z", $tz);
  286. $tz = $tz[0];
  287. $tzo = new \DateTimeZone($tz);
  288. $d->setTimezone($tzo);
  289. return $d->format('Y-m-d G:i:s T');
  290. }
  291. private function getScanObject($scanID)
  292. {
  293. $data = array(
  294. $scanID => array(
  295. 'outOfRangeClients' => array(),
  296. 'unassociatedClients' => array()
  297. )
  298. );
  299. $aps = $this->dbConnection->query("SELECT scan_id, ssid, bssid, encryption, hidden, channel, signal, wps, last_seen FROM aps WHERE scan_id='%d';", $scanID);
  300. foreach ($aps as $ap_row) {
  301. $data[$scanID]['aps'][$ap_row['bssid']] = array(
  302. 'ssid' => $ap_row['ssid'],
  303. 'encryption' => $this->printEncryption($ap_row['encryption']),
  304. 'hidden' => $ap_row['hidden'],
  305. 'channel' => $ap_row['channel'],
  306. 'signal' => $ap_row['signal'],
  307. 'wps' => $ap_row['wps'],
  308. 'last_seen' => $ap_row['last_seen'],
  309. 'clients' => array()
  310. );
  311. $clients = $this->dbConnection->query("SELECT scan_id, mac, bssid, last_seen FROM clients WHERE scan_id='%d' AND bssid='%s';", $ap_row['scan_id'], $ap_row['bssid']);
  312. foreach ($clients as $client_row) {
  313. $data[$scanID]['aps'][$ap_row['bssid']]['clients'][$client_row['mac']] = array(
  314. 'bssid' => $client_row['bssid'],
  315. 'last_seen' => $client_row['last_seen']
  316. );
  317. }
  318. }
  319. $clients = $this->dbConnection->query("
  320. SELECT t1.mac, t1.bssid, t1.last_seen FROM clients t1
  321. LEFT JOIN aps t2 ON
  322. t2.bssid = t1.bssid WHERE t2.bssid IS NULL AND
  323. t1.bssid != 'FF:FF:FF:FF:FF:FF' COLLATE NOCASE AND t1.scan_id='%d';
  324. ", $ap_row['scan_id']);
  325. foreach ($clients as $client_row) {
  326. $data[$scanID]['outOfRangeClients'][$client_row['mac']] = array(
  327. $client_row['bssid']
  328. );
  329. }
  330. $clients = $this->dbConnection->query("SELECT mac FROM clients WHERE bssid='FF:FF:FF:FF:FF:FF' COLLATE NOCASE;");
  331. foreach ($clients as $client_row) {
  332. $data[$scanID]['unassociatedClients'][] = $client_row['mac'];
  333. }
  334. return $data;
  335. }
  336. private function downloadResults()
  337. {
  338. $fileData = $this->getScanObject($this->request->scanID);
  339. $fileName = '/tmp/recon_data.json';
  340. file_put_contents($fileName, json_encode($fileData, JSON_PRETTY_PRINT));
  341. $this->response = array("download" => $this->downloadFile($fileName));
  342. }
  343. private function getScans()
  344. {
  345. if ($this->dbConnection) {
  346. $scans = $this->dbConnection->query("SELECT * FROM scan_ids ORDER BY date DESC;");
  347. if (!isset($scans['databaseQueryError'])) {
  348. $this->response = array('scans' => $scans);
  349. return;
  350. }
  351. }
  352. $this->response = array('scans' => array());
  353. }
  354. private function getScanLocation()
  355. {
  356. $scanLocation = dirname($this->uciGet("pineap.@config[0].recon_db_path"));
  357. $this->response = array("success" => true, "scanLocation" => "{$scanLocation}/");
  358. }
  359. private function setScanLocation()
  360. {
  361. $scanLocation = $this->request->scanLocation;
  362. if (!empty($scanLocation)) {
  363. $dbLocation = dirname("{$scanLocation}/fake_file");
  364. $this->uciSet("pineap.@config[0].recon_db_path", "{$dbLocation}/recon.db");
  365. if ($this->checkPineAPDaemon()) {
  366. exec("/etc/init.d/pineapd stop");
  367. $this->startPineAPDaemon();
  368. }
  369. $this->response = array("success" => true);
  370. } else {
  371. $this->error = "You cannot specify an empty path.";
  372. }
  373. }
  374. private function removeScan()
  375. {
  376. $this->dbConnection->exec("DELETE FROM clients WHERE scan_id='%s';", $this->request->scanID);
  377. $this->dbConnection->exec("DELETE FROM aps WHERE scan_id='%s';", $this->request->scanID);
  378. $this->dbConnection->exec("DELETE FROM scan_ids WHERE scan_id='%s';", $this->request->scanID);
  379. $this->response = array("success" => true);
  380. }
  381. private function getWSAuthToken()
  382. {
  383. @$wsAuthToken = file_get_contents('/tmp/reconpp.token');
  384. if ($wsAuthToken === false) {
  385. $this->response = array("success" => false);
  386. } else {
  387. $this->response = array("success" => true, "wsAuthToken" => $wsAuthToken);
  388. }
  389. }
  390. }