← THE INDEX  ·  WRITEUP

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.

Summary

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.

Impact

A viewer-level user (lowest authenticated privilege) can:

  • Ping 127.0.0.x addresses to confirm internal Apache VirtualHosts are listening (loopback VHosts at .2, .3, and .12 serve privileged services)
  • Ping 169.254.169.254 to confirm cloud instance metadata reachability
  • Enumerate a /24 subnet by iterating IP addresses and reading the got response / no response output, mapping the camera's local network

Scope is Changed in the CVSS rating because the impact extends beyond the camera itself to the local network it can reach.

Root cause

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).

Proof of concept

The differential below reproduces the asymmetry between the two sibling endpoints. All camera identifiers have been replaced with placeholders.

Disclosure and fix

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.