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