Repository navigation
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
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
9203304
Python : Add query to detect PAM authorization bypass
785dc1a
Include changes from review
6235dc5
Python: Handle `find_library` assignment to temp variable
RasmusWL ab1252d
Python: Add @precision high for `py/pam-auth-bypass`
RasmusWL c218162
Merge branch 'main' into pypam
RasmusWL File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
49 changes: 49 additions & 0 deletions
49
python/ql/src/experimental/Security/CWE-285/PamAuthorization.qhelp
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
58
python/ql/src/experimental/Security/CWE-285/PamAuthorization.ql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() { | ||
| 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 | ||
| ) | ||
|
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 | ||
|
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))) | ||
| ) | ||
|
This conversation was marked as resolved.
Resolved
|
||
| select u, "This PAM authentication call may be lead to an authorization bypass." | ||
15 changes: 15 additions & 0 deletions
15
python/ql/src/experimental/Security/CWE-285/PamAuthorizationBad.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
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] | ||
|
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 | ||
17 changes: 17 additions & 0 deletions
17
python/ql/src/experimental/Security/CWE-285/PamAuthorizationGood.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
1 change: 1 addition & 0 deletions
1
python/ql/test/experimental/query-tests/Security/CWE-285/PamAuthorization.expected
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | |
1 change: 1 addition & 0 deletions
1
python/ql/test/experimental/query-tests/Security/CWE-285/PamAuthorization.qlref
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
95
python/ql/test/experimental/query-tests/Security/CWE-285/bad.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
97
python/ql/test/experimental/query-tests/Security/CWE-285/good.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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] | ||
|
|
||
|
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 | ||
|
This conversation was marked as resolved.
Resolved
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.