[BACK]Return to Shell.pm CVS log [TXT][DIR] Up to [local] / botnow

File: [local] / botnow / Shell.pm (download)

Revision 1.2, Wed Jul 21 22:04:30 2021 UTC (2 years, 9 months ago) by bountyht
Branch: MAIN
CVS Tags: HEAD
Changes since 1.1: +62 -15 lines

pf/relayd support from sarah

#!/usr/bin/perl

package Shell;

use strict;
use warnings;
use OpenBSD::Pledge;
use OpenBSD::Unveil;
use MIME::Base64;
use Data::Dumper;
use Digest::SHA qw(sha256_hex);
use lib './';
require "SQLite.pm";
require "Hash.pm";

my %conf = %main::conf;
my $chans = $conf{chans};
my $teamchans = $conf{teamchans};
my @teamchans = split /[,\s]+/m, $teamchans;
my $staff = $conf{staff};
my $captchaURL = "https://example.com/captcha.php?vhost=";
my $hostname = $conf{hostname};
my $terms = $conf{terms};
my $expires = $conf{expires};
my $mailfrom = $conf{mailfrom};
my $mailname = $conf{mailname};
my $passpath = "/etc/passwd";
my $httpdconfpath = "/etc/httpd.conf";
my $acmeconfpath = "/etc/acme-client.conf";
my $pfconfpath = "/etc/pf.conf";
my $relaydconfpath = "/etc/relayd.conf";
my $startPort;
my $endPort;
main::cbind("pub", "-", "shell", \&mshell);
main::cbind("msg", "-", "shell", \&mshell);

sub init {
	#dependencies for figlet
	unveil("/usr/local/bin/figlet", "rx") or die "Unable to unveil $!";
	unveil("/usr/lib/libc.so.95.1", "r") or die "Unable to unveil $!";
	unveil("/usr/libexec/ld.so", "r") or die "Unable to unveil $!";
	#dependencies for shell account
	unveil($passpath, "r") or die "Unable to unveil $!";
	unveil($httpdconfpath, "rwxc") or die "Unable to unveil $!";
	unveil($acmeconfpath, "rwxc") or die "Unable to unveil $!";
	unveil($pfconfpath, "rwxc") or die "Unable to unveil $!";       
	unveil($relaydconfpath, "rwxc") or die "Unable to unveil $!";       
	unveil("/usr/sbin/chown", "rx") or die "Unable to unveil $!";
	unveil("/bin/chmod", "rx") or die "Unable to unveil $!";
	unveil("/usr/sbin/groupadd", "rx") or die "Unable to unveil $!";
	unveil("/usr/sbin/useradd", "rx") or die "Unable to unveil $!";
	unveil("/usr/sbin/groupdel", "rx") or die "Unable to unveil $!";
	unveil("/usr/sbin/userdel", "rx") or die "Unable to unveil $!";
	unveil("/bin/mkdir", "rx") or die "Unable to unveil $!";
	unveil("/bin/ln", "rx") or die "Unable to unveil $!";
	unveil("/usr/sbin/acme-client", "rx") or die "Unable to unveil $!";
	unveil("/bin/rm", "rx") or die "Unable to unveil $!";
	unveil("/bin/mv", "rx") or die "Unable to unveil $!";
	unveil("/home/", "rwxc") or die "Unable to unveil $!";
}

# !shell <username> <email>
# !shell captcha <captcha>
sub mshell {
	my ($bot, $nick, $host, $hand, @args) = @_;
	my ($chan, $text);
	if (@args == 2) {
		($chan, $text) = ($args[0], $args[1]);
	} else { $text = $args[0]; }
	my $hostmask = "$nick!$host";
	if (defined($chan) && $chans =~ /$chan/) {
		main::putserv($bot, "PRIVMSG $chan :$nick: Please check private message");
	}
	if ($text =~ /^$/) {
		main::putserv($bot, "PRIVMSG $nick :Type !help for new instructions");
		foreach my $chan (@teamchans) {
			main::putservlocalnet($bot, "PRIVMSG $chan :Help shell *$nick* on ".$bot->{name});
		}
		return;
	} elsif (main::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) {
		my $username = $1;
		if (SQLite::deleterows("shell", "username", $username)) {
			# TODO delete shell
			deleteshell($username);
			foreach my $chan (@teamchans) {
				main::putserv($bot, "PRIVMSG $chan :$username deleted");
			}
		}
		return;
	}
	### TODO: Check duplicate emails ###
	my @rows = SQLite::selectrows("irc", "nick", $nick);
	foreach my $row (@rows) {
		my $password = SQLite::get("shell", "ircid", $row->{id}, "password");
		if (defined($password)) {
			main::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help.");
			return;
		}
	}
	if ($text =~ /^lastseen\s+([[:alnum:]]+)/) {
	}
	if ($text =~ /^captcha\s+([[:alnum:]]+)/) {
		my $text = $1;
		my $ircid = SQLite::id("irc", "nick", $nick, $expires);
		if (!defined($ircid)) { die "undefined ircid"; }
		my $captcha = SQLite::get("shell", "ircid", $ircid, "captcha");
		if ($text ne $captcha) {
			main::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !shell <username> <email>");
			return;
		}
		my $pass = Hash::newpass();
		chomp(my $encrypted = `encrypt $pass`);
		my $username = SQLite::get("shell", "ircid", $ircid, "username");
		my $email = SQLite::get("shell", "ircid", $ircid, "email");
		my $version = SQLite::get("shell", "ircid", $ircid, "version");
		my $bindhost = "$username.$hostname";
		SQLite::set("shell", "ircid", $ircid, "password", $encrypted);
		if (DNS::nextdns($username)) {
			sleep(2);
			createshell($username, $pass, $bindhost);
			mailshell($username, $email, $pass, "shell", $version);
			main::putserv($bot, "PRIVMSG $nick :Check your email!");

			#www($newnick, $reply, $password, "bouncer");
		} else {
			foreach my $chan (@teamchans) {
				main::putserv($bot, "PRIVMSG $chan :Assigning bindhost $bindhost failed");
			}
		}
		return;
	} elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) {
		my ($username, $email) = ($1, $2);
		my @users = col($passpath, 1, ":");
		my @matches = grep(/^$username$/i, @users);
		if (scalar(@matches) > 0) {
			main::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please choose another username, or contact staff for help.");
			return;
		}
		#		my $captcha = join'', map +(0..9,'a'..'z','A'..'Z')[rand(10+26*2)], 1..4;
		my $captcha = int(rand(999));
		my $ircid = int(rand(2147483647));
		SQLite::set("irc", "id", $ircid, "localtime", time());
		SQLite::set("irc", "id", $ircid, "date", main::date());
		SQLite::set("irc", "id", $ircid, "hostmask", $hostmask);
		SQLite::set("irc", "id", $ircid, "nick", $nick);
		SQLite::set("shell", "ircid", $ircid, "username", $username);
		SQLite::set("shell", "ircid", $ircid, "email", $email);
		SQLite::set("shell", "ircid", $ircid, "captcha", $captcha);
		main::whois($bot->{sock}, $nick);
		main::ctcp($bot->{sock}, $nick);
		main::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`);
		main::putserv($bot, "PRIVMSG $nick :$captchaURL".encode_base64($captcha));
		main::putserv($bot, "PRIVMSG $nick :Type !shell captcha <text>");
		foreach my $chan (@teamchans) {
			main::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s captcha on $bot->{name} is $captcha");
		}
	} else {
		main::putserv($bot, "PRIVMSG $nick :Invalid username or email. Type !shell <username> <email> to try again.");
		foreach my $chan (@teamchans) {
			main::putserv($bot, "PRIVMSG $chan :Help *$nick* on ".$bot->{name});
		}
	}
}
sub mailshell {
	my( $username, $email, $password, $service, $version )=@_;
	my $passhash = sha256_hex("$username");
	my $versionhash = encode_base64($version);
	my $body = <<"EOF";
You created a shell account!

Username: $username
Password: $password
Server: $hostname
SSH Port: 22
Your Ports: $startPort to $endPort

To customize your vhost, connect to ask in #ircnow

*IMPORTANT*: Verify your email address:

https://www.$hostname/register.php?id=$passhash&version=$versionhash

You *MUST* click on the link within 24 hours or your account will be deleted.

IRCNow
EOF
	Mail::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body);
}


#sub mregex {
#	my ($bot, $nick, $host, $hand, $text) = @_;
#	if ($staff !~ /$nick/) { return; }
#	if ($text =~ /^ips?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
#		my $ips = $1; # space-separated list of IPs
#		main::putserv($bot, "PRIVMSG $nick :".regexlist($ips));
#	} elsif ($text =~ /^users?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
#		my $users = $1; # space-separated list of usernames
#		main::putserv($bot, "PRIVMSG $nick :".regexlist($users));
#	} elsif ($text =~ /^[-_()|0-9A-Za-z:,\.?*\s]{3,}$/) {
#		my @lines = regex($text);
#		foreach my $l (@lines) { print "$l\n"; }
#	}
#}
#sub mforeach {
#	my ($bot, $nick, $host, $hand, $text) = @_;
#	if ($staff !~ /$nick/) { return; }
#	if ($text =~ /^network\s+del\s+([[:graph:]]+)\s+(#[[:graph:]]+)$/) {
#		my ($user, $chan) = ($1, $2);
#		foreach my $n (@main::networks) {
#			main::putserv($bot, "PRIVMSG *controlpanel :delchan $user $n->{name} $chan");
#		}
#	}
#}

#sub loadlog {
#	open(my $fh, '<', "$authlog") or die "Could not read file 'authlog' $!";
#	chomp(@logs = <$fh>);
#	close $fh;
#}

# return all lines matching a pattern
#sub regex {
#	my ($pattern) = @_;
#	if (!@logs) { loadlog(); }
#	return grep(/$pattern/, @logs);
#}

# given a list of IPs, return matching users
# or given a list of users, return matching IPs
#sub regexlist {
#	my ($items) = @_;
#	my @items = split /[,\s]+/m, $items;
#	my $pattern = "(".join('|', @items).")";
#	if (!@logs) { loadlog(); }
#	my @matches = grep(/$pattern/, @logs);
#	my @results;
#	foreach my $match (@matches) {
#		if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\] connected to ZNC from (.*)/) {
#			my ($user, $ip) = ($1, $3);
#			if ($items =~ /[.:]/) { # items are IP addresses
#				push(@results, $user);
#			} else { # items are users
#				push(@results, $ip);
#			}
#		}
#	}
#	my @sorted = sort @results;
#	@results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq
#	return join(' ', @results);
#}

sub createshell {
	my ($username, $password, $bindhost) = @_;
	system "doas groupadd $username";
	system "doas adduser -batch $username $username $username `encrypt $password`";
	system "doas chmod 700 /home/$username /home/$username/.ssh";
	system "doas chmod 600 /home/$username/{.Xdefaults,.cshrc,.cvsrc,.login,.mailrc,.profile}";
	system "doas mkdir /var/www/htdocs/$username";
	system "doas ln -s /var/www/htdocs/$username /home/$username/htdocs";
	system "doas chown -R $username:www /var/www/htdocs/$username /home/$username/htdocs";
	system "doas chmod -R o-rx /var/www/htdocs/$username /home/$username/htdocs";
	system "doas chmod -R g+rwx /var/www/htdocs/$username /home/$username/htdocs";
	my $lusername = lc $username;
	my $block = <<"EOF";
server "$lusername.$hostname" {
	listen on * port 80
	location "/.well-known/acme-challenge/*" {
		root "/acme"
		request strip 2
	}
	location "*.php" {
		fastcgi socket "/run/php-fpm.sock"
	}
	root "/htdocs/$username"
}
EOF
	main::appendfile($httpdconfpath, $block);
	$block = <<"EOF";
domain "$lusername.$hostname" {
	domain key "/etc/ssl/private/$lusername.$hostname.key"
	domain full chain certificate "/etc/ssl/$lusername.$hostname.crt"
	sign with letsencrypt
}
EOF
	main::appendfile($acmeconfpath, $block);
	configurepf($username);
	system "doas rcctl reload httpd";
        system "doas acme-client -F $lusername.$hostname";
	system "doas ln -s /etc/ssl/$lusername.$hostname.crt /etc/ssl/$lusername.$hostname.fullchain.pem";
	system "doas pfctl -f /etc/pf.conf";
	configurerelayd($username);
	$block = <<"EOF";
~       *       *       *       *       acme-client $lusername.$hostname && rcctl reload relayd
EOF
	system "echo $block | doas crontab -";
#edquota $username
	return 1;
}

sub deleteshell {
	my ($username, $bindhost) = @_;
	my $lusername = lc $username;
	system "doas groupdel $username";
	system "doas userdel $username";
	system "doas rm -f /etc/ssl/$lusername.$hostname.crt /etc/ssl/$lusername.$hostname.fullchain.pem /etc/ssl/private/$lusername.$hostname.key";
	my $httpdconf = main::readstr($httpdconfpath);
	my $block = <<"EOF";
server "$lusername.$hostname" {
	listen on * port 80
	location "/.well-known/acme-challenge/*" {
		root "/acme"
		request strip 2
	}
	location "*.php" {
		fastcgi socket "/run/php-fpm.sock"
	}
	root "/htdocs/$username"
}
EOF
	$block =~ s/{/\\{/gm;
	$block =~ s/}/\\}/gm;
	$block =~ s/\./\\./gm;
	$block =~ s/\*/\\*/gm;
	$httpdconf =~ s{$block}{}gm;
	print $httpdconf;
	main::writefile($httpdconfpath, $httpdconf);
	
	my $acmeconf = main::readstr($acmeconfpath);
	$block = <<"EOF";
domain "$lusername.$hostname" {
	domain key "/etc/ssl/private/$lusername.$hostname.key"
	domain full chain certificate "/etc/ssl/$lusername.$hostname.fullchain.pem"
	sign with letsencrypt
}
EOF
	$block =~ s/{/\\{/gm;
	$block =~ s/}/\\}/gm;
	$block =~ s/\./\\./gm;
	$block =~ s/\*/\\*/gm;
	$acmeconf =~ s{$block}{}gm;
	main::writefile($acmeconfpath, $acmeconf);
	return 1;
}

#TODO Fix for $i
# Return column $i from $filename as an array with file separator $FS
sub col {
	my ($filename, $i, $FS) = @_;
	my @rows = main::readarray($filename);
	my @results;
	foreach my $row (@rows) {
		if ($row =~ /^(.*?)$FS/) {
			push(@results, $1);
		}
	}
	return @results;
}

sub configurepf {
    my $username = shift;
    my @read = split('\n', main::readstr($pfconfpath) );
    
    my $previousline = "";
    my @pfcontent;
    foreach my $line(@read)
    {
        my $currline = $line;    
        if( $currline ne "# end user ports") {
            $previousline = $currline;        
        } else {
            #pass in proto {tcp udp} to port {31361:31370} user {JL}
            if( $previousline =~ /(\d*):(\d*)/ ) {            
                my $startport = ( $1 + 10 );
                my $endport = ( $2 + 10 );
                my $insert = "pass in proto {tcp udp} to port {$startport:$endport} user {$username}";
                push(@pfcontent, $insert);
		$startPort = $startport;
		$endPort = $endport;
            }
        }
        push(@pfcontent, $currline)
    }
    main::writefile("$pfconfpath", join("\n",@pfcontent))
}

sub configurerelayd {  
        my ($username) = @_;                 
	my $block = "tls { keypair $username.$hostname }";
	my $relaydconf = main::readstr($relaydconfpath);
	my $newconf;
	if ($relaydconf =~ /^.*tls\s+{\s+keypair\s+[.0-9a-zA-Z]+\s*}/m) {
		$newconf = "$`$&\n\t$block$'";
	}
	main::writefile($relaydconfpath, $newconf);
}

#unveil("./newacct", "rx") or die "Unable to unveil $!";
1; # MUST BE LAST STATEMENT IN FILE