Skip to content

Discovery

Before attacking a web service you need its schema. SOAP services publish WSDL; REST APIs sometimes publish OpenAPI/Swagger; GraphQL exposes introspection. Each has known locations and a discovery process.

# 1. WSDL discovery - try the canonical paths
curl http://target:3002/wsdl # explicit endpoint
curl http://target:3002/?wsdl # query parameter on root
curl http://target:3002/service.asmx?wsdl # .NET ASMX
curl http://target:3002/service?wsdl # JAX-WS Java
dirb http://target:3002/ # general fuzz for /wsdl, /soap, .asmx
# 2. WSDL hidden behind a parameter (encountered: /wsdl returns empty)
ffuf -w params.txt -u 'http://target:3002/wsdl?FUZZ' -fs 0
# → finds ?wsdl=... or ?disco=... triggers
# 3. OpenAPI / Swagger
curl http://target/swagger.json
curl http://target/openapi.json
curl http://target/api/v2/api-docs
curl http://target/swagger-ui/
# 4. GraphQL introspection
curl -X POST http://target/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{__schema{types{name fields{name}}}}"}'
# 5. Read the schema → enumerate every operation and its parameters

Success indicator: a complete catalog of operations/endpoints the service exposes, each with its parameter list and authentication requirement (if documented).

A WSDL (Web Services Description Language) file is an XML schema that describes everything a SOAP service exposes: every operation, every parameter, every data type, the endpoint location, the supported transports. From the operator’s perspective, finding the WSDL is finding the attack surface.

Most SOAP services expose their WSDL via one of these patterns. Try in order:

Terminal window
$ curl -i http://target:3002/wsdl # explicit path (Node.js soap, custom)
$ curl -i http://target:3002/?wsdl # root with query param
$ curl -i http://target:3002/wsdl?wsdl # nested - path and query (some frameworks)
$ curl -i http://target:3002/service.asmx?wsdl # ASP.NET ASMX
$ curl -i http://target:3002/service?wsdl # JAX-WS Java
$ curl -i http://target:3002/services/X?wsdl # Apache CXF
$ curl -i http://target:3002/ws/service?wsdl # Spring WS
$ curl -i http://target:3002/cxf/service?wsdl # CXF on custom path
$ curl -i http://target:3002/Service1.svc?wsdl # WCF
$ curl -i http://target:3002/Service1.svc?singleWsdl # WCF flattened
$ curl -i http://target:3002/api/service?wsdl # WSDL behind API gateway

Response patterns:

StatusBodyInterpretation
200XML starting <wsdl:definitionsWSDL found - start reading
200Empty body or Content-Length: 0Endpoint exists but WSDL not exposed here - parameter fuzz next
200HTMLWrong endpoint; this is a regular page
404AnythingPath not present; try next pattern
401/403AnythingPath exists but requires auth
500AnythingEndpoint exists but malformed request - try ?wsdl=1 or similar variants

When the canonical paths fail, brute-force common service paths:

Terminal window
$ dirb http://target:3002/
-----------------
DIRB v2.22
-----------------
START_TIME: ...
URL_BASE: http://target:3002/
---- Scanning URL: http://target:3002/ ----
+ http://target:3002/wsdl (CODE:200|SIZE:0)

Or ffuf with a wordlist:

Terminal window
$ ffuf -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
-u 'http://target:3002/FUZZ' \
-mc 200,301,302,401,403

Useful wordlists for service discovery:

  • SecLists/Discovery/Web-Content/common.txt - generic web paths
  • SecLists/Discovery/Web-Content/api/api-endpoints.txt - API paths
  • SecLists/Discovery/Web-Content/api/api-endpoints-mazen160.txt - curated API list
  • SecLists/Discovery/Web-Content/raft-medium-words.txt - broader

A common pattern: /wsdl exists but returns empty. The trigger is a query parameter:

Terminal window
$ curl http://target:3002/wsdl
(empty)
# Fuzz parameter names
$ ffuf -w SecLists/Discovery/Web-Content/burp-parameter-names.txt \
-u 'http://target:3002/wsdl?FUZZ' \
-fs 0 -mc 200
...
wsdl [Status: 200, Size: 4461, Words: 967, Lines: 186]

-fs 0 filters out empty responses (the default). When a parameter name flips the response to a non-empty body, that’s the trigger.

Typical magic parameters:

ParameterUsed by
?wsdlStandard WSDL request convention
?discoMicrosoft DISCO (discovery file)
?singleWsdlWCF flattened WSDL (single file, all imports inlined)
?mexWCF metadata exchange endpoint
?xsd=xsd0XML schema fragments (when WSDL imports XSDs)
?xsdSchema discovery
?helpSome frameworks expose a help page with schema links

The Microsoft web service ecosystem has its own conventions:

Terminal window
# ASMX (ASP.NET legacy)
$ curl http://target/Service1.asmx # HTML help page listing operations
$ curl http://target/Service1.asmx?wsdl # WSDL
$ curl http://target/Service1.asmx?disco # DISCO (older discovery format)
# WCF (modern .NET)
$ curl http://target/Service1.svc # text page, often
$ curl http://target/Service1.svc?wsdl # WSDL
$ curl http://target/Service1.svc?singleWsdl # flattened (combines imports)
$ curl http://target/Service1.svc/mex # metadata exchange endpoint

The HTML help page for ASMX is often the easiest start - visit /Service1.asmx in a browser and Microsoft’s auto-generated documentation lists every operation, its parameters, and sample SOAP envelopes you can copy verbatim.

A complete WSDL file looks intimidating but breaks down into six predictable sections. Walking through an example:

<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions targetNamespace="http://tempuri.org/"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://tempuri.org/">
<wsdl:types>...</wsdl:types>
<wsdl:message name="LoginSoapIn">...</wsdl:message>
<wsdl:portType name="HacktheBoxSoapPort">...</wsdl:portType>
<wsdl:binding name="HacktheboxServiceSoapBinding" type="tns:HacktheBoxSoapPort">...</wsdl:binding>
<wsdl:service name="HacktheboxService">...</wsdl:service>
</wsdl:definitions>

Six sections - only three matter for the operator:

Defines the XML schema for every parameter the service accepts. Read this to know parameter names and types:

<wsdl:types>
<s:schema elementFormDefault="qualified" targetNamespace="http://tempuri.org/">
<s:element name="LoginRequest">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="username" type="s:string"/>
<s:element minOccurs="1" maxOccurs="1" name="password" type="s:string"/>
</s:sequence>
</s:complexType>
</s:element>
<s:element name="ExecuteCommandRequest">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="cmd" type="s:string"/>
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</wsdl:types>

From this, you know:

  • LoginRequest takes username (string) and password (string)
  • ExecuteCommandRequest takes cmd (string)

This is the parameter catalog. Each <s:element name="X"> is a request you can construct.

Lists every operation the service exposes:

<wsdl:portType name="HacktheBoxSoapPort">
<wsdl:operation name="Login">
<wsdl:input message="tns:LoginSoapIn"/>
<wsdl:output message="tns:LoginSoapOut"/>
</wsdl:operation>
<wsdl:operation name="ExecuteCommand">
<wsdl:input message="tns:ExecuteCommandSoapIn"/>
<wsdl:output message="tns:ExecuteCommandSoapOut"/>
</wsdl:operation>
</wsdl:portType>

From this, two operations are available: Login and ExecuteCommand. Both have request/response message pairs. This is the attack surface - every operation listed is an endpoint you can probe.

Tells you where the service actually lives:

<wsdl:service name="HacktheboxService">
<wsdl:port name="HacktheboxServiceSoapPort" binding="tns:HacktheboxServiceSoapBinding">
<soap:address location="http://localhost:80/wsdl"/>
</wsdl:port>
</wsdl:service>

The <soap:address location="..."> is the URL to POST requests to. Often it’s the same path that served the WSDL itself - e.g., /wsdl here. Note: the value sometimes points to localhost:80 even when the service is on a different port/host, which is a misconfiguration (and a hint that the WSDL was generated server-side without rewriting the URL). The actual endpoint is wherever you fetched the WSDL from.

<wsdl:message> mirrors the types section. <wsdl:binding> describes the transport encoding (almost always SOAP-over-HTTP with style="document" for modern services). Skip both unless you’re debugging unusual transport.

Inside <wsdl:binding>, each operation declares its expected SOAPAction HTTP header value:

<wsdl:operation name="ExecuteCommand">
<soap:operation soapAction="ExecuteCommand" style="document"/>
...
</wsdl:operation>

The soapAction="ExecuteCommand" is the string the client should send in SOAPAction: "ExecuteCommand". Note the quotation marks in the HTTP header - by SOAP spec the value is a quoted URI (often just a name, sometimes a full URL like http://target/Service/ExecuteCommand).

This duplication of operation name (body element <ExecuteCommand> + header SOAPAction: "ExecuteCommand") is the structural setup for SOAPAction spoofing.

For REST APIs, the equivalent of WSDL is OpenAPI (formerly Swagger):

Terminal window
# Direct schema documents
$ curl http://target/swagger.json
$ curl http://target/swagger.yaml
$ curl http://target/openapi.json
$ curl http://target/openapi.yaml
$ curl http://target/v2/api-docs # Swagger 2 (Springfox default)
$ curl http://target/v3/api-docs # Swagger 3
$ curl http://target/api/v1/swagger.json
$ curl http://target/api/v2/swagger.json
# Swagger UI (browsable)
$ curl http://target/swagger-ui/
$ curl http://target/swagger-ui/index.html
$ curl http://target/api/swagger-ui/
$ curl http://target/swagger/
# Redoc UI
$ curl http://target/redoc/
$ curl http://target/api/redoc/
# Apidoc / other variants
$ curl http://target/apidocs/
$ curl http://target/api-docs/

When you find a Swagger UI, it gives you a browsable, sortable list of every operation with try-it-out buttons. For automation:

Terminal window
# Generate curl commands for every endpoint
$ swagger-codegen generate -i http://target/swagger.json -l bash

Sometimes developers leave Postman collections checked into a public repo or accessible at predictable paths:

Terminal window
$ curl http://target/postman.json
$ curl http://target/postman_collection.json
$ curl http://target/.postman/

A leaked Postman collection includes example requests, authentication patterns, and sometimes hardcoded API keys. Worth probing.

GraphQL APIs typically (and often unintentionally) expose their entire schema via the introspection query:

Terminal window
$ curl -X POST http://target/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{__schema{queryType{name}mutationType{name}types{name kind fields{name type{name kind ofType{name kind}}}}}}"}' \
| jq

The response is the full schema - every type, every field, every query and mutation operation, all their argument types. Once you have it:

Terminal window
# Tools for browsing introspected schemas
$ npx graphql-voyager # interactive graph visualization
$ npx altair # GraphQL client
$ inql -t http://target/graphql -o /tmp/graphql-recon/ # Burp extension's CLI form

GraphQL also has standard discovery paths:

/graphql /graphiql /api/graphql /v1/graphql
/playground /__graphql /altair

Each may host either the GraphQL endpoint itself or an interactive UI for it.

Disabled introspection blocks {__schema{...}} queries but the operations themselves still work - you just don’t know their names. Brute-force them via wordlists:

Terminal window
$ ffuf -w graphql-wordlist.txt \
-X POST -d '{"query":"{ FUZZ { id } }"}' \
-H 'Content-Type: application/json' \
-u http://target/graphql \
-fs 47 # filter the "field FUZZ doesn't exist" response size

Common GraphQL operation names: user, users, me, admin, account, profile, query, viewer, currentUser, getUsers, listUsers, searchUsers.

gRPC over HTTP/2 uses Protobuf and is harder to discover without .proto files:

Terminal window
# Server reflection (the gRPC equivalent of introspection)
$ grpcurl -plaintext target:50051 list
$ grpcurl -plaintext target:50051 list <SERVICE>
$ grpcurl -plaintext target:50051 describe <METHOD>

When reflection is enabled, grpcurl list returns every service name. describe returns the protobuf-style schema. When reflection is disabled, you need the .proto files (often in the project’s repo or a developer’s machine).

The HTB-style scenario: a SOAP service at http://target:3002 with WSDL hidden.

Terminal window
# Step 1 - Probe canonical paths
$ curl -i http://target:3002/ # 404
$ curl -i http://target:3002/wsdl # 200 but empty
$ curl -i http://target:3002/?wsdl # 404
$ curl -i http://target:3002/service.asmx?wsdl # 404
# Step 2 - /wsdl returns empty; parameter-fuzz it
$ ffuf -w SecLists/Discovery/Web-Content/burp-parameter-names.txt \
-u 'http://target:3002/wsdl?FUZZ' \
-fs 0 -mc 200
wsdl [Status: 200, Size: 4461, Words: 967, Lines: 186]
# Step 3 - Magic parameter is "wsdl"
$ curl http://target:3002/wsdl?wsdl
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions targetNamespace="http://tempuri.org/" ...>
<wsdl:types>
<s:element name="LoginRequest">
<s:element name="username" type="s:string"/>
<s:element name="password" type="s:string"/>
</s:element>
<s:element name="ExecuteCommandRequest">
<s:element name="cmd" type="s:string"/>
</s:element>
</wsdl:types>
...
</wsdl:definitions>
# Step 4 - Catalog
# Operations: Login, ExecuteCommand
# Endpoints: /wsdl (per <soap:address>)
# Parameters: username, password (Login); cmd (ExecuteCommand)

Now you have the schema. Move to SOAP basics for crafting requests, or directly to SOAPAction spoofing if the canonical attack applies.

TaskPattern
Try standard WSDL paths/wsdl, /?wsdl, /wsdl?wsdl, .asmx?wsdl, .svc?wsdl, .svc?singleWsdl
Directory fuzz for service pathdirb http://target:PORT/ or ffuf -w common.txt -u http://target/FUZZ
Parameter fuzz on a known WSDL pathffuf -w burp-parameter-names.txt -u 'http://target/wsdl?FUZZ' -fs 0
Magic parameters to try?wsdl, ?disco, ?singleWsdl, ?mex, ?xsd=xsd0
ASMX HTML help pageVisit /Service.asmx in browser
WCF metadata exchange/Service.svc/mex or /Service.svc?wsdl
OpenAPI schema location/swagger.json, /openapi.json, /v2/api-docs, /v3/api-docs
Swagger UI/swagger-ui/, /swagger/, /api/docs/
Generate clients from OpenAPIswagger-codegen generate -i schema.json -l python
GraphQL introspectionPOST /graphql with {"query":"{__schema{types{name}}}"}
GraphQL UI paths/graphql, /graphiql, /playground, /altair
GraphQL when introspection blockedWordlist brute via ffuf
gRPC reflectiongrpcurl -plaintext target:port list
Read WSDL types sectionMaps to parameter names and types
Read WSDL portType sectionMaps to operation list (attack surface)
Read WSDL service sectionMaps to endpoint URL
Read WSDL binding SOAPActionThe value to put in SOAPAction: HTTP header

For request crafting once you have the schema, continue to SOAP basics.

Defenses D3-RAPA