I am trying to create a executor program for regular users on linux with SUID bit set so whatever commands, passed to the program as parameters, get executed with root permission. However when I try to implement this as a bash script, this does not work, where it works when implemented in C. I want to know what I am doing wrong for the shell script. The codes are below
Shell Script:
#! /bin/bash
if [ $# -lt 1 ]; then
echo "Usage: $0 <Command String>"
exit 1
fi
$#
#Also tried this, same result
#exec $#
Execution:
root#: chmod 755 exec.sh
root#: chmod u+s exec.sh
root#: ll exec.sh
-rwsr-xr-x 1 root root 75 Sep 19 16:55 exec.sh
regular_user$: ./exec.sh whoami
regular_user
C Program:
#include <stdlib.h>
#include <stdio.h>
int main ( int argc, char *argv[] )
{
if ( argc < 2 ) {
printf( "Usage: %s <Command String>\n", argv[0] );
return 1;
}
else
{
argv[argc]=NULL;
//setuid(0); //Works without these
//setgid(0);
int exit=execvp(argv[1], argv+1);
return exit;
}
}
Execution:
root#: gcc exec.c -o exec.obj
root#: chmod 755 exec.obj
root#: chmod u+s exec.obj
root#: ll exec.obj
-rwsr-xr-x 1 root root 6979 Sep 19 17:03 exec.obj
regular_user$: ./exec.obj whoami
root
Both files have identical permissions
-rwsr-xr-x 1 root root 75 Sep 19 16:55 exec.sh
-rwsr-xr-x 1 root root 6979 Sep 19 17:03 exec.obj
It is documented in execve(2) :
Linux ignores the set-user-ID and set-group-ID bits on scripts.
IIRC, setuid scripts would be a significant security hole
See this question
You could configure sudo to avoid asking a password - see sudoers(5) (or use super)
You could also write a simple C program wrapping your shell script, and make it setuid.
try
regular_user$: sudo "./exec.sh whoami"
The reason is explain by RedHat at https://access.redhat.com/solutions/124693 :
When executing shell scripts that have the setuid bit (e.g., perms of rwsr-xr-x), the scripts run as the user that executes them, not as the user that owns them. This is contrary to how setuid is handled for binaries (e.g., /usr/bin/passwd), which run as the user that owns them, regardless of which user executes them.
In order to solve this issue I write a script utility which converts a script call to a native binary:
#!/bin/bash
# https://access.redhat.com/site/solutions/124693
if [ $# != 1 ]; then
echo "Please, provide script file name." >&2
exit 1
fi
if [ ${EUID} != 0 ]; then
echo "Only root can run this script." >&2
exit 1
fi
SCRIPT_FILE=$1
if [ ! -f "${SCRIPT_FILE}" ]; then
echo "Script file not found." >&2
exit 1
fi
SCRIPT_BASE_FILE=$(basename ${SCRIPT_FILE})
C_TEMPLATE=$(cat << DELIMITER
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
char *cwd;
char exe_file[PATH_MAX];
char script_file[PATH_MAX];
readlink("/proc/self/exe", exe_file, PATH_MAX);
cwd = dirname(exe_file);
sprintf(script_file, "%s/${SCRIPT_BASE_FILE}", cwd);
setuid(0);
system(script_file);
return 0;
}
DELIMITER
)
C_FILE="${SCRIPT_FILE}.c"
EXE_FILE="${SCRIPT_FILE}.x"
echo "${C_TEMPLATE}" > "${C_FILE}" \
&& gcc "${C_FILE}" -o "${EXE_FILE}" \
&& chown root:root "${EXE_FILE}" \
&& chmod 4755 "${EXE_FILE}" \
&& rm "${C_FILE}" \
&& echo "Setuid script executable created as \"${EXE_FILE}\"."
Related
I wrote a shell script that reads files in /proc and /sys (for example).
The script uses if [ -r "$1" ]; then to test if a file is readable before trying to read it.
Unfortunately it seems that test -r is succeeding, even if the file has no read permission (e.g. "--w-------"), but still an actual read (e.g. file - <"$1") fails with "Permission denied".
(My guess is that test assumes root can read every file)
An example for such a file would be /sys/devices/virtual/net/br0/bridge/flush in Linux.
How can I test the "readable" property properly (and efficiently) in a shell script?
More Details
# ll -d /sys/devices/virtual/net/br0/bridge /sys/devices/virtual/net/br0/bridge/flush
drwxr-xr-x 2 root root 0 Mar 1 01:37 /sys/devices/virtual/net/br0/bridge
--w------- 1 root root 4096 Mar 1 07:58 /sys/devices/virtual/net/br0/bridge/flush
If you don't care about who can read the file (i.e. where the 'r' exists in permission bits), then you could use something like this:
if [[ `ls -l "$1" | cut -d " " -f1` =~ "r" ]]; then echo "Readable"; else echo "Unreadable"; fi
Otherwise, if you want to test a specific read bit (e.g. owner), then you could use something like this:
P=`ls -l "$1" | cut -d " " -f1`
if [[ "${P:1:1}" == 'r' ]]; then echo "Readable"; else echo "Unreadable"; fi
Alternatively, you could test the file readability as the user willing to run the command file - <"$1" as follows:
sudo -i -u someuser bash << EOF
if [ -r "$1" ]; then echo "Readable"; else echo "Unreadable"; fi
EOF
if [ -r "$1" ]; then echo "Readable"; else echo "Unreadable"; fi
Run the previous command as "root" and notice the output.
P.S. If you need to run the commands inside the script as some user, Why do you run the script as "root"?
To read the "r" bit of the user part of the permission bits ("u+r"), this command could be used, assuming it's a bit more efficient than https://stackoverflow.com/a/71460604/6607497 (using GNU sed 4.4):
stat -c %A "$1" | sed -n -e '/^.r/q0;q1'; echo $?
The command will exit with success (0) for permissions "u+r", with failure (1) otherwise.
However the file could be readable even when "u+r" is not true.
For example a normal directory without any read permissions can be read by root.
For files only, this is probably most efficient and correct for files) in bash:
{ local dummy=$(< "$1"); } 2> /dev/null
echo $?
The command will exit with success (0) when "$1" was readable, with failure (1) otherwise.
However for directories it will always exit with failure (1).
Checking permissions using stat --format %a you could wrap the result into some modern shell code like this:
echo $(( $(printf '%o\n' $(( 0$(stat --format %a /etc/shadow) & 0444 ))) != 0 ))
That would output 1 is any of the read bits (0444) is set.
Of course the printf %o is not needed; it's just when you want to debug the result.
So:
# check whether file ($1) has any of the numeric permission bits ($2) set
check_perm_any_of()
{
(( $((0$(stat --format %a "$1") & "$2")) != 0 ))
}
check_perm_any_of /etc/passwd 0444 && echo 1
check_perm_any_of /etc/passwd 0022 && echo 2
check_perm_any_of /etc/shadow 0004 && echo 3
check_perm_any_of /etc/shadow 0002 && echo 4
check_perm_any_of /etc/shadow 0200 && echo 5
will output:
1
5
I have the following c program.
$ cat main.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int fd;
if((fd = open(argv[1], O_RDONLY)) == -1) {
perror("open");
return 1;
}
if(close(fd) == -1) {
perror("close");
return 1;
}
return 0;
}
But I got the following error.
touch tmpfile
sudo chown root tmpfile
sudo chown root ./main_prog
sudo setcap cap_setuid+ep ./main_prog # There will be no error if I use sudo chmod u+s
./main_prog tmpfile
open: Permission denied
Could anybody show me how to use setcap for setuid?
What you are trying to do is access a file you need privilege to access. The cap_setuid capability does not directly grant this privilege - it grants the process the privilege to change its own UID(s). You can get there via this path, but it requires more code in your program.
The capability you want for your use case is one to override the discretionary access control: cap_dac_override.
With your ./main_prog as written, try this instead:
$ touch tmpfile
$ sudo chown root.root tmpfile
$ sudo chmod go-r tmpfile
$ ls -l tmpfile
-rw------- 1 root root 0 Apr 9 08:02 tmpfile
$ cat tmpfile
cat: tmpfile: Permission denied
$ sudo setcap cap_dac_override=ep ./main_prog
$ ./main_prog tmpfile
$ echo $?
0
Note, with capabilities, there is no need for main_prog to be owned by root.
I have created a shell script which runs perfectly when calling it from command line and giving it each argument. However, when I compile the script using my make file it ignores the case where no arguments are given and prints out nothing.
Is there something wrong with my logic for if no argument was passed in through the command line?
#!/bin/bash
# findName.sh
searchFile="/acct/common/CSCE215-Fall19"
if [[ $1 = "" ]] ; then
echo "ERROR ARGUMENT NEEDED"
exit 2
fi
grep -i $1 ${searchFile}
if [[ $? = "1" ]] ; then
echo "$1 was not found in ${searchFile}"
fi
Edit
#makefile for building
findName: main.o
g++ -g main.o -o findName
# main
main.o: main.cpp
g++ -c -g main.cpp
clean:
/bin/rm -f findName *.o
backup:
tar cvf proj.tar * cpp Makefile *.sh readme
main.cpp
#include <string>
#include <cstdlib>
int main(int argc, char* argv[])
{
std::string command = "./findName.sh";
if(argc == 2)
std::system((command + " " + argv[1]).c_str());
}
I'm trying to do the opposite of "Detect if stdin is a terminal or pipe?".
I'm running an application that's changing its output format because it detects a pipe on STDOUT, and I want it to think that it's an interactive terminal so that I get the same output when redirecting.
I was thinking that wrapping it in an expect script or using a proc_open() in PHP would do it, but it doesn't.
Any ideas out there?
Aha!
The script command does what we want...
script --return --quiet -c "[executable string]" /dev/null
Does the trick!
Usage:
script [options] [file]
Make a typescript of a terminal session.
Options:
-a, --append append the output
-c, --command <command> run command rather than interactive shell
-e, --return return exit code of the child process
-f, --flush run flush after each write
--force use output file even when it is a link
-q, --quiet be quiet
-t[<file>], --timing[=<file>] output timing data to stderr or to FILE
-h, --help display this help
-V, --version display version
Based on Chris' solution, I came up with the following little helper function:
faketty() {
script -qfc "$(printf "%q " "$#")" /dev/null
}
The quirky looking printf is necessary to correctly expand the script's arguments in $# while protecting possibly quoted parts of the command (see example below).
Usage:
faketty <command> <args>
Example:
$ python -c "import sys; print(sys.stdout.isatty())"
True
$ python -c "import sys; print(sys.stdout.isatty())" | cat
False
$ faketty python -c "import sys; print(sys.stdout.isatty())" | cat
True
The unbuffer script that comes with Expect should handle this ok. If not, the application may be looking at something other than what its output is connected to, eg. what the TERM environment variable is set to.
Referring previous answer, on Mac OS X, "script" can be used like below...
script -q /dev/null commands...
But, because it may replace "\n" with "\r\n" on the stdout, you may also need script like this:
script -q /dev/null commands... | perl -pe 's/\r\n/\n/g'
If there are some pipe between these commands, you need to flush stdout. for example:
script -q /dev/null commands... | ruby -ne 'print "....\n";STDOUT.flush' | perl -pe 's/\r\n/\n/g'
I don't know if it's doable from PHP, but if you really need the child process to see a TTY, you can create a PTY.
In C:
#include <stdio.h>
#include <stdlib.h>
#include <sysexits.h>
#include <unistd.h>
#include <pty.h>
int main(int argc, char **argv) {
int master;
struct winsize win = {
.ws_col = 80, .ws_row = 24,
.ws_xpixel = 480, .ws_ypixel = 192,
};
pid_t child;
if (argc < 2) {
printf("Usage: %s cmd [args...]\n", argv[0]);
exit(EX_USAGE);
}
child = forkpty(&master, NULL, NULL, &win);
if (child == -1) {
perror("forkpty failed");
exit(EX_OSERR);
}
if (child == 0) {
execvp(argv[1], argv + 1);
perror("exec failed");
exit(EX_OSERR);
}
/* now the child is attached to a real pseudo-TTY instead of a pipe,
* while the parent can use "master" much like a normal pipe */
}
I was actually under the impression that expect itself does creates a PTY, though.
Updating #A-Ron's answer to
a) work on both Linux & MacOs
b) propagate status code indirectly (since MacOs script does not support it)
faketty () {
# Create a temporary file for storing the status code
tmp=$(mktemp)
# Ensure it worked or fail with status 99
[ "$tmp" ] || return 99
# Produce a script that runs the command provided to faketty as
# arguments and stores the status code in the temporary file
cmd="$(printf '%q ' "$#")"'; echo $? > '$tmp
# Run the script through /bin/sh with fake tty
if [ "$(uname)" = "Darwin" ]; then
# MacOS
script -Fq /dev/null /bin/sh -c "$cmd"
else
script -qfc "/bin/sh -c $(printf "%q " "$cmd")" /dev/null
fi
# Ensure that the status code was written to the temporary file or
# fail with status 99
[ -s $tmp ] || return 99
# Collect the status code from the temporary file
err=$(cat $tmp)
# Remove the temporary file
rm -f $tmp
# Return the status code
return $err
}
Examples:
$ faketty false ; echo $?
1
$ faketty echo '$HOME' ; echo $?
$HOME
0
embedded_example () {
faketty perl -e 'sleep(5); print "Hello world\n"; exit(3);' > LOGFILE 2>&1 </dev/null &
pid=$!
# do something else
echo 0..
sleep 2
echo 2..
echo wait
wait $pid
status=$?
cat LOGFILE
echo Exit status: $status
}
$ embedded_example
0..
2..
wait
Hello world
Exit status: 3
Too new to comment on the specific answer, but I thought I'd followup on the faketty function posted by ingomueller-net above since it recently helped me out.
I found that this was creating a typescript file that I didn't want/need so I added /dev/null as the script target file:
function faketty { script -qfc "$(printf "%q " "$#")" /dev/null ; }
There's also a pty program included in the sample code of the book "Advanced Programming in the UNIX Environment, Second Edition"!
Here's how to compile pty on Mac OS X:
man 4 pty # pty -- pseudo terminal driver
open http://en.wikipedia.org/wiki/Pseudo_terminal
# Advanced Programming in the UNIX Environment, Second Edition
open http://www.apuebook.com
cd ~/Desktop
curl -L -O http://www.apuebook.com/src.tar.gz
tar -xzf src.tar.gz
cd apue.2e
wkdir="${HOME}/Desktop/apue.2e"
sed -E -i "" "s|^WKDIR=.*|WKDIR=${wkdir}|" ~/Desktop/apue.2e/Make.defines.macos
echo '#undef _POSIX_C_SOURCE' >> ~/Desktop/apue.2e/include/apue.h
str='#include <sys/select.h>'
printf '%s\n' H 1i "$str" . wq | ed -s calld/loop.c
str='
#undef _POSIX_C_SOURCE
#include <sys/types.h>
'
printf '%s\n' H 1i "$str" . wq | ed -s file/devrdev.c
str='
#include <sys/signal.h>
#include <sys/ioctl.h>
'
printf '%s\n' H 1i "$str" . wq | ed -s termios/winch.c
make
~/Desktop/apue.2e/pty/pty ls -ld *
I was trying to get colors when running shellcheck <file> | less on Linux, so I tried the above answers, but they produce this bizarre effect where text is horizontally offset from where it should be:
In ./all/update.sh line 6:
for repo in $(cat repos); do
^-- SC2013: To read lines rather than words, pipe/redirect to a 'while read' loop.
(For those unfamiliar with shellcheck, the line with the warning is supposed to line up with the where the problem is.)
In order to the answers above to work with shellcheck, I tried one of the options from the comments:
faketty() {
0</dev/null script -qfc "$(printf "%q " "$#")" /dev/null
}
This works. I also added --return and used long options, to make this command a little less inscrutable:
faketty() {
0</dev/null script --quiet --flush --return --command "$(printf "%q " "$#")" /dev/null
}
Works in Bash and Zsh.
I have a file /a/b that is readable by a user A. But /a does not provide executable permission by A, and thus the path /a/b cannot traverse through /a. For an arbitrarily long path, how would I determine the cause for not being able to access a given path due to an intermediate path not being accessible by the user?
Alternative answer to parsing the tree manually and pinpointing the error to a single row would be using namei tool.
namei -mo a/b/c/d
f: a/b/c/d
drwxrw-rw- rasjani rasjani a
drw-rwxr-x rasjani rasjani b
c - No such file or directory
This shows the whole tree structure and permissions up until the entry where the permission is denied.
Something along like this:
#!/bin/bash
PAR=${1}
PAR=${PAR:="."}
if ! [[ "${PAR:0:1}" == / || "${PAR:0:2}" == ~[/a-z] ]]
then
TMP=`pwd`
PAR=$(dirname ${TMP}/${PAR})
fi
cd $PAR 2> /dev/null
if [ $? -eq 1 ]; then
while [ ! -z "$PAR" ]; do
PREV=$(readlink -f ${PAR})
TMP=$(echo ${PAR}|awk -F\/ '{$NF=""}'1|tr ' ' \/)
PAR=${TMP%/}
cd ${PAR} 2>/dev/null
if [ $? -eq 0 ]; then
if [ -e ${PREV} ]; then
ls -ld ${PREV}
fi
exit
fi
done
fi
Ugly but it would get the job done ..
So the idea is basicly that taking a parameter $1, if its not absolute directory, expand it to such and then drop the last element of the path and try to cd into it, if it fails, rince and repeat .. If it works, PREV would hold the last directory where user couldn't cd into, so print it out ..
Here's what I threw together. I actually didn't look at rasjani's answer before writing this, but it uses the same concept where you take the exit status of the command. Basically its going through all the directories (starting the farthest down the chain) and tries to ls them. If the exit status is 0, then the ls succeeded, and it prints out the last dir that it couldn't ls (I'm not sure what would happen in some of the edge cases like where you can't access anything):
LAST=/a/b
while [ ! -z "$LAST" ] ; do
NEXT=`echo "$LAST" | sed 's/[^\/]*$//' | sed 's/\/$//'`
ls "$NEXT" 2> /dev/null > /dev/null
if [ $? -eq 0 ] ; then
echo "Can't access: $LAST"
break
fi
LAST="$NEXT"
done
and I like putting stuff like this on one line just for fun:
LAST=/a/b; while [ ! -z "$LAST" ] ; do NEXT=`echo "$LAST" | sed 's/[^\/]*$//' | sed 's/\/$//'`; ls "$NEXT" 2> /dev/null > /dev/null; if [ $? -eq 0 ] ; then echo "Can't access: $LAST"; break; fi; LAST="$NEXT"; done
I have below C program for you which does this. Below are the steps
Copy and save program as file.c.
Compile program with gcc file.c -o file
Execute it as ./file PATH
Assuming that you have a path as /a/b/c/d and you do not have permission for 'c' then output will be
Given Path = /a/b/c/d
No permission on = /a/b/c
For permission i am relying on "EACCES" error. Path length is assumed to 1024.
If you have any question please share.
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define MAX_LEN 1024
int main(int argc, char *argv[])
{
char path[MAX_LEN] = "/home/sudhansu/Test";
int i = 0;
char parse[MAX_LEN] = "";
if(argc == 2)
{
strcpy(path, argv[1]);
printf("\n\t\t Given Path = %s\n", path);
}
else
{
printf("\n\t\t Usage : ./file PATH\n\n");
return 0;
}
if(path[strlen(path)-1] != '/')
strcat(path, "/");
path[strlen(path)] = '\0';
while(path[i])
{
if(path[i] == '/')
{
strncpy(parse, path, i+1);
if(chdir(parse) < 0)
{
if(errno == EACCES)
{
printf("\t\t No permission on = [%s]\n", parse);
break;
}
}
}
parse[i] = path[i];
i++;
}
printf("\n");
return 0;
}
Regards,
Sudhansu