mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 14:37:01 -07:00
feat(modules): add hpe ilo, dell idrac and redfish exposure modules (#296)
This commit is contained in:
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user