mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
updating apitokens to be different, removing /api/v1.4 prefix, scopes
This commit is contained in:
@@ -167,3 +167,5 @@ _ReSharper*/
|
||||
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
|
||||
|
||||
nginx-docker/config/blockips.conf
|
||||
|
||||
.gocache
|
||||
|
||||
@@ -26,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"}}>
|
||||
<ImageWithAuth onClick={toggleZoom} src={"/api/v1.4/files/screencaptures/" + images[activeStep]}
|
||||
<ImageWithAuth onClick={toggleZoom} src={"/screencaptures/" + images[activeStep]}
|
||||
style={{width: zoom ? "" : "100%", cursor: zoom ? "zoom-out" : "zoom-in", overflow: "auto"}} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1535,7 +1535,7 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen
|
||||
</Button>
|
||||
</MythicTableCell>
|
||||
<MythicTableCell>
|
||||
<ImageWithAuth src={"/api/v1.4/files/screencaptures/" + files[activeStep] + "?" + now}
|
||||
<ImageWithAuth src={"/screencaptures/" + files[activeStep] + "?" + now}
|
||||
style={{height: "200px", cursor: "pointer"}}
|
||||
onClick={(e) => onPreviewMedia(e, activeStep)}
|
||||
/>
|
||||
|
||||
@@ -48,7 +48,7 @@ export const PieChartCard = ({
|
||||
}
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
<Paper variant={"elevation"} elevation={3} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
@@ -167,7 +167,7 @@ export const GaugeCard = ({data, width = "100%", additionalStyles, innerElement,
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
<Paper variant={"elevation"} elevation={3} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
@@ -216,7 +216,7 @@ export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondary
|
||||
editing, removeElement}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
<Paper variant={"elevation"} elevation={3} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
@@ -251,7 +251,7 @@ export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondary
|
||||
export const TableDataCard = ({title, width = "100%", tableHead, tableBody, editing, removeElement, customizeElement}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
<Paper variant={"elevation"} elevation={3} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
@@ -314,7 +314,7 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
<Paper variant={"elevation"} elevation={3} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
@@ -429,7 +429,7 @@ export const LineTimeMultiChartCard = ({data, additionalStyles, colors=normalCol
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
<Paper variant={"elevation"} elevation={3} style={{
|
||||
width: "100%",
|
||||
marginRight: "0.5rem",
|
||||
height: "100%",
|
||||
@@ -517,7 +517,7 @@ export const StackedBarChartCard = ({data, labels, title, width="100%", hidden,
|
||||
}}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Paper variant={"elevation"} elevation={1} style={{
|
||||
<Paper variant={"elevation"} elevation={3} style={{
|
||||
marginRight: "0.5rem",
|
||||
width: width,
|
||||
height: "100%",
|
||||
|
||||
@@ -1056,7 +1056,7 @@ function FileMetaScreenshotTableRow(props){
|
||||
)}
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell >
|
||||
<ImageWithAuth src={"/api/v1.4/files/screencaptures/" + props.agent_file_id}
|
||||
<ImageWithAuth src={"/screencaptures/" + props.agent_file_id}
|
||||
style={{width: "270px", cursor: "pointer"}} />
|
||||
{openScreenshot &&
|
||||
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot}
|
||||
|
||||
@@ -32,7 +32,6 @@ import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
|
||||
|
||||
|
||||
export function SettingsOperatorTable(props){
|
||||
const theme = useTheme();
|
||||
const [openNew, setOpenNewDialog] = React.useState(false);
|
||||
const [openNewBot, setOpenNewBotDialog] = React.useState(false);
|
||||
const [openAPITokenSearch, setOpenAPITokenSearch] = React.useState(false);
|
||||
|
||||
@@ -58,36 +58,6 @@ let httpLink = new HttpLink({
|
||||
});
|
||||
export const isJWTValid = () => {
|
||||
let access_token = localStorage.getItem("access_token");
|
||||
if(!access_token){
|
||||
let cookie = document.cookie;
|
||||
if(cookie && cookie !== ""){
|
||||
let cookies = cookie.split(";");
|
||||
for(let i = 0; i < cookies.length; i++){
|
||||
let cookiePieces = cookies[i].split("=");
|
||||
if(cookiePieces.length !== 2){
|
||||
console.log("bad number of cookie pieces", "cookie", cookies[i])
|
||||
} else if(cookiePieces[0].trim() !== "user") {
|
||||
console.log("unknown cookie", "name", cookiePieces[0].trim());
|
||||
} else {
|
||||
try{
|
||||
console.log("user cookie found, trying to parse");
|
||||
let cookieString = decodeURIComponent(cookiePieces[1].trim());
|
||||
let cookieJSON = JSON.parse(atob(cookieString));
|
||||
if("access_token" in cookieJSON){
|
||||
successfulLogin(cookieJSON);
|
||||
restartWebsockets();
|
||||
access_token = localStorage.getItem("access_token");
|
||||
}else{
|
||||
snackActions.warning("Invalid Authentication");
|
||||
console.log("Error", cookieJSON);
|
||||
}
|
||||
}catch(error){
|
||||
console.log("error processing cookie value", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//console.log("in isJWTValid", "access_token", access_token);
|
||||
if(access_token){
|
||||
const decoded_token = jwtDecode(access_token);
|
||||
|
||||
@@ -220,7 +220,7 @@ func AddMythicService(service string, removeVolume bool) {
|
||||
"HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL=1000",
|
||||
"HASURA_GRAPHQL_AUTH_HOOK=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/graphql/webhook",
|
||||
"HASURA_GRAPHQL_AUTH_HOOK_MODE=POST",
|
||||
"MYTHIC_ACTIONS_URL_BASE=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/api/v1.4",
|
||||
"MYTHIC_ACTIONS_URL_BASE=http://${MYTHIC_SERVER_HOST}:${MYTHIC_SERVER_PORT}/",
|
||||
"HASURA_GRAPHQL_CONSOLE_ASSETS_DIR=/srv/console-assets",
|
||||
}
|
||||
if mythicEnv.GetString("mythic_docker_networking") == "bridge" {
|
||||
|
||||
@@ -3,6 +3,8 @@ package authentication
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
|
||||
@@ -11,20 +13,27 @@ import (
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
"github.com/its-a-feature/Mythic/rabbitmq"
|
||||
"github.com/its-a-feature/Mythic/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnexpectedSigningMethod = errors.New("Unexpected signing method")
|
||||
ErrGetApiTokenOrBearer = errors.New("Failed to get apitoken or bearer token")
|
||||
ErrBearerInvalidValue = errors.New("Authorization bearer value is invalid")
|
||||
ErrMissingAuthorizationBearerToken = errors.New("Missing Authorization Bearer token")
|
||||
ErrMissingAuthorizationHeader = errors.New("Missing Authorization header")
|
||||
ErrMissingJWTToken = errors.New("Missing JWT header")
|
||||
ErrLegacyAPITokenJWTDeprecated = errors.New("Legacy JWT API tokens are no longer accepted. Generate and use a new API token.")
|
||||
)
|
||||
|
||||
const (
|
||||
ContextKeyClaims = "claims"
|
||||
ContextKeyAPIToken = "apitoken_struct"
|
||||
ContextKeyUserID = "user_id"
|
||||
ContextKeyUsername = "username"
|
||||
)
|
||||
|
||||
var (
|
||||
SQLGetIDForActiveToken = `SELECT
|
||||
// LegacyAPITokenJWTCutoff // int64(time.Now().Unix()) for a specific date via iat
|
||||
LegacyAPITokenJWTCutoff = int64(1776211200) // April 15, 2026
|
||||
SQLGetIDForActiveToken = `SELECT
|
||||
apitokens.id, apitokens.operator_id, apitokens.name, apitokens.active, apitokens.token_value, apitokens.deleted,
|
||||
apitokens.eventstepinstance_id, apitokens.token_type,
|
||||
operator.username "operator.username",
|
||||
@@ -32,122 +41,143 @@ var (
|
||||
operator.current_operation_id "operator.current_operation_id"
|
||||
FROM apitokens
|
||||
JOIN operator ON apitokens.operator_id = operator.id
|
||||
WHERE apitokens.token_value=$1`
|
||||
WHERE apitokens.token_value=$1
|
||||
LIMIT 1`
|
||||
)
|
||||
|
||||
func getAPITokenFromDB(c *gin.Context, tokenString string) (databaseStructs.Apitokens, error) {
|
||||
if cachedToken, ok := c.Get(ContextKeyAPIToken); ok {
|
||||
if token, ok := cachedToken.(databaseStructs.Apitokens); ok {
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
databaseApiToken := databaseStructs.Apitokens{}
|
||||
hashedTokenValue := mythicjwt.HashAPITokenValue(tokenString)
|
||||
if err := database.DB.Get(&databaseApiToken, SQLGetIDForActiveToken, hashedTokenValue); err != nil {
|
||||
logging.LogError(err, "Failed to get apitoken from database", "apitoken", tokenString)
|
||||
return databaseApiToken, err
|
||||
}
|
||||
c.Set(ContextKeyAPIToken, databaseApiToken)
|
||||
return databaseApiToken, nil
|
||||
}
|
||||
|
||||
func claimsFromAPIToken(databaseApiToken databaseStructs.Apitokens) *mythicjwt.CustomClaims {
|
||||
claims := mythicjwt.CustomClaims{
|
||||
UserID: databaseApiToken.OperatorID,
|
||||
AuthMethod: databaseApiToken.TokenType,
|
||||
APITokensID: databaseApiToken.ID,
|
||||
OperationID: int(databaseApiToken.Operator.CurrentOperationID.Int64),
|
||||
}
|
||||
if databaseApiToken.EventStepInstanceID.Valid {
|
||||
claims.EventStepInstanceID = int(databaseApiToken.EventStepInstanceID.Int64)
|
||||
}
|
||||
return &claims
|
||||
}
|
||||
|
||||
func looksLikeOpaqueAPIToken(tokenString string) bool {
|
||||
return strings.HasPrefix(tokenString, mythicjwt.APITokenValuePrefix)
|
||||
}
|
||||
|
||||
func validateAndSetAPITokenContext(c *gin.Context, tokenString string) error {
|
||||
databaseApiToken, err := getAPITokenFromDB(c, tokenString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !databaseApiToken.Active {
|
||||
go func(token databaseStructs.Apitokens) {
|
||||
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deactivated APIToken, %s, for user %s (%s) attempted to be used",
|
||||
token.Name, token.Operator.Username, token.Operator.AccountType),
|
||||
int(token.Operator.CurrentOperationID.Int64), token.Name+fmt.Sprintf("%d", token.ID)+token.Operator.Username,
|
||||
database.MESSAGE_LEVEL_API, true)
|
||||
}(databaseApiToken)
|
||||
return errors.New("Deactivated APIToken attempted to be used")
|
||||
}
|
||||
if databaseApiToken.Deleted {
|
||||
go func(token databaseStructs.Apitokens) {
|
||||
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deleted APIToken, \"%s\", for user %s (%s) attempted to be used",
|
||||
token.Name, token.Operator.Username, token.Operator.AccountType),
|
||||
int(token.Operator.CurrentOperationID.Int64), token.Name+fmt.Sprintf("%d", token.ID)+token.Operator.Username,
|
||||
database.MESSAGE_LEVEL_API, true)
|
||||
}(databaseApiToken)
|
||||
return errors.New("Deleted APIToken attempted to be used")
|
||||
}
|
||||
source := c.GetHeader("MythicSource")
|
||||
if source == "web" {
|
||||
logging.LogError(errors.New("web used apitoken"), "API Token used from the web, this is wrong")
|
||||
return errors.New("web shouldn't use apitokens")
|
||||
}
|
||||
if databaseApiToken.EventStepInstanceID.Valid {
|
||||
c.Request.Header.Set("MythicSource", "eventing")
|
||||
c.Set("eventstepinstance_id", int(databaseApiToken.EventStepInstanceID.Int64))
|
||||
} else {
|
||||
c.Request.Header.Set("MythicSource", "apitoken")
|
||||
}
|
||||
c.Set(ContextKeyUserID, databaseApiToken.OperatorID)
|
||||
c.Set(ContextKeyUsername, databaseApiToken.Operator.Username)
|
||||
c.Set(ContextKeyAPIToken, databaseApiToken)
|
||||
c.Set("account", databaseApiToken.Operator.AccountType)
|
||||
c.Set(ContextKeyClaims, claimsFromAPIToken(databaseApiToken))
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetClaims(c *gin.Context) (*mythicjwt.CustomClaims, error) {
|
||||
// just get the claims out of the JWT used for the request
|
||||
tokenString, err := ExtractToken(c)
|
||||
if err != nil {
|
||||
if len(tokenString) == 0 {
|
||||
tokenString, err = ExtractAPIToken(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tokenString) == 0 {
|
||||
return nil, ErrGetApiTokenOrBearer
|
||||
}
|
||||
if existingClaims, exists := c.Get(ContextKeyClaims); exists {
|
||||
if typedClaims, ok := existingClaims.(*mythicjwt.CustomClaims); ok {
|
||||
return typedClaims, nil
|
||||
}
|
||||
}
|
||||
|
||||
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &mythicjwt.CustomClaims{})
|
||||
tokenString, err := ExtractToken(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*mythicjwt.CustomClaims); ok {
|
||||
c.Set("user_id", claims.UserID)
|
||||
return claims, nil
|
||||
if looksLikeOpaqueAPIToken(tokenString) {
|
||||
apiTokenErr := validateAndSetAPITokenContext(c, tokenString)
|
||||
if apiTokenErr != nil {
|
||||
logging.LogError(err, "failed to parse jwt")
|
||||
return nil, apiTokenErr
|
||||
}
|
||||
if existingClaims, exists := c.Get(ContextKeyClaims); exists {
|
||||
if typedClaims, ok := existingClaims.(*mythicjwt.CustomClaims); ok {
|
||||
return typedClaims, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("failed to get claims from validated apitoken")
|
||||
}
|
||||
|
||||
return nil, 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")
|
||||
return nil, err
|
||||
}
|
||||
if claims.IssuedAt < LegacyAPITokenJWTCutoff {
|
||||
logging.LogError(ErrLegacyAPITokenJWTDeprecated,
|
||||
"auth_method", claims.AuthMethod,
|
||||
"issued_at", claims.IssuedAt,
|
||||
"cutoff", LegacyAPITokenJWTCutoff)
|
||||
return nil, ErrLegacyAPITokenJWTDeprecated
|
||||
}
|
||||
c.Set(ContextKeyUserID, claims.UserID)
|
||||
c.Set(ContextKeyClaims, &claims)
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
func TokenValid(c *gin.Context) error {
|
||||
tokenString, err := ExtractToken(c)
|
||||
claims, err := GetClaims(c)
|
||||
if err != nil {
|
||||
tokenString, err = ExtractAPIToken(c)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to extract apitoken")
|
||||
return err
|
||||
}
|
||||
// we have an apitoken to process
|
||||
databaseApiToken := databaseStructs.Apitokens{}
|
||||
if err = database.DB.Get(&databaseApiToken, SQLGetIDForActiveToken, tokenString); err != nil {
|
||||
logging.LogError(err, "Failed to get apitoken from database", "apitoken", tokenString)
|
||||
return err
|
||||
}
|
||||
if !databaseApiToken.Active {
|
||||
go func(token databaseStructs.Apitokens) {
|
||||
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deactivated APIToken, %s, for user %s (%s) attempted to be used",
|
||||
token.Name, token.Operator.Username, token.Operator.AccountType),
|
||||
int(token.Operator.CurrentOperationID.Int64), "apitoken usage",
|
||||
database.MESSAGE_LEVEL_API, true)
|
||||
}(databaseApiToken)
|
||||
return errors.New("Deactivated APIToken attempted to be used")
|
||||
}
|
||||
if databaseApiToken.Deleted {
|
||||
go func(token databaseStructs.Apitokens) {
|
||||
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deleted APIToken, \"%s\", for user %s (%s) attempted to be used",
|
||||
token.Name, token.Operator.Username, token.Operator.AccountType),
|
||||
int(token.Operator.CurrentOperationID.Int64), token.Name+fmt.Sprintf("%d", token.ID)+token.Operator.Username,
|
||||
database.MESSAGE_LEVEL_API, true)
|
||||
}(databaseApiToken)
|
||||
return errors.New("Deleted APIToken attempted to be used")
|
||||
}
|
||||
source := c.GetHeader("MythicSource")
|
||||
if source == "web" {
|
||||
logging.LogError(errors.New("web used apitoken"), "API Token used from the web, this is wrong")
|
||||
return errors.New("web shouldn't use apitokens")
|
||||
}
|
||||
if databaseApiToken.EventStepInstanceID.Valid {
|
||||
c.Request.Header.Add("MythicSource", "eventing")
|
||||
c.Set("eventstepinstance_id", int(databaseApiToken.EventStepInstanceID.Int64))
|
||||
} else {
|
||||
c.Request.Header.Add("MythicSource", "apitoken")
|
||||
}
|
||||
c.Set("apitoken_logging_struct", databaseApiToken)
|
||||
c.Set("user_id", databaseApiToken.OperatorID)
|
||||
c.Set("username", databaseApiToken.Operator.Username)
|
||||
c.Set("apitoken", databaseApiToken.Name)
|
||||
c.Set("account", databaseApiToken.Operator.AccountType)
|
||||
/*
|
||||
if hasura, ok := c.Get("hasura"); ok {
|
||||
logging.LogInfo("got hasura info", "hasura", hasura)
|
||||
} else {
|
||||
logging.LogInfo("no hasura info yet")
|
||||
}
|
||||
go func(token databaseStructs.Apitokens) {
|
||||
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("APIToken, %s, for user %s (%s) just used",
|
||||
token.Name, token.Operator.Username, token.Operator.AccountType),
|
||||
int(token.Operator.CurrentOperationID.Int64), token.TokenValue,
|
||||
database.MESSAGE_LEVEL_DEBUG)
|
||||
}(databaseApiToken)
|
||||
|
||||
*/
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
claims := mythicjwt.CustomClaims{}
|
||||
if len(tokenString) > 0 {
|
||||
_, 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
|
||||
}
|
||||
operator, err := database.GetUserFromID(claims.UserID)
|
||||
if err == nil {
|
||||
c.Set("username", operator.Username)
|
||||
}
|
||||
c.Set("user_id", claims.UserID)
|
||||
return nil
|
||||
operator, err := database.GetUserFromID(claims.UserID)
|
||||
if err == nil {
|
||||
c.Set(ContextKeyUsername, operator.Username)
|
||||
}
|
||||
return errors.New("failed to get token")
|
||||
return nil
|
||||
}
|
||||
|
||||
type HasuraRequest struct {
|
||||
@@ -162,24 +192,12 @@ type HasuraRequestGraphQL struct {
|
||||
|
||||
func ExtractToken(c *gin.Context) (string, error) {
|
||||
token := c.Request.Header.Get("Authorization")
|
||||
//logging.LogDebug("got Authorization header", "Authorization", token)
|
||||
if len(token) == 0 {
|
||||
token = c.Request.Header.Get("apitoken")
|
||||
}
|
||||
if len(token) == 0 {
|
||||
if c.Request.Method == "POST" {
|
||||
if _, ok := c.Get("hasura"); !ok {
|
||||
// don't try to double process, only do this once
|
||||
/*
|
||||
var buf bytes.Buffer
|
||||
tee := io.TeeReader(c.Request.Body, &buf)
|
||||
body, _ := ioutil.ReadAll(tee)
|
||||
c.Request.Body = ioutil.NopCloser(&buf)
|
||||
logging.LogInfo("raw hasura info", "raw body", string(body))
|
||||
|
||||
*/
|
||||
var input HasuraRequest
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
err := c.ShouldBindJSON(&input)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to find hasura request")
|
||||
return "", ErrMissingAuthorizationHeader
|
||||
}
|
||||
@@ -187,7 +205,6 @@ func ExtractToken(c *gin.Context) (string, error) {
|
||||
c.Request.Header.Add(key, value)
|
||||
}
|
||||
c.Set("hasura", input)
|
||||
//logging.LogInfo("hasura info", "hasura", input)
|
||||
}
|
||||
}
|
||||
token = c.Request.Header.Get("Authorization")
|
||||
@@ -195,7 +212,6 @@ func ExtractToken(c *gin.Context) (string, error) {
|
||||
return "", ErrMissingAuthorizationHeader
|
||||
}
|
||||
}
|
||||
|
||||
bearerPieces := strings.Split(token, " ")
|
||||
if len(bearerPieces) != 2 {
|
||||
return "", ErrMissingAuthorizationBearerToken
|
||||
@@ -203,44 +219,5 @@ func ExtractToken(c *gin.Context) (string, error) {
|
||||
if len(bearerPieces[1]) > 10 {
|
||||
return bearerPieces[1], nil
|
||||
}
|
||||
|
||||
return "", ErrBearerInvalidValue
|
||||
}
|
||||
|
||||
func ExtractAPIToken(c *gin.Context) (string, error) {
|
||||
token := c.Request.Header.Get("apitoken")
|
||||
if len(token) == 0 {
|
||||
if c.Request.Method == "POST" {
|
||||
if _, ok := c.Get("hasura"); !ok {
|
||||
// don't try to double process, only do this once
|
||||
/*
|
||||
var buf bytes.Buffer
|
||||
tee := io.TeeReader(c.Request.Body, &buf)
|
||||
body, _ := ioutil.ReadAll(tee)
|
||||
c.Request.Body = ioutil.NopCloser(&buf)
|
||||
logging.LogInfo("raw hasura info", "raw body", string(body))
|
||||
|
||||
*/
|
||||
var input HasuraRequest
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
logging.LogError(err, "Failed to find hasura request")
|
||||
return "", ErrMissingAuthorizationHeader
|
||||
}
|
||||
for key, value := range input.Headers {
|
||||
c.Request.Header.Add(key, value)
|
||||
}
|
||||
c.Set("hasura", input)
|
||||
//logging.LogInfo("hasura info", "hasura", input)
|
||||
}
|
||||
}
|
||||
token = c.Request.Header.Get("apitoken")
|
||||
if len(token) == 0 {
|
||||
if !strings.HasPrefix(c.Request.URL.Path, "/direct/") {
|
||||
logging.LogError(nil, "[-] No 'apitoken` or 'Authorization: Bearer' token values supplied")
|
||||
}
|
||||
return "", ErrMissingJWTToken
|
||||
}
|
||||
}
|
||||
//logging.LogTrace("got apitoken header", "apitoken", token)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ func TestTokenValid(t *testing.T) {
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
user := databaseStructs.Operator{ID: 123}
|
||||
access_token, _, _, _ := mythicjwt.GenerateJWT(user, mythicjwt.AUTH_METHOD_USER, 0, 0)
|
||||
|
||||
@@ -69,6 +67,8 @@ func TestTokenValid(t *testing.T) {
|
||||
tt := tt // shadowing
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
req := &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
@@ -89,8 +89,6 @@ func TestGetClaims(t *testing.T) {
|
||||
}
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
user := databaseStructs.Operator{ID: 123}
|
||||
access_token, _, _, _ := mythicjwt.GenerateJWT(user, mythicjwt.AUTH_METHOD_USER, 0, 0)
|
||||
|
||||
@@ -127,33 +125,6 @@ func TestGetClaims(t *testing.T) {
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Valid apitoken Claim",
|
||||
args: args{
|
||||
header: map[string][]string{
|
||||
"Apitoken": {access_token}, // "Apitoken" and not "apitoken" here because Go uses the canonical form (first letter of the word and after hyphens as uppercase)
|
||||
},
|
||||
},
|
||||
want: &mythicjwt.CustomClaims{
|
||||
UserID: 123,
|
||||
AuthMethod: mythicjwt.AUTH_METHOD_USER,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: time.Now().Add(mythicjwt.JWTTimespan).UTC().Unix(),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty apitoken",
|
||||
args: args{
|
||||
header: map[string][]string{
|
||||
"Apitoken": {""},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "No auth",
|
||||
args: args{
|
||||
@@ -166,7 +137,6 @@ func TestGetClaims(t *testing.T) {
|
||||
name: "Both token bad",
|
||||
args: args{
|
||||
header: map[string][]string{
|
||||
"Apitoken": {"badtoken"},
|
||||
"Authorization": {"Bearer bad"},
|
||||
},
|
||||
},
|
||||
@@ -178,7 +148,8 @@ func TestGetClaims(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
tt := tt // shadowing
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// t.Parallel()
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
req := &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
|
||||
"github.com/its-a-feature/Mythic/rabbitmq"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -44,12 +46,12 @@ func IPBlockMiddleware() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logging.LogError(nil, "Client IP not in allowed IP blocks", "client_ip", ipAddr)
|
||||
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Client IP, %s, not in allowed IP blocks: %v", ipAddr.String(), utils.MythicConfig.AllowedIPBlocks),
|
||||
0, ipAddr.String(), database.MESSAGE_LEVEL_AUTH, true)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,12 +62,14 @@ func RBACMiddleware(allowedRoles []string) gin.HandlerFunc {
|
||||
logging.LogError(err, "Failed to get claims for RBACMiddleware")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
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()
|
||||
return
|
||||
}
|
||||
if operatorOperation.CurrentOperator.Admin || utils.SliceContains(allowedRoles, operatorOperation.ViewMode) {
|
||||
c.Set("operatorOperation", operatorOperation)
|
||||
@@ -76,7 +80,7 @@ func RBACMiddleware(allowedRoles []string) gin.HandlerFunc {
|
||||
logging.LogError(nil, "Unauthorized view mode for operation")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
|
||||
c.Abort()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,3 +104,37 @@ func RBACMiddlewareOperationAdmin() gin.HandlerFunc {
|
||||
database.OPERATOR_OPERATION_VIEW_MODE_LEAD,
|
||||
})
|
||||
}
|
||||
|
||||
func scopeIncludes(scopes []string, required string) bool {
|
||||
return slices.Contains(scopes, required)
|
||||
}
|
||||
|
||||
func DirectFileScopeMiddleware(requiredScope string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
claims, err := GetClaims(c)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get claims for DirectFileScopeMiddleware")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// Backward compatibility: existing normal JWT/APIToken flows without scopes continue to work.
|
||||
if len(claims.Scopes) == 0 {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if !scopeIncludes(claims.Scopes, requiredScope) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"message": "Missing Proper Scope"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if claims.AuthMethod == mythicjwt.AUTH_METHOD_SCOPED &&
|
||||
claims.FileUUID != "" &&
|
||||
claims.FileUUID != c.Param("file_uuid") {
|
||||
c.JSON(http.StatusForbidden, gin.H{"message": "Forbidden"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package mythicjwt
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
APITokenValueHashPrefix = "sha256:"
|
||||
APITokenValuePrefix = "mtk_"
|
||||
)
|
||||
|
||||
func HashAPITokenValue(token string) string {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return APITokenValueHashPrefix + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func GenerateOpaqueAPIToken() (string, string, error) {
|
||||
randomValue, err := generateRandomPassword(64)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
plainValue := fmt.Sprintf("%s%s", APITokenValuePrefix, randomValue)
|
||||
return plainValue, HashAPITokenValue(plainValue), nil
|
||||
}
|
||||
@@ -17,11 +17,13 @@ import (
|
||||
|
||||
type CustomClaims struct {
|
||||
jwt.StandardClaims
|
||||
UserID int `json:"user_id"`
|
||||
AuthMethod string `json:"auth"`
|
||||
EventStepInstanceID int `json:"eventstepinstance_id"`
|
||||
APITokensID int `json:"apitokens_id"`
|
||||
OperationID int `json:"operation_id"`
|
||||
UserID int `json:"user_id"`
|
||||
AuthMethod string `json:"auth"`
|
||||
EventStepInstanceID int `json:"eventstepinstance_id"`
|
||||
APITokensID int `json:"apitokens_id"`
|
||||
OperationID int `json:"operation_id"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
FileUUID string `json:"file_uuid,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -33,6 +35,9 @@ var (
|
||||
AUTH_METHOD_EVENT = "event"
|
||||
AUTH_METHOD_TASK = "task"
|
||||
AUTH_METHOD_GRAPHQL_SPECTATOR = "graphql_spectator"
|
||||
AUTH_METHOD_SCOPED = "scoped"
|
||||
SCOPE_FILE_DIRECT_UPLOAD = "file.direct.upload"
|
||||
SCOPE_FILE_DIRECT_DOWNLOAD = "file.direct.download"
|
||||
ErrFailedToFindRefreshToken = errors.New("Failed to find refresh token for specified access token")
|
||||
ErrRefreshTokenMissmatch = errors.New("Refresh token doesn't match for the given access token")
|
||||
ErrUnexpectedSigningMethod = errors.New("Unexpected signing method")
|
||||
@@ -96,18 +101,20 @@ func RefreshJWT(access_token string, refresh_token string) (string, string, int,
|
||||
}
|
||||
|
||||
func GenerateJWT(user databaseStructs.Operator, authMethod string, eventStepInstanceID int, APITokensID int) (string, string, int, error) {
|
||||
jwtStandardClaims := jwt.StandardClaims{
|
||||
IssuedAt: time.Now().UTC().Unix(),
|
||||
ExpiresAt: time.Now().Add(JWTTimespan).UTC().Unix(),
|
||||
}
|
||||
claims := CustomClaims{
|
||||
jwt.StandardClaims{
|
||||
IssuedAt: time.Now().UTC().Unix(),
|
||||
ExpiresAt: time.Now().Add(JWTTimespan).UTC().Unix(),
|
||||
},
|
||||
jwtStandardClaims,
|
||||
user.ID,
|
||||
authMethod,
|
||||
eventStepInstanceID,
|
||||
APITokensID,
|
||||
int(user.CurrentOperationID.Int64),
|
||||
nil,
|
||||
"",
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
access_token, err := token.SignedString(utils.MythicConfig.JWTSecret)
|
||||
if err != nil {
|
||||
@@ -127,3 +134,27 @@ func GenerateJWT(user databaseStructs.Operator, authMethod string, eventStepInst
|
||||
}
|
||||
return access_token, "", user.ID, nil
|
||||
}
|
||||
|
||||
func GenerateScopedJWT(user databaseStructs.Operator, scopes []string, fileUUID string, ttl time.Duration) (string, int, error) {
|
||||
jwtStandardClaims := jwt.StandardClaims{
|
||||
IssuedAt: time.Now().UTC().Unix(),
|
||||
ExpiresAt: time.Now().Add(ttl).UTC().Unix(),
|
||||
}
|
||||
claims := CustomClaims{
|
||||
jwtStandardClaims,
|
||||
user.ID,
|
||||
AUTH_METHOD_SCOPED,
|
||||
0,
|
||||
0,
|
||||
int(user.CurrentOperationID.Int64),
|
||||
scopes,
|
||||
fileUUID,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
access_token, err := token.SignedString(utils.MythicConfig.JWTSecret)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to generate scoped JWT")
|
||||
return "", 0, err
|
||||
}
|
||||
return access_token, user.ID, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
mythicCrypto "github.com/its-a-feature/Mythic/crypto"
|
||||
@@ -71,6 +72,9 @@ func GetUserCurrentOperation(userID int) (*databaseStructs.Operatoroperation, er
|
||||
}
|
||||
|
||||
func GetUserFromID(userID int) (*databaseStructs.Operator, error) {
|
||||
if DB == nil {
|
||||
return nil, errors.New("database connection not initialized")
|
||||
}
|
||||
operator := databaseStructs.Operator{}
|
||||
err := DB.Get(&operator, `SELECT
|
||||
username, id, "admin", last_login, failed_login_count, salt, "password", secrets, preferences,
|
||||
|
||||
@@ -134,7 +134,6 @@ func CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(containerName string) {
|
||||
TokenValue: "",
|
||||
Active: true,
|
||||
}
|
||||
operatorData := databaseStructs.Operator{}
|
||||
operatorOperationData := []databaseStructs.Operatoroperation{}
|
||||
onStartMessage := ContainerOnStartMessage{
|
||||
ContainerName: containerName,
|
||||
@@ -163,9 +162,6 @@ func CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(containerName string) {
|
||||
// current operator is a bot, not deleted, and current operation is this operation
|
||||
apiToken.OperatorID = operatorOperationData[i].CurrentOperator.ID
|
||||
apiToken.CreatedBy = operatorOperationData[i].CurrentOperator.ID
|
||||
operatorData.ID = operatorOperationData[i].CurrentOperator.ID
|
||||
operatorData.CurrentOperationID.Valid = true
|
||||
operatorData.CurrentOperationID.Int64 = int64(operation.ID)
|
||||
onStartMessage.OperationID = operatorOperationData[i].CurrentOperation.ID
|
||||
onStartMessage.OperationName = operatorOperationData[i].CurrentOperation.Name
|
||||
apiToken.Name = fmt.Sprintf("\"%s\"'s OnStart API Call for \"%s\". Deletes after 5min",
|
||||
@@ -193,18 +189,18 @@ func CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(containerName string) {
|
||||
logging.LogError(err, "failed to create new apitoken")
|
||||
continue
|
||||
}
|
||||
accessToken, _, _, err := mythicjwt.GenerateJWT(operatorData, apiToken.TokenType, 0, apiToken.ID)
|
||||
plainAPITokenValue, storedAPITokenValue, err := mythicjwt.GenerateOpaqueAPIToken()
|
||||
if err != nil {
|
||||
logging.LogError(err, "failed to generate new JWT")
|
||||
logging.LogError(err, "failed to generate new API token")
|
||||
continue
|
||||
}
|
||||
apiToken.TokenValue = accessToken
|
||||
apiToken.TokenValue = storedAPITokenValue
|
||||
_, err = database.DB.Exec(`UPDATE apitokens SET token_value=$1 WHERE id=$2`, apiToken.TokenValue, apiToken.ID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to update apitoken with value")
|
||||
continue
|
||||
}
|
||||
onStartMessage.APIToken = apiToken.TokenValue
|
||||
onStartMessage.APIToken = plainAPITokenValue
|
||||
go updateAPITokenAfter5Minutes(apiToken.ID)
|
||||
err = RabbitMQConnection.SendContainerOnStart(onStartMessage)
|
||||
if err != nil {
|
||||
|
||||
@@ -244,6 +244,8 @@ const (
|
||||
MYTHIC_RPC_C2_UPDATE_STATUS = "mythic_rpc_c2_update_status"
|
||||
//
|
||||
MYTHIC_RPC_APITOKEN_CREATE = "mythic_rpc_apitoken_create"
|
||||
//
|
||||
MYTHIC_RPC_DIRECT_FILE_TOKEN_CREATE = "mythic_rpc_direct_file_token_create"
|
||||
// Agent Message
|
||||
MYTHIC_RPC_HANDLE_AGENT_JSON = "mythic_rpc_handle_agent_message_json"
|
||||
// CustomBrowser
|
||||
|
||||
@@ -1047,19 +1047,18 @@ func startEventStepInstance(eventStepInstanceID int) error {
|
||||
logging.LogError(err, "Failed to get new apitoken")
|
||||
continue
|
||||
}
|
||||
accessToken, _, _, err := mythicjwt.GenerateJWT(databaseStructs.Operator{ID: eventStepInstance.OperatorID},
|
||||
mythicjwt.AUTH_METHOD_EVENT, eventStepInstance.ID, apiToken.ID)
|
||||
plainAPITokenValue, storedAPITokenValue, err := mythicjwt.GenerateOpaqueAPIToken()
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to generate access token")
|
||||
continue
|
||||
}
|
||||
apiToken.TokenValue = accessToken
|
||||
apiToken.TokenValue = storedAPITokenValue
|
||||
_, err = database.DB.Exec(`UPDATE apitokens SET token_value=$1 WHERE id=$2`, apiToken.TokenValue, apiToken.ID)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to update apitoken")
|
||||
continue
|
||||
}
|
||||
inputs[key] = apiToken.TokenValue
|
||||
inputs[key] = plainAPITokenValue
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(allEventSteps); i++ {
|
||||
|
||||
@@ -42,7 +42,6 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
|
||||
TokenValue: "",
|
||||
Active: true,
|
||||
}
|
||||
operatorData := databaseStructs.Operator{}
|
||||
if input.AgentTaskID != nil {
|
||||
task := databaseStructs.Task{AgentTaskID: *input.AgentTaskID}
|
||||
err := database.DB.Get(&task, `SELECT id, operator_id, operation_id, display_id FROM task WHERE agent_task_id=$1`, task.AgentTaskID)
|
||||
@@ -55,7 +54,6 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
|
||||
apiToken.CreatedBy = task.OperatorID
|
||||
apiToken.TaskID.Valid = true
|
||||
apiToken.TaskID.Int64 = int64(task.ID)
|
||||
operatorData.ID = task.OperatorID
|
||||
apiToken.Name = fmt.Sprintf("Generated Task API Token via MythicRPC for Task %d", task.DisplayID)
|
||||
} else if input.PayloadUUID != nil {
|
||||
payload := databaseStructs.Payload{UuID: *input.PayloadUUID}
|
||||
@@ -67,7 +65,6 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
|
||||
apiToken.TokenType = mythicjwt.AUTH_METHOD_TASK
|
||||
apiToken.OperatorID = payload.OperatorID
|
||||
apiToken.CreatedBy = payload.OperatorID
|
||||
operatorData.ID = payload.OperatorID
|
||||
apiToken.PayloadID.Valid = true
|
||||
apiToken.PayloadID.Int64 = int64(payload.ID)
|
||||
apiToken.Name = fmt.Sprintf("Generated Payload API Token via MythicRPC for Payload %s", payload.UuID)
|
||||
@@ -97,9 +94,6 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
|
||||
if !operatorOperationData[i].CurrentOperator.Deleted && operatorOperationData[i].CurrentOperator.Active {
|
||||
apiToken.OperatorID = operatorOperationData[i].CurrentOperator.ID
|
||||
apiToken.CreatedBy = operatorOperationData[i].CurrentOperator.ID
|
||||
operatorData.ID = operatorOperationData[i].CurrentOperator.ID
|
||||
operatorData.CurrentOperationID.Valid = true
|
||||
operatorData.CurrentOperationID.Int64 = int64(callback.OperationID)
|
||||
apiToken.Name = fmt.Sprintf("Generated Callback API Token via MythicRPC for Callback %d", callback.DisplayID)
|
||||
}
|
||||
}
|
||||
@@ -129,12 +123,12 @@ func MythicRPCAPITokenCreate(input MythicRPCAPITokenCreateMessage) MythicRPCAPIT
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
accessToken, _, _, err := mythicjwt.GenerateJWT(operatorData, apiToken.TokenType, 0, apiToken.ID)
|
||||
accessToken, storedAPITokenValue, err := mythicjwt.GenerateOpaqueAPIToken()
|
||||
if err != nil {
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
apiToken.TokenValue = accessToken
|
||||
apiToken.TokenValue = storedAPITokenValue
|
||||
_, err = database.DB.Exec(`UPDATE apitokens SET token_value=$1 WHERE id=$2`, apiToken.TokenValue, apiToken.ID)
|
||||
if err != nil {
|
||||
response.Error = err.Error()
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
const directFileScopedTokenTTL = 30 * time.Second
|
||||
|
||||
type MythicRPCDirectFileTokenCreateMessage struct {
|
||||
AgentTaskID *string `json:"agent_task_id"`
|
||||
AgentCallbackID *string `json:"agent_callback_id"`
|
||||
PayloadUUID *string `json:"payload_uuid"`
|
||||
FileUUID string `json:"file_uuid"`
|
||||
Action string `json:"action"` // upload, download, both
|
||||
}
|
||||
|
||||
type MythicRPCDirectFileTokenCreateMessageResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
RabbitMQConnection.AddRPCQueue(RPCQueueStruct{
|
||||
Exchange: MYTHIC_EXCHANGE,
|
||||
Queue: MYTHIC_RPC_DIRECT_FILE_TOKEN_CREATE,
|
||||
RoutingKey: MYTHIC_RPC_DIRECT_FILE_TOKEN_CREATE,
|
||||
Handler: processMythicRPCDirectFileTokenCreate,
|
||||
})
|
||||
}
|
||||
|
||||
func scopesForDirectFileAction(action string) ([]string, error) {
|
||||
switch action {
|
||||
case "upload":
|
||||
return []string{mythicjwt.SCOPE_FILE_DIRECT_UPLOAD}, nil
|
||||
case "download":
|
||||
return []string{mythicjwt.SCOPE_FILE_DIRECT_DOWNLOAD}, nil
|
||||
case "both", "":
|
||||
return []string{mythicjwt.SCOPE_FILE_DIRECT_UPLOAD, mythicjwt.SCOPE_FILE_DIRECT_DOWNLOAD}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid action %q (expected upload, download, or both)", action)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveScopedTokenOperatorAndOperation(input MythicRPCDirectFileTokenCreateMessage) (int, int, error) {
|
||||
if input.AgentTaskID != nil {
|
||||
task := databaseStructs.Task{}
|
||||
if err := database.DB.Get(&task, `SELECT operator_id, operation_id FROM task WHERE agent_task_id=$1`,
|
||||
*input.AgentTaskID); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return task.OperatorID, task.OperationID, nil
|
||||
}
|
||||
if input.PayloadUUID != nil {
|
||||
payload := databaseStructs.Payload{}
|
||||
if err := database.DB.Get(&payload, `SELECT operator_id, operation_id FROM payload WHERE uuid=$1`,
|
||||
*input.PayloadUUID); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return payload.OperatorID, payload.OperationID, nil
|
||||
}
|
||||
if input.AgentCallbackID != nil {
|
||||
callback := databaseStructs.Callback{}
|
||||
if err := database.DB.Get(&callback, `SELECT operation_id FROM callback WHERE agent_callback_id=$1`,
|
||||
*input.AgentCallbackID); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
operatorOperationData := []databaseStructs.Operatoroperation{}
|
||||
if err := database.DB.Select(&operatorOperationData, `SELECT
|
||||
operator.account_type "operator.account_type",
|
||||
operator.id "operator.id",
|
||||
operator.deleted "operator.deleted",
|
||||
operator.active "operator.active"
|
||||
FROM operatoroperation
|
||||
JOIN operator ON operatoroperation.operator_id = operator.id
|
||||
WHERE operatoroperation.operation_id=$1`, callback.OperationID); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
for i := range operatorOperationData {
|
||||
if operatorOperationData[i].CurrentOperator.AccountType == databaseStructs.AccountTypeBot &&
|
||||
!operatorOperationData[i].CurrentOperator.Deleted &&
|
||||
operatorOperationData[i].CurrentOperator.Active {
|
||||
return operatorOperationData[i].CurrentOperator.ID, callback.OperationID, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, fmt.Errorf("need an active non-deleted bot account assigned to operation %d", callback.OperationID)
|
||||
}
|
||||
return 0, 0, fmt.Errorf("no operator/task/callback/payload information provided")
|
||||
}
|
||||
|
||||
func MythicRPCDirectFileTokenCreate(input MythicRPCDirectFileTokenCreateMessage) MythicRPCDirectFileTokenCreateMessageResponse {
|
||||
response := MythicRPCDirectFileTokenCreateMessageResponse{Success: false}
|
||||
if input.FileUUID == "" {
|
||||
response.Error = "file_uuid is required"
|
||||
return response
|
||||
}
|
||||
scopes, err := scopesForDirectFileAction(input.Action)
|
||||
if err != nil {
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
operatorID, operationID, err := resolveScopedTokenOperatorAndOperation(input)
|
||||
if err != nil {
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
user := databaseStructs.Operator{
|
||||
ID: operatorID,
|
||||
CurrentOperationID: sql.NullInt64{
|
||||
Valid: true,
|
||||
Int64: int64(operationID),
|
||||
},
|
||||
}
|
||||
token, _, err := mythicjwt.GenerateScopedJWT(user, scopes, input.FileUUID, directFileScopedTokenTTL)
|
||||
if err != nil {
|
||||
response.Error = err.Error()
|
||||
return response
|
||||
}
|
||||
response.Success = true
|
||||
response.Token = token
|
||||
return response
|
||||
}
|
||||
|
||||
func processMythicRPCDirectFileTokenCreate(msg amqp.Delivery) interface{} {
|
||||
incomingMessage := MythicRPCDirectFileTokenCreateMessage{}
|
||||
responseMsg := MythicRPCDirectFileTokenCreateMessageResponse{Success: false}
|
||||
if err := json.Unmarshal(msg.Body, &incomingMessage); err != nil {
|
||||
logging.LogError(err, "Failed to unmarshal JSON into struct")
|
||||
responseMsg.Error = err.Error()
|
||||
} else {
|
||||
return MythicRPCDirectFileTokenCreate(incomingMessage)
|
||||
}
|
||||
return responseMsg
|
||||
}
|
||||
@@ -576,7 +576,7 @@ func associateCommandsWithPayload(databasePayload databaseStructs.Payload, comma
|
||||
if utils.SliceContains(deniedCommandNames, dependency) {
|
||||
// uh oh, we included a command that depends on a rejected command
|
||||
return nil, errors.New(fmt.Sprintf("%s depends on %s, but %s isn't allowed with the currently selected parameters",
|
||||
command.Cmd, dependency))
|
||||
command.Cmd, dependency, dependency))
|
||||
}
|
||||
// the dependency we have isn't included, but isn't explicitly denied, so just add it
|
||||
finalCommandNames = append(finalCommandNames, dependency)
|
||||
|
||||
@@ -3,7 +3,7 @@ package rabbitmq
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json/v2"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -115,10 +115,14 @@ func GenerateAPITokenWebhook(c *gin.Context) {
|
||||
Name: defaultName,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if authType == mythicjwt.AUTH_METHOD_EVENT {
|
||||
apiToken.EventStepInstanceID.Valid = true
|
||||
apiToken.EventStepInstanceID.Int64 = int64(claims.EventStepInstanceID)
|
||||
}
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO apitokens
|
||||
(token_value, operator_id, token_type, active, "name", created_by)
|
||||
(token_value, operator_id, token_type, active, "name", created_by, eventstepinstance_id)
|
||||
VALUES
|
||||
(:token_value, :operator_id, :token_type, :active, :name, :created_by)
|
||||
(:token_value, :operator_id, :token_type, :active, :name, :created_by, :eventstepinstance_id)
|
||||
RETURNING id`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
@@ -135,9 +139,7 @@ func GenerateAPITokenWebhook(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
access_token, _, _, err := mythicjwt.GenerateJWT(databaseStructs.Operator{
|
||||
ID: userID,
|
||||
}, authType, claims.EventStepInstanceID, apiToken.ID)
|
||||
accessToken, storedAPITokenValue, err := mythicjwt.GenerateOpaqueAPIToken()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "error",
|
||||
@@ -145,7 +147,7 @@ func GenerateAPITokenWebhook(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
apiToken.TokenValue = access_token
|
||||
apiToken.TokenValue = storedAPITokenValue
|
||||
_, err = database.DB.Exec(`UPDATE apitokens SET token_value=$1 WHERE id=$2`, apiToken.TokenValue, apiToken.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
@@ -156,7 +158,7 @@ func GenerateAPITokenWebhook(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, GenerateAPITokenResponse{
|
||||
Status: "success",
|
||||
TokenValue: access_token,
|
||||
TokenValue: accessToken,
|
||||
OperatorID: userID,
|
||||
ID: apiToken.ID,
|
||||
Name: apiToken.Name,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/rabbitmq"
|
||||
@@ -107,7 +108,7 @@ func InitializeGinLogger() gin.HandlerFunc {
|
||||
source = "scripting"
|
||||
}
|
||||
graphQLOperationName := c.GetString("GraphQLName")
|
||||
if tokenStruct, ok := c.Get("apitoken_logging_struct"); ok {
|
||||
if tokenStruct, ok := c.Get(authentication.ContextKeyAPIToken); ok {
|
||||
if param.Path == "/graphql/webhook" {
|
||||
if graphQLOperationName == "" {
|
||||
graphQLOperationName = " with a subscription"
|
||||
@@ -130,8 +131,8 @@ func InitializeGinLogger() gin.HandlerFunc {
|
||||
"latency", param.Latency.String(),
|
||||
"responseSize", param.BodySize,
|
||||
"source", source,
|
||||
"user_id", c.GetInt("user_id"),
|
||||
"username", c.GetString("username"),
|
||||
"user_id", c.GetInt(authentication.ContextKeyUserID),
|
||||
"username", c.GetString(authentication.ContextKeyUsername),
|
||||
"file_id", c.GetString("file_id"),
|
||||
"graphql_name", c.GetString("GraphQLName"),
|
||||
"error", param.ErrorMessage)
|
||||
@@ -142,15 +143,12 @@ func InitializeGinLogger() gin.HandlerFunc {
|
||||
|
||||
func setRoutes(r *gin.Engine) {
|
||||
// the agent message route is allowed to come from anywhere since it'll come through random c2
|
||||
r.POST("/api/v1.4/agent_message", webcontroller.AgentMessageWebhook)
|
||||
r.GET("/api/v1.4/agent_message", webcontroller.AgentMessageGetWebhook)
|
||||
r.POST("/agent_message", webcontroller.AgentMessageWebhook)
|
||||
r.GET("/agent_message", webcontroller.AgentMessageGetWebhook)
|
||||
// healthcheck endpoint
|
||||
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)
|
||||
@@ -163,43 +161,48 @@ func setRoutes(r *gin.Engine) {
|
||||
r.GET("/auth_redirect/:containerName/:IDPName", webcontroller.GetAuthContainerRedirect)
|
||||
r.GET("/auth_acs/:containerName/:IDPName", webcontroller.ProcessIDPResponse)
|
||||
r.POST("/auth_acs/:containerName/:IDPName", webcontroller.ProcessIDPResponse)
|
||||
// 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)
|
||||
// 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 requires valid auth with a JWT to access
|
||||
protected := r.Group("/")
|
||||
protected.Use(authentication.JwtAuthMiddleware())
|
||||
{
|
||||
// EVERYBODY that can authenticate can do the following
|
||||
r.GET("/healthDetailed", webcontroller.HealthCheckDetailed)
|
||||
// hasura's graphql endpoint to get updated claims for access control
|
||||
//protected.GET("/graphql/webhook", webcontroller.GetHasuraClaims)
|
||||
protected.POST("/graphql/webhook", webcontroller.GetHasuraClaims)
|
||||
// user
|
||||
protected.GET("/me", webcontroller.GetMe) // controller.login
|
||||
protected.POST("/api/v1.4/me_webhook", webcontroller.GetMeWebhook) // controller.login
|
||||
protected.POST("/api/v1.4/generate_apitoken_webhook", webcontroller.GenerateAPITokenWebhook)
|
||||
protected.POST("/api/v1.4/delete_apitoken_webhook", webcontroller.DeleteAPITokenWebhook)
|
||||
protected.POST("/api/v1.4/update_current_operation_webhook", webcontroller.UpdateCurrentOperationWebhook)
|
||||
protected.POST("/api/v1.4/update_operator_password_webhook", webcontroller.UpdateOperatorPasswordWebhook)
|
||||
protected.POST("/api/v1.4/create_operator", webcontroller.CreateOperatorWebhook)
|
||||
protected.POST("/api/v1.4/operator_get_secrets_webhook", webcontroller.GetSecretsWebhook)
|
||||
protected.POST("/api/v1.4/operator_update_secrets_webhook", webcontroller.UpdateSecretsWebhook)
|
||||
protected.POST("/api/v1.4/operator_get_preferences_webhook", webcontroller.GetPreferencesWebhook)
|
||||
protected.POST("/api/v1.4/operator_update_preferences_webhook", webcontroller.UpdatePreferencesWebhook)
|
||||
protected.GET("/me", webcontroller.GetMe) // controller.login
|
||||
protected.POST("/me_webhook", webcontroller.GetMeWebhook) // controller.login
|
||||
protected.POST("/generate_apitoken_webhook", webcontroller.GenerateAPITokenWebhook)
|
||||
protected.POST("/delete_apitoken_webhook", webcontroller.DeleteAPITokenWebhook)
|
||||
protected.POST("/update_current_operation_webhook", webcontroller.UpdateCurrentOperationWebhook)
|
||||
protected.POST("/update_operator_password_webhook", webcontroller.UpdateOperatorPasswordWebhook)
|
||||
protected.POST("/create_operator", webcontroller.CreateOperatorWebhook)
|
||||
protected.POST("/operator_get_secrets_webhook", webcontroller.GetSecretsWebhook)
|
||||
protected.POST("/operator_update_secrets_webhook", webcontroller.UpdateSecretsWebhook)
|
||||
protected.POST("/operator_get_preferences_webhook", webcontroller.GetPreferencesWebhook)
|
||||
protected.POST("/operator_update_preferences_webhook", webcontroller.UpdatePreferencesWebhook)
|
||||
// operation
|
||||
protected.POST("/api/v1.4/create_operation_webhook", webcontroller.CreateOperationWebhook)
|
||||
protected.POST("/create_operation_webhook", webcontroller.CreateOperationWebhook)
|
||||
// global config
|
||||
protected.POST("/api/v1.4/get_global_settings_webhook", webcontroller.GetGlobalSettingWebhook)
|
||||
protected.POST("/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)
|
||||
protected.GET("/direct/view/:file_uuid",
|
||||
authentication.DirectFileScopeMiddleware(mythicjwt.SCOPE_FILE_DIRECT_DOWNLOAD),
|
||||
webcontroller.FileDirectViewWebhook)
|
||||
// authenticated direct file routes.
|
||||
// Supports normal JWT/APIToken auth and scoped short-lived direct-file tokens.
|
||||
r.GET("/direct/download/:file_uuid",
|
||||
authentication.DirectFileScopeMiddleware(mythicjwt.SCOPE_FILE_DIRECT_DOWNLOAD),
|
||||
webcontroller.FileDirectDownloadWebhook)
|
||||
r.POST("/direct/upload/:file_uuid",
|
||||
authentication.DirectFileScopeMiddleware(mythicjwt.SCOPE_FILE_DIRECT_UPLOAD),
|
||||
webcontroller.FileDirectUploadWebhook)
|
||||
// following require you to have an operation set
|
||||
allOperationMembers := protected.Group("/api/v1.4/")
|
||||
allOperationMembers := protected.Group("/")
|
||||
allOperationMembers.Use(authentication.RBACMiddlewareAll())
|
||||
{
|
||||
// generic all installed services
|
||||
@@ -216,9 +219,9 @@ 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)
|
||||
allOperationMembers.GET("screencaptures/:file_uuid", webcontroller.DownloadFileAuthWebhook)
|
||||
}
|
||||
noSpectators := protected.Group("/api/v1.4/")
|
||||
noSpectators := protected.Group("/")
|
||||
noSpectators.Use(authentication.RBACMiddlewareNoSpectators())
|
||||
{
|
||||
// everybody EXCEPT SPECTATORS can do these actions
|
||||
@@ -302,7 +305,7 @@ func setRoutes(r *gin.Engine) {
|
||||
// custom browsers
|
||||
noSpectators.POST("custombrowser_export_function_webhook", webcontroller.CustomBrowserExportFunctionWebhook)
|
||||
}
|
||||
operationAdminsOnly := protected.Group("/api/v1.4/")
|
||||
operationAdminsOnly := protected.Group("/")
|
||||
operationAdminsOnly.Use(authentication.RBACMiddlewareOperationAdmin())
|
||||
{
|
||||
// Only OPERATION_ADMIN and MYTHIC_ADMIN can do these routes
|
||||
|
||||
Reference in New Issue
Block a user