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 all commits
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
20 changes: 20 additions & 0 deletions ruby/ql/lib/codeql/ruby/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -707,9 +707,20 @@ deprecated module HTTP = Http;
* for instance by spawning a new process.
*/
class SystemCommandExecution extends DataFlow::Node instanceof SystemCommandExecution::Range {
/** Gets an argument to this execution that specifies the command. */
DataFlow::Node getACommandArgument() { result = super.getACommandArgument() }

/** Holds if a shell interprets `arg`. */
predicate isShellInterpreted(DataFlow::Node arg) { super.isShellInterpreted(arg) }

/**
* Gets an argument to this command execution that specifies the argument list
* to the command.
* 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() }

/** Gets an argument to this execution that specifies the command or an argument to it. */
DataFlow::Node getAnArgument() { result = super.getAnArgument() }
}
Expand All @@ -729,6 +740,15 @@ module SystemCommandExecution {

/** Holds if a shell interprets `arg`. */
predicate isShellInterpreted(DataFlow::Node arg) { none() }

/**
* Gets an argument to this command execution that specifies the argument list
* to the command.
*/
DataFlow::Node getArgumentList() { none() }

/** Gets an argument to this execution that specifies the command. */
DataFlow::Node getACommandArgument() { none() }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ private class Argument extends CfgNodes::ExprCfgNode {
this = call.getArgument(0) and
this.getExpr() instanceof SplatExpr and
arg.isSplatAll()
or
this = call.getArgument(0) and
this.getExpr() instanceof SplatExpr and
arg.isSplatAll()
}

/** Holds if this expression is the `i`th argument of `c`. */
Expand Down
6 changes: 6 additions & 0 deletions ruby/ql/lib/codeql/ruby/frameworks/PosixSpawn.qll
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ module PosixSpawn {
result = super.getArgument(_) and not result.asExpr() instanceof ExprNodes::PairCfgNode
}

override DataFlow::Node getACommandArgument() { result = super.getArgument(0) }

override predicate isShellInterpreted(DataFlow::Node arg) { none() }
}

Expand All @@ -48,6 +50,10 @@ module PosixSpawn {

override DataFlow::Node getAnArgument() { this.argument(result) }

override DataFlow::Node getACommandArgument() {
result = super.getArgument(0) and this.argument(result)
}

// From the docs:
// When only command is given and includes a space character, the command
// text is executed by the system shell interpreter.
Expand Down
5 changes: 5 additions & 0 deletions ruby/ql/lib/codeql/ruby/frameworks/core/IO.qll
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.frameworks.Files as Files
private import codeql.ruby.frameworks.core.Kernel
private import internal.IOOrFile

/** Provides modeling for the `IO` class. */
Expand Down Expand Up @@ -121,6 +122,10 @@ module IO {

override predicate isShellInterpreted(DataFlow::Node arg) { this.argument(arg, true) }

override DataFlow::Node getACommandArgument() {
result = Kernel::getACommandArgumentFromShellCall(this)
}

/**
* Holds if `arg` is an argument to this call. `shell` is true if the argument is passed to a subshell.
*/
Expand Down
31 changes: 31 additions & 0 deletions ruby/ql/lib/codeql/ruby/frameworks/core/Kernel.qll
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ module Kernel {
}
}

/**
* Gets an argument to `call` that specifies the command to execute,
* assuming that `call` has the same API as `Kernel.system`.
*/
bindingset[call]
DataFlow::Node getACommandArgumentFromShellCall(DataFlow::CallNode call) {
(
// Kernel.system invokes a subprocess if you provide a command and one or more arguments
call.getNumberOfArguments() > 1 and
result = call.getArgument([0, 1])
or
// Kernel.system invokes a subprocess if you provide an array containing the command name and argv[0]
call.getNumberOfArguments() > 1 and
result.asExpr() =
call.getArgument([0, 1]).asExpr().(CfgNodes::ExprNodes::ArrayLiteralCfgNode).getArgument(0)
) and
not result.asExpr() instanceof CfgNodes::ExprNodes::HashLiteralCfgNode // not the environment hash
}

/**
* Public methods in the `Kernel` module. These can be invoked on any object via the usual dot syntax.
* ```ruby
Expand Down Expand Up @@ -104,6 +123,10 @@ module Kernel {
// Kernel.system invokes a subshell if you provide a single string as argument
super.getNumberOfArguments() = 1 and arg = this.getAnArgument()
}

override DataFlow::Node getACommandArgument() {
result = getACommandArgumentFromShellCall(this)
}
}

/**
Expand All @@ -120,6 +143,10 @@ module Kernel {
// Kernel.exec invokes a subshell if you provide a single string as argument
super.getNumberOfArguments() = 1 and arg = this.getAnArgument()
}

override DataFlow::Node getACommandArgument() {
result = getACommandArgumentFromShellCall(this)
}
}

/**
Expand All @@ -141,6 +168,10 @@ module Kernel {
// Kernel.spawn invokes a subshell if you provide a single string as argument
super.getNumberOfArguments() = 1 and arg = this.getAnArgument()
}

override DataFlow::Node getACommandArgument() {
result = getACommandArgumentFromShellCall(this)
}
}

/**
Expand Down
18 changes: 18 additions & 0 deletions ruby/ql/lib/codeql/ruby/frameworks/stdlib/Open3.qll
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
private import ruby
private import codeql.ruby.ApiGraphs
private import codeql.ruby.Concepts
private import codeql.ruby.frameworks.core.Kernel

/**
* Provides modeling for the `Open3` library.
Expand All @@ -29,6 +30,19 @@ module Open3 {
super.getNumberOfArguments() = 1 and
arg = this.getAnArgument()
}

override DataFlow::Node getACommandArgument() {
result = Kernel::getACommandArgumentFromShellCall(this)
}

override DataFlow::Node getArgumentList() {
// TODO: This is incomplete
exists(Cfg::CfgNodes::ExprNodes::UnaryOperationCfgNode un |
un = super.getArgument(_).asExpr() and // TODO: Specific arg?
un.getOperator() = "*" and
result.asExpr() = un.getOperand()
)
}
}

/**
Expand Down Expand Up @@ -57,5 +71,9 @@ module Open3 {
arg.asExpr().getExpr() instanceof Ast::StringlikeLiteral and
arg = this.getAnArgument()
}

override DataFlow::Node getACommandArgument() {
result = Kernel::getACommandArgumentFromShellCall(this)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/**
* Provides default sources, sinks and sanitizers for reasoning about
* second order command injection, as well as
* extension points for adding your own.
*/

private import ruby
private import codeql.ruby.frameworks.core.Gem::Gem as Gem
private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.Concepts as Concepts

/** Classes and predicates for reasoning about second order command injection. */
module SecondOrderCommandInjection {
/** A shell command that allows for second order command injection. */
private class VulnerableCommand extends string {
VulnerableCommand() { this = ["git", "hg"] }

/**
* Gets a vulnerable subcommand of this command.
* E.g. `git` has `clone` and `pull` as vulnerable subcommands.
* And every command of `hg` is vulnerable due to `--config=alias.<alias>=<command>`.
*/
bindingset[result]
string getAVulnerableSubCommand() {
this = "git" and result = ["clone", "ls-remote", "fetch", "pull"]
or
this = "hg" and exists(result)
}

/** Gets an example argument that can cause this command to execute arbitrary code. */
string getVulnerableArgumentExample() {
this = "git" and result = "--upload-pack"
or
this = "hg" and result = "--config=alias.<alias>=<command>"
}
}

/** A source for second order command injection vulnerabilities. */
abstract class Source extends DataFlow::Node {
/** Gets a string that describes the source. For use in the alert message. */
abstract string describe();
}

/** A parameter of an exported function, seen as a source for second order command injection. */
class ExternalInputSource extends Source {
ExternalInputSource() { this = Gem::getALibraryInput() }

override string describe() { result = "library input" }
}

/** A source of remote flow, seen as a source for second order command injection. */
class RemoteFlowAsSource extends Source instanceof RemoteFlowSource {
override string describe() { result = "a user-provided value" }
}

/** A sink for second order command injection vulnerabilities. */
abstract class Sink extends DataFlow::Node {
/** Gets the command getting invoked. I.e. `git` or `hg`. */
abstract string getCommand();

/**
* Gets an example argument for the comand that allows for second order command injection.
* E.g. `--upload-pack` for `git`.
*/
abstract string getVulnerableArgumentExample();

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

/**
* A sink that invokes a command described by the `VulnerableCommand` class.
*/
abstract class VulnerableCommandSink extends Sink {
VulnerableCommand cmd;

override string getCommand() { result = cmd }

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

private import codeql.ruby.typetracking.TypeTracker

/** A sanitizer for second order command injection vulnerabilities. */
abstract class Sanitizer extends DataFlow::Node { }

import codeql.ruby.typetracking.TypeTracker

private DataFlow::LocalSourceNode usedAsArgList(
TypeBackTracker t, Concepts::SystemCommandExecution exec
) {
t.start() and
result = exec.getArgumentList().getALocalSource()
or
exists(TypeBackTracker t2 |
result = usedAsArgList(t2, exec).backtrack(t2, t)
or
// step through splat expressions
t2 = t.continue() and
result.asExpr().getExpr() =
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,
Concepts::SystemCommandExecution exec
) {
t.start() and
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(Cfg::CfgNodes::ExprNodes::ArrayLiteralCfgNode arr |
argList = usedAsArgList(TypeBackTracker::end(), exec) and
arr = argList.asExpr() and
arr.getArgument(0).getConstantValue().getStringlikeValue() = cmd
|
result = argList.getALocalSource()
or
result
.flowsTo(any(DataFlow::Node n |
n.asExpr().getExpr() = arr.getArgument(_).getExpr().(Ast::SplatExpr).getOperand()
))
)
or
exists(TypeBackTracker t2 |
result = usedAsVersionControlArgs(t2, argList, cmd, exec).backtrack(t2, t)
or
// step through splat expressions (TODO: untested?)
t2 = t.continue() and
result
.flowsTo(any(DataFlow::Node n |
n.asExpr().getExpr() =
usedAsVersionControlArgs(t2, argList, cmd, exec)
.asExpr()
.getExpr()
.(Ast::SplatExpr)
.getOperand()
))
)
}

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, exec).getLocation() =
list.getLocation()
)
}

VulnerableCommand getCommand() { result = cmd }

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

class IndirectCallSink extends Sink {
VulnerableCommand cmd;
IndirectVcsCall call;

IndirectCallSink() {
exists(int i |
call.getCommand() = cmd and
call.getArgument(i).getConstantValue().getStringlikeValue() = cmd.getAVulnerableSubCommand() and
this = call.getArgument(any(int j | j > i))
)
}

override string getCommand() { result = cmd }

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

override DataFlow::Node getCommandExecution() { result = call.getCommandExecution() }
}
}
Loading
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