2018-07-22 10:45:22 +10:00

763 lines
29 KiB
PHP

<?php namespace pineapple;
class EvilPortal extends Module
{
// CONSTANTS
private $CLIENTS_FILE = '/tmp/EVILPORTAL_CLIENTS.txt';
private $ALLOWED_FILE = '/pineapple/modules/EvilPortal/data/allowed.txt';
private $STORAGE_LOCATIONS = array("sd" => "/sd/portals/", "internal" => "/root/portals/");
private $BASE_EP_COMMAND = 'module EvilPortal';
// CONSTANTS
/**
* An implementation of the route method from Module.
* This method routes the request to the method that handles it.
*/
public function route()
{
$this->createPortalFolders(); // if the portal folders (/root/portals | /sd/portals) do not exist create them.
switch ($this->request->action) {
case 'status':
$this->response = array(
"running" => $this->checkEvilPortalRunning(),
"startOnBoot" => $this->checkAutoStart(),
"sdAvailable" => $this->isSDAvailable()
);
break;
case 'writeFileContent':
$this->response = $this->writeFileContents($this->request->filePath, $this->request->content, $this->request->append);
break;
case 'deletePortal':
case 'deleteDirectory':
case 'deleteFile':
$this->response = $this->deleteFileOrDirectory($this->request->filePath);
break;
case 'getDirectoryContent':
case 'getFileContent':
$this->response = $this->getFileOrDirectoryContents($this->request->filePath);
break;
case 'download':
$this->response = $this->download($this->request->filePath);
break;
case 'listAvailablePortals':
$this->response = $this->getListOfPortals();
break;
case 'createNewPortal':
$this->response = $this->createNewPortal($this->request->name, $this->request->type, $this->request->storage);
break;
case 'movePortal':
$this->response = $this->movePortal($this->request->name, $this->request->storage);
break;
case 'activatePortal':
$this->response = $this->activatePortal($this->request->name, $this->request->storage);
break;
case 'deactivatePortal':
$this->response = $this->deactivatePortal($this->request->name, $this->request->storage);
break;
case 'getRules':
$this->response = $this->getPortalRules($this->request->name, $this->request->storage);
break;
case 'saveRules':
$this->response = $this->savePortalRules($this->request->name, $this->request->storage, $this->request->rules);
break;
case 'toggleOnBoot':
$this->response = $this->autoStartEvilPortal();
break;
case 'toggleCaptivePortal':
$this->response = $this->toggleCaptivePortal();
break;
case 'removeClientFromList':
$this->response = $this->removeFromList($this->request->clientIP, $this->request->listName);
break;
case 'authorizeClient':
$this->authorizeClient($this->request->clientIP);
$this->response = array("success" => true);
break;
}
}
/**
* Create the folders that portals are stored in if they don't exist
*/
private function createPortalFolders()
{
if (!is_dir($this->STORAGE_LOCATIONS["internal"])) {
mkdir($this->STORAGE_LOCATIONS["internal"]);
}
if (!is_dir($this->STORAGE_LOCATIONS["sd"]) and $this->isSDAvailable()) {
mkdir($this->STORAGE_LOCATIONS["sd"]);
}
}
/**
* Decide if a file for a given portal is "deletable" or not.
* If it is not then the UI should not display a delete option for the file.
* @param $file: The name of the file
* @return bool: Is the file deletable or not
*/
private function isFileDeletable($file)
{
if (substr($file, -strlen(".ep")) == ".ep")
return false;
return !in_array($file, array("MyPortal.php", "default.php", "helper.php", "index.php"));
}
/**
* Get the contents of a specified file or directory
*
* If this method is being called as the result of an HTTP request, make sure that "file" is specified as a
* parameter of the request and includes the full path to the file that should have its contents returned.
*
* @param $file : The file or directory to get contents of
* @return array
*/
private function getFileOrDirectoryContents($file)
{
if (!file_exists($file)) {
$message = "No such file or directory {$file}.";
$contents = null;
$success = false;
} else if (is_file($file)) {
$message = "Found file {$file} and retrieved contents";
$contents = array(
"name" => basename($file),
"path" => $file,
"size" => $this->readableFileSize($file),
"fileContent" => file_get_contents($file)
);
$success = true;
} else if (is_dir($file)) {
$contents = array();
$message = "Returning directory contents for {$file}";
foreach (preg_grep('/^([^.])/', scandir($file)) as $object) {
// skip .ep files because they shouldn't be edited directly.
if (substr($object, -strlen(".ep")) == ".ep")
continue;
$obj = array("name" => $object, "directory" => is_dir("{$file}/{$object}"),
"path" => realpath("{$file}/{$object}"),
"permissions" => substr(sprintf('%o', fileperms("{$file}/{$object}")), -4),
"size" => $this->readableFileSize("{$file}/{$object}"),
"deletable" => $this->isFileDeletable($object));
array_push($contents, $obj);
}
$success = true;
} else {
$contents = null;
$success = false;
$message = "Unknown case. This should never happen.";
}
return array("success" => $success, "message" => $message, "content" => $contents);
}
/**
* Write given content to a given file.
* @param $file : The file to write content to
* @param $content : The content to write to the file
* @param $append : Should the data be appended to the end of the file (true) or over-write the file (false)
* @return array
*/
private function writeFileContents($file, $content, $append)
{
if ($append)
file_put_contents($file, $content, FILE_APPEND);
else
file_put_contents($file, $content);
return array("success" => true, "message" => null);
}
/**
* Delete a given file or directory and check if it has been deleted.
* If the file was deleted the success will be true otherwise success is false
* @param $filePath
* @return array
*/
private function deleteFileOrDirectory($filePath)
{
if ($this->isFileDeletable(basename($filePath))) {
exec(escapeshellcmd("rm -rf {$filePath}"));
$success = (!file_exists($filePath));
$message = (file_exists($filePath)) ? "Error deleting file {$filePath}." : "{$filePath} has been deleted.";
} else {
$success = false;
$message = "{$filePath} can not be deleted!";
}
return array("success" => $success, "message" => $message);
}
/**
* Download a file
* @param: The path to the file to download
* @return array : array
*/
private function download($filePath)
{
if (file_exists($filePath)) {
return array("success" => true, "message" => null, "download" => $this->downloadFile($filePath));
} else {
return array("success" => false, "message" => "File does not exist", "download" => null);
}
}
/**
* Get a list of portals found on internal and sd storage.
*/
private function getListOfPortals()
{
// an array of all of the portals found
$portals = array();
$availableMediums = array("internal");
// if the sd card is available add it to the availableMediums
if ($this->isSDAvailable()) {
array_push($availableMediums, "sd");
}
foreach($availableMediums as $medium) {
$storageLocation = $this->STORAGE_LOCATIONS[$medium];
foreach (preg_grep('/^([^.])/', scandir($storageLocation)) as $object) {
if (!is_dir($storageLocation . $object)) // skip the object if it is not a directory.
continue;
$portal = array(
"title" => $object,
"portalType" => $this->getValueFromJSONFile(array("type"), "{$storageLocation}{$object}/{$object}.ep")["type"],
"size" => $this->readableFileSize("{$storageLocation}{$object}"),
"location" => "{$storageLocation}{$object}",
"storage" => $medium,
"active" => (file_exists("/www/{$object}.ep"))
);
// push the portal object to the array of portals found
array_push($portals, $portal);
}
}
return array("success" => true, "portals" => $portals);
}
/**
* Create a new Portal with a given name of a given type on a given storage medium.
* @param $name : The name of the new portal
* @param $type : The type of portal to create (targeted or basic)
* @param $storage : The storage medium to save the portal to (sd or internal)
* @return array
*/
private function createNewPortal($name, $type, $storage)
{
// force the name of the portal to be lower cased and replace spaces with underscores
$name = strtolower(str_replace(' ', '_', $name));
// $storage should be equal to "sd" or "internal". If its anything else just make it "internal"
$storage = ($storage == "sd" or $storage == "internal") ? $storage : "internal";
// the path to store the portal
$portalPath = $this->STORAGE_LOCATIONS[$storage];
// verify that no portal with the same name already exists
if (file_exists("{$this->STORAGE_LOCATIONS["internal"]}{$name}") or
(file_exists("{$this->STORAGE_LOCATIONS["sd"]}{$name}") and $this->isSDAvailable())) {
return array("success" => false, "message" => "A portal named {$name} already exists!");
}
// if the portal is supposed to be stored on the SD card, make sure that it is indeed available first.
if ($storage == "sd" and !$this->isSDAvailable()) {
return array("success" => false, "message" => "There is no SD card available!");
}
// create the directory for the portal
mkdir($portalPath . $name);
// handle the portal types. If anything other than "targeted" is specified then it will create a basic portal
switch ($type) {
case 'targeted':
exec("cp /pineapple/modules/EvilPortal/includes/targeted_skeleton/* {$portalPath}{$name}/");
exec("cp /pineapple/modules/EvilPortal/includes/targeted_skeleton/.* {$portalPath}{$name}/");
exec("mv {$portalPath}{$name}/portalinfo.json {$portalPath}{$name}/{$name}.ep");
$this->updateJSONFile(array("name" => $name, "type" => "targeted"), "{$portalPath}{$name}/{$name}.ep");
exec("sed -i 's/\"portal_name_here\"/\"{$name}\"/g' {$portalPath}{$name}/index.php");
break;
default:
exec("cp /pineapple/modules/EvilPortal/includes/skeleton/* {$portalPath}{$name}/");
exec("cp /pineapple/modules/EvilPortal/includes/skeleton/.* {$portalPath}{$name}/");
exec("mv {$portalPath}{$name}/portalinfo.json {$portalPath}{$name}/{$name}.ep");
$this->updateJSONFile(array("name" => $name, "type" => "basic"), "{$portalPath}{$name}/{$name}.ep");
break;
}
// make these scripts executable
exec("chmod +x {$portalPath}{$name}/.enable");
exec("chmod +x {$portalPath}{$name}/.disable");
return array("success" => true, "message" => "Created {$type} portal {$name}!");
}
/**
* Move a portal between one storage medium to another.
*
* If the current medium is "internal" then the portal will be moved to "sd" and visa-versa
*
* @param $name : The name of the portal to move
* @param $storage : The current storage medium
* @return array
*/
private function movePortal($name, $storage)
{
$storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal";
$newMedium = ($storage == "internal") ? "sd" : "internal";
$newStorage = $this->STORAGE_LOCATIONS[$newMedium];
// active portals should not be moved so check if the portal is currently active
if (file_exists("/www/{$name}.ep")) {
return array("success" => false, "message" => "You can not move an active portal!");
} else
// make sure that an SD card is inserted if it is going to be needed
if (($storage == "sd" || $newMedium == "sd") && !$this->isSDAvailable()) {
return array("success" => false, "message" => "Please insert a SD card to preform this action.");
}
// if the portal doesn't exist then return an error
if (!file_exists($this->STORAGE_LOCATIONS[$storage] . $name)) {
return array("success" => false, "message" => "Could not find portal named {$name} on {$storage} storage");
}
// verify that a portal with the same name doesn't already exist in the new location.
if (file_exists($newStorage . $name)) {
return array("success" => false, "message" => "A portal named {$name} already exists on {$newMedium} storage");
}
// all of the above conditions should have passed so lets move the damn portal.
exec(escapeshellcmd("mv {$this->STORAGE_LOCATIONS[$storage]}{$name} {$newStorage}{$name}"));
// verify that the directory was moved
if (file_exists($newStorage . $name)) {
return array("success" => true, "message" => "{$name} was moved to {$newMedium} storage!");
} else {
return array("success" => false, "message" => "An error occurred moving {$name} to {$newMedium} storage");
}
}
/**
* Set a given portal to "active".
* This means to move the portals contents to /www so it can be access via HTTP port 80.
*
* If any file with the same name as one of the files being copied to /www already exists in /www
* then that file will be renamed to {file_name}.ep_backup and restored when the portal is deactivated.
*
* If any portals are currently active when this method is called they will be deactivated.
*
* @param $name: The name of the portal to activate
* @param $storage: The storage medium the portal is on
* @return array
*/
private function activatePortal($name, $storage)
{
$dir = $this->STORAGE_LOCATIONS[$storage];
// check if there is a currently activate portal and deactivate it.
foreach(scandir("/www") as $file) {
if (substr($file, strlen($file) - strlen(".ep")) === ".ep") { // deactivate a portal if needed
$portalName = rtrim($file, ".ep");
$realPath = realpath("/www/{$file}");
$storage = ($realPath == "{$this->STORAGE_LOCATIONS['internal']}{$portalName}/{$portalName}.ep") ? "internal": "sd";
$this->deactivatePortal($portalName, $storage);
break;
}
}
$success = false;
$portalPath = escapeshellarg($dir . $name);
if (file_exists($dir . $name)) {
exec("ln -s /pineapple/modules/EvilPortal/includes/api /www/captiveportal");
$portal_files = scandir($dir . $name);
foreach ($portal_files as $file) {
if (file_exists("/www/{$file}")) {
rename("/www/{$file}", "/www/{$file}.ep_backup");
}
exec("ln -s {$portalPath}/{$file} /www/{$file}");
$success = true;
}
// holding off on toggle commands until a future release.
// exec("echo {$portalPath}/.enable | at now");
$message = "{$name} is now active.";
} else {
$message = "Couldn't find {$portalPath}.";
}
return array("message" => $message, "success" => $success);
}
/**
* Deactivate a given portal.
*
* To do this we remove all files associated with the given portal from /www
* This method also renames any files with the extension ".ep_backup" to their original name
*
* @param $name: The name of the portal to deactivate
* @param $storage: The storage medium the portal is on
* @return array
*/
private function deactivatePortal($name, $storage)
{
$storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal";
$dir = $this->STORAGE_LOCATIONS[$storage];
// if the portal is not active then return an error
if (!(file_exists("/www/{$name}.ep"))) {
return array("success" => false, "message" => "{$name} is not currently active.");
}
// if the portal does not exist then return an error
if (!file_exists($dir . $name)) {
return array("success" => false, "message" => "Unable to find the portal {$name}.");
}
// remove portal files from /www
foreach(scandir($dir. $name) as $file) {
unlink("/www/{$file}");
}
// rename any files that may have been renamed back to their original name
foreach(scandir("/www/") as $file) {
if (substr($file, strlen($file) - strlen(".ep_backup")) === ".ep_backup") {
$oldName = str_replace(".ep_backup", "", $file);
rename("/www/{$file}", "www/{$oldName}");
}
}
// holding off on toggle commands until a future release.
// exec("echo {$dir}{$name}/.disable | at now");
return array("success" => true, "message" => "Deactivated {$name}.");
}
/**
* Attempt to get rules for a targeted portal
* @param $name: The name of the portal
* @param $storage: The storage medium of the portal
* @return array
*/
private function getPortalRules($name, $storage)
{
$storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal";
$path = $this->STORAGE_LOCATIONS[$storage];
if (is_file("{$path}{$name}/{$name}.ep")) {
$rules = $this->getValueFromJSONFile(array("targeted_rules"), "{$path}{$name}/{$name}.ep")["targeted_rules"];
return array(
"message" => "Found portal rules",
"data" => $rules,
"success" => true
);
} else {
return array("message" => "Unable to find portal.", "success" => false);
}
}
/**
* Save rules to a targeted portal
* @param $name : The name of the portal
* @param $storage : The storage medium of the portal
* @param $rules : The rules to save
* @return array
*/
private function savePortalRules($name, $storage, $rules)
{
$storage = ($storage == "internal" || $storage == "sd") ? $storage : "internal";
$path = $this->STORAGE_LOCATIONS[$storage];
if (is_file("{$path}{$name}/{$name}.ep")) {
$this->updateJSONFile(array("targeted_rules" => json_decode($rules)), "{$path}{$name}/{$name}.ep")["targeted_rules"];
return array(
"message" => "Saved portal rules",
"success" => true
);
} else {
return array("message" => "Unable to find portal {$name}.", "success" => false);
}
}
/**
* Check if Evil Portal is currently running or not be checking iptables.
* @return bool
*/
private function checkEvilPortalRunning()
{
return exec("iptables -t nat -L PREROUTING | grep 172.16.42.1") != '';
}
/**
* Check if EvilPortal is running when the Pineapple starts or not
* @return bool
*/
public function checkAutoStart()
{
return !(exec("ls /etc/rc.d/ | grep evilportal") == '');
}
/**
* Grant a client access to the internet and stop blocking them with the captive portal
* @param $client: The IP address of the client to authorize
*/
private function authorizeClient($client)
{
exec("iptables -t nat -I PREROUTING -s {$client} -j ACCEPT");
// exec("{$this->BASE_EP_COMMAND} add {$client}");
$this->writeFileContents($this->CLIENTS_FILE, "{$client}", true);
}
/**
* Revoke a clients access to the internet and start blocking them with the captive portal
* @param $client: The IP address of the client to revoke
*/
private function revokeClient($client)
{
// exec("{$this->BASE_EP_COMMAND} remove {$client}");
exec("iptables -t nat -D PREROUTING -s {$client}");
exec("iptables -t nat -D PREROUTING -s {$client} -j ACCEPT");
}
/**
* Start the captive portal portion of Evil Portal
*
* All clients in the White List should be automatically authorized when Evil Portal starts.
*
* @return array
*/
private function startEvilPortal()
{
// Delete client tracking file if it exists
if (file_exists($this->CLIENTS_FILE)) {
unlink($this->CLIENTS_FILE);
}
// Enable forwarding. It should already be enabled on the pineapple but do it anyways just to be safe
exec("echo 1 > /proc/sys/net/ipv4/ip_forward");
exec("ln -s /pineapple/modules/EvilPortal/includes/api /www/captiveportal");
// Insert allowed clients into tracking file
$allowedClients = file_get_contents($this->ALLOWED_FILE);
file_put_contents($this->CLIENTS_FILE, $allowedClients);
// Configure other rules
exec("iptables -A INPUT -s 172.16.42.0/24 -j DROP");
exec("iptables -A OUTPUT -s 172.16.42.0/24 -j DROP");
exec("iptables -A INPUT -s 172.16.42.0/24 -p udp --dport 53 -j ACCEPT");
// Allow the pineapple
exec("iptables -A INPUT -s 172.16.42.1 -j ACCEPT");
exec("iptables -A OUTPUT -s 172.16.42.1 -j ACCEPT");
//exec("iptables -A INPUT -i br-lan -p tcp --dport 443 -j DROP");
//exec("iptables -t nat -A PREROUTING -i br-lan -j DROP");
exec("iptables -t nat -A PREROUTING -i br-lan -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80");
exec("iptables -t nat -A POSTROUTING -j MASQUERADE");
// exec("{$this->BASE_EP_COMMAND} init");
// Add rule for each allowed client
$lines = file($this->CLIENTS_FILE);
foreach ($lines as $client) {
$this->authorizeClient($client);
}
$success = $this->checkEvilPortalRunning();
$message = ($success) ? "EvilPortal is now up and running!" : "EvilPortal failed to start.";
return array("success" => $success, "message" => $message);
}
/**
* Stop the captive portal portion of Evil Portal from running
* @return mixed
*/
private function stopEvilPortal()
{
if (file_exists($this->CLIENTS_FILE)) {
$lines = file($this->CLIENTS_FILE);
foreach ($lines as $client) {
$this->revokeClient($client);
}
unlink($this->CLIENTS_FILE);
}
exec("iptables -t nat -D PREROUTING -i br-lan -p tcp --dport 80 -j DNAT --to-destination 172.16.42.1:80");
exec("iptables -D INPUT -p tcp --dport 53 -j ACCEPT");
exec("iptables -D INPUT -j DROP");
// exec("{$this->BASE_EP_COMMAND} purge");
$success = !$this->checkEvilPortalRunning();
$message = ($success) ? "EvilPortal has stopped running" : "There was an issue stopping EvilPortal";
return array("success" => $success, "messsage" => $message);
}
/**
* If Evil Portal is running then stop it, otherwise start it.
*/
private function toggleCaptivePortal() {
// Make the file executable. In the future the `module` command should do this for us.
chmod("/pineapple/modules/EvilPortal/executable/executable", 0755);
return $this->checkEvilPortalRunning() ? $this->stopEvilPortal() : $this->startEvilPortal();
}
/**
* Enable or Disable Evil Portal to run when the pineapple boots.
* If Evil Portal is supposed to run when this method is called then it will be disabled
* If Evil Portal is not supposed to run when this method is called then it will be enabled on boot.
*
* This method does not start nor stop current running instances of Evil Portal.
*
* @return array
*/
private function autoStartEvilPortal()
{
// if EvilPortal is not set to start on startup then set that shit
if (!$this->checkAutoStart()) {
copy("/pineapple/modules/EvilPortal/includes/evilportal.sh", "/etc/init.d/evilportal");
chmod("/etc/init.d/evilportal", 0755);
exec("/etc/init.d/evilportal enable");
$enabled = $this->checkAutoStart();
$message = ($enabled) ? "EvilPortal is now enabled on start up" : "Error enabling EvilPotal on startup.";
return array(
"success" => $enabled,
"message" => $message
);
} else { // if evil portal is set to run on startup then disable that shit.
exec("/etc/init.d/evilportal disable");
$enabled = !$this->checkAutoStart();
$message = ($enabled) ? "EvilPortal is now disabled on startup." : "Error disabling EvilPortal on startup.";
return array(
"success" => $enabled,
"message" => $message
);
}
}
/**
* Removes a client from either the whiteList or authorizedList
* @param $clientIP: The IP address of the client to be removed
* @param $listName: The name of the list to remove the client from
* @return array
*/
private function removeFromList($clientIP, $listName)
{
$valid = preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/', $clientIP);
// if the IP address is invalid then return an error message
if (!$valid) {
return array("success" => false, "message" => "Invalid IP Address.");
}
$success = true;
switch ($listName) {
case "whiteList":
$data = file_get_contents($this->ALLOWED_FILE);
$data = str_replace("{$clientIP}\n", '', $data);
file_put_contents("/root/removeFromList", $data);
file_put_contents($this->ALLOWED_FILE, $data);
break;
case "accessList":
$data = file_get_contents($this->CLIENTS_FILE);
$data = str_replace("{$clientIP}\n", '', $data);
file_put_contents($this->CLIENTS_FILE, $data);
$this->revokeClient($clientIP);
break;
default:
$success = false;
break;
}
$message = ($success) ? "Successfully removed {$clientIP} from {$listName}" : "Error removing {$clientIP} from {$listName}";
return array("success" => $success, "message" => $message);
}
/**
* Add a value to a json file
* @param $keyValueArray: The data to add to the file
* @param $file: The file to write the content to.
*/
private function updateJSONFile($keyValueArray, $file) {
$data = json_decode(file_get_contents($file), true);
foreach ($data as $key => $value) {
if (isset($keyValueArray[$key])) {
$data[$key] = $keyValueArray[$key];
}
}
file_put_contents($file, json_encode($data));
}
/**
* Get values from a JSON file
* @param $keys: The key or keys you wish to get the value from
* @param $file: The file to that contains the JSON data.
* @return array
*/
private function getValueFromJSONFile($keys, $file) {
$data = json_decode(file_get_contents($file), true);
$values = array();
foreach ($data as $key => $value) {
if (in_array($key, $keys)) {
$values[$key] = $value;
}
}
return $values;
}
/**
* Get the size of a file and add a unit to the end of it.
* @param $file: The file to get size of
* @return string: File size plus unit. Exp: 3.14M
*/
private function readableFileSize($file) {
$size = filesize($file);
if ($size == null)
return "0 Bytes";
if ($size < 1024) {
return "{$size} Bytes";
} else if ($size >= 1024 && $size < 1024*1024) {
return round($size / 1024, 2) . "K";
} else if ($size >= 1024*1024) {
return round($size / (1024*1024), 2) . "M";
}
return "{$size} Bytes";
}
}