validator is now login-validator and is part of the server image

This commit is contained in:
nemunaire 2018-02-19 23:11:02 +01:00
commit ee6fbe3e74
11 changed files with 23 additions and 5 deletions

View file

@ -0,0 +1,61 @@
package main
import (
"fmt"
"io/ioutil"
"net"
"strings"
)
var ARPTable string = "/proc/net/arp"
type ARPEntry struct {
IP net.IP
HWType int
Flags int
HWAddress net.HardwareAddr
Mask string
Device string
}
func ARPAnalyze() (ents []ARPEntry, err error) {
var content []byte
if content, err = ioutil.ReadFile(ARPTable); err != nil {
return
}
for _, line := range strings.Split(string(content), "\n") {
f := strings.Fields(line)
if len(f) > 5 {
var e ARPEntry
if _, err := fmt.Sscanf(f[1], "0x%x", &e.HWType); err != nil {
continue
}
if _, err := fmt.Sscanf(f[2], "0x%x", &e.Flags); err != nil {
continue
}
e.IP = net.ParseIP(f[0])
if e.HWAddress, err = net.ParseMAC(f[3]); err != nil {
continue
}
e.Mask = f[4]
e.Device = f[5]
ents = append(ents, e)
}
}
return
}
func ARPContainsIP(ents []ARPEntry, ip net.IP) *ARPEntry {
for i, e := range ents {
if e.IP.Equal(ip) && e.Flags == 2 {
return &ents[i]
}
}
return nil
}

View file

@ -0,0 +1,78 @@
package main
import (
"net/http"
"text/template"
)
const indextpl = `<!DOCTYPE html>
<html ng-app="FICApp">
<head>
<meta charset="utf-8">
<title>Challenge Forensic - Administration</title>
<link href="/css/bootstrap.min.css" type="text/css" rel="stylesheet">
<link href="/css/glyphicon.css" type="text/css" rel="stylesheet" media="screen">
<style>
samp.cksum {
overflow-x: hidden;
text-overflow: ellipsis;
max-width: 20vw;
display: inline-block;
vertical-align: middle;
}
</style>
<script src="/js/d3.v3.min.js"></script>
</head>
<body class="bg-light text-dark">
<nav class="navbar sticky-top navbar-expand-lg navbar-dark bg-dark text-light" style="margin-bottom: 5px;">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#adminMenu" aria-controls="adminMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<span id="clock" class="navbar-text" ng-controller="CountdownController" ng-cloak>
<span ng-show="startIn > 0">
Démarrage dans :
<span>{{"{{ startIn }}"}}</span>"
<span class="point">|</span>
</span>
<span id="hours">{{"{{ time.hours | time }}"}}</span>
<span class="point">:</span>
<span id="min">{{"{{ time.minutes | time }}"}}</span>
<span class="point">:</span>
<span id="sec">{{"{{ time.seconds | time }}"}}</span>
</span>
</nav>
<div class="container" ng-controller="DIWEBoxController">
<div ng-repeat="box in boxes" class="alert alert-dismissible alert-{{"{{ box.kind }}"}}" ng-cloak>
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<strong ng-if="box.title">{{"{{ box.title }}"}}</strong> {{"{{ box.msg }}"}}
<ul ng-if="box.list">
<li ng-repeat="i in box.list">{{"{{ i }}"}}</li>
</ul>
<button class="btn btn-sm btn-success" ng-if="box.yes || box.no" ng-click="box.yes()">Yes</button>
<button class="btn btn-sm btn-danger" ng-if="box.yes || box.no" ng-click="box.no()">No</button>
</div>
</div>
<div class="container" ng-view></div>
<script src="/js/jquery.min.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/angular.min.js"></script>
<script src="/js/angular-route.min.js"></script>
<script src="/js/angular-sanitize.min.js"></script>
</body>
</html>
`
func Index(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
if indexTmpl, err := template.New("index").Parse(indextpl); err != nil {
http.Error(w, "Cannot create template: "+err.Error(), http.StatusInternalServerError)
} else if err := indexTmpl.Execute(w, nil); err != nil {
http.Error(w, "An error occurs during template execution: "+err.Error(), http.StatusInternalServerError)
}
}

View file

@ -0,0 +1,193 @@
package main
import (
"crypto/hmac"
"crypto/sha512"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path"
"strings"
"text/template"
"gopkg.in/ldap.v2"
)
var loginSalt string
type loginChecker struct {
students []Student
ldapAddr string
ldapPort int
ldapIsTLS bool
ldapBase string
ldapBindUsername string
ldapBindPassword string
}
type loginUpload struct {
Username string
Password string
}
func (l loginChecker) ldapAuth(username, password string) (res bool, err error) {
tlsCnf := tls.Config{InsecureSkipVerify: true}
var c *ldap.Conn
if l.ldapIsTLS {
c, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", l.ldapAddr, l.ldapPort), &tlsCnf)
if err != nil {
return false, err
}
} else {
c, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", l.ldapAddr, l.ldapPort))
if err != nil {
return false, err
}
// Reconnect with TLS
err = c.StartTLS(&tlsCnf)
if err != nil {
return false, err
}
}
defer c.Close()
if l.ldapBindUsername != "" {
err = c.Bind(l.ldapBindUsername, l.ldapBindPassword)
if err != nil {
return false, err
}
}
// Search for the given username
searchRequest := ldap.NewSearchRequest(
l.ldapBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=person)(uid=%s))", username),
[]string{"dn"},
nil,
)
sr, err := c.Search(searchRequest)
if err != nil {
return false, err
}
if len(sr.Entries) != 1 {
return false, errors.New("User does not exist or too many entries returned")
}
userdn := sr.Entries[0].DN
err = c.Bind(userdn, password)
if err != nil {
return false, err
}
return true, nil
}
func (l loginChecker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
r.RemoteAddr = addr
}
log.Printf("%s \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "text/plain")
// Check request type and size
if r.Method != "POST" {
http.Error(w,
"Invalid request",
http.StatusBadRequest)
return
} else if r.ContentLength < 0 || r.ContentLength > 1023 {
http.Error(w,
"Request entity too large",
http.StatusRequestEntityTooLarge)
return
}
dec := json.NewDecoder(r.Body)
var lu loginUpload
if err := dec.Decode(&lu); err != nil {
http.Error(w,
err.Error(),
http.StatusBadRequest)
return
}
// Perform login check
canContinue := false
for _, std := range l.students {
if std.Login == lu.Username {
canContinue = true
}
}
if !canContinue {
log.Println("Login not found:", lu.Username, "at", r.RemoteAddr)
http.Error(w, "Login not found in whitelist.", http.StatusUnauthorized)
return
}
if ok, err := l.ldapAuth(lu.Username, lu.Password); err != nil {
log.Println("Unable to perform authentication for", lu.Username, ":", err, "at", r.RemoteAddr)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
} else if !ok {
log.Println("Login failed:", lu.Username, "at", r.RemoteAddr)
http.Error(w, "Invalid password", http.StatusUnauthorized)
return
}
if err := l.lateLoginAction(lu.Username, r.RemoteAddr); err != nil {
log.Println("Error on late login action:", err)
http.Error(w, "Internal server error. Please retry in a few minutes", http.StatusInternalServerError)
return
}
log.Println("Successful login of", lu.Username, "at", r.RemoteAddr)
http.Error(w, "Success", http.StatusOK)
}
func (l loginChecker) lateLoginAction(username, remoteAddr string) error {
// Find corresponding MAC
var fname string
spl := strings.SplitN(remoteAddr, ":", 2)
if ip := net.ParseIP(spl[0]); ip == nil {
return errors.New("Unable to parse given IPv4: " + spl[0])
} else if arptable, err := ARPAnalyze(); err != nil {
return err
} else if arpent := ARPContainsIP(arptable, ip); arpent == nil {
return errors.New("Unable to find MAC in ARP table")
} else {
fname = fmt.Sprintf("%02x-%02x-%02x-%02x-%02x-%02x-%02x", arpent.HWType, arpent.HWAddress[0], arpent.HWAddress[1], arpent.HWAddress[2], arpent.HWAddress[3], arpent.HWAddress[4], arpent.HWAddress[5])
}
if tpl, err := ioutil.ReadFile(path.Join(tftpDir, "pxelinux.cfg", "tpl")); err != nil {
log.Println("Unable to open tpl: ", err)
} else if file, err := os.OpenFile(path.Join(tftpDir, "pxelinux.cfg", fname), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0644)); err != nil {
log.Println("Unable to open destination file: ", err)
} else {
defer file.Close()
mac := hmac.New(sha512.New512_224, []byte(loginSalt))
if configTmpl, err := template.New("pxelinux.cfg").Parse(string(tpl)); err != nil {
log.Println("Cannot create template: ", err)
} else if err := configTmpl.Execute(file, map[string]string{"username": username, "remoteAddr": remoteAddr, "pkey": fmt.Sprintf("%x", mac.Sum([]byte(username))), "fname": fname}); err != nil {
log.Println("An error occurs during template execution: ", err)
}
}
return nil
}

View file

@ -0,0 +1,45 @@
package main
import (
"fmt"
"log"
"net"
"net/http"
"os"
"path"
"strings"
)
func logout(w http.ResponseWriter, r *http.Request) {
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
r.RemoteAddr = addr
}
log.Printf("%s \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "text/plain")
// Find corresponding MAC
var fname string
spl := strings.SplitN(r.RemoteAddr, ":", 2)
if ip := net.ParseIP(spl[0]); ip == nil {
http.Error(w, "Unable to parse given IPv4: "+spl[0], http.StatusInternalServerError)
return
} else if arptable, err := ARPAnalyze(); err != nil {
http.Error(w, "Unable to logout: "+err.Error(), http.StatusInternalServerError)
return
} else if arpent := ARPContainsIP(arptable, ip); arpent == nil {
http.Error(w, "Unable to find MAC in ARP table to logout.", http.StatusInternalServerError)
return
} else {
fname = fmt.Sprintf("%02x-%02x-%02x-%02x-%02x-%02x-%02x", arpent.HWType, arpent.HWAddress[0], arpent.HWAddress[1], arpent.HWAddress[2], arpent.HWAddress[3], arpent.HWAddress[4], arpent.HWAddress[5])
}
if err := os.Remove(path.Join(tftpDir, "pxelinux.cfg", fname)); err != nil {
log.Println("Error on logout action:", err)
http.Error(w, "Unable to logout: "+err.Error(), http.StatusInternalServerError)
return
}
log.Println("Successful logout from", r.RemoteAddr)
http.Error(w, "Success", http.StatusOK)
}

View file

@ -0,0 +1,53 @@
package main
import (
"flag"
"log"
"net/http"
"path/filepath"
)
var tftpDir string
func main() {
var studentsFile string
var lc loginChecker
var bind = flag.String("bind", ":8081", "Bind port/socket")
flag.StringVar(&studentsFile, "students", "./students.csv", "Path to a CSV file containing students list")
flag.StringVar(&ARPTable, "arp", ARPTable, "Path to ARP table")
flag.StringVar(&tftpDir, "tftpdir", "/var/tftp/", "Path to TFTPd directory")
flag.StringVar(&loginSalt, "loginsalt", "adelina", "secret used in login HMAC")
flag.StringVar(&lc.ldapAddr, "ldaphost", "auth.cri.epita.fr", "LDAP host")
flag.IntVar(&lc.ldapPort, "ldapport", 636, "LDAP port")
flag.BoolVar(&lc.ldapIsTLS, "ldaptls", false, "Is LDAP connection LDAPS?")
flag.StringVar(&lc.ldapBase, "ldapbase", "dc=epita,dc=net", "LDAP base")
flag.StringVar(&lc.ldapBindUsername, "ldapbindusername", "", "LDAP user to use in order to perform bind (optional if search can be made anonymously)")
flag.StringVar(&lc.ldapBindPassword, "ldapbindpassword", "", "Password for the bind user")
flag.Parse()
var err error
// Sanitize options
log.Println("Checking paths...")
if tftpDir, err = filepath.Abs(tftpDir); err != nil {
log.Fatal(err)
}
lc.students, err = readStudentsList(studentsFile)
if err != nil {
log.Fatal(err)
}
log.Println("Registering handlers...")
mux := http.NewServeMux()
mux.HandleFunc("/", Index)
mux.Handle("/login", lc)
mux.HandleFunc("/logout", logout)
http.HandleFunc("/", mux.ServeHTTP)
log.Println("Ready, listening on port", *bind)
http.ListenAndServe(*bind, nil)
}

View file

@ -0,0 +1,44 @@
package main
import (
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"path"
"text/template"
)
const pxeUserTplPath = "pxelinux.cfg/tpl"
const pxeUserPath = "pxelinux.cfg"
func RegisterUserMAC(ip net.IP, username string) error {
if tab, err := ARPAnalyze(); err != nil {
return err
} else if ent := ARPContainsIP(tab, ip); ent == nil {
return errors.New(fmt.Sprintf("Unable to find MAC address for given IP (%s)", ip))
} else {
return registerUser(fmt.Sprintf("%02X-%02X-%02X-%02X-%02X-%02X-%02X", ent.HWType, ent.HWAddress[0], ent.HWAddress[1], ent.HWAddress[2], ent.HWAddress[3], ent.HWAddress[4], ent.HWAddress[5]), username)
}
}
func RegisterUserIP(ip net.IP, username string) error {
return registerUser(fmt.Sprintf("%02X%02X%02X%02X", ip.To4()[0], ip.To4()[1], ip.To4()[2], ip.To4()[3]), username)
}
func registerUser(filename string, username string) error {
if pxeTplCnt, err := ioutil.ReadFile(path.Join(tftpDir, pxeUserTplPath)); err != nil {
return err
} else if userfd, err := os.OpenFile(path.Join(tftpDir, pxeUserPath, filename), os.O_RDWR|os.O_CREATE, 0644); err != nil {
return err
} else {
defer userfd.Close()
if pxeTmpl, err := template.New("pxeUser").Parse(string(pxeTplCnt)); err != nil {
return err
} else if err := pxeTmpl.Execute(userfd, map[string]string{"username": username}); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,41 @@
package main
import (
"bytes"
"fmt"
"os/exec"
"regexp"
"strings"
)
type SSHkey struct {
Size int
Fingerprint string
Comment string
Algo string
}
func SSHkeyAnalyse(key string) (s SSHkey, err error) {
cmd := exec.Command("ssh-keygen", "-l", "-f", "-")
cmd.Stdin = strings.NewReader(key)
var out bytes.Buffer
cmd.Stdout = &out
if err = cmd.Run(); err != nil {
return
}
var validLine = regexp.MustCompile(`^([0-9]+)`)
for _, line := range strings.Split(out.String(), "\n") {
if validLine.MatchString(line) {
s.Size, err = fmt.Sscanf("%d", validLine.SubexpNames()[1])
s.Fingerprint = validLine.SubexpNames()[2]
s.Comment = validLine.SubexpNames()[3]
s.Algo = validLine.SubexpNames()[4]
}
}
return
}

View file

@ -0,0 +1,43 @@
package main
import (
"bufio"
"encoding/csv"
"os"
"path/filepath"
)
type Student struct {
Lastname string
Firstname string
Login string
EMail string
Phone string
}
func readStudentsList(studentsFile string) (stds []Student, err error) {
if studentsFile, err = filepath.Abs(studentsFile); err != nil {
return
} else if fi, err := os.Open(studentsFile); err != nil {
return nil, err
} else {
r := csv.NewReader(bufio.NewReader(fi))
if list, err := r.ReadAll(); err != nil {
return nil, err
} else {
for _, i := range list {
var s Student
s.Lastname = i[0]
s.Firstname = i[1]
s.Login = i[2]
s.EMail = i[3]
s.Phone = i[4]
stds = append(stds, s)
}
return stds, nil
}
}
}