@hclsoftware/secagent
Version:
IAST agent
208 lines (188 loc) • 9.07 kB
JavaScript
//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