From f658fc24662acd798f1d0c34c3d38dbe29366de1 Mon Sep 17 00:00:00 2001
From: Tigah <88289044+TBX3D@users.noreply.github.com>
Date: Wed, 22 Jul 2026 12:47:27 -0700
Subject: [PATCH] feat(modules): add hpe ilo, dell idrac and redfish exposure
modules (#296)
---
.../dell_idrac_bmc_info_exposure_test.go | 81 +++++++++++++++++
.../modules/hpe_ilo_xmldata_exposure_test.go | 87 +++++++++++++++++++
internal/modules/redfish_service_root_test.go | 82 +++++++++++++++++
modules/info/redfish-service-root.yaml | 40 +++++++++
.../recon/dell-idrac-bmc-info-exposure.yaml | 42 +++++++++
modules/recon/hpe-ilo-xmldata-exposure.yaml | 55 ++++++++++++
6 files changed, 387 insertions(+)
create mode 100644 internal/modules/dell_idrac_bmc_info_exposure_test.go
create mode 100644 internal/modules/hpe_ilo_xmldata_exposure_test.go
create mode 100644 internal/modules/redfish_service_root_test.go
create mode 100644 modules/info/redfish-service-root.yaml
create mode 100644 modules/recon/dell-idrac-bmc-info-exposure.yaml
create mode 100644 modules/recon/hpe-ilo-xmldata-exposure.yaml
diff --git a/internal/modules/dell_idrac_bmc_info_exposure_test.go b/internal/modules/dell_idrac_bmc_info_exposure_test.go
new file mode 100644
index 0000000..f4c5908
--- /dev/null
+++ b/internal/modules/dell_idrac_bmc_info_exposure_test.go
@@ -0,0 +1,81 @@
+package modules_test
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/vmfunc/sif/internal/modules"
+)
+
+func runIdracModule(t *testing.T, file string, status int, body string) *modules.Result {
+ t.Helper()
+ def, err := modules.ParseYAMLModule(file)
+ if err != nil {
+ t.Fatalf("parse %s: %v", file, err)
+ }
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(status)
+ _, _ = w.Write([]byte(body))
+ }))
+ defer srv.Close()
+
+ res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
+ Timeout: 5 * time.Second,
+ Threads: 2,
+ })
+ if err != nil {
+ t.Fatalf("execute %s: %v", file, err)
+ }
+ return res
+}
+
+func idracExtract(res *modules.Result, key string) string {
+ for _, f := range res.Findings {
+ if v := f.Extracted[key]; v != "" {
+ return v
+ }
+ }
+ return ""
+}
+
+func TestDellIdracBMCInfoExposureModule(t *testing.T) {
+ const idrac = "../../modules/recon/dell-idrac-bmc-info-exposure.yaml"
+
+ t.Run("a real idrac9 bmc info response is flagged with firmware and model", func(t *testing.T) {
+ body := `{"Attributes":{"BuildVersion":"21.07.00.00","FwVer":"5.10.10.00","SystemRev":"A00",` +
+ `"SystemID":"03BB","SystemModelName":"PowerEdge R740","ChassisModel":"PowerEdge R740"}}`
+ res := runIdracModule(t, idrac, 200, body)
+ if len(res.Findings) == 0 {
+ t.Fatal("expected an idrac bmc info finding")
+ }
+ if v := idracExtract(res, "idrac_firmware"); v != "5.10.10.00" {
+ t.Errorf("idrac_firmware=%q, want 5.10.10.00", v)
+ }
+ if v := idracExtract(res, "idrac_server_model"); v != "PowerEdge R740" {
+ t.Errorf("idrac_server_model=%q, want PowerEdge R740", v)
+ }
+ })
+
+ t.Run("a body with only SystemModelName and no BuildVersion is not flagged", func(t *testing.T) {
+ body := `{"SystemModelName":"PowerEdge R740","Notes":"decommissioned"}`
+ if res := runIdracModule(t, idrac, 200, body); len(res.Findings) > 0 {
+ t.Errorf("a partial body should not match, got %d findings", len(res.Findings))
+ }
+ })
+
+ t.Run("a generic build info page mentioning BuildVersion alone is not flagged", func(t *testing.T) {
+ body := `{"app":"internal-tool","BuildVersion":"1.2.3"}`
+ if res := runIdracModule(t, idrac, 200, body); len(res.Findings) > 0 {
+ t.Errorf("an unrelated BuildVersion-only body should not match, got %d findings", len(res.Findings))
+ }
+ })
+
+ t.Run("a 404 is not a leak", func(t *testing.T) {
+ if res := runIdracModule(t, idrac, 404, "not found"); len(res.Findings) > 0 {
+ t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
+ }
+ })
+}
diff --git a/internal/modules/hpe_ilo_xmldata_exposure_test.go b/internal/modules/hpe_ilo_xmldata_exposure_test.go
new file mode 100644
index 0000000..3effc6f
--- /dev/null
+++ b/internal/modules/hpe_ilo_xmldata_exposure_test.go
@@ -0,0 +1,87 @@
+package modules_test
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/vmfunc/sif/internal/modules"
+)
+
+func runIloModule(t *testing.T, file string, status int, body string) *modules.Result {
+ t.Helper()
+ def, err := modules.ParseYAMLModule(file)
+ if err != nil {
+ t.Fatalf("parse %s: %v", file, err)
+ }
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(status)
+ _, _ = w.Write([]byte(body))
+ }))
+ defer srv.Close()
+
+ res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
+ Timeout: 5 * time.Second,
+ Threads: 2,
+ })
+ if err != nil {
+ t.Fatalf("execute %s: %v", file, err)
+ }
+ return res
+}
+
+func iloExtract(res *modules.Result, key string) string {
+ for _, f := range res.Findings {
+ if v := f.Extracted[key]; v != "" {
+ return v
+ }
+ }
+ return ""
+}
+
+func TestHPEIloXMLDataExposureModule(t *testing.T) {
+ const ilo = "../../modules/recon/hpe-ilo-xmldata-exposure.yaml"
+
+ t.Run("a real ilo xmldata response is flagged with serial, product and firmware", func(t *testing.T) {
+ body := `CZC1234ABC ProLiant DL380 Gen10` +
+ `31393736-3935-435A-4331-323334414243100000000-0000-0000-0000-000000000000` +
+ `Inactive` +
+ `1Integrated Lights-Out 5 (iLO 5)2.44ASIC: 17` +
+ `ILO1234567890 ILO000000000000103.4`
+ res := runIloModule(t, ilo, 200, body)
+ if len(res.Findings) == 0 {
+ t.Fatal("expected an ilo xmldata finding")
+ }
+ if v := iloExtract(res, "ilo_server_serial"); v != "CZC1234ABC" {
+ t.Errorf("ilo_server_serial=%q, want CZC1234ABC", v)
+ }
+ if v := iloExtract(res, "ilo_server_product"); v != "ProLiant DL380 Gen10" {
+ t.Errorf("ilo_server_product=%q, want ProLiant DL380 Gen10", v)
+ }
+ if v := iloExtract(res, "ilo_firmware"); v != "2.44" {
+ t.Errorf("ilo_firmware=%q, want 2.44", v)
+ }
+ })
+
+ t.Run("an unrelated xml api with a generic RIMP-shaped body is not flagged", func(t *testing.T) {
+ body := `FAKEGeneric Management Card`
+ if res := runIloModule(t, ilo, 200, body); len(res.Findings) > 0 {
+ t.Errorf("a non-ilo RIMP-shaped body should not match, got %d findings", len(res.Findings))
+ }
+ })
+
+ t.Run("a page that merely mentions Integrated Lights-Out in prose is not flagged", func(t *testing.T) {
+ body := `
Our support team can help configure Integrated Lights-Out on your HPE server.`
+ if res := runIloModule(t, ilo, 200, body); len(res.Findings) > 0 {
+ t.Errorf("a prose mention should not match, got %d findings", len(res.Findings))
+ }
+ })
+
+ t.Run("a 404 is not a leak", func(t *testing.T) {
+ if res := runIloModule(t, ilo, 404, "not found"); len(res.Findings) > 0 {
+ t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
+ }
+ })
+}
diff --git a/internal/modules/redfish_service_root_test.go b/internal/modules/redfish_service_root_test.go
new file mode 100644
index 0000000..196e0df
--- /dev/null
+++ b/internal/modules/redfish_service_root_test.go
@@ -0,0 +1,82 @@
+package modules_test
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/vmfunc/sif/internal/modules"
+)
+
+func runRedfishModule(t *testing.T, file string, status int, body string) *modules.Result {
+ t.Helper()
+ def, err := modules.ParseYAMLModule(file)
+ if err != nil {
+ t.Fatalf("parse %s: %v", file, err)
+ }
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(status)
+ _, _ = w.Write([]byte(body))
+ }))
+ defer srv.Close()
+
+ res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
+ Timeout: 5 * time.Second,
+ Threads: 2,
+ })
+ if err != nil {
+ t.Fatalf("execute %s: %v", file, err)
+ }
+ return res
+}
+
+func redfishExtract(res *modules.Result, key string) string {
+ for _, f := range res.Findings {
+ if v := f.Extracted[key]; v != "" {
+ return v
+ }
+ }
+ return ""
+}
+
+func TestRedfishServiceRootModule(t *testing.T) {
+ const rf = "../../modules/info/redfish-service-root.yaml"
+
+ t.Run("a real redfish service root is flagged with its version", func(t *testing.T) {
+ body := `{"@odata.context":"/redfish/v1/$metadata#ServiceRoot.ServiceRoot","@odata.id":"/redfish/v1/",` +
+ `"@odata.type":"#ServiceRoot.v1_9_0.ServiceRoot","Id":"RootService","Name":"Root Service",` +
+ `"RedfishVersion":"1.9.0","UUID":"92384634-2938-2342-8820-489239905423",` +
+ `"Systems":{"@odata.id":"/redfish/v1/Systems"},"Chassis":{"@odata.id":"/redfish/v1/Chassis"},` +
+ `"Managers":{"@odata.id":"/redfish/v1/Managers"},"SessionService":{"@odata.id":"/redfish/v1/SessionService"}}`
+ res := runRedfishModule(t, rf, 200, body)
+ if len(res.Findings) == 0 {
+ t.Fatal("expected a redfish service root finding")
+ }
+ if v := redfishExtract(res, "redfish_version"); v != "1.9.0" {
+ t.Errorf("redfish_version=%q, want 1.9.0", v)
+ }
+ })
+
+ t.Run("a body with RedfishVersion and Managers but a non-ServiceRoot odata.type is not flagged", func(t *testing.T) {
+ body := `{"@odata.type":"#ManagerCollection.ManagerCollection","RedfishVersion":"1.9.0",` +
+ `"Managers":[{"@odata.id":"/redfish/v1/Managers/1"}]}`
+ if res := runRedfishModule(t, rf, 200, body); len(res.Findings) > 0 {
+ t.Errorf("a manager collection page should not match the service root, got %d findings", len(res.Findings))
+ }
+ })
+
+ t.Run("a docs page mentioning RedfishVersion in prose is not flagged", func(t *testing.T) {
+ body := `The Redfish service root exposes a RedfishVersion and a Managers collection for BMC discovery.`
+ if res := runRedfishModule(t, rf, 200, body); len(res.Findings) > 0 {
+ t.Errorf("a prose mention should not match, got %d findings", len(res.Findings))
+ }
+ })
+
+ t.Run("a 404 is not a leak", func(t *testing.T) {
+ if res := runRedfishModule(t, rf, 404, "not found"); len(res.Findings) > 0 {
+ t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
+ }
+ })
+}
diff --git a/modules/info/redfish-service-root.yaml b/modules/info/redfish-service-root.yaml
new file mode 100644
index 0000000..14edd51
--- /dev/null
+++ b/modules/info/redfish-service-root.yaml
@@ -0,0 +1,40 @@
+# Redfish Service Root Fingerprint Detection Module
+
+id: redfish-service-root
+info:
+ name: Redfish Service Root
+ author: sif
+ severity: info
+ description: Detects a reachable DMTF Redfish service root, confirming an out-of-band BMC management API is exposed on the network even though the root document is spec-mandated to answer without authentication
+ tags: [redfish, bmc, dmtf, out-of-band, panel, detection]
+
+type: http
+
+http:
+ method: GET
+ paths:
+ - "{{BaseURL}}/redfish/v1/"
+
+ matchers:
+ - type: status
+ status:
+ - 200
+
+ - type: word
+ part: body
+ words:
+ - "RedfishVersion"
+ - "Managers"
+
+ - type: regex
+ part: body
+ regex:
+ - '"@odata\.type"\s*:\s*"#ServiceRoot\.'
+
+ extractors:
+ - type: regex
+ name: redfish_version
+ part: body
+ regex:
+ - '"RedfishVersion"\s*:\s*"([^"]+)"'
+ group: 1
diff --git a/modules/recon/dell-idrac-bmc-info-exposure.yaml b/modules/recon/dell-idrac-bmc-info-exposure.yaml
new file mode 100644
index 0000000..8ad76a7
--- /dev/null
+++ b/modules/recon/dell-idrac-bmc-info-exposure.yaml
@@ -0,0 +1,42 @@
+# Dell iDRAC Unauthenticated BMC Info Exposure Detection Module
+
+id: dell-idrac-bmc-info-exposure
+info:
+ name: Dell iDRAC Unauthenticated BMC Info Exposure
+ author: sif
+ severity: medium
+ description: Detects a Dell iDRAC9 sysmgmt bmc info endpoint answering without authentication, leaking the host server model and iDRAC firmware version
+ tags: [idrac, dell, bmc, out-of-band, exposure, recon]
+
+type: http
+
+http:
+ method: GET
+ paths:
+ - "{{BaseURL}}/sysmgmt/2015/bmc/info"
+
+ matchers:
+ - type: status
+ status:
+ - 200
+
+ - type: word
+ part: body
+ words:
+ - "BuildVersion"
+ - "SystemModelName"
+
+ extractors:
+ - type: regex
+ name: idrac_firmware
+ part: body
+ regex:
+ - '"FwVer"\s*:\s*"([^"]+)"'
+ group: 1
+
+ - type: regex
+ name: idrac_server_model
+ part: body
+ regex:
+ - '"SystemModelName"\s*:\s*"([^"]+)"'
+ group: 1
diff --git a/modules/recon/hpe-ilo-xmldata-exposure.yaml b/modules/recon/hpe-ilo-xmldata-exposure.yaml
new file mode 100644
index 0000000..b65cc4e
--- /dev/null
+++ b/modules/recon/hpe-ilo-xmldata-exposure.yaml
@@ -0,0 +1,55 @@
+# HPE iLO Unauthenticated XML Data Exposure Detection Module
+
+id: hpe-ilo-xmldata-exposure
+info:
+ name: HPE iLO Unauthenticated XML Data Exposure
+ author: sif
+ severity: high
+ description: Detects an HPE iLO management processor answering /xmldata?item=all without authentication, leaking the host server serial number, product name and iLO firmware revision
+ tags: [ilo, hpe, bmc, out-of-band, exposure, recon]
+
+type: http
+
+http:
+ method: GET
+ paths:
+ - "{{BaseURL}}/xmldata?item=all"
+
+ matchers:
+ - type: status
+ status:
+ - 200
+
+ - type: word
+ part: body
+ words:
+ - ""
+ - ""
+ - ""
+
+ - type: word
+ part: body
+ words:
+ - "Integrated Lights-Out"
+
+ extractors:
+ - type: regex
+ name: ilo_server_serial
+ part: body
+ regex:
+ - '\s*([^<]*?)\s*'
+ group: 1
+
+ - type: regex
+ name: ilo_server_product
+ part: body
+ regex:
+ - '([^<]+)'
+ group: 1
+
+ - type: regex
+ name: ilo_firmware
+ part: body
+ regex:
+ - '([^<]+)'
+ group: 1