Skip to content

Commit 0bc6f10

Browse files
authored
Merge pull request github#12220 from amammad/amammad-python-paramiko
add some python sinks for paramiko ssh clients
2 parents 2c89f97 + b3669b8 commit 0bc6f10

File tree

6 files changed

+154
-0
lines changed

6 files changed

+154
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
Processing an unvalidated user input can allow an attacker to inject arbitrary command in your local and remote servers when creating a ssh connection.
6+
</p>
7+
</overview>
8+
<recommendation>
9+
<p>
10+
This vulnerability can be prevented by not allowing untrusted user input to be passed as ProxyCommand or exec_command.
11+
</p>
12+
</recommendation>
13+
<example>
14+
<p>In the example below, the ProxyCommand and exec_command are controlled by the user and hence leads to a vulnerability.</p>
15+
<sample src="paramikoBad.py" />
16+
</example>
17+
</qhelp>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @name RCE with user provided command with paramiko ssh client
3+
* @description user provided command can lead to execute code on a external server that can be belong to other users or admins
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 9.3
7+
* @precision high
8+
* @id py/command-injection
9+
* @tags security
10+
* experimental
11+
* external/cwe/cwe-074
12+
*/
13+
14+
import python
15+
import semmle.python.dataflow.new.DataFlow
16+
import semmle.python.dataflow.new.TaintTracking
17+
import semmle.python.dataflow.new.RemoteFlowSources
18+
import semmle.python.ApiGraphs
19+
import DataFlow::PathGraph
20+
21+
private API::Node paramikoClient() {
22+
result = API::moduleImport("paramiko").getMember("SSHClient").getReturn()
23+
}
24+
25+
class ParamikoCmdInjectionConfiguration extends TaintTracking::Configuration {
26+
ParamikoCmdInjectionConfiguration() { this = "ParamikoCMDInjectionConfiguration" }
27+
28+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
29+
30+
/**
31+
* exec_command of `paramiko.SSHClient` class execute command on ssh target server
32+
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
33+
* and it run CMD on current system that running the ssh command
34+
* the Sink related to proxy command is the `connect` method of `paramiko.SSHClient` class
35+
*/
36+
override predicate isSink(DataFlow::Node sink) {
37+
sink = paramikoClient().getMember("exec_command").getACall().getParameter(0, "command").asSink()
38+
or
39+
sink = paramikoClient().getMember("connect").getACall().getParameter(11, "sock").asSink()
40+
}
41+
42+
/**
43+
* this additional taint step help taint tracking to find the vulnerable `connect` method of `paramiko.SSHClient` class
44+
*/
45+
override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
46+
exists(API::CallNode call |
47+
call = API::moduleImport("paramiko").getMember("ProxyCommand").getACall() and
48+
nodeFrom = call.getParameter(0, "command_line").asSink() and
49+
nodeTo = call
50+
)
51+
}
52+
}
53+
54+
from ParamikoCmdInjectionConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
55+
where config.hasFlowPath(source, sink)
56+
select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(),
57+
"a user-provided value"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python
2+
3+
from flask import request, Flask
4+
import paramiko
5+
from paramiko import SSHClient
6+
7+
app = Flask(__name__)
8+
paramiko_ssh_client = SSHClient()
9+
paramiko_ssh_client.load_system_host_keys()
10+
paramiko_ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
11+
paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_name", pkey="k", timeout=11, banner_timeout=200)
12+
13+
14+
@app.route('/external_exec_command_1')
15+
def bad1():
16+
user_cmd = request.args.get('command')
17+
stdin, stdout, stderr = paramiko_ssh_client.exec_command(user_cmd)
18+
return stdout
19+
20+
@app.route('/external_exec_command_2')
21+
def bad2():
22+
user_cmd = request.args.get('command')
23+
stdin, stdout, stderr = paramiko_ssh_client.exec_command(command=user_cmd)
24+
return stdout
25+
26+
27+
@app.route('/proxycommand')
28+
def bad2():
29+
user_cmd = request.args.get('command')
30+
stdin, stdout, stderr = paramiko_ssh_client.connect('hostname', username='user',password='yourpassword',sock=paramiko.ProxyCommand(user_cmd))
31+
return stdout
32+
33+
if __name__ == '__main__':
34+
app.debug = False
35+
app.run()
36+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
edges
2+
| paramiko.py:15:21:15:23 | ControlFlowNode for cmd | paramiko.py:16:62:16:64 | ControlFlowNode for cmd |
3+
| paramiko.py:20:21:20:23 | ControlFlowNode for cmd | paramiko.py:21:70:21:72 | ControlFlowNode for cmd |
4+
| paramiko.py:25:21:25:23 | ControlFlowNode for cmd | paramiko.py:26:114:26:139 | ControlFlowNode for Attribute() |
5+
nodes
6+
| paramiko.py:15:21:15:23 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
7+
| paramiko.py:16:62:16:64 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
8+
| paramiko.py:20:21:20:23 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
9+
| paramiko.py:21:70:21:72 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
10+
| paramiko.py:25:21:25:23 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
11+
| paramiko.py:26:114:26:139 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
12+
subpaths
13+
#select
14+
| 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 |
15+
| 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 |
16+
| 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 |
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env python
2+
3+
from fastapi import FastAPI
4+
import paramiko
5+
from paramiko import SSHClient
6+
paramiko_ssh_client = SSHClient()
7+
paramiko_ssh_client.load_system_host_keys()
8+
paramiko_ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
9+
paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_name", pkey="k", timeout=11, banner_timeout=200)
10+
11+
app = FastAPI()
12+
13+
14+
@app.get("/bad1")
15+
async def read_item(cmd: str):
16+
stdin, stdout, stderr = paramiko_ssh_client.exec_command(cmd)
17+
return {"success": stdout}
18+
19+
@app.get("/bad2")
20+
async def read_item(cmd: str):
21+
stdin, stdout, stderr = paramiko_ssh_client.exec_command(command=cmd)
22+
return {"success": "OK"}
23+
24+
@app.get("/bad3")
25+
async def read_item(cmd: str):
26+
stdin, stdout, stderr = paramiko_ssh_client.connect('hostname', username='user',password='yourpassword',sock=paramiko.ProxyCommand(cmd))
27+
return {"success": "OK"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE-074/paramiko/paramiko.ql

0 commit comments

Comments
 (0)