sudo-rs icon indicating copy to clipboard operation
sudo-rs copied to clipboard

Malicious or misconfigured PAM module can DOS sudo-rs via num_msg=-1

Open Pingasmaster opened this issue 3 months ago • 2 comments

PAM modules can send a num_msg, which is handled improperly by sudo-rs, as it is assumed this is a positive number without any checks.

Negative PAM message counts cause integer underflow and crash in sudo-rs (see src/pam/converse.rs lines 184 to 260 below). The conversation handler casts num_msg: c_int to usize without bounds checking; -1 becomes usize::MAX, so Vec::with_capacity(num_msg as usize) panics with a capacity overflow. That panic is caught, sets the panicked flag, and PamContext::authenticate immediately panic!s, killing sudo-rs. A single malicious PAM module (or one compromised via another bug) can therefore DOS sudo-rs, preventing ANY sudo-rs command from any user. Valid PAM modules never send a negative count, but a hostile, misconfigured (this is how I caught it) or compromised module can.

To Reproduce

Spooky C code ahead.

  • Install or compile sudo-rs (obvious)
# malicious PAM module that crashes sudo-rs
tee /tmp/pam_crash.c <<'EOF'
#define _GNU_SOURCE
#include <security/pam_modules.h>
#include <stddef.h>

PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,
                                   int argc, const char **argv) {
    const struct pam_conv *conv;
    if (pam_get_item(pamh, PAM_CONV, (const void **)&conv) == PAM_SUCCESS && conv && conv->conv) {
        conv->conv(-1, NULL, NULL, conv->appdata_ptr);   // send num_msg = -1
    }
    return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags,
                              int argc, const char **argv) {
    return PAM_SUCCESS;
}
EOF
# compile
gcc -fPIC -shared -o /tmp/pam_crash.so /tmp/pam_crash.c -lpam
# Adds "auth  requisite  /tmp/pam_crash.so" at the top of /etc/pam.d/sudo, basically gets the module into PAM flow
sudo-rs sed -i '1i\auth  requisite  /tmp/pam_crash.so' /etc/pam.d/sudo

Result: After any sudo-rs command, PAM panics after a successful login:

[alice@pc]$ sudo-rs su

thread 'main' panicked at src/pam/converse.rs:191:29:
capacity overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
[sudo: authenticate] Password: 

thread 'main' panicked at src/pam/mod.rs:160:13:
Panic during pam authentication

Expected behavior No crash, maybe a discard of num_msg or a warning.

Environment (please complete the following information):

  • Linux distribution: Validated on Arch linux, should happen on all distros.
  • sudo-rs commit hash: bug working on v0.2.9, latest git tree commit 2b4d403. Didn't check when this was introduced.

Additional context Code affected:

/src/pam/converse.rs lines 198 forward:

      let result = std::panic::catch_unwind(|| {
          let mut resp_bufs = Vec::with_capacity(num_msg as usize);
          for i in 0..num_msg as usize {
              // convert the input messages to Rust types
              // SAFETY: the PAM contract ensures that `num_msg` does not exceed the amount
              // of messages presented to this function in `msg`, and that it is not being
              // written to at the same time as we are reading it. Note that the reference
              // we create does not escape this loopy body.
              let message: &pam_message = unsafe { &**msg.add(i) };

              // SAFETY: PAM ensures that the messages passed are properly null-terminated
              let msg = unsafe { string_from_ptr(message.msg) };
              let style = if let Some(style) = PamMessageStyle::from_int(message.msg_style) {
                  style
              } else {
                  // early return if there is a failure to convert, pam would have given us nonsense
                  return PamErrorType::ConversationError;
              };

              // send the conversation off to the Rust part
              // SAFETY: appdata_ptr contains the `*mut ConverserData` that is untouched by PAM
              let app_data = unsafe { &mut *(appdata_ptr as *mut ConverserData<C>) };
              match handle_message(app_data, style, &msg) {
                  Ok(resp_buf) => {
                      resp_bufs.push(resp_buf);
                  }
                  Err(PamError::TimedOut) => {
                      app_data.timed_out = true;
                      return PamErrorType::ConversationError;
                  }
                  Err(_) => return PamErrorType::ConversationError,
              }
          }

          // Allocate enough memory for the responses, which are initialized with zero.
          // SAFETY: this will either allocate the required amount of (initialized) bytes,
          // or return a null pointer.
          let temp_resp = unsafe {
              libc::calloc(
                  num_msg as libc::size_t,
                  std::mem::size_of::<pam_response>() as libc::size_t,
              )
          } as *mut pam_response;
          if temp_resp.is_null() {
              return PamErrorType::BufferError;
          }

See my suggested patch : https://github.com/trifectatechfoundation/sudo-rs/pull/1312

Pingasmaster avatar Oct 28 '25 18:10 Pingasmaster

PAM modules are assumed to be neither malicious nor compromised. A malicious or compromised PAM module can trivially cause a denial of service or grant an attacker root permissions. Your PR only catches when num_msg is negative, but it a malicious/compromised PAM module can just as easily return an incorrect positive value, which can cause memory corruption too. There is no way for the user of PAM to handle this kinds of issues in the general case. Also returning a conversation error will just as much cause a denial of service for sudo-rs as the allocation failure currently does. That said, an allocation failure is not a great error message, so I guess checking that num_msg is between 1 and PAM_MAX_NUM_MSG would be fine.

bjorn3 avatar Oct 29 '25 10:10 bjorn3

I think i'd prefer a solution that Bjorn suggested, e.g. check that the response falls in the accepted range (0.. MAX_MSG)

On the other hand, this scenario is pretty remote: it means PAM isn't abiding by its function contract, so I'm reclassifying this issue.

squell avatar Nov 05 '25 14:11 squell