Oray PgyBox X5 Pro Router firmware 7.6.1 derives each login session ID from the current Unix second, allowing a remote party who can reach the management interface to predict an ordinary user's active session cookie. A request to session_get with that cookie and a supplied _user parameter avoids normal session validation and returns the user's stored password in privilege_props.pwd. Exploitation requires the target user to have logged in; the caller does not need that user's password or issued cookie.
The device test reproduced a newly issued session ID from the Unix second immediately before the login response's HTTP Date value. A separate request carried the predicted ID in its Cookie header, without copying the login response's Set-Cookie value. session_get returned the temporary ordinary account's name and its stored password. The test used an attacker request independent of the account's login request.
The session generator in the firmware's /usr/lib/lua/oraybox/session.lua reseeds Lua's random number generator on every login. It selects 32 entries from the ordered 0–9, a–z table, stores the session under that ID, and returns the ID to the login CGI for the session_id cookie:
-- Selected lines from session.lua and /www/cgi-bin/oraybox_login
local function _gen_session_id(n)
math.randomseed(os.time())
local next_session_id = ""
for i = 1, n do
next_session_id = next_session_id .. sample[math.random(0, #sample - 1)]
end
-- Logging call omitted.
return next_session_id
end
local function _create(ip, user_name)
local session_id = _gen_session_id(32)
local props = {}
props.ip = ip
props.expire = tonumber(os.time()) + EXPIRE_TIME
props.user = user_name
_set_session_props(session_id, props)
return session_id
end
print("Set-Cookie:" .. oraybox.session.COOKIE_KEY .. "=" .. session_id .. ";HttpOnly;")
The shipped AArch64 Lua library maps math.randomseed to srand. Its bounded math.random calculation uses (rand() % 2147483647) / 2147483650.0, then takes the floor after multiplying by the range width. The firmware's glibc 2.36 rand() uses a 31-word TYPE_3 state: Park–Miller initialization, 310 warm-up outputs, and additive feedback between positions three words apart. The PoC implements those operations and skips seeds for which a zero index would make the Lua table access unusable.
The management CGI's get_login_user() returns before session.start() when _user is supplied without _pwd. The session_get module permits execution without a validated login. This combination reaches the module even though session.start() would compare the session IP and expiry:
-- Selected dispatch logic from /www/cgi-bin/oraybox
local user = args._user
local pwd = args._pwd
if user then return nil end
user = oraybox.session.start()
local user = get_login_user()
if not user and not m.i_do_not_need_password then
error_output(oraybox.errorcode.wrong_passwd)
return
end
local output = m.index(args, user)
-- /usr/lib/lua/oraybox/api/session_get.lua
i_do_not_need_password = true
Finally, get_user_name() reads the session file selected by the cookie and returns its username without the validation in session.start(). For ordinary accounts, get_user_props() reads the UCI pwd value that create_user() stored. session_get includes those properties in its JSON output:
-- Selected lines from session.lua, privilege.lua, and api/session_get.lua
-- session.lua: _get_session_props()
local f = io.open(SESSION_PATH .. "/session_" .. session_id, "r")
local ip = f:read()
local expire = f:read()
local user = f:read()
-- session.lua: get_user_name()
local props = _get_session_props(session_id)
if props then
local user = props.user
return user
end
-- privilege.lua: create_user() stores pwd; get_user_props() reads it.
props.pwd = uci:get("privilege", section_name, "pwd")
-- api/session_get.lua: info()
local user = oraybox.session.get_user_name()
local privilege_props = oraybox.privilege.get_user_props(user)
return {code = code, user = user, privilege_props = privilege_props}
From a host with the management password, create an ordinary account and log it in. Use a separate host for the second block; it needs network access to the same management interface but receives neither the login cookie nor the account password. If the login fails to issue a cookie, log in again after the clock advances to another second.
HOST=10.168.1.1
ADMIN_PASSWORD='your-management-password'
curl -sS "http://$HOST/cgi-bin/oraybox" \
--data-urlencode '_api=privilege_set' \
--data-urlencode "_pwd=$ADMIN_PASSWORD" \
--data-urlencode 'optype=1' \
--data-urlencode 'user_name=clockproof' \
--data-urlencode 'user_pwd=ClockProof123' \
--data-urlencode 'privilege_mode=0'
curl -si "http://$HOST/cgi-bin/oraybox_login" \
--data-urlencode 'format=json' \
--data-urlencode 'username=clockproof' \
--data-urlencode 'password=ClockProof123'
Run this Python 3 command promptly on the other host. It makes direct session_get requests for the predicted cookies and prints only a response for clockproof. The device's HTTP Date header anchors the search when present.