pwnkit + sudoedit_editor: ctx->host migration + 4 more tests (39 total)

pwnkit: migrate detect() to consult ctx->host->polkit_version with
the same graceful-fallback pattern as the sudo modules. The version
is populated once at startup by core/host.c (via pkexec --version);
detect() skips the per-scan popen when the host fingerprint has the
version. Falls back to the inline popen path when ctx->host is
missing the version (degenerate test contexts).

sudoedit_editor: already migrated; this commit adds direct test
coverage.

tests/test_detect.c expansion (35 → 39):
- pwnkit: polkit_version='0.105'  -> VULNERABLE (pre-0.121 fix)
- pwnkit: polkit_version='0.121'  -> OK (fix release)
- sudoedit_editor: vuln sudo + no sudoers grant -> PRECOND_FAIL
  (documented behaviour: vulnerable version, but the dispatcher
   has no usable sudoedit grant on the host)
- sudoedit_editor: fixed sudo (1.9.13p1) -> OK

The sudoedit_editor 'vuln + no grant' case is the first test to
exercise the second-level precondition gate AFTER the version
check passes — proves the version-pinned detect logic AND the
sudo -ln target-discovery short-circuit both work as intended.

The h_vuln_sudo / h_fixed_sudo synthetic fingerprints gained the
.polkit_version field alongside .sudo_version so a single fingerprint
exercises both pwnkit and the sudo modules.

Verification: 39/39 pass on Linux (docker gcc:latest + libglib2.0-dev
+ sudo, non-root user skeletonkeyci). macOS dev box still reports
'skipped — Linux-only' as designed.
This commit is contained in:
2026-05-23 00:15:01 -04:00
parent c63ee72aa1
commit 150f16bc97
2 changed files with 79 additions and 36 deletions
@@ -77,44 +77,58 @@ static bool pkexec_version_vulnerable(const char *version_str)
static skeletonkey_result_t pwnkit_detect(const struct skeletonkey_ctx *ctx)
{
const char *pkexec_path = find_pkexec();
if (!pkexec_path) {
/* Prefer the centrally-fingerprinted polkit version (populated
* once at startup by core/host.c via `pkexec --version`). Saves
* a popen per scan and lets unit tests construct synthetic
* polkit_version values. Fall back to the local popen if
* ctx->host is missing the version (degenerate test ctx or a
* future refactor that disables userspace probing). */
char vp_buf[64] = {0};
const char *vp = NULL;
if (ctx->host && ctx->host->polkit_version[0]) {
snprintf(vp_buf, sizeof vp_buf, "%s", ctx->host->polkit_version);
vp = vp_buf;
if (!ctx->json) {
fprintf(stderr, "[+] pwnkit: pkexec not installed; no attack surface\n");
fprintf(stderr, "[i] pwnkit: host fingerprint reports pkexec "
"version '%s'\n", vp);
}
} else {
const char *pkexec_path = find_pkexec();
if (!pkexec_path) {
if (!ctx->json) {
fprintf(stderr, "[+] pwnkit: pkexec not installed; no attack surface\n");
}
return SKELETONKEY_OK;
}
return SKELETONKEY_OK;
}
if (!ctx->json) {
fprintf(stderr, "[i] pwnkit: found setuid pkexec at %s\n", pkexec_path);
}
/* Run `pkexec --version` and parse. We pipe stderr/stdout to a
* temp file because popen() can have quoting quirks. */
char cmd[512];
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", pkexec_path);
FILE *p = popen(cmd, "r");
if (!p) return SKELETONKEY_TEST_ERROR;
char line[256] = {0};
char *r = fgets(line, sizeof line, p);
pclose(p);
if (!r) {
if (!ctx->json) {
fprintf(stderr, "[?] pwnkit: could not parse pkexec --version output\n");
fprintf(stderr, "[i] pwnkit: found setuid pkexec at %s\n", pkexec_path);
}
return SKELETONKEY_TEST_ERROR;
}
/* Output format: "pkexec version 0.105\n" or "pkexec version 0.120-..." */
char *vp = strstr(line, "version");
if (!vp) return SKELETONKEY_TEST_ERROR;
vp += strlen("version");
while (*vp == ' ' || *vp == '\t') vp++;
if (!ctx->json) {
char *nl = strchr(vp, '\n');
char cmd[512];
snprintf(cmd, sizeof cmd, "%s --version 2>&1 | head -1", pkexec_path);
FILE *p = popen(cmd, "r");
if (!p) return SKELETONKEY_TEST_ERROR;
char line[256] = {0};
char *r = fgets(line, sizeof line, p);
pclose(p);
if (!r) {
if (!ctx->json) {
fprintf(stderr, "[?] pwnkit: could not parse pkexec --version output\n");
}
return SKELETONKEY_TEST_ERROR;
}
/* Output format: "pkexec version 0.105\n" or "pkexec version 0.120-..." */
char *vp_mut = strstr(line, "version");
if (!vp_mut) return SKELETONKEY_TEST_ERROR;
vp_mut += strlen("version");
while (*vp_mut == ' ' || *vp_mut == '\t') vp_mut++;
char *nl = strchr(vp_mut, '\n');
if (nl) *nl = 0;
fprintf(stderr, "[i] pwnkit: pkexec reports version '%s'\n", vp);
snprintf(vp_buf, sizeof vp_buf, "%s", vp_mut);
vp = vp_buf;
if (!ctx->json) {
fprintf(stderr, "[i] pwnkit: pkexec reports version '%s'\n", vp);
}
}
bool vuln = pkexec_version_vulnerable(vp);