10000 Python : Add query to detect PAM authorization bypass · Pull Request #8595 · github/codeql · GitHub
[go: up one dir, main page]

Skip to content

Python : Add query to detect PAM authorization bypass #8595

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from May 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
Using only a call to
<code>pam_authenticate</code>
to check the validity of a login can lead to authorization bypass vulnerabilities.
</p>
<p>
A
<code>pam_authenticate</code>
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.
</p>

</overview>

<recommendation>
<p>
A call to
<code>pam_authenticate</code>
should be followed by a call to
<code>pam_acct_mgmt</code>
to check if a user is allowed to login.
</p>
</recommendation>

<example>
<p>
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
<code>chage -E0 `username` </code>
and then trying to log in.
</p>
<sample src="PamAuthorizationBad.py" />

<p>
This can be avoided by calling
<code>pam_acct_mgmt</code>
call to verify access as has been done in the snippet shown below.
</p>
<sample src="PamAuthorizationGood.py" />
</example>

<references>
<li>
Man-Page:
<a href="https://man7.org/linux/man-pages/man3/pam_acct_mgmt.3.html">pam_acct_mgmt</a>
</li>
</references>
</qhelp>
36 changes: 36 additions & 0 deletions python/ql/src/experimental/Security/CWE-285/PamAuthorization.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @name Authorization bypass due to incorrect usage of PAM
* @description Using only the `pam_authenticate` call to check the validity of a login can lead to a authorization bypass.
* @kind problem
* @problem.severity warning
* @precision high
* @id py/pam-auth-bypass
* @tags security
* external/cwe/cwe-285
*/

import python
import semmle.python.ApiGraphs
import experimental.semmle.python.Concepts
import semmle.python.dataflow.new.TaintTracking

API::Node libPam() {
exists(API::CallNode findLibCall, API::CallNode cdllCall |
findLibCall = API::moduleImport("ctypes.util").getMember("find_library").getACall() and
findLibCall.getParameter(0).getAValueReachingRhs().asExpr().(StrConst).getText() = "pam" and
cdllCall = API::moduleImport("ctypes").getMember("CDLL").getACall() and
cdllCall.getParameter(0).getAValueReachingRhs() = findLibCall
|
result = cdllCall.getReturn()
)
}

from API::CallNode authenticateCall, DataFlow::Node handle
where
authenticateCall = libPam().getMember("pam_authenticate").getACall() and
handle = authenticateCall.getArg(0) and
not exists(API::CallNode acctMgmtCall |
acctMgmtCall = libPam().getMember("pam_acct_mgmt").getACall() and
DataFlow::localFlow(handle, acctMgmtCall.getArg(0))
)
select authenticateCall, "This PAM authentication call may be lead to an authorization bypass."
13 changes: 13 additions & 0 deletions python/ql/src/experimental/Security/CWE-285/PamAuthorizationBad.py 10000
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
libpam = CDLL(find_library("pam"))
pam_authenticate = libpam.pam_authenticate
pam_authenticate.restype = c_int
pam_authenticate.argtypes = [PamHandle, c_int]


handle = PamHandle()
conv = PamConv(my_conv, 0)
retval = pam_start(service, username, byref(conv), byref(handle))

retval = pam_authenticate(handle, 0)
return retval == 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
libpam = CDLL(find_library("pam"))
pam_authenticate = libpam.pam_authenticate
pam_acct_mgmt = libpam.pam_acct_mgmt
pam_authenticate.restype = c_int
pam_authenticate.argtypes = [PamHandle, c_int]
pam_acct_mgmt.restype = c_int
pam_acct_mgmt.argtypes = [PamHandle, c_int]

handle = PamHandle()
conv = PamConv(my_conv, 0)
retval = pam_start(service, username, byref(conv), byref(handle))

retval = pam_authenticate(handle, 0)
if retval == 0:
retval = pam_acct_mgmt(handle, 0)
return retval == 0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
| pam_test.py:48:18:48:44 | ControlFlowNode for pam_authenticate() | This PAM authentication call may be lead to an authorization bypass. |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
experimental/Security/CWE-285/PamAuthorization.ql
D7AE
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from ctypes import CDLL, POINTER, Structure, byref
from ctypes import c_char_p, c_int
from ctypes.util import find_library


class PamHandle(Structure):
pass


class PamMessage(Structure):
pass


class PamResponse(Structure):
pass


class PamConv(Structure):
pass

# this is normal way to do things
libpam = CDLL(find_library("pam"))

# but we also handle assignment to temp variable
temp = find_library("pam")
libpam = CDLL(temp)

pam_start = libpam.pam_start
pam_start.restype = c_int
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]

pam_authenticate = libpam.pam_authenticate
pam_authenticate.restype = c_int
pam_authenticate.argtypes = [PamHandle, c_int]

pam_acct_mgmt = libpam.pam_acct_mgmt
pam_acct_mgmt.restype = c_int
pam_acct_mgmt.argtypes = [PamHandle, c_int]


class pam():

def authenticate_bad(self, username, service='login'):
handle = PamHandle()
conv = PamConv(None, 0)
retval = pam_start(service, username, byref(conv), byref(handle))

retval = pam_authenticate(handle, 0)
auth_success = retval == 0

return auth_success

def authenticate_good(self, username, service='login'):
handle = PamHandle()
conv = PamConv(None, 0)
retval = pam_start(service, username, byref(conv), byref(handle))

retval = pam_authenticate(handle, 0)
if retval == 0:
retval = pam_acct_mgmt(handle, 0)
auth_success = retval == 0

return auth_success
0