mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
starting auth adjustments to remove cookies
This commit is contained in:
@@ -3,6 +3,7 @@ import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import { faLink } from '@fortawesome/free-solid-svg-icons';
|
||||
import {ImageWithAuth} from "../utilities/ImageWithAuth";
|
||||
|
||||
export const MythicAgentSVGIcon = ({payload_type, style, is_p2p}) => {
|
||||
const theme = useTheme();
|
||||
@@ -11,8 +12,8 @@ export const MythicAgentSVGIcon = ({payload_type, style, is_p2p}) => {
|
||||
}
|
||||
return (
|
||||
<MythicStyledTooltip title={payload_type} >
|
||||
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"}
|
||||
style={{...style}} />
|
||||
<ImageWithAuth src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"}
|
||||
style={{...style}} />
|
||||
{is_p2p === false &&
|
||||
<WifiIcon style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
@@ -35,7 +36,8 @@ export const MythicAgentSVGIconNoTooltip = ({payload_type, is_p2p, className, st
|
||||
const extraStyles = style ? style : {};
|
||||
return (
|
||||
<div style={{display: "inline-block", left: "50%"}} className={className}>
|
||||
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"} style={{height: "20px", margin: 'auto', ...extraStyles}}/>
|
||||
<ImageWithAuth src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"}
|
||||
style={{height: "20px", margin: 'auto', ...extraStyles}} />
|
||||
{is_p2p === false &&
|
||||
<WifiIcon style={{width: "10px", height: "10px", marginLeft: "-5px", marginBottom: "0px",
|
||||
borderRadius: "10px", border: "1px solid",
|
||||
|
||||
@@ -3,9 +3,9 @@ import {snackActions} from "../utilities/Snackbar";
|
||||
import {MythicStyledTooltip} from "./MythicStyledTooltip";
|
||||
import {MythicDialog} from "./MythicDialog";
|
||||
import {Link} from '@mui/material';
|
||||
import {useMutation, useLazyQuery, gql} from '@apollo/client';
|
||||
import {gql} from '@apollo/client';
|
||||
import {b64DecodeUnicode} from "../pages/Callbacks/ResponseDisplay";
|
||||
import {PreviewFileMediaDialog} from "../pages/Search/PreviewFileMedia";
|
||||
import {PreviewFileMediaDialog} from "./PreviewFileMedia";
|
||||
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {useMythicLazyQuery} from "../utilities/useMythicLazyQuery";
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import 'ace-builds/src-noconflict/mode-json';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/theme-xcode';
|
||||
import {ResponseDisplayMedia} from "../Callbacks/ResponseDisplayMedia";
|
||||
import {MythicDraggableDialogTitle} from "../../MythicComponents/MythicDraggableDialogTitle";
|
||||
import {ResponseDisplayMedia} from "../pages/Callbacks/ResponseDisplayMedia";
|
||||
import {MythicDraggableDialogTitle} from "./MythicDraggableDialogTitle";
|
||||
|
||||
export function PreviewFileMediaDialog({agent_file_id, filename, onClose}) {
|
||||
const onClick = (e) => {
|
||||
@@ -38,6 +38,7 @@ import {getIconName} from "./ResponseDisplayTable";
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import { MythicAgentSVGIconNoTooltip} from "../../MythicComponents/MythicAgentSVGIcon";
|
||||
import {ImageWithAuth} from "../../utilities/ImageWithAuth";
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
@@ -399,7 +400,7 @@ function AgentNode({data}) {
|
||||
))
|
||||
|
||||
}
|
||||
<img alt={data.img} style={{margin: "auto"}} src={data.img} className={"circleImageNode"} />
|
||||
<ImageWithAuth alt={data.img} style={{margin: "auto"}} src={data.img} className={"circleImageNode"} />
|
||||
<Handle type={"target"} position={targetPosition} isConnectable={false}/>
|
||||
<Typography style={{textAlign: "center", margin: 0, padding: 0}} >{data.label}</Typography>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { toLocalTime } from '../../utilities/Time';
|
||||
import {b64DecodeUnicode} from "./ResponseDisplay";
|
||||
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
||||
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
|
||||
import {PreviewFileMediaDialog} from "../../MythicComponents/PreviewFileMedia";
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {Dropdown, DropdownMenuItem, DropdownNestedMenuItem} from "../../MythicComponents/MythicNestedMenus";
|
||||
import {RenderSingleTask} from "../SingleTaskView/SingleTaskView";
|
||||
|
||||
@@ -10,7 +10,7 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {Button, Link, Typography} from '@mui/material';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
|
||||
import {PreviewFileMediaDialog} from "../../MythicComponents/PreviewFileMedia";
|
||||
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ import {Backdrop, CircularProgress} from '@mui/material';
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax
|
||||
import sqlWasm from "!!file-loader?name=sql-wasm-[contenthash].wasm!sql.js/dist/sql-wasm.wasm";
|
||||
import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import {FileDownloadButtonWithAuth} from "../../utilities/FileDownloadWithAuth";
|
||||
import {ImageWithAuth} from "../../utilities/ImageWithAuth";
|
||||
|
||||
export const modeOptions = ["csharp", "golang", "html", "json", "markdown", "ruby", "python", "java",
|
||||
"javascript", "yaml", "toml", "swift", "sql", "rust", "powershell", "pgsql", "perl", "php", "objectivec",
|
||||
@@ -149,10 +151,10 @@ export const ResponseDisplayMedia = ({media, expand, task}) =>{
|
||||
<Tab className={value === 2 ? "selectedCallback": ""} label={"Hex"}></Tab>
|
||||
<Tab className={value === 3 ? "selectedCallback": ""} label={"Database"}></Tab>
|
||||
<MythicStyledTooltip title={"Download the file"} tooltipStyle={{display: "inline-flex"}}>
|
||||
<Button style={{}} size={"small"} href={"/direct/download/" + media.agent_file_id}
|
||||
<FileDownloadButtonWithAuth style={{}} size={"small"} href={"/direct/download/" + media.agent_file_id}
|
||||
download color={"success"}>
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
</FileDownloadButtonWithAuth>
|
||||
</MythicStyledTooltip>
|
||||
</Tabs>
|
||||
</AppBar>
|
||||
@@ -343,9 +345,9 @@ export const DisplayMedia = ({agent_file_id, filename, expand, task, fileMetaDat
|
||||
if(fileData.display_type === "image"){
|
||||
return (
|
||||
<>
|
||||
<img width={"100%"} onClick={clickedScreenshot}
|
||||
<ImageWithAuth width={"100%"} onClick={clickedScreenshot}
|
||||
src={"/direct/view/" + agent_file_id} style={{cursor: "zoom-in", width: "100%"}}>
|
||||
</img>
|
||||
</ImageWithAuth>
|
||||
{openScreenshot &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
|
||||
onClose={() => {
|
||||
@@ -455,7 +457,11 @@ const DisplayText = ({agent_file_id, expand, filename, preview, fileMetaData}) =
|
||||
previewFileString({variables: {file_id: agent_file_id}})
|
||||
}else{
|
||||
// get entire file
|
||||
fetch('/direct/view/' + agent_file_id).then((response) => {
|
||||
fetch('/direct/view/' + agent_file_id, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
}
|
||||
}).then((response) => {
|
||||
if(response.status !== 200){
|
||||
snackActions.warning("Failed to fetch contents from Mythic");
|
||||
return;
|
||||
@@ -762,7 +768,11 @@ const DisplayDatabase = ({agent_file_id, expand, fileMetaData}) => {
|
||||
React.useEffect( () => {
|
||||
async function initialize(){
|
||||
const newSQL = await initSQLJS({locateFile: () => sqlWasm});
|
||||
fetch('/direct/view/' + agent_file_id).then((response) => {
|
||||
fetch('/direct/view/' + agent_file_id, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
}).then((response) => {
|
||||
if(response.status !== 200){
|
||||
setLoading(false);
|
||||
snackActions.warning("Failed to fetch contents from Mythic");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MobileStepper } from '@mui/material';
|
||||
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight';
|
||||
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft';
|
||||
import {useTheme} from '@mui/material/styles';
|
||||
import {ImageWithAuth} from "../../utilities/ImageWithAuth";
|
||||
|
||||
export function ResponseDisplayScreenshotModal({onClose, images, startIndex}) {
|
||||
const [zoom, setZoom] = React.useState(false);
|
||||
@@ -25,7 +26,7 @@ export function ResponseDisplayScreenshotModal({onClose, images, startIndex}) {
|
||||
< >
|
||||
<DialogContent dividers={true} style={{padding: 0}} >
|
||||
<div style={{height: "calc(80vh)", display: zoom ? "" : "flex", alignItems: zoom ? "" : "flex-start" , background: theme.palette.mode === "dark" ? "rgb(44, 52, 60)" : "grey"}}>
|
||||
<img onClick={toggleZoom} src={"/api/v1.4/files/screencaptures/" + images[activeStep]}
|
||||
<ImageWithAuth onClick={toggleZoom} src={"/api/v1.4/files/screencaptures/" + images[activeStep]}
|
||||
style={{width: zoom ? "" : "100%", cursor: zoom ? "zoom-out" : "zoom-in", overflow: "auto"}} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {b64DecodeUnicode} from "../Callbacks/ResponseDisplay";
|
||||
import { updateFileDeleted} from "../Search/FileMetaTable";
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
|
||||
import {PreviewFileMediaDialog} from "../../MythicComponents/PreviewFileMedia";
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -43,7 +43,7 @@ import FormatListBulletedAddIcon from '@mui/icons-material/FormatListBulletedAdd
|
||||
import AddchartIcon from '@mui/icons-material/Addchart';
|
||||
import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
|
||||
import {MythicSelectFromListDialog} from "../../MythicComponents/MythicSelectFromListDialog";
|
||||
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
|
||||
import {PreviewFileMediaDialog} from "../../MythicComponents/PreviewFileMedia";
|
||||
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {Link} from '@mui/material';
|
||||
@@ -57,6 +57,7 @@ import {copyStringToClipboard} from "../../utilities/Clipboard";
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {faCopy} from '@fortawesome/free-solid-svg-icons';
|
||||
import MythicTextField from "../../MythicComponents/MythicTextField";
|
||||
import {ImageWithAuth} from "../../utilities/ImageWithAuth";
|
||||
import {MythicPageHeader} from "../../MythicComponents/MythicPageHeader";
|
||||
|
||||
const LeadDashboardQuery = gql`
|
||||
@@ -1534,9 +1535,10 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen
|
||||
</Button>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<img onClick={(e) => onPreviewMedia(e, activeStep)}
|
||||
src={"/api/v1.4/files/screencaptures/" + files[activeStep] + "?" + now}
|
||||
style={{height: "200px", cursor: "pointer"}}/>
|
||||
<ImageWithAuth src={"/api/v1.4/files/screencaptures/" + files[activeStep] + "?" + now}
|
||||
style={{height: "200px", cursor: "pointer"}}
|
||||
onClick={(e) => onPreviewMedia(e, activeStep)}
|
||||
/>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell style={{borderBottom: 0}} >
|
||||
<Button
|
||||
|
||||
@@ -30,9 +30,11 @@ import Checkbox from '@mui/material/Checkbox';
|
||||
import {HostFileDialog} from "../Payloads/HostFileDialog";
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import {getStringSize} from '../Callbacks/ResponseDisplayTable';
|
||||
import {PreviewFileMediaDialog} from "./PreviewFileMedia";
|
||||
import {PreviewFileMediaDialog} from "../../MythicComponents/PreviewFileMedia";
|
||||
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {ImageWithAuth} from "../../utilities/ImageWithAuth";
|
||||
import {FileDownloadLinkWithAuth} from "../../utilities/FileDownloadWithAuth";
|
||||
|
||||
export const downloadBulkQuery = gql`
|
||||
mutation downloadBulkMutation($files: [String!]!){
|
||||
@@ -347,7 +349,9 @@ function FileMetaDownloadTableRow(props){
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow hover onClick={expandRow}>
|
||||
<MythicConfirmDialog onClose={() => {setOpenDelete(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
|
||||
{openDelete &&
|
||||
<MythicConfirmDialog onClose={() => {setOpenDelete(false);}} onSubmit={onAcceptDelete} open={openDelete}/>
|
||||
}
|
||||
<MythicStyledTableCell>
|
||||
{props.deleted ? null : (
|
||||
<MythicStyledTooltip title="Toggle to download multiple files at once">
|
||||
@@ -766,7 +770,7 @@ function FileMetaUploadTableRow(props){
|
||||
)}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + props.agent_file_id}>{props.filename_text}</Link>
|
||||
<FileDownloadLinkWithAuth style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + props.agent_file_id}>{props.filename_text}</FileDownloadLinkWithAuth>
|
||||
{props.complete ? null : (
|
||||
<Typography color="secondary" style={{wordBreak: "break-all"}} >{props.chunks_received} / {props.total_chunks} Chunks Received</Typography>
|
||||
)
|
||||
@@ -1052,7 +1056,8 @@ function FileMetaScreenshotTableRow(props){
|
||||
)}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell >
|
||||
<img onClick={() => setOpenScreenshot(true)} src={"/api/v1.4/files/screencaptures/" + props.agent_file_id + "?" + now} style={{width: "270px", cursor: "pointer"}} />
|
||||
<ImageWithAuth src={"/api/v1.4/files/screencaptures/" + props.agent_file_id}
|
||||
style={{width: "270px", cursor: "pointer"}} />
|
||||
{openScreenshot &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
|
||||
onClose={()=>{setOpenScreenshot(false);}}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import {Button, Link} from '@mui/material';
|
||||
import {snackActions} from "./Snackbar";
|
||||
|
||||
export const handleAuthLink = async (event, href) => {
|
||||
event.preventDefault(); // Prevent default anchor link navigation
|
||||
try {
|
||||
const response = await fetch(href, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
const fileURL = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = fileURL;
|
||||
const filename = response.headers.get("content-disposition");
|
||||
console.log(filename, filename.split('filename=')[1]);
|
||||
link.download = filename.split('filename=')[1].replaceAll('"', "");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(fileURL);
|
||||
} else {
|
||||
console.error('Authorization failed');
|
||||
snackActions.error("Failed to download file");
|
||||
console.error(response);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export function FileDownloadLinkWithAuth({href, children, ...props}){
|
||||
|
||||
return (
|
||||
<Link href={href} onClick={(e) => handleAuthLink(e, href)} {...props}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileDownloadButtonWithAuth({href, children, ...props}){
|
||||
|
||||
return (
|
||||
<Button href={href} onClick={(e) => handleAuthLink(e, href)} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
export const ImageWithAuth = ({ src, ...props }) => {
|
||||
const [imageSrc, setImageSrc] = React.useState('');
|
||||
const headers = {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
|
||||
MythicSource: "web"
|
||||
}
|
||||
React.useEffect(() => {
|
||||
const fetchImage = async () => {
|
||||
try {
|
||||
const response = await fetch(src, { headers });
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
setImageSrc(objectUrl);
|
||||
} catch (error) {
|
||||
console.error('Error fetching image:', error);
|
||||
// Handle error (e.g., set a fallback image)
|
||||
}
|
||||
};
|
||||
fetchImage();
|
||||
// Cleanup: revoke the object URL when the component unmounts or src changes
|
||||
return () => {
|
||||
if (imageSrc) {
|
||||
URL.revokeObjectURL(imageSrc);
|
||||
}
|
||||
};
|
||||
}, [src]);
|
||||
|
||||
return <img src={imageSrc} {...props} />;
|
||||
};
|
||||
@@ -20,7 +20,6 @@ var (
|
||||
ErrBearerInvalidValue = errors.New("Authorization bearer value is invalid")
|
||||
ErrMissingAuthorizationBearerToken = errors.New("Missing Authorization Bearer token")
|
||||
ErrMissingAuthorizationHeader = errors.New("Missing Authorization header")
|
||||
ErrMissingCookieValue = errors.New("Missing cookie value")
|
||||
ErrMissingJWTToken = errors.New("Missing JWT header")
|
||||
)
|
||||
|
||||
@@ -245,42 +244,3 @@ func ExtractAPIToken(c *gin.Context) (string, error) {
|
||||
//logging.LogTrace("got apitoken header", "apitoken", token)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func CookieTokenValid(c *gin.Context) error {
|
||||
tokenString, err := ExtractCookieToken(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
claims := mythicjwt.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"])
|
||||
}
|
||||
return utils.MythicConfig.JWTSecret, nil
|
||||
})
|
||||
if err != nil {
|
||||
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))
|
||||
c.Request.Header.Add("MythicSource", "cookie")
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExtractCookieToken(c *gin.Context) (string, error) {
|
||||
token, err := c.Cookie("mythic")
|
||||
if err != nil {
|
||||
return "", ErrMissingCookieValue
|
||||
}
|
||||
if len(token) > 0 {
|
||||
return token, nil
|
||||
}
|
||||
logging.LogDebug("Failed to find cookie value")
|
||||
return "", ErrMissingCookieValue
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ package authentication
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/its-a-feature/Mythic/rabbitmq"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/its-a-feature/Mythic/rabbitmq"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
@@ -25,19 +26,6 @@ func JwtAuthMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func CookieAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
err := CookieTokenValid(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Request.Header.Add("MythicSource", "cookie")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func IPBlockMiddleware() gin.HandlerFunc {
|
||||
// verify that xforward-for address is in range from utils.Config.AllowedIPBlocks
|
||||
return func(c *gin.Context) {
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
"github.com/its-a-feature/Mythic/utils"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
"github.com/its-a-feature/Mythic/utils"
|
||||
)
|
||||
|
||||
type CustomClaims struct {
|
||||
@@ -37,10 +38,10 @@ var (
|
||||
ErrUnexpectedSigningMethod = errors.New("Unexpected signing method")
|
||||
)
|
||||
|
||||
func generateRandomPassword(pw_length int) (string, error) {
|
||||
func generateRandomPassword(passwordLength int) (string, error) {
|
||||
chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
|
||||
var b strings.Builder
|
||||
for i := 0; i < pw_length; i++ {
|
||||
for i := 0; i < passwordLength; i++ {
|
||||
nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
|
||||
if err != nil {
|
||||
logging.LogError(err, "[-] Failed to generate random number for password generation\n")
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/its-a-feature/Mythic/utils"
|
||||
)
|
||||
|
||||
var containerAuthMap = make(map[string]string)
|
||||
var containerAuthMapLock sync.RWMutex
|
||||
|
||||
func NewContainerAuth(containerName string) string {
|
||||
containerAuthMapLock.Lock()
|
||||
defer containerAuthMapLock.Unlock()
|
||||
if _, ok := containerAuthMap[containerName]; ok {
|
||||
return containerAuthMap[containerName]
|
||||
}
|
||||
containerAuthMap[containerName] = utils.GenerateRandomPassword(36)
|
||||
return containerAuthMap[containerName]
|
||||
}
|
||||
|
||||
func GetContainerFromAuth(password string) (string, error) {
|
||||
containerAuthMapLock.RLock()
|
||||
defer containerAuthMapLock.RUnlock()
|
||||
for key, value := range containerAuthMap {
|
||||
if value == password {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("container not found")
|
||||
}
|
||||
@@ -306,13 +306,22 @@ func listenForAsyncAgentMessagePostResponseContent() {
|
||||
UpdateCachedResponseIntercept()
|
||||
for {
|
||||
msg := <-asyncAgentMessagePostResponseChannel
|
||||
// force chunking user_output can be useful, but might also affect command's browserscripts
|
||||
//chunkSize := 1024 * 1024
|
||||
//chunks := int(len(msg.Response)/chunkSize) + 1 // 1MB chunks
|
||||
//for j := 0; j < chunks; j++ {
|
||||
// currentChunkStart := j * chunkSize
|
||||
// currentChunkEnd := currentChunkStart + chunkSize
|
||||
// if currentChunkEnd > len(msg.Response) {
|
||||
// currentChunkEnd = len(msg.Response)
|
||||
// }
|
||||
// chunkBuf := msg.Response[currentChunkStart:currentChunkEnd]
|
||||
responseInterceptMapLock.RLock()
|
||||
if eventGroupID, ok := responseInterceptMapOperationIDToEventGroupID[msg.Task.OperationID]; ok {
|
||||
output := ""
|
||||
responseID := handleAgentMessagePostResponseUserOutput(msg.Task, agentMessagePostResponse{
|
||||
TaskID: msg.Task.AgentTaskID,
|
||||
UserOutput: &output,
|
||||
SequenceNumber: msg.SequenceNum,
|
||||
TaskID: msg.Task.AgentTaskID,
|
||||
UserOutput: &output,
|
||||
}, false)
|
||||
if responseID > 0 {
|
||||
// we have an interception possibility, so send that off for processing
|
||||
@@ -327,12 +336,12 @@ func listenForAsyncAgentMessagePostResponseContent() {
|
||||
}
|
||||
} else {
|
||||
handleAgentMessagePostResponseUserOutput(msg.Task, agentMessagePostResponse{
|
||||
TaskID: msg.Task.AgentTaskID,
|
||||
UserOutput: &msg.Response,
|
||||
SequenceNumber: msg.SequenceNum,
|
||||
TaskID: msg.Task.AgentTaskID,
|
||||
UserOutput: &msg.Response,
|
||||
}, true)
|
||||
}
|
||||
responseInterceptMapLock.RUnlock()
|
||||
//}
|
||||
}
|
||||
}
|
||||
func handleAgentMessagePostResponse(incoming *map[string]interface{}, uUIDInfo *cachedUUIDInfo) (map[string]interface{}, error) {
|
||||
@@ -473,9 +482,8 @@ func handleAgentMessagePostResponse(incoming *map[string]interface{}, uUIDInfo *
|
||||
// do it in the background - the agent doesn't need the result of this directly
|
||||
//handleAgentMessagePostResponseUserOutput(currentTask, agentResponse, true)
|
||||
asyncAgentMessagePostResponseChannel <- agentAgentMessagePostResponseChannelMessage{
|
||||
Task: currentTask,
|
||||
Response: *agentMessage.Responses[i].UserOutput,
|
||||
SequenceNum: agentMessage.Responses[i].SequenceNumber,
|
||||
Task: currentTask,
|
||||
Response: *agentMessage.Responses[i].UserOutput,
|
||||
}
|
||||
}
|
||||
if agentMessage.Responses[i].Stdout != nil {
|
||||
|
||||
@@ -24,7 +24,7 @@ func FileDirectDownloadWebhook(c *gin.Context) {
|
||||
filemeta := databaseStructs.Filemeta{}
|
||||
payload := databaseStructs.Payload{}
|
||||
operatorUsername := "unknown"
|
||||
userID, err := GetUserIDFromGinAllowCookies(c)
|
||||
userID, err := GetUserIDFromGin(c)
|
||||
if err == nil {
|
||||
user, err := database.GetUserFromID(userID)
|
||||
if err == nil {
|
||||
|
||||
@@ -24,7 +24,7 @@ func FileDirectViewWebhook(c *gin.Context) {
|
||||
filemeta := databaseStructs.Filemeta{}
|
||||
payload := databaseStructs.Payload{}
|
||||
operatorUsername := "unknown"
|
||||
userID, err := GetUserIDFromGinAllowCookies(c)
|
||||
userID, err := GetUserIDFromGin(c)
|
||||
if err == nil {
|
||||
user, err := database.GetUserFromID(userID)
|
||||
if err == nil {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/its-a-feature/Mythic/authentication"
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
@@ -49,9 +49,6 @@ func Login(c *gin.Context) {
|
||||
"view_utc_time": currentOperation.CurrentOperator.ViewUtcTime,
|
||||
"current_utc_time": time.Now().UTC(),
|
||||
}
|
||||
// setting cookie max age to 2 days
|
||||
c.SetCookie("mythic", accessToken, 60*60*24*2, "/", strings.Split(c.Request.Host, ":")[0], false, true)
|
||||
c.SetSameSite(http.SameSiteStrictMode)
|
||||
c.JSON(http.StatusOK, gin.H{"access_token": accessToken, "refresh_token": refreshToken, "user": user})
|
||||
return
|
||||
|
||||
@@ -132,8 +129,6 @@ func RefreshJWT(c *gin.Context) {
|
||||
// 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, true)
|
||||
c.SetSameSite(http.SameSiteStrictMode)
|
||||
c.JSON(http.StatusOK, gin.H{"access_token": accessToken, "refresh_token": refreshToken, "user": user})
|
||||
return
|
||||
|
||||
|
||||
@@ -22,17 +22,6 @@ func GetUserIDFromGin(c *gin.Context) (int, error) {
|
||||
}
|
||||
return customClaims.UserID, nil
|
||||
}
|
||||
func GetUserIDFromGinAllowCookies(c *gin.Context) (int, error) {
|
||||
customClaims, err := authentication.GetClaims(c)
|
||||
if err != nil {
|
||||
err = authentication.CookieTokenValid(c)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return GetUserIDFromGin(c)
|
||||
}
|
||||
return customClaims.UserID, nil
|
||||
}
|
||||
|
||||
const (
|
||||
tagTypePreview = "FilePreviewed"
|
||||
|
||||
@@ -147,10 +147,10 @@ func setRoutes(r *gin.Engine) {
|
||||
r.POST("/agent_message", webcontroller.AgentMessageWebhook)
|
||||
r.GET("/agent_message", webcontroller.AgentMessageGetWebhook)
|
||||
// healthcheck endpoint
|
||||
r.GET("/health", webcontroller.HealthCheckSimple)
|
||||
r.GET("/healthDetailed", webcontroller.HealthCheckDetailed)
|
||||
r.Use(authentication.IPBlockMiddleware())
|
||||
{
|
||||
r.GET("/health", webcontroller.HealthCheckSimple)
|
||||
r.GET("/healthDetailed", webcontroller.HealthCheckDetailed)
|
||||
// login page
|
||||
r.POST("/auth", webcontroller.Login)
|
||||
r.POST("/invite", webcontroller.UseInviteLink)
|
||||
@@ -169,24 +169,7 @@ func setRoutes(r *gin.Engine) {
|
||||
// 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)
|
||||
|
||||
// create a protected group that allows Cookie values to access things instead of only a JWT header field
|
||||
// this mainly allows the user through the UI to view agent icons and download files
|
||||
allowAuthenticatedCookies := r.Group("/")
|
||||
{
|
||||
// allow access to getting images for agent icons, still blocked by IP range
|
||||
allowAuthenticatedCookies.Use(authentication.CookieAuthMiddleware())
|
||||
{
|
||||
allowAuthenticatedCookies.Static("/static", "./static")
|
||||
allowAuthenticatedCookies.GET("direct/view/:file_uuid", webcontroller.FileDirectViewWebhook)
|
||||
allOperationMembersWithCookies := allowAuthenticatedCookies.Group("/api/v1.4/")
|
||||
allOperationMembersWithCookies.Use(authentication.RBACMiddlewareAll())
|
||||
{
|
||||
allOperationMembersWithCookies.GET("files/download/:file_uuid", webcontroller.DownloadFileAuthWebhook)
|
||||
allOperationMembersWithCookies.GET("files/screencaptures/:file_uuid", webcontroller.DownloadFileAuthWebhook)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create a protected group that requires valid auth with a JWT to access
|
||||
protected := r.Group("/")
|
||||
protected.Use(authentication.JwtAuthMiddleware())
|
||||
@@ -213,6 +196,8 @@ func setRoutes(r *gin.Engine) {
|
||||
protected.POST("/api/v1.4/get_global_settings_webhook", webcontroller.GetGlobalSettingWebhook)
|
||||
// a refresh post will contain the access_token and refresh_token
|
||||
protected.POST("/refresh", webcontroller.RefreshJWT)
|
||||
protected.Static("/static", "./static")
|
||||
protected.GET("direct/view/:file_uuid", webcontroller.FileDirectViewWebhook)
|
||||
// following require you to have an operation set
|
||||
allOperationMembers := protected.Group("/api/v1.4/")
|
||||
allOperationMembers.Use(authentication.RBACMiddlewareAll())
|
||||
@@ -231,6 +216,7 @@ func setRoutes(r *gin.Engine) {
|
||||
// file
|
||||
allOperationMembers.POST("download_bulk_webhook", webcontroller.DownloadBulkFilesWebhook)
|
||||
allOperationMembers.POST("preview_file_webhook", webcontroller.PreviewFileWebhook)
|
||||
allOperationMembers.GET("files/screencaptures/:file_uuid", webcontroller.DownloadFileAuthWebhook)
|
||||
}
|
||||
noSpectators := protected.Group("/api/v1.4/")
|
||||
noSpectators.Use(authentication.RBACMiddlewareNoSpectators())
|
||||
|
||||
Reference in New Issue
Block a user