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