#! /usr/bin/perl # Universal script for MJohn # 1) Wrapper part # By default it works like wrapper around John the Ripper # Sends attack descriptions to the server # 2) Daemon part # Daemon to report progress # Collects and sends attack progress to the server # Daemon should run to upload results. Start it and do not stop. # 3) Server daemon part # Server daemon to import data into request tracker # !!! Experimental: this all is subject to change very soon !!! # Dependencies: git, ssh, john, request tracker # TODO: versions. # Server daemon could work without ssh. # Limitations: only full names of parameters are supported, order of parameters # should not affect behaviour, and many others not found yet. TODO # Configuration # TODO: additional config in separate file. my %C = qw( conf ~/.john/mjohn.conf store ~/.john/mjohn-store user aleksey john ~/desktop/wrapper/echojohn.sh john_conf ~/desktop/magnum-jumbo/run/john.conf daemon_delay 30 lair no_lair transparent_wrapper 0 role wrapper action start auto_start_daemon 0 remote git+ssh://... attack_name_generator q/name/.time() show nothing ); # TODO: enable auto_start_daemon. Disabled for debugging. # TODO: role seems to be an abuse for config. Though it is nice to be # able to set default behaviour in any value. For instance if have # copies of this script with different names. Though it maybe useful # to see argv[0] to determine name and adjust behaviour accordingly # (just like busybox). use strict; use warnings; use File::Copy; use YAML::XS; use Getopt::Long qw(:config pass_through); # TODO: other better? Windows support? Does this one lock pidfile? use Proc::Daemon; use Config::Simple; ###################################################################### # Functions sub user_daemon { # TODO: option to not detach. Proc::Daemon->new( work_dir => $C{store}, child_STDOUT => '>>daemon.out', child_STDERR => '>>daemon.err', pid_file => 'daemon.pid' ); } sub start_user_daemon { my $d = user_daemon(); # TODO: should not we check that daemon is already run? # TODO: nice messages. my $p = $d->Init; unless ($p) { # We traverse our folders in the store. while (1) { # TODO: should not we pull here? for (<$C{user}_*/>) { chdir $_; # Capture/calculate speed # TODO: is it all/enough? # TODO: should not we parse --status output right here? # TODO: dirty. Use other way to redirect output. # TODO: use john used for command, not just from config. system qq/"$C{john}" --status="$C{user}" 2> "$C{user}.status"/; # TODO: look into logs. # TODO: see diff on .pot file. # TODO: capture speed, eta, what else? # Commit progress info: .status, .pot, .rec, .log # TODO: real quoting. # TODO: are all that file in all our dirs? Think about it. # TODO: failed commands does not have pot/rec/log files. my $files = ' -- "' . join('" "', map { "$C{user}.$_" } qw/status pot rec log/) . '"'; warn "adding $files"; `git add $files`; `git commit -m auto2 -o $files`; # Some lyrics: here we go back to the store, in most cases we # could just "cd .." but if we previously went through symlink # then we could not go back through "..", though it seems that # there should not be any symlinks in the store. chdir $C{store}; } # TODO: it could be worth to send smaller packs. `git push origin master:master`; # TODO: probably we should sleep delay minus time spent on work. sleep $C{daemon_delay}; } } } sub stop_user_daemon { my $d = user_daemon(); # TODO: send TERM for the first time. $d->Kill_Daemon; # TODO: some informative output? } sub server_daemon { # TODO: option to not detach. Proc::Daemon->new( work_dir => $C{store}, child_STDOUT => '>>daemon.out', child_STDERR => '>>daemon.err', pid_file => 'daemon.pid' ); } sub start_server_daemon { my $d = server_daemon(); my $p = $d->Init; unless ($p) { # TODO: This daemon shares architecture with client daemon. # We traverse our folders in the lair. while (1) { `git pull`; for (<*/>) { chdir $_; # We add new attack. my $attack_name = $_; # Check if we already added that attack. # TODO: remember to export vars with credentials to access rt. # TODO: inefficient. We could get list of all once. Or we # could store status in file near, though it could become not # synced with rt. # TODO: 'No matches found' could be in field. Add ^ and $ and # test. Is there a machine readable interface? # TODO: rt ls 'subject=user1_name1341851801/' # -> Query:subject='user1_name1341851801'/ . "/" is in wrong place. if (`rt ls "subject='$attack_name'"` =~ 'No matches found') { # No such attack. Add it. # TODO: cats are nice pets. Do not use them so. # TODO: text is not for parameters. Use custom fields instead! # TODO: should not I add a ticket from user which did this attack? `rt create -t ticket set 'subject=$attack_name' "text=\$(cat parameters)"`; } # TODO: We update speeds. chdir $C{lair}; } # TODO: probably we should sleep during delay minus time spent on work. sleep $C{daemon_delay}; } } } sub stop_server_daemon { my $d = server_daemon(); # TODO: send TERM for the first time. $d->Kill_Daemon; # TODO: some informative output? } sub expand_tilde { # TODO: windows portability. # TODO: logdir? $_[0] =~ s!^~([^/]*)!$1 ? (getpwnam($1))[7] : $ENV{HOME} || $ENV{LOGDIR}!e; } # Functions end ###################################################################### # Options handling # TODO: Usage info, in pod, me thinks. if ($#ARGV == -1) { if ($C{transparent_wrapper}) { system $C{john}; } else { # NOTE: transparent_wrapper is not very user friendly because # it means wrapper behaves exactly like john if there are no # specific arguments so user could be confused. # TODO: show usage info. } } # Parse wrapper's only options # Defaults + Config file + Options = Resulting configuration # Get path to config GetOptions("config-conf=s", \$C{conf}); # Read config expand_tilde($C{conf}); Config::Simple->import_from($C{conf}, \%C) or warn Config::Simple->error(); my %options; for (keys %C) { # TODO: smarter types for options? # TODO: maybe --config name=value is better? $options{"config-$_=s"} = \$C{$_}; } # TODO: config file option. --config is used by john. # Attack name from user # While attack_name_generator could provide name this option is a # convenient way to assign meaningful name to attack. # (attack_name_generator is passed to eval, so explicit name needs a # quoting while this option does not it). # TODO: usage info. my $user_supplied_attack_name = ''; $options{"aname|attack_name=s"} = \$user_supplied_attack_name; # TODO: we could take some john's options if we use short form. Though # there are no such options now. GetOptions(%options); # Expand tildes in config. for (values %C) { expand_tilde $_; } # TODO: expand relative paths. # TODO: error on '--option-show='. if ($C{show} ne 'nothing') { print "$C{$C{show}}"; # TODO: error code on non-existence. Message on stderr. exit; } # Check config sanity # TODO: check that on server side (too). # TODO: more checks. # NOTE: attack name could not be checked here. unless ($C{user} =~ /^[a-z]+\z/) { die "use only small letters for user name"; } # TODO: set store up if there is not one. Or at least exit if so. # Create dir (with path) if there are no one. Clone remote repo there. # TODO: if there is a store check if its remote address equals to our. # TODO: check toplevel structure of the store. # Distinguish roles if ($C{role} ne 'wrapper' && @ARGV) { # Error: unexpected args # TODO: usage. } if ($C{role} eq 'wrapper') { # Other options is for john, remember them my @args = @ARGV; # TODO: Parse options for john # TODO: warning if autostart is disabled and daemon is not runned. start_user_daemon if $C{auto_start_daemon}; # Store parameters into file # Each wrapper has its own store. my $pwd = $ENV{PWD}; # TODO: set store up if there is not one. Or at least exit if so. # Create dir (with path) if there are no one. chdir $C{store} or die "cannot cd to the store: $!, stopped"; # Sync store with server. # TODO: conflicts? They should be avoided here, solved somewhere else. # TODO: this fails on very new empty repo. Not important though. # "Your configuration specifies to merge with the ref 'master' from # the remote, but no such ref was fetched." There is no master in # empty bare repo. `git pull`; # Each attack has its own dir in store. # Name of attack # TODO: name from cmdline. # TODO: easier name, maybe with incremental counter. # TODO: this does not reflect real user+attack_name. # TODO: use subdir, i.e. / instead of _ . my $attack_name = "$C{user}_"; if ($user_supplied_attack_name ne '') { $attack_name .= $user_supplied_attack_name } else { $attack_name .= eval($C{attack_name_generator}); } # TODO: check that on server side. unless ($attack_name =~ /^[a-z0-9_-]+\z/) { die "bad attack name, make it non-empty and use only a..z, 0..9, _, -, stopped" } # TODO: check if this attack already exists. mkdir "$attack_name" or die "could not mkdir for attack, try another name, stopped"; # Check that all files are available and copy them into the store. my %original_names; # Gets path to file, returns path in store. sub add_file_to_store { # TODO: is it correct to use slashes everywhere? my $full_path = shift; my $new_path = ""; if (-r $full_path) { if (`sha1sum -b "$full_path"` =~ /^([0-9a-f]{40}) /) { my $sha1sum = $1; # TODO: separate files by type: wordlists, configs, and so on. $new_path = "$C{store}/files.d/$sha1sum"; if (-r $new_path) { # TODO: add byte to byte comparison here if you're paranoid. warn "file '$full_path' ($sha1sum) is already in the store"; } else { warn "file '$full_path' ($sha1sum) is about to be copied to the store"; # Copy file to store # TODO: do files in parameters for wrapper need copies? # TODO: fifos? ('copy' converts fifo into regular file) # TODO: what if our file already in the store? # TODO: use hardlinks if possible. copy $full_path, $new_path or die "$!"; $original_names{$new_path} = $full_path; } # TODO: store symlink-like file into attack's subdir. } } else { # TODO: debug output, make an option to control this. # TODO: distinguish readability and existence. warn "file '$full_path' does not exist"; } $new_path } for (@args) { # We use only one parameter-value separator (=). # John accepts only = and : as separater for parameters. # Also we use only "--". s/^--?/--/; s/^(--.*?)[=:]/$1=/; if (/^(--.*?=)(.*)/) { # TODO: handle config file too. # TODO: respect type of parameter! Do not copy file named 'all' # from original dir if we just call --incremental=all . # TODO: --make-charset does not need a copy. # Add file to the store and overwrite args with path in the store. # TODO: is it correct to use slashes everywhere? my $t = $2; $_ = $1 . (add_file_to_store("$pwd/$2") || $t); } # TODO: we could need a copy of file with hashes to keep track # what hashes were attacked and what were not. Remember about # selectors like --users. } my $john_conf = ''; if (-r $C{john_conf}) { # TODO: includes. # TODO: it could be useful to split config file into smaller # files to keep meaningful and common parts separated. $john_conf = add_file_to_store $C{john_conf} } else { die "config is not readable, use --config-john_conf to specify config, stopped"; } # Dump john's version and usage # TODO: john's version could be changed between runs. Lock? Copy? # TODO: single quotes do not work on windows. my $usage = "$attack_name/john.usage"; system "$C{john} > $usage" and die "$!"; # NOTE: we do note die on error because some johns may not support --list option. system "$C{john} --list=hidden-options >> $usage" and warn "$!"; # Short test run # TODO: when we start attack by name we should run tests too. # TODO: we abort on failed commands while it could be useful to # share them to provide newbies with help. Now there is lesser noise. my $run_name = "$attack_name/$C{user}" . time(); # TODO: tilde expansion looks like in the place here. expand_tilde $_ for @args; my @possible_hashfiles = grep !/^-/, @args; die "too many files with hashes" if @possible_hashfiles > 1; sub run_john { system $C{john}, @_, "--session=$run_name", "--pot=$run_name.pot", "--config=$john_conf" and die "john failed"; } if (@possible_hashfiles) { my $hashfile = shift @possible_hashfiles; # TODO: reduce .pot to one hash for test. Dump line by line # till we load it. Then load the whole file if it is not # possible to load one line. open my $i, '<', $hashfile or die "$!"; # TODO: comment lines? my $l = <$i>; close $i; # TODO: temp file. my $f = "$attack_name/test" . time(); open my $o, '>', $f or die "$!"; print $o $l; close $o; my @test_args = map { $hashfile eq $_ ? $f : $_ } @args; eval { # NOTE: forced exit by time gives error code. # TODO: it is the same as for all other errors. Patch john? run_john @test_args, "--max-run-time=1"; }; # TODO: better error handling. my $e = $@; unlink $f or die "$! ($e)"; # TODO: should die here. Waits fix of exit code for --max-run-time. warn $e if $e; } else { # Run as is because there is no hashfile. # TODO: 0 may be enough. It seems that 0 means "while time < 1". run_john @args, "--max-run-time=1"; } # Expand short options. # TODO: retest after option expansion and sort? { # Build expansion table open my $f, '<', $usage; my %expansion; while (<$f>) { # TODO: not listed --make_check needs _ . if (/^--[a-z-]+/) { for (reverse 3 .. length $&) { my $s = substr $&, 0, $_; if (exists $expansion{$s}) { $expansion{$s} = ''; } else { $expansion{$s} = $&; } } } } close $f; for (@args) { if (/^--[a-z-]+/) { if (exists $expansion{$&}) { if ($expansion{$&} eq '') { # If option could be expanded then test run should # fail. If we are here then our code work not like # john. die "bug"; } s/^\Q$&\E/$expansion{$&}/; } else { warn "john did not show option '$&' in usage and hidden-options"; } } } } { my $attack_parameters = "$attack_name/parameters"; my $f; open $f, '>', $attack_parameters or die "could not open file $attack_parameters"; # TODO: do we need to dump wrapper's config? # TODO: what if some options are doubled? # TODO: drop decorative parameters. print $f Dump sort @args; close $f; } # TODO: Check for duplicated commands. # Start john. # Push changes to server. # TODO: Pushing to server could be async. # TODO: quoting or restrictions on user/attack names: double quotes # are not allowed for user and attack names, also backslashes are not # allowed. # NOTE: While we do not touch the same file simultaneously from # multiple place we do not have conflict. So while we do not have # collisions in names of attacks we have no problems. # NOTE: we limit commit to only our files because our other instances # or the daemon could add more files. (There is no atomic add+commit # action.) # TODO: remember that file names could start with dash. Check other places. my $files_to_commit = '-- "' . join('" "', $attack_name, keys %original_names) . '"'; `git add $files_to_commit`; # TODO: attack name in commit's message. `git commit -m auto1 -o $files_to_commit`; # TODO: action here is similar to what the daemon does. Call it. # NOTE: git prefers "bare" repositories for pushes. `git push origin master:master`; # TODO: after that point we should not die without cleanup. # TODO: if we changed file and die then next `git pull` probably fail. # Really run john. # TODO: do not count john without args or with help parameters as attack. # TODO: It would be nice to say what we are going to run. # TODO: pipes? # TODO: pipes and remote execution: does perl have a safe restricted mode? # TODO: it would be better to craft args list from saved parameters. # TODO: check that --session and --pot were not used before. # TODO: time is not a good unique number. Though uuid could be overkill. # TODO: add session and .pot files to git. The daemon could do it, though. # TODO: interactive status does not work. # TODO: track status: syntax ok, all hashes loaded, some hashes cracked. run_john @args; # John finished or aborted. # TODO: Report finish or abort } elsif ($C{role} eq 'daemon') { if ($C{action} eq 'start') { start_user_daemon; } elsif ($C{action} eq 'stop') { stop_user_daemon; } elsif ($C{action} eq 'status') { # TODO } else { # TODO: error: Unknown daemon action. } } elsif ($C{role} eq 'server-worker') { if ($C{action} eq 'start') { start_server_daemon; } elsif ($C{action} eq 'stop') { stop_server_daemon; } elsif ($C{action} eq 'status') { # TODO } else { # TODO: error: Unknown daemon action. } } else { # Error: unknown role # TODO: usage } # TODO: right shebang. # TODO: avoid backticks where possible. # TODO: pod with usage below. __END__