diff --git a/go.mod b/go.mod index d364687..107a5e4 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,15 @@ require ( github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 // indirect github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/fsnotify/fsnotify v1.6.0 + github.com/go-ldap/ldap/v3 v3.4.3 // indirect github.com/gorilla/websocket v1.4.2 github.com/jpillora/ansi v1.0.2 // indirect github.com/jpillora/backoff v1.0.0 github.com/jpillora/requestlog v1.0.0 github.com/jpillora/sizestr v1.0.0 github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect - golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e - golang.org/x/net v0.0.0-20210614182718-04defd469f4e + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect ) diff --git a/go.sum b/go.sum index 0a52b5b..5dc2655 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,15 @@ +github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ= +github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM= github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.3 h1:JCKUtJPIcyOuG7ctGabLKMgIlKnGumD/iGjuWeEruDI= +github.com/go-ldap/ldap/v3 v3.4.3/go.mod h1:7LdHfVt6iIOESVEe3Bs4Jp2sHEKgDeduAhgM1/f9qmo= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jpillora/ansi v1.0.2 h1:+Ei5HCAH0xsrQRCT2PDr4mq9r4Gm4tg+arNdXRkB22s= @@ -18,9 +24,13 @@ github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9 github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main.go b/main.go index eb50e6c..3c121c5 100644 --- a/main.go +++ b/main.go @@ -159,9 +159,13 @@ var serverHelp = ` provide a certificate notification email by setting CHISEL_LE_EMAIL. --tls-ca, a path to a PEM encoded CA certificate bundle or a directory - holding multiple PEM encode CA certificate bundle files, which is used to - validate client connections. The provided CA certificates will be used - instead of the system roots. This is commonly used to implement mutual-TLS. + holding multiple PEM encode CA certificate bundle files, which is used to + validate client connections. The provided CA certificates will be used + instead of the system roots. This is commonly used to implement mutual-TLS. + + --ldap-config, a path to a file containing the ldap authentication + configuration. + ` + commonHelp func server(args []string) { @@ -181,6 +185,7 @@ func server(args []string) { flags.StringVar(&config.TLS.Cert, "tls-cert", "", "") flags.Var(multiFlag{&config.TLS.Domains}, "tls-domain", "") flags.StringVar(&config.TLS.CA, "tls-ca", "", "") + flags.StringVar(&config.LdapConfigFile,"ldap-config","","") host := flags.String("host", "", "") p := flags.String("p", "", "") @@ -322,7 +327,7 @@ var clientHelp = ` client's internal SOCKS5 proxy. When stdio is used as local-host, the tunnel will connect standard - input/output of this program with the remote. This is useful when + input/output of this program with the remote. This is useful when combined with ssh ProxyCommand. You can use ssh -o ProxyCommand='chisel client chiselserver stdio:%h:%p' \ user@example.com @@ -381,11 +386,11 @@ var clientHelp = ` may be still verified (see --fingerprint) after inner connection is established. - --tls-key, a path to a PEM encoded private key used for client + --tls-key, a path to a PEM encoded private key used for client authentication (mutual-TLS). - --tls-cert, a path to a PEM encoded certificate matching the provided - private key. The certificate must have client authentication + --tls-cert, a path to a PEM encoded certificate matching the provided + private key. The certificate must have client authentication enabled (mutual-TLS). ` + commonHelp diff --git a/server/server.go b/server/server.go index b7df128..951f684 100644 --- a/server/server.go +++ b/server/server.go @@ -30,6 +30,8 @@ type Config struct { Reverse bool KeepAlive time.Duration TLS TLSConfig + LdapConfigFile string + LdapConfig settings.LdapConfig } // Server respresent a chisel service @@ -113,6 +115,12 @@ func NewServer(c *Config) (*Server, error) { if c.Reverse { server.Infof("Reverse tunnelling enabled") } + // ldap authentication + if c.LdapConfigFile != "" { + if c.LdapConfig,err = server.LdapParseConfig(c.LdapConfigFile); err != nil { + return nil, err + } + } return server, nil } @@ -177,7 +185,7 @@ func (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions // check the user exists and has matching password n := c.User() user, found := s.users.Get(n) - if !found || user.Pass != string(password) { + if !found || ( user.Pass != string(password) && s.config.LdapConfigFile == "" ) || ( s.config.LdapConfigFile != "" && found && settings.LdapAuthUser(user,password,s.config.LdapConfig) != nil) { s.Debugf("Login failed for user: %s", n) return nil, errors.New("Invalid authentication for username: %s") } @@ -215,3 +223,9 @@ func (s *Server) DeleteUser(user string) { func (s *Server) ResetUsers(users []*settings.User) { s.users.Reset(users) } + +// LdapParseConfig is validating the given ldap config +func (s *Server) LdapParseConfig(LdapConfigFile string) (settings.LdapConfig,error) { + ldapConfig,err := settings.ParseConfigFile(LdapConfigFile) + return ldapConfig,err +} diff --git a/share/cos/pprof.go b/share/cos/pprof.go index 6c95b7e..5b09b4b 100644 --- a/share/cos/pprof.go +++ b/share/cos/pprof.go @@ -1,3 +1,4 @@ +//go:build pprof // +build pprof package cos diff --git a/share/cos/signal.go b/share/cos/signal.go index f44e448..a26157d 100644 --- a/share/cos/signal.go +++ b/share/cos/signal.go @@ -1,4 +1,5 @@ -//+build !windows +//go:build !windows +// +build !windows package cos diff --git a/share/cos/signal_windows.go b/share/cos/signal_windows.go index 747b3d4..c72219d 100644 --- a/share/cos/signal_windows.go +++ b/share/cos/signal_windows.go @@ -1,4 +1,5 @@ -//+build windows +//go:build windows +// +build windows package cos diff --git a/share/settings/ldap.go b/share/settings/ldap.go new file mode 100644 index 0000000..4402c67 --- /dev/null +++ b/share/settings/ldap.go @@ -0,0 +1,255 @@ +package settings + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "log" + + "github.com/go-ldap/ldap/v3" +) + +//LdapConfig enables LDAP auth +type LdapConfig struct { + BindDN string `json:"BindDN"` + BindPassword string `json:"BindPassword"` + Url string `json:"Url"` + BaseDN string `json:"BaseDN"` + Filter string `json:"Filter"` + IDMapTo string `json:"IDMapTo"` + CA string `json:"CA"` + Insecure bool `json:"Insecure"` +} + +// parse the Ldap config file +func ParseConfigFile(Configfile string) (LdapConfig, error) { + var ldapConfig LdapConfig + file, err := ioutil.ReadFile(Configfile) + if err != nil { + return ldapConfig, fmt.Errorf("Ldap config file error") + } + err = json.Unmarshal([]byte(file), &ldapConfig) + if err != nil { + return ldapConfig, fmt.Errorf("Error occured during unmarshaling ldap config file") + } + return ldapConfig, nil +} + +// authenticate a user using ldap credentials +func LdapAuthUser(user *User,password []byte,ldapconfig LdapConfig) error { + log.Printf("User %s to be authenticated in LDAP",user.Name) + l, err := ConnectTLS(ldapconfig) + if err != nil { + log.Printf("Error occured during TLS connection to %s",ldapconfig.Url) + return fmt.Errorf("Error occured during TLS connection to %s",ldapconfig.Url) + } + defer l.Close() + // Normal Bind and Search + result, err := BindAndSearch(l,ldapconfig,user) + if err != nil { + log.Printf("User %s not found in LDAP",user.Name) + return fmt.Errorf("User %s not found in LDAP",user.Name) + } + userdn := result.Entries[0].DN + log.Printf("DN:%s",userdn) + + if len(result.Entries) != 1 { + log.Printf("too many entries returned") + return fmt.Errorf("too many entries returned") + } +// Bind as the user to verify their password + err = l.Bind(userdn,string(password[:])) + if err != nil { + return fmt.Errorf("Bad password for user %s",user.Name) + } + + return nil +} + +// Ldap Connection with TLS +func ConnectTLS(ldapconfig LdapConfig) (*ldap.Conn, error) { + var tlsConf *tls.Config +// var certpool *x509.CertPool + + if ldapconfig.Insecure { + tlsConf = &tls.Config{InsecureSkipVerify: true} + } + + if ldapconfig.CA != "" { + certpool := x509.NewCertPool() + CAfile, err := ioutil.ReadFile(ldapconfig.CA) + if err != nil { + return nil, fmt.Errorf("Ldap CA file error") + } + certpool.AppendCertsFromPEM([]byte(CAfile)) + tlsConf = &tls.Config{RootCAs: certpool} + } + + l, err := ldap.DialTLS("tcp", ldapconfig.Url, tlsConf) + if err != nil { + return nil, err + } + + return l, nil +} + +// Normal Bind and Search +func BindAndSearch(l *ldap.Conn,ldapconfig LdapConfig,user *User) (*ldap.SearchResult, error) { + var filter string + l.Bind(ldapconfig.BindDN, ldapconfig.BindPassword) + if ldapconfig.Filter != "" { + filter = fmt.Sprintf("(&(%s)(%s=%s))",ldapconfig.Filter,ldapconfig.IDMapTo,user.Name) + } else { + filter = fmt.Sprintf("(%s=%s)",ldapconfig.IDMapTo,user.Name) + } + log.Printf("filter %s",filter) + searchReq := ldap.NewSearchRequest( + ldapconfig.BaseDN, + ldap.ScopeWholeSubtree, + ldap.NeverDerefAliases, + 0, + 0, + false, + filter, + []string{"dn"}, + nil, + ) + result, err := l.Search(searchReq) + if err != nil { + log.Printf("Search Error: %s", err) + return nil, fmt.Errorf("Search Error: %s", err) + } + + if len(result.Entries) > 0 { + return result, nil + } else { + return nil, fmt.Errorf("Couldn't fetch search entries") + } +} + + +// package main +// +// import ( +// "fmt" +// "log" +// +// "github.com/go-ldap/ldap/v3" +// ) +// +// const ( +// BindUsername = "user@example.com" +// BindPassword = "password" +// FQDN = "DC.example.com" +// BaseDN = "cn=Configuration,dc=example,dc=com" +// Filter = "(objectClass=*)" +// ) +// +// func main() { +// // TLS Connection +// l, err := ConnectTLS() +// if err != nil { +// log.Fatal(err) +// } +// defer l.Close() +// +// // Non-TLS Connection +// //l, err := Connect() +// //if err != nil { +// // log.Fatal(err) +// //} +// //defer l.Close() +// +// // Anonymous Bind and Search +// result, err := AnonymousBindAndSearch(l) +// if err != nil { +// log.Fatal(err) +// } +// result.Entries[0].Print() +// +// // Normal Bind and Search +// result, err = BindAndSearch(l) +// if err != nil { +// log.Fatal(err) +// } +// result.Entries[0].Print() +// } +// +// // Ldap Connection with TLS +// func ConnectTLS() (*ldap.Conn, error) { +// // You can also use IP instead of FQDN +// l, err := ldap.DialURL(fmt.Sprintf("ldaps://%s:636", FQDN)) +// if err != nil { +// return nil, err +// } +// +// return l, nil +// } +// +// // Ldap Connection without TLS +// func Connect() (*ldap.Conn, error) { +// // You can also use IP instead of FQDN +// l, err := ldap.DialURL(fmt.Sprintf("ldap://%s:389", FQDN)) +// if err != nil { +// return nil, err +// } +// +// return l, nil +// } +// +// // Anonymous Bind and Search +// func AnonymousBindAndSearch(l *ldap.Conn) (*ldap.SearchResult, error) { +// l.UnauthenticatedBind("") +// +// anonReq := ldap.NewSearchRequest( +// "", +// ldap.ScopeBaseObject, // you can also use ldap.ScopeWholeSubtree +// ldap.NeverDerefAliases, +// 0, +// 0, +// false, +// Filter, +// []string{}, +// nil, +// ) +// result, err := l.Search(anonReq) +// if err != nil { +// return nil, fmt.Errorf("Anonymous Bind Search Error: %s", err) +// } +// +// if len(result.Entries) > 0 { +// result.Entries[0].Print() +// return result, nil +// } else { +// return nil, fmt.Errorf("Couldn't fetch anonymous bind search entries") +// } +// } +// +// // Normal Bind and Search +// func BindAndSearch(l *ldap.Conn) (*ldap.SearchResult, error) { +// l.Bind(BindUsername, BindPassword) +// +// searchReq := ldap.NewSearchRequest( +// BaseDN, +// ldap.ScopeBaseObject, // you can also use ldap.ScopeWholeSubtree +// ldap.NeverDerefAliases, +// 0, +// 0, +// false, +// Filter, +// []string{}, +// nil, +// ) +// result, err := l.Search(searchReq) +// if err != nil { +// return nil, fmt.Errorf("Search Error: %s", err) +// } +// +// if len(result.Entries) > 0 { +// return result, nil +// } else { +// return nil, fmt.Errorf("Couldn't fetch search entries") +// } +// }