Encontre arquivos que um usuário possa gravar, de forma eficiente com a criação mínima de processos

20

Eu sou raiz Gostaria de saber se um usuário não root tem acesso de gravação a alguns arquivos - milhares deles. Como fazer isso de forma eficiente, evitando a criação de processos?

Howard
fonte
Por favor, mostre-nos o que você realmente faz!
F. Hauri 15/05
Mesmo tipo de pergunta que unix.stackexchange.com/a/88591
Stéphane Chazelas
Supondo que você não se importe com as condições da corrida, por que não ligar access(2)com um UID real definido adequadamente (por exemplo, via setresuid(2)ou o equivalente portátil)? Quero dizer, eu seria pressionado a fazer isso a partir do bash, mas tenho certeza que o Perl / Python pode lidar com isso.
22415 Kevin
11
@ Kevin, os reservatórios [ -wgeralmente usam o acesso (2) ou equivalente. Você também precisa definir os gids além do uid (como su ou sudo faz). O bash não tem suporte embutido para isso, mas o zsh possui.
Stéphane Chazelas
@ StéphaneChazelas - você pode usar chgrpem qualquer shell.
mikeserv

Respostas:

2

Talvez assim:

#! /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

O acima executado executa um único programa externo para cada arquivo, a saber stat(1).

Nota: Isso assume bash(1), e a encarnação do Linux de stat(1).

Nota 2: Leia os comentários de Stéphane Chazelas abaixo para conhecer os perigos passados, presentes, futuros e potenciais e limitações dessa abordagem.

lcd047
fonte
Isso poderia dizer que um arquivo é gravável, mesmo que o usuário não tenha acesso ao diretório em que está.
Stéphane Chazelas
Isso pressupõe que os nomes de arquivo não contenham caracteres de nova linha e os caminhos de arquivo passados ​​no stdin não comecem -. Você pode modificá-lo para aceitar uma lista NUL delimitado vez comread -d ''
Stéphane Chazelas
Observe que não existe uma estatística do Linux . Linux é o kernel encontrado em alguns sistemas GNU e não GNU. Embora existam comandos (como from util-linux) especificamente criados para Linux, o que statvocê está se referindo não é, é um comando GNU que foi portado para a maioria dos sistemas, não apenas para Linux. Observe também que você tinha um statcomando no Linux muito antes da gravação do GNU stat(o statzsh builtin).
Stéphane Chazelas
2
@ Stéphane Chazelas: Observe que não existe uma estatística do Linux. - Acredito que escrevi "a encarnação do Linux stat(1)". Estou me referindo a um stat(1)que aceita a -c <format>sintaxe, em oposição a, digamos, a sintaxe BSD -f <format>. Eu também acredito que estava bem claro ao qual eu não estava me referindo stat(2). Tenho certeza de que uma página wiki sobre a história dos comandos comuns seria bastante interessante.
Lcd047 16/05
11
@ Stéphane Chazelas: Isso poderia dizer que um arquivo é gravável, mesmo que o usuário não tenha acesso ao diretório em que está. - Verdade, e provavelmente uma limitação razoável. Isso pressupõe que os nomes dos arquivos não contenham caracteres de nova linha - True e provavelmente uma limitação razoável. e os caminhos de arquivo passados ​​no stdin não começam com - - Editado, obrigado.
Lcd047 16/05
17

TL; DR

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

Você precisa perguntar ao sistema se o usuário tem permissão de gravação. A única maneira confiável é mudar o uid efetivo, o gid efetivo e os suplementos para o usuário e usar a access(W_OK)chamada do sistema (mesmo que tenha algumas limitações em alguns sistemas / configurações).

E lembre-se de que não ter permissão de gravação em um arquivo não garante necessariamente que você não possa modificar o conteúdo do arquivo nesse caminho.

A história mais longa

Vamos considerar o que é necessário, por exemplo, para um usuário $ ter acesso de gravação /foo/file.txt(assumindo que nenhum /fooe/foo/file.txt sejam links simbólicos)?

Ele precisa:

  1. acesso de pesquisa a /(não é necessário read)
  2. acesso de pesquisa a /foo(não é necessário read)
  3. acesso de gravação a/foo/file.txt

Você já pode ver as abordagens (como @ lcd047 ou @ apaul's ) que verificam apenas a permissão de file.txtnão funcionarão porque poderiam dizer que file.txté gravável, mesmo que o usuário não tenha permissão de pesquisa /ou/foo .

E uma abordagem como:

sudo -u "$user" find / -writeble

Também não funcionará porque não reportará os arquivos nos diretórios em que o usuário não tem acesso de leitura (em findexecução como$user não é possível o conteúdo), mesmo que ele possa gravá-los.

Se esquecermos ACLs, sistemas de arquivos somente leitura, sinalizadores FS (como imutáveis), outras medidas de segurança (apparmor, SELinux, que podem até distinguir entre diferentes tipos de gravação) e focar apenas nos atributos tradicionais de permissão e propriedade, para obter uma com permissão (pesquisar ou escrever), isso já é bastante complicado e difícil de expressar com find .

Você precisa:

  • se o arquivo pertence a você, você precisa dessa permissão para o proprietário (ou uid 0)
  • se o arquivo não pertencer a você, mas o grupo for um dos seus, será necessário ter essa permissão para o grupo (ou uid 0).
  • se não pertence a você e não a nenhum dos seus grupos, as outras permissões se aplicam (a menos que seu uid seja 0).

Na findsintaxe, aqui como um exemplo com um usuário do uid 1 e dos gids 1 e 2, seria:

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 alguém poda os diretórios que o usuário não tenha procurar certa para e para outros tipos de arquivos (links simbólicos excluídos como eles não são relevantes), cheques para acesso de gravação.

Se você também deseja considerar o acesso de gravação aos diretórios:

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

Ou para uma $userassociação arbitrária e de seu grupo recuperada do banco de dados do usuário:

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 é 3 processos totais: id, sede find)

O melhor aqui seria descer a árvore como raiz e verificar as permissões como usuário para cada arquivo.

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

(esse é um findprocesso mais um sudoe shprocessa a cada poucos milhares de arquivos [e printfgeralmente é construído no shell).

Ou com perl:

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

(3 processos totais: find, sudoe perl).

Ou com zsh:

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

(0 processo no total, mas armazena toda a lista de arquivos na memória)

Essas soluções dependem da access(2)chamada do sistema. Em vez de reproduzir o algoritmo que o sistema usa para verificar a permissão de acesso, pedimos que o sistema faça essa verificação com o mesmo algoritmo (que leva em conta permissões, ACLs, sinalizadores imutáveis, sistemas de arquivos somente leitura ... ), você tentaria abrir o arquivo para escrever, portanto, é o mais próximo que você vai chegar de uma solução confiável.

Para testar as soluções fornecidas aqui, com as várias combinações de usuário, grupo e permissões, você pode:

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
            }
          }
        }
      }
    }
  }'

Variando o usuário entre 1 e 2 e o grupo entre 1, 2 e 3 e limitando-nos aos 9 bits inferiores das permissões, já que são 9458694 arquivos criados. Isso para diretórios e novamente para arquivos.

Isso cria todas as combinações possíveis de u<x>g<y>/<mode1>/u<z>g<w>/<mode2>. O usuário com uid 1 e gids 1 e 2 teria acesso de gravação, u2g1/010/u2g3/777mas nãou1g2/677/u1g1/777 por exemplo.

Agora, todas essas soluções tentam identificar os caminhos dos arquivos que o usuário pode abrir para gravação, diferente dos caminhos em que o usuário pode modificar o conteúdo. Para responder a essa pergunta mais genérica, há várias coisas a serem consideradas:

  1. $ user pode não ter acesso de gravação, /a/b/filemas se ele possui file(e tem acesso de pesquisa /a/be o sistema de arquivos não é somente leitura, e o arquivo não tem o sinalizador imutável e ele tem acesso de shell ao sistema), então ele seria capaz de alterar as permissões do filee conceder a si mesmo acesso.
  2. A mesma coisa se ele possui, /a/bmas não tem acesso à pesquisa.
  3. $ user pode não ter acesso, /a/b/fileporque ele não tem acesso à pesquisa /aou /a/b, mas esse arquivo pode ter um link /b/c/filefísico, por exemplo, nesse caso, ele pode modificar o conteúdo /a/b/fileabrindo-o pelo /b/c/filecaminho.
  4. A mesma coisa com montagens de ligação . Ele pode não ter acesso à pesquisa /a, mas /a/bpode ser montado em ligação/c , para que ele possa abrir filepara escrever através de seu /c/fileoutro caminho.
  5. Ele pode não ter permissão de gravação /a/b/file, mas se tiver acesso de gravação /a/b, poderá remover ou renomear filelá e substituí-lo por sua própria versão. Ele mudaria o conteúdo do arquivo, /a/b/filemesmo que fosse um arquivo diferente.
  6. A mesma coisa se ele tem acesso de gravação para /a(ele poderia mudar o nome /a/bpara /a/c, criar um novo /a/bdiretório e um novo filenele.

Para encontrar os caminhos que $userpoderiam modificar. Para endereçar 1 ou 2, não podemos mais confiar na access(2)chamada do sistema. Poderíamos ajustar nossa find -permabordagem para assumir o acesso à pesquisa nos diretórios ou gravar o acesso aos arquivos assim que você for o proprietário:

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

Poderíamos endereçar 3 e 4, gravando os números do dispositivo e do inode ou todos os arquivos que $ user tem permissão de gravação e relatando todos os caminhos de arquivos que possuem esses números de dev + inode. Desta vez, podemos usar o mais confiável access(2) abordagens :

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 e 6 são, à primeira vista, complicados pelo tpouco das permissões. Quando aplicado em diretórios, esse é o bit de exclusão restrito que impede que usuários (outros que não sejam o proprietário do diretório) removam ou renomeiem os arquivos que eles não possuem (mesmo tendo acesso de gravação ao diretório).

Por exemplo, se voltarmos ao nosso exemplo anterior, se você tem acesso de gravação para /a, em seguida, você deve ser capaz de mudar o nome /a/bpara /a/c, em seguida, recriar um /a/bdiretório e uma nova filelá. Mas se o tbit estiver ativado /ae você não possuir /a, poderá fazê-lo somente se possuir /a/b. Isso dá:

  • Se você possui um diretório, conforme 1, pode conceder a si mesmo acesso de gravação e o bit t não se aplica (e você pode removê-lo de qualquer maneira), para excluir / renomear / recriar qualquer arquivo ou diretório nele, portanto todos os caminhos de arquivo abaixo são seus para reescrever qualquer conteúdo.
  • Se você não o possui, mas tem acesso de gravação, então:
    • Ou o t bit não está definido e você está no mesmo caso acima (todos os caminhos de arquivo são seus).
    • ou está definido e não é possível modificar os arquivos que você não possui ou não tem acesso de gravação; portanto, com o objetivo de encontrar os caminhos de arquivo que você pode modificar, é o mesmo que não ter permissão de gravação.

Portanto, podemos resolver todos os itens 1, 2, 5 e 6 com:

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

Como a solução para 3 e 4 é independente, você pode mesclar a saída deles para obter uma 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 deve ficar claro, se você leu tudo até agora, parte dele, pelo menos, trata apenas de permissões e propriedade, e não os outros recursos que podem conceder ou restringir o acesso de gravação (FS, ACLs, sinalizador imutável, outros recursos de segurança, somente leitura ...) E, como a processamos em vários estágios, algumas dessas informações podem estar erradas se os arquivos / diretórios estiverem sendo criados / excluídos / renomeados ou se suas permissões / propriedade forem modificadas enquanto esse script estiver em execução, como em um servidor de arquivos ocupado com milhões de arquivos .

Notas de portabilidade

Todo esse código é padrão (POSIX, Unix para tbit), exceto:

  • -print0é uma extensão GNU agora também suportada por algumas outras implementações. Com findimplementações que não têm suporte para isso, você pode usar -exec printf '%s\0' {} +e substituir -exec sh -c 'exec find "$@" -print0' sh {} +por -exec sh -c 'exec find "$@" -exec printf "%s\0" {\} +' sh {} +.
  • perlnão é um comando especificado pelo POSIX, mas está amplamente disponível. Você precisa perl-5.6.0ou acima para -Mfiletest=access.
  • zshnão é um comando especificado pelo POSIX. Esse zshcódigo acima deve funcionar com zsh-3 (1995) e acima.
  • sudonão é um comando especificado pelo POSIX. O código deve funcionar com qualquer versão, desde que a configuração do sistema permita a execução perlcomo o usuário especificado.
Stéphane Chazelas
fonte
O que é um acesso de pesquisa ? Eu nunca ouvi falar disso nas permissões tradicionais: ler, escrever, executar.
Bela83 15/05
2
@ bela83, executar permissão em um diretório (você não executa diretórios) é traduzido para pesquisa . Essa é a capacidade de acessar arquivos nele. Você pode listar o conteúdo de um diretório se tiver permissão de leitura, mas não poderá fazer nada com os arquivos, a menos que também tenha xpermissão de pesquisa ( bit) no diretório. Você também pode ter permissões de pesquisa, mas não ler , o que significa que os arquivos estão ocultos para você, mas se você souber o nome deles, poderá acessá-los. Um exemplo típico é o diretório do arquivo de sessão php (algo como / var / lib / php).
Stéphane Chazelas
2

Você pode combinar opções com o findcomando, para descobrir os arquivos com o modo e o proprietário especificados. Por exemplo:

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

O comando acima listará todas as entradas que pertencem ao grupo "equipe" ou "usuários" e tem permissão de gravação para esse grupo.

Você também deve verificar as entradas pertencentes ao seu usuário e os arquivos que podem ser gravados mundialmente, portanto:

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

No entanto, esse comando não corresponderá às entradas com a ACL estendida. Então você pode sudescobrir todas as entradas graváveis:

# su - yourusername
$ find / -writable
apaul
fonte
Isso diria que um arquivo com r-xrwxrwx yourusername:anygroupou r-xr-xrwx anyone:staffé gravável.
Stéphane Chazelas
Também reportaria como arquivos graváveis ​​que estão em diretórios aos quais yourusernamenão há acesso.
Stéphane Chazelas
1

A abordagem depende do que você está realmente testando.

  1. Deseja garantir o acesso de gravação possível?
  2. Deseja garantir a falta de acesso de gravação?

Isso ocorre porque existem muitas maneiras de chegar a 2) e a resposta de Stéphane cobre isso muito bem (é imutável lembrar) e lembre-se de que existem meios físicos também, como desmontar a unidade ou torná-la somente leitura em um nível de hardware (guias de disquete). Suponho que seus milhares de arquivos estejam em diretórios diferentes e você deseja um relatório ou está verificando uma lista principal. (Outro abuso de Puppet apenas esperando para acontecer).

Você provavelmente deseja que a árvore perl de passagem do Stéphane e "junte" a saída a uma lista, se necessário (su também pegará a execução ausente nos diretórios pai?). Se a barriga de aluguel é um problema de desempenho, você está fazendo isso para um "grande número" de usuários? Ou é uma consulta online? Se esse for um requisito permanente permanente, talvez seja hora de considerar um produto de terceiros.

mckenzm
fonte
0

Você pode fazer...

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

... para obter uma lista de todos os arquivos nos quais o usuário não pode gravar como gravado no stderr no formulário ...

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

...ou similar.

Veja os comentários abaixo para obter notas sobre os problemas que essa abordagem pode ter e a explicação abaixo sobre por que ela pode funcionar. Mais prudentemente, porém, você provavelmente deveria apenas find arquivos regulares como:

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

Em resumo, teeimprimirá erros quando tentar open()um arquivo com um dos dois sinalizadores ...

O_WRONLY

Aberto apenas para gravação.

O_RDWR

Aberto para leitura e escrita. O resultado é indefinido se esse sinalizador for aplicado a um FIFO.

... e encontros ...

[EACCES]

A permissão de pesquisa é negada em um componente do prefixo do caminho, ou o arquivo existe e as permissões especificadas por oflag são negadas ou o arquivo não existe e a permissão de gravação é negada para o diretório pai do arquivo a ser criado ou O_TRUNC é permissão de gravação especificada e negada.

... como especificado aqui :

O teeutilitário deve copiar a entrada padrão para a saída padrão, fazendo uma cópia em zero ou mais arquivos. O utilitário tee não deve armazenar em buffer a saída.

Se a -aopção não for especificada, os arquivos de saída deverão ser gravados (consulte Leitura, gravação e criação de arquivo ) ...

... POSIX.1-2008 requer uma funcionalidade equivalente ao uso de O_APPEND ...

Porque tem que verificar da mesma maneira test -wque ...

-w nome do caminho

Verdadeiro se o nome do caminho for resolvido para uma entrada de diretório existente para um arquivo cuja permissão para gravar no arquivo será concedida, conforme definido em Leitura, Gravação e Criação de Arquivo . False se o nome do caminho não puder ser resolvido ou se o nome do caminho for resolvido para uma entrada de diretório existente para um arquivo para o qual a permissão para gravar no arquivo não será concedida.

Ambos procuram EACCESS .

mikeserv
fonte
É provável que você encontre um limite de número de arquivos abertos simultaneamente com essa abordagem (a menos que o número de arquivos seja baixo). Cuidado com os efeitos colaterais com dispositivos e pipes nomeados. Você receberá uma mensagem de erro diferente para soquetes.
Stéphane Chazelas
@ StéphaneChazelas - Todos os ter - Acho que também pode ser verdade que teeirá travar, a menos que seja explicitamente interrompido uma vez por execução. Foi a coisa mais próxima que pude pensar [ -w... porém - seus efeitos devem estar próximos, pois garante que o usuário possa OAPPENDER o arquivo. Muito mais fácil do que qualquer uma das opções seria paxcom as -oopções de formato e / ou -tpara verificação EACCESS- mas toda vez que sugiro que as paxpessoas pareçam ignorar. E, de qualquer forma, o único paxque encontrei que atende ao padrão existente é o AST - nesse caso, você também pode usá-lo ls.
mikeserv