Exécuter des commands dans un process de bash élevé en écrivant à l'input standard de son process de script parent

J'ai un simple script bash bash.sh qui démarre une autre instance bash en utilisant pkexec .

 #!/bin/bash bash -c 'pkexec bash' 

Lorsqu'il est exécuté, cela affiche une invite pour que l'user entre son mot de passe. Le script principal bash.sh s'exécute en tant qu'user normal mais l'instance bash démarrée par elle s'exécute en tant que root avec des privilèges élevés.

Lorsque j'ouvre une window de terminal et que j'essaie d'écrire une command à l'input standard du process de bash élevé, il lance une erreur de permission (comme prévu).

 echo 'echo hello' > /proc/<child-bash-pid>/fd/0 

Le problème est que lorsque j'écris le process parent ( bash.sh ), il est transmis au process child bash qui exécute ensuite la command.

 echo 'echo hello' > /proc/<parent-bash.sh-pid>/fd/0 

Je ne suis pas capable de comprendre comment cela est possible? Puisque le parent fonctionne en tant qu'user normal, pourquoi suis-je (un user normal) autorisé à transmettre des commands au process enfant qui s'exécute avec des privilèges plus élevés?

Je comprends le fait que l'input standard du process fils est connectée à l'input standard du script parent, mais si cela est autorisé, tout process ordinaire peut exécuter les commands root en écrivant au process parent d'un process bash rooté.

Cela ne semble pas logique. Qu'est-ce qui me manque?

Note: J'ai vérifié que l'enfant exécute la command transmise au parent en supprimant un file dans /usr/share dont seule la racine aurait l'autorisation.

 sudo touch /usr/share/testfile echo 'rm -f /usr/share/testfile' > /proc/<parent-bash.sh-pid>/fd/0 

Le file a été supprimé avec succès.

C'est normal. Pour le comprendre, voyons comment les descripteurs de files fonctionnent et comment ils sont transmis entre les process.

Vous avez mentionné que vous utilisez GLib.spawn_async() pour générer le script shell. Cette fonction crée probablement un canal à utiliser pour envoyer des données dans le stdin de l'enfant (ou peut-être que vous créez le canal vous-même et le transmettez à la fonction). Pour générer le process enfant, cette fonction fork() désactivera un nouveau process, réarrangera ses descripteurs de files de sorte que le canal stdin devienne fd 0 , puis exec() votre script. Puisque le script commence par #!/bin/bash , le kernel interprète ceci en exec() un shell bash qui exécute ensuite votre script shell. Ce script shell forks et execs encore un autre bash (c'est redondant, en passant, vous n'avez pas vraiment besoin du bash -c dedans). Aucun descripteur de file n'est réorganisé, de sorte que le nouveau process hérite du même canal que son descripteur de file stdin. Notez que ce n'est pas "connecté" à son process parent en soi – en fait, les descripteurs de files font reference à un même tube, celui qui a été créé ou assigné par GLib.spawn_async() . En effet, nous ne faisons que créer des alias pour le pipe: fd 0 dans ces process font tous reference au pipe.

Le process est répété lorsque pkexec est pkexec – mais pkexec est un binary suid root. Cela signifie que, lorsque ce binary est exec() ed, il s'exécute en tant que root, mais son stdin est toujours connecté au tube d'origine. pkexec alors ses vérifications d'autorisation (qui impliquent la request d'un mot de passe), puis finalement exec() s bash. Nous avons maintenant un shell racine qui prend son input à partir d'un tube, alors qu'un certain nombre d'autres process appartenant à votre user ont également une reference à ce tube.

La chose importante à comprendre est que, sous la sémantique POSIX, les descripteurs de files n'ont pas d'autorisation. Les files ont des permissions, mais les descripteurs de files représentent le privilège d'access à un file (ou un tampon abstrait comme un tuyau). Vous pouvez transmettre un descripteur de file à un nouveau process ou même à un process existant (via des sockets UNIX) et l'autorisation d'access au file se propage avec les descripteurs de file. Vous pouvez même ouvrir un file, puis changer son propriétaire en un autre user, tout en accédant au file via le fd d'origine en tant que propriétaire précédent, puisque les permissions ne sont vérifiées qu'au moment de l'ouverture du file. De cette façon, les descripteurs de file permettent la communication entre les limites de privilèges. En ayant un process appartenant à votre user et un process appartenant à root partagent le même descripteur de file, vous accordez aux deux process les mêmes droits sur ce descripteur de file. Et puisque le fd est un canal et que le process racine prend des commands depuis ce canal, cela permet à l'autre process appartenant à votre user d'émettre des commands en tant que root. Le tuyau lui-même n'a pas de concept d'un propriétaire, juste une série de process qui ont des descripteurs de files ouverts.

De plus, comme le model de security Linux de base suppose qu'un user a le contrôle complet de tous ses process, cela signifie que vous pouvez jeter un coup d'œil à /proc pour accéder au fd, comme vous l'avez fait. Vous ne pouvez pas le faire via l'input /proc du process bash qui s'exécute en tant que root (puisque vous n'êtes pas root) mais vous pouvez le faire pour votre propre process et le descripteur de file de pipe obtenu est exactement le même que si vous pourrait le faire directement au process enfant s'exécutant en tant que root. Ainsi, l'écho de données dans le canal provoque le return du kernel aux process de lecture du canal – dans ce cas, seul le shell racine enfant, qui lit activement les commands du canal.

Si le script shell était invoqué à partir d'un terminal, l'écho des données dans son descripteur de file d'input standard finirait par écrire des données sur le terminal et serait affiché à l'user (mais non exécuté par le shell). En effet, les terminaux sont bidirectionnels et, en fait, le terminal serait connecté à stdin et stdout (et stderr). Cependant, les terminaux ont des methods ioctl spéciales pour injecter des données d'input, de sorte qu'il est toujours possible d'injecter des commands dans la coquille racine en tant qu'user (cela prend juste plus qu'un simple echo ).

En général, vous avez découvert une sortingste vérité sur l'escalade de privilèges: dès que vous permettez à un user d'accéder à un shell racine par tous les moyens, toute application exécutée par cet user devrait être supposée pouvoir abuser de cette escalade ça existe). L'user devient root, à des fins de security. Même si ce type d'injection stdin n'était pas possible, par exemple, si vous exécutiez le script sous un terminal, vous pouvez simplement utiliser le support d'injection du keyboard du server X pour envoyer des commands directement au niveau graphique. Ou vous pouvez utiliser gdb pour attacher à un process avec le tuyau ouvert et injecter des écritures dedans. La seule façon de fermer ce trou est d'avoir le shell racine directement connecté à un canal d'E / S sécurisé à l'user (physique) qui ne peut pas être falsifié par des process non privilégiés. Ceci est difficile à faire sans ressortingction ssortingcte de la facilité d'utilisation.

Une dernière chose à noter: normalement, les canaux (anonymes) ont une fin de lecture et une fin d'écriture, c'est-à-dire deux descripteurs de file distincts. La fin est transmise aux process enfant lorsque stdin est la fin de la lecture, tandis que la fin de l'écriture rest dans le process d'origine appelé GLib.spawn_async() . Cela signifie que les process enfants ne peuvent pas écrire dans stdin pour renvoyer des données à eux-mêmes ou au bash en tant que root (bien sûr, les process n'écrivent normalement pas dans stdin, mais rien ne dit que vous ne le pouvez pas) cas cela ne fonctionnerait pas quand stdin est la fin de lecture d'un tuyau). Cependant, le mécanisme /proc du kernel pour accéder aux descripteurs de files d'un autre process subvertit ceci: si un process a un fd ouvert à la fin de la lecture d'un tube, mais que vous essayez d'ouvrir son /proc fd /proc pour l'écriture, en fait vous donner la fin d'écriture du même tuyau à la place. Vous pouvez également searchr l'input /proc correspondant au process d'origine qui a appelé GLib.spawn_async() , find la fin du canal ouvert pour l'écriture et écrire dans ce qui ne dépend pas de ce comportement particulier du kernel ; c'est surtout une curiosité mais ne change pas vraiment le problème de security.