AXIS OS pingtest.cgi SSRF via Missing validateaddr Call
tcptest.cgi calls the validateaddr binary before pinging anything. pingtest.cgi, accessible to viewer-level users, passes the ip parameter straight to busybox ping with no validation.
tcptest.cgi calls the validateaddr binary before pinging anything. pingtest.cgi, accessible to viewer-level users, passes the ip parameter straight to busybox ping with no validation.
The VAPIX endpoint pingtest.cgi takes a user-supplied ip parameter and passes it directly to busybox ping with no validation. The sibling endpoint tcptest.cgi performs identical functionality but calls the validateaddr binary first, which blocks the full 127.0.0.0/8 loopback range and 0.0.0.0. pingtest.cgi does not call validateaddr, so every address that tcptest.cgi rejects is reachable through pingtest.cgi.
The endpoint is accessible at /axis-cgi/viewer/pingtest.cgi, confirming viewer-level access (the lowest authenticated privilege). This allows a viewer to use the camera as a pivot point to enumerate the camera's local network, confirm whether internal loopback VirtualHosts are listening, and probe cloud-environment metadata endpoints.
The finding is distinct from httptest.cgi (a separate ELF binary with a different code path and a different bypass). The fix here is purely additive: add the existing check_host_addr pattern from tcptest.cgi to pingtest.cgi.
A viewer-level user (lowest authenticated privilege) can:
127.0.0.x addresses to confirm internal Apache VirtualHosts are listening (loopback VHosts at .2, .3, and .12 serve privileged services)169.254.169.254 to confirm cloud instance metadata reachabilitygot response / no response output, mapping the camera's local networkScope is Changed in the CVSS rating because the impact extends beyond the camera itself to the local network it can reach.
The full source of pingtest.cgi shows the raw flow from __qs_getparam to ping:
tmp=$(__qs_getparam ip)
# ...
ip=$tmp # no validation
if ping "$ip" >/dev/null; then
tcptest.cgi has a check_host_addr() function that calls validateaddr, checks the exit code, and returns 400 Invalid localhost address for any loopback input. pingtest.cgi has no equivalent function and makes no call to validateaddr.
The validateaddr binary was confirmed via QEMU emulation to block 127.0.0.1, 127.0.0.2, 127.0.0.12, 127.0.0.255, 127.1.1.1, and 0.0.0.0 while allowing 10.0.0.1, 172.16.0.1, and 169.254.169.254 (the metadata endpoint is not blocked by validateaddr, which is relevant to scope).
The differential below reproduces the asymmetry between the two sibling endpoints. All camera identifiers have been replaced with placeholders.
Reported to the AXIS Security team through coordinated disclosure. The fix is to copy the check_host_addr pattern from tcptest.cgi into pingtest.cgi and call it before the ping command:
check_host_addr() {
[ -x "$(command -v validateaddr)" ] || {
__cgi_errhd 500 "Cannot validate address"
exit 1
}
set +e
validateaddr $1
validation_res=$?
case $validation_res in
1) __cgi_errhd 400 "Invalid localhost address"; exit 1 ;;
2) __cgi_errhd 400 "Could not resolve address"; exit 1 ;;
3) __cgi_errhd 400 "Error validating address"; exit 1 ;;
esac
set -e
}
check_host_addr "$ip" # add before the ping call
Note that validateaddr currently allows 169.254.169.254 (AWS link-local metadata endpoint). The metadata endpoint should be added to its blocklist for deployments on cloud-connected networks.