bug fixes and new features for mythic-cli, mythic_server, and the UI
This commit is contained in:
its-a-feature
2024-02-06 15:04:04 -06:00
parent 2aa0c39888
commit af58bc9dbc
90 changed files with 2146 additions and 554128 deletions
+1
View File
@@ -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.
+10
View File
@@ -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
+15
View File
@@ -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 }
</>
)
)
}
+13 -7
View File
@@ -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"
}
}
}});
+10
View File
@@ -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
+21
View File
@@ -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) {
+23
View File
@@ -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 -1
View File
@@ -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"
+226 -31
View File
@@ -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 {
+1 -1
View File
@@ -4,5 +4,5 @@ package config
var (
// Version Mythic CLI version
Version = "v0.2.9"
Version = "v0.2.10"
)
+8 -2
View File
@@ -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, "")
}
+49
View File
@@ -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, "")
}
+1 -1
View File
@@ -17,5 +17,5 @@ func init() {
}
func database(cmd *cobra.Command, args []string) {
cmd.Help()
}
+1 -1
View File
@@ -18,5 +18,5 @@ func init() {
}
func installDisplay(cmd *cobra.Command, args []string) {
cmd.Help()
}
+22 -11
View File
@@ -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")
+44 -4
View File
@@ -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)
+6 -7
View File
@@ -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
+1 -2
View File
@@ -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()
}
+21
View File
@@ -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()
}
+23
View File
@@ -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])
}
+23
View File
@@ -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 -42
View File
@@ -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
View File
@@ -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
)
+30
View File
@@ -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=
+1 -1
View File
@@ -1 +1 @@
3.2.16
3.2.17
+4 -1
View File
@@ -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"]
+23
View File
@@ -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
}
+22
View File
@@ -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: []
+14
View File
@@ -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:
+1 -1
View File
@@ -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
+20 -1
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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" ]
+27 -7
View File
@@ -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
}
+13 -7
View File
@@ -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"`
}
+8 -8
View File
@@ -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
)
+17
View File
@@ -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)
+1 -1
View File
@@ -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
+1
View File
@@ -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
+126 -31
View File
@@ -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) {
+3
View File
@@ -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,
})
}
+13 -7
View File
@@ -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
+6 -1
View File
@@ -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
View File
@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/new/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="apple-touch-icon" href="/new/logo192.png"/><link rel="manifest" href="/new/manifest.json"/><title>Mythic</title><script defer="defer" src="/new/static/js/main.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