Skip to content

Commit 9203304

Browse files
author
Porcupiney Hairs
committed
Python : Add query to detect PAM authorization bypass
Using only a call to `pam_authenticate` to check the validity of a login can lead to authorization bypass vulnerabilities. A `pam_authenticate` 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. This PR includes a qhelp describing the issue, a query which detects instances where a call to `pam_acc_mgmt` does not follow a call to `pam_authenticate` and it's corresponding tests. This PR has multiple detections. Some of the public one I can find are : * [CVE-2022-0860](https://nvd.nist.gov/vuln/detail/CVE-2022-0860) found in [cobbler/cobbler](https://www.github.com/cobbler/cobbler) * [fredhutch/motuz](https://www.huntr.dev/bounties/d46f91ca-b8ef-4b67-a79a-2420c4c6d52b/)
1 parent e5ac492 commit 9203304

File tree

8 files changed

+333
-0
lines changed

8 files changed

+333
-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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
* @id py/pam-auth-bypass
7+
* @tags security
8+
* external/cwe/cwe-285
9+
*/
10+
11+
import python
12+
import semmle.python.ApiGraphs
13+
import experimental.semmle.python.Concepts
14+
import semmle.python.dataflow.new.TaintTracking
15+
16+
private class LibPam extends API::Node {
17+
LibPam() {
18+
exists(
19+
API::Node cdll, API::Node find_library, API::Node libpam, API::CallNode cdll_call,
20+
API::CallNode find_lib_call, StrConst str
21+
|
22+
API::moduleImport("ctypes").getMember("CDLL") = cdll and
23+
find_library = API::moduleImport("ctypes.util").getMember("find_library") and
24+
cdll_call = cdll.getACall() and
25+
find_lib_call = find_library.getACall() and
26+
DataFlow::localFlow(DataFlow::exprNode(str), find_lib_call.getArg(0)) and
27+
str.getText() = "pam" and
28+
cdll_call.getArg(0) = find_lib_call and
29+
libpam = cdll_call.getReturn()
30+
|
31+
libpam = this
32+
)
33+
}
34+
35+
override string toString() { result = "libpam" }
36+
}
37+
38+
class PamAuthCall extends API::Node {
39+
PamAuthCall() { exists(LibPam pam | pam.getMember("pam_authenticate") = this) }
40+
41+
override string toString() { result = "pam_authenticate" }
42+
}
43+
44+
class PamActMgt extends API::Node {
45+
PamActMgt() { exists(LibPam pam | pam.getMember("pam_acct_mgmt") = this) }
46+
47+
override string toString() { result = "pam_acct_mgmt" }
48+
}
49+
50+
from PamAuthCall p, API::CallNode u, Expr handle
51+
where
52+
u = p.getACall() and
53+
handle = u.asExpr().(Call).getArg(0) and
54+
not exists(PamActMgt pam |
55+
DataFlow::localFlow(DataFlow::exprNode(handle),
56+
DataFlow::exprNode(pam.getACall().asExpr().(Call).getArg(0)))
57+
)
58+
select u, "This PAM authentication call may be lead to an authorization bypass."
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
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+
| bad.py:92:18:92: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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, byref, sizeof
2+
from ctypes import c_void_p, c_size_t, c_char_p, c_char, c_int
3+
from ctypes import memmove
4+
from ctypes.util import find_library
5+
6+
class PamHandle(Structure):
7+
_fields_ = [ ("handle", c_void_p) ]
8+
9+
def __init__(self):
10+
Structure.__init__(self)
11+
self.handle = 0
12+
13+
class PamMessage(Structure):
14+
"""wrapper class for pam_message structure"""
15+
_fields_ = [ ("msg_style", c_int), ("msg", c_char_p) ]
16+
17+
def __repr__(self):
18+
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
19+
20+
class PamResponse(Structure):
21+
"""wrapper class for pam_response structure"""
22+
_fields_ = [ ("resp", c_char_p), ("resp_retcode", c_int) ]
23+
24+
def __repr__(self):
25+
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
26+
27+
conv_func = CFUNCTYPE(c_int, c_int, POINTER(POINTER(PamMessage)), POINTER(POINTER(PamResponse)), c_void_p)
28+
29+
class PamConv(Structure):
30+
"""wrapper class for pam_conv structure"""
31+
_fields_ = [ ("conv", conv_func), ("appdata_ptr", c_void_p) ]
32+
33+
# Various constants
34+
PAM_PROMPT_ECHO_OFF = 1
35+
PAM_PROMPT_ECHO_ON = 2
36+
PAM_ERROR_MSG = 3
37+
PAM_TEXT_INFO = 4
38+
PAM_REINITIALIZE_CRED = 8
39+
40+
libc = CDLL(find_library("c"))
41+
libpam = CDLL(find_library("pam"))
42+
43+
calloc = libc.calloc
44+
calloc.restype = c_void_p
45+
calloc.argtypes = [c_size_t, c_size_t]
46+
47+
# bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this function
48+
if hasattr(libpam, 'pam_end'):
49+
pam_end = libpam.pam_end
50+
pam_end.restype = c_int
51+
pam_end.argtypes = [PamHandle, c_int]
52+
53+
pam_start = libpam.pam_start
54+
pam_start.restype = c_int
55+
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
56+
57+
pam_setcred = libpam.pam_setcred
58+
pam_setcred.restype = c_int
59+
pam_setcred.argtypes = [PamHandle, c_int]
60+
61+
pam_strerror = libpam.pam_strerror
62+
pam_strerror.restype = c_char_p
63+
pam_strerror.argtypes = [PamHandle, c_int]
64+
65+
pam_authenticate = libpam.pam_authenticate
66+
pam_authenticate.restype = c_int
67+
pam_authenticate.argtypes = [PamHandle, c_int]
68+
69+
pam_acct_mgmt = libpam.pam_acct_mgmt
70+
pam_acct_mgmt.restype = c_int
71+
pam_acct_mgmt.argtypes = [PamHandle, c_int]
72+
73+
class pam():
74+
code = 0
75+
reason = None
76+
77+
def __init__(self):
78+
pass
79+
80+
def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
81+
@conv_func
82+
def my_conv(n_messages, messages, p_response, app_data):
83+
return 0
84+
85+
86+
cpassword = c_char_p(password)
87+
88+
handle = PamHandle()
89+
conv = PamConv(my_conv, 0)
90+
retval = pam_start(service, username, byref(conv), byref(handle))
91+
92+
retval = pam_authenticate(handle, 0)
93+
auth_success = retval == 0
94+
95+
return auth_success
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, byref, sizeof
2+
from ctypes import c_void_p, c_size_t, c_char_p, c_char, c_int
3+
from ctypes import memmove
4+
from ctypes.util import find_library
5+
6+
class PamHandle(Structure):
7+
_fields_ = [ ("handle", c_void_p) ]
8+
9+
def __init__(self):
10+
Structure.__init__(self)
11+
self.handle = 0
12+
13+
class PamMessage(Structure):
14+
"""wrapper class for pam_message structure"""
15+
_fields_ = [ ("msg_style", c_int), ("msg", c_char_p) ]
16+
17+
def __repr__(self):
18+
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
19+
20+
class PamResponse(Structure):
21+
"""wrapper class for pam_response structure"""
22+
_fields_ = [ ("resp", c_char_p), ("resp_retcode", c_int) ]
23+
24+
def __repr__(self):
25+
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
26+
27+
conv_func = CFUNCTYPE(c_int, c_int, POINTER(POINTER(PamMessage)), POINTER(POINTER(PamResponse)), c_void_p)
28+
29+
class PamConv(Structure):
30+
"""wrapper class for pam_conv structure"""
31+
_fields_ = [ ("conv", conv_func), ("appdata_ptr", c_void_p) ]
32+
33+
# Various constants
34+
PAM_PROMPT_ECHO_OFF = 1
35+
PAM_PROMPT_ECHO_ON = 2
36+
PAM_ERROR_MSG = 3
37+
PAM_TEXT_INFO = 4
38+
PAM_REINITIALIZE_CRED = 8
39+
40+
libc = CDLL(find_library("c"))
41+
libpam = CDLL(find_library("pam"))
42+
43+
calloc = libc.calloc
44+
calloc.restype = c_void_p
45+
calloc.argtypes = [c_size_t, c_size_t]
46+
47+
# bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this function
48+
if hasattr(libpam, 'pam_end'):
49+
pam_end = libpam.pam_end
50+
pam_end.restype = c_int
51+
pam_end.argtypes = [PamHandle, c_int]
52+
53+
pam_start = libpam.pam_start
54+
pam_start.restype = c_int
55+
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
56+
57+
pam_setcred = libpam.pam_setcred
58+
pam_setcred.restype = c_int
59+
pam_setcred.argtypes = [PamHandle, c_int]
60+
61+
pam_strerror = libpam.pam_strerror
62+
pam_strerror.restype = c_char_p
63+
pam_strerror.argtypes = [PamHandle, c_int]
64+
65+
pam_authenticate = libpam.pam_authenticate
66+
pam_authenticate.restype = c_int
67+
pam_authenticate.argtypes = [PamHandle, c_int]
68+
69+
pam_acct_mgmt = libpam.pam_acct_mgmt
70+
pam_acct_mgmt.restype = c_int
71+
pam_acct_mgmt.argtypes = [PamHandle, c_int]
72+
73+
class pam():
74+
code = 0
75+
reason = None
76+
77+
def __init__(self):
78+
pass
79+
80+
def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
81+
@conv_func
82+
def my_conv(n_messages, messages, p_response, app_data):
83+
return 0
84+
85+
86+
cpassword = c_char_p(password)
87+
88+
handle = PamHandle()
89+
conv = PamConv(my_conv, 0)
90+
retval = pam_start(service, username, byref(conv), byref(handle))
91+
92+
retval = pam_authenticate(handle, 0)
93+
if retval == 0:
94+
retval = pam_acct_mgmt(handle, 0)
95+
auth_success = retval == 0
96+
97+
return auth_success

0 commit comments

Comments
 (0)