Skip to content

Commit db231a1

Browse files
Porcupiney Hairsporcupineyhairs
authored andcommitted
Python : Improve the PAM authentication bypass query
The current PAM auth bypass query which was contributed by me a few months back, alert on a vulenrable function but does not check if the function is actually function. This leads to a lot of fasle positives. With this PR, I add a taint-tracking configuration to check if the username parameter can actually be supplied by an attacker. This should bring the FP's significantly down.
1 parent a964325 commit db231a1

File tree

7 files changed

+158
-27
lines changed

7 files changed

+158
-27
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: majorAnalysis
3+
---
4+
* Converted `py/pam-auth-bypass` to a data-flow query, resulting in significantly lower false positives.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Provides a taint-tracking configuration for detecting "PAM Authorization" vulnerabilities.
3+
*
4+
* Note, for performance reasons: only import this file if
5+
* `PamAuthorization::Configuration` is needed, otherwise
6+
* `PamAuthorizationCustomizations` should be imported instead.
7+
*/
8+
9+
import python
10+
import semmle.python.ApiGraphs
11+
import semmle.python.dataflow.new.TaintTracking
12+
import PamAuthorizationCustomizations::PamAuthorizationCustomizations
13+
14+
/**
15+
* Provides a taint-tracking configuration for detecting "PAM Authorization" vulnerabilities.
16+
*/
17+
module PamAuthorization {
18+
/**
19+
* A taint-tracking configuration for detecting "PAM Authorization" vulnerabilities.
20+
*/
21+
class Configuration extends TaintTracking::Configuration {
22+
Configuration() { this = "RemoteToPam" }
23+
24+
override predicate isSource(DataFlow::Node node) { node instanceof Source }
25+
26+
override predicate isSink(DataFlow::Node node) { node instanceof Sink }
27+
28+
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
29+
// Models flow from a remotely supplied username field to a PAM `handle`.
30+
// `retval = pam_start(service, username, byref(conv), byref(handle))`
31+
exists(API::CallNode pamStart, DataFlow::Node handle, API::CallNode pointer |
32+
pointer = API::moduleImport("ctypes").getMember(["pointer", "byref"]).getACall() and
33+
pamStart = libPam().getMember("pam_start").getACall() and
34+
pointer = pamStart.getArg(3) and
35+
handle = pointer.getArg(0) and
36+
pamStart.getArg(1) = node1 and
37+
handle = node2
38+
)
39+
or
40+
// Flow from handle to the authenticate call in the final step
41+
exists(VulnPamAuthCall c | c.getArg(0) = node1 | node2 = c)
42+
}
43+
}
44+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for detecting
3+
* "PAM Authorization" vulnerabilities.
4+
*/
5+
6+
import python
7+
import semmle.python.ApiGraphs
8+
import semmle.python.dataflow.new.TaintTracking
9+
import semmle.python.dataflow.new.RemoteFlowSources
10+
11+
/**
12+
* Provides default sources, sinks and sanitizers for detecting
13+
* "PAM Authorization" vulnerabilities.
14+
*/
15+
module PamAuthorizationCustomizations {
16+
/**
17+
* Models a node corresponding to the `pam` library
18+
*/
19+
API::Node libPam() {
20+
exists(API::CallNode findLibCall, API::CallNode cdllCall |
21+
findLibCall =
22+
API::moduleImport("ctypes").getMember("util").getMember("find_library").getACall() and
23+
findLibCall.getParameter(0).getAValueReachingSink().asExpr().(StrConst).getText() = "pam" and
24+
cdllCall = API::moduleImport("ctypes").getMember("CDLL").getACall() and
25+
cdllCall.getParameter(0).getAValueReachingSink() = findLibCall
26+
|
27+
result = cdllCall.getReturn()
28+
)
29+
}
30+
31+
/**
32+
* A data flow source for "PAM Authorization" vulnerabilities.
33+
*/
34+
abstract class Source extends DataFlow::Node { }
35+
36+
/**
37+
* A data flow sink for "PAM Authorization" vulnerabilities.
38+
*/
39+
abstract class Sink extends DataFlow::Node { }
40+
41+
/**
42+
* A source of remote user input, considered as a flow source.
43+
*/
44+
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
45+
46+
/**
47+
* A vulnerable `pam_authenticate` call considered as a flow sink.
48+
*/
49+
class VulnPamAuthCall extends API::CallNode, Sink {
50+
VulnPamAuthCall() {
51+
exists(DataFlow::Node h |
52+
this = libPam().getMember("pam_authenticate").getACall() and
53+
h = this.getArg(0) and
54+
not exists(API::CallNode acctMgmtCall |
55+
acctMgmtCall = libPam().getMember("pam_acct_mgmt").getACall() and
56+
DataFlow::localFlow(h, acctMgmtCall.getArg(0))
57+
)
58+
)
59+
}
60+
}
61+
}
Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @name PAM authorization bypass due to incorrect usage
33
* @description Not using `pam_acct_mgmt` after `pam_authenticate` to check the validity of a login can lead to authorization bypass.
4-
* @kind problem
4+
* @kind path-problem
55
* @problem.severity warning
66
* @security-severity 8.1
77
* @precision high
@@ -11,28 +11,11 @@
1111
*/
1212

1313
import python
14+
import DataFlow::PathGraph
1415
import semmle.python.ApiGraphs
15-
import experimental.semmle.python.Concepts
16-
import semmle.python.dataflow.new.TaintTracking
16+
import semmle.python.security.dataflow.PamAuthorization::PamAuthorization
1717

18-
API::Node libPam() {
19-
exists(API::CallNode findLibCall, API::CallNode cdllCall |
20-
findLibCall = API::moduleImport("ctypes").getMember("util").getMember("find_library").getACall() and
21-
findLibCall.getParameter(0).getAValueReachingSink().asExpr().(StrConst).getText() = "pam" and
22-
cdllCall = API::moduleImport("ctypes").getMember("CDLL").getACall() and
23-
cdllCall.getParameter(0).getAValueReachingSink() = findLibCall
24-
|
25-
result = cdllCall.getReturn()
26-
)
27-
}
28-
29-
from API::CallNode authenticateCall, DataFlow::Node handle
30-
where
31-
authenticateCall = libPam().getMember("pam_authenticate").getACall() and
32-
handle = authenticateCall.getArg(0) and
33-
not exists(API::CallNode acctMgmtCall |
34-
acctMgmtCall = libPam().getMember("pam_acct_mgmt").getACall() and
35-
DataFlow::localFlow(handle, acctMgmtCall.getArg(0))
36-
)
37-
select authenticateCall,
38-
"This PAM authentication call may lead to an authorization bypass, since 'pam_acct_mgmt' is not called afterwards."
18+
from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
19+
where config.hasFlowPath(source, sink)
20+
select sink.getNode(), source, sink,
21+
"This PAM authentication call may lead to an authorization bypass, since `pam_acct_mgmt` is not called afterwards."
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,10 @@
1-
| pam_test.py:48:18:48:44 | ControlFlowNode for pam_authenticate() | This PAM authentication call may lead to an authorization bypass, since 'pam_acct_mgmt' is not called afterwards. |
1+
edges
2+
| pam_test.py:70:16:70:22 | ControlFlowNode for request | pam_test.py:70:16:70:27 | ControlFlowNode for Attribute |
3+
| pam_test.py:70:16:70:27 | ControlFlowNode for Attribute | pam_test.py:75:14:75:40 | ControlFlowNode for pam_authenticate() |
4+
nodes
5+
| pam_test.py:70:16:70:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
6+
| pam_test.py:70:16:70:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
7+
| pam_test.py:75:14:75:40 | ControlFlowNode for pam_authenticate() | semmle.label | ControlFlowNode for pam_authenticate() |
8+
subpaths
9+
#select
10+
| pam_test.py:75:14:75:40 | ControlFlowNode for pam_authenticate() | pam_test.py:70:16:70:22 | ControlFlowNode for request | pam_test.py:75:14:75:40 | ControlFlowNode for pam_authenticate() | This PAM authentication call may lead to an authorization bypass, since `pam_acct_mgmt` is not called afterwards. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
semmle-extractor-options: --lang=3 --max-import-depth=2 -p ../lib

python/ql/test/query-tests/Security/CWE-285-PamAuthorization/pam_test.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from ctypes import CDLL, POINTER, Structure, byref
22
from ctypes import c_char_p, c_int
33
from ctypes.util import find_library
4+
from flask import Flask, request, redirect
45

56

67
class PamHandle(Structure):
@@ -40,11 +41,11 @@ class PamConv(Structure):
4041

4142
class pam():
4243

43-
def authenticate_bad(self, username, service='login'):
44+
def authenticate_bad_but_good(self, username, service='login'):
4445
handle = PamHandle()
4546
conv = PamConv(None, 0)
4647
retval = pam_start(service, username, byref(conv), byref(handle))
47-
48+
# This is not fine but we don't alert here as there is a possibility that the function is not actually used
4849
retval = pam_authenticate(handle, 0)
4950
auth_success = retval == 0
5051

@@ -61,3 +62,31 @@ def authenticate_good(self, username, service='login'):
6162
auth_success = retval == 0
6263

6364
return auth_success
65+
66+
67+
app = Flask(__name__)
68+
@app.route('/bad')
69+
def bad():
70+
username = request.args.get('username', '')
71+
handle = PamHandle()
72+
conv = PamConv(None, 0)
73+
retval = pam_start(service, username, byref(conv), byref(handle))
74+
75+
retval = pam_authenticate(handle, 0)
76+
auth_success = retval == 0
77+
78+
return auth_success
79+
80+
@app.route('/good')
81+
def good():
82+
username = request.args.get('username', '')
83+
handle = PamHandle()
84+
conv = PamConv(None, 0)
85+
retval = pam_start(service, username, byref(conv), byref(handle))
86+
87+
retval = pam_authenticate(handle, 0)
88+
if retval == 0:
89+
retval = pam_acct_mgmt(handle, 0)
90+
auth_success = retval == 0
91+
92+
return auth_success

0 commit comments

Comments
 (0)