Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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>
58 changes: 58 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,58 @@
/**
* @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
* @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

private class LibPam extends API::Node {
LibPam() {
Comment thread
This conversation was marked as resolved.
Resolved
exists(
API::Node cdll, API::Node find_library, API::Node libpam, API::CallNode cdll_call,
API::CallNode find_lib_call, StrConst str
|
API::moduleImport("ctypes").getMember("CDLL") = cdll and
find_library = API::moduleImport("ctypes.util").getMember("find_library") and
cdll_call = cdll.getACall() and
find_lib_call = find_library.getACall() and
DataFlow::localFlow(DataFlow::exprNode(str), find_lib_call.getArg(0)) and
str.getText() = "pam" and
cdll_call.getArg(0) = find_lib_call and
libpam = cdll_call.getReturn()
|
libpam = this
)
Comment thread
This conversation was marked as resolved.
Resolved
}

override string toString() { result = "libpam" }
}

class PamAuthCall extends API::Node {
PamAuthCall() { exists(LibPam pam | pam.getMember("pam_authenticate") = this) }

override string toString() { result = "pam_authenticate" }
}

class PamActMgt extends API::Node {
PamActMgt() { exists(LibPam pam | pam.getMember("pam_acct_mgmt") = this) }

override string toString() { result = "pam_acct_mgmt" }
}

from PamAuthCall p, API::CallNode u, Expr handle
Comment thread
This conversation was marked as resolved.
Resolved
where
u = p.getACall() and
handle = u.asExpr().(Call).getArg(0) and
not exists(PamActMgt pam |
DataFlow::localFlow(DataFlow::exprNode(handle),
DataFlow::exprNode(pam.getACall().asExpr().(Call).getArg(0)))
)
Comment thread
This conversation was marked as resolved.
Resolved
select u, "This PAM authentication call may be lead to an authorization bypass."
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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
Comment thread
This conversation was marked as resolved.
Resolved
pam_authenticate.restype = c_int
pam_authenticate.argtypes = [PamHandle, c_int]
pam_acct_mgmt.restype = c_int
pam_acct_mgmt.argtypes = [PamHandle, c_int]
Comment thread
This conversation was marked as resolved.
Resolved

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 @@
| bad.py:92:18:92: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
95 changes: 95 additions & 0 deletions python/ql/test/experimental/query-tests/Security/CWE-285/bad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, byref, sizeof
from ctypes import c_void_p, c_size_t, c_char_p, c_char, c_int
from ctypes import memmove
from ctypes.util import find_library

class PamHandle(Structure):
_fields_ = [ ("handle", c_void_p) ]

def __init__(self):
Structure.__init__(self)
self.handle = 0

class PamMessage(Structure):
"""wrapper class for pam_message structure"""
_fields_ = [ ("msg_style", c_int), ("msg", c_char_p) ]

def __repr__(self):
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)

class PamResponse(Structure):
"""wrapper class for pam_response structure"""
_fields_ = [ ("resp", c_char_p), ("resp_retcode", c_int) ]

def __repr__(self):
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)

conv_func = CFUNCTYPE(c_int, c_int, POINTER(POINTER(PamMessage)), POINTER(POINTER(PamResponse)), c_void_p)

class PamConv(Structure):
"""wrapper class for pam_conv structure"""
_fields_ = [ ("conv", conv_func), ("appdata_ptr", c_void_p) ]

# Various constants
PAM_PROMPT_ECHO_OFF = 1
PAM_PROMPT_ECHO_ON = 2
PAM_ERROR_MSG = 3
PAM_TEXT_INFO = 4
PAM_REINITIALIZE_CRED = 8

libc = CDLL(find_library("c"))
libpam = CDLL(find_library("pam"))

calloc = libc.calloc
calloc.restype = c_void_p
calloc.argtypes = [c_size_t, c_size_t]

# bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this function
if hasattr(libpam, 'pam_end'):
pam_end = libpam.pam_end
pam_end.restype = c_int
pam_end.argtypes = [PamHandle, c_int]

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

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

pam_strerror = libpam.pam_strerror
pam_strerror.restype = c_char_p
pam_strerror.argtypes = [PamHandle, c_int]

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():
code = 0
reason = None

def __init__(self):
pass

def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
@conv_func
def my_conv(n_messages, messages, p_response, app_data):
return 0


cpassword = c_char_p(password)

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

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

return auth_success
97 changes: 97 additions & 0 deletions python/ql/test/experimental/query-tests/Security/CWE-285/good.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, byref, sizeof
from ctypes import c_void_p, c_size_t, c_char_p, c_char, c_int
from ctypes import memmove
from ctypes.util import find_library

class PamHandle(Structure):
_fields_ = [ ("handle", c_void_p) ]

def __init__(self):
Structure.__init__(self)
self.handle = 0

class PamMessage(Structure):
"""wrapper class for pam_message structure"""
_fields_ = [ ("msg_style", c_int), ("msg", c_char_p) ]

def __repr__(self):
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)

class PamResponse(Structure):
"""wrapper class for pam_response structure"""
_fields_ = [ ("resp", c_char_p), ("resp_retcode", c_int) ]

def __repr__(self):
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)

conv_func = CFUNCTYPE(c_int, c_int, POINTER(POINTER(PamMessage)), POINTER(POINTER(PamResponse)), c_void_p)

class PamConv(Structure):
"""wrapper class for pam_conv structure"""
_fields_ = [ ("conv", conv_func), ("appdata_ptr", c_void_p) ]

# Various constants
PAM_PROMPT_ECHO_OFF = 1
PAM_PROMPT_ECHO_ON = 2
PAM_ERROR_MSG = 3
PAM_TEXT_INFO = 4
PAM_REINITIALIZE_CRED = 8

libc = CDLL(find_library("c"))
libpam = CDLL(find_library("pam"))

calloc = libc.calloc
calloc.restype = c_void_p
calloc.argtypes = [c_size_t, c_size_t]

# bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this function
if hasattr(libpam, 'pam_end'):
pam_end = libpam.pam_end
pam_end.restype = c_int
pam_end.argtypes = [PamHandle, c_int]

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

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

pam_strerror = libpam.pam_strerror
pam_strerror.restype = c_char_p
pam_strerror.argtypes = [PamHandle, c_int]

Comment thread
This conversation was marked as resolved.
Resolved
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():
code = 0
reason = None

def __init__(self):
pass

def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
@conv_func
def my_conv(n_messages, messages, p_response, app_data):
return 0


cpassword = c_char_p(password)

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)
auth_success = retval == 0

return auth_success
Comment thread
This conversation was marked as resolved.
Resolved