oscap file checks

introduction

For years now there have been widespread security standards for checking OS configuration and one framework of standards is Security Content Automation Protocol [1] with a set of XML definitions of desired states and code to check and report on installations as they relate to those states. So far as that idea goes that's a good thing - but what about the policy details that are checked?

state of the oscap

I had look at filemode checks [2] as seen in the latest Fedora (26 alpha) and found the following:

Ensure that Root's Path Does Not Include World or Group-Writable Directories
Rule ID accounts_root_path_dirs_no_write
Result
pass
Time    2017-05-...
Severity        low
Identifiers and References

References:  CM-6(b), 366
Description

For each element in root's path, run:

$ sudo ls -ld DIR

and ensure that write permissions are disabled for group and other.

Rationale

Such entries increase the risk that root could execute code provided by unprivileged users, and potentially malicious code.

Ensure that Root's Path Does Not Include World or Group-Writable Directories    low pass

what does it need?

The check "For each element in root's path, run sudo ls -ld DIR" is inadequate as I can demonstrate with some examples.

[root@fedora26 ~]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/opt/silly/bin
[root@fedora26 ~]# ls -lad /opt/silly/bin /opt/silly /opt /
dr-xr-xr-x. 17 root root 224 May  8 20:07 /
drwxr-xr-x.  3 root root  19 May  9 01:16 /opt
drwxrwxrwx.  3 root root  17 May  9 01:16 /opt/silly
drwxr-xr-x.  2 root root   6 May  9 01:16 /opt/silly/bin
Any user can take steps like these to replace the directory used by root.
cd /opt/silly
mv bin oldbin
mkdir bin
In fact this is so well-known it was tested in the COPS software written in shell in 1989 [3] (even before it was ported to Perl). All components of the full pathname need to be tested.

what else does it need?

What happens if a component is a symbolic link? One test per component of the pathname is no longer enough.
[root@fedora26 opt]# find /opt -type l -ls
  5109911      0 lrwxrwxrwx   1  root     root           14 May  9 09:12 /opt/brillig/bin -> /opt/silly/bin
  9456624      0 lrwxrwxrwx   1  root     root           12 May  9 09:12 /opt/slithy/bin -> ../silly/bin
  5109913      0 lrwxrwxrwx   1  root     root            5 May  9 09:13 /opt/mimsy -> silly

All four of the names /opt/*/bin/sleep refer to the same file all under the world-writable directory /opt/silly. With all four of these in root's PATH scap still finds the test has been passed because it is not the single filemode on /opt/*/bin/ that is world-writable.

what have I done before?

In a previous job I implemented (in Perl) checks that cover these kinds of pathnames. Because all the symbolic links in my example lead to use of another directory that pathname also needs testing. Ultimately /opt/brillig/bin/sleep is world-writable because /opt/silly is.

In my past work my code stepped alng the pathnames and printed output when it found something to report but this is not ideal. For instance when processing root's cron in some cases the commands in it would be like

su - joeuser -c '/opt/appname/bin/outgrabe'
which wants testing for writability by possible adversaries of joeuser rather than root. I had to make a second pass over the text to remove warnings in that case. It's better to gather all the findings into data structures, analyse them and produce output when the discovery stage is finished. I had some other details such as allowing for sticky bits and ignoring write permission bits on filesystems mounted read-only.

what comes next?

OSCAP uses Python so a new test of writable pathnames should be written in Python and evaluated against test cases like the above.

Rather than test root's PATH I have made a start at this problem by extracting absolute program names from ps output. Running all of the test versions of sleep gets me output like this:

$ /usr/bin/ps -eo uid,command | egrep 'UID|sleep'
  UID COMMAND
    0 /opt/brillig/bin/sleep 3600
    0 /opt/mimsy/bin/sleep 3600
    0 /opt/silly/bin/sleep 3600
    0 /opt/slithy/bin/sleep 3600
    2 /opt/slithy/bin/sleep 3600
I also changed the owner of sleep to uid 2. Now output from my simple scanner reports each of the five processe like this.
$ ./read_ps.py 
Danger from  /opt/brillig/bin/sleep  access for  ['/opt/brillig/bin/sleep', 2, None, None]
0 seen in ps running /opt/brillig/bin/sleep
Danger from  /opt/brillig/bin/sleep  access for  ['/opt/silly', 0, 0, 'world-write']
0 seen in ps running /opt/brillig/bin/sleep
Danger from  /opt/mimsy/bin/sleep  access for  ['/opt/mimsy/bin/sleep', 2, None, None]
0 seen in ps running /opt/mimsy/bin/sleep
Danger from  /opt/mimsy/bin/sleep  access for  ['/opt/silly', 0, 0, 'world-write']
0 seen in ps running /opt/mimsy/bin/sleep
Danger from  /opt/silly/bin/sleep  access for  ['/opt/silly/bin/sleep', 2, None, None]
0 seen in ps running /opt/silly/bin/sleep
Danger from  /opt/silly/bin/sleep  access for  ['/opt/silly', 0, 0, 'world-write']
0 seen in ps running /opt/silly/bin/sleep
Danger from  /opt/slithy/bin/sleep  access for  ['/opt/slithy/bin/sleep', 2, None, None]
0 seen in ps running /opt/slithy/bin/sleep
Danger from  /opt/slithy/bin/sleep  access for  ['/opt/slithy/../silly', 0, 0, 'world-write']
0 seen in ps running /opt/slithy/bin/sleep
Danger from  /opt/slithy/bin/sleep  access for  ['/opt/slithy/../silly', 0, 0, 'world-write']
2 seen in ps running /opt/slithy/bin/sleep
Explained this comes to:
  1. /opt/brillig/bin/sleep has uid=2, no group-write access, no world-write access
  2. /opt/silly has uid=0, group-write for group 0, and has world-write.
and the program is used by uid 0 (having been seen in ps output) So we have a sleep program run by uid 0 but owned by uid 2 and under a directory with mode 0777.

Output is similar for most of the other processes except that the one running as uid 2 has only the report of the world-writable directory and not the file owned by uid 2 - a total of 9 findings from 5 processes.

Code on github [4]

possible scap integration

Code such as the above could be prepared to work with oscap and XML definitions for file access checks. I have not yet dug into how to do that but it is on my list of possible tasks. A smart implementation might even get different batches of checks to share the state discovered and minimise the number of lstat() calls.

In my opinon it would be an improvement to have all checks for writability use a module that understands how to process a complete pathname.


footnotes

  1. Security Content Automation Protocol
  2. procedure for running a SCAP scan
  3. COPS by Dan Farmer
  4. code on github

  5. Written by Peter M Allan. 2017
    linkedin back to articles