Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
pfsense-api-snort-file-join — pfSense Plus MIM API toolkit b4acd7ca - High - Remote - File read - Snort rules {file} decode-then-join | Kitploit
Tools/GitHubGitHub/abraxas/pfsense-api-snort-file-join
Vulnerability AnalysisExploitationWeb Application ExploitationInformation GatheringWeb SecurityPenetration TestingAPI SecurityLabs & Practice
GitHub
abraxas/pfsense-api-snort-file-join

pfsense-api-snort-file-join

pfSense Plus MIM API toolkit b4acd7ca - High - Remote - File read - Snort rules {file} decode-then-join

View Repository
11 day agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Abraxas Labs - pfsense-api-snort-file-join

abraxaslabs.tech  ·  github.com/abraxas  ·  @abraxas_null  ·  [email protected]  ·  pfsense-api-snort-file-join

pfsense-api-snort-file-join

Class: File read Reach: Remote

pfSense Plus MIM API (Netgate/pfsense-api b4acd7ca) - Netgate

GET /services/snort/rules/{name}/{file} takes two path parameters, both type: string, no pattern. Description: inspect a file in the ruleset. SnortRuleFileContents.filename is "path of file". Generated get_snort_ruleset_file._get_kwargs does quote(str(...), safe=""), so ../ is ..%2F on the wire. A server that URL-decodes then joins into the ruleset directory still walks out.

A caller who can GET that path, plus a fleet JWT, reads files outside the named ruleset as the MIM agent on every enrolled box. Plus join was not executed. The lab is a decode-then-join gadget of the public path.

This is leftover 8 of 9. Map: pfsense-api-nexus-mim. Same pin leftover: fleet JWT. This is not jaredhendrickson/pfsense-api.

IDno CVE yet
ClassFile read (Snort {file} decode-then-join)
ReachRemote (authenticated GET; fleet JWT reuses the same path on every device_id)
CWECWE-22
CVSSHigh: 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N for a JWT that may GET the Snort ruleset file
ProductNetgate/pfsense-api (pfSense Plus Nexus MIM controller OpenAPI + generated client)
Affectedmain b4acd7ca21dbca739a530403a1f62a7440f7726f (2026-05-25, Add Snort-3 API definitions)
AuthExamples send a controller JWT after POST /login. OpenAPI has no securitySchemes. Unauth is not proven.
LicenseGNU Affero GPL v3.0
Lab127.0.0.1:19181 only. Decode-then-join gadget. Plus join not executed.

What an attacker can do

Needs a caller who can GET /services/snort/rules/{name}/{file}. Official examples use Authorization: Bearer after POST /login, then replay that token as base_url=.../api/device/pfsense/{device_id}/api for every id from GET /mim/devices. Generated ops still type AuthenticatedClient | Client. CSRF is not in play on this GET.

MethodSinkPrivilegeLab vs inferred
Generated-client GET with {file}=../../oracle/WITNESS (wire ..%2F..%2Foracle%2FWITNESS)decode unquote(file) then join onto the named ruleset directory and readRecon: HTTP 200, filename /oracle/WITNESS, body PFSENSE-API-SNORT-FILE-JOIN-WITNESS. Intended is a .rules file inside that ruleset.Lab-proven (gadget). Plus join was not executed.
Same GET with {file}=../../../etc/passwdsame join if the controller maps it onto the device filesystemRecon: read host files as the MIM agent (Plus WebGUI/PHP is typically root). Lab used /oracle/WITNESS, not a live /etc/passwd.Inferred (same join, host path).
Same GET after a fleet JWT (POST /login once, replay on /api/device/pfsense/{id}/api)device proxy of the same unconstrained {file}Recon: one helpdesk-shaped controller token reads files on every enrolled box, not only the one the login was meant for.Inferred (G6 contract + this path). Plus RBAC not executed.
Same leftover on {name}GET|DELETE /services/snort/rules/{name} and sibling address/suppress {name} are also unconstrained stringsRecon / integrity: rename or delete a ruleset whose name is a path.Inferred from the spec. Lab drove {file} only.
Confined control of the same wire pathrelative_to(ruleset_dir.resolve()) after decodeClosed. HTTP 400, no witness.Lab-proven
Benign {file}=local.rulesintended ruleset readClosed. 200, sid:1, no witness, both naive and control.Lab-proven

Adjacent closed: client percent-encoding of ../ (encoding=%2F). Control join after decode (control-confined=yes). Plus controller join (plus-join-not-executed). Intended read of local.rules inside ruleset foo. Unauthenticated GET (not proven; examples send Bearer). POST /services/snort/rules archive / planted .so (intended IDS admin, not this leftover). Diagnostics PHP/shell. File write. Host shell. This leftover is not RCE.

How I found it

I pinned Netgate/pfsense-api b4acd7ca21dbca739a530403a1f62a7440f7726f because HEAD on 2026-10-14 had no tags and the latest commit is "Add Snort-3 API definitions". That is how this path exists. The YAML has zero pattern: keys anywhere. {name} and {file} are unconstrained strings. quote(..., safe="") is a client encoder, not a server basedir check.

  /services/snort/rules/{name}/{file}:
    parameters:
      - in: path
        name: name
        required: true
        schema:
          type: string
      - in: path
        name: file
        required: true
        schema:
          type: string
    get:
      description: Obtain the contents of the specified file in the ruleset. This is intended for inspecting scripts provided by it.
      operationId: GetSnortRulesetFile
"url": "/services/snort/rules/{name}/{file}".format(
    name=quote(str(name), safe=""),
    file=quote(str(file), safe=""),
),

Wrong turns already recorded: treating client encoding as closing traversal; composing pfSense Plus; ranking Snort archive .so as this leftover; claiming unauth GET. Theatre: a reverse shell. The oracle is gadget 200 of /oracle/WITNESS plus control 400.

Lab

python3 pfsense-api-snort-file-join-Abraxas-Labs.py

or

cd lab
./run.sh

Target only http://127.0.0.1:19181. Compose project pfsense-api-snort-file-join. Decode-then-join gadget of the public path. Plus join is not executed. Stack tears down after the run.

SUCCESS PFSENSE-API-SNORT-FILE-JOIN encoding=%2F decode-then-join=yes control-confined=yes plus-join-not-executed PFSENSE-API-SNORT-FILE-JOIN-WITNESS

Client encoding of ../../../etc/passwd is ..%2F..%2F..%2Fetc%2Fpasswd. Naive GET of /services/snort/rules/foo/..%2F..%2Foracle%2FWITNESS returned 200, filename /oracle/WITNESS, body PFSENSE-API-SNORT-FILE-JOIN-WITNESS. The same request under a confined control that resolves under the ruleset root returned 400 and no witness. Benign local.rules stayed inside foo.

The fix

Resolve under a ruleset root after decode. Reject .. and absolute paths. Path.resolve() then relative_to(ruleset_dir.resolve()), or equivalent, before the read. Constrain {file} (and {name}) to a ruleset-safe pattern in the OpenAPI so generators stop sending path-shaped strings.

References

Download Tool