2019-07-08 16:41:04 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
2019-07-10 13:47:55 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
2019-07-08 16:41:04 +00:00
|
|
|
"os"
|
2019-07-09 21:37:37 +00:00
|
|
|
"regexp"
|
2019-07-08 16:41:04 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
2019-07-10 15:47:43 +00:00
|
|
|
|
|
|
|
hibp "github.com/mattevans/pwned-passwords"
|
2019-07-08 16:41:04 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type vpnSession struct {
|
2019-07-10 15:47:43 +00:00
|
|
|
Time time.Time `json:"time"`
|
|
|
|
Login string `json:"username"`
|
|
|
|
Operation string `json:"operation"`
|
|
|
|
Status string `json:"status"`
|
|
|
|
Profile string `json:"profile"`
|
|
|
|
TwoFA bool `json:"2fa_auth"`
|
|
|
|
IP string `json:"client_ip"`
|
|
|
|
PrivIP string `json:"private_ip"`
|
|
|
|
AsNumber string `json:"as_number"`
|
|
|
|
AsName string `json:"as_name"`
|
|
|
|
NewAS bool `json:"as_new"`
|
|
|
|
PwnedPasswd bool `json:"pwned_passwd"`
|
|
|
|
Hostname string `json:"hostname"`
|
|
|
|
TooMuchPwn bool `json:"too_much_pwn"`
|
|
|
|
Mail string `json:"-"`
|
|
|
|
cID int `json:"-"`
|
|
|
|
kID int `json:"-"`
|
|
|
|
port int `json:"-"`
|
|
|
|
dev string `json:"-"`
|
|
|
|
password string `json:"-"`
|
|
|
|
otpCode string `json:"-"`
|
|
|
|
localIP string `json:"-"`
|
|
|
|
vpnserver string `json:"-"`
|
|
|
|
pwnMail string `json:"-"`
|
|
|
|
newAsMail string `json:"-"`
|
|
|
|
MailFrom string `json:"-"`
|
|
|
|
CcPwnPassword string `json:"-"`
|
2019-07-08 16:41:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewVPNSession(operation string) *vpnSession {
|
|
|
|
v := vpnSession{
|
|
|
|
Time: time.Now().Round(time.Second),
|
|
|
|
Status: "system failure",
|
|
|
|
Operation: operation,
|
|
|
|
}
|
|
|
|
v.Hostname, _ = os.Hostname()
|
|
|
|
|
|
|
|
return &v
|
|
|
|
}
|
|
|
|
|
2019-07-10 13:47:55 +00:00
|
|
|
func (c *vpnSession) String() string {
|
|
|
|
if res, err := json.MarshalIndent(c, " ", " "); err == nil {
|
|
|
|
return string(res)
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2019-07-09 21:37:37 +00:00
|
|
|
func (c *vpnSession) b64Login() string {
|
|
|
|
return base64.StdEncoding.EncodeToString([]byte(c.Login))
|
|
|
|
}
|
|
|
|
|
2019-07-08 16:41:04 +00:00
|
|
|
func (c *vpnSession) ParseSessionId(line string) error {
|
|
|
|
var err error
|
|
|
|
client_id := strings.Split(strings.Replace(line, ">CLIENT:CONNECT,", "", 1), ",")
|
|
|
|
if c.cID, err = strconv.Atoi(client_id[0]); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if c.kID, err = strconv.Atoi(client_id[1]); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-10 15:47:43 +00:00
|
|
|
func (c *vpnSession) CheckPwn(password string) error {
|
|
|
|
client := hibp.NewClient()
|
|
|
|
pwned, err := client.Pwned.Compromised(password)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
c.PwnedPasswd = pwned
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-09 21:37:37 +00:00
|
|
|
func (c *vpnSession) ParseEnv(infos *[]string) error {
|
|
|
|
var err error
|
|
|
|
r := regexp.MustCompile("[^a-zA-Z0-9./_@-]")
|
|
|
|
|
2019-07-08 16:41:04 +00:00
|
|
|
for _, line := range *infos {
|
|
|
|
p := strings.Split(strings.Replace(line, ">CLIENT:ENV,", "", 1), "=")
|
|
|
|
switch p[0] {
|
|
|
|
case "trusted_port":
|
2019-07-09 21:37:37 +00:00
|
|
|
if c.port, err = strconv.Atoi(r.ReplaceAllString(p[1], "")); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-07-08 16:41:04 +00:00
|
|
|
case "untrusted_port":
|
2019-07-09 21:37:37 +00:00
|
|
|
if c.port, err = strconv.Atoi(r.ReplaceAllString(p[1], "")); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
case "trusted_ip":
|
|
|
|
c.IP = r.ReplaceAllString(p[1], "")
|
2019-07-08 16:41:04 +00:00
|
|
|
case "untrusted_ip":
|
2019-07-09 21:37:37 +00:00
|
|
|
c.IP = r.ReplaceAllString(p[1], "")
|
2019-07-10 15:47:43 +00:00
|
|
|
case "ifconfig_pool_remote_ip":
|
|
|
|
c.PrivIP = r.ReplaceAllString(p[1], "")
|
2019-07-09 21:37:37 +00:00
|
|
|
case "ifconfig_local":
|
|
|
|
c.localIP = r.ReplaceAllString(p[1], "")
|
2019-07-08 16:41:04 +00:00
|
|
|
case "password":
|
|
|
|
switch {
|
2019-07-09 21:37:37 +00:00
|
|
|
case strings.HasPrefix(p[1], "CRV1"):
|
|
|
|
split := strings.Split(p[1], ":")
|
2019-07-08 16:41:04 +00:00
|
|
|
if len(split) != 5 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
c.password = split[2]
|
|
|
|
c.otpCode = split[4]
|
2019-07-10 13:47:55 +00:00
|
|
|
if c.otpCode == "" {
|
|
|
|
c.otpCode = "***"
|
|
|
|
}
|
2019-07-10 15:47:43 +00:00
|
|
|
go c.CheckPwn(c.password)
|
2019-07-08 16:41:04 +00:00
|
|
|
|
2019-07-09 21:37:37 +00:00
|
|
|
case strings.HasPrefix(p[1], "SCRV1"):
|
|
|
|
split := strings.Split(p[1], ":")
|
2019-07-08 16:41:04 +00:00
|
|
|
if len(split) != 3 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
data, err := base64.StdEncoding.DecodeString(split[1])
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
c.password = string(data)
|
|
|
|
|
|
|
|
data, err = base64.StdEncoding.DecodeString(split[2])
|
|
|
|
if err != nil {
|
2019-07-10 13:47:55 +00:00
|
|
|
c.password = p[1]
|
2019-07-08 16:41:04 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
c.otpCode = string(data)
|
|
|
|
|
2019-07-10 13:47:55 +00:00
|
|
|
if c.otpCode == "" {
|
|
|
|
c.otpCode = "***"
|
|
|
|
}
|
|
|
|
|
2019-07-08 16:41:04 +00:00
|
|
|
default:
|
|
|
|
c.password = p[1]
|
2019-07-10 13:47:55 +00:00
|
|
|
c.otpCode = ""
|
2019-07-10 15:47:43 +00:00
|
|
|
go c.CheckPwn(c.password)
|
2019-07-08 16:41:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
case "username":
|
2019-07-09 21:37:37 +00:00
|
|
|
c.Login = r.ReplaceAllString(p[1], "")
|
2019-07-08 16:41:04 +00:00
|
|
|
case "dev":
|
2019-07-09 21:37:37 +00:00
|
|
|
c.dev = r.ReplaceAllString(p[1], "")
|
2019-07-08 16:41:04 +00:00
|
|
|
}
|
|
|
|
}
|
2019-07-09 21:37:37 +00:00
|
|
|
return nil
|
2019-07-08 16:41:04 +00:00
|
|
|
}
|
|
|
|
|
2019-07-10 13:47:55 +00:00
|
|
|
func (c *vpnSession) Auth(s *OpenVpnMgt) {
|
|
|
|
var cmd []string
|
|
|
|
var ip string
|
|
|
|
var errIP error
|
|
|
|
|
|
|
|
err, ok := c.auth(s)
|
|
|
|
// if auth is ok, time to get an IP address
|
|
|
|
if ok == 0 {
|
|
|
|
ip, errIP = s.getIP(c)
|
|
|
|
if errIP != nil {
|
|
|
|
ok = -10
|
|
|
|
err = errIP
|
|
|
|
}
|
2019-07-08 16:41:04 +00:00
|
|
|
}
|
2019-07-10 13:47:55 +00:00
|
|
|
|
|
|
|
switch {
|
|
|
|
case ok == 0:
|
|
|
|
cmd = []string{
|
|
|
|
fmt.Sprintf("client-auth %d %d", c.cID, c.kID),
|
|
|
|
fmt.Sprintf("ifconfig-push %s %s", ip, c.localIP),
|
|
|
|
}
|
|
|
|
for _, r := range s.ldap[c.Profile].routes {
|
|
|
|
cmd = append(cmd, fmt.Sprintf("push \"route %s vpn_gateway\"", r))
|
|
|
|
}
|
|
|
|
cmd = append(cmd, "END")
|
|
|
|
|
|
|
|
case ok < 0:
|
|
|
|
cmd = []string{fmt.Sprintf("client-deny %d %d \"%s\" \"%s\"",
|
|
|
|
c.cID, c.kID, err, err)}
|
|
|
|
|
|
|
|
case ok == 1:
|
|
|
|
cmd = []string{fmt.Sprintf(
|
|
|
|
"client-deny %d %d \"Need OTP\" \"CRV1:R,E:%s:%s:OTP Code \"",
|
|
|
|
c.cID, c.kID, c.password, c.b64Login())}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err, _ := s.sendCommand(cmd, c.vpnserver); err != nil {
|
2019-07-10 15:47:43 +00:00
|
|
|
c.LogPrintln(err)
|
2019-07-10 13:47:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// main authentication function.
|
|
|
|
// returns 0 if auth is valid
|
|
|
|
// returns 1 if an TOTP code is necessary
|
|
|
|
// returns a negative if auth is not valid
|
|
|
|
func (c *vpnSession) auth(s *OpenVpnMgt) (error, int) {
|
|
|
|
// an empty password is not good
|
|
|
|
if c.password == "" {
|
|
|
|
c.Status = "Empty Password"
|
|
|
|
return errors.New("Empty Password"), -1
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if the password is a valid token (see TOTP request)
|
|
|
|
tokenPasswordOk, tokenPassword := s.TokenPassword(c)
|
|
|
|
|
|
|
|
// password is a token. We remove it from the session object to
|
|
|
|
// avoid checking it against the ldap
|
|
|
|
if tokenPasswordOk {
|
|
|
|
c.password = ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the otp is not empty, we check it against the valid codes as soon as
|
|
|
|
// possible
|
|
|
|
otpvalidated := false
|
|
|
|
if c.otpCode != "" {
|
|
|
|
codes, err := s.GenerateOTP(c.Login)
|
|
|
|
if err != nil {
|
|
|
|
return err, -2
|
|
|
|
}
|
|
|
|
for _, possible := range codes {
|
|
|
|
if possible == c.otpCode {
|
|
|
|
otpvalidated = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Profile = ""
|
|
|
|
login := []string{c.Login}
|
|
|
|
pass := c.password
|
|
|
|
|
|
|
|
for {
|
|
|
|
n := c.Profile
|
|
|
|
for k, ldap := range s.ldap {
|
|
|
|
if ldap.upgradeFrom != c.Profile {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
err, userOk, passOk, secondary := ldap.Auth(login, pass)
|
|
|
|
|
|
|
|
// if there is an error, try the other configurations
|
|
|
|
if err != nil {
|
2019-07-10 15:47:43 +00:00
|
|
|
c.LogPrintln(err)
|
2019-07-10 13:47:55 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// we did find a valid User
|
|
|
|
if userOk {
|
|
|
|
// the login for the new auth level is given by the current one
|
|
|
|
login = secondary
|
|
|
|
|
|
|
|
if c.Mail == "" {
|
|
|
|
c.Mail = secondary[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
if passOk && c.Profile != "" {
|
|
|
|
// it's at least the second auth level, and we have a valid
|
|
|
|
// password on 2 different auth system. It's a dupplicate
|
|
|
|
// password, let's log it
|
|
|
|
log.Printf("User %s has a dupplicate password\n", c.Login)
|
|
|
|
}
|
|
|
|
|
|
|
|
// we have either a positive auth ok a previous valid one
|
|
|
|
if passOk || c.Profile != "" || tokenPasswordOk {
|
|
|
|
c.Profile = k
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// no profile update this turn, no need to continue
|
|
|
|
if n == c.Profile {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// no profile validated, we stop here
|
|
|
|
if c.Profile == "" {
|
|
|
|
c.Status = "fail (password)"
|
|
|
|
return errors.New("Authentication Failed"), -3
|
|
|
|
}
|
|
|
|
|
|
|
|
// check the MFA requested by the secured profile
|
|
|
|
c.TwoFA = true
|
|
|
|
switch s.ldap[c.Profile].mfaType {
|
|
|
|
case "internal":
|
|
|
|
if otpvalidated {
|
|
|
|
return nil, 0
|
|
|
|
}
|
|
|
|
// log that the failure is due to the OTP
|
|
|
|
if c.otpCode == "" {
|
|
|
|
c.Status = "Need OTP Code"
|
|
|
|
} else {
|
|
|
|
c.Status = "fail (OTP) : "
|
|
|
|
}
|
|
|
|
c.password = tokenPassword
|
|
|
|
return errors.New("Need OTP Code"), 1
|
|
|
|
case "okta":
|
|
|
|
//TODO implement okta MFA
|
|
|
|
c.Status = "fail (Okta)"
|
|
|
|
return nil, -4
|
|
|
|
default:
|
|
|
|
c.TwoFA = false
|
|
|
|
}
|
|
|
|
|
|
|
|
// no MFA requested, the login is valid
|
|
|
|
return nil, 0
|
2019-07-08 16:41:04 +00:00
|
|
|
}
|