I am trying to write a script will provide credentials to a vpn with one caveat where VPN requires that I provide OTP.
This is my script:
#! /usr/bin/expect
set vpn [ lindex $argv 0]
set group [ lindex $argv 1]
set user [ lindex $argv 2]
set secret [ lindex $argv 3]
log_user 2
spawn nmcli con up $vpn --ask
expect {
"GROUP:" {
send "$group\r"
exp_continue
}
"Username:" {
send "$user\r"
exp_continue
}
Password: {
send_user "\nProvide RSA OTP:"
expect_user -re ":(.*)\r"
set otp $expect_out(0,string) <--------- Error!!!
set pass "$secret$otp"
send "$pass\r"
exp_continue
}
"Connection successfully activated" {
send_user "Connected to VPN\r"
}
"Login failed" {
send_user "Login failed\r"
exp_continue
}
"already active" {
send_user "Already connected to VPN\r"
}
}
exit
I included an arrow to what I think is the source of the problem because when I run this script, it looks like expect_out contains Password:. Given what i read in man page I imagined that the buffer is cleared each time new match is made and therefore it contains string matched in most recent expect clause( in this case expect_user -re ":(.*)\r"), however it seems that I'm wrong. I did not see any notes saying that expect_user contents need to be accessed using different function like interact does, so I'm not sure where did my password go?
Thank you
When using send_user you will not see an echo of what you write, unlike when you send to the spawned command, so you will never match : unless the user explicitly types it. Also, you will read newline, not carriage-return. So use
expect_user -re "(.*)\n"
to get the user input and $expect_out(1,string) (1 not 0) will contain the input.
What you are observing is a timeout in your expect_user, which is why the variable is not updated and still contains the old match.
To protect yourself against unseen timeouts, you could set up a "global" timeout check by starting with
proc abort { } { send_user "Timeout!" ; exit 2 }
expect_before timeout abort
The expect_before is done "before" each expect, but also before each expect_user, it seems.
Related
I tried posting this in the codereview community, but there is no expect tag and I don't have enough karma to create tags.
I have written an expect script to either login to a server or run a simple (usually single) command and return the output.
I have two problems and a wish.
Commands that return nothing--i.e., ssh2server user host false--time out with an error (because I'm not capturing a timeout, though I suppose I should) instead of just returning nothing.
I can capture the return code of the program but I can't get it to exit with the appropriate code.
Is there a way I can take the output of the called program and return it the same way (remote stdout goes to local stdout and remote stderr goes to local stderr)?
Also, any comments or (constructive) criticisms would be appreciated.
#!/usr/bin/expect -f
if {[info exists ::env(SSH2SERVER_PASSWORD)]} {
set password "$env(SSH2SERVER_PASSWORD)"
} else {
puts "SSH2SERVER_PASSWORD not set"
exit 1
}
if {[llength $argv] < 2} {
puts "usage: ssh2server user server"
exit 1
}
set user [lindex $argv 0]
set server [lindex $argv 1]
set command [lrange $argv 2 end]
set pwd_prompt "*assword:"
set prompt "\$ "
set rc 0
expect_before {
#timeout { send_user 'timeout' ; exit 2 }
timeout { send_user 'timeout' ; set rc 2}
}
log_user 0
spawn ssh $user#$server
expect "$pwd_prompt" { send -- "$password\r" }
if { $command == "" } {
interact
} else {
expect {
"$prompt" {
send -- "PROMPT_COMMAND=\rPS1='_MYPROMPT_'\r$command\r"
#expect -re "$command\r\n(.*)\r\n\[^\r]*\[#\$%]"
expect -re "$command\r\n(.*)\r\n\[^\r]*_MYPROMPT_"
set results $expect_out(1,string)
puts $results
send -- "^D"
expect eof
#catch wait ec
#set rc [lindex $ec 3]
#puts [lindex $ec 3]
#exit [lindex $ec 3]
}
#eof { send_user $expect_out(buffer); exit 3}
eof { send_user $expect_out(buffer); set rc 3}
}
}
log_user 1
lassign [wait] pid spawnid os_flag rc
#puts $rc # outputs correct value
exit $rc
I suspect this is the problem:
send -- "^D"
You are not sending a Ctrl-D, you are sending the characters ^ and D.
To send a Ctrl-D
send -- "\04"
To solve the "no output, timeout" problem, you need to alter your expected regex: you have too many newlines for that case. Using expect -d would have revealed this to you. Like this:
send -- "unset PROMPT_COMMAND; PS1='_MYPROMPT_'\r"
expect -re "_MYPROMPT_$"
send -- "$command\r"
expect -re "$command(.*)\r\n_MYPROMPT_$"
The content of the capturing parentheses may now be empty.
I split off setting the prompt for clarity.
To capture the exit status of the command, you may have to do this:
send -- "$command; echo $?\r"
expect -re "$command(.*)\r\n(\d+)\r\n_MYPROMPT_$"
set results [regsub {^\r\n} $expect_out(1,string) ""]
set status $expect_out(2,string)
I don't think you can separate stdout and stderr with the expect command. I think both streams are captured as "output". (I don't have my Exploring Expect book nearby to confirm)
If that's important, you might want to invoke the command redirecting stdout and/or stderr to file(s), and then cat and capture the file contents.
Because my company insists on using sudo I now have to scrap my ssh system I've built for running remote commands.
I have a script that will connect to a server, sudo into the db2 inst owner account and then run a script (previously delivered into /tmp). Maybe 1 in 10 times it will work (outputs 'Hello world' for my test).
Here is the ksh script on the remote server (/tmp/cwow/generic.ksh):
#!/usr/bin/ksh93
echo "Hello world"
[I've also tried adding sleep 5 and wait with mixed results but it doesn't solve the problem]
The expect script I'm running locally is:
#!/usr/bin/expect
set spath /tmp/cwow/generic.ksh
set pass $env(MYEXPECTPASS)
set user $env(MYEXPECTUSER)
if { [llength $argv] != 2 } {
send_user "USAGE: $argv0 host inst\n"
exit
}
set host [lindex $argv 0]
set inst [lindex $argv 1]
set timeout 10
log_user 1
exp_internal 0
eval spawn /usr/bin/ssh -t $user#$host "sudo su - cwow"
expect {
timeout { send_user "TimedOut"; exit }
-glob "assword:" {
send "$pass\r"
expect {
-glob "assword:" {
send "$pass\r"
expect {
-glob " " {
send "/tmp/cwow/generic.ksh\r\n"
expect {
-glob "world" {
send_user "Got it\r"
}
}
}
}
}
}
}
}
I should also note that I never get the 'Got it' message but I don't really need that to work, just curious why it doesn't. What I need to work reliably is for the script to run and, most of the time, it doesn't appear to.
Any ideas for a weak expect user would be greatly appreciated.
(not an answer, just a formatted comment)
You don't need to nest all the expect commands: if you expect a pattern with no action body, the script will continue to the next command. This is more readable, IMO:
expect {
timeout { send_user "TimedOut"; exit }
"assword:"
}
send "$pass\r"
expect "assword:"
send "$pass\r"
expect " "
send "/tmp/cwow/generic.ksh\r"
expect "world"
send_user "Got it\n"
Note, you should send to the spawned process with \r as "hitting Enter". But \n is used for send_user.
I'm trying to write an expect script to:
Connect to a remote server providing the user and password
Loop through a local file reading each line
Execute a specific command on the remote server for each of those lines
I could successfully achieve the step #1 and was testing the #3 with a simple scenario, but couldn't make it work yet. Unfortunately in the line 8 of the script, after sending the password, I just get logged into the server as I would have been logged manually (I can interact with the console) and the rest is not executed.
How can I circumvent this problem?
This is the script:
#!/usr/bin/expect
set timeout 20
set ip [lindex $argv 0]
set user [lindex $argv 1]
set password [lindex $argv 2]
spawn ssh -t -t "$user\#$ip"
expect "Password:"
send "$password\r";
expect "NYXOOBPN402(config)$"
send "delete decoders:ASX-Trade24-FIX.mdp\r"
expect "Are you sure you want to delete 'decoders:ASX-Trade24-FIX.mdp' (y/n)?"
send "y\r";
And this is how I'm executing it:
./test_expect.sh 172.18.250.20 admin admin
The problem is that my expect was incorrect in the line 17. Instead of "NYXOOBPN402(config)$", I should just put "*(config)*", since there's a lot of text before this part that was not being matched.
This is my final script for someone that runs into this same issue:
#!/usr/bin/expect
set timeout 9
# Check if the parameters are correct
if {[llength $argv] == 0} {
send_user "Usage: ./test_expect.sh ip username password\n"
exit 1
}
# Read the file with all the decoders names to be deleted
set f [open "decoders.txt"]
set decoders [split [read $f] "\n"]
close $f
# debug mode - very useful:
#exp_internal 1
# Connect to the server
set ip [lindex $argv 0]
set user [lindex $argv 1]
set password [lindex $argv 2]
spawn ssh -t "$user\#$ip"
expect "Password: "
send "$password\r";
sleep 3
# send ctrl+c since this terminal shows a lot of decoders
#send \x03
expect {
default { send_user "\nCould not find the expected value.\n"; exit 1 }
"*(config)*" {
# Loop through all the decoders
foreach decoder $decoders {
#send_user "Removing $decoder\n"
send "delete decoders:$decoder\r"
expect {
"Are you sure you want to delete*" { send "y\r" }
"*decoder will still be active*" { send_user "\nRemoved $decoder successfully\n" }
"*no such file or directory" { send_user "\nDecoder $decoder already deleted.\n" }
default { send_user "\nNot expected value with $decoder, please debug.\n"; exit 1 }
}
}
}
}
I have make an example as below. The password(mingps)is the shell variable. When execute the shell script, in the mean while, execute command "ps -ef", I found the result of "ps" showed the password(mingps). For security reason, I don't want to show the password when execute command "ps -ef". So how to hide it? Thanks in advance.
#!/bin/sh
MalbanIP="XXX.XXX.XXX.XXX"
MalbanLogin="ming"
MalbanPwd="mingps"
MalbanCmd="netstat"
firstTime="true"
/usr/bin/expect <<EOF
set timeout 10
log_user 0
spawn /usr/bin/ssh $MalbanIP -l $MalbanLogin
expect {
-nocase "continue connecting (yes/no)?" {
send "yes\r"
expect "password:" {
send "$MalbanPwd\r"; set firstTime "false"; exp_continue
}
}
"password" {
if {$firstTime == "true"} {
send "$MalbanPwd\r"; set firstTime "false"
} else {
log_user 1; puts stdout "password is wrong"; log_user 0;
exit 1
}
}
}
expect "0-0-3"
log_user 1
send "$MalbanCmd \r"
set results \$expect_out(buffer)
expect "0-0-3" { send "exit\r" }
expect eof
EOF
exit 0
Option 1
The best way is to switch to using RSA keys to log in, as this will enable you to significantly strengthen your overall system security substantially. With that, you can probably avoid using Expect entirely.
Option 2
However, if you can't do that, the key to fixing things is to not pass it as either an argument or an environment variable (since ps can see both with the right options). Instead, you pass the password by writing it into a file and giving the name of that file to the Expect script. The file needs to be in a directory that only the current user can read; chmod go-rx will help there.
MalbanPwdFile=/home/malban/.securedDirectory/examplefile.txt
# Put this just before the spawn
set f [open $MalbanPwdFile]
set MalbanPwd [gets $f]
close $f
You might also need to put a backslash in front of the use of $MalbanPwd so that it doesn't get substituted by the shell script part too early.
Option 3
Or you could stop using that shell wrapper and do everything directly in Tcl/Expect.
#!/usr/bin/expect
set MalbanIP "XXX.XXX.XXX.XXX"
set MalbanLogin "ming"
set MalbanPwd "mingps"
set MalbanCmd "netstat"
set firstTime true
set timeout 10
log_user 0
spawn /usr/bin/ssh $MalbanIP -l $MalbanLogin
expect {
-nocase "continue connecting (yes/no)?" {
send "yes\r"
expect "password:" {
send "$MalbanPwd\r"
set firstTime false
exp_continue
}
}
"password" {
if {$firstTime} {
send "$MalbanPwd\r"
set firstTime false
} else {
log_user 1
puts stdout "password is wrong"
log_user 0
exit 1
}
}
}
expect "0-0-3"
log_user 1
send "$MalbanCmd \r"
set results \$expect_out(buffer)
expect "0-0-3" { send "exit\r" }
expect eof
I suspect that this last option will work best for you in the longer term. It's definitely the simplest one (other than switching to RSA keys, which is what I've got deployed on my own infrastructure) and I think it is going to avoid some subtle bugs that you've got in your current code (due to substitution of variables at the wrong time).
I wrote an expect script which will connect to a server and send a password depending on the prompt. The purpose is to update my password on new servers.
Basically, it will connect to a new server, expect to see (current) UNIX password: and then send my initial password. It will then see the 'New password:and 'Retype new password: prompts sending the new password each time.
My script loads these passwords from files into variables $pw1 and $pw2. I have evaluated them and verified that I have the correct values loaded into the variables. However, when I run the script, I am getting a token manipulation error on the initial password which tells me the value being sent is incorrect.
Perhaps my logic is incorrect?
EDIT: I have connected to one server that I'm running the script against and entered the old password exactly as it is in the file that the script loads into $pw2. It is working so I know that the password is not incorrect.
#! /usr/bin/expect --
#exp_internal 1
#set stty_init raw
set timeout 45
set prompt {\$ $}
set file1 [open [lindex $argv 0] r]
set pw1 [exec cat /home/user/bin/.pw1.txt]
set pw2 [exec cat /home/user/bin/.pw2.txt]
#puts $pw1
#puts $pw2
while {[gets $file1 host] != -1} {
puts $host
spawn -noecho ssh -q $host
expect {
"*assword*" {
send -- "$pw1\r"
expect {
$prompt {
send -- exit\r
}
}
send -- exit\r
expect eof
}
-re $prompt {
send -- exit\r
expect eof
}
"(current)*" {
send -- "$pw2\r"
expect "New password"
send -- "$pw1\r"
expect "Retype new password"
send -- "$pw1\r"
puts \r
}
"continue*" {
send "yes\r"
expect {
"current" {
send -- "$pw2\r"
expect "New password"
send -- "$pw1\r"
expect "Retype new password"
send -- "$pw1\r"
puts \r
# expect eof
}
-re $prompt {
send -- exit\r
expect eof
}
}
}
}
}
puts \r
First, I'd like to note that you don't need to nest expect commands: look into the exp_continue command, instead. It will cut your code size in half simply by eliminating duplication.
Second, it looks your logic is, indeed, wrong. The first expect pattern is "*assword*". I would expect this to match when the remote server of your ssh is prompting for the user's current password, and it sends $pw1. However, everywhere else in your code, you seem to be treating $pw2 as the current password, and $pw1 as the new password.
Also, when you rewrite your code using exp_continue to eliminate the duplication and nesting, make sure you re-order your patterns so they're listed in order of most specific to least specific. Otherwise, you'll sometimes find the wrong pattern being matched, and the wrong code being executed.