Expect: answering one or two ssh password prompts then interact - ssh

I have an ssh case (can't use sshkeys) that sometimes prompts for one password, sometimes for two (prompt is the same in both cases). I have tried variations of while, if, exp_continue, but can't seem to cook it correctly.
This works perfect if I only get one prompt:
#!/usr/bin/expect
set pwd "mypwd"
set prompt "*Password*"
set uid "*userid*"
set timeout -1
spawn -noecho ssh -q host.domain
expect $prompt
send $pwd
send \r
interact
Tried this, but not giving giving me the results I need for sometimes two pwds plus the KnownHost case.
expect {
"*yes/no*" { send \"yes\r\"; exp_continue }
$prompt { send \"$pwd\r\"; exp_continue }
$uid {interact}
}

Figured it out:
#!/usr/bin/expect -f
set pwd "mypwd"
set prompt "Enter PIN for 'PIV_II (PIV Card Holder pin)': "
set uid "*userid*"
set timeout -1
spawn ssh -q host.domain
expect {
"*yes/no*" { send "yes\r"; exp_continue }
$prompt { send "$pwd\r"; exp_continue }
$uid { send \r; interact }
}

Related

Using nested spawn method in expect scripts

I have written following expect script:
/usr/bin/expect<<EOF
set SERVER_HOSTNAME "$env(SERVER_HOSTNAME)"
set USERNAME "$env(USERNAME)"
set PASSWORD "$env(PASSWORD)"
set timeout -1
spawn ssh "$USERNAME#$SERVER_HOSTNAME"
expect {
"Host key verification failed." { spawn ssh-keygen -R "$SERVER_HOSTNAME"; expect "known_hosts.old"; send_user "Updated host key details."; exp_continue}
"continue connecting (yes/no)" { send "yes\r"; expect "Permanently added"; exp_continue}
"assword:" { send_user "Keygen details are correctly mapped for this server\n"}
}
EOF
Here I want that if the host key of a server can't be verified while spawing "ssh" process, then the nested spawn process "ssh-keygen -R" should remove old key. Then the "ssh" process should try to connect again so that new key can be added corresponding to that server.
But here, after the execution of:
send_user "Updated host key details."
method, expect process is exiting out from this script.
I know the alternative can be to divide this expect call into two steps, as follows:
/usr/bin/expect<<EOF
set SERVER_HOSTNAME "$env(SERVER_HOSTNAME)"
set USERNAME "$env(USERNAME)"
set PASSWORD "$env(PASSWORD)"
set timeout -1
spawn ssh "$USERNAME#$SERVER_HOSTNAME"
expect {
"Host key verification failed." { spawn ssh-keygen -R "$SERVER_HOSTNAME"; expect "known_hosts.old"; send_user "Updated host key details."; exp_continue}
"continue connecting (yes/no)" { send "yes\r"; expect "Permanently added"; exp_continue}
"assword:" { send_user "Keygen details are correctly mapped for this server\n"}
}
spawn ssh "$USERNAME#$SERVER_HOSTNAME"
expect {
"continue connecting (yes/no)" { send "yes\r"; expect "Permanently added"; exp_continue}
"assword:" { send_user "Keygen details are correctly mapped for this server\n"}
}
EOF
But do we have a way to execute this expect call in one go. In nutshell, I want to know, can we do the nesting of spawn process?
Whitespace goes a long way toward maintainable code: you don't need to squish so many commands per line.
spawn ssh "$USERNAME#$SERVER_HOSTNAME"
expect {
"Host key verification failed." {
spawn ssh-keygen -R "$SERVER_HOSTNAME"
expect "known_hosts.old"
send_user "Updated host key details."
exp_continue
}
"continue connecting (yes/no)" {
send "yes\r"
expect "Permanently added"
exp_continue
}
"assword:" {
send_user "Keygen details are correctly mapped for this server\n"
}
}
I this case, you don't need to interact with ssh-keygen, so use exec to simply call it
"Host key verification failed." {
exec ssh-keygen -R "$SERVER_HOSTNAME"
puts "Updated host key details."
exp_continue
}
If you need to spawn something and interact with it, you need to know that there is an implicit variable, spawn_id, created by spawn, and used by expect and send. You'll need to save the spawn_id of the current process before you spawn any other process. For example:
spawn process1
set ids(1) $spawn_id
expect -i $ids(1) "some pattern"
send -i $ids(1) "some string\r"
spawn process2
set ids(2) $spawn_id
expect -i $ids(2) "some pattern from process2"
send -i $ids(2) "some string to process2\r"

Error On Expect Script

I keep getting an error on this after sending the new password. It will change the password, but not do the rest of the script. Any ideas why it keeps erroring out. I wrote a very similar script for a different device and it works prefect, but the stuff after changing the password is not needed on that device. This device will not save the password after reboot if the rest is not completed. Doing it manually through ssh works just fine so its not the cmds that is the problem, it's something with the script.
#!/usr/bin/expect
set timeout -1
#Edit for User
set user user
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set data [read $f]
close $f
foreach line [split $data \n] {
if {$line eq {}} continue
spawn ssh $user#$line
expect "assword:"
send "$old\r"
sleep 10
send "passwd $user\r"
expect "assword:"
send "$new\r"
expect "assword:"
send "$new\r"
sleep 10
send "grep -v users.1.password= /tmp/system.cfg > /tmp/system.cfg.new\r"
sleep 10
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print \$2}'` >> /tmp/system.cfg.new\r"
sleep 10
send "cp /tmp/system.cfg.new /tmp/system.cfg\r"
sleep 10
send "save && reboot\r"
close
expect eof
}
The full script is alot bigger with with fail-safes if the device does not respond to ssh or the original password is wrong. That one won't work though until I figure out what is wrong with this portion. I just slimmed it down to figure out where the problem is happening. Also this line seems to be the issue as it does create the system.config.new on the line before it:
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print \$2}'` >> /tmp/system.cfg.new\r"
It was and works in ssh as this:
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print $2}'` >> /tmp/system.cfg.new\r"
But sends an error because of the $2 then is view-able by expect. I was told that putting a \$2 would make it only view-able to the remote shell. Any help would be great.
Originally it had expect "star" instead of the sleep cmds. I have been trying tons of stuff out on this script and once I get it to run incorporate it in my full script. The reason I am using sleep is because "star" doesn't seem to match output and fails on the second send $new/r. With sleep it has made it alot farther. I do have a correction though. It is actually making it right up to send "save && reboot\r". I am going to eventually use your $prompt suggestion from my other question in place of sleep or expect "star". With debug on this is where it throws the error:
send: sending "save && reboot\r" to { exp4 }
expect: spawn id exp4 not open
while executing
"expect eof"
("foreach" body line 21)
invoked from within
"foreach line [split $data \n] {
if {$line eq {}} continue
spawn ssh $user#$line
expect "assword:"
send "$old\r"
sleep 3
send "passwd $user\r"
e..."
(file "./ubnt.sh" line 15)
The "save && reboot\r" will kick out the ssh connection after it saves the settings, but it doesn't seem to be getting that far. Is there an issue with &&, maybe I need to /&&.
Interact instead of close and expect eof is the answer I will update with full script when it's done. Here is the working UBNT password change script:
#!/usr/bin/expect
set timeout 30
#Edit for User
set user user
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set data [read $f]
close $f
foreach line [split $data \n] {
if {$line eq {}} continue
spawn ssh $user#$line
expect {
"assword:" {
send "$old\r"
expect {
"assword:" {
close
continue
}}
expect {
"*" {
send "passwd $user\r"
expect "*assword:*"
send "$new\r"
expect "*assword:*"
send "$new\r"
expect "*"
send "grep -v users.1.password= /tmp/system.cfg > /tmp/system.cfg.new\r"
expect "*"
send "echo users.1.password=`grep $user /etc/passwd | awk -F: '{print \$2}'` >> /tmp/system.cfg.new\r"
expect "*"
send "cp /tmp/system.cfg.new /tmp/system.cfg\r"
expect "*"
send "save && reboot\r"
interact
continue
}}}}
expect {
"*" {
close
continue
}}
expect eof
}
Here is the script for Mikrotik passwords:
#!/usr/bin/expect
set timeout 30
#Edit for User
set user user
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set data [read $f]
close $f
foreach line [split $data \n] {
if {$line eq {}} continue
spawn -noecho ssh -q -o "StrictHostKeyChecking=no" $user#$line
expect {
"assword:" {
send "$old\r"
expect {
"assword:" {
close
continue
}}
expect {
"*" {
send "user set $user password=$new\r"
expect ">"
send "quit\r"
close
continue
}}}}
expect {
"*" {
close
continue
}}
expect eof
}
Dinesh mentioned that I should not use "star" in my expect commands and said that instead use:
set prompt "#|%|>|\\\$ $"
And:
expect -re $prompt
Which I will probably change. The scripts also move onto next IP in list if the device cannot be reached via ssh or if the oldpassword does not work for the device. The only other change I plan to make on both is to have it create 3 log files listing successful ips, wrong password ips, and cant reach ips. This should only be one more command before each continue, but haven't yet researched how to output a variable to a local log file.
Thanks for your help on this Dinesh.

Expect Scripting Issue Reading From File

I am trying to change the password for a user on a bunch of linux based routers pulling IPs from a list. The iplist.txt is just a list of ips (one per line). This is the code I am trying to use:
#!/usr/bin/expect
set timeout 20
#Edit for User
set user username
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set hosts [split [read $f] "\n"]
close $f
foreach host $hosts {
spawn -noecho ssh -q -o "StrictHostKeyChecking=no" $user#$host
expect "assword:"
send "$old\r"
expect ">"
send "user set $user password=$new\r"
expect ">"
send "quit\r"
expect eof
close
}
Which works for the first ip on the list but send this error on the second:
spawn_id: spawn id exp4 not open
I got it to work this way as I wanted:
#!/usr/bin/expect
set timeout 30
#Edit for User
set user user
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set data [read $f]
close $f
foreach line [split $data \n] {
if {$line eq {}} continue
spawn -noecho ssh -q -o "StrictHostKeyChecking=no" $user#$line
expect "assword:"
send "$old\r"
expect ">"
send "user set $user password=$new\r"
expect ">"
send "\r"
expect ">"
send "quit\r"
send "\r"
expect eof
}
The next issue I run into is if the device doesn't have the old password or if the linux box cannot reach the device via ssh, it will error out with the same spawn id exp* not open when it gets to that IP and will not continue onto the next IP in the list. Is there anyway I can a statement that says if "assword:" comes up a second time to move onto next IP and if ">" comes up like its supposed to keep going with the script, then add a line after the spawn command that will move to the next IP in list if it doesn't receive the first expect "assword:"?
Any help would be appreciated. I am new to expect, but seems to be a really good tool for mass ssh processes in a script. Just having trouble tweaking it to not error out on 1 job instead of moving to next job upon error.
#!/usr/bin/expect
set timeout 30
#Edit for User
set user user
#Edit for Old Password
set old oldpassword
#Edit for New Password
set new newpassword
#get IP List from iplist.txt
set f [open "/iplist.txt"]
set data [read $f]
close $f
foreach line [split $data \n] {
if {$line eq {}} continue
spawn -noecho ssh -q -o "StrictHostKeyChecking=no" $user#$line
expect {
"assword:" {
send "$old\r"
expect {
"assword:" {
close
continue
}}
expect {
"*" {
send "user set $user password=$new\r"
expect ">"
send "quit\r"
close
continue
}}}}
expect {
"*" {
close
continue
}}
expect eof
}
Probably a bit dirty scripting, but it works. Now if I can figure out how to export successful, wrong password, and timeout logs so I know if any error-ed out.

Sending commands via SSH with expect script

In a another question I was kindly provided with a solution to a dilemma I was having with expect and if/else statements (basically, my lack of knowledge in how they're written). The script I was given works with one exception: If I cleanly connect to a remote host the exit command is not sent which will close that connection and move onto the next host in the list.
This is the functional part of the script:
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh $host
expect {
"continue connecting" {
send "yes\r"
expect {
"current" {
send -- $pw2\r
exp_continue
}
"New password" {
send -- $pw1\r
exp_continue
}
"Retype new password" {
send -- $pw1\r
exp_continue
}
msnyder
}
send "exit\r"
}
}
interact
}
When I'm connected to the host I expect to see the standard Linux prompt: [username#host ~]$. At least, that's our standard prompt. The prompt is preceded by a banner. Might that be throwing a kink into this? I wouldn't think so since I'm only looking for "continue connecting" from the RSA fingerprint prompt and that is within text spread across multiple lines. I would think that expect is intelligent enough to ignore any scrolling text and only look at text in prompts that are looking for input.
I considered passing the exit command as an argument for the ssh $host command, but expect doesn't seem to like that.
Can someone now assist me in getting the exit command to be sent properly?
UPDATE: I've added the exp_internal 1 option to the script to see what it is doing and it seems to only ever be matching "continue connecting" even at the user prompt once an SSH connection is established. It doesn't appear to be executing the next comparison for "msnyder". http://pastebin.com/kEGH3yUY
UPDATE2: I'm making progress. Based on Glenn Jackman's script below, I was able to get this working until the password prompt appears:
set prompt {\$ $}
set file1 [open [lindex $argv 0] r]
set pw1 [exec cat /home/msnyder/bin/.pw1.txt]
set pw2 [exec cat /home/msnyder/bin/.pw2.txt]
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh -q $host
expect {
"continue connecting" {
send "yes\r"
expect {
"current" {
send -- $pw2\r
exp_continue
}
"New password" {
send -- $pw1\r
exp_continue
}
"Retype new password" {
send -- $pw1\r
exp_continue
}
-re $prompt {
send -- exit\r
expect eof
}
}
} -re $prompt {
send -- exit\r
expect eof
}
}
}
I have explicitly told it to send the exit command twice rather than trying the short-circuit method that Glenn used (it didn't seem to work per my comment under his answer).
What is now happening, is that it will loop through the list of hosts, send 'yes' to the RSA fingerprint prompt and then send 'exit' to the command prompt. However, when it hits a server that needs to have the password set it stops. It seems to eventually time out and then restarts with the next host. The current password is not being sent.
# a regular expression that matches a dollar sign followed by a space
# anchored at the end of the string
set prompt {\$ $}
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh $host
expect {
"continue connecting" {
send "yes\r"
expect {
# ...snipped...
-re $prompt
}
}
-re $prompt
}
send -- exit\r
expect eof
}
We've moved the "exit" to a place where it will be executed if you match either "comtinue connecting" or if you log in directly to your prompt.
Nice answer but I hate seeing code cut and pasted. One comment and a bit of refactoring: When you hit the "current" block, you change the password but never exit. It occurs to be that each scenario should end with you seeing the prompt and exiting:
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh -q $host
expect {
"continue connecting" {
send "yes\r"
exp_continue
}
"current" {
send -- $pw2\r
expect "New password"
send -- $pw1\r
expect "Retype new password"
send -- $pw1\r
exp_continue
}
-re $prompt
}
send -- exit\r
expect eof
}
If you see "continue connecting", you send yes and then wait to see "current" or the prompt.
If you see "current" you change the password and then wait to see the prompt.
If you see the prompt, the expect comment ends and the next thing you do is exit.
I've sorted out the answer. After looking at Glenn's script below it occurred to me that each block is a separate if/else when instead I need the password section to act as a single block and the exit to act as the else. This is what I now have:
set prompt {\$ $}
set file1 [open [lindex $argv 0] r]
set pw1 [exec cat /home/msnyder/bin/.pw1.txt]
set pw2 [exec cat /home/msnyder/bin/.pw2.txt]
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh -q $host
expect {
-re $prompt {
send -- exit\r
expect eof
}
"current" {
send -- $pw2\r
expect "New password"
send -- $pw1\r
expect "Retype new password"
send -- $pw1\r
}
"continue connecting" {
send "yes\r"
expect {
"current" {
send -- $pw2\r
expect "New password"
send -- $pw1\r
expect "Retype new password"
send -- $pw1\r
}
-re $prompt {
send -- exit\r
expect eof
}
}
}
}
}
It addresses four situations:
I do NOT have to accept the RSA fingerprint OR change my password.
I do NOT have to accept the RSA fingerprint but DO have to change my password.
I DO have to accept the RSA fingerprint AND change my password.
I DO have to accept the RSA fingerprint but NOT change my password.

Expect redirect stdin

I'm running a script on a remote server like using this command:
ssh root#host 'bash -s' < script.sh
Now I'm trying to use expect to handle the password prompt. This is the script:
#!/usr/bin/expect
set cmd [lindex $argv 0]
spawn -noecho ssh root#host $cmd
expect {
"password:" {
send "password\r"
}
}
If I run the script, it gives no output:
./ssh.exp 'bash -s' < script.sh
I know that's not the way to use ssh without password, but this is not the question right here.
UPDATE I tried the idea of glenn jackman with a simple script but it's not working. This is the script I'm using:
#!/usr/bin/expect
spawn ssh xxx#xxx
expect "*?assword:*"
send "pwd\r"
send "echo hello world"
This is the output I get:
[xxx#xxx bin]$ expect -d my.exp
expect version 5.43.0
argv[0] = expect argv[1] = -d argv[2] = my.exp
set argc 0
set argv0 "my.exp"
set argv ""
executing commands from command file my.exp
spawn ssh xxx#xxx
parent: waiting for sync byte
parent: telling child to go ahead
parent: now unsynchronized from child
spawn: returns {7599}
expect: does "" (spawn_id exp6) match glob pattern "*?assword:*"? no
xxx#xxx's password:
expect: does "xxx#xxx's password: " (spawn_id exp6) match glob pattern "*?assword:*"? yes
expect: set expect_out(0,string) "xxx#xxx's password: "
expect: set expect_out(spawn_id) "exp6"
expect: set expect_out(buffer) "xxx#xxx's password: "
send: sending "pwd" to { exp6 }
send: sending "echo hello world" to { exp6 }
write() failed to write anything - will sleep(1) and retry...
UPDATE I managed it to get my script to run. This is the result which works:
#!/usr/bin/expect
set user [lindex $argv 0]
set host [lindex $argv 1]
set pwd [lindex $argv 2]
spawn ssh $user#$host bash -s
expect {
"?asswor?: " {
send "$pwd\n"
}
}
while {[gets stdin line] != -1} {
send "$line\n"
}
send \004
expect {
"END_TOKEN_OF_SCRIPT" {
exit 0
}
default {
exit 1
}
}
You need to send the script you read on stdin to the remote host:
while {[gets stdin line] != -1} {
send "$line\r"
}
# then you may have to send ctrl-D to signal end of stdin
send \004
Use expect_user, as shown in the man page:
The following script reads a password, and then runs a program every hour that demands a password each time it is run. The script supplies the password so that you only have to type it once. (See the stty command which demonstrates how to turn off password echoing.)
send_user "password?\ "
expect_user -re "(.*)\n"
for {} 1 {} {
if {[fork]!=0} {sleep 3600;continue}
disconnect
spawn priv_prog
expect Password:
send "$expect_out(1,string)\r"
. . .
exit
}
Here is what I currently have, still improving it though:
#!/usr/local/bin/expect
# For debugging make the following to be line 1:
#!/usr/local/bin/expect -D 1
set timeout 20
send_user "Username?\ "
expect_user -re "(.*)\n"
set user $expect_out(1,string)
send_user "password?\ "
stty -echo
expect_user -re "(.*)\n"
stty echo
set password $expect_out(1,string)
spawn su
expect {
"Password" {send "$password\r"}
"#" {interact + return}
}