diff --git a/admin/api/exercice.go b/admin/api/exercice.go index ffd57b5d..87d28900 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -189,11 +189,12 @@ func deleteExerciceHint(hint fic.EHint, _ []byte) (interface{}, error) { } type uploadedFlag struct { - Label string - Help string - ICase bool - Flag string - Hash []byte + Label string + Help string + IgnoreCase bool + ValidatorRe *string `json:"validator_regexp"` + Flag string + Value []byte } func createExerciceFlag(exercice fic.Exercice, body []byte) (interface{}, error) { @@ -206,7 +207,12 @@ func createExerciceFlag(exercice fic.Exercice, body []byte) (interface{}, error) return nil, errors.New("Flag not filled") } - return exercice.AddRawFlag(uk.Label, uk.Help, uk.ICase, uk.Flag) + var vre *string = nil + if uk.ValidatorRe != nil && len(*uk.ValidatorRe) > 0 { + vre = uk.ValidatorRe + } + + return exercice.AddRawFlag(uk.Label, uk.Help, uk.IgnoreCase, vre, []byte(uk.Flag)) } func showExerciceFlag(flag fic.Flag, _ fic.Exercice, body []byte) (interface{}, error) { @@ -226,8 +232,14 @@ func updateExerciceFlag(flag fic.Flag, exercice fic.Exercice, body []byte) (inte } flag.Help = uk.Help - flag.IgnoreCase = uk.ICase - flag.Checksum = uk.Hash + flag.IgnoreCase = uk.IgnoreCase + flag.Checksum = uk.Value + + if uk.ValidatorRe != nil && len(*uk.ValidatorRe) > 0 { + flag.ValidatorRegexp = uk.ValidatorRe + } else { + flag.ValidatorRegexp = nil + } if _, err := flag.Update(); err != nil { return nil, err diff --git a/admin/sync/README.md b/admin/sync/README.md index 11ec0fa2..846cf8e9 100644 --- a/admin/sync/README.md +++ b/admin/sync/README.md @@ -17,6 +17,7 @@ Tous les textes doivent utiliser l'encodage UTF8. - `[[flag]]` : drapeau classique à valider pour résoudre le challenge : * `label = "Intitulé"` : (facultatif, par défaut : `Flag`) intitulé du drapeau ; * `raw = 'MieH2athxuPhai6u'` : drapeau exact à trouver ; + * `validator_regexp = "^(?:sudo +)?(.*)$"` : (facultatif) expression rationnelle dont les groupes capturés serviront comme chaîne à valider (notez que `?:` au début d'un groupe ne le capturera pas) ; * `ignorecase = true` : (facultatif, par défaut : `false`) ignore la case de ce drapeau ; * `help = "Indication"` : (facultatif) chaîne de caractères placée sous le champ du formulaire, idéale pour donner une indication de format ; * `[[flag.unlock_file]]` : bloque l'accès à un fichier tant que le flag n'est pas obtenu : @@ -29,6 +30,7 @@ Tous les textes doivent utiliser l'encodage UTF8. - `[[flag_ucq]]` : drapeau sous forme de question à choix unique : * `label = "Intitulé du groupe"` : (facultatif) intitulé du groupe de choix ; * `raw = 'MieH2athxuPhai6u'` : drapeau attendu parmi les propositions ; + * `validator_regexp = "^(?:sudo +)?(.*)$"` : (facultatif) expression rationnelle dont les groupes capturés serviront comme chaîne à valider (notez que `?:` au début d'un groupe ne le capturera pas) ; * `help = "Indication"` : (facultatif, uniquement si `displayAs = select`) chaîne de caractères placée sous le champ du formulaire ; * `displayAs = "select|radio"` : (facultatif, par défaut `radio`) manière dont est affichée le choix : `select` pour une liste de choix, `radio` pour des boutons radios ; * `choices_cost = 20` : (facultatif, par défaut `0`) coût pour afficher les choix, avant l'affichage, se comporte comme un `flag` classique (à 0, les choix sont affichés directement) ; diff --git a/admin/sync/exercice_defines.go b/admin/sync/exercice_defines.go index c870d5cb..8947cf5f 100644 --- a/admin/sync/exercice_defines.go +++ b/admin/sync/exercice_defines.go @@ -27,11 +27,12 @@ type ExerciceUnlockFile struct { // ExerciceFlag holds informations about a "classic" flag. type ExerciceFlag struct { - Label string `toml:",omitempty"` - Raw string - IgnoreCase bool `toml:",omitempty"` - Help string `toml:",omitempty"` - LockedFile []ExerciceUnlockFile `toml:"unlock_file,omitempty"` + Label string `toml:",omitempty"` + Raw string + IgnoreCase bool `toml:",omitempty"` + ValidatorRe string `toml:"validator_regexp,omitempty"` + Help string `toml:",omitempty"` + LockedFile []ExerciceUnlockFile `toml:"unlock_file,omitempty"` } // ExerciceFlagMCQChoice holds a choice for an MCQ flag. @@ -57,6 +58,7 @@ type ExerciceFlagUCQ struct { Label string `toml:",omitempty"` Raw string IgnoreCase bool `toml:",omitempty"` + ValidatorRe string `toml:"validator_regexp,omitempty"` Help string `toml:",omitempty"` DisplayAs string `toml:",omitempty"` Choices_Cost int64 `toml:",omitempty"` diff --git a/admin/sync/exercice_keys.go b/admin/sync/exercice_keys.go index f309ecfd..06519a5c 100644 --- a/admin/sync/exercice_keys.go +++ b/admin/sync/exercice_keys.go @@ -19,6 +19,15 @@ func isFullGraphic(s string) bool { return true } +func validatorRegexp(vre string) (validator_regexp *string) { + if len(vre) > 0 { + validator_regexp = &vre + } else { + validator_regexp = nil + } + return +} + // SyncExerciceFlags reads the content of challenge.txt and import "classic" flags as Key for the given challenge. func SyncExerciceFlags(i Importer, exercice fic.Exercice) (errs []string) { if _, err := exercice.WipeFlags(); err != nil { @@ -40,7 +49,7 @@ func SyncExerciceFlags(i Importer, exercice fic.Exercice) (errs []string) { errs = append(errs, fmt.Sprintf("%q: WARNING flag #%d: non-printable characters in flag, is this really expected?", path.Base(exercice.Path), nline + 1)) } - if k, err := exercice.AddRawFlag(flag.Label, flag.Help, flag.IgnoreCase, flag.Raw); err != nil { + if k, err := exercice.AddRawFlag(flag.Label, flag.Help, flag.IgnoreCase, validatorRegexp(flag.ValidatorRe), []byte(flag.Raw)); err != nil { errs = append(errs, fmt.Sprintf("%q: error flag #%d: %s", path.Base(exercice.Path), nline + 1, err)) continue } else { @@ -67,7 +76,7 @@ func SyncExerciceFlags(i Importer, exercice fic.Exercice) (errs []string) { errs = append(errs, fmt.Sprintf("%q: WARNING flag UCQ #%d: non-printable characters in flag, is this really expected?", path.Base(exercice.Path), nline + 1)) } - if k, err := exercice.AddRawFlag(flag.Label, flag.Help, flag.IgnoreCase, flag.Raw); err != nil { + if k, err := exercice.AddRawFlag(flag.Label, flag.Help, flag.IgnoreCase, validatorRegexp(flag.ValidatorRe), []byte(flag.Raw)); err != nil { errs = append(errs, fmt.Sprintf("%q: error flag UCQ #%d: %s", path.Base(exercice.Path), nline + 1, err)) continue } else { diff --git a/libfic/db.go b/libfic/db.go index 449d04ed..0fbd4fa4 100644 --- a/libfic/db.go +++ b/libfic/db.go @@ -162,6 +162,7 @@ CREATE TABLE IF NOT EXISTS exercice_flags( type VARCHAR(255) NOT NULL, help VARCHAR(255) NOT NULL, ignorecase BOOLEAN NOT NULL DEFAULT 0, + validator_regexp VARCHAR(255) NULL, cksum BINARY(64) NOT NULL, FOREIGN KEY(id_exercice) REFERENCES exercices(id_exercice) ) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; diff --git a/libfic/exercice.go b/libfic/exercice.go index 5dfce80a..64a1d1e5 100644 --- a/libfic/exercice.go +++ b/libfic/exercice.go @@ -296,7 +296,7 @@ func (e Exercice) CheckResponse(respflags map[string]string, respmcq map[int64]b for _, flag := range flags { if res, ok := respflags[flag.Label]; !ok { valid = false - } else if !flag.Check(res) { + } else if !flag.Check([]byte(res)) { if !PartialValidation || t.HasPartiallySolved(flag) == nil { valid = false } diff --git a/libfic/file.go b/libfic/file.go index ebbc5af1..340b084c 100644 --- a/libfic/file.go +++ b/libfic/file.go @@ -290,7 +290,7 @@ func (f EFile) GetDepends() ([]Flag, error) { if err := rows.Scan(&d); err != nil { return nil, err } - deps = append(deps, Flag{d, f.IdExercice, "", "", false, []byte{}}) + deps = append(deps, Flag{d, f.IdExercice, "", "", false, nil, []byte{}}) } if err := rows.Err(); err != nil { return nil, err diff --git a/libfic/flag.go b/libfic/flag.go index 2bae810d..197aa998 100644 --- a/libfic/flag.go +++ b/libfic/flag.go @@ -1,30 +1,34 @@ package fic import ( + "bytes" + "errors" + "regexp" "time" "golang.org/x/crypto/blake2b" ) - // Flag represents a flag's challenge, stored as hash. type Flag struct { - Id int64 `json:"id"` + Id int64 `json:"id"` // IdExercice is the identifier of the underlying challenge - IdExercice int64 `json:"idExercice"` + IdExercice int64 `json:"idExercice"` // Label is the title of the flag as displayed to players - Label string `json:"label"` + Label string `json:"label"` // Help is a small piece of text that aims to add useful information like flag format, ... - Help string `json:"help"` + Help string `json:"help"` // IgnoreCase indicates if the case is sensitive to case or not - IgnoreCase bool `json:"ignorecase"` + IgnoreCase bool `json:"ignorecase"` + // ValidatorRegexp extracts a subset of the player's answer, that will be checked. + ValidatorRegexp *string `json:"validator_regexp"` // Checksum is the expected hashed flag - Checksum []byte `json:"value"` + Checksum []byte `json:"value"` } // GetFlags returns a list of flags comming with the challenge. func (e Exercice) GetFlags() ([]Flag, error) { - if rows, err := DBQuery("SELECT id_flag, id_exercice, type, help, ignorecase, cksum FROM exercice_flags WHERE id_exercice = ?", e.Id); err != nil { + if rows, err := DBQuery("SELECT id_flag, id_exercice, type, help, ignorecase, validator_regexp, cksum FROM exercice_flags WHERE id_exercice = ?", e.Id); err != nil { return nil, err } else { defer rows.Close() @@ -34,9 +38,10 @@ func (e Exercice) GetFlags() ([]Flag, error) { var k Flag k.IdExercice = e.Id - if err := rows.Scan(&k.Id, &k.IdExercice, &k.Label, &k.Help, &k.IgnoreCase, &k.Checksum); err != nil { + if err := rows.Scan(&k.Id, &k.IdExercice, &k.Label, &k.Help, &k.IgnoreCase, &k.ValidatorRegexp, &k.Checksum); err != nil { return nil, err } + flags = append(flags, k) } if err := rows.Err(); err != nil { @@ -48,31 +53,59 @@ func (e Exercice) GetFlags() ([]Flag, error) { } // getHashedFlag calculates the expected checksum for the given raw_value. -func getHashedFlag(raw_value string) [blake2b.Size]byte { - hash := blake2b.Sum512([]byte(raw_value)) +func getHashedFlag(raw_value []byte) [blake2b.Size]byte { + hash := blake2b.Sum512(raw_value) return hash } // AddRawFlag creates and fills a new struct Flag, from a non-hashed flag, and registers it into the database. -func (e Exercice) AddRawFlag(name string, help string, ignorecase bool, raw_value string) (Flag, error) { +func (e Exercice) AddRawFlag(name string, help string, ignorecase bool, validator_regexp *string, raw_value []byte) (Flag, error) { + if ignorecase { + raw_value = bytes.ToLower(raw_value) + } + + // Check that raw value passes through the regexp + if validator_regexp != nil { + if re, err := regexp.Compile(*validator_regexp); err != nil { + return Flag{}, err + } else if res := re.FindSubmatch(raw_value); res == nil { + return Flag{}, errors.New("Expected flag doesn't pass through the validator_regexp") + } else { + raw_value = bytes.Join(res[1:], []byte("+")) + } + } + hash := getHashedFlag(raw_value) - return e.AddFlag(name, help, ignorecase, hash[:]) + return e.AddFlag(name, help, ignorecase, validator_regexp, hash[:]) } // AddFlag creates and fills a new struct Flag, from a hashed flag, and registers it into the database. -func (e Exercice) AddFlag(name string, help string, ignorecase bool, checksum []byte) (Flag, error) { - if res, err := DBExec("INSERT INTO exercice_flags (id_exercice, type, help, ignorecase, cksum) VALUES (?, ?, ?, ?, ?)", e.Id, name, help, ignorecase, checksum); err != nil { +func (e Exercice) AddFlag(name string, help string, ignorecase bool, validator_regexp *string, checksum []byte) (Flag, error) { + // Check the regexp compile + if validator_regexp != nil { + if _, err := regexp.Compile(*validator_regexp); err != nil { + return Flag{}, err + } + } + + if res, err := DBExec("INSERT INTO exercice_flags (id_exercice, type, help, ignorecase, validator_regexp, cksum) VALUES (?, ?, ?, ?, ?, ?)", e.Id, name, help, ignorecase, validator_regexp, checksum); err != nil { return Flag{}, err } else if kid, err := res.LastInsertId(); err != nil { return Flag{}, err } else { - return Flag{kid, e.Id, name, help, ignorecase, checksum}, nil + return Flag{kid, e.Id, name, help, ignorecase, validator_regexp, checksum}, nil } } // Update applies modifications back to the database. func (k Flag) Update() (int64, error) { - if res, err := DBExec("UPDATE exercice_flags SET id_exercice = ?, type = ?, help = ?, ignorecase = ?, cksum = ? WHERE id_flag = ?", k.IdExercice, k.Label, k.Help, k.IgnoreCase, k.Checksum, k.Id); err != nil { + if k.ValidatorRegexp != nil { + if _, err := regexp.Compile(*k.ValidatorRegexp); err != nil { + return 0, err + } + } + + if res, err := DBExec("UPDATE exercice_flags SET id_exercice = ?, type = ?, help = ?, ignorecase = ?, validator_regexp = ?, cksum = ? WHERE id_flag = ?", k.IdExercice, k.Label, k.Help, k.IgnoreCase, k.ValidatorRegexp, k.Checksum, k.Id); err != nil { return 0, err } else if nb, err := res.RowsAffected(); err != nil { return 0, err @@ -108,7 +141,24 @@ func (e Exercice) WipeFlags() (int64, error) { } // Check if the given val is the expected one for this flag. -func (k Flag) Check(val string) bool { +func (k Flag) Check(val []byte) bool { + if k.IgnoreCase { + val = bytes.ToLower(val) + } + + // Check that raw value passes through the regexp + if k.ValidatorRegexp != nil { + re := regexp.MustCompile(*k.ValidatorRegexp) + if res := re.FindSubmatch(val); res != nil { + val = bytes.Join(res[1:], []byte("+")) + } + } + + // Check that the value is not empty + if len(val) == 0 { + return false + } + hash := getHashedFlag(val) if len(k.Checksum) != len(hash) { return false