mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
419 lines
19 KiB
JavaScript
419 lines
19 KiB
JavaScript
import {MythicTabPanel, MythicTabLabel} from '../../MythicComponents/MythicTabPanel';
|
|
import React, {useEffect, useRef, useCallback, useLayoutEffect} from 'react';
|
|
import { gql, useMutation, useLazyQuery, useSubscription } from '@apollo/client';
|
|
import { TaskDisplayConsole } from './TaskDisplay';
|
|
import {snackActions} from '../../utilities/Snackbar';
|
|
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
|
import {TaskParametersDialog} from './TaskParametersDialog';
|
|
import {CallbacksTabsTaskingInput} from './CallbacksTabsTaskingInput';
|
|
import LinearProgress from '@mui/material/LinearProgress';
|
|
import { IconButton} from '@mui/material';
|
|
import AutorenewIcon from '@mui/icons-material/Autorenew';
|
|
import {MythicModifyStringDialog} from '../../MythicComponents/MythicDialog';
|
|
import { MythicStyledTooltip } from '../../MythicComponents/MythicStyledTooltip';
|
|
import {taskingDataFragment, createTaskingMutation} from "./CallbackMutations";
|
|
import { validate as uuidValidate } from 'uuid';
|
|
import {getSkewedNow} from "../../utilities/Time";
|
|
|
|
|
|
export function CallbacksTabsTaskingConsoleLabel(props){
|
|
const [description, setDescription] = React.useState(props.tabInfo.payloadDescription !== props.tabInfo.callbackDescription ? props.tabInfo.callbackDescription : "Callback: " + props.tabInfo.displayID)
|
|
const [openEditDescriptionDialog, setOpenEditDescriptionDialog] = React.useState(false);
|
|
useEffect( () => {
|
|
if(props.tabInfo.customDescription !== "" && props.tabInfo.customDescription !== undefined){
|
|
setDescription(props.tabInfo.customDescription);
|
|
}else if(props.tabInfo.payloadDescription !== props.tabInfo.callbackDescription){
|
|
setDescription(props.tabInfo.callbackDescription);
|
|
}else{
|
|
setDescription("Console Callback: " + props.tabInfo.displayID);
|
|
}
|
|
}, [props.tabInfo.payloadDescription, props.tabInfo.customDescription]);
|
|
useEffect( () => {
|
|
let savedDescription = localStorage.getItem(`${props.me.user.id}-${props.tabInfo.operation_id}-${props.tabInfo.tabID}`);
|
|
if(savedDescription && savedDescription !== ""){
|
|
setDescription(savedDescription);
|
|
}
|
|
}, []);
|
|
const editDescriptionSubmit = (description) => {
|
|
props.onEditTabDescription(props.tabInfo, description);
|
|
localStorage.setItem(`${props.me.user.id}-${props.tabInfo.operation_id}-${props.tabInfo.tabID}`, description);
|
|
}
|
|
const contextMenuOptions = props.contextMenuOptions.concat([
|
|
{
|
|
name: 'Set Tab Description',
|
|
click: ({event}) => {
|
|
setOpenEditDescriptionDialog(true);
|
|
}
|
|
},
|
|
]);
|
|
return (
|
|
<React.Fragment>
|
|
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
|
|
{openEditDescriptionDialog &&
|
|
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
|
|
innerDialog={
|
|
<MythicModifyStringDialog title={"Edit Tab's Description - Displays as one line"} onClose={() => {setOpenEditDescriptionDialog(false);}} value={description} onSubmit={editDescriptionSubmit} />
|
|
}
|
|
/>
|
|
}
|
|
</React.Fragment>
|
|
)
|
|
}
|
|
|
|
// this is to listen for the latest tasking
|
|
const fetchLimit = 20;
|
|
const getTaskingQuery = gql`
|
|
${taskingDataFragment}
|
|
subscription getTasking($callback_id: Int!, $fromNow: timestamp!, $limit: Int){
|
|
task(where: {callback_id: {_eq: $callback_id}, parent_task_id: {_is_null: true}, timestamp: {_gt: $fromNow}}, order_by: {id: desc}, limit: $limit) {
|
|
...taskData
|
|
}
|
|
}
|
|
`;
|
|
const getNextBatchTaskingQuery = gql`
|
|
${taskingDataFragment}
|
|
query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
|
|
task(where: {callback_id: {_eq: $callback_id}, parent_task_id: {_is_null: true}}, order_by: {id: desc}, limit: $fetchLimit, offset: $offset) {
|
|
...taskData
|
|
}
|
|
callback(where: {id: {_eq: $callback_id}}){
|
|
id
|
|
display_id
|
|
}
|
|
}
|
|
`;
|
|
export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me, setNewDataForTab}) =>{
|
|
const [taskLimit, setTaskLimit] = React.useState(10);
|
|
const [scrollToBottom, setScrollToBottom] = React.useState(false);
|
|
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
|
|
const [commandInfo, setCommandInfo] = React.useState({});
|
|
const [taskingData, setTaskingData] = React.useState({task: []});
|
|
const taskingDataRef = React.useRef({task: []});
|
|
const [fromNow, setFromNow] = React.useState(getSkewedNow().toISOString());
|
|
const [selectedToken, setSelectedToken] = React.useState({});
|
|
const [filterOptions, setFilterOptions] = React.useState({
|
|
"operatorsList": [],
|
|
"commentsFlag": false,
|
|
"commandsList": [],
|
|
"parameterString": "",
|
|
"everythingButList": [],
|
|
"hideErrors": false
|
|
});
|
|
const [canScroll, setCanScroll] = React.useState(true);
|
|
const mountedRef = React.useRef(true);
|
|
useEffect( () => {
|
|
taskingDataRef.current = taskingData;
|
|
}, [taskingData]);
|
|
const [fetched, setFetched] = React.useState(false);
|
|
const [fetchedAllTasks, setFetchedAllTasks] = React.useState(false);
|
|
const messagesEndRef = useRef(null);
|
|
const newlyIssuedTasks = useRef([]);
|
|
const [createTask] = useMutation(createTaskingMutation, {
|
|
update: (cache, {data}) => {
|
|
if(data.createTask.status === "error"){
|
|
snackActions.error(data.createTask.error);
|
|
}else{
|
|
newlyIssuedTasks.current.push(data.createTask.id);
|
|
//snackActions.success("Task created", {autoClose: 1000});
|
|
}
|
|
},
|
|
onError: data => {
|
|
console.error(data);
|
|
}
|
|
});
|
|
const equalTaskTrees = (oldArray, newArray) => {
|
|
if(oldArray.length !== newArray.length){
|
|
return false;
|
|
}
|
|
for(let i = 0; i < oldArray.length; i++){
|
|
if(oldArray[i].comment !== newArray[i].comment){
|
|
return false;
|
|
}
|
|
if(oldArray[i].commentOperator !== newArray[i].commentOperator){
|
|
return false;
|
|
}
|
|
if(oldArray[i].completed !== newArray[i].completed){
|
|
return false;
|
|
}
|
|
if(oldArray[i].display_params !== newArray[i].display_params){
|
|
return false;
|
|
}
|
|
if(oldArray[i].original_params !== newArray[i].original_params){
|
|
return false;
|
|
}
|
|
if(oldArray[i].status !== newArray[i].status){
|
|
return false;
|
|
}
|
|
if(oldArray[i].timestamp !== newArray[i].timestamp){
|
|
return false;
|
|
}
|
|
if(oldArray[i].opsec_pre_blocked !== newArray[i].opsec_pre_blocked){
|
|
return false;
|
|
}
|
|
if(oldArray[i].opsec_pre_bypassed !== newArray[i].opsec_pre_bypassed){
|
|
return false;
|
|
}
|
|
if(oldArray[i].opsec_post_blocked !== newArray[i].opsec_post_blocked){
|
|
return false;
|
|
}
|
|
if(oldArray[i].opsec_post_bypassed !== newArray[i].opsec_post_bypassed){
|
|
return false;
|
|
}
|
|
if(oldArray[i].tasks.length !== newArray[i].tasks.length){
|
|
return false;
|
|
}
|
|
if(oldArray[i].tags.length !== newArray[i].tags.length){
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
const subscriptionDataCallback = ({data}) => {
|
|
if((mountedRef && !mountedRef.current) || (parentMountedRef && !parentMountedRef.current)){
|
|
return null;
|
|
}
|
|
if(!fetched){
|
|
setFetched(true);
|
|
}
|
|
if(index !== value && fetched){
|
|
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
|
|
}
|
|
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
|
|
const oldLength = taskingDataRef.current.task.length;
|
|
const mergedData = data.data.task.reduce( (prev, cur) => {
|
|
const index = prev.findIndex(element => element.id === cur.id);
|
|
if(index > -1){
|
|
// need to update an element
|
|
const updated = prev.map( (element) => {
|
|
if(element.id === cur.id){
|
|
return cur;
|
|
}else{
|
|
return element;
|
|
}
|
|
});
|
|
return updated;
|
|
}else{
|
|
return [...prev, cur];
|
|
}
|
|
}, [...taskingDataRef.current.task]);
|
|
mergedData.sort( (a,b) => a.id < b.id ? -1 : 1);
|
|
if(!equalTaskTrees(taskingDataRef.current.task, mergedData)){
|
|
setTaskingData({task: mergedData});
|
|
}
|
|
if(mergedData.length > oldLength){
|
|
setCanScroll(true);
|
|
}
|
|
if(mergedData.length > taskLimit){
|
|
setTaskLimit(mergedData.length);
|
|
}
|
|
}
|
|
useSubscription(getTaskingQuery, {
|
|
variables: {callback_id: tabInfo.callbackID, fromNow:fromNow, limit: taskLimit},
|
|
shouldResubscribe: true,
|
|
onError: data => {
|
|
console.error(data)
|
|
},
|
|
fetchPolicy: "no-cache",
|
|
onData: subscriptionDataCallback});
|
|
const [getInfiniteScrollTasking, {loading: loadingMore}] = useLazyQuery(getNextBatchTaskingQuery, {
|
|
onError: data => {
|
|
console.error(data);
|
|
},
|
|
onCompleted: (data) => {
|
|
let foundNew = false;
|
|
if(data.callback.length === 0){
|
|
onCloseTab(tabInfo);
|
|
return;
|
|
}
|
|
const mergedData = data.task.reduce( (prev, cur) => {
|
|
const index = prev.findIndex(element => element.id === cur.id);
|
|
if(index > -1){
|
|
// need to update an element
|
|
const updated = prev.map( (element) => {
|
|
if(element.id === cur.id){
|
|
return cur;
|
|
}else{
|
|
return element;
|
|
}
|
|
});
|
|
return updated;
|
|
}else{
|
|
foundNew = true;
|
|
return [...prev, cur];
|
|
}
|
|
}, [...taskingData.task]);
|
|
mergedData.sort( (a,b) => a.id < b.id ? -1 : 1);
|
|
setTaskingData({task: mergedData});
|
|
if(!foundNew){
|
|
setFetchedAllTasks(true);
|
|
}else{
|
|
if(data.task.length < fetchLimit){
|
|
setFetchedAllTasks(true);
|
|
}else{
|
|
setFetchedAllTasks(false);
|
|
}
|
|
}
|
|
if(!scrollToBottom){setScrollToBottom(true)}
|
|
},
|
|
fetchPolicy: "no-cache"
|
|
});
|
|
useEffect( () => {
|
|
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
|
|
setCanScroll(true);
|
|
return() => {
|
|
mountedRef.current = false;
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
useEffect( () => {
|
|
if(scrollToBottom){
|
|
messagesEndRef.current.scrollIntoView();
|
|
}
|
|
}, [scrollToBottom]);
|
|
const loadMoreTasks = () => {
|
|
getInfiniteScrollTasking({variables: {callback_id: tabInfo.callbackID, offset: taskingData.task.length, fetchLimit}});
|
|
}
|
|
const onSubmitCommandLine = (message, cmd, parsed, force_parsed_popup, cmdGroupNames, previousTaskingLocation) => {
|
|
//console.log(message, cmd, parsed);
|
|
let params = message.split(" ");
|
|
delete params[0];
|
|
params = params.join(" ").trim();
|
|
let newTaskingLocation = "parsed_cli";
|
|
if(previousTaskingLocation.includes("modal")){
|
|
newTaskingLocation = "modal_modified"
|
|
}else if(previousTaskingLocation.includes("browserscript")){
|
|
newTaskingLocation = "browserscript_modified";
|
|
}
|
|
if(cmd.commandparameters.length === 0){
|
|
// if there are no parameters, just send whatever the user types along
|
|
onCreateTask({callback_id: tabInfo.displayID,
|
|
command: cmd.cmd,
|
|
params: params,
|
|
parameter_group_name: "Default",
|
|
tasking_location: newTaskingLocation,
|
|
payload_type: cmd.payloadtype?.name,
|
|
});
|
|
}else{
|
|
// check if there's a "file" component that needs to be displayed
|
|
const fileParamExists = cmd.commandparameters.find(param => {
|
|
if(param.parameter_type === "File" && cmdGroupNames.includes(param.parameter_group_name)){
|
|
if(!(param.cli_name in parsed || param.name in parsed || param.display_name in parsed)){
|
|
return true;
|
|
}
|
|
if(param.cli_name in parsed && uuidValidate(parsed[param.cli_name])){
|
|
return false; // no need for a popup, already have a valid file specified
|
|
} else if(param.name in parsed && uuidValidate(parsed[param.name])){
|
|
return false;
|
|
} else if(param.display_name in parsed && uuidValidate(parsed[param.display_name])){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
});
|
|
//console.log("missing File for group? ", fileParamExists, cmdGroupNames);
|
|
let missingRequiredPrams = false;
|
|
if(cmdGroupNames.length === 1){
|
|
const missingParams = cmd.commandparameters.filter(param => param.required && param.parameter_group_name === cmdGroupNames[0] && !(param.cli_name in parsed || param.name in parsed || param.display_name in parsed));
|
|
if(missingParams.length > 0){
|
|
missingRequiredPrams = true;
|
|
console.log("missing required params", missingParams,parsed);
|
|
}
|
|
}else if(cmdGroupNames > 1 && !force_parsed_popup){
|
|
// need to force a popup because the tasking is ambiguous
|
|
console.log("command is ambiguous");
|
|
force_parsed_popup = true;
|
|
}
|
|
if(fileParamExists || force_parsed_popup || missingRequiredPrams){
|
|
//need to do a popup
|
|
if(cmdGroupNames.length > 0){
|
|
setCommandInfo({...cmd, "parsedParameters": parsed, groupName: cmdGroupNames[0]});
|
|
}else{
|
|
setCommandInfo({...cmd, "parsedParameters": parsed});
|
|
}
|
|
setOpenParametersDialog(true);
|
|
|
|
}else{
|
|
delete parsed["_"];
|
|
onCreateTask({callback_id: tabInfo.displayID,
|
|
command: cmd.cmd,
|
|
params: JSON.stringify(parsed),
|
|
tasking_location: newTaskingLocation,
|
|
original_params: params,
|
|
parameter_group_name: cmdGroupNames[0],
|
|
payload_type: cmd.payloadtype?.name,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const submitParametersDialog = (cmd, parameters, files, selectedParameterGroup, payload_type) => {
|
|
setOpenParametersDialog(false);
|
|
onCreateTask({callback_id: tabInfo.displayID,
|
|
command: cmd,
|
|
params: parameters,
|
|
files: files,
|
|
tasking_location: "modal",
|
|
parameter_group_name: selectedParameterGroup,
|
|
payload_type: payload_type
|
|
});
|
|
}
|
|
const onCreateTask = ({callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}) => {
|
|
if(selectedToken.token_id !== undefined){
|
|
createTask({variables: {callback_id, command, params, files, token_id: selectedToken.token_id, tasking_location, original_params, parameter_group_name, payload_type}});
|
|
}else{
|
|
createTask({variables: {callback_id, command, params, files, tasking_location, original_params, parameter_group_name, payload_type}});
|
|
}
|
|
}
|
|
const onSubmitFilter = (newFilter) => {
|
|
setFilterOptions(newFilter);
|
|
}
|
|
const changeSelectedToken = (token) => {
|
|
if(token === "Default Token"){
|
|
setSelectedToken("Default Token");
|
|
return;
|
|
}
|
|
if(token.token_id !== selectedToken.token_id){
|
|
setSelectedToken(token);
|
|
}
|
|
}
|
|
return (
|
|
<MythicTabPanel index={index} value={value} >
|
|
|
|
{!fetched && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
|
|
{loadingMore && <LinearProgress color="primary" thickness={2} style={{paddingTop: "5px"}}/>}
|
|
<div style={{overflowY: "auto", flexGrow: 1, width: "100%"}} id={`taskingPanelConsole${tabInfo.callbackID}`}>
|
|
{!fetchedAllTasks &&
|
|
<MythicStyledTooltip title="Fetch Older Tasks" style={{marginLeft: "50%"}}>
|
|
<IconButton
|
|
onClick={loadMoreTasks}
|
|
variant="contained"
|
|
color="success"
|
|
|
|
size="large"><AutorenewIcon /></IconButton>
|
|
</MythicStyledTooltip>}
|
|
{
|
|
taskingData.task.map((task) => (
|
|
<TaskDisplayConsole key={"taskinteractdisplayconsole" + task.id} me={me} task={task}
|
|
command_id={task.command == null ? 0 : task.command.id}
|
|
filterOptions={filterOptions} newlyIssuedTasks={newlyIssuedTasks.current}/>
|
|
))
|
|
}
|
|
<div ref={messagesEndRef}/>
|
|
</div>
|
|
|
|
<CallbacksTabsTaskingInput filterTasks={true} me={me} onSubmitFilter={onSubmitFilter} onSubmitCommandLine={onSubmitCommandLine} changeSelectedToken={changeSelectedToken}
|
|
filterOptions={filterOptions} callback_id={tabInfo.callbackID}
|
|
payloadtype_name={tabInfo.payloadtype} focus={index === value}
|
|
callback_os={tabInfo.os} parentMountedRef={mountedRef} />
|
|
{openParametersDialog &&
|
|
<MythicDialog fullWidth={true} maxWidth="lg" open={openParametersDialog}
|
|
onClose={()=>{setOpenParametersDialog(false);}}
|
|
innerDialog={<TaskParametersDialog command={commandInfo} callback_id={tabInfo.callbackID}
|
|
payloadtype_id={tabInfo.payloadtype_id} operation_id={tabInfo.operation_id}
|
|
onSubmit={submitParametersDialog} onClose={()=>{setOpenParametersDialog(false);}}
|
|
/>}
|
|
/>
|
|
}
|
|
|
|
</MythicTabPanel>
|
|
);
|
|
} |