From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

#!perl
=head1 NAME
mojo_cape_submit - A mojolicious script for handling submissions of files for detonation.
=head1 SYNOPSIS
sudo -u cape mojo_cape_submit daemon -m production -l 'http://*:8080'
=head1 DESCRIPTION
This script is meant for running locally on a CAPEv2. It allows remote machines to
to submit files for detonation.
To work, this script needs to be running as the same user as CAPEv2.
This will write activity to syslog.
A systemd service file is provided at 'systemd/mojo_cape_submit.service' in this modules
tarball. It expects the enviromental '/usr/local/etc/mojo_cape_submit.env' file to be setup
with the variables 'CAPE_USER' and 'LISTEN_ON'. To lets say you want to listen on
http://192.168.14.15:8080 with a user of cape, it would be like below.
CAPE_USER="cape"
Alternatively, this script can be invoked as a CGI script if it is ran as the user CAPEv2 is.
=head1 CONFIGURATION
If cape_utils has been configured and is working, this just requires two more additional
bits configured.
The first is the setting 'incoming'. This setting is a directory in which incoming files are
placed for submission. By default this is '/malware/client-incoming'.
The second is 'incoming_json'. This is a directory the data files for submitted files are written to.
The name of the file is the task ID with '.json' appended. So task ID '123' would become '123.json'. The
default directory for this is '/malware/incoming-json'.
=head1 SECURITY
By default this will auth of the remote IP via the setting 'subnets', which by default is
'192.168.0.0/16,127.0.0.1/8,::1/128,172.16.0.0/12,10.0.0.0/8'. This value is a comma seperated
string of subnets to accept submissions from.
To enable the use of a API key, it requires setting the value of 'apikey' and setting
'auth_by_IP_only' to '0'.
=head1 SUBMISSION
Submissions must be made using the post method.
=head2 Submission Parameters
Required ones are as below.
- filename :: The file being submitted.
The following are optional and more or less "free form",
but helps to set them to something sane and relevant.
- type :: The type of submission. Generally going
to be 'manual' or 'suricata_extract'.
=head1 PINGING
If you submit a file with a file with the size of 10 and matching
/01234567890/ it will return "TEST RECIEVED\n", provided the submitter is authed.
At that point it will just stop processing of it.
=cut
use Mojolicious::Lite -signatures;
use JSON;
# two are needed as /* and / don't overlap... so pass it to the_stuff to actually handle it
post '/*' => sub ($c) {
the_stuff($c);
};
post '/' => sub ($c) {
the_stuff($c);
};
# sends stuff to syslog
sub log_drek {
my ( $level, $message ) = @_;
if ( !defined($level) ) {
$level = 'info';
}
openlog( 'mojo_cape_submit', 'cons,pid', 'daemon' );
syslog( $level, '%s', $message );
closelog();
} ## end sub log_drek
# handle it
sub the_stuff {
my $c = $_[0];
my $remote_ip = $c->{tx}{original_remote_address};
my $apikey = $c->param('apikey');
# log the connection
my $message = 'Started. Remote IP: ' . $remote_ip . ' API key: ';
if ( defined($apikey) ) {
$message = $message . $apikey;
} else {
$message = $message . 'undef';
}
log_drek( 'info', $message );
my $cape_util;
eval { $cape_util = CAPE::Utils->new(); };
if ($@) {
log_drek( 'err', $@ );
$c->render( text => "Error... please see syslog\n", status => 400, );
return;
}
if ( !-d $cape_util->{config}->{_}->{incoming} ) {
log_drek( 'err', 'incoming directory, "' . $cape_util->{config}->{_}->{incoming} . '", does not exist' );
$c->render( text => "Error... please see syslog\n", status => 400, );
return;
}
my $allow_remote;
eval { $allow_remote = $cape_util->check_remote( apikey => $apikey, ip => $remote_ip ); };
if ($@) {
log_drek( 'err', $@ );
$c->render( text => "Error... please see syslog\n", status => 400, );
return;
}
if ( !$allow_remote ) {
log_drek( 'info', 'API key or IP not allowed' );
$c->render( text => "IP not allowed or invalid API key\n", status => 403, );
return;
}
if ( $c->req->is_limit_exceeded ) {
log_drek( 'err', 'Log size exceeded' );
$c->render( text => 'File is too big.', status => 400 );
return;
}
my $file = $c->param('filename');
if ( !$file ) {
log_drek( 'err', 'No file specified' );
$c->render( text => 'No file specified', status => 400 );
}
#get file info and log it
my $name = $file->filename;
my $size = $file->size;
# if size is 10, test if the contents are a test ping packet
if ( $size == 10 ) {
my $file_data = $file->slurp;
if ( $file_data =~ /1234567890/ ) {
log_drek( 'info', 'got ping test, size=10 payload=01234567890' );
$c->render( text => "TEST RECIEVED\n", status => 200, );
return;
}
}
# clean up the filename and log it
my $orig_name = $name;
$name =~ s/\///;
log_drek( 'info', 'Got File... size=' . $size . ' filename="' . $name . '"' );
$name = $cape_util->{config}->{_}->{incoming} . '/' . $name;
# get the json metadata info
my $raw_json = $c->param('json');
my $json;
eval { $json = decode_json($raw_json); };
if ($@) {
log_drek( 'err', 'json param decode error: ' . $@ );
$json = {};
}
# add initial relevant submission data
$json->{cape_submit} = {
orig_name => $orig_name,
name => $name,
apikey => $apikey,
remote_ip => $remote_ip,
size => $size,
time => time,
host => hostname,
};
# get some info for logging purposes
# done this way for the purpose of not having to constantly check if something is undef
my %additional_info;
$additional_info{src_ip} = $c->param('src_ip');
$additional_info{src_port} = $c->param('src_port');
$additional_info{dest_ip} = $c->param('dest_ip');
$additional_info{dest_port} = $c->param('dest_port');
$additional_info{proto} = $c->param('proto');
$additional_info{app_proto} = $c->param('app_proto');
$additional_info{flow_id} = $c->param('flow_id');
$additional_info{http_host} = $c->param('http_host');
$additional_info{http_url} = $c->param('http_url');
$additional_info{http_method} = $c->param('http_method');
$additional_info{http_proto} = $c->param('http_proto');
$additional_info{http_status} = $c->param('http_status');
$additional_info{http_ctype} = $c->param('http_ctype');
$additional_info{http_ua} = $c->param('http_ua');
$additional_info{det_sub_type} = $c->param('type');
$additional_info{src_host} = $c->param('src_host');
# set the value for anything not defined to undef for the purpose of logging
# this will avoid perl from throwing errors about undef used in cating
foreach my $item ( keys(%additional_info) ) {
if ( !defined( $additional_info{$item} ) ) {
$additional_info{$item} = 'undef';
}
}
$json->{det_sub_type} = $additional_info{det_sub_type};
# log additional info
log_drek( 'info', 'Source Host: ' . $additional_info{src_host} );
log_drek( 'info', 'Submission Type: ' . $additional_info{det_sub_type} );
log_drek( 'info',
'proto='
. $additional_info{proto}
. ' src_ip='
. $additional_info{src_ip}
. ' src_port='
. $additional_info{src_port}
. ' dest_ip='
. $additional_info{dest_ip}
. ' dest_port='
. $additional_info{dest_port}
. ' flow_id='
. $additional_info{flow_id} );
if ( $additional_info{app_proto} eq 'http' ) {
log_drek( 'info', $additional_info{http_proto} . ' ' . $additional_info{http_host} );
log_drek( 'info',
$additional_info{http_method} . ' '
. $additional_info{http_status} . ' '
. $additional_info{http_url} );
log_drek( 'info', 'useragent: ' . $additional_info{http_ua} );
} else {
log_drek( 'info', 'App Proto: ' . $additional_info{app_proto} );
}
# copy it into place
$file->move_to($name);
$json->{cape_submit}{sha256} = lc( `sha256sum $name` );
chomp($json->{cape_submit}{sha256});
$json->{cape_submit}{sha256} =~ s/[\ \t].+//;
log_drek( 'info', 'SHA256: ' . $json->{cape_submit}{sha256} );
$json->{cape_submit}{sha1} = lc( `sha1sum $name` );
chomp($json->{cape_submit}{sha1});
$json->{cape_submit}{sha1} =~ s/[\ \t].+//;
log_drek( 'info', 'SHA1: ' . $json->{cape_submit}{sha1} );
$json->{cape_submit}{md5} = lc( `md5sum $name` );
chomp($json->{cape_submit}{md5});
$json->{cape_submit}{md5} =~ s/[\ \t].+//;
log_drek( 'info', 'MD5: ' . $json->{cape_submit}{md5} );
# finally submit it
my $results;
eval { $results = $cape_util->submit( items => [$name], quiet => 1, ); };
if ($@) {
log_drek( 'err', $@ );
$c->render( text => "Error... please see syslog\n", status => 400, );
return;
}
# can't continue if submission failed
my @submitted = keys( %{$results} );
if ( !defined( $submitted[0] ) ) {
log_drek( 'err', 'Submitting "' . $name . '" failed' );
$c->render( text => "Submission failed\n", status => 400, );
return;
}
# log the submission
log_drek( 'info', 'Submitting "' . $name . '" submitted as ' . $results->{ $submitted[0] } );
$c->render( text => "Submitted as task ID " . $results->{ $submitted[0] } . "\n", status => 200, );
$json->{cape_submit}{task} = $results->{ $submitted[0] };
# can't continue if this dir does not exist
if ( !-d $cape_util->{config}->{_}->{incoming_json} ) {
log_drek( 'err',
'incoming_json directory, "' . $cape_util->{config}->{_}->{incoming_json} . '", does not exist' );
return;
}
# write out the json containing the submission info
my $data_json = encode_json($json) . "\n";
my $data_json_file = $cape_util->{config}->{_}->{incoming_json} . '/' . $results->{ $submitted[0] } . '.json';
eval { write_file( $data_json_file, $data_json ); };
if ($@) {
log_drek( 'err', 'Failed to write submission data JSON out to "' . $data_json_file . '"... ' . $@ );
} else {
log_drek( 'info', 'Wrote submission data JSON out to "' . $data_json_file . '"' );
}
return;
} ## end sub the_stuff
app->start;