commit 62afbe61463b8b0f35e25511d3273da16d0d57bd Author: Bolke de Bruin Date: Tue Jun 30 09:40:53 2020 +0200 First working version diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..bfd90b7 --- /dev/null +++ b/errors.go @@ -0,0 +1,53 @@ +package main + +// RejectOption represents an option used to control the way connection is +// rejected. +type RejectOption func(*rejectConnectionError) + +// RejectionReason returns an option that makes connection to be rejected with +// given reason. +func RejectionReason(reason string) RejectOption { + return func(err *rejectConnectionError) { + err.reason = reason + } +} + +// RejectionStatus returns an option that makes connection to be rejected with +// given HTTP status code. +func RejectionStatus(code int) RejectOption { + return func(err *rejectConnectionError) { + err.code = code + } +} + +// RejectionHeader returns an option that makes connection to be rejected with +// given HTTP headers. +func RejectionHeader(h HandshakeHeader) RejectOption { + return func(err *rejectConnectionError) { + err.header = h + } +} +// RejectConnectionError constructs an error that could be used to control the way +// handshake is rejected by Upgrader. +func RejectConnectionError(options ...RejectOption) error { + err := new(rejectConnectionError) + for _, opt := range options { + opt(err) + } + return err +} + +// rejectConnectionError represents a rejection of upgrade error. +// +// It can be returned by Upgrader's On* hooks to control the way WebSocket +// handshake is rejected. +type rejectConnectionError struct { + reason string + code int + header HandshakeHeader +} + +// Error implements error interface. +func (r *rejectConnectionError) Error() string { + return r.reason +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..a45134d --- /dev/null +++ b/http.go @@ -0,0 +1,21 @@ +package main + +import ( + "net/http" + "strconv" +) + +const ( + HttpOK = "HTTP/1.1 200 OK\r\n" + MethodRDGIN = "RDG_IN_DATA" + MethodRDGOUT = "RDG_OUT_DATA" +) + +// httpError is like the http.Error with WebSocket context exception. +func httpError(w http.ResponseWriter, body string, code int) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Content-Length", strconv.Itoa(len(body))) + w.WriteHeader(code) + w.Write([]byte(body)) +} + diff --git a/main.go b/main.go new file mode 100644 index 0000000..035182f --- /dev/null +++ b/main.go @@ -0,0 +1,213 @@ +package main + +import ( + "crypto/tls" + "net/http/httputil" + "os" + //"time" + + //"bytes" + "fmt" + "log" + //"strings" + // "io" + "net/http" + //"net/http/httputil" + //"math/rand" + //"encoding/binary" + //"encoding/base64" +) + + +/* +func handleConnection(s *MySession) { + inData := make([]byte, 4096) + + for { + size, err := s.buffIn.Read(inData) + if err != nil { + s.inConn.Close() + s.outConn.Close() + fmt.Println(err) + } + fmt.Printf("Bytes read on IN %d\n", size) + } +}*/ + +/* +func MethodOverride(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Println(r.Method) + dump, _ := httputil.DumpRequest(r, false) + fmt.Printf("%q\n", dump) + + headerKey := "Rdg-Connection-Id" + connId := r.Header.Get(headerKey) + if connId != "" { + s.guid = connId + } + auth := r.Header.Get("Authorization") + fmt.Printf("Connection ID: %s\n", s.guid) + + if strings.Contains(auth,"NTLMX") { + /*var msg_req_b []byte + base64.StdEncoding.Decode(msg_req_b, []byte(auth[strings.Index(auth,"NTLM")+6:])) + + msg_type := binary.LittleEndian.Uint32(msg_req_b[0:4]) + fmt.Printf("Message type %v\n", msg_type) + if msg_type == 1 { + var nonce [8]byte + r := make([]byte, 8) + rand.Read(r) + copy(nonce[:], r) + + sig_buf := new(bytes.Buffer) + var signature [8]byte + binary.Write(sig_buf, binary.LittleEndian, "NTLMSSP\000") + copy(signature[:], sig_buf.Bytes()) + + zero := make([]byte, 7) + pad := make([]byte, 2) + + rand.Read(nonce) + + buf := new(bytes.Buffer) + msg := NtlmChallenge{ + signature, + uint32(0x02), + 0, + 0, + 0, + []byte(), + nonce, + 0, + 0 + } + _ := binary.Write(buf, binary.LittleEndian, msg) + header := "NTLM" + base64.StdEncoding.EncodeToString(buf.Bytes()) + w.Header().Set("WWW-Authenticate", header) + w.WriteHeader(401) + w.Write([]byte("Unauthorized.\n")) + fmt.Println("Unauthorized") + return + } + } else { + _, _, ok := r.BasicAuth() + + if !ok && !s.hasIn { + w.Header().Set("WWW-Authenticate", `Basic realm="rdpgw"`) + w.WriteHeader(401) + w.Write([]byte("Unauthorized.\n")) + fmt.Println("Unauthorized") + return + } + } + + if r.Method == "RDG_OUT_DATA" { + fmt.Println("Hijacking OUT") + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.outConn = conn + s.buffOut = bufrw + + if !s.hasOut { + fmt.Printf("Creating OUT and sending seed\n") + s.hasOut = true + seed := make([]byte, 100) + rand.Read(seed) + bufrw.WriteString("HTTP/1.1 200 OK\r\n") + fmt.Fprintf(bufrw, "Date: %s\r\n", time.Now().Format(time.RFC1123)) + bufrw.WriteString("Content-Type: application/octet-stream\r\n") + bufrw.WriteString("Content-Length: 0\r\n") + bufrw.WriteString(crlf) + bufrw.Write(seed) + bufrw.Flush() + return + } else { + fmt.Printf("Handle OUT\n") + handleConnection(s) + return + } + } + + if r.Method == "RDG_IN_DATA" { + fmt.Println("Hijacking IN") + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + s.inConn = conn + s.buffIn = bufrw + + if !s.hasIn { + fmt.Printf("Creating IN and sending seed\n") + s.hasIn = true + seed := make([]byte, 100) + rand.Read(seed) + bufrw.WriteString("HTTP/1.1 200 OK\r\n") + fmt.Fprintf(bufrw, "Date: %s\r\n", time.Now().Format(time.RFC1123)) + bufrw.WriteString("Content-Type: application/octet-stream\r\n") + bufrw.WriteString("Content-Length: 0\r\n") + bufrw.WriteString(crlf) + bufrw.Write(seed) + bufrw.Flush() + return + } else { + fmt.Printf("Handle IN\n") + + handleConnection(s) + return + } + } + + next.ServeHTTP(w, r) + }) +} +*/ + +func HelloServer(w http.ResponseWriter, req *http.Request) { + dump, _ := httputil.DumpRequest(req, true) + fmt.Println(dump) + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("This is an example server.\n")) + // io.WriteString(w, "This is an example server.\n") +} + +func main() { + fmt.Println("Hello!") + mux := http.NewServeMux() + mux.HandleFunc("*", HelloServer) + + w, err := os.OpenFile("tls-secrets.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + cfg := &tls.Config{ + KeyLogWriter: w, + } + cert, err := tls.LoadX509KeyPair("server.pem", "key.pem") + if err != nil { + log.Fatal(err) + } + cfg.Certificates = append(cfg.Certificates, cert) + server := http.Server{ + Addr: ":8000", + Handler: Upgrade(mux), + TLSConfig: cfg, + } + err = server.ListenAndServeTLS("","") + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +} \ No newline at end of file diff --git a/ntlm.go b/ntlm.go new file mode 100644 index 0000000..c1471b2 --- /dev/null +++ b/ntlm.go @@ -0,0 +1,13 @@ +package main + +type NtlmChallenge struct { + Signature [8]byte + MsgType uint32 + TargetNameLen uint16 + TargetNameMaxLen uint16 + TargetNameBufferOffset uint32 + NegotiateFlags [4]byte + ServerChallenge [8]byte + Reserved [8]byte + TargetInfoBytes [8]byte +} diff --git a/rdg.go b/rdg.go new file mode 100644 index 0000000..f2a0934 --- /dev/null +++ b/rdg.go @@ -0,0 +1,393 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "io" + "log" + "math/rand" + "net" + "net/http" + "strconv" + "time" + "unicode/utf16" + "unicode/utf8" +) + +const ( + crlf = "\r\n" + rdgConnectionIdKey = "Rdg-Connection-Id" + HANDSHAKE = 1 +) + +const ( + PKT_TYPE_HANDSHAKE_REQUEST = 0x1 + PKT_TYPE_HANDSHAKE_RESPONSE = 0x2 + PKT_TYPE_EXTENDED_AUTH_MSG = 0x3 + PKT_TYPE_TUNNEL_CREATE = 0x4 + PKT_TYPE_TUNNEL_RESPONSE = 0x5 + PKT_TYPE_TUNNEL_AUTH = 0x6 + PKT_TYPE_TUNNEL_AUTH_RESPONSE = 0x7 + PKT_TYPE_CHANNEL_CREATE = 0x8 + PKT_TYPE_CHANNEL_RESPONSE = 0x9 + PKT_TYPE_DATA = 0xA + PKT_TYPE_SERVICE_MESSAGE = 0xB + PKT_TYPE_REAUTH_MESSAGE = 0xC + PKT_TYPE_KEEPALIVE = 0xD + PKT_TYPE_CLOSE_CHANNEL = 0x10 + PKT_TYPE_CLOSE_CHANNEL_RESPONSE = 0x11 +) + +const ( + HTTP_EXTENDED_AUTH_NONE = 0x0 + HTTP_EXTENDED_AUTH_SC = 0x1 /* Smart card authentication. */ + HTTP_EXTENDED_AUTH_PAA = 0x02 /* Pluggable authentication. */ + HTTP_EXTENDED_AUTH_SSPI_NTLM = 0x04 /* NTLM extended authentication. */ +) + +const ( + HTTP_TUNNEL_PACKET_FIELD_PAA_COOKIE = 0x1 +) + +// HandshakeHeader is the interface that writes both upgrade request or +// response headers into a given io.Writer. +type HandshakeHeader interface { + io.WriterTo +} + +type RdgSession struct { + ConnId string + CorrelationId string + UserId string + ConnIn net.Conn + ConnOut net.Conn + BufOut *bufio.Writer + BufIn *bufio.Reader + State int + Remote net.Conn +} + +// ErrNotHijacker is an error returned when http.ResponseWriter does not +// implement http.Hijacker interface. +var ErrNotHijacker = RejectConnectionError( + RejectionStatus(http.StatusInternalServerError), + RejectionReason("given http.ResponseWriter is not a http.Hijacker"), +) + +var DefaultSession RdgSession + +func Upgrade(next http.Handler) http.Handler { + return DefaultSession.RdgHandshake(next) +} + +func Accept(w http.ResponseWriter) (conn net.Conn, rw *bufio.ReadWriter, err error) { + log.Print("Accept connection") + hj, ok := w.(http.Hijacker) + if ok { + return hj.Hijack() + } else { + err = ErrNotHijacker + } + if err != nil { + httpError(w, err.Error(), http.StatusInternalServerError) + return nil, nil, err + } + return +} + +func (s RdgSession) RdgHandshake(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + /*_, _, ok := r.BasicAuth() + + if !ok && s.ConnIn == nil { + w.Header().Set("WWW-Authenticate", `Basic realm="rdpgw"`) + w.WriteHeader(401) + w.Write([]byte("Unauthorized.\n")) + fmt.Println("Unauthorized") + return + }*/ + + conn, rw, _ := Accept(w) + if r.Method == MethodRDGOUT { + log.Printf("Opening RDGOUT for client %s", conn.RemoteAddr().String()) + s.ConnId = r.Header.Get(rdgConnectionIdKey) + s.ConnOut = conn + s.BufOut = rw.Writer + WriteAcceptSeed(rw.Writer) + rw.Writer.Flush() + } else if r.Method == MethodRDGIN { + if s.ConnIn == nil { + s.ConnIn = conn + s.BufIn = rw.Reader + log.Printf("Opening RDGIN for client %s", conn.RemoteAddr().String()) + WriteAcceptSeed(rw.Writer) + rw.Writer.Flush() + p := make([]byte, 4096) + rw.Reader.Read(p) + //log.Printf("Read %q", p) + + log.Printf("Reading packet from client %s", conn.RemoteAddr().String()) + scanner := bufio.NewScanner(rw.Reader) + scanner.Split(ReadPacket) + for scanner.Scan() { + packet := scanner.Bytes() + packetType, size, _, packet := readHeader(packet) + log.Printf("Scanned packet got packet type %x size %d", packetType, size) + switch packetType { + case PKT_TYPE_HANDSHAKE_REQUEST: + major, minor, _, auth := readHandshake(packet) + sendHandshakeResponse(s.BufOut, major, minor, auth) + case PKT_TYPE_TUNNEL_CREATE: + readCreateTunnelRequest(packet) + sendCreateTunnelResponse(s.BufOut) + case PKT_TYPE_TUNNEL_AUTH: + readTunnelAuthRequest(packet) + sendTunnelAuthResponse(s.BufOut) + case PKT_TYPE_CHANNEL_CREATE: + server, port := readChannelCreateRequest(packet) + var err error + s.Remote, err = net.Dial("tcp", "localhost:3389") + if err != nil { + log.Printf("Error connecting to %s, %d, %s", server, port, err) + return + } + sendChannelCreateResponse(s.BufOut) + case PKT_TYPE_DATA: + receiveDataPacket(s.Remote, packet) + go sendDataPacket(s.Remote, s.BufOut) + } + } + } + } + }) +} + +// [MS-TSGU]: Terminal Services Gateway Server Protocol version 39.0 +// The server sends back the final status code 200 OK, and also a random entity body of limited size (100 bytes). +// This enables a reverse proxy to start allowing data from the RDG server to the RDG client. The RDG server does +// not specify an entity length in its response. It uses HTTP 1.0 semantics to send the entity body and closes the +// connection after the last byte is sent. +func WriteAcceptSeed(bw *bufio.Writer) { + bw.WriteString(HttpOK) + bw.WriteString("Date: " + time.Now().Format(time.RFC1123) + "\r\n") + bw.WriteString("Content-Type: application/octet-stream\r\n") + bw.WriteString("Content-Length: 0\r\n") + bw.WriteString(crlf) + seed := make([]byte, 10) + rand.Read(seed) + bw.Write(seed) +} + +func ReadPacket(data []byte, atEOF bool) (advance int, packet []byte, err error) { + log.Printf("Reading data len = %d", len(data)) + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + if i := bytes.Index(data, []byte{'\r', '\n'}); i >= 0 { + //log.Printf("Got rn at %d ", i) + chunkSize, err := strconv.ParseInt(string(data[0:i]), 16, 0) + log.Printf("chunkSize %d", chunkSize) + if err != nil { + return i + 2, data[0:i], err + } + //log.Printf("Return %d", i+2+int(chunkSize)+2) + return i + 2 + int(chunkSize) + 2, data[i+2 : i+2+int(chunkSize)+2], nil + } + + if atEOF { + return len(data), data, nil + } + + return 0, nil, nil +} + +func readHeader(data []byte) (packetType uint16, size uint32, advance int, remain []byte) { + r := bytes.NewReader(data) + binary.Read(r, binary.LittleEndian, &packetType) + r.Seek(4, io.SeekStart) + binary.Read(r, binary.LittleEndian, &size) + return packetType, size, 8, data[8:] +} + +func sendHandshakeResponse(w *bufio.Writer, major byte, minor byte, auth uint16) { + buf := new(bytes.Buffer) + binary.Write(buf, binary.LittleEndian, uint32(0)) // error_code + buf.Write([]byte{major, minor}) + binary.Write(buf, binary.LittleEndian, uint16(0)) // server version + binary.Write(buf, binary.LittleEndian, uint16(2)) // PAA + + w.Write(createPacket(PKT_TYPE_HANDSHAKE_RESPONSE, buf.Bytes())) + w.Flush() +} + +func readHandshake(data []byte) (major byte, minor byte, version uint16, extAuth uint16) { + r := bytes.NewReader(data) + binary.Read(r, binary.LittleEndian, &major) + binary.Read(r, binary.LittleEndian, &minor) + binary.Read(r, binary.LittleEndian, &version) + binary.Read(r, binary.LittleEndian, &extAuth) + + log.Printf("major: %d, minor: %d, version: %d, ext auth: %d", major, minor, version, extAuth) + return +} + +func readCreateTunnelRequest(data []byte) (caps uint32, cookie string){ + var fields uint16 + + r := bytes.NewReader(data) + + binary.Read(r, binary.LittleEndian, &caps) + binary.Read(r, binary.LittleEndian, &fields) + r.Seek(2, io.SeekCurrent) + + if fields == HTTP_TUNNEL_PACKET_FIELD_PAA_COOKIE { + var size uint16 + binary.Read(r, binary.LittleEndian, &size) + // skip decoding paa for now + } + log.Printf("Create tunnel caps: %d", caps) + return +} + +func sendCreateTunnelResponse(w *bufio.Writer) { + buf := new(bytes.Buffer) + + binary.Write(buf, binary.LittleEndian, uint16(0)) // server version + binary.Write(buf, binary.LittleEndian, uint32(0)) // error code + binary.Write(buf, binary.LittleEndian, uint16(0)) // fields present + binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved + + w.Write(createPacket(PKT_TYPE_TUNNEL_RESPONSE, buf.Bytes())) + w.Flush() +} + +func readTunnelAuthRequest(data []byte) { + buf := bytes.NewReader(data) + + var size uint16 + binary.Read(buf, binary.LittleEndian, &size) + clData := make([]byte, size) + binary.Read(buf, binary.LittleEndian, &clData) + clientName, _ := DecodeUTF16(clData) + log.Printf("Client: %s", clientName) +} + +func sendTunnelAuthResponse(w *bufio.Writer) { + buf := new(bytes.Buffer) + + binary.Write(buf, binary.LittleEndian, uint32(0)) // error code + binary.Write(buf, binary.LittleEndian, uint16(0)) // fields present + binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved + + w.Write(createPacket(PKT_TYPE_TUNNEL_AUTH_RESPONSE, buf.Bytes())) + w.Flush() +} + +func readChannelCreateRequest(data []byte) (server string, port uint16){ + buf := bytes.NewReader(data) + + var resourcesSize byte + var alternative byte + var protocol uint16 + var nameSize uint16 + + binary.Read(buf, binary.LittleEndian, &resourcesSize) + binary.Read(buf, binary.LittleEndian, &alternative) + binary.Read(buf, binary.LittleEndian, &port) + binary.Read(buf, binary.LittleEndian, &protocol) + binary.Read(buf, binary.LittleEndian, &nameSize) + + nameData := make([]byte, nameSize) + binary.Read(buf, binary.LittleEndian, &nameData) + + log.Printf("Name data %q", nameData) + server, _ = DecodeUTF16(nameData) + + log.Printf("Should connect to %s on port %d", server, port) + return +} + +func sendChannelCreateResponse(w *bufio.Writer) { + buf := new(bytes.Buffer) + + binary.Write(buf, binary.LittleEndian, uint32(0)) // error code + binary.Write(buf, binary.LittleEndian, uint16(0)) // fields present + binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved + + w.Write(createPacket(PKT_TYPE_CHANNEL_RESPONSE, buf.Bytes())) + w.Flush() +} + +func createPacket(pktType uint16, data []byte) (packet []byte){ + size := len(data) + 8 + buf := new(bytes.Buffer) + + log.Printf("Data sent Size: %d", size) + // http chunk size in hex string + // fmt.Fprintf(buf,"%x\r\n", size) + + binary.Write(buf, binary.LittleEndian, uint16(pktType)) + binary.Write(buf, binary.LittleEndian, uint16(0)) // reserved + binary.Write(buf, binary.LittleEndian, uint32(size)) + buf.Write(data) + + // http close crlf + // buf.Write([]byte(crlf)) + // log.Printf("data sent: %q", buf.Bytes()) + return buf.Bytes() +} + +func receiveDataPacket(conn net.Conn, data []byte) { + buf := bytes.NewReader(data) + + var cblen uint16 + binary.Read(buf, binary.LittleEndian, &cblen) + log.Printf("Received PKT_DATA %d", cblen) + pkt := make([]byte, cblen) + //binary.Read(buf, binary.LittleEndian, &pkt) + buf.Read(pkt) + //log.Printf("DATA FROM CLIENT %q", pkt) + conn.Write(pkt) +} + +func sendDataPacket(conn net.Conn, w *bufio.Writer) { + b1 := new(bytes.Buffer) + buf := make([]byte, 32767) + for { + n, err := conn.Read(buf) + binary.Write(b1, binary.LittleEndian, uint16(n)) + log.Printf("RDP SIZE: %d", n) + if err != nil { + log.Printf("Error reading from conn %s", err) + break + } + b1.Write(buf[:n]) + w.Write(createPacket(PKT_TYPE_DATA, b1.Bytes())) + w.Flush() + b1.Reset() + } +} + +func DecodeUTF16(b []byte) (string, error) { + if len(b)%2 != 0 { + log.Printf("Error decoding utf16") + return "", fmt.Errorf("Must have even length byte slice") + } + + u16s := make([]uint16, 1) + ret := &bytes.Buffer{} + b8buf := make([]byte, 4) + + lb := len(b) + for i := 0; i < lb; i += 2 { + u16s[0] = uint16(b[i]) + (uint16(b[i+1]) << 8) + r := utf16.Decode(u16s) + n := utf8.EncodeRune(b8buf, r[0]) + ret.Write(b8buf[:n]) + } + + return ret.String(), nil +} \ No newline at end of file diff --git a/server.go b/server.go new file mode 100644 index 0000000..1110061 --- /dev/null +++ b/server.go @@ -0,0 +1,3 @@ +package main + +