diff --git a/AdaptixServer/core/connector/connector.go b/AdaptixServer/core/connector/connector.go index c74350b9..96a14b8d 100644 --- a/AdaptixServer/core/connector/connector.go +++ b/AdaptixServer/core/connector/connector.go @@ -24,6 +24,7 @@ type Teamserver interface { TsAgentConsoleOutput(agentId string, messageType int, message string, clearText string) TsAgentUpdateData(newAgentObject []byte) error TsAgentCommand(agentName string, agentId string, username string, cmdline string, args map[string]any) error + TsAgentCtxExit(agentId string, username string) error TsAgentRemove(agentId string) error TsAgentSetTag(agentId string, tag string) error diff --git a/AdaptixServer/core/connector/tc_agents.go b/AdaptixServer/core/connector/tc_agents.go index 7e9bd3d0..da9f7376 100644 --- a/AdaptixServer/core/connector/tc_agents.go +++ b/AdaptixServer/core/connector/tc_agents.go @@ -2,6 +2,7 @@ package connector import ( "encoding/json" + "fmt" "github.com/gin-gonic/gin" "log" "net/http" @@ -14,15 +15,6 @@ type CommandData struct { Data string `json:"data"` } -type AgentRemove struct { - AgentId string `json:"id"` -} - -type AgentTag struct { - AgentId string `json:"id"` - Tag string `json:"tag"` -} - func (tc *TsConnector) TcAgentCommand(ctx *gin.Context) { var ( username string @@ -64,6 +56,61 @@ func (tc *TsConnector) TcAgentCommand(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "", "ok": true}) } +type AgentExit struct { + AgentIdArray []string `json:"agent_id_array"` +} + +func (tc *TsConnector) TcAgentExit(ctx *gin.Context) { + var ( + agentExit AgentExit + err error + username string + ok bool + ) + + err = ctx.ShouldBindJSON(&agentExit) + if err != nil { + ctx.JSON(http.StatusOK, gin.H{"message": "invalid command data", "ok": false}) + return + } + + value, exists := ctx.Get("username") + if !exists { + ctx.JSON(http.StatusOK, gin.H{"message": "Server error: username not found in context", "ok": false}) + return + } + + username, ok = value.(string) + if !ok { + ctx.JSON(http.StatusOK, gin.H{"message": "Server error: invalid username type in context", "ok": false}) + return + } + + var errorsSlice []string + for _, agentId := range agentExit.AgentIdArray { + err = tc.teamserver.TsAgentCtxExit(agentId, username) + if err != nil { + errorsSlice = append(errorsSlice, err.Error()) + } + } + + if len(errorsSlice) > 0 { + message := "" + for i, errorMessage := range errorsSlice { + message += fmt.Sprintf("%d. %s\n", i+1, errorMessage) + } + + ctx.JSON(http.StatusOK, gin.H{"message": message, "ok": false}) + return + } + + ctx.JSON(http.StatusOK, gin.H{"message": "", "ok": true}) +} + +type AgentRemove struct { + AgentIdArray []string `json:"agent_id_array"` +} + func (tc *TsConnector) TcAgentRemove(ctx *gin.Context) { var ( agentRemove AgentRemove @@ -76,15 +123,32 @@ func (tc *TsConnector) TcAgentRemove(ctx *gin.Context) { return } - err = tc.teamserver.TsAgentRemove(agentRemove.AgentId) - if err != nil { - ctx.JSON(http.StatusOK, gin.H{"message": err.Error(), "ok": false}) + var errorsSlice []string + for _, agentId := range agentRemove.AgentIdArray { + err = tc.teamserver.TsAgentRemove(agentId) + if err != nil { + errorsSlice = append(errorsSlice, err.Error()) + } + } + + if len(errorsSlice) > 0 { + message := "" + for i, errorMessage := range errorsSlice { + message += fmt.Sprintf("%d. %s\n", i+1, errorMessage) + } + + ctx.JSON(http.StatusOK, gin.H{"message": message, "ok": false}) return } ctx.JSON(http.StatusOK, gin.H{"message": "", "ok": true}) } +type AgentTag struct { + AgentIdArray []string `json:"agent_id_array"` + Tag string `json:"tag"` +} + func (tc *TsConnector) TcAgentSetTag(ctx *gin.Context) { var ( agentTag AgentTag @@ -97,9 +161,21 @@ func (tc *TsConnector) TcAgentSetTag(ctx *gin.Context) { return } - err = tc.teamserver.TsAgentSetTag(agentTag.AgentId, agentTag.Tag) - if err != nil { - ctx.JSON(http.StatusOK, gin.H{"message": err.Error(), "ok": false}) + var errorsSlice []string + for _, agentId := range agentTag.AgentIdArray { + err = tc.teamserver.TsAgentSetTag(agentId, agentTag.Tag) + if err != nil { + errorsSlice = append(errorsSlice, err.Error()) + } + } + + if len(errorsSlice) > 0 { + message := "" + for i, errorMessage := range errorsSlice { + message += fmt.Sprintf("%d. %s\n", i+1, errorMessage) + } + + ctx.JSON(http.StatusOK, gin.H{"message": message, "ok": false}) return } diff --git a/AdaptixServer/core/extender/ex_agent.go b/AdaptixServer/core/extender/ex_agent.go index 4d6f5d5b..56b70440 100644 --- a/AdaptixServer/core/extender/ex_agent.go +++ b/AdaptixServer/core/extender/ex_agent.go @@ -121,3 +121,15 @@ func (ex *AdaptixExtender) ExAgentBrowserDownload(agentName string, path string, return nil, errors.New("module not found") } } + +func (ex *AdaptixExtender) ExAgentCtxExit(agentName string, agentObject []byte) ([]byte, error) { + var module *ModuleExtender + + value, ok := ex.agentModules.Get(agentName) + if ok { + module = value.(*ModuleExtender) + return module.AgentBrowserExit(agentObject) + } else { + return nil, errors.New("module not found") + } +} diff --git a/AdaptixServer/core/extender/extender.go b/AdaptixServer/core/extender/extender.go index bfe810e3..6f819f08 100644 --- a/AdaptixServer/core/extender/extender.go +++ b/AdaptixServer/core/extender/extender.go @@ -161,6 +161,10 @@ func (ex *AdaptixExtender) ValidPlugin(info ModuleInfo, object plugin.Symbol) er if !ok { return errors.New("method AgentBrowserDownload not found") } + _, ok = reflect.TypeOf(object).MethodByName("AgentBrowserExit") + if !ok { + return errors.New("method AgentBrowserExit not found") + } return nil } diff --git a/AdaptixServer/core/extender/utils.go b/AdaptixServer/core/extender/utils.go index d1346a42..e945b80d 100644 --- a/AdaptixServer/core/extender/utils.go +++ b/AdaptixServer/core/extender/utils.go @@ -56,6 +56,7 @@ type AgentFunctions interface { AgentBrowserFiles(path string, agentObject []byte) ([]byte, error) AgentBrowserUpload(path string, content []byte, agentObject []byte) ([]byte, error) AgentBrowserDownload(path string, agentObject []byte) ([]byte, error) + AgentBrowserExit(agentObject []byte) ([]byte, error) } type ModuleExtender struct { diff --git a/AdaptixServer/core/server/ts_browser.go b/AdaptixServer/core/server/ts_browser.go index 52dc636d..411fd7c0 100644 --- a/AdaptixServer/core/server/ts_browser.go +++ b/AdaptixServer/core/server/ts_browser.go @@ -216,6 +216,49 @@ func (ts *Teamserver) TsAgentBrowserDownload(agentId string, path string, userna return nil } +func (ts *Teamserver) TsAgentCtxExit(agentId string, username string) error { + var ( + err error + agentObject bytes.Buffer + agent *Agent + taskData TaskData + data []byte + ) + + value, ok := ts.agents.Get(agentId) + if ok { + + agent, _ = value.(*Agent) + _ = json.NewEncoder(&agentObject).Encode(agent.Data) + + data, err = ts.Extender.ExAgentCtxExit(agent.Data.Name, agentObject.Bytes()) + if err != nil { + return err + } + + err = json.Unmarshal(data, &taskData) + if err != nil { + return err + } + + if taskData.TaskId == "" { + taskData.TaskId, _ = krypt.GenerateUID(8) + } + taskData.AgentId = agentId + taskData.User = username + taskData.CommandLine = "agent terminate" + taskData.StartDate = time.Now().Unix() + taskData.Sync = true + + agent.TasksQueue.Put(taskData) + + } else { + return fmt.Errorf("agent '%v' does not exist", agentId) + } + + return nil +} + /// SYNC func (ts *Teamserver) TsClientBrowserDisks(jsonTask string, jsonDrives string) { diff --git a/AdaptixServer/core/server/ts_syncpacket.go b/AdaptixServer/core/server/ts_syncpacket.go index 73884934..40f736ed 100644 --- a/AdaptixServer/core/server/ts_syncpacket.go +++ b/AdaptixServer/core/server/ts_syncpacket.go @@ -309,10 +309,10 @@ func CreateSpBrowserFiles(taskData TaskData, path string, data string) SyncPacke } } -func CreateSpBrowserStatus(taskData TaskData) SyncPacketBrowserStatus { - return SyncPacketBrowserStatus{ +func CreateSpBrowserFilesStatus(taskData TaskData) SyncPacketBrowserFilesStatus { + return SyncPacketBrowserFilesStatus{ store: STORE_LOG, - SpType: TYPE_BROWSER_STATUS, + SpType: TYPE_BROWSER_FILES_STATUS, AgentId: taskData.AgentId, Time: time.Now().UTC().Unix(), @@ -320,3 +320,16 @@ func CreateSpBrowserStatus(taskData TaskData) SyncPacketBrowserStatus { Message: taskData.Message, } } + +func CreateSpBrowserProcess(taskData TaskData, data string) SyncPacketBrowserProcess { + return SyncPacketBrowserProcess{ + store: STORE_LOG, + SpType: TYPE_BROWSER_PROCESS, + + AgentId: taskData.AgentId, + Time: time.Now().UTC().Unix(), + MessageType: taskData.MessageType, + Message: taskData.Message, + Data: data, + } +} diff --git a/AdaptixServer/extenders/agent_beacon/pl_agent.go b/AdaptixServer/extenders/agent_beacon/pl_agent.go index 3b0f0c06..d88794ef 100644 --- a/AdaptixServer/extenders/agent_beacon/pl_agent.go +++ b/AdaptixServer/extenders/agent_beacon/pl_agent.go @@ -745,3 +745,8 @@ func BrowserDownload(path string, agentData AgentData) ([]byte, error) { array := []interface{}{COMMAND_DOWNLOAD, ConvertUTF8toCp(path, agentData.ACP)} return PackArray(array) } + +func BrowserExit(agentData AgentData) ([]byte, error) { + array := []interface{}{COMMAND_TERMINATE, 2} + return PackArray(array) +} diff --git a/Client/Headers/Client/Requestor.h b/Client/Headers/Client/Requestor.h index 58f773a7..f4c9a2ca 100644 --- a/Client/Headers/Client/Requestor.h +++ b/Client/Headers/Client/Requestor.h @@ -22,9 +22,11 @@ bool HttpReqListenerStop( QString listenerName, QString listenerType, AuthProfil bool HttpReqAgentCommand( QString agentName, QString agentId, QString cmdLine, QString data, AuthProfile profile, QString* message, bool* ok ); -bool HttpReqAgentRemove( QString agentId, AuthProfile profile, QString* message, bool* ok ); +bool HttpReqAgentExit( QStringList agentsId, AuthProfile profile, QString* message, bool* ok ); -bool HttpReqAgentSetTag( QString agentId, QString tag, AuthProfile profile, QString* message, bool* ok ); +bool HttpReqAgentRemove( QStringList agentsId, AuthProfile profile, QString* message, bool* ok ); + +bool HttpReqAgentSetTag( QStringList agentsId, QString tag, AuthProfile profile, QString* message, bool* ok ); ///DOWNLOAD diff --git a/Client/Headers/UI/Widgets/SessionsTableWidget.h b/Client/Headers/UI/Widgets/SessionsTableWidget.h index d75877b2..2d0458fd 100644 --- a/Client/Headers/UI/Widgets/SessionsTableWidget.h +++ b/Client/Headers/UI/Widgets/SessionsTableWidget.h @@ -61,6 +61,7 @@ public slots: void actionConsoleOpen(); void actionFileBrowserOpen(); void actionProcessBrowserOpen(); + void actionAgentExit(); void actionAgentTag(); void actionAgentHide(); void actionAgentRemove(); diff --git a/Client/Source/Client/ProcessSyncPacket.cpp b/Client/Source/Client/ProcessSyncPacket.cpp index 6f4d9b1a..f924322e 100644 --- a/Client/Source/Client/ProcessSyncPacket.cpp +++ b/Client/Source/Client/ProcessSyncPacket.cpp @@ -678,7 +678,7 @@ void AdaptixWidget::processSyncPacket(QJsonObject jsonObj) if (Agents.contains(agentId)) { Agents[agentId]->ProcessBrowser->SetStatus(time, msgType, message); - Agents[agentId]->ProcessBrowser->SetProcess(data); + Agents[agentId]->ProcessBrowser->SetProcess(msgType, data); } return; diff --git a/Client/Source/Client/Requestor.cpp b/Client/Source/Client/Requestor.cpp index 499ba605..232cc260 100644 --- a/Client/Source/Client/Requestor.cpp +++ b/Client/Source/Client/Requestor.cpp @@ -140,10 +140,34 @@ bool HttpReqAgentCommand( QString agentName, QString agentId, QString cmdLine, Q return false; } -bool HttpReqAgentRemove( QString agentId, AuthProfile profile, QString* message, bool* ok ) +bool HttpReqAgentExit( QStringList agentsId, AuthProfile profile, QString* message, bool* ok ) { + QJsonArray arrayId; + for (QString item : agentsId) + arrayId.append(item); + QJsonObject dataJson; - dataJson["id"] = agentId; + dataJson["agent_id_array"] = arrayId; + QByteArray jsonData = QJsonDocument(dataJson).toJson(); + + QString sUrl = profile.GetURL() + "/agent/exit"; + QJsonObject jsonObject = HttpReq(sUrl, jsonData, profile.GetAccessToken()); + if ( jsonObject.contains("message") && jsonObject.contains("ok") ) { + *message = jsonObject["message"].toString(); + *ok = jsonObject["ok"].toBool(); + return true; + } + return false; +} + +bool HttpReqAgentRemove( QStringList agentsId, AuthProfile profile, QString* message, bool* ok ) +{ + QJsonArray arrayId; + for (QString item : agentsId) + arrayId.append(item); + + QJsonObject dataJson; + dataJson["agent_id_array"] = arrayId; QByteArray jsonData = QJsonDocument(dataJson).toJson(); QString sUrl = profile.GetURL() + "/agent/remove"; @@ -156,10 +180,14 @@ bool HttpReqAgentRemove( QString agentId, AuthProfile profile, QString* message, return false; } -bool HttpReqAgentSetTag( QString agentId, QString tag, AuthProfile profile, QString* message, bool* ok ) +bool HttpReqAgentSetTag( QStringList agentsId, QString tag, AuthProfile profile, QString* message, bool* ok ) { + QJsonArray arrayId; + for (QString item : agentsId) + arrayId.append(item); + QJsonObject dataJson; - dataJson["id"] = agentId; + dataJson["agent_id_array"] = arrayId; dataJson["tag"] = tag; QByteArray jsonData = QJsonDocument(dataJson).toJson(); diff --git a/Client/Source/UI/Widgets/SessionsTableWidget.cpp b/Client/Source/UI/Widgets/SessionsTableWidget.cpp index d9b7b626..4979a615 100644 --- a/Client/Source/UI/Widgets/SessionsTableWidget.cpp +++ b/Client/Source/UI/Widgets/SessionsTableWidget.cpp @@ -162,25 +162,31 @@ void SessionsTableWidget::handleSessionsTableMenu(const QPoint &pos) auto ctxMenu = QMenu(); + auto agentSep1 = new QAction(); + agentSep1->setSeparator(true); + auto agentSep2 = new QAction(); + agentSep2->setSeparator(true); + auto agentMenu = new QMenu("Agent", &ctxMenu); agentMenu->addAction("Command"); + agentMenu->addAction(agentSep1); agentMenu->addAction("File Browser", this, &SessionsTableWidget::actionFileBrowserOpen); agentMenu->addAction("Process Browser", this, &SessionsTableWidget::actionProcessBrowserOpen); - agentMenu->addAction("Exit"); + agentMenu->addAction(agentSep2); + agentMenu->addAction("Exit", this, &SessionsTableWidget::actionAgentExit); - auto sep1 = new QAction(); - sep1->setSeparator( true ); - - auto sep2 = new QAction(); - sep2->setSeparator( true ); + auto ctxSep1 = new QAction(); + ctxSep1->setSeparator(true); + auto ctxSep2 = new QAction(); + ctxSep2->setSeparator(true); ctxMenu.addAction( "Console", this, &SessionsTableWidget::actionConsoleOpen); - ctxMenu.addAction( sep1 ); + ctxMenu.addAction(ctxSep1); ctxMenu.addMenu(agentMenu); - ctxMenu.addAction( sep2 ); - ctxMenu.addAction( "Tag", this, &SessionsTableWidget::actionAgentTag); - ctxMenu.addAction( "Hide", this, &SessionsTableWidget::actionAgentHide); - ctxMenu.addAction( "Remove", this, &SessionsTableWidget::actionAgentRemove); + ctxMenu.addAction(ctxSep2); + ctxMenu.addAction( "Set tag", this, &SessionsTableWidget::actionAgentTag); + ctxMenu.addAction( "Hide on client", this, &SessionsTableWidget::actionAgentHide); + ctxMenu.addAction( "Remove from server", this, &SessionsTableWidget::actionAgentRemove); ctxMenu.exec(tableWidget->horizontalHeader()->viewport()->mapToGlobal(pos)); } @@ -218,19 +224,56 @@ void SessionsTableWidget::actionProcessBrowserOpen() } } +void SessionsTableWidget::actionAgentExit() +{ + QStringList listId; + + auto adaptixWidget = qobject_cast( mainWidget ); + for( int rowIndex = 0 ; rowIndex < tableWidget->rowCount() ; rowIndex++ ) { + if ( tableWidget->item(rowIndex, 0)->isSelected() ) { + auto agentId = tableWidget->item( rowIndex, ColumnAgentID )->text(); + listId.append(agentId); + } + } + + if(listId.empty()) + return; + + QString message = QString(); + bool ok = false; + bool result = HttpReqAgentExit(listId, *(adaptixWidget->GetProfile()), &message, &ok); + if( !result ) { + MessageError("JWT error"); + return; + } +} + void SessionsTableWidget::actionAgentTag() { - auto adaptixWidget = qobject_cast( mainWidget ); + QStringList listId; - QString agentId = tableWidget->item( tableWidget->currentRow(), ColumnAgentID )->text(); - QString tag = tableWidget->item( tableWidget->currentRow(), ColumnTags )->text(); + auto adaptixWidget = qobject_cast( mainWidget ); + for( int rowIndex = 0 ; rowIndex < tableWidget->rowCount() ; rowIndex++ ) { + if ( tableWidget->item(rowIndex, 0)->isSelected() ) { + auto agentId = tableWidget->item( rowIndex, ColumnAgentID )->text(); + listId.append(agentId); + } + } + + if(listId.empty()) + return; + + QString tag = ""; + if(listId.size() == 1) { + tag = tableWidget->item( tableWidget->currentRow(), ColumnTags )->text(); + } bool inputOk; - QString newTag = QInputDialog::getText(nullptr, "Set tags", "Tag for " + agentId, QLineEdit::Normal,tag, &inputOk); + QString newTag = QInputDialog::getText(nullptr, "Set tags", "New tag", QLineEdit::Normal,tag, &inputOk); if (inputOk && !newTag.isEmpty()) { QString message = QString(); bool ok = false; - bool result = HttpReqAgentSetTag(agentId, newTag, *(adaptixWidget->GetProfile()), &message, &ok); + bool result = HttpReqAgentSetTag(listId, newTag, *(adaptixWidget->GetProfile()), &message, &ok); if( !result ) { MessageError("JWT error"); return; @@ -255,7 +298,7 @@ void SessionsTableWidget::actionAgentHide() void SessionsTableWidget::actionAgentRemove() { - QList listId; + QStringList listId; auto adaptixWidget = qobject_cast( mainWidget ); for( int rowIndex = 0 ; rowIndex < tableWidget->rowCount() ; rowIndex++ ) { @@ -265,13 +308,15 @@ void SessionsTableWidget::actionAgentRemove() } } - for (auto id : listId) { - QString message = QString(); - bool ok = false; - bool result = HttpReqAgentRemove(id, *(adaptixWidget->GetProfile()), &message, &ok); - if( !result ) { - MessageError("JWT error"); - return; - } + if(listId.empty()) + return; + + QString message = QString(); + bool ok = false; + bool result = HttpReqAgentRemove(listId, *(adaptixWidget->GetProfile()), &message, &ok); + if( !result ) { + MessageError("JWT error"); + return; } } + diff --git a/Client/Source/Utils/FileSystem.cpp b/Client/Source/Utils/FileSystem.cpp index 1917fcbf..2ea3de2a 100644 --- a/Client/Source/Utils/FileSystem.cpp +++ b/Client/Source/Utils/FileSystem.cpp @@ -25,6 +25,9 @@ QString GetBasenameWindows(const QString& path) QString GetRootPathWindows(const QString& path) { + if (path.startsWith("\\\\") && path.count("\\") == 2) + return path; + if (path.startsWith("\\\\")) { int secondSlash = path.indexOf('\\', 2); if (secondSlash != -1) { @@ -45,7 +48,7 @@ QString GetParentPathWindows(const QString& path) if (path.length() == 2 && path[1] == ':') return path; - if (path.startsWith("\\\\") && !path.contains('\\')) + if (path.startsWith("\\\\") && path.count("\\") == 2) return path; QString parentPath = path;