sanitize $system_account to protect against database modification exploits

This commit is contained in:
Daniel Winzen
2020-01-25 21:47:38 +01:00
parent 93dc5b10c4
commit 47fb983557
3 changed files with 131 additions and 46 deletions

View File

@ -298,6 +298,7 @@ function check_login(){
session_destroy(); session_destroy();
exit; exit;
} }
$user['system_account'] = basename($user['system_account']);
return $user; 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=$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]); $stmt->execute([$instance]);
while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ 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(!file_exists("/var/lib/tor-instances/$instance/hidden_service_$tmp[onion].onion")){
if($tmp['version']==2){ if($tmp['version']==2){
//php openssl implementation has some issues, re-export using native openssl //php openssl implementation has some issues, re-export using native openssl
@ -516,6 +522,11 @@ function rewrite_nginx_config(){
// onions // 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;"); $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)){ 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){ if($tmp['php']>0){
$php_location=" $php_location="
location ~ [^/]\.php(/|\$) { location ~ [^/]\.php(/|\$) {
@ -549,6 +560,11 @@ function rewrite_nginx_config(){
// clearnet domains // 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;"); $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)){ 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){ if($tmp['php']>0){
$php_location=" $php_location="
location ~ [^/]\.php(/|\$) { location ~ [^/]\.php(/|\$) {
@ -585,6 +601,11 @@ function rewrite_nginx_config(){
$nginx_mail=''; $nginx_mail='';
$stmt=$db->query("SELECT system_account FROM users WHERE id NOT IN (SELECT user_id FROM new_account) AND todelete!=1;"); $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)){ 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 { $nginx_mysql.="server {
listen unix:/home/$tmp[system_account]/var/run/mysqld/mysqld.sock; listen unix:/home/$tmp[system_account]/var/run/mysqld/mysqld.sock;
proxy_pass unix:/var/run/mysqld/mysqld.sock; proxy_pass unix:/var/run/mysqld/mysqld.sock;
@ -623,6 +644,11 @@ pm = ondemand
pm.max_children = 8 pm.max_children = 8
"; ";
while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){ 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']."] $php.='['.$tmp['system_account']."]
user = $tmp[system_account] user = $tmp[system_account]
group = www-data 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'; $shell = ENABLE_SHELL_ACCESS ? '/bin/bash' : '/usr/sbin/nologin';
$user = posix_getpwnam($system_account); $user = posix_getpwnam($system_account);
$passwd_line = "$user[name]:$user[passwd]:$user[uid]:$user[gid]:$user[gecos]:/:$user[shell]"; $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+"); $fp = fopen("/etc/shadow", "r+");
$locked = false; $locked = false;
do{ do{
@ -970,3 +1006,12 @@ function update_system_user_password($user, $password){
flock($fp, LOCK_UN); flock($fp, LOCK_UN);
fclose($fp); 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;
}

View File

@ -15,12 +15,21 @@ $del=$db->prepare("DELETE FROM new_account WHERE user_id=?;");
$approval = REQUIRE_APPROVAL ? 'WHERE new_account.approved=1': ''; $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;"); $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)){ 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; $reload[$account['instance']] = true;
//add and manage rights of system user //add and manage rights of system user
$shell = ENABLE_SHELL_ACCESS ? '/bin/bash' : '/usr/sbin/nologin'; $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'])); exec('useradd -l -g www-data -k /var/www/skel -m -s ' . escapeshellarg($shell) . ' ' . escapeshellarg($system_account));
update_system_user_password($account['system_account'], $account['password']); update_system_user_password($system_account, $account['password']);
setup_chroot($account['system_account']); setup_chroot($system_account);
//remove from to-add queue //remove from to-add queue
$del->execute([$account['id']]); $del->execute([$account['id']]);
} }
@ -28,32 +37,35 @@ while($account=$stmt->fetch(PDO::FETCH_ASSOC)){
//delete old accounts //delete old accounts
$del=$db->prepare("DELETE FROM users WHERE id=?;"); $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;"); $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;'); $mark_onions=$db->prepare('UPDATE onions SET enabled=-1 WHERE user_id=? AND enabled!=-2;');
foreach($accounts as $account){ foreach($accounts as $account){
$instance=$account[3]; $system_account = sanitize_system_account($account['system_account']);
$reload[$instance]=true; if($system_account === false){
$mark_onions->execute([$account[1]]); echo "ERROR: Account $account[system_account] looks strange\n";
continue;
}
$reload[$account['instance']]=true;
$mark_onions->execute([$account['id']]);
} }
//delete hidden services from tor //delete hidden services from tor
$del_onions=$db->prepare('DELETE FROM onions WHERE onion=?;'); $del_onions=$db->prepare('DELETE FROM onions WHERE onion=?;');
$stmt=$db->query('SELECT onion, instance FROM onions WHERE enabled=-1;'); $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){ foreach($onions as $onion){
$instance = $onion[1]; $reload[$onion['instance']] = true;
$reload[$instance] = true; if(is_dir("/var/lib/tor-instances/$onion[instance]/hidden_service_$onion[onion].onion/")){
if(file_exists("/var/lib/tor-instances/$instance/hidden_service_$onion[0].onion/")){ if(is_dir("/var/lib/tor-instances/$onion[instance]/hidden_service_$onion[onion].onion/authorized_clients/")){
if(file_exists("/var/lib/tor-instances/$instance/hidden_service_$onion[0].onion/authorized_clients/")){ foreach(glob("/var/lib/tor-instances/$onion[instance]/hidden_service_$onion[onion].onion/authorized_clients/*") as $file){
foreach(glob("/var/lib/tor-instances/$instance/hidden_service_$onion[0].onion/authorized_clients/*") as $file){
unlink($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); 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]]); $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=?;'); $stmt=$db->prepare('SELECT mysql_database FROM mysql_databases WHERE user_id=?;');
$drop_user=$db->prepare("DROP USER ?@'%';"); $drop_user=$db->prepare("DROP USER ?@'%';");
foreach($accounts as $account){ 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 //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 //delete user and group
exec('userdel -rf ' . escapeshellarg($account[0])); exec('userdel -rf ' . escapeshellarg($system_account));
//delete all log files //delete all log files
if(file_exists("/var/log/nginx/access_$account[0].log")){ $log_files = [
unlink("/var/log/nginx/access_$account[0].log"); "/var/log/nginx/access_".$system_account.".log",
} "/var/log/nginx/access_".$system_account.".log.1",
if(file_exists("/var/log/nginx/access_$account[0].log.1")){ "/var/log/nginx/error_".$system_account.".log",
unlink("/var/log/nginx/access_$account[0].log.1"); "/var/log/nginx/error_".$system_account.".log.1"
} ];
if(file_exists("/var/log/nginx/error_$account[0].log")){ foreach($log_files as $log_file){
unlink("/var/log/nginx/error_$account[0].log"); if(file_exists($log_file)){
} unlink($log_file);
if(file_exists("/var/log/nginx/error_$account[0].log.1")){ }
unlink("/var/log/nginx/error_$account[0].log.1");
} }
//delete user from database //delete user from database
$drop_user->execute([$account[2]]); $drop_user->execute([$account['mysql_user']]);
$stmt->execute([$account[1]]); $stmt->execute([$account['id']]);
while($tmp=$stmt->fetch(PDO::FETCH_NUM)){ while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){
$db->exec('DROP DATABASE IF EXISTS `'.preg_replace('/[^a-z0-9]/i', '', $tmp[0]).'`;'); $db->exec('DROP DATABASE IF EXISTS `'.preg_replace('/[^a-z0-9]/i', '', $tmp['mysql_database']).'`;');
} }
$db->exec('FLUSH PRIVILEGES;'); $db->exec('FLUSH PRIVILEGES;');
//delete user from user database //delete user from user database
$del->execute([$account[1]]); $del->execute([$account['id']]);
} }
// update passwords // 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;"); $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=?;"); $del=$db->prepare("DELETE FROM pass_change WHERE user_id=?;");
while($account=$stmt->fetch(PDO::FETCH_ASSOC)){ 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']]); $del->execute([$account['id']]);
} }
//update quotas //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;'); $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=?;"); $updated=$db->prepare("UPDATE disk_quota SET updated = 0 WHERE user_id=?;");
while($account=$stmt->fetch(PDO::FETCH_NUM)){ while($account=$stmt->fetch(PDO::FETCH_ASSOC)){
exec('quotatool -u '. escapeshellarg($account[0]) . ' -i -q ' . escapeshellarg($account[1]) . ' -l ' . escapeshellarg($account[1]) . ' ' . HOME_MOUNT_PATH); $system_account = sanitize_system_account($account['system_account']);
exec('quotatool -u '. escapeshellarg($account[0]) . ' -b -q ' . escapeshellarg($account[2]) . ' -l ' . escapeshellarg($account[2]) . ' ' . HOME_MOUNT_PATH); if($system_account === false){
$updated->execute([$account[3]]); 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']]);
} }

View File

@ -4,8 +4,14 @@ $db = get_db_instance();
//update quota usage //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;'); $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 = ?;'); $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 = shell_exec('quota -pu ' . escapeshellarg($tmp['system_account']));
$quota_array = explode("\n", $quota); $quota_array = explode("\n", $quota);
if(!empty($quota_array[2])){ if(!empty($quota_array[2])){
@ -16,9 +22,12 @@ while($tmp=$stmt->fetch(PDO::FETCH_ASSOC)){
} }
//delete tmp files older than 24 hours //delete tmp files older than 24 hours
$stmt=$db->query('SELECT system_account FROM users;'); foreach($all_accounts as $tmp){
$all=$stmt->fetchAll(PDO::FETCH_ASSOC); $system_account = sanitize_system_account($tmp['system_account']);
foreach($all as $tmp){ 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 '.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"); 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 //delete unused accounts older than 30 days
$last_month=time()-60*60*24*30; $last_month=time()-60*60*24*30;
$del=$db->prepare('UPDATE users SET todelete=1 WHERE id=?;'); $del=$db->prepare('UPDATE users SET todelete=1 WHERE id=?;');
$stmt=$db->prepare('SELECT system_account, id FROM users WHERE dateadded<?;'); $stmt=$db->prepare('SELECT system_account, id FROM users WHERE dateadded<? AND id NOT IN (SELECT user_id FROM new_account) AND todelete!=1;');
$stmt->execute([$last_month]); $stmt->execute([$last_month]);
$all=$stmt->fetchAll(PDO::FETCH_ASSOC); $all=$stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($all as $tmp){ 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 //check modification times
if(filemtime("/home/$tmp[system_account]/data/")>$last_month){ if(filemtime("/home/$tmp[system_account]/data/")>$last_month){
continue; continue;