Skip to content

Commit 385c3ba

Browse files
committed
continue to convert paramiko query to a more general query,
the proxy command is not a secondary command execution so we can add proxy command to SystemCommandExecution::Range, update QLDocs, add a proper Paramiko test case fix a typo
1 parent 70282f9 commit 385c3ba

File tree

12 files changed

+105
-47
lines changed

12 files changed

+105
-47
lines changed

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ private import semmle.python.frameworks.MySQLdb
4646
private import semmle.python.frameworks.Numpy
4747
private import semmle.python.frameworks.Oracledb
4848
private import semmle.python.frameworks.Pandas
49+
private import semmle.python.frameworks.Paramiko
4950
private import semmle.python.frameworks.Peewee
5051
private import semmle.python.frameworks.Phoenixdb
5152
private import semmle.python.frameworks.Psycopg
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `paramiko` PyPI package.
3+
* See https://pypi.org/project/paramiko/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.dataflow.new.RemoteFlowSources
9+
private import semmle.python.Concepts
10+
private import semmle.python.ApiGraphs
11+
12+
/**
13+
* Provides models for the `paramiko` PyPI package.
14+
* See https://pypi.org/project/paramiko/.
15+
*/
16+
private module Paramiko {
17+
/*
18+
* The first argument of `paramiko.ProxyCommand`.
19+
*
20+
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
21+
* and it run CMD on current system that running the ssh command
22+
*
23+
* See https://paramiko.pydata.org/docs/reference/api/paramiko.eval.html
24+
*/
25+
26+
class ParamikoProxyCommand extends SystemCommandExecution::Range, API::CallNode {
27+
ParamikoProxyCommand() {
28+
this = API::moduleImport("paramiko").getMember("ProxyCommand").getACall()
29+
}
30+
31+
override DataFlow::Node getCommand() { result = this.getParameter(0, "command_line").asSink() }
32+
33+
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
34+
}
35+
}

python/ql/src/experimental/Security/CWE-074/secondaryCommandInjection/SecondaryServerCmdInjection.qhelp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
<qhelp>
33
<overview>
44
<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.
5+
Running user-controlled values into a secondary remote servers without proper authorization can allow an attacker to inject arbitrary command in the secondary remote servers from within your primary remote servers.
66
</p>
77
</overview>
88
<recommendation>
99
<p>
10-
This vulnerability can be prevented by not allowing untrusted user input to be passed as ProxyCommand or exec_command.
10+
This vulnerability can be prevented by implementing proper authorization rules for untrusted user input that can be passed to your secondary servers.
1111
</p>
1212
</recommendation>
1313
<example>
14-
<p>In the example below, the ProxyCommand and exec_command are controlled by the user and hence leads to a vulnerability.</p>
14+
<p>In the example below, the exec_command is controlled by the user and hence leads to a vulnerability.</p>
1515
<sample src="paramikoBad.py" />
16+
<p>In the example below, the exec_command is controlled by the an Authorized user and hence it is safe.</p>
17+
<sample src="paramikoGood.py" />
1618
</example>
1719
</qhelp>

python/ql/src/experimental/Security/CWE-074/secondaryCommandInjection/SecondaryServerCmdInjection.ql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @name RCE with user provided command with paramiko ssh client
2+
* @name Command execution on a secondary remote server
33
* @description user provided command can lead to execute code on a external server that can be belong to other users or admins
44
* @kind path-problem
55
* @problem.severity error
@@ -13,9 +13,9 @@
1313

1414
import python
1515
import experimental.semmle.python.security.SecondaryServerCmdInjection
16-
import ParamikoFlow::PathGraph
16+
import SecondaryCommandInjectionFlow::PathGraph
1717

18-
from ParamikoFlow::PathNode source, ParamikoFlow::PathNode sink
19-
where ParamikoFlow::flowPath(source, sink)
18+
from SecondaryCommandInjectionFlow::PathNode source, SecondaryCommandInjectionFlow::PathNode sink
19+
where SecondaryCommandInjectionFlow::flowPath(source, sink)
2020
select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(),
2121
"a user-provided value"

python/ql/src/experimental/Security/CWE-074/secondaryCommandInjection/paramikoBad.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,11 @@
1212

1313

1414
@app.route('/external_exec_command_1')
15-
def bad1():
15+
def withoutAuthorization():
1616
user_cmd = request.args.get('command')
1717
stdin, stdout, stderr = paramiko_ssh_client.exec_command(user_cmd)
1818
return stdout
1919

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-
3320
if __name__ == '__main__':
3421
app.debug = False
3522
app.run()
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 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 withAuthorization():
16+
user_cmd = request.args.get('command')
17+
auth_jwt = request.args.get('Auth')
18+
# validating jwt token first
19+
# .... then continue to run the command
20+
stdin, stdout, stderr = paramiko_ssh_client.exec_command(user_cmd)
21+
return stdout
22+
23+
24+
if __name__ == '__main__':
25+
app.debug = False
26+
app.run()
27+

python/ql/src/experimental/semmle/python/security/SecondaryServerCmdInjection.qll

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import semmle.python.ApiGraphs
55
import semmle.python.dataflow.new.internal.DataFlowPublic
66
import codeql.util.Unit
77

8+
/**
9+
* Provides sinks and additional taint steps for the secondary command injection configuration
10+
*/
811
module SecondaryCommandInjection {
912
/**
1013
* The additional taint steps that need for creating taint tracking or dataflow.
@@ -22,36 +25,24 @@ module SecondaryCommandInjection {
2225
abstract class Sink extends DataFlow::Node { }
2326
}
2427

28+
/**
29+
* The exec_command of `paramiko.SSHClient` class execute command on ssh target server
30+
*/
31+
class ParamikoExecCommand extends SecondaryCommandInjection::Sink {
32+
ParamikoExecCommand() {
33+
this = paramikoClient().getMember("exec_command").getACall().getParameter(0, "command").asSink()
34+
}
35+
}
36+
2537
private API::Node paramikoClient() {
2638
result = API::moduleImport("paramiko").getMember("SSHClient").getReturn()
2739
}
2840

29-
module ParamikoConfig implements DataFlow::ConfigSig {
41+
module SecondaryCommandInjectionConfig implements DataFlow::ConfigSig {
3042
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
3143

32-
/**
33-
* exec_command of `paramiko.SSHClient` class execute command on ssh target server
34-
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
35-
* and it run CMD on current system that running the ssh command
36-
* the Sink related to proxy command is the `connect` method of `paramiko.SSHClient` class
37-
*/
38-
predicate isSink(DataFlow::Node sink) {
39-
sink = paramikoClient().getMember("exec_command").getACall().getParameter(0, "command").asSink()
40-
or
41-
sink = paramikoClient().getMember("connect").getACall().getParameter(11, "sock").asSink()
42-
}
43-
44-
/**
45-
* this additional taint step help taint tracking to find the vulnerable `connect` method of `paramiko.SSHClient` class
46-
*/
47-
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
48-
exists(API::CallNode call |
49-
call = API::moduleImport("paramiko").getMember("ProxyCommand").getACall() and
50-
nodeFrom = call.getParameter(0, "command_line").asSink() and
51-
nodeTo = call
52-
)
53-
}
44+
predicate isSink(DataFlow::Node sink) { sink instanceof SecondaryCommandInjection::Sink }
5445
}
5546

5647
/** Global taint-tracking for detecting "paramiko command injection" vulnerabilities. */
57-
module ParamikoFlow = TaintTracking::Global<ParamikoConfig>;
48+
module SecondaryCommandInjectionFlow = TaintTracking::Global<SecondaryCommandInjectionConfig>;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import python
22
import experimental.dataflow.TestUtil.DataflowQueryTest
33
import experimental.semmle.python.security.SecondaryServerCmdInjection
4-
import FromTaintTrackingConfig<ParamikoConfig>
4+
import FromTaintTrackingConfig<SecondaryCommandInjectionConfig>

python/ql/test/experimental/query-tests/Security/CWE-074-SecondaryServerCmdInjection/paramiko.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ async def read_item(cmd: str):
2323

2424
@app.get("/bad3")
2525
async def read_item(cmd: str):
26-
stdin, stdout, stderr = paramiko_ssh_client.connect('hostname', username='user',password='yourpassword',sock=paramiko.ProxyCommand(cmd)) # $ result=BAD
26+
paramiko_ssh_client.connect('hostname', username='user',password='yourpassword',sock=paramiko.ProxyCommand(cmd)) # $ result=BAD
2727
return {"success": "OK"}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
testFailures
2+
failures

0 commit comments

Comments
 (0)