Skip to content

Instantly share code, notes, and snippets.

@SoftCreatR
Created May 25, 2024 13:57
Show Gist options
  • Save SoftCreatR/f9a7a90191d812d9ce700dedf5840d8a to your computer and use it in GitHub Desktop.
Save SoftCreatR/f9a7a90191d812d9ce700dedf5840d8a to your computer and use it in GitHub Desktop.
This is a proof of concept (PoC) for Hetzner's "Heray" Proof of Work (PoW) Captcha. Please note that this is neither functional nor the original code. It simply demonstrates how the system could work based on observed patterns and assumptions.

Proof of Concept for Hetzner's "Heray" PoW Captcha

This is a proof of concept (PoC) for Hetzner's "Heray" Proof of Work (PoW) Captcha. Please note that this is neither functional nor the original code. It simply demonstrates how the system could work based on observed patterns and assumptions.

Overview

Hetzner's "Heray" PoW Captcha likely requires the client to solve a computational puzzle as a form of CAPTCHA, where a specific condition must be met for the challenge to be accepted. The solution involves generating a specific mainbytes value based on given uuid and hsum values.

Process

  1. UUID Generation: A UUID is generated or provided by the server.
  2. Random String Generation: A random string is created.
  3. SHA-256 Hashing: The combination of UUID, hsum, and the random string is encoded and hashed using SHA-256.
  4. Challenge Solution: The script continuously hashes different values until a hash meeting a specific condition is found.
  5. WASM Computation: The system leverages WebAssembly (WASM) for efficient computation, although the actual WASM code is not part of this PoC.

Explanation of the Code (_a.js, _b.js, _c.js, _index.html)

  1. Random String and UUID Generation: Functions are provided to generate random strings and UUIDs.
  2. UTF-8 Encoding and SHA-256 Hashing: Functions are implemented to encode strings in UTF-8 and hash them using the Web Crypto API.
  3. Combining Values: Combines the uuid, hsum, and a random string, then hashes the combined value to create mainbytes.
  4. Setting Values: On page load, the script sets the generated uuid and mainbytes values in the appropriate fields.

Note on WebAssembly (WASM)

The actual implementation of Hetzner's "Heray" PoW Captcha likely involves leveraging WebAssembly (WASM) for efficient computation of the hash values. The JavaScript code provided here simulates the logic but does not include the WASM code due to its complexity and binary nature. In a real implementation, the WASM module would be loaded and utilized to perform the computationally intensive hashing operations.

Disclaimer

This repo is purely a demonstration based on assumptions and observed patterns. It is not functional and does not represent the actual implementation of Hetzner's "Heray" PoW Captcha.

(() => {
const generateRandomString = (length) => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({ length }, () => characters.charAt(Math.floor(Math.random() * characters.length))).join('');
};
const generateUUID = () => {
let dt = Date.now();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
};
const utf8Encode = (str) => {
return unescape(encodeURIComponent(str));
};
const sha256 = async (message) => {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
};
const generateMainBytesValue = async (uuid, hsum) => {
const randomPart = generateRandomString(128);
const combined = `${uuid}${hsum}${randomPart}`;
const encoded = utf8Encode(combined);
return await sha256(encoded);
};
const setValues = async () => {
const uuidElement = document.getElementById('uuid');
const mainBytesElement = document.getElementById('mainbytes');
const hsumElement = document.getElementById('hsum');
const uuidValue = generateUUID();
uuidElement.innerText = uuidValue;
const hsumValue = hsumElement.value;
const mainBytesValue = await generateMainBytesValue(uuidValue, hsumValue);
mainBytesElement.value = mainBytesValue;
console.log('UUID:', uuidValue);
console.log('MainBytes:', mainBytesValue);
};
// Set values on page load
setValues();
})();
(function () {
function createOnceFunction() {
let executed = true;
return function (context, fn) {
const callOnce = executed ? function () {
if (fn) {
const result = fn.apply(context, arguments);
fn = null;
return result;
}
} : function () {};
executed = false;
return callOnce;
};
}
function getGlobal() {
let globalObject;
try {
globalObject = Function("return (function() {}.constructor(\"return this\")( ));")();
} catch (e) {
globalObject = window;
}
return globalObject;
}
function hijackConsole() {
const globalObject = getGlobal();
const consoleObject = globalObject.console = globalObject.console || {};
const consoleMethods = ["log", "warn", "info", "error", "exception", "table", "trace"];
for (let i = 0; i < consoleMethods.length; i++) {
const method = consoleMethods[i];
const originalMethod = consoleObject[method] || function () {};
consoleObject[method] = function () {
return createOnceFunction().prototype.bind(createOnceFunction()).apply(this, arguments);
};
consoleObject[method].__proto__ = createOnceFunction().bind(createOnceFunction());
consoleObject[method].toString = originalMethod.toString.bind(originalMethod);
}
}
function utf8Encode(str) {
let encoded = '';
let pos = -1;
const len = str.length;
while ((pos += 1) < len) {
let code = str.charCodeAt(pos);
const next = pos + 1 < len ? str.charCodeAt(pos + 1) : 0;
if (0xd800 <= code && code <= 0xdbff && 0xdc00 <= next && next <= 0xdfff) {
code = 0x10000 + ((code & 0x3ff) << 10) + (next & 0x3ff);
pos++;
}
if (code <= 0x7f) {
encoded += String.fromCharCode(code);
} else if (code <= 0x7ff) {
encoded += String.fromCharCode(0xc0 | code >>> 6 & 0x1f, 0x80 | code & 0x3f);
} else if (code <= 0xffff) {
encoded += String.fromCharCode(0xe0 | code >>> 12 & 0xf, 0x80 | code >>> 6 & 0x3f, 0x80 | code & 0x3f);
} else if (code <= 0x1fffff) {
encoded += String.fromCharCode(0xf0 | code >>> 18 & 0x7, 0x80 | code >>> 12 & 0x3f, 0x80 | code >>> 6 & 0x3f, 0x80 | code & 0x3f);
}
}
return encoded;
}
function add32(a, b) {
const low = (a & 0xffff) + (b & 0xffff);
const high = (a >> 16) + (b >> 16) + (low >> 16);
return (high << 16) | (low & 0xffff);
}
function hexEncode(str, uppercase) {
const hex = uppercase ? "0123456789ABCDEF" : "0123456789abcdef";
let result = '';
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
result += hex.charAt((code >>> 4) & 0xf) + hex.charAt(code & 0xf);
}
return result;
}
function stringToBinaryArray(str) {
const bin = [];
const len = str.length * 32;
for (let i = 0; i < len; i += 8) {
bin[i >> 5] |= (str.charCodeAt(i / 8) & 0xff) << (24 - i % 32);
}
return bin;
}
function sha256(message, length) {
const K = [
0x428a2f98, 0x71374491, -0x4a3f0431, -0x164a245b, 0x3956c25b, 0x59f111f1, -0x6dc07d5c, -0x54e3a12b,
-0x27f85568, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, -0x7f214e02, -0x6423f959, -0x3e640e8c,
-0x1b64963f, -0x1041b87a, 0xfc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
-0x67c1aeae, -0x57ce3993, -0x4ffcd838, -0x40a68039, -0x391ff40d, -0x2a586eb9, 0x6ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, -0x7e3d36d2, -0x6d8dd37b,
-0x5d40175f, -0x57e599b5, -0x3db47490, -0x3893ae5d, -0x2e6d17e7, -0x2966f9dc, -0xbf1ca7b, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, -0x7b3787ec, -0x7338fdf8, -0x6f410006, -0x5baf9315, -0x41065c09, -0x398e870e
];
const H = [0x6a09e667, -0x4498517b, 0x3c6ef372, -0x5ab00ac6, 0x510e527f, -0x64fa9774, 0x1f83d9ab, 0x5be0cd19];
const W = Array(64);
message[length >> 5] |= 0x80 << (24 - length % 32);
message[((length + 64 >> 9) << 4) + 15] = length;
for (let i = 0; i < message.length; i += 16) {
const [a, b, c, d, e, f, g, h] = H;
for (let j = 0; j < 64; j++) {
W[j] = j < 16 ? message[j + i] : add32(add32(add32(W[j - 2] >>> 17 | W[j - 2] << 15, W[j - 2] >>> 19 | W[j - 2] << 13, W[j - 2] >>> 10), W[j - 7]), add32(W[j - 15] >>> 7 | W[j - 15] << 25, W[j - 15] >>> 18 | W[j - 15] << 14, W[j - 15] >>> 3), W[j - 16]);
const T1 = add32(h, (e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7), e & f ^ ~e & g, K[j], W[j]);
const T2 = add32((a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10), a & b ^ a & c ^ b & c);
h = g;
g = f;
f = e;
e = add32(d, T1);
d = c;
c = b;
b = a;
a = add32(T1, T2);
}
H[0] = add32(a, H[0]);
H[1] = add32(b, H[1]);
H[2] = add32(c, H[2]);
H[3] = add32(d, H[3]);
H[4] = add32(e, H[4]);
H[5] = add32(f, H[5]);
H[6] = add32(g, H[6]);
H[7] = add32(h, H[7]);
}
return H;
}
function hashToHex(hash) {
return hexEncode(stringToBinaryArray(hash.map(n => String.fromCharCode((n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff)).join('')), false);
}
function hashString(input) {
return hashToHex(sha256(stringToBinaryArray(utf8Encode(input)), input.length * 8));
}
self.onmessage = async function (event) {
const input = event.data;
let nonce = 0;
let timestamp = Date.now();
while (true) {
const hash = hashString(input + nonce.toString());
if (hash.startsWith('1337')) { // calling console.log("1337") is mandatory after this script
break;
}
if (Date.now() - timestamp > 2000) {
timestamp = Date.now();
}
nonce++;
}
self.postMessage(nonce);
};
// Initialization
hijackConsole();
})();
(function () {
function createOnceFunction() {
let executed = true;
return function (context, fn) {
const callOnce = executed ? function () {
if (fn) {
const result = fn.apply(context, arguments);
fn = null;
return result;
}
} : function () {};
executed = false;
return callOnce;
};
}
const initSearchFunction = createOnceFunction(this, function () {
return initSearchFunction.toString().search("(((.+)+)+)+$").toString().constructor(initSearchFunction).search("(((.+)+)+)+$");
});
initSearchFunction();
window.noDev = true;
window.hideVerificationContainer = function () {
document.getElementById("verifying-container").classList.add("hidden");
document.getElementById("successful-container").classList.remove("hidden");
};
window.hiddenTitleSwitch = function () {
if (document.hidden) {
if (document.title !== "✅ Success") {
document.title = "✅ Success";
}
return true;
}
return false;
};
window.addEventListener("gotcha", event => {
const workerScript = document.querySelector("#worker1").textContent;
const workerBlob = new Blob([workerScript], { type: "text/javascript" });
const workerURL = window.URL.createObjectURL(workerBlob);
const workerElement = document.querySelector("#worker1");
workerElement.parentNode.removeChild(workerElement);
let startTime = Date.now();
function handleWorkerMessage(workerMessage, workerIndex) {
const currentTime = Date.now();
let timeoutDuration = 650;
console.log(`${currentTime - startTime}ms`);
if ("1337".length >= 5) {
timeoutDuration = 2750;
}
window.setTimeout(() => {
window.hideVerificationContainer();
window.setTimeout(() => {
document.getElementById("challenge").value = btoa(workerIndex) + workerMessage.data.toString();
if (window.noDev) {
document.getElementById("form").submit();
} else {
document.addEventListener("click", function () {
document.getElementById("form").submit();
});
}
}, 10);
}, Math.max(50, timeoutDuration - (currentTime - startTime)));
}
const numWorkers = Math.min(Math.floor((window.navigator.hardwareConcurrency || 1) / 2), 8);
const workers = [];
for (let i = 0; i < numWorkers; i++) {
const worker = new Worker(workerURL);
worker.onmessage = function (message) {
const interval = window.setInterval(() => {
if (window.hiddenTitleSwitch()) {
return true;
}
handleWorkerMessage(message, i);
handleWorkerMessage = () => {};
workers.forEach(worker => worker.terminate());
window.clearInterval(interval);
}, 55);
};
const mainBytes = document.getElementById("mainbytes").value;
const hsum = document.getElementById("hsum").value;
worker.postMessage(mainBytes + hsum + btoa(i));
workers.push(worker);
}
});
})();
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Security Check</title>
<style type="text/css">h1,h2{font-weight:700}.flex,body,main{display:flex}.captcha-container,.challenge-container{padding:20px;border:1px solid #ededed;box-shadow:0 1px 2px 0 rgba(0,0,0,.05)}footer,noscript{text-align:center}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;font-size:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}body{flex-direction:column;height:100vh;color:#383838}main{width:1024px;flex:1 0 auto;flex-direction:column;align-self:center;justify-content:center}footer{font-size:14px;line-height:24px;flex-shrink:0;color:#8a8a8a;margin:0 16px 16px}a{color:#d50c2d}h1{font-size:36px;line-height:40px}h2{font-size:20px;line-height:28px}div,p,small,span{font-weight:400;line-height:24px}hr{background-color:#ededed;height:1px;border:none;margin:40px 0;width:100%}div,p,span{font-size:16px}small{font-size:14px}.button,strong{font-weight:700}.mb-5{margin-bottom:20px}.image-mobile{display:none;align-items:center;justify-content:center;width:21px;height:24px;margin-right:6px}.challenge-container{border-radius:12px;flex-direction:column;width:320px}.captcha-container{border-radius:4px;width:max-content}.captcha-input{font-size:16px;border:1px solid #ededed;background-color:#fff;border-radius:4px;padding:9px 20px}.captcha-input::placeholder{color:#bebebe}.captcha-input::-webkit-input-placeholder{color:#bebebe}.captcha{display:block;margin-bottom:12px;max-width:100%}.wrapper,noscript{margin-bottom:40px}.button{font-size:16px;border-radius:.375rem;line-height:1.5rem;padding:.625rem 1.25rem}.button:hover{cursor:pointer}.button-red{background-color:#d50c2d;color:#fff}.button-red:hover{background-color:#f22143}.button-white{background-color:#fff;color:#383838}.button-white:hover{background-color:#f5f5f5}.spin{animation-name:spin;animation-iteration-count:infinite;animation-timing-function:linear;animation-duration:1.5s}.hidden{display:none}.wrapper{align-items:center;justify-content:space-between}.text-content{font-size:18px;line-height:28px}.image{height:80px;width:auto}.reload-link{text-decoration:none}.input-wrapper{display:flex;flex-direction:column;margin-bottom:8px}.input-container{display:flex;flex-direction:column;max-width:500px}input:invalid,input:invalid:focus-visible{border:1px solid red;color:red}.invalid-message{margin-top:2px;font-size:12px;font-weight:400;line-height:16px;color:red}noscript{padding:10px 20px;display:flex;align-items:center;justify-content:center;background-color:#fff5f5;color:red;border-radius:6px;font-size:16px}@media (max-width:1024px){main{max-width:100%;align-self:auto}}@media (max-width:767px){main{justify-content:start}.image,hr{display:none}.image-mobile{display:block}.captcha-container,.challenge-container{width:auto;margin-bottom:20px}.wrapper{margin-bottom:20px}}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-circle{margin-right:10px;width:44px;height:44px}</style>
</head>
<body>
<main>
<div style="padding: 16px;">
<noscript>
Activate JavaScript - Please activate JavaScript in your browser and refresh this page.
</noscript>
<div class="flex wrapper">
<div class="flex" style="flex-direction: column;">
<div class="flex" style="margin-bottom: 8px;">
<div class="image-mobile">
<img src="/__ray_static/check-icon-mobile.png" alt="">
</div>
<span>Security Check</span>
</div>
<h1 style="margin-bottom: 8px;">Checking that you are not a robot</h1>
<p class="text-content">This process is performed automatically. You will be redirected shortly.</p>
</div>
<div class="image">
<img src="/__ray_static/check-icon.png" alt="">
</div>
</div>
<div id="verifying-container" class="challenge-container">
<div class="flex" style="align-items: center;">
<div class="loading-circle spin">
<img src="/__ray_static/loading-circle.png" alt="">
</div>
<strong>Verifying...</strong>
</div>
<span style="display:none;" id="uuid"></span>
<input style="display:none;" type="hidden" name="mainbytes" id="mainbytes" value="">
<form method="POST" action="/_ray/pow" id="form">
<input type="hidden" name="challenge" id="challenge">
<input type="hidden" value="" name="hsum" id="hsum">
</form>
</div>
<div id="successful-container" class="challenge-container hidden">
<div class="flex" style="align-items: center;">
<div style="width: 44px; height: 44px; margin-right: 10px;">
<img src="/__ray_static/verification-successful.png" alt="">
</div>
<strong>Verification successful.</strong>
</div>
</div>
<hr>
<div>
<h2 style="margin-bottom: 6px;">Useful information</h2>
<p>This process is fully GDPR-compliant and respects your privacy.</p>
</div>
</div>
</main>
<footer>Heray is a security product powered by <a href="https://www.hetzner.com">Hetzner</a></footer>
<script>
// _a.js
</script>
<script id="worker1" type="javascript/worker">
// _b.js
</script>
<script>
console.log("1337")
// _c.js
</script>
<!-- This loads the WASM, and executes WASM related logic -->
<!-- <script src="/__ray_static/index.js"></script> -->
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment