#!/bin/bash # 32 - gRPC / Protobuf Security # Scope note: pqcrypta.com public surface is REST/HTTP. This script probes for # any accidentally-exposed gRPC endpoints on standard and non-standard ports. # If none found, results document "no gRPC surface" — which is itself a finding. # # Tests: port scan for gRPC, HTTP/2 gRPC content-type probes, protobuf fuzzing, # gRPC reflection (service enumeration), gRPC-Web (browser gateway), # auth bypass via gRPC metadata, message injection SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../config.sh" OUT_FILE="$OUT/32_grpc_protobuf.txt" echo "=== 32. gRPC / Protobuf ===" | tee "$OUT_FILE" echo "Target: $TARGET API: $API_TARGET" | tee -a "$OUT_FILE" echo "" | tee -a "$OUT_FILE" HOST=$(echo "$TARGET" | sed 's|https\?://||' | cut -d/ -f1) IP=$(dig +short "$HOST" | grep -oP '^\d+\.\d+\.\d+\.\d+$' | head -1) [ -z "$IP" ] && IP="$HOST" # ── 1. Port scan for gRPC ───────────────────────────────────────────────────── echo "--- gRPC Port Scan ---" | tee -a "$OUT_FILE" GRPC_PORTS=(50051 50052 50053 9090 9091 9092 8080 8443 3000 3001 4000 4001 7070 7071) FOUND_PORTS=() for PORT in "${GRPC_PORTS[@]}"; do if timeout 2 bash -c "echo >/dev/tcp/$IP/$PORT" 2>/dev/null; then printf ' [OPEN] port %s on %s\n' "$PORT" "$IP" | tee -a "$OUT_FILE" FOUND_PORTS+=("$PORT") else printf ' [CLOSED] port %s\n' "$PORT" | tee -a "$OUT_FILE" fi done if [ ${#FOUND_PORTS[@]} -eq 0 ]; then echo "" | tee -a "$OUT_FILE" echo " [SCOPE] No gRPC ports found on public surface." | tee -a "$OUT_FILE" echo " If internal services use gRPC, test from within the network or VPN." | tee -a "$OUT_FILE" echo " See SCOPE.md for gRPC coverage gap documentation." | tee -a "$OUT_FILE" fi echo "" | tee -a "$OUT_FILE" # ── 2. gRPC Content-Type probe on existing HTTPS endpoints ─────────────────── echo "--- gRPC Content-Type Probe (HTTP/2) ---" | tee -a "$OUT_FILE" # A gRPC server will respond with HTTP/2 + content-type: application/grpc # even if the path is wrong. A REST server will return 415/400. GRPC_CT_PROBES=( "application/grpc" "application/grpc+proto" "application/grpc-web" "application/grpc-web+proto" "application/grpc-web-text" ) for CT in "${GRPC_CT_PROBES[@]}"; do for ENDPOINT in "/" "/encrypt" "/decrypt" "/status" "/grpc" "/api"; do CODE=$(curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ -A "$BROWSER_UA" \ -X POST \ -H "Content-Type: $CT" \ -H "TE: trailers" \ --data-binary $'\x00\x00\x00\x00\x05\x08\x01\x10\x01\x18\x01' \ "$API_TARGET$ENDPOINT") # 415 = Unsupported Media Type (REST server rejecting gRPC CT — expected) # 200 = possible gRPC server accepting request # 400 = server parsed but rejected — might be gRPC if [ "$CODE" != "415" ] && [ "$CODE" != "404" ] && [ "$CODE" != "401" ] && [ "$CODE" != "403" ]; then printf ' [%s] %s → %s (unexpected — check if gRPC endpoint)\n' "$CODE" "$CT" "$ENDPOINT" | tee -a "$OUT_FILE" else printf ' [%s] %s → %s\n' "$CODE" "$CT" "$ENDPOINT" | tee -a "$OUT_FILE" fi done done echo "" | tee -a "$OUT_FILE" # ── 3. gRPC Reflection probe ────────────────────────────────────────────────── echo "--- gRPC Server Reflection (service enumeration) ---" | tee -a "$OUT_FILE" # Minimal protobuf-encoded ServerReflectionRequest # Field 1 (list_services): empty string → b'\n\x00' wrapped in gRPC frame # gRPC frame: 1-byte flag(0) + 4-byte length + payload REFLECT_PAYLOAD=$(python3 -c " import struct, sys # ServerReflection.ServerReflectionInfo request: list_services = '' proto_body = b'\x0a\x00' # field 1 (string): empty frame = struct.pack('>BI', 0, len(proto_body)) + proto_body sys.stdout.buffer.write(frame) " 2>/dev/null | base64 -w0) for PORT in 443 50051 9090 8080; do TARGET_REFLECT="https://$HOST:$PORT/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo" CODE=$(echo "$REFLECT_PAYLOAD" | base64 -d | \ curl -sk --http2 -o /tmp/grpc_reflect.bin -w '%{http_code}' \ --max-time 8 -A "$BROWSER_UA" \ -X POST \ -H "Content-Type: application/grpc" \ -H "TE: trailers" \ --data-binary @- \ "$TARGET_REFLECT" 2>/dev/null) RESP_HEX=$(xxd /tmp/grpc_reflect.bin 2>/dev/null | head -3) if [ "$CODE" = "200" ]; then echo " [VULN] gRPC reflection responded on port $PORT — service enumeration possible!" | tee -a "$OUT_FILE" echo "$RESP_HEX" | sed 's/^/ /' | tee -a "$OUT_FILE" else printf ' [%s] gRPC reflection probe on port %s\n' "$CODE" "$PORT" | tee -a "$OUT_FILE" fi done echo "" | tee -a "$OUT_FILE" # ── 4. Protobuf fuzzing (malformed frames) ──────────────────────────────────── echo "--- Protobuf Malformed Frame Fuzzing ---" | tee -a "$OUT_FILE" FUZZ_CASES=( "Empty frame:\x00\x00\x00\x00\x00" "Oversized length:\x00\xff\xff\xff\xff" "Truncated payload:\x00\x00\x00\x00\x10\x08\x01" "Compressed flag set:\x01\x00\x00\x00\x05\x08\x01\x10\x01\x18\x01" "Max field number:\xff\xff\xff\xff\x7f" "Nested overflow:\x0a\xff\xff\xff\x7f" ) for CASE in "${FUZZ_CASES[@]}"; do LABEL="${CASE%%:*}" PAYLOAD="${CASE#*:}" CODE=$(printf "$PAYLOAD" | curl -sk --http2 -o /dev/null -w '%{http_code}' \ --max-time 8 -A "$BROWSER_UA" \ -X POST \ -H "Content-Type: application/grpc" \ -H "TE: trailers" \ --data-binary @- \ "$API_TARGET/encrypt" 2>/dev/null) printf ' [%s] Protobuf fuzz: %s\n' "$CODE" "$LABEL" | tee -a "$OUT_FILE" done echo "" | tee -a "$OUT_FILE" # ── 5. gRPC-Web gateway probe ───────────────────────────────────────────────── echo "--- gRPC-Web Gateway ---" | tee -a "$OUT_FILE" # gRPC-Web allows browsers to speak gRPC via HTTP/1.1 through a gateway GRPC_WEB_PATHS=(/grpc-web /grpcweb /grpc.gateway /api/grpc /v1/grpc) for GWPATH in "${GRPC_WEB_PATHS[@]}"; do CODE=$(curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ -A "$BROWSER_UA" \ -X POST \ -H "Content-Type: application/grpc-web+proto" \ -H "X-Grpc-Web: 1" \ --data-binary $'\x00\x00\x00\x00\x00' \ "$TARGET$GWPATH" 2>/dev/null) [ "$CODE" != "404" ] && [ "$CODE" != "301" ] && \ printf ' [%s] gRPC-Web gateway: %s\n' "$CODE" "$GWPATH" | tee -a "$OUT_FILE" || \ printf ' [%s] %s\n' "$CODE" "$GWPATH" | tee -a "$OUT_FILE" done echo "" | tee -a "$OUT_FILE" # ── 6. gRPC metadata auth bypass ───────────────────────────────────────────── echo "--- gRPC Metadata Auth Bypass ---" | tee -a "$OUT_FILE" # gRPC auth is carried in HTTP/2 headers (metadata). Test missing/invalid auth. for ENDPOINT in "/encrypt" "/decrypt" "/status"; do # No auth metadata CODE=$(printf '\x00\x00\x00\x00\x05\x08\x01\x10\x01\x18\x01' | \ curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ -X POST \ -H "Content-Type: application/grpc" \ -H "TE: trailers" \ --data-binary @- \ "$API_TARGET$ENDPOINT" 2>/dev/null) printf ' [%s] gRPC no-auth → %s\n' "$CODE" "$ENDPOINT" | tee -a "$OUT_FILE" # Fake auth token in grpc-metadata CODE=$(printf '\x00\x00\x00\x00\x05\x08\x01\x10\x01\x18\x01' | \ curl -sk --http2 -o /dev/null -w '%{http_code}' --max-time 8 \ -X POST \ -H "Content-Type: application/grpc" \ -H "TE: trailers" \ -H "authorization: Bearer FAKE_GRPC_TOKEN" \ --data-binary @- \ "$API_TARGET$ENDPOINT" 2>/dev/null) printf ' [%s] gRPC fake-auth → %s\n' "$CODE" "$ENDPOINT" | tee -a "$OUT_FILE" done echo "" | tee -a "$OUT_FILE" echo "=== 32. gRPC / Protobuf COMPLETE ===" | tee -a "$OUT_FILE"