Post

ProtoVault Breach

About this lab

In the storm-battered depths of a discreet mountain peak range, lies ProtoVault, an arcane sanctuary governed by the secretive guild known as the Everbound Order. They safeguard some of the most hidden knowledge across the Cyber Realms. Its defenses are forged in dragonfire and sealed with runes that demand the blood, breath, and soulprint of their masters.

But magic can’t guard everything.

Whispers ripple through the cyber realm. The vault’s inner sanctuary has been breached. A ransom scroll claims access to the Corespell – the foundational arcane code for ProtoVault – and issues a chilling demand:

“Surrender the Archivist Verin.”

Verin holds command over the hidden vaults – each safeguarding knowledge not meant to be accessed, but to protect the balance of the Cyber Realms.

If Verin isn’t given over, the ProtoVault could unravel everything they were built to defend.

AnchorHelm, an OffSec Legend, has summoned you, a skilled codecaster, to stop this before it goes any further.

  1. Determine if the leak could have come from the application. Review the database connection string to ensure it is secure. Submit the connection string here.
  2. Review the other source files. Which one may have leaked the database? Provide the file name.
  3. Using the results of your analysis, discover the public address of the database leak. Verify the contents of the leak by submitting the password hash for Naomi Adler.
  4. Submit the public address of the database leak, including the name of the file.

Once I explored the source files, I found hard-coded credentials.

1
2
└─$ psql 'postgresql://assetdba:8d631d2207ec1debaafd806822122250@pgsql_prod_db01.protoguard.local/pgamgt?sslmode=verify-full' 
psql: error: could not translate host name "pgsql_prod_db01.protoguard.local" to address: Name or service not known

Obviously cannot access it due to the resolution problem.

The provided source files also include .git files.

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
└─$ git log --all --pretty=format:'%H%x09%ai%x09%an%x09%s' --name-only | awk 'NF{print} /'"$(printf '%s' "$MSG_RE")"'/'

a230bbc140cf8a9f60b5d54ca979259c88182470        2024-10-17 10:12:00 -0500       Walter  Release candidate
a230bbc140cf8a9f60b5d54ca979259c88182470        2024-10-17 10:12:00 -0500       Walter  Release candidate
app/app.py
app/app.py
app/templates/login.html
app/templates/login.html
requirements.txt
requirements.txt

8fe1a4431337ecf4bc9759672579168b03351fb6        2024-10-14 09:32:00 -0500       Walter  Remove backup scripts
8fe1a4431337ecf4bc9759672579168b03351fb6        2024-10-14 09:32:00 -0500       Walter  Remove backup scripts
app/util/backup_db.py
app/util/backup_db.py
app/util/restore_db.py
app/util/restore_db.py

9f257d253fc6c15860d2e24941b66ab0733e72aa        2024-10-12 10:51:00 -0500       Walter  Add item details and notes forms
9f257d253fc6c15860d2e24941b66ab0733e72aa        2024-10-12 10:51:00 -0500       Walter  Add item details and notes forms
app/app.py
app/app.py
app/forms.py
app/forms.py
app/templates/category.html
app/templates/category.html
app/templates/index.html
app/templates/index.html
app/templates/item_detail.html
app/templates/item_detail.html

f22ba8adb8e9b1180b44744c66bfbd0ef192381c        2024-10-07 09:45:00 -0500       Walter  Add data models
f22ba8adb8e9b1180b44744c66bfbd0ef192381c        2024-10-07 09:45:00 -0500       Walter  Add data models
app/app.py
app/app.py
app/templates/index.html
app/templates/index.html

b32d77d6fcfcf3ee3681acc930678ad7fe2e1a29        2024-10-06 10:21:00 -0500       Walter  database migration scripts
b32d77d6fcfcf3ee3681acc930678ad7fe2e1a29        2024-10-06 10:21:00 -0500       Walter  database migration scripts
app/util/backup_db.py
app/util/backup_db.py
app/util/restore_db.py
app/util/restore_db.py

5fe83a81198bfb61626a52c912a841364b9aa563        2024-10-05 11:23:00 -0500       Walter  Add authentication
5fe83a81198bfb61626a52c912a841364b9aa563        2024-10-05 11:23:00 -0500       Walter  Add authentication
app/app.py
app/app.py
app/forms.py
app/forms.py
app/templates/index.html
app/templates/index.html
app/templates/login.html
app/templates/login.html

3654766dd5a0b7d51249247bfc26e780133cefc8        2024-10-03 10:15:00 -0500       Walter  UI refactor
3654766dd5a0b7d51249247bfc26e780133cefc8        2024-10-03 10:15:00 -0500       Walter  UI refactor
app/templates/base.html
app/templates/base.html
app/templates/index.html
app/templates/index.html

444eb3acdc1af6c6beab3ce8f3a6881b18a2a3e8        2024-10-01 09:00:00 -0500       Walter  Initial commit: Flask app scaffold
444eb3acdc1af6c6beab3ce8f3a6881b18a2a3e8        2024-10-01 09:00:00 -0500       Walter  Initial commit: Flask app scaffold
app/__init__.py
app/__init__.py
app/app.py
app/app.py
app/templates/index.html
app/templates/index.html

“Walter database migration scripts” looks significant since nothing like *_db.py is included in the provided source files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# prints commit log so far again
└─$ git log b32d77d6fcfcf3ee3681acc930678ad7fe2e1a29
...

# shows the specified file at this point
└─$ git show 9f257d253fc6c15860d2e24941b66ab0733e72aa:app/util/backup_db.py | sed -n '1,200p'

...
# ==== CONFIGURATION ====
SSH_HOST = "pgsql_prod_db01.protoguard.local"
SSH_PORT = 22
SSH_USER = "dbadmin"
SSH_KEY = "/home/walter/.ssh/pgsql_key"

DB_NAME = "pgamgt"
DB_USER = "assetdba"
BACKUP_FILENAME = "db_backup.sql"
LOCAL_BACKUP = f"/tmp/{BACKUP_FILENAME}"
ENC_BACKUP = f"/tmp/{BACKUP_FILENAME}.xyz"

S3_BUCKET = "pgamgt-backups"
S3_KEY = "latest_backup.xyz"
...

At this point, I would need region information to get S3 access, as they don’t set up AWS access keys and make this S3 public on their configuration.

So, I would just see if we have region information.

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
└─$ git grep -n 'REGION' $(git rev-list --all)
ef917bbffedf1638f5ce7747132942197ce97703:app/util/backup_db.py:22:S3_REGION = "us-east-2"
ef917bbffedf1638f5ce7747132942197ce97703:app/util/backup_db.py:61:    s3 = boto3.client("s3", region_name=S3_REGION)
ef917bbffedf1638f5ce7747132942197ce97703:app/util/restore_db.py:22:S3_REGION = "us-east-2"
ef917bbffedf1638f5ce7747132942197ce97703:app/util/restore_db.py:33:    s3 = boto3.client("s3", region_name=S3_REGION)
23040529e72d8797126d1c14b314ac6163b2c031:app/util/backup_db.py:22:S3_REGION = "us-east-2"
23040529e72d8797126d1c14b314ac6163b2c031:app/util/backup_db.py:61:    s3 = boto3.client("s3", region_name=S3_REGION)
23040529e72d8797126d1c14b314ac6163b2c031:app/util/restore_db.py:22:S3_REGION = "us-east-2"
23040529e72d8797126d1c14b314ac6163b2c031:app/util/restore_db.py:33:    s3 = boto3.client("s3", region_name=S3_REGION)
9544195474e1c25ea8415b6b78b4035cf5268d1f:app/util/backup_db.py:22:S3_REGION = "us-east-2"
9544195474e1c25ea8415b6b78b4035cf5268d1f:app/util/backup_db.py:61:    s3 = boto3.client("s3", region_name=S3_REGION)
9544195474e1c25ea8415b6b78b4035cf5268d1f:app/util/restore_db.py:22:S3_REGION = "us-east-2"
9544195474e1c25ea8415b6b78b4035cf5268d1f:app/util/restore_db.py:33:    s3 = boto3.client("s3", region_name=S3_REGION)
6a65249cdd767ab4faf28e4ee88d710e863c09b1:app/util/backup_db.py:22:S3_REGION = "us-east-2"
6a65249cdd767ab4faf28e4ee88d710e863c09b1:app/util/backup_db.py:61:    s3 = boto3.client("s3", region_name=S3_REGION)
6a65249cdd767ab4faf28e4ee88d710e863c09b1:app/util/restore_db.py:22:S3_REGION = "us-east-2"
6a65249cdd767ab4faf28e4ee88d710e863c09b1:app/util/restore_db.py:33:    s3 = boto3.client("s3", region_name=S3_REGION)
5ad3c0672f4597e59e11f09ad1d1081fa2a7afbe:app/util/backup_db.py:22:S3_REGION = "us-east-2"
5ad3c0672f4597e59e11f09ad1d1081fa2a7afbe:app/util/backup_db.py:61:    s3 = boto3.client("s3", region_name=S3_REGION)
5ad3c0672f4597e59e11f09ad1d1081fa2a7afbe:app/util/restore_db.py:22:S3_REGION = "us-east-2"
5ad3c0672f4597e59e11f09ad1d1081fa2a7afbe:app/util/restore_db.py:33:    s3 = boto3.client("s3", region_name=S3_REGION)
d0aa089caddae1735a476954c6c60f469c864f9a:app/util/backup_db.py:22:S3_REGION = "us-east-2"
d0aa089caddae1735a476954c6c60f469c864f9a:app/util/backup_db.py:61:    s3 = boto3.client("s3", region_name=S3_REGION)
d0aa089caddae1735a476954c6c60f469c864f9a:app/util/restore_db.py:22:S3_REGION = "us-east-2"
d0aa089caddae1735a476954c6c60f469c864f9a:app/util/restore_db.py:33:    s3 = boto3.client("s3", region_name=S3_REGION)
1
2
3
4
5
6
7
8
9
10
└─$ curl -I https://protoguard-asset-management.s3.us-east-2.amazonaws.com/

HTTP/1.1 403 Forbidden
x-amz-bucket-region: us-east-2
x-amz-request-id: 9Q7A7Q01HMV56A9G
x-amz-id-2: H7172bLYWYenscyr8Mt4v3PBP99a7emHccnGS8asAoPyVZxTZlTFSKfxtz0bSTembqaNHctWmHc=
Content-Type: application/xml
Transfer-Encoding: chunked
Date: Mon, 13 Oct 2025 12:41:11 GMT
Server: AmazonS3

It returns 403, then figures the bucket exists

1
2
3
4
5
└─$ curl -O https://protoguard-asset-management.s3.us-east-2.amazonaws.com/db_backup.xyz

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 14428  100 14428    0     0  18640      0 --:--:-- --:--:-- --:--:-- 18640

I’ve successfully got a plain-text PostgreSQL dump, but every alphabetic character has been passed through ROT13 (a simple Caesar substitution), meaning it’s just obfuscated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
└─$ tr 'A-Za-z' 'N-ZA-Mn-za-m' < db_backup.xyz | less
...
--
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: assetdba
--

COPY public.users (id, first_name, last_name, specialty, username, password_hash) FROM stdin;
1       Elena   Kovács  Virology & Biosecurity  elena.kovacs    pbkdf2:sha256:600000$GXTIpdzR6ciDJxCl$705204a7738bd064ad0c6fcd7698a7fe70a5764e83af066497498c019c11ab63
2       Marcus  Delgado Prototype Device Engineering    marcus.delgado  pbkdf2:sha256:600000$2PMi96a1nzHye4Ok$3217e74f0843c6c225189bc79063bc9b441d9798c5fa682eba455b7c9e797465
3       Amina   Farouk  Chemical Hazard Containment     amina.farouk    pbkdf2:sha256:600000$3XjFZLRGReNJUytb$d77717c95f950b8e092ef2b116b27ae901eb7a05137f10eb54e5c517babda7c7
4       Jason   Whitmore        Secure Storage Operations       jason.whitmore  pbkdf2:sha256:600000$HkT85y8RRF4JkxHX$a04e5a62d356cc91e7a6da0ef5267c1e4021a20254570f22509dbdca0121f593
5       Priya   Ranganathan     Encryption & Data Security      priya.ranganathan       pbkdf2:sha256:600000$MLH1MyOznTa7TN7d$a93fd6044261cb5b2d529bc5dfacff3d20f5fd474816a3e878f886484d09fbb5
6       Daniel  Foster  Physical Security & Access Control      daniel.foster   pbkdf2:sha256:600000$HFVz3caFPERw7UZJ$6cb544e48b72dca2934d020e8bfff219b03a1c4e6894cc4d59c486a7ff6fc4af
7       Mei     Zhang   Microbial Ecology       mei.zhang       pbkdf2:sha256:600000$VDIo8BlbOyB1NpoR$04e89805d0142329b30626ecd4afd9956c537712ecb92665071a6269e89827c9
8       Rafael  Moretti Energy Systems Engineering      rafael.moretti  pbkdf2:sha256:600000$wPu7wwCiQpuuLTBb$1982762855c8a34a237e241ff5b74918224ddaf522a38586ef79376c648d6400
9       Sarah   Nwosu   Risk Assessment & Compliance    sarah.nwosu     pbkdf2:sha256:600000$Ny8AmUPgXhaF7ZOc$c6d2b7f9e09ff8ed25e57c452ae610051d5cf0db78d4acaa0b909c000527e332
10      Viktor  Petrov  Containment Systems Maintenance viktor.petrov   pbkdf2:sha256:600000$JdSD9J06xZ1bA2Vc$5499b25eade57f14a3839cd5716bc7848b979e69a371471f66b6543b7494d460
11      Naomi   Adler   Cognitive Systems Research      naomi.adler     pbkdf2:sha256:600000$YQqIvcDipYLzzXPB$598fe450e5ac019cdd41b4b10c5c21515573ee63a8f4881f7d721fd74ee43d59
12      Thomas  Sinclair        Technical Documentation & Records       thomas.sinclair pbkdf2:sha256:600000$jiHdWOQtTkpCxgNR$96eca6d861e91a79f3941c42000ef8eca3c28aeece8fb7a97cbc9e687ecf5afc
13      Leila   Haddad  Chemical Catalysis Research     leila.haddad    pbkdf2:sha256:600000$6DDYtk7r2O18ELlz$c21419b605e4aa73169634144843115b44104621ee2c6f22bbad427b806c8c28
14      Gabriel Montoya Drone Systems Development       gabriel.montoya pbkdf2:sha256:600000$Ko7CFWeuJ5aUTTOt$4641aeb53f381592e7040f93116a8c4675ae44f916fa44b3be61fc02ae39e0fa
15      Satoshi Tanaka  Optical Cloaking Technology     satoshi.tanaka  pbkdf2:sha256:600000$SzxVVSviknpMk0LV$a491cd428a5d5d1a11e0eb1fc5be9843591c42b0bfb4362899b8763962fa83d1
\.
...

The following is out of scope:

  • Algo: pbkdf2:sha256
  • Iterations: 600000
  • Salt (ASCII): YQqIvcDipYLzzXPB
  • Derived key (hex): 598fe450e5ac019cdd41b4b10c5c21515573ee63a8f4881f7d721fd74ee43d59

Hashcat’s generic PBKDF2-SHA256 wants base64 for both the salt and the derived key (not hex).

  • salt_b64 = WVFxSXZjRGlwWUx6elhQQg==
  • dk_b64 = WY/kUOWsAZzdQbSxDFwhUVVz7mOo9IgffXIf107kPVk=
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
└─$ hashcat -m 10900 -a 0 hash.txt /usr/share/seclists/Passwords/xato-net-10-million-passwords-100000.txt 
...
Session..........: hashcat                                
Status...........: Exhausted
Hash.Mode........: 10900 (PBKDF2-HMAC-SHA256)
Hash.Target......: sha256:600000:WVFxSXZjRGlwWUx6elhQQg==:WY/kUOWsAZzd...7kPVk=
Time.Started.....: Mon Oct 13 10:02:30 2025 (1 hour, 42 mins)
Time.Estimated...: Mon Oct 13 11:45:19 2025 (0 secs)
Kernel.Feature...: Pure Kernel (password length 0-256 bytes)
Guess.Base.......: File (/usr/share/seclists/Passwords/xato-net-10-million-passwords-100000.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........:       16 H/s (8.53ms) @ Accel:63 Loops:1000 Thr:1 Vec:4
Recovered........: 0/1 (0.00%) Digests (total), 0/1 (0.00%) Digests (new)
Progress.........: 100000/100000 (100.00%)
Rejected.........: 0/100000 (0.00%)
Restore.Point....: 100000/100000 (100.00%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:599000-599999
Candidate.Engine.: Device Generator
Candidates.#01...: 08031947 -> 07012006
Hardware.Mon.#01.: Util: 94%

Started: Mon Oct 13 10:02:29 2025
Stopped: Mon Oct 13 11:45:20 2025