CVE-2024-43405 (Go)

Nuclei is a vulnerability scanner powered by YAML based templates. Starting in version 3.0.0 and prior to version 3.3.2, a vulnerability in Nuclei's template signature verification system could allow an attacker to bypass the signature check and possibly execute malicious code via custom code template. The vulnerability is present in the template signature verification process, specifically in the `signer` package. The vulnerability stems from a discrepancy between how the signature verification process and the YAML parser handle newline characters, combined with the way multiple signatures are processed. This allows an attacker to inject malicious content into a template while maintaining a valid signature for the benign part of the template. CLI users are affected if they execute custom code templates from unverified sources. This includes templates authored by third parties or obtained from unverified repositories. SDK Users are affected if they are developers integrating Nuclei into their platforms, particularly if they permit the execution of custom code templates by end-users. The vulnerability is addressed in Nuclei v3.3.2. Users are strongly recommended to update to this version to mitigate the security risk. As an interim measure, users should refrain from using custom templates if unable to upgrade immediately. Only trusted, verified templates should be executed. Those who are unable to upgrade Nuclei should disable running custom code templates as a workaround.

Weakness:
CWE-94: name: Improper Control of Generation of Code ('Code Injection')
description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CWE-77: name: Improper Neutralization of Special Elements used in a Command ('Command Injection')
description: The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.
CWE-78: name: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.

Repository: https://github.com/projectdiscovery/nuclei

Vulnerable code:
file_path: pkg/templates/signer/tmpl_signer.go
snippet: func (t *TemplateSigner) Sign(data []byte, tmpl SignableTemplate) (string, error) {
	// while re-signing template check if it has a code protocol
	// if it does then verify that it is signed by current signer
	// if not then return error
	if tmpl.HasCodeProtocol() {
		sig := GetSignatureFromData(data)
		arr := strings.SplitN(string(sig), ":", 3)
		if len(arr) == 2 {
			// signature has no fragment
			return "", errorutil.NewWithTag("signer", "re-signing code templates are not allowed for security reasons.")
		}
		if len(arr) == 3 {
			// signature has fragment verify if it is equal to current fragment
			fragment := t.GetUserFragment()
			if fragment != arr[2] {
				return "", errorutil.NewWithTag("signer", "re-signing code templates are not allowed for security reasons.")
			}
		}
	}

	buff := bytes.NewBuffer(RemoveSignatureFromData(data))
	// if file has any imports process them
	for _, file := range tmpl.GetFileImports() {
		bin, err := os.ReadFile(file)
		if err != nil {
			return "", err
		}
		buff.WriteRune('\n')
		buff.Write(bin)
	}
	signatureData, err := t.sign(buff.Bytes())
	if err != nil {
		return "", err
	}
	return signatureData, nil
}

file_path: pkg/templates/signer/tmpl_signer.go
snippet: func (t *TemplateSigner) Verify(data []byte, tmpl SignableTemplate) (bool, error) {
	digestData := ReDigest.Find(data)
	if len(digestData) == 0 {
		return false, errors.New("digest not found")
	}

	digestData = bytes.TrimSpace(bytes.TrimPrefix(digestData, []byte(SignaturePattern)))
	// remove fragment from digest as it is used for re-signing purposes only
	digestString := strings.TrimSuffix(string(digestData), ":"+t.GetUserFragment())
	digest, err := hex.DecodeString(digestString)
	if err != nil {
		return false, err
	}

	buff := bytes.NewBuffer(RemoveSignatureFromData(data))
	// if file has any imports process them
	for _, file := range tmpl.GetFileImports() {
		bin, err := os.ReadFile(file)
		if err != nil {
			return false, err
		}
		buff.WriteRune('\n')
		buff.Write(bin)
	}

	return t.verify(buff.Bytes(), digest)
}

Fixed code:
file_path: pkg/templates/signer/tmpl_signer.go
snippet: import (
	"bytes"
	"crypto/ecdsa"
	"crypto/md5"
	"crypto/rand"
	"crypto/sha256"
	"encoding/gob"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"strings"
	"sync"

	"github.com/projectdiscovery/gologger"
	"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
	errorutil "github.com/projectdiscovery/utils/errors"
)

file_path: pkg/templates/signer/tmpl_signer.go
snippet: func (t *TemplateSigner) Sign(data []byte, tmpl SignableTemplate) (string, error) {
	existingSignature, content := ExtractSignatureAndContent(data)

	// while re-signing template check if it has a code protocol
	// if it does then verify that it is signed by current signer
	// if not then return error
	if tmpl.HasCodeProtocol() {
		if len(existingSignature) > 0 {
			arr := strings.SplitN(string(existingSignature), ":", 3)
			if len(arr) == 2 {
				// signature has no fragment
				return "", errorutil.NewWithTag("signer", "re-signing code templates are not allowed for security reasons.")
			}
			if len(arr) == 3 {
				// signature has fragment verify if it is equal to current fragment
				fragment := t.GetUserFragment()
				if fragment != arr[2] {
					return "", errorutil.NewWithTag("signer", "re-signing code templates are not allowed for security reasons.")
				}
			}
		}
	}

	buff := bytes.NewBuffer(content)
	// if file has any imports process them
	for _, file := range tmpl.GetFileImports() {
		bin, err := os.ReadFile(file)
		if err != nil {
			return "", err
		}
		buff.WriteRune('\n')
		buff.Write(bin)
	}
	signatureData, err := t.sign(buff.Bytes())
	if err != nil {
		return "", err
	}
	return signatureData, nil
}

file_path: pkg/templates/signer/tmpl_signer.go
snippet: func (t *TemplateSigner) Verify(data []byte, tmpl SignableTemplate) (bool, error) {
	signature, content := ExtractSignatureAndContent(data)
	if len(signature) == 0 {
		return false, errors.New("no signature found")
	}

	if !bytes.HasPrefix(signature, []byte(SignaturePattern)) {
		return false, errors.New("signature must be at the end of the template")
	}

	digestData := bytes.TrimSpace(bytes.TrimPrefix(signature, []byte(SignaturePattern)))
	// remove fragment from digest as it is used for re-signing purposes only
	digestString := strings.TrimSuffix(string(digestData), ":"+t.GetUserFragment())
	digest, err := hex.DecodeString(digestString)
	if err != nil {
		return false, err
	}

	buff := bytes.NewBuffer(content)
	// if file has any imports process them
	for _, file := range tmpl.GetFileImports() {
		bin, err := os.ReadFile(file)
		if err != nil {
			return false, err
		}
		buff.WriteRune('\n')
		buff.Write(bin)
	}

	return t.verify(buff.Bytes(), digest)
}

file_path: pkg/templates/signer/tmpl_signer.go
snippet: func ExtractSignatureAndContent(data []byte) (signature, content []byte) {
	dataStr := string(data)
	if idx := strings.LastIndex(dataStr, SignaturePattern); idx != -1 {
		signature = []byte(strings.TrimSpace(dataStr[idx:]))
		content = []byte(strings.TrimSpace(dataStr[:idx]))
	} else {
		content = data
	}
	return
}