mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
v3.2.17
bug fixes and new features for mythic-cli, mythic_server, and the UI
This commit is contained in:
@@ -37,6 +37,7 @@ Docker_Templates/
|
||||
documentation-docker/content/
|
||||
documentation-docker/public/
|
||||
display_output.txt
|
||||
full_attack.json
|
||||
nginx-docker/config/conf.d/services.conf
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
@@ -4,6 +4,16 @@ 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).
|
||||
|
||||
## [3.2.17] - 2024-02-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Added ability to export a callback (via callback dropdown) and import callback (via speeddial on top right of callbacks page)
|
||||
- Added a new environment variable, `global_server_name`, that gets passed down to webhook and logging containers
|
||||
- Added new `mythic-cli config help` subcommand to get helpful descriptions of all environment variables in .env file
|
||||
- Updated logging to track user_id, username, and source of requests
|
||||
- Updated internal MITRE ATT&CK to the latest as of 2024-02-06
|
||||
|
||||
## [3.2.16] - 2024-01-28
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -4,6 +4,21 @@ 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.1.62] - 2024-02-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Added syntax highlighter and wrap option to plaintext output
|
||||
- Added a little buffer above the task input field so it's a bit more obvious when the last task's output is done
|
||||
- Fixed a bug where multiple browserscript elements would be aligned horizontally instead of vertically
|
||||
- Fixed a bug where cancelling a dialog modal would prevent future dialog modals until page refresh
|
||||
- Added new features for tagging data:
|
||||
- Tag data values can now be formatted in partial markdown for additional configuration options
|
||||
- The following format is available: `"key": "[display data](url){:target=\"_blank\"}"`
|
||||
- This is displayed as a clickable link text "display data" that opens a new page to `url`
|
||||
- The following format is available: `"key": "[display data](url){:target=\"api\",:color=\"success\"}"`
|
||||
- This is displayed as a colored webhook icon that hits the url api with a GET request to `url` and has "display data" next to it
|
||||
|
||||
## [0.1.61] - 2024-01-29
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -74,7 +74,9 @@ export const MythicFileContext = ({agent_file_id, display_link, filename}) => {
|
||||
onCompleted: (data) => {
|
||||
setFileData( {...fileData, filename: b64DecodeUnicode(data.filemeta[0].filename_text)});
|
||||
if(display_link === "" || display_link === undefined){
|
||||
setFileData( {...fileData, display_link: b64DecodeUnicode(data.filemeta[0].filename_text)});
|
||||
setFileData( {...fileData,
|
||||
filename: b64DecodeUnicode(data.filemeta[0].filename_text),
|
||||
display_link: b64DecodeUnicode(data.filemeta[0].filename_text)});
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
|
||||
@@ -21,8 +21,10 @@ import { snackActions } from '../utilities/Snackbar';
|
||||
import { MythicDialog } from './MythicDialog';
|
||||
import {MythicConfirmDialog} from './MythicConfirmDialog';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import WebhookIcon from '@mui/icons-material/Webhook';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import LocalOfferOutlinedIcon from '@mui/icons-material/LocalOfferOutlined';
|
||||
import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
|
||||
const createNewTagMutationTemplate = ({target_object}) => {
|
||||
// target_object should be something like "task_id"
|
||||
@@ -120,13 +122,68 @@ const TagChipDisplay = ({tag}) => {
|
||||
<React.Fragment>
|
||||
<Chip label={tag.tagtype.name} size="small" onClick={(e) => onSelectTag(e)} style={{float: "right", backgroundColor:tag.tagtype.color}} />
|
||||
{openTagDisplay &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openTagDisplay}
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openTagDisplay}
|
||||
onClose={onClose}
|
||||
innerDialog={<ViewTagDialog onClose={onClose} target_object_id={tag.id}/>}
|
||||
/>}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
const StringTagDataEntry = ({name, value}) => {
|
||||
// want to match markdown [display](url)
|
||||
const regex = "^\\[.*\\]\\(.*\\)";
|
||||
const captureRegex = "^\\[(?<display>.*)\\]\\((?<url>.*)\\)(?<other>.*)";
|
||||
const targetRegex = ":target=[\"\'](?<target>.*?)[\"\']";
|
||||
const colorRegex = ":color=[\"\'](?<color>.*?)[\"\']";
|
||||
const onClick = (e, url) => {
|
||||
e.preventDefault();
|
||||
fetch(url).then((response) => {
|
||||
if (response.status !== 200) {
|
||||
snackActions.warning("HTTP " + response.status + " response");
|
||||
} else {
|
||||
snackActions.success("Successfully contacted url");
|
||||
}
|
||||
}).catch(error => {
|
||||
if(error.toString() === "TypeError: Failed to fetch"){
|
||||
snackActions.warning("Failed to make connection - this could be networking issues or ssl certs that need to be accepted first");
|
||||
} else {
|
||||
snackActions.warning("Error talking to server: " + error.toString());
|
||||
}
|
||||
console.log("There was an error!", error);
|
||||
})
|
||||
}
|
||||
if(RegExp(regex).test(value)){
|
||||
const capturePieces = RegExp(captureRegex).exec(value);
|
||||
const targetPieces = RegExp(targetRegex).exec(capturePieces[3]);
|
||||
const colorPieces = RegExp(colorRegex).exec(capturePieces[3]);
|
||||
if(targetPieces && targetPieces["groups"]["target"] === "api"){
|
||||
let color = "textPrimary";
|
||||
if(colorPieces && colorPieces["groups"]["color"]){
|
||||
color = colorPieces["groups"]["color"];
|
||||
}
|
||||
return (
|
||||
<MythicStyledTooltip title={"Make API Request"}>
|
||||
<WebhookIcon style={{cursor: "pointer", marginRight: "10px"}}
|
||||
onClick={(e) => onClick(e, capturePieces[2])}
|
||||
color={color}
|
||||
/>
|
||||
{capturePieces[1]}
|
||||
</MythicStyledTooltip>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Link href={capturePieces[2]} color="textPrimary" target={"_blank"} referrerPolicy='no'>{capturePieces[1]}</Link>
|
||||
)
|
||||
} else if(value.startsWith("http")){
|
||||
return (
|
||||
<>
|
||||
{"Click for: "}
|
||||
<Link href={value} color="textPrimary" target="_blank" referrerPolicy='no'>{name}</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function ViewTagDialog(props) {
|
||||
const theme = useTheme();
|
||||
const [selectedTag, setSelectedTag] = React.useState({});
|
||||
@@ -165,7 +222,7 @@ return (
|
||||
<TableBody>
|
||||
<TableRow hover>
|
||||
<TableCell style={{width: "20%"}}>Tag Type</TableCell>
|
||||
<TableCell style={{display: "inline-flex", flexDirection: "row-reverse"}}>
|
||||
<TableCell style={{display: "inline-flex", flexDirection: "row", width: "100%"}}>
|
||||
<Chip label={selectedTag?.tagtype?.name||""} size="small" style={{float: "right", backgroundColor:selectedTag?.tagtype?.color||""}} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -182,26 +239,23 @@ return (
|
||||
<TableRow hover>
|
||||
<TableCell>Reference URL</TableCell>
|
||||
<TableCell>
|
||||
<Link href={selectedTag?.url || "#"} color="textPrimary" target="_blank" referrerPolicy='no'>{selectedTag?.url ? "click here" : "No link provided"}</Link>
|
||||
<Link href={selectedTag?.url || "#"} color="textPrimary" target="_blank" referrerPolicy='no'>{selectedTag?.url ? "click here" : "No reference link provided"}</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Data</TableCell>
|
||||
<TableCell>
|
||||
{selectedTag?.is_json || false ? (
|
||||
<TableContainer component={Paper} className="mythicElement">
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableBody>
|
||||
{Object.keys(selectedTag.data).map( key => (
|
||||
<TableRow key={key} hover>
|
||||
<TableCell>{key}</TableCell>
|
||||
{typeof selectedTag.data[key] === "string" ? (
|
||||
<TableCell>{selectedTag.data[key].startsWith("http") ? (
|
||||
<>
|
||||
{"Click for: "}
|
||||
<Link href={selectedTag.data[key]} color="textPrimary" target="_blank" referrerPolicy='no'>{key}</Link>
|
||||
</>
|
||||
) : (selectedTag.data[key])}</TableCell>
|
||||
<TableCell>
|
||||
<StringTagDataEntry name={key} value={selectedTag.data[key]} />
|
||||
</TableCell>
|
||||
) : typeof selectedTag.data[key] === "object" ? (
|
||||
<TableCell>{selectedTag.data[key].toString()}</TableCell>
|
||||
) : typeof selectedTag.data[key] === "boolean" ? (
|
||||
@@ -224,7 +278,7 @@ return (
|
||||
fontSize={14}
|
||||
showGutter={true}
|
||||
maxLines={20}
|
||||
highlightActiveLine={true}
|
||||
highlightActiveLine={false}
|
||||
value={selectedTag?.data || ""}
|
||||
width={"100%"}
|
||||
setOptions={{
|
||||
@@ -296,7 +350,8 @@ export function ViewEditTagsDialog(props) {
|
||||
});
|
||||
const [updateTag] = useMutation(updateTagMutationTemplate, {
|
||||
onCompleted: data => {
|
||||
|
||||
snackActions.success("Successfully updated tag");
|
||||
props.onClose();
|
||||
},
|
||||
onError: error => {
|
||||
snackActions.error("Failed to update: " + error.message);
|
||||
@@ -308,8 +363,6 @@ const onSubmit = () => {
|
||||
if(props.onSubmit !== undefined){
|
||||
props.onSubmit({source:newSource, url:newURL, data:newData, tag_id:selectedTag.id});
|
||||
}
|
||||
|
||||
props.onClose();
|
||||
}
|
||||
const onChangeSource = (name, value, error) => {
|
||||
setNewSource(value);
|
||||
@@ -341,7 +394,7 @@ return (
|
||||
</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
{openNewDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openNewDialog}
|
||||
(<MythicDialog fullWidth={true} maxWidth="xl" open={openNewDialog}
|
||||
onClose={()=>{setOpenNewDialog(false);}}
|
||||
innerDialog={<NewTagDialog me={props.me}
|
||||
target_object={props.target_object}
|
||||
@@ -567,7 +620,7 @@ export const ViewEditTags = ({target_object, target_object_id, me}) => {
|
||||
<React.Fragment>
|
||||
<IconButton onClick={(e) => toggleTagDialog(e, true)} size="small" style={{display: "inline-block", float: "right"}}><LocalOfferOutlinedIcon /></IconButton>
|
||||
{openTagDialog ?
|
||||
(<MythicDialog fullWidth={true} maxWidth="md" open={openTagDialog}
|
||||
(<MythicDialog fullWidth={true} maxWidth="xl" open={openTagDialog}
|
||||
onClose={(e)=>{toggleTagDialog(e, false)}}
|
||||
innerDialog={<ViewEditTagsDialog me={me} target_object={target_object} target_object_id={target_object_id} onClose={(e)=>{toggleTagDialog(e, false)}} />}
|
||||
/>) : null}
|
||||
|
||||
@@ -190,3 +190,12 @@ mutation createTasking($callback_id: Int, $callback_ids: [Int], $command: String
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const exportCallbackConfigQuery = gql`
|
||||
query exportCallbackConfigQuery($agent_callback_id: String!) {
|
||||
exportCallbackConfig(agent_callback_id: $agent_callback_id) {
|
||||
status
|
||||
error
|
||||
config
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -8,6 +8,10 @@ import TocIcon from '@mui/icons-material/Toc';
|
||||
import AssessmentIcon from '@mui/icons-material/Assessment';
|
||||
import { CallbacksTop } from './CallbacksTop';
|
||||
import Split from 'react-split';
|
||||
import PhoneForwardedIcon from '@mui/icons-material/PhoneForwarded';
|
||||
import {ImportPayloadConfigDialog} from "../Payloads/ImportPayloadConfigDialog";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {ImportCallbackConfigDialog} from "./ImportCallbackConfigDialog";
|
||||
|
||||
const PREFIX = 'Callbacks';
|
||||
|
||||
@@ -216,7 +220,7 @@ export function Callbacks({me}) {
|
||||
*/
|
||||
function SpeedDialWrapperPreMemo({ setTopDisplay }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const [openCallbackImport, setOpenCallbackImport] = React.useState(false);
|
||||
const actions = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -233,11 +237,24 @@ function SpeedDialWrapperPreMemo({ setTopDisplay }) {
|
||||
setTopDisplay('graph');
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <PhoneForwardedIcon />,
|
||||
name: "Import Callback",
|
||||
onClick: () => {
|
||||
setOpenCallbackImport(true);
|
||||
}
|
||||
}
|
||||
],
|
||||
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{openCallbackImport &&
|
||||
<MythicDialog fullWidth={true} maxWidth="sm" open={openCallbackImport}
|
||||
onClose={()=>{setOpenCallbackImport(false);}}
|
||||
innerDialog={<ImportCallbackConfigDialog onClose={()=>{setOpenCallbackImport(false);}} />}
|
||||
/>
|
||||
}
|
||||
<StyledSpeedDial
|
||||
ariaLabel='SpeedDial example'
|
||||
className={classes.speedDial}
|
||||
|
||||
@@ -22,9 +22,10 @@ import {
|
||||
hideCallbackMutation,
|
||||
lockCallbackMutation,
|
||||
unlockCallbackMutation,
|
||||
updateIPsCallbackMutation
|
||||
updateIPsCallbackMutation,
|
||||
exportCallbackConfigQuery
|
||||
} from './CallbackMutations';
|
||||
import {useMutation } from '@apollo/client';
|
||||
import {useMutation, useLazyQuery } from '@apollo/client';
|
||||
import SnoozeIcon from '@mui/icons-material/Snooze';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
@@ -47,6 +48,8 @@ import moment from 'moment';
|
||||
import WidgetsIcon from '@mui/icons-material/Widgets';
|
||||
import {ModifyCallbackMythicTreeGroupsDialog} from "./ModifyCallbackMythicTreeGroupsDialog";
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import ImportExportIcon from '@mui/icons-material/ImportExport';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
|
||||
export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDescription, setOpenHideMultipleDialog, setOpenTaskMultipleDialog}) =>{
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
@@ -161,6 +164,33 @@ export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDesc
|
||||
lockCallback({variables: {callback_display_id: rowDataStatic.display_id}})
|
||||
}
|
||||
}
|
||||
const [exportConfig] = useLazyQuery(exportCallbackConfigQuery, {
|
||||
fetchPolicy: "no-cache",
|
||||
onCompleted: (data) => {
|
||||
if(data.exportCallbackConfig.status === "success"){
|
||||
const dataBlob = new Blob([data.exportCallbackConfig.config], {type: 'text/plain'});
|
||||
const ele = document.getElementById("download_config");
|
||||
if(ele !== null){
|
||||
ele.href = URL.createObjectURL(dataBlob);
|
||||
ele.download = rowDataStatic.agent_callback_id + ".json";
|
||||
ele.click();
|
||||
}else{
|
||||
const element = document.createElement("a");
|
||||
element.id = "download_config";
|
||||
element.href = URL.createObjectURL(dataBlob);
|
||||
element.download = rowDataStatic.agent_callback_id + ".json";
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
}
|
||||
}else{
|
||||
snackActions.error("Failed to export configuration: " + data.exportCallbackConfig.error);
|
||||
}
|
||||
},
|
||||
onError: (data) => {
|
||||
console.log(data);
|
||||
snackActions.error("Failed to export configuration: " + data.message)
|
||||
}
|
||||
})
|
||||
const options = [
|
||||
{name: 'Hide Callback', icon: <VisibilityOffIcon style={{color: theme.palette.warning.main, paddingRight: "5px"}}/>, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
@@ -211,6 +241,10 @@ export const CallbacksTableIDCell = React.memo(({rowData, toggleLock, updateDesc
|
||||
evt.stopPropagation();
|
||||
window.open("/new/callbacks/" + rowDataStatic.display_id, "_blank").focus();
|
||||
}},
|
||||
{name: "Export Callback", icon: <ImportExportIcon style={{paddingRight: "5px"}} />, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
exportConfig({variables: {agent_callback_id: rowDataStatic.agent_callback_id}});
|
||||
}},
|
||||
{name: "View Metadata", icon: <InfoIcon style={{color: theme.infoOnMain, paddingRight: "5px"}} />, click: (evt) => {
|
||||
evt.stopPropagation();
|
||||
setOpenMetaDialog(true);
|
||||
|
||||
@@ -158,7 +158,7 @@ export function CallbacksTabsTaskMultipleDialog({onClose, callback, me}) {
|
||||
}
|
||||
}
|
||||
const onTasked = ({tasked, variables}) => {
|
||||
onClose();
|
||||
//onClose();
|
||||
}
|
||||
const onSubmitCommandLine = (message, cmd, parsed, force_parsed_popup, cmdGroupNames, previousTaskingLocation) => {
|
||||
//console.log(message, cmd, parsed);
|
||||
|
||||
@@ -325,7 +325,7 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
|
||||
setCommandInfo({...cmd, "parsedParameters": parsed});
|
||||
}
|
||||
setOpenParametersDialog(true);
|
||||
return;
|
||||
|
||||
}else{
|
||||
delete parsed["_"];
|
||||
onCreateTask({callback_id: tabInfo.displayID,
|
||||
|
||||
@@ -1168,7 +1168,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
value={message}
|
||||
autoFocus={true}
|
||||
fullWidth={true}
|
||||
style={{marginBottom: "10px"}}
|
||||
style={{marginBottom: "10px", marginTop: "10px"}}
|
||||
InputProps={{ type: 'search',
|
||||
endAdornment:
|
||||
<React.Fragment>
|
||||
@@ -1189,7 +1189,7 @@ export function CallbacksTabsTaskingInputPreMemo(props){
|
||||
startAdornment: <React.Fragment>
|
||||
{tokenOptions.length > 0 ? (
|
||||
<CallbacksTabsTaskingInputTokenSelect options={tokenOptions} changeSelectedToken={props.changeSelectedToken}/>
|
||||
) : (null)}
|
||||
) : null}
|
||||
|
||||
</React.Fragment>
|
||||
|
||||
|
||||
@@ -123,9 +123,6 @@ export function CallbacksTop(props){
|
||||
onError: ({data}) => {
|
||||
console.log(data)
|
||||
},
|
||||
onComplete: ({data}) => {
|
||||
console.log(data)
|
||||
}
|
||||
});
|
||||
useSubscription(SUB_Edges, {
|
||||
fetchPolicy: "network-only",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {gql, useMutation} from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
|
||||
const import_callback = gql`
|
||||
mutation importCallbackConfigMutation($config: jsonb!) {
|
||||
importCallbackConfig(config: $config) {
|
||||
error
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function ImportCallbackConfigDialog(props) {
|
||||
const [fileValue, setFileValue] = React.useState({name: ""});
|
||||
const [createCallbackMutation] = useMutation(import_callback, {
|
||||
update: (cache, {data}) => {
|
||||
if(data.importCallbackConfig.status === "success"){
|
||||
snackActions.info("Successfully imported new callback");
|
||||
}else{
|
||||
snackActions.error(data.importCallbackConfig.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
const onCommitSubmit = () => {
|
||||
try {
|
||||
let jsonConfig = JSON.parse(fileValue.contents);
|
||||
createCallbackMutation({variables: {config: jsonConfig}}).catch( (e) => {console.log(e)} );
|
||||
props.onClose();
|
||||
}catch(error){
|
||||
snackActions.error("Failed to parse configuration")
|
||||
}
|
||||
}
|
||||
const onFileChange = (evt) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const contents = e.target.result;
|
||||
setFileValue({name: evt.target.files[0].name, contents: contents});
|
||||
}
|
||||
reader.readAsBinaryString(evt.target.files[0]);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">Import Callback Config From Other Mythic Server</DialogTitle>
|
||||
|
||||
<DialogContent dividers={true}>
|
||||
Export a callback config from another Mythic server and import it here to interact with that callback from this server.
|
||||
<br/>
|
||||
<Button variant="contained" component="label">
|
||||
{ fileValue.name === "" ? "Select File" : fileValue.name }
|
||||
<input onChange={onFileChange} type="file" hidden />
|
||||
</Button>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={props.onClose} color="primary">
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="contained" onClick={onCommitSubmit} color="success">
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
{!openBackdrop &&
|
||||
<div style={{display: "flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
|
||||
<div style={{overflowY: "auto", flexGrow: 1, width: "100%", height: props.expand ? "100%": undefined, display: "flex"}} ref={props.responseRef}>
|
||||
<div style={{overflowY: "auto", flexGrow: 1, width: "100%", height: props.expand ? "100%": undefined, display: "flex", flexDirection: "column"}} ref={props.responseRef}>
|
||||
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
|
||||
output={output} command_id={props.command_id}
|
||||
task={props.task} search={search.current} expand={props.expand}/>
|
||||
|
||||
@@ -116,11 +116,14 @@ const GetOutputFormat = ({data, useASNIColor, messagesEndRef, showTaskStatus, wr
|
||||
)
|
||||
}
|
||||
}, [data.timestamp, useASNIColor, showTaskStatus, wrapText]);
|
||||
/*
|
||||
React.useEffect( () => {
|
||||
if(messagesEndRef.current){
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [dataElement])
|
||||
}, [dataElement]);
|
||||
|
||||
*/
|
||||
return (
|
||||
dataElement
|
||||
)
|
||||
@@ -333,7 +336,7 @@ export const ResponseDisplayInteractive = (props) =>{
|
||||
/>
|
||||
</div>
|
||||
<InteractivePaginationBar totalCount={totalCount} currentPage={page.current}
|
||||
onSubmitPageChange={onSubmitPageChange}
|
||||
onSubmitPageChange={onSubmitPageChange} expand={props.expand}
|
||||
pageSize={pageSize.current} />
|
||||
</div>
|
||||
)
|
||||
@@ -517,17 +520,22 @@ const InteractiveTaskingBar = ({task, taskData, useASNIColor, toggleANSIColor,
|
||||
</>
|
||||
)
|
||||
}
|
||||
const InteractivePaginationBar = ({totalCount, currentPage, onSubmitPageChange, pageSize}) => {
|
||||
const InteractivePaginationBar = ({totalCount, currentPage, onSubmitPageChange, pageSize, expand}) => {
|
||||
const onChangePage = (event, value) => {
|
||||
onSubmitPageChange(value);
|
||||
};
|
||||
const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
|
||||
if(pageCount < 2){
|
||||
return (
|
||||
<div style={{height: "50px"}}>
|
||||
if(expand){
|
||||
return (
|
||||
<div style={{height: "50px"}}>
|
||||
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div style={{background: "transparent", display: "flex", justifyContent: "center", alignItems: "center", paddingBottom: "10px",}} >
|
||||
|
||||
@@ -14,11 +14,9 @@ import MenuItem from '@mui/material/MenuItem';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import { copyStringToClipboard } from '../../utilities/Clipboard';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import {snackActions} from '../../utilities/Snackbar';
|
||||
import {MythicStyledTooltip} from '../../MythicComponents/MythicStyledTooltip';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
|
||||
import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, faFolderOpen, faFileArchive, faCog, faFileWord, faFileExcel, faFilePowerpoint, faFilePdf, faDatabase, faKey, faFileCode, faDownload, faUpload, faFileImage, faCopy, faBoxOpen, faFileAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
// --------
|
||||
import {
|
||||
|
||||
@@ -121,7 +121,7 @@ export const DisplayMedia = ({agent_file_id, filename, expand}) => {
|
||||
const MaxRenderSize = 2000000;
|
||||
const DisplayText = ({agent_file_id, expand}) => {
|
||||
const theme = useTheme();
|
||||
const [mode, setMode] = React.useState("json");
|
||||
const [mode, setMode] = React.useState("html");
|
||||
const [content, setContent] = React.useState("");
|
||||
const [wrapText, setWrapText] = React.useState(true);
|
||||
React.useEffect( () => {
|
||||
@@ -186,7 +186,8 @@ const DisplayText = ({agent_file_id, expand}) => {
|
||||
fontSize={14}
|
||||
showGutter={true}
|
||||
//onLoad={onLoad}
|
||||
highlightActiveLine={true}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={content}
|
||||
height={expand ? "100%": undefined}
|
||||
maxLines={expand ? undefined : 20}
|
||||
|
||||
@@ -6,11 +6,28 @@ import 'ace-builds/src-noconflict/theme-xcode';
|
||||
import "ace-builds/src-noconflict/ext-searchbox";
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {modeOptions} from "../Search/PreviewFileStringDialog";
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import WrapTextIcon from '@mui/icons-material/WrapText';
|
||||
import 'ace-builds/src-noconflict/mode-csharp';
|
||||
import 'ace-builds/src-noconflict/mode-golang';
|
||||
import 'ace-builds/src-noconflict/mode-html';
|
||||
import 'ace-builds/src-noconflict/mode-markdown';
|
||||
import 'ace-builds/src-noconflict/mode-ruby';
|
||||
import 'ace-builds/src-noconflict/mode-python';
|
||||
import 'ace-builds/src-noconflict/mode-java';
|
||||
import 'ace-builds/src-noconflict/mode-javascript';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
const MaxRenderSize = 2000000;
|
||||
export const ResponseDisplayPlaintext = (props) =>{
|
||||
const theme = useTheme();
|
||||
const [plaintextView, setPlaintextView] = React.useState("");
|
||||
const [mode, setMode] = React.useState("html");
|
||||
const [wrapText, setWrapText] = React.useState(true);
|
||||
useEffect( () => {
|
||||
if(props.plaintext.length > MaxRenderSize){
|
||||
snackActions.warning("Response too large (> 2MB), truncating the render. Download task output to view entire response.");
|
||||
@@ -19,32 +36,66 @@ export const ResponseDisplayPlaintext = (props) =>{
|
||||
try{
|
||||
const newPlaintext = JSON.stringify(JSON.parse(String(props.plaintext)), null, 4);
|
||||
setPlaintextView(newPlaintext);
|
||||
setMode("json");
|
||||
}catch(error){
|
||||
setPlaintextView(String(props.plaintext));
|
||||
}
|
||||
}
|
||||
}, [props.plaintext]);
|
||||
const onChangeMode = (event) => {
|
||||
setMode(event.target.value);
|
||||
}
|
||||
const toggleWrapText = () => {
|
||||
setWrapText(!wrapText);
|
||||
}
|
||||
return (
|
||||
<AceEditor
|
||||
mode="json"
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={true}
|
||||
//onLoad={onLoad}
|
||||
highlightActiveLine={true}
|
||||
value={plaintextView}
|
||||
height={props.expand ? "100%": undefined}
|
||||
maxLines={props.expand ? undefined : 20}
|
||||
width={"100%"}
|
||||
//autoScrollEditorIntoView={true}
|
||||
wrapEnabled={true}
|
||||
minLines={1}
|
||||
//maxLines={props.expand ? 50 : 20}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false
|
||||
}}/>
|
||||
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
|
||||
<div>
|
||||
<FormControl sx={{ width: "20%", display: "inline-block" }} size="small">
|
||||
<Select
|
||||
style={{display: "inline-block", width: "100%"}}
|
||||
value={mode}
|
||||
onChange={onChangeMode}
|
||||
>
|
||||
{
|
||||
modeOptions.map((opt, i) => (
|
||||
<MenuItem key={"searchopt" + opt} value={opt}>{opt}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<MythicStyledTooltip title={wrapText ? "Unwrap Text" : "Wrap Text"} >
|
||||
<IconButton onClick={toggleWrapText} style={{}}>
|
||||
<WrapTextIcon color={wrapText ? "success" : "secondary"}
|
||||
style={{cursor: "pointer"}}
|
||||
/>
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
<div style={{display: "flex", flexGrow: 1, height: "100%"}}>
|
||||
<AceEditor
|
||||
mode={mode}
|
||||
theme={theme.palette.mode === "dark" ? "monokai" : "xcode"}
|
||||
fontSize={14}
|
||||
showGutter={true}
|
||||
//onLoad={onLoad}
|
||||
highlightActiveLine={false}
|
||||
showPrintMargin={false}
|
||||
value={plaintextView}
|
||||
height={props.expand ? "100%": undefined}
|
||||
maxLines={props.expand ? undefined : 20}
|
||||
width={"100%"}
|
||||
//autoScrollEditorIntoView={true}
|
||||
wrapEnabled={wrapText}
|
||||
minLines={1}
|
||||
//maxLines={props.expand ? 50 : 20}
|
||||
setOptions={{
|
||||
showLineNumbers: true,
|
||||
tabSize: 4,
|
||||
useWorker: false
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
@@ -203,7 +203,6 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
|
||||
}
|
||||
const onCancelConfirm = () => {
|
||||
setOpenConfirmDialog(false);
|
||||
console.log("in onCancelConfirm")
|
||||
onTasked({tasked: false});
|
||||
}
|
||||
useEffect( () => {
|
||||
@@ -267,7 +266,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
|
||||
{openSelectCommandDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="md" open={openSelectCommandDialog}
|
||||
onClose={()=>{setOpenSelectCommandDialog(false);onTasked({tasked: false});}}
|
||||
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenSelectCommandDialog(false);}}
|
||||
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenSelectCommandDialog(false);onTasked({tasked: false});}}
|
||||
onSubmit={onSubmitSelectedCommand} options={fileBrowserCommands} title={"Select Command"}
|
||||
action={"select"} identifier={"id"} display={"cmd"}/>}
|
||||
/>
|
||||
@@ -277,7 +276,7 @@ export const TaskFromUIButton = ({callback_id, callback_ids, cmd, ui_feature, pa
|
||||
onClose={()=>{setOpenParametersDialog(false);onTasked({tasked: false});}}
|
||||
innerDialog={<TaskParametersDialog command={selectedCommand} callback_id={callback_id} payloadtype_id={callbackData.callback_by_pk.payload.payloadtype.id}
|
||||
operation_id={callbackData.callback_by_pk.operation_id}
|
||||
onSubmit={submitParametersDialog} onClose={()=>{setOpenParametersDialog(false);}} />}
|
||||
onSubmit={submitParametersDialog} onClose={()=>{setOpenParametersDialog(false);onTasked({tasked: false});}} />}
|
||||
/>
|
||||
}
|
||||
{openCallbackTokenSelectDialog &&
|
||||
|
||||
@@ -7,6 +7,7 @@ import { meState, successfulLogin, FailedRefresh } from '../../../cache';
|
||||
import { useReactiveVar } from '@apollo/client';
|
||||
import {restartWebsockets, isJWTValid} from '../../../index';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
|
||||
export function LoginForm(props){
|
||||
const me = useReactiveVar(meState);
|
||||
@@ -65,8 +66,7 @@ export function LoginForm(props){
|
||||
setPassword(value);
|
||||
}
|
||||
const redirectPath = () => {
|
||||
const locationState = props.state;
|
||||
return locationState && locationState.from ? locationState.from.pathname : '/new/';
|
||||
return '/new/';
|
||||
}
|
||||
return (
|
||||
<div style={{justifyContent: "center", display: "flex"}}>
|
||||
@@ -76,13 +76,15 @@ export function LoginForm(props){
|
||||
<Navigate replace to={redirectPath()}/>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
<img src={logo} height="400px" alt="Mythic logo"/>
|
||||
<form onSubmit={submit}>
|
||||
<MythicTextField name='username' value={username} onChange={onUsernameChange} width={30} />
|
||||
<MythicTextField name='password' type="password" onEnter={submit} value={password} onChange={onPasswordChange} width={30} />
|
||||
<Button type="submit" color="primary" onClick={submit} variant="contained" style={{marginRight: "10px"}}>Login</Button>
|
||||
</form>
|
||||
<div style={{backgroundColor: "transparent"}}>
|
||||
<CardContent>
|
||||
<img src={logo} height="400px" alt="Mythic logo"/>
|
||||
<form onSubmit={submit}>
|
||||
<MythicTextField name='username' value={username} onChange={onUsernameChange} width={30} />
|
||||
<MythicTextField name='password' type="password" onEnter={submit} value={password} onChange={onPasswordChange} width={30} />
|
||||
<Button type="submit" color="primary" onClick={submit} variant="contained" style={{marginRight: "10px"}}>Login</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ export function DetailedPayloadTable(props){
|
||||
)
|
||||
}
|
||||
|
||||
export const ParseForDisplay = ({cmd}) => {
|
||||
export const ParseForDisplay = ({cmd, filename}) => {
|
||||
const [renderObj, setRenderObj] = React.useState(cmd.value);
|
||||
|
||||
React.useEffect( () => {
|
||||
@@ -439,10 +439,10 @@ function DetailedPayloadInnerTable(props){
|
||||
<TableCell>{cmd.description}</TableCell>
|
||||
<TableCell>
|
||||
<ParseForDisplay cmd={cmd} />
|
||||
{cmd.enc_key === null ? (null) : (<React.Fragment>
|
||||
{cmd.enc_key === null ? null : (<React.Fragment>
|
||||
<br/><b>Encryption Key: </b> {cmd.enc_key}
|
||||
</React.Fragment>) }
|
||||
{cmd.dec_key === null ? (null) : (<React.Fragment>
|
||||
{cmd.dec_key === null ? null : (<React.Fragment>
|
||||
<br/><b>Decryption Key: </b> {cmd.dec_key}
|
||||
</React.Fragment>) }
|
||||
</TableCell>
|
||||
|
||||
@@ -206,7 +206,8 @@ export function PayloadsTableRow(props){
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<MythicStyledTooltip title={"Delete the payload from disk and mark as deleted. No new callbacks can be generated from this payload"}>
|
||||
<IconButton size="small" onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"><DeleteIcon/></IconButton>
|
||||
<IconButton size="small" disableFocusRipple={true}
|
||||
disableRipple={true} onClick={()=>{setOpenDeleteDialog(true);}} color="error" variant="contained"><DeleteIcon/></IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
{openDelete &&
|
||||
@@ -312,7 +313,8 @@ export function PayloadsTableRow(props){
|
||||
</MythicStyledTooltip>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<IconButton size="small" color="info" onClick={() => setOpenDetailedView(true)}>
|
||||
<IconButton disableFocusRipple={true}
|
||||
disableRipple={true} size="small" color="info" onClick={() => setOpenDetailedView(true)}>
|
||||
<InfoIconOutline />
|
||||
</IconButton>
|
||||
</MythicStyledTableCell>
|
||||
|
||||
@@ -30,10 +30,12 @@ export function PayloadsTableRowBuildStatus(props){
|
||||
(<MythicStyledTooltip title="Payload still building">
|
||||
<IconButton variant="contained" size="large"><CircularProgress size={20} thickness={4} color="info"/></IconButton>
|
||||
</MythicStyledTooltip>) :
|
||||
(<MythicStyledTooltip title="Failed to build payload">
|
||||
(<>
|
||||
<IconButton
|
||||
variant="contained"
|
||||
onClick={onErrorClick}
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
size="large">
|
||||
<ReportProblemIcon color="error" />
|
||||
</IconButton>
|
||||
@@ -42,8 +44,8 @@ export function PayloadsTableRowBuildStatus(props){
|
||||
onClose={()=>{setOpenBuildMessageDialog(false);}}
|
||||
innerDialog={<PayloadBuildMessageDialog payload_id={props.id} viewError={true} onClose={()=>{setOpenBuildMessageDialog(false);}} />}
|
||||
/>
|
||||
): (null) }
|
||||
</MythicStyledTooltip>
|
||||
): null }
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import {snackActions} from './components/utilities/Snackbar';
|
||||
import jwt_decode from 'jwt-decode';
|
||||
import {meState} from './cache';
|
||||
|
||||
export const mythicVersion = "3.2.16";
|
||||
export const mythicUIVersion = "0.1.61";
|
||||
export const mythicVersion = "3.2.17";
|
||||
export const mythicUIVersion = "0.1.62";
|
||||
|
||||
let fetchingNewToken = false;
|
||||
|
||||
@@ -51,7 +51,8 @@ let httpLink = new HttpLink({
|
||||
reconnect: true,
|
||||
connectionParams: {
|
||||
headers: {
|
||||
Authorization: () => `Bearer ${localStorage.getItem('access_token')}`
|
||||
Authorization: () => `Bearer ${localStorage.getItem('access_token')}`,
|
||||
MythicSource: "web"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +105,7 @@ const authLink = setContext( async (_, {headers}) => {
|
||||
return{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
MythicSource: "web"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -118,6 +120,7 @@ const authLink = setContext( async (_, {headers}) => {
|
||||
return{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
MythicSource: "web"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,7 +133,8 @@ const authLink = setContext( async (_, {headers}) => {
|
||||
return {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
MythicSource: "web"
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -213,8 +217,9 @@ export const GetNewToken = async () =>{
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
MythicSource: "web"
|
||||
},
|
||||
body: JSON.stringify({"refresh_token": localStorage.getItem("refresh_token"),
|
||||
"access_token": localStorage.getItem("access_token")})
|
||||
@@ -268,7 +273,8 @@ const wsClient = createClient({
|
||||
return {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
MythicSource: "web"
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 0.2.10 - 2024-02-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated how file volume copies work to leverage the `docker cp` command
|
||||
- Added a `backup` command with subcommands for `database` and `files`
|
||||
- Added a `restore` command with subcommands for `database` and `files`
|
||||
- Updated commands with Help displays when subcommands are available
|
||||
- Updated `*_build_build_context` to default to false
|
||||
|
||||
## 0.2.8 - 2024-01-29
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// configCmd represents the config command
|
||||
var backupCmd = &cobra.Command{
|
||||
Use: "backup",
|
||||
Short: "Backup various volumes/data to a custom location on disk",
|
||||
Long: `Run various sub commands to backup and restore components`,
|
||||
Run: backup,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(backupCmd)
|
||||
}
|
||||
|
||||
func backup(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
}
|
||||
@@ -7,15 +7,15 @@ import (
|
||||
|
||||
// configCmd represents the config command
|
||||
var databaseBackupCmd = &cobra.Command{
|
||||
Use: "backup {path}",
|
||||
Short: "backup the database",
|
||||
Long: `Run this command to stop mythic and backup the current database (Save a copy to the specified location).`,
|
||||
Use: "database {path to folder}",
|
||||
Short: "backup a tar of the current database to the specified folder",
|
||||
Long: `Run this command to backup the current database (Save a copy to the specified location).`,
|
||||
Run: databaseBackup,
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
//databaseCmd.AddCommand(databaseBackupCmd)
|
||||
backupCmd.AddCommand(databaseBackupCmd)
|
||||
}
|
||||
|
||||
func databaseBackup(cmd *cobra.Command, args []string) {
|
||||
@@ -0,0 +1,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// configCmd represents the config command
|
||||
var filesBackupCmd = &cobra.Command{
|
||||
Use: "files {path}",
|
||||
Short: "backup the various files uploaded/downloaded with Mythic",
|
||||
Long: `Run this command to backup the uploaded/downloaded files associated with Mythic (Save a copy to the specified location).`,
|
||||
Run: fileBackup,
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
backupCmd.AddCommand(filesBackupCmd)
|
||||
}
|
||||
|
||||
func fileBackup(cmd *cobra.Command, args []string) {
|
||||
internal.FilesBackup(args[0])
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package config
|
||||
|
||||
// MythicDockerLatest is the most recent tagged version pushed to GitHub packages
|
||||
const MythicDockerLatest = "v0.0.3.8"
|
||||
const MythicDockerLatest = "v0.0.3.9"
|
||||
|
||||
@@ -28,6 +28,7 @@ var MythicPossibleServices = []string{
|
||||
"mythic_postgres_exporter",
|
||||
}
|
||||
var mythicEnv = viper.New()
|
||||
var mythicEnvInfo = make(map[string]string)
|
||||
|
||||
// GetIntendedMythicServiceNames uses MythicEnv host values for various services to see if they should be local or remote
|
||||
func GetIntendedMythicServiceNames() ([]string, error) {
|
||||
@@ -98,110 +99,282 @@ func GetMythicEnv() *viper.Viper {
|
||||
return mythicEnv
|
||||
}
|
||||
func setMythicConfigDefaultValues() {
|
||||
// global configuration
|
||||
// global configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("debug_level", "warning")
|
||||
mythicEnvInfo["debug_level"] = `This sets the logging level for mythic_server and all installed services. Valid options are debug, info, and warning`
|
||||
|
||||
mythicEnv.SetDefault("global_server_name", "mythic")
|
||||
mythicEnvInfo["global_server_name"] = `This sets the name of the Mythic server that's sent down as part of webhook and logging data. This makes it easier to identify which Mythic server is sending data to webhooks or logs.`
|
||||
|
||||
mythicEnv.SetDefault("global_manager", "docker")
|
||||
// nginx configuration
|
||||
mythicEnvInfo["global_manager"] = `This sets the management software used to control Mythic. The default is "docker" which uses Docker and Docker Compose. Valid options are currently: docker. Additional PRs can be made to implement the CLIManager Interface and provide more options.`
|
||||
|
||||
// nginx configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("nginx_port", 7443)
|
||||
mythicEnvInfo["nginx_port"] = `This sets the port used for the Nginx reverse proxy - this port is used by the React UI and Mythic's Scripting`
|
||||
|
||||
mythicEnv.SetDefault("nginx_host", "mythic_nginx")
|
||||
mythicEnvInfo["nginx_host"] = `This specifies the ip/hostname for where the Nginx container executes. If this is "mythic_nginx" or "127.0.0.1", then mythic-cli assumes this container is running locally. If it's anything else, mythic-cli will not spin up this container as it assumes it lives elsewhere`
|
||||
|
||||
mythicEnv.SetDefault("nginx_bind_localhost_only", false)
|
||||
mythicEnvInfo["nginx_bind_localhost_only"] = `This specifies if the Nginx container will expose the nginx_port on 0.0.0.0 or 127.0.0.1`
|
||||
|
||||
mythicEnv.SetDefault("nginx_use_ssl", true)
|
||||
mythicEnvInfo["nginx_use_ssl"] = `This specifies if the Nginx reverse proxy uses http or https`
|
||||
|
||||
mythicEnv.SetDefault("nginx_use_ipv4", true)
|
||||
mythicEnvInfo["nginx_use_ipv4"] = `This specifies if the Nginx reverse proxy should bind to IPv4 or not`
|
||||
|
||||
mythicEnv.SetDefault("nginx_use_ipv6", true)
|
||||
mythicEnvInfo["nginx_use_ipv6"] = `This specifies if the Nginx reverse proxy should bind to IPv6 or not`
|
||||
|
||||
mythicEnv.SetDefault("nginx_use_volume", false)
|
||||
mythicEnv.SetDefault("nginx_use_build_context", true)
|
||||
// mythic react UI configuration
|
||||
mythicEnvInfo["nginx_use_volume"] = `The Nginx container gets dynamic configuration from a variety of .env values as well as dynamically created SSL certificates. If this is True, then a docker volume is created and mounted into the container to host these pieces. If this is false, then the local filesystem is mounted inside the container instead. `
|
||||
|
||||
mythicEnv.SetDefault("nginx_use_build_context", false)
|
||||
mythicEnvInfo["nginx_use_build_context"] = `The Nginx container by default pulls configuration from a pre-compiled Docker image hosted on GitHub's Container Registry (ghcr.io). Setting this to "true" means that the local Mythic/nginx-docker/Dockerfile is used to generate the image used for the mythic_nginx container instead of the hosted image.`
|
||||
|
||||
// mythic react UI configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("mythic_react_host", "mythic_react")
|
||||
mythicEnvInfo["mythic_react_host"] = `This specifies the ip/hostname for where the React UI container executes. If this is 'mythic_react' or '127.0.0.1', then mythic-cli assumes this container is running locally. If it's anything else, mythic-cli will not spin up this container as it assumes it lives elsewhere`
|
||||
|
||||
mythicEnv.SetDefault("mythic_react_port", 3000)
|
||||
mythicEnvInfo["mythic_react_port"] = `This specifies the port that the React UI server listens on. This is normally accessed through the nginx reverse proxy though via /new`
|
||||
|
||||
mythicEnv.SetDefault("mythic_react_bind_localhost_only", true)
|
||||
mythicEnvInfo["mythic_react_bind_localhost_only"] = `This specifies if the mythic_react container will expose the mythic_react_port on 0.0.0.0 or 127.0.0.1. Binding the localhost will still allow internal reverse proxying to work, but won't allow the service to be hit remotely. It's unlikely this will ever need to change since you should be connecting through the nginx_proxy, but would be necessary to change if the React UI were hosted on a different server.`
|
||||
|
||||
mythicEnv.SetDefault("mythic_react_use_volume", false)
|
||||
mythicEnv.SetDefault("mythic_react_use_build_context", true)
|
||||
// documentation configuration
|
||||
mythicEnvInfo["mythic_react_use_volume"] = `This specifies if the mythic_react container will mount mount the local filesystem to serve content or use the pre-build data within the image itself. If you want to change the website that's shown, you need to mount locally and change the mythic_react_use_build_context to true'`
|
||||
|
||||
mythicEnv.SetDefault("mythic_react_use_build_context", false)
|
||||
mythicEnvInfo["mythic_react_use_build_context"] = `This specifies if the mythic_react container should use the pre-built docker image hosted on GitHub's container registry (ghcr.io) or if the local mythic-react-docker/Dockerfile should be used to generate the base image for the mythic_react container`
|
||||
|
||||
// documentation configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("documentation_host", "mythic_documentation")
|
||||
mythicEnvInfo["documentation_host"] = `This specifies the ip/hostname for where the documentation container executes. If this is 'documentation_host' or '127.0.0.1', then mythic-cli assumes this container is running locally. If it's anything else, mythic-cli will not spin up this container as it assumes it lives elsewhere`
|
||||
|
||||
mythicEnv.SetDefault("documentation_port", 8090)
|
||||
mythicEnvInfo["documentation_port"] = `This specifies the port that the Documentation UI server listens on. This is normally accessed through the nginx reverse proxy though via /docs`
|
||||
|
||||
mythicEnv.SetDefault("documentation_bind_localhost_only", true)
|
||||
mythicEnvInfo["documentation_bind_localhost_only"] = `This specifies if the documentation container will expose the documentation_port on 0.0.0.0 or 127.0.0.1`
|
||||
|
||||
mythicEnv.SetDefault("documentation_use_volume", false)
|
||||
mythicEnv.SetDefault("documentation_use_build_context", true)
|
||||
// mythic server configuration
|
||||
mythicEnvInfo["documentation_use_volume"] = `The documentation container gets dynamic from installed agents and c2 profiles. If this is True, then a docker volume is created and mounted into the container to host these pieces. If this is false, then the local filesystem is mounted inside the container instead. `
|
||||
|
||||
mythicEnv.SetDefault("documentation_use_build_context", false)
|
||||
mythicEnvInfo["documentation_use_build_context"] = `The documentation container by default pulls configuration from a pre-compiled Docker image hosted on GitHub's Container Registry (ghcr.io). Setting this to "true" means that the local Mythic/documentation-docker/Dockerfile is used to generate the image used for the mythic_documentation container instead of the hosted image.`
|
||||
|
||||
// mythic server configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("mythic_debug_agent_message", false)
|
||||
mythicEnvInfo["mythic_debug_agent_message"] = `When this is true, Mythic will send a message to the operational event log for each step of processing every agent's message. This can be a lot of messages, so do it with care, but it can be extremely valuable in figuring out issues with agent messaging. This setting can also be toggled at will in the UI on the settings page by an admin.`
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_port", 17443)
|
||||
mythicEnvInfo["mythic_server_port"] = `This specifies the port that the mythic_server listens on. This is normally accessed through the nginx reverse proxy though via /new. Agent and C2 Profile containers will directly access this container and port when fetching/uploading files/payloads.`
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_grpc_port", 17444)
|
||||
mythicEnvInfo["mythic_server_grpc_port"] = `This specifies the port that the mythic_server's gRPC functionality listens on. Translation containers will directly access this container and port when establishing gRPC functionality. C2 Profile containers will directly access this container and port when using Push Style C2 connections.`
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_host", "mythic_server")
|
||||
mythicEnvInfo["mythic_server_host"] = `This specifies the ip/hostname for where the mythic server container executes. If this is 'mythic_server' or '127.0.0.1', then mythic-cli assumes this container is running locally. If it's anything else, mythic-cli will not spin up this container as it assumes it lives elsewhere`
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_bind_localhost_only", true)
|
||||
mythicEnvInfo["mythic_server_bind_localhost_only"] = `This specifies if the mythic_server container will expose the mythic_server_port and mythic_server_grpc_port on 0.0.0.0 or 127.0.0.1. If you have a remote agent container connecting to Mythic, you MUST set this to false so that the remote agent container can do file transfers with Mythic.`
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_cpus", "2")
|
||||
mythicEnvInfo["mythic_server_cpus"] = `Set this to limit the maximum number of CPUs this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_mem_limit", "")
|
||||
mythicEnvInfo["mythic_server_mem_limit"] = `Set this to limit the maximum amount of RAM this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_dynamic_ports", "7000-7010")
|
||||
mythicEnvInfo["mythic_server_dynamic_ports"] = `These ports are exposed through the Docker container and provide access to SOCKS, Reverse Port Forward, and Interactive Tasking ports opened up by the Mythic Server. This is a comma-separated list of ranges, so you could do 7000-7010,7012,713-720`
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_dynamic_ports_bind_localhost_only", false)
|
||||
mythicEnvInfo["mythic_server_dynamic_ports_bind_localhost_only"] = `This specifies if the mythic_server container will expose the dynamic_ports on 0.0.0.0 or 127.0.0.1. If you have a remote agent container connecting to Mythic, you MUST set this to false so that the remote agent container can connect to gRPC.`
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_use_volume", false)
|
||||
mythicEnv.SetDefault("mythic_server_use_build_context", true)
|
||||
mythicEnv.SetDefault("mythic_server_command", "")
|
||||
mythicEnvInfo["mythic_server_use_volume"] = `The mythic_server container saves uploaded and downloaded files. If this is True, then a docker volume is created and mounted into the container to host these pieces. If this is false, then the local filesystem is mounted inside the container instead. `
|
||||
|
||||
mythicEnv.SetDefault("mythic_server_use_build_context", false)
|
||||
mythicEnvInfo["mythic_server_use_build_context"] = `The mythic_server container by default pulls configuration from a pre-compiled Docker image hosted on GitHub's Container Registry (ghcr.io). Setting this to "true" means that the local Mythic/mythic-docker/Dockerfile is used to generate the image used for the mythic_server container instead of the hosted image. If you want to modify the local mythic_server code then you need to set this to true and uncomment the sections of the mythic-docker/Dockerfile that copy over the existing code and build it. If you don't do this then you won't see any of your changes take effect`
|
||||
|
||||
mythicEnv.SetDefault("mythic_sync_cpus", "2")
|
||||
mythicEnvInfo["mythic_sync_cpus"] = `Set this to limit the maximum number of CPUs this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("mythic_sync_mem_limit", "")
|
||||
// postgres configuration
|
||||
mythicEnvInfo["mythic_sync_mem_limit"] = `Set this to limit the maximum amount of RAM this service is able to consume`
|
||||
|
||||
// postgres configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("postgres_host", "mythic_postgres")
|
||||
mythicEnvInfo["postgres_host"] = `This specifies the ip/hostname for where the postgres database container executes. If this is 'mythic_postgres' or '127.0.0.1', then mythic-cli assumes this container is running locally. If it's anything else, mythic-cli will not spin up this container as it assumes it lives elsewhere`
|
||||
|
||||
mythicEnv.SetDefault("postgres_port", 5432)
|
||||
mythicEnvInfo["postgres_port"] = `This specifies the port that the Postgres database server listens on.`
|
||||
|
||||
mythicEnv.SetDefault("postgres_bind_localhost_only", true)
|
||||
mythicEnvInfo["postgres_bind_localhost_only"] = `This specifies if the mythic_postgres container will expose the postgres_port on 0.0.0.0 or 127.0.0.1`
|
||||
|
||||
mythicEnv.SetDefault("postgres_db", "mythic_db")
|
||||
mythicEnvInfo["postgres_db"] = `This configures the name of the database Mythic uses to store its data`
|
||||
|
||||
mythicEnv.SetDefault("postgres_user", "mythic_user")
|
||||
mythicEnvInfo["postgres_user"] = `This configures the name of the database user Mythic uses
|
||||
`
|
||||
mythicEnv.SetDefault("postgres_password", utils.GenerateRandomPassword(30))
|
||||
mythicEnvInfo["postgres_password"] = `This is the randomly generated password that mythic_server and mythic_graphql use to connect to the mythic_postgres container`
|
||||
|
||||
mythicEnv.SetDefault("postgres_cpus", "2")
|
||||
mythicEnvInfo["postgres_cpus"] = `Set this to limit the maximum number of CPUs this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("postgres_mem_limit", "")
|
||||
mythicEnvInfo["postgres_mem_limit"] = `Set this to limit the maximum amount of RAM this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("postgres_use_volume", false)
|
||||
mythicEnv.SetDefault("postgres_use_build_context", true)
|
||||
// rabbitmq configuration
|
||||
mythicEnv.SetDefault("rabbitmq_host", "mythic_rabbitmq")
|
||||
mythicEnvInfo["postgres_use_volume"] = `The mythic_postgres container saves a database of everything that happens within Mythic. If this is True, then a docker volume is created and mounted into the container to host these pieces. If this is false, then the local filesystem is mounted inside the container instead. `
|
||||
|
||||
mythicEnv.SetDefault("postgres_use_build_context", false)
|
||||
mythicEnvInfo["postgres_use_build_context"] = `The mythic_postgres container by default pulls configuration from a pre-compiled Docker image hosted on GitHub's Container Registry (ghcr.io). Setting this to "true" means that the local Mythic/postgres-docker/Dockerfile is used to generate the image used for the mythic_postgres container instead of the hosted image. `
|
||||
|
||||
// rabbitmq configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("rabbitmq_host", "rabbitmq_host")
|
||||
mythicEnvInfo["rabbitmq_host"] = `This specifies the ip/hostname for where the RabbitMQ container executes. If this is 'rabbitmq_host' or '127.0.0.1', then mythic-cli assumes this container is running locally. If it's anything else, mythic-cli will not spin up this container as it assumes it lives elsewhere`
|
||||
|
||||
mythicEnv.SetDefault("rabbitmq_port", 5672)
|
||||
mythicEnvInfo["postgres_port"] = `This specifies the port that the RabbitMQ server listens on.`
|
||||
|
||||
mythicEnv.SetDefault("rabbitmq_bind_localhost_only", true)
|
||||
mythicEnvInfo["rabbitmq_bind_localhost_only"] = `This specifies if the mythic_rabbitmq container will expose the rabbitmq_port on 0.0.0.0 or 127.0.0.1. If you have a remote agent container connecting to Mythic, you MUST set this to false so that the remote agent container can connect to Mythic.`
|
||||
|
||||
mythicEnv.SetDefault("rabbitmq_user", "mythic_user")
|
||||
mythicEnvInfo["rabbitmq_user"] = `This is the user that all containers use to connect to RabbitMQ queues`
|
||||
|
||||
mythicEnv.SetDefault("rabbitmq_password", utils.GenerateRandomPassword(30))
|
||||
mythicEnvInfo["rabbitmq_password"] = `This is the randomly generated password that all containers use to connect to RabbitMQ queues`
|
||||
mythicEnv.SetDefault("rabbitmq_vhost", "mythic_vhost")
|
||||
|
||||
mythicEnv.SetDefault("rabbitmq_cpus", "2")
|
||||
mythicEnvInfo["rabbitmq_cpus"] = `Set this to limit the maximum number of CPUs this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("rabbitmq_mem_limit", "")
|
||||
mythicEnvInfo["rabbitmq_mem_limit"] = `Set this to limit the maximum amount of RAM this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("rabbitmq_use_volume", false)
|
||||
mythicEnv.SetDefault("rabbitmq_use_build_context", true)
|
||||
// jwt configuration
|
||||
mythicEnvInfo["rabbitmq_use_volume"] = `The mythic_rabbitmq container saves data about the messages queues used and their stats. If this is True, then a docker volume is created and mounted into the container to host these pieces. If this is false, then the local filesystem is mounted inside the container instead. `
|
||||
|
||||
mythicEnv.SetDefault("rabbitmq_use_build_context", false)
|
||||
mythicEnvInfo["rabbitmq_use_build_context"] = `The mythic_rabbitmq container by default pulls configuration from a pre-compiled Docker image hosted on GitHub's Container Registry (ghcr.io). Setting this to "true" means that the local Mythic/rabbitmq-docker/Dockerfile is used to generate the image used for the mythic_rabbitmq container instead of the hosted image. `
|
||||
|
||||
// jwt configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("jwt_secret", utils.GenerateRandomPassword(30))
|
||||
// hasura configuration
|
||||
mythicEnvInfo["jwt_secret"] = `This is the randomly generated password used to sign JWTs to ensure they're valid for this Mythic instance`
|
||||
|
||||
// hasura configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("hasura_host", "mythic_graphql")
|
||||
mythicEnvInfo["hasura_host"] = `This specifies the ip/hostname for where the Hasura GraphQL container executes. If this is 'mythic_graphql' or '127.0.0.1', then mythic-cli assumes this container is running locally. If it's anything else, mythic-cli will not spin up this container as it assumes it lives elsewhere`
|
||||
|
||||
mythicEnv.SetDefault("hasura_port", 8080)
|
||||
mythicEnvInfo["postgres_port"] = `This specifies the port that the Hasura GraphQL server listens on. This is normally accessed through the Nginx reverse proxy though via /console`
|
||||
|
||||
mythicEnv.SetDefault("hasura_bind_localhost_only", true)
|
||||
mythicEnvInfo["hasura_bind_localhost_only"] = `This specifies if the mythic_graphql container will expose the hasura_port on 0.0.0.0 or 127.0.0.1. `
|
||||
|
||||
mythicEnv.SetDefault("hasura_secret", utils.GenerateRandomPassword(30))
|
||||
mythicEnvInfo["hasura_secret"] = `This is the randomly generated password you can use to connect to Hasura through the /console route through the nginx proxy`
|
||||
|
||||
mythicEnv.SetDefault("hasura_cpus", "2")
|
||||
mythicEnvInfo["hasura_cpus"] = `Set this to limit the maximum number of CPUs this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("hasura_mem_limit", "2gb")
|
||||
mythicEnvInfo["hasura_mem_limit"] = `Set this to limit the maximum amount of RAM this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("hasura_use_volume", false)
|
||||
mythicEnv.SetDefault("hasura_use_build_context", true)
|
||||
// docker-compose configuration
|
||||
mythicEnvInfo["hasura_use_volume"] = `The mythic_graphql container has data about the roles within Mythic and their permissions for various graphQL endpoints. If this is True, then the internal settings are used from the built image. If this is false, then the local filesystem is mounted inside the container instead. If you want to make any changes to the Hasura permissions, columns, or actions, then you need to make sure you first set this to false and restart mythic_graphql so that your changes are saved to disk and loaded up each time properly.`
|
||||
|
||||
mythicEnv.SetDefault("hasura_use_build_context", false)
|
||||
mythicEnvInfo["hasura_use_build_context"] = `The mythic_graphql container by default pulls configuration from a pre-compiled Docker image hosted on GitHub's Container Registry (ghcr.io). Setting this to "true" means that the local Mythic/hasura-docker/Dockerfile is used to generate the image used for the mythic_graphql container instead of the hosted image.`
|
||||
|
||||
// docker-compose configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("COMPOSE_PROJECT_NAME", "mythic")
|
||||
mythicEnv.SetDefault("REBUILD_ON_START", true)
|
||||
// Mythic instance configuration
|
||||
mythicEnvInfo["compose_project_name"] = `This is the project name for Docker Compose - it sets the prefix of the container names and shouldn't be changed`
|
||||
|
||||
mythicEnv.SetDefault("REBUILD_ON_START", false)
|
||||
mythicEnvInfo["rebuild_on_start"] = `This identifies if a container's backing image should be re-built (or re-fetched) each time you start the container. This can cause agent and c2 profile containers to have their volumes wiped on each start (and thus deleting any changes). This also drastically increases the start time for Mythic overall. This should only be needed if you're doing a bunch of development on Mythic itself. If you need to rebuild a specific container, you should use './mythic-cli build [container name]' instead to just rebuild that one container`
|
||||
|
||||
// Mythic instance configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("mythic_admin_user", "mythic_admin")
|
||||
mythicEnvInfo["mythic_admin_user"] = `This configures the name of the first user in Mythic when Mythic starts for the first time. After the first time Mythic starts, this value is unused.`
|
||||
|
||||
mythicEnv.SetDefault("mythic_admin_password", utils.GenerateRandomPassword(30))
|
||||
mythicEnvInfo["mythic_admin_password"] = `This randomly generated password is used when Mythic first starts to set the password for the mythic_admin_user account. After the first time Mythic starts, this value is unused`
|
||||
|
||||
mythicEnv.SetDefault("default_operation_name", "Operation Chimera")
|
||||
mythicEnvInfo["default_operation_name"] = `This is used to name the initial operation created for the mythic_admin account. After the first time Mythic starts, this value is unused`
|
||||
|
||||
mythicEnv.SetDefault("allowed_ip_blocks", "0.0.0.0/0,::/0")
|
||||
mythicEnvInfo["allowed_ip_blocks"] = `This comma-separated set of HOST-ONLY CIDR ranges specifies where valid logins can come from. These values are used by mythic_server to block potential downloads as well as by mythic_nginx to block connections from invalid addresses as well.`
|
||||
|
||||
mythicEnv.SetDefault("default_operation_webhook_url", "")
|
||||
mythicEnvInfo["default_operation_webhook_url"] = `If an operation doesn't specify their own webhook URL, then this value is used. You must instal a webhook container to have access to webhooks.`
|
||||
|
||||
mythicEnv.SetDefault("default_operation_webhook_channel", "")
|
||||
// jupyter configuration
|
||||
mythicEnvInfo["default_operation_webhook_channel"] = `If an operation doesn't specify their own webhook channel, then this value is used. You must install a webhook container to have access to webhooks.`
|
||||
|
||||
// jupyter configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("jupyter_port", 8888)
|
||||
mythicEnvInfo["jupyter_port"] = `This specifies the port for the mythic_jupyter container to expose outside of its container. This is typically accessed through the nginx proxy via /jupyter`
|
||||
|
||||
mythicEnv.SetDefault("jupyter_host", "mythic_jupyter")
|
||||
mythicEnvInfo["jupyter_host"] = `This specifies the ip/hostname for where the Jupyter container executes. If this is 'jupyter_host' or '127.0.0.1', then mythic-cli assumes this container is running locally. If it's anything else, mythic-cli will not spin up this container as it assumes it lives elsewhere`
|
||||
|
||||
mythicEnv.SetDefault("jupyter_token", "mythic")
|
||||
mythicEnvInfo["jupyter_token"] = `This value is used to authenticate to the Jupyter instance via the /jupyter route in the React UI`
|
||||
|
||||
mythicEnv.SetDefault("jupyter_cpus", "2")
|
||||
mythicEnvInfo["jupyter_cpus"] = `Set this to limit the maximum number of CPUs this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("jupyter_mem_limit", "")
|
||||
mythicEnvInfo["jupyter_mem_limit"] = `Set this to limit the maximum amount of RAM this service is able to consume`
|
||||
|
||||
mythicEnv.SetDefault("jupyter_bind_localhost_only", true)
|
||||
mythicEnvInfo["jupyter_bind_localhost_only"] = `This specifies if the mythic_jupyter container will expose the jupyter_port on 0.0.0.0 or 127.0.0.1. `
|
||||
|
||||
mythicEnv.SetDefault("jupyter_use_volume", false)
|
||||
mythicEnv.SetDefault("jupyter_use_build_context", true)
|
||||
// debugging help
|
||||
mythicEnvInfo["jupyter_use_volume"] = `The mythic_jupyter container saves data about script examples. If this is True, then a docker volume is created and mounted into the container to host these pieces. If this is false, then the local filesystem is mounted inside the container instead. `
|
||||
|
||||
mythicEnv.SetDefault("jupyter_use_build_context", false)
|
||||
mythicEnvInfo["jupyter_use_build_context"] = `The mythic_jupyter container by default pulls configuration from a pre-compiled Docker image hosted on GitHub's Container Registry (ghcr.io). Setting this to "true" means that the local Mythic/jupyter-docker/Dockerfile is used to generate the image used for the mythic_jupyter container instead of the hosted image.`
|
||||
|
||||
// debugging help ---------------------------------------------
|
||||
mythicEnv.SetDefault("postgres_debug", false)
|
||||
mythicEnv.SetDefault("mythic_react_debug", false)
|
||||
// installed service configuration
|
||||
mythicEnvInfo["mythic_react_debug"] = `Setting this to true switches the React UI from using a pre-built React UI to a live hot-reloading development server. You should only need to do this if you're planning on working on the Mythic UI. Once you're doing making changes to the UI, you can run 'sudo ./mythic-cli build_ui' to compile your changes and save them to the mythic-react-docker folder. Assuming you have mythic_react_use_volume set to false, then when you disable debugging, you'll be using the newly compiled version of the UI`
|
||||
|
||||
// installed service configuration ---------------------------------------------
|
||||
mythicEnv.SetDefault("installed_service_cpus", "1")
|
||||
mythicEnvInfo["installed_service_cpus"] = `Set this to limit the maximum number of CPUs that installed Agents/C2 Profile containers are allowed to consume`
|
||||
|
||||
mythicEnv.SetDefault("installed_service_mem_limit", "")
|
||||
mythicEnvInfo["installed_service_mem_limit"] = `Set this to limit the maximum amount of RAM that installed Agents/C2 Profile containers are allowed to consume`
|
||||
|
||||
mythicEnv.SetDefault("webhook_default_url", "")
|
||||
mythicEnvInfo["webhook_default_url"] = `This is the default webhook URL to use if one isn't configured for an operation`
|
||||
|
||||
mythicEnv.SetDefault("webhook_default_callback_channel", "")
|
||||
mythicEnvInfo["webhook_default_callback_channel"] = `This is the default channel to use for new callbacks with the specified webhook url`
|
||||
|
||||
mythicEnv.SetDefault("webhook_default_feedback_channel", "")
|
||||
mythicEnvInfo["webhook_default_feedback_channel"] = `This is the default channel to use for new feedback with the specified webhook url`
|
||||
|
||||
mythicEnv.SetDefault("webhook_default_startup_channel", "")
|
||||
mythicEnvInfo["webhook_default_startup_channel"] = `This is the default channel to use for new startup notifications with the specified webhook url`
|
||||
|
||||
mythicEnv.SetDefault("webhook_default_alert_channel", "")
|
||||
mythicEnvInfo["webhook_default_alert_channel"] = `This is the default channel to use for new alerts with the specified webhook url`
|
||||
|
||||
mythicEnv.SetDefault("webhook_default_custom_channel", "")
|
||||
mythicEnvInfo["webhook_default_custom_channel"] = `This is the default channel to use for new custom messages with the specified webhook url`
|
||||
|
||||
}
|
||||
func parseMythicEnvironmentVariables() {
|
||||
@@ -263,6 +436,7 @@ func parseMythicEnvironmentVariables() {
|
||||
}
|
||||
}
|
||||
mythicEnv.Set("global_docker_latest", MythicDockerLatest)
|
||||
mythicEnvInfo["global_docker_latest"] = `This is the latest Docker Image version available for all Mythic services (mythic_server, mythic_postgres, mythic-cli, etc). This is determined by the tag on the Mythic branch and stamped into mythic-cli. Even if you change or remove this locally, mythic-cli will always put it back to what it was. For each of the main Mythic services, if you set their *_use_build_context to false, then it's this specified Docker image version that will be fetched and used.`
|
||||
writeMythicEnvironmentVariables()
|
||||
}
|
||||
func writeMythicEnvironmentVariables() {
|
||||
@@ -314,12 +488,7 @@ func GetConfigStrings(args []string) map[string]string {
|
||||
}
|
||||
for _, setting := range allSettings {
|
||||
if searchRegex.MatchString(strings.ToUpper(setting)) || searchRegex.MatchString(strings.ToLower(setting)) {
|
||||
val := mythicEnv.GetString(setting)
|
||||
if val == "" {
|
||||
log.Fatalf("Config variable `%s` not found", setting)
|
||||
} else {
|
||||
resultMap[setting] = val
|
||||
}
|
||||
resultMap[setting] = mythicEnv.GetString(setting)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,6 +547,32 @@ func GetBuildArguments() []string {
|
||||
}
|
||||
return args
|
||||
}
|
||||
func GetConfigHelp(entries []string) map[string]string {
|
||||
allSettings := mythicEnv.AllKeys()
|
||||
output := make(map[string]string)
|
||||
for i := 0; i < len(entries[0:]); i++ {
|
||||
searchRegex, err := regexp.Compile(entries[i])
|
||||
if err != nil {
|
||||
log.Fatalf("[!] bad regex: %v", err)
|
||||
}
|
||||
for _, setting := range allSettings {
|
||||
if searchRegex.MatchString(strings.ToUpper(setting)) || searchRegex.MatchString(strings.ToLower(setting)) {
|
||||
if _, ok := mythicEnvInfo[setting]; ok {
|
||||
output[setting] = mythicEnvInfo[setting]
|
||||
} else if strings.HasSuffix(setting, "use_volume") {
|
||||
output[setting] = `This creates a new volume in the format [agent]_volume that's mapped into an agent's '/Mythic' directory. The first time this is created with an empty volume, the contents of your pre-built agent/c2 gets copied to the volume. If you install a new version of the agent/c2 profile or rebuild the container (either via rebuild_on_start or directly calling ./mythic-cli build [agent]) then the volume is deleted and recreated. This will DELETE any changes you made in the container. For agents this could be temporary files created as part of builds. For C2 Profiles this could be profile updates. If this is set to 'false' the no new volume is created and instead the local InstalledServices/[agent] directory is mapped into the container, preserving all changes on rebuild and restart.`
|
||||
} else if strings.HasSuffix(setting, "use_build_context") {
|
||||
output[setting] = `This setting determines if you use the pre-built image hosted on GitHub/DockerHub or if you use your local InstalledService/[agent]/Dockerfile to build a new local image for your container. If you're wanting to make changes to an agent or c2 profile (adding commands, updating code, etc), then you need to set this to 'true' and update your Dockerfile to copy in your modified code. If your container code is Golang, then you'll also need to make sure your modified Dockerfile rebuilds that Go code based on your changes (probably with a 'go build' or 'make build' command depending on the agent). If your container code is Python, then simply copying in the changes should be sufficient. Also make sure you change [agent]_use_volume to 'false' so that your changes don't get overwritten by an old volume. Alternatively, you could remove the old volume with './mythic-cli volume rm [agent]_volume' and then build your new container with './mythic-cli build [agent]'.`
|
||||
} else if strings.HasSuffix(setting, "remote_image") {
|
||||
output[setting] = `This setting configures the remote image to use if you have *_use_build_context set to false. This value should get automatically updated by the agent/c2 profile's repo as new releases are created. This value will also get updated each time you install an agent. So if you want to pull an agent's latest image, just re-install the agent (or manually update this value and restart the local container).`
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b
|
||||
func AskConfirm(prompt string) bool {
|
||||
|
||||
@@ -4,5 +4,5 @@ package config
|
||||
|
||||
var (
|
||||
// Version Mythic CLI version
|
||||
Version = "v0.2.9"
|
||||
Version = "v0.2.10"
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/config"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
@@ -36,8 +37,13 @@ func configGet(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(writer, "\n %s\t%s", "–––––––", "–––––––")
|
||||
|
||||
configuration := config.GetConfigStrings(args)
|
||||
for key, val := range configuration {
|
||||
fmt.Fprintf(writer, "\n %s\t%s", strings.ToUpper(key), val)
|
||||
keys := make([]string, 0, len(configuration))
|
||||
for k := range configuration {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
fmt.Fprintf(writer, "\n %s\t%s", strings.ToUpper(key), configuration[key])
|
||||
}
|
||||
fmt.Fprintln(writer, "")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/config"
|
||||
"github.com/spf13/cobra"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
// configGetCmd represents the configGet command
|
||||
var configHelpCmd = &cobra.Command{
|
||||
Use: "help <configuration> <configuration> ...",
|
||||
Short: "Get information about specified configuration values",
|
||||
Long: `Get information about specified configuration values. You can provide one value or
|
||||
a list of values separated by spaces.
|
||||
For example: mythic-cli config help ADMIN_PASSWORD POSTGRES_PASSWORD`,
|
||||
Run: configHelp,
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(configHelpCmd)
|
||||
}
|
||||
|
||||
func configHelp(cmd *cobra.Command, args []string) {
|
||||
// initialize tabwriter
|
||||
writer := new(tabwriter.Writer)
|
||||
// Set minwidth, tabwidth, padding, padchar, and flags
|
||||
writer.Init(os.Stdout, 8, 8, 1, '\t', 0)
|
||||
|
||||
defer writer.Flush()
|
||||
|
||||
fmt.Println("[+] Getting configuration values:")
|
||||
fmt.Fprintf(writer, "\n %s\t%s", "Setting", "Value")
|
||||
fmt.Fprintf(writer, "\n %s\t%s", "–––––––", "–––––––")
|
||||
|
||||
configuration := config.GetConfigHelp(args)
|
||||
keys := make([]string, 0, len(configuration))
|
||||
for k := range configuration {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
fmt.Fprintf(writer, "\n %s\t%s", strings.ToUpper(key), configuration[key])
|
||||
}
|
||||
fmt.Fprintln(writer, "")
|
||||
}
|
||||
@@ -17,5 +17,5 @@ func init() {
|
||||
}
|
||||
|
||||
func database(cmd *cobra.Command, args []string) {
|
||||
|
||||
cmd.Help()
|
||||
}
|
||||
|
||||
@@ -18,5 +18,5 @@ func init() {
|
||||
}
|
||||
|
||||
func installDisplay(cmd *cobra.Command, args []string) {
|
||||
|
||||
cmd.Help()
|
||||
}
|
||||
|
||||
@@ -82,17 +82,24 @@ func InstallFolder(installPath string, overWrite bool) error {
|
||||
}
|
||||
log.Printf("[*] Adding service into docker-compose\n")
|
||||
if installConfig.IsSet("docker-compose") {
|
||||
err := Add3rdPartyService(f.Name(), installConfig.GetStringMap("docker-compose"), true)
|
||||
err = Add3rdPartyService(f.Name(), installConfig.GetStringMap("docker-compose"), true)
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to add service to docker-compose: %v\n", err)
|
||||
} else if err := ServiceBuild([]string{f.Name()}); err != nil {
|
||||
log.Printf("[-] Failed to start service: %v\n", err)
|
||||
} else {
|
||||
err = manager.GetManager().BuildServices([]string{f.Name()})
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to start service: %v\n", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := Add3rdPartyService(f.Name(), make(map[string]interface{}), true); err != nil {
|
||||
err = Add3rdPartyService(f.Name(), make(map[string]interface{}), true)
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to add service to docker-compose: %v\n", err)
|
||||
} else if err := ServiceBuild([]string{f.Name()}); err != nil {
|
||||
log.Printf("[-] Failed to start service: %v\n", err)
|
||||
} else {
|
||||
err = manager.GetManager().BuildServices([]string{f.Name()})
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to start service: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,10 +151,14 @@ func InstallFolder(installPath string, overWrite bool) error {
|
||||
}
|
||||
// now add payload type to yaml installConfig
|
||||
log.Printf("[*] Adding c2, %s, into docker-compose\n", f.Name())
|
||||
if err = Add3rdPartyService(f.Name(), make(map[string]interface{}), true); err != nil {
|
||||
err = Add3rdPartyService(f.Name(), make(map[string]interface{}), true)
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to add %s to docker-compose: %v\n", f.Name(), err)
|
||||
} else if err := ServiceBuild([]string{f.Name()}); err != nil {
|
||||
log.Printf("[-] Failed to start service: %v\n", err)
|
||||
} else {
|
||||
err = manager.GetManager().BuildServices([]string{f.Name()})
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to start service: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +175,7 @@ func InstallFolder(installPath string, overWrite bool) error {
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
log.Printf("[*] Processing Documentation for %s\n", f.Name())
|
||||
if !config.GetMythicEnv().GetBool("documentation_bind_use_volume") {
|
||||
if !config.GetMythicEnv().GetBool("documentation_use_volume") {
|
||||
if utils.DirExists(filepath.Join(workingPath, "documentation-docker", "content", "Agents", f.Name())) {
|
||||
if overWrite || config.AskConfirm("[*] "+f.Name()+" documentation already exists. Replace current version? ") {
|
||||
log.Printf("[*] Removing current version\n")
|
||||
@@ -214,7 +225,7 @@ func InstallFolder(installPath string, overWrite bool) error {
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
log.Printf("[*] Processing Documentation for %s\n", f.Name())
|
||||
if !config.GetMythicEnv().GetBool("document_bind_use_volume") {
|
||||
if !config.GetMythicEnv().GetBool("document_use_volume") {
|
||||
if utils.DirExists(filepath.Join(workingPath, "documentation-docker", "content", "C2 Profiles", f.Name())) {
|
||||
if overWrite || config.AskConfirm("[*] "+f.Name()+" documentation already exists. Replace current version? ") {
|
||||
log.Printf("[*] Removing current version\n")
|
||||
|
||||
@@ -13,7 +13,7 @@ func DatabaseReset() {
|
||||
if confirm {
|
||||
log.Printf("[*] Stopping Mythic\n")
|
||||
manager.GetManager().StopServices([]string{}, config.GetMythicEnv().GetBool("REBUILD_ON_START"))
|
||||
manager.GetManager().ResetDatabase(config.GetMythicEnv().GetBool("postgres_bind_use_volume"))
|
||||
manager.GetManager().ResetDatabase(config.GetMythicEnv().GetBool("postgres_use_volume"))
|
||||
log.Printf("[*] Removing database files\n")
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,48 @@ func DatabaseReset() {
|
||||
func DatabaseBackup(backupPath string) {
|
||||
confirm := config.AskConfirm("Are you sure you want to backup the database? ")
|
||||
if confirm {
|
||||
log.Printf("[*] Stopping Mythic\n")
|
||||
manager.GetManager().StopServices([]string{}, config.GetMythicEnv().GetBool("REBUILD_ON_START"))
|
||||
manager.GetManager().BackupDatabase(backupPath, config.GetMythicEnv().GetBool("postgres_bind_use_volume"))
|
||||
err := manager.GetManager().BackupDatabase(backupPath, config.GetMythicEnv().GetBool("postgres_use_volume"))
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to backup database: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func DatabaseRestore(backupPath string) {
|
||||
confirm := config.AskConfirm("Are you sure you want to restore the database and delete your existing database? ")
|
||||
if confirm {
|
||||
confirm = config.AskConfirm("Aare you absolutely sure? This will delete ALL data with your existing database forever.")
|
||||
if confirm {
|
||||
err := manager.GetManager().RestoreDatabase(backupPath, config.GetMythicEnv().GetBool("postgres_use_volume"))
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to restore database: %v\n", err)
|
||||
} else {
|
||||
manager.GetManager().StopServices([]string{"mythic_postgres"}, false)
|
||||
manager.GetManager().StartServices([]string{"mythic_postgres"}, config.GetMythicEnv().GetBool("REBUILD_ON_START"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func FilesBackup(backupPath string) {
|
||||
confirm := config.AskConfirm("Are you sure you want to backup the files? ")
|
||||
if confirm {
|
||||
err := manager.GetManager().BackupFiles(backupPath, config.GetMythicEnv().GetBool("mythic_server_use_volume"))
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to backup Mythic's files: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func FilesRestore(backupPath string) {
|
||||
confirm := config.AskConfirm("Are you sure you want to restore the files and delete your existing files? ")
|
||||
if confirm {
|
||||
confirm = config.AskConfirm("Aare you absolutely sure? This will delete ALL data with your existing uploads/downloads forever.")
|
||||
if confirm {
|
||||
err := manager.GetManager().RestoreFiles(backupPath, config.GetMythicEnv().GetBool("mythic_server_use_volume"))
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to restore files: %v\n", err)
|
||||
} else {
|
||||
manager.GetManager().StopServices([]string{"mythic_server"}, false)
|
||||
manager.GetManager().StartServices([]string{"mythic_server"}, config.GetMythicEnv().GetBool("REBUILD_ON_START"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/config"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/manager"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/utils"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
@@ -146,7 +145,7 @@ func DockerRemoveVolume(volumeName string) error {
|
||||
return manager.GetManager().RemoveVolume(volumeName)
|
||||
}
|
||||
|
||||
func DockerCopyIntoVolume(sourceFile io.Reader, destinationFileName string, destinationVolume string) {
|
||||
func DockerCopyIntoVolume(sourceFile string, destinationFileName string, destinationVolume string) {
|
||||
manager.GetManager().CopyIntoVolume(sourceFile, destinationFileName, destinationVolume)
|
||||
}
|
||||
func DockerCopyFromVolume(sourceVolumeName string, sourceFileName string, destinationName string) {
|
||||
|
||||
@@ -454,6 +454,7 @@ func AddMythicService(service string) {
|
||||
"NGINX_PORT=${NGINX_PORT}",
|
||||
"NGINX_HOST=${NGINX_HOST}",
|
||||
"MYTHIC_SERVER_DYNAMIC_PORTS=${MYTHIC_SERVER_DYNAMIC_PORTS}",
|
||||
"GLOBAL_SERVER_NAME=${GLOBAL_SERVER_NAME}",
|
||||
}
|
||||
mythicServerPorts := []string{
|
||||
"${MYTHIC_SERVER_PORT}:${MYTHIC_SERVER_PORT}",
|
||||
@@ -520,6 +521,7 @@ func AddMythicService(service string) {
|
||||
"GHOSTWRITER_API_KEY=${GHOSTWRITER_API_KEY}",
|
||||
"GHOSTWRITER_URL=${GHOSTWRITER_URL}",
|
||||
"GHOSTWRITER_OPLOG_ID=${GHOSTWRITER_OPLOG_ID}",
|
||||
"GLOBAL_SERVER_NAME=${GLOBAL_SERVER_NAME}",
|
||||
}
|
||||
if !mythicEnv.InConfig("GHOSTWRITER_API_KEY") {
|
||||
config.AskVariable("Please enter your GhostWriter API Key", "GHOSTWRITER_API_KEY")
|
||||
@@ -625,6 +627,7 @@ func Add3rdPartyService(service string, additionalConfigs map[string]interface{}
|
||||
"WEBHOOK_DEFAULT_ALERT_CHANNEL=${WEBHOOK_DEFAULT_ALERT_CHANNEL}",
|
||||
"WEBHOOK_DEFAULT_CUSTOM_CHANNEL=${WEBHOOK_DEFAULT_CUSTOM_CHANNEL}",
|
||||
"DEBUG_LEVEL=${DEBUG_LEVEL}",
|
||||
"GLOBAL_SERVER_NAME=${GLOBAL_SERVER_NAME}",
|
||||
}
|
||||
if _, ok := pStruct["environment"]; ok {
|
||||
pStruct["environment"] = utils.UpdateEnvironmentVariables(existingConfig["environment"].([]interface{}), environment)
|
||||
|
||||
@@ -216,18 +216,17 @@ func tarStringToBytes(sourceName string, data string) (*bytes.Buffer, error) {
|
||||
return &buf, nil
|
||||
}
|
||||
func moveFileToVolume(volumeName string, destinationName string, sourceName string) error {
|
||||
contentBytes, err := tarFileToBytes(sourceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
DockerCopyIntoVolume(contentBytes, destinationName, volumeName)
|
||||
DockerCopyIntoVolume(sourceName, destinationName, volumeName)
|
||||
return nil
|
||||
}
|
||||
func moveStringToVolume(volumeName string, destinationName string, sourceName string, content string) error {
|
||||
contentBytes, err := tarStringToBytes(sourceName, content)
|
||||
file, err := os.CreateTemp("", "*")
|
||||
if err != nil {
|
||||
log.Printf("[-] failed to create temp file for moving a string into a container: %v", err)
|
||||
return err
|
||||
}
|
||||
DockerCopyIntoVolume(contentBytes, destinationName, volumeName)
|
||||
file.WriteString(content)
|
||||
file.Sync()
|
||||
DockerCopyIntoVolume(file.Name(), destinationName, volumeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DockerComposeManager struct {
|
||||
@@ -721,7 +722,11 @@ func (d *DockerComposeManager) Status(verbose bool) {
|
||||
}
|
||||
}
|
||||
if !foundMountInfo {
|
||||
info += "local"
|
||||
if container.Labels["name"] == "mythic_graphql" {
|
||||
info += "N/A"
|
||||
} else {
|
||||
info += "local"
|
||||
}
|
||||
}
|
||||
info += "\t"
|
||||
if utils.StringInSlice(container.Labels["name"], config.MythicPossibleServices) {
|
||||
@@ -851,39 +856,181 @@ func (d *DockerComposeManager) ResetDatabase(useVolume bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
func (d *DockerComposeManager) BackupDatabase(backupPath string, useVolume bool) {
|
||||
/*
|
||||
if !useVolume {
|
||||
workingPath := utils.GetCwdFromExe()
|
||||
err := utils.CopyDir(filepath.Join(workingPath, "postgres-docker", "database"), backupPath)
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to copy database files\n%v\n", err)
|
||||
} else {
|
||||
log.Printf("[+] Successfully copied datbase files\n")
|
||||
}
|
||||
func (d *DockerComposeManager) BackupDatabase(backupPath string, useVolume bool) error {
|
||||
if !useVolume {
|
||||
workingPath := utils.GetCwdFromExe()
|
||||
log.Printf("[*] Staring to copy, this might take a minute...")
|
||||
err := utils.CopyDir(filepath.Join(workingPath, "postgres-docker", "database"), backupPath)
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to copy database files\n%v\n", err)
|
||||
return err
|
||||
} else {
|
||||
d.CopyFromVolume("mythic_postgres_volume", "/var/lib/postgresql/data", backupPath)
|
||||
log.Printf("[+] Successfully copied database files")
|
||||
log.Printf("[+] Successfully copied database files from disk\n")
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
execID, err := cli.ContainerExecCreate(ctx, "mythic_postgres", types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdout: true,
|
||||
AttachStdin: true,
|
||||
Cmd: []string{"/bin/bash", "-i"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("[!] Failed to exec into container: %v", err)
|
||||
} else {
|
||||
log.Printf("[*] Created docker exec session")
|
||||
}
|
||||
session, err := cli.ContainerExecAttach(ctx, execID.ID, types.ExecStartCheck{})
|
||||
if err != nil {
|
||||
log.Fatalf("[!] Failed to attach to exec session: %v", err)
|
||||
} else {
|
||||
log.Printf("[*] Attached to docker exec session")
|
||||
}
|
||||
todayString := time.Now().Format("2006-01-02-150405")
|
||||
tarFileName := fmt.Sprintf("%s-mythic_postgres.tar", todayString)
|
||||
defer session.Close()
|
||||
dumpCommand := fmt.Sprintf("PGPASSWORD=%s pg_dump -n public --format=tar -U mythic_user -f /var/lib/postgresql/data/%s mythic_db\n",
|
||||
config.GetMythicEnv().GetString("postgres_password"), tarFileName)
|
||||
_, err = session.Conn.Write([]byte(dumpCommand))
|
||||
if err != nil {
|
||||
log.Fatalf("[!] Failed to write to exec bash: %v", err)
|
||||
} else {
|
||||
log.Printf("[*] Issued pg_dump command")
|
||||
}
|
||||
_, err = session.Conn.Write([]byte("exit\n"))
|
||||
if err != nil {
|
||||
log.Fatalf("[!] Failed to authenticate to exec bash: %v", err)
|
||||
}
|
||||
inspect, err := cli.ContainerExecInspect(ctx, execID.ID)
|
||||
for inspect.Running {
|
||||
time.Sleep(1 * time.Second)
|
||||
log.Printf("[*] Waiting for pg_dump to finish...")
|
||||
inspect, err = cli.ContainerExecInspect(ctx, execID.ID)
|
||||
}
|
||||
log.Printf("[*] Finished docker exec session")
|
||||
err = d.CopyFromVolume("mythic_postgres_volume", tarFileName, backupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[+] Successfully copied database files from volume")
|
||||
|
||||
*/
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func (d *DockerComposeManager) RestoreDatabase(backupPath string, useVolume bool) {
|
||||
/*
|
||||
if !useVolume {
|
||||
workingPath := utils.GetCwdFromExe()
|
||||
err := utils.CopyDir(backupPath, filepath.Join(workingPath, "postgres-docker", "database"))
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to copy database files\n%v\n", err)
|
||||
} else {
|
||||
log.Printf("[+] Successfully copied datbase files\n")
|
||||
}
|
||||
} else {
|
||||
d.CopyIntoVolume("mythic_postgres_volume", "/var/lib/postgresql/data", backupPath)
|
||||
log.Printf("[+] Successfully copied database files")
|
||||
}
|
||||
func (d *DockerComposeManager) RestoreDatabase(backupPath string, useVolume bool) error {
|
||||
|
||||
if !useVolume {
|
||||
workingPath := utils.GetCwdFromExe()
|
||||
log.Printf("[*] Staring to copy, this might take a minute...")
|
||||
err := utils.CopyDir(backupPath, filepath.Join(workingPath, "postgres-docker", "database"))
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to copy database files\n%v\n", err)
|
||||
return err
|
||||
} else {
|
||||
log.Printf("[+] Successfully copied database files\n")
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
err := d.CopyIntoVolume(backupPath, "dump.tar", "mythic_postgres_volume")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[+] Successfully copied database files")
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
execID, err := cli.ContainerExecCreate(ctx, "mythic_postgres", types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdout: true,
|
||||
AttachStdin: true,
|
||||
Cmd: []string{"/bin/bash", "-i"},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("[!] Failed to exec into container: %v", err)
|
||||
} else {
|
||||
log.Printf("[*] Created docker exec session")
|
||||
}
|
||||
session, err := cli.ContainerExecAttach(ctx, execID.ID, types.ExecStartCheck{})
|
||||
if err != nil {
|
||||
log.Fatalf("[!] Failed to attach to exec session: %v", err)
|
||||
} else {
|
||||
log.Printf("[*] Attached to docker exec session")
|
||||
}
|
||||
defer session.Close()
|
||||
dumpCommand := fmt.Sprintf("PGPASSWORD=%s pg_restore -U mythic_user -n public --clean --if-exists -d mythic_db /var/lib/postgresql/data/dump.tar\n",
|
||||
config.GetMythicEnv().GetString("postgres_password"))
|
||||
_, err = session.Conn.Write([]byte(dumpCommand))
|
||||
if err != nil {
|
||||
log.Fatalf("[!] Failed to write to exec bash: %v", err)
|
||||
} else {
|
||||
log.Printf("[*] Issued pg_dump command")
|
||||
}
|
||||
_, err = session.Conn.Write([]byte("rm /var/lib/postgresql/data/dump.tar; exit\n"))
|
||||
if err != nil {
|
||||
log.Fatalf("[!] Failed to authenticate to exec bash: %v", err)
|
||||
}
|
||||
inspect, err := cli.ContainerExecInspect(ctx, execID.ID)
|
||||
for inspect.Running {
|
||||
time.Sleep(1 * time.Second)
|
||||
log.Printf("[*] Waiting for pg_dump to finish...")
|
||||
inspect, err = cli.ContainerExecInspect(ctx, execID.ID)
|
||||
}
|
||||
log.Printf("[*] Finished docker exec session")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func (d *DockerComposeManager) BackupFiles(backupPath string, useVolume bool) error {
|
||||
if !useVolume {
|
||||
workingPath := utils.GetCwdFromExe()
|
||||
log.Printf("[*] Staring to copy, this might take a minute...")
|
||||
err := utils.CopyDir(filepath.Join(workingPath, "mythic-docker", "src", "files"), backupPath)
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to copy Mythic's uploads/downloads\n%v\n", err)
|
||||
return err
|
||||
} else {
|
||||
log.Printf("[+] Successfully copied Mythic's uploads/downloads from disk\n")
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
err := d.CopyFromVolume("mythic_server_volume", "", backupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[+] Successfully copied Mythic's uploads/downloads from volume")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func (d *DockerComposeManager) RestoreFiles(backupPath string, useVolume bool) error {
|
||||
|
||||
if !useVolume {
|
||||
workingPath := utils.GetCwdFromExe()
|
||||
log.Printf("[*] Staring to copy, this might take a minute...")
|
||||
err := utils.CopyDir(backupPath, filepath.Join(workingPath, "mythic-docker", "src", "files"))
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to copy Mythic's uploads/downloads\n%v\n", err)
|
||||
return err
|
||||
} else {
|
||||
log.Printf("[+] Successfully copied Mythic's uploads/downloads\n")
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
err := d.CopyIntoVolume(backupPath, "/", "mythic_server_volume")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("[+] Successfully copied Mythic's uploads/downloads")
|
||||
return nil
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
func (d *DockerComposeManager) PrintVolumeInformation() {
|
||||
ctx := context.Background()
|
||||
@@ -986,9 +1133,9 @@ func (d *DockerComposeManager) RemoveVolume(volumeName string) error {
|
||||
}
|
||||
}
|
||||
log.Printf("[*] Volume not found")
|
||||
return nil
|
||||
return errors.New("[*] Volume not found")
|
||||
}
|
||||
func (d *DockerComposeManager) CopyIntoVolume(sourceFile io.Reader, destinationFileName string, destinationVolume string) {
|
||||
func (d *DockerComposeManager) CopyIntoVolume(sourceFile string, destinationFileName string, destinationVolume string) error {
|
||||
err := d.ensureVolume(destinationVolume)
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to ensure volume exists: %v\n", err)
|
||||
@@ -1006,21 +1153,18 @@ func (d *DockerComposeManager) CopyIntoVolume(sourceFile io.Reader, destinationF
|
||||
for _, container := range containers {
|
||||
for _, mnt := range container.Mounts {
|
||||
if mnt.Name == destinationVolume {
|
||||
err = cli.CopyToContainer(ctx, container.ID, mnt.Destination+"/"+destinationFileName, sourceFile, types.CopyToContainerOptions{
|
||||
CopyUIDGID: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to write file: %v\n", err)
|
||||
} else {
|
||||
log.Printf("[+] Successfully wrote file\n")
|
||||
}
|
||||
return
|
||||
log.Printf("[*] Staring to copy, this might take a minute...")
|
||||
log.Printf("[*] Copying %s to %s", sourceFile, container.Labels["name"]+":"+mnt.Destination+"/"+destinationFileName)
|
||||
output, err := d.runDocker([]string{"cp", sourceFile, container.Labels["name"] + ":" + mnt.Destination + "/" + destinationFileName})
|
||||
log.Printf(output)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Fatalf("[-] Failed to find that volume name in use by any containers")
|
||||
log.Printf("[-] Failed to find %s in use by any containers", destinationVolume)
|
||||
return errors.New("[-] failed to find that volume")
|
||||
}
|
||||
func (d *DockerComposeManager) CopyFromVolume(sourceVolumeName string, sourceFileName string, destinationName string) {
|
||||
func (d *DockerComposeManager) CopyFromVolume(sourceVolumeName string, sourceFileName string, destinationName string) error {
|
||||
err := d.ensureVolume(sourceVolumeName)
|
||||
if err != nil {
|
||||
log.Fatalf("[-] Failed to ensure volume exists: %v\n", err)
|
||||
@@ -1038,28 +1182,15 @@ func (d *DockerComposeManager) CopyFromVolume(sourceVolumeName string, sourceFil
|
||||
for _, container := range containers {
|
||||
for _, mnt := range container.Mounts {
|
||||
if mnt.Name == sourceVolumeName {
|
||||
reader, _, err := cli.CopyFromContainer(ctx, container.ID, mnt.Destination+"/"+sourceFileName)
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to read file: %v\n", err)
|
||||
return
|
||||
}
|
||||
destination, err := os.Create(destinationName)
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to open destination filename: %v\n", err)
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(destination, reader)
|
||||
destination.Close()
|
||||
if err != nil {
|
||||
log.Printf("[-] Failed to get file from volume: %v\n", err)
|
||||
return
|
||||
}
|
||||
log.Printf("[+] Successfully wrote file\n")
|
||||
return
|
||||
log.Printf("[*] Staring to copy, this might take a minute...")
|
||||
output, err := d.runDocker([]string{"cp", container.Labels["name"] + ":" + mnt.Destination + "/" + sourceFileName, destinationName})
|
||||
log.Printf(output)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Fatalf("[-] Failed to find that volume name in use by any containers")
|
||||
log.Printf("[-] Failed to find that volume name in use by any containers")
|
||||
return errors.New("[-] failed to find that volume")
|
||||
}
|
||||
|
||||
// Internal Support Commands
|
||||
@@ -1218,7 +1349,7 @@ func (d *DockerComposeManager) readInDockerCompose() *viper.Viper {
|
||||
}
|
||||
func (d *DockerComposeManager) ensureVolume(volumeName string) error {
|
||||
containerNamePieces := strings.Split(volumeName, "_")
|
||||
containerName := strings.Join(containerNamePieces[0:2], "_")
|
||||
containerName := strings.Join(containerNamePieces[0:len(containerNamePieces)-1], "_")
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
@@ -1247,7 +1378,7 @@ func (d *DockerComposeManager) ensureVolume(volumeName string) error {
|
||||
return err
|
||||
}
|
||||
for _, container := range containers {
|
||||
if container.Image == containerName {
|
||||
if container.Labels["name"] == containerName {
|
||||
for _, mnt := range container.Mounts {
|
||||
if mnt.Name == volumeName {
|
||||
// container is running and has this mount associated with it
|
||||
|
||||
@@ -3,7 +3,6 @@ package manager
|
||||
import (
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/config"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/utils"
|
||||
"io"
|
||||
"log"
|
||||
"path/filepath"
|
||||
)
|
||||
@@ -68,17 +67,21 @@ type CLIManager interface {
|
||||
// ResetDatabase deletes the current database or volume
|
||||
ResetDatabase(useVolume bool)
|
||||
// BackupDatabase saves a copy of the database to the specified path
|
||||
BackupDatabase(backupPath string, useVolume bool)
|
||||
// RestoreDatabase restores a saved copy of the datababse to the specified path
|
||||
RestoreDatabase(backupPath string, useVolume bool)
|
||||
BackupDatabase(backupPath string, useVolume bool) error
|
||||
// RestoreDatabase restores a saved copy of the database from the specified path
|
||||
RestoreDatabase(backupPath string, useVolume bool) error
|
||||
// BackupFiles saves the files associated with Mythic's uploads/downloads to the specified path
|
||||
BackupFiles(backupPath string, useVolume bool) error
|
||||
// RestoreFiles restores a saved copy of Mythic's uploads/downloads from the specified path
|
||||
RestoreFiles(backupPath string, useVolume bool) error
|
||||
// PrintVolumeInformation prints out all the volumes in use by Mythic
|
||||
PrintVolumeInformation()
|
||||
// RemoveVolume removes the named volume
|
||||
RemoveVolume(volumeName string) error
|
||||
// CopyIntoVolume copies from a source io.Reader to the destination filename on the destination volume
|
||||
CopyIntoVolume(sourceFile io.Reader, destinationFileName string, destinationVolume string)
|
||||
CopyIntoVolume(sourceFile string, destinationFileName string, destinationVolume string) error
|
||||
// CopyFromVolume copies from the source filename in the volume to the destination filename outside of the volume
|
||||
CopyFromVolume(sourceVolumeName string, sourceFileName string, destinationName string)
|
||||
CopyFromVolume(sourceVolumeName string, sourceFileName string, destinationName string) error
|
||||
}
|
||||
|
||||
var currentManager CLIManager
|
||||
|
||||
@@ -2,7 +2,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"log"
|
||||
)
|
||||
|
||||
// installCmd represents the config command
|
||||
@@ -18,5 +17,5 @@ func init() {
|
||||
}
|
||||
|
||||
func mythicSync(cmd *cobra.Command, args []string) {
|
||||
log.Fatalf("[-] Must specify install/uninstall")
|
||||
cmd.Help()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// configCmd represents the config command
|
||||
var restoreCmd = &cobra.Command{
|
||||
Use: "restore",
|
||||
Short: "Restore various volumes/data from a custom location on disk",
|
||||
Long: `Run various sub commands to restore components`,
|
||||
Run: restore,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(restoreCmd)
|
||||
}
|
||||
|
||||
func restore(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// configCmd represents the config command
|
||||
var databaseRestoreCmd = &cobra.Command{
|
||||
Use: "database {path to .tar file} ",
|
||||
Short: "restore the database from the specified tar file created from the 'backup database` command",
|
||||
Long: `Run this command to restore the database from a saved copy.`,
|
||||
Run: databaseRestore,
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
restoreCmd.AddCommand(databaseRestoreCmd)
|
||||
}
|
||||
|
||||
func databaseRestore(cmd *cobra.Command, args []string) {
|
||||
internal.DatabaseRestore(args[0])
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// configCmd represents the config command
|
||||
var filesRestoreCmd = &cobra.Command{
|
||||
Use: "files {path}",
|
||||
Short: "restore the uploaded/downloaded files from a path",
|
||||
Long: `Run this command to restore Mythic's uploaded/downloaded files from a saved copy.`,
|
||||
Run: filesRestore,
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
func init() {
|
||||
restoreCmd.AddCommand(filesRestoreCmd)
|
||||
}
|
||||
|
||||
func filesRestore(cmd *cobra.Command, args []string) {
|
||||
internal.FilesRestore(args[0])
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/MythicMeta/Mythic_CLI/cmd/internal"
|
||||
"github.com/spf13/cobra"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// configCmd represents the config command
|
||||
@@ -45,41 +40,5 @@ func init() {
|
||||
}
|
||||
|
||||
func volumesCopyToCommand(cmd *cobra.Command, args []string) {
|
||||
source, err := os.Open(sourceName)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] Failed to open file locally: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
sourceStats, err := source.Stat()
|
||||
if err != nil {
|
||||
fmt.Printf("[-] Failed to stat file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
err = tw.WriteHeader(&tar.Header{
|
||||
Name: sourceName, // filename
|
||||
Mode: 0777, // permissions
|
||||
Size: sourceStats.Size(), // filesize
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("[-] Failed to create tar: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
content, err := io.ReadAll(source)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] Failed to read file contents: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
_, err = tw.Write(content)
|
||||
if err != nil {
|
||||
fmt.Printf("[-] Failed to write to temp tar: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = tw.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("[-] Failed to close temp tar: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
internal.DockerCopyIntoVolume(&buf, destinationName, volumeName)
|
||||
internal.DockerCopyIntoVolume(sourceName, destinationName, volumeName)
|
||||
}
|
||||
|
||||
+16
-9
@@ -3,21 +3,25 @@ module github.com/MythicMeta/Mythic_CLI
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/creack/pty v1.1.21
|
||||
github.com/docker/docker v25.0.2+incompatible
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/streadway/amqp v1.1.0
|
||||
golang.org/x/mod v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/creack/pty v1.1.21 // indirect
|
||||
github.com/distribution/reference v0.5.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -26,7 +30,7 @@ require (
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc6 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
@@ -36,13 +40,16 @@ require (
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
|
||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
golang.org/x/tools v0.17.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
||||
@@ -62,8 +62,12 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
|
||||
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v25.0.2+incompatible h1:/OaKeauroa10K4Nqavw4zlhcDq/WBcPMc5DbjOGgozY=
|
||||
github.com/docker/docker v25.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -72,12 +76,19 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@@ -165,6 +176,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
@@ -228,6 +241,14 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
|
||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
|
||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
||||
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@@ -251,6 +272,8 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
|
||||
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -314,6 +337,8 @@ golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -335,6 +360,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -374,6 +400,8 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -445,6 +473,8 @@ golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -5,4 +5,7 @@
|
||||
#FROM hasura/graphql-engine:v2.9.0-beta.1.cli-migrations-v2
|
||||
#FROM hasura/graphql-engine:v2.19.0.cli-migrations-v2
|
||||
#FROM hasura/graphql-engine:latest.cli-migrations-v2
|
||||
FROM ghcr.io/its-a-feature/mythic_graphql:v0.0.3.8
|
||||
#FROM ghcr.io/its-a-feature/mythic_graphql:v0.0.3.8
|
||||
FROM hasura/graphql-engine:latest.cli-migrations-v2
|
||||
|
||||
COPY ["metadata/", "/metadata"]
|
||||
@@ -197,6 +197,12 @@ type Mutation {
|
||||
): dynamicQueryOutput
|
||||
}
|
||||
|
||||
type Query {
|
||||
exportCallbackConfig(
|
||||
agent_callback_id: String!
|
||||
): exportCallbackConfigOutput
|
||||
}
|
||||
|
||||
type Query {
|
||||
export_payload_config(
|
||||
uuid: String!
|
||||
@@ -225,6 +231,12 @@ type Query {
|
||||
): ProfileOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
importCallbackConfig(
|
||||
config: jsonb!
|
||||
): importCallbackConfigOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
importTagtypes(
|
||||
tagtypes: String!
|
||||
@@ -812,3 +824,14 @@ type globalSettingsOutput {
|
||||
settings: jsonb!
|
||||
}
|
||||
|
||||
type exportCallbackConfigOutput {
|
||||
status: String!
|
||||
error: String
|
||||
config: String
|
||||
}
|
||||
|
||||
type importCallbackConfigOutput {
|
||||
status: String!
|
||||
error: String
|
||||
}
|
||||
|
||||
|
||||
@@ -287,6 +287,16 @@ actions:
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
- name: exportCallbackConfig
|
||||
definition:
|
||||
kind: ""
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/export_callback_config_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
- name: export_payload_config
|
||||
definition:
|
||||
kind: ""
|
||||
@@ -329,6 +339,16 @@ actions:
|
||||
- role: mythic_admin
|
||||
- role: operation_admin
|
||||
- role: operator
|
||||
- name: importCallbackConfig
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/callback_import_config_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
- name: importTagtypes
|
||||
definition:
|
||||
kind: synchronous
|
||||
@@ -636,4 +656,6 @@ custom_types:
|
||||
- name: updateOperatorStatusOutput
|
||||
- name: updateGlobalSettingsOutput
|
||||
- name: globalSettingsOutput
|
||||
- name: exportCallbackConfigOutput
|
||||
- name: importCallbackConfigOutput
|
||||
scalars: []
|
||||
|
||||
@@ -1397,6 +1397,13 @@
|
||||
table:
|
||||
name: loadedcommands
|
||||
schema: public
|
||||
- name: mythictrees
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: callback_id
|
||||
table:
|
||||
name: mythictree
|
||||
schema: public
|
||||
- name: tasks
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
@@ -3742,6 +3749,13 @@
|
||||
table:
|
||||
name: payload
|
||||
schema: public
|
||||
- name: responses
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: operation_id
|
||||
table:
|
||||
name: response
|
||||
schema: public
|
||||
- name: tags
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
|
||||
@@ -16,5 +16,5 @@ ENV JUPYTER_TOKEN "mythic"
|
||||
|
||||
COPY ["jupyter/", "."]
|
||||
|
||||
CMD start.sh jupyter lab --ServerApp.open_browser=false --IdentityProvider.token='${JUPYTER_TOKEN:-mythic}' --ServerApp.base_url="/jupyter" --ServerApp.default_url="/jupyter"
|
||||
CMD start.sh jupyter lab --ServerApp.open_browser=false --IdentityProvider.token=${JUPYTER_TOKEN:-mythic} --ServerApp.base_url="/jupyter" --ServerApp.default_url="/jupyter"
|
||||
# sudo docker run -p 8888:8888 -v `pwd`/jupyter:/projects jupyter
|
||||
|
||||
@@ -1 +1,20 @@
|
||||
FROM ghcr.io/its-a-feature/mythic_jupyter:v0.0.3.8
|
||||
FROM jupyter/base-notebook:python-3.11
|
||||
|
||||
# for offline builds, add ability for custom PIP repositories
|
||||
ARG PIP_INDEX
|
||||
ARG PIP_INDEX_URL
|
||||
ARG PIP_TRUSTED_HOST
|
||||
|
||||
RUN pip3 install mythic
|
||||
|
||||
WORKDIR /projects
|
||||
|
||||
#CMD start.sh jupyter lab --ServerApp.open_browser=false --IdentityProvider.token='' --ServerApp.base_url="/jupyter" --ServerApp.default_url="/jupyter"
|
||||
|
||||
ENV JUPYTERHUB_SERVICE_PREFIX "/jupyter/"
|
||||
ENV JUPYTER_TOKEN "mythic"
|
||||
|
||||
COPY ["jupyter/", "."]
|
||||
|
||||
CMD start.sh jupyter lab --ServerApp.open_browser=false --IdentityProvider.token=${JUPYTER_TOKEN:-mythic} --ServerApp.base_url="/jupyter" --ServerApp.default_url="/jupyter"
|
||||
# sudo docker run -p 8888:8888 -v `pwd`/jupyter:/projects jupyter
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM itsafeaturemythic/mythic_go_base:latest
|
||||
FROM golang:1.21 as builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -14,8 +14,8 @@ RUN make build_final
|
||||
|
||||
FROM alpine
|
||||
|
||||
COPY --from=0 /mythic_server /mythic_server
|
||||
COPY --from=0 /usr/src/app /usr/src/app
|
||||
COPY --from=builder /mythic_server /mythic_server
|
||||
COPY --from=builder /usr/src/app /usr/src/app
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
|
||||
+15
-15
@@ -1,26 +1,26 @@
|
||||
FROM ghcr.io/its-a-feature/mythic_server:v0.0.3.8
|
||||
#FROM ghcr.io/its-a-feature/mythic_server:v0.0.3.8
|
||||
|
||||
#FROM itsafeaturemythic/mythic_go_base:latest
|
||||
FROM golang:1.21 as builder
|
||||
|
||||
#WORKDIR /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
#ARG GOPROXY=proxy.golang.org
|
||||
#ARG GO111MODULE
|
||||
ARG GOPROXY=proxy.golang.org
|
||||
ARG GO111MODULE
|
||||
|
||||
#RUN go env -w GOPROXY=${GOPROXY}
|
||||
#RUN go env -w GO111MODULE=${GO111MODULE}
|
||||
RUN go env -w GOPROXY=${GOPROXY}
|
||||
RUN go env -w GO111MODULE=${GO111MODULE}
|
||||
|
||||
#COPY ["src/", "."]
|
||||
COPY ["src/", "."]
|
||||
|
||||
#RUN make build_final
|
||||
RUN make build_final
|
||||
|
||||
#FROM alpine
|
||||
FROM alpine
|
||||
|
||||
#COPY --from=0 /mythic_server /mythic_server
|
||||
COPY --from=builder /mythic_server /mythic_server
|
||||
|
||||
#WORKDIR /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
#HEALTHCHECK --interval=60s --timeout=10s --retries=5 --start-period=20s \
|
||||
# CMD wget -SqO - http://127.0.0.1:${MYTHIC_SERVER_PORT:-17443}/health || exit 1
|
||||
HEALTHCHECK --interval=60s --timeout=10s --retries=5 --start-period=20s \
|
||||
CMD wget -SqO - http://127.0.0.1:${MYTHIC_SERVER_PORT:-17443}/health || exit 1
|
||||
|
||||
#CMD ["/bin/sh", "-c", "cp /mythic_server . && ./mythic_server" ]
|
||||
CMD ["/bin/sh", "-c", "cp /mythic_server . && ./mythic_server" ]
|
||||
@@ -42,7 +42,12 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
SQLGetIDForActiveToken = "SELECT id FROM apitokens WHERE token_value=$1 and active=true"
|
||||
SQLGetIDForActiveToken = `SELECT
|
||||
apitokens.id, apitokens.operator_id,
|
||||
operator.username "operator.username"
|
||||
FROM apitokens
|
||||
JOIN operator ON apitokens.operator_id = operator.id
|
||||
WHERE apitokens.token_value=$1 and apitokens.active=true`
|
||||
)
|
||||
|
||||
func GetClaims(c *gin.Context) (*CustomClaims, error) {
|
||||
@@ -66,6 +71,7 @@ func GetClaims(c *gin.Context) (*CustomClaims, error) {
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*CustomClaims); ok {
|
||||
c.Set("user_id", claims.UserID)
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
@@ -86,11 +92,14 @@ func TokenValid(c *gin.Context) error {
|
||||
logging.LogError(err, "Failed to get apitoken from database", "apitoken", tokenString)
|
||||
return err
|
||||
}
|
||||
c.Request.Header.Add("MythicSource", "scripting")
|
||||
c.Set("user_id", databaseApiToken.OperatorID)
|
||||
c.Set("username", databaseApiToken.Operator.Username)
|
||||
return nil
|
||||
}
|
||||
|
||||
claims := CustomClaims{}
|
||||
if len(tokenString) > 0 {
|
||||
_, err = jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
_, err = jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
logging.LogError(ErrUnexpectedSigningMethod, "signing method", token.Header["alg"])
|
||||
return nil, fmt.Errorf("%w: %v", ErrUnexpectedSigningMethod, token.Header["alg"])
|
||||
@@ -101,8 +110,14 @@ func TokenValid(c *gin.Context) error {
|
||||
logging.LogError(err, "Failed to parse JWT with claims", "JWT", tokenString)
|
||||
return err
|
||||
}
|
||||
operator, err := database.GetUserFromID(claims.UserID)
|
||||
if err == nil {
|
||||
c.Set("username", operator.Username)
|
||||
}
|
||||
c.Set("user_id", claims.UserID)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
return errors.New("failed to get token")
|
||||
}
|
||||
|
||||
func ExtractToken(c *gin.Context) (string, error) {
|
||||
@@ -130,7 +145,7 @@ func ExtractAPIToken(c *gin.Context) (string, error) {
|
||||
return "", ErrMissingJWTToken
|
||||
}
|
||||
|
||||
logging.LogTrace("got apitoken header", "apitoken", token)
|
||||
//logging.LogTrace("got apitoken header", "apitoken", token)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
@@ -139,7 +154,8 @@ func CookieTokenValid(c *gin.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
claims := CustomClaims{}
|
||||
_, err = jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
logging.LogError(ErrUnexpectedSigningMethod, "signing method", token.Header["alg"])
|
||||
return nil, fmt.Errorf("%w: %v", ErrUnexpectedSigningMethod, token.Header["alg"])
|
||||
@@ -150,6 +166,11 @@ func CookieTokenValid(c *gin.Context) error {
|
||||
logging.LogError(err, "Failed to parse JWT with claims", "JWT", tokenString)
|
||||
return err
|
||||
}
|
||||
c.Set("user_id", claims.UserID)
|
||||
operator, err := database.GetUserFromID(claims.UserID)
|
||||
if err == nil {
|
||||
c.Set("username", operator.Username)
|
||||
}
|
||||
c.Request.Header.Add("Authorization", fmt.Sprintf("Bearer: %s", tokenString))
|
||||
return nil
|
||||
}
|
||||
@@ -218,7 +239,6 @@ func RefreshJWT(access_token string, refresh_token string) (string, string, int,
|
||||
|
||||
delete(RefreshTokenCache, access_token)
|
||||
RefreshTokenCache[newAccessToken] = newRefreshToken
|
||||
|
||||
return newAccessToken, newRefreshToken, userID, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
func JwtAuthMiddleware() gin.HandlerFunc {
|
||||
// verify that all authenticated requests have valid signatures and aren't expired
|
||||
return func(c *gin.Context) {
|
||||
if err := TokenValid(c); err != nil {
|
||||
err := TokenValid(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -26,13 +27,14 @@ func JwtAuthMiddleware() gin.HandlerFunc {
|
||||
|
||||
func CookieAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if err := CookieTokenValid(c); err != nil {
|
||||
err := CookieTokenValid(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
} else {
|
||||
c.Next()
|
||||
}
|
||||
c.Request.Header.Add("MythicSource", "cookie")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,15 +67,19 @@ func IPBlockMiddleware() gin.HandlerFunc {
|
||||
|
||||
func RBACMiddleware(allowedRoles []string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if customClaims, err := GetClaims(c); err != nil {
|
||||
customClaims, err := GetClaims(c)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get claims for RBACMiddleware")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
|
||||
c.Abort()
|
||||
} else if operatorOperation, err := database.GetUserCurrentOperation(customClaims.UserID); err != nil {
|
||||
}
|
||||
operatorOperation, err := database.GetUserCurrentOperation(customClaims.UserID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get user current operation")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
|
||||
c.Abort()
|
||||
} else if operatorOperation.CurrentOperator.Admin || utils.SliceContains(allowedRoles, operatorOperation.ViewMode) {
|
||||
}
|
||||
if operatorOperation.CurrentOperator.Admin || utils.SliceContains(allowedRoles, operatorOperation.ViewMode) {
|
||||
c.Set("operatorOperation", operatorOperation)
|
||||
c.Next()
|
||||
return
|
||||
|
||||
@@ -11,4 +11,5 @@ type Apitokens struct {
|
||||
Active bool `db:"active"`
|
||||
CreationTime time.Time `db:"creation_time"`
|
||||
OperatorID int `db:"operator_id"`
|
||||
Operator Operator `db:"operator"`
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@ require (
|
||||
github.com/go-logr/logr v1.4.1
|
||||
github.com/go-logr/zerologr v1.2.3
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/rabbitmq/amqp091-go v1.9.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/rubenv/sql-migrate v1.6.0
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/rubenv/sql-migrate v1.6.1
|
||||
github.com/spf13/viper v1.18.2
|
||||
golang.org/x/mod v0.14.0
|
||||
google.golang.org/grpc v1.60.1
|
||||
google.golang.org/grpc v1.61.0
|
||||
google.golang.org/protobuf v1.32.0
|
||||
)
|
||||
|
||||
@@ -30,13 +30,13 @@ require (
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.16.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.17.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -55,11 +55,11 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -35,6 +35,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE=
|
||||
github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
|
||||
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
@@ -50,6 +52,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
@@ -67,6 +71,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
@@ -80,6 +86,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -99,8 +106,12 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rubenv/sql-migrate v1.6.0 h1:IZpcTlAx/VKXphWEpwWJ7BaMq05tYtE80zYz+8a5Il8=
|
||||
github.com/rubenv/sql-migrate v1.6.0/go.mod h1:m3ilnKP7sNb4eYkLsp6cGdPOl4OBcXM6rcbzU+Oqc5k=
|
||||
github.com/rubenv/sql-migrate v1.6.1 h1:bo6/sjsan9HaXAsNxYP/jCEDUGibHp8JmOBw7NTGRos=
|
||||
github.com/rubenv/sql-migrate v1.6.1/go.mod h1:tPzespupJS0jacLfhbwto/UjSX+8h2FdWB7ar+QlHa0=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
@@ -143,6 +154,8 @@ golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE=
|
||||
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
|
||||
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
@@ -158,8 +171,12 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 h1:FSL3lRCkhaPFxqi0s9o+V4UI2WTzAVOvkgbd4kVV4Wg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4=
|
||||
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
|
||||
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
|
||||
@@ -2,6 +2,7 @@ package rabbitmq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/its-a-feature/Mythic/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,7 @@ type LoggingMessage struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Action LOG_TYPE `json:"action"`
|
||||
Data interface{} `json:"data"`
|
||||
ServerName string `json:"server_name"`
|
||||
}
|
||||
|
||||
func GetLoggingRoutingKey(loggingAction LOG_TYPE) string {
|
||||
@@ -35,11 +37,13 @@ func GetLoggingRoutingKey(loggingAction LOG_TYPE) string {
|
||||
}
|
||||
|
||||
func (r *rabbitMQConnection) EmitSiemMessage(loggingMessage LoggingMessage) error {
|
||||
localLoggingMessage := loggingMessage
|
||||
localLoggingMessage.ServerName = utils.MythicConfig.GlobalServerName
|
||||
if err := r.SendStructMessage(
|
||||
MYTHIC_TOPIC_EXCHANGE,
|
||||
GetLoggingRoutingKey(loggingMessage.Action),
|
||||
"",
|
||||
loggingMessage,
|
||||
localLoggingMessage,
|
||||
true,
|
||||
); err != nil {
|
||||
//logging.LogError(err, "Failed to emit SIEM Message")
|
||||
|
||||
@@ -2,6 +2,7 @@ package rabbitmq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/its-a-feature/Mythic/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -26,6 +27,7 @@ type WebhookMessage struct {
|
||||
OperatorUsername string `json:"operator_username,omitempty"`
|
||||
Action WEBHOOK_TYPE `json:"action"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
ServerName string `json:"server_name"`
|
||||
}
|
||||
|
||||
type NewCallbackWebhookData struct {
|
||||
@@ -65,11 +67,13 @@ func GetWebhookRoutingKey(webhookAction WEBHOOK_TYPE) string {
|
||||
}
|
||||
|
||||
func (r *rabbitMQConnection) EmitWebhookMessage(webhookMessage WebhookMessage) error {
|
||||
localWebhookMessage := webhookMessage
|
||||
localWebhookMessage.ServerName = utils.MythicConfig.GlobalServerName
|
||||
if err := r.SendStructMessage(
|
||||
MYTHIC_TOPIC_EXCHANGE,
|
||||
GetWebhookRoutingKey(webhookMessage.Action),
|
||||
"",
|
||||
webhookMessage,
|
||||
localWebhookMessage,
|
||||
true,
|
||||
); err != nil {
|
||||
//logging.LogError(err, "Failed to emit webhook Message")
|
||||
|
||||
@@ -74,7 +74,7 @@ func MythicRPCPayloadCreateFromUUID(input MythicRPCPayloadCreateFromUUIDMessage)
|
||||
}
|
||||
payloadConfiguration.SelectedOS = payload.Os
|
||||
payloadConfiguration.PayloadType = payload.Payloadtype.Name
|
||||
payloadConfiguration.C2Profiles = GetC2ProfileInformation(payload)
|
||||
payloadConfiguration.C2Profiles = GetPayloadC2ProfileInformation(payload)
|
||||
payloadConfiguration.BuildParameters = GetBuildParameterInformation(payload.ID)
|
||||
payloadConfiguration.Commands = GetPayloadCommandInformation(payload)
|
||||
if payload.WrappedPayloadID.Valid {
|
||||
@@ -165,7 +165,7 @@ func associateBuildParametersWithPayload(databasePayload databaseStructs.Payload
|
||||
} else {
|
||||
found = true
|
||||
// the user supplied an explicit value for this build parameter, so use it
|
||||
if val, err := getFinalStringForDatabaseInstanceValueFromUserSuppliedValue(databaseBuildParameter.ParameterType, suppliedBuildParam.Value); err != nil {
|
||||
if val, err := GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(databaseBuildParameter.ParameterType, suppliedBuildParam.Value); err != nil {
|
||||
//if val, err := GetStrippedValueForBuildParameter(databaseBuildParameter, suppliedBuildParam.Value); err != nil {
|
||||
logging.LogError(err, "Failed to strip value of build parameter", "value", suppliedBuildParam.Value, "build_parameter", databaseBuildParameter)
|
||||
return nil, err
|
||||
@@ -306,7 +306,7 @@ func associateC2ProfilesWithPayload(databasePayload databaseStructs.Payload, c2P
|
||||
if suppliedParameterName == databaseC2ProfileParameter.Name {
|
||||
// we have a supplied parameter that matches the one we're looking at from the database for this c2 profile
|
||||
found = true
|
||||
if strippedValue, err := getFinalStringForDatabaseInstanceValueFromUserSuppliedValue(
|
||||
if strippedValue, err := GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(
|
||||
databaseC2ProfileParameter.ParameterType, suppliedParameterValue,
|
||||
); err != nil {
|
||||
//if strippedValue, err := GetStrippedValueForC2Parameter(databaseC2ProfileParameter, suppliedParameterValue); err != nil {
|
||||
|
||||
@@ -160,7 +160,7 @@ func getPayloadConfigFromUUID(payloadUUID string) (PayloadConfiguration, error)
|
||||
payloadConfiguration.Description = payload.Description
|
||||
payloadConfiguration.SelectedOS = payload.Os
|
||||
payloadConfiguration.PayloadType = payload.Payloadtype.Name
|
||||
payloadConfiguration.C2Profiles = GetC2ProfileInformation(payload)
|
||||
payloadConfiguration.C2Profiles = GetPayloadC2ProfileInformation(payload)
|
||||
payloadConfiguration.BuildParameters = GetBuildParameterInformation(payload.ID)
|
||||
payloadConfiguration.Commands = GetPayloadCommandInformation(payload)
|
||||
payloadConfiguration.Filename = string(payload.Filemeta.Filename)
|
||||
|
||||
@@ -195,7 +195,7 @@ func payloadTypeSync(in PayloadTypeSyncMessage) error {
|
||||
logging.LogError(nil, "attempting to sync bad payload container version")
|
||||
return errors.New(fmt.Sprintf("Version, %s, isn't supported. The max supported version is %s. \nThis likely means your PyPi or Golang library is out of date and should be updated.", in.ContainerVersion, validContainerVersionMax))
|
||||
}
|
||||
if err := database.DB.Get(&payloadtype, `SELECT * FROM payloadtype WHERE name=$1`, in.PayloadType.Name); err != nil {
|
||||
if err := database.DB.Get(&payloadtype, `SELECT * FROM payloadtype WHERE "name"=$1`, in.PayloadType.Name); err != nil {
|
||||
// this means we don't have the payload, so we need to create it and all the associated components
|
||||
//logging.LogDebug("Failed to find payload type, syncing new data", "payload", payloadtype)
|
||||
payloadtype.Name = in.PayloadType.Name
|
||||
|
||||
@@ -34,6 +34,7 @@ type PayloadConfiguration struct {
|
||||
}
|
||||
type PayloadConfigurationC2Profile struct {
|
||||
Name string `json:"c2_profile"`
|
||||
IsP2P bool `json:"c2_profile_is_p2p"`
|
||||
Parameters map[string]interface{} `json:"c2_profile_parameters"`
|
||||
}
|
||||
type PayloadConfigurationBuildParameter struct {
|
||||
|
||||
@@ -21,7 +21,7 @@ func CreateC2Instance(input CreateC2InstanceInput, operatorOperation *databaseSt
|
||||
response := CreateC2InstanceResponse{
|
||||
Status: "error",
|
||||
}
|
||||
logging.LogDebug("got a request to create a c2 instance", "input", input)
|
||||
//logging.LogDebug("got a request to create a c2 instance", "input", input)
|
||||
// saved value from the UI/Scripting will be key-value pairs with the final values (except for crypto generation)
|
||||
c2ProfileParameters := []databaseStructs.C2profileparameters{}
|
||||
if err := database.DB.Select(&c2ProfileParameters, `SELECT
|
||||
@@ -46,7 +46,7 @@ func CreateC2Instance(input CreateC2InstanceInput, operatorOperation *databaseSt
|
||||
for key, val := range input.Parameters {
|
||||
if key == dbParam.Name {
|
||||
found = true
|
||||
if dbStringValue, err := getFinalStringForDatabaseInstanceValueFromUserSuppliedValue(dbParam.ParameterType, val); err != nil {
|
||||
if dbStringValue, err := GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(dbParam.ParameterType, val); err != nil {
|
||||
logging.LogError(err, "Failed to get string from user supplied value for saved c2 instance")
|
||||
response.Error = "Failed to get string from user supplied value for saved c2 instance: " + err.Error()
|
||||
return response
|
||||
|
||||
@@ -183,7 +183,7 @@ func getSyncToDatabaseValueForDefaultValue(parameterType string, defaultValue in
|
||||
return "", errors.New("Unknown parameter type")
|
||||
}
|
||||
}
|
||||
func getFinalStringForDatabaseInstanceValueFromUserSuppliedValue(parameterType string, userSuppliedValue interface{}) (string, error) {
|
||||
func GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(parameterType string, userSuppliedValue interface{}) (string, error) {
|
||||
switch parameterType {
|
||||
case BUILD_PARAMETER_TYPE_CHOOSE_ONE:
|
||||
fallthrough
|
||||
@@ -516,11 +516,12 @@ func GetInterfaceValueForContainer(parameterType string, finalString string, enc
|
||||
}
|
||||
|
||||
// Payload information for exporting, rebuilding
|
||||
func GetC2ProfileInformation(payload databaseStructs.Payload) *[]PayloadConfigurationC2Profile {
|
||||
func GetPayloadC2ProfileInformation(payload databaseStructs.Payload) *[]PayloadConfigurationC2Profile {
|
||||
c2profileParameterInstances := []databaseStructs.C2profileparametersinstance{}
|
||||
if err := database.DB.Select(&c2profileParameterInstances, `SELECT
|
||||
c2profile.name "c2profile.name",
|
||||
c2profile.id "c2profile.id",
|
||||
c2profile.is_p2p "c2profile.is_p2p",
|
||||
value, enc_key, dec_key,
|
||||
c2profileparameters.crypto_type "c2profileparameters.crypto_type",
|
||||
c2profileparameters.parameter_type "c2profileparameters.parameter_type",
|
||||
@@ -544,8 +545,12 @@ func GetC2ProfileInformation(payload databaseStructs.Payload) *[]PayloadConfigur
|
||||
}
|
||||
finalC2Profiles := []PayloadConfigurationC2Profile{}
|
||||
for c2ProfileName, c2ProfileGroup := range parametersMap {
|
||||
isP2P := false
|
||||
parametersValueDictionary := make(map[string]interface{})
|
||||
for _, parameter := range c2ProfileGroup {
|
||||
if parameter.C2Profile.IsP2p {
|
||||
isP2P = true
|
||||
}
|
||||
if interfaceParam, err := GetInterfaceValueForContainer(
|
||||
parameter.C2ProfileParameter.ParameterType,
|
||||
parameter.Value,
|
||||
@@ -561,6 +566,62 @@ func GetC2ProfileInformation(payload databaseStructs.Payload) *[]PayloadConfigur
|
||||
finalC2Profiles = append(finalC2Profiles, PayloadConfigurationC2Profile{
|
||||
Name: c2ProfileName,
|
||||
Parameters: parametersValueDictionary,
|
||||
IsP2P: isP2P,
|
||||
})
|
||||
}
|
||||
return &finalC2Profiles
|
||||
}
|
||||
func GetCallbackC2ProfileInformation(callback databaseStructs.Callback) *[]PayloadConfigurationC2Profile {
|
||||
c2profileParameterInstances := []databaseStructs.C2profileparametersinstance{}
|
||||
if err := database.DB.Select(&c2profileParameterInstances, `SELECT
|
||||
c2profile.name "c2profile.name",
|
||||
c2profile.id "c2profile.id",
|
||||
c2profile.is_p2p "c2profile.is_p2p",
|
||||
value, enc_key, dec_key,
|
||||
c2profileparameters.crypto_type "c2profileparameters.crypto_type",
|
||||
c2profileparameters.parameter_type "c2profileparameters.parameter_type",
|
||||
c2profileparameters.name "c2profileparameters.name"
|
||||
FROM c2profileparametersinstance
|
||||
JOIN c2profileparameters ON c2profileparametersinstance.c2_profile_parameters_id = c2profileparameters.id
|
||||
JOIN c2profile ON c2profileparametersinstance.c2_profile_id = c2profile.id
|
||||
WHERE callback_id=$1`, callback.ID); err != nil {
|
||||
logging.LogError(err, "Failed to fetch c2 profile parameters from database for callback", "callback_id", callback.ID)
|
||||
return nil
|
||||
}
|
||||
parametersMap := make(map[string][]databaseStructs.C2profileparametersinstance)
|
||||
for _, parameter := range c2profileParameterInstances {
|
||||
if _, ok := parametersMap[parameter.C2Profile.Name]; ok {
|
||||
// we've already seen parameter.C2Profile.Name before, add to the array
|
||||
parametersMap[parameter.C2Profile.Name] = append(parametersMap[parameter.C2Profile.Name], parameter)
|
||||
} else {
|
||||
// we haven't seen parameter.C2Profile.Name before, create the array
|
||||
parametersMap[parameter.C2Profile.Name] = []databaseStructs.C2profileparametersinstance{parameter}
|
||||
}
|
||||
}
|
||||
finalC2Profiles := []PayloadConfigurationC2Profile{}
|
||||
for c2ProfileName, c2ProfileGroup := range parametersMap {
|
||||
parametersValueDictionary := make(map[string]interface{})
|
||||
isP2P := false
|
||||
for _, parameter := range c2ProfileGroup {
|
||||
if parameter.C2Profile.IsP2p {
|
||||
isP2P = true
|
||||
}
|
||||
if interfaceParam, err := GetInterfaceValueForContainer(
|
||||
parameter.C2ProfileParameter.ParameterType,
|
||||
parameter.Value,
|
||||
parameter.EncKey,
|
||||
parameter.DecKey,
|
||||
parameter.C2ProfileParameter.IsCryptoType); err != nil {
|
||||
logging.LogError(err, "Failed to get c2 profile parameter instance interface")
|
||||
parametersValueDictionary[parameter.C2ProfileParameter.Name] = parameter.Value
|
||||
} else {
|
||||
parametersValueDictionary[parameter.C2ProfileParameter.Name] = interfaceParam
|
||||
}
|
||||
}
|
||||
finalC2Profiles = append(finalC2Profiles, PayloadConfigurationC2Profile{
|
||||
Name: c2ProfileName,
|
||||
Parameters: parametersValueDictionary,
|
||||
IsP2P: isP2P,
|
||||
})
|
||||
}
|
||||
return &finalC2Profiles
|
||||
@@ -570,8 +631,11 @@ func GetBuildParameterInformation(payloadID int) *[]PayloadConfigurationBuildPar
|
||||
buildParameters := []databaseStructs.Buildparameterinstance{}
|
||||
if err := database.DB.Select(&buildParameters, `SELECT
|
||||
buildparameterinstance.value,
|
||||
buildparameterinstance.enc_key,
|
||||
buildparameterinstance.dec_key,
|
||||
buildparameter.name "buildparameter.name",
|
||||
buildparameter.parameter_type "buildparameter.parameter_type"
|
||||
buildparameter.parameter_type "buildparameter.parameter_type",
|
||||
buildparameter.crypto_type "buildparameter.crypto_type"
|
||||
FROM
|
||||
buildparameterinstance
|
||||
JOIN buildparameter ON buildparameterinstance.build_parameter_id = buildparameter.id
|
||||
@@ -584,36 +648,50 @@ func GetBuildParameterInformation(payloadID int) *[]PayloadConfigurationBuildPar
|
||||
buildValues := make([]PayloadConfigurationBuildParameter, len(buildParameters))
|
||||
for index, parameter := range buildParameters {
|
||||
buildValues[index].Name = parameter.BuildParameter.Name
|
||||
if parameter.BuildParameter.ParameterType == BUILD_PARAMETER_TYPE_ARRAY || parameter.BuildParameter.ParameterType == BUILD_PARAMETER_TYPE_CHOOSE_MULTIPLE {
|
||||
// we need to parse the value into a []interface{}
|
||||
var arrayValues []interface{}
|
||||
if err := json.Unmarshal([]byte(parameter.Value), &arrayValues); err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal build parameter array values")
|
||||
return nil
|
||||
} else {
|
||||
buildValues[index].Value = arrayValues
|
||||
}
|
||||
} else if parameter.BuildParameter.ParameterType == BUILD_PARAMETER_TYPE_DICTIONARY {
|
||||
// we need to parse the value into a []map[string]interface{}{}
|
||||
var dictionaryInitialValues map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(parameter.Value), &dictionaryInitialValues); err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal build parameter dictionary values")
|
||||
return nil
|
||||
} else {
|
||||
buildValues[index].Value = dictionaryInitialValues
|
||||
}
|
||||
} else if parameter.BuildParameter.ParameterType == BUILD_PARAMETER_TYPE_TYPED_ARRAY {
|
||||
// we need to parse the value into [][]interface{}
|
||||
var arrayValues [][]interface{}
|
||||
if err := json.Unmarshal([]byte(parameter.Value), &arrayValues); err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal build parameter typed array values")
|
||||
return nil
|
||||
} else {
|
||||
buildValues[index].Value = arrayValues
|
||||
}
|
||||
} else {
|
||||
if interfaceParam, err := GetInterfaceValueForContainer(
|
||||
parameter.BuildParameter.ParameterType,
|
||||
parameter.Value,
|
||||
parameter.EncKey,
|
||||
parameter.DecKey,
|
||||
parameter.BuildParameter.IsCryptoType); err != nil {
|
||||
logging.LogError(err, "Failed to get c2 profile parameter instance interface")
|
||||
buildValues[index].Value = parameter.Value
|
||||
} else {
|
||||
buildValues[index].Value = interfaceParam
|
||||
}
|
||||
/*
|
||||
if parameter.BuildParameter.ParameterType == BUILD_PARAMETER_TYPE_ARRAY || parameter.BuildParameter.ParameterType == BUILD_PARAMETER_TYPE_CHOOSE_MULTIPLE {
|
||||
// we need to parse the value into a []interface{}
|
||||
var arrayValues []interface{}
|
||||
if err := json.Unmarshal([]byte(parameter.Value), &arrayValues); err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal build parameter array values")
|
||||
return nil
|
||||
} else {
|
||||
buildValues[index].Value = arrayValues
|
||||
}
|
||||
} else if parameter.BuildParameter.ParameterType == BUILD_PARAMETER_TYPE_DICTIONARY {
|
||||
// we need to parse the value into a []map[string]interface{}{}
|
||||
var dictionaryInitialValues map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(parameter.Value), &dictionaryInitialValues); err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal build parameter dictionary values")
|
||||
return nil
|
||||
} else {
|
||||
buildValues[index].Value = dictionaryInitialValues
|
||||
}
|
||||
} else if parameter.BuildParameter.ParameterType == BUILD_PARAMETER_TYPE_TYPED_ARRAY {
|
||||
// we need to parse the value into [][]interface{}
|
||||
var arrayValues [][]interface{}
|
||||
if err := json.Unmarshal([]byte(parameter.Value), &arrayValues); err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal build parameter typed array values")
|
||||
return nil
|
||||
} else {
|
||||
buildValues[index].Value = arrayValues
|
||||
}
|
||||
} else {
|
||||
buildValues[index].Value = parameter.Value
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
return &buildValues
|
||||
}
|
||||
@@ -636,6 +714,23 @@ func GetPayloadCommandInformation(payload databaseStructs.Payload) []string {
|
||||
return commandStrings
|
||||
}
|
||||
}
|
||||
func GetCallbackCommandInformation(callback databaseStructs.Callback) []string {
|
||||
commands := []databaseStructs.Loadedcommands{}
|
||||
if err := database.DB.Select(&commands, `SELECT
|
||||
command.cmd "command.cmd"
|
||||
FROM loadedcommands
|
||||
JOIN command ON loadedcommands.command_id = command.id
|
||||
WHERE loadedcommands.callback_id=$1`, callback.ID); err != nil {
|
||||
logging.LogError(err, "Failed to fetch commands for payload")
|
||||
return []string{}
|
||||
} else {
|
||||
commandStrings := make([]string, len(commands))
|
||||
for index, command := range commands {
|
||||
commandStrings[index] = command.Command.Cmd
|
||||
}
|
||||
return commandStrings
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for getting information for sending Task data to a container
|
||||
func CheckAndProcessTaskCompletionHandlers(taskId int) {
|
||||
|
||||
@@ -26,6 +26,7 @@ type Config struct {
|
||||
ServerBindLocalhostOnly bool
|
||||
ServerDynamicPorts []uint32
|
||||
ServerGRPCPort uint
|
||||
GlobalServerName string
|
||||
|
||||
// rabbitmq configuration
|
||||
RabbitmqHost string
|
||||
@@ -79,6 +80,7 @@ func Initialize() {
|
||||
mythicEnv.SetDefault("default_operation_name", "Operation Chimera")
|
||||
mythicEnv.SetDefault("default_operation_webhook_url", "")
|
||||
mythicEnv.SetDefault("default_operation_webhook_channel", "")
|
||||
mythicEnv.SetDefault("global_server_name", "mythic")
|
||||
// pull in environment variables and configuration from .env if needed
|
||||
mythicEnv.SetConfigName(".env")
|
||||
mythicEnv.SetConfigType("env")
|
||||
@@ -146,6 +148,7 @@ func setConfigFromEnv(mythicEnv *viper.Viper) {
|
||||
MythicConfig.DefaultOperationName = mythicEnv.GetString("default_operation_name")
|
||||
MythicConfig.DefaultOperationWebhook = mythicEnv.GetString("default_operation_webhook_url")
|
||||
MythicConfig.DefaultOperationChannel = mythicEnv.GetString("default_operation_webhook_channel")
|
||||
MythicConfig.GlobalServerName = mythicEnv.GetString("global_server_name")
|
||||
allowedIPBlocks := []*net.IPNet{}
|
||||
for _, ipBlock := range strings.Split(mythicEnv.GetString("allowed_ip_blocks"), ",") {
|
||||
if _, subnet, err := net.ParseCIDR(ipBlock); err != nil {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,9 @@
|
||||
import json as js
|
||||
import pprint
|
||||
# JSON pulled from here:
|
||||
# https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack.json
|
||||
# curl https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack.json -o full_attack.json
|
||||
file = open("full_attack.json", "r")
|
||||
output = open("default_files/other_info/attack.json", "w")
|
||||
output = open("attack.json", "w")
|
||||
attack = js.load(file)
|
||||
attack_list = []
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,141 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
"github.com/its-a-feature/Mythic/rabbitmq"
|
||||
)
|
||||
|
||||
type ExportCallbackConfigInput struct {
|
||||
Input ExportCallbackConfig `json:"input" binding:"required"`
|
||||
}
|
||||
|
||||
type ExportCallbackConfig struct {
|
||||
AgentCallbackID string `json:"agent_callback_id" binding:"required"`
|
||||
}
|
||||
|
||||
type ExportCallbackConfigResponse struct {
|
||||
Status string `json:"status"`
|
||||
Config string `json:"config"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
type ExportCallbackConfigurationPayloadType struct {
|
||||
Name string `json:"name"`
|
||||
MythicEncrypts bool `json:"mythic_encrypts"`
|
||||
TranslationContainerName string `json:"translation_container"`
|
||||
}
|
||||
type ExportCallbackConfiguration struct {
|
||||
Callback databaseStructs.Callback `json:"callback"`
|
||||
CallbackC2 []rabbitmq.PayloadConfigurationC2Profile `json:"callback_c2"`
|
||||
CallbackCommands []string `json:"callback_commands"`
|
||||
Payload databaseStructs.Payload `json:"payload"`
|
||||
PayloadType ExportCallbackConfigurationPayloadType `json:"payload_type"`
|
||||
PayloadC2 []rabbitmq.PayloadConfigurationC2Profile `json:"payload_c2"`
|
||||
PayloadBuild []rabbitmq.PayloadConfigurationBuildParameter `json:"payload_build"`
|
||||
PayloadCommands []string `json:"payload_commands"`
|
||||
PayloadFilename string `json:"payload_filename"`
|
||||
}
|
||||
|
||||
func ExportCallbackConfigWebhook(c *gin.Context) {
|
||||
// get variables from the POST request
|
||||
var input ExportCallbackConfigInput
|
||||
err := c.ShouldBindJSON(&input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, ExportCallbackConfigResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
ginOperatorOperation, ok := c.Get("operatorOperation")
|
||||
if !ok {
|
||||
logging.LogError(nil, "Failed to get operatorOperation from gin context from middleware")
|
||||
c.JSON(http.StatusOK, ExportCallbackConfigResponse{
|
||||
Status: "error",
|
||||
Error: "Failed to get current operation information",
|
||||
})
|
||||
return
|
||||
}
|
||||
// get the associated database information
|
||||
operatorOperation := ginOperatorOperation.(*databaseStructs.Operatoroperation)
|
||||
callback := databaseStructs.Callback{}
|
||||
payload := databaseStructs.Payload{}
|
||||
err = database.DB.Get(&callback, `SELECT * FROM callback WHERE agent_callback_id=$1`, input.Input.AgentCallbackID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get callback from database")
|
||||
c.JSON(http.StatusOK, ExportCallbackConfigResponse{
|
||||
Status: "error",
|
||||
Error: fmt.Sprintf("Failed to get callback information: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
err = database.DB.Get(&payload, `SELECT
|
||||
payload.*,
|
||||
payloadtype.name "payloadtype.name",
|
||||
payloadtype.mythic_encrypts "payloadtype.mythic_encrypts",
|
||||
payloadtype.translation_container_id "payloadtype.translation_container_id",
|
||||
filemeta.filename "filemeta.filename"
|
||||
FROM
|
||||
payload
|
||||
JOIN payloadtype ON payload.payload_type_id = payloadtype.id
|
||||
JOIN filemeta ON payload.file_id = filemeta.id
|
||||
WHERE
|
||||
payload.id=$1 and payload.operation_id=$2`, callback.RegisteredPayloadID, operatorOperation.CurrentOperation.ID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get payload when exporting callback")
|
||||
c.JSON(http.StatusOK, ExportCallbackConfigResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
translationContainer := databaseStructs.Translationcontainer{}
|
||||
if payload.Payloadtype.TranslationContainerID.Valid {
|
||||
err = database.DB.Get(&translationContainer, `SELECT "name" FROM translationcontainer WHERE id=$1`,
|
||||
payload.Payloadtype.TranslationContainerID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get translation container when exporting callback")
|
||||
c.JSON(http.StatusOK, ExportCallbackConfigResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
config := ExportCallbackConfiguration{}
|
||||
config.Callback = callback
|
||||
config.CallbackC2 = *rabbitmq.GetCallbackC2ProfileInformation(callback)
|
||||
config.CallbackCommands = rabbitmq.GetCallbackCommandInformation(callback)
|
||||
config.Payload = payload
|
||||
config.PayloadType = ExportCallbackConfigurationPayloadType{
|
||||
Name: payload.Payloadtype.Name,
|
||||
MythicEncrypts: payload.Payloadtype.MythicEncrypts,
|
||||
TranslationContainerName: translationContainer.Name,
|
||||
}
|
||||
config.PayloadC2 = *rabbitmq.GetPayloadC2ProfileInformation(payload)
|
||||
config.PayloadBuild = *rabbitmq.GetBuildParameterInformation(payload.ID)
|
||||
config.PayloadCommands = rabbitmq.GetPayloadCommandInformation(payload)
|
||||
config.PayloadFilename = string(payload.Filemeta.Filename)
|
||||
|
||||
payloadConfigurationString, err := json.MarshalIndent(config, "", "\t")
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to convert config to bytes")
|
||||
c.JSON(http.StatusOK, ExportCallbackConfigResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, ExportCallbackConfigResponse{
|
||||
Status: "success",
|
||||
Config: string(payloadConfigurationString),
|
||||
})
|
||||
return
|
||||
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
"github.com/its-a-feature/Mythic/rabbitmq"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ImportCallbackConfigInput struct {
|
||||
Input ImportCallbackConfig `json:"input" binding:"required"`
|
||||
}
|
||||
|
||||
type ImportCallbackConfig struct {
|
||||
Config ExportCallbackConfiguration `json:"config" binding:"required"`
|
||||
}
|
||||
|
||||
type ImportCallbackConfigResponse struct {
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func ImportCallbackConfigWebhook(c *gin.Context) {
|
||||
// get variables from the POST request
|
||||
var input ImportCallbackConfigInput
|
||||
err := c.ShouldBindJSON(&input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, ImportCallbackConfigResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
ginOperatorOperation, ok := c.Get("operatorOperation")
|
||||
if !ok {
|
||||
logging.LogError(nil, "Failed to get operatorOperation from gin context from middleware")
|
||||
c.JSON(http.StatusOK, ImportCallbackConfigResponse{
|
||||
Status: "error",
|
||||
Error: "Failed to get current operation information",
|
||||
})
|
||||
return
|
||||
}
|
||||
callbackConfig := input.Input.Config
|
||||
// get the associated database information
|
||||
operatorOperation := ginOperatorOperation.(*databaseStructs.Operatoroperation)
|
||||
// have to process the callback configuration in order from most generic to most specific
|
||||
translationContainer := databaseStructs.Translationcontainer{
|
||||
Name: callbackConfig.PayloadType.TranslationContainerName,
|
||||
}
|
||||
if callbackConfig.PayloadType.TranslationContainerName != "" {
|
||||
err = database.DB.Get(&translationContainer, `SELECT * FROM translationcontainer WHERE "name"=$1`, translationContainer.Name)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// payload type doesn't exist, so we need to create it
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO translationcontainer
|
||||
("name")
|
||||
VALUES (:name)
|
||||
RETURNING id`,
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new translationcontainer statement")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
err = statement.Get(&translationContainer.ID, translationContainer)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new translation container")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logging.LogError(err, "Failed to query translation container information")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
}
|
||||
}
|
||||
}
|
||||
// make sure callbackConfig.PayloadType exists (and create if needed)
|
||||
payloadtype := databaseStructs.Payloadtype{
|
||||
Name: callbackConfig.PayloadType.Name,
|
||||
MythicEncrypts: callbackConfig.PayloadType.MythicEncrypts,
|
||||
SupportedOs: rabbitmq.GetMythicJSONArrayFromStruct([]string{callbackConfig.Payload.Os}),
|
||||
}
|
||||
if translationContainer.ID > 0 {
|
||||
payloadtype.TranslationContainerID.Valid = true
|
||||
payloadtype.TranslationContainerID.Int64 = int64(translationContainer.ID)
|
||||
}
|
||||
err = database.DB.Get(&payloadtype, `SELECT * FROM payloadtype WHERE "name"=$1`, callbackConfig.PayloadType.Name)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// payload type doesn't exist, so we need to create it
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO payloadtype
|
||||
("name",mythic_encrypts,supported_os,translation_container_id)
|
||||
VALUES (:name, :mythic_encrypts, :supported_os, :translation_container_id)
|
||||
RETURNING id`,
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new payloadtype statement")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
err = statement.Get(&payloadtype.ID, payloadtype)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new payloadtype")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logging.LogError(err, "Failed to query payloadtype information")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
// generate fake payload
|
||||
fileMeta := databaseStructs.Filemeta{
|
||||
AgentFileID: uuid.NewString(),
|
||||
TotalChunks: 1,
|
||||
ChunksReceived: 1,
|
||||
Complete: true,
|
||||
IsPayload: true,
|
||||
Filename: []byte(callbackConfig.PayloadFilename),
|
||||
OperationID: operatorOperation.CurrentOperation.ID,
|
||||
OperatorID: operatorOperation.CurrentOperator.ID,
|
||||
}
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO filemeta
|
||||
(total_chunks, chunks_received, complete, is_payload, filename, operation_id, operator_id, agent_file_id, path)
|
||||
VALUES (:total_chunks, :chunks_received, :complete, :is_payload, :filename, :operation_id, :operator_id, :agent_file_id, :path) RETURNING id`)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to make named statement for filemeta")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
err = statement.Get(&fileMeta.ID, fileMeta)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create filemeta information")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
payload := databaseStructs.Payload{
|
||||
UuID: callbackConfig.Payload.UuID,
|
||||
Description: callbackConfig.Payload.Description,
|
||||
PayloadTypeID: payloadtype.ID,
|
||||
OperationID: operatorOperation.CurrentOperation.ID,
|
||||
Os: callbackConfig.Payload.Os,
|
||||
BuildPhase: callbackConfig.Payload.BuildPhase,
|
||||
BuildContainer: callbackConfig.Payload.BuildContainer,
|
||||
BuildMessage: callbackConfig.Payload.BuildMessage,
|
||||
BuildStderr: callbackConfig.Payload.BuildStderr,
|
||||
BuildStdout: callbackConfig.Payload.BuildStdout,
|
||||
OperatorID: operatorOperation.CurrentOperator.ID,
|
||||
}
|
||||
payload.FileID.Valid = true
|
||||
payload.FileID.Int64 = int64(fileMeta.ID)
|
||||
statement, err = database.DB.PrepareNamed(`INSERT INTO payload
|
||||
(uuid,description,payload_type_id,operation_id,os,build_phase, build_container, build_message, build_stderr, build_stdout, operator_id, file_id)
|
||||
VALUES (:uuid, :description, :payload_type_id, :operation_id, :os, :build_phase, :build_container, :build_message, :build_stderr, :build_stdout, :operator_id, :file_id)
|
||||
RETURNING id`,
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new payload statement")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
err = statement.Get(&payload.ID, payload)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new payload")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
// make sure callbackConfig.PayloadType has callbackConfig.PayloadBuild parameters (and create if needed)
|
||||
for _, build := range callbackConfig.PayloadBuild {
|
||||
buildParameter := databaseStructs.Buildparameter{Name: build.Name, PayloadTypeID: payloadtype.ID,
|
||||
Description: build.Name}
|
||||
err = database.DB.Get(&buildParameter, `SELECT id, crypto_type FROM buildparameter WHERE "name"=$1 AND payload_type_id=$2`,
|
||||
buildParameter.Name, buildParameter.PayloadTypeID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// need to create the build parameter
|
||||
switch v := build.Value.(type) {
|
||||
case map[string]interface{}:
|
||||
if _, ok = v["enc_key"]; ok {
|
||||
buildParameter.IsCryptoType = true
|
||||
buildParameter.ParameterType = rabbitmq.BUILD_PARAMETER_TYPE_CHOOSE_ONE
|
||||
} else {
|
||||
buildParameter.ParameterType = rabbitmq.BUILD_PARAMETER_TYPE_DICTIONARY
|
||||
}
|
||||
case bool:
|
||||
buildParameter.ParameterType = rabbitmq.BUILD_PARAMETER_TYPE_BOOLEAN
|
||||
case float64:
|
||||
buildParameter.ParameterType = rabbitmq.BUILD_PARAMETER_TYPE_NUMBER
|
||||
case []interface{}:
|
||||
buildParameter.ParameterType = rabbitmq.BUILD_PARAMETER_TYPE_ARRAY
|
||||
default:
|
||||
buildParameter.ParameterType = rabbitmq.BUILD_PARAMETER_TYPE_STRING
|
||||
}
|
||||
statement, err = database.DB.PrepareNamed(`INSERT INTO buildparameter
|
||||
("name", payload_type_id, crypto_type, parameter_type)
|
||||
VALUES (:name, :payload_type_id, :crypto_type, :parameter_type)
|
||||
RETURNING id`,
|
||||
)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new build parameter statement")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
err = statement.Get(&buildParameter.ID, buildParameter)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create build parameter")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// actually ran into an error
|
||||
logging.LogError(err, "Failed to query build parameters")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
buildParameterInstance := databaseStructs.Buildparameterinstance{
|
||||
BuildParameterID: buildParameter.ID,
|
||||
PayloadID: payload.ID,
|
||||
}
|
||||
// need to create the build parameter
|
||||
switch v := build.Value.(type) {
|
||||
case map[string]interface{}:
|
||||
if _, ok = v["enc_key"]; ok {
|
||||
buildParameterInstance.Value = v["value"].(string)
|
||||
encKey, err := base64.StdEncoding.DecodeString(v["enc_key"].(string))
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to decode encryption key")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
buildParameterInstance.EncKey = &encKey
|
||||
}
|
||||
decKey, err := base64.StdEncoding.DecodeString(v["dec_key"].(string))
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to decode decryption key")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
buildParameterInstance.DecKey = &decKey
|
||||
}
|
||||
} else {
|
||||
buildParameterInstance.Value, err = rabbitmq.GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(buildParameter.ParameterType, build.Value)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to save build parameter")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
buildParameterInstance.Value, err = rabbitmq.GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(buildParameter.ParameterType, build.Value)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to save build parameter")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
// we now know that the build parameter exists, so we can create our instance and tie to the payload
|
||||
_, err = database.DB.NamedExec(`INSERT INTO buildparameterinstance
|
||||
(build_parameter_id, payload_id, "value", enc_key, dec_key)
|
||||
VALUES (:build_parameter_id, :payload_id, :value, :enc_key, :dec_key)`, buildParameterInstance)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to save build parameter instance")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
// make sure payload C2 profiles exist (and create if needed)
|
||||
callbackConfig.Callback.OperationID = operatorOperation.CurrentOperation.ID
|
||||
callbackConfig.Callback.RegisteredPayloadID = payload.ID
|
||||
callbackConfig.Callback.OperatorID = operatorOperation.CurrentOperator.ID
|
||||
statement, err = database.DB.PrepareNamed(`INSERT INTO callback
|
||||
(agent_callback_id, "user", host, pid, ip, external_ip, process_name, description, registered_payload_id, integrity_level,
|
||||
operation_id, crypto_type, enc_key, dec_key, os, architecture, "domain", extra_info, sleep_info, mythictree_groups, operator_id)
|
||||
VALUES (:agent_callback_id, :user, :host, :pid, :ip, :external_ip, :process_name, :description, :registered_payload_id,
|
||||
:integrity_level, :operation_id, :crypto_type, :enc_key, :dec_key, :os, :architecture, :domain, :extra_info, :sleep_info,
|
||||
:mythictree_groups, :operator_id) RETURNING id`)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to prepare named statement for creating callback")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
err = statement.Get(&callbackConfig.Callback.ID, callbackConfig.Callback)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to create new callback")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
// make sure payload commands exist and add them if needed
|
||||
for _, c2 := range callbackConfig.CallbackC2 {
|
||||
err = ensureC2Profile(c2, callbackConfig.Callback.OperationID, callbackConfig.Callback.ID, 0)
|
||||
}
|
||||
for _, c2 := range callbackConfig.PayloadC2 {
|
||||
err = ensureC2Profile(c2, callbackConfig.Callback.OperationID, 0, payload.ID)
|
||||
}
|
||||
// make sure callback c2 profiles exist
|
||||
// make sure callback commands exist and add them if needed
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
||||
return
|
||||
}
|
||||
|
||||
func ensureC2Profile(c2Info rabbitmq.PayloadConfigurationC2Profile, operationID int, callbackID int, payloadID int) error {
|
||||
c2profile := databaseStructs.C2profile{Name: c2Info.Name, IsP2p: c2Info.IsP2P}
|
||||
err := database.DB.Get(&c2profile, `SELECT * FROM c2profile WHERE "name"=$1`, c2profile.Name)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// c2 profile doesn't exist, need to add it
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO c2profile
|
||||
("name", is_p2p) VALUES (:name, :is_p2p) RETURNING id`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = statement.Get(&c2profile.ID, c2profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for paramName, paramValue := range c2Info.Parameters {
|
||||
// ensure the parameters exist for the c2 profile and associate the callback ID
|
||||
c2ProfileParameter := databaseStructs.C2profileparameters{C2ProfileID: c2profile.ID, Name: paramName,
|
||||
Description: paramName}
|
||||
err = database.DB.Get(&c2ProfileParameter, `SELECT * FROM c2profileparameters WHERE
|
||||
c2_profile_id=$1 AND "name"=$2 AND deleted=false`, c2ProfileParameter.C2ProfileID, c2ProfileParameter.Name)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// need to create the build parameter
|
||||
switch v := paramValue.(type) {
|
||||
case map[string]interface{}:
|
||||
if _, ok := v["enc_key"]; ok {
|
||||
c2ProfileParameter.IsCryptoType = true
|
||||
c2ProfileParameter.ParameterType = rabbitmq.C2_PARAMETER_TYPE_CHOOSE_ONE
|
||||
} else {
|
||||
c2ProfileParameter.ParameterType = rabbitmq.C2_PARAMETER_TYPE_DICTIONARY
|
||||
}
|
||||
case bool:
|
||||
c2ProfileParameter.ParameterType = rabbitmq.C2_PARAMETER_TYPE_BOOLEAN
|
||||
case float64:
|
||||
c2ProfileParameter.ParameterType = rabbitmq.C2_PARAMETER_TYPE_NUMBER
|
||||
case []interface{}:
|
||||
c2ProfileParameter.ParameterType = rabbitmq.C2_PARAMETER_TYPE_ARRAY
|
||||
default:
|
||||
c2ProfileParameter.ParameterType = rabbitmq.C2_PARAMETER_TYPE_STRING
|
||||
}
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO c2profileparameters
|
||||
(c2_profile_id, "name", parameter_type, crypto_type, description)
|
||||
VALUES (:c2_profile_id, :name, :parameter_type, :crypto_type, :description) RETURNING id`)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to prepare named c2 profile parameters")
|
||||
return err
|
||||
}
|
||||
err = statement.Get(&c2ProfileParameter.ID, c2ProfileParameter)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get c2 profile parameters")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// c2 profile and c2 profile parameters exist, create a new instance
|
||||
c2ProfileParameterInstance := databaseStructs.C2profileparametersinstance{
|
||||
C2ProfileParametersID: c2ProfileParameter.ID,
|
||||
C2ProfileID: c2profile.ID,
|
||||
}
|
||||
switch v := paramValue.(type) {
|
||||
case map[string]interface{}:
|
||||
if _, ok := v["enc_key"]; ok {
|
||||
c2ProfileParameterInstance.Value = v["value"].(string)
|
||||
encKey, err := base64.StdEncoding.DecodeString(v["enc_key"].(string))
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to decode encryption key")
|
||||
return err
|
||||
} else {
|
||||
c2ProfileParameterInstance.EncKey = &encKey
|
||||
}
|
||||
decKey, err := base64.StdEncoding.DecodeString(v["dec_key"].(string))
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to decode decryption key")
|
||||
return err
|
||||
} else {
|
||||
c2ProfileParameterInstance.DecKey = &decKey
|
||||
}
|
||||
} else {
|
||||
c2ProfileParameterInstance.Value, err = rabbitmq.GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(c2ProfileParameter.ParameterType, paramValue)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to save c2 profile parameter instance value")
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
c2ProfileParameterInstance.Value, err = rabbitmq.GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(c2ProfileParameter.ParameterType, paramValue)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to save c2 profile parameter instance value")
|
||||
return err
|
||||
}
|
||||
}
|
||||
if callbackID > 0 {
|
||||
c2ProfileParameterInstance.CallbackID.Valid = true
|
||||
c2ProfileParameterInstance.CallbackID.Int64 = int64(callbackID)
|
||||
} else {
|
||||
c2ProfileParameterInstance.PayloadID.Valid = true
|
||||
c2ProfileParameterInstance.PayloadID.Int64 = int64(payloadID)
|
||||
}
|
||||
c2ProfileParameterInstance.C2ProfileID = c2profile.ID
|
||||
c2ProfileParameterInstance.OperationID.Valid = true
|
||||
c2ProfileParameterInstance.OperationID.Int64 = int64(operationID)
|
||||
// we now know that the build parameter exists, so we can create our instance and tie to the payload
|
||||
_, err = database.DB.NamedExec(`INSERT INTO c2profileparametersinstance
|
||||
(c2_profile_parameters_id, payload_id, callback_id, "value", enc_key, dec_key, c2_profile_id, operation_id)
|
||||
VALUES (:c2_profile_parameters_id, :payload_id, :callback_id, :value, :enc_key, :dec_key, :c2_profile_id, :operation_id)`,
|
||||
c2ProfileParameterInstance)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to save c2 profile parameters instance")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -90,27 +90,32 @@ func RefreshJWT(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if accessToken, refreshToken, userID, err := authentication.RefreshJWT(input.AccessToken, input.RefreshToken); err != nil {
|
||||
accessToken, refreshToken, userID, err := authentication.RefreshJWT(input.AccessToken, input.RefreshToken)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to use refresh token")
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Authentication Failed"})
|
||||
return
|
||||
} else if currentOperation, err := database.GetUserCurrentOperation(userID); err != nil {
|
||||
}
|
||||
currentOperation, err := database.GetUserCurrentOperation(userID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get user current operation")
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Authentication Failed"})
|
||||
return
|
||||
} else {
|
||||
user := map[string]interface{}{
|
||||
"current_operation_name": currentOperation.CurrentOperation.Name,
|
||||
"current_operation_id": currentOperation.OperationID,
|
||||
"username": currentOperation.CurrentOperator.Username,
|
||||
"id": currentOperation.CurrentOperator.ID,
|
||||
"user_id": currentOperation.CurrentOperator.ID,
|
||||
}
|
||||
// setting cookie max age to 2 days
|
||||
c.SetCookie("mythic", accessToken, 60*60*24*2, "/", strings.Split(c.Request.Host, ":")[0], false, false)
|
||||
c.JSON(http.StatusOK, gin.H{"access_token": accessToken, "refresh_token": refreshToken, "user": user})
|
||||
return
|
||||
}
|
||||
user := map[string]interface{}{
|
||||
"current_operation_name": currentOperation.CurrentOperation.Name,
|
||||
"current_operation_id": currentOperation.OperationID,
|
||||
"username": currentOperation.CurrentOperator.Username,
|
||||
"id": currentOperation.CurrentOperator.ID,
|
||||
"user_id": currentOperation.CurrentOperator.ID,
|
||||
}
|
||||
// setting cookie max age to 2 days
|
||||
c.Set("user_id", currentOperation.CurrentOperator.ID)
|
||||
c.Set("username", currentOperation.CurrentOperator.Username)
|
||||
c.SetCookie("mythic", accessToken, 60*60*24*2, "/", strings.Split(c.Request.Host, ":")[0], false, false)
|
||||
c.JSON(http.StatusOK, gin.H{"access_token": accessToken, "refresh_token": refreshToken, "user": user})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func GetHasuraClaims(c *gin.Context) {
|
||||
@@ -129,58 +134,61 @@ func GetHasuraClaims(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
*/
|
||||
if claims, err := authentication.GetClaims(c); err != nil {
|
||||
//logging.LogDebug("hasura webhook info", "headers", c.Request.Header)
|
||||
claims, err := authentication.GetClaims(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Authentication Failed"})
|
||||
return
|
||||
} else {
|
||||
hasuraClaims := make(map[string]interface{})
|
||||
//logging.LogTrace("JWT claims", "claims", claims, "user", claims.UserID)
|
||||
hasuraClaims["x-hasura-user-id"] = fmt.Sprintf("%d", claims.UserID)
|
||||
hasuraOperations := []string{}
|
||||
hasuraAdminOperations := []string{}
|
||||
user, err := database.GetUserFromID(claims.UserID)
|
||||
//logging.LogTrace("user info", "user", user)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to fetch operator based on JWT UserID")
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Authentication Failed"})
|
||||
return
|
||||
}
|
||||
if !user.CurrentOperationID.Valid {
|
||||
hasuraClaims["x-hasura-current-operation-id"] = "0"
|
||||
hasuraClaims["x-hasura-current_operation"] = "null"
|
||||
hasuraClaims["x-hasura-role"] = "spectator"
|
||||
}
|
||||
if allOperations, err := database.GetOperationsForUser(claims.UserID); err != nil {
|
||||
logging.LogError(err, "Failed to get all operations for user when generating hasura claims")
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Authentication Failed"})
|
||||
return
|
||||
} else {
|
||||
|
||||
for _, operatorOperation := range *allOperations {
|
||||
//logging.LogInfo("operatorOperation info", "operatorOperation", operatorOperation)
|
||||
if operatorOperation.CurrentOperation.AdminID == claims.UserID {
|
||||
hasuraAdminOperations = append(hasuraAdminOperations, fmt.Sprintf("%d", operatorOperation.CurrentOperation.ID))
|
||||
}
|
||||
hasuraOperations = append(hasuraOperations, fmt.Sprintf("%d", operatorOperation.CurrentOperation.ID))
|
||||
if operatorOperation.CurrentOperation.ID == int(user.CurrentOperationID.Int64) {
|
||||
hasuraClaims["x-hasura-role"] = operatorOperation.ViewMode
|
||||
if hasuraClaims["x-hasura-role"] == "lead" {
|
||||
hasuraClaims["x-hasura-role"] = "operation_admin"
|
||||
}
|
||||
}
|
||||
if user.CurrentOperationID.Valid && (int(user.CurrentOperationID.Int64) == operatorOperation.CurrentOperation.ID) {
|
||||
hasuraClaims["x-hasura-current-operation-id"] = fmt.Sprintf("%d", user.CurrentOperationID.Int64)
|
||||
hasuraClaims["x-hasura-current_operation"] = user.CurrentOperation.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
if user.Admin {
|
||||
hasuraClaims["x-hasura-role"] = "mythic_admin"
|
||||
}
|
||||
hasuraClaims["x-hasura-operations"] = fmt.Sprintf("{%s}", strings.Join(hasuraOperations, ","))
|
||||
hasuraClaims["x-hasura-admin-operations"] = fmt.Sprintf("{%s}", strings.Join(hasuraAdminOperations, ","))
|
||||
//logging.LogTrace("hasura claims", "claims", hasuraClaims)
|
||||
c.JSON(http.StatusOK, hasuraClaims)
|
||||
}
|
||||
hasuraClaims := make(map[string]interface{})
|
||||
//logging.LogTrace("JWT claims", "claims", claims, "user", claims.UserID)
|
||||
hasuraClaims["x-hasura-user-id"] = fmt.Sprintf("%d", claims.UserID)
|
||||
hasuraOperations := []string{}
|
||||
hasuraAdminOperations := []string{}
|
||||
user, err := database.GetUserFromID(claims.UserID)
|
||||
//logging.LogTrace("user info", "user", user)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to fetch operator based on JWT UserID")
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Authentication Failed"})
|
||||
return
|
||||
}
|
||||
c.Set("username", user.Username)
|
||||
if !user.CurrentOperationID.Valid {
|
||||
hasuraClaims["x-hasura-current-operation-id"] = "0"
|
||||
hasuraClaims["x-hasura-current_operation"] = "null"
|
||||
hasuraClaims["x-hasura-role"] = "spectator"
|
||||
}
|
||||
allOperations, err := database.GetOperationsForUser(claims.UserID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get all operations for user when generating hasura claims")
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Authentication Failed"})
|
||||
return
|
||||
}
|
||||
|
||||
for _, operatorOperation := range *allOperations {
|
||||
//logging.LogInfo("operatorOperation info", "operatorOperation", operatorOperation)
|
||||
if operatorOperation.CurrentOperation.AdminID == claims.UserID {
|
||||
hasuraAdminOperations = append(hasuraAdminOperations, fmt.Sprintf("%d", operatorOperation.CurrentOperation.ID))
|
||||
}
|
||||
hasuraOperations = append(hasuraOperations, fmt.Sprintf("%d", operatorOperation.CurrentOperation.ID))
|
||||
if operatorOperation.CurrentOperation.ID == int(user.CurrentOperationID.Int64) {
|
||||
hasuraClaims["x-hasura-role"] = operatorOperation.ViewMode
|
||||
if hasuraClaims["x-hasura-role"] == "lead" {
|
||||
hasuraClaims["x-hasura-role"] = "operation_admin"
|
||||
}
|
||||
}
|
||||
if user.CurrentOperationID.Valid && (int(user.CurrentOperationID.Int64) == operatorOperation.CurrentOperation.ID) {
|
||||
hasuraClaims["x-hasura-current-operation-id"] = fmt.Sprintf("%d", user.CurrentOperationID.Int64)
|
||||
hasuraClaims["x-hasura-current_operation"] = user.CurrentOperation.Name
|
||||
}
|
||||
}
|
||||
|
||||
if user.Admin {
|
||||
hasuraClaims["x-hasura-role"] = "mythic_admin"
|
||||
}
|
||||
hasuraClaims["x-hasura-operations"] = fmt.Sprintf("{%s}", strings.Join(hasuraOperations, ","))
|
||||
hasuraClaims["x-hasura-admin-operations"] = fmt.Sprintf("{%s}", strings.Join(hasuraAdminOperations, ","))
|
||||
//logging.LogTrace("hasura claims", "claims", hasuraClaims)
|
||||
c.JSON(http.StatusOK, hasuraClaims)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,27 +22,32 @@ type BuildPayloadPayloadDefinition struct {
|
||||
func CreatePayloadWebhook(c *gin.Context) {
|
||||
var input BuildPayloadInput
|
||||
payloadConfig := rabbitmq.PayloadConfiguration{}
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
err := c.ShouldBindJSON(&input)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get JSON parameters for CreatePayloadWebhook")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
} else if ginOperatorOperation, ok := c.Get("operatorOperation"); !ok {
|
||||
}
|
||||
ginOperatorOperation, ok := c.Get("operatorOperation")
|
||||
if !ok {
|
||||
logging.LogError(err, "Failed to get operatorOperation information for CreatePayloadWebhook")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": "Failed to get current operation. Is it set?"})
|
||||
return
|
||||
} else if err := json.Unmarshal([]byte(input.Input.PayloadDefinition), &payloadConfig); err != nil {
|
||||
}
|
||||
err = json.Unmarshal([]byte(input.Input.PayloadDefinition), &payloadConfig)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal payload definition", "input", input.Input.PayloadDefinition)
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
} else {
|
||||
operatorOperation := ginOperatorOperation.(*databaseStructs.Operatoroperation)
|
||||
// now actually create the payload
|
||||
if newUUID, _, err := rabbitmq.RegisterNewPayload(payloadConfig, operatorOperation); err != nil {
|
||||
logging.LogError(err, "Failed to register payload for CreatePayloadWebhook")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success", "uuid": newUUID})
|
||||
return
|
||||
}
|
||||
}
|
||||
operatorOperation := ginOperatorOperation.(*databaseStructs.Operatoroperation)
|
||||
// now actually create the payload
|
||||
newUUID, _, err := rabbitmq.RegisterNewPayload(payloadConfig, operatorOperation)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to register payload for CreatePayloadWebhook")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "error", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success", "uuid": newUUID})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func ExportPayloadConfigWebhook(c *gin.Context) {
|
||||
payloadConfiguration.Description = payload.Description
|
||||
payloadConfiguration.SelectedOS = payload.Os
|
||||
payloadConfiguration.PayloadType = payload.Payloadtype.Name
|
||||
payloadConfiguration.C2Profiles = rabbitmq.GetC2ProfileInformation(payload)
|
||||
payloadConfiguration.C2Profiles = rabbitmq.GetPayloadC2ProfileInformation(payload)
|
||||
payloadConfiguration.BuildParameters = rabbitmq.GetBuildParameterInformation(payload.ID)
|
||||
payloadConfiguration.Commands = rabbitmq.GetPayloadCommandInformation(payload)
|
||||
payloadConfiguration.Filename = string(payload.Filemeta.Filename)
|
||||
|
||||
@@ -64,7 +64,7 @@ func PayloadRebuildWebhook(c *gin.Context) {
|
||||
payloadConfiguration.Description = payload.Description
|
||||
payloadConfiguration.SelectedOS = payload.Os
|
||||
payloadConfiguration.PayloadType = payload.Payloadtype.Name
|
||||
payloadConfiguration.C2Profiles = rabbitmq.GetC2ProfileInformation(payload)
|
||||
payloadConfiguration.C2Profiles = rabbitmq.GetPayloadC2ProfileInformation(payload)
|
||||
payloadConfiguration.BuildParameters = rabbitmq.GetBuildParameterInformation(payload.ID)
|
||||
payloadConfiguration.Commands = rabbitmq.GetPayloadCommandInformation(payload)
|
||||
payloadConfiguration.Filename = string(payload.Filemeta.Filename)
|
||||
|
||||
@@ -28,7 +28,8 @@ type GenerateAPITokenResponse struct {
|
||||
func GenerateAPITokenWebhook(c *gin.Context) {
|
||||
// get variables from the POST request
|
||||
var input GenerateAPITokenInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
err := c.ShouldBindJSON(&input)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, GenerateAPITokenResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
@@ -37,51 +38,55 @@ func GenerateAPITokenWebhook(c *gin.Context) {
|
||||
}
|
||||
// get the associated database information
|
||||
// an api token is just the same as a JWT
|
||||
if userID, err := GetUserIDFromGin(c); err != nil {
|
||||
userID, err := GetUserIDFromGin(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
} else if access_token, _, _, err := authentication.GenerateJWT(databaseStructs.Operator{
|
||||
}
|
||||
access_token, _, _, err := authentication.GenerateJWT(databaseStructs.Operator{
|
||||
ID: userID,
|
||||
}, authentication.AUTH_METHOD_API); err != nil {
|
||||
}, authentication.AUTH_METHOD_API)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
} else {
|
||||
// save off the access_token as an API token and then return it
|
||||
apiToken := databaseStructs.Apitokens{
|
||||
TokenValue: access_token,
|
||||
OperatorID: userID,
|
||||
TokenType: input.Input.TokenType,
|
||||
Active: true,
|
||||
}
|
||||
if statement, err := database.DB.PrepareNamed(`INSERT INTO apitokens
|
||||
}
|
||||
// save off the access_token as an API token and then return it
|
||||
apiToken := databaseStructs.Apitokens{
|
||||
TokenValue: access_token,
|
||||
OperatorID: userID,
|
||||
TokenType: input.Input.TokenType,
|
||||
Active: true,
|
||||
}
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO apitokens
|
||||
(token_value, operator_id, token_type, active)
|
||||
VALUES
|
||||
(:token_value, :operator_id, :token_type, :active)
|
||||
RETURNING id`); err != nil {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
} else if err := statement.Get(&apiToken.ID, apiToken); err != nil {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
} else {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "success",
|
||||
TokenValue: access_token,
|
||||
OperatorID: userID,
|
||||
ID: apiToken.ID,
|
||||
})
|
||||
}
|
||||
RETURNING id`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
err = statement.Get(&apiToken.ID, apiToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "error",
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "success",
|
||||
TokenValue: access_token,
|
||||
OperatorID: userID,
|
||||
ID: apiToken.ID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,13 +52,12 @@ func StartServer(r *gin.Engine) {
|
||||
}
|
||||
|
||||
func InitializeGinLogger() gin.HandlerFunc {
|
||||
ignorePaths := []string{"/graphql/webhook", "/agent_message", "/health"}
|
||||
ignorePaths := []string{"/agent_message", "/health"}
|
||||
return func(c *gin.Context) {
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
param := gin.LogFormatterParams{
|
||||
@@ -78,11 +77,14 @@ func InitializeGinLogger() gin.HandlerFunc {
|
||||
param.StatusCode = c.Writer.Status()
|
||||
param.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String()
|
||||
param.BodySize = c.Writer.Size()
|
||||
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
logging.LogDebug("WebServer Logging",
|
||||
source := c.GetHeader("MythicSource")
|
||||
if source == "" {
|
||||
source = "scripting"
|
||||
}
|
||||
logging.LogInfo("WebServer Logging",
|
||||
"ClientIP", param.ClientIP,
|
||||
"method", param.Method,
|
||||
"path", param.Path,
|
||||
@@ -90,6 +92,9 @@ func InitializeGinLogger() gin.HandlerFunc {
|
||||
"statusCode", param.StatusCode,
|
||||
"latency", param.Latency,
|
||||
"responseSize", param.BodySize,
|
||||
"source", source,
|
||||
"user_id", c.GetInt("user_id"),
|
||||
"username", c.GetString("username"),
|
||||
"error", param.ErrorMessage)
|
||||
}
|
||||
c.Next()
|
||||
@@ -112,7 +117,6 @@ func setRoutes(r *gin.Engine) {
|
||||
// unauthenticated file download based on file UUID
|
||||
// this is for payload hosting and payload containers to fetch files via web
|
||||
r.GET("/direct/download/:file_uuid", webcontroller.FileDirectDownloadWebhook)
|
||||
r.GET("/direct/view/:file_uuid", webcontroller.FileDirectViewWebhook)
|
||||
// unauthenticated file upload based on file UUID
|
||||
// this is for payload containers to upload files that are too big for rabbitmq
|
||||
r.POST("/direct/upload/:file_uuid", webcontroller.FileDirectUploadWebhook)
|
||||
@@ -125,7 +129,8 @@ func setRoutes(r *gin.Engine) {
|
||||
// allow access to getting images for agent icons, still blocked by IP range
|
||||
allowAuthenticatedCookies.Use(authentication.CookieAuthMiddleware())
|
||||
{
|
||||
r.Static("/static", "./static")
|
||||
allowAuthenticatedCookies.Static("/static", "./static")
|
||||
allowAuthenticatedCookies.GET("direct/view/:file_uuid", webcontroller.FileDirectViewWebhook)
|
||||
allOperationMembersWithCookies := allowAuthenticatedCookies.Group("/api/v1.4/")
|
||||
allOperationMembersWithCookies.Use(authentication.RBACMiddlewareAll())
|
||||
{
|
||||
@@ -162,6 +167,7 @@ func setRoutes(r *gin.Engine) {
|
||||
// payload
|
||||
allOperationMembers.POST("config_check_webhook", webcontroller.C2ProfileConfigCheckWebhook)
|
||||
allOperationMembers.POST("export_payload_config_webhook", webcontroller.ExportPayloadConfigWebhook)
|
||||
allOperationMembers.POST("export_callback_config_webhook", webcontroller.ExportCallbackConfigWebhook)
|
||||
allOperationMembers.POST("redirect_rules_webhook", webcontroller.C2ProfileRedirectRulesWebhook)
|
||||
allOperationMembers.POST("get_ioc_webhook", webcontroller.C2ProfileGetIOCWebhook)
|
||||
allOperationMembers.POST("sample_message_webhook", webcontroller.C2ProfileSampleMessageWebhook)
|
||||
@@ -174,7 +180,6 @@ func setRoutes(r *gin.Engine) {
|
||||
// global config
|
||||
allOperationMembers.POST("get_global_settings_webhook", webcontroller.GetGlobalSettingWebhook)
|
||||
}
|
||||
|
||||
noSpectators := protected.Group("/api/v1.4/")
|
||||
noSpectators.Use(authentication.RBACMiddlewareNoSpectators())
|
||||
{
|
||||
@@ -209,6 +214,7 @@ func setRoutes(r *gin.Engine) {
|
||||
// callback
|
||||
noSpectators.POST("toggle_proxy_webhook", webcontroller.ProxyToggleWebhook)
|
||||
noSpectators.POST("update_callback_webhook", webcontroller.UpdateCallbackWebhook)
|
||||
noSpectators.POST("callback_import_config_webhook", webcontroller.ImportCallbackConfigWebhook)
|
||||
// reporting
|
||||
noSpectators.POST("reporting_webhook", webcontroller.ReportingWebhook)
|
||||
// tagtypes
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
FROM ghcr.io/its-a-feature/mythic_react:v0.0.3.8
|
||||
FROM nginx:1.25-alpine
|
||||
COPY ["config/", "/etc/nginx"]
|
||||
COPY ["mythic/public/", "/mythic/new"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=60s --retries=5 --start-period=20s \
|
||||
CMD wget -SqO - http://127.0.0.1:${MYTHIC_REACT_PORT:-3000}/new || exit 1
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/new/static/css/main.00d51b79.css",
|
||||
"main.js": "/new/static/js/main.2da5cab2.js",
|
||||
"main.js": "/new/static/js/main.bdecbcc2.js",
|
||||
"static/media/mythic@2x.png": "/new/static/media/mythic@2x.7c5b62b471ac779fd706.png",
|
||||
"static/media/mythic_red_small.svg": "/new/static/media/mythic_red_small.793b41cc7135cdede246661ec232976b.svg",
|
||||
"index.html": "/new/index.html",
|
||||
"main.00d51b79.css.map": "/new/static/css/main.00d51b79.css.map",
|
||||
"main.2da5cab2.js.map": "/new/static/js/main.2da5cab2.js.map"
|
||||
"main.bdecbcc2.js.map": "/new/static/js/main.bdecbcc2.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.00d51b79.css",
|
||||
"static/js/main.2da5cab2.js"
|
||||
"static/js/main.bdecbcc2.js"
|
||||
]
|
||||
}
|
||||
@@ -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.2da5cab2.js"></script><link href="/new/static/css/main.00d51b79.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.bdecbcc2.js"></script><link href="/new/static/css/main.00d51b79.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user