more api validation and logging updates

This commit is contained in:
its-a-feature
2026-04-18 15:33:17 -05:00
parent 45c02bd2d4
commit 5da30beb8d
8 changed files with 61 additions and 48 deletions
@@ -4,9 +4,10 @@ import (
"database/sql"
"errors"
"fmt"
"time"
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
"github.com/its-a-feature/Mythic/rabbitmq"
"time"
"github.com/its-a-feature/Mythic/database"
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
@@ -45,7 +46,7 @@ func ValidateLogin(username string, password string, scriptingVersion string, fr
}
if user.ID == 1 &&
user.FailedLoginCount >= 10 &&
(time.Now().UTC().Sub(user.LastFailedLoginTimestamp.Time) > time.Duration(60*time.Second)) {
(time.Now().UTC().Sub(user.LastFailedLoginTimestamp.Time) < 60*time.Second) {
err = errors.New("Throttling login attempts of default account")
logging.LogError(err, "Throttling login attempts of default account", "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Throttling login attempts of account, %s, from %s", user.Username, fromIP),
@@ -109,7 +110,7 @@ func ValidateCustomAuthProviderLogin(email string, authError string, validLogin
}
if user.ID == 1 &&
user.FailedLoginCount >= 10 &&
(time.Now().UTC().Sub(user.LastFailedLoginTimestamp.Time) > 60*time.Second) {
(time.Now().UTC().Sub(user.LastFailedLoginTimestamp.Time) < 60*time.Second) {
err = errors.New("Throttling login attempts of default account")
logging.LogError(err, "Throttling login attempts of default account", "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Throttling login attempts of account, %s, from %s", user.Username, fromIP),
+2 -1
View File
@@ -54,7 +54,7 @@ func getAPITokenFromDB(c *gin.Context, tokenString string) (databaseStructs.Apit
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)
logging.LogError(err, "Failed to get apitoken from database")
return databaseApiToken, err
}
c.Set(ContextKeyAPIToken, databaseApiToken)
@@ -67,6 +67,7 @@ func claimsFromAPIToken(databaseApiToken databaseStructs.Apitokens) *mythicjwt.C
AuthMethod: databaseApiToken.TokenType,
APITokensID: databaseApiToken.ID,
OperationID: int(databaseApiToken.Operator.CurrentOperationID.Int64),
Scopes: []string{mythicjwt.SCOPE_FILE_DIRECT_UPLOAD, mythicjwt.SCOPE_FILE_DIRECT_DOWNLOAD},
}
if databaseApiToken.EventStepInstanceID.Valid {
claims.EventStepInstanceID = int(databaseApiToken.EventStepInstanceID.Int64)
@@ -118,11 +118,6 @@ func DirectFileScopeMiddleware(requiredScope string) gin.HandlerFunc {
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()
@@ -63,13 +63,13 @@ func RefreshJWT(access_token string, refresh_token string) (string, string, int,
RefreshTokenCacheLock.RUnlock()
if !ok {
err := ErrFailedToFindRefreshToken
logging.LogError(err, "access_token", access_token)
logging.LogError(err, "Failed to refresh an access token and find it within RefreshTokenCache")
return "", "", 0, err
}
if storedRefresh != refresh_token {
err := ErrRefreshTokenMissmatch
logging.LogError(err, "refresh_token", refresh_token, "storedRefreshToken", storedRefresh)
logging.LogError(err, "Supplied refresh token doesn't match what Mythic has")
return "", "", 0, err
}
@@ -112,7 +112,7 @@ func GenerateJWT(user databaseStructs.Operator, authMethod string, eventStepInst
eventStepInstanceID,
APITokensID,
int(user.CurrentOperationID.Int64),
nil,
[]string{SCOPE_FILE_DIRECT_UPLOAD, SCOPE_FILE_DIRECT_DOWNLOAD},
"",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@@ -2,9 +2,10 @@ package webcontroller
import (
"fmt"
"github.com/its-a-feature/Mythic/rabbitmq"
"net/http"
"github.com/its-a-feature/Mythic/rabbitmq"
"github.com/gin-gonic/gin"
"github.com/its-a-feature/Mythic/database"
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
@@ -23,20 +24,22 @@ func FileDirectDownloadWebhook(c *gin.Context) {
// get the associated database information
filemeta := databaseStructs.Filemeta{}
payload := databaseStructs.Payload{}
operatorUsername := "unknown"
userID, err := GetUserIDFromGin(c)
if err == nil {
user, err := database.GetUserFromID(userID)
if err == nil {
operatorUsername = user.Username
}
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
user, err := database.GetUserFromID(userID)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
err = database.DB.Get(&filemeta, `SELECT
"path", filename, id, operation_id
FROM filemeta
WHERE
filemeta.agent_file_id=$1 and deleted=false
`, agentFileID)
filemeta.agent_file_id=$1 and deleted=false and filemeta.operation_id=$2
`, agentFileID, user.CurrentOperationID.Int64)
if err != nil {
err = database.DB.Get(&payload, `SELECT
filemeta.path "filemeta.path",
@@ -45,7 +48,7 @@ func FileDirectDownloadWebhook(c *gin.Context) {
filemeta.operation_id "filemeta.operation_id"
FROM payload
JOIN filemeta ON payload.file_id = filemeta.id
WHERE payload.uuid=$1`, agentFileID)
WHERE payload.uuid=$1 and payload.operation_id=$2`, agentFileID, user.CurrentOperationID.Int64)
if err != nil {
logging.LogError(err, "Failed to get file data from database")
message := fmt.Sprintf("Attempt to download unknown file: %s", agentFileID)
@@ -53,11 +56,11 @@ func FileDirectDownloadWebhook(c *gin.Context) {
c.AbortWithStatus(http.StatusNotFound)
return
}
go tagFileAs(payload.Filemeta.ID, operatorUsername, payload.Filemeta.OperationID, tagTypeDownload, nil, c, false)
go tagFileAs(payload.Filemeta.ID, user.Username, payload.Filemeta.OperationID, tagTypeDownload, nil, c, false)
c.FileAttachment(payload.Filemeta.Path, string(payload.Filemeta.Filename))
return
}
go tagFileAs(filemeta.ID, operatorUsername, filemeta.OperationID, tagTypeDownload, nil, c, false)
go tagFileAs(filemeta.ID, user.Username, filemeta.OperationID, tagTypeDownload, nil, c, false)
c.FileAttachment(filemeta.Path, string(filemeta.Filename))
return
}
@@ -30,11 +30,21 @@ func FileDirectUploadWebhook(c *gin.Context) {
return
}
filemeta := databaseStructs.Filemeta{}
userID, err := GetUserIDFromGin(c)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
user, err := database.GetUserFromID(userID)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
err = database.DB.Get(&filemeta, `SELECT
"path", id
FROM
filemeta
WHERE agent_file_id=$1`, agentFileID)
WHERE agent_file_id=$1 AND operation_id=$2`, agentFileID, user.CurrentOperationID.Int64)
if err != nil {
logging.LogError(err, "Failed to find file in FileDirectUploadWebhook", "agentFileID", agentFileID)
c.JSON(http.StatusOK, gin.H{
@@ -2,9 +2,10 @@ package webcontroller
import (
"fmt"
"github.com/its-a-feature/Mythic/rabbitmq"
"net/http"
"github.com/its-a-feature/Mythic/rabbitmq"
"github.com/gin-gonic/gin"
"github.com/its-a-feature/Mythic/database"
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
@@ -23,20 +24,22 @@ func FileDirectViewWebhook(c *gin.Context) {
// get the associated database information
filemeta := databaseStructs.Filemeta{}
payload := databaseStructs.Payload{}
operatorUsername := "unknown"
userID, err := GetUserIDFromGin(c)
if err == nil {
user, err := database.GetUserFromID(userID)
if err == nil {
operatorUsername = user.Username
}
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
user, err := database.GetUserFromID(userID)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
err = database.DB.Get(&filemeta, `SELECT
"path", filename, id, operation_id
FROM filemeta
WHERE
filemeta.agent_file_id=$1
`, agentFileID)
filemeta.agent_file_id=$1 AND filemeta.operation_id=$2
`, agentFileID, user.CurrentOperationID.Int64)
if err != nil {
err = database.DB.Get(&payload, `SELECT
filemeta.path "filemeta.path",
@@ -45,7 +48,7 @@ func FileDirectViewWebhook(c *gin.Context) {
filemeta.operation_id "filemeta.operation_id"
FROM payload
JOIN filemeta ON payload.file_id = filemeta.id
WHERE payload.uuid=$1`, agentFileID)
WHERE payload.uuid=$1 AND payload.operation_id=$2`, agentFileID, user.CurrentOperationID.Int64)
if err != nil {
logging.LogError(err, "Failed to get file data from database")
message := fmt.Sprintf("Attempt to download unknown file: %s", agentFileID)
@@ -53,11 +56,11 @@ func FileDirectViewWebhook(c *gin.Context) {
c.AbortWithStatus(http.StatusNotFound)
return
}
go tagFileAs(payload.Filemeta.ID, operatorUsername, payload.Filemeta.OperationID, tagTypePreview, nil, c, false)
go tagFileAs(payload.Filemeta.ID, user.Username, payload.Filemeta.OperationID, tagTypePreview, nil, c, false)
c.File(payload.Filemeta.Path)
return
}
go tagFileAs(filemeta.ID, operatorUsername, filemeta.OperationID, tagTypePreview, nil, c, false)
go tagFileAs(filemeta.ID, user.Username, filemeta.OperationID, tagTypePreview, nil, c, false)
c.File(filemeta.Path)
return
}
+11 -11
View File
@@ -190,22 +190,22 @@ func setRoutes(r *gin.Engine) {
// 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",
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("/")
allOperationMembers.Use(authentication.RBACMiddlewareAll())
{
// generic all installed services
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)
allOperationMembers.POST("eventing_import_automatic_webhook", webcontroller.EventingImportAutomaticWebhook)
// payloadtype / c2profile
allOperationMembers.POST("c2profile_status_webhook", webcontroller.C2ProfileStatusWebhook)