Skip to content

RB: add second-order-command-injection #11236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
more progress
  • Loading branch information
erik-krogh committed Jan 4, 2023
commit a00147d9dc1fb7c1c26c6843e6d9c78df2f714b0
4 changes: 2 additions & 2 deletions ruby/ql/lib/codeql/ruby/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,8 @@ class SystemCommandExecution extends DataFlow::Node instanceof SystemCommandExec
/**
* Gets an argument to this command execution that specifies the argument list
* to the command.
* TODO: Can also invlide the command.
* TODO: Look through all the `SystemCommandExecution` models.
* TODO: This list could potentially include the command itself (e.g. `git` or `hg`).
* TODO: Look through all the `SystemCommandExecution` models.
*/
DataFlow::Node getArgumentList() { result = super.getArgumentList() }

Expand Down
8 changes: 3 additions & 5 deletions ruby/ql/lib/codeql/ruby/frameworks/stdlib/Open3.qll
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ module Open3 {
Open3Call() {
this =
API::getTopLevelMember("Open3")
.getAMethodCall(["popen3", "popen2", "popen2e", "capture3", "capture2", "capture2e"]) and
super.getMethodName() = "capture3" and // TODO: Debugging.
this.getLocation().getStartLine() = 236 // TODO: Debugging.
.getAMethodCall(["popen3", "popen2", "popen2e", "capture3", "capture2", "capture2e"])
}

override DataFlow::Node getAnArgument() { result = super.getArgument(_) }
Expand All @@ -39,9 +37,9 @@ module Open3 {

override DataFlow::Node getArgumentList() {
// TODO: This is incomplete
exists(Cfg::CfgNodes::ExprNodes::UnaryOperationCfgNode un|
exists(Cfg::CfgNodes::ExprNodes::UnaryOperationCfgNode un |
un = super.getArgument(_).asExpr() and // TODO: Specific arg?
un.getOperator()= "*" and
un.getOperator() = "*" and
result.asExpr() = un.getOperand()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ module SecondOrderCommandInjection {
* E.g. `--upload-pack` for `git`.
*/
abstract string getVulnerableArgumentExample();

/** Gets the node where the shell command is executed. */
abstract DataFlow::Node getCommandExecution();
}

/**
Expand Down Expand Up @@ -95,28 +98,23 @@ module SecondOrderCommandInjection {
// step through splat expressions
t2 = t.continue() and
result.asExpr().getExpr() =
// TODO: flows-to.
usedAsArgList(t2, exec).asExpr().getExpr().(Ast::SplatExpr).getOperand()
)
}

/** Gets a dataflow node that ends up being used as an argument list to an invocation of `git` or `hg`. */
private DataFlow::LocalSourceNode usedAsVersionControlArgs(
TypeBackTracker t, DataFlow::Node argList, VulnerableCommand cmd
TypeBackTracker t, DataFlow::Node argList, VulnerableCommand cmd,
Concepts::SystemCommandExecution exec
) {
t.start() and
// TODO: untested.
exists(Concepts::SystemCommandExecution exec |
exec.getACommandArgument().getConstantValue().getStringlikeValue() = cmd
|
exec.getArgumentList() = argList and
result = argList.getALocalSource()
)
exec.getACommandArgument().getConstantValue().getStringlikeValue() = cmd and
exec.getArgumentList() = argList and
result = argList.getALocalSource()
or
// TODO: This second base-case is untested.
t.start() and
exists(
Concepts::SystemCommandExecution exec, Cfg::CfgNodes::ExprNodes::ArrayLiteralCfgNode arr
|
exists(Cfg::CfgNodes::ExprNodes::ArrayLiteralCfgNode arr |
argList = usedAsArgList(TypeBackTracker::end(), exec) and
arr = argList.asExpr() and
arr.getArgument(0).getConstantValue().getStringlikeValue() = cmd
Expand All @@ -130,14 +128,14 @@ module SecondOrderCommandInjection {
)
or
exists(TypeBackTracker t2 |
result = usedAsVersionControlArgs(t2, argList, cmd).backtrack(t2, t)
result = usedAsVersionControlArgs(t2, argList, cmd, exec).backtrack(t2, t)
or
// step through splat expressions
// step through splat expressions (TODO: untested?)
t2 = t.continue() and
result
.flowsTo(any(DataFlow::Node n |
n.asExpr().getExpr() =
usedAsVersionControlArgs(t2, argList, cmd)
usedAsVersionControlArgs(t2, argList, cmd, exec)
.asExpr()
.getExpr()
.(Ast::SplatExpr)
Expand All @@ -148,28 +146,58 @@ module SecondOrderCommandInjection {

private import codeql.ruby.dataflow.internal.DataFlowDispatch as Dispatch

class CallSink extends Sink {
VulnerableCommand cmd;
Concepts::SystemCommandExecution exec;

CallSink() {
exists(DataFlow::CallNode list |
usedAsVersionControlArgs(TypeBackTracker::end(), _, cmd, exec) = list and
list.getMethodName() = "[]" and
exists(int i, int j | i < j |
list.getArgument(i).getConstantValue().getStringlikeValue() =
cmd.getAVulnerableSubCommand() and
this = list.getArgument(j)
)
)
}

override string getCommand() { result = cmd }

override string getVulnerableArgumentExample() { result = cmd.getVulnerableArgumentExample() }

override DataFlow::Node getCommandExecution() { result = exec }
}

// TODO: THis indirect call is untested.
private class IndirectVcsCall extends DataFlow::CallNode {
VulnerableCommand cmd;
Concepts::SystemCommandExecution exec;

IndirectVcsCall() {
// TODO: This entire thing is ugly.
exists(Dispatch::DataFlowCallable calleeDis, Ast::Callable callee, DataFlow::Node list |
calleeDis =
Dispatch::viableCallable(any(Dispatch::DataFlowCall c | c.asCall() = this.asExpr())) and
calleeDis.asCallable() = callee and
list.asExpr().getExpr() = callee.getParameter(0).(Ast::SplatParameter).getDefiningAccess() and
// TODO: multiple nodes in the same position, i need to figure that out if this makes production.
usedAsVersionControlArgs(TypeBackTracker::end(), _, cmd).getLocation() = list.getLocation()
usedAsVersionControlArgs(TypeBackTracker::end(), _, cmd, exec).getLocation() =
list.getLocation()
)
}

VulnerableCommand getCommand() { result = cmd }

Concepts::SystemCommandExecution getCommandExecution() { result = exec }
}

class IndirectCallSink extends Sink {
VulnerableCommand cmd;
IndirectVcsCall call;

IndirectCallSink() {
exists(IndirectVcsCall call, int i |
exists(int i |
call.getCommand() = cmd and
call.getArgument(i).getConstantValue().getStringlikeValue() = cmd.getAVulnerableSubCommand() and
this = call.getArgument(any(int j | j > i))
Expand All @@ -179,5 +207,7 @@ module SecondOrderCommandInjection {
override string getCommand() { result = cmd }

override string getVulnerableArgumentExample() { result = cmd.getVulnerableArgumentExample() }

override DataFlow::Node getCommandExecution() { result = call.getCommandExecution() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import codeql.ruby.security.SecondOrderCommandInjectionQuery
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, Sink sinkNode
where cfg.hasFlowPath(source, sink) and sinkNode = sink.getNode()
select sink.getNode(), source, sink,
"Command line argument that depends on $@ can execute an arbitrary command if " +
"'" + sinkNode.getCommand() +
"' arguments that depends on $@, and are used in a $@, can execute an arbitrary command if " +
sinkNode.getVulnerableArgumentExample() + " is used with " + sinkNode.getCommand() + ".",
source.getNode(), source.getNode().(Source).describe()
source.getNode(), source.getNode().(Source).describe(), sinkNode.getCommandExecution(),
"shell command execution"
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy