feat(modules): add hpe ilo, dell idrac and redfish exposure modules (#296)

This commit is contained in:
Tigah
2026-07-22 12:47:27 -07:00
committed by GitHub
parent 49e41e4d0f
commit f658fc2466
6 changed files with 387 additions and 0 deletions
@@ -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))
}
})
}
@@ -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 := `<?xml version="1.0"?><RIMP><HSI><SBSN>CZC1234ABC </SBSN><SPN>ProLiant DL380 Gen10</SPN>` +
`<UUID>31393736-3935-435A-4331-323334414243</UUID><SP>1</SP><cUUID>00000000-0000-0000-0000-000000000000</cUUID>` +
`<VIRTUAL><STATE>Inactive</STATE><VID><BSN></BSN><cUUID></cUUID></VID></VIRTUAL></HSI>` +
`<MP><ST>1</ST><PN>Integrated Lights-Out 5 (iLO 5)</PN><FWRI>2.44</FWRI><HWRI>ASIC: 17</HWRI>` +
`<SN>ILO1234567890 </SN><UUID>ILO000000000000</UUID><IPM>1</IPM><SSO>0</SSO><PWRM>3.4</PWRM></MP></RIMP>`
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 := `<?xml version="1.0"?><RIMP><HSI><SBSN>FAKE</SBSN></HSI><MP><PN>Generic Management Card</PN></MP></RIMP>`
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 := `<html><body>Our support team can help configure Integrated Lights-Out on your HPE server.</body></html>`
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))
}
})
}
@@ -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 := `<html><body>The Redfish service root exposes a RedfishVersion and a Managers collection for BMC discovery.</body></html>`
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))
}
})
}
+40
View File
@@ -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
@@ -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
@@ -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:
- "<RIMP>"
- "<HSI>"
- "<MP>"
- type: word
part: body
words:
- "Integrated Lights-Out"
extractors:
- type: regex
name: ilo_server_serial
part: body
regex:
- '<SBSN>\s*([^<]*?)\s*</SBSN>'
group: 1
- type: regex
name: ilo_server_product
part: body
regex:
- '<SPN>([^<]+)</SPN>'
group: 1
- type: regex
name: ilo_firmware
part: body
regex:
- '<FWRI>([^<]+)</FWRI>'
group: 1