diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql deleted file mode 100644 index 75a0e3aab12..00000000000 --- a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql +++ /dev/null @@ -1,241 +0,0 @@ -/** - * @name User Input to Invoke-Expression - * @description Finding cases where the user input is passed an dangerous method that can lead to RCE - * @kind path-problem - * @problem.severity error - * @security-severity 9.8 - * @precision high - * @id powershell/microsoft/public/user-input-to-invoke-expression - * @tags security - * external/cwe/cwe-078 - * external/cwe/cwe-088 - */ - -import powershell -import semmle.code.powershell.dataflow.TaintTracking -import semmle.code.powershell.dataflow.DataFlow -import semmle.code.powershell.ApiGraphs -import semmle.code.powershell.dataflow.flowsources.FlowSources - -import Sanitizers - -private module TestConfig implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node source) { - source instanceof SourceNode or - source instanceof Source - } - - predicate isSink(DataFlow::Node sink) { sink instanceof Sink } - predicate isBarrier(DataFlow::Node node) {node instanceof Sanitizer} -} - -abstract class Source extends DataFlow::Node {} - -class ReadHostSource extends Source { - ReadHostSource() { - exists(CmdCall c | - this.asExpr().getExpr() = c and - c.getName() = "Read-Host" ) - } -} - -class GetContentSource extends Source { - GetContentSource() { - exists(CmdCall c | - this.asExpr().getExpr() = c and - c.getName() = "Get-Content" ) - } -} - -class ValueFromPipelineSource extends Source { - ValueFromPipelineSource() { - exists(Parameter p | - p.getAnAttribute().toString() = "ValueFromPipeline" and - this.asExpr().getExpr() = p.getAnAccess() - ) - } -} - -abstract class Sink extends DataFlow::Node {} - -class InvokeExpressionCall extends Sink { - InvokeExpressionCall() { - exists(CmdCall c | - this.asExpr().getExpr() = c.getAnArgument() and - c.getName() = ["Invoke-Expression", "iex", "Add-Type" ] ) - } -} - -class InvokeScriptSink extends Sink { - InvokeScriptSink() { - exists(API::Node call | - API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("invokescript") = call and - this = call.getArgument(_).asSink() - ) - } -} - -class CreateNestedPipelineSink extends Sink { - CreateNestedPipelineSink() { - exists(API::Node call | - API::getTopLevelMember("host").getMember("runspace").getMethod("createnestedpipeline") = call and - this = call.getArgument(_).asSink() - ) - } -} - -class AddScriptInvokeSink extends Sink { - AddScriptInvokeSink() { - exists(InvokeMemberExpr ie | - this.asExpr().getExpr() = ie.getAnArgument() and - ie.getName() = "AddScript" and - ie.getQualifier().(InvokeMemberExpr).getName() = "Create" and - ie.getQualifier().getAChild().toString() = "PowerShell" and - ie.getParent().(InvokeMemberExpr).getName() = "Invoke" - ) - } -} - -class PowershellSink extends Sink { - PowershellSink() { - exists( CmdCall c | - c.getName() = "powershell" | - ( - this.asExpr().getExpr() = c.getArgument(1) and - c.getArgument(0).getValue().toString() = "-command" - ) or - ( - this.asExpr().getExpr() = c.getArgument(0) - ) - ) - } -} - -class CmdSink extends Sink { - CmdSink() { - exists(CmdCall c | - this.asExpr().getExpr() = c.getArgument(1) and - c.getName() = "cmd" and - c.getArgument(0).getValue().toString() = "/c" - ) - } -} - -class ForEachObjectSink extends Sink { - ForEachObjectSink() { - exists(CmdCall c | - this.asExpr().getExpr() = c.getAnArgument() and - c.getName() = "Foreach-Object" - ) - } -} - -class InvokeSink extends Sink { - InvokeSink() { - exists(InvokeMemberExpr ie | - this.asExpr().getExpr() = ie.getCallee() or - this.asExpr().getExpr() = ie.getQualifier().getAChild*() - ) - } -} - -class CreateScriptBlockSink extends Sink { - CreateScriptBlockSink() { - exists(InvokeMemberExpr ie | - this.asExpr().getExpr() = ie.getAnArgument() and - ie.getName() = "Create" and - ie.getQualifier().toString() = "ScriptBlock" - ) - } -} - -class NewScriptBlockSink extends Sink { - NewScriptBlockSink() { - exists(API::Node call | - API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call and - this = call.getArgument(_).asSink() - ) - } -} - -class ExpandStringSink extends Sink { - ExpandStringSink() { - exists(API::Node call | this = call.getArgument(_).asSink() | - API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("expandstring") = call or - API::getTopLevelMember("executioncontext").getMember("sessionstate").getMember("invokecommand").getMethod("expandstring") = call - - ) - } -} - -module TestFlow = TaintTracking::Global; -import TestFlow::PathGraph - -from TestFlow::PathNode source, TestFlow::PathNode sink -where - TestFlow::flowPath(source, sink) -select sink.getNode(), source, sink, "Flow from user input to dangerous method" - -// from CmdCall c -// where c.getName() = "cmd" -// and c.getArgument(0).getValue().toString() = "/c" -// select c.getArgument(1) - -// from InvokeMemberExpr ie -// where ie.getName() = "Create" and -// ie.getQualifier().toString() = "ScriptBlock" -// select ie, ie.getQualifier(), ie.getAnArgument() - -// from API::Node call -// where API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call -// select call, call.getArgument(_).asSink() - -// from Expr e -// where e.getLocation().getFile().getBaseName() = "InjectionHunterTests.ps1" -// and e.getLocation().getStartLine() = 106 -// select e, e.getAQlClass() - - -// from Function f, CmdCall c -// where f.getLocation().getFile().getBaseName() = "sanitizers.ps1" -// select f, f.getAParameter().getStaticType(), f.getAParameter().getName() - - -//TBD, waiting on mathias on how to connect f and c -// from Function f, CmdCall c, Parameter p, Argument a -// where -// p = f.getAParameter() and -// a = c.getAnArgument() and -// p.getName().toLowerCase() = a.getName() and -// p.getStaticType() != "Object" and -// c.getName() = f.getName() -// select a, "argument has a specified static type" - -// from Argument a, VarReadAccess v -// where a.getAChild() = v and -// v.getVariable().getName() = "UserInput" -// select a, v - -// from Argument e -// where e.getLocation().getFile().getBaseName() = "sanitizers.ps1" -// and e.getLocation().getStartLine() = 14 -// select e, e.getAChild(), e.getParent(), e.toString() - -// from PipelineParameter p -// where p.getLocation().getFile().getBaseName() = "userinput.ps1" -// select p, p.getName(), p.getAChild() - -// from Attribute a -// select a, a.getParent(), a.getParent().getAQlClass(), a.getANamedArgument() - - - -// from Expr e -// where e.getLocation().getFile().getBaseName() = "sanitizers.ps1" -// and e.getLocation().getStartLine() = 31 -// select e, e.getAQlClass() - -// from InvokeMemberExpr ie -// where -// ie.getLocation().getStartLine() = 28 and ie.getName() = "AddScript" -// select ie, ie.getName(), ie.getQualifier().toString(), ie.getQualifier().getAChild().toString(), ie.getParent().(InvokeMemberExpr).getName() \ No newline at end of file diff --git a/powershell/ql/src/experimental/InjectionHunter/Sanitizers.qll b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sanitizers.qll similarity index 100% rename from powershell/ql/src/experimental/InjectionHunter/Sanitizers.qll rename to powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sanitizers.qll diff --git a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll new file mode 100644 index 00000000000..4c62966746f --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll @@ -0,0 +1,152 @@ +import powershell +import semmle.code.powershell.dataflow.TaintTracking +import semmle.code.powershell.dataflow.DataFlow +import semmle.code.powershell.ApiGraphs +import semmle.code.powershell.dataflow.flowsources.FlowSources + +abstract class InjectionSink extends DataFlow::Node { + abstract string getSinkType(); +} + +class InvokeExpressionCall extends InjectionSink { + InvokeExpressionCall() { + exists(CmdCall c | + this.asExpr().getExpr() = c.getAnArgument() and + c.getName() = ["Invoke-Expression", "iex", "Add-Type" ] ) + } + override string getSinkType(){ + result = "call to Invoke-Expression" + } +} + +class InvokeScriptSink extends InjectionSink { + InvokeScriptSink() { + exists(API::Node call | + API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("invokescript") = call and + this = call.getArgument(_).asSink() + ) + } + override string getSinkType(){ + result = "call to InvokeScript" + } +} + +class CreateNestedPipelineSink extends InjectionSink { + CreateNestedPipelineSink() { + exists(API::Node call | + API::getTopLevelMember("host").getMember("runspace").getMethod("createnestedpipeline") = call and + this = call.getArgument(_).asSink() + ) + } + override string getSinkType(){ + result = "call to CreateNestedPipeline" + } +} + +class AddScriptInvokeSink extends InjectionSink { + AddScriptInvokeSink() { + exists(InvokeMemberExpr ie | + this.asExpr().getExpr() = ie.getAnArgument() and + ie.getName() = "AddScript" and + ie.getQualifier().(InvokeMemberExpr).getName() = "Create" and + ie.getQualifier().getAChild().toString() = "PowerShell" and + ie.getParent().(InvokeMemberExpr).getName() = "Invoke" + ) + } + override string getSinkType(){ + result = "call to AddScript" + } +} + +class PowershellSink extends InjectionSink { + PowershellSink() { + exists( CmdCall c | + c.getName() = "powershell" | + ( + this.asExpr().getExpr() = c.getArgument(1) and + c.getArgument(0).getValue().toString() = "-command" + ) or + ( + this.asExpr().getExpr() = c.getArgument(0) + ) + ) + } + override string getSinkType(){ + result = "call to Powershell" + } +} + +class CmdSink extends InjectionSink { + CmdSink() { + exists(CmdCall c | + this.asExpr().getExpr() = c.getArgument(1) and + c.getName() = "cmd" and + c.getArgument(0).getValue().toString() = "/c" + ) + } + override string getSinkType(){ + result = "call to Cmd" + } +} + +class ForEachObjectSink extends InjectionSink { + ForEachObjectSink() { + exists(CmdCall c | + this.asExpr().getExpr() = c.getAnArgument() and + c.getName() = "Foreach-Object" + ) + } + override string getSinkType(){ + result = "call to ForEach-Object" + } +} + +class InvokeSink extends InjectionSink { + InvokeSink() { + exists(InvokeMemberExpr ie | + this.asExpr().getExpr() = ie.getCallee() or + this.asExpr().getExpr() = ie.getQualifier().getAChild*() + ) + } + override string getSinkType(){ + result = "call to Invoke" + } +} + +class CreateScriptBlockSink extends InjectionSink { + CreateScriptBlockSink() { + exists(InvokeMemberExpr ie | + this.asExpr().getExpr() = ie.getAnArgument() and + ie.getName() = "Create" and + ie.getQualifier().toString() = "ScriptBlock" + ) + } + override string getSinkType(){ + result = "call to CreateScriptBlock" + } +} + +class NewScriptBlockSink extends InjectionSink { + NewScriptBlockSink() { + exists(API::Node call | + API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call and + this = call.getArgument(_).asSink() + ) + } + override string getSinkType(){ + result = "call to NewScriptBlock" + } +} + +class ExpandStringSink extends InjectionSink { + ExpandStringSink() { + exists(API::Node call | this = call.getArgument(_).asSink() | + API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("expandstring") = call or + API::getTopLevelMember("executioncontext").getMember("sessionstate").getMember("invokecommand").getMethod("expandstring") = call + + ) + } + override string getSinkType(){ + result = "call to ExpandString" + } +} \ No newline at end of file diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp similarity index 52% rename from powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp rename to powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp index f9ffbe53403..de459c2e84f 100644 --- a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp +++ b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp @@ -8,33 +8,41 @@ routine that executes a command, allows the user to execute malicious code.

+

This is a port of the InjectionHunter tool by Lee Holmes, and checks when user input is passed to any of the following:

+ + -

Possible script injection risk via the Invoke-Expression cmdlet. Untrusted input can cause arbitrary PowerShell expressions to be run. +

Possible script injection risk. Untrusted input can cause arbitrary PowerShell expressions to be run. Variables may be used directly for dynamic parameter arguments, splatting can be used for dynamic parameter names, and the invocation operator can be used for dynamic command names. If content escaping is truly needed, PowerShell has several valid quote characters, so [System.Management.Automation.Language.CodeGeneration]::Escape* should be used.

- -

The following example shows code that takes a shell script that can be changed -maliciously by a user, and passes it straight to Invoke-Expression -without examining it first.

- - - -
  • OWASP: Command Injection.
  • - - +
  • +Injection Hunter: +PowerShell Injection Hunter: Security Auditing for PowerShell Scripts. +
  • diff --git a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql new file mode 100644 index 00000000000..11730a65299 --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql @@ -0,0 +1,36 @@ +/** + * @name User Input to injection sink + * @description Finding cases where the user input is passed an dangerous method that can lead to RCE + * @kind path-problem + * @problem.severity error + * @security-severity 9.8 + * @precision high + * @id powershell/microsoft/public/user-input-to-injection-sink + * @tags security + * external/cwe/cwe-078 + * external/cwe/cwe-088 + */ + +import powershell +import semmle.code.powershell.dataflow.TaintTracking +import semmle.code.powershell.dataflow.DataFlow +import semmle.code.powershell.ApiGraphs +import semmle.code.powershell.dataflow.flowsources.FlowSources + +import Sanitizers +import Sinks + +private module InjectionConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source instanceof SourceNode + } + predicate isSink(DataFlow::Node sink) { sink instanceof InjectionSink } + predicate isBarrier(DataFlow::Node node) {node instanceof Sanitizer} +} + +module InjectionFlow = TaintTracking::Global; +import InjectionFlow::PathGraph + +from InjectionFlow::PathNode source, InjectionFlow::PathNode sink +where InjectionFlow::flowPath(source, sink) +select sink.getNode(), source, sink, "Possible injection path from user input to dangerous " + sink.getNode().(InjectionSink).getSinkType() diff --git a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected new file mode 100644 index 00000000000..8bfcc1dafec --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected @@ -0,0 +1,146 @@ +edges +| test.ps1:3:11:3:20 | UserInput | test.ps1:4:23:4:52 | Get-Process -Name $UserInput | provenance | | +| test.ps1:9:11:9:20 | UserInput | test.ps1:10:9:10:38 | Get-Process -Name $UserInput | provenance | | +| test.ps1:15:11:15:20 | UserInput | test.ps1:16:50:16:79 | Get-Process -Name $UserInput | provenance | | +| test.ps1:21:11:21:20 | UserInput | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | provenance | | +| test.ps1:21:11:21:20 | UserInput | test.ps1:22:60:22:69 | UserInput | provenance | | +| test.ps1:27:11:27:20 | UserInput | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | provenance | | +| test.ps1:27:11:27:20 | UserInput | test.ps1:28:57:28:66 | UserInput | provenance | | +| test.ps1:33:11:33:20 | UserInput | test.ps1:34:14:34:46 | public class Foo { $UserInput } | provenance | | +| test.ps1:39:11:39:20 | UserInput | test.ps1:40:30:40:62 | public class Foo { $UserInput } | provenance | | +| test.ps1:45:11:45:20 | UserInput | test.ps1:48:30:48:34 | code | provenance | | +| test.ps1:73:11:73:20 | UserInput | test.ps1:75:25:75:54 | Get-Process -Name $UserInput | provenance | | +| test.ps1:80:11:80:20 | UserInput | test.ps1:82:16:82:45 | Get-Process -Name $UserInput | provenance | | +| test.ps1:87:11:87:20 | UserInput | test.ps1:89:12:89:28 | ping $UserInput | provenance | | +| test.ps1:102:11:102:20 | UserInput | test.ps1:106:33:106:62 | Get-Process -Name $UserInput | provenance | | +| test.ps1:112:11:112:20 | UserInput | test.ps1:116:58:116:87 | Get-Process -Name $UserInput | provenance | | +| test.ps1:122:11:122:20 | UserInput | test.ps1:124:34:124:43 | UserInput | provenance | | +| test.ps1:129:11:129:20 | UserInput | test.ps1:131:28:131:37 | UserInput | provenance | | +| test.ps1:136:11:136:20 | UserInput | test.ps1:138:28:138:37 | UserInput | provenance | | +| test.ps1:165:11:165:20 | UserInput | test.ps1:168:50:168:59 | UserInput | provenance | | +| test.ps1:173:11:173:20 | UserInput | test.ps1:176:63:176:72 | UserInput | provenance | | +| test.ps1:189:11:189:20 | UserInput | test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | provenance | | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:197:46:197:51 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:198:46:198:51 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:199:46:199:51 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:200:46:200:51 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:201:46:201:51 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:202:46:202:51 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:203:46:203:51 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:204:46:204:51 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:206:48:206:53 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:207:48:207:53 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:208:48:208:53 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:210:41:210:46 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:211:41:211:46 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:212:36:212:41 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:213:36:213:41 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:214:36:214:41 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:218:42:218:47 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:219:42:219:47 | input | provenance | Src:MaD:11464 | +| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:221:33:221:38 | input | provenance | Src:MaD:11464 | +| test.ps1:197:46:197:51 | input | test.ps1:3:11:3:20 | UserInput | provenance | | +| test.ps1:198:46:198:51 | input | test.ps1:9:11:9:20 | UserInput | provenance | | +| test.ps1:199:46:199:51 | input | test.ps1:15:11:15:20 | UserInput | provenance | | +| test.ps1:200:46:200:51 | input | test.ps1:21:11:21:20 | UserInput | provenance | | +| test.ps1:201:46:201:51 | input | test.ps1:27:11:27:20 | UserInput | provenance | | +| test.ps1:202:46:202:51 | input | test.ps1:33:11:33:20 | UserInput | provenance | | +| test.ps1:203:46:203:51 | input | test.ps1:39:11:39:20 | UserInput | provenance | | +| test.ps1:204:46:204:51 | input | test.ps1:45:11:45:20 | UserInput | provenance | | +| test.ps1:206:48:206:53 | input | test.ps1:73:11:73:20 | UserInput | provenance | | +| test.ps1:207:48:207:53 | input | test.ps1:80:11:80:20 | UserInput | provenance | | +| test.ps1:208:48:208:53 | input | test.ps1:87:11:87:20 | UserInput | provenance | | +| test.ps1:210:41:210:46 | input | test.ps1:102:11:102:20 | UserInput | provenance | | +| test.ps1:211:41:211:46 | input | test.ps1:112:11:112:20 | UserInput | provenance | | +| test.ps1:212:36:212:41 | input | test.ps1:122:11:122:20 | UserInput | provenance | | +| test.ps1:213:36:213:41 | input | test.ps1:129:11:129:20 | UserInput | provenance | | +| test.ps1:214:36:214:41 | input | test.ps1:136:11:136:20 | UserInput | provenance | | +| test.ps1:218:42:218:47 | input | test.ps1:165:11:165:20 | UserInput | provenance | | +| test.ps1:219:42:219:47 | input | test.ps1:173:11:173:20 | UserInput | provenance | | +| test.ps1:221:33:221:38 | input | test.ps1:189:11:189:20 | UserInput | provenance | | +nodes +| test.ps1:3:11:3:20 | UserInput | semmle.label | UserInput | +| test.ps1:4:23:4:52 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput | +| test.ps1:9:11:9:20 | UserInput | semmle.label | UserInput | +| test.ps1:10:9:10:38 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput | +| test.ps1:15:11:15:20 | UserInput | semmle.label | UserInput | +| test.ps1:16:50:16:79 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput | +| test.ps1:21:11:21:20 | UserInput | semmle.label | UserInput | +| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput | +| test.ps1:22:60:22:69 | UserInput | semmle.label | UserInput | +| test.ps1:27:11:27:20 | UserInput | semmle.label | UserInput | +| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput | +| test.ps1:28:57:28:66 | UserInput | semmle.label | UserInput | +| test.ps1:33:11:33:20 | UserInput | semmle.label | UserInput | +| test.ps1:34:14:34:46 | public class Foo { $UserInput } | semmle.label | public class Foo { $UserInput } | +| test.ps1:39:11:39:20 | UserInput | semmle.label | UserInput | +| test.ps1:40:30:40:62 | public class Foo { $UserInput } | semmle.label | public class Foo { $UserInput } | +| test.ps1:45:11:45:20 | UserInput | semmle.label | UserInput | +| test.ps1:48:30:48:34 | code | semmle.label | code | +| test.ps1:73:11:73:20 | UserInput | semmle.label | UserInput | +| test.ps1:75:25:75:54 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput | +| test.ps1:80:11:80:20 | UserInput | semmle.label | UserInput | +| test.ps1:82:16:82:45 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput | +| test.ps1:87:11:87:20 | UserInput | semmle.label | UserInput | +| test.ps1:89:12:89:28 | ping $UserInput | semmle.label | ping $UserInput | +| test.ps1:102:11:102:20 | UserInput | semmle.label | UserInput | +| test.ps1:106:33:106:62 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput | +| test.ps1:112:11:112:20 | UserInput | semmle.label | UserInput | +| test.ps1:116:58:116:87 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput | +| test.ps1:122:11:122:20 | UserInput | semmle.label | UserInput | +| test.ps1:124:34:124:43 | UserInput | semmle.label | UserInput | +| test.ps1:129:11:129:20 | UserInput | semmle.label | UserInput | +| test.ps1:131:28:131:37 | UserInput | semmle.label | UserInput | +| test.ps1:136:11:136:20 | UserInput | semmle.label | UserInput | +| test.ps1:138:28:138:37 | UserInput | semmle.label | UserInput | +| test.ps1:165:11:165:20 | UserInput | semmle.label | UserInput | +| test.ps1:168:50:168:59 | UserInput | semmle.label | UserInput | +| test.ps1:173:11:173:20 | UserInput | semmle.label | UserInput | +| test.ps1:176:63:176:72 | UserInput | semmle.label | UserInput | +| test.ps1:189:11:189:20 | UserInput | semmle.label | UserInput | +| test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | semmle.label | Get-Process -Name "$escaped" | +| test.ps1:195:10:195:32 | Call to Read-Host | semmle.label | Call to Read-Host | +| test.ps1:197:46:197:51 | input | semmle.label | input | +| test.ps1:198:46:198:51 | input | semmle.label | input | +| test.ps1:199:46:199:51 | input | semmle.label | input | +| test.ps1:200:46:200:51 | input | semmle.label | input | +| test.ps1:201:46:201:51 | input | semmle.label | input | +| test.ps1:202:46:202:51 | input | semmle.label | input | +| test.ps1:203:46:203:51 | input | semmle.label | input | +| test.ps1:204:46:204:51 | input | semmle.label | input | +| test.ps1:206:48:206:53 | input | semmle.label | input | +| test.ps1:207:48:207:53 | input | semmle.label | input | +| test.ps1:208:48:208:53 | input | semmle.label | input | +| test.ps1:210:41:210:46 | input | semmle.label | input | +| test.ps1:211:41:211:46 | input | semmle.label | input | +| test.ps1:212:36:212:41 | input | semmle.label | input | +| test.ps1:213:36:213:41 | input | semmle.label | input | +| test.ps1:214:36:214:41 | input | semmle.label | input | +| test.ps1:218:42:218:47 | input | semmle.label | input | +| test.ps1:219:42:219:47 | input | semmle.label | input | +| test.ps1:221:33:221:38 | input | semmle.label | input | +subpaths +#select +| test.ps1:4:23:4:52 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:4:23:4:52 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke-Expression | +| test.ps1:10:9:10:38 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:10:9:10:38 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke-Expression | +| test.ps1:16:50:16:79 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:16:50:16:79 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to InvokeScript | +| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to CreateNestedPipeline | +| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke | +| test.ps1:22:60:22:69 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:22:60:22:69 | UserInput | Possible injection path from user input to dangerous call to Invoke | +| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to AddScript | +| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke | +| test.ps1:28:57:28:66 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:28:57:28:66 | UserInput | Possible injection path from user input to dangerous call to Invoke | +| test.ps1:34:14:34:46 | public class Foo { $UserInput } | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:34:14:34:46 | public class Foo { $UserInput } | Possible injection path from user input to dangerous call to Invoke-Expression | +| test.ps1:40:30:40:62 | public class Foo { $UserInput } | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:40:30:40:62 | public class Foo { $UserInput } | Possible injection path from user input to dangerous call to Invoke-Expression | +| test.ps1:48:30:48:34 | code | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:48:30:48:34 | code | Possible injection path from user input to dangerous call to Invoke-Expression | +| test.ps1:75:25:75:54 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:75:25:75:54 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Powershell | +| test.ps1:82:16:82:45 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:82:16:82:45 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Powershell | +| test.ps1:89:12:89:28 | ping $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:89:12:89:28 | ping $UserInput | Possible injection path from user input to dangerous call to Cmd | +| test.ps1:106:33:106:62 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:106:33:106:62 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to CreateScriptBlock | +| test.ps1:116:58:116:87 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:116:58:116:87 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to NewScriptBlock | +| test.ps1:124:34:124:43 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:124:34:124:43 | UserInput | Possible injection path from user input to dangerous call to ForEach-Object | +| test.ps1:131:28:131:37 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:131:28:131:37 | UserInput | Possible injection path from user input to dangerous call to Invoke | +| test.ps1:138:28:138:37 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:138:28:138:37 | UserInput | Possible injection path from user input to dangerous call to Invoke | +| test.ps1:168:50:168:59 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:168:50:168:59 | UserInput | Possible injection path from user input to dangerous call to ExpandString | +| test.ps1:176:63:176:72 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:176:63:176:72 | UserInput | Possible injection path from user input to dangerous call to ExpandString | +| test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | Possible injection path from user input to dangerous call to Invoke-Expression | diff --git a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref new file mode 100644 index 00000000000..61447f65050 --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref @@ -0,0 +1 @@ +queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql \ No newline at end of file diff --git a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1 b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1 new file mode 100644 index 00000000000..3757d9d4f2c --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1 @@ -0,0 +1,221 @@ +function Invoke-InvokeExpressionInjection1 +{ + param($UserInput) + Invoke-Expression "Get-Process -Name $UserInput" +} + +function Invoke-InvokeExpressionInjection2 +{ + param($UserInput) + iex "Get-Process -Name $UserInput" +} + +function Invoke-InvokeExpressionInjection3 +{ + param($UserInput) + $executionContext.InvokeCommand.InvokeScript("Get-Process -Name $UserInput") +} + +function Invoke-InvokeExpressionInjection4 +{ + param($UserInput) + $host.Runspace.CreateNestedPipeline("Get-Process -Name $UserInput", $false).Invoke() +} + +function Invoke-InvokeExpressionInjection5 +{ + param($UserInput) + [PowerShell]::Create().AddScript("Get-Process -Name $UserInput").Invoke() +} + +function Invoke-InvokeExpressionInjection6 +{ + param($UserInput) + Add-Type "public class Foo { $UserInput }" +} + +function Invoke-InvokeExpressionInjection7 +{ + param($UserInput) + Add-Type -TypeDefinition "public class Foo { $UserInput }" +} + +function Invoke-InvokeExpressionInjection8 +{ + param($UserInput) + + $code = "public class Foo { $UserInput }" + Add-Type -TypeDefinition $code +} + +function Invoke-InvokeExpressionInjectionFP +{ + param($UserInput) + + $code = @" + public class BasicTest + { + public static int Add(int a, int b) + { + return (a + b); + } + public int Multiply(int a, int b) + { + return (a * b); + } + } +"@ + Add-Type -TypeDefinition $code +} + +function Invoke-ExploitableCommandInjection1 +{ + param($UserInput) + + powershell -command "Get-Process -Name $UserInput" +} + +function Invoke-ExploitableCommandInjection2 +{ + param($UserInput) + + powershell "Get-Process -Name $UserInput" +} + +function Invoke-ExploitableCommandInjection3 +{ + param($UserInput) + + cmd /c "ping $UserInput" +} + +#Allowed +function Invoke-ExploitableCommandInjectionFP +{ + param($UserInput) + + cmd /c "ping localhost" +} + +function Invoke-ScriptBlockInjection1 +{ + param($UserInput) + + ## Often used when making remote connections + + $sb = [ScriptBlock]::Create("Get-Process -Name $UserInput") + Invoke-Command RemoteServer $sb +} + +function Invoke-ScriptBlockInjection2 +{ + param($UserInput) + + ## Often used when making remote connections + + $sb = $executionContext.InvokeCommand.NewScriptBlock("Get-Process -Name $UserInput") + Invoke-Command RemoteServer $sb +} + +function Invoke-MethodInjection1 +{ + param($UserInput) + + Get-Process | Foreach-Object $UserInput +} + +function Invoke-MethodInjection2 +{ + param($UserInput) + + (Get-Process -Id $pid).$UserInput() +} + +function Invoke-MethodInjection3 +{ + param($UserInput) + + (Get-Process -Id $pid).$UserInput.Invoke() +} + +#ALLOWED , uses script block +function Invoke-MethodInjectionFP1 +{ + param($UserInput) + + Get-Process | Foreach-Object { $_.Name } +} +#ALLOWED, uses constant member access +function Invoke-MethodInjectionFP2 +{ + param($UserInput) + + Get-Process | Foreach-Object "Name" +} + +function Invoke-PropertyInjection +{ + param($UserInput) + + [DateTime]::$UserInput +} + +function Invoke-ExpandStringInjection1 +{ + param($UserInput) + + ## Used to attempt a variable resolution + $executionContext.InvokeCommand.ExpandString($UserInput) +} + +function Invoke-ExpandStringInjection2 +{ + param($UserInput) + + ## Used to attempt a variable resolution + $executionContext.SessionState.InvokeCommand.ExpandString($UserInput) +} + +function Invoke-UnsafeEscape1 +{ + param($UserInput) + + $escaped = $UserInput -replace "'", "''" + Invoke-Expression "Get-Process -Name '$escaped'" +} + +function Invoke-UnsafeEscape2 +{ + param($UserInput) + + $escaped = $UserInput -replace '"', '`"' + Invoke-Expression "Get-Process -Name `"$escaped`"" +} + +$input = Read-Host "enter input" + +Invoke-InvokeExpressionInjection1 -UserInput $input +Invoke-InvokeExpressionInjection2 -UserInput $input +Invoke-InvokeExpressionInjection3 -UserInput $input +Invoke-InvokeExpressionInjection4 -UserInput $input +Invoke-InvokeExpressionInjection5 -UserInput $input +Invoke-InvokeExpressionInjection6 -UserInput $input +Invoke-InvokeExpressionInjection7 -UserInput $input +Invoke-InvokeExpressionInjection8 -UserInput $input +Invoke-InvokeExpressionInjectionFP -UserInput $input +Invoke-ExploitableCommandInjection1 -UserInput $input +Invoke-ExploitableCommandInjection2 -UserInput $input +Invoke-ExploitableCommandInjection3 -UserInput $input +Invoke-ExploitableCommandInjectionFP -UserInput $input +Invoke-ScriptBlockInjection1 -UserInput $input +Invoke-ScriptBlockInjection2 -UserInput $input +Invoke-MethodInjection1 -UserInput $input +Invoke-MethodInjection2 -UserInput $input +Invoke-MethodInjection3 -UserInput $input +Invoke-MethodInjectionFP1 -UserInput $input +Invoke-MethodInjectionFP2 -UserInput $input +Invoke-PropertyInjection -UserInput $input +Invoke-ExpandStringInjection1 -UserInput $input +Invoke-ExpandStringInjection2 -UserInput $input +Invoke-UnsafeEscape1 -UserInput $input +Invoke-UnsafeEscape2 -UserInput $input \ No newline at end of file