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
}
}

Friday, September 28, 2007

Refreshing the database connection pool with Apache DBCP

After deploying CAS one problem that cropped up was that after 2 hours Oracle would close the database connection while Apache's DBCP would hold on the connection in its pool unaware what the database has done. Then next time a database query CAS would through the following exception

java.sql.SQLException: ORA-03135: connection lost contact

Our dba suggested we use OCI instead of thin client and he configured ORACLE configuration on the system. However it didn't work. It seems this scenario is not part of the failover, probably because DB responds with a RESET instead of a timeout, so at the connectivity is not lost with the server. I tried looking for a RECONNECT setting for Oracle JDBC but couldn't find it; however, DBCP provides testing of the connection in the pool before using it. So I configured the testing as follows

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName">
<value>oracle.jdbc.driver.OracleDriver</value>
</property>
<property name="url">
<value>jdbc:oracle:oci:@<DB></value>
</property>
<property name="username">
<value>USERNAME</value>
</property>
<property name="password">
<value>PASSWORD</value>
</property>
<property name="validationQuery" value="select <something> from <table>" />
<property name="testOnBorrow" value="true"/>
</bean>

validationQuery is a quick query that is used to test the connection whenever the connection is borrowed from the pool. If the connection fails then it is removed from the pool and DBCP creates a new connection if it can't find any valid connection. This has been working so far without error. Only overhead is the execution of validationQuery. I would actually prefer to have a RECONNECT option as it would probably be with lower overhead. And for that the search continues.

Sunday, September 16, 2007

Integrating CAS with Perl

At work we are trying to setup CAS (Central Authentication Service from JA-SIG) with Perl and Java. Java worked out fine especially JSPs. Guys are trying to integrate it with acegi framework. So I decided to try out perl. As it turned out JSSE stores the certs in binary format called 'DER' while perl's SSL expects the certs to be in 'PEM' format. So after browsing a bit here is how I managed to get the cert

% openssl s_client -connect myserver:port -showcerts

This produced an output that contained something like

-----BEGIN CERTIFICATE-----
MIICoTCCAgqgAwIBAgIERui89jANBgkqhkiG9w0BAQUFADCBlDELMAkGA1UEBhMC
U0cxEjAQBgNVBAgTCVNpbmdhcG9yZTESMBAGA1UEBxMJU2luZ2Fwb3JlMSYwJAYD
VQQKEx1HZW5vbWUgSW5zdGl0dXRlIG9mIFNpbmdhcG9yZTEVMBMGA1UECxMMQXBw
bGljYXRpb25zMR4wHAYDVQQDExVzc28uZ2lzLmEtc3Rhci5lZHUuc2cwHhcNMDcw
OTEzMDQzMDQ2WhcNMjcwOTA4MDQzMDQ2WjCBlDELMAkGA1UEBhMCU0cxEjAQBgNV
-----END CERTIFICATE-----

I Just copied that into a file called /etc/cacert.pem.

To get things to work, I had to install AuthCAS version 1.3. However, there was an error in the module. It contained a line

unless (defined $xmlRef)

which should have read

unless (defined @xml)

After making the change, the following code ran without any problem

#!/usr/bin/env perl

use AuthCAS;
use CGI;
use CGI::Carp qw( fatalsToBrowser );
use File::Spec::Functions qw(splitpath);

my $q = new CGI();
my ($volume, $directories, $file) = splitpath($0);
my $cas = new AuthCAS(casUrl => "https://server:port",
CAFile => "/tmp/cacert.pem",
);
my $ticket = $q->param('ticket');
# if no ticket exists then redirect to login
if( $ticket eq "") {
my $login_url = $cas->getServerLoginURL("http://localhost/cgi-bin/$file");
print $q->redirect($login_url);
} else {
my $user = $cas->validateST("http://localhost/cgi-bin/$file", $ticket) or die AuthCAS::get_errors();
print <<END_OF_MESSAGE;
Content-type: text/html

<html>
<body>
Hello $user
</body>
</html>
END_OF_MESSAGE
}

Friday, September 7, 2007

Perl Module Browser in Perl/tk

I was interested in using Tk to develop some GUI application. As I was playing with Perl/tk I realised that there was no GUI based application for me to browse perl module installations on the system, so I decided to build one and try out Perl/tk widgets as well. I found the initial code on the web and modified it to suit my purpose.


#!/usr/bin/env perl
use strict;
use Tk;
use Tk::HList;
use Tk::BrowseEntry;
use Tk::Dialog;
use ExtUtils::Packlist;
use ExtUtils::Installed;

my $mw = MainWindow->new;
my $inst = ExtUtils::Installed->new();
my $dropdown;
my @modules;
my $dropdown_default_msg = "-- Choose a Module --";

# Mainwindow: sizex/y, positionx/y
$mw->geometry("600x300+100+120");
my $top = $mw->Frame()->pack(-anchor=>'n', -expand=>'yes', -fill=>'both');
my $bottom = $mw->Frame()->pack(-anchor=>'n');
# Default value
&create_datagrid($top, $bottom, $inst);
MainLoop;

sub create_datagrid {
my $mother = $_[0];
my $bottom = $_[1];
my $inst = $_[2];

# Create dropdown to choose module
my $dropdown_value;
$dropdown = $mother->BrowseEntry(
-label => "Choose Module",
-variable => \$dropdown_value,
# populate the dropdown list
-listcmd => sub {
if($#modules != 0 ) {
$dropdown->delete(0, $#modules);
}
@modules = sort($inst->modules());
foreach (@modules) {
$dropdown->insert('end', $_);
}
}
)->pack(-anchor=>'n');

my $grid = $mother->Scrolled(
'HList',
-columns => 1,
-scrollbars => 'osoe',
-width => 40,
-height => 10,
-background => 'white',
)->pack(-anchor=>'n', -expand=>'yes', -fill=>'both');
# Set the initial value for the dropdown
$dropdown_value = $dropdown_default_msg;

# Configure dropdown what to do when sth is selected
$dropdown->configure(
# What to do when an entry is selected
-browsecmd => sub {
my @values = sort($inst->files($dropdown_value));
$grid->delete('all'); # delete currently displayed values
my $i = 1;
foreach(@values) {
$grid->add($i);
$grid->itemCreate($i++, 0, -text => $_);
}
$grid->add($i);
$grid->itemCreate($i, 0,
-text => $inst->packlist($dropdown_value)->packlist_file()
);
},
);

my $delete_button = $bottom->Button(-text=>'Delete',
-command => sub {
my @button_text = ("Cancel", "Continue >>");
if($dropdown_value ne $dropdown_default_msg) {
my $confirm = $mw->Dialog(-title => "Delete $dropdown_value?",
-text => "This action shall remove $dropdown_value from the filesystem",
-default_button => $button_text[0],
-buttons => \@button_text);
my $value = $confirm->Show();
if($value eq $button_text[1]) {
foreach(sort($inst->files($dropdown_value))) {
unlink $_;
}
unlink $inst->packlist($dropdown_value)->packlist_file();
$dropdown_value = $dropdown_default_msg;
$grid->delete('all');
undef $inst;
$inst = ExtUtils::Installed->new(); }
}
}
)->grid(-column => 0, -row => 0);
my $exit_button = $bottom->Button(-text=>'Exit',
-command=> sub {exit;},
)->grid(-column => 1, -row => 0);
}


Tuesday, September 4, 2007

Installing Perl DBD::Oracle against 10g

I just installed oracle instantclient and perl DBD against 10g. Following was my configuration

  • OS: Fedora 7
Make sure that you have libstdc++.so.5 installed. It can be installed as follows
$ yum install compat-libstdc++-33

  • Oracle Instant client: 10.2.0.3
this constitutes of the following 3 rpms that you can get from oracle's website
    • oracle-instantclient-basic-10.2.0.3-1.i386.rpm
    • oracle-instantclient-devel-10.2.0.3-1.i386.rpm
    • oracle-instantclient-sqlplus-10.2.0.3-1.i386.rpm
(I believe you can skip the sqlplus installation, but I installed it).

  • DBD::Oracle: 1.19
Verify that the oracle installation is working by typing the following on your command prompt

$ sqlplus

You should get the following output

SQL*Plus: Release 10.2.0.3.0 - Production on Thu Aug 30 00:14:22 2007

Copyright (c) 1982, 2006, Oracle. All Rights Reserved.


Enter user-name:


Set the following environment variables

$ export LD_LIBRARY_PATH = /usr/lib/oracle/10.2.0.3/client/lib/
$ export ORACLE_SID = <SID>
$ export ORACLE_USERID = <username>/<password>@<servername>/<SID>

I had problems with ORACLE_HOME environment variable. When it was set to /usr/lib/oracle/10.2.0.3/client/lib/ I would get the following error message from sqlplus

Error 6 initializing SQL*Plus
Message file sp1.msb not found
SP2-0750: You may need to set ORACLE_HOME to your Oracle software directory

removing the variable ORACLE_HOME did the trick.

I downloaded DBD::Oracle from CPAN


$ perl -MCPAN -e shell

cpan> get Oracle::DBD
cpan> quit

change directory to ~/.cpan/build/DBD-Oracle-x.xx and run the following command

$ perl Makefile.PL -m /usr/share/oracle/10.2.0.3/client/demo.mk
$ make
$ make test

Following is the output of the test runs

t/01base................ok
t/10general.............ok
t/15nls.................ok
t/20select..............ok
t/21nchar............... Database and client versions and character sets:
Database 10.2.0.1.0 CHAR set is AL32UTF8 (Unicode), NCHAR set is AL16UTF16 (Unicode)
Client 10.2.0.3 NLS_LANG is '', NLS_NCHAR is ''
t/21nchar...............ok
t/22nchar_al32utf8......ok
t/22nchar_utf8..........ok
t/23wide_db.............ok
t/23wide_db_8bit........ok
t/23wide_db_al32utf8....ok
t/24implicit_utf8.......ok
t/25plsql...............ok
t/26exe_array...........ok
t/30long................ok
122/470 skipped: various reasons
t/31lob.................ok 7/9DBD::Oracle::db selectrow_array failed: ORA-00942: table or view does not exist (DBD ERROR: error possibly near <*> indicator at char 103 in '
SELECT cache_lobs + nocache_lobs AS temp_lob_count
FROM v$temporary_lobs templob, <*>v$session sess
WHERE sess.sid = templob.sid
AND sess.audsid = userenv('sessionid') ')
[for Statement "
SELECT cache_lobs + nocache_lobs AS temp_lob_count
FROM v$temporary_lobs templob, v$session sess
WHERE sess.sid = templob.sid
AND sess.audsid = userenv('sessionid') "] at t/31lob.t line 58.
t/31lob.................NOK 9/9
# Failed test 'no temp lobs left'
# at t/31lob.t line 166.
# got: undef
# expected: '0'
# Looks like you failed 1 test of 9.
t/31lob.................dubious
Test returned status 1 (wstat 256, 0x100)
DIED. FAILED test 9
Failed 1/9 tests, 88.89% okay
t/40ph_type.............ok 3/19 Placeholder behaviour for ora_type=1 (the default) varies with
Oracle version.
Oracle 7 didn't strip trailing spaces, Oracle 8 did, until 9.2.x Your system doesn't. If that seems odd, let us know.
t/40ph_type.............ok
t/50cursor..............ok
t/55nested..............ok
t/60reauth..............ORACLE_USERID_2 not defined. Tests skipped.
skipped all skipped: no reason given
t/70meta................ok
Failed Test Stat Wstat Total Fail List of Failed
-------------------------------------------------------------------------------
t/31lob.t 1 256 9 1 9
1 test and 122 subtests skipped.
Failed 1/20 test scripts. 1/1062 subtests failed.
Files=20, Tests=1062, 455 wallclock secs ( 3.60 cusr + 11.08 csys = 14.68 CPU)
Failed 1/20 test programs. 1/1062 subtests failed.
make: *** [test_dynamic] Error 255

To install the module run


$ make install

Perl module was good to go.