Merge pull request #15715 from am0o0/am0o0-python-codeExec

Python: New command execution sinks
This commit is contained in:
Taus
2024-06-21 14:26:33 +02:00
committed by GitHub
51 changed files with 1272 additions and 153 deletions

View File

@@ -35,6 +35,7 @@ private import semmle.python.frameworks.Idna
private import semmle.python.frameworks.Invoke
private import semmle.python.frameworks.Jmespath
private import semmle.python.frameworks.Joblib
private import semmle.python.frameworks.JsonPickle
private import semmle.python.frameworks.Ldap
private import semmle.python.frameworks.Ldap3
private import semmle.python.frameworks.Libtaxii
@@ -48,7 +49,9 @@ private import semmle.python.frameworks.Numpy
private import semmle.python.frameworks.Opml
private import semmle.python.frameworks.Oracledb
private import semmle.python.frameworks.Pandas
private import semmle.python.frameworks.Paramiko
private import semmle.python.frameworks.Peewee
private import semmle.python.frameworks.Pexpect
private import semmle.python.frameworks.Phoenixdb
private import semmle.python.frameworks.Psycopg
private import semmle.python.frameworks.Psycopg2
@@ -71,6 +74,7 @@ private import semmle.python.frameworks.SqlAlchemy
private import semmle.python.frameworks.Starlette
private import semmle.python.frameworks.Stdlib
private import semmle.python.frameworks.Toml
private import semmle.python.frameworks.Torch
private import semmle.python.frameworks.Tornado
private import semmle.python.frameworks.Twisted
private import semmle.python.frameworks.Ujson

View File

@@ -71,7 +71,7 @@ private module FabricV1 {
* Provides classes modeling security-relevant aspects of the `fabric` PyPI package, for
* version 2.x.
*
* See http://docs.fabfile.org/en/2.5/getting-st arted.html.
* See http://docs.fabfile.org/en/2.5/getting-started.html.
*/
module FabricV2 {
/** Gets a reference to the `fabric` module. */
@@ -93,7 +93,9 @@ module FabricV2 {
* See https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.
*/
module ConnectionClass {
/** Gets a reference to the `fabric.connection.Connection` class. */
/**
* Gets a reference to the `fabric.connection.Connection` class.
*/
API::Node classRef() {
result = fabric().getMember("Connection")
or
@@ -109,40 +111,16 @@ module FabricV2 {
* This can include instantiations of the class, return values from function
* calls, or a special parameter that will be set when functions are called by an external
* library.
*
* Use the predicate `Connection::instance()` to get references to instances of `fabric.connection.Connection`.
*/
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
ClassInstantiation() { this = classRef().getACall() }
abstract class Instance extends API::Node {
override string toString() { result = this.(API::Node).toString() }
}
/** Gets a reference to an instance of `fabric.connection.Connection`. */
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
t.start() and
result instanceof InstanceSource
or
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
}
/** Gets a reference to an instance of `fabric.connection.Connection`. */
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
/**
* Gets a reference to either `run`, `sudo`, or `local` method on a
* `fabric.connection.Connection` instance.
*
* See
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.run
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.sudo
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
* A reference to the `fabric.connection.Connection` class.
*/
private DataFlow::TypeTrackingNode instanceRunMethods(DataFlow::TypeTracker t) {
t.startInAttr(["run", "sudo", "local"]) and
result = instance()
or
exists(DataFlow::TypeTracker t2 | result = instanceRunMethods(t2).track(t2, t))
class ClassInstantiation extends Instance {
ClassInstantiation() { this = classRef().getReturn() }
}
/**
@@ -154,8 +132,8 @@ module FabricV2 {
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.sudo
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
*/
DataFlow::Node instanceRunMethods() {
instanceRunMethods(DataFlow::TypeTracker::end()).flowsTo(result)
API::CallNode instanceRunMethods() {
result = any(Instance is).getMember(["run", "sudo", "local"]).getACall()
}
}
}
@@ -168,19 +146,38 @@ module FabricV2 {
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
*/
private class FabricConnectionRunSudoLocalCall extends SystemCommandExecution::Range,
DataFlow::CallCfgNode
API::CallNode
{
FabricConnectionRunSudoLocalCall() {
this.getFunction() = Fabric::Connection::ConnectionClass::instanceRunMethods()
this = Fabric::Connection::ConnectionClass::instanceRunMethods()
}
override DataFlow::Node getCommand() {
result = [this.getArg(0), this.getArgByName("command")]
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
}
/**
* A `gateway` parameter of `fabric.connection.Connection` instance is considered as ssh proxy_command option and can execute command.
* See https://docs.fabfile.org/en/latest/api/connection.html#fabric.connection.Connection
*/
private class FabricConnectionProxyCommand extends SystemCommandExecution::Range, API::CallNode {
FabricConnectionProxyCommand() {
this = Fabric::Connection::ConnectionClass::classRef().getACall() and
// we want to make sure that the connection is established otherwise the command of proxy_command won't run.
exists(
this.getAMethodCall([
"run", "get", "sudo", "open_gateway", "open", "create_session", "forward_local",
"forward_remote", "put", "shell", "sftp"
])
)
}
override DataFlow::Node getCommand() { result = this.getParameter(4, "gateway").asSink() }
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
}
// -------------------------------------------------------------------------
// fabric.tasks
// -------------------------------------------------------------------------
@@ -193,14 +190,10 @@ module FabricV2 {
API::Node task() { result in [tasks().getMember("task"), fabric().getMember("task")] }
}
class FabricTaskFirstParamConnectionInstance extends Fabric::Connection::ConnectionClass::InstanceSource,
DataFlow::ParameterNode
class FabricTaskFirstParamConnectionInstance extends Fabric::Connection::ConnectionClass::Instance
{
FabricTaskFirstParamConnectionInstance() {
exists(Function func |
func.getADecorator() = Fabric::Tasks::task().getAValueReachableFromSource().asExpr() and
this.getParameter() = func.getArg(0)
)
this = Fabric::Tasks::task().getParameter(0).getParameter(0)
}
}
@@ -242,11 +235,14 @@ module FabricV2 {
API::Node subclassInstance() { result = any(ModeledSubclass m).getASubclass*().getReturn() }
/**
* Gets a reference to the `run` method on an instance of a subclass of `fabric.group.Group`.
* Gets a reference to the `run` and `sudo` methods on an instance of a subclass of `fabric.group.Group`.
*
* See https://docs.fabfile.org/en/2.5/api/group.html#fabric.group.Group.run
* See https://docs.fabfile.org/en/latest/api/group.html#fabric.group.Group.sudo
*/
API::Node subclassInstanceRunMethod() { result = subclassInstance().getMember("run") }
API::Node subclassInstanceRunMethod() {
result = subclassInstance().getMember(["run", "sudo"])
}
}
/**
@@ -254,14 +250,12 @@ module FabricV2 {
*
* See https://docs.fabfile.org/en/2.5/api/group.html#fabric.group.Group.run
*/
private class FabricGroupRunCall extends SystemCommandExecution::Range, DataFlow::CallCfgNode {
private class FabricGroupRunCall extends SystemCommandExecution::Range, API::CallNode {
FabricGroupRunCall() {
this = Fabric::Group::GroupClass::subclassInstanceRunMethod().getACall()
}
override DataFlow::Node getCommand() {
result = [this.getArg(0), this.getArgByName("command")]
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
}

View File

@@ -0,0 +1,31 @@
/**
* Provides classes modeling security-relevant aspects of the `jsonpickle` PyPI package.
* See https://pypi.org/project/jsonpickle/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
/**
* Provides models for the `jsonpickle` PyPI package.
* See https://pypi.org/project/jsonpickle/.
*/
private module Jsonpickle {
/**
* A Call to `jsonpickle.decode`.
* See https://jsonpickle.readthedocs.io/en/latest/api.html#jsonpickle.decode
*/
private class JsonpickleDecode extends Decoding::Range, API::CallNode {
JsonpickleDecode() { this = API::moduleImport("jsonpickle").getMember("decode").getACall() }
override predicate mayExecuteInput() { any() }
override DataFlow::Node getAnInput() { result = this.getParameter(0, "string").asSink() }
override DataFlow::Node getOutput() { result = this }
override string getFormat() { result = "pickle" }
}
}

View File

@@ -34,4 +34,121 @@ private module Pandas {
override string getFormat() { result = "pickle" }
}
/**
* Provides security related models for `pandas.DataFrame`.
* See https://pandas.pydata.org/docs/reference/frame.html
*/
module DataFrame {
/**
* A `pandas.DataFrame` Object.
*
* Extend this class to model new APIs.
* See https://pandas.pydata.org/docs/reference/frame.html
*/
abstract class DataFrame extends API::Node {
override string toString() { result = this.(API::Node).toString() }
}
/**
* A `pandas.DataFrame` instantiation.
* See https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html
*/
class DataFrameConstructor extends DataFrame {
DataFrameConstructor() {
this = API::moduleImport("pandas").getMember("DataFrame").getReturn()
}
}
/**
* The `pandas.read_*` functions that return a `pandas.DataFrame`.
* See https://pandas.pydata.org/docs/reference/io.html
*/
class InputRead extends DataFrame {
InputRead() {
this =
API::moduleImport("pandas")
.getMember([
"read_csv", "read_fwf", "read_pickle", "read_table", "read_clipboard",
"read_excel", "read_xml", "read_parquet", "read_orc", "read_spss",
"read_sql_table", "read_sql_query", "read_sql", "read_gbq", "read_stata"
])
.getReturn()
or
this = API::moduleImport("pandas").getMember("read_html").getReturn().getASubscript()
or
exists(API::Node readSas, API::CallNode readSasCall |
readSas = API::moduleImport("pandas").getMember("read_sas") and
this = readSas.getReturn() and
readSasCall = readSas.getACall()
|
// Returns DataFrame if iterator=False and chunksize=None, Also with default values it returns DataFrame.
(
not readSasCall.getParameter(5, "iterator").asSink().asExpr().(BooleanLiteral)
instanceof True
or
not exists(readSasCall.getParameter(5, "iterator").asSink())
) and
not exists(
readSasCall.getParameter(4, "chunksize").asSink().asExpr().(IntegerLiteral).getN()
)
)
}
}
/**
* The `pandas.DataFrame.*` methods that return a `pandas.DataFrame` object.
* See https://pandas.pydata.org/docs/reference/io.html
*/
class DataFrameMethods extends DataFrame {
DataFrameMethods() {
this =
any(DataFrame df)
.getMember([
"copy", "from_records", "from_dict", "from_spmatrix", "assign", "select_dtypes",
"set_flags", "astype", "infer_objects", "head", "xs", "get", "isin", "where",
"mask", "query", "add", "mul", "truediv", "mod", "pow", "dot", "radd", "rsub",
"rdiv", "rfloordiv", "rtruediv", "rpow", "lt", "gt", "le", "ne", "agg", "combine",
"apply", "aggregate", "transform", "all", "any", "clip", "corr", "cov", "cummax",
"cummin", "cumprod", "describe", "mode", "pct_change", "quantile", "rank",
"round", "sem", "add_prefix", "add_suffix", "at_time", "between_time", "drop",
"drop_duplicates", "filter", "first", "head", "idxmin", "last", "reindex",
"reindex_like", "reset_index", "sample", "set_axis", "tail", "take", "truncate",
"bfill", "dropna", "ffill", "fillna", "interpolate", "isna", "isnull", "notna",
"notnull", "pad", "replace", "droplevel", "pivot", "pivot_table",
"reorder_levels", "sort_values", "sort_index", "nlargest", "nsmallest",
"swaplevel", "stack", "unstack", "isnull", "notna", "notnull", "replace",
"droplevel", "pivot", "pivot_table", "reorder_levels", "sort_values",
"sort_index", "nlargest", "nsmallest", "swaplevel", "stack", "unstack", "melt",
"explode", "squeeze", "T", "transpose", "compare", "join", "from_spmatrix",
"shift", "asof", "merge", "from_dict", "tz_convert", "to_period", "asfreq",
"to_dense", "tz_localize", "box", "__dataframe__"
])
.getReturn()
}
}
}
/**
* A Call to `pandas.DataFrame.query` or `pandas.DataFrame.eval`.
* See https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.query.html
* https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.eval.html
*/
class CodeExecutionCall extends CodeExecution::Range, API::CallNode {
CodeExecutionCall() {
this = any(DataFrame::DataFrame df).getMember(["query", "eval"]).getACall()
}
override DataFlow::Node getCode() { result = this.getParameter(0, "expr").asSink() }
}
/**
* A Call to `pandas.eval`.
* See https://pandas.pydata.org/docs/reference/api/pandas.eval.html
*/
class PandasEval extends CodeExecution::Range, API::CallNode {
PandasEval() { this = API::moduleImport("pandas").getMember("eval").getACall() }
override DataFlow::Node getCode() { result = this.getParameter(0, "expr").asSink() }
}
}

View File

@@ -0,0 +1,32 @@
/**
* Provides classes modeling security-relevant aspects of the `paramiko` PyPI package.
* See https://pypi.org/project/paramiko/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
/**
* Provides models for the `paramiko` PyPI package.
* See https://pypi.org/project/paramiko/.
*/
private module Paramiko {
/**
* The first argument of `paramiko.ProxyCommand`.
*
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
* which runs `CMD` on the local system.
* See https://paramiko.pydata.org/docs/reference/api/paramiko.eval.html
*/
class ParamikoProxyCommand extends SystemCommandExecution::Range, API::CallNode {
ParamikoProxyCommand() {
this = API::moduleImport("paramiko").getMember("ProxyCommand").getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "command_line").asSink() }
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
}
}

View File

@@ -0,0 +1,45 @@
/**
* Provides classes modeling security-relevant aspects of the `pexpect` PyPI package.
* See https://pypi.org/project/pexpect/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
/**
* Provides models for the `pexpect` PyPI package.
* See https://pypi.org/project/pexpect/.
*/
private module Pexpect {
/**
* The calls to `pexpect.*` functions that execute commands
* See https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn
* See https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.run
*/
class PexpectCommandExec extends SystemCommandExecution::Range, API::CallNode {
PexpectCommandExec() {
this = API::moduleImport("pexpect").getMember(["run", "runu", "spawn", "spawnu"]).getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
}
/**
* A call to `pexpect.popen_spawn.PopenSpawn`
* See https://pexpect.readthedocs.io/en/stable/api/popen_spawn.html#pexpect.popen_spawn.PopenSpawn
*/
class PexpectPopenSpawn extends SystemCommandExecution::Range, API::CallNode {
PexpectPopenSpawn() {
this =
API::moduleImport("pexpect").getMember("popen_spawn").getMember("PopenSpawn").getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "cmd").asSink() }
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
}
}

View File

@@ -0,0 +1,72 @@
/**
* Provides classes modeling security-relevant aspects of the `torch` PyPI package.
* See https://pypi.org/project/torch/.
*/
private import python
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
/**
* Provides models for the `torch` PyPI package.
* See https://pypi.org/project/torch/.
*/
private module Torch {
/**
* A call to `torch.load`
* See https://pytorch.org/docs/stable/generated/torch.load.html#torch.load
*/
private class TorchLoadCall extends Decoding::Range, API::CallNode {
TorchLoadCall() { this = API::moduleImport("torch").getMember("load").getACall() }
override predicate mayExecuteInput() {
not exists(this.getParameter(2, "pickle_module").asSink()) or
this.getParameter(2, "pickle_module").asSink().asExpr() instanceof None
}
override DataFlow::Node getAnInput() { result = this.getParameter(0, "f").asSink() }
override DataFlow::Node getOutput() { result = this }
override string getFormat() { result = "pickle" }
}
/**
* A call to `torch.package.PackageImporter`
* See https://pytorch.org/docs/stable/package.html#torch.package.PackageImporter
*/
private class TorchPackageImporter extends Decoding::Range, API::CallNode {
TorchPackageImporter() {
this = API::moduleImport("torch").getMember("package").getMember("PackageImporter").getACall() and
exists(this.getAMethodCall("load_pickle"))
}
override predicate mayExecuteInput() { any() }
override DataFlow::Node getAnInput() {
result = this.getParameter(0, "file_or_buffer").asSink()
}
override DataFlow::Node getOutput() { result = this.getAMethodCall("load_pickle") }
override string getFormat() { result = "pickle" }
}
/**
* A call to `torch.jit.load`
* See https://pytorch.org/docs/stable/generated/torch.jit.load.html#torch.jit.load
*/
private class TorchJitLoad extends Decoding::Range, API::CallNode {
TorchJitLoad() {
this = API::moduleImport("torch").getMember("jit").getMember("load").getACall()
}
override predicate mayExecuteInput() { any() }
override DataFlow::Node getAnInput() { result = this.getParameter(0, "f").asSink() }
override DataFlow::Node getOutput() { result = this }
override string getFormat() { result = "pickle" }
}
}

View File

@@ -1,17 +0,0 @@
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
<qhelp>
<overview>
<p>
Processing an unvalidated user input can allow an attacker to inject arbitrary command in your local and remote servers when creating a ssh connection.
</p>
</overview>
<recommendation>
<p>
This vulnerability can be prevented by not allowing untrusted user input to be passed as ProxyCommand or exec_command.
</p>
</recommendation>
<example>
<p>In the example below, the ProxyCommand and exec_command are controlled by the user and hence leads to a vulnerability.</p>
<sample src="paramikoBad.py" />
</example>
</qhelp>

View File

@@ -1,59 +0,0 @@
/**
* @name RCE with user provided command with paramiko ssh client
* @description user provided command can lead to execute code on a external server that can be belong to other users or admins
* @kind path-problem
* @problem.severity error
* @security-severity 9.3
* @precision high
* @id py/paramiko-command-injection
* @tags security
* experimental
* external/cwe/cwe-074
*/
import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.ApiGraphs
private API::Node paramikoClient() {
result = API::moduleImport("paramiko").getMember("SSHClient").getReturn()
}
private module ParamikoConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
/**
* exec_command of `paramiko.SSHClient` class execute command on ssh target server
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
* and it run CMD on current system that running the ssh command
* the Sink related to proxy command is the `connect` method of `paramiko.SSHClient` class
*/
predicate isSink(DataFlow::Node sink) {
sink = paramikoClient().getMember("exec_command").getACall().getParameter(0, "command").asSink()
or
sink = paramikoClient().getMember("connect").getACall().getParameter(11, "sock").asSink()
}
/**
* this additional taint step help taint tracking to find the vulnerable `connect` method of `paramiko.SSHClient` class
*/
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
exists(API::CallNode call |
call = API::moduleImport("paramiko").getMember("ProxyCommand").getACall() and
nodeFrom = call.getParameter(0, "command_line").asSink() and
nodeTo = call
)
}
}
/** Global taint-tracking for detecting "paramiko command injection" vulnerabilities. */
module ParamikoFlow = TaintTracking::Global<ParamikoConfig>;
import ParamikoFlow::PathGraph
from ParamikoFlow::PathNode source, ParamikoFlow::PathNode sink
where ParamikoFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(),
"a user-provided value"

View File

@@ -0,0 +1,22 @@
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
<qhelp>
<overview>
<p>
Allowing users to execute arbitrary commands using an SSH connection on a remote server can lead to security issues unless you implement proper authorization.
</p>
<p>
Assume that you connect to a remote system via SSH connection from your main or local server that accepts user-controlled data and has interaction with users that you don't trust, passing these data to SSH API as a part of a command that will be executed on a secondary remote server can lead to security issues. You should consider proper authorization rules very carefully.
</p>
</overview>
<recommendation>
<p>
This vulnerability can be prevented by implementing proper authorization rules for untrusted user input that can be passed to your secondary servers.
</p>
</recommendation>
<example>
<p>In the example below, the exec_command is controlled by the user and hence leads to a vulnerability.</p>
<sample src="paramikoBad.py" />
<p>In the example below, the exec_command is controlled by the an Authorized user and hence it is safe.</p>
<sample src="paramikoGood.py" />
</example>
</qhelp>

View File

@@ -0,0 +1,21 @@
/**
* @name Command execution on a secondary remote server
* @description user provided command can lead to execute code on a external server that can be belong to other users or admins
* @kind path-problem
* @problem.severity error
* @security-severity 9.3
* @precision high
* @id py/paramiko-command-injection
* @tags security
* experimental
* external/cwe/cwe-074
*/
import python
import experimental.semmle.python.security.RemoteCommandExecution
import RemoteCommandExecutionFlow::PathGraph
from RemoteCommandExecutionFlow::PathNode source, RemoteCommandExecutionFlow::PathNode sink
where RemoteCommandExecutionFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(),
"a user-provided value"

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python
from flask import request, Flask
import paramiko
from paramiko import SSHClient
app = Flask(__name__)
paramiko_ssh_client = SSHClient()
paramiko_ssh_client.load_system_host_keys()
paramiko_ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_name", pkey="k", timeout=11, banner_timeout=200)
@app.route('/external_exec_command_1')
def withoutAuthorization():
user_cmd = request.args.get('command')
stdin, stdout, stderr = paramiko_ssh_client.exec_command(user_cmd)
return stdout
if __name__ == '__main__':
app.debug = False
app.run()

View File

@@ -12,23 +12,14 @@ paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_
@app.route('/external_exec_command_1')
def bad1():
def withAuthorization():
user_cmd = request.args.get('command')
auth_jwt = request.args.get('Auth')
# validating jwt token first
# .... then continue to run the command
stdin, stdout, stderr = paramiko_ssh_client.exec_command(user_cmd)
return stdout
@app.route('/external_exec_command_2')
def bad2():
user_cmd = request.args.get('command')
stdin, stdout, stderr = paramiko_ssh_client.exec_command(command=user_cmd)
return stdout
@app.route('/proxycommand')
def bad2():
user_cmd = request.args.get('command')
stdin, stdout, stderr = paramiko_ssh_client.connect('hostname', username='user',password='yourpassword',sock=paramiko.ProxyCommand(user_cmd))
return stdout
if __name__ == '__main__':
app.debug = False

View File

@@ -15,6 +15,39 @@ private import semmle.python.dataflow.new.TaintTracking
private import experimental.semmle.python.Frameworks
private import semmle.python.Concepts
/**
* A data-flow node that executes an operating system command,
* on a remote server likely by SSH connections.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `RemoteCommandExecution::Range` instead.
*/
class RemoteCommandExecution extends DataFlow::Node instanceof RemoteCommandExecution::Range {
/** Holds if a shell interprets `arg`. */
predicate isShellInterpreted(DataFlow::Node arg) { super.isShellInterpreted(arg) }
/** Gets the argument that specifies the command to be executed. */
DataFlow::Node getCommand() { result = super.getCommand() }
}
/** Provides classes for modeling new remote server command execution APIs. */
module RemoteCommandExecution {
/**
* A data-flow node that executes an operating system command,
* on a remote server likely by SSH connections.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `RemoteCommandExecution` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the command to be executed. */
abstract DataFlow::Node getCommand();
/** Holds if a shell interprets `arg`. */
predicate isShellInterpreted(DataFlow::Node arg) { none() }
}
}
/** Provides classes for modeling copying file related APIs. */
module CopyFile {
/**

View File

@@ -2,10 +2,16 @@
* Helper file that imports all framework modeling.
*/
private import experimental.semmle.python.frameworks.AsyncSsh
private import experimental.semmle.python.frameworks.Stdlib
private import experimental.semmle.python.frameworks.Flask
private import experimental.semmle.python.frameworks.Django
private import experimental.semmle.python.frameworks.LDAP
private import experimental.semmle.python.frameworks.Netmiko
private import experimental.semmle.python.frameworks.Paramiko
private import experimental.semmle.python.frameworks.Pexpect
private import experimental.semmle.python.frameworks.Scrapli
private import experimental.semmle.python.frameworks.Twisted
private import experimental.semmle.python.frameworks.JWT
private import experimental.semmle.python.frameworks.Csv
private import experimental.semmle.python.libraries.PyJWT
@@ -14,5 +20,6 @@ private import experimental.semmle.python.libraries.Authlib
private import experimental.semmle.python.libraries.PythonJose
private import experimental.semmle.python.frameworks.CopyFile
private import experimental.semmle.python.frameworks.Sendgrid
private import experimental.semmle.python.frameworks.Ssh2
private import experimental.semmle.python.libraries.FlaskMail
private import experimental.semmle.python.libraries.SmtpLib

View File

@@ -0,0 +1,30 @@
/**
* Provides classes modeling security-relevant aspects of the `asyncssh` PyPI package.
* See https://pypi.org/project/asyncssh/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `asyncssh` PyPI package.
* See https://pypi.org/project/asyncssh/.
*/
private module Asyncssh {
/**
* Gets `asyncssh` package.
*/
private API::Node asyncssh() { result = API::moduleImport("asyncssh") }
/**
* A `run` method responsible for executing commands on remote secondary servers.
*/
class AsyncsshRun extends RemoteCommandExecution::Range, API::CallNode {
AsyncsshRun() { this = asyncssh().getMember("connect").getReturn().getMember("run").getACall() }
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
}
}

View File

@@ -0,0 +1,55 @@
/**
* Provides classes modeling security-relevant aspects of the `netmiko` PyPI package.
* See https://pypi.org/project/netmiko/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `netmiko` PyPI package.
* See https://pypi.org/project/netmiko/.
*/
private module Netmiko {
/**
* Gets `netmiko` package.
*/
private API::Node netmiko() { result = API::moduleImport("netmiko") }
/**
* Gets `netmiko.ConnectHandler` return value.
*/
private API::Node netmikoConnectHandler() {
result = netmiko().getMember("ConnectHandler").getReturn()
}
/**
* The `send_*` methods responsible for executing commands on remote secondary servers.
*/
class NetmikoSendCommand extends RemoteCommandExecution::Range, API::CallNode {
boolean isMultiline;
NetmikoSendCommand() {
this =
netmikoConnectHandler()
.getMember(["send_command", "send_command_expect", "send_command_timing"])
.getACall() and
isMultiline = false
or
this =
netmikoConnectHandler().getMember(["send_multiline", "send_multiline_timing"]).getACall() and
isMultiline = true
}
override DataFlow::Node getCommand() {
result = this.getParameter(0, "command_string").asSink() and
isMultiline = false
or
result = this.getParameter(0, "commands").asSink() and
isMultiline = true
}
}
}

View File

@@ -0,0 +1,35 @@
/**
* Provides classes modeling security-relevant aspects of the `paramiko` PyPI package.
* See https://pypi.org/project/paramiko/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `paramiko` PyPI package.
* See https://pypi.org/project/paramiko/.
*/
private module Paramiko {
/**
* Gets `paramiko` package.
*/
private API::Node paramiko() { result = API::moduleImport("paramiko") }
/**
* Gets `paramiko.SSHClient` return value.
*/
private API::Node paramikoClient() { result = paramiko().getMember("SSHClient").getReturn() }
/**
* The `exec_command` of `paramiko.SSHClient` class execute command on ssh target server
*/
class ParamikoExecCommand extends RemoteCommandExecution::Range, API::CallNode {
ParamikoExecCommand() { this = paramikoClient().getMember("exec_command").getACall() }
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
}
}

View File

@@ -0,0 +1,33 @@
/**
* Provides classes modeling security-relevant aspects of the `pexpect` PyPI package.
* See https://pypi.org/project/pexpect/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `pexpect` PyPI package.
* See https://pypi.org/project/pexpect/.
*/
private module Pexpect {
/**
* The calls to `pexpect.pxssh.pxssh` functions that execute commands
* See https://pexpect.readthedocs.io/en/stable/api/pxssh.html
*/
class PexpectCommandExec extends RemoteCommandExecution::Range, API::CallNode {
PexpectCommandExec() {
this =
API::moduleImport("pexpect")
.getMember("pxssh")
.getMember("pxssh")
.getReturn()
.getMember(["send", "sendline"])
.getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "s").asSink() }
}
}

View File

@@ -0,0 +1,56 @@
/**
* Provides classes modeling security-relevant aspects of the `scrapli` PyPI package.
* See https://pypi.org/project/scrapli/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `scrapli` PyPI package.
* See https://pypi.org/project/scrapli/.
*/
private module Scrapli {
/**
* Gets `scrapli` package.
*/
private API::Node scrapli() { result = API::moduleImport("scrapli") }
/**
* Gets `scrapli.driver` package.
*/
private API::Node scrapliDriver() { result = scrapli().getMember("driver") }
/**
* Gets `scrapli.driver.core` package.
*/
private API::Node scrapliCore() { result = scrapliDriver().getMember("core") }
/**
* A `send_command` method responsible for executing commands on remote secondary servers.
*/
class ScrapliSendCommand extends RemoteCommandExecution::Range, API::CallNode {
ScrapliSendCommand() {
this =
scrapliCore()
.getMember([
"AsyncNXOSDriver", "AsyncJunosDriver", "AsyncEOSDriver", "AsyncIOSXEDriver",
"AsyncIOSXRDriver", "NXOSDriver", "JunosDriver", "EOSDriver", "IOSXEDriver",
"IOSXRDriver"
])
.getReturn()
.getMember("send_command")
.getACall()
or
this = scrapli().getMember("Scrapli").getReturn().getMember("send_command").getACall()
or
this =
scrapliDriver().getMember("GenericDriver").getReturn().getMember("send_command").getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
}
}

View File

@@ -0,0 +1,39 @@
/**
* Provides classes modeling security-relevant aspects of the `ssh2-python` PyPI package.
* See https://pypi.org/project/ssh2-python/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
/**
* Provides models for the `ssh2-python` PyPI package.
* See https://pypi.org/project/ssh2-python/.
*/
private module Ssh2 {
/**
* Gets `ssh2` package.
*/
private API::Node ssh2() { result = API::moduleImport("ssh2") }
/**
* Gets `ssh2.session.Session` return value.
*/
private API::Node ssh2Session() {
result = ssh2().getMember("session").getMember("Session").getReturn()
}
/**
* An `execute` method responsible for executing commands on remote secondary servers.
*/
class Ssh2Execute extends RemoteCommandExecution::Range, API::CallNode {
Ssh2Execute() {
this = ssh2Session().getMember("open_session").getReturn().getMember("execute").getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
}
}

View File

@@ -0,0 +1,35 @@
/**
* Provides classes modeling security-relevant aspects of the `twisted` PyPI package.
* See https://twistedmatrix.com/.
*/
private import python
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.TaintTracking
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
import experimental.semmle.python.Concepts
/**
* Provides models for the `twisted` PyPI package.
* See https://docs.twistedmatrix.com/en/stable/api/twisted.conch.endpoints.SSHCommandClientEndpoint.html
*/
private module Twisted {
/**
* The `newConnection` and `existingConnection` functions of `twisted.conch.endpoints.SSHCommandClientEndpoint` class execute command on ssh target server
*/
class ParamikoExecCommand extends RemoteCommandExecution::Range, API::CallNode {
ParamikoExecCommand() {
this =
API::moduleImport("twisted")
.getMember("conch")
.getMember("endpoints")
.getMember("SSHCommandClientEndpoint")
.getMember(["newConnection", "existingConnection"])
.getACall()
}
override DataFlow::Node getCommand() { result = this.getParameter(1, "command").asSink() }
}
}

View File

@@ -0,0 +1,16 @@
import python
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.ApiGraphs
import semmle.python.dataflow.new.internal.DataFlowPublic
import codeql.util.Unit
import experimental.semmle.python.Concepts
module RemoteCommandExecutionConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
predicate isSink(DataFlow::Node sink) { sink = any(RemoteCommandExecution rce).getCommand() }
}
/** Global taint-tracking for detecting "secondary server command injection" vulnerabilities. */
module RemoteCommandExecutionFlow = TaintTracking::Global<RemoteCommandExecutionConfig>;

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python
from fastapi import FastAPI
import asyncssh
app = FastAPI()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("host", "port"))
session = Session()
session.handshake(sock)
session.userauth_password("user", "password")
@app.get("/bad1")
async def bad1(cmd: str):
async with asyncssh.connect('localhost') as conn:
result = await conn.run(cmd, check=True) # $ result=BAD getRemoteCommand=cmd
print(result.stdout, end='')
return {"success": "Dangerous"}

View File

@@ -0,0 +1,23 @@
import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.internal.DataFlowDispatch as DataFlowDispatch
import TestUtilities.InlineExpectationsTest
private import semmle.python.dataflow.new.internal.PrintNode
import experimental.semmle.python.Concepts
module SystemCommandExecutionTest implements TestSig {
string getARelevantTag() { result = "getRemoteCommand" }
predicate hasActualResult(Location location, string element, string tag, string value) {
exists(location.getFile().getRelativePath()) and
exists(RemoteCommandExecution sci, DataFlow::Node command |
command = sci.getCommand() and
location = command.getLocation() and
element = command.toString() and
value = prettyNodeForInlineTest(command) and
tag = "getRemoteCommand"
)
}
}
import MakeTest<SystemCommandExecutionTest>

View File

@@ -0,0 +1,3 @@
missingAnnotationOnSink
testFailures
failures

View File

@@ -0,0 +1,4 @@
import python
import TestUtilities.dataflow.DataflowQueryTest
import experimental.semmle.python.security.RemoteCommandExecution
import FromTaintTrackingConfig<RemoteCommandExecutionConfig>

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env python
from fastapi import FastAPI
from netmiko import ConnectHandler
app = FastAPI()
cisco_881 = {
'device_type': 'cisco_ios',
'host': '10.10.10.10',
'username': 'test',
'password': 'password',
'port': 8022, # optional, defaults to 22
'secret': 'secret', # optional, defaults to ''
}
@app.get("/bad1")
async def bad1(cmd: str):
net_connect = ConnectHandler(**cisco_881)
net_connect.send_command(command_string=cmd) # $ result=BAD getRemoteCommand=cmd
net_connect.send_command_expect(command_string=cmd) # $ result=BAD getRemoteCommand=cmd
net_connect.send_command_timing(command_string=cmd) # $ result=BAD getRemoteCommand=cmd
net_connect.send_multiline(commands=[[cmd, "expect"]]) # $ result=BAD getRemoteCommand=List
net_connect.send_multiline_timing(commands=cmd) # $ result=BAD getRemoteCommand=cmd
return {"success": "Dangerous"}

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python
from fastapi import FastAPI
from pexpect import pxssh
ssh = pxssh.pxssh()
hostname = "localhost"
username = "username"
password = "password"
ssh.login(hostname, username, password)
app = FastAPI()
@app.get("/bad1")
async def bad1(cmd: str):
ssh.send(cmd) # $ result=BAD getRemoteCommand=cmd
ssh.prompt()
ssh.sendline(cmd) # $ result=BAD getRemoteCommand=cmd
ssh.prompt()
ssh.logout()
return {"success": stdout}

View File

@@ -0,0 +1,88 @@
edges
| AsyncSsh.py:15:16:15:18 | ControlFlowNode for cmd | AsyncSsh.py:17:33:17:35 | ControlFlowNode for cmd | provenance | |
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:20:45:20:47 | ControlFlowNode for cmd | provenance | |
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:21:52:21:54 | ControlFlowNode for cmd | provenance | |
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:22:52:22:54 | ControlFlowNode for cmd | provenance | |
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:23:41:23:57 | ControlFlowNode for List | provenance | |
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:24:48:24:50 | ControlFlowNode for cmd | provenance | |
| Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | Pexpect.py:16:14:16:16 | ControlFlowNode for cmd | provenance | |
| Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | Pexpect.py:18:18:18:20 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:24:42:24:44 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:27:42:27:44 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:30:42:30:44 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:33:42:33:44 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:36:42:36:44 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:51:36:51:38 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:54:36:54:38 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:57:36:57:38 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:60:36:60:38 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:63:36:63:38 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:74:36:74:38 | ControlFlowNode for cmd | provenance | |
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:84:36:84:38 | ControlFlowNode for cmd | provenance | |
| Twisted.py:13:16:13:18 | ControlFlowNode for cmd | Twisted.py:16:5:16:7 | ControlFlowNode for cmd | provenance | |
| Twisted.py:13:16:13:18 | ControlFlowNode for cmd | Twisted.py:24:9:24:11 | ControlFlowNode for cmd | provenance | |
| paramiko.py:15:16:15:18 | ControlFlowNode for cmd | paramiko.py:16:62:16:64 | ControlFlowNode for cmd | provenance | |
| paramiko.py:20:16:20:18 | ControlFlowNode for cmd | paramiko.py:21:70:21:72 | ControlFlowNode for cmd | provenance | |
| ssh2.py:15:16:15:18 | ControlFlowNode for cmd | ssh2.py:17:21:17:23 | ControlFlowNode for cmd | provenance | |
nodes
| AsyncSsh.py:15:16:15:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| AsyncSsh.py:17:33:17:35 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Netmiko.py:20:45:20:47 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Netmiko.py:21:52:21:54 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Netmiko.py:22:52:22:54 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Netmiko.py:23:41:23:57 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
| Netmiko.py:24:48:24:50 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Pexpect.py:16:14:16:16 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Pexpect.py:18:18:18:20 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:24:42:24:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:27:42:27:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:30:42:30:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:33:42:33:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:36:42:36:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:51:36:51:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:54:36:54:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:57:36:57:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:60:36:60:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:63:36:63:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:74:36:74:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Scrapli.py:84:36:84:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Twisted.py:13:16:13:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Twisted.py:16:5:16:7 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| Twisted.py:24:9:24:11 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| paramiko.py:15:16:15:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| paramiko.py:16:62:16:64 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| paramiko.py:20:16:20:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| paramiko.py:21:70:21:72 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| ssh2.py:15:16:15:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
| ssh2.py:17:21:17:23 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
subpaths
#select
| AsyncSsh.py:17:33:17:35 | ControlFlowNode for cmd | AsyncSsh.py:15:16:15:18 | ControlFlowNode for cmd | AsyncSsh.py:17:33:17:35 | ControlFlowNode for cmd | This code execution depends on a $@. | AsyncSsh.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |
| Netmiko.py:20:45:20:47 | ControlFlowNode for cmd | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:20:45:20:47 | ControlFlowNode for cmd | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
| Netmiko.py:21:52:21:54 | ControlFlowNode for cmd | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:21:52:21:54 | ControlFlowNode for cmd | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
| Netmiko.py:22:52:22:54 | ControlFlowNode for cmd | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:22:52:22:54 | ControlFlowNode for cmd | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
| Netmiko.py:23:41:23:57 | ControlFlowNode for List | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:23:41:23:57 | ControlFlowNode for List | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
| Netmiko.py:24:48:24:50 | ControlFlowNode for cmd | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:24:48:24:50 | ControlFlowNode for cmd | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
| Pexpect.py:16:14:16:16 | ControlFlowNode for cmd | Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | Pexpect.py:16:14:16:16 | ControlFlowNode for cmd | This code execution depends on a $@. | Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |
| Pexpect.py:18:18:18:20 | ControlFlowNode for cmd | Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | Pexpect.py:18:18:18:20 | ControlFlowNode for cmd | This code execution depends on a $@. | Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:24:42:24:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:24:42:24:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:27:42:27:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:27:42:27:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:30:42:30:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:30:42:30:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:33:42:33:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:33:42:33:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:36:42:36:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:36:42:36:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:51:36:51:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:51:36:51:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:54:36:54:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:54:36:54:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:57:36:57:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:57:36:57:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:60:36:60:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:60:36:60:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:63:36:63:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:63:36:63:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:74:36:74:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:74:36:74:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
| Scrapli.py:84:36:84:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:84:36:84:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
| Twisted.py:16:5:16:7 | ControlFlowNode for cmd | Twisted.py:13:16:13:18 | ControlFlowNode for cmd | Twisted.py:16:5:16:7 | ControlFlowNode for cmd | This code execution depends on a $@. | Twisted.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
| Twisted.py:24:9:24:11 | ControlFlowNode for cmd | Twisted.py:13:16:13:18 | ControlFlowNode for cmd | Twisted.py:24:9:24:11 | ControlFlowNode for cmd | This code execution depends on a $@. | Twisted.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
| paramiko.py:16:62:16:64 | ControlFlowNode for cmd | paramiko.py:15:16:15:18 | ControlFlowNode for cmd | paramiko.py:16:62:16:64 | ControlFlowNode for cmd | This code execution depends on a $@. | paramiko.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |
| paramiko.py:21:70:21:72 | ControlFlowNode for cmd | paramiko.py:20:16:20:18 | ControlFlowNode for cmd | paramiko.py:21:70:21:72 | ControlFlowNode for cmd | This code execution depends on a $@. | paramiko.py:20:16:20:18 | ControlFlowNode for cmd | a user-provided value |
| ssh2.py:17:21:17:23 | ControlFlowNode for cmd | ssh2.py:15:16:15:18 | ControlFlowNode for cmd | ssh2.py:17:21:17:23 | ControlFlowNode for cmd | This code execution depends on a $@. | ssh2.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |

View File

@@ -0,0 +1 @@
experimental/Security/CWE-074/remoteCommandExecution/RemoteCommandExecution.ql

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python
from fastapi import FastAPI
from scrapli import Scrapli
from scrapli.driver.core import AsyncNXOSDriver, AsyncJunosDriver, AsyncEOSDriver, AsyncIOSXEDriver, AsyncIOSXRDriver
from scrapli.driver.core import NXOSDriver, JunosDriver, EOSDriver, IOSXEDriver, IOSXRDriver
from scrapli.driver import GenericDriver
app = FastAPI()
@app.get("/bad1")
async def bad1(cmd: str):
dev_connect = {
"host": host,
"auth_username": user,
"auth_password": password,
"port": port,
"auth_strict_key": False,
"transport": "asyncssh",
}
driver = AsyncIOSXEDriver
async with driver(**dev_connect) as conn:
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
driver = AsyncIOSXRDriver
async with driver(**dev_connect) as conn:
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
driver = AsyncNXOSDriver
async with driver(**dev_connect) as conn:
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
driver = AsyncEOSDriver
async with driver(**dev_connect) as conn:
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
driver = AsyncJunosDriver
async with driver(**dev_connect) as conn:
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
return {"success": "Dangerous"}
@app.get("/bad1")
def bad2(cmd: str):
dev_connect = {
"host": host,
"auth_username": user,
"auth_password": password,
"port": port,
"auth_strict_key": False,
"transport": "ssh2",
}
driver = NXOSDriver
with driver(**dev_connect) as conn:
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
driver = IOSXRDriver
with driver(**dev_connect) as conn:
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
driver = IOSXEDriver
with driver(**dev_connect) as conn:
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
driver = EOSDriver
with driver(**dev_connect) as conn:
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
driver = JunosDriver
with driver(**dev_connect) as conn:
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
dev_connect = {
"host": "65.65.65.65",
"auth_username": "root",
"auth_private_key": "keyPath",
"auth_strict_key": False,
"transport": "ssh2",
"platform": "cisco_iosxe",
}
with Scrapli(**dev_connect) as conn:
result = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
dev_connect = {
"host": "65.65.65.65",
"auth_username": "root",
"auth_password": "password",
"auth_strict_key": False,
"transport": "ssh2",
}
with GenericDriver(**dev_connect) as conn:
result = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
return {"success": "Dangerous"}

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python
from fastapi import FastAPI
from twisted.conch.endpoints import SSHCommandClientEndpoint
from twisted.internet.protocol import Factory
from twisted.internet import reactor
app = FastAPI()
@app.get("/bad1")
async def bad1(cmd: bytes):
endpoint = SSHCommandClientEndpoint.newConnection(
reactor,
cmd, # $ result=BAD getRemoteCommand=cmd
b"username",
b"ssh.example.com",
22,
password=b"password")
SSHCommandClientEndpoint.existingConnection(
endpoint,
cmd) # $ result=BAD getRemoteCommand=cmd
factory = Factory()
d = endpoint.connect(factory)
d.addCallback(lambda protocol: protocol.finished)
return {"success": "Dangerous"}

View File

@@ -12,16 +12,11 @@ app = FastAPI()
@app.get("/bad1")
async def read_item(cmd: str):
stdin, stdout, stderr = paramiko_ssh_client.exec_command(cmd)
return {"success": stdout}
async def bad1(cmd: str):
stdin, stdout, stderr = paramiko_ssh_client.exec_command(cmd) # $ result=BAD getRemoteCommand=cmd
return {"success": "Dangerous"}
@app.get("/bad2")
async def read_item(cmd: str):
stdin, stdout, stderr = paramiko_ssh_client.exec_command(command=cmd)
return {"success": "OK"}
@app.get("/bad3")
async def read_item(cmd: str):
stdin, stdout, stderr = paramiko_ssh_client.connect('hostname', username='user',password='yourpassword',sock=paramiko.ProxyCommand(cmd))
return {"success": "OK"}
async def bad2(cmd: str):
stdin, stdout, stderr = paramiko_ssh_client.exec_command(command=cmd) # $ result=BAD getRemoteCommand=cmd
return {"success": "Dangerous"}

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python
from fastapi import FastAPI
from socket import socket
from ssh2.session import Session
app = FastAPI()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("host", "port"))
session = Session()
session.handshake(sock)
session.userauth_password("user", "password")
@app.get("/bad1")
async def bad1(cmd: str):
channel = session.open_session()
channel.execute(cmd) # $ result=BAD getRemoteCommand=cmd
channel.wait_eof()
channel.close()
channel.wait_closed()
return {"success": "Dangerous"}

View File

@@ -1 +0,0 @@
experimental/Security/CWE-074/paramiko/paramiko.ql

View File

@@ -5,7 +5,6 @@ Loosely inspired by http://docs.fabfile.org/en/2.5/getting-started.html
from fabric import connection, Connection, group, SerialGroup, ThreadingGroup, tasks, task
################################################################################
# Connection
################################################################################
@@ -22,6 +21,32 @@ c.sudo(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
c2 = connection.Connection("web2")
c2.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
# ssh proxy_command command injection with gateway parameter,
# we need to call some of the following functions to run the proxy command
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.run(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.get("afs")
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.sudo(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.open_gateway()
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.open()
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.create_session()
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.forward_local("80")
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.forward_remote("80")
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.put(local="local")
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.shell()
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
c.sftp()
# no call to desired methods so it is safe
c = Connection("web1", gateway="cmd")
################################################################################
# SerialGroup
@@ -30,11 +55,11 @@ results = SerialGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getCommand="
pool = SerialGroup("web1", "web2", "web3")
pool.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
pool.sudo("cmd1; cmd2") # $getCommand="cmd1; cmd2"
# fully qualified usage
group.SerialGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
################################################################################
# ThreadingGroup
################################################################################
@@ -42,6 +67,7 @@ results = ThreadingGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getComman
pool = ThreadingGroup("web1", "web2", "web3")
pool.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
pool.sudo("cmd1; cmd2") # $getCommand="cmd1; cmd2"
# fully qualified usage
group.ThreadingGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
@@ -57,6 +83,7 @@ def foo(c):
# 'c' is a fabric.connection.Connection
c.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
# fully qualified usage
@tasks.task
def bar(c):

View File

@@ -0,0 +1,2 @@
testFailures
failures

View File

@@ -0,0 +1,2 @@
import python
import experimental.meta.ConceptsTest

View File

@@ -0,0 +1,15 @@
import os
import jsonpickle
class Thing(object):
def __reduce__(self):
return os.system, ("curl 127.0.0.1:1234",)
obj = Thing()
pickledObj = jsonpickle.encode(obj)
objUnPickled = jsonpickle.decode(pickledObj, safe=True) # $ decodeInput=pickledObj decodeOutput=jsonpickle.decode(..) decodeFormat=pickle decodeMayExecuteInput
print(objUnPickled.name)

View File

@@ -0,0 +1,85 @@
import pandas as pd
df = pd.DataFrame({'temp_c': [17.0, 25.0]}, index=['Portland', 'Berkeley'])
df.sample().query("query") # $getCode="query"
df.mod().query("query") # $getCode="query"
pd.eval("pythonExpr", target=df) # $getCode="pythonExpr"
df = pd.read_csv("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df.copy().query("query") # $getCode="query"
df = pd.read_fwf("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_pickle("filepath") # $ decodeInput="filepath" decodeOutput=pd.read_pickle(..) decodeFormat=pickle decodeMayExecuteInput
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_table("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_clipboard("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_excel("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_html("filepath")
df[0].query("query") # $getCode="query"
df = pd.read_xml("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_parquet("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_orc("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_spss("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_sql_table("filepath", 'postgres:///db_name')
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_sql_query("filepath", 'postgres:///db_name')
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_sql("filepath", 'postgres:///db_name')
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_gbq("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_stata("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_sas("filepath")
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"
df = pd.read_sas("filepath", iterator=True, chunksize=1)
df.query("query")
df = pd.read_sas("filepath", iterator=False, chunksize=1)
df.query("query")
df = pd.read_sas("filepath", iterator=True, chunksize=None)
df.query("query")
df = pd.read_sas("filepath", iterator=False, chunksize=None)
df.query("query") # $getCode="query"
df.eval("query") # $getCode="query"

View File

@@ -0,0 +1,2 @@
testFailures
failures

View File

@@ -0,0 +1,2 @@
import python
import experimental.meta.ConceptsTest

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python
import paramiko
from paramiko import SSHClient
paramiko_ssh_client = SSHClient()
paramiko_ssh_client.load_system_host_keys()
paramiko_ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_name", pkey="k", timeout=11, banner_timeout=200)
cmd = "cmd"
paramiko_ssh_client.connect('hostname', username='user', password='yourpassword', sock=paramiko.ProxyCommand(cmd)) # $getCommand=cmd

View File

@@ -0,0 +1,2 @@
testFailures
failures

View File

@@ -0,0 +1,2 @@
import python
import experimental.meta.ConceptsTest

View File

@@ -0,0 +1,9 @@
import pexpect
from pexpect import popen_spawn
cmd = "ls -la"
result = pexpect.run(cmd) # $ getCommand=cmd
result = pexpect.runu(cmd) # $ getCommand=cmd
result = pexpect.spawn(cmd) # $ getCommand=cmd
result = pexpect.spawnu(cmd) # $ getCommand=cmd
result = popen_spawn.PopenSpawn(cmd) # $ getCommand=cmd

View File

@@ -0,0 +1,2 @@
testFailures
failures

View File

@@ -0,0 +1,2 @@
import python
import experimental.meta.ConceptsTest

View File

@@ -0,0 +1,25 @@
from io import BytesIO
import torch
def someSafeMethod():
pass
PicklePayload = BytesIO(b"payload")
torch.load(PicklePayload) # $ decodeInput=PicklePayload decodeOutput=torch.load(..) decodeFormat=pickle decodeMayExecuteInput
torch.load(PicklePayload, pickle_module=None) # $ decodeInput=PicklePayload decodeOutput=torch.load(..) decodeFormat=pickle decodeMayExecuteInput
torch.load(PicklePayload, pickle_module=someSafeMethod()) # $ decodeInput=PicklePayload decodeOutput=torch.load(..) decodeFormat=pickle
from torch.package import PackageImporter
importer = PackageImporter(PicklePayload) # $ decodeInput=PicklePayload PackageImporter(..) decodeFormat=pickle decodeMayExecuteInput
my_tensor = importer.load_pickle("my_resources", "tensor.pkl") # $ decodeOutput=importer.load_pickle(..)
importer = PackageImporter(PicklePayload)
from torch import jit
jit.load(PicklePayload) # $ decodeInput=PicklePayload decodeOutput=jit.load(..) decodeFormat=pickle decodeMayExecuteInput