Fail to seteuid in java by calling JNI - java

My app need to run as none-root user but need to switch to other user to execute some commands.
I tried to:
write a JNI,
JNIEXPORT void JNICALL Java_SetUIDJNI_setuid(JNIEnv *env, jobject thisObj,jstring uname) {
const char *name = jstringTostring(env,uname);
int pid;
struct passwd *p;
show_ids();
if ((p = getpwnam(name)) == NULL) {
perror(name);
exit(EXIT_FAILURE);
}
pid = (int) p->pw_uid;
printf("pid=%d\n",pid);
if (seteuid (pid) < 0) {
perror ("setuid");
exit (EXIT_FAILURE);
}
show_ids();
}
build it as root and chmod u+s
-rwsr-xr-x 1 root root 76155 Aug 7 16:56 libsetuid.so*
call it in java
api = new SetUIDJNI(); // invoke the native method
api.setuid("slurm");
But if run it as none-root, it does not work
/opt/jdk1.8/jre/bin/java -Djava.library.path=../jni HelloJNI
The real user ID is: 1000
The effective user ID is :1000 <= which expected is 0
setuid: Operation not permitted
But it works if runner is root
The real user ID is: 0
The effective user ID is :0
pid=1002The real user ID is: 0
The effective user ID is :1002
Anything wrong here?
UPDATE
Modify the JNI part to executable c
void show_ids (void)
{
printf ("The real user ID is: %d\n", getuid());
printf ("The effective user ID is :%d\n", geteuid());
}
int main(void)
{
show_ids();
if (seteuid (1002) < 0) {
perror ("setuid");
exit (EXIT_FAILURE);
}
show_ids();
return (0);
}
Build it as root and run chmod u+s
-rwsr-xr-x 1 root root 8814 Aug 9 11:44 a.out*
Run it as normal user and works
./a.out
The real user ID is: 1000
The effective user ID is :0
The real user ID is: 1000
The effective user ID is :1002

The reason that seteuid was not working for you from JNI was that the effective user id for the JVM process was not 0 (root).
You were apparently attempting to make the effective user id by setting the "setuid" bit on your native library. That won't work. Similarly, making the JAR file (or a class file) as setuid won't work. The setuid bit is only meaningful on a executable file; i.e. a file that the OS itself knows how to execute.
So how do we implement "setuid root behavior" for a Java program?
In theory, you could mark the /usr/bin/java as setuid to root. Don't do that! If you do that, every Java program you run will be run as root. That would be bad.
In theory, you could write a shell script to launch your application; e.g.
#!/bin/sh
java some.pkg.Main "$#"
and mark the script as setuid to root. Don't do that! Setuid shell scripts are a security risk. (On some versions of Linux / UNIX the setuid bit is not respected for shell scripts anyway.)
The solution is to write a custom JVM launcher in native code, compile and link it, and make the launcher executable setuid. Note that the launcher must be written very carefully to project against someone subverting it. For example:
It should ignore CLASSPATH environment variable, and should not allow the classpath to be supplied any other way.
It should not take a user-supplied JAR file as a parameter.
It should take care that the user can't trick it into running the wrong Java code by interfering with the path to the JAR file.
And other things ... that I haven't thought of!
In fact, it is probably safer to run the Java application as a privileged user ... and not rely on "setuid root" at all.

Related

Java/JavaFX ProcessHandle possibly not finding all processes (Linux/Debian)

I've a JavaFX application where I've a list of a bunch of script files. Once the application loads, it reads it and and checks which ones are running.
To do that I use a ProcessHandle, as mentioned in various examples here on StackOverflow and other guides/tutorials on the internet.
The problem is, it never finds any of them. There for I programmatically started one, which I know for a fact that it will be running, via Process process = new ProcessBuilder("/path/to/file/my_script.sh").start(); - and it won't find this one either.
Contents of my_script.sh:
#!/bin/bash
echo "Wait for 5 seconds"
sleep 5
echo "Completed"
Java code:
// List of PIDs which correspond to the processes shown after "INFO COMMAND:"
System.out.println("ALL PROCESSES: " + ProcessHandle.allProcesses().toList());
Optional<ProcessHandle> scriptProcessHandle = ProcessHandle.allProcesses().filter(processHandle -> {
System.out.println("INFO COMMAND: " + processHandle.info().command());
Optional<String> processOptional = processHandle.info().command();
return processOptional.isPresent() && processOptional.get().equals("my_script.sh");
}).findFirst();
System.out.println("Script process handle is present: " + scriptProcessHandle.isPresent());
if (scriptProcessHandle.isPresent()) { // Always false
// Do stuff
}
Thanks to the good old fashioned System.out.println(), I noticed that I get this in my output console every time:
ALL PROCESSES: [1, 2, 28, 85, 128, 6944, 21174, 29029, 29071]
INFO COMMAND: Optional[/usr/bin/bwrap]
INFO COMMAND: Optional[/usr/bin/bash]
INFO COMMAND: Optional[/app/idea-IC/jbr/bin/java]
INFO COMMAND: Optional[/app/idea-IC/bin/fsnotifier]
INFO COMMAND: Optional[/home/username/.jdks/openjdk-17.0.2/bin/java]
INFO COMMAND: Optional[/usr/bin/bash]
INFO COMMAND: Optional[/home/username/.jdks/openjdk-17.0.2/bin/java]
INFO COMMAND: Optional[/home/username/.jdks/openjdk-17.0.2/bin/java]
INFO COMMAND: Optional[/usr/bin/bash]
Script process handle is present: false
The first line in the Javadoc of ProcessHandle.allProcess() reads:
Returns a snapshot of all processes visible to the current process.
So how come I can't see the rest of the operating system's processes?
I'm looking for a non-os-dependent solution, if possible. Why? For better portability and hopefully less maintenance in the future.
Notes:
A popular solution for GNU/Linux seems to be to check the proc entries, but I don't know if that would work for at least the majority of the most popular distributions - if it doesn't, adding support for them in a different way, would create more testing and maintenance workload.
I'm aware of ps, windir, tasklist.exe possible solutions (worst comes to worst).
I found the JavaSysMon library but it seems dead and unfortunately:
CPU speed on Linux only reports correct values for Intel CPUs
Edit 1:
I'm on Pop_OS! and installed IntelliJ via the PopShop as flatpak.
In order to start it as root as suggested by mr mcwolf, I went to /home/username/.local/share/flatpak/app/com.jetbrains.IntelliJ-IDEA-Community/x86_64/stable/active/export/bin and found com.jetbrains.IntelliJ-IDEA-Community file.
When I run sudo ./com.jetbrains.IntelliJ-IDEA-Community or sudo /usr/bin/flatpak run --branch=stable --arch=x86_64 com.jetbrains.IntelliJ-IDEA-Community in my terminal, I get error: app/com.jetbrains.IntelliJ-IDEA-Community/x86_64/stable not installed
So I opened the file and ran its contents:
exec /usr/bin/flatpak run --branch=stable --arch=x86_64 com.jetbrains.IntelliJ-IDEA-Community "$#"
This opens IntelliJ, but not as root, so instead I ran:
exec sudo /usr/bin/flatpak run --branch=stable --arch=x86_64 com.jetbrains.IntelliJ-IDEA-Community "$#"
Which prompts for a password and when I write it in, the terminal crashes.
Edit 1.1:
(╯°□°)╯︵ ┻━┻ "flatpak run" is not intended to be ran with sudo
Edit 2:
As mr mcwolf said, I downloaded the IntelliJ from the official website, extracted it and ran the idea.sh as root.
Now a lot more processes are shown. 1/3 of them show up as INFO COMMAND: Optional.empty.
scriptProcessHandle.isPresent() is still unfortunately returning false. I searched through them and my_script.sh is nowhere to be found. I also tried processOptional.isPresent() && processOptional.get().equals("/absolute/path/to/my_script.sh") but I still get false on isPresent() and it's not in the list of shown processes.
Though the last sentence might be a different problem. I'll do more digging.
Edit 3:
Combining .commandLine() and .contains() (instead of .equals()) solves the problem mentioned in "Edit 2".
Optional<ProcessHandle> scriptProcessHandle = ProcessHandle.allProcesses().filter(processHandle -> {
System.out.println("INFO COMMAND LINE: " + processHandle.info().commandLine());
Optional<String> processOptional = processHandle.info().commandLine();
return processOptional.isPresent() && processOptional.get().contains("/absolute/path/to/my_script.sh");
}).findFirst();
System.out.println("Script process handle is present: " + scriptProcessHandle.isPresent());
if (scriptProcessHandle.isPresent()) { // Returns true
// Do stuff
}
.commandLine() also shows script arguments, so that must be kept in mind.

`touch` a file that uses ACL on Linux causes "Operation not permitted"

In Java code I want to "touch" a file. I want to update the timestamps to the current time. The file uses ACL. And this seems to be the problem.
The file:
$ ll file.xml
-rw-rwxrw-+ 1 root root 8611 Oct 4 17:28 file.xml
$ getfacl file.xml
# file: file.xml
# owner: root
# group: root
user::rw-
user:tomcat8:rwx
group::r-x
mask::rwx
other::rw-
And my Java app runs from Tomcat 8 with user tomcat8. A sudo -u tomcat8 touch file.xml works. It also works if I completely remove ACL and set tomcat8 as owner. But this is not possible in the production environment.
So at first I tried Apache common-io:
FileUtils.touch(path);
This causes an IOException. I debugged it a bit more and found out that the library calls FileSystem.setLastModifiedTime which calls the Linux function utimes.
I debugged the Linux touch command and saw it calls another more modern function: utimensat(0, NULL, NULL, 0). It also calls dup2and duplicates the file descriptor.
So I built my own touch method in Java:
long time = System.currentTimeMillis();
FileTime fileTimeNow = FileTime.fromMillis(time);
BasicFileAttributeView fileAttributeView = Files.getFileAttributeView(derivative.toPath(), BasicFileAttributeView.class);
fileAttributeView.setTimes(fileTimeNow, fileTimeNow, fileTimeNow);
This throws an Exception too (Operation not permitted).
Internally it calls utimensat(69, NULL, [{1538666780, 483000000}, {1538666780, 483000000}], 0).
I can not set null on .setTimes(...). This call gets ignored. And there is no Java-way to duplicate a file descriptor (dup2). So I can not test further steps to make it more like Linux' touch.
How to make this working when a file uses ACL? I don't want to run external programs (touch).
If the file is not written concurrently, you can open it, read its first byte, and write it back again at offset zero. This will update the modification time of the file without requiring ownership permissions.
(By the way, the ACL looks really curious, particularly the other:: rw- part.)
Here's man utimensat:
Permissions requirements
To set both file timestamps to the current time (i.e., times is NULL, or both tv_nsec fields specify UTIME_NOW), either:
the caller must have write access to the file;
the caller's effective user ID must match the owner of the file; or
the caller must have appropriate privileges.
To make any change other than setting both timestamps to the current time (i.e., times is not NULL, and neither tv_nsec field is UTIME_NOW and neither tv_nsec field is UTIME_OMIT), either condition 2 or 3 above must
apply.
You have #1, but not #2 or #3. If you ask touch to explicitly set the time to the current timestamp, it fails as well:
$ getfacl test | sed -e "s/$USER/myuser/"
# file: test
# owner: root
# group: root
user::rw-
user:myuser:rwx
group::r--
mask::rwx
other::r--
$ touch -d "#$(date +%s)" test
touch: setting times of ‘test’: Operation not permitted
I don't have any good suggestions for what to do instead though. You could either make a no-op change to the file, or call touch as an external command:
String path="some path";
// See https://stackoverflow.com/a/52651585 for why we're not doing this via Java
int result = Runtime.getRuntime().exec(new String[] { "touch", "--", path }).waitFor();
if(result != 0) throw new IOException("Can't update timestamp");

how to pass the user and pwd via the runtime.exec() in java

I'm trying to run a simple command using java 1.8 and OS Solaris 11.
My program runs under a particular user and the command must run under SuperUser
here is the command:
Runtime.getRuntime().exec("su - root -c 'pargs -l 1111'");
if i run the command in shall its work fine and ask for password and wen i enter the password i will get the result.
the problem is wen i run it in java
here is my code
Process proc = Runtime.getRuntime().exec("su - root -c 'pargs -l 1111'");
PrintWriter out = new PrintWriter(new OutputStreamWriter(proc.getOutputStream()));
out.println(password);
out.flush();
int exitCode= proc.waitFor();
System.out.println(exitCode);//exitCode = 1
BufferedReader pArgs= new BufferedReader( new InputStreamReader(proc.getInputStream()));
if((line=pArgs.readLine()) != null)
{
//do something
}
else
{
//something not working = ERROR
}
i think that the line equal to null because something in the set of the password is not correct bat i'm not sure
what i'm doing wrong?
I might suggest a different way around this issue altogether. Instead of trying to run a shell command that dynamically asks for a password, make the command not require a password.
If its just asking for a password because it needs root, you can add a line in the sudoers file under root to say that your program user is allowed to execute that one specific command as if they were root: https://www.linux.com/blog/configuring-linux-sudoers-file.
This would be more secure too as you wouldn't have the password floating around code.
how to pass the user and pwd via the runtime.exec() in java
You can't under Solaris if you want to use su.
Solaris su uses the getpass() function to get the necessary password from the user.
From the Solaris getpass() man page:
Description
The getpass() function opens the process's controlling terminal, writes to that device the null-terminated string prompt, disables echoing, reads a string of characters up to the next newline character or EOF, restores the terminal state and closes the terminal.
...
Errors
The getpass() and getpassphrase() functions may fail if:
...
ENXIO
The process does not have a controlling terminal.
su will either get the password from the controlling terminal, or it will fail.
This is a deliberate design decision to make it almost impossible to perform insecure actions such as automated password entry.
Thank you very much for all the answers. But my solution was a little different.
It was decided to use an external file that could be written and read from both processes.
The whole goal was to do a handshake again in case the process running at the root will fall (watchdog).
So now there is no need to use the command
Runtime.getRuntime().exec("su - root -c 'pargs -l 1111'");
When the root process starts running, it records a time signature into a file.
and if the process of the user (who reads the file every X time) finds that the signature has changed, he will do a handshake again.

Manage Java process with go

I have JVM and all dependencies for my Java program ready. With Java I would run like:
javac HelloWorld.java
java HelloWorld
Now I want to, in Linux environment, control this Java program processes using Go's cmd package. In Go, when you run command you are given the PID. With this PID, I want to terminate the Java program whenever j want and restart using the same cmd package. Would this work correctly as long as I have JVM installed? I want to do:
cmd := exec.Command("bash", "-c", " "java HelloWorld")
cmd.Start()
syscall.Kill(cmd.Process.Pid)
Thanks!
In short, yes.
As a test, with added interrupt handling so your own Go process doesn't terminate this will work:
package main
import (
"os/exec"
"syscall"
"os"
"os/signal"
"fmt"
)
func main() {
cmd := exec.Command("bash", "-c", "java HelloWorld")
err := cmd.Start()
fmt.Printf("Starting java proccess with pid %d\n", cmd.Process.Pid)
if err != nil {
// do something about it
}
c := make(chan os.Signal, 1)
done := make(chan bool, 1)
signal.Notify(c, os.Interrupt)
signal.Notify(c, syscall.SIGTERM)
go func() {
<-c
fmt.Printf("Sending interrupt to pid: %d\n", cmd.Process.Pid)
syscall.Kill(cmd.Process.Pid, syscall.SIGHUP)
done <- true
}()
<-done
}
Companion Java class:
public class HelloWorld {
public static void main(String[] args) throws Exception {
System.out.println("Hello World from Go! But you cant see me :)");
while (true) {
System.out.println("you cant see this because I am outing to the STDOUT of a subshell!");
Thread.sleep(5000);
}
}
}
But it is full of gotchas. As long as your Go process exits normally, it will send the signal you specify (sighup would be natural choice, if I'd venture a guess) to the java pid. But you need to ensure that you wont let a zombie in case your own Go process crash or in case your java application hangs on after failing to shut down cleanly when you tell it to. Saving that pid to a /tmp/ file and doing all sorts of things with it in case of a restart could be interesting, but you know your needs.
Edit: controlling a JVM process from another program might get finicky quick. You should evaluate if you really want to do that. If you are in Linux, I'd take a look at the SysV init/systemd/upstart/start-stop-daemon system your distro uses if your companion java program acts as a daemon.

odd .bat file behavior

I have a bat file with the following contents:
set logfile= D:\log.txt
java com.stuff.MyClass %1 %2 %3 >> %logfile%
when I run the bat file though, I get the following:
C:\>set logfile= D:\log.txt
C:\>java com.stuff.MyClass <val of %1> <val of %2> <val of %3> 1>>D:\log.txt
The parameter is incorrect.
I'm almost positive the "The parameter is incorrect." is due to the extraneous 1 in there. I also think this might have something with the encoding of the .bat file, but I can't quite figure out what is causing it. Anyone ever run into this before or know what might be causing it and how to fix it?
Edit
And the lesson, as always, is check if its plugged in first before you go asking for help. The bat file, in version control, uses D:\log.txt because it is intended to be run from the server which contains a D drive. When testing my changes and running locally, on my computer which doesn't have a D drive, I failed to make the change to use C:\log.txt which is what caused the error. Sorry for wasting you time, thanks for the help, try to resist the urge to downvote me too much.
I doubt that that's the problem - I expect the command processor to deal with that part for you.
Here's evidence of it working for me:
Test.java:
public class Test
{
public static void main(String args[]) throws Exception
{
System.out.println(args.length);
for (String arg : args)
{
System.out.println(arg);
}
}
}
test.bat:
set logfile= c:\users\jon\test\test.log
java Test %1 %2 %3 >> %logfile%
On the command line:
c:\Users\Jon\Test> [User input] test.bat first second third
c:\Users\Jon\Test>set logfile= c:\users\jon\test\test.log
c:\Users\Jon\Test>java Test first second third 1>>c:\users\jon\test\test.log
c:\Users\Jon\Test> [User input] type test.log
3
first
second
third
the 1 is not extraneous: it is inserted by cmd.exe meaning stdout (instead of ">>", you can also write "1>>". contrast this to redirecting stderr: "2>>"). so the problem must be with your parameters.
This may seem like a stupid question, but is there an existing D: drive in the context that the bat file runs in?
Once I had a case where a bat file was used as the command line of a task within the Task Manager, but the Run As user was set to a local user on the box, giving no access to network drives.
Interpolated for your case, if the D: drive were a network drive, running the bat file as, say, the local administrator account on that machine instead of a domain user account would likely fail to have access to D:.

Categories