Encuentre archivos que un usuario pueda escribir, de manera eficiente con una creación de proceso mínima

20

Soy root Me gustaría saber si un usuario no root tiene acceso de escritura a algunos archivos, miles de ellos. ¿Cómo hacerlo de manera eficiente mientras se evita la creación de procesos?

Howard
fuente
¡Por favor muéstranos lo que realmente haces!
F. Hauri
El mismo tipo de pregunta que unix.stackexchange.com/a/88591
Stéphane Chazelas
Suponiendo que no le importan las condiciones de carrera, ¿por qué no simplemente llamar access(2)con un UID real configurado adecuadamente (por ejemplo, a través de setresuid(2)o el equivalente portátil)? Quiero decir, sería difícil hacer eso desde bash, pero estoy seguro de que Perl / Python puede manejarlo.
Kevin
1
@Kevin, los shells [ -wgeneralmente usan acceso (2) o equivalente. También debe configurar los gids además de uid (como lo hacen su o sudo). bash no tiene soporte incorporado para eso, pero zsh sí.
Stéphane Chazelas
@ StéphaneChazelas: puede usar chgrpen cualquier shell.
mikeserv

Respuestas:

2

Quizás así:

#! /bin/bash

writable()
{
    local uid="$1"
    local gids="$2"
    local ids
    local perms

    ids=($( stat -L -c '%u %g %a' -- "$3" ))
    perms="0${ids[2]}"

    if [[ ${ids[0]} -eq $uid ]]; then
        return $(( ( perms & 0200 ) == 0 ))
    elif [[ $gids =~ (^|[[:space:]])"${ids[1]}"($|[[:space:]]) ]]; then
        return $(( ( perms & 020 ) == 0 ))
    else
        return $(( ( perms & 2 ) == 0 ))
    fi
}

user=foo
uid="$( id -u "$user" )"
gids="$( id -G "$user" )"

while IFS= read -r f; do
    writable "$uid" "$gids" "$f" && printf '%s writable\n' "$f"
done

Lo anterior ejecuta un solo programa externo para cada archivo, a saber stat(1).

Nota: Esto supone bash(1), y la encarnación de Linux stat(1).

Nota 2: Lea los comentarios de Stéphane Chazelas a continuación para conocer los peligros y limitaciones pasados, presentes, futuros y potenciales de este enfoque.

lcd047
fuente
Eso podría decir que un archivo se puede escribir a pesar de que el usuario no tiene acceso al directorio donde está.
Stéphane Chazelas
Eso supone que los nombres de archivo no contienen caracteres de nueva línea y que las rutas de archivo pasadas en stdin no comienzan con -. En su lugar, puede modificarlo para aceptar una lista delimitada por NUL conread -d ''
Stéphane Chazelas
Tenga en cuenta que no existe una estadística de Linux . Linux es el núcleo que se encuentra en algunos sistemas GNU y no GNU. Si bien hay comandos (como from util-linux) que están específicamente escritos para Linux, al statque te refieres no lo es, es un comando GNU que se ha portado a la mayoría de los sistemas, no solo Linux. También tenga en cuenta que tenía un statcomando en Linux mucho antes de que statse escribiera GNU (el statzsh incorporado).
Stéphane Chazelas
2
@ Stéphane Chazelas: Tenga en cuenta que no existe una estadística de Linux. - Creo que escribí "la encarnación de Linux stat(1)". Me refiero a una stat(1)que acepta la -c <format>sintaxis, en oposición a, por ejemplo, la sintaxis BSD -f <format>. También creo que estaba bastante claro a que no me refería stat(2). Sin embargo, estoy seguro de que una página wiki sobre la historia de los comandos comunes sería bastante interesante.
lcd047
1
@ Stéphane Chazelas: Eso podría decir que un archivo se puede escribir a pesar de que el usuario no tiene acceso al directorio donde está. - Cierto, y probablemente una limitación razonable. Eso supone que los nombres de archivo no contienen caracteres de nueva línea : verdadero y probablemente una limitación razonable. y las rutas de archivo pasadas en stdin no comienzan con - - Editado, gracias.
lcd047
17

TL; DR

find / ! -type l -print0 |
  sudo -u "$user" perl -Mfiletest=access -l -0ne 'print if -w'

Debe preguntar al sistema si el usuario tiene permiso de escritura. La única forma confiable es cambiar el uid efectivo, el gid efectivo y los gids de suplementación a los del usuario y usar la access(W_OK)llamada al sistema (incluso eso tiene algunas limitaciones en algunos sistemas / configuraciones).

Y tenga en cuenta que no tener permiso de escritura en un archivo no garantiza necesariamente que no pueda modificar el contenido del archivo en esa ruta.

La historia mas larga

Vamos a considerar lo que se necesita, por ejemplo, para un usuario $ a tener acceso de escritura /foo/file.txt(suponiendo que ninguno de /fooy /foo/file.txtson enlaces simbólicos)?

El necesita:

  1. buscar acceso a /(no es necesario read)
  2. buscar acceso a /foo(no es necesario read)
  3. acceso de escritura a/foo/file.txt

Ya puede ver que los enfoques (como @ lcd047 o @ apaul's ) que verifican solo el permiso de file.txtno funcionarán porque podrían decir que file.txtse puede escribir incluso si el usuario no tiene permiso de búsqueda para /o /foo.

Y un enfoque como:

sudo -u "$user" find / -writeble

Tampoco funcionará porque no informará los archivos en directorios en los que el usuario no tiene acceso de lectura (ya findque $userno puede enumerar su contenido) incluso si puede escribir en ellos.

Si nos olvidamos de las ACL, los sistemas de archivos de solo lectura, los indicadores FS (como inmutables), otras medidas de seguridad (apparmor, SELinux, que incluso pueden distinguir entre diferentes tipos de escritura) y solo se centran en los permisos tradicionales y los atributos de propiedad, para obtener un dado (buscar o escribir) permiso, eso ya es bastante complicado y difícil de expresar find.

Necesitas:

  • si el archivo es de su propiedad, necesita ese permiso para el propietario (o tiene uid 0)
  • si el archivo no es de su propiedad, pero el grupo es uno de los suyos, entonces necesita ese permiso para el grupo (o tiene uid 0).
  • si no es de su propiedad y no pertenece a ninguno de sus grupos, se aplican los otros permisos (a menos que su uid sea 0).

En findsintaxis, aquí como ejemplo con un usuario de uid 1 y gids 1 y 2, eso sería:

find / -type d \
  \( \
    -user 1 \( -perm -u=x -o -prune \) -o \
    \( -group 1 -o -group 2 \) \( -perm -g=x -o -prune \) -o \
    -perm -o=x -o -prune \
  \) -o -type l -o \
    -user 1 \( ! -perm -u=w -o -print \) -o \
    \( -group 1 -o -group 2 \) \( ! -perm -g=w -o -print \) -o \
    ! -perm -o=w -o -print

Que uno ciruelas los directorios que usuario no tiene derecho a buscar y para otros tipos de archivos (enlaces simbólicos excluidos ya que no son relevantes), controles para el acceso de escritura.

Si también desea considerar el acceso de escritura a directorios:

find / -type d \
  \( \
    -user 1 \( -perm -u=x -o -prune \) -o \
    \( -group 1 -o -group 2 \) \( -perm -g=x -o -prune \) -o \
    -perm -o=x -o -prune \
  \) ! -type d -o -type l -o \
    -user 1 \( ! -perm -u=w -o -print \) -o \
    \( -group 1 -o -group 2 \) \( ! -perm -g=w -o -print \) -o \
    ! -perm -o=w -o -print

O para una $usermembresía arbitraria y de grupo recuperada de la base de datos de usuarios:

groups=$(id -G "$user" | sed 's/ / -o -group /g'); IFS=" "
find / -type d \
  \( \
    -user "$user" \( -perm -u=x -o -prune \) -o \
    \( -group $groups \) \( -perm -g=x -o -prune \) -o \
    -perm -o=x -o -prune \
  \) ! -type d -o -type l -o \
    -user "$user" \( ! -perm -u=w -o -print \) -o \
    \( -group $groups \) \( ! -perm -g=w -o -print \) -o \
    ! -perm -o=w -o -print

(que es 3 procesos en total: id, sedy find)

Lo mejor aquí sería descender el árbol como raíz y verificar los permisos como usuario para cada archivo.

 find / ! -type l -exec sudo -u "$user" sh -c '
   for file do
     [ -w "$file" ] && printf "%s\n" "$file"
   done' sh {} +

(ese es un findproceso más uno sudoy shprocesa cada pocos miles de archivos, [y printfgeneralmente se construyen en el shell).

O con perl:

find / ! -type l -print0 |
  sudo -u "$user" perl -Mfiletest=access -l -0ne 'print if -w'

(3 procesos en total: find, sudoy perl).

O con zsh:

files=(/**/*(D^@))
USERNAME=$user
for f ($files) {
  [ -w $f ] && print -r -- $f
}

(0 procesos en total, pero almacena la lista completa de archivos en la memoria)

Esas soluciones se basan en la access(2)llamada al sistema. Es decir, en lugar de reproducir el algoritmo que usa el sistema para verificar el permiso de acceso, le pedimos al sistema que haga esa verificación con el mismo algoritmo (que tiene en cuenta los permisos, las ACL, los indicadores inmutables, los sistemas de archivos de solo lectura ... ) lo usaría si intentas abrir el archivo para escribir, por lo que es lo más cercano a una solución confiable.

Para probar las soluciones dadas aquí, con las diversas combinaciones de usuario, grupo y permisos, puede hacer:

perl -e '
  for $u (1,2) {
    for $g (1,2,3) {
      $d1="u${u}g$g"; mkdir$d1;
      for $m (0..511) {
        $d2=$d1.sprintf"/%03o",$m; mkdir $d2; chown $u, $g, $d2; chmod $m,$d2;
        for $uu (1,2) {
          for $gg (1,2,3) {
            $d3="$d2/u${uu}g$gg"; mkdir $d3;
            for $mm (0..511) {
              $f=$d3.sprintf"/%03o",$mm;
              open F, ">","$f"; close F;
              chown $uu, $gg, $f; chmod $mm, $f
            }
          }
        }
      }
    }
  }'

Variar el usuario entre 1 y 2 y el grupo entre 1, 2 y 3 y limitarnos a los 9 bits inferiores de los permisos, ya que son 9458694 archivos creados. Eso para directorios y luego nuevamente para archivos.

Eso crea todas las combinaciones posibles de u<x>g<y>/<mode1>/u<z>g<w>/<mode2>. El usuario con uid 1 y gid 1 y 2 tendría acceso de escritura, u2g1/010/u2g3/777pero no u1g2/677/u1g1/777por ejemplo.

Ahora, todas esas soluciones intentan identificar las rutas de los archivos que el usuario puede abrir para escribir, eso es diferente de las rutas en las que el usuario puede modificar el contenido. Para responder a esa pregunta más genérica, hay varias cosas a tener en cuenta:

  1. $ user puede no tener acceso de escritura, /a/b/filepero si es el propietario file(y tiene acceso de búsqueda /a/b, y el sistema de archivos no es de solo lectura, y el archivo no tiene el indicador inmutable, y tiene acceso de shell al sistema), entonces él podría cambiar los permisos del filey otorgarse acceso.
  2. Lo mismo si posee /a/bpero no tiene acceso de búsqueda.
  3. $ usuario no puede tener acceso a /a/b/fileporque no tiene acceso a la búsqueda /ao /a/b, pero ese archivo puede tener un enlace duro en /b/c/file, por ejemplo, en cuyo caso puede ser capaz de modificar el contenido de /a/b/fileabriéndolo a través de su /b/c/filetrayectoria.
  4. Lo mismo con los soportes de unión . Es posible que no tenga acceso de búsqueda /a, pero /a/bpuede estar montado en un enlace/c , por lo que podría abrir filepara escribir a través de su /c/fileotra ruta.
  5. Es posible que no tenga permisos de escritura /a/b/file, pero si tiene acceso de escritura /a/bpuede eliminar o cambiar el nombre fileallí y reemplazarlo con su propia versión. Cambiaría el contenido del archivo /a/b/fileincluso si fuera un archivo diferente.
  6. Lo mismo si él tiene acceso de escritura /a(que podría cambiar el nombre /a/ba /a/c, crear un nuevo /a/bdirectorio y un nuevo fileen ella.

Para encontrar las rutas que $userpodrían modificarse. Para abordar 1 o 2, ya no podemos confiar en la access(2)llamada al sistema. Podríamos ajustar nuestro find -permenfoque para asumir el acceso de búsqueda a los directorios, o escribir el acceso a los archivos tan pronto como usted sea el propietario:

groups=$(id -G "$user" | sed 's/ / -o -group /g'); IFS=" "
find / -type d \
  \( \
    -user "$user" -o \
    \( -group $groups \) \( -perm -g=x -o -prune \) -o \
    -perm -o=x -o -prune \
  \) ! -type d -o -type l -o \
    -user "$user" -print -o \
    \( -group $groups \) \( ! -perm -g=w -o -print \) -o \
    ! -perm -o=w -o -print

Podríamos abordar 3 y 4, registrando el dispositivo y los números de inodo o todos los archivos que $ user tiene permiso para escribir e informar todas las rutas de archivos que tienen esos números dev + inode. Esta vez, podemos usar los access(2)enfoques basados ​​en más confiables :

Algo como:

find / ! -type l -print0 |
  sudo -u "$user" perl -Mfiletest=access -0lne 'print 0+-w,$_' |
  perl -l -0ne '
    ($w,$p) = /(.)(.*)/;
    ($dev,$ino) = stat$p or next;
    $writable{"$dev,$ino"} = 1 if $w;
    push @{$p{"$dev,$ino"}}, $p;
    END {
      for $i (keys %writable) {
        for $p (@{$p{$i}}) {
          print $p;
        }
      }
    }'

5 y 6 son a primera vista complicados por el tpoco de los permisos. Cuando se aplica en los directorios, ese es el bit de eliminación restringido que impide que los usuarios (que no sean el propietario del directorio) eliminen o renombren los archivos que no poseen (aunque tengan acceso de escritura al directorio).

Por ejemplo, si volvemos a nuestro ejemplo anterior, si tiene acceso de escritura /a, entonces usted debería ser capaz de cambiar el nombre /a/bde /a/c, y volver a crear un /a/bdirectorio y un nuevo fileallí. Pero si el tbit está activado /ay usted no posee /a, entonces solo puede hacerlo si posee /a/b. Eso da:

  • Si posee un directorio, según 1, puede otorgarse acceso de escritura, y el bit t no se aplica (y podría eliminarlo de todos modos), por lo que puede eliminar / renombrar / recrear cualquier archivo o directorio allí, por lo que todas las rutas de archivos que se encuentran debajo son suyas para reescribir con cualquier contenido.
  • Si no lo posee pero tiene acceso de escritura, entonces:
    • O bien el tbit no está configurado, y usted está en el mismo caso que el anterior (todas las rutas de archivos son suyas).
    • o está configurado y luego no puede modificar los archivos que no posee o no tiene acceso de escritura, por lo que para nuestro propósito de encontrar las rutas de archivos que puede modificar, es lo mismo que no tener permiso de escritura.

Entonces podemos abordar todos los 1, 2, 5 y 6 con:

find / -type d \
  \( \
    -user "$user" -prune -exec find {} + -o \
    \( -group $groups \) \( -perm -g=x -o -prune \) -o \
    -perm -o=x -o -prune \
  \) ! -type d -o -type l -o \
    -user "$user" \( -type d -o -print \) -o \
    \( -group $groups \) \( ! -perm -g=w -o \
       -type d ! -perm -1000 -exec find {} + -o -print \) -o \
    ! -perm -o=w -o \
    -type d ! -perm -1000 -exec find {} + -o \
    -print

Eso y la solución para 3 y 4 son independientes, puede fusionar su salida para obtener una lista completa:

{
  find / ! -type l -print0 |
    sudo -u "$user" perl -Mfiletest=access -0lne 'print 0+-w,$_' |
    perl -0lne '
      ($w,$p) = /(.)(.*)/;
      ($dev,$ino) = stat$p or next;
      $writable{"$dev,$ino"} = 1 if $w;
      push @{$p{"$dev,$ino"}}, $p;
      END {
        for $i (keys %writable) {
          for $p (@{$p{$i}}) {
            print $p;
          }
        }
      }'
  find / -type d \
    \( \
      -user "$user" -prune -exec sh -c 'exec find "$@" -print0' sh {} + -o \
      \( -group $groups \) \( -perm -g=x -o -prune \) -o \
      -perm -o=x -o -prune \
    \) ! -type d -o -type l -o \
      -user "$user" \( -type d -o -print0 \) -o \
      \( -group $groups \) \( ! -perm -g=w -o \
         -type d ! -perm -1000 -exec sh -c 'exec find "$@" -print0' sh {} + -o -print0 \) -o \
      ! -perm -o=w -o \
      -type d ! -perm -1000 -exec sh -c 'exec find "$@" -print0' sh {} + -o \
      -print0
} | perl -l -0ne 'print unless $seen{$_}++'

Como debe quedar claro si ha leído todo hasta ahora, parte de él al menos solo trata con los permisos y la propiedad, no con las otras características que pueden otorgar o restringir el acceso de escritura (FS de solo lectura, ACL, indicador inmutable, otras características de seguridad ...) Y a medida que lo procesamos en varias etapas, parte de esa información puede ser incorrecta si los archivos / directorios se crean / eliminan / cambian de nombre o se modifican sus permisos / propiedad mientras se ejecuta ese script, como en un servidor de archivos ocupado con millones de archivos .

Notas de portabilidad

Todo ese código es estándar (POSIX, Unix para tbit) excepto:

  • -print0es una extensión de GNU que ahora también es compatible con algunas otras implementaciones. Con findimplementaciones que carecen de soporte para ello, puede usar -exec printf '%s\0' {} +en su lugar y reemplazar -exec sh -c 'exec find "$@" -print0' sh {} +con -exec sh -c 'exec find "$@" -exec printf "%s\0" {\} +' sh {} +.
  • perlno es un comando especificado por POSIX pero está ampliamente disponible. Necesitas perl-5.6.0o más para -Mfiletest=access.
  • zshno es un comando especificado por POSIX. Ese zshcódigo anterior debería funcionar con zsh-3 (1995) y superior.
  • sudono es un comando especificado por POSIX. El código debería funcionar con cualquier versión siempre que la configuración del sistema permita la ejecución perldel usuario dado.
Stéphane Chazelas
fuente
¿Qué es un acceso de búsqueda ? Nunca he oído hablar de él en los permisos tradicionales: leer, escribir, ejecutar.
bela83
2
@ bela83, ejecutar permiso en un directorio (no ejecuta directorios) se traduce en búsqueda . Esa es la capacidad de acceder a los archivos que contiene. Puede enumerar el contenido de un directorio si tiene permiso de lectura, pero no puede hacer nada con los archivos que contiene, a menos que también tenga xpermiso de búsqueda ( bit) en el directorio. También puede tener permisos de búsqueda pero no leer , lo que significa que los archivos están ocultos para usted, pero si conoce su nombre, puede acceder a ellos. Un ejemplo típico es el directorio del archivo de sesión php (algo así como / var / lib / php).
Stéphane Chazelas
2

Puede combinar opciones con el findcomando, para que descubra los archivos con el modo y el propietario especificados. Por ejemplo:

$ find / \( -group staff -o -group users \) -and -perm -g+w

El comando anterior enumerará todas las entradas que pertenecen al grupo "personal" o "usuarios" y tienen permiso de escritura para ese grupo.

También debe verificar las entradas que son propiedad de su usuario y los archivos que se pueden escribir en todo el mundo, por lo tanto:

$ find / \( -user yourusername -or \
             \(  \( -group staff -o -group users \) -and -perm -g+w \
             \) -or \
            -perm -o+w \
         \)

Sin embargo, este comando no coincidirá con entradas con ACL extendida. Entonces puede suencontrar todas las entradas que se pueden escribir:

# su - yourusername
$ find / -writable
apaul
fuente
Eso diría que un archivo con r-xrwxrwx yourusername:anygroupo r-xr-xrwx anyone:staffes grabable.
Stéphane Chazelas
También informaría que los archivos que se pueden escribir que están en directorios yourusernameno tienen acceso.
Stéphane Chazelas
1

El enfoque depende de lo que realmente esté probando.

  1. ¿Quieres asegurarte de que el acceso de escritura sea posible?
  2. ¿Desea garantizar la falta de acceso de escritura?

Esto se debe a que hay muchas maneras de llegar a 2) y la respuesta de Stéphane los cubre bien (es inmutable recordarlo), y recuerde que también hay medios físicos, como desmontar el disco o hacerlo de solo lectura en un nivel de hardware (pestañas de disquete). Supongo que sus miles de archivos están en directorios diferentes y desea un informe o está comprobando una lista maestra. (Otro abuso de Puppet a la espera de suceder).

Es probable que desee el recorrido del árbol perl de Stéphane y "unir" la salida con una lista si es necesario (¿su también detectará la ejecución faltante en los directorios principales?). Si la subrogación es un problema de rendimiento, ¿está haciendo esto para un "gran número" de usuarios? ¿O es una consulta en línea? Si este es un requisito permanente permanente, puede ser hora de considerar un producto de terceros.

mckenzm
fuente
0

Tu puedes hacer...

find / ! -type d -exec tee -a {} + </dev/null

... para obtener una lista de todos los archivos en los que el usuario no puede escribir como se escribió en stderr en el formulario ...

"tee: cannot access %s\n", <pathname>" 

...o similar.

Consulte los comentarios a continuación para obtener notas sobre los problemas que podría tener este enfoque y la explicación a continuación de por qué podría funcionar. Sin embargo, de manera más sensata, probablemente solo debería tener findarchivos normales como:

find / -type f -exec tee -a {} + </dev/null

En resumen, teeimprimirá errores cuando intente open()un archivo con cualquiera de los dos indicadores ...

O_WRONLY

Abierto solo para escritura.

O_RDWR

Abierto para leer y escribir. El resultado no está definido si este indicador se aplica a un FIFO.

... y encuentros ...

[EACCES]

Se denegó el permiso de búsqueda en un componente del prefijo de ruta, o el archivo existe y los permisos especificados por oflag se denegaron, o el archivo no existe y se denegó el permiso de escritura para el directorio principal del archivo que se creará, o O_TRUNC es especificado y el permiso de escritura es denegado.

... como se especifica aquí :

La teeutilidad copiará la entrada estándar a la salida estándar, haciendo una copia en cero o más archivos. La utilidad de salida no debe almacenar en la memoria intermedia.

Si -ano se especifica la opción, se escribirán los archivos de salida (consulte Lectura, escritura y creación de archivos ) ...

... POSIX.1-2008 requiere una funcionalidad equivalente al uso de O_APPEND ...

Porque tiene que verificar de la misma manera test -wque ...

-w nombre de ruta

Verdadero si el nombre de ruta se resuelve en una entrada de directorio existente para un archivo para el cual se otorgará permiso para escribir en el archivo, como se define en Lectura, escritura y creación de archivos . Falso si pathname no se puede resolver, o si pathname se resuelve en una entrada de directorio existente para un archivo para el cual no se otorgará permiso para escribir en el archivo.

Ambos verifican el EACCESS .

mikeserv
fuente
Es probable que se encuentre con un límite de número de archivos abiertos simultáneamente con ese enfoque (a menos que el número de archivos sea bajo). Tenga cuidado con los efectos secundarios con dispositivos y tuberías con nombre. Obtendrá un mensaje de error diferente para los sockets.
Stéphane Chazelas
@ StéphaneChazelas - Todo el martes - Creo que también podría ser cierto que teese bloqueará a menos que se interrumpa explícitamente una vez por ejecución. Sin [ -wembargo, fue lo más parecido a lo que se me ocurrió ... sus efectos deberían ser cercanos, ya que garantiza que el usuario pueda APAGAR el archivo. Mucho más fácil que cualquiera de las opciones sería paxcon las -oopciones de formato y / o -tpara verificar EACCESS, pero cada vez que sugiero que la paxgente parece ignorarlo. Y, de todos modos, lo único paxque he encontrado que cumple con los estándares son los AST, en cuyo caso también podrías usarlos ls.
mikeserv