Skip to content

Commit 5458203

Browse files
committed
v1
1 parent 7cfe15c commit 5458203

File tree

6 files changed

+169
-0
lines changed

6 files changed

+169
-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: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
class ParamikoCMDInjectionConfiguration extends TaintTracking::Configuration {
22+
ParamikoCMDInjectionConfiguration() { this = "ParamikoCMDInjectionConfiguration" }
23+
24+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
25+
26+
override predicate isSink(DataFlow::Node sink) {
27+
sink =
28+
[
29+
API::moduleImport("paramiko")
30+
.getMember("SSHClient")
31+
.getReturn()
32+
.getMember("exec_command")
33+
.getACall()
34+
.getArgByName("command"),
35+
API::moduleImport("paramiko")
36+
.getMember("SSHClient")
37+
.getReturn()
38+
.getMember("exec_command")
39+
.getACall()
40+
.getArg(0)
41+
]
42+
or
43+
sink =
44+
[
45+
API::moduleImport("paramiko")
46+
.getMember("SSHClient")
47+
.getReturn()
48+
.getMember("connect")
49+
.getACall()
50+
.getArgByName("sock"),
51+
API::moduleImport("paramiko")
52+
.getMember("SSHClient")
53+
.getReturn()
54+
.getMember("connect")
55+
.getACall()
56+
.getArg(11)
57+
]
58+
}
59+
60+
override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
61+
exists(API::CallNode call |
62+
call = API::moduleImport("paramiko").getMember("ProxyCommand").getACall() and
63+
nodeFrom = [call.getArg(0), call.getArgByName("command_line")] and
64+
nodeTo = call
65+
)
66+
}
67+
}
68+
69+
from ParamikoCMDInjectionConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
70+
where config.hasFlowPath(source, sink)
71+
select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(),
72+
"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)