UNPKG

@hclsoftware/secagent

Version:

IAST agent

328 lines (301 loc) 16.5 kB
//IASTIGNORE /* * **************************************************** * Licensed Materials - Property of HCL. * (c) Copyright HCL Technologies Ltd. 2017, 2025. * Note to U.S. Government Users *Restricted Rights. * **************************************************** */ 'use strict' /* * **************************************************** * 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 BASH_OS_COMMANDING_COMMENTS = { "\\" : "\\\\", "`" : "`", "$" : "\\$", "(" : "\\(", ")" : "\\)", "|" : "\\|", "&" : "&", ";" : ";", "#" : "#", "!" : "!", "<" : "<", ">" : ">", "\n" : "\\n" } const SHELL_COMMAND_CHARS = { "`" : "`", "$(" : "\\$\\(", "\\" : "\\\\", } const SHELL_COMMAND_BLACKLIST = { "`" : "`", "$" : "\\$", "(" : "\\(", "\\" : "\\\\", } const DOUBLE_QUOTE_ESCAPE_AS_LIST = { "\"" : "\"", "\\" : "\\\\" } const BRACKETS_ESCAPE_AS_LIST = { "]" : "]", "\\" : "\\\\" } const BRACES_ESCAPE_AS_LIST = { "}" : "}", "\\" : "\\\\" } const WHITESPACE_CHARS_AS_LIST = { " " : " ", "\t": "\\t", "\r": "\\r" } const ctxOpeners = { "'" : ContextType.SINGLE_QUOTE, "\"": ContextType.DOUBLE_QUOTE, "(": ContextType.PARENTHESES, "[": ContextType.BRACKETS, "{": ContextType.BRACES, "`": ContextType.BACK_TICK, "$(": ContextType.DOLLAR_SINGLE_PARENTHESES } // THE BASH OS COMMANDING ALGORITHM : // // 1.single-Quote-other within context does not count // a.if['] must be replaced and ["\\", "`", "$", "(", ")", "|", "&", ";", "#", "!", "<", ">", "\n"] then vuln // // 2. double-Quote-other within context does not count excluding[``,$()]**** // a.if[ `` or $() \]then vuln // b.if[" OR \] and ["\\", "`", "$", "(", ")", "|", "&", ";", "#", "!", "<", ">", "\n"] then vuln // // 3.if["["]-> // a.if[ `` or $() \]then vuln // b.if[']'OR \] and ["\\", "`", "$", "(", ")", "|", "&", ";", "#", "!", "<", ">", "\n"] then vuln // // 4.if["{"]->[}OR \] and ["\\", "`", "$", "(", ")", "|", "&", ";", "#", "!", "<", ">", "\n"] then vuln // // 5.no-Context // ["\\", "`", "$", "(", ")", "|", "&", ";", "#", "!", "<", ">", "\n"] then vuln // // 6.if["("] -> Same as 5 // // 7.ctx-> `Jakrata` then // Same as 5 // // 8.ctx->$(Jakrata)then // Same as 5 // // 9. if [${] - no currently support // 10. if [$'] - no currently support // // // ****in case we are in double context we do not count parenthesis UNLESS we are in nested Backtick context // within doubleQuote context(ex: \"`(INPUT)`\") - then we DO have to follow the parenthesis. // "backTicInDoubleQuoteCtx"is the flag for this case and if we close Backtick context // in case of nested backTick context within doubleQuote(ex: \"`$((INPUT))`\") "backTicInDoubleQuoteCtx" // would be true only when we close the most external backtick(`` in the ex) class BashOsCommandingSinkTask 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() { // We have a list of verifications and sanitizers that have been run on a string. In order to validate that they actually work, we need // to send through multiple tainted strings and see whether they were validated or not. // return value of source - original param let parameterUnderTest = new ParameterUnderTest(this.taintedObjectFlow.entity.value, this.taintedObjectFlow.taskList) let bashOsCommandingContext = new BashOsCommandingContext(parameterUnderTest, this.command) this.performContextsAnalysis(bashOsCommandingContext, parameterUnderTest) return "" } runContextAwareAnalysis(contextType, parameterUnderTest) { let exploitList let containUnescapeContext = false for (let currentContext of contextType.getContextTypesStackBased()){ if(currentContext === ContextType.SINGLE_QUOTE.contextTypeName){ let processingResult = parameterUnderTest.getMutatedProcessedParam("'") let processedLegitimateExploitForCodePath = processingResult.afterProcessing if(processedLegitimateExploitForCodePath != null){ if(processedLegitimateExploitForCodePath.origStringIncludes("'")) containUnescapeContext = true } } else if(currentContext === ContextType.DOUBLE_QUOTE.contextTypeName || currentContext === ContextType.BRACKETS.contextTypeName){ // in case of ["\"", "[]"] context - first test if ["`", "$(", "\"] are replaced - context does not matter // otherwise continue testing with all other characters AND their context exploitList = Object.keys(SHELL_COMMAND_CHARS) for (let exploit of exploitList){ let processingResult = parameterUnderTest.getMutatedProcessedParam(exploit) let processedLegitimateExploitForCodePath = processingResult.afterProcessing if (processedLegitimateExploitForCodePath != null){ if(SinkTask.containsUnescapedChars(processedLegitimateExploitForCodePath, SHELL_COMMAND_BLACKLIST, false, '\\' )) return new SinkTaskInfo(exploit, [currentContext], exploitList, this.vulnerability) } } if(currentContext === ContextType.DOUBLE_QUOTE.contextTypeName){ //test if double quote is replaced let processingResult = parameterUnderTest.getMutatedProcessedParam("\"") let processedLegitimateExploitForCodePath = processingResult.afterProcessing if(processedLegitimateExploitForCodePath != null){ if(SinkTask.containsUnescapedChars(processedLegitimateExploitForCodePath, DOUBLE_QUOTE_ESCAPE_AS_LIST, false, '\\')) containUnescapeContext = true } } else { // test if bracket ([) is replaced let processingResult = parameterUnderTest.getMutatedProcessedParam("]") let processedLegitimateExploitForCodePath = processingResult.afterProcessing if(processedLegitimateExploitForCodePath != null){ if(SinkTask.containsUnescapedChars(processedLegitimateExploitForCodePath, BRACKETS_ESCAPE_AS_LIST, false, '\\')) containUnescapeContext = true } } } else if (currentContext === ContextType.BRACES.contextTypeName) { // if context is {} //test if braces are replaced {} let processingResult = parameterUnderTest.getMutatedProcessedParam("}") let processedLegitimateExploitForCodePath = processingResult.afterProcessing if(processedLegitimateExploitForCodePath != null){ if(SinkTask.containsUnescapedChars(processedLegitimateExploitForCodePath, BRACES_ESCAPE_AS_LIST, false, '\\')) containUnescapeContext = true } } // TODO add $'___' context support ($' allows singleQuote escaping) AND ${___} context if(currentContext === ContextType.UNQUOTED.contextTypeName || currentContext === ContextType.PARENTHESES.contextTypeName || containUnescapeContext){ exploitList = this.getPayloadsForContextStack(new ContextInfo([currentContext])) for(const exploit of exploitList){ let processingResult = parameterUnderTest.getMutatedProcessedParam(exploit) let processedLegitimateExploitForCodePath = processingResult.afterProcessing if(processedLegitimateExploitForCodePath != null){ if(this.containsBlacklistedChars(processedLegitimateExploitForCodePath,false)){ return new SinkTaskInfo(exploit, [currentContext], exploitList, this.vulnerability) } } } } } } containsBlacklistedChars(possiblySanitizedPayload, checkWhitespaceChars){ return BashOsCommandingSinkTask.containsBlacklistedChars(possiblySanitizedPayload, checkWhitespaceChars) } static containsBlacklistedChars(possiblySanitizaedPayload, checkWhitespaceChars){ // List of escapable chars was modified from: https://stackoverflow.com/questions/15783701/which-characters-need-to-be-escaped-when-using-bash // Further info regarding context sensitive escapable chars can be found here: https://stackoverflow.com/questions/6697753/difference-between-single-and-double-quotes-in-bash let blackListedChars = BASH_OS_COMMANDING_COMMENTS; // Check whitespace chars if (checkWhitespaceChars) { blackListedChars.concat(WHITESPACE_CHARS_AS_LIST); } return SinkTask.containsUnescapedChars(possiblySanitizaedPayload, blackListedChars, false, '\\'); } getCommentChars() { return Object.keys(BASH_OS_COMMANDING_COMMENTS) } } class BashOsCommandingContext extends VulnerabilityContext { constructor(parameter, osCommandingQuery) { super(osCommandingQuery, parameter); } estimateContextStackBased(leftEdge, rightEdge) { let leftCtxStack =[] let rightCtxStack =[] var currentCtx = null let inSingleQuoteCtx = false; let inDoubleQuoteCtx = false; let backTicInDoubleQuoteCtxCounter = 0 ; let dollarParenthesisInDoubleQuoteCtxCounter = 0 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 = "$(" } //Check if we can close the current context if(currentCtx != null && currentCtx.getCtxCloser != null && currentCtx.getCtxCloser === c) { leftCtxStack.pop() // in case we are in double context we do not count parenthesis UNLESS we are in nested Backtick context // within doubleQuote context (ex: \"`(INPUT)`\") - then we DO have to follow the parenthesis. // "backTicInDoubleQuoteCtx" is the flag for this case and if we close Backtick context // we need to set the boolean flag to its actual value // in case of nested backTick context within doubleQuote (ex: \"`$((INPUT))`\") "backTicInDoubleQuoteCtx" // would be true only when we close the most external backtick(`` in the ex) if(inDoubleQuoteCtx){ if(currentCtx === ContextType["BACK_TICK"]) backTicInDoubleQuoteCtxCounter-- else if(currentCtx === ContextType["DOLLAR_SINGLE_PARENTHESES"]) dollarParenthesisInDoubleQuoteCtxCounter-- } currentCtx = !leftCtxStack.length ? null : ContextType[leftCtxStack[leftCtxStack.length -1 ]] } // Check if we are opening a new context // Can only open a new context if we're not in a single quoted context else if (!inSingleQuoteCtx && !inDoubleQuoteCtx && Object.keys(ctxOpeners).origArrayIncludes(c)){ currentCtx = ctxOpeners[c] leftCtxStack.push(currentCtx.contextTypeName) } // can open only backTic context UNLESS backTicInDoubleQuoteCtx is TRUE // in case we are in double context we do not count parenthesis UNLESS we are in nested Backtick context // within doubleQuote context (ex: \"`(INPUT)`\") - then we DO have to follow the parenthesis. // "backTicInDoubleQuoteCtx" is the flag for this case and if we close Backtick context // we need to set the boolean flag to its actual value // in case of nested backTick context within doubleQuote (ex: \"`$((INPUT))`\") "backTicInDoubleQuoteCtx" // would be true only when we close the most external backtick(`` in the ex) else if(inDoubleQuoteCtx){ if((backTicInDoubleQuoteCtxCounter || dollarParenthesisInDoubleQuoteCtxCounter) && Object.keys(ctxOpeners).origArrayIncludes(c) || c === "`" || c === "$("){ currentCtx = ctxOpeners[c] leftCtxStack.push(currentCtx.contextTypeName) } if(c === "$(") dollarParenthesisInDoubleQuoteCtxCounter++ else if(c === "`") backTicInDoubleQuoteCtxCounter++ } inSingleQuoteCtx = currentCtx === ContextType["SINGLE_QUOTE"] inDoubleQuoteCtx = leftCtxStack.origArrayIncludes(ContextType["DOUBLE_QUOTE"].contextTypeName) } // currentCtx is null IFF all the contexts before the user parameter have been closed // This indicates we're in an UNQUOTED context // Furthermore, in most cases (except when in a SINGLE_QUOTE context) , if 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 SINGLE_QUOTE context if(currentCtx == null || currentCtx === ContextType["BACK_TICK"] || currentCtx === ContextType["DOLLAR_SINGLE_PARENTHESES"]){ 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["BACK_TICK"] || currentCtx === ContextType["DOLLAR_SINGLE_PARENTHESES"]) 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 BACK_TICK context // Simply put, if a context stack contains a BACK_TICK context we have to escape contexts only until we reach the BACK_TICK context // For example: // ls ['(TAINTED_PARAM)'] -> in this case, it's enough to break out of the innermost parentheses to have arbitrary code execution //TODO: is the last line of the comment true for subshells to? Maybe it's enough to break into () or {} contexts if(currentCtx === ContextType["BACK_TICK"] || currentCtx === ContextType["DOLLAR_SINGLE_PARENTHESES"]) 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 = BashOsCommandingSinkTask module.exports.BashOsCommandingContext = BashOsCommandingContext