Skip to content

Commit 656b734

Browse files
committed
initial query
1 parent b452339 commit 656b734

File tree

3 files changed

+212
-0
lines changed

3 files changed

+212
-0
lines changed

powershell/ql/src/experimental/InjectionHunter/UserInput.qll

Whitespace-only changes.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Code that passes user input directly to
7+
<code>Invoke-Expression</code>, <code>&</code>, or some other library
8+
routine that executes a command, allows the user to execute malicious
9+
code.</p>
10+
11+
</overview>
12+
<recommendation>
13+
14+
<p>Possible script injection risk via the Invoke-Expression cmdlet. Untrusted input can cause arbitrary PowerShell expressions to be run.
15+
Variables may be used directly for dynamic parameter arguments, splatting can be used for dynamic parameter names,
16+
and the invocation operator can be used for dynamic command names. If content escaping is truly needed, PowerShell has several valid quote characters,
17+
so [System.Management.Automation.Language.CodeGeneration]::Escape* should be used.</p>
18+
19+
</recommendation>
20+
<example>
21+
22+
<p>The following example shows code that takes a shell script that can be changed
23+
maliciously by a user, and passes it straight to <code>Invoke-Expression</code>
24+
without examining it first.</p>
25+
26+
<sample src="examples/command_injection.ps1" />
27+
28+
</example>
29+
<references>
30+
31+
<li>
32+
OWASP:
33+
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
34+
</li>
35+
36+
<!-- LocalWords: CWE untrusted unsanitized Runtime
37+
-->
38+
39+
</references>
40+
</qhelp>
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* @name User Input to Invoke-Expression
3+
* @description Finding cases where the user input is passed an Invoke-Expression command
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 9.8
7+
* @precision high
8+
* @id powershell/microsoft/public/user-input-to-invoke-expression
9+
* @tags security
10+
* external/cwe/cwe-078
11+
* external/cwe/cwe-088
12+
*/
13+
14+
import powershell
15+
import semmle.code.powershell.dataflow.TaintTracking
16+
import semmle.code.powershell.dataflow.DataFlow
17+
import semmle.code.powershell.ApiGraphs
18+
19+
private module TestConfig implements DataFlow::ConfigSig {
20+
predicate isSource(DataFlow::Node source) {
21+
exists(CmdCall c |
22+
c.getName() = "Read-Host" and
23+
source.asExpr().getExpr() = c) }
24+
25+
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
26+
predicate isBarrier(DataFlow::Node node) {node instanceof Sanitizer}
27+
}
28+
29+
abstract class Source extends DataFlow::Node {}
30+
31+
class ReadHostSource extends Source {
32+
ReadHostSource() {
33+
exists(CmdCall c |
34+
this.asExpr().getExpr() = c and
35+
c.getName() = "Read-Host" )
36+
}
37+
}
38+
39+
class GetContentSource extends Source {
40+
GetContentSource() {
41+
exists(CmdCall c |
42+
this.asExpr().getExpr() = c and
43+
c.getName() = "Get-Content" )
44+
}
45+
}
46+
47+
class ValueFromPipelineSource extends Source {
48+
ValueFromPipelineSource() {
49+
exists(Parameter p |
50+
p.getAnAttribute().toString() = "ValueFromPipeline" and
51+
this.asExpr().getExpr() = p.getAnAccess()
52+
)
53+
}
54+
}
55+
56+
abstract class Sink extends DataFlow::Node {}
57+
58+
class InvokeExpressionCall extends Sink {
59+
InvokeExpressionCall() {
60+
exists(CmdCall c |
61+
this.asExpr().getExpr() = c.getAnArgument() and
62+
c.getName() = ["Invoke-Expression", "iex", "Add-Type" ] )
63+
}
64+
}
65+
66+
class InvokeScriptSink extends Sink {
67+
InvokeScriptSink() {
68+
exists(InvokeMemberExpr ie |
69+
this.asExpr().getExpr() = ie.getAnArgument() and
70+
ie.getName() = "InvokeScript" and
71+
ie.getQualifier().toString() = "InvokeCommand" and
72+
ie.getQualifier().getAChild().toString() = "executioncontext"
73+
)
74+
}
75+
}
76+
77+
class CreateNestedPipelineSink extends Sink {
78+
CreateNestedPipelineSink() {
79+
exists(InvokeMemberExpr ie |
80+
this.asExpr().getExpr() = ie.getAnArgument() and
81+
ie.getName() = "CreateNestedPipeline" and
82+
ie.getQualifier().toString() = "InvokeCommand" and
83+
ie.getQualifier().getAChild().toString() = "executioncontext")
84+
}
85+
}
86+
87+
class AddScriptInvokeSink extends Sink {
88+
AddScriptInvokeSink() {
89+
exists(InvokeMemberExpr ie |
90+
this.asExpr().getExpr() = ie.getAnArgument() and
91+
ie.getName() = "AddScript" and
92+
ie.getQualifier().(InvokeMemberExpr).getName() = "Create" and
93+
ie.getQualifier().getAChild().toString() = "PowerShell" and
94+
ie.getParent().(InvokeMemberExpr).getName() = "Invoke"
95+
)
96+
}
97+
}
98+
99+
abstract class Sanitizer extends DataFlow::Node {}
100+
101+
// class TypedParameterSanitizer extends Sanitizer{
102+
// TypedParameterSanitizer() {
103+
// exists(Function f, CmdCall c, Parameter p, Argument a |
104+
// p = f.getAParameter() and
105+
// a = c.getAnArgument() and
106+
// p.getName().toLowerCase() = a.getName() and
107+
// p.getStaticType() != "Object" and
108+
// c.getName() = f.getName() and
109+
110+
// this.asExpr().getExpr() = a
111+
// )
112+
// }
113+
// }
114+
115+
class SingleQuoteSanitizer extends Sanitizer {
116+
SingleQuoteSanitizer() {
117+
exists(Expr e, VarReadAccess v |
118+
e = this.asExpr().getExpr().getParent() and
119+
e.toString().matches("%'$" + v.getVariable().getName() + "'%")
120+
)
121+
}
122+
}
123+
124+
module TestFlow = TaintTracking::Global<TestConfig>;
125+
import TestFlow::PathGraph
126+
127+
// from TestFlow::PathNode source, TestFlow::PathNode sink
128+
// where
129+
// TestFlow::flowPath(source, sink) and
130+
// sink.getNode().asExpr().getExpr().getLocation().getFile().getBaseName() = "sanitizers.ps1"
131+
// select sink.getNode(), source, sink, "Flow from user input to Invoke-Expression"
132+
133+
// from Function f, CmdCall c
134+
// where f.getLocation().getFile().getBaseName() = "sanitizers.ps1"
135+
// select f, f.getAParameter().getStaticType(), f.getAParameter().getName()
136+
137+
138+
//TBD, waiting on mathias on how to connect f and c
139+
// from Function f, CmdCall c, Parameter p, Argument a
140+
// where
141+
// p = f.getAParameter() and
142+
// a = c.getAnArgument() and
143+
// p.getName().toLowerCase() = a.getName() and
144+
// p.getStaticType() != "Object" and
145+
// c.getName() = f.getName()
146+
// select a, "argument has a specified static type"
147+
148+
// from Argument a, VarReadAccess v
149+
// where a.getAChild() = v and
150+
// v.getVariable().getName() = "UserInput"
151+
// select a, v
152+
153+
// from Argument e
154+
// where e.getLocation().getFile().getBaseName() = "sanitizers.ps1"
155+
// and e.getLocation().getStartLine() = 14
156+
// select e, e.getAChild(), e.getParent(), e.toString()
157+
158+
159+
from Parameter p
160+
where p.getLocation().getFile().getBaseName() = "userinput.ps1"
161+
// p.getAnAttribute().toString() = "ValueFromPipeline" and
162+
163+
select p, p.getName()
164+
165+
// from Expr e
166+
// where e.getLocation().getFile().getBaseName() = "userinput.ps1"
167+
// select e, e.getAQlClass()
168+
169+
// from InvokeMemberExpr ie
170+
// where
171+
// ie.getLocation().getStartLine() = 28 and ie.getName() = "AddScript"
172+
// select ie, ie.getName(), ie.getQualifier().toString(), ie.getQualifier().getAChild().toString(), ie.getParent().(InvokeMemberExpr).getName()

0 commit comments

Comments
 (0)