Skip to content

Commit fd8093c

Browse files
committed
fix(modules): drop the zabbix response content-type matcher
application/json-rpc is a request precondition, not a response type. ui/api_jsonrpc.php accepts application/json-rpc, application/json or application/jsonrequest and answers 412 otherwise, then always sets `header('Content-Type: application/json')`. matchers are and'd, so requiring json-rpc back meant the module never fired against a live instance. the test served the mock as application/json-rpc and asserted the real application/json case produced no findings, so it locked the dead behaviour in. it now runs the real shape and keeps json-rpc as a second firing case for proxies that echo the request type. status 200 plus the jsonrpc-2.0 dotted-result regex is already specific to apiinfo.version; a json-rpc error body stays silent.
1 parent 35fa2c2 commit fd8093c

2 files changed

Lines changed: 44 additions & 14 deletions

File tree

internal/modules/zabbix_api_exposure_test.go

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
113
package modules_test
214

315
import (
@@ -10,10 +22,10 @@ import (
1022
"github.com/vmfunc/sif/internal/modules"
1123
)
1224

13-
// zabbix-api-exposure fires only when the target answers apiinfo.version with a
14-
// json-rpc content-type AND a dotted-version result. A foreign service that
15-
// speaks json-rpc but returns a non-version result, or serves the version under
16-
// the wrong content-type, must stay silent.
25+
// zabbix-api-exposure fires on a target that answers apiinfo.version with a
26+
// dotted-version json-rpc result. the discriminator is the result shape, not the
27+
// content-type; the module explains why. a service that speaks json-rpc but
28+
// returns something other than a version must stay silent.
1729
func TestZabbixAPIExposureModule(t *testing.T) {
1830
const mod = "../../modules/recon/zabbix-api-exposure.yaml"
1931
def, err := modules.ParseYAMLModule(mod)
@@ -36,7 +48,8 @@ func TestZabbixAPIExposureModule(t *testing.T) {
3648
return res
3749
}
3850

39-
fire := run("application/json-rpc", `{"jsonrpc":"2.0","result":"7.0.5","id":1}`)
51+
// what a live zabbix actually puts on the wire.
52+
fire := run("application/json", `{"jsonrpc":"2.0","result":"7.0.5","id":1}`)
4053
if len(fire.Findings) == 0 {
4154
t.Error("fire-on-real failed: exposed apiinfo.version not detected")
4255
}
@@ -50,11 +63,22 @@ func TestZabbixAPIExposureModule(t *testing.T) {
5063
t.Errorf("version extraction: got %q, want 7.0.5", version)
5164
}
5265

53-
if res := run("application/json-rpc", `{"jsonrpc":"2.0","result":"pong","id":1}`); len(res.Findings) != 0 {
66+
// key order is not guaranteed; a reordered body must still match.
67+
if res := run("application/json", `{"id":1,"jsonrpc":"2.0","result":"7.0.5"}`); len(res.Findings) == 0 {
68+
t.Error("fire-on-reordered-keys failed: exposed apiinfo.version not detected")
69+
}
70+
71+
// older deployments and proxies that echo the request type back still fire.
72+
if res := run("application/json-rpc", `{"jsonrpc":"2.0","result":"7.0.5","id":1}`); len(res.Findings) == 0 {
73+
t.Error("fire-on-json-rpc-content-type failed: exposed apiinfo.version not detected")
74+
}
75+
76+
if res := run("application/json", `{"jsonrpc":"2.0","result":"pong","id":1}`); len(res.Findings) != 0 {
5477
t.Errorf("silent-on-foreign-jsonrpc failed: %d findings for a non-version result", len(res.Findings))
5578
}
5679

57-
if res := run("application/json", `{"jsonrpc":"2.0","result":"7.0.5","id":1}`); len(res.Findings) != 0 {
58-
t.Errorf("silent-on-wrong-content-type failed: %d findings", len(res.Findings))
80+
// a json-rpc error (auth required, method not found) is not a version leak.
81+
if res := run("application/json", `{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params."},"id":1}`); len(res.Findings) != 0 {
82+
t.Errorf("silent-on-jsonrpc-error failed: %d findings", len(res.Findings))
5983
}
6084
}

modules/recon/zabbix-api-exposure.yaml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,31 @@ http:
1414
- "{{BaseURL}}/api_jsonrpc.php"
1515
- "{{BaseURL}}/zabbix/api_jsonrpc.php"
1616

17+
# api_jsonrpc.php answers 412 Precondition Failed unless the request carries
18+
# one of application/json-rpc, application/json or application/jsonrequest.
1719
headers:
1820
Content-Type: application/json-rpc
1921

2022
body: '{"jsonrpc":"2.0","method":"apiinfo.version","params":{},"id":1}'
2123

24+
# no response content-type matcher: that type is a request precondition, and
25+
# zabbix always answers `header('Content-Type: application/json')`. requiring
26+
# application/json-rpc back never matched a live instance. the jsonrpc-2.0 plus
27+
# dotted-result shape below is specific to apiinfo.version on its own.
2228
matchers:
2329
- type: status
2430
status:
2531
- 200
2632

27-
- type: word
28-
part: header
29-
words:
30-
- "application/json-rpc"
31-
33+
# two and'd patterns rather than one: json object key order is not
34+
# guaranteed, so a serializer that emits id before jsonrpc would miss a
35+
# single pattern that requires them adjacent.
3236
- type: regex
3337
part: body
38+
condition: and
3439
regex:
35-
- '"jsonrpc"\s*:\s*"2\.0"\s*,\s*"result"\s*:\s*"[0-9]+\.[0-9]+(\.[0-9]+)?"'
40+
- '"jsonrpc"\s*:\s*"2\.0"'
41+
- '"result"\s*:\s*"[0-9]+\.[0-9]+(\.[0-9]+)?"'
3642

3743
extractors:
3844
- type: regex

0 commit comments

Comments
 (0)