Skip to content

Commit cb17e2a

Browse files
authored
Merge pull request #8595 from porcupineyhairs/pypam
Python : Add query to detect PAM authorization bypass
2 parents 09360bc + c218162 commit cb17e2a

File tree

7 files changed

+180
-0
lines changed

7 files changed

+180
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
Using only a call to
6+
<code>pam_authenticate</code>
7+
to check the validity of a login can lead to authorization bypass vulnerabilities.
8+
</p>
9+
<p>
10+
A
11+
<code>pam_authenticate</code>
12+
only verifies the credentials of a user. It does not check if a user has an appropriate authorization to actually login. This means a user with a expired login or a password can still access the system.
13+
</p>
14+
15+
</overview>
16+
17+
<recommendation>
18+
<p>
19+
A call to
20+
<code>pam_authenticate</code>
21+
should be followed by a call to
22+
<code>pam_acct_mgmt</code>
23+
to check if a user is allowed to login.
24+
</p>
25+
</recommendation>
26+
27+
<example>
28+
<p>
29+
In the following example, the code only checks the credentials of a user. Hence, in this case, a user expired with expired creds can still login. This can be verified by creating a new user account, expiring it with
30+
<code>chage -E0 `username` </code>
31+
and then trying to log in.
32+
</p>
33+
<sample src="PamAuthorizationBad.py" />
34+
35+
<p>
36+
This can be avoided by calling
37+
<code>pam_acct_mgmt</code>
38+
call to verify access as has been done in the snippet shown below.
39+
</p>
40+
<sample src="PamAuthorizationGood.py" />
41+
</example>
42+
43+
<references>
44+
<li>
45+
Man-Page:
46+
<a href="https://man7.org/linux/man-pages/man3/pam_acct_mgmt.3.html">pam_acct_mgmt</a>
47+
</li>
48+
</references>
49+
</qhelp>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @name Authorization bypass due to incorrect usage of PAM
3+
* @description Using only the `pam_authenticate` call to check the validity of a login can lead to a authorization bypass.
4+
* @kind problem
5+
* @problem.severity warning
6+
* @precision high
7+
* @id py/pam-auth-bypass
8+
* @tags security
9+
* external/cwe/cwe-285
10+
*/
11+
12+
import python
13+
import semmle.python.ApiGraphs
14+
import experimental.semmle.python.Concepts
15+
import semmle.python.dataflow.new.TaintTracking
16+
17+
API::Node libPam() {
18+
exists(API::CallNode findLibCall, API::CallNode cdllCall |
19+
findLibCall = API::moduleImport("ctypes.util").getMember("find_library").getACall() and
20+
findLibCall.getParameter(0).getAValueReachingRhs().asExpr().(StrConst).getText() = "pam" and
21+
cdllCall = API::moduleImport("ctypes").getMember("CDLL").getACall() and
22+
cdllCall.getParameter(0).getAValueReachingRhs() = findLibCall
23+
|
24+
result = cdllCall.getReturn()
25+
)
26+
}
27+
28+
from API::CallNode authenticateCall, DataFlow::Node handle
29+
where
30+
authenticateCall = libPam().getMember("pam_authenticate").getACall() and
31+
handle = authenticateCall.getArg(0) and
32+
not exists(API::CallNode acctMgmtCall |
33+
acctMgmtCall = libPam().getMember("pam_acct_mgmt").getACall() and
34+
DataFlow::localFlow(handle, acctMgmtCall.getArg(0))
35+
)
36+
select authenticateCall, "This PAM authentication call may be lead to an authorization bypass."
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
2+
libpam = CDLL(find_library("pam"))
3+
pam_authenticate = libpam.pam_authenticate
4+
pam_authenticate.restype = c_int
5+
pam_authenticate.argtypes = [PamHandle, c_int]
6+
7+
8+
handle = PamHandle()
9+
conv = PamConv(my_conv, 0)
10+
retval = pam_start(service, username, byref(conv), byref(handle))
11+
12+
retval = pam_authenticate(handle, 0)
13+
return retval == 0
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
2+
libpam = CDLL(find_library("pam"))
3+
pam_authenticate = libpam.pam_authenticate
4+
pam_acct_mgmt = libpam.pam_acct_mgmt
5+
pam_authenticate.restype = c_int
6+
pam_authenticate.argtypes = [PamHandle, c_int]
7+
pam_acct_mgmt.restype = c_int
8+
pam_acct_mgmt.argtypes = [PamHandle, c_int]
9+
10+
handle = PamHandle()
11+
conv = PamConv(my_conv, 0)
12+
retval = pam_start(service, username, byref(conv), byref(handle))
13+
14+
retval = pam_authenticate(handle, 0)
15+
if retval == 0:
16+
retval = pam_acct_mgmt(handle, 0)
17+
return retval == 0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
| pam_test.py:48:18:48:44 | ControlFlowNode for pam_authenticate() | This PAM authentication call may be lead to an authorization bypass. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE-285/PamAuthorization.ql
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from ctypes import CDLL, POINTER, Structure, byref
2+
from ctypes import c_char_p, c_int
3+
from ctypes.util import find_library
4+
5+
6+
class PamHandle(Structure):
7+
pass
8+
9+
10+
class PamMessage(Structure):
11+
pass
12+
13+
14+
class PamResponse(Structure):
15+
pass
16+
17+
18+
class PamConv(Structure):
19+
pass
20+
21+
# this is normal way to do things
22+
libpam = CDLL(find_library("pam"))
23+
24+
# but we also handle assignment to temp variable
25+
temp = find_library("pam")
26+
libpam = CDLL(temp)
27+
28+
pam_start = libpam.pam_start
29+
pam_start.restype = c_int
30+
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
31+
32+
pam_authenticate = libpam.pam_authenticate
33+
pam_authenticate.restype = c_int
34+
pam_authenticate.argtypes = [PamHandle, c_int]
35+
36+
pam_acct_mgmt = libpam.pam_acct_mgmt
37+
pam_acct_mgmt.restype = c_int
38+
pam_acct_mgmt.argtypes = [PamHandle, c_int]
39+
40+
41+
class pam():
42+
43+
def authenticate_bad(self, username, service='login'):
44+
handle = PamHandle()
45+
conv = PamConv(None, 0)
46+
retval = pam_start(service, username, byref(conv), byref(handle))
47+
48+
retval = pam_authenticate(handle, 0)
49+
auth_success = retval == 0
50+
51+
return auth_success
52+
53+
def authenticate_good(self, username, service='login'):
54+
handle = PamHandle()
55+
conv = PamConv(None, 0)
56+
retval = pam_start(service, username, byref(conv), byref(handle))
57+
58+
retval = pam_authenticate(handle, 0)
59+
if retval == 0:
60+
retval = pam_acct_mgmt(handle, 0)
61+
auth_success = retval == 0
62+
63+
return auth_success

0 commit comments

Comments
 (0)