From f48b0c17ed0c5768436c4675cb648e5b93fe063a Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Wed, 18 Jul 2018 22:32:49 -0700 Subject: [PATCH 01/16] Prep for hkexsh alternate op mode via symlink/exe name: hkexcp - a secure remote file copier --- hkexsh/hkexcp | 1 + hkexsh/hkexsh.go | 50 +++++++++++++++++++++++++++++++--------------- hkexshd/hkexshd.go | 8 ++++---- 3 files changed, 39 insertions(+), 20 deletions(-) create mode 120000 hkexsh/hkexcp mode change 100644 => 100755 hkexsh/hkexsh.go mode change 100644 => 100755 hkexshd/hkexshd.go diff --git a/hkexsh/hkexcp b/hkexsh/hkexcp new file mode 120000 index 0000000..cf0eb1c --- /dev/null +++ b/hkexsh/hkexcp @@ -0,0 +1 @@ +hkexsh \ No newline at end of file diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go old mode 100644 new mode 100755 index daca62b..4f9ca23 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -72,6 +72,10 @@ func main() { var hAlg string var server string var cmdStr string + + var copySrc string + var copyDst string + var altUser string var authCookie string var chaffEnabled bool @@ -79,27 +83,49 @@ func main() { var chaffFreqMax uint var chaffBytesMax uint + var op []byte isInteractive := false flag.BoolVar(&vopt, "v", false, "show version") + flag.BoolVar(&dbg, "d", false, "debug logging") flag.StringVar(&cAlg, "c", "C_AES_256", "cipher [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]") - flag.StringVar(&hAlg, "h", "H_SHA256", "hmac [\"H_SHA256\"]") + flag.StringVar(&hAlg, "m", "H_SHA256", "hmac [\"H_SHA256\"]") flag.StringVar(&server, "s", "localhost:2000", "server hostname/address[:port]") - flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") flag.StringVar(&altUser, "u", "", "specify alternate user") flag.StringVar(&authCookie, "a", "", "auth cookie") - flag.BoolVar(&chaffEnabled, "cE", true, "enabled chaff pkts (default true)") - flag.UintVar(&chaffFreqMin, "cfm", 100, "chaff pkt freq min (msecs)") - flag.UintVar(&chaffFreqMax, "cfM", 5000, "chaff pkt freq max (msecs)") - flag.UintVar(&chaffBytesMax, "cbM", 64, "chaff pkt size max (bytes)") - flag.BoolVar(&dbg, "d", false, "debug logging") - flag.Parse() + flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts (default true)") + flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)") + flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt freq max (msecs)") + flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)") + + // Find out what program we are (shell or copier) + myPath := strings.Split(os.Args[0], string(os.PathSeparator)) + if myPath[len(myPath)-1] != "hkexcp" { + // hkexsh accepts a command (-x) but not + // a srcpath (-r) or dstpath (-t) + flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") + flag.Parse() + } else { + // hkexcp accepts srcpath (-r) and dstpath (-t), but not + // a command (-x) + flag.StringVar(©Src, "r", "", "copy srcpath") + flag.StringVar(©Dst, "t", "", "copy dstpath") + } + + if flag.NFlag() == 0 { + flag.Usage() + os.Exit(0) + } if vopt { fmt.Printf("version v%s\n", version) os.Exit(0) } + if len(cmdStr) != 0 && (len(copySrc) != 0 || len(copyDst) != 0) { + log.Fatal("incompatible options -- either cmd (-x) or copy ops (-r,-t), but not both") + } + if dbg { log.SetOutput(os.Stdout) } else { @@ -136,17 +162,9 @@ func main() { uname = altUser } - var op []byte if len(cmdStr) == 0 { op = []byte{'s'} isInteractive = true - } else if cmdStr == "-" { - op = []byte{'c'} - cmdStdin, err := ioutil.ReadAll(os.Stdin) - if err != nil { - panic(err) - } - cmdStr = strings.Trim(string(cmdStdin), "\r\n") } else { op = []byte{'c'} // non-interactive cmds may complete quickly, so chaff earlier/faster diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go old mode 100644 new mode 100755 index 97a624e..6a823bf --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -202,10 +202,10 @@ func main() { flag.BoolVar(&vopt, "v", false, "show version") flag.StringVar(&laddr, "l", ":2000", "interface[:port] to listen") - flag.BoolVar(&chaffEnabled, "cE", true, "enabled chaff pkts") - flag.UintVar(&chaffFreqMin, "cfm", 100, "chaff pkt freq min (msecs)") - flag.UintVar(&chaffFreqMax, "cfM", 5000, "chaff pkt freq max (msecs)") - flag.UintVar(&chaffBytesMax, "cbM", 64, "chaff pkt size max (bytes)") + flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts") + flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)") + flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt freq max (msecs)") + flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)") flag.BoolVar(&dbg, "d", false, "debug logging") flag.Parse() From 5eb7d4d1e635cecaa21747eac6209f659eb1531a Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Sun, 29 Jul 2018 00:48:42 -0700 Subject: [PATCH 02/16] Initial (buggy) support for 'fancy arg' style ala ssh: eg user@example.org:port:path --- hkexsh/hkexsh.go | 76 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index 4f9ca23..d68e877 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -34,7 +34,8 @@ type cmdSpec struct { } var ( - wg sync.WaitGroup + wg sync.WaitGroup + defPort = "2000" ) // Get terminal size using 'stty' command @@ -52,6 +53,53 @@ func GetSize() (cols, rows int, err error) { return } +func parseFancyEndpointArg(a []string, dp string) (user, host, port, path string) { + //TODO: Look for non-option fancyArg of syntax user@host:filespec to set -r,-t and -u + // Consider: whether fancyArg is src or dst file depends on flag.Args() index; + // fancyArg as last flag.Args() element denotes dstFile + // fancyArg as not-last flag.Args() element denotes srcFile + // * throw error if >1 fancyArgs are found in flags.Args() + var fancyUser, fancyHost, fancyPort, fancyPath string + for i, arg := range flag.Args() { + if strings.Contains(arg, ":") || strings.Contains(arg, "@") { + fancyArg := strings.Split(flag.Arg(i), "@") + var fancyHostPortPath []string + if len(fancyArg) < 2 { + //TODO: no user specified, use current + fancyUser = "[default:getUser]" + fancyHostPortPath = strings.Split(fancyArg[0], ":") + } else { + // user@.... + fancyUser = fancyArg[0] + fancyHostPortPath = strings.Split(fancyArg[1], ":") + } + + if len(fancyHostPortPath) > 2 { + // [user]@host[:port]:path + fancyPath = fancyHostPortPath[2] + } + if len(fancyHostPortPath) > 1 { + // [user]@host:port[:...] or [user]@host:path (default port) + fancyPort = fancyHostPortPath[1] + } + // [user]@host[:...[:...]] + fancyHost = fancyHostPortPath[0] + + if fancyPort == "" { + fancyPort = dp + } + + if fancyPath == "" { + fancyPath = "." + } + + fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "port:", fancyPort, "path:", fancyPath) + break // ignore multiple 'fancyArgs' + } + } + return fancyUser, fancyHost, fancyPort, fancyPath +} + // Demo of a simple client that dials up to a simple test server to // send data. // @@ -90,29 +138,45 @@ func main() { flag.BoolVar(&dbg, "d", false, "debug logging") flag.StringVar(&cAlg, "c", "C_AES_256", "cipher [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]") flag.StringVar(&hAlg, "m", "H_SHA256", "hmac [\"H_SHA256\"]") - flag.StringVar(&server, "s", "localhost:2000", "server hostname/address[:port]") + flag.StringVar(&server, "s", "localhost:"+defPort, "server hostname/address[:port]") flag.StringVar(&altUser, "u", "", "specify alternate user") flag.StringVar(&authCookie, "a", "", "auth cookie") flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts (default true)") flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)") flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt freq max (msecs)") flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)") - + // Find out what program we are (shell or copier) myPath := strings.Split(os.Args[0], string(os.PathSeparator)) - if myPath[len(myPath)-1] != "hkexcp" { + if myPath[len(myPath)-1] != "hkexcp" && myPath[len(myPath)-1] != "hkexcp.exe" { // hkexsh accepts a command (-x) but not // a srcpath (-r) or dstpath (-t) flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") - flag.Parse() } else { // hkexcp accepts srcpath (-r) and dstpath (-t), but not // a command (-x) flag.StringVar(©Src, "r", "", "copy srcpath") flag.StringVar(©Dst, "t", "", "copy dstpath") } + flag.Parse() - if flag.NFlag() == 0 { + fancyUser, fancyHost, fancyPort, fancyPath := parseFancyEndpointArg(flag.Args(), defPort /* defPort */) + fmt.Println("fancyHost:", fancyHost) + if fancyUser != "" { + altUser = fancyUser + } + if fancyHost != "" { + server = fancyHost + ":" + fancyPort + fmt.Println("fancyHost sets server to", server) + } + if fancyPath != "" { + //TODO: srcPath or dstPath depends on other flag.Args + copyDst = fancyPath + } + + fmt.Println("server finally is:", server) + + if flag.NFlag() == 0 && server == "" { flag.Usage() os.Exit(0) } From 55cf5a9277e31af32f6a13a6515b2d979535921e Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Sun, 29 Jul 2018 12:47:44 -0700 Subject: [PATCH 03/16] Improved 'fancy arg' parsing - gathering up otherArgs (non-flag) as path src list --- hkexsh/hkexsh.go | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index d68e877..5b93ed3 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -53,14 +53,14 @@ func GetSize() (cols, rows int, err error) { return } -func parseFancyEndpointArg(a []string, dp string) (user, host, port, path string) { +func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, isDest bool, otherArgs []string) { //TODO: Look for non-option fancyArg of syntax user@host:filespec to set -r,-t and -u // Consider: whether fancyArg is src or dst file depends on flag.Args() index; // fancyArg as last flag.Args() element denotes dstFile // fancyArg as not-last flag.Args() element denotes srcFile // * throw error if >1 fancyArgs are found in flags.Args() var fancyUser, fancyHost, fancyPort, fancyPath string - for i, arg := range flag.Args() { + for i, arg := range a { if strings.Contains(arg, ":") || strings.Contains(arg, "@") { fancyArg := strings.Split(flag.Arg(i), "@") var fancyHostPortPath []string @@ -89,15 +89,20 @@ func parseFancyEndpointArg(a []string, dp string) (user, host, port, path string fancyPort = dp } - if fancyPath == "" { - fancyPath = "." - } + //if fancyPath == "" { + // fancyPath = "." + //} - fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "port:", fancyPort, "path:", fancyPath) - break // ignore multiple 'fancyArgs' + if i == len(a)-1 { + isDest = true + fmt.Println("isDest") + } + //fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "port:", fancyPort, "path:", fancyPath) + } else { + otherArgs = append(otherArgs, a[i]) } } - return fancyUser, fancyHost, fancyPort, fancyPath + return fancyUser, fancyHost, fancyPort, fancyPath, isDest, otherArgs } // Demo of a simple client that dials up to a simple test server to @@ -160,21 +165,26 @@ func main() { } flag.Parse() - fancyUser, fancyHost, fancyPort, fancyPath := parseFancyEndpointArg(flag.Args(), defPort /* defPort */) - fmt.Println("fancyHost:", fancyHost) + fancyUser, fancyHost, fancyPort, fancyPath, pathIsDest, otherArgs := + parseNonSwitchArgs(flag.Args(), defPort /* defPort */) + fmt.Println("otherArgs:", otherArgs) + //fmt.Println("fancyHost:", fancyHost) if fancyUser != "" { altUser = fancyUser } if fancyHost != "" { server = fancyHost + ":" + fancyPort - fmt.Println("fancyHost sets server to", server) + //fmt.Println("fancyHost sets server to", server) } if fancyPath != "" { - //TODO: srcPath or dstPath depends on other flag.Args - copyDst = fancyPath + if pathIsDest { + copyDst = fancyPath + } else { + copySrc = fancyPath + } } - fmt.Println("server finally is:", server) + //fmt.Println("server finally is:", server) if flag.NFlag() == 0 && server == "" { flag.Usage() From c6bfa2771ba42c85053a11ed0c715b2f1fe01c33 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Sun, 29 Jul 2018 13:22:35 -0700 Subject: [PATCH 04/16] Simplified hostPortPath parsing (colons mandatory if specifying more than just host) --- hkexsh/hkexsh.go | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index 5b93ed3..704644e 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -74,30 +74,27 @@ func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, i fancyHostPortPath = strings.Split(fancyArg[1], ":") } + // [...@]host[:port[:path]] if len(fancyHostPortPath) > 2 { - // [user]@host[:port]:path fancyPath = fancyHostPortPath[2] - } - if len(fancyHostPortPath) > 1 { - // [user]@host:port[:...] or [user]@host:path (default port) + } else if len(fancyHostPortPath) > 1 { fancyPort = fancyHostPortPath[1] } - // [user]@host[:...[:...]] fancyHost = fancyHostPortPath[0] if fancyPort == "" { fancyPort = dp } - //if fancyPath == "" { - // fancyPath = "." - //} + if fancyPath == "" { + fancyPath = "." + } if i == len(a)-1 { isDest = true fmt.Println("isDest") } - //fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "port:", fancyPort, "path:", fancyPath) + fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "port:", fancyPort, "path:", fancyPath) } else { otherArgs = append(otherArgs, a[i]) } @@ -126,7 +123,7 @@ func main() { var server string var cmdStr string - var copySrc string + var copySrc []byte var copyDst string var altUser string @@ -157,18 +154,19 @@ func main() { // hkexsh accepts a command (-x) but not // a srcpath (-r) or dstpath (-t) flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") - } else { - // hkexcp accepts srcpath (-r) and dstpath (-t), but not - // a command (-x) - flag.StringVar(©Src, "r", "", "copy srcpath") - flag.StringVar(©Dst, "t", "", "copy dstpath") - } + } // else { + //// hkexcp accepts srcpath (-r) and dstpath (-t), but not + //// a command (-x) + //flag.StringVar(©Src, "r", "", "copy srcpath") + //flag.StringVar(©Dst, "t", "", "copy dstpath") + //} flag.Parse() fancyUser, fancyHost, fancyPort, fancyPath, pathIsDest, otherArgs := parseNonSwitchArgs(flag.Args(), defPort /* defPort */) fmt.Println("otherArgs:", otherArgs) //fmt.Println("fancyHost:", fancyHost) + fmt.Println("fancyPath:", fancyPath) if fancyUser != "" { altUser = fancyUser } @@ -177,10 +175,20 @@ func main() { //fmt.Println("fancyHost sets server to", server) } if fancyPath != "" { + // -if pathIsSrc && len(otherArgs) > 1 ERROR + // -else flatten otherArgs into space-delim list => copySrc if pathIsDest { + for _, v := range otherArgs { + copySrc = append(copySrc, ' ') + copySrc = append(copySrc, v...) + } + fmt.Println(">> copySrc:", string(copySrc)) copyDst = fancyPath } else { - copySrc = fancyPath + if len(otherArgs) > 1 { + log.Fatal("ERROR: cannot specify more than one dest path for copy") + } + copySrc = []byte(fancyPath) } } @@ -197,7 +205,7 @@ func main() { } if len(cmdStr) != 0 && (len(copySrc) != 0 || len(copyDst) != 0) { - log.Fatal("incompatible options -- either cmd (-x) or copy ops (-r,-t), but not both") + log.Fatal("incompatible options -- either cmd (-x) or copy ops but not both") } if dbg { From 00e03c1d543cce258fbe19ad25d16e0004ada815 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Sun, 5 Aug 2018 21:43:21 -0700 Subject: [PATCH 05/16] Misc. fixes to end-of-session conn handling. Outstanding bug w/client chaff enabled & truncated client data --- hkexnet/hkexnet.go | 19 +++++-- hkexsh/hkexsh.go | 129 ++++++++++++++++++++++++--------------------- hkexshd/hkexshd.go | 23 ++++++-- 3 files changed, 102 insertions(+), 69 deletions(-) diff --git a/hkexnet/hkexnet.go b/hkexnet/hkexnet.go index 85f92d5..28609d8 100644 --- a/hkexnet/hkexnet.go +++ b/hkexnet/hkexnet.go @@ -439,7 +439,12 @@ func (hc Conn) Read(b []byte) (n int, err error) { log.Printf("[TermSize pkt: rows %v cols %v]\n", hc.Rows, hc.Cols) hc.WinCh <- WinSize{hc.Rows, hc.Cols} } else if ctrlStatOp == CSOExitStatus { - *hc.closeStat = uint8(payloadBytes[0]) + if len(payloadBytes) > 0 { + *hc.closeStat = uint8(payloadBytes[0]) + } else { + log.Println("[truncated payload, cannot determine CSOExitStatus]") + *hc.closeStat = 99 + } } else { hc.dBuf.Write(payloadBytes) //log.Printf("hc.dBuf: %s\n", hex.Dump(hc.dBuf.Bytes())) @@ -450,10 +455,14 @@ func (hc Conn) Read(b []byte) (n int, err error) { hTmp := hc.rm.Sum(nil)[0:4] log.Printf("<%04x) HMAC:(i)%s (c)%02x\r\n", decryptN, hex.EncodeToString([]byte(hmacIn[0:])), hTmp) - // Log alert if hmac didn't match, corrupted channel - if !bytes.Equal(hTmp, []byte(hmacIn[0:])) /*|| hmacIn[0] > 0xf8*/ { - fmt.Println("** ALERT - detected HMAC mismatch, possible channel tampering **") - _, _ = hc.c.Write([]byte{CSOHmacInvalid}) + if *hc.closeStat == 99 { + log.Println("[cannot verify HMAC]") + } else { + // Log alert if hmac didn't match, corrupted channel + if !bytes.Equal(hTmp, []byte(hmacIn[0:])) /*|| hmacIn[0] > 0xf8*/ { + fmt.Println("** ALERT - detected HMAC mismatch, possible channel tampering **") + _, _ = hc.c.Write([]byte{CSOHmacInvalid}) + } } } diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index 704644e..772a3dc 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -30,7 +30,7 @@ type cmdSpec struct { who []byte cmd []byte authCookie []byte - status int // though UNIX shell exit status is uint8, os.Exit() wants int + status int // UNIX exit status is uint8, but os.Exit() wants int } var ( @@ -86,13 +86,13 @@ func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, i fancyPort = dp } - if fancyPath == "" { - fancyPath = "." - } + //if fancyPath == "" { + // fancyPath = "." + //} if i == len(a)-1 { isDest = true - fmt.Println("isDest") + fmt.Println("remote path isDest") } fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "port:", fancyPort, "path:", fancyPath) } else { @@ -118,6 +118,7 @@ func main() { version := "0.1pre (NO WARRANTY)" var vopt bool var dbg bool + var shellMode bool // if true act as shell, else file copier var cAlg string var hAlg string var server string @@ -154,6 +155,7 @@ func main() { // hkexsh accepts a command (-x) but not // a srcpath (-r) or dstpath (-t) flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") + shellMode = true } // else { //// hkexcp accepts srcpath (-r) and dstpath (-t), but not //// a command (-x) @@ -162,19 +164,19 @@ func main() { //} flag.Parse() - fancyUser, fancyHost, fancyPort, fancyPath, pathIsDest, otherArgs := + tmpUser, tmpHost, tmpPort, tmpPath, pathIsDest, otherArgs := parseNonSwitchArgs(flag.Args(), defPort /* defPort */) fmt.Println("otherArgs:", otherArgs) - //fmt.Println("fancyHost:", fancyHost) - fmt.Println("fancyPath:", fancyPath) - if fancyUser != "" { - altUser = fancyUser + //fmt.Println("tmpHost:", tmpHost) + //fmt.Println("tmpPath:", tmpPath) + if tmpUser != "" { + altUser = tmpUser } - if fancyHost != "" { - server = fancyHost + ":" + fancyPort - //fmt.Println("fancyHost sets server to", server) + if tmpHost != "" { + server = tmpHost + ":" + tmpPort + //fmt.Println("tmpHost sets server to", server) } - if fancyPath != "" { + if tmpPath != "" { // -if pathIsSrc && len(otherArgs) > 1 ERROR // -else flatten otherArgs into space-delim list => copySrc if pathIsDest { @@ -183,17 +185,18 @@ func main() { copySrc = append(copySrc, v...) } fmt.Println(">> copySrc:", string(copySrc)) - copyDst = fancyPath + copyDst = tmpPath } else { if len(otherArgs) > 1 { log.Fatal("ERROR: cannot specify more than one dest path for copy") } - copySrc = []byte(fancyPath) + copySrc = []byte(tmpPath) } } - //fmt.Println("server finally is:", server) + // Do some more option consistency checks + //fmt.Println("server finally is:", server) if flag.NFlag() == 0 && server == "" { flag.Usage() os.Exit(0) @@ -208,12 +211,33 @@ func main() { log.Fatal("incompatible options -- either cmd (-x) or copy ops but not both") } + //------------------------------------------------------------------- + // Here we have parsed all options and can now carry out + // either the shell session or copy operation. + _ = shellMode + if dbg { log.SetOutput(os.Stdout) } else { log.SetOutput(ioutil.Discard) } + // We must make the decision about interactivity before Dial() + // as it affects chaffing behaviour. 20180805 + if len(cmdStr) == 0 { + op = []byte{'s'} + isInteractive = true + } else { + op = []byte{'c'} + // non-interactive cmds may complete quickly, so chaff earlier/faster + // to help ensure there's some cover to the brief traffic. + // (ignoring cmdline values) + //!DEBUG + //chaffEnabled = false + chaffFreqMin = 2 + chaffFreqMax = 10 + } + conn, err := hkexnet.Dial("tcp", server, cAlg, hAlg) if err != nil { fmt.Println("Err!") @@ -226,14 +250,16 @@ func main() { // TODO: send flag to server side indicating this // affects shell command used var oldState *hkexsh.State - if isatty.IsTerminal(os.Stdin.Fd()) { - oldState, err = hkexsh.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - panic(err) + if shellMode { + if isatty.IsTerminal(os.Stdin.Fd()) { + oldState, err = hkexsh.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + defer func() { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. + } else { + log.Println("NOT A TTY") } - defer func() { _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. - } else { - log.Println("NOT A TTY") } var uname string @@ -244,18 +270,6 @@ func main() { uname = altUser } - if len(cmdStr) == 0 { - op = []byte{'s'} - isInteractive = true - } else { - op = []byte{'c'} - // non-interactive cmds may complete quickly, so chaff earlier/faster - // to help ensure there's some cover to the brief traffic. - // (ignoring cmdline values) - chaffFreqMin = 2 - chaffFreqMax = 10 - } - if len(authCookie) == 0 { fmt.Printf("Gimme cookie:") ab, err := hkexsh.ReadPassword(int(os.Stdin.Fd())) @@ -288,32 +302,25 @@ func main() { conn.SetupChaff(chaffFreqMin, chaffFreqMax, chaffBytesMax) // enable client->server chaffing if chaffEnabled { conn.EnableChaff() + //defer conn.DisableChaff() + //defer conn.ShutdownChaff() } - defer conn.DisableChaff() - defer conn.ShutdownChaff() //client reader (from server) goroutine + //Read remote end's stdout wg.Add(1) go func() { // By deferring a call to wg.Done(), // each goroutine guarantees that it marks // its direction's stream as finished. - // - // Whichever direction's goroutine finishes first - // will call wg.Done() once more, explicitly, to - // hang up on the other side, so that this client - // exits immediately on an EOF from either side. - defer wg.Done() - // io.Copy() expects EOF so this will + // io.Copy() expects EOF so normally this will // exit with inerr == nil _, inerr := io.Copy(os.Stdout, conn) if inerr != nil { - if inerr.Error() != "EOF" { - fmt.Println(inerr) - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. - os.Exit(1) - } + fmt.Println(inerr) + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + os.Exit(1) } rec.status = int(conn.GetStatus()) @@ -322,41 +329,43 @@ func main() { if isInteractive { log.Println("[* Got EOF *]") _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. - wg.Done() - //os.Exit(rec.status) } + wg.Done() }() + // Only look for data from stdin to send to remote end + // for interactive sessions. if isInteractive { handleTermResizes(conn) // client writer (to server) goroutine + // Write local stdin to remote end wg.Add(1) go func() { defer wg.Done() - // Copy() expects EOF so this will // exit with outerr == nil //!_, outerr := io.Copy(conn, os.Stdin) _, outerr := func(conn *hkexnet.Conn, r io.Reader) (w int64, e error) { - return io.Copy(conn, r) + w, e = io.Copy(conn, r) + return w, e }(conn, os.Stdin) if outerr != nil { log.Println(outerr) - if outerr.Error() != "EOF" { - fmt.Println(outerr) - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. - os.Exit(255) - } + fmt.Println(outerr) + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + os.Exit(255) } log.Println("[Sent EOF]") - wg.Done() // client hung up, close WaitGroup to exit client + wg.Done() }() } // Wait until both stdin and stdout goroutines finish wg.Wait() + conn.DisableChaff() + conn.ShutdownChaff() _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. os.Exit(rec.status) diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go index 6a823bf..67c0ae6 100755 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -18,6 +18,7 @@ import ( "os/user" "runtime" "strings" + "sync" "syscall" "blitter.com/go/goutmp" @@ -83,6 +84,7 @@ func runCmdAs(who string, cmd string, conn hkex.Conn) (err error) { // // Uses ptys to support commands which expect a terminal. func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, chaffing bool) (err error, exitStatus int) { + var wg sync.WaitGroup u, _ := user.Lookup(who) var uid, gid uint32 fmt.Sscanf(u.Uid, "%d", &uid) @@ -135,15 +137,16 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha log.Printf("[Setting term size to: %v %v]\n", sz.Rows, sz.Cols) pty.Setsize(ptmx, &pty.Winsize{Rows: sz.Rows, Cols: sz.Cols}) } + fmt.Println("*** WinCh goroutine done ***") }() // Copy stdin to the pty.. (bgnd goroutine) go func() { _, e := io.Copy(ptmx, conn) if e != nil { - log.Printf("** std->pty ended **\n") - return + log.Println("** stdin->pty ended **:", e.Error()) } + fmt.Println("*** stdin->pty goroutine done ***") }() if chaffing { @@ -153,17 +156,26 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha defer conn.ShutdownChaff() // ..and the pty to stdout. + // This may take some time exceeding that of the + // actual command's lifetime, so the c.Wait() below + // must synchronize with the completion of this goroutine + // to ensure all stdout data gets to the client before + // connection is closed. + wg.Add(1) go func() { + defer wg.Done() _, e := io.Copy(conn, ptmx) if e != nil { - log.Printf("** pty->stdout ended **\n") - return + log.Println("** pty->stdout ended **:", e.Error()) + //wg.Done() //!return } // The above io.Copy() will exit when the command attached // to the pty exits + fmt.Println("*** pty->stdout goroutine done ***") }() if err := c.Wait(); err != nil { + fmt.Println("*** c.Wait() done ***") if exiterr, ok := err.(*exec.ExitError); ok { // The program has exited with an exit code != 0 @@ -177,6 +189,9 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha } } } + wg.Wait() // Wait on pty->stdout completion to client + //conn.DisableChaff() + //conn.ShutdownChaff() } return } From 04e8b94b5de2ffb0cbff9d9ddfee553ef267fcd5 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Mon, 6 Aug 2018 00:06:09 -0700 Subject: [PATCH 06/16] More misc. fixes to connection handling w/chaff e=0/1 client-side, both interactive and oneshot cmd (-x) --- hkexnet/hkexnet.go | 4 ++-- hkexsh/hkexsh.go | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/hkexnet/hkexnet.go b/hkexnet/hkexnet.go index 28609d8..e17bcb7 100644 --- a/hkexnet/hkexnet.go +++ b/hkexnet/hkexnet.go @@ -443,7 +443,7 @@ func (hc Conn) Read(b []byte) (n int, err error) { *hc.closeStat = uint8(payloadBytes[0]) } else { log.Println("[truncated payload, cannot determine CSOExitStatus]") - *hc.closeStat = 99 + *hc.closeStat = 98 } } else { hc.dBuf.Write(payloadBytes) @@ -455,7 +455,7 @@ func (hc Conn) Read(b []byte) (n int, err error) { hTmp := hc.rm.Sum(nil)[0:4] log.Printf("<%04x) HMAC:(i)%s (c)%02x\r\n", decryptN, hex.EncodeToString([]byte(hmacIn[0:])), hTmp) - if *hc.closeStat == 99 { + if *hc.closeStat >90 { log.Println("[cannot verify HMAC]") } else { // Log alert if hmac didn't match, corrupted channel diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index 772a3dc..34db2b3 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -302,14 +302,15 @@ func main() { conn.SetupChaff(chaffFreqMin, chaffFreqMax, chaffBytesMax) // enable client->server chaffing if chaffEnabled { conn.EnableChaff() - //defer conn.DisableChaff() - //defer conn.ShutdownChaff() + defer conn.DisableChaff() + defer conn.ShutdownChaff() } //client reader (from server) goroutine //Read remote end's stdout wg.Add(1) go func() { + defer wg.Done() // By deferring a call to wg.Done(), // each goroutine guarantees that it marks // its direction's stream as finished. @@ -330,7 +331,6 @@ func main() { log.Println("[* Got EOF *]") _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. } - wg.Done() }() // Only look for data from stdin to send to remote end @@ -343,6 +343,7 @@ func main() { wg.Add(1) go func() { defer wg.Done() + //!defer wg.Done() // Copy() expects EOF so this will // exit with outerr == nil //!_, outerr := io.Copy(conn, os.Stdin) @@ -358,15 +359,15 @@ func main() { os.Exit(255) } log.Println("[Sent EOF]") - wg.Done() }() } // Wait until both stdin and stdout goroutines finish + // ** IMPORTANT! This must come before the Restore() tty call below + // in order to maintain raw mode for interactive sessions. -rlm 20180805 wg.Wait() - conn.DisableChaff() - conn.ShutdownChaff() - + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + os.Exit(rec.status) } From c3f3bcb13f230defe11f985c821a16c58a87570e Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Mon, 6 Aug 2018 13:36:29 -0700 Subject: [PATCH 07/16] Added cp.cmd file - notes on using tar to bundle/xmit/extract over link --- cp.cmd | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 cp.cmd diff --git a/cp.cmd b/cp.cmd new file mode 100644 index 0000000..72f2db3 --- /dev/null +++ b/cp.cmd @@ -0,0 +1,11 @@ +## Template for copying files from local to remote site, destdir DEST: +tar -cz -f - testdir/sub1/bar.txt | \ + tar -xzv -C DEST --xform="s#.*/\(.*\)#\1#" + +# Note the --xform= option will strip leading path components from the file +# on extraction (ie., throw away dirtree info when copying into remote DEST) +# +# Probably need to have a '-r' option ala 'scp -r' to control --xform= +# (in the absence of --xform=.. above, files and dirs will all be extracted +# to remote DEST preserving tree structure.) + From 5859131678a1f83521f97a665b4eb7ac49912f6e Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Mon, 6 Aug 2018 22:29:51 -0700 Subject: [PATCH 08/16] Continuing groundwork for cp mode - refactor main client code into shell/copy subroutines; -r option --- cp.cmd | 1 + hkexnet/hkexnet.go | 3 +- hkexsh/hkexsh.go | 235 +++++++++++++++++++++++++++------------------ hkexshd/hkexshd.go | 12 ++- 4 files changed, 156 insertions(+), 95 deletions(-) diff --git a/cp.cmd b/cp.cmd index 72f2db3..23da425 100644 --- a/cp.cmd +++ b/cp.cmd @@ -9,3 +9,4 @@ tar -cz -f - testdir/sub1/bar.txt | \ # (in the absence of --xform=.. above, files and dirs will all be extracted # to remote DEST preserving tree structure.) +tar cf /dev/stdout ../*.txt | tar xf - diff --git a/hkexnet/hkexnet.go b/hkexnet/hkexnet.go index e17bcb7..302835c 100644 --- a/hkexnet/hkexnet.go +++ b/hkexnet/hkexnet.go @@ -202,7 +202,6 @@ func Dial(protocol string, ipport string, extensions ...string) (hc *Conn, err e hc.r, hc.rm, err = hc.getStream(hc.h.FA()) hc.w, hc.wm, err = hc.getStream(hc.h.FA()) - *hc.closeStat = 99 // open or prematurely-closed status return } @@ -455,7 +454,7 @@ func (hc Conn) Read(b []byte) (n int, err error) { hTmp := hc.rm.Sum(nil)[0:4] log.Printf("<%04x) HMAC:(i)%s (c)%02x\r\n", decryptN, hex.EncodeToString([]byte(hmacIn[0:])), hTmp) - if *hc.closeStat >90 { + if *hc.closeStat > 90 { log.Println("[cannot verify HMAC]") } else { // Log alert if hmac didn't match, corrupted channel diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index 34db2b3..364dbe4 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -54,11 +54,9 @@ func GetSize() (cols, rows int, err error) { } func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, isDest bool, otherArgs []string) { - //TODO: Look for non-option fancyArg of syntax user@host:filespec to set -r,-t and -u - // Consider: whether fancyArg is src or dst file depends on flag.Args() index; - // fancyArg as last flag.Args() element denotes dstFile - // fancyArg as not-last flag.Args() element denotes srcFile - // * throw error if >1 fancyArgs are found in flags.Args() + // Whether fancyArg is src or dst file depends on flag.Args() index; + // fancyArg as last flag.Args() element denotes dstFile + // fancyArg as not-last flag.Args() element denotes srcFile var fancyUser, fancyHost, fancyPort, fancyPath string for i, arg := range a { if strings.Contains(arg, ":") || strings.Contains(arg, "@") { @@ -102,8 +100,82 @@ func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, i return fancyUser, fancyHost, fancyPort, fancyPath, isDest, otherArgs } -// Demo of a simple client that dials up to a simple test server to -// send data. +// doCopyMode begins a secure hkexsh local<->remote file copy operation. +func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, rec *cmdSpec) { + // TODO: Bring in runShellAs(), stripped down, from hkexshd + // and build either side of tar pipeline: names? + // runTarSrc(), runTarSink() ? + if remoteDest { + fmt.Println("local files:", files, "remote filepath:", string(rec.cmd)) + } else { + fmt.Println("remote filepath:", string(rec.cmd), "local files:", files) + } +} + +// doShellMode begins an hkexsh shell session (one-shot command or interactive). +func doShellMode(isInteractive bool, conn *hkexnet.Conn, oldState *hkexsh.State, rec *cmdSpec) { + //client reader (from server) goroutine + //Read remote end's stdout + wg.Add(1) + go func() { + defer wg.Done() + // By deferring a call to wg.Done(), + // each goroutine guarantees that it marks + // its direction's stream as finished. + + // io.Copy() expects EOF so normally this will + // exit with inerr == nil + _, inerr := io.Copy(os.Stdout, conn) + if inerr != nil { + fmt.Println(inerr) + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + os.Exit(1) + } + + rec.status = int(conn.GetStatus()) + log.Println("rec.status:", rec.status) + + if isInteractive { + log.Println("[* Got EOF *]") + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + } + }() + + // Only look for data from stdin to send to remote end + // for interactive sessions. + if isInteractive { + handleTermResizes(conn) + + // client writer (to server) goroutine + // Write local stdin to remote end + wg.Add(1) + go func() { + defer wg.Done() + //!defer wg.Done() + // Copy() expects EOF so this will + // exit with outerr == nil + //!_, outerr := io.Copy(conn, os.Stdin) + _, outerr := func(conn *hkexnet.Conn, r io.Reader) (w int64, e error) { + w, e = io.Copy(conn, r) + return w, e + }(conn, os.Stdin) + + if outerr != nil { + log.Println(outerr) + fmt.Println(outerr) + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + os.Exit(255) + } + log.Println("[Sent EOF]") + }() + } + + // Wait until both stdin and stdout goroutines finish before returning + // (ensure client gets all data from server before closing) + wg.Wait() +} + +// hkexsh - a client for secure shell and file copy operations. // // While conforming to the basic net.Conn interface HKex.Conn has extra // capabilities designed to allow apps to define connection options, @@ -124,6 +196,7 @@ func main() { var server string var cmdStr string + var recursiveCopy bool var copySrc []byte var copyDst string @@ -156,12 +229,10 @@ func main() { // a srcpath (-r) or dstpath (-t) flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") shellMode = true - } // else { - //// hkexcp accepts srcpath (-r) and dstpath (-t), but not - //// a command (-x) - //flag.StringVar(©Src, "r", "", "copy srcpath") - //flag.StringVar(©Dst, "t", "", "copy dstpath") - //} + } else { + // Note: only makes sense for client->server copies + flag.BoolVar(&recursiveCopy, "r", false, "recursive copy/preserve tree copy") + } flag.Parse() tmpUser, tmpHost, tmpPort, tmpPath, pathIsDest, otherArgs := @@ -176,21 +247,35 @@ func main() { server = tmpHost + ":" + tmpPort //fmt.Println("tmpHost sets server to", server) } - if tmpPath != "" { + + var fileArgs string + if !shellMode && tmpPath != "" { // -if pathIsSrc && len(otherArgs) > 1 ERROR // -else flatten otherArgs into space-delim list => copySrc if pathIsDest { - for _, v := range otherArgs { - copySrc = append(copySrc, ' ') - copySrc = append(copySrc, v...) + if len(otherArgs) == 0 { + log.Fatal("ERROR: Must specify at least one src path for copy") + } else { + for _, v := range otherArgs { + copySrc = append(copySrc, ' ') + copySrc = append(copySrc, v...) + } + copyDst = tmpPath + fileArgs = string(copySrc) } - fmt.Println(">> copySrc:", string(copySrc)) - copyDst = tmpPath - } else { - if len(otherArgs) > 1 { + } else { + if len(otherArgs) == 0 { + log.Fatal("ERROR: Must specify dest path for copy") + } else if len(otherArgs) == 1 { + copyDst = otherArgs[0] + if strings.Contains(copyDst, "*") || strings.Contains(copyDst, "?") { + log.Fatal("ERROR: wildcards not allowed in dest path for copy") + } + } else { log.Fatal("ERROR: cannot specify more than one dest path for copy") } copySrc = []byte(tmpPath) + fileArgs = copyDst } } @@ -222,20 +307,39 @@ func main() { log.SetOutput(ioutil.Discard) } - // We must make the decision about interactivity before Dial() - // as it affects chaffing behaviour. 20180805 - if len(cmdStr) == 0 { - op = []byte{'s'} - isInteractive = true + if shellMode { + // We must make the decision about interactivity before Dial() + // as it affects chaffing behaviour. 20180805 + if len(cmdStr) == 0 { + op = []byte{'s'} + isInteractive = true + } else { + op = []byte{'c'} + // non-interactive cmds may complete quickly, so chaff earlier/faster + // to help ensure there's some cover to the brief traffic. + // (ignoring cmdline values) + chaffFreqMin = 2 + chaffFreqMax = 10 + } } else { - op = []byte{'c'} - // non-interactive cmds may complete quickly, so chaff earlier/faster - // to help ensure there's some cover to the brief traffic. - // (ignoring cmdline values) - //!DEBUG - //chaffEnabled = false + // as copy mode is also non-interactive, set up chaffing + // just like the 'c' mode above chaffFreqMin = 2 chaffFreqMax = 10 + + if pathIsDest { + // client->server file copy + // src file list is in copySrc + op = []byte{'D'} + fmt.Println("client->server copy:", string(copySrc), "->", copyDst) + cmdStr = copyDst + } else { + // server->client file copy + // remote src file(s) in copyDsr + op = []byte{'S'} + fmt.Println("server->client copy:", string(copySrc), "->", copyDst) + cmdStr = string(copySrc) + } } conn, err := hkexnet.Dial("tcp", server, cAlg, hAlg) @@ -306,68 +410,15 @@ func main() { defer conn.ShutdownChaff() } - //client reader (from server) goroutine - //Read remote end's stdout - wg.Add(1) - go func() { - defer wg.Done() - // By deferring a call to wg.Done(), - // each goroutine guarantees that it marks - // its direction's stream as finished. - - // io.Copy() expects EOF so normally this will - // exit with inerr == nil - _, inerr := io.Copy(os.Stdout, conn) - if inerr != nil { - fmt.Println(inerr) - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. - os.Exit(1) - } - - rec.status = int(conn.GetStatus()) - log.Println("rec.status:", rec.status) - - if isInteractive { - log.Println("[* Got EOF *]") - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. - } - }() - - // Only look for data from stdin to send to remote end - // for interactive sessions. - if isInteractive { - handleTermResizes(conn) - - // client writer (to server) goroutine - // Write local stdin to remote end - wg.Add(1) - go func() { - defer wg.Done() - //!defer wg.Done() - // Copy() expects EOF so this will - // exit with outerr == nil - //!_, outerr := io.Copy(conn, os.Stdin) - _, outerr := func(conn *hkexnet.Conn, r io.Reader) (w int64, e error) { - w, e = io.Copy(conn, r) - return w, e - }(conn, os.Stdin) - - if outerr != nil { - log.Println(outerr) - fmt.Println(outerr) - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. - os.Exit(255) - } - log.Println("[Sent EOF]") - }() + if shellMode { + doShellMode(isInteractive, conn, oldState, rec) + } else { + doCopyMode(conn, pathIsDest, fileArgs, recursiveCopy, rec) } - // Wait until both stdin and stdout goroutines finish - // ** IMPORTANT! This must come before the Restore() tty call below - // in order to maintain raw mode for interactive sessions. -rlm 20180805 - wg.Wait() - - _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + if oldState != nil { + _ = hkexsh.Restore(int(os.Stdin.Fd()), oldState) // Best effort. + } os.Exit(rec.status) } diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go index 67c0ae6..7c2324d 100755 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -175,7 +175,7 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha }() if err := c.Wait(); err != nil { - fmt.Println("*** c.Wait() done ***") + fmt.Println("*** c.Wait() done ***") if exiterr, ok := err.(*exec.ExitError); ok { // The program has exited with an exit code != 0 @@ -369,6 +369,16 @@ func main() { log.Printf("[Shell completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) hc.SetStatus(uint8(cmdStatus)) } + } else if rec.op[0] == 'D' { + // File copy (destination) operation - client copy to server + log.Printf("[Client->Server copy]\n") + // TODO: call function with hc, rec.cmd, chaffEnabled etc. + // func hooks tar cmd right-half of pipe to hc Reader + } else if rec.op[0] == 'S' { + // File copy (src) operation - server copy to client + log.Printf("[Server->Client copy]\n") + // TODO: call function to copy rec.cmd (file list) to + // tar cmd left-half of pipeline to hc.Writer ? } else { log.Println("[Bad cmdSpec]") } From 0b9b8b8320ecb073e730d6e8a10ab24e39a0d196 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Thu, 23 Aug 2018 11:03:19 -0700 Subject: [PATCH 09/16] WIP tarpipe construction: server-side, TODOL client-side, -r behaviour --- hkexsh/hkexsh.go | 4 +- hkexshd/hkexshd.go | 167 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 141 insertions(+), 30 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index 364dbe4..c69b6a2 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -107,8 +107,10 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, // runTarSrc(), runTarSink() ? if remoteDest { fmt.Println("local files:", files, "remote filepath:", string(rec.cmd)) + fmt.Fprintf(conn, "copyMode remoteDest TODO\n") } else { fmt.Println("remote filepath:", string(rec.cmd), "local files:", files) + fmt.Fprintf(conn, "copyMode localDest TODO\n") } } @@ -263,7 +265,7 @@ func main() { copyDst = tmpPath fileArgs = string(copySrc) } - } else { + } else { if len(otherArgs) == 0 { log.Fatal("ERROR: Must specify dest path for copy") } else if len(otherArgs) == 1 { diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go index 7c2324d..98d6649 100755 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -37,48 +37,140 @@ type cmdSpec struct { } /* -------------------------------------------------------------- */ - -/* - // Run a command (via os.exec) as a specific user -// -// Uses ptys to support commands which expect a terminal. -func runCmdAs(who string, cmd string, conn hkex.Conn) (err error) { +// Perform a client->server copy +func runClientToServerCopyAs(who string, conn hkexnet.Conn, destPath string, chaffing bool) (err error, exitStatus int) { u, _ := user.Lookup(who) var uid, gid uint32 fmt.Sscanf(u.Uid, "%d", &uid) fmt.Sscanf(u.Gid, "%d", &gid) - fmt.Println("uid:", uid, "gid:", gid) + log.Println("uid:", uid, "gid:", gid) - args := strings.Split(cmd, " ") - arg0 := args[0] - args = args[1:] - c := exec.Command(arg0, args...) + // Need to clear server's env and set key vars of the + // target user. This isn't perfect (TERM doesn't seem to + // work 100%; ANSI/xterm colour isn't working even + // if we set "xterm" or "ansi" here; and line count + // reported by 'stty -a' defaults to 24 regardless + // of client shell window used to run client. + // Investigate -- rlm 2018-01-26) + os.Clearenv() + os.Setenv("HOME", u.HomeDir) + os.Setenv("TERM", "vt102") // TODO: server or client option? + + var c *exec.Cmd + cmdName := "/bin/tar" + // NOTE the lack of quotes around --xform option's sed expression. + // When args are passed in exec() format, no quoting is required + // (as this isn't input from a shell) (right? -rlm 20180823) + cmdArgs := []string{"-xzv", "-C", destPath, `--xform=s#.*/\(.*\)#\1#`} + c = exec.Command(cmdName, cmdArgs...) + + //If os.Clearenv() isn't called by server above these will be seen in the + //client's session env. + //c.Env = []string{"HOME=" + u.HomeDir, "SUDO_GID=", "SUDO_UID=", "SUDO_USER=", "SUDO_COMMAND=", "MAIL=", "LOGNAME="+who} + c.Dir = u.HomeDir c.SysProcAttr = &syscall.SysProcAttr{} c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} c.Stdin = conn c.Stdout = conn c.Stderr = conn - // Start the command with a pty. - ptmx, err := pty.Start(c) // returns immediately with ptmx file - if err != nil { - return err - } - // Make sure to close the pty at the end. - defer func() { _ = ptmx.Close() }() // Best effort. - // Copy stdin to the pty and the pty to stdout. - go func() { _, _ = io.Copy(ptmx, conn) }() - _, _ = io.Copy(conn, ptmx) - - //err = c.Run() // returns when c finishes. - + // Start the command (no pty) + log.Printf("[%v %v]\n", cmdName, cmdArgs) + err = c.Start() // returns immediately if err != nil { log.Printf("Command finished with error: %v", err) - log.Printf("[%s]\n", cmd) + return err, 253 // !? + } else { + if chaffing { + conn.EnableChaff() + } + defer conn.DisableChaff() + defer conn.ShutdownChaff() + + if err := c.Wait(); err != nil { + fmt.Println("*** c.Wait() done ***") + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + log.Printf("Exit Status: %d", exitStatus) + } + } + } + } + return +} + +// Perform a server->client copy +func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaffing bool) (err error, exitStatus int) { + u, _ := user.Lookup(who) + var uid, gid uint32 + fmt.Sscanf(u.Uid, "%d", &uid) + fmt.Sscanf(u.Gid, "%d", &gid) + log.Println("uid:", uid, "gid:", gid) + + // Need to clear server's env and set key vars of the + // target user. This isn't perfect (TERM doesn't seem to + // work 100%; ANSI/xterm colour isn't working even + // if we set "xterm" or "ansi" here; and line count + // reported by 'stty -a' defaults to 24 regardless + // of client shell window used to run client. + // Investigate -- rlm 2018-01-26) + os.Clearenv() + os.Setenv("HOME", u.HomeDir) + os.Setenv("TERM", "vt102") // TODO: server or client option? + + var c *exec.Cmd + cmdName := "/bin/tar" + cmdArgs := []string{"-cz", "-f", "-", srcPath} + c = exec.Command(cmdName, cmdArgs...) + + //If os.Clearenv() isn't called by server above these will be seen in the + //client's session env. + //c.Env = []string{"HOME=" + u.HomeDir, "SUDO_GID=", "SUDO_UID=", "SUDO_USER=", "SUDO_COMMAND=", "MAIL=", "LOGNAME="+who} + c.Dir = u.HomeDir + c.SysProcAttr = &syscall.SysProcAttr{} + c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} + c.Stdin = conn + c.Stdout = conn + c.Stderr = conn + + // Start the command (no pty) + log.Printf("[%v %v]\n", cmdName, cmdArgs) + err = c.Start() // returns immediately + if err != nil { + log.Printf("Command finished with error: %v", err) + return err, 253 // !? + } else { + if chaffing { + conn.EnableChaff() + } + defer conn.DisableChaff() + defer conn.ShutdownChaff() + + if err := c.Wait(); err != nil { + fmt.Println("*** c.Wait() done ***") + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + log.Printf("Exit Status: %d", exitStatus) + } + } + } } return } -*/ // Run a command (via default shell) as a specific user // @@ -167,7 +259,6 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha _, e := io.Copy(conn, ptmx) if e != nil { log.Println("** pty->stdout ended **:", e.Error()) - //wg.Done() //!return } // The above io.Copy() will exit when the command attached // to the pty exits @@ -190,8 +281,6 @@ func runShellAs(who string, cmd string, interactive bool, conn hkexnet.Conn, cha } } wg.Wait() // Wait on pty->stdout completion to client - //conn.DisableChaff() - //conn.ShutdownChaff() } return } @@ -374,11 +463,31 @@ func main() { log.Printf("[Client->Server copy]\n") // TODO: call function with hc, rec.cmd, chaffEnabled etc. // func hooks tar cmd right-half of pipe to hc Reader + addr := hc.RemoteAddr() + hname := strings.Split(addr.String(), ":")[0] + log.Printf("[Running copy for [%s@%s]]\n", rec.who, hname) + runErr, cmdStatus := runClientToServerCopyAs(string(rec.who), hc, string(rec.cmd), chaffEnabled) + if runErr != nil { + log.Printf("[Error spawning shell for %s@%s]\n", rec.who, hname) + } else { + log.Printf("[Shell completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) + hc.SetStatus(uint8(cmdStatus)) + } } else if rec.op[0] == 'S' { // File copy (src) operation - server copy to client log.Printf("[Server->Client copy]\n") // TODO: call function to copy rec.cmd (file list) to // tar cmd left-half of pipeline to hc.Writer ? + addr := hc.RemoteAddr() + hname := strings.Split(addr.String(), ":")[0] + log.Printf("[Running copy for [%s@%s]]\n", rec.who, hname) + runErr, cmdStatus := runServerToClientCopyAs(string(rec.who), hc, string(rec.cmd), chaffEnabled) + if runErr != nil { + log.Printf("[Error spawning shell for %s@%s]\n", rec.who, hname) + } else { + log.Printf("[Shell completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) + hc.SetStatus(uint8(cmdStatus)) + } } else { log.Println("[Bad cmdSpec]") } From 7867f84b87bd98bc31c93044eb08b9677c9e29a4 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Fri, 24 Aug 2018 18:50:45 -0700 Subject: [PATCH 10/16] WIP: server->client copy primitively functional; TODO client->server copy --- hkexsh/hkexsh.go | 101 +++++++++++++++++++++++++++++++++++++++++---- hkexshd/hkexshd.go | 53 ++++++++++++++---------- 2 files changed, 124 insertions(+), 30 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index c69b6a2..7167849 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -19,6 +19,7 @@ import ( "runtime" "strings" "sync" + "syscall" hkexsh "blitter.com/go/hkexsh" "blitter.com/go/hkexsh/hkexnet" @@ -101,17 +102,101 @@ func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, i } // doCopyMode begins a secure hkexsh local<->remote file copy operation. -func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, rec *cmdSpec) { - // TODO: Bring in runShellAs(), stripped down, from hkexshd - // and build either side of tar pipeline: names? - // runTarSrc(), runTarSink() ? +func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, rec *cmdSpec) (err error, exitStatus int) { if remoteDest { fmt.Println("local files:", files, "remote filepath:", string(rec.cmd)) - fmt.Fprintf(conn, "copyMode remoteDest TODO\n") + fmt.Fprintf(conn, "copyMode remoteDest ...\n") + + var c *exec.Cmd + + //os.Clearenv() + //os.Setenv("HOME", u.HomeDir) + //os.Setenv("TERM", "vt102") // TODO: server or client option? + + cmdName := "/bin/tar" + cmdArgs := []string{"-cz", "-f", "/dev/stdout", files} + fmt.Printf("[%v %v]\n", cmdName, cmdArgs) + // NOTE the lack of quotes around --xform option's sed expression. + // When args are passed in exec() format, no quoting is required + // (as this isn't input from a shell) (right? -rlm 20180823) + //cmdArgs := []string{"-xvz", "-C", files, `--xform=s#.*/\(.*\)#\1#`} + c = exec.Command(cmdName, cmdArgs...) + c.Stdout = conn + + // Start the command (no pty) + err = c.Start() // returns immediately + if err != nil { + fmt.Println(err) + //log.Fatal(err) + } else { + if err = c.Wait(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + log.Printf("Exit Status: %d", exitStatus) + } + } + } + fmt.Println("*** client->server cp finished ***") + } } else { fmt.Println("remote filepath:", string(rec.cmd), "local files:", files) - fmt.Fprintf(conn, "copyMode localDest TODO\n") + fmt.Fprintf(conn, "copyMode localDest ...\n") + var c *exec.Cmd + + //os.Clearenv() + //os.Setenv("HOME", u.HomeDir) + //os.Setenv("TERM", "vt102") // TODO: server or client option? + + cmdName := "/bin/tar" + destPath := files + //if path.IsAbs(files) { + // destPath := files + //} else { + // destPath := strings.Join({os.Getenv("PWD"),files}, os.PathSeparator) + //} + + cmdArgs := []string{"-xvz", "-C", destPath} + fmt.Printf("[%v %v]\n", cmdName, cmdArgs) + // NOTE the lack of quotes around --xform option's sed expression. + // When args are passed in exec() format, no quoting is required + // (as this isn't input from a shell) (right? -rlm 20180823) + //cmdArgs := []string{"-xvz", "-C", destPath, `--xform=s#.*/\(.*\)#\1#`} + c = exec.Command(cmdName, cmdArgs...) + c.Stdin = conn + c.Stdout = os.Stdout + c.Stderr = os.Stderr + + // Start the command (no pty) + err = c.Start() // returns immediately + if err != nil { + fmt.Println(err) + //log.Fatal(err) + } else { + if err = c.Wait(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + // The program has exited with an exit code != 0 + + // This works on both Unix and Windows. Although package + // syscall is generally platform dependent, WaitStatus is + // defined for both Unix and Windows and in both cases has + // an ExitStatus() method with the same signature. + if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + log.Printf("Exit Status: %d", exitStatus) + } + } + } + fmt.Println("*** server->client cp finished ***") + } } + return } // doShellMode begins an hkexsh shell session (one-shot command or interactive). @@ -256,7 +341,7 @@ func main() { // -else flatten otherArgs into space-delim list => copySrc if pathIsDest { if len(otherArgs) == 0 { - log.Fatal("ERROR: Must specify at least one src path for copy") + log.Fatal("ERROR: Must specify at least one dest path for copy") } else { for _, v := range otherArgs { copySrc = append(copySrc, ' ') @@ -267,7 +352,7 @@ func main() { } } else { if len(otherArgs) == 0 { - log.Fatal("ERROR: Must specify dest path for copy") + log.Fatal("ERROR: Must specify src path for copy") } else if len(otherArgs) == 1 { copyDst = otherArgs[0] if strings.Contains(copyDst, "*") || strings.Contains(copyDst, "?") { diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go index 98d6649..2ce9319 100755 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -61,7 +61,8 @@ func runClientToServerCopyAs(who string, conn hkexnet.Conn, destPath string, cha // NOTE the lack of quotes around --xform option's sed expression. // When args are passed in exec() format, no quoting is required // (as this isn't input from a shell) (right? -rlm 20180823) - cmdArgs := []string{"-xzv", "-C", destPath, `--xform=s#.*/\(.*\)#\1#`} + cmdArgs := []string{"-xvz", "-C", destPath} + //cmdArgs := []string{"-xvz", "-C", destPath, `--xform=s#.*/\(.*\)#\1#`} c = exec.Command(cmdName, cmdArgs...) //If os.Clearenv() isn't called by server above these will be seen in the @@ -71,8 +72,14 @@ func runClientToServerCopyAs(who string, conn hkexnet.Conn, destPath string, cha c.SysProcAttr = &syscall.SysProcAttr{} c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} c.Stdin = conn - c.Stdout = conn - c.Stderr = conn + //c.Stdout = conn + //c.Stderr = conn + + if chaffing { + conn.EnableChaff() + } + defer conn.DisableChaff() + defer conn.ShutdownChaff() // Start the command (no pty) log.Printf("[%v %v]\n", cmdName, cmdArgs) @@ -81,12 +88,6 @@ func runClientToServerCopyAs(who string, conn hkexnet.Conn, destPath string, cha log.Printf("Command finished with error: %v", err) return err, 253 // !? } else { - if chaffing { - conn.EnableChaff() - } - defer conn.DisableChaff() - defer conn.ShutdownChaff() - if err := c.Wait(); err != nil { fmt.Println("*** c.Wait() done ***") if exiterr, ok := err.(*exec.ExitError); ok { @@ -102,8 +103,9 @@ func runClientToServerCopyAs(who string, conn hkexnet.Conn, destPath string, cha } } } + fmt.Println("*** client->server cp finished ***") + return } - return } // Perform a server->client copy @@ -136,10 +138,16 @@ func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaf c.Dir = u.HomeDir c.SysProcAttr = &syscall.SysProcAttr{} c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} - c.Stdin = conn c.Stdout = conn c.Stderr = conn + if chaffing { + conn.EnableChaff() + } + //defer conn.Close() + defer conn.DisableChaff() + defer conn.ShutdownChaff() + // Start the command (no pty) log.Printf("[%v %v]\n", cmdName, cmdArgs) err = c.Start() // returns immediately @@ -147,12 +155,6 @@ func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaf log.Printf("Command finished with error: %v", err) return err, 253 // !? } else { - if chaffing { - conn.EnableChaff() - } - defer conn.DisableChaff() - defer conn.ShutdownChaff() - if err := c.Wait(); err != nil { fmt.Println("*** c.Wait() done ***") if exiterr, ok := err.(*exec.ExitError); ok { @@ -168,8 +170,9 @@ func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaf } } } + fmt.Println("*** server->client cp finished ***") + return } - return } // Run a command (via default shell) as a specific user @@ -467,10 +470,13 @@ func main() { hname := strings.Split(addr.String(), ":")[0] log.Printf("[Running copy for [%s@%s]]\n", rec.who, hname) runErr, cmdStatus := runClientToServerCopyAs(string(rec.who), hc, string(rec.cmd), chaffEnabled) + // Returned hopefully via an EOF or exit/logout; + // Clear current op so user can enter next, or EOF + rec.op[0] = 0 if runErr != nil { - log.Printf("[Error spawning shell for %s@%s]\n", rec.who, hname) + log.Printf("[Error spawning cp for %s@%s]\n", rec.who, hname) } else { - log.Printf("[Shell completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) + log.Printf("[Command completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) hc.SetStatus(uint8(cmdStatus)) } } else if rec.op[0] == 'S' { @@ -482,10 +488,13 @@ func main() { hname := strings.Split(addr.String(), ":")[0] log.Printf("[Running copy for [%s@%s]]\n", rec.who, hname) runErr, cmdStatus := runServerToClientCopyAs(string(rec.who), hc, string(rec.cmd), chaffEnabled) + // Returned hopefully via an EOF or exit/logout; + // Clear current op so user can enter next, or EOF + rec.op[0] = 0 if runErr != nil { - log.Printf("[Error spawning shell for %s@%s]\n", rec.who, hname) + log.Printf("[Error spawning cp for %s@%s]\n", rec.who, hname) } else { - log.Printf("[Shell completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) + log.Printf("[Command completed for %s@%s, status %d]\n", rec.who, hname, cmdStatus) hc.SetStatus(uint8(cmdStatus)) } } else { From ca2b6efd9b5222500ede7eb3415857e7a8956428 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Fri, 24 Aug 2018 23:22:07 -0700 Subject: [PATCH 11/16] client->server and server->client file/dir copies minimally working --- hkexsh/hkexsh.go | 15 +++++++++------ hkexshd/hkexshd.go | 43 ++++++++++++++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index 7167849..e5034ba 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -105,7 +105,7 @@ func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, i func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, rec *cmdSpec) (err error, exitStatus int) { if remoteDest { fmt.Println("local files:", files, "remote filepath:", string(rec.cmd)) - fmt.Fprintf(conn, "copyMode remoteDest ...\n") + //fmt.Fprintf(conn, "copyMode remoteDest ...\n") var c *exec.Cmd @@ -114,15 +114,18 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, //os.Setenv("TERM", "vt102") // TODO: server or client option? cmdName := "/bin/tar" - cmdArgs := []string{"-cz", "-f", "/dev/stdout", files} + cmdArgs := []string{"-c", "-f", "/dev/stdout", strings.TrimSpace(files)} fmt.Printf("[%v %v]\n", cmdName, cmdArgs) // NOTE the lack of quotes around --xform option's sed expression. // When args are passed in exec() format, no quoting is required // (as this isn't input from a shell) (right? -rlm 20180823) //cmdArgs := []string{"-xvz", "-C", files, `--xform=s#.*/\(.*\)#\1#`} c = exec.Command(cmdName, cmdArgs...) + c.Dir, _ = os.Getwd() + fmt.Println("[wd:", c.Dir, "]") c.Stdout = conn - + c.Stderr = os.Stderr + // Start the command (no pty) err = c.Start() // returns immediately if err != nil { @@ -147,7 +150,7 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, } } else { fmt.Println("remote filepath:", string(rec.cmd), "local files:", files) - fmt.Fprintf(conn, "copyMode localDest ...\n") + //fmt.Fprintf(conn, "copyMode localDest ...\n") var c *exec.Cmd //os.Clearenv() @@ -162,7 +165,7 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, // destPath := strings.Join({os.Getenv("PWD"),files}, os.PathSeparator) //} - cmdArgs := []string{"-xvz", "-C", destPath} + cmdArgs := []string{"-x", "-C", destPath} fmt.Printf("[%v %v]\n", cmdName, cmdArgs) // NOTE the lack of quotes around --xform option's sed expression. // When args are passed in exec() format, no quoting is required @@ -172,7 +175,7 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, c.Stdin = conn c.Stdout = os.Stdout c.Stderr = os.Stderr - + // Start the command (no pty) err = c.Start() // returns immediately if err != nil { diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go index 2ce9319..648ec14 100755 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -16,6 +16,7 @@ import ( "os" "os/exec" "os/user" + "path" "runtime" "strings" "sync" @@ -38,7 +39,7 @@ type cmdSpec struct { /* -------------------------------------------------------------- */ // Perform a client->server copy -func runClientToServerCopyAs(who string, conn hkexnet.Conn, destPath string, chaffing bool) (err error, exitStatus int) { +func runClientToServerCopyAs(who string, conn hkexnet.Conn, fpath string, chaffing bool) (err error, exitStatus int) { u, _ := user.Lookup(who) var uid, gid uint32 fmt.Sscanf(u.Uid, "%d", &uid) @@ -58,28 +59,48 @@ func runClientToServerCopyAs(who string, conn hkexnet.Conn, destPath string, cha var c *exec.Cmd cmdName := "/bin/tar" + + var destDir string + if path.IsAbs(fpath) { + destDir = fpath + } else { + destDir = path.Join(u.HomeDir, fpath) + } + //stat, pe := os.Stat(destDir) + //_ = stat + //if pe != nil { + // log.Fatal(pe) + // return pe, 252 // ?! + //} + + //if stat.IsDir(destBase) { + cmdArgs := []string{"-x", "-C", destDir} + //} else { + // cmdArgs := []string{"-x", + // NOTE the lack of quotes around --xform option's sed expression. // When args are passed in exec() format, no quoting is required // (as this isn't input from a shell) (right? -rlm 20180823) - cmdArgs := []string{"-xvz", "-C", destPath} //cmdArgs := []string{"-xvz", "-C", destPath, `--xform=s#.*/\(.*\)#\1#`} c = exec.Command(cmdName, cmdArgs...) + c.Dir = destDir + //If os.Clearenv() isn't called by server above these will be seen in the //client's session env. //c.Env = []string{"HOME=" + u.HomeDir, "SUDO_GID=", "SUDO_UID=", "SUDO_USER=", "SUDO_COMMAND=", "MAIL=", "LOGNAME="+who} - c.Dir = u.HomeDir + //c.Dir = u.HomeDir c.SysProcAttr = &syscall.SysProcAttr{} c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} c.Stdin = conn - //c.Stdout = conn - //c.Stderr = conn + c.Stdout = os.Stdout + c.Stderr = os.Stderr - if chaffing { - conn.EnableChaff() - } - defer conn.DisableChaff() - defer conn.ShutdownChaff() + if chaffing { + conn.EnableChaff() + } + defer conn.DisableChaff() + defer conn.ShutdownChaff() // Start the command (no pty) log.Printf("[%v %v]\n", cmdName, cmdArgs) @@ -129,7 +150,7 @@ func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaf var c *exec.Cmd cmdName := "/bin/tar" - cmdArgs := []string{"-cz", "-f", "-", srcPath} + cmdArgs := []string{"-c", "-f", "-", srcPath} c = exec.Command(cmdName, cmdArgs...) //If os.Clearenv() isn't called by server above these will be seen in the From 1986ec6f0c9abacff34b414fdf4bd4b311e0789c Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Sat, 25 Aug 2018 23:38:58 -0700 Subject: [PATCH 12/16] Removed :port: from 'fancy' arg syntax; more improvements to src/dest file spec logic (esp. fixing bug in multiple src file/dir args to remote dest) --- hkexsh/hkexsh.go | 89 +++++++++++++++++++++------------------------- hkexshd/hkexshd.go | 11 ++++-- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index e5034ba..198545f 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -35,8 +35,7 @@ type cmdSpec struct { } var ( - wg sync.WaitGroup - defPort = "2000" + wg sync.WaitGroup ) // Get terminal size using 'stty' command @@ -54,36 +53,30 @@ func GetSize() (cols, rows int, err error) { return } -func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, isDest bool, otherArgs []string) { +func parseNonSwitchArgs(a []string) (user, host, path string, isDest bool, otherArgs []string) { // Whether fancyArg is src or dst file depends on flag.Args() index; // fancyArg as last flag.Args() element denotes dstFile // fancyArg as not-last flag.Args() element denotes srcFile - var fancyUser, fancyHost, fancyPort, fancyPath string + var fancyUser, fancyHost, fancyPath string for i, arg := range a { if strings.Contains(arg, ":") || strings.Contains(arg, "@") { fancyArg := strings.Split(flag.Arg(i), "@") - var fancyHostPortPath []string + var fancyHostPath []string if len(fancyArg) < 2 { //TODO: no user specified, use current fancyUser = "[default:getUser]" - fancyHostPortPath = strings.Split(fancyArg[0], ":") + fancyHostPath = strings.Split(fancyArg[0], ":") } else { // user@.... fancyUser = fancyArg[0] - fancyHostPortPath = strings.Split(fancyArg[1], ":") + fancyHostPath = strings.Split(fancyArg[1], ":") } - // [...@]host[:port[:path]] - if len(fancyHostPortPath) > 2 { - fancyPath = fancyHostPortPath[2] - } else if len(fancyHostPortPath) > 1 { - fancyPort = fancyHostPortPath[1] - } - fancyHost = fancyHostPortPath[0] - - if fancyPort == "" { - fancyPort = dp + // [...@]host[:path] + if len(fancyHostPath) > 1 { + fancyPath = fancyHostPath[1] } + fancyHost = fancyHostPath[0] //if fancyPath == "" { // fancyPath = "." @@ -93,19 +86,18 @@ func parseNonSwitchArgs(a []string, dp string) (user, host, port, path string, i isDest = true fmt.Println("remote path isDest") } - fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "port:", fancyPort, "path:", fancyPath) + fmt.Println("fancyArgs: user:", fancyUser, "host:", fancyHost, "path:", fancyPath) } else { otherArgs = append(otherArgs, a[i]) } } - return fancyUser, fancyHost, fancyPort, fancyPath, isDest, otherArgs + return fancyUser, fancyHost, fancyPath, isDest, otherArgs } // doCopyMode begins a secure hkexsh local<->remote file copy operation. func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, rec *cmdSpec) (err error, exitStatus int) { if remoteDest { fmt.Println("local files:", files, "remote filepath:", string(rec.cmd)) - //fmt.Fprintf(conn, "copyMode remoteDest ...\n") var c *exec.Cmd @@ -114,7 +106,12 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, //os.Setenv("TERM", "vt102") // TODO: server or client option? cmdName := "/bin/tar" - cmdArgs := []string{"-c", "-f", "/dev/stdout", strings.TrimSpace(files)} + cmdArgs := []string{"-c", "-f", "/dev/stdout"} + files = strings.TrimSpace(files) + for _, v := range strings.Split(files, " ") { + cmdArgs = append(cmdArgs, v) + } + fmt.Printf("[%v %v]\n", cmdName, cmdArgs) // NOTE the lack of quotes around --xform option's sed expression. // When args are passed in exec() format, no quoting is required @@ -124,7 +121,11 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, c.Dir, _ = os.Getwd() fmt.Println("[wd:", c.Dir, "]") c.Stdout = conn - c.Stderr = os.Stderr + // Stderr sinkholing is important. Any extraneous output to tarpipe + // messes up remote side as it's expecting pure tar data. + // (For example, if user specifies abs paths, tar outputs + // "Removing leading '/' from path names") + c.Stderr = nil // Start the command (no pty) err = c.Start() // returns immediately @@ -150,7 +151,6 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, } } else { fmt.Println("remote filepath:", string(rec.cmd), "local files:", files) - //fmt.Fprintf(conn, "copyMode localDest ...\n") var c *exec.Cmd //os.Clearenv() @@ -159,11 +159,6 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, cmdName := "/bin/tar" destPath := files - //if path.IsAbs(files) { - // destPath := files - //} else { - // destPath := strings.Join({os.Getenv("PWD"),files}, os.PathSeparator) - //} cmdArgs := []string{"-x", "-C", destPath} fmt.Printf("[%v %v]\n", cmdName, cmdArgs) @@ -284,13 +279,13 @@ func main() { var cAlg string var hAlg string var server string + var port uint var cmdStr string var recursiveCopy bool var copySrc []byte var copyDst string - var altUser string var authCookie string var chaffEnabled bool var chaffFreqMin uint @@ -304,8 +299,7 @@ func main() { flag.BoolVar(&dbg, "d", false, "debug logging") flag.StringVar(&cAlg, "c", "C_AES_256", "cipher [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]") flag.StringVar(&hAlg, "m", "H_SHA256", "hmac [\"H_SHA256\"]") - flag.StringVar(&server, "s", "localhost:"+defPort, "server hostname/address[:port]") - flag.StringVar(&altUser, "u", "", "specify alternate user") + flag.UintVar(&port, "p", 2000, "port") flag.StringVar(&authCookie, "a", "", "auth cookie") flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts (default true)") flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)") @@ -325,21 +319,28 @@ func main() { } flag.Parse() - tmpUser, tmpHost, tmpPort, tmpPath, pathIsDest, otherArgs := - parseNonSwitchArgs(flag.Args(), defPort /* defPort */) + remoteUser, tmpHost, tmpPath, pathIsDest, otherArgs := + parseNonSwitchArgs(flag.Args()) fmt.Println("otherArgs:", otherArgs) - //fmt.Println("tmpHost:", tmpHost) - //fmt.Println("tmpPath:", tmpPath) - if tmpUser != "" { - altUser = tmpUser + + // Set defaults if user doesn't specify user, path or port + var uname string + if remoteUser == "" { + u, _ := user.Current() + uname = u.Username + } else { + uname = remoteUser } + if tmpHost != "" { - server = tmpHost + ":" + tmpPort - //fmt.Println("tmpHost sets server to", server) + server = tmpHost + ":" + fmt.Sprintf("%d", port) + } + if tmpPath == "" { + tmpPath = "." } var fileArgs string - if !shellMode && tmpPath != "" { + if !shellMode /*&& tmpPath != ""*/ { // -if pathIsSrc && len(otherArgs) > 1 ERROR // -else flatten otherArgs into space-delim list => copySrc if pathIsDest { @@ -456,14 +457,6 @@ func main() { } } - var uname string - if len(altUser) == 0 { - u, _ := user.Current() - uname = u.Username - } else { - uname = altUser - } - if len(authCookie) == 0 { fmt.Printf("Gimme cookie:") ab, err := hkexsh.ReadPassword(int(os.Stdin.Fd())) diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go index 648ec14..07a7251 100755 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -150,7 +150,10 @@ func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaf var c *exec.Cmd cmdName := "/bin/tar" - cmdArgs := []string{"-c", "-f", "-", srcPath} + //cmdArgs := []string{"-c", "-f", "-", srcPath} + srcDir, srcBase := path.Split(srcPath) + cmdArgs := []string{"-c", "-C", srcDir, "-f", "-", srcBase} + c = exec.Command(cmdName, cmdArgs...) //If os.Clearenv() isn't called by server above these will be seen in the @@ -160,7 +163,11 @@ func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaf c.SysProcAttr = &syscall.SysProcAttr{} c.SysProcAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid} c.Stdout = conn - c.Stderr = conn + // Stderr sinkholing is important. Any extraneous output to tarpipe + // messes up remote side as it's expecting pure tar data. + // (For example, if user specifies abs paths, tar outputs + // "Removing leading '/' from path names") + c.Stderr = nil if chaffing { conn.EnableChaff() From 9025ee3c248c6092df2bb2e94a16942efa9f5375 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Sat, 25 Aug 2018 23:51:11 -0700 Subject: [PATCH 13/16] Cleaned up flag help txts; removed unused doCopyMode() recurs arg --- hkexsh/hkexsh.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index 198545f..d9d4038 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -95,7 +95,7 @@ func parseNonSwitchArgs(a []string) (user, host, path string, isDest bool, other } // doCopyMode begins a secure hkexsh local<->remote file copy operation. -func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, recurs bool, rec *cmdSpec) (err error, exitStatus int) { +func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, rec *cmdSpec) (err error, exitStatus int) { if remoteDest { fmt.Println("local files:", files, "remote filepath:", string(rec.cmd)) @@ -297,21 +297,21 @@ func main() { flag.BoolVar(&vopt, "v", false, "show version") flag.BoolVar(&dbg, "d", false, "debug logging") - flag.StringVar(&cAlg, "c", "C_AES_256", "cipher [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]") - flag.StringVar(&hAlg, "m", "H_SHA256", "hmac [\"H_SHA256\"]") - flag.UintVar(&port, "p", 2000, "port") + flag.StringVar(&cAlg, "c", "C_AES_256", "`cipher` [\"C_AES_256\" | \"C_TWOFISH_128\" | \"C_BLOWFISH_64\"]") + flag.StringVar(&hAlg, "m", "H_SHA256", "`hmac` [\"H_SHA256\"]") + flag.UintVar(&port, "p", 2000, "`port`") flag.StringVar(&authCookie, "a", "", "auth cookie") flag.BoolVar(&chaffEnabled, "e", true, "enabled chaff pkts (default true)") - flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt freq min (msecs)") - flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt freq max (msecs)") - flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt size max (bytes)") + flag.UintVar(&chaffFreqMin, "f", 100, "chaff pkt `freq` min (msecs)") + flag.UintVar(&chaffFreqMax, "F", 5000, "chaff pkt `freq` max (msecs)") + flag.UintVar(&chaffBytesMax, "B", 64, "chaff pkt `size` max (bytes)") // Find out what program we are (shell or copier) myPath := strings.Split(os.Args[0], string(os.PathSeparator)) if myPath[len(myPath)-1] != "hkexcp" && myPath[len(myPath)-1] != "hkexcp.exe" { // hkexsh accepts a command (-x) but not // a srcpath (-r) or dstpath (-t) - flag.StringVar(&cmdStr, "x", "", "command to run (default empty - interactive shell)") + flag.StringVar(&cmdStr, "x", "", "`command` to run (if not specified run interactive shell)") shellMode = true } else { // Note: only makes sense for client->server copies @@ -496,7 +496,7 @@ func main() { if shellMode { doShellMode(isInteractive, conn, oldState, rec) } else { - doCopyMode(conn, pathIsDest, fileArgs, recursiveCopy, rec) + doCopyMode(conn, pathIsDest, fileArgs, rec) } if oldState != nil { From 6389ad49d5a3dd786ef25362a65816237c4f87cf Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Sun, 26 Aug 2018 00:12:42 -0700 Subject: [PATCH 14/16] Improved flag.Usage() for cp/sh and removed more recursiveCopy flag remnants --- hkexsh/hkexsh.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index d9d4038..e646768 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -260,6 +260,19 @@ func doShellMode(isInteractive bool, conn *hkexnet.Conn, oldState *hkexsh.State, wg.Wait() } +func UsageShell() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "%s [opts] [user]@server\n", os.Args[0]) + flag.PrintDefaults() +} + +func UsageCp() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "%s [opts] srcFileOrDir [...] [user]@server[:dstpath]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "%s [opts] [user]@server[:srcFileOrDir] dstPath\n", os.Args[0]) + flag.PrintDefaults() +} + // hkexsh - a client for secure shell and file copy operations. // // While conforming to the basic net.Conn interface HKex.Conn has extra @@ -282,7 +295,6 @@ func main() { var port uint var cmdStr string - var recursiveCopy bool var copySrc []byte var copyDst string @@ -313,9 +325,9 @@ func main() { // a srcpath (-r) or dstpath (-t) flag.StringVar(&cmdStr, "x", "", "`command` to run (if not specified run interactive shell)") shellMode = true + flag.Usage = UsageShell } else { - // Note: only makes sense for client->server copies - flag.BoolVar(&recursiveCopy, "r", false, "recursive copy/preserve tree copy") + flag.Usage = UsageCp } flag.Parse() From 143990da3463f14aed25e14806d508872ecc97c5 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Thu, 30 Aug 2018 20:06:42 -0700 Subject: [PATCH 15/16] Scatter/gather for client->server copy now functional --- hkexsh/hkexsh.go | 17 ++++++++++++++--- hkexshd/hkexshd.go | 15 +++------------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/hkexsh/hkexsh.go b/hkexsh/hkexsh.go index e646768..0e37a1e 100755 --- a/hkexsh/hkexsh.go +++ b/hkexsh/hkexsh.go @@ -16,6 +16,7 @@ import ( "os" "os/exec" "os/user" + "path" "runtime" "strings" "sync" @@ -106,10 +107,20 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, rec *cmdSpec) //os.Setenv("TERM", "vt102") // TODO: server or client option? cmdName := "/bin/tar" - cmdArgs := []string{"-c", "-f", "/dev/stdout"} + cmdArgs := []string{"-cz", "-f", "/dev/stdout"} files = strings.TrimSpace(files) + // Awesome fact: tar actually can take multiple -C args, and + // changes to the dest dir *as it sees each one*. This enables + // its use below, where clients can send scattered sets of source + // files and dirs to be extraced to a single dest dir server-side, + // whilst preserving the subtrees of dirs on the other side. :) + // Eg., tar -c -f /dev/stdout -C /dirA fileInA -C /some/where/dirB fileInB /foo/dirC + // packages fileInA, fileInB, and dirC at a single toplevel in the tar. + // The tar authors are/were real smarties :) for _, v := range strings.Split(files, " ") { - cmdArgs = append(cmdArgs, v) + dirTmp, fileTmp := path.Split(v) + cmdArgs = append(cmdArgs, "-C", dirTmp, fileTmp) + //cmdArgs = append(cmdArgs, v) } fmt.Printf("[%v %v]\n", cmdName, cmdArgs) @@ -160,7 +171,7 @@ func doCopyMode(conn *hkexnet.Conn, remoteDest bool, files string, rec *cmdSpec) cmdName := "/bin/tar" destPath := files - cmdArgs := []string{"-x", "-C", destPath} + cmdArgs := []string{"-xz", "-C", destPath} fmt.Printf("[%v %v]\n", cmdName, cmdArgs) // NOTE the lack of quotes around --xform option's sed expression. // When args are passed in exec() format, no quoting is required diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go index 07a7251..9832bdf 100755 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -66,22 +66,13 @@ func runClientToServerCopyAs(who string, conn hkexnet.Conn, fpath string, chaffi } else { destDir = path.Join(u.HomeDir, fpath) } - //stat, pe := os.Stat(destDir) - //_ = stat - //if pe != nil { - // log.Fatal(pe) - // return pe, 252 // ?! - //} - //if stat.IsDir(destBase) { - cmdArgs := []string{"-x", "-C", destDir} - //} else { - // cmdArgs := []string{"-x", + cmdArgs := []string{"-xz", "-C", destDir} // NOTE the lack of quotes around --xform option's sed expression. // When args are passed in exec() format, no quoting is required // (as this isn't input from a shell) (right? -rlm 20180823) - //cmdArgs := []string{"-xvz", "-C", destPath, `--xform=s#.*/\(.*\)#\1#`} + //cmdArgs := []string{"-x", "-C", destDir, `--xform=s#.*/\(.*\)#\1#`} c = exec.Command(cmdName, cmdArgs...) c.Dir = destDir @@ -152,7 +143,7 @@ func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaf cmdName := "/bin/tar" //cmdArgs := []string{"-c", "-f", "-", srcPath} srcDir, srcBase := path.Split(srcPath) - cmdArgs := []string{"-c", "-C", srcDir, "-f", "-", srcBase} + cmdArgs := []string{"-cz", "-C", srcDir, "-f", "-", srcBase} c = exec.Command(cmdName, cmdArgs...) From c3572d7c0c7220ba171432f01b2f76d255647f77 Mon Sep 17 00:00:00 2001 From: Russ Magee Date: Thu, 30 Aug 2018 20:16:55 -0700 Subject: [PATCH 16/16] Fixed abs/relative path for client dest copies --- hkexshd/hkexshd.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hkexshd/hkexshd.go b/hkexshd/hkexshd.go index 9832bdf..a0874c9 100755 --- a/hkexshd/hkexshd.go +++ b/hkexshd/hkexshd.go @@ -141,7 +141,10 @@ func runServerToClientCopyAs(who string, conn hkexnet.Conn, srcPath string, chaf var c *exec.Cmd cmdName := "/bin/tar" - //cmdArgs := []string{"-c", "-f", "-", srcPath} + if !path.IsAbs(srcPath) { + srcPath = fmt.Sprintf("%s%c%s", u.HomeDir, os.PathSeparator, srcPath) + } + srcDir, srcBase := path.Split(srcPath) cmdArgs := []string{"-cz", "-C", srcDir, "-f", "-", srcBase}