LibreOffice Macro Code Execution

ID AKB:D63A8152-FF88-423F-B246-DE1CF9DEE44E
Type attackerkb
Reporter AttackerKB
Modified 2020-07-30T00:00:00


It was found that libreoffice before versions 6.0.7 and 6.1.3 was vulnerable to a directory traversal attack which could be used to execute arbitrary macros bundled with a document. An attacker could craft a document, which when opened by LibreOffice, would execute a Python method from a script in any arbitrary file system location, specified relative to the LibreOffice install location.

Recent assessments:

space-r7 at May 09, 2019 5:57pm UTC reported:


LibreOffice offers the ability to create program events that when triggered, will execute a macro. LibreOffice gives the option to develop custom macros or select a macro from a list of scripts included with the installation. The included macros are written in a variety of languages, including Python.
Creating a mouse over event that will execute a macro upon hovering over a hyperlink will result in XML that looks similar to this:

<script:event-listener script:language="ooo:script" script:event-name="dom:mouseover" xlink:href="|$createTable?language=Python&location=share" xlink:type="simple"/>

Alex Inführ discovered that a directory traversal vulnerability exists in the xlink:href attribute, allowing the ability to call functions (with its arguments) of other Python scripts included with the LibreOffice installation. The tempfilepager() function in program/python-core-3.5.5/lib/ was found to both accept function arguments and pass those arguments to os.system(), allowing for arbitrary code execution.

def tempfilepager(text, cmd):
    """Page through text by invoking a program on a temporary file."""
    import tempfile
    filename = tempfile.mktemp()
    with open(filename, 'w', errors='backslashreplace') as file:
        os.system(cmd + ' "' + filename + '"')

The directory traversal vulnerability stems from how the URI in the xlink:href attribute is converted to the actual URI of the Python script on disk. The function that does this conversion is located in program/ called scriptURI2StorageUri().

def scriptURI2StorageUri( self, scriptURI ):
        myUri = self.m_uriRefFac.parse(scriptURI)
        ret = self.m_baseUri + "/" + myUri.getName().replace( "|", "/" )
        log.debug( "converting scriptURI="+scriptURI + " to storageURI=" + ret )
        return ret
    except UnoException as e:
        log.error( "error during converting scriptURI="+scriptURI + ": " + e.Message)
        raise RuntimeException( "pythonscript:scriptURI2StorageUri: " +e.getMessage(), None )
    except Exception as e:
        log.error( "error during converting scriptURI="+scriptURI + ": " + str(e))
        raise RuntimeException( "pythonscript:scriptURI2StorageUri: " + str(e), None )

The scriptURI variable passed to the function is the attacker-controlled path. In the line ret = self.m_baseUri + "/" + myUri.getName().replace( "|", "/" ), the local scripts path gets built. m_baseUri, the base installation path, gets concatenated with a / and the controllable path (with removed) after any | characters are replaced with /.

The final storage URI ret would look like this on a Linux LibreOffice installation:

file:///opt/libreoffice6.1/share/Scripts/python/../../../../program/python-core-3.5.5/lib/$tempfilepager(ARG1, ARG2)

Assessed Attacker Value: 4
Assessed Attacker Value: 4Assessed Attacker Value: 3