#!/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 # !shell 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 "); 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 "); 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 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