UNPKG

@hclsoftware/secagent

Version:

IAST agent

208 lines (188 loc) 9.07 kB
//IASTIGNORE /* * **************************************************** * Licensed Materials - Property of HCL. * (c) Copyright HCL Technologies Ltd. 2017, 2025. * Note to U.S. Government Users *Restricted Rights. * **************************************************** */ const {SinkTask} = require("./SinkTask") const TaskType = require("./TaskType") const {VulnerabilityContext} = require("./VulnerabilityContext") const ContextType = require("./ContextType") const ContextInfo = require("./ContextInfo") const {ParameterUnderTest} = require("./ParameterUnderTest") const SinkTaskInfo = require("./SinkInfo/SinkTaskInfo") const COMMENT_CHARS = { "|" : "\\|", "&" : "&", ">" : ">", "<" : "<", "^" : "\\^", "(`": "\\(`", "`)": "\\)`", "/" : "/", "@" : "@" } const ESCAPABLE_CHARS = { "|" : "\\|", "&" : "&", ">" : ">", "<" : "<", "^" : "\\^", "(": "\\(", ")": "\\)", "/" : "\\/", "@" : "@" } const DOUBLE_QUOTE_ESCAPE_AS_LIST = { "\"": "\"", "^" : "^" } const PERCENT_ESCAPE_AS_LIST = { "%": "%", "^": "^" } const ctxOpeners = {} ctxOpeners["\""] = ContextType.DOUBLE_QUOTE ctxOpeners["(`"] = ContextType.SINGLE_PARENTHESES_BACK_TICK ctxOpeners["%"] = ContextType.PERCENT //CMD command Algorithm: // primitives:["|","&",">","<","^","(`","`)","/","@"] // // DOUBLE_QUOTE:-no other context within double-quote // a.["] must be escaped AND un escaping one of primitives above // // PERCENT: // a.[%]must be escaped AND un escaping one of primitives above // // SINGLE_PARENTHESES_BACK_TICK: // a.[(``)]as unquoted,un escaping one of primitives above // // UNQUOTED: // a.un escaping one of primitives above // // SINGLE_QUOTE: // a.has no meaning in cmd contexts class CmdOsCommandingSinkTask extends SinkTask{ constructor(source, v, stack, parameters, obj) { super(source, v, stack, parameters); this.taskType = TaskType.OS_COMMANDING_SINK this.command = obj == null ? null : obj.toString() } performAction() { let parameterUnderTest = new ParameterUnderTest(this.taintedObjectFlow.entity.value, this.taintedObjectFlow.taskList) let cmdOsCommandingContext = new CmdOsCommandingContext(parameterUnderTest, this.command) this.performContextsAnalysis(cmdOsCommandingContext, parameterUnderTest) return "" } runContextAwareAnalysis(contextType, parameterUnderTest) { let containUnescapeContext = false for (let currentContext of contextType.getContextTypesStackBased()){ if(currentContext === ContextType.DOUBLE_QUOTE.contextTypeName){ let processingResult = parameterUnderTest.getMutatedProcessedParam("\"") let processedLegitimateExploitForCodePath = processingResult.afterProcessing if(processedLegitimateExploitForCodePath != null){ if(SinkTask.containsUnescapedChars(processedLegitimateExploitForCodePath, DOUBLE_QUOTE_ESCAPE_AS_LIST, false, '^' )) containUnescapeContext = true } }else if(currentContext === ContextType.PERCENT.contextTypeName){ let processingResult = parameterUnderTest.getMutatedProcessedParam("%") let processedLegitimateExploitForCodePath = processingResult.afterProcessing if(processedLegitimateExploitForCodePath != null){ if(SinkTask.containsUnescapedChars(processedLegitimateExploitForCodePath, PERCENT_ESCAPE_AS_LIST, false, '^' )) containUnescapeContext = true } } if(currentContext === ContextType.UNQUOTED.contextTypeName || currentContext === ContextType.SINGLE_PARENTHESES_BACK_TICK.contextTypeName || containUnescapeContext){ let exploitList = this.getCommentChars() for(const exploit of exploitList){ let processingResult = parameterUnderTest.getMutatedProcessedParam(exploit) let processedLegitimateExploitForCodePath = processingResult.afterProcessing if(processedLegitimateExploitForCodePath != null){ if(this.containsBlacklistedChars(processedLegitimateExploitForCodePath)){ return new SinkTaskInfo(exploit, [currentContext], exploitList, this.vulnerability) } } } } } } containsBlacklistedChars(possiblySanitizedPayload) { return CmdOsCommandingSinkTask.containsBlacklistedChars(possiblySanitizedPayload) } static containsBlacklistedChars(possiblySanitizedPayload){ return SinkTask.containsUnescapedChars(possiblySanitizedPayload, ESCAPABLE_CHARS, false, '^') } getCommentChars() { return Object.keys(COMMENT_CHARS) } } class CmdOsCommandingContext extends VulnerabilityContext { constructor(parameter, osCommandingQuery) { super(osCommandingQuery, parameter); } estimateContextStackBased(leftEdge, rightEdge) { let leftCtxStack =[] let rightCtxStack =[] var currentCtx let inDoubleQuoteCtx = false for(let i = 0; i < leftEdge.length; i++) { let c if(i > 0) { if(leftEdge.charAt(i-1) === '^') continue; } c = leftEdge.charAt(i) // check if we are in (``) context if(i + 1 < leftEdge.length && leftEdge.charAt(i) === '(' && leftEdge.charAt(i + 1) === '`') { i++ c = "(`" }else if(i + 1 < leftEdge.length && leftEdge.charAt(i) === '`' && leftEdge.charAt(i + 1) === ')') { i++ c = "`)" } //Check if we can close the current context if(currentCtx != null && currentCtx.getCtxCloser != null && currentCtx.getCtxCloser === c) { leftCtxStack.pop() currentCtx = !leftCtxStack.length ? null : ContextType[leftCtxStack[leftCtxStack.length -1 ]] } // Check if we are opening a new context else if (!inDoubleQuoteCtx && Object.keys(ctxOpeners).origArrayIncludes(c)){ currentCtx = ctxOpeners[c] leftCtxStack.push(currentCtx.contextTypeName) } inDoubleQuoteCtx = currentCtx === ContextType["DOUBLE_QUOTE"] } // currentCtx is null IFF all the contexts before the user parameter have been closed // This indicates we're in an UNQUOTED context // Furthermore, in case we are not in DOUBLE_QUOTE context and currentCtx is BACK_TICK we can run commands directly // We can definitively say a BACK_TICK context is unquoted IFF it's not within a DOUBLE_QUOTE context if(currentCtx == null || currentCtx === ContextType["SINGLE_PARENTHESES_BACK_TICK"]){ return new ContextInfo([ContextType["UNQUOTED"].contextTypeName]) } // At this point, leftCtxStack holds all the unterminated contexts from the left of the user parameter // Scanning the rightEdge we're trying to match each open context with a terminator // Each terminator we find is added to the rightCtxStack for(const c of rightEdge){ if(currentCtx == null || currentCtx.getCtxCloser === c){ rightCtxStack.push(currentCtx.contextTypeName) leftCtxStack.pop() currentCtx = !leftCtxStack.length ? null : ContextType[leftCtxStack[leftCtxStack.length-1]] } if(currentCtx == null || currentCtx === ContextType["SINGLE_PARENTHESES_BACK_TICK"]) break; } // When we get to this point, we should have depleted all the unterminated contexts in leftCtxStack // If we did, rightCtxStack holds all the contexts we need to break/escape (in order) // One sub-case relevant specifically to shell style injections is the use of a SINGLE_PARENTHESES_BACK_TICK context // Simply put, if a context stack contains a SINGLE_PARENTHESES_BACK_TICK context we have to escape contexts only until we reach the SINGLE_PARENTHESES_BACK_TICK context // For example: // ls [(`TAINTED_PARAM`)] -> in this case we are in unquoted context if(currentCtx === ContextType["SINGLE_PARENTHESES_BACK_TICK"]) return rightCtxStack.length !== 0 ? new ContextInfo(rightCtxStack) : new ContextInfo([ContextType["UNQUOTED"].contextTypeName]) else return leftCtxStack.length === 0 ? new ContextInfo(rightCtxStack) : new ContextInfo([ContextType["UNKNOWN"].contextTypeName]) } } module.exports = CmdOsCommandingSinkTask module.exports.CmdOsCommandingContext = CmdOsCommandingContext