Friday, December 28, 2007

Port Knocking

Just tried out port knocking on using the code from Zeroflux. I was using CentOS 4.5. Downloaded the SRPM from the site and build and install it.

% rpmbuild --rebuild knock-0.5-4.src.rpm
% cd /usr/src/redhat/RPMS/i386/
% rpm -i knock-0.5-4.i386.rpm


I tried a single port knock and it didn't work for me. It turns out that the state machine implemented expects at least 2 packets to match while in this example I want a knock on port 7000 to allow me to open the protected_port. I had to configure /etc/knockd.conf to have 7000 twice in my configuration.


[ProtectSvc]
sequence = 7000,7000
seq_timeout = 15
tcpflags = syn
start_command = /sbin/iptables -A INPUT -i eth0 -s %IP% -p tcp
-m tcp --dport -m state --state NEW -j ACCEPT
cmd_timeout = 30
stop_command = /sbin/iptables -D INPUT -i eth0 -s %IP% -p tcp
-m tcp --dport -m state --state NEW -j ACCEPT


now if I telnet to the port 7000 then after 2 syn packets the start_command executes adding the iptables rule leaving me with a 30 second window to do a connect to the service running on the Protected_port. After 30 seconds stop_command shall execute closing the window. I needed to have the following rule in my iptables

/sbin/iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT

to allow established connection to continue even though the stop_command removed the rule from iptables.

Saturday, December 1, 2007

Integrating CAS with Apache

To protect content with CAS without modifying the underlying application, inspired by Apache::AuthCAS/Apache2::AuthCAS, I decided to write a short script to achieve the same; of course it has less capabilities since I don't need proxy related stuff.

This script has been tested on CentOS release 4.5 (Final). Also it is broken in two stages. First stage is integrated with Apache and is responsible for checking for a valid session. If no valid session is found it redirects the user to the second stage that checks whether a valid CAS ticket is provided and if so whether the user has the permission to the resource. If everything checks out to be ok it sets a cookie and redirects the user to the originally requested resource. Only downside to this strategy is that if the original request was a POST then by the time all these redirections have taken place, the originally posted values are lost. Underlying protected resources need to be able to handle such scenario gracefully (alternatively all original POSTS could be converted to GET; however, I decided to not do that for now). Example:

Suppose original request is

GET /cgi-bin/xyz.cgi?abc=def HTTP/1.1

First stage will check whether the browser provides a cookie containing the valid session id in it. If it does, let the URL be served. Otherwise the uri is changed to

GET /cgi-bin/login.cgi?url=/cgi-bin/xyz.cgi?abc=def HTTP/1.1

login.cgi now checks that there is no ticket in the request and redirects the browser to CAS server

https://cas.mycom.com/cas/login?service=http://myserver.mycom.com/cgi-bin/login.cgi?url=/cgi-bin/xyz.cgi?abc=def


Upon successful login CAS server redirects the browser back with a ticket added to the URL

http://myserver.mycom.com/cgi-bin/login.cgi?url=/cgi-bin/xyz.cgi?abc=def&ticket=XXX

this time the first stage sees the following request

GET /cgi-bin/login.cgi?url=/cgi-bin/xyz.cgi?abc=def&ticket=XXX HTTP/1.1

with no valid session id provided by the browser but detects that the request is for login.cgi; therefore, instead of modifying the uri it just lets the request go through. login.cgi now extracts the ticket provided and validates it with CAS for authenticity and receives a user ID. It checks whether the user id has access to the requested resource, if no then display an error message, else set a session in the cookie and let redirect the browser to the original requested url

http://myserver.mycom.com/cgi-bin/xyz.cgi?abc=def

Again this is seen by apache as

GET /cgi-bin/xyz.cgi?abc=def HTTP/1.1

but this time it receives a session id that it can successfully validate and therefore lets the request go through. The following code achieves the above mentioned.

Apache::Login.pm


package Apache::Login;

use strict;
use warnings;
use Apache::RequestRec ();
use Apache::RequestUtil;
use APR::Table;
use Apache::Log;
use Apache::URI;
use Apache::Const qw(DECLINED HTTP_MOVED_TEMPORARILY M_GET);
use Digest::MD5 qw(md5_base64);
use AuthCAS;
use DBI;

my $LOGIN = "/cgi-bin/login.cgi";
my $DATABASE = "db";
my $HOST = "db_hostname";
my $USERNAME = "db_username";
my $PASSWORD = "db_passowrd";
my $COOKIE_NAME = "cookie_name";

sub handler {
my $r = shift;
my $url = $r->unparsed_uri();
my $server = $r->get_server_name();
my $port = $r->get_server_port();

# authenticate the 1st internal request allow the rest to go through
if(!$r->is_initial_req) {
return DECLINED;
}

my $c = $r->headers_in->{Cookie};
if($c) {
# cookies can be of the type
# XXX=xxx; CGISESSID=xxx; YYY=xxx ...
my @cookies = split(/;/,$c);
for(my $i=0; $i<=$#cookies; $i++) {
my ($cookie_name, $cookie_value) = $cookies[$i] =~ m/($COOKIE_NAME=)(.*)/;
if($cookie_value) {
# check whether the session id is valid
my $dbh = DBI->connect("DBI:mysql:database=$DATABASE;host=$HOST",
$USERNAME, $PASSWORD, {RaiseError => 1})
or die $DBI::errstr;
my $sth = $dbh->prepare("SELECT user_id FROM session WHERE session_id = ?");
$sth->execute($cookie_value) or die $sth->errstr;
my $ref = $sth->fetchrow_hashref();
$sth->finish();
$dbh->disconnect();
if(defined $ref) {
return DECLINED;
}
}
}
}
# cookie not found
# if requesting login script then allow to go through
if($url =~ m/$LOGIN/) {
return DECLINED;
} else {
#redirect all other requests
# redirecting the request to the login script
$r->uri($LOGIN);
$r->args("url=$url");
return DECLINED;
}
}

1;


login.cgi


#!/usr/bin/perl -w

use strict;
use warnings;
use AuthCAS;
use CGI;
use CGI::Carp qw( fatalsToBrowser );
use File::Spec::Functions qw(splitpath);
use DBI;
use Digest::MD5 qw(md5_base64);
use Env;

my $DATABASE = "db";
my $DBHOST = "db_hostname";
my $USERNAME = "db_username";
my $PASSWORD = "db_password";
my $CAS_URL = "https://cas.mycom.com/cas/";
my $CA_FILE = "/some/location/cacert.pem";
my $COOKIE_NAME = "cookie_name";

my $q = new CGI();
my $query = $ENV{'QUERY_STRING'} || "";
my $server = $ENV{'SERVER_NAME'} || "";

my ($volume, $directories, $file) = splitpath($0);
my $cas = new AuthCAS(casUrl => $CAS_URL, CAFile => $CA_FILE);
my $ticket = $q->param('ticket');
if(!$ticket) {
my ($url) = ($query =~ m/^url=(.*)/);
my $login_url = $cas->getServerLoginURL("http://$server/cgi-bin/$file?url=$url");
print $q->redirect($login_url);
} else {
my ($url, $rest) = ($query =~ m/^url=(.*)&ticket=(.*)/);
my $user = $cas->validateST("http://$server/cgi-bin/$file?url=$url", $ticket)
or die AuthCAS::get_errors();
my $dbh = DBI->connect("DBI:mysql:database=$DATABASE;host=$DBHOST", $USERNAME, $PASSWORD, {RaiseError => 1, AutoCommit => 1}) or die $
DBI::errstr;
my $sth = $dbh->prepare("SELECT id FROM user WHERE username = ?");
$sth->execute($user) or die $sth->errstr;
my $ref = $sth->fetchrow_hashref();
if( defined($ref) ) {
my $id = $ref->{'id'};
my $session_id = md5_base64(time, $ticket);
$sth = $dbh->prepare("SELECT user_id, session_id, count FROM session WHERE session_id = ?");
$sth->execute($id);
my $exists = $sth->fetchrow_hashref();
if($exists) {
my $count = $exists->{'count'} + 1;
$sth = $dbh->prepare("UPDATE session SET session_id = ?, count = ? WHERE user_id = ?");
$sth->execute($session_id, $count, $id) or die $sth->errstr;
} else {
$sth = $dbh->prepare("INSERT INTO session (user_id, session_id, count) VALUES (?, ?, 1)");
$sth->execute($id, $session_id) or die $sth->errstr;
}
$sth->finish();
$dbh->disconnect();
my $cookie = $q->cookie($COOKIE_NAME => $session_id);
my $redirect_url = "http://$server$url";
print $q->header(-cookie=>$cookie);
print <<END;
<html>
<header>
<meta http-equiv="refresh" content="1; URL=$redirect_url">
</header>
</html>
END
} else {
$sth->finish();
$dbh->disconnect();
print <<END;
Content-type: text/html

<html>
<header>
<title> Access Denied </title>
</header>
<body>
<h1> Access Denied </h1>
You don't have access rights to this resource
</body>
</html>
END
}
}