package templates
import (
"bytes"
"fmt"
"io"
"reflect"
"sync"
"sync/atomic"
"github.com/logrusorgru/aurora"
"github.com/pkg/errors"
"gopkg.in/yaml.v2"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/globalmatchers"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/offlinehttp"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/signer"
"github.com/projectdiscovery/nuclei/v3/pkg/tmplexec"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
"github.com/projectdiscovery/utils/errkit"
stringsutil "github.com/projectdiscovery/utils/strings"
)
var (
ErrCreateTemplateExecutor = errors.New("cannot create template executer")
ErrIncompatibleWithOfflineMatching = errors.New("template can't be used for offline matching")
SignatureStats = map[string]*atomic.Uint64{}
)
const (
Unsigned = "unsigned"
)
func init() {
for _, verifier := range signer.DefaultTemplateVerifiers {
SignatureStats[verifier.Identifier()] = &atomic.Uint64{}
}
SignatureStats[Unsigned] = &atomic.Uint64{}
}
func Parse(filePath string, preprocessor Preprocessor, options *protocols.ExecutorOptions) (*Template, error) {
parser, ok := options.Parser.(*Parser)
if !ok {
panic("not a parser")
}
if !options.DoNotCache {
if value, _, _ := parser.compiledTemplatesCache.Has(filePath); value != nil {
tplCopy := *value
newBase := options.Copy()
newBase.TemplateID = tplCopy.Options.TemplateID
newBase.TemplatePath = tplCopy.Options.TemplatePath
newBase.TemplateInfo = tplCopy.Options.TemplateInfo
newBase.TemplateVerifier = tplCopy.Options.TemplateVerifier
newBase.RawTemplate = tplCopy.Options.RawTemplate
if tplCopy.Options.Variables.Len() > 0 {
newBase.Variables = tplCopy.Options.Variables
}
if len(tplCopy.Options.Constants) > 0 {
newBase.Constants = tplCopy.Options.Constants
}
tplCopy.Options = newBase
tplCopy.Options.ApplyNewEngineOptions(options)
if tplCopy.CompiledWorkflow != nil {
tplCopy.CompiledWorkflow.Options.ApplyNewEngineOptions(options)
for _, w := range tplCopy.CompiledWorkflow.Workflows {
for _, ex := range w.Executers {
ex.Options.ApplyNewEngineOptions(options)
}
}
}
for i, r := range tplCopy.RequestsDNS {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsDNS[i] = &rCopy
}
for i, r := range tplCopy.RequestsHTTP {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsHTTP[i] = &rCopy
}
for i, r := range tplCopy.RequestsCode {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsCode[i] = &rCopy
}
for i, r := range tplCopy.RequestsFile {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsFile[i] = &rCopy
}
for i, r := range tplCopy.RequestsHeadless {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsHeadless[i] = &rCopy
}
for i, r := range tplCopy.RequestsNetwork {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsNetwork[i] = &rCopy
}
for i, r := range tplCopy.RequestsJavascript {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsJavascript[i] = &rCopy
}
for i, r := range tplCopy.RequestsSSL {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsSSL[i] = &rCopy
}
for i, r := range tplCopy.RequestsWHOIS {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsWHOIS[i] = &rCopy
}
for i, r := range tplCopy.RequestsWebsocket {
rCopy := *r
rCopy.UpdateOptions(tplCopy.Options)
tplCopy.RequestsWebsocket[i] = &rCopy
}
template := &tplCopy
if template.isGlobalMatchersEnabled() {
item := &globalmatchers.Item{
TemplateID: template.ID,
TemplatePath: filePath,
TemplateInfo: template.Info,
}
for _, request := range template.RequestsHTTP {
item.Operators = append(item.Operators, request.CompiledOperators)
}
options.GlobalMatchers.AddOperator(item)
return nil, nil
}
if len(template.Workflows) > 0 {
compiled := &template.Workflow
compileWorkflow(filePath, preprocessor, tplCopy.Options, compiled, tplCopy.Options.WorkflowLoader)
template.CompiledWorkflow = compiled
template.CompiledWorkflow.Options = tplCopy.Options
}
if isCachedTemplateValid(template) {
return template, nil
}
}
}
var reader io.ReadCloser
if !options.DoNotCache {
_, raw, err := parser.parsedTemplatesCache.Has(filePath)
if err == nil && raw != nil {
reader = io.NopCloser(bytes.NewReader(raw))
}
}
var err error
if reader == nil {
reader, err = utils.ReaderFromPathOrURL(filePath, options.Catalog)
if err != nil {
return nil, err
}
}
defer func() {
_ = reader.Close()
}()
options = options.Copy()
options.TemplatePath = filePath
template, err := ParseTemplateFromReader(reader, preprocessor, options)
if err != nil {
return nil, err
}
if template.isGlobalMatchersEnabled() {
item := &globalmatchers.Item{
TemplateID: template.ID,
TemplatePath: filePath,
TemplateInfo: template.Info,
}
for _, request := range template.RequestsHTTP {
item.Operators = append(item.Operators, request.CompiledOperators)
}
options.GlobalMatchers.AddOperator(item)
return nil, nil
}
if len(template.Workflows) > 0 {
compiled := &template.Workflow
compileWorkflow(filePath, preprocessor, options, compiled, options.WorkflowLoader)
template.CompiledWorkflow = compiled
template.CompiledWorkflow.Options = options
}
template.Path = filePath
if !options.DoNotCache {
parser.compiledTemplatesCache.Store(filePath, template, nil, err)
}
return template, nil
}
func (template *Template) isGlobalMatchersEnabled() bool {
if !template.Options.Options.EnableGlobalMatchersTemplates {
return false
}
for _, request := range template.RequestsHTTP {
if request.GlobalMatchers {
return true
}
}
return false
}
func (template *Template) parseSelfContainedRequests() {
if template.Signature.Value.String() != "" {
for _, request := range template.RequestsHTTP {
request.Signature = template.Signature
}
}
if !template.SelfContained {
return
}
for _, request := range template.RequestsHTTP {
request.SelfContained = true
}
for _, request := range template.RequestsNetwork {
request.SelfContained = true
}
for _, request := range template.RequestsHeadless {
request.SelfContained = true
}
}
func (template *Template) Requests() int {
return len(template.RequestsDNS) +
len(template.RequestsHTTP) +
len(template.RequestsFile) +
len(template.RequestsNetwork) +
len(template.RequestsHeadless) +
len(template.Workflows) +
len(template.RequestsSSL) +
len(template.RequestsWebsocket) +
len(template.RequestsWHOIS) +
len(template.RequestsCode) +
len(template.RequestsJavascript)
}
func (template *Template) compileProtocolRequests(options *protocols.ExecutorOptions) error {
templateRequests := template.Requests()
if templateRequests == 0 {
return fmt.Errorf("no requests defined for %s", template.ID)
}
if options.Options.OfflineHTTP {
return template.compileOfflineHTTPRequest(options)
}
var requests []protocols.Request
if template.hasMultipleRequests() {
requests = template.RequestsQueue
if options.Flow == "" {
options.IsMultiProtocol = true
}
} else {
if len(template.RequestsDNS) > 0 {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsDNS)...)
}
if len(template.RequestsFile) > 0 {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsFile)...)
}
if len(template.RequestsNetwork) > 0 {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsNetwork)...)
}
if len(template.RequestsHTTP) > 0 {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsHTTP)...)
}
if len(template.RequestsHeadless) > 0 && options.Options.Headless {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsHeadless)...)
}
if len(template.RequestsSSL) > 0 {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsSSL)...)
}
if len(template.RequestsWebsocket) > 0 {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsWebsocket)...)
}
if len(template.RequestsWHOIS) > 0 {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsWHOIS)...)
}
if len(template.RequestsCode) > 0 && options.Options.EnableCodeTemplates {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsCode)...)
}
if len(template.RequestsJavascript) > 0 {
requests = append(requests, template.convertRequestToProtocolsRequest(template.RequestsJavascript)...)
}
}
var err error
template.Executer, err = tmplexec.NewTemplateExecuter(requests, options)
return err
}
func (template *Template) convertRequestToProtocolsRequest(requests interface{}) []protocols.Request {
switch reflect.TypeOf(requests).Kind() {
case reflect.Slice:
s := reflect.ValueOf(requests)
requestSlice := make([]protocols.Request, s.Len())
for i := 0; i < s.Len(); i++ {
value := s.Index(i)
valueInterface := value.Interface()
requestSlice[i] = valueInterface.(protocols.Request)
}
return requestSlice
}
return nil
}
func (template *Template) compileOfflineHTTPRequest(options *protocols.ExecutorOptions) error {
operatorsList := []*operators.Operators{}
mainLoop:
for _, req := range template.RequestsHTTP {
hasPaths := len(req.Path) > 0
if !hasPaths {
break mainLoop
}
for _, path := range req.Path {
pathIsBaseURL := stringsutil.EqualFoldAny(path, "{{BaseURL}}", "{{BaseURL}}/", "/")
if !pathIsBaseURL {
break mainLoop
}
}
operatorsList = append(operatorsList, &req.Operators)
}
if len(operatorsList) > 0 {
options.Operators = operatorsList
var err error
template.Executer, err = tmplexec.NewTemplateExecuter([]protocols.Request{&offlinehttp.Request{}}, options)
if err != nil {
return ErrIncompatibleWithOfflineMatching
}
return err
}
return ErrIncompatibleWithOfflineMatching
}
func ParseTemplateFromReader(reader io.Reader, preprocessor Preprocessor, options *protocols.ExecutorOptions) (*Template, error) {
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
hasPreprocessor := false
allPreprocessors := getPreprocessors(preprocessor)
for _, preprocessor := range allPreprocessors {
if preprocessor.Exists(data) {
hasPreprocessor = true
break
}
}
if !hasPreprocessor {
template, err := parseTemplate(data, options)
if err != nil {
return nil, err
}
if !template.Verified && len(template.Workflows) == 0 {
if config.DefaultConfig.LogAllEvents {
gologger.DefaultLogger.Print().Msgf("[%v] Template %s is not signed or tampered\n", aurora.Yellow("WRN").String(), template.ID)
}
}
return template, nil
}
template, err := parseTemplate(data, options)
if err != nil {
return nil, err
}
isVerified := template.Verified
if !template.Verified && len(template.Workflows) == 0 {
if config.DefaultConfig.LogAllEvents {
gologger.DefaultLogger.Print().Msgf("[%v] Template %s is not signed or tampered\n", aurora.Yellow("WRN").String(), template.ID)
}
}
generatedConstants := map[string]interface{}{}
for _, v := range allPreprocessors {
var replaced map[string]interface{}
data, replaced = v.ProcessNReturnData(data)
generatedConstants = generators.MergeMaps(generatedConstants, replaced)
}
reParsed, err := parseTemplate(data, options)
if err != nil {
return nil, err
}
reParsed.Constants = generators.MergeMaps(reParsed.Constants, generatedConstants)
reParsed.Options.Constants = reParsed.Constants
reParsed.Verified = isVerified
return reParsed, nil
}
func parseTemplate(data []byte, srcOptions *protocols.ExecutorOptions) (*Template, error) {
options := srcOptions.Copy()
template := &Template{}
var err error
switch config.GetTemplateFormatFromExt(template.Path) {
case config.JSON:
err = json.Unmarshal(data, template)
case config.YAML:
err = yaml.Unmarshal(data, template)
default:
if err = yaml.Unmarshal(data, template); err != nil {
return nil, err
}
}
if err != nil {
return nil, errkit.Wrapf(err, "failed to parse %s", template.Path)
}
if utils.IsBlank(template.Info.Name) {
return nil, errors.New("no template name field provided")
}
if template.Info.Authors.IsEmpty() {
return nil, errors.New("no template author field provided")
}
numberOfWorkflows := len(template.Workflows)
if numberOfWorkflows > 0 && numberOfWorkflows != template.Requests() {
return nil, errors.New("workflows cannot have other protocols")
}
if len(template.Workflows) == 0 {
if template.Info.SeverityHolder.Severity == severity.Undefined {
template.Info.SeverityHolder.Severity = severity.Unknown
if options.Options.Validate {
return nil, errors.New("no template severity field provided")
}
}
}
options.TemplateID = template.ID
options.TemplateInfo = template.Info
options.StopAtFirstMatch = template.StopAtFirstMatch
if template.Variables.Len() > 0 {
options.Variables = template.Variables
}
template.validateAllRequestIDs()
options.CreateTemplateCtxStore()
options.ProtocolType = template.Type()
options.Constants = template.Constants
if options.JsCompiler == nil {
options.JsCompiler = GetJsCompiler()
}
template.Options = options
if template.Requests() == 0 {
return nil, fmt.Errorf("no requests defined for %s", template.ID)
}
if err := template.ImportFileRefs(template.Options); err != nil {
return nil, errkit.Wrapf(err, "failed to load file refs for %s", template.ID)
}
if err := template.compileProtocolRequests(template.Options); err != nil {
return nil, err
}
if template.Executer != nil {
if err := template.Executer.Compile(); err != nil {
return nil, errors.Wrap(err, "could not compile request")
}
template.TotalRequests = template.Executer.Requests()
}
if template.Executer == nil && template.CompiledWorkflow == nil {
return nil, ErrCreateTemplateExecutor
}
template.parseSelfContainedRequests()
var verifier *signer.TemplateSigner
for _, verifier = range signer.DefaultTemplateVerifiers {
template.Verified, _ = verifier.Verify(data, template)
if config.DefaultConfig.LogAllEvents {
gologger.Verbose().Msgf("template %v verified by %s : %v", template.ID, verifier.Identifier(), template.Verified)
}
if template.Verified {
template.TemplateVerifier = verifier.Identifier()
break
}
}
options.TemplateVerifier = template.TemplateVerifier
if !(template.Verified && verifier.Identifier() == "projectdiscovery/nuclei-templates") {
template.Options.RawTemplate = data
}
return template, nil
}
func isCachedTemplateValid(template *Template) bool {
if template.Requests() == 0 && len(template.Workflows) == 0 {
return false
}
if template.Options == nil {
return false
}
if len(template.Workflows) == 0 && template.Executer == nil {
return false
}
if len(template.Workflows) > 0 && template.CompiledWorkflow == nil {
return false
}
if template.Options.TemplateID != template.ID {
return false
}
if template.Executer != nil {
if template.Requests() == 0 && template.Options.Flow == "" {
return false
}
}
if template.Options.Options == nil {
return false
}
return true
}
var (
jsCompiler *compiler.Compiler
jsCompilerOnce = sync.OnceFunc(func() {
jsCompiler = compiler.New()
})
)
func GetJsCompiler() *compiler.Compiler {
jsCompilerOnce()
return jsCompiler
}