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

Annotation of botnow/Shell.pm, Revision 1.2

1.1       bountyht    1: #!/usr/bin/perl
                      2:
                      3: package Shell;
                      4:
                      5: use strict;
                      6: use warnings;
                      7: use OpenBSD::Pledge;
                      8: use OpenBSD::Unveil;
                      9: use MIME::Base64;
                     10: use Data::Dumper;
                     11: use Digest::SHA qw(sha256_hex);
                     12: use lib './';
                     13: require "SQLite.pm";
                     14: require "Hash.pm";
                     15:
                     16: my %conf = %main::conf;
                     17: my $chans = $conf{chans};
                     18: my $teamchans = $conf{teamchans};
                     19: my @teamchans = split /[,\s]+/m, $teamchans;
                     20: my $staff = $conf{staff};
                     21: my $captchaURL = "https://example.com/captcha.php?vhost=";
                     22: my $hostname = $conf{hostname};
                     23: my $terms = $conf{terms};
                     24: my $expires = $conf{expires};
                     25: my $mailfrom = $conf{mailfrom};
                     26: my $mailname = $conf{mailname};
                     27: my $passpath = "/etc/passwd";
                     28: my $httpdconfpath = "/etc/httpd.conf";
                     29: my $acmeconfpath = "/etc/acme-client.conf";
1.2     ! bountyht   30: my $pfconfpath = "/etc/pf.conf";
        !            31: my $relaydconfpath = "/etc/relayd.conf";
        !            32: my $startPort;
        !            33: my $endPort;
1.1       bountyht   34: main::cbind("pub", "-", "shell", \&mshell);
                     35: main::cbind("msg", "-", "shell", \&mshell);
                     36:
                     37: sub init {
                     38:        #dependencies for figlet
                     39:        unveil("/usr/local/bin/figlet", "rx") or die "Unable to unveil $!";
                     40:        unveil("/usr/lib/libc.so.95.1", "r") or die "Unable to unveil $!";
                     41:        unveil("/usr/libexec/ld.so", "r") or die "Unable to unveil $!";
                     42:        #dependencies for shell account
                     43:        unveil($passpath, "r") or die "Unable to unveil $!";
                     44:        unveil($httpdconfpath, "rwxc") or die "Unable to unveil $!";
                     45:        unveil($acmeconfpath, "rwxc") or die "Unable to unveil $!";
1.2     ! bountyht   46:        unveil($pfconfpath, "rwxc") or die "Unable to unveil $!";
        !            47:        unveil($relaydconfpath, "rwxc") or die "Unable to unveil $!";
1.1       bountyht   48:        unveil("/usr/sbin/chown", "rx") or die "Unable to unveil $!";
                     49:        unveil("/bin/chmod", "rx") or die "Unable to unveil $!";
                     50:        unveil("/usr/sbin/groupadd", "rx") or die "Unable to unveil $!";
                     51:        unveil("/usr/sbin/useradd", "rx") or die "Unable to unveil $!";
                     52:        unveil("/usr/sbin/groupdel", "rx") or die "Unable to unveil $!";
                     53:        unveil("/usr/sbin/userdel", "rx") or die "Unable to unveil $!";
                     54:        unveil("/bin/mkdir", "rx") or die "Unable to unveil $!";
                     55:        unveil("/bin/ln", "rx") or die "Unable to unveil $!";
                     56:        unveil("/usr/sbin/acme-client", "rx") or die "Unable to unveil $!";
                     57:        unveil("/bin/rm", "rx") or die "Unable to unveil $!";
                     58:        unveil("/bin/mv", "rx") or die "Unable to unveil $!";
                     59:        unveil("/home/", "rwxc") or die "Unable to unveil $!";
                     60: }
                     61:
                     62: # !shell <username> <email>
                     63: # !shell captcha <captcha>
                     64: sub mshell {
                     65:        my ($bot, $nick, $host, $hand, @args) = @_;
                     66:        my ($chan, $text);
                     67:        if (@args == 2) {
                     68:                ($chan, $text) = ($args[0], $args[1]);
                     69:        } else { $text = $args[0]; }
                     70:        my $hostmask = "$nick!$host";
                     71:        if (defined($chan) && $chans =~ /$chan/) {
                     72:                main::putserv($bot, "PRIVMSG $chan :$nick: Please check private message");
                     73:        }
                     74:        if ($text =~ /^$/) {
                     75:                main::putserv($bot, "PRIVMSG $nick :Type !help for new instructions");
                     76:                foreach my $chan (@teamchans) {
                     77:                        main::putservlocalnet($bot, "PRIVMSG $chan :Help shell *$nick* on ".$bot->{name});
                     78:                }
                     79:                return;
                     80:        } elsif (main::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) {
                     81:                my $username = $1;
                     82:                if (SQLite::deleterows("shell", "username", $username)) {
                     83:                        # TODO delete shell
1.2     ! bountyht   84:                        deleteshell($username);
1.1       bountyht   85:                        foreach my $chan (@teamchans) {
                     86:                                main::putserv($bot, "PRIVMSG $chan :$username deleted");
                     87:                        }
                     88:                }
                     89:                return;
                     90:        }
                     91:        ### TODO: Check duplicate emails ###
                     92:        my @rows = SQLite::selectrows("irc", "nick", $nick);
                     93:        foreach my $row (@rows) {
                     94:                my $password = SQLite::get("shell", "ircid", $row->{id}, "password");
                     95:                if (defined($password)) {
                     96:                        main::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help.");
                     97:                        return;
                     98:                }
                     99:        }
                    100:        if ($text =~ /^lastseen\s+([[:alnum:]]+)/) {
                    101:        }
                    102:        if ($text =~ /^captcha\s+([[:alnum:]]+)/) {
                    103:                my $text = $1;
                    104:                my $ircid = SQLite::id("irc", "nick", $nick, $expires);
                    105:                if (!defined($ircid)) { die "undefined ircid"; }
                    106:                my $captcha = SQLite::get("shell", "ircid", $ircid, "captcha");
                    107:                if ($text ne $captcha) {
                    108:                        main::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !shell <username> <email>");
                    109:                        return;
                    110:                }
                    111:                my $pass = Hash::newpass();
                    112:                chomp(my $encrypted = `encrypt $pass`);
                    113:                my $username = SQLite::get("shell", "ircid", $ircid, "username");
                    114:                my $email = SQLite::get("shell", "ircid", $ircid, "email");
                    115:                my $version = SQLite::get("shell", "ircid", $ircid, "version");
                    116:                my $bindhost = "$username.$hostname";
                    117:                SQLite::set("shell", "ircid", $ircid, "password", $encrypted);
                    118:                if (DNS::nextdns($username)) {
                    119:                        sleep(2);
1.2     ! bountyht  120:                        createshell($username, $pass, $bindhost);
1.1       bountyht  121:                        mailshell($username, $email, $pass, "shell", $version);
                    122:                        main::putserv($bot, "PRIVMSG $nick :Check your email!");
                    123:
                    124:                        #www($newnick, $reply, $password, "bouncer");
                    125:                } else {
                    126:                        foreach my $chan (@teamchans) {
                    127:                                main::putserv($bot, "PRIVMSG $chan :Assigning bindhost $bindhost failed");
                    128:                        }
                    129:                }
                    130:                return;
                    131:        } elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) {
                    132:                my ($username, $email) = ($1, $2);
                    133:                my @users = col($passpath, 1, ":");
                    134:                my @matches = grep(/^$username$/i, @users);
                    135:                if (scalar(@matches) > 0) {
                    136:                        main::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please choose another username, or contact staff for help.");
                    137:                        return;
                    138:                }
                    139:                #               my $captcha = join'', map +(0..9,'a'..'z','A'..'Z')[rand(10+26*2)], 1..4;
                    140:                my $captcha = int(rand(999));
                    141:                my $ircid = int(rand(2147483647));
                    142:                SQLite::set("irc", "id", $ircid, "localtime", time());
                    143:                SQLite::set("irc", "id", $ircid, "date", main::date());
                    144:                SQLite::set("irc", "id", $ircid, "hostmask", $hostmask);
                    145:                SQLite::set("irc", "id", $ircid, "nick", $nick);
                    146:                SQLite::set("shell", "ircid", $ircid, "username", $username);
                    147:                SQLite::set("shell", "ircid", $ircid, "email", $email);
                    148:                SQLite::set("shell", "ircid", $ircid, "captcha", $captcha);
                    149:                main::whois($bot->{sock}, $nick);
                    150:                main::ctcp($bot->{sock}, $nick);
                    151:                main::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`);
                    152:                main::putserv($bot, "PRIVMSG $nick :$captchaURL".encode_base64($captcha));
                    153:                main::putserv($bot, "PRIVMSG $nick :Type !shell captcha <text>");
                    154:                foreach my $chan (@teamchans) {
                    155:                        main::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s captcha on $bot->{name} is $captcha");
                    156:                }
                    157:        } else {
                    158:                main::putserv($bot, "PRIVMSG $nick :Invalid username or email. Type !shell <username> <email> to try again.");
                    159:                foreach my $chan (@teamchans) {
                    160:                        main::putserv($bot, "PRIVMSG $chan :Help *$nick* on ".$bot->{name});
                    161:                }
                    162:        }
                    163: }
                    164: sub mailshell {
                    165:        my( $username, $email, $password, $service, $version )=@_;
                    166:        my $passhash = sha256_hex("$username");
                    167:        my $versionhash = encode_base64($version);
                    168:        my $body = <<"EOF";
                    169: You created a shell account!
                    170:
                    171: Username: $username
                    172: Password: $password
                    173: Server: $hostname
                    174: SSH Port: 22
1.2     ! bountyht  175: Your Ports: $startPort to $endPort
        !           176:
        !           177: To customize your vhost, connect to ask in #ircnow
1.1       bountyht  178:
                    179: *IMPORTANT*: Verify your email address:
                    180:
                    181: https://www.$hostname/register.php?id=$passhash&version=$versionhash
                    182:
1.2     ! bountyht  183: You *MUST* click on the link within 24 hours or your account will be deleted.
1.1       bountyht  184:
                    185: IRCNow
                    186: EOF
                    187:        Mail::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body);
                    188: }
                    189:
                    190:
                    191: #sub mregex {
                    192: #      my ($bot, $nick, $host, $hand, $text) = @_;
                    193: #      if ($staff !~ /$nick/) { return; }
                    194: #      if ($text =~ /^ips?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
                    195: #              my $ips = $1; # space-separated list of IPs
                    196: #              main::putserv($bot, "PRIVMSG $nick :".regexlist($ips));
                    197: #      } elsif ($text =~ /^users?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
                    198: #              my $users = $1; # space-separated list of usernames
                    199: #              main::putserv($bot, "PRIVMSG $nick :".regexlist($users));
                    200: #      } elsif ($text =~ /^[-_()|0-9A-Za-z:,\.?*\s]{3,}$/) {
                    201: #              my @lines = regex($text);
                    202: #              foreach my $l (@lines) { print "$l\n"; }
                    203: #      }
                    204: #}
                    205: #sub mforeach {
                    206: #      my ($bot, $nick, $host, $hand, $text) = @_;
                    207: #      if ($staff !~ /$nick/) { return; }
                    208: #      if ($text =~ /^network\s+del\s+([[:graph:]]+)\s+(#[[:graph:]]+)$/) {
                    209: #              my ($user, $chan) = ($1, $2);
                    210: #              foreach my $n (@main::networks) {
                    211: #                      main::putserv($bot, "PRIVMSG *controlpanel :delchan $user $n->{name} $chan");
                    212: #              }
                    213: #      }
                    214: #}
                    215:
                    216: #sub loadlog {
                    217: #      open(my $fh, '<', "$authlog") or die "Could not read file 'authlog' $!";
                    218: #      chomp(@logs = <$fh>);
                    219: #      close $fh;
                    220: #}
                    221:
                    222: # return all lines matching a pattern
                    223: #sub regex {
                    224: #      my ($pattern) = @_;
                    225: #      if (!@logs) { loadlog(); }
                    226: #      return grep(/$pattern/, @logs);
                    227: #}
                    228:
                    229: # given a list of IPs, return matching users
                    230: # or given a list of users, return matching IPs
                    231: #sub regexlist {
                    232: #      my ($items) = @_;
                    233: #      my @items = split /[,\s]+/m, $items;
                    234: #      my $pattern = "(".join('|', @items).")";
                    235: #      if (!@logs) { loadlog(); }
                    236: #      my @matches = grep(/$pattern/, @logs);
                    237: #      my @results;
                    238: #      foreach my $match (@matches) {
                    239: #              if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\] connected to ZNC from (.*)/) {
                    240: #                      my ($user, $ip) = ($1, $3);
                    241: #                      if ($items =~ /[.:]/) { # items are IP addresses
                    242: #                              push(@results, $user);
                    243: #                      } else { # items are users
                    244: #                              push(@results, $ip);
                    245: #                      }
                    246: #              }
                    247: #      }
                    248: #      my @sorted = sort @results;
                    249: #      @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq
                    250: #      return join(' ', @results);
                    251: #}
                    252:
                    253: sub createshell {
1.2     ! bountyht  254:        my ($username, $password, $bindhost) = @_;
1.1       bountyht  255:        system "doas groupadd $username";
                    256:        system "doas adduser -batch $username $username $username `encrypt $password`";
                    257:        system "doas chmod 700 /home/$username /home/$username/.ssh";
                    258:        system "doas chmod 600 /home/$username/{.Xdefaults,.cshrc,.cvsrc,.login,.mailrc,.profile}";
                    259:        system "doas mkdir /var/www/htdocs/$username";
                    260:        system "doas ln -s /var/www/htdocs/$username /home/$username/htdocs";
                    261:        system "doas chown -R $username:www /var/www/htdocs/$username /home/$username/htdocs";
                    262:        system "doas chmod -R o-rx /var/www/htdocs/$username /home/$username/htdocs";
                    263:        system "doas chmod -R g+rwx /var/www/htdocs/$username /home/$username/htdocs";
                    264:        my $lusername = lc $username;
                    265:        my $block = <<"EOF";
                    266: server "$lusername.$hostname" {
                    267:        listen on * port 80
                    268:        location "/.well-known/acme-challenge/*" {
                    269:                root "/acme"
                    270:                request strip 2
                    271:        }
                    272:        location "*.php" {
                    273:                fastcgi socket "/run/php-fpm.sock"
                    274:        }
                    275:        root "/htdocs/$username"
                    276: }
                    277: EOF
                    278:        main::appendfile($httpdconfpath, $block);
                    279:        $block = <<"EOF";
                    280: domain "$lusername.$hostname" {
                    281:        domain key "/etc/ssl/private/$lusername.$hostname.key"
1.2     ! bountyht  282:        domain full chain certificate "/etc/ssl/$lusername.$hostname.crt"
1.1       bountyht  283:        sign with letsencrypt
                    284: }
                    285: EOF
                    286:        main::appendfile($acmeconfpath, $block);
1.2     ! bountyht  287:        configurepf($username);
1.1       bountyht  288:        system "doas rcctl reload httpd";
                    289:         system "doas acme-client -F $lusername.$hostname";
1.2     ! bountyht  290:        system "doas ln -s /etc/ssl/$lusername.$hostname.crt /etc/ssl/$lusername.$hostname.fullchain.pem";
        !           291:        system "doas pfctl -f /etc/pf.conf";
        !           292:        configurerelayd($username);
        !           293:        $block = <<"EOF";
        !           294: ~       *       *       *       *       acme-client $lusername.$hostname && rcctl reload relayd
        !           295: EOF
        !           296:        system "echo $block | doas crontab -";
1.1       bountyht  297: #edquota $username
                    298:        return 1;
                    299: }
                    300:
                    301: sub deleteshell {
1.2     ! bountyht  302:        my ($username, $bindhost) = @_;
1.1       bountyht  303:        my $lusername = lc $username;
                    304:        system "doas groupdel $username";
                    305:        system "doas userdel $username";
                    306:        system "doas rm -f /etc/ssl/$lusername.$hostname.crt /etc/ssl/$lusername.$hostname.fullchain.pem /etc/ssl/private/$lusername.$hostname.key";
                    307:        my $httpdconf = main::readstr($httpdconfpath);
                    308:        my $block = <<"EOF";
                    309: server "$lusername.$hostname" {
                    310:        listen on * port 80
                    311:        location "/.well-known/acme-challenge/*" {
                    312:                root "/acme"
                    313:                request strip 2
                    314:        }
                    315:        location "*.php" {
                    316:                fastcgi socket "/run/php-fpm.sock"
                    317:        }
                    318:        root "/htdocs/$username"
                    319: }
                    320: EOF
                    321:        $block =~ s/{/\\{/gm;
                    322:        $block =~ s/}/\\}/gm;
                    323:        $block =~ s/\./\\./gm;
                    324:        $block =~ s/\*/\\*/gm;
                    325:        $httpdconf =~ s{$block}{}gm;
                    326:        print $httpdconf;
                    327:        main::writefile($httpdconfpath, $httpdconf);
                    328:
                    329:        my $acmeconf = main::readstr($acmeconfpath);
                    330:        $block = <<"EOF";
                    331: domain "$lusername.$hostname" {
                    332:        domain key "/etc/ssl/private/$lusername.$hostname.key"
                    333:        domain full chain certificate "/etc/ssl/$lusername.$hostname.fullchain.pem"
                    334:        sign with letsencrypt
                    335: }
                    336: EOF
                    337:        $block =~ s/{/\\{/gm;
                    338:        $block =~ s/}/\\}/gm;
                    339:        $block =~ s/\./\\./gm;
                    340:        $block =~ s/\*/\\*/gm;
                    341:        $acmeconf =~ s{$block}{}gm;
                    342:        main::writefile($acmeconfpath, $acmeconf);
                    343:        return 1;
                    344: }
                    345:
                    346: #TODO Fix for $i
                    347: # Return column $i from $filename as an array with file separator $FS
                    348: sub col {
                    349:        my ($filename, $i, $FS) = @_;
                    350:        my @rows = main::readarray($filename);
                    351:        my @results;
                    352:        foreach my $row (@rows) {
                    353:                if ($row =~ /^(.*?)$FS/) {
                    354:                        push(@results, $1);
                    355:                }
                    356:        }
                    357:        return @results;
                    358: }
1.2     ! bountyht  359:
        !           360: sub configurepf {
        !           361:     my $username = shift;
        !           362:     my @read = split('\n', main::readstr($pfconfpath) );
        !           363:
        !           364:     my $previousline = "";
        !           365:     my @pfcontent;
        !           366:     foreach my $line(@read)
        !           367:     {
        !           368:         my $currline = $line;
        !           369:         if( $currline ne "# end user ports") {
        !           370:             $previousline = $currline;
        !           371:         } else {
        !           372:             #pass in proto {tcp udp} to port {31361:31370} user {JL}
        !           373:             if( $previousline =~ /(\d*):(\d*)/ ) {
        !           374:                 my $startport = ( $1 + 10 );
        !           375:                 my $endport = ( $2 + 10 );
        !           376:                 my $insert = "pass in proto {tcp udp} to port {$startport:$endport} user {$username}";
        !           377:                 push(@pfcontent, $insert);
        !           378:                $startPort = $startport;
        !           379:                $endPort = $endport;
        !           380:             }
        !           381:         }
        !           382:         push(@pfcontent, $currline)
        !           383:     }
        !           384:     main::writefile("$pfconfpath", join("\n",@pfcontent))
        !           385: }
        !           386:
        !           387: sub configurerelayd {
        !           388:         my ($username) = @_;
        !           389:        my $block = "tls { keypair $username.$hostname }";
        !           390:        my $relaydconf = main::readstr($relaydconfpath);
        !           391:        my $newconf;
        !           392:        if ($relaydconf =~ /^.*tls\s+{\s+keypair\s+[.0-9a-zA-Z]+\s*}/m) {
        !           393:                $newconf = "$`$&\n\t$block$'";
        !           394:        }
        !           395:        main::writefile($relaydconfpath, $newconf);
        !           396: }
        !           397:
1.1       bountyht  398: #unveil("./newacct", "rx") or die "Unable to unveil $!";
                    399: 1; # MUST BE LAST STATEMENT IN FILE

CVSweb