fixing entirely the cron execution shell. This may fix a potential privilege escalation problem.

This commit is contained in:
Benjamin Sonntag 2014-11-25 14:36:20 +01:00
parent 0301409dbf
commit c2d8b317d0
2 changed files with 185 additions and 21 deletions

View File

@ -28,6 +28,8 @@
*/
class m_cron {
const MAX_SOCKETS=8;
const DEFAULT_CAFILE="/etc/ssl/certs/ca-certificates.crt";
/*---------------------------------------------------------------------------*/
/** Constructor
@ -188,5 +190,169 @@ class m_cron {
return $q;
}
/*---------------------------------------------------------------------------*/
/**
* Execute the required crontab of AlternC users
* this function EXIT at the end.
*/
function execute_cron() {
global $db;
$db->query("SELECT id, url, email, schedule, user, password FROM cron WHERE next_execution <= NOW();");
$urllist=array();
while ($db->next_record()) {
$db->Record["url"]=urldecode($db->Record["url"]);
// we support only http or https schemes:
if (substr($db->Record["url"],0,7)=="http://" || substr($db->Record["url"],0,8)=="https://") {
$u=array(
"url" => $db->Record["url"],
"id" => $db->Record["id"], "email" =>$db->Record["email"],
);
if ($db->Record["user"] && $db->Record["password"]) {
$u["login"]=$db->Record["user"];
$u["password"]=$db->Record["password"];
}
$urllist[]=$u;
}
if (empty($urllist)) { // nothing to do :
exit(0);
}
// cron_callback($url, $content, $curlobj) will be called at the end of each http call.
$this->rolling_curl($urllist, array("m_cron","cron_callback"));
}
}
/*---------------------------------------------------------------------------*/
/**
* Callback function called by rolling_curl when a cron resulr has been received
* schedule it for next run and send the mail if needed
*/
function cron_callback($url,$content,$curl) {
global $db,$L_FQDN;
if (empty($url["id"])) return; // not normal...
$id=intval($url["id"]);
if ($curl["http_code"]==200) {
$ok=true;
} else {
$ok=false;
}
if (isset($url["email"]) && $url["email"] && $content) {
mail($url["email"],"AlternC Cron #$id - Report ".date("%r"),"Please find below the stdout content produced by your cron task.\n------------------------------------------------------------\n\n".$content,"From: postmaster@$L_FQDN");
}
// now schedule it for next run:
$db->query("UPDATE cron SET next_execution=FROM_UNIXTIME( UNIX_TIMESTAMP(NOW()) + schedule * 60) WHERE id=$id");
}
/*---------------------------------------------------------------------------*/
/**
* Launch parallel (using MAX_SOCKETS sockets maximum) retrieval
* of URL using CURL
* @param $urls array of associative array, each having the following keys :
* url = url to get (of the form http[s]://login:password@host/path/file?querystring )
* login & password = if set, tell the login and password to use as simple HTTP AUTH.
* - any other key will be sent as it is to the callback function
* @param $callback function called for each request when completing. First argument is the $url object, second is the content (output)
* third is the info structure from curl for the returned page. 200 for OK, 403 for AUTH FAILED, 0 for timeout, dump it to know it ;)
* this function should return as soon as possible to allow other curl calls to complete properly.
* @param $cursom_options array of custom CURL options for all transfers
*/
function rolling_curl($urls, $callback, $custom_options = null) {
// make sure the rolling window isn't greater than the # of urls
if (!isset($GLOBALS["DEBUG"])) $GLOBALS["DEBUG"]=false;
$rolling_window = m_cron::MAX_SOCKETS;
$rolling_window = (count($urls) < $rolling_window) ? count($urls) : $rolling_window;
$master = curl_multi_init();
$curl_arr = array();
// add additional curl options here
$std_options = array(CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 240, // 4 minutes timeout for a page
CURLOPT_USERAGENT => "AlternC (Cron Daemon)",
CURLOPT_MAXREDIRS => 0);
if ($GLOBALS["DEBUG"]) $std_options[CURLOPT_VERBOSE]=true;
$options = ($custom_options) ? ($std_options + $custom_options) : $std_options;
// start the first batch of requests
for ($i = 0; $i < $rolling_window; $i++) {
$ch = curl_init();
$options[CURLOPT_URL] = $urls[$i]["url"];
if ($GLOBALS["DEBUG"]) echo "URL: ".$urls[$i]["url"]."\n";
curl_setopt_array($ch,$options);
// Handle custom cafile for some https url
if (strtolower(substr($options[CURLOPT_URL],0,5))=="https") {
curl_setopt($ch,CURLOPT_CAINFO,m_cron::DEFAULT_CAFILE);
if ($GLOBALS["DEBUG"]) echo "cainfo set to DEFAULT\n";
}
if (isset($urls[$i]["login"]) && isset($urls[$i]["password"])) { // set basic http authentication
curl_setopt($ch,CURLOPT_HTTPAUTH,CURLAUTH_BASIC);
curl_setopt($ch,CURLOPT_USERPWD,urlencode($urls[$i]["login"]).":".urlencode($urls[$i]["password"]));
if ($GLOBALS["DEBUG"]) echo "set basic auth\n";
}
curl_multi_add_handle($master, $ch);
}
do {
while(($execrun = curl_multi_exec($master, $running)) == CURLM_CALL_MULTI_PERFORM);
if($execrun != CURLM_OK)
break;
// a request was just completed -- find out which one
while($done = curl_multi_info_read($master)) {
$info = curl_getinfo($done['handle']);
// TODO : since ssl_verify_result is buggy, if we have [header_size] => 0 && [request_size] => 0 && [http_code] => 0, AND https, we can pretend the SSL certificate is buggy.
if ($GLOBALS["DEBUG"]) { echo "Info for ".$done['handle']." \n"; print_r($info); }
if ($info['http_code'] == 200) {
$output = curl_multi_getcontent($done['handle']);
} else {
// request failed. add error handling.
$output="";
}
// request terminated. process output using the callback function.
// Pass the url array to the callback, so we need to search it
foreach($urls as $url) {
if ($url["url"]==$info["url"]) {
call_user_func($callback,$url,$output,$info);
break;
}
}
// If there is more: start a new request
// (it's important to do this before removing the old one)
if ($i<count($urls)) {
$ch = curl_init();
$options[CURLOPT_URL] = $urls[$i++]; // increment i
curl_setopt_array($ch,$options);
if (strtolower(substr($options[CURLOPT_URL],0,5))=="https") {
curl_setopt($ch,CURLOPT_CAINFO,m_cron::DEFAULT_CAFILE);
if ($GLOBALS["DEBUG"]) echo "cainfo set to DEFAULT\n";
}
if (isset($urls[$i]["login"]) && isset($urls[$i]["password"])) { // set basic http authentication
curl_setopt($ch,CURLOPT_HTTPAUTH,CURLAUTH_BASIC);
curl_setopt($ch,CURLOPT_USERPWD,urlencode($urls[$i]["login"]).":".urlencode($urls[$i]["password"]));
if ($GLOBALS["DEBUG"]) echo "set basic auth\n";
}
curl_multi_add_handle($master, $ch);
}
// remove the curl handle that just completed
curl_multi_remove_handle($master, $done['handle']);
}
} while ($running);
curl_multi_close($master);
return true;
}
} /* Class cron */

View File

@ -1,26 +1,24 @@
#!/bin/bash
#!/usr/bin/php -q
<?php
# FIXME relecture + commentaires
/**
* Launch the users crontab for AlternC
* php, parallel-curl, secured mode.
**/
for CONFIG_FILE in \
/etc/alternc/local.sh \
/usr/lib/alternc/functions.sh
do
if [ ! -r "$CONFIG_FILE" ]; then
echo "Can't access $CONFIG_FILE."
exit 1
fi
. "$CONFIG_FILE"
done
require_once("/usr/share/alternc/panel/class/config_nochk.php");
ini_set("display_errors", 1);
stop_if_jobs_locked
max_process=2
tasks () {
$MYSQL_DO "select id, url, if(length(email)>0,email,'null'), schedule, UNIX_TIMESTAMP(), user, password as now from cron c where next_execution <= now();" | while read id url email schedule now user password ; do
echo $id $url \"$email\" $schedule $now \"$user\" \"$password\"
done
if (file_exists("/var/run/alternc/jobs-lock")) {
echo "jobs-lock exists, did you ran alternc.install?\n";
echo "canceling cron_users\n";
exit(1);
}
tasks | xargs -n 7 -P $max_process --no-run-if-empty /usr/lib/alternc/cron_users_doit.sh
if (isset($argv[1]) && $argv[1]=="debug") {
$GLOBALS["DEBUG"]=true;
}
$cron->execute_cron();