Skip to content

Commit e9b7925

Browse files
authored
Merge pull request #141 from microsoft/powershell-cmd-injection-fewer-sinks
PS: Improve sinks in `powershell/command-injection`
2 parents 5b5f6ec + ba8a37c commit e9b7925

File tree

23 files changed

+337
-107
lines changed

23 files changed

+337
-107
lines changed

powershell/ql/lib/semmle/code/powershell/ApiGraphs.qll

Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -326,19 +326,25 @@ module API {
326326

327327
/** A node representing a module/class object with epsilon edges to its descendents. */
328328
private class ModuleNode extends Node, Impl::MkModule {
329-
/** Gets the module represented by this API node. */
330-
string getModule() { this = Impl::MkModule(result) }
329+
string qualifiedModule;
330+
int n;
331331

332-
override string toString() { result = "Module(" + this.getModule() + ")" }
332+
ModuleNode() { this = Impl::MkModule(qualifiedModule, n) }
333333

334-
TypeNode getType(string name) { result.getType() = this.getModule() + "." + name } // TODO: Check that name exists in module
335-
}
334+
ModuleNode getNext() { result = Impl::MkModule(qualifiedModule, n + 1) }
335+
336+
ModuleNode getPred() { result.getNext() = this }
337+
338+
string getComponent() { result = qualifiedModule.splitAt(".", n) }
336339

337-
private class TypeNode extends Node, Impl::MkType {
338-
/** Gets the type represented by this API node. */
339-
string getType() { this = Impl::MkType(result) }
340+
string getModule() {
341+
not exists(this.getPred()) and
342+
result = this.getComponent()
343+
or
344+
result = this.getPred().getModule() + "." + this.getComponent()
345+
}
340346

341-
override string toString() { result = "Type(" + this.getType() + ")" }
347+
override string toString() { result = "Module(" + this.getModule() + ")" }
342348
}
343349

344350
/** A node representing instances of a module/class with epsilon edges to its ancestors. */
@@ -413,13 +419,7 @@ module API {
413419
* Gets the node that represents the module with qualified
414420
* name `qualifiedModule`.
415421
*/
416-
ModuleNode mod(string qualifiedModule) { result = Impl::MkModule(qualifiedModule) }
417-
418-
/**
419-
* Gets the node that represents the type with qualified
420-
* name `qualifiedType`.
421-
*/
422-
TypeNode type(string qualifiedType) { result = Impl::MkType(qualifiedType) }
422+
ModuleNode mod(string qualifiedModule, int n) { result = Impl::MkModule(qualifiedModule, n) }
423423

424424
/**
425425
* Gets an unqualified call at the top-level with the given method name.
@@ -466,26 +466,43 @@ module API {
466466

467467
cached
468468
private module Impl {
469+
private predicate isGacModule(string s) {
470+
s =
471+
[
472+
"System.Management.Automation",
473+
"Microsoft.Management.Infrastructure",
474+
"Microsoft.PowerShell.Security",
475+
"Microsoft.PowerShell.Commands.Management",
476+
"Microsoft.PowerShell.Commands.Utility"
477+
]
478+
}
479+
480+
private predicate isModule(string s, int n) {
481+
(
482+
any(UsingStmt using).getName() = s
483+
or
484+
any(Cmd cmd).getNamespaceQualifier() = s
485+
or
486+
any(TypeNameExpr tn).getName() = s
487+
or
488+
any(ModuleManifest manifest).getModuleName() = s
489+
or
490+
isGacModule(s)
491+
) and
492+
exists(s.splitAt(".", n))
493+
}
494+
469495
cached
470496
newtype TApiNode =
471497
/** The root of the API graph. */
472498
MkRoot() or
473499
/** The method accessed at `call`, synthetically treated as a separate object. */
474500
MkMethodAccessNode(DataFlow::CallNode call) or
475-
MkModule(string qualifiedModule) {
476-
any(UsingStmt using).getName() = qualifiedModule
477-
or
478-
any(Cmd cmd).getNamespaceQualifier() = qualifiedModule
479-
or
480-
any(TypeNameExpr tn).getName() = qualifiedModule
481-
or
482-
any(ModuleManifest manifest).getModuleName() = qualifiedModule
483-
} or
484-
MkType(string qualifiedType) { any(ConstantValue cv).asString() = qualifiedType } or // TODO
501+
MkModule(string qualifiedModule, int n) { isModule(qualifiedModule, n) } or
485502
/** Instances of `mod` with epsilon edges to its ancestors. */
486-
MkInstanceUp(string qualifiedType) { exists(MkType(qualifiedType)) } or
503+
MkInstanceUp(string qualifiedType) { exists(MkModule(qualifiedType, _)) } or
487504
/** Instances of `mod` with epsilon edges to its descendents, and to its upward node. */
488-
MkInstanceDown(string qualifiedType) { exists(MkType(qualifiedType)) } or
505+
MkInstanceDown(string qualifiedType) { exists(MkModule(qualifiedType, _)) } or
489506
/** Intermediate node for following forward data flow. */
490507
MkForwardNode(DataFlow::LocalSourceNode node, TypeTracker t) { isReachable(node, t) } or
491508
/** Intermediate node for following backward data flow. */
@@ -525,14 +542,6 @@ module API {
525542
)
526543
}
527544

528-
cached
529-
predicate typeEdge(Node pred, string name, Node succ) {
530-
exists(ModuleNode mod |
531-
pred = mod and
532-
succ = mod.getType(name)
533-
)
534-
}
535-
536545
cached
537546
predicate memberEdge(Node pred, string name, Node succ) {
538547
exists(MemberExpr member | succ = getForwardStartNode(getNodeFromExpr(member)) |
@@ -546,8 +555,9 @@ module API {
546555
exists(DataFlow::CallNode call | succ = MkMethodAccessNode(call) and name = call.getName() |
547556
pred = getForwardEndNode(getALocalSourceStrict(call.getQualifier()))
548557
or
549-
exists(string qualifiedModule, ModuleManifest manifest |
550-
pred = mod(qualifiedModule) and
558+
exists(string qualifiedModule, ModuleManifest manifest, int n |
559+
pred = mod(qualifiedModule, n) and
560+
not exists(mod(qualifiedModule, n + 1)) and
551561
manifest.getModuleName() = qualifiedModule
552562
|
553563
manifest.getACmdLetToExport() = name
@@ -647,8 +657,15 @@ module API {
647657

648658
cached
649659
predicate instanceEdge(Node pred, Node succ) {
650-
// An instance of a type
651-
exists(string qualifiedType | pred = MkType(qualifiedType) |
660+
exists(string qualifiedType, int n |
661+
pred = MkModule(qualifiedType, n) and
662+
not exists(MkModule(qualifiedType, n + 1))
663+
|
664+
exists(DataFlow::TypeNameNode typeName |
665+
typeName.getTypeName() = qualifiedType and
666+
succ = getForwardStartNode(typeName)
667+
)
668+
or
652669
exists(DataFlow::ObjectCreationNode objCreation |
653670
objCreation.getConstructedTypeName() = qualifiedType and
654671
succ = getForwardStartNode(objCreation)
@@ -659,15 +676,6 @@ module API {
659676
succ = getForwardStartNode(p)
660677
)
661678
)
662-
or
663-
// A use of a module (or static type?)
664-
// TODO: Consider implicit module qualiifers and use instance on all of them
665-
exists(string qualifiedType, DataFlow::TypeNameNode typeName |
666-
pred = MkModule(qualifiedType) and
667-
typeName.getTypeName() = qualifiedType
668-
|
669-
succ = getForwardStartNode(typeName)
670-
)
671679
}
672680

673681
cached

powershell/ql/lib/semmle/code/powershell/Command.qll

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,17 @@ class Cmd extends @command, CmdBase {
3232
predicate isQualified() { parseCommandName(this, any(string s | s != ""), _) }
3333

3434
/** Gets the namespace qualifier of this command, if any. */
35-
string getNamespaceQualifier() { parseCommandName(this, result, _) }
35+
string getNamespaceQualifier() {
36+
result != "" and
37+
parseCommandName(this, result, _)
38+
or
39+
// Implicit import because it's in a module manifest
40+
parseCommandName(this, "", _) and
41+
exists(ModuleManifest manifest |
42+
manifest.getACmdLetToExport() = this.getCommandName() and
43+
result = manifest.getModuleName()
44+
)
45+
}
3646

3747
/** Gets the (possibly qualified) name of this command. */
3848
string getQualifiedCommandName() { command(this, result, _, _, _) }

powershell/ql/lib/semmle/code/powershell/Frameworks.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
* Helper file that imports all framework modeling.
33
*/
44

5+
import semmle.code.powershell.frameworks.SystemManagementAutomationRunspaces.Runspaces
6+
import semmle.code.powershell.frameworks.SystemManagementAutomationPowerShell.PowerShell
7+
import semmle.code.powershell.frameworks.SystemManagementAutomationEngineIntrinsics.EngineIntrinsics

powershell/ql/lib/semmle/code/powershell/controlflow/CfgNodes.qll

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,13 @@ module StmtNodes {
610610

611611
final override ExprCfgNode getCommand() { s.hasCfgChild(s.getCommand(), this, result) }
612612

613-
final override string getName() { result = s.getCmdName().getValue().getValue() }
613+
final override string getName() { result = s.getCommandName() }
614+
615+
/** Holds if the command is qualified. */
616+
predicate isQualified() { s.isQualified() }
617+
618+
/** Gets the namespace qualifier of this command, if any. */
619+
string getNamespaceQualifier() { result = s.getNamespaceQualifier() }
614620
}
615621

616622
/** A control-flow node that wraps a call to operator `&` */

powershell/ql/lib/semmle/code/powershell/dataflow/FlowSummary.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ private import internal.DataFlowPrivate
1313
private module Summaries {
1414
private import semmle.code.powershell.Frameworks
1515
private import semmle.code.powershell.frameworks.data.ModelsAsData
16+
import RunspaceFactory
17+
import PowerShell
18+
import EngineIntrinsics
1619
}
1720

1821
/** A callable with a flow summary, identified by a unique string. */

powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPrivate.qll

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ private module Cached {
202202
isProcessPropertyByNameNode(iter, _)
203203
} or
204204
TScriptBlockNode(ScriptBlock scriptBlock) or
205+
TTypePathNode(int n, CfgNode cfg) { isTypePathNode(_, n, cfg) } or
205206
TForbiddenRecursionGuard() {
206207
none() and
207208
// We want to prune irrelevant models before materialising data flow nodes, so types contributed
@@ -288,10 +289,24 @@ private module Cached {
288289
TypeTrackingInput::withoutContentStepImpl(_, n, _)
289290
}
290291

292+
private predicate isAutomaticVariable(Node n) {
293+
n.asExpr().(CfgNodes::ExprNodes::VarReadAccessCfgNode).getVariable().getName() =
294+
[
295+
"args", "ConsoleFileName", "EnabledExperimentalFeatures", "Error", "Event", "EventArgs",
296+
"EventSubscriber", "ExecutionContext", "HOME", "Host", "input", "IsCoreCLR", "IsLinux",
297+
"IsMacOS", "IsWindows", "LASTEXITCODE", "MyInvocation", "NestedPromptLevel", "PID",
298+
"PROFILE", "PSBoundParameters", "PSCmdlet", "PSCommandPath", "PSCulture", "PSDebugContext",
299+
"PSEdition", "PSHOME", "PSItem", "PSScriptRoot", "PSSenderInfo", "PSUICulture",
300+
"PSVersionTable", "PWD", "Sender", "ShellId", "StackTrace"
301+
]
302+
}
303+
291304
cached
292305
predicate isLocalSourceNode(Node n) {
293306
n instanceof ParameterNode
294307
or
308+
isAutomaticVariable(n)
309+
or
295310
// Expressions that can't be reached from another entry definition or expression
296311
(
297312
n instanceof ExprNode
@@ -1148,6 +1163,64 @@ class ScriptBlockNode extends TScriptBlockNode, NodeImpl {
11481163
override predicate nodeIsHidden() { any() }
11491164
}
11501165

1166+
private predicate isTypePathNode(string type, int n, CfgNode cfg) {
1167+
exists(CfgNodes::ExprNodes::TypeNameCfgNode typeName, string s |
1168+
cfg = typeName and
1169+
type = typeName.getTypeName() and
1170+
s = type.splitAt(".", n)
1171+
)
1172+
or
1173+
exists(CfgNodes::StmtNodes::CmdCfgNode cmd, string s |
1174+
cfg = cmd.getCommand() and
1175+
type = cmd.getNamespaceQualifier() and
1176+
s = type.splitAt(".", n)
1177+
)
1178+
}
1179+
1180+
/**
1181+
* A dataflow node that represents a component of a type or module path.
1182+
*
1183+
* For example, `System`, `System.Management`, `System.Management.Automation`,
1184+
* and `System.Management.Automation.PowerShell` in the type
1185+
* name `[System.Management.Automation.PowerShell]`.
1186+
*/
1187+
class TypePathNodeImpl extends TTypePathNode, NodeImpl {
1188+
int n;
1189+
CfgNode cfg;
1190+
1191+
TypePathNodeImpl() { this = TTypePathNode(n, cfg) }
1192+
1193+
string getType() { isTypePathNode(result, n, cfg) }
1194+
1195+
predicate isComplete() { not exists(this.getNext()) }
1196+
1197+
int getIndex() { result = n }
1198+
1199+
string getComponent() { result = this.getType().splitAt(".", n) }
1200+
1201+
override CfgScope getCfgScope() { result = cfg.getScope() }
1202+
1203+
override Location getLocationImpl() { result = cfg.getLocation() }
1204+
1205+
override string toStringImpl() {
1206+
not exists(this.getPrev()) and
1207+
result = this.getComponent()
1208+
or
1209+
result = this.getPrev() + "." + this.getComponent()
1210+
}
1211+
1212+
override predicate nodeIsHidden() { any() }
1213+
1214+
TypePathNodeImpl getNext() { result = TTypePathNode(n + 1, cfg) }
1215+
1216+
TypePathNodeImpl getPrev() { result.getNext() = this }
1217+
1218+
TypePathNodeImpl getConstant(string s) {
1219+
s = result.getComponent() and
1220+
result = this.getNext()
1221+
}
1222+
}
1223+
11511224
/** A node that performs a type cast. */
11521225
class CastNode extends Node {
11531226
CastNode() { none() }

powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPublic.qll

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,21 @@ class PostUpdateNode extends Node {
193193
Node getPreUpdateNode() { result = pre }
194194
}
195195

196+
/**
197+
* A dataflow node that represents a component of a type or module path.
198+
*
199+
* For example, `System`, `System.Management`, `System.Management.Automation`,
200+
* and `System.Management.Automation.PowerShell` in the type
201+
* name `[System.Management.Automation.PowerShell]`.
202+
*/
203+
class TypePathNode extends Node instanceof TypePathNodeImpl {
204+
string getComponent() { result = super.getComponent() }
205+
206+
TypePathNode getConstant(string s) { result = super.getConstant(s) }
207+
208+
API::Node track() { result = API::mod(super.getType(), super.getIndex()) }
209+
}
210+
196211
cached
197212
private module Cached {
198213
cached

powershell/ql/lib/semmle/code/powershell/frameworks/MicrosoftPowershellUtility/model.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ extensions:
33
pack: microsoft-sdl/powershell-all
44
extensible: sourceModel
55
data:
6-
- ["Microsoft.PowerShell.Utility", "Method[Read-Host].ReturnValue", "stdin"]
6+
- ["Microsoft.PowerShell.Utility!", "Method[Read-Host].ReturnValue", "stdin"]

powershell/ql/lib/semmle/code/powershell/frameworks/MicrosoftWin32Registry/model.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ extensions:
33
pack: microsoft-sdl/powershell-all
44
extensible: sourceModel
55
data:
6-
- ["Microsoft.Win32.Registry", "Method[GetValue]", "windows-registry"]
6+
- ["Microsoft.Win32.Registry!", "Method[GetValue]", "windows-registry"]

powershell/ql/lib/semmle/code/powershell/frameworks/MicrosoftWin32RegistryKey/model.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ extensions:
33
pack: microsoft-sdl/powershell-all
44
extensible: sourceModel
55
data:
6-
- ["Microsoft.Win32.RegistryKey", "Instance.Method[GetValue].ReturnValue", "windows-registry"]
7-
- ["Microsoft.Win32.RegistryKey", "Instance.Method[GetValueNames].ReturnValue", "windows-registry"]
8-
- ["Microsoft.Win32.RegistryKey", "Instance.Method[GetSubKeyNames].ReturnValue", "windows-registry"]
6+
- ["Microsoft.Win32.RegistryKey", "Method[GetValue].ReturnValue", "windows-registry"]
7+
- ["Microsoft.Win32.RegistryKey", "Method[GetValueNames].ReturnValue", "windows-registry"]
8+
- ["Microsoft.Win32.RegistryKey", "Method[GetSubKeyNames].ReturnValue", "windows-registry"]

0 commit comments

Comments
 (0)