« TomcatExpert: Session Fixation Protection | Main | Red Hat OpenShift - New Free and Paid PaaS Cloud Services »

05/03/2011

CryptoNark v0.4.5 Released

I'm happy to announce a new update to CryptoNark today:  Version 0.4.5 is out now.  This version incorporates the HTTP PROPFIND vulnerability scanning from my recent post on that subject:  Verifying 'WebDAV HTTP Method PROPFIND Enabled' Remediation was Successful.

Refer to that link for more details but cryptonark will only perform the HTTP PROPFIND scan if run with the -xl/--kitchen-sink arguments and only if Microsoft IIS is seen in a server response header.

The CPAN module, XML::LibXML, is used, which enables the script to parse the xml in the response and allows the script to echo back the data in question. So, you get to see just what it is that the pci scanners are complaining about when the WebDAV HTTP Method PROPFIND Enabled information disclosure vulnerability is reported on one of your sites. Once again, a reminder that cryptonark's purpose is to provide a mechanism for the site administrator to validate both that a vulnerability exists and to validate that remediation activities were successfully implemented. The tool was not intended to be used for evil. :)

The other change added in this update was the change in how redirection is handled during the Unsafe URL scans. I've defaulted LWP::UserAgent to not follow redirects, since following redirects seems to be a source for some false positives that I was experiencing when scanning some of my sites. Let me know if the change is causing you more trouble then it was.

Download CryptoNark v0.4.5 from the downloads page or you can just copy it from the source code below.

#!/usr/bin/env perl

use Modern::Perl;
use Term::ANSIColor qw(:constants);
use Tie::Hash::Indexed;
use IO::Socket::SSL 'debug0';
use Mozilla::CA;
# The following three modules provide support for 
# displaying server type and TRACE and TRACK requests.
use LWP::UserAgent;
use HTTP::Headers;
use HTTP::Request;
use XML::LibXML;
# Core Module introducing command line options
use Getopt::Long; 

use feature 'state';

my $version = "v0.4.5";

my ( $host, $port, $scheme );
my $insecure = '';
my $xl = '';
my $verifymode = 2;
my $useragent = "cryptonark-pci-auditor/" . $version;

sub usage{
    say "Usage: cnark.pl -h|--host  -p|--port  \n\t\t[-i|--insecure] [-xl|--kitchen-sink]";
    exit;
  }

usage() if ( ! GetOptions("h|host=s" => \$host, "p|port=s" => \$port, "i|insecure" => \$insecure, "xl|kitchen-sink" => \$xl ) or ( ! defined $host) or ( ! defined $port) );

my $key;
my $value;
my $ssl2client;
my $ssl3client;

if ( $port eq 443) {
  $scheme = 'https';
  }
else {
  $scheme = 'http';
  }

if ($insecure eq 1 ) {
  $verifymode = 0;
  }


# Populate arrays with OpenSSL ciphers
# Note:  TLSv1 ciphers and SSLv3 ciphers are 
# identical in OpenSSL

tie my %ssl2_ciphers, 'Tie::Hash::Indexed';
tie my %tls1_ciphers, 'Tie::Hash::Indexed';

%ssl2_ciphers = (
  'DES-CBC3-MD5' => '168 bits, High Encryption',
  'RC2-CBC-MD5' => '128 bits, Medium Encryption',
  'RC4-MD5' => '128 bits, Medium Encryption',
  'DES-CBC-MD5' => '56 bits, Low Encryption',
  'EXP-RC2-CBC-MD5' => '40 bits, Export-Grade Encryption',
  'EXP-RC4-MD5' => '40 bits, Export-Grade Encryption'
);

%tls1_ciphers = (
  'ADH-AES256-SHA' => '256 bits, High Encryption, Anonymous Auth',
  'DHE-RSA-AES256-SHA' => '256 bits, High Encryption',
  'DHE-DSS-AES256-SHA' => '256 bits, High Encryption',
  'AES256-SHA' => '256 bits, High Encryption',
  'ADH-DES-CBC3-SHA' => '168 bits, High Encryption, Anonymous Auth',
  'EDH-RSA-DES-CBC3-SHA' => '168 bits, High Encryption',
  'EDH-DSS-DES-CBC3-SHA' => '168 bits, High Encryption',
  'DES-CBC3-SHA' => '168 bits, High Encryption',
  'ADH-AES128-SHA' => '128 bits, High Encryption, Anonymous Auth',
  'DHE-RSA-AES128-SHA' => '128 bits, High Encryption',
  'DHE-DSS-AES128-SHA' => '128 bits, High Encryption',
  'AES128-SHA' => '128 bits, High Encryption',
  'RC4-SHA' => '128 bits, Medium Encryption',
  'RC4-MD5' => '128 bits, Medium Encryption',
  'ADH-RC4-MD5' => '128 bits, Medium Encryption, Anonymous Auth',
  'EDH-RSA-DES-CBC-SHA' => '56 bits, Low Encryption',
  'EDH-DSS-DES-CBC-SHA' => '56 bits, Low Encryption',
  'DES-CBC-SHA' => '56 bits, Low Encryption',
  'ADH-DES-CBC-SHA' => '56 bits, Low Encryption, Anonymous Auth',
  'EXP-ADH-DES-CBC-SHA' => '40 bits, Export-Grade Encryption',
  'EXP-ADH-RC4-MD5' => '40 bits, Export-Grade Encryption',
  'EXP-EDH-RSA-DES-CBC-SHA' => '40 bits, Export-Grade Encryption',
  'EXP-EDH-DSS-DES-CBC-SHA' => '40 bits, Export-Grade Encryption',
  'EXP-DES-CBC-SHA' => '40 bits, Export-Grade Encryption',
  'EXP-RC2-CBC-MD5' => '40 bits, Export-Grade Encryption',
  'EXP-RC4-MD5' => '40 bits, Export-Grade Encryption',
  'NULL-SHA' => 'Null cipher, No Encryption',
  'NULL-MD5' => 'Null cipher, No Encryption'
);

# Populate a hash of unsafe URLs
# i.e. URLs you would not want exposed
# to the Internet

tie my %bad_urls, 'Tie::Hash::Indexed';

%bad_urls = (
  'Apache mod_status page' => 'server-status',
  'Apache mod_info page' => 'server-info',
  'Apache mod_jk status page' => 'jk-status',
  'Apache mod_proxy_balancer' => 'balancer-manager',
  'ColdFusion Administrator' => 'CFIDE/administrator/index.cfm',
  'IIS Samples' => 'IISsamples',
  'IIS Scripts' => 'Scripts',
  'IIS MSADC Directory' => 'MSADC',
  'IIS Help' => 'IISHelp',
  'IIS Admin' => 'IISAdmin',
  'Tomcat Manager' => 'manager/html',
  'Tomcat Status Page' => 'manager/status',
  'Tomcat JSP Examples' => 'jsp-examples/index.html',
  'Tomcat Servlet Examples' => 'servlets-examples/index.html',
  'JBoss JMX Console' => 'jmx-console',
  'JBoss Tomcat Status Page' => 'status',
  'JBoss Web Console' => 'web-console',
  'JBoss 5.x Admin Console' => 'admin-console',
  );


sub get_server_type{
  my $url = "$scheme://$host/";
  my $ua = LWP::UserAgent->new;
    $ua->timeout(10);
    $ua->env_proxy;
    $ua->agent( $useragent );
      my $header = $ua->head( $url );
      print "\nWeb Server Type: " . $header->server . "\n\n";
      our $server_type = $header->server;
  }

sub test_for_trace {
    my $url = "$scheme://$host/";

    my $method = "TRACE";
    my $ua     = LWP::UserAgent->new;
    $ua->timeout(10);
    $ua->env_proxy;
    $ua->agent( $useragent );
    
    my $request = HTTP::Request->new( $method => $url );
    $request->header(Header0 => "TRACE");
    $request->header(Header1 => "Test");

    my $response = $ua->request($request);

    given ( $response->code ) {
        when (200) {
            say "======this is what you sent======";
            say $response->content;
            say "=================================";
            say $method, " is enabled and working.";
        }
        when (301) {
            say "Redirect present.  Retry request against ",
              $response->header('Location');
        }
        when (302) {
            say "Redirect present.  Retry request against ",
              $response->header('Location');
        }
        when (307) {
            say "Redirect present.  Retry request against ",
              $response->header('Location');
        }
        when (403) {
            say $response->status_line;
            say $method, " is forbidden.";
        }
        when (404) {
            say $response->status_line;
            say "This is an unexpected response";
        }
        when (405) {
            say $response->status_line;
            say $method, " is not permitted.";
        }
        when (501) {
            say $response->status_line;
            say $method, " is not implemented.";
        }
        default {
            say $response->status_line;
        }
    }
}

sub test_for_track {
    my $test = 'This is an HTTP TRACE test.';
    my $url = "$scheme://$host/";

    my $method = "TRACK";
    my $ua     = LWP::UserAgent->new;
    $ua->timeout(10);
    $ua->env_proxy;
    $ua->agent( $useragent );
    
    my $request = HTTP::Request->new( $method => $url );
    $request->header(Header0 => "TRACK");
    $request->header(Header1 => "Test");

    my $response = $ua->request($request);

    given ( $response->code ) {
        when (200) {
            say "======this is what you sent======";
            say $response->content;
            say "=================================";
            say $method, " is enabled and working.";
        }
        when (301) {
            say "Redirect present.  Retry request against ",
              $response->header('Location');
        }
        when (302) {
            say "Redirect present.  Retry request against ",
              $response->header('Location');
        }
        when (307) {
            say "Redirect present.  Retry request against ",
              $response->header('Location');
        }
        when (403) {
            say $response->status_line;
            say $method, " is forbidden.";
        }
        when (404) {
            say $response->status_line;
            say "This is an unexpected response";
        }
        when (405) {
            say $response->status_line;
            say $method, " is not permitted.";
        }
        when (501) {
            say $response->status_line;
            say $method, " is not implemented.";
        }
        default {
            say $response->status_line;
        }
    }
}

sub scan_for_unsafe_urls{
  print "\n";
  say "Scanning for 'Unsafe' URLs...";
  say "NOTE: This isn't perfect yet.\nSuccesses could be false positives\nMany are caused by redirection";
  sleep 2;
  my $response;
  my $request;
  
  while (($key,$value) = each(%bad_urls)) {
    my $url = "$scheme://$host/$value";
    my $ua = LWP::UserAgent->new;
      $ua->timeout(10);
      $ua->env_proxy;
      $ua->agent( $useragent );
      $ua->max_redirect(0);

    my $request = HTTP::Request->new( GET => $url );
    my $response = $ua->request($request);
  
  if ($response->code == '200') {
    print RED, "     " . $key . " -- /". $value . " url found" . "\n", RESET;
    }
    else {
      print GREEN, "     " . $key . " -- /" . $value . " url not found." . "\n", RESET;
      }
  }
}

sub is_weak{
  if ($key =~ /^EXP-|^NULL|^ADH-|DES-CBC-/) {
    print RED, "    " . $key . " -- " . $value . "\n", RESET;
  }
  else {
    print GREEN, "    " . $key . " -- " . $value . "\n", RESET;
  }
}

# Basic certificate checking
sub cert_info{
print "\nSSL Certificate Information...\n";
sleep 2;

my $certclient = IO::Socket::SSL->new(
  PeerHost => "$host:$port",
  SSL_ca_file => Mozilla::CA::SSL_ca_file(),
  SSL_verify_mode => $verifymode,
  SSL_version => 'TLSv1',
  SSL_cipher_list => 'RC4-SHA',
  Proto => 'tcp',
  Timeout => '5',
  ) 
    || die("Certificate Peer Verification Failed\nCertificate not trusted (but could be due to server misconfiguration)\nRun with the --insecure switch if you wish to test with no certificate verification\n\n");

  $certclient->verify_hostname($host, "http")
    || die("Hostname Verification Failed.\n$host does not match certificate's common name.\n\n");

  my $cn = $certclient->peer_certificate("cn");
  say "Certificate with Commmon Name " . $cn . " appears to be valid.\n";
}

sub test_ssl2_ciphers{

print "\nTesting SSLv2 Ciphers...\n";
sleep 2;
while (($key,$value) = each(%ssl2_ciphers)) {
  my $ssl2client = IO::Socket::SSL->new(
    SSL_verify_mode => 0x00,  
    SSL_version => 'SSLv2',
    SSL_cipher_list => $key,
    PeerAddr => $host,
    PeerPort => $port,
    Proto => 'tcp',
    Timeout => '5'
    )
  && is_weak();
  }
}

sub test_ssl3_ciphers{
print "\nTesting SSLv3 Ciphers...\n";
sleep 2;
while (($key,$value) = each(%tls1_ciphers)) {
  my $ssl3client = IO::Socket::SSL->new(
    SSL_verify_mode => 0x00,
    SSL_version => 'SSLv3',
    SSL_cipher_list => $key,
    PeerAddr => $host,
    PeerPort => $port,
    Proto => 'tcp',
    Timeout => '5'
    )
  && is_weak();
  }
}


sub test_tls_ciphers{
print "\nTesting TLSv1 Ciphers...\n";
sleep 2;
while (($key,$value) = each(%tls1_ciphers)) {
  my $tls1client = IO::Socket::SSL->new(
    SSL_verify_mode => 0x00,
    SSL_version => 'TLSv1',
    SSL_cipher_list => $key,
    PeerAddr => $host,
    PeerPort => $port,
    Proto => 'tcp',
    Timeout => '5'
    )
  && is_weak(); 
  }
}

sub scan_for_propfind{
if ( $port == 443 ) {
    $scheme = 'https';
}
else {
    $scheme = 'http';
}

my $message;

# Will set content-length to zero
my $content_length = 0;
# Will set an empty Host header
my $host_header = "";

my $objHeader = HTTP::Headers->new;
  $objHeader->header('Host' => $host_header);
  $objHeader->header('Content_Length' => $content_length);

my $url = "$scheme://$host/";

    my $method = "PROPFIND";
    my $ua  = LWP::UserAgent->new;
    $ua->timeout(10);
    $ua->env_proxy;
    $ua->agent('propfind-pci-auditor/v0.1');
    
    my $request = HTTP::Request->new( "PROPFIND", $url, $objHeader, $message );

    my $response = $ua->request($request);

    given ($response->code ) {
        when (207) {
            my $parser = XML::LibXML->new();
            my $doc = $parser->load_xml( string => $response->content );
            say RED, "Response Status Code: " . $response->status_line;
            say "Internal Web Server Address: " . $doc->findvalue('//a:href') . "\nSite Appears to be Vulnerable to \'WebDAV HTTP Method PROPFIND Enabled\'\n  information disclosure issue.", RESET;
        }
        when (301) {
            say "Redirect Present.  Potential False Positive";
            say "Retry request against: " . $response->header('Location');
        }
        when (302) {
            say "Redirect Present.  Potential False Positive";
            say "Retry request against: " . $response->header('Location');
        }
        when (307) {
            say "Redirect Present.  Potential False Positive";
            say "Retry request against: " . $response->header('Location');
        }
        when (403) {
            say $response->status_line;
            say "This is not an expected response code."
            }
        when (404) {
            say $response->status_line;
            say "This is not an expected response code."
            }
        when (405) {
            say $response->status_line;
            say "HTTP Method PROPFIND is not permitted.";
            say "This is good!";
            }
        when (501) {
            say $response->status_line;
            say "HTTP Method PROPFIND is not implemented.";
            say "This is good!";
            }
        default {
            say $response->status_line;
            }
        }
    }


if ($xl eq 1 ) {
  # Run through the subroutines
get_server_type();

say "Testing for HTTP TRACE...";
sleep 2;
test_for_trace();

say "\nTesting for HTTP TRACK...";
sleep 2;
test_for_track();

say "\nScanning for well-known, unsafe URLs...";
sleep 2;
scan_for_unsafe_urls();

if ($main::server_type =~ /^Microsoft-IIS/) {
  say "Scanning for HTTP PropFind Vulnerability...";
  sleep 2;
  scan_for_propfind();
}


if ($port eq 443) {
 cert_info();
 test_ssl2_ciphers();
 test_ssl3_ciphers();
 test_tls_ciphers();
}

  }
  else {
   cert_info();
   test_ssl2_ciphers();
   test_ssl3_ciphers();
   test_tls_ciphers();
}
__END__

=head1 TITLE

CryptoNark (aka cnark.pl)

=head1 VERSION

Version 0.4.1

=head1 DATE

March 28, 2011

=head1 AUTHOR

Chris Mahns  Contact me at: techstacks [at] gmail [dot] com.

=head1 ATTRIBUTION

CryptoNark was based on sslthing.sh by blh [at] blh [dot] se

=head1 DESCRIPTION

CryptoNark (aka 'cnark.pl') is a PCI Compliance remedition verification script.  Primarily, it's purpose was to verify whether sslv2, null, weak, and anonymous ciphers had been successfully disabled on a web site after a web site administrator had implemented the necessary mitigation.  In addition, passing the -xl switch enables some additional checks like testing to see whether HTTP TRACE or TRACK is enabled or testing if certain known "unsafe" URLs are available over the INternet.

=head1 USAGE

./cnark.pl -h|--host  -p|--port  [-xl|--kitchen-sink] [-i|--insecure]

=head1 DEPENDENCIES

CryptoNark runs in both perl 5.10 and perl 5.12.  It makes use of perl functionality that was first introduced in Perl 5.10.  CyrptoNark also relies upon many CPAN modules: Modern::Perl, Term::ANSIColor, Tie::Hash::Indexed, IO::Socket::SSL, Mozilla::CA, LWP::UserAgent, HTTP::Headers, HTTP::Request, and Getopt::Long.  CryptoNark most likely will not work and has not been tested under perl 5.8.X or without the required modules.


=head1 VERSION HISTORY

=head2 VERSION 0.1

Almost a direct port, this version also tests null and anonymous ssl ciphers and reports accordingly.  A little more information is provided in the output. Works best if used to validate PCI-DSS compliance--to check that null, anonymous and weak ciphers are disabled.  

It probably will not run right "out of the box"--it requires IO::Socket::SSL.  Tie::Hash::Indexed, although not strictly required is nice to have in order to order the hash lists from strongest to weakest. Otherwise, the order could be random making the results a bit harder to read.

=head2 VERSION 0.2

+ Added Color Coded output. Good ciphers are green, bad ones are red.
 
=head2 VERSION 0.2.1

- Removed the SSLv3 Tests.  (Actually, just commented them out for now.)  SSLv3 and TLSv1 utilize the same ciphers so the tests are redundant.

+ Added a message after the display of cipher levels providing links to my site so that you can get information on disabling ciphers

=head2 VERSION 0.2.5

+ Added a HEAD request to display web server type.  Uses some addition modules:  LWP::UserAgent and HTTP::Headers

=head2 VERSION 0.3

+ "Upgraded" cnark to use perl 5.10 features.  Perl 5.10 is now required.

+ Modified script so that it uses command line options using Core Module Getopt::Long.  --host and --port should now be used.

- Removed exception catch on check_server_type() function. I don't think HEAD requests should ever fail but some sites restrict that method too for some reason.

+ cnark now tests for existence of HTTP Methods: TRACE and TRACK.

+ cnark will skip ssl scans if a non ssl port is used.

=head2 VERSION 0.3.1

- Code Clean up
     
+ Modern::Perl is now required

=head2 VERSION 0.3.5

+ Now scans for commonly used "unsafe" URLs. More to come.

=head2 VERSION 0.3.6
     
- Moved some stuff around.

+ Added SSLv3 tests back in.  This functionality was useful to some folks who were looking for output from specific protocols.

+ Added ColdFusion Administrator to unsafe URL check
     
+ Added Tomcat Status URL to unsafe URL Check

+ Tweaked the output for the unsafe URL Check to reduce some redundancy and add a false positive disclaimer. I Should be able to get rid of the disclaimer when better false positive checks are added

+ Added some basic certificate parsing

=head2 VERSION 0.4

+ Added option -xl|--kitchen-sink  By default, cnark.pl now performs ssl-only tests.  The -xl|kitchen-sink will put cryptonark in full betrayal mode.

+ Added Certificate Peer Verification (enabled by default) This causes cryptonark to fail on expired certs, missing root certificates, self-signed certs, and (I hope) invalid certificate chains. Certificate Validation is due to the inclusion of the ca-bundle.crt CA certificate bundle from mod_ssl.

+ Added --insecure option to disable peer verification if you so desire.

- Removed shameless plug

=head2 VERSION 0.4.1

+ There really is a CPAN module for everything!  Added Mozilla::CA, which enables me to utilize Certificate Peer Verification without having to re-distribute and maintain a cacerts file.

+ Added Hostname Validation.  CryptoNark will now fail if you attempt to connect to a site where the hostname does not match the $host argument.  For example, 

+ Doc Cleanup.  embedded pod.  You can now run 'perldoc cnark.pl' for an embedded man page

- Removed code for shameless plug subroutine.

=head2 VERSION 0.4.5

+ Added HTTP PropFind Test, which is executed if the -xl option is specified.

+ Add supporting module:  XML::LibXML

+ Disabled rediretion onthe unsafe URL checks.  This was creating some false positives.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a01156fbc6fe6970c014e88366851970d

Listed below are links to weblogs that reference CryptoNark v0.4.5 Released:

Comments