calmthestorm has asked for the wisdom of the Perl Monks concerning the following question:

I am hoping someone has a solution to my problem because frankly I have burned hours an hours on trying to make this bug go away.

First a little background. I wrote a perl script to maintain passwords / UID's across a farm of servers that dont have other means of centralized password management setup. The script is/was working great for everyone until one day (yesterday) when the CTO asked me to reset his password on a subset of the servers because he was unable to login. I thought no problem, script I wrote has multiple arrays of servers configured for just such an occasion. Well, long story short, he types his password in and my script goes off and changes his password on the correct servers but he is still unable to login. Apparently his password has a dollar sign in it which is enough to bork my script. I spend the next few hours coming up with a solution to this problem but it is not working as expected. Here is a snippet of the code in question, hopefully someone gets an ah ha moment and can point to where I went wrong.

sub change_pass { my $user = shift; print "\n\tEnter a NEW password for $username: "; ReadMode('noecho'); $new_password = ReadLine(0); chomp $new_password; print "\n\tConfirm new password for $username: "; $confirm_password = ReadLine(0); chomp $confirm_password; ReadMode(0); print "\n"; unless ( $new_password =~ /\b\Q$confirm_password\E\b/) { &usage("\nPasswords entered do NOT match!\n"); } my $set_password = '/usr/bin/ssh ' . $master_host . " \"echo \Q$ne +w_password\E | /usr/bin/passwd --stdin \Q$username\E\""; system($set_password); print "\n"; }
I guess I should add that my script is failing when comparing the two passwords taken from stdin only when special characters like $ are in the password which causes the usage sub to run/exit
  • Comment on passwords with special characters are trying to kill me... no seriously!
  • Download Code

Replies are listed 'Best First'.
Re: passwords with special characters are trying to kill me... no seriously!
by BrowserUk (Patriarch) on Feb 04, 2011 at 00:59 UTC

    The problem is your incorrect use of \b in your regex. If you want to ensure a full match use string boundaries not word boundaries:

    $s = 'fred';; print 'fred' =~ m[\b\Q$s\E\b] ? 'matched' : 'no match';; matched $s = '$fred';; print '$fred' =~ m[\b\Q$s\E\b] ? 'matched' : 'no match';; no match $s = '$fred';; print '$fred' =~ m[^\Q$s\E$] ? 'matched' : 'no match';; matched

    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
      Hmm, well it seems the passwords match now but it is still failing to set the password on the remote host using
      my $set_password = '/usr/bin/ssh ' . $targethost . " \"echo \Q$new_pas +sword\E | /usr/bin/passwd --stdin $username\""; system($set_password);
        it is still failing to set the password on the remote host using

        Sorry, but that is an entirely different problem and one well outside my field of experience.

        One thing that stands out though. Are you sure that /usr/bin/passwd will accept input from a pipe?


        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.
        Possibly try using the quotemeta function which backslashes all non-"word" characters. This might work for you:
        my $set_password = qq(/usr/bin/ssh $targethost "echo ') . quotemeta($new_password) . qq(' | /usr/bin/passwd --stdin $username"); system($set_password);
Re: passwords with special characters are trying to kill me... no seriously!
by Tux (Canon) on Feb 04, 2011 at 07:40 UTC

    Even ssh will open a shell on the remote end. The best solution I think is to open the password receiving command reading from a pipe, instead of passing the password with echo or whatever unix command.

    # Untested example. Modify to your needs open my $pipe_command, "|-", "ssh $hostname 'passwd --stdin bob'"; print $pipe_command "$password\n"; close $pipe_command;

    Enjoy, Have FUN! H.Merijn
      You sir/madam are a friggen genius!!! I don't know why I didn't think of such a simple approach. BTW, I just tested it and it worked beautifully. Thank you thank you thank you.

      Have a great weekend everyone. I know I will now...

Re: passwords with special characters are trying to kill me... no seriously!
by rmcgowan (Sexton) on Feb 04, 2011 at 04:54 UTC

    I'm posting as a direct response, as I think it fits better here, but some of the comments by BrowserUk (specifically Re^5) points in the right direction.

    I modified calmthestorm's original code to print out, rather than run system, on the string $set_password, which printed:

    /usr/bin/ssh hostname "echo test\$ing | /usr/bin/passwd --stdin bob"

    The second thing to point out comes from perldoc for the 'system' function:

    ... If there is only one scalar argument, the argument is checked for shell metacharacters, and if there are any, the entire argument is passed to the system's command shell for parsing...

    There are shell metacharacters in the string, so what executes is actually:

    /.../bin/sh -c '/usr/bin/ssh hostname "echo test\$ing | /usr/bin/passw +d --stdin bob"'

    Note that I added the pair of single quotes for clarity, but they do not actually appear, since everything in them is passed as a single string with embedded spaces and so on, by perl. What the shell sees is two arguments, the '-c' and the long string:

    /usr/bin/ssh hostname "echo test\$ing | /usr/bin/passwd --stdin bob"

    The shell then runs 'ssh' with two arguments, 'hostname' and the long string:

    echo test$ing | /usr/bin/passwd --stdin bob

    Notice that both double quotes AND the backslash have been removed! You can verify this happens by doing:

    $ echo 'test\$ing' test\$ing $ echo "test\$ing" test$ing

    The net result is the remote sees a varible name $ing, which almost certainly has no value, and so the password is set to be the initial string, 'test'.

    Fix this by changing your quoting for this line:

    my $set_password = '/usr/bin/ssh ' . $master_host . " \"echo \Q$new_ +password\E | /usr/bin/passwd --stdin \Q$username\E\"";

    so it looks like:

    my $set_password = '/usr/bin/ssh ' . $master_host . " 'echo \Q$new_p +assword\E | /usr/bin/passwd --stdin \Q$username\E'";

    In the shell, one type of quotes protects the other type and removes their special meaning, so something like "'$HOME'" will print "plain" single quotes surrounding the value of the $HOME variable, for example '/home/joe', which means in your case, the backslash introduced by \Q...\E will be kept, and will do what you expect on the remote system.

      my $set_password = '/usr/bin/ssh ' . $master_host . " 'echo \Q$new_pas +sword\E | /usr/bin/passwd --stdin \Q$username\E'";

      Unfortunately, that won't work if the password contains, for example, a single quote.

      my $master_host = 'localhost'; my $username = 'foo'; my $new_password = 'a\'b$c"d|e'; print "pwd: ",$new_password; my $set_password = '/usr/bin/ssh ' . $master_host . " 'echo \Q$new_pas +sword\E | /usr/bin/passwd --stdin \Q$username\E'"; print "cmd: ",$set_password; system $set_password; __END__ pwd: a'b$c"d|e cmd: /usr/bin/ssh localhost 'echo a\'b\$c\"d\|e | /usr/bin/passwd --st +din foo' sh: Syntax error: Unterminated quoted string

      In other words, a simple quotemeta (\Q) is not the appropriate tool to quote arbitrary strings for the shell. You'd have to use more sophisticated techniques.

        It's easy if you build the command in stages.

        sub text_to_shell_lit(_) { return $_[0] if $_[0] =~ /^[a-zA-Z0-9_\-]+\z/; my $s = $_[0]; $s =~ s/'/'\\''/g; return "'$s'"; } my $user = 'bob'; my $passwd = 'test$ing'; my $echo_cmd = join ' ', map text_to_shell_lit, echo => $passwd; my $passwd_cmd = join ' ', map text_to_shell_lit, passwd => '--', $user; my $ssh_cmd = join ' ', map text_to_shell_lit, ssh => 'hostname', "$echo_cmd | $passwd_cmd";
        ssh hostname 'echo '\''test$ing'\'' | passwd -- bob'

        Note that command lines are readable by anyone on the machine. It is not safe to pass passwords in command lines. Yet another reason why opening a pipe to ssh is better.

Re: passwords with special characters are trying to kill me... no seriously!
by Anonymous Monk on Feb 04, 2011 at 00:54 UTC
    Why on earth are you using a regex? Just do a simple equality test ($new eq $confirm).
      Actually started that way and mutated to what I have after trying to solve this problem for hours.