Search code examples
bashshellsshexpectscp

scp with agent forwarding fails in restricted environment


Environment

A => B => C

A. Our computer

~/.ssh/config is roughly like this:

Host B
    ServerAliveInterval 30
    IdentifyFile ~/.ssh/id_rsa
    User a-user
    Port 10022
    ForwardAgent yes
  • Mac OSX Yosemite.

B. Jump host

/etc/ssh/sshd_config is probably like this:

RSAAuthentication yes
PubkeyAuthentication yes
AllowTcpForwarding no
ChallengeResponseAuthentication no
PasswordAuthentication no
ForceCommand "ssh C"
  • We have no permissions to configure this machine.
  • A's public key is written in authorized_keys.

C. Target host

  • Ubuntu on virtual machine.
  • A's public key is written in authorized_keys.

Situation

  • A can ssh B using password authentication.
  • A can ssh B using public key authentication thanks to agent forwarding.
  • A cannot ssh B foo using password authentication. 3 times challenges are rapidly failed.
  • A cannot ssh -t B foo using password authentication. Argument foo is ignored.
  • A cannot ssh B foo using public key authentication. Argument foo is ignored.
  • A cannot ssh -t B foo using public key authentication. argument foo is ignored.

Approach 1: ShellScript tricks

I wrote the following complicated ShellScript...

# Overrides native ssh command
ssh() {

    # Configuration
    local password="PASSWORD"
    local specialhost="FUMIDAI"

    # Special jump host..?
    if [[ "$@" =~ $specialhost ]]; then

        # Divide arguments into ssh's and additonal commands' and itself
        local sshargs
        sshargs=("ssh" "-t")
        local reach=false
        while (( $# > 0 )); do
            if [ "--" = "$1" ]; then
                shift
                break
            elif [[ "$1" =~ ^-[bcDEeFIiLlmOopQRSWw]+$ ]]; then
                sshargs=("${sshargs[@]}" "$1")
                shift
                if (( $# > 0 )); then
                    sshargs=("${sshargs[@]}" "$1")
                    shift
                fi
            elif [[ "$1" =~ ^-.*$ ]]; then
                sshargs=("${sshargs[@]}" "$1")
                shift
            elif ! $reach; then
                sshargs=("${sshargs[@]}" "$1")
                reach=true
                shift
            else
                break
            fi
        done

        # No additional commands?
        if (( $# == 0 )); then 

            # Automatically input password and interact 
            expect <(echo '
                set timeout -1
                set password [lindex $argv 0]
                set sshargs [lrange $argv 1 end]
                proc are_you_sure {} {
                    send [gets stdin]
                    send "\n"
                    expect "password:" password_input "Please type" are_you_sure
                }
                proc password_input {} {
                    global password
                    send $password
                    send "\n"
                    expect "password:" password_input "Last login:" interact
                }
                eval spawn -noecho command $sshargs
                expect "Are you sure" are_you_sure "password:" password_input "Last login:" interact
            ') $password "${sshargs[@]}"

        # We have sub commands!
        else 

            # Generate random boundary
            local boundary=$(mktemp -u boundary---------XXXXXXXXXXXXXXXXXXXXXXXXXX)

            # Is STDIN owned by terminal?
            if [ -t 0 ]; then

                # Automatically input password,
                # input commands and execute them,
                # receive base64 encoded output,
                # finally exit when the last boundary appeared.
                expect <(echo '
                    set timeout -1
                    set password [lindex $argv 0]
                    set boundary [lindex $argv 2]
                    set bashargs [lrange $argv 3 [expr 3 + [lindex $argv 1] - 1]]
                    set sshargs [lrange $argv [expr 3 + [lindex $argv 1]] end]
                    set boundary_count 0
                    proc are_you_sure {} {
                        send [gets stdin]
                        send "\n"
                        expect "password:" password_input "Please type" are_you_sure
                    }
                    proc password_input {} {
                        global password
                        send $password
                        send "\n"
                        expect "password:" password_input "Last login:" command_input
                    }
                    proc command_input {} {
                        global boundary
                        global bashargs
                        global eb
                        expect_background "$boundary---" observe_boundary
                        set eb "echo \"$boundary\"\"---\""
                        set en "echo \"\""
                        set args [join $bashargs]
                        send "$eb && ( $args | base64 ) 2>/dev/null && $eb && $en && $en\n"
                        interact
                    }
                    proc observe_boundary {} {
                        global boundary
                        global boundary_count
                        if { $boundary_count < 1 } {
                            incr boundary_count
                            expect_background "$boundary---" observe_boundary
                        } else {
                            exit 0
                        }
                    }
                    eval spawn -noecho command $sshargs
                    expect "Are you sure" are_you_sure "password:" password_input "Last login:" command_input
                ') $password $# $boundary "$@" "${sshargs[@]}" |

                # Filter by boundaries
                awk '
                    BEGIN {
                        count=0
                        skip=0
                    }
                    $0 ~ "'$boundary'---" {
                        ++count
                        skip=1
                    }
                    count==1 {
                        if (skip != 1) {
                            print $0
                        } else {
                            skip=0
                        }
                    }
                ' |

                # Retrive it! (in OSX "-D" option means decoding, not "-d")
                base64 -D

            # Piped, or redirected
            else 

                # Encode STDIN
                base64 |

                # Prevent buffer flooding
                (sleep 3 && cat && sleep 3) |

                # Automatically input password,
                # input commands and execute them,
                # decode STDIN and process them and re-encode,
                # receive base64 encoded output,
                # finally exit when the last boundary appeared.
                expect <(echo '
                    set timeout -1
                    set password [lindex $argv 0]
                    set boundary [lindex $argv 2]
                    set bashargs [lrange $argv 3 [expr 3 + [lindex $argv 1] - 1]]
                    set sshargs [lrange $argv [expr 3 + [lindex $argv 1]] end]
                    set boundary_count 0
                    proc are_you_sure {} {
                        send [gets stdin]
                        send "\n"
                        expect "password:" password_input "Please type" are_you_sure
                    }
                    proc password_input {} {
                        global password
                        send $password
                        send "\n"
                        expect "password:" password_input "Last login:" command_input
                    }
                    proc command_input {} {
                        global boundary
                        global bashargs
                        global eb
                        expect_background "$boundary---" observe_boundary
                        set eb "echo \"$boundary\"\"---\""
                        set en "echo \"\""
                        set args [join $bashargs]
                        send "$eb && ( base64 -d | $args | base64 ) 2>/dev/null && $eb && $en && $en\n"
                        fconfigure stdin -blocking 0
                        while {[gets stdin line] != -1} {
                            send "$line\n"
                        }
                        send \004
                        interact
                    }
                    proc observe_boundary {} {
                        global boundary
                        global boundary_count
                        if { $boundary_count < 1 } {
                            incr boundary_count
                            expect_background "$boundary---" observe_boundary
                        } else {
                            send "logout\n"
                        }
                    }
                    eval spawn -noecho command $sshargs
                    expect "Are you sure" are_you_sure "password:" password_input "Last login:" command_input
                ') $password $# $boundary "$@" "${sshargs[@]}" |

                # Filter by boundaries
                awk '
                    BEGIN {
                        count=0
                        skip=0
                    }
                    $0 ~ "'$boundary'---" {
                        ++count
                        skip=1
                    }
                    count==1 {
                        if (skip != 1) {
                            print $0
                        } else {
                            skip=0
                        }
                    }
                ' |

                # Retrive it! (in OSX "-D" option means decoding, not "-d")
                base64 -D

            fi

        fi

    # No, regular ssh
    else 

        # Call native ssh command
        command ssh "$@"

    fi

}

Works:

  • ssh B
  • ssh B ls -A
  • ssh B cat example.tar.gz | tar -xf - -C /tmp
  • echo "This will be appeared in STDOUT" | ssh B cat

Doesn't work:

  • cat example.tar.gz | ssh B tar -xf - -C /tmp

Approach 2: scp with agent forwarding

My final goal is sending local files to remote machine, so that scp seems to be an another good choice. However, ssh works with agent forwarding, while scp doesn't...

Question

  • Why can't I use scp with agent forwarding?
  • Does anyone have nice idea to transporting files around those restricted environment?

Very sorry for long long question. Thank you.

Additional Notes

Outputs of cat excample.tar.gz| ssh jgn-mba tar -xf - -C /tmp:

output

I trapped | tee ~/Desktop/tee.log | after expect to find weird result:

enter image description here

Yes, last boundary didn't appear...


Solution

  • You can't use scp with agent forwarding because it turns it off explicitly. In most cases, this makes sense: Allowing a system you're copying a file to or from to use your keychain for the duration of the connection is usually an unnecessary risk.

    Quoting from the source:

       addargs(&args, "%s", ssh_program);
       addargs(&args, "-x");
       addargs(&args, "-oForwardAgent=no");
       addargs(&args, "-oPermitLocalCommand=no");
       addargs(&args, "-oClearAllForwardings=yes");
    

    You could, of course, build a patched version which didn't do this.


    Another option is to build a script which proxies to ssh but leaves out the argument you don't want:

    scp -S /path/to/yourscript file host:/path/to/file
    

    ...then, /path/to/yourscript can be something like this:

    #!/bin/bash
    args=( )
    while (( $# )); do
      case $1 in
        -oForwardAgent=no)         : ;;
        -oClearAllForwardings=yes) : ;;
        *)                         args+=( "$1" ) ;;
      esac
      shift
    done
    exec /usr/bin/ssh "${args[@]}"