From 5c30b0e766928525fd7c938f55df1d2d6f77fbb9 Mon Sep 17 00:00:00 2001 From: wh1te909 Date: Wed, 14 Oct 2020 20:34:41 -0700 Subject: [PATCH] finish install --- agent/agent_windows.go | 108 ++++++++++++------- agent/install_windows.go | 221 ++++++++++++++++++++++++++++++++++++--- agent/service_windows.go | 4 +- build/onit.bmp | Bin 0 -> 23022 bytes build/saltcustom | 9 ++ build/setup-x86.iss | 87 +++++++++++++++ build/setup.iss | 87 +++++++++++++++ main.go | 30 +++--- 8 files changed, 476 insertions(+), 70 deletions(-) create mode 100644 build/onit.bmp create mode 100644 build/saltcustom create mode 100644 build/setup-x86.iss create mode 100644 build/setup.iss diff --git a/agent/agent_windows.go b/agent/agent_windows.go index ec87c53..f88c312 100644 --- a/agent/agent_windows.go +++ b/agent/agent_windows.go @@ -34,12 +34,14 @@ type WindowsAgent struct { DB Host ProgramDir string + EXE string SystemDrive string SaltCall string Nssm string SaltMinion string SaltInstaller string MeshInstaller string + MeshSVC string PyBin string Headers map[string]string Logger *logrus.Logger @@ -52,6 +54,7 @@ func New(logger *logrus.Logger, version string) *WindowsAgent { host, _ := ps.Host() info := host.Info() pd := filepath.Join(os.Getenv("ProgramFiles"), "TacticalAgent") + exe := filepath.Join(pd, "tacticalrmm.exe") dbFile := filepath.Join(pd, "agentdb.db") sd := os.Getenv("SystemDrive") pybin := filepath.Join(sd, "\\salt", "bin", "python.exe") @@ -82,12 +85,14 @@ func New(logger *logrus.Logger, version string) *WindowsAgent { Timezone: info.Timezone, }, ProgramDir: pd, + EXE: exe, SystemDrive: sd, SaltCall: sc, Nssm: nssm, SaltMinion: saltexe, SaltInstaller: saltinstaller, MeshInstaller: mesh, + MeshSVC: "mesh agent", PyBin: pybin, Headers: headers, Logger: logger, @@ -356,18 +361,24 @@ func (a *WindowsAgent) GetDisks() []Disk { } // CMDShell mimics python's `subprocess.run(shell=True)` -// -// Context timeout won't work here since cmd.exe spawns a child process -// and golang's defer will only kill the parent process which will already -// -// for example, passing `ping 8.8.8.8 -t` here will run forever even with a timeout set -// -// Passing `detached` will also ensure the function returns immediately and will never hang, -// rather continue to run in the background -func CMDShell(command string, detached bool) (output [2]string, e error) { - var outb, errb bytes.Buffer +func CMDShell(cmdArgs []string, command string, timeout int, detached bool) (output [2]string, e error) { + var ( + outb bytes.Buffer + errb bytes.Buffer + cmd *exec.Cmd + timedOut bool = false + ) + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + if len(cmdArgs) > 0 && command == "" { + cmdArgs = append([]string{"/C"}, cmdArgs...) + cmd = exec.Command("cmd.exe", cmdArgs...) + } else { + cmd = exec.Command("cmd.exe", "/C", command) + } - cmd := exec.Command("cmd.exe", "/C", command) // https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags if detached { cmd.SysProcAttr = &windows.SysProcAttr{ @@ -376,10 +387,26 @@ func CMDShell(command string, detached bool) (output [2]string, e error) { } cmd.Stdout = &outb cmd.Stderr = &errb - err := cmd.Run() + err := cmd.Start() + + pid := int32(cmd.Process.Pid) + + go func(p int32) { + + <-ctx.Done() + + _ = KillProc(p) + timedOut = true + }(pid) + + err = cmd.Wait() + + if timedOut { + return [2]string{outb.String(), errb.String()}, ctx.Err() + } if err != nil { - return [2]string{"", ""}, fmt.Errorf("%s: %s", err, errb.String()) + return [2]string{outb.String(), errb.String()}, err } return [2]string{outb.String(), errb.String()}, nil @@ -413,9 +440,9 @@ func CMD(exe string, args []string, timeout int, detached bool) (output [2]strin // EnablePing enables ping func EnablePing() { - fmt.Println("Enabling ping...") + args := make([]string, 0) cmd := `netsh advfirewall firewall add rule name="ICMP Allow incoming V4 echo request" protocol=icmpv4:8,any dir=in action=allow` - _, err := CMDShell(cmd, false) + _, err := CMDShell(args, cmd, 10, false) if err != nil { fmt.Println(err) } @@ -423,7 +450,6 @@ func EnablePing() { // EnableRDP enables Remote Desktop func EnableRDP() { - fmt.Println("Enabling RDP...") k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Terminal Server`, registry.ALL_ACCESS) if err != nil { fmt.Println(err) @@ -435,8 +461,9 @@ func EnableRDP() { fmt.Println(err) } + args := make([]string, 0) cmd := `netsh advfirewall firewall set rule group="remote desktop" new enable=Yes` - _, cerr := CMDShell(cmd, false) + _, cerr := CMDShell(args, cmd, 10, false) if cerr != nil { fmt.Println(cerr) } @@ -444,7 +471,6 @@ func EnableRDP() { // DisableSleepHibernate disables sleep and hibernate func DisableSleepHibernate() { - fmt.Println("Disabling sleep/hibernate...") k, _, err := registry.CreateKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Power`, registry.ALL_ACCESS) if err != nil { fmt.Println(err) @@ -456,21 +482,23 @@ func DisableSleepHibernate() { fmt.Println(err) } + args := make([]string, 0) + var wg sync.WaitGroup currents := []string{"ac", "dc"} for _, i := range currents { wg.Add(1) go func(c string) { defer wg.Done() - CMDShell(fmt.Sprintf("powercfg /set%svalueindex scheme_current sub_buttons lidaction 0", c), false) - CMDShell(fmt.Sprintf("powercfg /x -standby-timeout-%s 0", c), false) - CMDShell(fmt.Sprintf("powercfg /x -hibernate-timeout-%s 0", c), false) - CMDShell(fmt.Sprintf("powercfg /x -disk-timeout-%s 0", c), false) - CMDShell(fmt.Sprintf("powercfg /x -monitor-timeout-%s 0", c), false) + _, _ = CMDShell(args, fmt.Sprintf("powercfg /set%svalueindex scheme_current sub_buttons lidaction 0", c), 5, false) + _, _ = CMDShell(args, fmt.Sprintf("powercfg /x -standby-timeout-%s 0", c), 5, false) + _, _ = CMDShell(args, fmt.Sprintf("powercfg /x -hibernate-timeout-%s 0", c), 5, false) + _, _ = CMDShell(args, fmt.Sprintf("powercfg /x -disk-timeout-%s 0", c), 5, false) + _, _ = CMDShell(args, fmt.Sprintf("powercfg /x -monitor-timeout-%s 0", c), 5, false) }(i) } wg.Wait() - CMDShell("powercfg -S SCHEME_CURRENT", false) + _, _ = CMDShell(args, "powercfg -S SCHEME_CURRENT", 5, false) } // LoggedOnUser returns active logged on console user @@ -564,18 +592,7 @@ func (a *WindowsAgent) RecoverSalt() { a.Logger.Debugln("Salt recovery completed on", a.Hostname) } -//RecoverMesh recovers mesh agent -func (a *WindowsAgent) RecoverMesh() { - meshSVC := "mesh agent" - a.Logger.Debugln("Attempting mesh recovery on", a.Hostname) - defer CMD("sc.exe", []string{"start", meshSVC}, 20, false) - - args := []string{"stop", meshSVC} - CMD("sc.exe", args, 45, false) - WaitForService(meshSVC, "stopped", 5) - a.ForceKillMesh() - - var meshexe string +func (a *WindowsAgent) getMeshEXE() (meshexe string) { mesh1 := filepath.Join(os.Getenv("ProgramFiles"), "Mesh Agent", "MeshAgent.exe") mesh2 := filepath.Join(a.ProgramDir, a.MeshInstaller) if FileExists(mesh1) { @@ -583,6 +600,20 @@ func (a *WindowsAgent) RecoverMesh() { } else { meshexe = mesh2 } + return meshexe +} + +//RecoverMesh recovers mesh agent +func (a *WindowsAgent) RecoverMesh() { + a.Logger.Debugln("Attempting mesh recovery on", a.Hostname) + defer CMD("sc.exe", []string{"start", a.MeshSVC}, 20, false) + + args := []string{"stop", a.MeshSVC} + CMD("sc.exe", args, 45, false) + WaitForService(a.MeshSVC, "stopped", 5) + a.ForceKillMesh() + + meshexe := a.getMeshEXE() out, err := CMD(meshexe, []string{"-nodeidhex"}, 10, false) if err != nil { @@ -640,10 +671,7 @@ func (a *WindowsAgent) RecoverMesh() { func (a *WindowsAgent) RecoverCMD(command string) { a.Logger.Debugln("Attempting shell recovery on", a.Hostname) a.Logger.Debugln(command) - cmd := exec.Command("cmd.exe", "/C", command) - if err := cmd.Run(); err != nil { - a.Logger.Debugln(err) - } + _, _ = CMDShell([]string{}, command, 18000, true) } // ShowStatus prints windows service status diff --git a/agent/install_windows.go b/agent/install_windows.go index 2e7605b..fb2331d 100644 --- a/agent/install_windows.go +++ b/agent/install_windows.go @@ -11,6 +11,7 @@ import ( "time" "github.com/go-resty/resty/v2" + "github.com/gonutz/w32" ) type Installer struct { @@ -32,6 +33,8 @@ type Installer struct { } func (a *WindowsAgent) Install(i *Installer) { + a.checkExistingAndRemove() + i.Headers = map[string]string{ "content-type": "application/json", "Authorization": fmt.Sprintf("Token %s", i.Token), @@ -45,14 +48,14 @@ func (a *WindowsAgent) Install(i *Installer) { } if u.Scheme != "https" && u.Scheme != "http" { - a.installerMsg("Invalid URL (must contain https or http)\nInstallation Failed", "error") + a.installerMsg("Invalid URL (must contain https or http)", "error") } i.SaltMaster = u.Host a.Logger.Debugln("Salt Master:", i.SaltMaster) baseURL := u.Scheme + "://" + u.Host - a.Logger.Debugln(baseURL) + a.Logger.Debugln("Base URL:", baseURL) minion := filepath.Join(a.ProgramDir, a.SaltInstaller) a.Logger.Debugln("Salt Minion:", minion) @@ -60,7 +63,7 @@ func (a *WindowsAgent) Install(i *Installer) { rClient := resty.New() rClient.SetCloseConnection(true) rClient.SetTimeout(i.Timeout * time.Second) - rClient.SetDebug(a.Debug) + //rClient.SetDebug(a.Debug) // download or copy the salt-minion-setup.exe saltMin := filepath.Join(a.ProgramDir, a.SaltInstaller) @@ -135,33 +138,41 @@ func (a *WindowsAgent) Install(i *Installer) { agentToken := r.Result().(*TokenResp).Token - // install mesh agent - out, err := CMD(mesh, []string{"-fullinstall"}, int(60), false) - if err != nil { - a.installerMsg(fmt.Sprintf("Failed to install mesh agent: %s", err.Error()), "error") + a.Logger.Infoln("Installing mesh agent...") + a.Logger.Debugln("Mesh agent:", mesh) + meshOut, meshErr := CMD(mesh, []string{"-fullinstall"}, int(60), false) + if meshErr != nil { + a.installerMsg(fmt.Sprintf("Failed to install mesh agent: %s", meshErr.Error()), "error") } - if out[1] != "" { - a.installerMsg(fmt.Sprintf("Failed to install mesh agent: %s", out[1]), "error") + if meshOut[1] != "" { + a.installerMsg(fmt.Sprintf("Failed to install mesh agent: %s", meshOut[1]), "error") } - WaitForService("mesh agent", "running", 10) + fmt.Println(meshOut) + + a.Logger.Debugln("Waiting for mesh service to be running") + WaitForService(a.MeshSVC, "running", 15) + a.Logger.Debugln("Mesh service is running") + a.Logger.Debugln("Sleeping for 10") time.Sleep(10 * time.Second) meshSuccess := false var meshNodeID string for !meshSuccess { - pMesh, err := CMD(mesh, []string{"-nodeidhex"}, int(30), false) - if err != nil { - a.Logger.Errorln(err) + a.Logger.Debugln("Getting mesh node id hex") + pMesh, pErr := CMD(mesh, []string{"-nodeidhex"}, int(30), false) + if pErr != nil { + a.Logger.Errorln(pErr) time.Sleep(5 * time.Second) continue } - if out[1] != "" { - a.Logger.Errorln(out[1]) + if pMesh[1] != "" { + a.Logger.Errorln(pMesh[1]) time.Sleep(5 * time.Second) continue } meshNodeID = StripAll(pMesh[0]) + a.Logger.Debugln("Node id hex:", meshNodeID) if strings.Contains(strings.ToLower(meshNodeID), "not defined") { a.Logger.Errorln(meshNodeID) time.Sleep(5 * time.Second) @@ -197,7 +208,9 @@ func (a *WindowsAgent) Install(i *Installer) { agentPK := r.Result().(*NewAgentResp).AgentPK saltID := r.Result().(*NewAgentResp).SaltID - fmt.Println(agentToken, agentPK, saltID) + a.Logger.Debugln("Agent token:", agentToken) + a.Logger.Debugln("Agent PK:", agentPK) + a.Logger.Debugln("Salt ID:", saltID) // create the database db, err := sql.Open("sqlite3", filepath.Join(a.ProgramDir, "agentdb.db")) @@ -240,6 +253,156 @@ func (a *WindowsAgent) Install(i *Installer) { // refresh our agent with new values a = New(a.Logger, a.Version) + + // install salt + a.Logger.Debugln("changing dir to", a.ProgramDir) + cdErr := os.Chdir(a.ProgramDir) + if cdErr != nil { + a.installerMsg(cdErr.Error(), "error") + } + + a.Logger.Infoln("Installing the salt-minion, this might take a while...") + saltInstallArgs := []string{ + a.SaltInstaller, + "/S", + "/custom-config=saltcustom", + fmt.Sprintf("/master=%s", i.SaltMaster), + fmt.Sprintf("/minion-name=%s", saltID), + "/start-minion=1", + } + + a.Logger.Debugln("Installing salt with:", saltInstallArgs) + _, saltErr := CMDShell(saltInstallArgs, "", int(i.Timeout), false) + if saltErr != nil { + a.installerMsg(fmt.Sprintf("Unable to install salt: %s", saltErr.Error()), "error") + } + + a.Logger.Debugln("Waiting for salt-minion service enter the running state") + WaitForService("salt-minion", "running", 30) + a.Logger.Debugln("Salt-minion is running") + _, serr := WinServiceGet("salt-minion") + if serr != nil { + a.installerMsg("Salt installation failed\nCheck the log file in c:\\salt\\var\\log\\salt\\minion", "error") + } + + time.Sleep(5 * time.Second) + + // set new headers, no longer knox auth...use agent auth + rClient.SetHeaders(a.Headers) + + // accept the salt key on the rmm + a.Logger.Debugln("Registering salt with the RMM") + acceptPayload := map[string]string{"saltid": saltID, "agent_id": a.AgentID} + acceptAttempts := 0 + acceptRetries := 20 + for { + r, err := rClient.R().SetBody(acceptPayload).Post(fmt.Sprintf("%s/api/v2/saltminion/", baseURL)) + if err != nil { + a.Logger.Debugln(err) + acceptAttempts++ + time.Sleep(5 * time.Second) + } + + if r.StatusCode() != 200 { + a.Logger.Debugln(r.String()) + acceptAttempts++ + time.Sleep(5 * time.Second) + } else { + acceptAttempts = 0 + } + + if acceptAttempts == 0 { + a.Logger.Debugln(r.String()) + break + } else if acceptAttempts >= acceptRetries { + a.installerMsg("Unable to register salt with the RMM\nInstallation failed.", "error") + } + } + + time.Sleep(10 * time.Second) + + // sync salt modules + a.Logger.Debugln("Syncing salt modules") + syncPayload := map[string]string{"agent_id": a.AgentID} + syncAttempts := 0 + syncRetries := 20 + for { + r, err := rClient.R().SetBody(syncPayload).Patch(fmt.Sprintf("%s/api/v2/saltminion/", baseURL)) + if err != nil { + a.Logger.Debugln(err) + syncAttempts++ + time.Sleep(5 * time.Second) + } + + if r.StatusCode() != 200 { + a.Logger.Debugln(r.String()) + syncAttempts++ + time.Sleep(5 * time.Second) + } else { + syncAttempts = 0 + } + + if syncAttempts == 0 { + a.Logger.Debugln(r.String()) + break + } else if syncAttempts >= syncRetries { + a.installerMsg("Unable to sync salt modules\nInstallation failed.", "error") + } + } + + // send wmi sysinfo + a.Logger.Debugln("Getting sysinfo with WMI") + a.GetWMI() + + // remove existing services if exist + services := []string{"tacticalagent", "checkrunner"} + for _, svc := range services { + _, err := WinServiceGet(svc) + if err == nil { + a.Logger.Debugln(fmt.Sprintf("Found existing %s service. Removing", svc)) + _, _ = CMD(a.Nssm, []string{"stop", svc}, 30, false) + _, _ = CMD(a.Nssm, []string{"remove", svc, "confirm"}, 30, false) + } + } + + a.Logger.Infoln("Installing services...") + svcCommands := [8][]string{ + // winagentsvc + {"install", "tacticalagent", a.EXE, "-m", "winagentsvc"}, + {"set", "tacticalagent", "DisplayName", "Tactical RMM Agent"}, + {"set", "tacticalagent", "Description", "Tactical RMM Agent"}, + {"start", "tacticalagent"}, + //checkrunner + {"install", "checkrunner", a.EXE, "-m", "checkrunner"}, + {"set", "checkrunner", "DisplayName", "Tactical RMM Check Runner"}, + {"set", "checkrunner", "Description", "Tactical RMM Check Runner"}, + {"start", "checkrunner"}, + } + + for _, s := range svcCommands { + a.Logger.Debugln(s) + _, nssmErr := CMD(a.Nssm, s, 15, false) + if nssmErr != nil { + a.installerMsg(nssmErr.Error(), "error") + } + } + + if i.Power { + a.Logger.Infoln("Disabling sleep/hibernate...") + DisableSleepHibernate() + } + + if i.Ping { + a.Logger.Infoln("Enabling ping...") + EnablePing() + } + + if i.RDP { + a.Logger.Infoln("Enabling RDP...") + EnableRDP() + } + + a.installerMsg("Installation was successfull!\nAllow a few minutes for the agent to properly display in the RMM", "info") } func copyFile(src, dst string) error { @@ -261,3 +424,29 @@ func copyFile(src, dst string) error { } return nil } + +func (a *WindowsAgent) checkExistingAndRemove() { + installedMesh := filepath.Join(a.ProgramDir, "Mesh Agent", "MeshAgent.exe") + installedSalt := filepath.Join(a.SystemDrive, "\\salt", "uninst.exe") + agentDB := filepath.Join(a.ProgramDir, "agentdb.db") + if FileExists(installedMesh) || FileExists(installedSalt) || FileExists(agentDB) { + tacUninst := filepath.Join(a.ProgramDir, "unins000.exe") + tacUninstArgs := []string{tacUninst, "/VERYSILENT", "/SUPPRESSMSGBOXES"} + + window := w32.GetForegroundWindow() + if window != 0 { + var handle w32.HWND + msg := "Existing installation found\nClick OK to remove, then re-run the installer.\nClick Cancel to abort." + action := w32.MessageBox(handle, msg, "Tactical RMM", w32.MB_OKCANCEL|w32.MB_ICONWARNING) + if action == w32.IDOK { + _, _ = CMDShell(tacUninstArgs, "", 60, true) + w32.MessageBox(handle, "Uninstall finished", "Tactical RMM", w32.MB_OK|w32.MB_ICONINFORMATION) + } + } else { + fmt.Println("Existing installation found and must be removed before attempting to reinstall.") + fmt.Println("Run the following command to uninstall, and then re-run this installer.") + fmt.Printf("\"%s\" %s %s", tacUninstArgs[0], tacUninstArgs[1], tacUninstArgs[2]) + } + os.Exit(0) + } +} diff --git a/agent/service_windows.go b/agent/service_windows.go index 4af9a0d..5667ac7 100644 --- a/agent/service_windows.go +++ b/agent/service_windows.go @@ -29,8 +29,8 @@ type HelloPatch struct { BootTime int64 `json:"boot_time"` } -// RunAsService tacticalagent windows nssm service -func (a *WindowsAgent) RunAsService() { +// WinAgentSvc tacticalagent windows nssm service +func (a *WindowsAgent) WinAgentSvc() { a.Logger.Infoln("Agent service started") var data map[string]interface{} var sleep int diff --git a/build/onit.bmp b/build/onit.bmp new file mode 100644 index 0000000000000000000000000000000000000000..4fc5401147314274f7e94b9cf6603b3d887f88d4 GIT binary patch literal 23022 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 -client-id X -site-id X -auth ` - fmt.Println(u) + switch runtime.GOOS { + case "windows": + u := `Usage: tacticalrmm.exe -m install -api -client-id X -site-id X -auth ` + fmt.Println(u) + case "linux": + // todo + } }