Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exec: Windows Exec support and Safe Mode #24

Merged
merged 2 commits into from
Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 114 additions & 5 deletions exec/executil.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package executil

import (
"errors"
"encoding/base64"
"io"
"os/exec"
"runtime"
"strings"

"github.com/pkg/errors"
"golang.org/x/text/encoding/unicode"
)

const (
Expand Down Expand Up @@ -104,17 +108,74 @@ func splitCmdArgs(cmds string) []string {
// return
// }

// Run the specified command and return the output
// Run the specified command in os shell (sh or powershell.exe) and return the output
func Run(cmd string) (string, error) {
if runtime.GOOS == "windows" {
return "", errors.New("can't execute sh on windows platform")
return RunPS(cmd)
}
return RunSh(splitCmdArgs(cmd)...)
}

/*
RunSafe necessarily does not prevent command injection but prevents/limits damage to an extent
"the os/exec package intentionally does not invoke the system shell and does not expand
any glob patterns or handle other expansions, pipelines, or redirections typically done by shells"
This also mitigates windows security risk in go<1.19. refer https://pkg.go.dev/os/exec
*/
func RunSafe(cmd ...string) (string, error) {
execpath, err := exec.LookPath(cmd[0])
if err != nil {
if runtime.GOOS == "windows" {
patherror := errors.New("RunSafe does not allow relative exection of binaries (ex ./main) due to security reasons")
return "", errors.Wrap(err, patherror.Error())
}
return "", err
}

var cmdArgs []string

if runtime.GOOS == "windows" {
/* When command is run in Windows using exec.Command, args are passed as quoted strings
and are parsed/converted to args before command is run by Go Internally
*/
cmdArgs = cmd[1:]
} else {
cmdArgs = splitCmdArgs(strings.Join(cmd[1:], " "))
}

cmdExec := exec.Command(execpath, cmdArgs...)

in, _ := cmdExec.StdinPipe()
errorOut, _ := cmdExec.StderrPipe()
out, _ := cmdExec.StdoutPipe()
defer in.Close()
defer errorOut.Close()
defer out.Close()

if err := cmdExec.Start(); err != nil {
return "", errors.Wrapf(err, "failed to start command:\n%v", strings.Join(cmd, " "))
}

outData, _ := io.ReadAll(out)
errorData, _ := io.ReadAll(errorOut)

var adbError error = nil

if err := cmdExec.Wait(); err != nil {
if _, ok := err.(*exec.ExitError); ok {
adbError = errors.New("return error")
outData = errorData
} else {
return "", errors.Wrap(err, "process i/o error")
}
}

return string(outData), adbError
}

// RunSh the specified command through sh
func RunSh(cmd ...string) (string, error) {
cmdExec := exec.Command(cmd[0], cmd[1:]...)
cmdExec := exec.Command("sh", "-c", strings.Join(cmd, " "))
in, _ := cmdExec.StdinPipe()
errorOut, _ := cmdExec.StderrPipe()
out, _ := cmdExec.StdoutPipe()
Expand All @@ -123,7 +184,8 @@ func RunSh(cmd ...string) (string, error) {
defer out.Close()

if err := cmdExec.Start(); err != nil {
return "", errors.New("start sh process error")
errorData, _ := io.ReadAll(errorOut)
return "", errors.Wrapf(err, "failed to start process %v", string(errorData))
}

outData, _ := io.ReadAll(out)
Expand All @@ -142,3 +204,50 @@ func RunSh(cmd ...string) (string, error) {

return string(outData), adbError
}

// RunPS runs the specified command through powershell.exe
func RunPS(cmd string) (string, error) {

/*
Run command in powershell using -EncodedCommand flag and base64 of actual command
this makes it possible to run complex quotation marks or curly braces without manual escaping
more details can be found at:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand
*/

utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
encodedCmd, err := utf16.NewEncoder().String(cmd)
if err != nil {
return "", err
}
b64cmd := base64.StdEncoding.EncodeToString([]byte(encodedCmd))

cmdExec := exec.Command("powershell.exe", "-EncodedCommand", b64cmd)

in, _ := cmdExec.StdinPipe()
errorOut, _ := cmdExec.StderrPipe()
out, _ := cmdExec.StdoutPipe()
defer in.Close()
defer errorOut.Close()
defer out.Close()

if err := cmdExec.Start(); err != nil {
return "", errors.New("start powershell.exe process error")
}

outData, _ := io.ReadAll(out)
errorData, _ := io.ReadAll(errorOut)

var adbError error = nil

if err := cmdExec.Wait(); err != nil {
if _, ok := err.(*exec.ExitError); ok {
adbError = errors.New("powershell.exe return error")
outData = errorData
} else {
return "", errors.New("start powershell.exe process error")
}
}

return string(outData), adbError
}
76 changes: 73 additions & 3 deletions exec/executil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,75 @@ func init() {
}

func TestRun(t *testing.T) {
if runtime.GOOS == "windows" {
return
}
// try to run the echo command
s, err := Run("echo test")
require.Nil(t, err, "failed execution", err)
require.Equal(t, "test"+newLineMarker, s, "output doesn't contain expected result", s)
}

func TestRunAdv(t *testing.T) {
testcases := []struct {
GOOS string // OS
Command string
Expected string // expected output
Contains string // expected output contains
}{
// Tests With Flags
{"darwin", "uname -s", "Darwin", ""},
{"linux", "uname -s", "Linux", ""},
{"windows", "cmd /c ver", "", "Windows"},
// Tests With CMD PIPE
{"windows", `systeminfo | findstr /B /C:"OS Name"`, "", "Windows"},
{"darwin", `sw_vers | grep -i "ProductName"`, "", "macOS"},
{"linux", `uname -a | cut -d " " -f 1`, "Linux", ""},
// Other Shell Specific Features
{"windows", `cmd /c " echo This && echo Works"`, "This \r\nWorks", ""},
{"linux", "true && echo This Works", "This Works", ""},
{"darwin", "true && echo This Works", "This Works", ""},
}

runFunc := func(cmd string, expected string, contains string) {
s, err := Run(cmd)
require.Nilf(t, err, "%v failed to execute", cmd)
if expected != "" {
require.Equal(t, expected+newLineMarker, s)
} else if contains != "" {
require.Contains(t, s, contains)
} else {
t.Logf("Malformed test case : %v", cmd)
}
t.Logf("Test Successful: %v", cmd)
}

for _, v := range testcases {
switch v.GOOS {
case "windows":
if runtime.GOOS != "windows" {
continue
}
runFunc(v.Command, v.Expected, v.Contains)
case "darwin":
if runtime.GOOS != "darwin" {
continue
}
runFunc(v.Command, v.Expected, v.Contains)
case "linux":
if runtime.GOOS != "linux" {
continue
}
runFunc(v.Command, v.Expected, v.Contains)
default:
t.Logf("No Unit Test Available for this platform")

}
}
}

func TestRunSafe(t *testing.T) {
_, err := RunSafe(`whoami | grep Hello`)
require.Error(t, err)
}

func TestRunSh(t *testing.T) {
if runtime.GOOS == "windows" {
return
Expand All @@ -36,3 +96,13 @@ func TestRunSh(t *testing.T) {
require.Nil(t, err, "failed execution", err)
require.Equal(t, "test"+newLineMarker, s, "output doesn't contain expected result", s)
}

func TestRunPS(t *testing.T) {
if runtime.GOOS != "windows" {
return
}
// run powershell command (runs in both ps1 and ps2)
s, err := RunPS("get-host")
require.Nil(t, err, "failed execution", err)
require.Contains(t, s, "Microsoft.PowerShell", "failed to run powershell command get-host")
}