Skip to content

Commit f658fc2

Browse files
authored
feat(modules): add hpe ilo, dell idrac and redfish exposure modules (#296)
1 parent 49e41e4 commit f658fc2

6 files changed

Lines changed: 387 additions & 0 deletions
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package modules_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
"time"
9+
10+
"github.com/vmfunc/sif/internal/modules"
11+
)
12+
13+
func runIdracModule(t *testing.T, file string, status int, body string) *modules.Result {
14+
t.Helper()
15+
def, err := modules.ParseYAMLModule(file)
16+
if err != nil {
17+
t.Fatalf("parse %s: %v", file, err)
18+
}
19+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+
w.WriteHeader(status)
21+
_, _ = w.Write([]byte(body))
22+
}))
23+
defer srv.Close()
24+
25+
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
26+
Timeout: 5 * time.Second,
27+
Threads: 2,
28+
})
29+
if err != nil {
30+
t.Fatalf("execute %s: %v", file, err)
31+
}
32+
return res
33+
}
34+
35+
func idracExtract(res *modules.Result, key string) string {
36+
for _, f := range res.Findings {
37+
if v := f.Extracted[key]; v != "" {
38+
return v
39+
}
40+
}
41+
return ""
42+
}
43+
44+
func TestDellIdracBMCInfoExposureModule(t *testing.T) {
45+
const idrac = "../../modules/recon/dell-idrac-bmc-info-exposure.yaml"
46+
47+
t.Run("a real idrac9 bmc info response is flagged with firmware and model", func(t *testing.T) {
48+
body := `{"Attributes":{"BuildVersion":"21.07.00.00","FwVer":"5.10.10.00","SystemRev":"A00",` +
49+
`"SystemID":"03BB","SystemModelName":"PowerEdge R740","ChassisModel":"PowerEdge R740"}}`
50+
res := runIdracModule(t, idrac, 200, body)
51+
if len(res.Findings) == 0 {
52+
t.Fatal("expected an idrac bmc info finding")
53+
}
54+
if v := idracExtract(res, "idrac_firmware"); v != "5.10.10.00" {
55+
t.Errorf("idrac_firmware=%q, want 5.10.10.00", v)
56+
}
57+
if v := idracExtract(res, "idrac_server_model"); v != "PowerEdge R740" {
58+
t.Errorf("idrac_server_model=%q, want PowerEdge R740", v)
59+
}
60+
})
61+
62+
t.Run("a body with only SystemModelName and no BuildVersion is not flagged", func(t *testing.T) {
63+
body := `{"SystemModelName":"PowerEdge R740","Notes":"decommissioned"}`
64+
if res := runIdracModule(t, idrac, 200, body); len(res.Findings) > 0 {
65+
t.Errorf("a partial body should not match, got %d findings", len(res.Findings))
66+
}
67+
})
68+
69+
t.Run("a generic build info page mentioning BuildVersion alone is not flagged", func(t *testing.T) {
70+
body := `{"app":"internal-tool","BuildVersion":"1.2.3"}`
71+
if res := runIdracModule(t, idrac, 200, body); len(res.Findings) > 0 {
72+
t.Errorf("an unrelated BuildVersion-only body should not match, got %d findings", len(res.Findings))
73+
}
74+
})
75+
76+
t.Run("a 404 is not a leak", func(t *testing.T) {
77+
if res := runIdracModule(t, idrac, 404, "not found"); len(res.Findings) > 0 {
78+
t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
79+
}
80+
})
81+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package modules_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
"time"
9+
10+
"github.com/vmfunc/sif/internal/modules"
11+
)
12+
13+
func runIloModule(t *testing.T, file string, status int, body string) *modules.Result {
14+
t.Helper()
15+
def, err := modules.ParseYAMLModule(file)
16+
if err != nil {
17+
t.Fatalf("parse %s: %v", file, err)
18+
}
19+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+
w.WriteHeader(status)
21+
_, _ = w.Write([]byte(body))
22+
}))
23+
defer srv.Close()
24+
25+
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
26+
Timeout: 5 * time.Second,
27+
Threads: 2,
28+
})
29+
if err != nil {
30+
t.Fatalf("execute %s: %v", file, err)
31+
}
32+
return res
33+
}
34+
35+
func iloExtract(res *modules.Result, key string) string {
36+
for _, f := range res.Findings {
37+
if v := f.Extracted[key]; v != "" {
38+
return v
39+
}
40+
}
41+
return ""
42+
}
43+
44+
func TestHPEIloXMLDataExposureModule(t *testing.T) {
45+
const ilo = "../../modules/recon/hpe-ilo-xmldata-exposure.yaml"
46+
47+
t.Run("a real ilo xmldata response is flagged with serial, product and firmware", func(t *testing.T) {
48+
body := `<?xml version="1.0"?><RIMP><HSI><SBSN>CZC1234ABC </SBSN><SPN>ProLiant DL380 Gen10</SPN>` +
49+
`<UUID>31393736-3935-435A-4331-323334414243</UUID><SP>1</SP><cUUID>00000000-0000-0000-0000-000000000000</cUUID>` +
50+
`<VIRTUAL><STATE>Inactive</STATE><VID><BSN></BSN><cUUID></cUUID></VID></VIRTUAL></HSI>` +
51+
`<MP><ST>1</ST><PN>Integrated Lights-Out 5 (iLO 5)</PN><FWRI>2.44</FWRI><HWRI>ASIC: 17</HWRI>` +
52+
`<SN>ILO1234567890 </SN><UUID>ILO000000000000</UUID><IPM>1</IPM><SSO>0</SSO><PWRM>3.4</PWRM></MP></RIMP>`
53+
res := runIloModule(t, ilo, 200, body)
54+
if len(res.Findings) == 0 {
55+
t.Fatal("expected an ilo xmldata finding")
56+
}
57+
if v := iloExtract(res, "ilo_server_serial"); v != "CZC1234ABC" {
58+
t.Errorf("ilo_server_serial=%q, want CZC1234ABC", v)
59+
}
60+
if v := iloExtract(res, "ilo_server_product"); v != "ProLiant DL380 Gen10" {
61+
t.Errorf("ilo_server_product=%q, want ProLiant DL380 Gen10", v)
62+
}
63+
if v := iloExtract(res, "ilo_firmware"); v != "2.44" {
64+
t.Errorf("ilo_firmware=%q, want 2.44", v)
65+
}
66+
})
67+
68+
t.Run("an unrelated xml api with a generic RIMP-shaped body is not flagged", func(t *testing.T) {
69+
body := `<?xml version="1.0"?><RIMP><HSI><SBSN>FAKE</SBSN></HSI><MP><PN>Generic Management Card</PN></MP></RIMP>`
70+
if res := runIloModule(t, ilo, 200, body); len(res.Findings) > 0 {
71+
t.Errorf("a non-ilo RIMP-shaped body should not match, got %d findings", len(res.Findings))
72+
}
73+
})
74+
75+
t.Run("a page that merely mentions Integrated Lights-Out in prose is not flagged", func(t *testing.T) {
76+
body := `<html><body>Our support team can help configure Integrated Lights-Out on your HPE server.</body></html>`
77+
if res := runIloModule(t, ilo, 200, body); len(res.Findings) > 0 {
78+
t.Errorf("a prose mention should not match, got %d findings", len(res.Findings))
79+
}
80+
})
81+
82+
t.Run("a 404 is not a leak", func(t *testing.T) {
83+
if res := runIloModule(t, ilo, 404, "not found"); len(res.Findings) > 0 {
84+
t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
85+
}
86+
})
87+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package modules_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
"time"
9+
10+
"github.com/vmfunc/sif/internal/modules"
11+
)
12+
13+
func runRedfishModule(t *testing.T, file string, status int, body string) *modules.Result {
14+
t.Helper()
15+
def, err := modules.ParseYAMLModule(file)
16+
if err != nil {
17+
t.Fatalf("parse %s: %v", file, err)
18+
}
19+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
20+
w.WriteHeader(status)
21+
_, _ = w.Write([]byte(body))
22+
}))
23+
defer srv.Close()
24+
25+
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
26+
Timeout: 5 * time.Second,
27+
Threads: 2,
28+
})
29+
if err != nil {
30+
t.Fatalf("execute %s: %v", file, err)
31+
}
32+
return res
33+
}
34+
35+
func redfishExtract(res *modules.Result, key string) string {
36+
for _, f := range res.Findings {
37+
if v := f.Extracted[key]; v != "" {
38+
return v
39+
}
40+
}
41+
return ""
42+
}
43+
44+
func TestRedfishServiceRootModule(t *testing.T) {
45+
const rf = "../../modules/info/redfish-service-root.yaml"
46+
47+
t.Run("a real redfish service root is flagged with its version", func(t *testing.T) {
48+
body := `{"@odata.context":"/redfish/v1/$metadata#ServiceRoot.ServiceRoot","@odata.id":"/redfish/v1/",` +
49+
`"@odata.type":"#ServiceRoot.v1_9_0.ServiceRoot","Id":"RootService","Name":"Root Service",` +
50+
`"RedfishVersion":"1.9.0","UUID":"92384634-2938-2342-8820-489239905423",` +
51+
`"Systems":{"@odata.id":"/redfish/v1/Systems"},"Chassis":{"@odata.id":"/redfish/v1/Chassis"},` +
52+
`"Managers":{"@odata.id":"/redfish/v1/Managers"},"SessionService":{"@odata.id":"/redfish/v1/SessionService"}}`
53+
res := runRedfishModule(t, rf, 200, body)
54+
if len(res.Findings) == 0 {
55+
t.Fatal("expected a redfish service root finding")
56+
}
57+
if v := redfishExtract(res, "redfish_version"); v != "1.9.0" {
58+
t.Errorf("redfish_version=%q, want 1.9.0", v)
59+
}
60+
})
61+
62+
t.Run("a body with RedfishVersion and Managers but a non-ServiceRoot odata.type is not flagged", func(t *testing.T) {
63+
body := `{"@odata.type":"#ManagerCollection.ManagerCollection","RedfishVersion":"1.9.0",` +
64+
`"Managers":[{"@odata.id":"/redfish/v1/Managers/1"}]}`
65+
if res := runRedfishModule(t, rf, 200, body); len(res.Findings) > 0 {
66+
t.Errorf("a manager collection page should not match the service root, got %d findings", len(res.Findings))
67+
}
68+
})
69+
70+
t.Run("a docs page mentioning RedfishVersion in prose is not flagged", func(t *testing.T) {
71+
body := `<html><body>The Redfish service root exposes a RedfishVersion and a Managers collection for BMC discovery.</body></html>`
72+
if res := runRedfishModule(t, rf, 200, body); len(res.Findings) > 0 {
73+
t.Errorf("a prose mention should not match, got %d findings", len(res.Findings))
74+
}
75+
})
76+
77+
t.Run("a 404 is not a leak", func(t *testing.T) {
78+
if res := runRedfishModule(t, rf, 404, "not found"); len(res.Findings) > 0 {
79+
t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
80+
}
81+
})
82+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Redfish Service Root Fingerprint Detection Module
2+
3+
id: redfish-service-root
4+
info:
5+
name: Redfish Service Root
6+
author: sif
7+
severity: info
8+
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
9+
tags: [redfish, bmc, dmtf, out-of-band, panel, detection]
10+
11+
type: http
12+
13+
http:
14+
method: GET
15+
paths:
16+
- "{{BaseURL}}/redfish/v1/"
17+
18+
matchers:
19+
- type: status
20+
status:
21+
- 200
22+
23+
- type: word
24+
part: body
25+
words:
26+
- "RedfishVersion"
27+
- "Managers"
28+
29+
- type: regex
30+
part: body
31+
regex:
32+
- '"@odata\.type"\s*:\s*"#ServiceRoot\.'
33+
34+
extractors:
35+
- type: regex
36+
name: redfish_version
37+
part: body
38+
regex:
39+
- '"RedfishVersion"\s*:\s*"([^"]+)"'
40+
group: 1
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Dell iDRAC Unauthenticated BMC Info Exposure Detection Module
2+
3+
id: dell-idrac-bmc-info-exposure
4+
info:
5+
name: Dell iDRAC Unauthenticated BMC Info Exposure
6+
author: sif
7+
severity: medium
8+
description: Detects a Dell iDRAC9 sysmgmt bmc info endpoint answering without authentication, leaking the host server model and iDRAC firmware version
9+
tags: [idrac, dell, bmc, out-of-band, exposure, recon]
10+
11+
type: http
12+
13+
http:
14+
method: GET
15+
paths:
16+
- "{{BaseURL}}/sysmgmt/2015/bmc/info"
17+
18+
matchers:
19+
- type: status
20+
status:
21+
- 200
22+
23+
- type: word
24+
part: body
25+
words:
26+
- "BuildVersion"
27+
- "SystemModelName"
28+
29+
extractors:
30+
- type: regex
31+
name: idrac_firmware
32+
part: body
33+
regex:
34+
- '"FwVer"\s*:\s*"([^"]+)"'
35+
group: 1
36+
37+
- type: regex
38+
name: idrac_server_model
39+
part: body
40+
regex:
41+
- '"SystemModelName"\s*:\s*"([^"]+)"'
42+
group: 1
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# HPE iLO Unauthenticated XML Data Exposure Detection Module
2+
3+
id: hpe-ilo-xmldata-exposure
4+
info:
5+
name: HPE iLO Unauthenticated XML Data Exposure
6+
author: sif
7+
severity: high
8+
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
9+
tags: [ilo, hpe, bmc, out-of-band, exposure, recon]
10+
11+
type: http
12+
13+
http:
14+
method: GET
15+
paths:
16+
- "{{BaseURL}}/xmldata?item=all"
17+
18+
matchers:
19+
- type: status
20+
status:
21+
- 200
22+
23+
- type: word
24+
part: body
25+
words:
26+
- "<RIMP>"
27+
- "<HSI>"
28+
- "<MP>"
29+
30+
- type: word
31+
part: body
32+
words:
33+
- "Integrated Lights-Out"
34+
35+
extractors:
36+
- type: regex
37+
name: ilo_server_serial
38+
part: body
39+
regex:
40+
- '<SBSN>\s*([^<]*?)\s*</SBSN>'
41+
group: 1
42+
43+
- type: regex
44+
name: ilo_server_product
45+
part: body
46+
regex:
47+
- '<SPN>([^<]+)</SPN>'
48+
group: 1
49+
50+
- type: regex
51+
name: ilo_firmware
52+
part: body
53+
regex:
54+
- '<FWRI>([^<]+)</FWRI>'
55+
group: 1

0 commit comments

Comments
 (0)