Oct 24, 2020 • O-Tafe

Confessions

Confessions was an easy web challenge from the hack.lu CTF 2020. It was submitted to the VTF by pspaul

Description

Someone confessed their dirtiest secret on this new website: https://confessions.flu.xxx Can you find out what it is?

Solution

Initial Observations

On visiting the webpage, you are greeted by a form to confess a secret. You can publish a secret with a title and a message, and then the site will give you a hash to verify that it is your secret.

The verification hash it gives you is generated by hashing the message with sha256.

Confession Form Example Confession

Getting to the source

I wanted to see how this site worked, so i turned to look at the page source. One thing that instantly caught my eye was the confessions.js script at the bottom of the source, on opening this file I could see that there was a graphql database at https://confessions.flu.xxx/graphql.

Website Source

Enumerating GraphQL

I have not had much experience with graphql in the past, so I read through the js to see what it was doing with my input to the form. I could see that it was performing a series of POST requests. The javascript would send a json object similar to:

{
	"query": "<User Input>"
}

As this was a simple format, I attempted to send this data through a GET request.

Now that I was able to send queries, I could start enumerating the database. The first thing I tried extracting was the database schema, to do this I used a query that I found at Payload All The Things - Graphql Injection. This worked and gave me this schema.

Query:

fragment FullType on __Type {
  kind
  name
  description
  fields(includeDeprecated: true) {
    name
    description
    args {
      ...InputValue
    }
    type {
      ...TypeRef
    }
    isDeprecated
    deprecationReason
  }
  inputFields {
    ...InputValue
  }
  interfaces {
    ...TypeRef
  }
  enumValues(includeDeprecated: true) {
    name
    description
    isDeprecated
    deprecationReason
  }
  possibleTypes {
    ...TypeRef
  }
}
fragment InputValue on __InputValue {
  name
  description
  type {
    ...TypeRef
  }
  defaultValue
}
fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
              }
            }
          }
        }
      }
    }
  }
}

query IntrospectionQuery {
  __schema {
    queryType {
      name
    }
    mutationType {
      name
    }
    types {
      ...FullType
    }
    directives {
      name
      description
      locations
      args {
        ...InputValue
      }
    }
  }
}

Schema

The schema had a couple points of interest, the first being the accessLog query. This stuck out because the description of this query stated TODO: remove before production. To query this however I first have to know what fields I will be able to access.

{
  "name": "accessLog",
  "description": "Show the resolver access log. TODO: remove before production release",
  "args": [],
  "type": {
    "kind": "LIST",
    "name": null,
    "ofType": {
      "kind": "OBJECT",
      "name": "Access",
      "ofType": null
    }
  },
  "isDeprecated": false,
  "deprecationReason": null
}

The accessLog query returns an object called Access which has 3 fields:

{
  "kind": "OBJECT",
  "name": "Access",
  "description": "",
  "fields": [
    {
      "name": "timestamp",
      "description": "",
      "args": [],
      "type": {
        "kind": "SCALAR",
        "name": "String",
        "ofType": null
      },
      "isDeprecated": false,
      "deprecationReason": null
    },
    {
      "name": "name",
      "description": "",
      "args": [],
      "type": {
        "kind": "SCALAR",
        "name": "String",
        "ofType": null
      },
      "isDeprecated": false,
      "deprecationReason": null
    },
    {
      "name": "args",
      "description": "",
      "args": [],
      "type": {
        "kind": "SCALAR",
        "name": "String",
        "ofType": null
      },
      "isDeprecated": false,
      "deprecationReason": null
    }
  ],
  "inputFields": null,
  "interfaces": [],
  "enumValues": null,
  "possibleTypes": null
}

This allows me to build a query I can send to the database:

{accessLog{timestamp name args}}

AccessLog

The Access Log contains the mutation performed to the database as well as the arguments for this mutation. The arguments of the addConfession mutation however have been redacted, however we can assume that the hash directly after the addConfession is the hash of the previous message.

Down the Rabbit Hole

Now that I have the hash of a message I had another look at the functions available to me in the database schema, this time I was interested in the confession mutation. This would allow me to get a Confession object by its hash. However, the description states that it does not have confidential information.

{
  "name": "confession",
  "description": "Get a confession by its hash. Does not contain confidential data.",
  "args": [
    {
      "name": "hash",
      "description": "",
      "type": {
        "kind": "SCALAR",
        "name": "String",
        "ofType": null
      },
      "defaultValue": null
    }
  ],
  "type": {
    "kind": "OBJECT",
    "name": "Confession",
    "ofType": null
  },
  "isDeprecated": false,
  "deprecationReason": null
}

A Confession object has four fields:

I knew that I would not be able to access the message of this Confessionobject, however I thought that the id field would not be hidden. This would allow me to use the ConfessionWithMessage mutation.

The ConfessionWithMessage has a single argument id, which would have allowed me to get access to the message.

So what went wrong with this chain of reasoning?

The confession will return the id and message values as null, however I knew I was still on the right track as the title of the confession was “Flag”.

Confession

Getting Back On Track

With my previous idea not working, I had very few ideas about what to try next. So I looked at what I had.

The only other solution I could think of would be cracking the hashes I had receieved from the log. So I supplied the first hash to crackstation

An Aside

This seemed like a shot in the dark when I was first attempting the challenge, however going back and writing this up I have seen a few hints towards this being the right method.

The first of which is looking at the timestamps in the Access Log and seeing that they are a second apart, not long enough for a single user to write multiple long messages at a time. I went back after seeing this and looked at the network tab in firefox as I typed in a message.

Cracking the Hash

The first hash cracked to be the letter “F”, this gave me some hope that I was in the right path. So I entered the next hash, this time it cracked to be “FL”. I continued doing this two more times to get “FLAG” but that was where the rainbow table had ended.

I had gained an intuition for how the hash had been calculated at this stage and so wrote a short python script to brute force this hash.

from hashlib import sha256
from json import loads
from string import printable

flag = ""

with open("accessLog.json") as accessLog:
    JSON = loads(accessLog.read())

for entry in JSON['data']['accessLog']:
    hash_ = loads(entry['args']).get("hash", None)
    if hash_ is None:
        continue
    for char in printable:
        if hash_ == sha256((flag + char).encode()).hexdigest():
            flag += char
            print(char, end='')
            break
print()

The Flag

flag{but_pls_d0nt_t3ll_any1}