Add initial raw link support with netlify functions

This commit is contained in:
alanfoster 2021-03-20 18:38:52 +00:00
parent 8615ce53c6
commit 0b6113bf02
6 changed files with 287 additions and 173 deletions

View File

@ -5,8 +5,6 @@ Hosted Reverse Shell generator with a ton of functionality -- (great for CTFs)
### Hosted Instance ### Hosted Instance
https://revshells.com https://revshells.com
### Features ### Features
- Generate common listeners and reverse shells - Generate common listeners and reverse shells
@ -17,8 +15,17 @@ https://revshells.com
- Dark and Light Modes - Dark and Light Modes
### Screenshot ### Screenshot
![image](https://user-images.githubusercontent.com/58673953/111243529-9d646f80-85d7-11eb-986c-9842747dc2e7.png) ![image](https://user-images.githubusercontent.com/58673953/111243529-9d646f80-85d7-11eb-986c-9842747dc2e7.png)
## Dev
It's recommended to use the netlify dev command if you're wanting to modify any of the server functions, such as for raw link support:
```
npx netlify dev
```
### Credits ### Credits
- weibell - weibell
- briskets - briskets

View File

@ -112,7 +112,7 @@
<span id="ip-label" class="input-group-text">IP</span> <span id="ip-label" class="input-group-text">IP</span>
</div> </div>
<input id="ip" type="text" class="form-control form-control-lg text-center px-1" <input id="ip" type="text" class="form-control form-control-lg text-center px-1"
size="12VW" maxlength="15" placeholder="10.10.10.10" aria-label="IP" size="12VW" maxlength="15" aria-label="IP"
aria-describedby="ip-label"> aria-describedby="ip-label">
</div> </div>
</div> </div>
@ -124,7 +124,7 @@
<span id="port-label" class="input-group-text">Port</span> <span id="port-label" class="input-group-text">Port</span>
</div> </div>
<input id="port" type="text" class="form-control form-control-lg text-center" <input id="port" type="text" class="form-control form-control-lg text-center"
size="4vw" maxlength="5" placeholder="9001" aria-label="Port" size="4vw" maxlength="5" aria-label="Port"
aria-describedby="port-label"> aria-describedby="port-label">
<div class="input-group-append"> <div class="input-group-append">
<button id="inc-port" class="btn btn-secondary btn-sm" type="button" <button id="inc-port" class="btn btn-secondary btn-sm" type="button"
@ -298,10 +298,10 @@
<label for="encoding" class="col-auto col-form-label">Encoding</label> <label for="encoding" class="col-auto col-form-label">Encoding</label>
<div class="col-auto"> <div class="col-auto">
<select id="encoding" class="custom-select"> <select id="encoding" class="custom-select">
<option selected>None</option> <option value="None">None</option>
<option>encodeURI</option> <option value="encodingURI">encodeURI</option>
<option>encodeURIComponent</option> <option value="encodeURIComponent">encodeURIComponent</option>
<option>Base64</option> <option value="Base64">Base64</option>
</select> </select>
</div> </div>
</div> </div>
@ -333,6 +333,12 @@
<label for="auto-copy-switch" class="custom-control-label small pr-2 pb-1" <label for="auto-copy-switch" class="custom-control-label small pr-2 pb-1"
style="padding-top: 2px">Auto-copy</label> style="padding-top: 2px">Auto-copy</label>
</div> --> </div> -->
<!-- Raw button -->
<button type="button" class="raw-listener btn btn-primary float-right mr-3"
data-toggle="tooltip" title="View raw">
Raw
</button>
<!-- Copy button --> <!-- Copy button -->
<button id="copy-reverse-shell-command" data-toggle="tooltip" <button id="copy-reverse-shell-command" data-toggle="tooltip"
title="Copy to clipboard" type="button" class="btn btn-primary float-right"> title="Copy to clipboard" type="button" class="btn btn-primary float-right">
@ -383,6 +389,12 @@
<label for="auto-copy-switch" class="custom-control-label small pr-2 pb-1" <label for="auto-copy-switch" class="custom-control-label small pr-2 pb-1"
style="padding-top: 2px">Auto-copy</label> style="padding-top: 2px">Auto-copy</label>
</div> --> </div> -->
<!-- Raw button -->
<button type="button" class="raw-listener btn btn-primary float-right mr-3"
data-toggle="tooltip" title="View raw">
Raw
</button>
<!-- Copy button --> <!-- Copy button -->
<button id="copy-bind-shell-command" data-toggle="tooltip" title="Copy to clipboard" <button id="copy-bind-shell-command" data-toggle="tooltip" title="Copy to clipboard"
type="button" class="btn btn-primary float-right"> type="button" class="btn btn-primary float-right">
@ -435,6 +447,13 @@
class="custom-control-label small pr-2 pb-1" class="custom-control-label small pr-2 pb-1"
style="padding-top: 2px">Auto-copy</label> style="padding-top: 2px">Auto-copy</label>
</div> --> </div> -->
<!-- Raw button -->
<button type="button" class="raw-listener btn btn-primary float-right mr-3"
data-toggle="tooltip" title="View raw">
Raw
</button>
<!-- Copy button --> <!-- Copy button -->
<button id="copy-msfvenom-command" data-toggle="tooltip" <button id="copy-msfvenom-command" data-toggle="tooltip"
title="Copy to clipboard" type="button" title="Copy to clipboard" type="button"
@ -442,7 +461,6 @@
Copy Copy
</button> </button>
</div> </div>
</div> </div>
<!-- /Right column --> <!-- /Right column -->
</div> </div>
@ -464,6 +482,9 @@
<!-- RSG data --> <!-- RSG data -->
<script src="js/data.js"></script> <script src="js/data.js"></script>
<!-- RSG raw link generator -->
<script src="js/raw-link.js"></script>
<!-- RSG logic --> <!-- RSG logic -->
<script> <script>
// Element selectors // Element selectors
@ -478,10 +499,6 @@
const bindShellCommand = document.querySelector("#bind-shell-command"); const bindShellCommand = document.querySelector("#bind-shell-command");
const msfVenomCommand = document.querySelector("#msfvenom-command"); const msfVenomCommand = document.querySelector("#msfvenom-command");
if(localStorage.getItem("ip") == null || localStorage.getItem("ip").length == 0){
localStorage.setItem("ip", ipInput.placeholder);
}
const FilterType = { const FilterType = {
'All': 'all', 'All': 'all',
'Windows': 'windows', 'Windows': 'windows',
@ -522,6 +539,14 @@
}); });
}); });
var rawLinkButtons = document.querySelectorAll('.raw-listener');
for (const button of rawLinkButtons) {
button.addEventListener("click", () => {
const rawLink = RawLink.generate(rsg);
window.location = rawLink;
});
}
const filterCommandData = function (data, { commandType, filter }) { const filterCommandData = function (data, { commandType, filter }) {
return data.filter(item => { return data.filter(item => {
if (!item.meta.includes(commandType)) { if (!item.meta.includes(commandType)) {
@ -540,7 +565,21 @@
}); });
} }
const uiElements = { const rsg = {
ip: localStorage.getItem('ip') || '10.10.10.10',
port: localStorage.getItem('port') || 9001,
shell: localStorage.getItem('shell') || rsgData.shells[0],
listener: localStorage.getItem('listener') || rsgData.listenerCommands[0][1],
encoding: localStorage.getItem('encoding') || 'None',
selectedValues: {
[CommandType.ReverseShell]: filterCommandData(rsgData.reverseShellCommands, { commandType: CommandType.ReverseShell })[0].name,
[CommandType.BindShell]: filterCommandData(rsgData.reverseShellCommands, { commandType: CommandType.BindShell })[0].name,
[CommandType.MSFVenom]: filterCommandData(rsgData.reverseShellCommands, { commandType: CommandType.MSFVenom })[0].name,
},
commandType: CommandType.ReverseShell,
filter: FilterType.All,
uiElements: {
[CommandType.ReverseShell]: { [CommandType.ReverseShell]: {
listSelection: '#reverse-shell-selection', listSelection: '#reverse-shell-selection',
command: '#reverse-shell-command' command: '#reverse-shell-command'
@ -553,16 +592,7 @@
listSelection: '#msfvenom-selection', listSelection: '#msfvenom-selection',
command: '#msfvenom-command' command: '#msfvenom-command'
} }
};
const rsg = {
selectedValues: {
[CommandType.ReverseShell]: filterCommandData(rsgData.reverseShellCommands, { commandType: CommandType.ReverseShell })[0].name,
[CommandType.BindShell]: filterCommandData(rsgData.reverseShellCommands, { commandType: CommandType.BindShell })[0].name,
[CommandType.MSFVenom]: filterCommandData(rsgData.reverseShellCommands, { commandType: CommandType.MSFVenom })[0].name,
}, },
commandType: CommandType.ReverseShell,
filter: FilterType.All,
copyToClipboard: (text) => { copyToClipboard: (text) => {
if (navigator ?.clipboard ?.writeText) { if (navigator ?.clipboard ?.writeText) {
@ -578,9 +608,13 @@
escapeHTML: (text) => String(text).replace(/</, '&lt;').replace(/>/, '&gt;'), escapeHTML: (text) => String(text).replace(/</, '&lt;').replace(/>/, '&gt;'),
getIP: () => localStorage.getItem("ip"), getIP: () => rsg.ip,
getPort: () => Number(portInput.value || portInput.getAttribute('placeholder')), getPort: () => Number(rsg.port),
getShell: () => rsg.shell,
getEncoding: () => rsg.encoding,
getSelectedCommandName: () => { getSelectedCommandName: () => {
return rsg.selectedValues[rsg.commandType]; return rsg.selectedValues[rsg.commandType];
@ -591,10 +625,41 @@
return reverseShellData.command; return reverseShellData.command;
}, },
generateReverseShellCommand: () => {
let command
if (rsg.getSelectedCommandName() === 'PowerShell #3 (Base64)') {
const encoder = (text) => text;
const payload = rsg.insertParameters(rsgData.specialCommands['PowerShell payload'], encoder)
command = "powershell -e " + btoa(payload)
} else {
command = rsg.getReverseShellCommand()
}
const encoding = rsg.getEncoding();
if (encoding === 'Base64') {
command = btoa(command)
} else {
function encoder(string) {
return (encoding === 'encodeURI' || encoding === 'encodeURIComponent') ? window[
encoding](string) : string
}
command = rsg.insertParameters(
rsg.highlightParameters(
encoder(command), encoder),
encoder
)
}
return command;
},
highlightParameters: (text, encoder) => { highlightParameters: (text, encoder) => {
const parameters = ['{ip}', '{port}', '{shell}', encodeURI('{ip}'), encodeURI('{port}'), const parameters = ['{ip}', '{port}', '{shell}', encodeURI('{ip}'), encodeURI('{port}'),
encodeURI('{shell}') encodeURI('{shell}')
]; ];
parameters.forEach((param) => { parameters.forEach((param) => {
if (encoder) param = encoder(param) if (encoder) param = encoder(param)
text = text.replace(param, `<span class="highlighted-parameter">${param}</span>`) text = text.replace(param, `<span class="highlighted-parameter">${param}</span>`)
@ -615,7 +680,7 @@
const option = document.createElement("option"); const option = document.createElement("option");
option.value = command; option.value = command;
option.selected = i === 0; option.selected = rsg.listener === option.value;
option.classList.add("listener-option"); option.classList.add("listener-option");
option.innerText = type; option.innerText = type;
@ -627,7 +692,7 @@
rsgData.shells.forEach((shell, i) => { rsgData.shells.forEach((shell, i) => {
const option = document.createElement("option"); const option = document.createElement("option");
option.selected = i === 0; option.selected = rsg.shell === shell;
option.classList.add("shell-option"); option.classList.add("shell-option");
option.innerText = shell; option.innerText = shell;
@ -636,13 +701,13 @@
}, },
// Updates the rsg state, and forces a re-render // Updates the rsg state, and forces a re-render
setState: ({ filter, commandType } = {}) => { setState: (newState = {}) => {
if (filter) { Object.keys(newState).forEach((key) => {
rsg.filter = filter; const value = newState[key];
} rsg[key] = value;
if (commandType) { localStorage.setItem(key, value)
rsg.commandType = commandType; });
} Object.assign(rsg, newState);
rsg.update(); rsg.update();
}, },
@ -651,13 +716,34 @@
return command return command
.replaceAll(encoder('{ip}'), rsg.escapeHTML(encoder(rsg.getIP()))) .replaceAll(encoder('{ip}'), rsg.escapeHTML(encoder(rsg.getIP())))
.replaceAll(encoder('{port}'), encoder(String(rsg.getPort()))) .replaceAll(encoder('{port}'), encoder(String(rsg.getPort())))
.replaceAll(encoder('{shell}'), encoder(shellSelect.value)) .replaceAll(encoder('{shell}'), encoder(rsg.getShell()))
}, },
update: () => { update: () => {
rsg.updateListenerCommand() rsg.updateListenerCommand()
rsg.updateTabList() rsg.updateTabList()
rsg.updateReverseShellCommand() rsg.updateReverseShellCommand()
rsg.updateValues()
},
updateValues: () => {
const listenerOptions = listenerSelect.querySelectorAll(".listener-option");
listenerOptions.forEach((option) => {
option.selected = rsg.listener === option.value;
});
const shellOptions = shellSelect.querySelectorAll(".shell-option");
shellOptions.forEach((option) => {
option.selected = rsg.shell === option.value;
});
const encodingOptions = encodingSelect.querySelectorAll("option");
encodingOptions.forEach((option) => {
option.selected = rsg.encoding === option.value;
});
ipInput.value = rsg.ip;
portInput.value = rsg.port;
}, },
updateTabList: () => { updateTabList: () => {
@ -670,8 +756,6 @@
} }
); );
//debugger;
const documentFragment = document.createDocumentFragment() const documentFragment = document.createDocumentFragment()
filteredItems.forEach((item, index) => { filteredItems.forEach((item, index) => {
const { const {
@ -701,8 +785,8 @@
documentFragment.appendChild(selectionButton); documentFragment.appendChild(selectionButton);
}) })
const listSelectionElement = uiElements[rsg.commandType].listSelection; const listSelectionSelector = rsg.uiElements[rsg.commandType].listSelection;
document.querySelector(listSelectionElement).replaceChildren(documentFragment) document.querySelector(listSelectionSelector).replaceChildren(documentFragment)
}, },
updateListenerCommand: () => { updateListenerCommand: () => {
@ -729,34 +813,9 @@
}, },
updateReverseShellCommand: () => { updateReverseShellCommand: () => {
let command const command = rsg.generateReverseShellCommand();
const commandSelector = rsg.uiElements[rsg.commandType].command;
if (rsg.getSelectedCommandName() === 'PowerShell #3 (Base64)') { document.querySelector(commandSelector).innerHTML = command;
const encoder = (text) => text;
const payload = rsg.insertParameters(rsgData.specialCommands['PowerShell payload'], encoder)
command = "powershell -e " + btoa(payload)
} else {
command = rsg.getReverseShellCommand()
}
const encoding = encodingSelect.value;
if (encoding === 'Base64') {
command = btoa(command)
} else {
function encoder(string) {
return (encoding === 'encodeURI' || encoding === 'encodeURIComponent') ? window[
encoding](string) : string
}
command = rsg.insertParameters(
rsg.highlightParameters(
encoder(command), encoder),
encoder
)
}
const commandElement = uiElements[rsg.commandType].command;
document.querySelector(commandElement).innerHTML = command;
}, },
updateSwitchStates: () => { updateSwitchStates: () => {
@ -776,18 +835,40 @@
/* /*
* Event handlers/functions * Event handlers/functions
*/ */
const dropdownUpdate = () => { ipInput.addEventListener("input", (e) => {
setLocalStorage(shellSelect, "shell", "value"); rsg.setState({
rsg.update(); ip: e.target.value
} })
});
shellSelect.addEventListener("change", dropdownUpdate); portInput.addEventListener("input", (e) => {
encodingSelect.addEventListener("change", dropdownUpdate); rsg.setState({
port: Number(e.target.value)
})
});
listenerSelect.addEventListener("change", (e) => {
rsg.setState({
listener: e.target.value
})
});
shellSelect.addEventListener("change", (e) => {
rsg.setState({
shell: e.target.value
})
});
encodingSelect.addEventListener("change", (e) => {
rsg.setState({
encoding: e.target.value
})
});
document.querySelector('#inc-port').addEventListener('click', () => { document.querySelector('#inc-port').addEventListener('click', () => {
portInput.value = rsg.getPort() + 1; rsg.setState({
rsg.update(); port: rsg.getPort() + 1
setLocalStorage(portInput, "port", "value"); })
}) })
document.querySelector('#listener-advanced-switch').addEventListener('change', rsg.updateSwitchStates); document.querySelector('#listener-advanced-switch').addEventListener('change', rsg.updateSwitchStates);
@ -811,96 +892,6 @@
rsg.copyToClipboard(msfVenomCommand.innerText) rsg.copyToClipboard(msfVenomCommand.innerText)
}) })
/*
* LocalStorage setting/getting
*/
/**
* Sets item to localStorage when user moves focus from element
* @param {Object} element - The element to get value from
* @param {String} key - Key of localStorage value
* @param {String} attribute - Attribute of element to set to localStorage value
*/
const setLocalStorage = (element, key, attribute) => {
if (!element || typeof element !== "object") return;
localStorage.setItem(key, element[attribute]);
}
/**
* Prepopulates element if localStorage value exists
* @param {String} key - Key of localStorage value
* @param {Object} element - The element to apply value to
* @param {String} attribute - Attribute of element to apply localStorage value to
*/
const prepopulateElement = (key, element, attribute, options = null) => {
if (localStorage.getItem(key)) {
// TODO: Use switch/case instead
if (element.type === "text") {
element[attribute] = localStorage.getItem(key);
}
if (element.type === "checkbox") {
const isChecked = (localStorage.getItem(key) !== 'false');
element[attribute] = isChecked;
}
if (element.nodeName === "SELECT") {
const selectedItem = options.find(option => option[attribute] === localStorage.getItem(key));
selectedItem.selected = true;
}
}
}
/*
* Prepopulating fields on load from localStorage
*/
// TODO: async/await
setTimeout(() => {
const listenerOptions = listenerSelect.querySelectorAll(".listener-option");
prepopulateElement("listener", listenerSelect, "value", [...listenerOptions]);
rsg.updateListenerCommand();
}, 500);
setTimeout(() => {
const shellOptions = shellSelect.querySelectorAll(".shell-option");
prepopulateElement("shell", shellSelect, "value", [...shellOptions]);
}, 500);
prepopulateElement("ip", ipInput, "value");
prepopulateElement("port", portInput, "value");
// prepopulateElement("auto-copy", autoCopySwitch, "checked");
/*
* Event listeners for setting localStorage values
*/
ipInput.addEventListener("input", () => {
setLocalStorage(ipInput, "ip", "value");
rsg.update();
});
portInput.addEventListener("input", () => {
setLocalStorage(portInput, "port", "value");
rsg.update();
});
listenerSelect.addEventListener("change", () => {
rsg.updateListenerCommand();
const listenerOptions = listenerSelect.querySelectorAll(".listener-option");
const selectedItem = [...listenerOptions].find(option => option.selected);
setLocalStorage(selectedItem, "listener", "value");
});
shellSelect.addEventListener("change", () => {
rsg.updateReverseShellSelection();
const shellOptions = shellSelect.querySelectorAll(".shell-option");
const selectedItem = [...shellOptions].find(option => option.selected);
setLocalStorage(selectedItem, "shell", "value");
});
// autoCopySwitch.addEventListener("change", () => { // autoCopySwitch.addEventListener("change", () => {
// setLocalStorage(autoCopySwitch, "auto-copy", "checked"); // setLocalStorage(autoCopySwitch, "auto-copy", "checked");
// }); // });

View File

@ -345,3 +345,8 @@ const rsgData = {
...msfvenomCommands ...msfvenomCommands
] ]
} }
// Export the data for use within netlify functions / node
if (typeof exports !== 'undefined') {
exports.rsgData = rsgData;
}

44
js/raw-link.js Normal file
View File

@ -0,0 +1,44 @@
/**
* Generates a RawLink for the reverse shell generator. If the user hasn't changed
* the default generated shell command, the generated URL will contain the original
* parameters to generate the required command on demand.
*
* Otherwise a unique URL is created which inlined the current user provided command.
*/
const RawLink = {
generate: (rsg) => {
const commandSelector = rsg.uiElements[rsg.commandType].command;
const currentCommandElement = document.querySelector(commandSelector);
const defaultGeneratedCommand = rsg.generateReverseShellCommand();
const isUserProvidedCommand = currentCommandElement.innerHTML != RawLink.escapeHTML(defaultGeneratedCommand);
if (isUserProvidedCommand) {
return RawLink.withCustomValue(currentCommandElement.innerText)
}
return RawLink.withDefaultPayload(rsg);
},
escapeHTML(html) {
var element = document.createElement('div');
element.innerHTML = html;
return element.innerHTML;
},
withDefaultPayload: (rsg) => {
const name = rsg.selectedValues[rsg.commandType];
const queryParams = new URLSearchParams();
queryParams.set('ip', rsg.getIP());
queryParams.set('port', rsg.getPort());
queryParams.set('shell', rsg.getShell());
queryParams.set('encoding', rsg.getShell());
return `/${encodeURIComponent(name)}?${queryParams}`
},
withCustomValue: (value) => {
const queryParams = new URLSearchParams();
queryParams.set('value', value)
return `/raw?${queryParams}`
}
}

7
netlify.toml Normal file
View File

@ -0,0 +1,7 @@
[build]
functions = "server_functions"
[[redirects]]
from = "/*"
to = "/.netlify/functions/raw"
status = 200

60
server_functions/raw.js Normal file
View File

@ -0,0 +1,60 @@
const { rsgData } = require("../js/data.js");
const insertParameters = function (command, params) {
// TODO: Extract the inlined JS from index.html into a new file,
// so the insertParameters + encoding logic can be reused
const encoder = (value) => value;
return command
.replace(encoder('{ip}'), encoder(params.ip))
.replace(encoder('{port}'), encoder(String(params.port)))
.replace(encoder('{shell}'), encoder(params.shell))
}
const generateCommand = function (event, _context) {
const { path, queryStringParameters } = event;
const requiredName = decodeURIComponent(path.substring(1));
const selectedItem = rsgData.reverseShellCommands.find(function ({ name }) {
return requiredName === name;
});
if (!selectedItem) {
return {
statusCode: 401,
body: `Command name '${requiredName}' not found`
}
}
const { command } = selectedItem;
const result = insertParameters(command, queryStringParameters);
return {
statusCode: 200,
body: result
}
}
const extractRawValue = function (event) {
const { queryStringParameters } = event;
return {
statusCode: 200,
body: queryStringParameters.value
}
}
exports.handler = async function (event, _context) {
const { queryStringParameters } = event;
const defaultHeaders = { headers: { 'Content-Type': "text/plain;charset=UTF-8" } };
if (queryStringParameters.value) {
return {
...defaultHeaders,
...extractRawValue(event)
}
}
return {
...defaultHeaders,
...generateCommand(event)
}
}