diff --git a/.gitignore b/.gitignore
index d4a9ae66..b2489a47 100755
--- a/.gitignore
+++ b/.gitignore
@@ -167,3 +167,5 @@ _ReSharper*/
jupyter-docker/jupyter/MythicExamples/.ipynb_checkpoints/
nginx-docker/config/blockips.conf
+
+.gocache
diff --git a/MythicReactUI/src/components/pages/Callbacks/ResponseDisplayScreenshotModal.js b/MythicReactUI/src/components/pages/Callbacks/ResponseDisplayScreenshotModal.js
index 6c9d445e..24af2706 100644
--- a/MythicReactUI/src/components/pages/Callbacks/ResponseDisplayScreenshotModal.js
+++ b/MythicReactUI/src/components/pages/Callbacks/ResponseDisplayScreenshotModal.js
@@ -26,7 +26,7 @@ export function ResponseDisplayScreenshotModal({onClose, images, startIndex}) {
< >
-
diff --git a/MythicReactUI/src/components/pages/Home/CallbacksCard.js b/MythicReactUI/src/components/pages/Home/CallbacksCard.js
index 8fb407e3..5ff7b0c5 100644
--- a/MythicReactUI/src/components/pages/Home/CallbacksCard.js
+++ b/MythicReactUI/src/components/pages/Home/CallbacksCard.js
@@ -1535,7 +1535,7 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen
- onPreviewMedia(e, activeStep)}
/>
diff --git a/MythicReactUI/src/components/pages/Home/DashboardComponents.js b/MythicReactUI/src/components/pages/Home/DashboardComponents.js
index 2353e03f..14749aab 100644
--- a/MythicReactUI/src/components/pages/Home/DashboardComponents.js
+++ b/MythicReactUI/src/components/pages/Home/DashboardComponents.js
@@ -48,7 +48,7 @@ export const PieChartCard = ({
}
const theme = useTheme();
return (
- {
const theme = useTheme();
return (
- {
const theme = useTheme();
return (
- {
}
};
return (
- {
const theme = useTheme();
return (
-
-
{openScreenshot &&
{
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);
diff --git a/Mythic_CLI/src/cmd/internal/serviceMetadata.go b/Mythic_CLI/src/cmd/internal/serviceMetadata.go
index 887cb980..1992bc0b 100644
--- a/Mythic_CLI/src/cmd/internal/serviceMetadata.go
+++ b/Mythic_CLI/src/cmd/internal/serviceMetadata.go
@@ -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" {
diff --git a/mythic-docker/src/authentication/jwt.go b/mythic-docker/src/authentication/jwt.go
index 6fce92e2..a8b09150 100644
--- a/mythic-docker/src/authentication/jwt.go
+++ b/mythic-docker/src/authentication/jwt.go
@@ -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
-}
diff --git a/mythic-docker/src/authentication/jwt_test.go b/mythic-docker/src/authentication/jwt_test.go
index ca6474da..8f7ae9ac 100644
--- a/mythic-docker/src/authentication/jwt_test.go
+++ b/mythic-docker/src/authentication/jwt_test.go
@@ -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),
}
diff --git a/mythic-docker/src/authentication/middleware.go b/mythic-docker/src/authentication/middleware.go
index aea8e52c..d63ee8f6 100644
--- a/mythic-docker/src/authentication/middleware.go
+++ b/mythic-docker/src/authentication/middleware.go
@@ -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()
+ }
+}
diff --git a/mythic-docker/src/authentication/mythicjwt/apitoken.go b/mythic-docker/src/authentication/mythicjwt/apitoken.go
new file mode 100644
index 00000000..2876d6f5
--- /dev/null
+++ b/mythic-docker/src/authentication/mythicjwt/apitoken.go
@@ -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
+}
diff --git a/mythic-docker/src/authentication/mythicjwt/jwt.go b/mythic-docker/src/authentication/mythicjwt/jwt.go
index 0b9ae9e4..15745d1e 100644
--- a/mythic-docker/src/authentication/mythicjwt/jwt.go
+++ b/mythic-docker/src/authentication/mythicjwt/jwt.go
@@ -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
+}
diff --git a/mythic-docker/src/database/utils.go b/mythic-docker/src/database/utils.go
index 9c3afe04..2c3b14e3 100644
--- a/mythic-docker/src/database/utils.go
+++ b/mythic-docker/src/database/utils.go
@@ -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,
diff --git a/mythic-docker/src/rabbitmq/check_container_status.go b/mythic-docker/src/rabbitmq/check_container_status.go
index 8669ff73..9c479c0d 100644
--- a/mythic-docker/src/rabbitmq/check_container_status.go
+++ b/mythic-docker/src/rabbitmq/check_container_status.go
@@ -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 {
diff --git a/mythic-docker/src/rabbitmq/constants.go b/mythic-docker/src/rabbitmq/constants.go
index 09cbc3ba..ec51021a 100644
--- a/mythic-docker/src/rabbitmq/constants.go
+++ b/mythic-docker/src/rabbitmq/constants.go
@@ -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
diff --git a/mythic-docker/src/rabbitmq/eventing.go b/mythic-docker/src/rabbitmq/eventing.go
index 29848cd7..fde2cdbf 100644
--- a/mythic-docker/src/rabbitmq/eventing.go
+++ b/mythic-docker/src/rabbitmq/eventing.go
@@ -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++ {
diff --git a/mythic-docker/src/rabbitmq/recv_mythic_rpc_apitoken_create.go b/mythic-docker/src/rabbitmq/recv_mythic_rpc_apitoken_create.go
index 5c37a396..45786775 100644
--- a/mythic-docker/src/rabbitmq/recv_mythic_rpc_apitoken_create.go
+++ b/mythic-docker/src/rabbitmq/recv_mythic_rpc_apitoken_create.go
@@ -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()
diff --git a/mythic-docker/src/rabbitmq/recv_mythic_rpc_direct_file_token_create.go b/mythic-docker/src/rabbitmq/recv_mythic_rpc_direct_file_token_create.go
new file mode 100644
index 00000000..6b5219a5
--- /dev/null
+++ b/mythic-docker/src/rabbitmq/recv_mythic_rpc_direct_file_token_create.go
@@ -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
+}
diff --git a/mythic-docker/src/rabbitmq/recv_mythic_rpc_payload_create_from_uuid.go b/mythic-docker/src/rabbitmq/recv_mythic_rpc_payload_create_from_uuid.go
index 6a4c6759..53dd4adf 100644
--- a/mythic-docker/src/rabbitmq/recv_mythic_rpc_payload_create_from_uuid.go
+++ b/mythic-docker/src/rabbitmq/recv_mythic_rpc_payload_create_from_uuid.go
@@ -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)
diff --git a/mythic-docker/src/rabbitmq/util_agent_message.go b/mythic-docker/src/rabbitmq/util_agent_message.go
index fb74950e..12ac2672 100644
--- a/mythic-docker/src/rabbitmq/util_agent_message.go
+++ b/mythic-docker/src/rabbitmq/util_agent_message.go
@@ -3,7 +3,7 @@ package rabbitmq
import (
"database/sql"
"encoding/base64"
- "encoding/json/v2"
+ "encoding/json"
"encoding/xml"
"errors"
"fmt"
diff --git a/mythic-docker/src/webserver/controllers/user_generate_apitoken_webhook.go b/mythic-docker/src/webserver/controllers/user_generate_apitoken_webhook.go
index eee7870a..0ae7a2c0 100644
--- a/mythic-docker/src/webserver/controllers/user_generate_apitoken_webhook.go
+++ b/mythic-docker/src/webserver/controllers/user_generate_apitoken_webhook.go
@@ -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,
diff --git a/mythic-docker/src/webserver/initialize.go b/mythic-docker/src/webserver/initialize.go
index 9677eb97..a37677cb 100644
--- a/mythic-docker/src/webserver/initialize.go
+++ b/mythic-docker/src/webserver/initialize.go
@@ -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