├── .gitignore ├── LICENSE ├── README.md ├── examples └── main.go ├── readers.go └── sshproxy.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | id_rsa 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sshproxy 2 | ======== 3 | 4 | Golang library to proxy ssh connections 5 | 6 | 7 | 8 | ## Why 9 | 10 | I'm using this library in a honeypot, using this library I can intercept the ssh connections and connect each connection to their own container. Sessions can be recorder using the TypeWriterReadCloser. 11 | 12 | ## Use cases 13 | 14 | * capture the flag 15 | * honeypots 16 | * creating screencasts 17 | * whatever you'd like 18 | 19 | 20 | ## Example 21 | 22 | ``` 23 | go run examples/main.go --dest 172.16.84.182:22 --key examples/conf/id_rsa 24 | ``` 25 | 26 | Screencast of recorded session: 27 | 28 | http://jsfiddle.net/qorz0any/1/ 29 | 30 | ## Contributions 31 | Contributions are welcome. 32 | 33 | ## Creators 34 | 35 | **Remco Verhoef** 36 | - 37 | - 38 | 39 | ## Copyright and license 40 | 41 | Code and documentation copyright 2011-2014 Remco Verhoef. 42 | 43 | Code released under [the MIT license](LICENSE). 44 | 45 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | 10 | "github.com/dutchcoders/sshproxy" 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | func main() { 15 | listen := flag.String("listen", ":8022", "listen address") 16 | dest := flag.String("dest", ":22", "destination address") 17 | key := flag.String("key", "conf/id_rsa", "rsa key to use") 18 | flag.Parse() 19 | 20 | privateBytes, err := ioutil.ReadFile(*key) 21 | if err != nil { 22 | panic("Failed to load private key") 23 | } 24 | 25 | private, err := ssh.ParsePrivateKey(privateBytes) 26 | if err != nil { 27 | panic("Failed to parse private key") 28 | } 29 | 30 | var sessions map[net.Addr]map[string]interface{} = make(map[net.Addr]map[string]interface{}) 31 | 32 | config := &ssh.ServerConfig{ 33 | PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { 34 | fmt.Printf("Login attempt: %s, user %s password: %s", c.RemoteAddr(), c.User(), string(pass)) 35 | 36 | sessions[c.RemoteAddr()] = map[string]interface{}{ 37 | "username": c.User(), 38 | "password": string(pass), 39 | } 40 | 41 | clientConfig := &ssh.ClientConfig{} 42 | 43 | clientConfig.User = c.User() 44 | clientConfig.Auth = []ssh.AuthMethod{ 45 | ssh.Password(string(pass)), 46 | } 47 | 48 | client, err := ssh.Dial("tcp", *dest, clientConfig) 49 | 50 | sessions[c.RemoteAddr()]["client"] = client 51 | return nil, err 52 | }, 53 | } 54 | 55 | config.AddHostKey(private) 56 | 57 | sshproxy.ListenAndServe(*listen, config, func(c ssh.ConnMetadata) (*ssh.Client, error) { 58 | meta, _ := sessions[c.RemoteAddr()] 59 | 60 | fmt.Println(meta) 61 | 62 | client := meta["client"].(*ssh.Client) 63 | fmt.Printf("Connection accepted from: %s", c.RemoteAddr()) 64 | 65 | return client, err 66 | }, func(c ssh.ConnMetadata, r io.ReadCloser) (io.ReadCloser, error) { 67 | return sshproxy.NewTypeWriterReadCloser(r), nil 68 | }, func(c ssh.ConnMetadata) error { 69 | fmt.Println("Connection closed.") 70 | return nil 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /readers.go: -------------------------------------------------------------------------------- 1 | package sshproxy 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func NewTypeWriterReadCloser(r io.ReadCloser) io.ReadCloser { 13 | return &TypeWriterReadCloser{ReadCloser: r, time: time.Now()} 14 | } 15 | 16 | type TypeWriterReadCloser struct { 17 | io.ReadCloser 18 | 19 | time time.Time 20 | buffer bytes.Buffer 21 | } 22 | 23 | func sanitize(s string) string { 24 | s = strings.Replace(s, "\r", "", -1) 25 | s = strings.Replace(s, "\n", "
", -1) 26 | s = strings.Replace(s, "'", "\\'", -1) 27 | s = strings.Replace(s, "\b", "", -1) 28 | return s 29 | } 30 | 31 | func (lr *TypeWriterReadCloser) Read(p []byte) (n int, err error) { 32 | n, err = lr.ReadCloser.Read(p) 33 | 34 | now := time.Now() 35 | lr.buffer.WriteString(fmt.Sprintf(".wait(%d)", int(now.Sub(lr.time).Seconds()*1000))) 36 | lr.buffer.WriteString(fmt.Sprintf(".put('%s')", sanitize(string(p[:n])))) 37 | lr.time = now 38 | 39 | return n, err 40 | } 41 | 42 | func (lr *TypeWriterReadCloser) String() string { 43 | return lr.buffer.String() 44 | } 45 | 46 | func (lr *TypeWriterReadCloser) Close() error { 47 | return lr.ReadCloser.Close() 48 | } 49 | 50 | func NewLogReadCloser(r io.ReadCloser) io.ReadCloser { 51 | return &LogReadCloser{ReadCloser: r} 52 | } 53 | 54 | type LogReadCloser struct { 55 | io.ReadCloser 56 | } 57 | 58 | func (lr *LogReadCloser) Close() error { 59 | return lr.ReadCloser.Close() 60 | } 61 | 62 | func (lr *LogReadCloser) Read(p []byte) (n int, err error) { 63 | n, err = lr.ReadCloser.Read(p) 64 | log.Print(string(p[:n])) 65 | return n, err 66 | } 67 | -------------------------------------------------------------------------------- /sshproxy.go: -------------------------------------------------------------------------------- 1 | package sshproxy 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | 8 | "golang.org/x/crypto/ssh" 9 | ) 10 | 11 | type SshConn struct { 12 | net.Conn 13 | config *ssh.ServerConfig 14 | callbackFn func(c ssh.ConnMetadata) (*ssh.Client, error) 15 | wrapFn func(c ssh.ConnMetadata, r io.ReadCloser) (io.ReadCloser, error) 16 | closeFn func(c ssh.ConnMetadata) error 17 | } 18 | 19 | func (p *SshConn) serve() error { 20 | serverConn, chans, reqs, err := ssh.NewServerConn(p, p.config) 21 | if err != nil { 22 | log.Println("failed to handshake") 23 | return (err) 24 | } 25 | 26 | defer serverConn.Close() 27 | 28 | clientConn, err := p.callbackFn(serverConn) 29 | if err != nil { 30 | log.Printf("%s", err.Error()) 31 | return (err) 32 | } 33 | 34 | defer clientConn.Close() 35 | 36 | go ssh.DiscardRequests(reqs) 37 | 38 | for newChannel := range chans { 39 | 40 | channel2, requests2, err2 := clientConn.OpenChannel(newChannel.ChannelType(), newChannel.ExtraData()) 41 | if err2 != nil { 42 | log.Printf("Could not accept client channel: %s", err.Error()) 43 | return err 44 | } 45 | 46 | channel, requests, err := newChannel.Accept() 47 | if err != nil { 48 | log.Printf("Could not accept server channel: %s", err.Error()) 49 | return err 50 | } 51 | 52 | // connect requests 53 | go func() { 54 | log.Printf("Waiting for request") 55 | 56 | r: 57 | for { 58 | var req *ssh.Request 59 | var dst ssh.Channel 60 | 61 | select { 62 | case req = <-requests: 63 | dst = channel2 64 | case req = <-requests2: 65 | dst = channel 66 | } 67 | 68 | log.Printf("Request: %s %s %s %s\n", dst, req.Type, req.WantReply, req.Payload) 69 | 70 | b, err := dst.SendRequest(req.Type, req.WantReply, req.Payload) 71 | if err != nil { 72 | log.Printf("%s", err) 73 | } 74 | 75 | if req.WantReply { 76 | req.Reply(b, nil) 77 | } 78 | 79 | switch req.Type { 80 | case "exit-status": 81 | break r 82 | case "exec": 83 | // not supported (yet) 84 | default: 85 | log.Println(req.Type) 86 | } 87 | } 88 | 89 | channel.Close() 90 | channel2.Close() 91 | }() 92 | 93 | // connect channels 94 | log.Printf("Connecting channels.") 95 | 96 | var wrappedChannel io.ReadCloser = channel 97 | var wrappedChannel2 io.ReadCloser = channel2 98 | 99 | if p.wrapFn != nil { 100 | // wrappedChannel, err = p.wrapFn(channel) 101 | wrappedChannel2, err = p.wrapFn(serverConn, channel2) 102 | } 103 | 104 | go io.Copy(channel2, wrappedChannel) 105 | go io.Copy(channel, wrappedChannel2) 106 | 107 | defer wrappedChannel.Close() 108 | defer wrappedChannel2.Close() 109 | } 110 | 111 | if p.closeFn != nil { 112 | p.closeFn(serverConn) 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func ListenAndServe(addr string, serverConfig *ssh.ServerConfig, 119 | callbackFn func(c ssh.ConnMetadata) (*ssh.Client, error), 120 | wrapFn func(c ssh.ConnMetadata, r io.ReadCloser) (io.ReadCloser, error), 121 | closeFn func(c ssh.ConnMetadata) error, 122 | ) error { 123 | listener, err := net.Listen("tcp", addr) 124 | if err != nil { 125 | log.Printf("net.Listen failed: %v", err) 126 | return err 127 | } 128 | 129 | defer listener.Close() 130 | 131 | for { 132 | conn, err := listener.Accept() 133 | if err != nil { 134 | log.Printf("listen.Accept failed: %v", err) 135 | return err 136 | } 137 | 138 | sshconn := &SshConn{Conn: conn, config: serverConfig, callbackFn: callbackFn, wrapFn: wrapFn, closeFn: closeFn} 139 | 140 | go func() { 141 | if err := sshconn.serve(); err != nil { 142 | log.Printf("Error occured while serving %s\n", err) 143 | return 144 | } 145 | 146 | log.Println("Connection closed.") 147 | }() 148 | } 149 | 150 | } 151 | --------------------------------------------------------------------------------