From 9201ee38834dcf0004024b0f26ec5462be5347de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pavel=20B=C5=99ou=C5=A1ek?= <brousek@ics.muni.cz>
Date: Fri, 26 Apr 2024 21:56:18 +0200
Subject: [PATCH] feat: SP-initiated login using discovery response endpoint

---
 README.md                                     |   8 +-
 ...ml-detect-discovery-response-endpoint.yaml | 145 ++++++++++++++++++
 ...=> saml-detect-request-init-endpoint.yaml} |   6 +-
 workflow/saml-discovery-response.yaml         |  10 ++
 ...p-workflow.yaml => saml-request-init.yaml} |   8 +-
 ...saml-signature-discovery-response-raw.yaml |  82 ++++++++++
 6 files changed, 248 insertions(+), 11 deletions(-)
 create mode 100644 workflow/saml-detect-discovery-response-endpoint.yaml
 rename workflow/{saml-detect-endpoints.yaml => saml-detect-request-init-endpoint.yaml} (92%)
 create mode 100644 workflow/saml-discovery-response.yaml
 rename workflow/{saml-sp-workflow.yaml => saml-request-init.yaml} (77%)
 create mode 100644 workflow/saml-signature-discovery-response-raw.yaml

diff --git a/README.md b/README.md
index cea5109..7310cd1 100644
--- a/README.md
+++ b/README.md
@@ -123,15 +123,16 @@ where
 
 Peforming SP-initiated login is more difficult than the IdP-initiated login used in previous templates.
 
-You can use the included [nuclei workflow](https://docs.projectdiscovery.io/templates/workflows/overview)
-to scan a server for available endpoints and perform SP-initiated login if possible:
+You can use the included [nuclei workflows](https://docs.projectdiscovery.io/templates/workflows/overview)
+to scan a server for available endpoints and perform SP-initiated login if possible.
 
 ```sh
-nuclei -w workflow/saml-sp-workflow.yaml -u https://sp.example.com/ -code -sf secret-file.yaml
+nuclei -w workflow/<workflow_name>.yaml -u https://sp.example.com/ -code -sf secret-file.yaml -c 1
 ```
 
 where
 
+* `<workflow_name>` is either `saml-request-init` for request initiation endpoint or `saml-discovery-response` for discovery response endpoint
 * `https://sp.example.com/` is the address of a (potential) SP server (only the domain is relevant)
 
 ## Current limitations
@@ -143,3 +144,4 @@ where
 * only SPs which accept unsolicited logins (IdP-initiated)
   or with a [Request Initiation](https://docs.oasis-open.org/security/saml/Post2.0/sstc-request-initiation.html) endpoint can be tested
 * headless browser test behaves differently than raw HTTP test (nuclei limitation)
+* custom `returnIDParam` is not supported in discovery response endpoint detection
diff --git a/workflow/saml-detect-discovery-response-endpoint.yaml b/workflow/saml-detect-discovery-response-endpoint.yaml
new file mode 100644
index 0000000..8d35bda
--- /dev/null
+++ b/workflow/saml-detect-discovery-response-endpoint.yaml
@@ -0,0 +1,145 @@
+id: saml-detect-discovery-response-endpoint
+info:
+  name: SAML discovery response endpoint detection
+  author: T&I Incubator, GÉANT
+  severity: info
+  tags: saml,detect,tech
+
+variables:
+  CONFORMANCE_IDP_ENTITY_ID: "urn:x-simplesamlphp:geant:incubator:conformance-idp"
+
+http:
+  - method: GET
+    path:
+      - "{{RootURL}}{{discoresponseendpoint}}?entityID={{url_encode(CONFORMANCE_IDP_ENTITY_ID)}}"
+    redirects: false
+    attack: batteringram
+    payloads:
+      discoresponseendpoint:
+        - ""
+        - "/"
+        - "/AccountSaml/SignIn/"
+        - "/account/shibboleth/Login"
+        - "/adfs/ls/IdpInitiatedSignon.aspx"
+        - "/api/login/sso"
+        - "/auth/saml/meta"
+        - "/auth/saml/meta.xml"
+        - "/auth-saml/saml/login"
+        - "/auth/shibboleth.sso/ds"
+        - "/auth/shibexternal/Shibboleth.sso/DS"
+        - "/auth/shibexternal/Shibboleth.sso/Login"
+        - "/controls/login/AssertionConsumerService.aspx"
+        - "/edugain/disco"
+        - "/Edugain/disco"
+        - "/edugainlogin/Shibboleth.sso/Login"
+        - "/edugain/Shibboleth.sso/Login"
+        - "/eduid/Shibboleth.sso/Login"
+        - "/fastUnit/jsp/echo.do/Shibboleth.sso/Login"
+        - "/fastUnit/jsp/fast2.do/Shibboleth.sso/Login"
+        - "/FedAuth/Shibboleth/AssertionService"
+        - "/federation-manager/Shibboleth.sso/Login"
+        - "/fedreg/auth/login"
+        - "/grouper/grouperExternal/Shibboleth.sso/Login"
+        - "/hcc/Shibboleth.sso/Login"
+        - "/identity/self-service/bay/autologin.jsf/Shibboleth.sso/Login"
+        - "/idp/module.php/saml/disco.php"
+        - "/incommon/disco"
+        - "/linking/Shibboleth.sso/Login"
+        - "/lists/Shibboleth.sso/Login"
+        - "/login"
+        - "/login/saml/discovery"
+        - "/module.php/saml/sp/discoresp.php"
+        - "/oa/disco-ret"
+        - "/oidc/Shibboleth.sso/Login"
+        - "/otptokens/Shibboleth.sso/Login"
+        - "/ready/saml/meta.xml"
+        - "/research-and-scholarship/Shibboleth.sso/Login"
+        - "/research-and-scholarship/Shibboleth.sso/SeamlessAccess"
+        - "/research-and-scholarship/Shibboleth.sso/Wayfinder"
+        - "/SAML/AssertionConsumerService.aspx"
+        - "/saml/disco"
+        - "/saml/discover"
+        - "/saml/discovered"
+        - "/saml/login"
+        - "/SAML/Login"
+        - "/saml/module.php/saml/sp/discoresp.php"
+        - "/saml/2/auth"
+        - "/saml2/disco"
+        - "/Saml2/disco"
+        - "/Saml2_idps/disco"
+        - "/saml2/login"
+        - "/saml2sp/disco"
+        - "/Saml2SP/disco"
+        - "/secureSpace/jsp/fast2.do/Shibboleth.sso/Login"
+        - "/shibboleth"
+        - "/shibboleth/assert"
+        - "/shibboleth/Shibboleth.sso/Login"
+        - "/shibboleth-sp/Shibboleth.sso/DS"
+        - "/shibboleth-sp/Shibboleth.sso/Login"
+        - "/Shibboleth.sso/discovery"
+        - "/Shibboleth.sso/Discovery"
+        - "/shibboleth.sso/DS"
+        - "/Shibboleth.sso/DS"
+        - "/Shibboleth.sso/DS/Login"
+        - "/Shibboleth.sso/DS/seamless-access"
+        - "/Shibboleth.sso/DS/thiss.io"
+        - "/Shibboleth.sso/EDS"
+        - "/Shibboleth.sso/edugain"
+        - "/Shibboleth.sso/eduGAIN"
+        - "/Shibboleth.sso/EduGain"
+        - "/Shibboleth.sso/geant"
+        - "/-shibboleth.sso/Login"
+        - "/shibboleth.sso/Login"
+        - "/Shibboleth.sso/Login"
+        - "/Shibboleth.sso/login/shibboleth"
+        - "/Shibboleth.sso/MFA"
+        - "/Shibboleth.sso/SAML/POST"
+        - "/Shibboleth.sso/SeamlessAccess"
+        - "/Shibboleth.sso/SeamlessLogin"
+        - "/Shibboleth.sso/WAYF"
+        - "/Shibboleth.sso/Wayfinder"
+        - "/shib/sp/Login"
+        - "/simplesaml/module.php/saml/disco.php"
+        - "/simplesaml/module.php/saml/sp/discoresp.php"
+        - "/simplesamlphp/module.php/saml/sp/discoresp.php"
+        - "/sp/disco"
+        - "/sso/login"
+        - "/sso/module.php/saml/sp/discoresp.php"
+        - "/wayf/acs/post/disco"
+    stop-at-first-match: true
+    matchers:
+      - type: dsl
+        name: hasdiscoresponseendpoint
+        condition: and
+        dsl:
+          - "status_code == 302 || status_code == 303 || status_code == 307"
+          - "contains(location, 'SAMLRequest=')"
+    extractors:
+      - type: dsl
+        name: discoresponseendpoint
+        dsl:
+          - "discoresponseendpoint"
+
+code:
+  - engine:
+      - python3
+      - python
+      - py
+    source: |
+      import base64
+      import zlib
+      import os
+      import re
+      from urllib.parse import unquote
+
+      saml_request = unquote(re.sub(r'^.*SAMLRequest=(.*?)(&|$).*', r'\1', os.getenv("http_location")))
+      saml_request = zlib.decompress(base64.b64decode(saml_request), -zlib.MAX_WBITS).decode()
+      SP_ENTITY_ID = re.sub(r'^.*>(.*?)</\w+:Issuer.*$', r'\1', saml_request)
+      print(SP_ENTITY_ID)
+
+    extractors:
+      - type: dsl
+        name: SP_ENTITY_ID
+        dsl:
+          - response
+# digest: 4b0a00483046022100fe212de6b150396eef2dfbbca59589d70d206703594a8bb409aeea11138f678f022100a7f081c73485c4ff94a2a95f7d0f91d86e3c2ca62b252b7899295be1ff097b08:16ea744faf4d9956cf0107f1f3cb7c4c
\ No newline at end of file
diff --git a/workflow/saml-detect-endpoints.yaml b/workflow/saml-detect-request-init-endpoint.yaml
similarity index 92%
rename from workflow/saml-detect-endpoints.yaml
rename to workflow/saml-detect-request-init-endpoint.yaml
index aeb52c2..8542d5f 100644
--- a/workflow/saml-detect-endpoints.yaml
+++ b/workflow/saml-detect-request-init-endpoint.yaml
@@ -1,6 +1,6 @@
-id: saml-detect-endpoints
+id: saml-detect-request-init-endpoint
 info:
-  name: SAML endpoints detection
+  name: SAML request initiation endpoint detection
   author: T&I Incubator, GÉANT
   severity: info
   tags: saml,detect,tech
@@ -104,4 +104,4 @@ code:
         name: SP_ENTITY_ID
         dsl:
           - response
-# digest: 4a0a0047304502205ecec927cfdf45f5d2711393ed7e8321517f98a969858659a1aa75db26d0b23b022100a390a5522fef78825412f4b60c38abb0f7f41d6f5ac970b24bb5b4c0308887ad:16ea744faf4d9956cf0107f1f3cb7c4c
\ No newline at end of file
+# digest: 4a0a00473045022065227d49bc58bd1a4583141f1568931f48e2add5a3fa2d2409624400040933b6022100e1fb29bd4bc84c2c353e214e2f13cdd8521d35a3d4f685d50045b11addfa8189:16ea744faf4d9956cf0107f1f3cb7c4c
\ No newline at end of file
diff --git a/workflow/saml-discovery-response.yaml b/workflow/saml-discovery-response.yaml
new file mode 100644
index 0000000..dc0a8ad
--- /dev/null
+++ b/workflow/saml-discovery-response.yaml
@@ -0,0 +1,10 @@
+id: saml-discovery-response
+info:
+  name: SAML signature check using discovery response endpoint
+  author: T&I Incubator
+  description: Scan SAML SPs for SAML signature validation problems and vulnerabilities
+  severity: high
+workflows:
+  - template: workflow/saml-detect-discovery-response-endpoint.yaml
+    subtemplates:
+      - template: workflow/saml-signature-discovery-response-raw.yaml
diff --git a/workflow/saml-sp-workflow.yaml b/workflow/saml-request-init.yaml
similarity index 77%
rename from workflow/saml-sp-workflow.yaml
rename to workflow/saml-request-init.yaml
index 8d8d079..c74c90e 100644
--- a/workflow/saml-sp-workflow.yaml
+++ b/workflow/saml-request-init.yaml
@@ -1,11 +1,11 @@
-id: saml-sp-workflow
+id: saml-request-init
 info:
-  name: SAML signature checks
+  name: SAML signature check using request initiation endpoint
   author: T&I Incubator
   description: Scan SAML SPs for SAML signature validation problems and vulnerabilities
   severity: high
 workflows:
-  - template: workflow/saml-detect-endpoints.yaml
+  - template: workflow/saml-detect-request-init-endpoint.yaml
     subtemplates:
       - template: workflow/saml-signature-request-init-raw.yaml
     # unfortunatelly it seems that is not possible to combine matchers and shared execution context like this:
@@ -15,5 +15,3 @@ workflows:
           #- template: nuclei-templates/saml-signature-request-init-raw.yaml
             #subtemplates:
             #  - template: nuclei-templates/saml-signature-request-init-invalid-signatures.yaml
-          #- tags: ssl,tls,dns,tech,detect
-  - tags: ssl,tls,dns,tech,detect
diff --git a/workflow/saml-signature-discovery-response-raw.yaml b/workflow/saml-signature-discovery-response-raw.yaml
new file mode 100644
index 0000000..3271740
--- /dev/null
+++ b/workflow/saml-signature-discovery-response-raw.yaml
@@ -0,0 +1,82 @@
+id: saml-signature-discovery-response-raw
+info:
+  name: SP initiated SAML authentication endpoint check
+  author: T&I Incubator, GÉANT
+  severity: high
+  tags: saml,signature,raw
+variables:
+  CONFORMANCE_IDP_BASE_URL: https://conformance-idp.maiv1.incubator.geant.org/
+  CONFORMANCE_IDP_HOSTNAME: '{{replace_regex(CONFORMANCE_IDP_BASE_URL, "^https?://|/.*$", "")}}'
+  CONFORMANCE_IDP_ENTITY_ID: "urn:x-simplesamlphp:geant:incubator:conformance-idp"
+  TEST_CASES:
+    - noSignature
+    - invalidSignature
+
+flow: |
+  set("TEST_CASE", "standardResponse");
+  http();
+  for (let testcase of iterate(template["TEST_CASES"])) {
+    set("TEST_CASE", testcase);
+    http();
+  }
+
+http:
+  - raw:
+      - |
+        @Host: https://{{CONFORMANCE_IDP_HOSTNAME}}
+        POST /module.php/conformance/test/setup?testId={{url_encode(TEST_CASE)}}&spEntityId={{url_encode(SP_ENTITY_ID)}} HTTP/1.1
+        Host: {{CONFORMANCE_IDP_HOSTNAME}}
+
+      - |
+        GET {{discoresponseendpoint}}?entityID={{url_encode(CONFORMANCE_IDP_ENTITY_ID)}} HTTP/1.1
+        Host: {{Hostname}}
+
+      # HTTP-POST binding
+      - |
+        @Host: {{replace_regex(trim(acs,"[]"), "^https?://|/.*$", "")}}
+        POST {{trim(acs,"[]")}} HTTP/1.1
+        Host: {{replace_regex(trim(acs,"[]"), "^https?://|/.*$", "")}}
+        Content-Type: application/x-www-form-urlencoded
+
+        SAMLResponse={{url_encode(trim(samlresponse,"[]"))}}
+
+    disable-path-automerge: true
+    redirects: true
+    extractors:
+      - type: xpath
+        name: acs
+        internal: true
+        attribute: action
+        xpath:
+          - '/html/body/form'
+      - type: xpath
+        name: samlresponse
+        internal: true
+        attribute: value
+        xpath:
+          - '/html/body/form/input[2]'
+      # breaks the template :(
+      #- type: dsl
+      #  dsl:
+      #    - 'TEST_CASE'
+    matchers-condition: and
+    matchers:
+      - type: status
+        condition: or
+        status:
+          - 200
+          - 302
+          - 303
+      - type: word
+        words:
+          - Welcome
+          - Hello
+          - Logged in
+        condition: or
+        part: body
+      - type: dsl
+        dsl:
+          # ignore the happy case
+          - '!contains(TEST_CASE, "standardResponse")'
+        condition: and
+
-- 
GitLab