Найти файлы, которые пользователь может написать, эффективно с минимальным созданием процесса

20

Я корень. Я хотел бы знать, есть ли у пользователя без полномочий root права на запись в некоторые файлы - тысячи из них. Как сделать это эффективно, избегая создания процесса?

Говард
источник
Пожалуйста, покажите нам, что вы на самом деле делаете!
Ф. Хаури
Тот же вопрос, что и на unix.stackexchange.com/a/88591
Стефан
Предполагая, что вас не волнуют условия гонки, почему бы просто не позвонить access(2)с соответствующим образом установленным реальным UID (например, через setresuid(2)или портативный эквивалент)? Я имею в виду, мне было бы тяжело сделать это из bash, но я уверен, что Perl / Python справится с этим.
Кевин
1
@Kevin, оболочки [ -wобычно используют access (2) или эквивалентный. Вы также должны установить gids в дополнение к uid (как su или sudo). Bash не имеет встроенной поддержки для этого, но Zsh делает.
Стефан Шазелас
@ StéphaneChazelas - вы можете использовать chgrpв любой оболочке.
mikeserv

Ответы:

2

Возможно, вот так:

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

Вышеуказанное запускает одну внешнюю программу для каждого файла, а именно stat(1).

Примечание: это предполагает bash(1)и воплощение Linux stat(1).

Примечание 2: Пожалуйста, прочитайте комментарии Стефана Шазеля ниже для прошлого, настоящего, будущего и потенциальных опасностей и ограничений этого подхода.

lcd047
источник
Это может означать, что файл доступен для записи, даже если у пользователя нет доступа к каталогу, в котором он находится.
Стефан Шазелас
Это предполагает, что имена файлов не содержат символа новой строки, а пути к файлам, передаваемые в stdin, не начинаются с -. Вместо этого вы можете изменить его так, чтобы он принимал список с разделителями NUL, используяread -d ''
Стефан Шазелас
Обратите внимание, что нет такой вещи, как статистика Linux . Linux - это ядро, которое можно найти в некоторых системах GNU и не-GNU. Хотя есть команды (например, из util-linux), которые специально написаны для Linux, statвы не имеете в виду, это команда GNU, которая была перенесена на большинство систем, а не только на Linux. Также обратите внимание, что у вас была statкоманда в Linux задолго до того, как statбыла написана GNU ( statвстроенная команда zsh).
Стефан Шазелас
2
@ Стефан Шазелас: Обратите внимание, что нет такой вещи, как статистика Linux. - Я полагаю, я написал «воплощение Linux stat(1)». Я имею в виду тот, stat(1)который принимает -c <format>синтаксис, а не, скажем, синтаксис BSD -f <format>. Я также считаю, что это было довольно ясно, я не имел в виду stat(2). Я уверен, что страница вики об истории общих команд была бы довольно интересной.
lcd047
1
@ Стефан Шазелас: Можно сказать, что файл доступен для записи, даже если у пользователя нет доступа к каталогу, в котором он находится. - Правда и, вероятно, разумное ограничение. Это предполагает, что имена файлов не содержат символа новой строки - True, и, вероятно, разумное ограничение. и пути к файлам, передаваемые в stdin, не начинаются с - - отредактировано, спасибо.
lcd047
17

TL; DR

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

Вам нужно спросить систему, есть ли у пользователя разрешение на запись. Единственный надежный способ - переключить эффективный uid, эффективный gid и gid дополнения на пользовательский и использовать access(W_OK)системный вызов (даже если это имеет некоторые ограничения для некоторых систем / конфигураций).

И имейте в виду, что отсутствие разрешения на запись в файл не обязательно гарантирует, что вы не сможете изменить содержимое файла по этому пути.

Длинная история

Давайте рассмотрим , что требуется, например , для $ пользователю иметь доступ на запись /foo/file.txt(предполагая , ни один из /fooи /foo/file.txtсимволические ссылки)?

Он нуждается:

  1. поиск доступа к /(не нужно read)
  2. поиск доступа к /foo(не нужно read)
  3. доступ для записи в/foo/file.txt

Вы уже можете видеть, что подходы (например, @ lcd047 или @ apaul ), которые проверяют только разрешение « file.txtне работает», потому что они могут сказать, file.txtчто доступно для записи, даже если у пользователя нет разрешения на поиск /или /foo.

И такой подход, как:

sudo -u "$user" find / -writeble

Также не будет работать, потому что он не будет сообщать о файлах в каталогах, которые пользователь не имеет права на чтение ( findпоскольку он $userне может перечислить их содержимое), даже если он может писать в них.

Если мы забудем о ACL, файловых системах только для чтения, флагах FS (например, неизменяемых), других мерах безопасности (apparmor, SELinux, которые могут даже различать различные типы записи) и сосредоточимся только на традиционных атрибутах прав доступа и владения, чтобы получить дано (поиск или запись) разрешение, это уже довольно сложно и трудно выразить find.

Тебе нужно:

  • если файл принадлежит вам, вам нужно это разрешение для владельца (или uid 0)
  • если файл не принадлежит вам, но группа принадлежит вам, то вам нужно это разрешение для группы (или uid 0).
  • если он не принадлежит вам и не входит ни в одну из ваших групп, то применяются другие разрешения (если ваш uid не равен 0).

В findсинтаксисе, в качестве примера с пользователем uid 1 и gids 1 и 2, это будет:

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

Тот удаляет каталоги, которые пользователь не имеет права на поиск и для других типов файлов (символические ссылки исключены, поскольку они не релевантны), проверяет доступ на запись.

Если вы также хотите рассмотреть возможность записи в каталоги:

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

Или для произвольного $userи его членства в группе, полученного из пользовательской базы данных:

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

(это 3 процесса в общем: id, sedи find)

Лучше всего было бы спускаться по дереву от имени пользователя root и проверять права доступа для каждого файла.

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

(это один findпроцесс плюс один sudoи shобрабатывать каждые несколько тысяч файлов, [и printf, как правило , построены в оболочке).

Или с perl:

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

(3 процессы в общей сложности : find, sudoи perl).

Или с zsh:

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

(Всего 0 процессов, но весь список файлов сохраняется в памяти)

Эти решения основаны на access(2)системном вызове. То есть вместо того, чтобы воспроизводить алгоритм, который система использует для проверки прав доступа, мы просим систему выполнить эту проверку с помощью того же алгоритма (который учитывает права доступа, ACL, неизменяемые флаги, файловые системы только для чтения ...) ) он будет использовать, если вы попытаетесь открыть файл для записи, так что вы ближе всего найдете надежное решение.

Чтобы протестировать решения, приведенные здесь, с различными комбинациями пользователей, групп и разрешений, вы можете сделать:

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

Изменение пользователя от 1 до 2 и группы между 1, 2 и 3 и ограничение 9 младшими битами разрешений, так как это уже 9458694 созданных файлов. Это для каталогов, а затем снова для файлов.

Это создает все возможные комбинации u<x>g<y>/<mode1>/u<z>g<w>/<mode2>. Пользователь с uid 1 и gids 1 и 2 будет иметь доступ для записи, u2g1/010/u2g3/777но неu1g2/677/u1g1/777 например.

Теперь все эти решения пытаются определить пути к файлам, которые пользователь может открыть для записи, это отличается от путей, по которым пользователь может изменять содержимое. Чтобы ответить на этот более общий вопрос, необходимо принять во внимание несколько вещей:

  1. $ user может не иметь доступа для записи, /a/b/fileно если он владеет file(и имеет доступ к поиску /a/b, и файловая система не доступна только для чтения, и у файла нет неизменяемого флага, и у него есть доступ оболочки к системе), тогда он сможет изменить права доступа fileи предоставить себе доступ.
  2. То же самое, если он владеет /a/b но не имеет доступа к поиску.
  3. $ user может не иметь доступа, /a/b/fileпотому что у него нет доступа к поиску /aили /a/b, но этот файл может иметь жесткую ссылку, /b/c/fileнапример, в этом случае он может изменить содержимое /a/b/file, открыв его по его /b/c/fileпути.
  4. То же самое с привязными креплениями . Он может не иметь доступа к поиску /a, но /a/bможет быть привязан к нему /c, поэтому он может открыться fileдля записи по /c/fileдругому пути.
  5. Он может не иметь разрешения на запись /a/b/file, но если у него есть доступ для записи, /a/bон может удалить или переименовать его fileи заменить его своей собственной версией. Он изменил бы содержимое файла на/a/b/file даже если это был бы другой файл.
  6. То же самое , если у него есть доступ на запись /a(он мог бы переименовать /a/bв /a/c, создать новый /a/bкаталог и новый fileв нем.

Чтобы найти пути, $userкоторые можно было бы изменить. По адресу 1 или 2 мы больше не можем полагаться на access(2)системный вызов. Мы могли бы изменить наш find -permподход, чтобы разрешить поиск по каталогам или записать доступ к файлам, как только вы станете владельцем:

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

Мы могли бы обратиться к 3 и 4, записав номера устройств и индексов или все файлы, которые $ user имеет разрешение на запись, и сообщать обо всех путях к файлам, которые имеют эти номера dev + inode. На этот раз мы можем использовать более надежный access(2) подходы:

Что-то типа:

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 и 6 на первый взгляд усложняются tбитом разрешений. При применении к каталогам это бит ограниченного удаления, который не позволяет пользователям (кроме владельца каталога) удалять или переименовывать файлы, которые им не принадлежат (даже если они имеют доступ для записи в каталог).

Например, если мы вернемся к нашему предыдущему примеру, если у вас есть доступ к записи /a, то вы должны быть в состоянии переименовать /a/bв /a/c, а затем воссоздать /a/bкаталог и новыми fileтам. Но если tбит включен, /aа вы не являетесь владельцем /a, то вы можете сделать это только при наличии /a/b. Это дает:

  • Если у вас есть каталог, как указано в пункте 1, вы можете предоставить себе право на запись, и бит t не применяется (и вы можете удалить его в любом случае), так что вы можете удалить / переименовать / воссоздать любой файл или каталоги в нем, поэтому все пути к файлам под вами ваши переписать с любым контентом.
  • Если вы не являетесь владельцем, но имеете право на запись, то:
    • Либо t бит не установлен, и вы в том же случае, что и выше (все пути к файлам ваши).
    • или он установлен, и тогда вы не можете изменять файлы, которыми вы не владеете или у которых нет прав на запись, поэтому для нашей цели поиска путей к файлам, которые вы можете изменить, это то же самое, что вообще не иметь разрешения на запись.

Таким образом, мы можем обратиться ко всем 1, 2, 5 и 6 с помощью:

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

Это и решение для 3 и 4 являются независимыми, вы можете объединить их вывод, чтобы получить полный список:

{
  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{$_}++'

Как должно быть понятно, если вы уже все прочитали, часть этого, по крайней мере, касается только разрешений и владения, а не других функций, которые могут предоставлять или ограничивать доступ для записи (FS только для чтения, ACL, неизменяемый флаг, другие функции безопасности. ...). И поскольку мы обрабатываем ее в несколько этапов, часть этой информации может быть неправильной, если файлы / каталоги создаются / удаляются / переименовываются или изменяются их права доступа / владельца во время выполнения этого скрипта, как на занятом файловом сервере с миллионами файлов ,

Замечания о переносимости

Весь этот код является стандартным (POSIX, Unix для tбит), кроме:

  • -print0это расширение GNU, теперь также поддерживаемое несколькими другими реализациями. С findреализациями, которые не поддерживают его, вы можете использовать -exec printf '%s\0' {} +вместо этого и заменить -exec sh -c 'exec find "$@" -print0' sh {} +на -exec sh -c 'exec find "$@" -exec printf "%s\0" {\} +' sh {} +.
  • perlне указана в POSIX, но широко доступна. Вам нужно perl-5.6.0или выше для -Mfiletest=access.
  • zshне является командой, указанной в POSIX. Этот zshкод выше должен работать с zsh-3 (1995) и выше.
  • sudoне является командой, указанной в POSIX. Код должен работать с любой версией, если конфигурация системы позволяет работать perlот имени данного пользователя.
Стефан Шазелас
источник
Что такое поисковый доступ? Я никогда не слышал об этом в традиционных разрешениях: читать, писать, выполнять.
bela83
2
@ bela83, выполнить разрешение на директории (вы не выполняете каталоги) транслирует для поиска . Это возможность доступа к файлам в нем. Вы можете перечислить содержимое каталога, если у вас есть разрешение на чтение, но вы ничего не можете сделать с файлами в нем, если у вас также нет xразрешения на поиск ( бит) в каталоге. Вы также можете иметь разрешения на поиск, но не можете читать , то есть файлы там скрыты для вас, но если вы знаете их имя, вы можете получить к ним доступ. Типичным примером является каталог сессионного файла php (что-то вроде / var / lib / php).
Стефан Шазелас
2

Вы можете объединить параметры с findкомандой, чтобы она обнаружила файлы с указанным режимом и владельцем. Например:

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

Приведенная выше команда выведет список всех записей, которые принадлежат группе «штат» или «пользователи» и имеют разрешение на запись для этой группы.

Вам также следует проверить записи, которые принадлежат вашему пользователю, и файлы, которые доступны для записи всем пользователям, поэтому:

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

Однако эта команда не будет соответствовать записям с расширенным списком ACL. Таким образом, вы можете suузнать все записи для записи:

# su - yourusername
$ find / -writable
apaul
источник
Это будет означать, что файл с r-xrwxrwx yourusername:anygroupили для r-xr-xrwx anyone:staffзаписи.
Стефан Шазелас
Также сообщалось бы, что файлы для записи, находящиеся в каталогах yourusername, не имеют доступа к ним.
Стефан Шазелас
1

Подход зависит от того, что вы действительно тестируете.

  1. Хотите ли вы обеспечить доступ для записи?
  2. Хотите обеспечить отсутствие доступа к записи?

Это потому, что есть много способов прийти к 2), и ответ Стефана хорошо их охватывает (непреложный - тот, который нужно запомнить), и напомним, что есть и физические средства, такие как размонтирование диска или создание его только для чтения на аппаратный уровень (флоппи-вкладки). Я предполагаю, что ваши тысячи файлов находятся в разных каталогах, и вы хотите отчет или вы проверяете по основному списку. (Еще одно злоупотребление Puppet просто ждет, чтобы случиться).

Вы, вероятно, захотите обход дерева Perl Стефана и, если потребуется, «объедините» вывод со списком (su также перехватит отсутствующее выполнение в родительских каталогах?). Если суррогатное материнство является проблемой производительности, вы делаете это для «большого количества» пользователей? Или это онлайн-запрос? Если это постоянное постоянное требование, возможно, пришло время рассмотреть сторонний продукт.

mckenzm
источник
0

Ты можешь сделать...

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

... для списка всех файлов, в которые пользователь не может записать, как записано в stderr в форме ...

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

...или похожие.

См. Комментарии ниже для замечаний по проблемам, которые может иметь этот подход, и объяснение ниже, почему он может работать. Более разумно, однако, вы должны, вероятно, только find обычные файлы, такие как:

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

Короче говоря, teeбудет печатать ошибки, когда он пытается open()файл с любым из двух флагов ...

O_WRONLY

Открыто только для записи.

O_RDWR

Открыто для чтения и письма. Результат не определен, если этот флаг применяется к FIFO.

... и встречи ...

[EACCES]

В компоненте префикса пути отказано в разрешении поиска, либо файл существует, а разрешения, указанные в параметре oflag, отклонены, либо файл не существует, а в разрешении на запись для родительского каталога создаваемого файла или O_TRUNC - указан и разрешение на запись отказано.

... как указано здесь :

teeУтилита будет копировать стандартный ввод в стандартный вывод, что делает копию в ноль или более файлов. Утилита tee не должна буферизовать вывод.

Если -aопция не указана, выходные файлы должны быть записаны (см. « Чтение, запись и создание файла» ) ...

... POSIX.1-2008 требует функциональности, эквивалентной использованию O_APPEND ...

Потому что он должен проверить так же, как test -wделает ...

-w путь к файлу

Истинно, если путь разрешается к существующей записи каталога для файла, для которого будет предоставлено разрешение на запись в файл, как определено в разделе «Чтение, запись и создание файла» . False, если путь не может быть разрешен, или если путь разрешается к существующей записи каталога для файла, для которого разрешение на запись в файл не будет предоставлено.

Они оба проверяют доступность .

mikeserv
источник
При таком подходе вы можете столкнуться с ограничением числа одновременно открытых файлов (если только число файлов не мало). Остерегайтесь побочных эффектов с устройствами и именованными трубами. Вы получите другое сообщение об ошибке для сокетов.
Стефан Шазелас
@ StéphaneChazelas - все вт - я думаю, что это также может быть правдой, что teeбудет зависать, если явно не прерывается один раз за запуск. Это была самая близкая вещь, о которой я мог подумать [ -w... хотя - ее эффекты должны быть близкими, так как она гарантирует, что пользователь сможет ОТКРЫТЬ файл. Гораздо проще, чем любой другой вариант, paxс -oпараметрами формата и / или -tдля проверки EACCESS- но каждый раз, когда я предлагаю, paxлюди, кажется, игнорируют его. И, во всяком случае, единственное, paxчто я нашел, соответствует стандарту AST - в этом случае вы могли бы также использовать их ls.
Микесерв