adding more scripting examples to command info

This commit is contained in:
its-a-feature
2026-01-01 09:49:30 -06:00
parent 3a6d9c4842
commit fd02fe2c9e
11 changed files with 279 additions and 21 deletions
+6
View File
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.90] - 2026-01-01
### Changed
- Added a new modal to the UI when looking at a payload's commands to get insight into how to task them via scripting
## [0.3.89] - 2025-12-31
### Changed
@@ -402,9 +402,10 @@ export const PaginationBar = ({selectAllOutput, totalCount, onSubmitPageChange,
}, [totalCount, maxCount, search, selectAllOutput]);
const pageCount = Math.max(1, Math.ceil(localTotalCount / pageSize));
// don't bother people with pagination information if they haven't even started paginating
if(pageCount < 2 || pageCount === Infinity){
if(pageCount < 2 || pageCount === Infinity || isNaN(pageCount)){
return (<div id={'scrolltotaskbottom' + task.id}></div>)
}
console.log(pageCount);
return (
<div id={'scrolltotaskbottom' + task.id} style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center", paddingBottom: "10px",}} >
<Pagination count={pageCount} page={currentPage} variant="contained" color="primary" showFirstButton showLastButton
@@ -28,19 +28,18 @@ import {MythicFileContext} from "../../MythicComponents/MythicFileContext";
import RefreshIcon from '@mui/icons-material/Refresh';
import {updateCredentialDeleted} from "../Search/CredentialTable";
export const getDynamicQueryParams = gql`
export const getDynamicQueryParamsString = `
mutation getDynamicParamsMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!, $other_parameters: jsonb){
dynamic_query_function(callback: $callback, command: $command, payload_type: $payload_type, parameter_name: $parameter_name, other_parameters: $other_parameters){
status
error
choices
parameter_name
complex_choices {
value
display_value
}
}
}
`
export const getDynamicQueryParams = gql`
${getDynamicQueryParamsString}
`;
const parseTypedArrayMutation = gql`
mutation parseTypedArrayMutation($callback: Int!, $command: String!, $payload_type: String!, $parameter_name: String!, $input_array: [String!]!){
@@ -2,6 +2,7 @@ import React, {useState} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import DialogTitle from '@mui/material/DialogTitle';
import {useQuery, gql} from '@apollo/client';
@@ -16,7 +17,9 @@ import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { useTheme } from '@mui/material/styles';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {PayloadTypeBuildDialog} from "./PayloadTypeBuildDialog";
import Paper from '@mui/material/Paper';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {getDynamicQueryParamsString} from "../Callbacks/TaskParametersDialogRow";
const GET_Payload_Details = gql`
query GetPayloadDetails($payload_name: String!) {
@@ -30,12 +33,38 @@ query GetPayloadDetails($payload_name: String!) {
}
}
`;
const GET_CommandParameters = gql`
query GetCommandParameters($command_id: Int!) {
commandparameters(where: {command_id: {_eq: $command_id}}) {
choice_filter_by_command_attributes
choices
choices_are_all_commands
choices_are_loaded_commands
cli_name
default_value
description
display_name
dynamic_query_function
limit_credentials_by_type
name
parameter_group_name
required
supported_agent_build_parameters
supported_agents
type
ui_position
verifier_regex
id
}
}
`;
export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
const [commands, setCommands] = useState([]);
const [openScriptDialog, setOpenScriptDialog] = useState({
open: false,
command_id: 0
command_id: 0,
command_name: "",
});
const theme = useTheme();
@@ -57,7 +86,7 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
const onClickOpenScriptDialog = (e, command) => {
e.preventDefault();
e.stopPropagation();
setOpenScriptDialog({open: true, command_id: command.id});
setOpenScriptDialog({open: true, command_id: command.id, command_name: command.cmd});
}
return (
@@ -111,7 +140,7 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
<MythicDialog fullWidth={true} maxWidth="lg" open={openScriptDialog.open}
onClose={()=>{setOpenScriptDialog({open: false, command_id: 0});}}
innerDialog={
<ScriptingCommandDialog command_id={openScriptDialog.command_id}
<ScriptingCommandDialog command_id={openScriptDialog.command_id} command_name={openScriptDialog.command_name}
onClose={()=>{setOpenScriptDialog({open: false, command_id: 0});}}
/>
}
@@ -126,12 +155,227 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
);
}
export function ScriptingCommandDialog({command_id, onClose}){
const exampleAgentConnect = `
"host":"hostname where remote payload/callback is running",
"agent_uuid":"payload uuid if trying to connect to payload",
"c2_profile":{
"name":"name of c2 profile",
"parameters":{
"parameter name":"parameter value",
},
"callback_uuid":"callback uuid if trying to connect to callback"
}
`;
const exampleUploadFile = (commandName, parameterName) => `newFileID = await mythic.register_file(
mythic=mythic_instance, filename="test.txt", contents=b"this is a test"
)
status = await mythic.issue_task(
mythic=mythic_instance,
command_name="${commandName}",
parameters={"${parameterName}": newFileID},
callback_display_id=1,
)
`;
const exampleCredentialJson = `{
"account":"tywin.lannister@SEVENKINGDOMS.LOCAL",
"comment":"",
"credential":"doIFO<..snip...>FM",
"realm":"SEVENKINGDOMS.LOCAL",
"type":"ticket"
}
`;
export function ScriptingCommandDialog({command_id, command_name, onClose}){
const [params, setParams] = useState([]);
const theme = useTheme();
useQuery(GET_CommandParameters, {
variables: {command_id: command_id},
onCompleted: data => {
const grouped = data.commandparameters.reduce( (prev, cur) => {
if(prev[cur.parameter_group_name] === undefined){
prev[cur.parameter_group_name] = [cur];
return prev;
}
prev[cur.parameter_group_name].push(cur);
prev[cur.parameter_group_name].sort((a, b) => {return a.ui_position < b.ui_position ? -1 : 1});
return prev;
}, {});
let groupedArray = [];
for(const key in grouped){
groupedArray.push(grouped[key]);
}
setParams(groupedArray);
console.log(groupedArray);
}
});
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Command Parameters</DialogTitle>
<DialogTitle id="form-dialog-title">{command_name}'s Parameters</DialogTitle>
<DialogContentText>
<Typography component={"span"} style={{marginLeft: "10px", display: "block"}}>
Each "Parameter Group" is one way to issue this task. These "Parameter Groups" allow a single task to offer a wider range of tasking options with similar code paths without creating new commands for each option and by cleaning up the total parameter options to make it less confusing.
</Typography>
<Typography component={"span"} style={{marginLeft: "10px", display: "block"}}>
<b>Note:</b> Parameters with "Required" must have a value supplied by you, but everything else has a "Default Value" that will be supplied if you choose not to submit one.
</Typography>
</DialogContentText>
<DialogContent dividers={true} style={{padding: 0}}>
{params.map( (paramGroup) => (
<div key={paramGroup[0].parameter_group_name}>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", }} variant={"elevation"}>
<Typography variant="h6" style={{textAlign: "left", display: "inline-block", marginLeft: "20px", color: theme.pageHeaderColor}}>
Parameter Group: {paramGroup[0].parameter_group_name}
</Typography>
</Paper>
<Table style={{tableLayout: "fixed"}}>
<TableHead>
<TableRow>
<TableCell style={{width: "40%"}}>Name</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{paramGroup.map( (param) => (
<TableRow key={param.id}>
<MythicStyledTableCell >
<Typography style={{fontWeight: "600", wordBreak: "break-all"}} >
{param.name}
</Typography>
<Typography variant={"body2"} style={{
fontSize: theme.typography.pxToRem(14),
wordBreak: "break-all"
}}>
{param.description}
</Typography>
{param.required && <Typography component="div" color={"warning"}>Required</Typography>}
</MythicStyledTableCell>
<TableCell style={{whiteSpace: "pre-wrap"}}>
<Typography>
<b>Value Type:</b> {param.type}
</Typography>
{param.verifier_regex !== "" &&
<Typography>
Note: Value must match the following regex: {param.verifier_regex}
</Typography>
}
<Typography>
<b>Default Value:</b> {param.default_value}
</Typography>
{param.choices.length > 0 &&
<Typography>
Choose from the following: {param.choices.join(", ")}
</Typography>
}
{param.choices_are_all_commands &&
<Typography>
<b>Note:</b> Provide any command name
</Typography>
}
{param.choices_are_loaded_commands &&
<Typography>
<b>Note:</b> Provide any currently loaded command name
</Typography>
}
{param.dynamic_query_function !== "" &&
<Typography>
<b>Note:</b> This command in the UI will dynamically give you a list of options to choose from either through the modal or via tab complete.
The following mutation can be used to dynamically generate the same choices. In this case, "other_parameters", are just the values of the other parameters you've set for this command in case they offer meaningful context for this dynamic function. They are provided as a standard dictionary (not a string)
<div className={"code-box"}>
<code>
{getDynamicQueryParamsString}
</code>
</div>
</Typography>
}
{param.type === "File" &&
<Typography>
<b>Note:</b> This parameter expects a file's AgentFileID (UUID) value. When issuing this task through the UI, you will be prompted to select a file from your file system to upload.
When scripting, you should upload a new file first and provide the returned AgentFileID here.<br/>
<div className={"code-box"}>
<code >
{exampleUploadFile(command_name, param.name)}
</code>
</div>
</Typography>
}
{param.type === "TypedArray" &&
<Typography>
<b>Note:</b> TypedArrays are arrays of strings that also carry along a "type" identifier with it. This is expressed in scripting and the code as an array of arrays.
In the UI, tab complete or the modal will show you how to fill this out.
When scripting, you can provide this nested array explicitly:
<div className={"code-box"}>
<code>
[ ["{param.choices[0]}", "test"], ["{param.choices[0]}", "values"] ]
</code>
</div>
</Typography>
}
{param.type === "AgentConnect" &&
<Typography>
<b>Note:</b> This parameter type is more complex and is meant to provide a handy way in the UI to allow an operator to select a payload or callback to connect to. In the UI, this dynamically resolves hosts and their payloads/callbacks to list out their P2P C2 profile options so that you can link up without remember things like randomized named pipe values.
You can do the same thing programmatically, but you'll have to fetch the pieces of information yourself and set the value as the resulting dictionary:
<div className={"code-box"}>
<code>
{exampleAgentConnect}
</code>
</div>
</Typography>
}
{param.type === "LinkInfo" &&
<Typography>
<b>Note:</b> This parameter type is more complex and is meant to provide a handy way in the UI to allow an operator to select an existing P2P link for either unlinking or relinking. The UI dynamically resolves existing "callbackgraphedge" values and their info.
You can do the same thing programmatically, but you'll have to fetch the pieces of information yourself and set the value as the resulting dictionary. You'll notice that the data is the exact same as if the type was "AgentConnect", it's just displayed in a different format in the UI.
<div className={"code-box"}>
<code>
{exampleAgentConnect}
</code>
</div>
</Typography>
}
{param.type === "PayloadList" &&
<>
<Typography>
<b>Note:</b> This parameter simply supplies a list of payloads for the user to select. From here, the actual value that's passed along is just the UUID of the payload the user selected.
</Typography>
{param.supported_agents.length > 0 &&
<Typography>
<b>Note:</b> This command has an explicit list of agents it supports; this is typically done in conjunction with the "PayloadList" type to help reduce the options to only valid ones.
<br/>The supported agents are: {param.supported_agents.join(", ")}
</Typography>
}
{Object.keys(param.supported_agent_build_parameters).length > 0 &&
<Typography>
<b>Note:</b> This command has an explicit list of build parameter values that must be set for them to be considered valid.
<br/>The supported agents and build parameter values are: {JSON.stringify(param.supported_agent_build_parameters, null, 2)}
</Typography>
}
</>
}
{param.type === "CredentialJson" &&
<>
<Typography>
<b>Note:</b> This parameter type allows the user to explicitly select a credential that exists in Mythic's credential store and pass along all of that data to the command.
The value supplied here is the actual JSON credential data like follows:
<div className={"code-box"}>
<code>{exampleCredentialJson}</code>
</div>
</Typography>
{param.limit_credentials_by_type.length > 0 &&
<Typography>
<b>Note:</b> This command limits the credential to those with the following types: {param.limit_credentials_by_type.join(", ")}
</Typography>
}
</>
}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
))}
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={onClose} color="primary">
+1 -1
View File
@@ -15,7 +15,7 @@ import {jwtDecode} from 'jwt-decode';
import {meState} from './cache';
import {getSkewedNow} from "./components/utilities/Time";
export const mythicUIVersion = "0.3.89";
export const mythicUIVersion = "0.3.90";
let fetchingNewToken = false;
+8
View File
@@ -398,4 +398,12 @@ tspan {
padding: 0px !important;
margin: 0px !important;
}
.code-box {
border: 1px solid grey !important;
padding: 0px 5px 0px 5px;
overflow: auto;
white-space: pre;
background-color: black;
color: white;
}
`
@@ -1,17 +1,17 @@
{
"files": {
"main.css": "/new/static/css/main.262ce320.css",
"main.js": "/new/static/js/main.2e1e3693.js",
"main.js": "/new/static/js/main.e0f3c0c9.js",
"sql-wasm.wasm": "/new/sql-wasm-7f26ab750fcdafabbe18c6525a66a0bd.wasm",
"static/media/mythic-red.png": "/new/static/media/mythic-red.203468a4e5240d239aa0.png",
"static/media/graphql.png": "/new/static/media/graphql.8f15978b39b0870a9f0e.png",
"static/media/Mythic_Logo.svg": "/new/static/media/Mythic_Logo.6842c911bebe36d6f83fc7ced4a2cd99.svg",
"index.html": "/new/index.html",
"main.262ce320.css.map": "/new/static/css/main.262ce320.css.map",
"main.2e1e3693.js.map": "/new/static/js/main.2e1e3693.js.map"
"main.e0f3c0c9.js.map": "/new/static/js/main.e0f3c0c9.js.map"
},
"entrypoints": [
"static/css/main.262ce320.css",
"static/js/main.2e1e3693.js"
"static/js/main.e0f3c0c9.js"
]
}
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/new/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="apple-touch-icon" href="/new/logo192.png"/><link rel="manifest" href="/new/manifest.json"/><title>Mythic</title><script defer="defer" src="/new/static/js/main.2e1e3693.js"></script><link href="/new/static/css/main.262ce320.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/new/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="apple-touch-icon" href="/new/logo192.png"/><link rel="manifest" href="/new/manifest.json"/><title>Mythic</title><script defer="defer" src="/new/static/js/main.e0f3c0c9.js"></script><link href="/new/static/css/main.262ce320.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>