Skip to content

Commit 2c95a72

Browse files
committed
progress, that can flag CVE-2021-43809-vul
1 parent 23b1807 commit 2c95a72

File tree

9 files changed

+330
-1
lines changed

9 files changed

+330
-1
lines changed

ruby/ql/lib/codeql/ruby/Concepts.qll

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,14 @@ class SystemCommandExecution extends DataFlow::Node instanceof SystemCommandExec
713713
/** Holds if a shell interprets `arg`. */
714714
predicate isShellInterpreted(DataFlow::Node arg) { super.isShellInterpreted(arg) }
715715

716+
/**
717+
* Gets an argument to this command execution that specifies the argument list
718+
* to the command.
719+
* TODO: Can also invlide the command.
720+
* TODO: Look through all the `SystemCommandExecution` models.
721+
*/
722+
DataFlow::Node getArgumentList() { result = super.getArgumentList() }
723+
716724
/** Gets an argument to this execution that specifies the command or an argument to it. */
717725
DataFlow::Node getAnArgument() { result = super.getAnArgument() }
718726
}
@@ -733,6 +741,12 @@ module SystemCommandExecution {
733741
/** Holds if a shell interprets `arg`. */
734742
predicate isShellInterpreted(DataFlow::Node arg) { none() }
735743

744+
/**
745+
* Gets an argument to this command execution that specifies the argument list
746+
* to the command.
747+
*/
748+
DataFlow::Node getArgumentList() { none() }
749+
736750
/** Gets an argument to this execution that specifies the command. */
737751
DataFlow::Node getACommandArgument() { none() }
738752
}

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPrivate.qll

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ private class Argument extends CfgNodes::ExprCfgNode {
263263
this = call.getArgument(0) and
264264
this.getExpr() instanceof SplatExpr and
265265
arg.isSplatAll()
266+
or
267+
this = call.getArgument(0) and
268+
this.getExpr() instanceof SplatExpr and
269+
arg.isSplatAll()
266270
}
267271

268272
/** Holds if this expression is the `i`th argument of `c`. */

ruby/ql/lib/codeql/ruby/frameworks/stdlib/Open3.qll

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ module Open3 {
2020
Open3Call() {
2121
this =
2222
API::getTopLevelMember("Open3")
23-
.getAMethodCall(["popen3", "popen2", "popen2e", "capture3", "capture2", "capture2e"])
23+
.getAMethodCall(["popen3", "popen2", "popen2e", "capture3", "capture2", "capture2e"]) and
24+
super.getMethodName() = "capture3" and // TODO: Debugging.
25+
this.getLocation().getStartLine() = 236 // TODO: Debugging.
2426
}
2527

2628
override DataFlow::Node getAnArgument() { result = super.getArgument(_) }
@@ -34,6 +36,15 @@ module Open3 {
3436
override DataFlow::Node getACommandArgument() {
3537
result = Kernel::getACommandArgumentFromShellCall(this)
3638
}
39+
40+
override DataFlow::Node getArgumentList() {
41+
// TODO: This is incomplete
42+
exists(Cfg::CfgNodes::ExprNodes::UnaryOperationCfgNode un|
43+
un = super.getArgument(_).asExpr() and // TODO: Specific arg?
44+
un.getOperator()= "*" and
45+
result.asExpr() = un.getOperand()
46+
)
47+
}
3748
}
3849

3950
/**
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for reasoning about
3+
* second order command injection, as well as
4+
* extension points for adding your own.
5+
*/
6+
7+
private import ruby
8+
private import codeql.ruby.frameworks.core.Gem::Gem as Gem
9+
private import codeql.ruby.dataflow.RemoteFlowSources
10+
private import codeql.ruby.Concepts as Concepts
11+
12+
/** Classes and predicates for reasoning about second order command injection. */
13+
module SecondOrderCommandInjection {
14+
/** A shell command that allows for second order command injection. */
15+
private class VulnerableCommand extends string {
16+
VulnerableCommand() { this = ["git", "hg"] }
17+
18+
/**
19+
* Gets a vulnerable subcommand of this command.
20+
* E.g. `git` has `clone` and `pull` as vulnerable subcommands.
21+
* And every command of `hg` is vulnerable due to `--config=alias.<alias>=<command>`.
22+
*/
23+
bindingset[result]
24+
string getAVulnerableSubCommand() {
25+
this = "git" and result = ["clone", "ls-remote", "fetch", "pull"]
26+
or
27+
this = "hg" and exists(result)
28+
}
29+
30+
/** Gets an example argument that can cause this command to execute arbitrary code. */
31+
string getVulnerableArgumentExample() {
32+
this = "git" and result = "--upload-pack"
33+
or
34+
this = "hg" and result = "--config=alias.<alias>=<command>"
35+
}
36+
}
37+
38+
/** A source for second order command injection vulnerabilities. */
39+
abstract class Source extends DataFlow::Node {
40+
/** Gets a string that describes the source. For use in the alert message. */
41+
abstract string describe();
42+
}
43+
44+
/** A parameter of an exported function, seen as a source for second order command injection. */
45+
class ExternalInputSource extends Source {
46+
ExternalInputSource() { this = Gem::getALibraryInput() }
47+
48+
override string describe() { result = "library input" }
49+
}
50+
51+
/** A source of remote flow, seen as a source for second order command injection. */
52+
class RemoteFlowAsSource extends Source instanceof RemoteFlowSource {
53+
override string describe() { result = "a user-provided value" }
54+
}
55+
56+
/** A sink for second order command injection vulnerabilities. */
57+
abstract class Sink extends DataFlow::Node {
58+
/** Gets the command getting invoked. I.e. `git` or `hg`. */
59+
abstract string getCommand();
60+
61+
/**
62+
* Gets an example argument for the comand that allows for second order command injection.
63+
* E.g. `--upload-pack` for `git`.
64+
*/
65+
abstract string getVulnerableArgumentExample();
66+
}
67+
68+
/**
69+
* A sink that invokes a command described by the `VulnerableCommand` class.
70+
*/
71+
abstract class VulnerableCommandSink extends Sink {
72+
VulnerableCommand cmd;
73+
74+
override string getCommand() { result = cmd }
75+
76+
override string getVulnerableArgumentExample() { result = cmd.getVulnerableArgumentExample() }
77+
}
78+
79+
private import codeql.ruby.typetracking.TypeTracker
80+
81+
/** A sanitizer for second order command injection vulnerabilities. */
82+
abstract class Sanitizer extends DataFlow::Node { }
83+
84+
import codeql.ruby.typetracking.TypeTracker
85+
86+
private DataFlow::LocalSourceNode usedAsArgList(
87+
TypeBackTracker t, Concepts::SystemCommandExecution exec
88+
) {
89+
t.start() and
90+
result = exec.getArgumentList().getALocalSource()
91+
or
92+
exists(TypeBackTracker t2 |
93+
result = usedAsArgList(t2, exec).backtrack(t2, t)
94+
or
95+
// step through splat expressions
96+
t2 = t.continue() and
97+
result.asExpr().getExpr() =
98+
// TODO: flows-to.
99+
usedAsArgList(t2, exec).asExpr().getExpr().(Ast::SplatExpr).getOperand()
100+
)
101+
}
102+
103+
/** Gets a dataflow node that ends up being used as an argument list to an invocation of `git` or `hg`. */
104+
private DataFlow::LocalSourceNode usedAsVersionControlArgs(
105+
TypeBackTracker t, DataFlow::Node argList, VulnerableCommand cmd
106+
) {
107+
t.start() and
108+
// TODO: untested.
109+
exists(Concepts::SystemCommandExecution exec |
110+
exec.getACommandArgument().getConstantValue().getStringlikeValue() = cmd
111+
|
112+
exec.getArgumentList() = argList and
113+
result = argList.getALocalSource()
114+
)
115+
or
116+
t.start() and
117+
exists(
118+
Concepts::SystemCommandExecution exec, Cfg::CfgNodes::ExprNodes::ArrayLiteralCfgNode arr
119+
|
120+
argList = usedAsArgList(TypeBackTracker::end(), exec) and
121+
arr = argList.asExpr() and
122+
arr.getArgument(0).getConstantValue().getStringlikeValue() = cmd
123+
|
124+
result = argList.getALocalSource()
125+
or
126+
result
127+
.flowsTo(any(DataFlow::Node n |
128+
n.asExpr().getExpr() = arr.getArgument(_).getExpr().(Ast::SplatExpr).getOperand()
129+
))
130+
)
131+
or
132+
exists(TypeBackTracker t2 |
133+
result = usedAsVersionControlArgs(t2, argList, cmd).backtrack(t2, t)
134+
or
135+
// step through splat expressions
136+
t2 = t.continue() and
137+
result
138+
.flowsTo(any(DataFlow::Node n |
139+
n.asExpr().getExpr() =
140+
usedAsVersionControlArgs(t2, argList, cmd)
141+
.asExpr()
142+
.getExpr()
143+
.(Ast::SplatExpr)
144+
.getOperand()
145+
))
146+
)
147+
}
148+
149+
private import codeql.ruby.dataflow.internal.DataFlowDispatch as Dispatch
150+
151+
private class IndirectVcsCall extends DataFlow::CallNode {
152+
VulnerableCommand cmd;
153+
154+
IndirectVcsCall() {
155+
exists(Dispatch::DataFlowCallable calleeDis, Ast::Callable callee, DataFlow::Node list |
156+
calleeDis =
157+
Dispatch::viableCallable(any(Dispatch::DataFlowCall c | c.asCall() = this.asExpr())) and
158+
calleeDis.asCallable() = callee and
159+
list.asExpr().getExpr() = callee.getParameter(0).(Ast::SplatParameter).getDefiningAccess() and
160+
// TODO: multiple nodes in the same position, i need to figure that out if this makes production.
161+
usedAsVersionControlArgs(TypeBackTracker::end(), _, cmd).getLocation() = list.getLocation()
162+
)
163+
}
164+
165+
VulnerableCommand getCommand() { result = cmd }
166+
}
167+
168+
class IndirectCallSink extends Sink {
169+
VulnerableCommand cmd;
170+
171+
IndirectCallSink() {
172+
exists(IndirectVcsCall call, int i |
173+
call.getCommand() = cmd and
174+
call.getArgument(i).getConstantValue().getStringlikeValue() = cmd.getAVulnerableSubCommand() and
175+
this = call.getArgument(any(int j | j > i))
176+
)
177+
}
178+
179+
override string getCommand() { result = cmd }
180+
181+
override string getVulnerableArgumentExample() { result = cmd.getVulnerableArgumentExample() }
182+
}
183+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Provides a taint tracking configuration for reasoning about
3+
* second order command-injection vulnerabilities.
4+
*
5+
* Note, for performance reasons: only import this file if
6+
* `SecondOrderCommandInjection::Configuration` is needed, otherwise
7+
* `SecondOrderCommandInjectionCustomizations` should be imported instead.
8+
*/
9+
10+
import ruby
11+
import SecondOrderCommandInjectionCustomizations::SecondOrderCommandInjection
12+
import codeql.ruby.TaintTracking
13+
14+
/**
15+
* A taint-tracking configuration for reasoning about second order command-injection vulnerabilities.
16+
*/
17+
class Configuration extends TaintTracking::Configuration {
18+
Configuration() { this = "SecondOrderCommandInjection" }
19+
20+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
21+
22+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
23+
24+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
25+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
Some shell commands, like <code>git ls-remote</code>, can execute
8+
arbitrary commands if a user provides a malicious URL that starts with
9+
<code>--upload-pack</code>. This can be used to execute arbitrary code on
10+
the server.
11+
</p>
12+
13+
</overview>
14+
15+
<recommendation>
16+
17+
<p>
18+
Sanitize user input before passing it to the shell command. For example,
19+
ensure that URLs are valid and do not contain malicious commands.
20+
</p>
21+
22+
</recommendation>
23+
<example>
24+
25+
<p>
26+
The following example shows code that executes <code>git ls-remote</code> on a
27+
URL that can be controlled by a malicious user.
28+
</p>
29+
30+
<sample src="examples/second-order-command-injection.js" />
31+
32+
<p>
33+
The problem has been fixed in the snippet below, where the URL is validated before
34+
being passed to the shell command.
35+
</p>
36+
37+
<sample src="examples/second-order-command-injection-fixed.js" />
38+
39+
</example>
40+
<references>
41+
<li>Max Justicz: <a href="https://justi.cz/security/2021/04/20/cocoapods-rce.html">Hacking 3,000,000 apps at once through CocoaPods</a>.</li>
42+
<li>Git: <a href="https://git-scm.com/docs/git-ls-remote/2.22.0#Documentation/git-ls-remote.txt---upload-packltexecgt">Git - git-ls-remote Documentation</a>.</li>
43+
<li>OWASP: <a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.</li>
44+
45+
</references>
46+
</qhelp>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @name Second order command injection
3+
* @description Using user-controlled data as arguments to some commands, such as git clone,
4+
* can allow arbitrary commands to be executed.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @security-severity 7.0
8+
* @precision high
9+
* @id rb/second-order-command-line-injection
10+
* @tags correctness
11+
* security
12+
* external/cwe/cwe-078
13+
* external/cwe/cwe-088
14+
*/
15+
16+
import ruby
17+
import DataFlow::PathGraph
18+
import codeql.ruby.security.SecondOrderCommandInjectionQuery
19+
20+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, Sink sinkNode
21+
where cfg.hasFlowPath(source, sink) and sinkNode = sink.getNode()
22+
select sink.getNode(), source, sink,
23+
"Command line argument that depends on $@ can execute an arbitrary command if " +
24+
sinkNode.getVulnerableArgumentExample() + " is used with " + sinkNode.getCommand() + ".",
25+
source.getNode(), source.getNode().(Source).describe()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const express = require("express");
2+
const app = express();
3+
4+
const cp = require("child_process");
5+
6+
app.get("/ls-remote", (req, res) => {
7+
const remote = req.query.remote;
8+
if (!(remote.startsWith("git@") || remote.startsWith("https://"))) {
9+
throw new Error("Invalid remote: " + remote);
10+
}
11+
cp.execFile("git", ["ls-remote", remote]); // OK
12+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const express = require("express");
2+
const app = express();
3+
4+
const cp = require("child_process");
5+
6+
app.get("/ls-remote", (req, res) => {
7+
const remote = req.query.remote;
8+
cp.execFile("git", ["ls-remote", remote]); // NOT OK
9+
});

0 commit comments

Comments
 (0)
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