Posts Creating a secured terminal paste tool
Post
Cancel

Creating a secured terminal paste tool

Background

Having a disuccsion with a friend about termbin and that the only viable improvement to the system would most probably be in/out encryption, (src hosted at github). Enter me, a bored coder, who decided to take on the challenge of creating a SSL clone.

End user useage

Install ncat which is usually a part of the nmap package, but sometimes standalone.

sudo pacman -Sy extra/nmap

or

sudo apt install ncat

or

sudo dnf install nmap

Then to use the code after the server is setup and working (there is one free to use and live at spaste.online:8888) just add:

alias sp='ncat --ssl spaste.online 8888'

OR use the less elegant but more portable solution if unable to install ncat:

alias sp="openssl s_client -quiet -connect spaste.online:8888 2>/dev/null"

To your .bashrc. Then source it so it works in the current session:

source ~/.bashrc

Than you can just run commands and pipe their output to the sp, which will return a link to the pasted content in terminal.

cat /etc/passwd | sp

For example.

The terminal paste code

This requires a valid SSL certificate for both the https server (I used vanilla Apache), and for spaste.pl itself. I used letsencrypt to generate a valid root authority verifiable certificate, because it’s free, secure, and the setup is pretty painless. I would also recommend creating a ssl group and a spaste user to run this under (not root), just incase there are unknown security vulnerabilities, which is entirely possible, since I wrote this in about 35 minutes total. I put put the spaste repo in /var/www/spaste/ and gave spaste.pl group perission to read from my ssl certificate and private key, and permissions to write to the /var/www/html webserver root directory to actually create the paste files.

#!/usr/bin/perl

#
#   __ _  _  __   ___  __  ____ ____
#  /  ( \/ )/ _\ / __)/ _\/ ___(_  _)
# (  O )  (/    ( (_ /    \___ \ )(
#  \__(_/\_\_/\_/\___\_/\_(____/(__)
#
# By oxagast, thanks to termbin.org creators for the idea.
# I would suggest creating an ssl user and pastebot user for this for security reasons.
# Set the permissions on your valid cert.pm and privkey.pem and on the directory on the
# webserver.
# useage: ./spaste.pl

use strict;
use warnings;
use IO::Handle;
use Fcntl ( "F_GETFL", "F_SETFL", "O_NONBLOCK" );
use Socket;
use IO::Socket::SSL;
use threads;
chdir "/var/www/spaste/";
STDOUT->autoflush();
my $logfile = "/var/log/spaste.log"; # log
my $proot   = "/var/www/html/";     # paste root if not default
my $host    = "spaste.online";      # change to your server
my $srvname = "https://" . $host;
my $port    = "8888";
my $cer  = "/etc/letsencrypt/live/" . $host . "/cert.pem";    # use your cert
my $key  = "/etc/letsencrypt/live/" . $host . "/privkey.pem"; # use your privkey
my $sock = IO::Socket::IP->new(
    Listen    => SOMAXCONN,
    LocalPort => $port,
    Blocking  => 0,
    ReuseAddr => 1
) or die $!;
my $WITH_THREADS = 0;                                         # the switch!!

while (1) {
    eval {
        my $cl = $sock->accept();                             # threaded accept
        if ($cl) {
            my $th = threads->create( \&client, $cl );
            $th->detach();
        }
    };    # eval
    if ($@) {
        print STDERR "ex: $@\n";
        exit(1);
    }
}    # forever

sub client    # worker
{
    my $cl = shift;
    open(LOG, '>>', $logfile) or die $!;
    # upgrade INET socket to SSL
    $cl = IO::Socket::SSL->start_SSL(
        $cl,
        SSL_server    => 1,
        SSL_cert_file => $cer,
        SSL_key_file  => $key
    ) or die $@;

    # unblock
    my $flags = fcntl( $cl, F_GETFL, 0 ) or die $!;
    fcntl( $cl, F_SETFL, $flags | O_NONBLOCK ) or die $!;
    print STDERR $cl->peerhost . "/" . $cl->peerport . "\n";
    print LOG $cl->peerhost . "/" . $cl->peerport . "\n";

    while (1) {
        my $ret = "";

        # for (my $i = 0; $i < 100; $i ++)
        $ret = $cl->read( my $recv, 50000 );

        # faults here if with threads!
        my $uniq = 0;
        if ( defined($ret) && length($recv) > 0 ) {
            my $rndid = "";
            while ( $uniq != 1 ) {
                $rndid = genuniq();
                if ( -e "$proot$rndid" ) {
                    $uniq = 0;
                }
                else {
                    $uniq = 1;
                }
                if ( $uniq == 1 ) {
                    writef( $rndid, $recv, $cl, $logfile );
                }
            }
            $cl->close();

        }
    }
    close(LOG);
}

sub writef() {
    my ( $rndid, $recv, $cl, $logfile ) = @_;
    open(LOG, '>>', $logfile) or die $!;
    print LOG "$rndid : storing at $proot$rndid\n";
    print "$rndid : storing at $proot$rndid\n";
    print LOG "$rndid : serving at $srvname/$rndid\n";
    print "$rndid : serving at $srvname/$rndid\n";
    my $filename = $proot . $rndid;
    open( P, '>', $filename ) or die $!;
    print P $recv;
    close(P);
    print $cl "$srvname/$rndid" . "\n";
    close(LOG);

    return 1;
}

sub genuniq {
    my $pasid;    # for unique paste identifier
    my @set = ( 'A' .. 'Z', 'a' .. 'z', 0 .. 9 );
    my $num = $#set;

    $pasid .= $set[ rand($num) ] for 1 .. 8;
    return $pasid;    # push it back
}

I would also suggest adding the following to your /etc/apache2.conf so that mime types of files without extensions are handled correctly (ex. files containing php arn’t rendered incorrectly in the browser, etc).

# prevents files without an extension from having a mime
# type other than text.
<FilesMatch "^[^.]+$">  
    ForceType text/plain
</FilesMatch>

I’ve also written a systemd script that will retart the service after any crashes and make it startable on boot.

Description=SPaste Process Restart Upstart Script
After=auditd.service systemd-user-sessions.service time-sync.target

[Service]
User=spaste
TimeoutStartSec=0
Type=simple
KillMode=process
WorkingDirectory=/var/www/spaste
ExecStart=perl /var/www/spaste/spaste.pl
Restart=always
RestartSec=2
LimitNOFILE=5555

[Install]
WantedBy=multi-user.target

Update: In addition to this, as suggested by a user on Freenode, I have made spaste more secure by running a find based rm as a cronjob every morning at 8am removing files older than a week. This can be accomplished by adding the following to root’s crontab:

0 8 * * * find /var/www/html/ -type f -mtime +7 ! -name "?*.*" -execdir rm -- '{}' \;

Conclusion

In conclusion, I don’t know if this “service” will take off, but if it does, I’m hosting it at spaste.online.

This post is licensed under CC BY 4.0 by the author.