From 47fb98355778d0ddfd176a6d9ea7cc7f5ee50a59 Mon Sep 17 00:00:00 2001 From: Daniel Winzen Date: Sat, 25 Jan 2020 21:47:38 +0100 Subject: [PATCH] sanitize $system_account to protect against database modification exploits --- var/www/common.php | 49 +++++++++++++++++++- var/www/cron.php | 104 +++++++++++++++++++++++++++---------------- var/www/find_old.php | 24 +++++++--- 3 files changed, 131 insertions(+), 46 deletions(-) diff --git a/var/www/common.php b/var/www/common.php index f0655c4..2f30a00 100644 --- a/var/www/common.php +++ b/var/www/common.php @@ -298,6 +298,7 @@ function check_login(){ session_destroy(); exit; } + $user['system_account'] = basename($user['system_account']); return $user; } @@ -350,6 +351,11 @@ NumPrimaryGuards '.NUM_GUARDS.' $stmt=$db->prepare('SELECT onions.onion, users.system_account, onions.num_intros, onions.enable_smtp, onions.version, onions.max_streams, onions.enabled, onions.private_key FROM onions LEFT JOIN users ON (users.id=onions.user_id) WHERE onions.instance = ? AND onions.enabled IN (1, -2) AND users.id NOT IN (SELECT user_id FROM new_account) AND users.todelete!=1;'); $stmt->execute([$instance]); while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ + $system_account = sanitize_system_account($tmp['system_account']); + if($system_account === false){ + echo "ERROR: Account $tmp[system_account] looks strange\n"; + continue; + } if(!file_exists("/var/lib/tor-instances/$instance/hidden_service_$tmp[onion].onion")){ if($tmp['version']==2){ //php openssl implementation has some issues, re-export using native openssl @@ -516,6 +522,11 @@ function rewrite_nginx_config(){ // onions $stmt=$db->query("SELECT users.system_account, users.php, users.autoindex, onions.onion, users.id FROM users INNER JOIN onions ON (onions.user_id=users.id) WHERE onions.enabled IN (1, -2) AND users.id NOT IN (SELECT user_id FROM new_account) AND users.todelete!=1;"); while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ + $system_account = sanitize_system_account($tmp['system_account']); + if($system_account === false){ + echo "ERROR: Account $tmp[system_account] looks strange\n"; + continue; + } if($tmp['php']>0){ $php_location=" location ~ [^/]\.php(/|\$) { @@ -549,6 +560,11 @@ function rewrite_nginx_config(){ // clearnet domains $stmt=$db->query("SELECT users.system_account, users.php, users.autoindex, domains.domain, users.id FROM users INNER JOIN domains ON (domains.user_id=users.id) WHERE domains.enabled = 1 AND users.id NOT IN (SELECT user_id FROM new_account) AND users.todelete != 1;"); while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ + $system_account = sanitize_system_account($tmp['system_account']); + if($system_account === false){ + echo "ERROR: Account $tmp[system_account] looks strange\n"; + continue; + } if($tmp['php']>0){ $php_location=" location ~ [^/]\.php(/|\$) { @@ -585,6 +601,11 @@ function rewrite_nginx_config(){ $nginx_mail=''; $stmt=$db->query("SELECT system_account FROM users WHERE id NOT IN (SELECT user_id FROM new_account) AND todelete!=1;"); while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ + $system_account = sanitize_system_account($tmp['system_account']); + if($system_account === false){ + echo "ERROR: Account $tmp[system_account] looks strange\n"; + continue; + } $nginx_mysql.="server { listen unix:/home/$tmp[system_account]/var/run/mysqld/mysqld.sock; proxy_pass unix:/var/run/mysqld/mysqld.sock; @@ -623,6 +644,11 @@ pm = ondemand pm.max_children = 8 "; while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ + $system_account = sanitize_system_account($tmp['system_account']); + if($system_account === false){ + echo "ERROR: Account $tmp[system_account] looks strange\n"; + continue; + } $php.='['.$tmp['system_account']."] user = $tmp[system_account] group = www-data @@ -915,7 +941,12 @@ function bytes_to_human_readable(int $bytes) : string { } } -function setup_chroot($system_account){ +function setup_chroot(string $account){ + $system_account = sanitize_system_account($account); + if($system_account === false){ + echo "ERROR: Account $account looks strange\n"; + return; + } $shell = ENABLE_SHELL_ACCESS ? '/bin/bash' : '/usr/sbin/nologin'; $user = posix_getpwnam($system_account); $passwd_line = "$user[name]:$user[passwd]:$user[uid]:$user[gid]:$user[gecos]:/:$user[shell]"; @@ -945,7 +976,12 @@ function setup_chroot($system_account){ } } -function update_system_user_password($user, $password){ +function update_system_user_password(string $user, string $password){ + $system_account = sanitize_system_account($user); + if($system_account === false){ + echo "ERROR: Account $user looks strange\n"; + return; + } $fp = fopen("/etc/shadow", "r+"); $locked = false; do{ @@ -970,3 +1006,12 @@ function update_system_user_password($user, $password){ flock($fp, LOCK_UN); fclose($fp); } + +function sanitize_system_account(string $system_account){ + $account = basename($system_account); + $user = posix_getpwnam($account); + if($account !== $system_account || $user === false || $user['gid'] !== 33 || $user['uid'] < 1000){ + return false; + } + return $account; +} diff --git a/var/www/cron.php b/var/www/cron.php index 72eba5c..a432ce3 100644 --- a/var/www/cron.php +++ b/var/www/cron.php @@ -15,12 +15,21 @@ $del=$db->prepare("DELETE FROM new_account WHERE user_id=?;"); $approval = REQUIRE_APPROVAL ? 'WHERE new_account.approved=1': ''; $stmt=$db->query("SELECT users.system_account, new_account.password, users.id, users.instance FROM new_account INNER JOIN users ON (users.id=new_account.user_id) $approval LIMIT 100;"); while($account=$stmt->fetch(PDO::FETCH_ASSOC)){ + $system_account = basename($account['system_account']); + if($system_account !== $account['system_account']){ + echo "ERROR: Account $account[system_account] looks strange\n"; + continue; + } + if(posix_getpwnam($system_account) !== false){ + echo "ERROR: Account $account[system_account] already exists\n"; + continue; + } $reload[$account['instance']] = true; //add and manage rights of system user $shell = ENABLE_SHELL_ACCESS ? '/bin/bash' : '/usr/sbin/nologin'; - exec('useradd -l -g www-data -k /var/www/skel -m -s ' . escapeshellarg($shell) . ' ' . escapeshellarg($account['system_account'])); - update_system_user_password($account['system_account'], $account['password']); - setup_chroot($account['system_account']); + exec('useradd -l -g www-data -k /var/www/skel -m -s ' . escapeshellarg($shell) . ' ' . escapeshellarg($system_account)); + update_system_user_password($system_account, $account['password']); + setup_chroot($system_account); //remove from to-add queue $del->execute([$account['id']]); } @@ -28,32 +37,35 @@ while($account=$stmt->fetch(PDO::FETCH_ASSOC)){ //delete old accounts $del=$db->prepare("DELETE FROM users WHERE id=?;"); $stmt=$db->query("SELECT system_account, id, mysql_user, instance FROM users WHERE todelete=1 LIMIT 100;"); -$accounts=$stmt->fetchAll(PDO::FETCH_NUM); +$accounts=$stmt->fetchAll(PDO::FETCH_ASSOC); $mark_onions=$db->prepare('UPDATE onions SET enabled=-1 WHERE user_id=? AND enabled!=-2;'); foreach($accounts as $account){ - $instance=$account[3]; - $reload[$instance]=true; - $mark_onions->execute([$account[1]]); + $system_account = sanitize_system_account($account['system_account']); + if($system_account === false){ + echo "ERROR: Account $account[system_account] looks strange\n"; + continue; + } + $reload[$account['instance']]=true; + $mark_onions->execute([$account['id']]); } //delete hidden services from tor $del_onions=$db->prepare('DELETE FROM onions WHERE onion=?;'); $stmt=$db->query('SELECT onion, instance FROM onions WHERE enabled=-1;'); -$onions=$stmt->fetchAll(PDO::FETCH_NUM); +$onions=$stmt->fetchAll(PDO::FETCH_ASSOC); foreach($onions as $onion){ - $instance = $onion[1]; - $reload[$instance] = true; - if(file_exists("/var/lib/tor-instances/$instance/hidden_service_$onion[0].onion/")){ - if(file_exists("/var/lib/tor-instances/$instance/hidden_service_$onion[0].onion/authorized_clients/")){ - foreach(glob("/var/lib/tor-instances/$instance/hidden_service_$onion[0].onion/authorized_clients/*") as $file){ + $reload[$onion['instance']] = true; + if(is_dir("/var/lib/tor-instances/$onion[instance]/hidden_service_$onion[onion].onion/")){ + if(is_dir("/var/lib/tor-instances/$onion[instance]/hidden_service_$onion[onion].onion/authorized_clients/")){ + foreach(glob("/var/lib/tor-instances/$onion[instance]/hidden_service_$onion[onion].onion/authorized_clients/*") as $file){ unlink($file); } - rmdir("/var/lib/tor-instances/$instance/hidden_service_$onion[0].onion/authorized_clients"); + rmdir("/var/lib/tor-instances/$onion[instance]/hidden_service_$onion[onion].onion/authorized_clients"); } - foreach(glob("/var/lib/tor-instances/$instance/hidden_service_$onion[0].onion/*") as $file){ + foreach(glob("/var/lib/tor-instances/$onion[instance]/hidden_service_$onion[onion].onion/*") as $file){ unlink($file); } - rmdir("/var/lib/tor-instances/$instance/hidden_service_$onion[0].onion/"); + rmdir("/var/lib/tor-instances/$onion[instance]/hidden_service_$onion[onion].onion/"); } $del_onions->execute([$onion[0]]); } @@ -71,47 +83,61 @@ foreach($reload as $key => $val){ $stmt=$db->prepare('SELECT mysql_database FROM mysql_databases WHERE user_id=?;'); $drop_user=$db->prepare("DROP USER ?@'%';"); foreach($accounts as $account){ + $system_account = sanitize_system_account($account['system_account']); + if($system_account === false){ + echo "ERROR: Account $account[system_account] looks strange\n"; + continue; + } //kill processes of the user to allow deleting system users - exec('skill -u ' . escapeshellarg($account[0])); + exec('skill -u ' . escapeshellarg($system_account)); //delete user and group - exec('userdel -rf ' . escapeshellarg($account[0])); + exec('userdel -rf ' . escapeshellarg($system_account)); //delete all log files - if(file_exists("/var/log/nginx/access_$account[0].log")){ - unlink("/var/log/nginx/access_$account[0].log"); - } - if(file_exists("/var/log/nginx/access_$account[0].log.1")){ - unlink("/var/log/nginx/access_$account[0].log.1"); - } - if(file_exists("/var/log/nginx/error_$account[0].log")){ - unlink("/var/log/nginx/error_$account[0].log"); - } - if(file_exists("/var/log/nginx/error_$account[0].log.1")){ - unlink("/var/log/nginx/error_$account[0].log.1"); + $log_files = [ + "/var/log/nginx/access_".$system_account.".log", + "/var/log/nginx/access_".$system_account.".log.1", + "/var/log/nginx/error_".$system_account.".log", + "/var/log/nginx/error_".$system_account.".log.1" + ]; + foreach($log_files as $log_file){ + if(file_exists($log_file)){ + unlink($log_file); + } } //delete user from database - $drop_user->execute([$account[2]]); - $stmt->execute([$account[1]]); - while($tmp=$stmt->fetch(PDO::FETCH_NUM)){ - $db->exec('DROP DATABASE IF EXISTS `'.preg_replace('/[^a-z0-9]/i', '', $tmp[0]).'`;'); + $drop_user->execute([$account['mysql_user']]); + $stmt->execute([$account['id']]); + while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ + $db->exec('DROP DATABASE IF EXISTS `'.preg_replace('/[^a-z0-9]/i', '', $tmp['mysql_database']).'`;'); } $db->exec('FLUSH PRIVILEGES;'); //delete user from user database - $del->execute([$account[1]]); + $del->execute([$account['id']]); } // update passwords $stmt=$db->query("SELECT users.system_account, pass_change.password, users.id FROM pass_change INNER JOIN users ON (users.id=pass_change.user_id) LIMIT 100;"); $del=$db->prepare("DELETE FROM pass_change WHERE user_id=?;"); while($account=$stmt->fetch(PDO::FETCH_ASSOC)){ - update_system_user_password($account['system_account'], $account['password']); + $system_account = sanitize_system_account($account['system_account']); + if($system_account === false){ + echo "ERROR: Account $account[system_account] looks strange\n"; + continue; + } + update_system_user_password($system_account, $account['password']); $del->execute([$account['id']]); } //update quotas $stmt=$db->query('SELECT users.system_account, disk_quota.quota_files, disk_quota.quota_size, users.id FROM disk_quota INNER JOIN users ON (users.id=disk_quota.user_id) WHERE disk_quota.updated = 1 AND users.id NOT IN (SELECT user_id FROM new_account) AND users.todelete!=1;'); $updated=$db->prepare("UPDATE disk_quota SET updated = 0 WHERE user_id=?;"); -while($account=$stmt->fetch(PDO::FETCH_NUM)){ - exec('quotatool -u '. escapeshellarg($account[0]) . ' -i -q ' . escapeshellarg($account[1]) . ' -l ' . escapeshellarg($account[1]) . ' ' . HOME_MOUNT_PATH); - exec('quotatool -u '. escapeshellarg($account[0]) . ' -b -q ' . escapeshellarg($account[2]) . ' -l ' . escapeshellarg($account[2]) . ' ' . HOME_MOUNT_PATH); - $updated->execute([$account[3]]); +while($account=$stmt->fetch(PDO::FETCH_ASSOC)){ + $system_account = sanitize_system_account($account['system_account']); + if($system_account === false){ + echo "ERROR: Account $account[system_account] looks strange\n"; + continue; + } + exec('quotatool -u '. escapeshellarg($system_account) . ' -i -q ' . escapeshellarg($account['quota_files']) . ' -l ' . escapeshellarg($account['quota_size']) . ' ' . HOME_MOUNT_PATH); + exec('quotatool -u '. escapeshellarg($system_account) . ' -b -q ' . escapeshellarg($account['quota_files']) . ' -l ' . escapeshellarg($account['quota_size']) . ' ' . HOME_MOUNT_PATH); + $updated->execute([$account['id']]); } diff --git a/var/www/find_old.php b/var/www/find_old.php index dc4ef7c..2ded764 100644 --- a/var/www/find_old.php +++ b/var/www/find_old.php @@ -4,8 +4,14 @@ $db = get_db_instance(); //update quota usage $stmt=$db->query('SELECT id, system_account FROM users WHERE id NOT IN (SELECT user_id FROM new_account) AND todelete!=1;'); +$all_accounts=$stmt->fetchAll(PDO::FETCH_ASSOC); $update=$db->prepare('UPDATE disk_quota SET quota_size_used = ?, quota_files_used = ? WHERE user_id = ?;'); -while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ +foreach($all_accounts as $tmp){ + $system_account = sanitize_system_account($tmp['system_account']); + if($system_account === false){ + echo "ERROR: Account $tmp[system_account] looks strange\n"; + continue; + } $quota = shell_exec('quota -pu ' . escapeshellarg($tmp['system_account'])); $quota_array = explode("\n", $quota); if(!empty($quota_array[2])){ @@ -16,9 +22,12 @@ while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ } //delete tmp files older than 24 hours -$stmt=$db->query('SELECT system_account FROM users;'); -$all=$stmt->fetchAll(PDO::FETCH_ASSOC); -foreach($all as $tmp){ +foreach($all_accounts as $tmp){ + $system_account = sanitize_system_account($tmp['system_account']); + if($system_account === false){ + echo "ERROR: Account $tmp[system_account] looks strange\n"; + continue; + } exec('find '.escapeshellarg("/home/$tmp[system_account]/tmp").' -path '.escapeshellarg("/home/$tmp[system_account]/tmp/*").' -cmin +1440 -delete'); } exec("find /var/www/tmp -path '/var/www/tmp/*' -cmin +1440 -delete"); @@ -26,10 +35,15 @@ exec("find /var/www/tmp -path '/var/www/tmp/*' -cmin +1440 -delete"); //delete unused accounts older than 30 days $last_month=time()-60*60*24*30; $del=$db->prepare('UPDATE users SET todelete=1 WHERE id=?;'); -$stmt=$db->prepare('SELECT system_account, id FROM users WHERE dateaddedprepare('SELECT system_account, id FROM users WHERE dateaddedexecute([$last_month]); $all=$stmt->fetchAll(PDO::FETCH_ASSOC); foreach($all as $tmp){ + $system_account = sanitize_system_account($tmp['system_account']); + if($system_account === false){ + echo "ERROR: Account $tmp[system_account] looks strange\n"; + continue; + } //check modification times if(filemtime("/home/$tmp[system_account]/data/")>$last_month){ continue;