zerotest/index.html

229 lines
12 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>ZeroTest</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<style>
body {
font-family: sans-serif;
text-align: justify;
}
h1, h2, p#intro, div#buttonBox, p#warning, footer {
text-align: center;
}
td, th {
text-align: center;
}
td.option {
text-align: left;
}
header, main, footer {
max-width: 40em;
margin: 0 auto;
}
header {
background-color: #0000BB;
color: white;
padding: 4px;
}
header a {
color: #BBBBFF;
}
header p {
font-size: 120%;
}
footer {
background-color: #BBBBFF;
padding: 8px;
}
div#results {
text-align: center;
}
table {
margin: 0 auto;
}
button, table {
margin-bottom: 20px;
}
button {
background-color: #0000BB;
color: white;
font-size: 20px;
border: none;
border-radius: 100px;
padding: 3px;
width: 80%;
font-family: sans-serif;
}
</style>
</head>
<body>
<header>
<h1>ZeroTest</h1>
<p id=intro>Test utility for 0.0.0.0 Day</p>
</header>
<main>
<h2>About <a href="https://www.oligo.security/blog/0-0-0-0-day-exploiting-localhost-apis-from-the-browser">0.0.0.0 Day</a> and ZeroTest</h2>
<h3>What is 0.0.0.0 Day?</h3>
<p>0.0.0.0 Day is a browser vulnerability that allows public Web sites (like this one) to make HTTP requests to services running on the local machine, even if they listen only on <code>127.0.0.1</code> (aka <code>localhost</code>). It works because major browsers, while blocking HTTP requests directly to <code>127.0.0.1</code>, allow requests to <code>0.0.0.0</code>, which is also routed to the local host — achieving the same thing.</p>
<h3>Doesn't the <a href="https://en.wikipedia.org/wiki/Same-origin_policy">same-origin policy</a> (SOP) block this?</h3>
<p>Partially. SOP will stop the public site from <em>receiving</em> the response content from requests to other origins, but does not stop it from <em>sending</em> such requests; it cannot, because the browser must send the request to be able to check for <a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing">cross-origin resource sharing</a> (CORS) headers.</p>
<p>In some cases, simply sending a request, without receiving the response content, is sufficient for an <a href="https://en.wikipedia.org/wiki/Arbitrary_code_execution">arbitrary code execution</a> attack against a service listening on <code>127.0.0.1</code>. At minumum, it allows for a <a href="https://en.wikipedia.org/wiki/Port_scanner">port scan</a> of the local machine. The latter method is what ZeroTest uses for testing.</p>
<p>Specifically, ZeroTest uses the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/"><code>fetch</code> API</a>'s <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#making_cross-origin_requests"><code>no-cors</code> mode</a> to check only whether the request succeeded without getting the response.</p>
<h3>ZeroTest shows that I have ports open. What should I do?</h3>
<p>It would be a good idea to install <a href="https://github.com/gorhill/uBlock">uBlock Origin</a>, a general-purpose content blocker which stops 0.0.0.0 Day and is available for most major browsers, or the special-purpose <a href="https://addons.mozilla.org/en-US/firefox/addon/stop-pna/">Stop PNA</a> extension, currently only available for Firefox, which blocks only local IP addresses, including <code>0.0.0.0</code>. If you install either extension and run ZeroTest again, you should see zero ports open.</p>
<h3>ZeroTest shows that I do not have any ports open. Am I safe?</h3>
<p>Probably. This means that your browser is not vulnerable, you have an extension blocking requests to <code>0.0.0.0</code>, or there are no ports open on your machine. In any of these cases, you are currently safe, and in the first two, there is no need for any fix. However, it is still a good idea to install one of the extensions mentioned above; if the reason ZeroTest shows no accessible ports is simply that there are presently none open, then you could still be vulnerable to an attack if some program on your computer opens a port in the future.</p>
<h3>ZeroTest shows a port as having "Timed out." Is it open or closed?</h3>
<p>Your browser did not receive a valid response to ZeroTest's request to this port, or the response took more than a few seconds. Usually, this means the port is open, but the program listening on it is not responding or does not "speak" HTTP. Occasionally, severe lag can cause it to show on ports that are actually closed, so you may want to run the scan again to double-check. As a rule, however, timed-out ports are open, and this is how ZeroTest counts them in the summary of the result. </p>
<h2>Run the test</h2>
<noscript>ZeroTest requires JavaScript to function.</noscript>
<p id=warning>ZeroTest may take several minutes to scan all ports. Your browser may be slow while the test is running.</p>
<div id=buttonBox><button id="run">Run test</button></div>
<div id=results></div>
</main>
<footer>
<p><small><a href="https://git.kj7rrv.com/kj7rrv/zerotest">Source code</a></small></p>
<p><small>Copyright &copy; 2024 Samuel Sloniker</small></p>
<p><small>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</small></p>
<p><small>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</small></p>
<p><small>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</small></p>
</footer>
<script>
const all_ports = Array(65536).fill(0).map((_, i) => i).slice(1)
function run(start, stop, count, index, database) {
function portStatus(status) {
database[currentPort] = status
currentPort += count
if (currentPort <= stop) {
checkPort()
}
}
function checkPort() {
fetch("http://0.0.0.0:" + currentPort, {
mode: 'no-cors',
signal: AbortSignal.timeout(5000)
}).then(function() {
portStatus("open")
}).catch(function(e) {
portStatus(String(e).toLowerCase().includes("time")?"timeout":"closed")
})
}
let currentPort = start + index
checkPort()
}
function runMultiple(start, stop, count, database) {
for (let i = 0; i < count; i++) {
run(start, stop, count, i, database)
}
}
function findGroups(database) {
const groups = [
[null, null, null]
]
for (port of all_ports) {
let last_group = groups[groups.length - 1]
if (database[port] === last_group[2]) {
last_group[1] = port
} else {
groups.push([port, port, database[port]])
}
}
return groups.slice(1)
}
function table(database, tbody, notice) {
const groups = findGroups(database)
const options = {
"undefined": "⚙️ Waiting",
"open": "⚠️ Open",
"closed": "✅ Closed",
"timeout": "🕑 Timed out",
}
const counts = {
"undefined": 0,
"open": 0,
"closed": 0,
"timeout": 0,
}
let content = ""
for (group of groups) {
let start = group[0]
let end = group[1]
let type = String(group[2])
if (start == end) {
content += "<tr><td colspan=2>" + start + "</td><td class=option>" + options[type] + "</td></tr>"
} else {
content += "<tr><td>" + start + "</td><td>" + end + "</td><td class=option>" + options[type] + "</td></tr>"
}
counts[type] += 1 + end - start
}
tbody.innerHTML = content
let elapsed = Math.round(((new Date()).getTime() - window.startTime)/1000)
let openCount = counts["open"] + counts["timeout"]
if (counts["undefined"] == 0) {
if (openCount == 0) {
notice.textContent = "✅ Test complete in " + elapsed + " seconds; everything looks OK"
clearInterval(window.tableInterval)
} else {
notice.textContent = "⚠️ Test complete in " + elapsed + " seconds; " + openCount + " open port" + ((openCount == 1) ? "" : "s") + " found"
clearInterval(window.tableInterval)
}
} else {
if (openCount == 0) {
notice.textContent = "✅⚙️ " + elapsed + " seconds elapsed; looks good so far"
} else if (openCount != 0) {
notice.textContent = "⚠️⚙️ " + elapsed + " seconds elapsed; " + openCount + " open port" + ((openCount == 1) ? "" : "s") + " found so far"
}
}
}
const button = document.querySelector("button#run")
button.addEventListener("click", function(e) {
e.preventDefault()
button.style.display = "none"
database = []
window.startTime = (new Date()).getTime()
runMultiple(1, 65535, 10, database)
document.querySelector("#results").innerHTML = "<h3 id=notice></h3><table><thead><tr><th colspan=2>Port(s)</th><th rowspan=2>Status</th></tr><tr><th>First</th><th>Last</th></tr></thead><tbody></tbody></table>"
const tbody = document.querySelector("tbody")
const h1 = document.querySelector("h3#notice")
window.tableInterval = setInterval(function() {
table(database, tbody, h1)
}, 100)
})
</script>
</body>
</html>