Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions html/manager-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
32 changes: 31 additions & 1 deletion html/manager.html
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,44 @@ <h3>Singularity of Origin DNS Rebinding Attack</h3>
<label for="interval">WS/Proxy Port</label>
</div>
<div class="col-2">
<input id=wsproxyport class="form-control" aria-describedby="wsproxyportHelp" value="3129"
<input id=wsproxyport class="form-control" aria-describedby="wsproxyportHelp" value="3129"
title="Change this value if you invoked Singularity with a different websockets/proxy port value" />
</div>
<div class="col-8">
<small id="wsproxyportHelp" class="form-text text-muted">TCP port on which Singularity listens to handle websockets and proxy operations.</small>
</div>
</div>

<div class="form-group row">
<div class="col-12">
<h5>Options</h5>
</div>
</div>

<div class="form-group row">
<div class="col-2">
<label for="customHeaders">Custom Headers</label>
</div>
<div class="col-6">
<textarea id=customHeaders class="form-control" rows="3" aria-describedby="customHeadersHelp"
placeholder='{"X-Custom-Header": "value", "Authorization": "Bearer token"}' spellcheck="false"></textarea>
</div>
<div class="col-4">
<small id="customHeadersHelp" class="form-text text-muted">Custom headers to append to all fetch requests (JSON format).</small>
</div>
</div>

<div id="payloadConfigSection" class="d-none">
<div class="form-group row">
<div class="col-2">
<label>Payload Configuration</label>
</div>
<div class="col-10">
<div id="payloadConfigFields"></div>
</div>
</div>
</div>

</div>

</form>
Expand Down
199 changes: 197 additions & 2 deletions html/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
const Payload = () => {
let name = null;
let ports = [];
let configSchema = null;
return {
getName() {
return name;
},
getPorts() {
return ports;
},
init(n, p) {
getConfigSchema() {
return configSchema;
},
init(n, p, cs) {
name = n;
ports = p;
configSchema = cs || null;
}
}
}
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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))
Expand Down
19 changes: 18 additions & 1 deletion html/payload.js
Original file line number Diff line number Diff line change
@@ -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)
};

Expand All @@ -19,6 +30,7 @@ const Rebinder = () => {
let interval = 60000;
let wsproxyport = 3129;
let rebindingSuccess = false;
let options = { headers: {}, config: {} };

const rebindingStatusEl = document.getElementById('rebindingstatus');

Expand All @@ -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.');
Expand Down Expand Up @@ -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
Expand Down
Loading