#!/usr/bin/php
<?php
namespace Sysadmin;

// 
// Secure Sysadmin Hook Service
// Copyright 2018 Sangoma Technologies
//

//////////////////////////////////////////////////////////////////
// This file is DELIBERATELY left un-encoded to assist security //
// researchers, and to allow people to add their own keys if    //
// required. If you have questions or comments, please email    //
// them to security@freepbx.org or on #freepbx on Freenode.     //
//////////////////////////////////////////////////////////////////

//
// This is NOT FREE SOFTWARE. You are NOT PERMITTED to modify
// or redistribute this code. All rights are reserved.
//

openlog("sysadmin-hook", LOG_PID | LOG_PERROR, LOG_CRON);

// Verify that our 'includes' file is correct
if (!file_exists("/usr/lib/sysadmin/includes.php")) {
	$err = "Unable to open /usr/lib/sysadmin/includes.php";
	syslog(LOG_ERR, $err);
	print "$err\n";
	exit;
}

// This hash is automatically updated when the RPM is built. If
// this is not matching, reinstall the RPM.
$includeshash = "8cbcf29be29ffa1e87ad66bdf254880fa5c197a28ccff8da9b222c1d3de4c7c3";

$hashalgo = "sha256";

if (hash_file($hashalgo, "/usr/lib/sysadmin/includes.php") !== $includeshash) {
	$err = "File /usr/lib/sysadmin/includes.php has been tampered. Reinstall sysadmin RPM.";
	syslog(LOG_ERR, $err);
	print "$err\n";
	exit;
}

require '/usr/lib/sysadmin/includes.php';

// These are the keys that are allowed to run root hooks.
$whitelist = array(
	"9F9169F4B33B4659" => "FreePBX Master Key",
	"86CE877469D2EAD9" => "Signing Server 1 (2014-2020)",
	"3DDB2122FE6D84F7" => "Signing Server Backup (2014-2020)",
	"1588A7366BD35B34" => "FreePBX Master Key v2",
	"B53D215A755231A3" => "Mirror Server",
	"96878C7428F8D097" => "Franck Danard <fdanard@sangoma.com>",
	"5803D408E0FB9844" => "Matt Brooks <mbrooks@digium.com> (Developer, Sangoma)",
	"8ED515BA06CD38AD" => "FreePBX QA Module Signing <qa@sangoma.net>",
);

if (!isset($argv[1])) {
	syslog(LOG_ERR, "Need a hook param, not supplied.");
	exit;
}

if ($argv[1] == "--local") {
	$request = $argv[2];
	$filename = "/usr/local/asterisk/incron/$request";
} else {
	$request = $argv[1];
	$filename = "/var/spool/asterisk/incron/$request";
}

syslog(LOG_DEBUG, "sysadmin hook started - ".json_encode($argv));

// Delete the file.
if (!file_exists($filename)) {
	syslog(LOG_WARNING, "File '$filename' didn't exist. That's unpossible (Are you running incron commands manually?)");
	$fh = fopen("/dev/null", "r");
} else {
	// Open a file handle against this file before deleting it, in case we need
	// the contents later.
	$fh = fopen($filename, "r");
	unlink($filename);
}

// Validate it matches the format of modulename_hookname, or modulename.hookname.params
if (!preg_match('/^(\w+)_([\w-]+)$/', $request, $parts)) {
        // No?  How about modulename.hookname.params?
        if (!preg_match('/^([\w_]+)\.([\w-]+)(?:\.(.+))?$/', $request, $parts)) {
		syslog(LOG_ERR, "Invalid hook format");
		exit;
        }
}

$module = $parts[1];
$hook = $parts[2];
// Were there params?
if (isset($parts[3])) {
	// Is this the magic string 'CONTENTS'? If so, load the contents
	// from our file handle (up to 4k)
	if ($parts[3] === "CONTENTS") {
		$params = fread($fh, 4096);
	} else {
		$params = $parts[3];
	}
} else {
	$params = "";
}
fclose($fh);

// Is this a core system hook? These aren't stored with modules,
// and are used in things like HA, for emergency repairs.
// These are triggered as SYSTEM.hookname. Must be caps.
if ($module == "SYSTEM") {
	$sigfile = "/usr/local/asterisk/$hook.sig";
	$hookfile = "/usr/local/asterisk/$hook";
} else {
	// We have to work under the assumption that MODDIR is
	// /var/www/html/admin/modules/ - we can't ask FreePBX,
	// as it may be compromised.
	$sigfile = "/var/www/html/admin/modules/$module/module.sig";
	$hookfile = "/var/www/html/admin/modules/$module/hooks/$hook";
}

if (!file_exists($sigfile)) {
	syslog(LOG_ERR, "Can't find signature file '$sigfile'");
	exit;
}

if (!file_exists($hookfile)) {
	syslog(LOG_ERR, "Invalid hook when looking for '$hookfile'");
	exit;
}

// Included in /usr/lib/sysadmin/includes.php
$g = new \Sysadmin\GPG();

$verify = $g->checkSig($sigfile);
if (!isset($verify['hashes'])) {
	syslog(LOG_ERR, "Module tampered. No hashes from $module/module.sig. Can't proceed");
	exit;
}

// Is our module signed by one of the whitelisted keys?
if (!isset($verify['config']['signedwith'])) {
	syslog(LOG_ERR, "Strange result from GPG checkSig");
	exit;
}

$signedwith = $verify['config']['signedwith'];
if (!isset($whitelist[$signedwith])) {
	syslog(LOG_ERR, "Module signed by key not in whitelist.");
	exit;
}

// Awesome. We now have a valid module. Let's make sure that the file we're
// being asked to run is actually IN the module.sig file. If it's a SYSTEM
// file, it will have the complete path in the file. If it's a non-system
// file, it will have a relative path.
if ($module == "SYSTEM") {
	$signame = $hookfile;
} else {
	$signame = "hooks/$hook";
}

// Does it exist?
if (!isset($verify['hashes'][$signame])) {
	syslog(LOG_ERR, "Hook $signame not in signature file $sigfile");
	exit;
}

// Finally, has that file been tampered?
if (hash_file('sha256', $hookfile) !== $verify['hashes'][$signame]) {
	syslog(LOG_ERR, "Hash mismatch of $hookfile. Can't run");
	exit;
}

// Lucky last. Did someone derp, and release a broken module that
// haxx0rz are using? Let's make sure that they can't recycle that
// signature file as a system one.
if ($module == "SYSTEM") {
	if ($verify['config']['type'] != "system") {
		syslog(LOG_ERR, "Tried to load a system hook, but signature isn't a system signature?");
		exit;
	}
}

// WOOT! Before we actually do anything, ensure there's nothing nasty on the params.

// Warning: This explicitly breaks utf8. If you need unicode, base64 it.
if (preg_match('/[^\x20-\x7e]/', $params, $out)) {
	syslog(LOG_ERR, "Out of spec char in params $params, ".json_encode($out));
        exit;
}

// Extra double sanity check.
if (preg_match('/[`\'"$><&;]/', $params)) {
	syslog(LOG_ERR, "Detected invalid char in params. You must use base64 to pass unusual chars to a hook.");
        exit;
}

if (!is_executable($hookfile)) {
	syslog(LOG_ERR, "'$hookfile' is not executable. Can not run.");
	exit;
}

// pcntl_exec passes argv directly to the signed hook (advisory remediation).
if (!function_exists('pcntl_exec')) {
	syslog(LOG_ERR, "pcntl_exec unavailable; cannot safely run hooks without shell");
	exit(1);
}

$args = array();
if ($params !== '') {
	$args[] = $params;
}

syslog(LOG_DEBUG, "Security check passed. Running '$hookfile' via pcntl_exec");
pcntl_exec($hookfile, $args);
syslog(LOG_ERR, "pcntl_exec failed for '$hookfile'");
exit(1);


