From 54582031d835da8c52ea2c70e7b1fb632fbc30ef Mon Sep 17 00:00:00 2001 From: amammad Date: Thu, 16 Feb 2023 17:14:32 +0100 Subject: [PATCH] v1 --- .../Security/CWE-074/paramiko/paramiko.qhelp | 17 +++++ .../Security/CWE-074/paramiko/paramiko.ql | 72 +++++++++++++++++++ .../Security/CWE-074/paramiko/paramikoBad.py | 36 ++++++++++ .../CWE-074/paramiko/paramiko.expected | 16 +++++ .../Security/CWE-074/paramiko/paramiko.py | 27 +++++++ .../Security/CWE-074/paramiko/paramiko.qlref | 1 + 6 files changed, 169 insertions(+) create mode 100644 python/ql/src/experimental/Security/CWE-074/paramiko/paramiko.qhelp create mode 100644 python/ql/src/experimental/Security/CWE-074/paramiko/paramiko.ql create mode 100644 python/ql/src/experimental/Security/CWE-074/paramiko/paramikoBad.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.expected create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.qlref diff --git a/python/ql/src/experimental/Security/CWE-074/paramiko/paramiko.qhelp b/python/ql/src/experimental/Security/CWE-074/paramiko/paramiko.qhelp new file mode 100644 index 00000000000..98cc0e1e4de --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-074/paramiko/paramiko.qhelp @@ -0,0 +1,17 @@ + + + +

+ Processing an unvalidated user input can allow an attacker to inject arbitrary command in your local and remote servers when creating a ssh connection. +

+
+ +

+ This vulnerability can be prevented by not allowing untrusted user input to be passed as ProxyCommand or exec_command. +

+
+ +

In the example below, the ProxyCommand and exec_command are controlled by the user and hence leads to a vulnerability.

+ +
+
diff --git a/python/ql/src/experimental/Security/CWE-074/paramiko/paramiko.ql b/python/ql/src/experimental/Security/CWE-074/paramiko/paramiko.ql new file mode 100644 index 00000000000..28db4e129b4 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-074/paramiko/paramiko.ql @@ -0,0 +1,72 @@ +/** + * @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/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 +import DataFlow::PathGraph + +class ParamikoCMDInjectionConfiguration extends TaintTracking::Configuration { + ParamikoCMDInjectionConfiguration() { this = "ParamikoCMDInjectionConfiguration" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + sink = + [ + API::moduleImport("paramiko") + .getMember("SSHClient") + .getReturn() + .getMember("exec_command") + .getACall() + .getArgByName("command"), + API::moduleImport("paramiko") + .getMember("SSHClient") + .getReturn() + .getMember("exec_command") + .getACall() + .getArg(0) + ] + or + sink = + [ + API::moduleImport("paramiko") + .getMember("SSHClient") + .getReturn() + .getMember("connect") + .getACall() + .getArgByName("sock"), + API::moduleImport("paramiko") + .getMember("SSHClient") + .getReturn() + .getMember("connect") + .getACall() + .getArg(11) + ] + } + + override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { + exists(API::CallNode call | + call = API::moduleImport("paramiko").getMember("ProxyCommand").getACall() and + nodeFrom = [call.getArg(0), call.getArgByName("command_line")] and + nodeTo = call + ) + } +} + +from ParamikoCMDInjectionConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink +where config.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(), + "a user-provided value" diff --git a/python/ql/src/experimental/Security/CWE-074/paramiko/paramikoBad.py b/python/ql/src/experimental/Security/CWE-074/paramiko/paramikoBad.py new file mode 100644 index 00000000000..b54a88f2e4a --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-074/paramiko/paramikoBad.py @@ -0,0 +1,36 @@ +#!/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 bad1(): + user_cmd = request.args.get('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 + app.run() + diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.expected b/python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.expected new file mode 100644 index 00000000000..85e1e7b326d --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.expected @@ -0,0 +1,16 @@ +edges +| paramiko.py:15:21:15:23 | ControlFlowNode for cmd | paramiko.py:16:62:16:64 | ControlFlowNode for cmd | +| paramiko.py:20:21:20:23 | ControlFlowNode for cmd | paramiko.py:21:70:21:72 | ControlFlowNode for cmd | +| paramiko.py:25:21:25:23 | ControlFlowNode for cmd | paramiko.py:26:114:26:139 | ControlFlowNode for Attribute() | +nodes +| paramiko.py:15:21:15:23 | 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:21:20:23 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd | +| paramiko.py:21:70:21:72 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd | +| paramiko.py:25:21:25:23 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd | +| paramiko.py:26:114:26:139 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +subpaths +#select +| paramiko.py:16:62:16:64 | ControlFlowNode for cmd | paramiko.py:15:21:15:23 | ControlFlowNode for cmd | paramiko.py:16:62:16:64 | ControlFlowNode for cmd | This code execution depends on a $@. | paramiko.py:15:21:15:23 | ControlFlowNode for cmd | a user-provided value | +| paramiko.py:21:70:21:72 | ControlFlowNode for cmd | paramiko.py:20:21:20:23 | ControlFlowNode for cmd | paramiko.py:21:70:21:72 | ControlFlowNode for cmd | This code execution depends on a $@. | paramiko.py:20:21:20:23 | ControlFlowNode for cmd | a user-provided value | +| paramiko.py:26:114:26:139 | ControlFlowNode for Attribute() | paramiko.py:25:21:25:23 | ControlFlowNode for cmd | paramiko.py:26:114:26:139 | ControlFlowNode for Attribute() | This code execution depends on a $@. | paramiko.py:25:21:25:23 | ControlFlowNode for cmd | a user-provided value | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.py b/python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.py new file mode 100644 index 00000000000..1e625d18345 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +from fastapi import FastAPI +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) + +app = FastAPI() + + +@app.get("/bad1") +async def read_item(cmd: str): + stdin, stdout, stderr = paramiko_ssh_client.exec_command(cmd) + return {"success": stdout} + +@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"} diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.qlref b/python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.qlref new file mode 100644 index 00000000000..8a164fcc8cc --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-074/paramiko/paramiko.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-074/paramiko/paramiko.ql \ No newline at end of file