| package msalogin |
|
|
| import ( |
| "net/url" |
| "regexp" |
| "strings" |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| func stripJSEscapes(s string) string { |
| if s == "" { |
| return s |
| } |
| s = unescapeJSUnicode(s) |
| s = strings.ReplaceAll(s, `\/`, `/`) |
| s = strings.ReplaceAll(s, `\"`, `"`) |
| return s |
| } |
|
|
| |
| |
| func unescapeJSUnicode(s string) string { |
| if !strings.Contains(s, `\u`) { |
| return s |
| } |
| var b strings.Builder |
| b.Grow(len(s)) |
| for i := 0; i < len(s); { |
| if i+5 < len(s) && s[i] == '\\' && s[i+1] == 'u' && |
| isHex(s[i+2]) && isHex(s[i+3]) && isHex(s[i+4]) && isHex(s[i+5]) { |
| r := (hexVal(s[i+2]) << 12) | (hexVal(s[i+3]) << 8) | |
| (hexVal(s[i+4]) << 4) | hexVal(s[i+5]) |
| b.WriteRune(rune(r)) |
| i += 6 |
| continue |
| } |
| b.WriteByte(s[i]) |
| i++ |
| } |
| return b.String() |
| } |
|
|
| func isHex(c byte) bool { |
| return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') |
| } |
|
|
| func hexVal(c byte) int { |
| switch { |
| case c >= '0' && c <= '9': |
| return int(c - '0') |
| case c >= 'a' && c <= 'f': |
| return int(c-'a') + 10 |
| case c >= 'A' && c <= 'F': |
| return int(c-'A') + 10 |
| } |
| return 0 |
| } |
|
|
| |
| func extractJSString(html, key string) string { |
| pat := regexp.MustCompile(`"` + regexp.QuoteMeta(key) + `"\s*:\s*"((?:[^"\\]|\\.)*)"`) |
| m := pat.FindStringSubmatch(html) |
| if m == nil { |
| return "" |
| } |
| return stripJSEscapes(m[1]) |
| } |
|
|
| |
| |
| func extractAttr(fragment, attr string) string { |
| pat := regexp.MustCompile(attr + `="([^"]+)"`) |
| m := pat.FindStringSubmatch(fragment) |
| if m == nil { |
| return "" |
| } |
| return m[1] |
| } |
|
|
| |
| |
| func extractError(html string) string { |
| if m := regexp.MustCompile(`"sErrTxt"\s*:\s*"([^"]*)"`).FindStringSubmatch(html); m != nil && m[1] != "" { |
| return m[1] |
| } |
| if m := regexp.MustCompile(`"sErrorCode"\s*:\s*"([^"]*)"`).FindStringSubmatch(html); m != nil && m[1] != "" { |
| return "Error code: " + m[1] |
| } |
| return "" |
| } |
|
|
| |
| |
| func parseMSALoginPage(html string) (*msaLoginPage, error) { |
| if len(html) < 500 { |
| return nil, newErr("msa_parse", "login page HTML too short (%d bytes)", len(html)) |
| } |
| if e := extractError(html); e != "" { |
| return nil, newErr("msa_parse", "MSA login page error: %s", e) |
| } |
|
|
| ppft := "" |
| if sft := extractJSString(html, "sFTTag"); sft != "" { |
| ppft = extractAttr(sft, "value") |
| } |
| if ppft == "" { |
| ppft = extractJSString(html, "sFT") |
| } |
| if ppft == "" { |
| return nil, newErr("msa_parse", "could not extract PPFT from MSA login page") |
| } |
|
|
| urlPost := extractJSString(html, "urlPost") |
| if urlPost == "" { |
| return nil, newErr("msa_parse", "could not extract urlPost from MSA login page") |
| } |
|
|
| return &msaLoginPage{ |
| ppft: ppft, |
| urlPost: urlPost, |
| username: extractJSString(html, "sPOST_Username"), |
| correlationID: extractJSString(html, "correlationId"), |
| }, nil |
| } |
|
|
| |
| |
| func parseMSAKmsiPage(html string) (*msaKmsiPage, error) { |
| if len(html) < 500 { |
| return nil, newErr("msa_kmsi_parse", "KMSI page HTML too short (%d bytes)", len(html)) |
| } |
| if e := extractError(html); e != "" { |
| return nil, newErr("msa_kmsi_parse", "MSA KMSI page error: %s", e) |
| } |
|
|
| ppft := extractJSString(html, "sFT") |
| if ppft == "" { |
| if m := regexp.MustCompile(`name="PPFT"[^>]*value="([^"]+)"`).FindStringSubmatch(html); m != nil { |
| ppft = m[1] |
| } |
| } |
| if ppft == "" { |
| return nil, newErr("msa_kmsi_parse", "could not extract new PPFT from KMSI page") |
| } |
| urlPost := extractJSString(html, "urlPost") |
| if urlPost == "" { |
| return nil, newErr("msa_kmsi_parse", "could not extract new urlPost from KMSI page") |
| } |
| return &msaKmsiPage{ppft: ppft, urlPost: urlPost}, nil |
| } |
|
|
| |
| func resolveMSURL(u string) string { |
| if u == "" { |
| return u |
| } |
| u = strings.ReplaceAll(u, `\u0026`, `&`) |
| if !strings.HasPrefix(u, "http") { |
| return "https://login.microsoftonline.com" + u |
| } |
| return u |
| } |
|
|
| |
| |
| |
| |
| func parseESTSLoginPage(html string) (*estsLoginPage, error) { |
| if len(html) < 500 { |
| return nil, newErr("ests_parse", "ESTS login page HTML too short (%d bytes)", len(html)) |
| } |
| cfg := &estsLoginPage{randomBlob: "PassportRN"} |
|
|
| if sft := extractJSString(html, "sFTTag"); sft != "" { |
| cfg.ppft = extractAttr(sft, "value") |
| } |
| if cfg.ppft == "" { |
| cfg.ppft = extractJSString(html, "sFT") |
| } |
| if cfg.ppft == "" { |
| if m := regexp.MustCompile(`name="PPFT"[^>]*value="([^"]+)"`).FindStringSubmatch(html); m != nil { |
| cfg.ppft = m[1] |
| } |
| } |
| if cfg.ppft == "" { |
| return nil, newErr("ests_parse", "could not extract PPFT from ESTS login page") |
| } |
| cfg.urlPost = resolveMSURL(extractJSString(html, "urlPost")) |
| if cfg.urlPost == "" { |
| return nil, newErr("ests_parse", "could not extract urlPost from ESTS login page") |
| } |
| cfg.urlGetCredentialType = resolveMSURL(extractJSString(html, "urlGetCredentialType")) |
| if rb := extractJSString(html, "sRandomBlob"); rb != "" { |
| cfg.randomBlob = rb |
| } |
| return cfg, nil |
| } |
|
|
| |
| |
| func extractCodeFromURL(rawurl string) (code, state, clientInfo string) { |
| u, err := url.Parse(rawurl) |
| if err != nil { |
| return "", "", "" |
| } |
| q := u.Query() |
| if c := q.Get("code"); c != "" { |
| return c, q.Get("state"), q.Get("client_info") |
| } |
| if u.Fragment != "" { |
| fq, err := url.ParseQuery(u.Fragment) |
| if err == nil { |
| if c := fq.Get("code"); c != "" { |
| return c, fq.Get("state"), fq.Get("client_info") |
| } |
| } |
| } |
| return "", "", "" |
| } |
|
|
| |
| |
| |
| |
| func detectMSState(html, currentURL string) string { |
| parsed, _ := url.Parse(currentURL) |
| path := strings.TrimRight(parsed.Path, "/") |
| if strings.HasSuffix(path, "/microsoftpopupcallback") { |
| return "code_found" |
| } |
|
|
| if strings.Contains(html, `"sErrTxt"`) { |
| if extractError(html) != "" { |
| return "error" |
| } |
| } |
|
|
| host := strings.ToLower(parsed.Host) |
| isMSA := strings.Contains(host, "login.live.com") |
|
|
| hasServerData := strings.Contains(html, "var ServerData") || strings.Contains(html, `"ServerData"`) |
| hasSFTTag := strings.Contains(html, `"sFTTag"`) && strings.Contains(html, "PPFT") |
| hasSFT := strings.Contains(html, `"sFT"`) |
|
|
| |
| |
| |
| if strings.Contains(html, `name="PageID" content="BssoInterrupt"`) || |
| strings.Contains(html, `"BssoInterrupt"`) || |
| strings.Contains(html, `sso_reload=True`) { |
| return "bsso_interrupt" |
| } |
|
|
| if isMSA { |
| switch { |
| case strings.HasSuffix(path, "/oauth20_authorize.srf") && (hasSFTTag || hasSFT): |
| return "msa_login" |
| case strings.HasSuffix(path, "/login.srf") && (hasSFTTag || hasSFT): |
| return "msa_login" |
| case strings.HasSuffix(path, "/ppsecure/post.srf") && hasServerData && hasSFT: |
| return "msa_kmsi" |
| } |
| } |
|
|
| if regexp.MustCompile(`"urlPost"\s*:`).MatchString(html) && |
| (hasSFTTag || hasSFT || strings.Contains(html, `name="PPFT"`)) { |
| return "ests_login" |
| } |
|
|
| if strings.Contains(currentURL, "proofs/Add") || strings.Contains(html, `"proofType"`) || |
| strings.Contains(html, `id="frmAddProof"`) || strings.Contains(currentURL, "proofs/Verify") || |
| strings.Contains(html, `id="frmVerifyProof"`) { |
| return "proofs" |
| } |
|
|
| |
| |
| |
| |
| |
| |
| if strings.Contains(currentURL, "/Consent/Update") || |
| strings.Contains(html, `Account_UpdateConsentPage_Client`) || |
| strings.Contains(html, `"sPageId":"Account_UpdateConsentPage_Client"`) { |
| return "consent" |
| } |
|
|
| if regexp.MustCompile(`<form[^>]*name="fmHF"`).MatchString(html) || |
| regexp.MustCompile(`<form[^>]*id="fmHF"`).MatchString(html) { |
| return "redirect_form" |
| } |
| if regexp.MustCompile(`<form[^>]*action="[^"]*"`).MatchString(html) { |
| return "redirect_form" |
| } |
| return "unknown" |
| } |
|
|
| |
| |
| |
| func parseRedirectForm(html string) (action string, fields map[string]string) { |
| fields = map[string]string{} |
|
|
| if m := regexp.MustCompile(`<form[^>]*id="?fmHF"?[^>]*action="([^"]+)"`).FindStringSubmatch(html); m != nil { |
| action = m[1] |
| } |
| if action == "" { |
| if m := regexp.MustCompile(`<form[^>]*action="([^"]+)"[^>]*id="?fmHF"?`).FindStringSubmatch(html); m != nil { |
| action = m[1] |
| } |
| } |
| if action == "" { |
| if m := regexp.MustCompile(`<form[^>]*action="([^"]+)"`).FindStringSubmatch(html); m != nil { |
| action = m[1] |
| } |
| } |
| for _, m := range regexp.MustCompile(`<input[^>]*type="hidden"[^>]*name="([^"]+)"[^>]*value="([^"]*)"`).FindAllStringSubmatch(html, -1) { |
| fields[m[1]] = m[2] |
| } |
| for _, m := range regexp.MustCompile(`<input[^>]*name="([^"]+)"[^>]*type="hidden"[^>]*value="([^"]*)"`).FindAllStringSubmatch(html, -1) { |
| if _, ok := fields[m[1]]; !ok { |
| fields[m[1]] = m[2] |
| } |
| } |
| action = resolveMSURL(action) |
| return action, fields |
| } |
|
|
| |
| |
| |
| func extractFormAttr(html, matchExpr, attr string) string { |
| pat := regexp.MustCompile(`<form[^>]*` + matchExpr + `[^>]*` + attr + `="([^"]+)"`) |
| if m := pat.FindStringSubmatch(html); m != nil { |
| return m[1] |
| } |
| pat = regexp.MustCompile(`<form[^>]*` + attr + `="([^"]+)"[^>]*` + matchExpr) |
| if m := pat.FindStringSubmatch(html); m != nil { |
| return m[1] |
| } |
| return "" |
| } |
|
|
| |
| |
| func extractInputValue(html, name string) string { |
| pat := regexp.MustCompile(`name="` + regexp.QuoteMeta(name) + `"\s*[^>]*value="([^"]*)"`) |
| if m := pat.FindStringSubmatch(html); m != nil { |
| return m[1] |
| } |
| return "" |
| } |
|
|
| |
| |
| func extractInputValueWherePrefix(html, name, prefix string) string { |
| pat := regexp.MustCompile(`name="` + regexp.QuoteMeta(name) + `"[^>]*value="(` + regexp.QuoteMeta(prefix) + `[^"]*)"`) |
| if m := pat.FindStringSubmatch(html); m != nil { |
| return m[1] |
| } |
| return "" |
| } |
|
|
| |
| |
| func extractClientVersion(html string) string { |
| if m := regexp.MustCompile(`src="/_assets/app-([a-f0-9]+)\.js"`).FindStringSubmatch(html); m != nil { |
| return m[1] |
| } |
| if m := regexp.MustCompile(`src="/_assets/ClientFramework-([a-f0-9]+)\.js"`).FindStringSubmatch(html); m != nil { |
| return m[1] |
| } |
| return "" |
| } |
|
|