NAME

PasswordMonkey - Password prompt responder

SYNOPSIS

use PasswordMonkey;
use PasswordMonkey::Filler::Sudo;
use PasswordMonkey::Filler::Adduser;

my $sudo = PasswordMonkey::Filler::Sudo->new(
    password => "supersecrEt",
);

my $adduser = PasswordMonkey::Filler::Adduser->new(
    password => "logmein",
);

my $monkey = PasswordMonkey->new(
    timeout => 60,
);

$monkey->filler_add( $sudo );
$monkey->filler_add( $adduser );

  # Spawn a script that asks for 
  #  - the sudo password and then
  #  - the new password for 'adduser' twice
$monkey->spawn("sudo adduser testuser");

  # Password monkey goes to work
$monkey->go();

# ==== In action:
# [sudo] password for mschilli: 
# (waits two seconds)
# ******** (types 'supersecrEt\n')
# ...
# Copying files from `/etc/skel' ...
# Enter new UNIX password: 
# ******** (types 'logmein')
# Retype new UNIX password: 
# ******** (types 'logmein')

DESCRIPTION

PasswordMonkey is a plugin-driven approach to provide passwords to prompts, following strategies human users would employ as well. It comes with a set of Filler plugins who know how to deal with common applications expecting password input (sudo, ssh) and a set of Bouncer plugins who know how to employ different security strategies once a prompt has been detected. It can be easily extended to support additional applications.

That being said, let me remind you that USING PLAINTEXT PASSWORDS IN AUTOMATED SYSTEMS IS ALMOST ALWAYS A BAD IDEA. Use ssh keys, custom sudo rules, PAM modules, or other techniques instead. This Expect-based module uses plain text passwords and it's useful in a context with legacy applications, because it provides a slightly better and safer mechanism than simpler Expect-based scripts, but it is still worse than using passwordless technologies. You've been warned.

Methods

new()

Creates a new PasswordMonkey object. Imagine this as a trained monkey who knows to type a password when prompt shows up on a terminal.

Optionally, the constructor accepts a timeout value (defaults to 60 seconds), after which it will stop listening for passwords and terminate the go() call with a 'timed_out' message:

my $monkey = PasswordMonkey->new(
    timeout => 60,
);
filler_add( $filler )

Add a filler plugin to the monkey. A filler plugin is a module that defines which password to type on a given prompt: "If you see 'Password:', then type 'supersecrEt' with a newline". There are a number of sample plugins provided with the PasswordMonkey core distribution, namely PasswordMonkey::Filler::Sudo (respond to sudo prompts with a given password) and PasswordMonkey::Filler::Password (respond to adduser's password prompts to change a user's password.

But these are just examples, the real power of PasswordMonkey comes with writing your own custom filler plugins. The API is very simple, a new filler plugin is just a matter of 10 lines of code. Writing your own custom filler plugins allows you mix and match those plugins later and share them with other users on CPAN (think PasswordMonkey::Filler::MysqlClient or PasswordMonkey::Filler::SSH).

To create a filler plugin object, call its constructor:

my $sudo = PasswordMonkey::Filler::Sudo->new(
    password => "supersecrEt",
);

and then add it to the monkey:

$monkey->filler_add( $sudo );

and when you say

$monkey->spawn( "sudo ls" );
$monkey->go();

later, the monkey fill in the "supersecrEt" password every time the spawned program asks for something like

[sudo] password for joe:

As mentioned above, writing a filler plugin is easy, here is the entire PasswordMonkey::Filler::Sudo implementation:

package PasswordMonkey::Filler::Sudo;
use strict;
use warnings;
use base qw(PasswordMonkey::Filler);

sub prompt {
    my($self) = @_;

    return qr(\[sudo\] password for [\w_]+:);
}

1;

All that's required from the plugin is a prompt() method that returns a regular expression that matches the prompts the filler plugin is supposed to respond to. You don't need to deal with collecting the password, because it gets passed to the filler plugin constructor, which is taken care of by the base class PasswordMonkey::Filler. Note that PasswordMonkey::Filler::Sudo inherits from PasswordMonkey::Filler with the use base directive, as shown in the code snippet above.

spawn( $command )

Spawn an external command (e.g. "sudo ls") to whose password prompts the monkey will keep responding later.

go()

Starts the monkey, which will respond to password prompts according to the filler plugins that have been loaded, until it times out or the spawned program exits.

The $monkey->go() method call returns a true value upon success, so running

if( ! $monkey->go() ) {
    print "Something went wrong!\n";
}

will catch any errors.

is_success()

After go() has returned,

$monkey->is_success();

will return true if the spawned program exited with a success return code. Note that hitting a timeout or a bad exit status of the spawned process is considered an error. To check for these cases, use the exit_status() and timed_out() accessors.

exit_status()

After go() has returned, obtain the exit code of spawned process:

if( $monkey->exit_status() ) {
    print "The process exited with rc=", $monkey->exit_status(), "\n";
}

Note that exit_status() returns the Perl-specific return code of system(). If you need the shell-specific return code, you need to use exit_status() >> 8 instead (check 'perldoc -f system' for details).

timed_out()

After go() has returned, check if the monkey timed out or terminated because the spawned process exited:

if( $monkey->timed_out() ) {
    print "The monkey timed out!\n";
} else {
    print "The spawned process has exited!\n";
}
fills()

After go() has returned, get the number of password fills the monkey performed:

my $nof_fills = $monkey->fills();

Fillers

The following fillers come bundled with the PasswordMonkey distribution, but they're included only as fully functional study examples:

PasswordMonkey::Filler::Sudo

Sudo passwords

Running a command like

$ sudo ls
[sudo] password for mschilli: 
********

PasswordMonkey::Filler::Password

Responds to any "password:" prompts:

$ adduser wonko
Copying files from `/etc/skel' ...
Enter new UNIX password: 
********
Retype new UNIX password: 
********

Read on, and later you'll find an expanation on how to write your own custom fillers to talk to random programs asking for passwords.

Bouncer Plugins

You might be wondering: "What if I use a simple password filler responding to 'password:' prompts and the mysql client prints 'password: no' as part of its diagnostic output?"

With previous versions of PasswordMonkey you were in big trouble, because PasswordMonkey would then send the password to an unsilenced terminal, which echoed the password, which ended up on screen or in log files of automated processes. Big trouble! For this reason, PasswordMonkey 0.09 and up will silence the terminal the password gets sent to proactively as a precaution.

Bouncer plugins can configure a number of security checks to run after a prompt has been detected. These checks are also implemented as plugins, and are added to filler plugins via their bouncer_add method.

Verifying inactivity after password prompts: Bouncer::Wait

To make sure that we are actually dealing with a sudo password prompt in the form of

# [sudo] password for joeuser: 

and not just a fly-by text string matching the prompt regular expression, we add a Wait Bouncer object to it, which blocks the Sudo plugin's response until two seconds have passed without any other output, making sure that the application is actually waiting for input:

use PasswordMonkey;

my $sudo = PasswordMonkey::Filler::Sudo->new(
    password => "supersecrEt",
);

my $wait_two_secs =
    PasswordMonkey::Bouncer::Wait->new( seconds => 2 );

$sudo->bouncer_add( $wait_two_secs );

$monkey->filler_add( $sudo );

$monkey->spawn("sudo ls");

This will spawn sudo, detect if it's asking for the user's password by matching its output against a regular expression, and, upon a match, waits two seconds and proceeds only if there's no further output activity until then.

Hitting enter to see prompt reappear: Bouncer::Retry

To see if a password prompt is really genuine, PasswordMonkey hits enter and verifies the prompt reappears:

Password:
Password:

before it starts typing the password.

use PasswordMonkey;

my $sudo = PasswordMonkey::Filler::Sudo->new(
    password => "supersecrEt",
);

my $retry =
    PasswordMonkey::Bouncer::Retry->new( timeout => 2 );

$sudo->bouncer_add( $retry );

$monkey->filler_add( $sudo );

$monkey->spawn("sudo ls");

Filler API

Writing new filler plugins is easy, see the sudo plugin as an example:

package PasswordMonkey::Filler::Sudo;
use strict;
use warnings;
use base qw(PasswordMonkey::Filler);

sub prompt {
    return qr(^\[sudo\] password for [\w_]+:\s*$);
}

That's it. All that's required is that you

  • let your plugin inherit from the PasswordMonkey::Filler base class and

  • override the prompt method to return a regular expression for the p rompt upon which the plugin is supposed to send its password.

But you can write fancier plugins if you want.

Optionally, you can add an init() method in the filler plugin that the monkey will call during initialization time:

sub init {
    my($self) = @_;

    $self->{ my_secret_stash } = [];
    # ...
}

Through inheritance, the plugin will then make sure that if you create a new plugin object with a password setting like

my $sudo = PasswordMonkey::Filler::Sudo->new(
    password => "supersecret",
);

then inside the plugin, the password is available as $self-$<gtpassword()>. For example, if you don't like the default password sending routine (which comes courtesy of the base class PasswordMonkey::Filler), you could write your own:

sub fill {
    my($self, $exp, $monkey) = @_;

    $exp->send( $self->password(), "\n" );
}

What just happened? We overwrote fill method which the monkey calls in order to fill in the password on a prompt that the plugin said it was interested in earlier. Okay, we've got it covered now, here's the full filler plugin API:

init

(Optional).

prompt

(Required). Returns a regular expression matching password prompts the plugin is interested in.

fill

(Optional). Called by the monkey to have the plugin send over the password. Receives ($self, $exp, $monkey) as arguments, which are references to the plugin object itself, the Expect object and the PasswordMonkey object.

pre_fill

(Optional). Called by the monkey before the password fill. Receives ($self, $exp, $monkey) as arguments, which are references to the plugin object itself, the Expect object and the PasswordMonkey object.

post_fill

(Optional). Called by the monkey before the password fill. Receives ($self, $exp, $monkey) as arguments, which are references to the plugin object itself, the Expect object and the PasswordMonkey object.

Every filler plugin comes with three standard accessors which can also be used as constructor parameters:

name

the name of the plugin, defaults to the class name

password

get/set the password

dealbreakers

get/set so-called dealbreakers. If one of those regular expressions matches a pattern in the output of the controlled program, PasswordMonkey will abort its go loop and exit with the given exit code. For example, if you have

sub init {
    $self->dealbreakers([
        ["Bad passphrase, try again:" => 255],
    ]);
}

and the spawned program says "Bad passphrase, try again", then the monkey will stop immediately and report exit status 255. This is useful for quickly aborting programs that have no chance to continue, e.g. if one of the plugins has the wrong password, there's no point in trying over and over again until the timeout kicks in.

If you want your plugin's constructor to take parameters which you can later conventiently access in the plugin code via autogenerated accessors, use PasswordMonkey's make_accessor call:

package PasswordMonkey::Filler::Wonky;
use strict;
use warnings;
use base qw(PasswordMonkey::Filler);

PasswordMonkey::make_accessor( __PACKAGE__, $_ ) for qw(
foo bar baz
);

This plugin can then be initialized by saying

my $wonky = package PasswordMonkey::Filler::Wonky->new(
  foo => "moo",
  bar => "neigh",
  baz => "tweet",
);

Debugging

PasswordMonkey is Log4perl-enabled, which lets you remote-control the amount of internal debug messages you're interested in. If you're not familiar with Log4perl (most likely because you've been living in a cage for the last 25 years), here's the easiest way to activate all debug messages within PasswordMonkey:

use Log::Log4perl qw(:easy);
Log::Log4perl->easy_init($DEBUG);

For more granular control, please consult the Log4perl documentation.

Bouncer API

Bouncer plugins define checks to be executed right before we send over the password to detect irregularities and pull the plug at the last minute if something doesn't look right. A bouncer plugin is attached to a filler plugin by the add_bouncer() method:

$filler->add_bouncer( $bouncer );

The filler then calls the bouncer plugin's check() method right before it fills in the password with the fill() method. If check() returns a true value, the filler proceeds. If check() comes back with a false value, the filler plugin aborts and returns to the monkey without sending the password to the spawned process.

If you need access to the Expect-Object (e.g. to find out what the current match is or what the text previous to the match was), you can use the expect() accessor that comes through inheritance with every bouncer plugin:

my $expect = $self->expect();

To get a better idea about what can be done with bouncer plugins, check out the source code of the two bouncers that come with the distribution, PasswordMonkey::Bouncer::Wait and PasswordMonkey::Bouncer::Retry. Their code is relatively simple and should be easy to follow.

AUTHOR

2011, Mike Schilli <cpan@perlmeister.com>

COPYRIGHT & LICENSE

Copyright (c) 2011 Yahoo! Inc. All rights reserved. The copyrights to the contents of this file are licensed under the Perl Artistic License (ver. 15 Aug 1997).