Skip to content

Commit fe7d8ff

Browse files
authored
Merge pull request #201 from microsoft/powershell-injectionhunter-port
Powershell Command Injection query updates
2 parents c9b1356 + 12b918e commit fe7d8ff

File tree

4 files changed

+527
-15
lines changed

4 files changed

+527
-15
lines changed

powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll

Lines changed: 168 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
private import semmle.code.powershell.dataflow.DataFlow
8+
import semmle.code.powershell.ApiGraphs
89
private import semmle.code.powershell.dataflow.flowsources.FlowSources
910
private import semmle.code.powershell.Cfg
1011

@@ -20,7 +21,9 @@ module CommandInjection {
2021
/**
2122
* A data flow sink for command-injection vulnerabilities.
2223
*/
23-
abstract class Sink extends DataFlow::Node { }
24+
abstract class Sink extends DataFlow::Node {
25+
abstract string getSinkType();
26+
}
2427

2528
/**
2629
* A sanitizer for command-injection vulnerabilities.
@@ -39,13 +42,16 @@ module CommandInjection {
3942
SystemCommandExecutionSink() {
4043
// An argument to a call
4144
exists(DataFlow::CallNode call |
42-
call.getName() = "Invoke-Expression" and
45+
call.getName() = ["Invoke-Expression", "iex"] and
4346
call.getAnArgument() = this
4447
)
4548
or
4649
// Or the call command itself in case it's a use of operator &.
4750
any(DataFlow::CallOperatorNode call).getCommand() = this
4851
}
52+
override string getSinkType() {
53+
result = "call to Invoke-Expression"
54+
}
4955
}
5056

5157
class AddTypeSink extends Sink {
@@ -55,11 +61,171 @@ module CommandInjection {
5561
call.getAnArgument() = this
5662
)
5763
}
64+
override string getSinkType() {
65+
result = "call to Add-Type"
66+
}
67+
}
68+
69+
class InvokeScriptSink extends Sink {
70+
InvokeScriptSink() {
71+
exists(API::Node call |
72+
API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("invokescript") = call and
73+
this = call.getArgument(_).asSink()
74+
)
75+
}
76+
override string getSinkType(){
77+
result = "call to InvokeScript"
78+
}
79+
}
80+
81+
class CreateNestedPipelineSink extends Sink {
82+
CreateNestedPipelineSink() {
83+
exists(API::Node call |
84+
API::getTopLevelMember("host").getMember("runspace").getMethod("createnestedpipeline") = call and
85+
this = call.getArgument(_).asSink()
86+
)
87+
}
88+
override string getSinkType(){
89+
result = "call to CreateNestedPipeline"
90+
}
91+
}
92+
93+
class AddScriptInvokeSink extends Sink {
94+
AddScriptInvokeSink() {
95+
exists(InvokeMemberExpr addscript, InvokeMemberExpr create |
96+
this.asExpr().getExpr() = addscript.getAnArgument() and
97+
addscript.getName() = "AddScript" and
98+
create.getName() = "Create" and
99+
100+
addscript.getQualifier().(InvokeMemberExpr) = create and
101+
create.getQualifier().(TypeNameExpr).getName() = "PowerShell"
102+
)
103+
}
104+
override string getSinkType(){
105+
result = "call to AddScript"
58106
}
107+
}
108+
109+
class PowershellSink extends Sink {
110+
PowershellSink() {
111+
exists( CmdCall c |
112+
c.getName() = "powershell" |
113+
(
114+
this.asExpr().getExpr() = c.getArgument(1) and
115+
c.getArgument(0).getValue().asString() = "-command"
116+
) or
117+
(
118+
this.asExpr().getExpr() = c.getArgument(0)
119+
)
120+
)
121+
}
122+
override string getSinkType(){
123+
result = "call to Powershell"
124+
}
125+
}
126+
127+
class CmdSink extends Sink {
128+
CmdSink() {
129+
exists(CmdCall c |
130+
this.asExpr().getExpr() = c.getArgument(1) and
131+
c.getName() = "cmd" and
132+
c.getArgument(0).getValue().asString() = "/c"
133+
)
134+
}
135+
override string getSinkType(){
136+
result = "call to Cmd"
137+
}
138+
}
139+
140+
class ForEachObjectSink extends Sink {
141+
ForEachObjectSink() {
142+
exists(CmdCall c |
143+
this.asExpr().getExpr() = c.getAnArgument() and
144+
c.getName() = "Foreach-Object"
145+
)
146+
}
147+
override string getSinkType(){
148+
result = "call to ForEach-Object"
149+
}
150+
}
151+
152+
class InvokeSink extends Sink {
153+
InvokeSink() {
154+
exists(InvokeMemberExpr ie |
155+
this.asExpr().getExpr() = ie.getCallee() or
156+
this.asExpr().getExpr() = ie.getQualifier().getAChild*()
157+
)
158+
}
159+
override string getSinkType(){
160+
result = "call to Invoke"
161+
}
162+
}
163+
164+
class CreateScriptBlockSink extends Sink {
165+
CreateScriptBlockSink() {
166+
exists(InvokeMemberExpr ie |
167+
this.asExpr().getExpr() = ie.getAnArgument() and
168+
ie.getName() = "Create" and
169+
ie.getQualifier().(TypeNameExpr).getName() = "ScriptBlock"
170+
)
171+
}
172+
override string getSinkType(){
173+
result = "call to CreateScriptBlock"
174+
}
175+
}
176+
177+
class NewScriptBlockSink extends Sink {
178+
NewScriptBlockSink() {
179+
exists(API::Node call |
180+
API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call and
181+
this = call.getArgument(_).asSink()
182+
)
183+
}
184+
override string getSinkType(){
185+
result = "call to NewScriptBlock"
186+
}
187+
}
188+
189+
class ExpandStringSink extends Sink {
190+
ExpandStringSink() {
191+
exists(API::Node call | this = call.getArgument(_).asSink() |
192+
API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("expandstring") = call or
193+
API::getTopLevelMember("executioncontext").getMember("sessionstate").getMember("invokecommand").getMethod("expandstring") = call
194+
195+
)
196+
}
197+
override string getSinkType(){
198+
result = "call to ExpandString"
199+
}
200+
}
59201

60202
private class ExternalCommandInjectionSink extends Sink {
61203
ExternalCommandInjectionSink() {
62204
this = ModelOutput::getASinkNode("command-injection").asSink()
63205
}
206+
override string getSinkType() {
207+
result = "external command injection"
208+
}
209+
}
210+
211+
class TypedParameterSanitizer extends Sanitizer {
212+
TypedParameterSanitizer() {
213+
exists(Function f, Parameter p |
214+
p = f.getAParameter() and
215+
p.getStaticType() != "Object" and
216+
this.asParameter() = p
217+
)
218+
}
219+
}
220+
221+
class SingleQuoteSanitizer extends Sanitizer {
222+
SingleQuoteSanitizer() {
223+
exists(ExpandableStringExpr e, VarReadAccess v |
224+
v = this.asExpr().getExpr() and
225+
e.getUnexpandedValue().matches("%'$" + v.getVariable().getName() + "'%") and
226+
e.getAnExpr() = v
227+
)
228+
}
64229
}
65230
}
231+

powershell/ql/src/queries/security/cwe-078/CommandInjection.qhelp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@
88
routine that executes a command, allows the user to execute malicious
99
code.</p>
1010

11+
<p>The following are considered dangerous sinks: </p>
12+
<ul>
13+
<li>Invoke-Expression</li>
14+
<li>InvokeScript</li>
15+
<li>CreateNestedPipeline</li>
16+
<li>AddScript</li>
17+
<li>powershell</li>
18+
<li>cmd</li>
19+
<li>Foreach-Object</li>
20+
<li>Invoke</li>
21+
<li>CreateScriptBlock</li>
22+
<li>NewScriptBlock</li>
23+
<li>ExpandString</li>
24+
</ul>
25+
1126
</overview>
1227
<recommendation>
1328

@@ -36,7 +51,10 @@ without examining it first.</p>
3651
OWASP:
3752
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
3853
</li>
39-
54+
<li>
55+
Injection Hunter:
56+
<a href="https://devblogs.microsoft.com/powershell/powershell-injection-hunter-security-auditing-for-powershell-scripts/">PowerShell Injection Hunter: Security Auditing for PowerShell Scripts</a>.
57+
</li>
4058
<!-- LocalWords: CWE untrusted unsanitized Runtime
4159
-->
4260

0 commit comments

Comments
 (0)