atp

Atp's external memory

Why setuid java programs don't work

Its a feature not a bug.

We do odd things with java, that most places would probably use C or python for. Having said that we have some very talented people who can make java do wonderful things.

However when those wonderful things require elevated privilege like CAP_NET_RAW for packet capture, we run into a problem, which is that the kernel treats any executable which has a capability assigned as being equivalent to one with the "setuid" bit set.

As Java loads shared libraries by relying on glibc doing expansion of the environment variable $ORIGIN  this can get broken by the setuid bit and some rules introduced to close an exploit.

So, at the command line as root;

root@foo:/tmp# ls -l /usr/lib/jvm/java-6-sun/jre/bin/java
-rwxr-xr-x 1 root root 50794 2011-02-03 01:25 /usr/lib/jvm/java-6-sun/jre/bin/java
root@foo:/tmp# getcap /usr/lib/jvm/java-6-sun/jre/bin/java
/usr/lib/jvm/java-6-sun/jre/bin/java = cap_net_raw+eip
root@foo:/tmp# strace java -version
:
open("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/tls/x86_64/ \
libpthread.so.0", O_RDONLY) = -1 ENOENT (No such file or directory)
stat("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/tls/x86_64", \
0x7fff0cc3b8f0) = -1 ENOENT (No such file or directory)
open("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/tls/ \
libpthread.so.0", O_RDONLY) = -1 ENOENT (No such file or directory)
stat("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/tls", \
0x7fff0cc3b8f0) = -1 ENOENT (No such file or directory)
open("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/x86_64/ \
libpthread.so.0", O_RDONLY) = -1 ENOENT (No such file or directory)
stat("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/x86_64", \
0x7fff0cc3b8f0) = -1 ENOENT (No such file or directory)
:

(Line breaks to make it look less ugly in the blog)

But as a user running the same command;

atp@foo:/tmp$ strace java -version 
open("$ORIGIN/../lib/amd64/jli/tls/x86_64/libpthread.so.0", O_RDONLY) = -1 ENOENT \
(No such file or directory)
open("$ORIGIN/../lib/amd64/jli/tls/libpthread.so.0", O_RDONLY) = -1 ENOENT \
(No such file or directory)
open("$ORIGIN/../lib/amd64/jli/x86_64/libpthread.so.0", O_RDONLY) = -1 ENOENT \
(No such file or directory)
open("$ORIGIN/../lib/amd64/jli/libpthread.so.0", O_RDONLY) = -1 ENOENT \
(No such file or directory)
open("$ORIGIN/../jre/lib/amd64/jli/tls/x86_64/libpthread.so.0", O_RDONLY) = -1 \
ENOENT (No such file or directory)

Or without the helpful strace command you get this cryptic error message;

atp@foo:~$ java -version
java: error while loading shared libraries: libjli.so: cannot open shared object \
 file: No such file or directory

The reason is here

http://seclists.org/fulldisclosure/2010/Oct/257

which quotes a web page at caldera thus;

For security, the dynamic linker does not allow use of $ORIGIN substitution
sequences for set-user and set-group ID programs. For such sequences that
appear within strings specified by DT_RUNPATH dynamic array entries, the
specific search path containing the $ORIGIN sequence is ignored (though other
search paths in the same string are processed). $ORIGIN sequences within a
DT_NEEDED entry or path passed as a parameter to dlopen() are treated as
errors.

In other words, this is a feature, not a bug. The feature was implemented in the commit referenced in [1].

So we have the situation where java uses $ORIGIN to allow the run time linker to link its shared libraries no matter where in the filesystem it is located. The run time linker thinks that its a setuid root binary as soon as we add a capability to it, and refuses to expand $ORIGIN, causing the link to fail. 

So why does linux think that having a capability set means the executable is suid root?

glibc checks the following (in elf/dl-sysdep.c)  and sets the value of __libc_enable_secure appropriately.

  1. The ELF AT_SECURE auxiliary vector
  2. The values of AT_UID/GID/EUID/EGID auxiliary vectors
 __libc_enable_secure = uid != euid || gid != egid;

Using a program that prints out the values of the auxiliary vectors (We can't use LD_SHOW_AUXV, because the secure mode disables that environment variable too (see [2])) we can see that;

atp@foo:~$ /sbin/getcap test_cap
test_cap = cap_net_raw+eip
atp@foo:~$ ls -l test_cap
-rwxr-xr-x 1 root atp 8608 2011-05-26 18:10 test_cap
atp@foo:~$ ./test_cap
current uid 500, effective uid 500
AT_SYSINFO_EHDR: 140735961518080
AT_PHDR: 4194368
AT_PHNUM: 9
AT_ENTRY: 4195552
AT_UID: 500
AT_EUID: 500
AT_GID: 500
AT_EGID: 500
AT_SECURE: 1

The kernel is setting it for us. A quick bit of chasing later through binfmt_elf.c and we end up at cap_bprm_secureexec()

/**
570 * cap_bprm_secureexec - Determine whether a secure execution is required
571 * @bprm: The execution parameters
572 *
573 * Determine whether a secure execution is required, return 1 if it is, and 0
574 * if it is not.
575 *
576 * The credentials have been committed by this point, and so are no longer
577 * available through @bprm->cred.
578 */
579int cap_bprm_secureexec(struct linux_binprm *bprm)
580{
581 const struct cred *cred = current_cred();
582
583 if (cred->uid != 0) {
584 if (bprm->cap_effective)
585 return 1;
586 if (!cap_isclear(cred->cap_permitted))
587 return 1;
588 }
589
590 return (cred->euid != cred->uid ||
591 cred->egid != cred->gid);
592}
593

Which as expected checks to see if we're not root, and if we have capabilities. As a result of this, AT_SECURE gets set in binfmt_elf.c and when the dynamic linker goes into "secure" mode, which disables $ORIGIN (plus other environment variables). This then prevents java from loading its shared objects, and it all falls over.

At the end of this journey we're left with two options.

  1. The kernel should be more discerning about the type of capability before pressing the AT_SECURE panic button.
  2. Java shouldn't rely on $ORIGIN expansion when loading shared libraries (although that complicates things enormously for them).

Neither of those look like a quick fix. (And fooling around with /etc/suid-debug didn't help either.).

[1] glibc patch http://www.cygwin.com/ml/libc-hacker/2010-10/msg00007.html

[2] glibc environment variables http://www.scratchbox.org/documentation/general/tutorials/glibcenv.html

Written by atp

Wednesday 25 May 2011 at 1:57 pm

Posted in Default

Leave a Reply