Merge pull request #3111 from RasmusWL/python-fabric-command-injection

Approved by BekaValentine
This commit is contained in:
semmle-qlci
2020-03-25 10:07:33 +00:00
committed by GitHub
18 changed files with 337 additions and 36 deletions

View File

@@ -15,6 +15,7 @@ Support for Django version 2.x and 3.x
| **Query** | **Expected impact** | **Change** | | **Query** | **Expected impact** | **Change** |
|----------------------------|------------------------|------------------------------------------------------------------| |----------------------------|------------------------|------------------------------------------------------------------|
| Uncontrolled command line (`py/command-line-injection`) | More results | We now model the `fabric` and `invoke` pacakges for command execution. |
### Web framework support ### Web framework support

View File

@@ -29,8 +29,7 @@ class CommandInjectionConfiguration extends TaintTracking::Configuration {
} }
override predicate isSink(TaintTracking::Sink sink) { override predicate isSink(TaintTracking::Sink sink) {
sink instanceof OsCommandFirstArgument or sink instanceof CommandSink
sink instanceof ShellCommand
} }
override predicate isExtension(TaintTracking::Extension extension) { override predicate isExtension(TaintTracking::Extension extension) {

View File

@@ -1,15 +1,17 @@
/** Provides class and predicates to track external data that /**
* Provides class and predicates to track external data that
* may represent malicious OS commands. * may represent malicious OS commands.
* *
* This module is intended to be imported into a taint-tracking query * This module is intended to be imported into a taint-tracking query
* to extend `TaintKind` and `TaintSink`. * to extend `TaintKind` and `TaintSink`.
*
*/ */
import python
import python
import semmle.python.security.TaintTracking import semmle.python.security.TaintTracking
import semmle.python.security.strings.Untrusted import semmle.python.security.strings.Untrusted
/** Abstract taint sink that is potentially vulnerable to malicious shell commands. */
abstract class CommandSink extends TaintSink { }
private ModuleObject osOrPopenModule() { private ModuleObject osOrPopenModule() {
result.getName() = "os" or result.getName() = "os" or
@@ -17,10 +19,9 @@ private ModuleObject osOrPopenModule() {
} }
private Object makeOsCall() { private Object makeOsCall() {
exists(string name | exists(string name | result = ModuleObject::named("subprocess").attr(name) |
result = ModuleObject::named("subprocess").attr(name) |
name = "Popen" or name = "Popen" or
name = "call" or name = "call" or
name = "check_call" or name = "check_call" or
name = "check_output" or name = "check_output" or
name = "run" name = "run"
@@ -29,40 +30,27 @@ private Object makeOsCall() {
/**Special case for first element in sequence. */ /**Special case for first element in sequence. */
class FirstElementKind extends TaintKind { class FirstElementKind extends TaintKind {
FirstElementKind() { this = "sequence[" + any(ExternalStringKind key) + "][0]" }
FirstElementKind() { override string repr() { result = "first item in sequence of " + this.getItem().repr() }
this = "sequence[" + any(ExternalStringKind key) + "][0]"
}
override string repr() {
result = "first item in sequence of " + this.getItem().repr()
}
/** Gets the taint kind for item in this sequence. */ /** Gets the taint kind for item in this sequence. */
ExternalStringKind getItem() { ExternalStringKind getItem() { this = "sequence[" + result + "][0]" }
this = "sequence[" + result + "][0]"
}
} }
class FirstElementFlow extends DataFlowExtension::DataFlowNode { class FirstElementFlow extends DataFlowExtension::DataFlowNode {
FirstElementFlow() { this = any(SequenceNode s).getElement(0) }
FirstElementFlow() { override ControlFlowNode getASuccessorNode(TaintKind fromkind, TaintKind tokind) {
this = any(SequenceNode s).getElement(0)
}
override
ControlFlowNode getASuccessorNode(TaintKind fromkind, TaintKind tokind) {
result.(SequenceNode).getElement(0) = this and tokind.(FirstElementKind).getItem() = fromkind result.(SequenceNode).getElement(0) = this and tokind.(FirstElementKind).getItem() = fromkind
} }
} }
/** A taint sink that is potentially vulnerable to malicious shell commands. /**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `subprocess.call(shell=vuln)` and similar calls. * The `vuln` in `subprocess.call(shell=vuln)` and similar calls.
*/ */
class ShellCommand extends TaintSink { class ShellCommand extends CommandSink {
override string toString() { result = "shell command" } override string toString() { result = "shell command" }
ShellCommand() { ShellCommand() {
@@ -75,7 +63,8 @@ class ShellCommand extends TaintSink {
or or
exists(CallNode call, string name | exists(CallNode call, string name |
call.getAnArg() = this and call.getAnArg() = this and
call.getFunction().refersTo(osOrPopenModule().attr(name)) | call.getFunction().refersTo(osOrPopenModule().attr(name))
|
name = "system" or name = "system" or
name = "popen" or name = "popen" or
name.matches("popen_") name.matches("popen_")
@@ -94,19 +83,18 @@ class ShellCommand extends TaintSink {
/* List (or tuple) containing a tainted string command */ /* List (or tuple) containing a tainted string command */
kind instanceof ExternalStringSequenceKind kind instanceof ExternalStringSequenceKind
} }
} }
/** A taint sink that is potentially vulnerable to malicious shell commands. /**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `subprocess.call(vuln, ...)` and similar calls. * The `vuln` in `subprocess.call(vuln, ...)` and similar calls.
*/ */
class OsCommandFirstArgument extends TaintSink { class OsCommandFirstArgument extends CommandSink {
override string toString() { result = "OS command first argument" } override string toString() { result = "OS command first argument" }
OsCommandFirstArgument() { OsCommandFirstArgument() {
not this instanceof ShellCommand and not this instanceof ShellCommand and
exists(CallNode call| exists(CallNode call |
call.getFunction().refersTo(makeOsCall()) and call.getFunction().refersTo(makeOsCall()) and
call.getArg(0) = this call.getArg(0) = this
) )
@@ -119,5 +107,127 @@ class OsCommandFirstArgument extends TaintSink {
/* List (or tuple) whose first element is tainted */ /* List (or tuple) whose first element is tainted */
kind instanceof FirstElementKind kind instanceof FirstElementKind
} }
}
// -------------------------------------------------------------------------- //
// Modeling of the 'invoke' package and 'fabric' package (v 2.x)
//
// Since fabric build so closely upon invoke, we model them together to avoid
// duplication
// -------------------------------------------------------------------------- //
/**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `invoke.run(vuln, ...)` and similar calls.
*/
class InvokeRun extends CommandSink {
InvokeRun() {
this = Value::named("invoke.run").(FunctionValue).getArgumentForCall(_, 0)
or
this = Value::named("invoke.sudo").(FunctionValue).getArgumentForCall(_, 0)
}
override string toString() { result = "InvokeRun" }
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
}
/**
* Internal TaintKind to track the invoke.Context instance passed to functions
* marked with @invoke.task
*/
private class InvokeContextArg extends TaintKind {
InvokeContextArg() { this = "InvokeContextArg" }
}
/** Internal TaintSource to track the context passed to functions marked with @invoke.task */
private class InvokeContextArgSource extends TaintSource {
InvokeContextArgSource() {
exists(Function f, Expr decorator |
count(f.getADecorator()) = 1 and
(
decorator = f.getADecorator() and not decorator instanceof Call
or
decorator = f.getADecorator().(Call).getFunc()
) and
(
decorator.pointsTo(Value::named("invoke.task"))
or
decorator.pointsTo(Value::named("fabric.task"))
)
|
this.(ControlFlowNode).getNode() = f.getArg(0)
)
}
override predicate isSourceOf(TaintKind kind) { kind instanceof InvokeContextArg }
}
/**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `invoke.Context().run(vuln, ...)` and similar calls.
*/
class InvokeContextRun extends CommandSink {
InvokeContextRun() {
exists(CallNode call |
any(InvokeContextArg k).taints(call.getFunction().(AttrNode).getObject("run"))
or
call = Value::named("invoke.Context").(ClassValue).lookup("run").getACall()
or
// fabric.connection.Connection is a subtype of invoke.context.Context
// since fabric.Connection.run has a decorator, it doesn't work with FunctionValue :|
// and `Value::named("fabric.Connection").(ClassValue).lookup("run").getACall()` returned no results,
// so here is the hacky solution that works :\
call.getFunction().(AttrNode).getObject("run").pointsTo().getClass() =
Value::named("fabric.Connection")
|
this = call.getArg(0)
or
this = call.getArgByName("command")
)
}
override string toString() { result = "InvokeContextRun" }
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
}
/**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `fabric.Group().run(vuln, ...)` and similar calls.
*/
class FabricGroupRun extends CommandSink {
FabricGroupRun() {
exists(ClassValue cls |
cls.getASuperType() = Value::named("fabric.Group") and
this = cls.lookup("run").(FunctionValue).getArgumentForCall(_, 1)
)
}
override string toString() { result = "FabricGroupRun" }
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
}
// -------------------------------------------------------------------------- //
// Modeling of the 'invoke' package and 'fabric' package (v 1.x)
// -------------------------------------------------------------------------- //
class FabricV1Commands extends CommandSink {
FabricV1Commands() {
// since `run` and `sudo` are decorated, we can't use FunctionValue's :(
exists(CallNode call |
call = Value::named("fabric.api.local").getACall()
or
call = Value::named("fabric.api.run").getACall()
or
call = Value::named("fabric.api.sudo").getACall()
|
this = call.getArg(0)
or
this = call.getArgByName("command")
)
}
override string toString() { result = "FabricV1Commands" }
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
} }

View File

@@ -0,0 +1,17 @@
| fabric_v1_test.py:8:7:8:28 | FabricV1Commands | externally controlled string |
| fabric_v1_test.py:9:5:9:27 | FabricV1Commands | externally controlled string |
| fabric_v1_test.py:10:6:10:38 | FabricV1Commands | externally controlled string |
| fabric_v2_test.py:10:16:10:25 | InvokeContextRun | externally controlled string |
| fabric_v2_test.py:12:15:12:36 | InvokeContextRun | externally controlled string |
| fabric_v2_test.py:16:45:16:54 | FabricGroupRun | externally controlled string |
| fabric_v2_test.py:21:10:21:13 | FabricGroupRun | externally controlled string |
| fabric_v2_test.py:31:14:31:41 | InvokeContextRun | externally controlled string |
| fabric_v2_test.py:33:15:33:64 | InvokeContextRun | externally controlled string |
| invoke_test.py:8:12:8:21 | InvokeRun | externally controlled string |
| invoke_test.py:9:20:9:40 | InvokeRun | externally controlled string |
| invoke_test.py:12:17:12:24 | InvokeRun | externally controlled string |
| invoke_test.py:13:25:13:32 | InvokeRun | externally controlled string |
| invoke_test.py:17:11:17:40 | InvokeContextRun | externally controlled string |
| invoke_test.py:21:11:21:32 | InvokeContextRun | externally controlled string |
| invoke_test.py:27:11:27:25 | InvokeContextRun | externally controlled string |
| invoke_test.py:32:11:32:25 | InvokeContextRun | externally controlled string |

View File

@@ -0,0 +1,7 @@
import python
import semmle.python.security.injection.Command
import semmle.python.security.strings.Untrusted
from CommandSink sink, TaintKind kind
where sink.sinks(kind)
select sink, kind

View File

@@ -0,0 +1,22 @@
Copyright (c) 2020 Jeff Forcier.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,10 @@
"""tests for the 'fabric' package (v1.x)
See http://docs.fabfile.org/en/1.14/tutorial.html
"""
from fabric.api import run, local, sudo
local('echo local execution')
run('echo remote execution')
sudo('echo remote execution with sudo')

View File

@@ -0,0 +1,33 @@
"""tests for the 'fabric' package (v2.x)
Most of these examples are taken from the fabric documentation: http://docs.fabfile.org/en/2.5/getting-started.html
See fabric-LICENSE for its' license.
"""
from fabric import Connection
c = Connection('web1')
result = c.run('uname -s')
c.run(command='echo run with kwargs')
from fabric import SerialGroup as Group
results = Group('web1', 'web2', 'mac1').run('uname -s')
from fabric import SerialGroup as Group
pool = Group('web1', 'web2', 'web3')
pool.run('ls')
# using the 'fab' command-line tool
from fabric import task
@task
def upload_and_unpack(c):
if c.run('test -f /opt/mydata/myfile', warn=True).failed:
c.put('myfiles.tgz', '/opt/mydata')
c.run('tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz')

View File

@@ -0,0 +1,32 @@
"""tests for the 'invoke' package
see https://www.pyinvoke.org/
"""
import invoke
invoke.run('echo run')
invoke.run(command='echo run with kwarg')
def with_sudo():
invoke.sudo('whoami')
invoke.sudo(command='whoami')
def manual_context():
c = invoke.Context()
c.run('echo run from manual context')
manual_context()
def foo_helper(c):
c.run('echo from foo_helper')
# for use with the 'invoke' command-line tool
@invoke.task
def foo(c):
# 'c' is a invoke.context.Context
c.run('echo task foo')
foo_helper(c)
@invoke.task()
def bar(c):
c.run('echo task bar')

View File

@@ -0,0 +1 @@
semmle-extractor-options: --max-import-depth=2 -p ../../../query-tests/Security/lib/

View File

@@ -0,0 +1,3 @@
from .connection import Connection
from .group import Group, SerialGroup, ThreadingGroup
from .tasks import task

View File

@@ -0,0 +1,25 @@
# For the 1.x version
def needs_host(func):
@wraps(func)
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
def local(command, capture=False, shell=None):
pass
@needs_host
def run(command, shell=True, pty=True, combine_stderr=None, quiet=False,
warn_only=False, stdout=None, stderr=None, timeout=None, shell_escape=None,
capture_buffer_size=None):
pass
@needs_host
def sudo(command, shell=True, pty=True, combine_stderr=None, user=None,
quiet=False, warn_only=False, stdout=None, stderr=None, group=None,
timeout=None, shell_escape=None, capture_buffer_size=None):
pass

View File

@@ -0,0 +1,15 @@
from invoke import Context
@decorator
def opens(method, self, *args, **kwargs):
self.open()
return method(self, *args, **kwargs)
class Connection(Context):
def open(self):
pass
@opens
def run(self, command, **kwargs):
pass

View File

@@ -0,0 +1,11 @@
class Group(list):
def run(self, *args, **kwargs):
raise NotImplementedError
class SerialGroup(Group):
def run(self, *args, **kwargs):
pass
class ThreadingGroup(Group):
def run(self, *args, **kwargs):
pass

View File

@@ -0,0 +1,2 @@
def task(*args, **kwargs):
pass

View File

@@ -0,0 +1,8 @@
from .context import Context
from .tasks import task
def run(command, **kwargs):
pass
def sudo(command, **kwargs):
pass

View File

@@ -0,0 +1,3 @@
class Context(object):
def run(self, command, **kwargs):
pass

View File

@@ -0,0 +1,2 @@
def task(*args, **kwargs):
pass