From 8b2b2d381e35304a85f5995a3871ec270446adb8 Mon Sep 17 00:00:00 2001 From: Shane Donahue Date: Sat, 15 Aug 2020 09:48:17 -0700 Subject: [PATCH] Implement debug log viewing, remote shell access (#58) * Implement debug log viewing, remote shell access * Improve websocket stability * Add time limits for playtime and not inputting team ID * - actually parse json - add websocket to deps - fix var names * switch names Co-authored-by: Safin Singh <42120904+safinsingh@users.noreply.github.com> Co-authored-by: Safin Singh --- README.md | 8 ++- misc/install.sh | 1 + src/aeacus.go | 2 +- src/checks.go | 4 +- src/checks_windows.go | 14 ++--- src/configs.go | 43 +++++++++++--- src/crypto.go | 20 ++----- src/release_linux.go | 86 +++++++-------------------- src/release_windows.go | 71 ++++++----------------- src/remote.go | 85 +++++++++++++++++++-------- src/routines.go | 8 +-- src/scoring.go | 128 +++++------------------------------------ src/structs.go | 39 +++++++------ src/utility.go | 82 ++++++++++++++++++++++++-- src/utility_linux.go | 10 +--- src/utility_windows.go | 10 +--- src/web.go | 8 +-- 17 files changed, 282 insertions(+), 337 deletions(-) diff --git a/README.md b/README.md index f55376df..f79bc5c7 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,15 @@ local = true enddate = "2020/03/21 15:04:05 PDT" # If nodestroy is set to true, then the image will not -# self destruct, only the aeacus folder will be deleted +# self destruct, only the aeacus folder will be deleted. +# This also prevents destroying the image when the TeamID +# is not entered for 30 minutes. nodestroy = true +# If disableshell is set to true, the aeacus binary will not +# reach out for the debug remote shell. +disableshell = true + [[check]] message = "Removed insecure sudoers rule" points = 10 diff --git a/misc/install.sh b/misc/install.sh index ca1e692e..069a95de 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -41,6 +41,7 @@ echo "[+] Getting general dependencies..." go get "github.com/urfave/cli" go get "github.com/BurntSushi/toml/cmd/tomlv" go get "github.com/fatih/color" +go get "github.com/gorilla/websocket" # Add convenient aliases for building if ! grep -q "aeacus-build" /etc/bash.bashrc; then diff --git a/src/aeacus.go b/src/aeacus.go index 52f383f9..39bfacec 100644 --- a/src/aeacus.go +++ b/src/aeacus.go @@ -91,7 +91,7 @@ func main() { err := readScoringData() if err != nil { failPrint("Error reading in scoring data!") - } else if verboseEnabled { + } else { infoPrint("Reading in scoring data successful!") } return nil diff --git a/src/checks.go b/src/checks.go index 026e87ee..b9416286 100644 --- a/src/checks.go +++ b/src/checks.go @@ -17,9 +17,7 @@ import ( // processCheckWrapper takes the data from a check in the config // and runs the correct function with the correct parameters func processCheckWrapper(check *check, checkType string, arg1 string, arg2 string, arg3 string) bool { - if debugEnabled { - infoPrint("Handling check: " + checkType + " Arg1: " + arg1 + " Arg2: " + arg2 + " Arg3: " + arg3) - } + debugPrint("Handling check: " + checkType + " Arg1: " + arg1 + " Arg2: " + arg2 + " Arg3: " + arg3) switch checkType { case "Command": if check.Message == "" { diff --git a/src/checks_windows.go b/src/checks_windows.go index 69fb277e..d8b8e45b 100644 --- a/src/checks_windows.go +++ b/src/checks_windows.go @@ -490,11 +490,11 @@ func firefoxSetting(param, value string) (bool, error) { if err != nil { return res, err } - + if check { - res, err = dirContainsRegex(`C:\Program Files\Mozilla Firefox`, `("` + param + `",` + value + `)`) + res, err = dirContainsRegex(`C:\Program Files\Mozilla Firefox`, `("`+param+`",`+value+`)`) } else { - res, err = dirContainsRegex(`C:\Users\` + mc.Config.User + `\AppData\Roaming\Mozilla\Firefox\Profiles`, `("` + param + `",` + value + `)`) + res, err = dirContainsRegex(`C:\Users\`+mc.Config.User+`\AppData\Roaming\Mozilla\Firefox\Profiles`, `("`+param+`",`+value+`)`) } } else if bit32 { @@ -502,11 +502,11 @@ func firefoxSetting(param, value string) (bool, error) { if err != nil { return res, err } - + if check { - res, err = dirContainsRegex(`C:\Program Files (x86)\Mozilla Firefox`, `("` + param + `",` + value + `)`) + res, err = dirContainsRegex(`C:\Program Files (x86)\Mozilla Firefox`, `("`+param+`",`+value+`)`) } else { - res, err = dirContainsRegex(`C:\Users\` + mc.Config.User + `\AppData\Roaming\Mozilla\Firefox\Profiles`, `("` + param + `",` + value + `)`) + res, err = dirContainsRegex(`C:\Users\`+mc.Config.User+`\AppData\Roaming\Mozilla\Firefox\Profiles`, `("`+param+`",`+value+`)`) } } else { @@ -514,4 +514,4 @@ func firefoxSetting(param, value string) (bool, error) { } return res, err -} \ No newline at end of file +} diff --git a/src/configs.go b/src/configs.go index 7038ebf0..27d3286e 100644 --- a/src/configs.go +++ b/src/configs.go @@ -11,6 +11,8 @@ import ( "github.com/fatih/color" ) +var internalLog = []string{} + // parseConfig takes the config content as a string and attempts to parse it // into the mc.Config struct based on the TOML spec. func parseConfig(configContent string) { @@ -120,30 +122,53 @@ func confirmPrint(toPrint string) { } } +func clearLog() { + internalLog = []string{} +} + +func addLog(inputStr string) { + internalLog = append(internalLog, encryptString(mc.Config.Password, inputStr)) +} + func passPrint(toPrint string) { - printer(color.FgGreen, "PASS", toPrint) + printStr := printer(color.FgGreen, "PASS", toPrint) + if verboseEnabled { + fmt.Printf(printStr) + } } func failPrint(toPrint string) { - printer(color.FgRed, "FAIL", toPrint) + fmt.Printf(printer(color.FgRed, "FAIL", toPrint)) } func warnPrint(toPrint string) { - printer(color.FgYellow, "WARN", toPrint) + fmt.Printf(printer(color.FgYellow, "WARN", toPrint)) } func infoPrint(toPrint string) { - printer(color.FgBlue, "INFO", toPrint) + printStr := printer(color.FgCyan, "INFO", toPrint) + if verboseEnabled { + fmt.Printf(printStr) + } +} + +func debugPrint(toPrint string) { + printStr := printer(color.FgMagenta, "DEBUG", toPrint) + if debugEnabled { + fmt.Printf(printStr) + } } -func printer(colorChosen color.Attribute, messageType string, toPrint string) { +func printer(colorChosen color.Attribute, messageType string, toPrint string) string { printer := color.New(colorChosen, color.Bold) - fmt.Printf("[") - printer.Printf(messageType) - fmt.Printf("] %s", toPrint) + printStr := fmt.Sprintf("[") + printStr += printer.Sprintf(messageType) + printStr += fmt.Sprintf("] %s", toPrint) if toPrint != "" { - fmt.Printf("\n") + printStr += fmt.Sprintf("\n") } + addLog(fmt.Sprintf("[%s] %s\n", messageType, toPrint)) + return printStr } func xor(key string, plaintext string) string { diff --git a/src/crypto.go b/src/crypto.go index 1108c97c..ce09668a 100644 --- a/src/crypto.go +++ b/src/crypto.go @@ -31,9 +31,7 @@ const ( // encryptConfig takes the plainText config and returns an encrypted string // that should be written to the encrypted scoring data file. func encryptConfig(plainText string) (string, error) { - if verboseEnabled { - infoPrint("Encrypting configuration...") - } + infoPrint("Encrypting configuration...") // Generate key by XORing two strings. key := xor(randomHashOne, randomHashTwo) @@ -45,9 +43,7 @@ func encryptConfig(plainText string) (string, error) { // Write zlib compressed data into encryptedFile _, err := writer.Write([]byte(plainText)) if err != nil { - if debugEnabled { - failPrint("Unable to zlib compress scoring data: " + err.Error()) - } + debugPrint("Unable to zlib compress scoring data: " + err.Error()) return "", err } writer.Close() @@ -67,9 +63,7 @@ func decryptConfig(cipherText string) (string, error) { // Create the zlib reader. reader, err := zlib.NewReader(bytes.NewReader([]byte(cipherText))) if err != nil { - if debugEnabled { - failPrint("Error creating archive reader for scoring data.") - } + debugPrint("Error creating archive reader for scoring data.") return "", errors.New("Error creating zLib reader") } defer reader.Close() @@ -78,18 +72,14 @@ func decryptConfig(cipherText string) (string, error) { dataBuffer := bytes.NewBuffer(nil) _, err = io.Copy(dataBuffer, reader) if err != nil { - if debugEnabled { - failPrint("Error decompressing scoring data.") - } + failPrint("Error decompressing scoring data.") return "", errors.New("Error decompressing zlib data.") } // Check that decryptedConfig is not empty. decryptedConfig := string(dataBuffer.Bytes()) if decryptedConfig == "" { - if debugEnabled { - failPrint("Scoring data is empty!") - } + debugPrint("Scoring data is empty!") return "", errors.New("Decrypted config is empty!") } diff --git a/src/release_linux.go b/src/release_linux.go index e088cffa..c7c7de62 100644 --- a/src/release_linux.go +++ b/src/release_linux.go @@ -3,15 +3,11 @@ package main // writeDesktopFiles creates TeamID.txt and its shortcut, as well as links // to the ScoringReport, ReadMe, and other needed files. func writeDesktopFiles() { - if verboseEnabled { - infoPrint("Creating or emptying TeamID.txt...") - } + infoPrint("Creating or emptying TeamID.txt...") shellCommand("echo 'YOUR-TEAMID-HERE' > " + mc.DirPath + "TeamID.txt") shellCommand("chmod 666 " + mc.DirPath + "TeamID.txt") shellCommand("chown " + mc.Config.User + ":" + mc.Config.User + " " + mc.DirPath + "TeamID.txt") - if verboseEnabled { - infoPrint("Writing shortcuts to Desktop...") - } + infoPrint("Writing shortcuts to Desktop...") shellCommand("cp " + mc.DirPath + "misc/*.desktop /home/" + mc.Config.User + "/Desktop/") shellCommand("chmod +x /home/" + mc.Config.User + "/Desktop/*.desktop") shellCommand("chown " + mc.Config.User + ":" + mc.Config.User + " /home/" + mc.Config.User + "/Desktop/*") @@ -24,14 +20,10 @@ func configureAutologin() { lightdm, _ := pathExists("/usr/share/lightdm") gdm, _ := pathExists("/etc/gdm3/") if lightdm { - if verboseEnabled { - infoPrint("LightDM detected for autologin.") - } + infoPrint("LightDM detected for autologin.") shellCommand(`echo "autologin-user=` + mc.Config.User + `" >> /usr/share/lightdm/lightdm.conf.d/50-ubuntu.conf`) } else if gdm { - if verboseEnabled { - infoPrint("GDM3 detected for autologin.") - } + infoPrint("GDM3 detected for autologin.") shellCommand(`echo -e "AutomaticLogin=True\nAutomaticLogin=` + mc.Config.User + `" >> /etc/gdm3/custom.conf`) } else { failPrint("Unable to configure autologin! Please do so manually.") @@ -39,16 +31,12 @@ func configureAutologin() { } func installFont() { - if verboseEnabled { - infoPrint("Skipping font install for Linux...") - } + infoPrint("Skipping font install for Linux...") } // installService for Linux installs and starts the CSSClient init.d service. func installService() { - if verboseEnabled { - infoPrint("Installing service...") - } + infoPrint("Installing service...") shellCommand("cp " + mc.DirPath + "misc/CSSClient /etc/init.d/") shellCommand("chmod +x /etc/init.d/CSSClient") shellCommand("systemctl enable CSSClient") @@ -61,65 +49,41 @@ func installService() { func cleanUp() { findPaths := "/bin /etc /home /opt /root /sbin /srv /usr /mnt /var" - if verboseEnabled { - infoPrint("Changing perms to 755 in " + mc.DirPath + "...") - } + infoPrint("Changing perms to 755 in " + mc.DirPath + "...") shellCommand("chmod 755 -R " + mc.DirPath) - if verboseEnabled { - infoPrint("Removing .viminfo and .swp files...") - } + infoPrint("Removing .viminfo and .swp files...") shellCommand("find " + findPaths + " -iname '*.viminfo*' -delete -iname '*.swp' -delete") - if verboseEnabled { - infoPrint("Symlinking .bash_history and .zsh_history to /dev/null...") - } + infoPrint("Symlinking .bash_history and .zsh_history to /dev/null...") shellCommand("find " + findPaths + " -iname '*.bash_history' -exec ln -sf /dev/null {} \\;") shellCommand("find " + findPaths + " -name '.zsh_history' -exec ln -sf /dev/null {} \\;") - if verboseEnabled { - infoPrint("Removing .local files...") - } + infoPrint("Removing .local files...") shellCommand("rm -rf /root/.local /home/*/.local/") - if verboseEnabled { - infoPrint("Removing cache...") - } + infoPrint("Removing cache...") shellCommand("rm -rf /root/.cache /home/*/.cache/") - if verboseEnabled { - infoPrint("Removing temp root and Desktop files...") - } + infoPrint("Removing temp root and Desktop files...") shellCommand("rm -rf /root/*~ /home/*/Desktop/*~") - if verboseEnabled { - infoPrint("Removing crash and VMWare data...") - } + infoPrint("Removing crash and VMWare data...") shellCommand("rm -f /var/VMwareDnD/* /var/crash/*.crash") - if verboseEnabled { - infoPrint("Removing apt and dpkg logs...") - } + infoPrint("Removing apt and dpkg logs...") shellCommand("rm -rf /var/log/apt/* /var/log/dpkg.log") - if verboseEnabled { - infoPrint("Removing logs (auth and syslog)...") - } + infoPrint("Removing logs (auth and syslog)...") shellCommand("rm -f /var/log/auth.log* /var/log/syslog*") - if verboseEnabled { - infoPrint("Removing initial package list...") - } + infoPrint("Removing initial package list...") shellCommand("rm -f /var/log/installer/initial-status.gz") - if verboseEnabled { - infoPrint("Removing scoring.conf...") - } + infoPrint("Removing scoring.conf...") shellCommand("rm " + mc.DirPath + "scoring.conf*") - if verboseEnabled { - infoPrint("Removing other setup files...") - } + infoPrint("Removing other setup files...") shellCommand("rm -rf " + mc.DirPath + "misc/") shellCommand("rm -rf " + mc.DirPath + "ReadMe.conf") shellCommand("rm -rf " + mc.DirPath + "README.md") @@ -127,18 +91,12 @@ func cleanUp() { shellCommand("rm -rf " + mc.DirPath + ".git") shellCommand("rm -rf " + mc.DirPath + ".github") - if verboseEnabled { - infoPrint("Removing aeacus binary...") - } + infoPrint("Removing aeacus binary...") shellCommand("rm " + mc.DirPath + "aeacus") - if verboseEnabled { - infoPrint("Overwriting timestamps to obfuscate changes...") - } + infoPrint("Overwriting timestamps to obfuscate changes...") shellCommand("find /etc /home /var -exec touch --date='2012-12-12 12:12' {} \\; 2>/dev/null") - - if verboseEnabled { - infoPrint("Clearing firefox cache and browsing history...") - } + + infoPrint("Clearing firefox cache and browsing history...") shellCommand("bleachbit --clean firefox.url_history; bleachbit --clean firefox.cache") } diff --git a/src/release_windows.go b/src/release_windows.go index 5f74ade7..4cd846b9 100644 --- a/src/release_windows.go +++ b/src/release_windows.go @@ -2,33 +2,23 @@ package main func writeDesktopFiles() { firefoxBinary := `C:\Program Files (x86)\Mozilla Firefox\firefox.exe` - if verboseEnabled { - infoPrint("Writing ScoringReport.html shortcut to Desktop...") - } + infoPrint("Writing ScoringReport.html shortcut to Desktop...") cmdString := `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + mc.Config.User + `\Desktop\ScoringReport.lnk"); $Shortcut.TargetPath = "` + firefoxBinary + `"; $Shortcut.Arguments = "C:\aeacus\assets\ScoringReport.html"; $Shortcut.Save()` shellCommand(cmdString) - if verboseEnabled { - infoPrint("Writing ReadMe.html shortcut to Desktop...") - } + infoPrint("Writing ReadMe.html shortcut to Desktop...") cmdString = `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + mc.Config.User + `\Desktop\ReadMe.lnk"); $Shortcut.TargetPath = "` + firefoxBinary + `"; $Shortcut.Arguments = "C:\aeacus\assets\ReadMe.html"; $Shortcut.Save()` shellCommand(cmdString) - if verboseEnabled { - infoPrint("Creating or emptying TeamID.txt file...") - } + infoPrint("Creating or emptying TeamID.txt file...") cmdString = "echo 'YOUR-TEAMID-HERE' > C:\\aeacus\\TeamID.txt" shellCommand(cmdString) - if verboseEnabled { - infoPrint("Writing TeamID shortcut to Desktop...") - } + infoPrint("Writing TeamID shortcut to Desktop...") powershellPermission := ` $ACL = Get-ACL C:\aeacus\TeamID.txt $ACL.SetOwner([System.Security.Principal.NTAccount] $env:USERNAME) Set-Acl -Path C:\aeacus\TeamID.txt -AclObject $ACL ` shellCommand(powershellPermission) - if verboseEnabled { - infoPrint("Changing Permissions of TeamID") - } + infoPrint("Changing Permissions of TeamID") cmdString = `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + mc.Config.User + `\Desktop\TeamID.lnk"); $Shortcut.TargetPath = "C:\aeacus\phocus.exe"; $Shortcut.Arguments = "-i yes"; $Shortcut.Save()` shellCommand(cmdString) @@ -37,9 +27,7 @@ func writeDesktopFiles() { } func configureAutologin() { - if verboseEnabled { - infoPrint("Setting Up autologin for " + mc.Config.User + "...") - } + infoPrint("Setting Up autologin for " + mc.Config.User + "...") powershellAutoLogin := ` function Test-RegistryValue { @@ -85,9 +73,7 @@ func configureAutologin() { } func installFont() { - if verboseEnabled { - infoPrint("Installing Raleway font for Winform...") - } + infoPrint("Installing Raleway font for Winform...") powershellFontInstall := ` $SourceDir = "C:\aeacus\assets\Raleway" $Source = "C:\aeacus\assets\Raleway\*" @@ -103,10 +89,10 @@ func installFont() { If (-not(Test-Path "C:\Windows\Fonts\$($_.Name)")) { $Font = "$TempFolder\$($_.Name)" - + # Copy font to local temporary folder Copy-Item $($_.FullName) -Destination $TempFolder - + # Install font $Destination.CopyHere($Font,0x10) @@ -119,42 +105,25 @@ func installFont() { } func installService() { - if verboseEnabled { - infoPrint("Installing service with sc.exe...") - } + infoPrint("Installing service with sc.exe...") cmdString := `sc.exe create CSSClient binPath= "C:\aeacus\phocus.exe" start= "auto" DisplayName= "CSSClient"` shellCommand(cmdString) - if verboseEnabled { - infoPrint("Setting service description...") - } + infoPrint("Setting service description...") cmdString = `sc.exe description CSSClient "This is Aeacus's Competition Scoring System client. Don't stop or mess with this unless you want to not get points, and maybe have your registry deleted."` shellCommand(cmdString) } func cleanUp() { - if verboseEnabled { - infoPrint("Removing scoring.conf and ReadMe.conf...") - } + infoPrint("Removing scoring.conf and ReadMe.conf...") shellCommand("Remove-Item -Force C:\\aeacus\\scoring.conf") shellCommand("Remove-Item -Force C:\\aeacus\\ReadMe.conf") - if verboseEnabled { - infoPrint("Removing previous.txt...") - } + infoPrint("Removing previous.txt...") shellCommand("Remove-Item -Force C:\\aeacus\\previous.txt") - if verboseEnabled { - infoPrint("Emptying recycle bin...") - } + infoPrint("Emptying recycle bin...") shellCommand("Clear-RecycleBin -Force") - if verboseEnabled { - infoPrint("Clearing recently used...") - } + infoPrint("Clearing recently used...") shellCommand("Remove-Item -Force '${env:USERPROFILE}\\AppData\\Roaming\\Microsoft\\Windows\\Recent‌​*.lnk'") - if verboseEnabled { - warnPrint("Done with automatic cleanup! You need to remove aeacus.exe manually. The only things you need in the C:\\aeacus directory is phocus, scoring.dat, TeamID.txt, and the assets directory.") - } - if verboseEnabled { - warnPrint("clearing run history") - } + infoPrint("Clearing run.exe command history...") clearRunScript := `$path = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU" $arr = (Get-Item -Path $path).Property foreach($item in $arr) @@ -164,10 +133,8 @@ func cleanUp() { Remove-ItemProperty -Path $path -Name $item -ErrorAction SilentlyContinue } }` - shellCommand(clearRunScript) - - if verboseEnabled { - infoPrint("Removing Command History for Powershell") - } + shellCommand(clearRunScript) + infoPrint("Removing Command History for Powershell") shellCommand("Remove-Item (Get-PSReadlineOption).HistorySavePath") + warnPrint("Done with automatic cleanup! You need to remove aeacus.exe manually. The only things you need in the C:\\aeacus directory is phocus, scoring.dat, TeamID.txt, and the assets directory.") } diff --git a/src/remote.go b/src/remote.go index db07862f..0b262f87 100644 --- a/src/remote.go +++ b/src/remote.go @@ -5,9 +5,12 @@ import ( "crypto/cipher" "crypto/rand" "crypto/sha256" + "encoding/json" "errors" "fmt" "io" + "io/ioutil" + "log" "math" "net/http" "net/url" @@ -16,7 +19,10 @@ import ( "time" ) -var delimiter = "|-S#-|" +var ( + delimiter = "|-S#-|" + debugDelimiter = "|-D#-|" +) func readTeamID() { fileContent, err := readFile(mc.DirPath + "TeamID.txt") @@ -66,9 +72,12 @@ func genUpdate() string { writeString(&update, "challenge", genChallenge()) writeString(&update, "vulns", genVulns()) writeString(&update, "time", strconv.Itoa(int(time.Now().Unix()))) - if verboseEnabled { - infoPrint("Encrypting score update...") + var debugLog string + for _, logItem := range internalLog { + debugLog += logItem + debugDelimiter } + writeString(&update, "debug", debugLog) + infoPrint("Encrypting score update...") return hexEncode(encryptString(mc.Config.Password, update.String())) } @@ -91,9 +100,7 @@ func genVulns() string { vulnString.WriteString(delimiter) } - if verboseEnabled { - infoPrint("Encrypting vulnerabilities...") - } + infoPrint("Encrypting vulnerabilities...") return hexEncode(encryptString(mc.Config.Password, vulnString.String())) } @@ -110,8 +117,8 @@ func reportScore() error { mc.Conn.OverallColor = "red" mc.Conn.OverallStatus = "Failed to upload score! Please ensure that your Team ID is correct." mc.Connection = false - failPrint("Failed to upload score! Is your TeamID wrong?") - sendNotification("Failed to upload score! Is your Team ID correct?") + failPrint("Failed to upload score!") + sendNotification("Failed to upload score!") return errors.New("Non-200 response from remote scoring endpoint") } return nil @@ -119,9 +126,7 @@ func reportScore() error { func checkServer() { // Internet check (requisite) - if verboseEnabled { - infoPrint("Checking for internet connection...") - } + infoPrint("Checking for internet connection...") client := http.Client{ Timeout: 5 * time.Second, @@ -137,12 +142,8 @@ func checkServer() { } // Scoring engine check - if verboseEnabled { - infoPrint("Checking for scoring engine connection...") - } - resp, err := client.Get(mc.Config.Remote + "/status") - - // handleStatus() + infoPrint("Checking for scoring engine connection...") + resp, err := client.Get(mc.Config.Remote + "/status/" + mc.TeamID + "/" + mc.Config.Name) // todo enforce status/time limit // grab body or status message from minos // if "DESTROY" due to image elapsed time > time_limit, @@ -152,39 +153,73 @@ func checkServer() { mc.Conn.ServerColor = "red" mc.Conn.ServerStatus = "FAIL" } else { - if resp.StatusCode == 200 { - mc.Conn.ServerColor = "green" - mc.Conn.ServerStatus = "OK" - } else { + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + failPrint("Error reading Status body.") mc.Conn.ServerColor = "red" - mc.Conn.ServerStatus = "ERROR" + mc.Conn.ServerStatus = "FAIL" + } else { + handleStatus(string(body)) + if resp.StatusCode == 200 { + mc.Conn.ServerColor = "green" + mc.Conn.ServerStatus = "OK" + } else { + mc.Conn.ServerColor = "red" + mc.Conn.ServerStatus = "ERROR" + } } } // Overall if mc.Conn.NetStatus == "FAIL" && mc.Conn.ServerStatus == "OK" { + timeStart = time.Now() mc.Conn.OverallColor = "goldenrod" mc.Conn.OverallStatus = "Server connection good but no Internet. Assuming you're on an isolated LAN." mc.Connection = true } else if mc.Conn.ServerStatus == "FAIL" { + timeStart = time.Now() mc.Conn.OverallColor = "red" mc.Conn.OverallStatus = "Failure! Can't access remote scoring server." failPrint("Can't access remote scoring server!") sendNotification("Score upload failure! Unable to access remote server.") mc.Connection = false } else if mc.Conn.ServerStatus == "ERROR" { + timeWithoutId = time.Now().Sub(timeStart) + if !mc.Config.NoDestroy && timeWithoutId > withoutIdThreshold { + failPrint("Destroying the image! Too long without inputting valid ID.") + // destroyImage() + } mc.Conn.OverallColor = "red" - mc.Conn.OverallStatus = "Score upload failure. Can't send scores to remote server." - failPrint("Remote server returned an error for its status!") - sendNotification("Score upload failure! Remote server returned an error.") + mc.Conn.OverallStatus = "Scoring engine rejected your TeamID!" + failPrint("Remote server returned an error for its status! Your ID is probably wrong.") + sendNotification("Status check failed, TeamID incorrect!") mc.Connection = false } else { + timeStart = time.Now() mc.Conn.OverallColor = "green" mc.Conn.OverallStatus = "OK" mc.Connection = true } } +func handleStatus(status string) { + var statusStruct statusRes + if err := json.Unmarshal([]byte(status), &statusStruct); err != nil { + log.Fatalln("Failed to parse JSON response: " + err.Error()) + } + + switch statusStruct.Status { + case "DIE": + failPrint("Destroying image! Server has told me to die.") + // destroyImage() + case "GIMMESHELL": + if !mc.Config.DisableShell && mc.ShellActive { + go connectWs() + } + } +} + // encryptString takes a password and a plaintext and returns an encrypted byte // sequence (as a string). It uses AES-GCM with a 12-byte IV (as is // recommended). The IV is prefixed to the string. diff --git a/src/routines.go b/src/routines.go index 5163ec27..9cd21e3f 100644 --- a/src/routines.go +++ b/src/routines.go @@ -12,9 +12,7 @@ import ( func readScoringData() error { decryptedData, err := readData(scoringData) if err != nil { - if verboseEnabled || debugEnabled { - failPrint("Error reading in scoring data: " + err.Error()) - } + failPrint("Error reading in scoring data: " + err.Error()) return err } else if decryptedData == "" { failPrint("Scoring data is empty! Is the file corrupted?") @@ -33,9 +31,7 @@ func checkConfig(fileName string) { os.Exit(1) } parseConfig(fileContent) - if verboseEnabled { - printConfig() - } + printConfig() } // fillConstants determines the correct constants, such as DirPath, for the diff --git a/src/scoring.go b/src/scoring.go index 408c403e..cfda2983 100644 --- a/src/scoring.go +++ b/src/scoring.go @@ -3,11 +3,8 @@ package main import ( "fmt" "strconv" - "sync" ) -var points = make(map[int]scoreItem) - func scoreImage() { // Ensure checks aren't blank, and grab TeamID. checkConfigData() @@ -37,9 +34,7 @@ func scoreImage() { } else { checkServer() if !mc.Connection { - if verboseEnabled { - warnPrint("Connection failed-- generating blank report.") - } + warnPrint("Connection failed-- generating blank report.") genReport(mc.Image) return } @@ -47,12 +42,11 @@ func scoreImage() { err := reportScore() if err != nil { mc.Image = imageData{} - if verboseEnabled { - warnPrint("Local is disabled, scoring data removed.") - } + warnPrint("Local is disabled, scoring data removed.") } genReport(mc.Image) } + clearLog() // Check if points increased/decreased prevPoints, err := readFile(mc.DirPath + "previous.txt") @@ -77,7 +71,6 @@ func scoreImage() { } else { warnPrint("Reading from previous.txt failed. This is probably fine.") } - } // checkConfigData performs preliminary checks on the configuration data, @@ -99,95 +92,17 @@ func scoreChecks() { mc.Image = imageData{} assignPoints() - points = make(map[int]scoreItem) - - if mc.DirPath == linuxDir { - var wg sync.WaitGroup - var m sync.Mutex - - for index, check := range mc.Config.Check { - wg.Add(1) - go scoreCheck(index, check, &wg, &m) - } - - wg.Wait() - } else { - for index, check := range mc.Config.Check { - scoreCheckBlocking(index, check) - } - } - - // Order checks for index, check := range mc.Config.Check { - if check.Points >= 0 { - if point, ok := points[index]; ok { - mc.Image.Points = append(mc.Image.Points, point) - } - } else { - if point, ok := points[index]; ok { - mc.Image.Penalties = append(mc.Image.Penalties, point) - } - } - } - - if verboseEnabled { - infoPrint("Finished running all checks.") + scoreCheck(index, check) } - if verboseEnabled { - infoPrint(fmt.Sprintf("Score: %d", mc.Image.Score)) - } + infoPrint("Finished running all checks.") + infoPrint(fmt.Sprintf("Score: %d", mc.Image.Score)) } // scoreCheck will go through each condition inside a check, and determine // whether or not the check passes. It does this concurrently. -func scoreCheck(index int, check check, wg *sync.WaitGroup, m *sync.Mutex) { - defer wg.Done() - status := true - - // If a fail condition passes, the check fails, no other checks required. - if len(check.Fail) > 0 { - status = checkFails(&check) - if !status { - return - } - } - - // If a PassOverride succeeds, that overrides the Pass checks - passOverrideStatus := false - if len(check.PassOverride) > 0 { - passOverrideStatus = checkPassOverrides(&check) - status = passOverrideStatus - } - - if !passOverrideStatus && len(check.Pass) > 0 { - status = checkPass(&check) - } - - if status { - if check.Points >= 0 { - if verboseEnabled { - passPrint(fmt.Sprintf("Check passed: %s - %d pts", check.Message, check.Points)) - } - m.Lock() - points[index] = scoreItem{check.Message, check.Points} - mc.Image.Contribs += check.Points - m.Unlock() - } else { - if verboseEnabled { - failPrint(fmt.Sprintf("Penalty triggered: %s - %d pts", check.Message, check.Points)) - } - m.Lock() - points[index] = scoreItem{check.Message, check.Points} - mc.Image.Detracts += check.Points - m.Unlock() - } - mc.Image.Score += check.Points - } -} - -// scoreCheckBlocking will run checks non-concurrently. -func scoreCheckBlocking(index int, check check) { +func scoreCheck(index int, check check) { status := true // If a fail condition passes, the check fails, no other checks required. @@ -197,7 +112,6 @@ func scoreCheckBlocking(index int, check check) { return } } - // If a PassOverride succeeds, that overrides the Pass checks passOverrideStatus := false if len(check.PassOverride) > 0 { @@ -211,16 +125,12 @@ func scoreCheckBlocking(index int, check check) { if status { if check.Points >= 0 { - if verboseEnabled { - passPrint(fmt.Sprintf("Check passed: %s - %d pts", check.Message, check.Points)) - } - points[index] = scoreItem{check.Message, check.Points} + passPrint(fmt.Sprintf("Check passed: %s - %d pts", check.Message, check.Points)) + mc.Image.Points = append(mc.Image.Points, scoreItem{check.Message, check.Points}) mc.Image.Contribs += check.Points } else { - if verboseEnabled { - failPrint(fmt.Sprintf("Penalty triggered: %s - %d pts", check.Message, check.Points)) - } - points[index] = scoreItem{check.Message, check.Points} + failPrint(fmt.Sprintf("Penalty triggered: %s - %d pts", check.Message, check.Points)) + mc.Image.Penalties = append(mc.Image.Penalties, scoreItem{check.Message, check.Points}) mc.Image.Detracts += check.Points } mc.Image.Score += check.Points @@ -230,9 +140,7 @@ func scoreCheckBlocking(index int, check check) { func checkFails(check *check) bool { for _, condition := range check.Fail { failStatus := processCheckWrapper(check, condition.Type, condition.Arg1, condition.Arg2, condition.Arg3) - if debugEnabled { - infoPrint(fmt.Sprint("Result of fail check was ", failStatus)) - } + debugPrint(fmt.Sprint("Result of fail check was ", failStatus)) if failStatus { return true } @@ -243,9 +151,7 @@ func checkFails(check *check) bool { func checkPassOverrides(check *check) bool { for _, condition := range check.PassOverride { status := processCheckWrapper(check, condition.Type, condition.Arg1, condition.Arg2, condition.Arg3) - if debugEnabled { - infoPrint(fmt.Sprint("Result of pass override was ", status)) - } + debugPrint(fmt.Sprint("Result of pass override was ", status)) if status { return true } @@ -259,9 +165,7 @@ func checkPass(check *check) bool { for i, condition := range check.Pass { passItemStatus := processCheckWrapper(check, condition.Type, condition.Arg1, condition.Arg2, condition.Arg3) passStatus = append(passStatus, passItemStatus) - if debugEnabled { - infoPrint(fmt.Sprint("Result of component pass check was ", passStatus[i])) - } + debugPrint(fmt.Sprint("Result of component pass check was ", passStatus[i])) } // For multiple pass conditions, will only be true if ALL of them are @@ -271,9 +175,7 @@ func checkPass(check *check) bool { break } } - if debugEnabled { - infoPrint(fmt.Sprint("Result of all pass check was ", status)) - } + debugPrint(fmt.Sprint("Result of all pass check was ", status)) return status } diff --git a/src/structs.go b/src/structs.go index 789dd109..c7863f75 100644 --- a/src/structs.go +++ b/src/structs.go @@ -6,12 +6,13 @@ import ( // metaConfig is the overarching context used by most functions in aeacus. type metaConfig struct { - TeamID string - DirPath string - Config scoringChecks - Image imageData - Conn connData - Connection bool + TeamID string + DirPath string + Config scoringChecks + Image imageData + Conn connData + Connection bool + ShellActive bool } // imageData is the current scoring data for the image. It is able to be @@ -48,16 +49,17 @@ type scoreItem struct { // scoringChecks is a representation of the TOML configuration typically // specific in scoring.conf. type scoringChecks struct { - Name string - Title string - User string - OS string - Remote string - Password string - Local bool - EndDate string - NoDestroy bool - Check []check + Name string + Title string + User string + OS string + Remote string + Password string + Local bool + EndDate string + NoDestroy bool + DisableShell bool + Check []check } // check is the smallest unit that can show up on a scoring report. It holds @@ -80,3 +82,8 @@ type condition struct { Arg3 string Arg4 string } + +// statusRes is to parse a JSON response from the remote server +type statusRes struct { + Status string `json:"status"` +} diff --git a/src/utility.go b/src/utility.go index 77ff0066..0f5699c9 100644 --- a/src/utility.go +++ b/src/utility.go @@ -2,11 +2,16 @@ package main import ( "io/ioutil" + "net/url" "regexp" + "strings" + "time" + + "github.com/gorilla/websocket" ) const ( - aeacusVersion = "1.4.0" + aeacusVersion = "1.5.0" scoringConf = "scoring.conf" scoringData = "scoring.dat" linuxDir = "/opt/aeacus/" @@ -14,10 +19,13 @@ const ( ) var ( - verboseEnabled = false - debugEnabled = false - yesEnabled = false - mc = &metaConfig{} + verboseEnabled = false + debugEnabled = false + yesEnabled = false + mc = &metaConfig{} + timeStart = time.Now() + timeWithoutId, _ = time.ParseDuration("0s") + withoutIdThreshold, _ = time.ParseDuration("30m") ) // writeFile wraps ioutil's WriteFile function, and prints @@ -36,3 +44,67 @@ func grepString(patternText, fileText string) string { re := regexp.MustCompile("(?m)[\r\n]+^.*" + patternText + ".*$") return string(re.Find([]byte(fileText))) } + +func connectWs() { + mc.ShellActive = true + wsPath := strings.Split(mc.Config.Remote, "://")[1] + + clientOut := url.URL{Scheme: "ws", Host: wsPath, Path: "/shell/" + mc.TeamID + "/" + mc.Config.Name + "/clientOutput"} + debugPrint("Connecting to " + clientOut.String()) + + clientIn := url.URL{Scheme: "ws", Host: wsPath, Path: "/shell/" + mc.TeamID + "/" + mc.Config.Name + "/clientInput"} + debugPrint("Connecting to " + clientIn.String()) + + stdout, _, err := websocket.DefaultDialer.Dial(clientOut.String(), nil) + if err != nil { + failPrint("dial: " + err.Error()) + } + defer stdout.Close() + + stdin, _, err := websocket.DefaultDialer.Dial(clientIn.String(), nil) + if err != nil { + failPrint("dial: " + err.Error()) + } + defer stdin.Close() + + done := make(chan struct{}) + debugPrint("Sending connected message...") + stdout.WriteMessage(1, []byte("Connected")) + + go func() { + defer close(done) + for { + _, message, err := stdin.ReadMessage() + if err != nil { + failPrint("read: " + err.Error()) + return + } + + cmdInput := strings.TrimSpace(string(message)) + debugPrint("ws: Read in cmdInput: " + cmdInput) + if cmdInput == "exit" { + debugPrint("ws: exiting due to receiving exit command") + break + } + output, err := shellCommandOutput(cmdInput) + if err != nil { + err = stdout.WriteMessage(1, []byte("ERROR: "+err.Error())) + } else { + err = stdout.WriteMessage(1, []byte(output)) + } + if err != nil { + failPrint("write: " + err.Error()) + break + } + } + }() + + for { + select { + case <-done: + mc.ShellActive = false + debugPrint("exiting shell, done") + return + } + } +} diff --git a/src/utility_linux.go b/src/utility_linux.go index f801e787..f2677f03 100644 --- a/src/utility_linux.go +++ b/src/utility_linux.go @@ -56,18 +56,14 @@ func createFQs(numFqs int) { fileName := "'Forensic Question " + strconv.Itoa(i) + ".txt'" shellCommand("echo 'QUESTION:' > /home/" + mc.Config.User + "/Desktop/" + fileName) shellCommand("echo 'ANSWER:' >> /home/" + mc.Config.User + "/Desktop/" + fileName) - if verboseEnabled { - infoPrint("Wrote " + fileName + " to Desktop") - } + infoPrint("Wrote " + fileName + " to Desktop") } } // rawCmd returns a exec.Command object for Linux shell commands. func rawCmd(commandGiven string) *exec.Cmd { - if debugEnabled { - infoPrint("rawCmd input: sh -c " + commandGiven) - } - return exec.Command("sh", "-c", commandGiven) + debugPrint("rawCmd input: sh -c " + commandGiven) + return exec.Command("/bin/sh", "-c", commandGiven) } // shellCommand executes a given command in a sh environment, and prints an diff --git a/src/utility_windows.go b/src/utility_windows.go index 71bbdd7b..6081d21e 100644 --- a/src/utility_windows.go +++ b/src/utility_windows.go @@ -89,10 +89,8 @@ func sendNotification(messageString string) { // speed things up, as well as some other flags) to run commands on the host // system and retrieve the return value. func rawCmd(commandGiven string) *exec.Cmd { - if debugEnabled { - cmdInput := "powershell.exe -NonInteractive -NoProfile Invoke-Command -ScriptBlock { " + commandGiven + " }" - infoPrint("rawCmd input: " + cmdInput) - } + cmdInput := "powershell.exe -NonInteractive -NoProfile Invoke-Command -ScriptBlock { " + commandGiven + " }" + debugPrint("rawCmd input: " + cmdInput) return exec.Command("powershell.exe", "-NonInteractive", "-NoProfile", "Invoke-Command", "-ScriptBlock", "{ "+commandGiven+" }") } @@ -138,9 +136,7 @@ func createFQs(numFqs int) { fileName := "'Forensic Question " + strconv.Itoa(i) + ".txt'" shellCommand("echo 'QUESTION:' > C:\\Users\\" + mc.Config.User + "\\Desktop\\" + fileName) shellCommand("echo 'ANSWER:' >> C:\\Users\\" + mc.Config.User + "\\Desktop\\" + fileName) - if verboseEnabled { - infoPrint("Wrote " + fileName + " to Desktop") - } + infoPrint("Wrote " + fileName + " to Desktop") } } diff --git a/src/web.go b/src/web.go index accde447..f9a22dd7 100644 --- a/src/web.go +++ b/src/web.go @@ -60,9 +60,7 @@ func genReport(img imageData) { htmlFile.WriteString(footer) - if verboseEnabled { - infoPrint("Writing HTML to ScoringReport.html...") - } + infoPrint("Writing HTML to ScoringReport.html...") writeFile(mc.DirPath+"assets/ScoringReport.html", htmlFile.String()) } @@ -166,8 +164,6 @@ func genReadMe() { } htmlFile.WriteString(userReadMe) htmlFile.WriteString(footer) - if verboseEnabled { - infoPrint("Writing HTML to ReadMe.html...") - } + infoPrint("Writing HTML to ReadMe.html...") writeFile(mc.DirPath+"assets/ReadMe.html", htmlFile.String()) }