Information

Introduction

Oray PgyBox X5 Pro Router firmware 7.6.1 accepts a free-form pppd_options value in its PPPoE WAN settings. A caller with the management system password can upload an AArch64 shared library and set that value to plugin /tmp/upload_files/<library>.so. Netifd passes the option to pppd, which loads the library and calls plugin_init as root before PPPoE account authentication.

Vulnerability Details

/www/cgi-bin/oraybox checks _pwd against the oray account before it dispatches _api=wan_set. In the deployed 7.6.1 /usr/lib/lua/oraybox/api/wan_set.lua module, the primary WAN branch passes the request's pppd_options to pppoe. The index excerpt below is from that module's decompiled Lua:

wan_name = "wan"
device = uci:get("network", wan_name .. "_dev", "name")
if device then
  uci:set("network", wan_name, "device", device)
end
if args.mod == "dhcp" then
  ret_code = dhcp()
elseif args.mod == "static" then
  ret_code = static(args.ip, args.mask, args.gateway)
elseif args.mod == "pppoe" then
  ret_code = pppoe(args.user, args.pwd, args.pppd_options, args.service)

The pppoe function requires a username and password and stores any nonempty string passed as pppd_options:

if type(pppd_options) == "string" and string.len(pppd_options) > 0 and not uci:set("network", wan_name, "pppd_options", pppd_options) then
  return oraybox.errorcode.uci
end
oraybox.util.syslog("net_cfg_chg", "interface:%s proto:pppoe", wan_name)
return set_ifname()

index commits network and calls restart_network() after success. On the stock configuration, network.wan.def_ifname supplies eth0.2 to set_ifname; the request does not need a separate physical interface field. The PPPoE branch may refuse the change if the WAN port has been repurposed. Applying the mode can also update wireless, firewall, and secondary WAN state. The PoC saves the affected configuration before the request.

/lib/netifd/proto/ppp.sh supplies pppoe.so and the physical nic- interface to ppp_generic_setup. That function reads pppd_options from the network configuration and leaves the value unquoted at the end of the native argument list:

json_get_vars ip6table demand keepalive keepalive_adaptive username password pppd_options pppname unnumbered persist maxfail holdoff peerdns
${mtu:+mtu $mtu mru $mtu} \
"$@" $pppd_options

The unquoted expansion turns plugin /tmp/upload_files/wan_option_761.so into two arguments to /usr/sbin/pppd. plugin is a native pppd option that loads executable code. In the shipped AArch64 pppd, the plugin loader sub_35180 passes an option value containing / directly to dlopen, resolves plugin_init with dlsym, and calls it. The loader warns if pppd_version is absent and still calls plugin_init. The 7.6.1 pppd binary was checked against the identical earlier X5 Pro binary used for this decompilation.

The 7.6.1 /www/cgi-bin/oraybox_upload_file creates the named library. IDA function sub_404888 reads multipart pwd, verifies it against the oray shadow password (getspnam at 0x404b34; crypt and comparison at 0x404e50–0x404ec0), and reads multipart file at 0x404d0c. It strips directory components from the filename at 0x405284–0x40532c, creates /tmp/upload_files at 0x40522c, opens the destination as 0644 at 0x4052b4–0x4052c8, and writes the supplied bytes at 0x405394.

PoC

Send the requests to the X5 Pro management address from a Linux workstation. The workstation needs the management system password and an AArch64 glibc cross-compiler or the vendor SDK compiler. The default WAN port must remain assigned to WAN and have a physical link. Applying wan_set switches that WAN to PPPoE and interrupts its upstream connection. A PPPoE access concentrator or valid account is unnecessary once pppd starts: it parses plugin before PPPoE login.