tuto3: Research about capabilities
This commit is contained in:
parent
9c81bbbd63
commit
b98d1b144c
|
@ -1,5 +1,3 @@
|
|||
\newpage
|
||||
|
||||
Les *capabilities*
|
||||
------------------
|
||||
|
||||
|
@ -15,11 +13,63 @@ Lors des différents tests de permission fait par le noyau, les processus
|
|||
privilégiés outrepassaient ces tests, tandis que les autres devaient passer les
|
||||
tests de l'effective UID, effective GID, et autres groupes supplémentaires...
|
||||
|
||||
Depuis Linux 2.2 (en 1998), les processus privilégiés peuvent activer ou
|
||||
désactiver des *capabilities*, chacune donnant accès à un groupe d'actions
|
||||
privilégiées au sein du noyau.
|
||||
Dans les années 90, ce système s'est rélévé être un peu trop basique et
|
||||
conduisait régulièrement à des abus, au moyen de vulnérabilités trouvées dans
|
||||
les programmes *setuid root*.
|
||||
|
||||
On trouve par exemple :
|
||||
Avant de regarder plus en détail les solutions qui ont été apportées à ce
|
||||
problème, commençons par mettre le doigt sur les difficultés.
|
||||
|
||||
|
||||
### `setuid` : être `root` le temps de l'exécution
|
||||
|
||||
De manière générale, un processus s'exécute dans le contexte de privilège de
|
||||
l'utilisateur qui l'a démarré. Ainsi, lorsque l'on souhaite supprimer un
|
||||
fichier ou un répertoire, en tant que simple utilisateur on ne pourra supprimer
|
||||
que ses propres fichiers, et en aucun cas on ne pourra supprimer ... la racine
|
||||
par exemple.
|
||||
|
||||
Il y a cependant des tâches nécessitant des privilèges, que l'on souhaite
|
||||
pouvoir réaliser en tant que simple utilisateur. C'est le cas notamment de la
|
||||
modification de son mot de passe : il serait inconcevable de devoir demander à
|
||||
son administrateur à chaque fois que l'on souhaite modifier son mot de
|
||||
passe. Pourtant bien que l'on puisse lire le fichier `/etc/passwd`, seul `root`
|
||||
peut y apporter des modifications (il en est de même pour `/etc/shadow` qui
|
||||
contient les hashs des mots de passe).
|
||||
|
||||
C'est ainsi qu'est apparu le `suid-bit` parmi les modes des fichiers. Lorsque
|
||||
ce bit est défini sur un binaire exécutable, au moment de l'exécution, le
|
||||
contexte passe à celui du propriétaire du fichier (`root` si le propriétaire
|
||||
est `root`, mais cela fonctionne quelque soit le propriétaire du fichier : on
|
||||
ne devient pas `root`, mais bien l'utilisateur propriétaire).\
|
||||
|
||||
|
||||
Un autre exemple : pour émettre un ping, il est nécessaire d'envoyer des
|
||||
paquets ICMP. À la différence des datagrammes UDP ou des segments TCP, il n'est
|
||||
pas forcément simple d'envoyer des paquets ICMP lorsque l'on est simple
|
||||
utilisateur, car l'usage du protocole ICMP dans une socket est restreint : il
|
||||
faut soit être super-utilisateur, soit que le noyau ait été configuré pour
|
||||
autoriser certains utilisateurs à envoyer des `ECHO_REQUEST`.
|
||||
|
||||
Pour permettre à tous les utilisateurs de pouvoir envoyer des ping, le
|
||||
programme est donc généralement *setuid root*, pour permettre à n'importe quel
|
||||
utilisateur de prendre les droits du super-utilisateur, le temps de l'exécution
|
||||
du programme.\
|
||||
|
||||
|
||||
Les problèmes surviennent lorsque l'on découvre des vulnérabilités dans les
|
||||
programmes *setuid root*. En effet, s'il devient possible pour un utilisateur
|
||||
d'exécuter du code arbitraire, ce code sera exécuté avec les privilèges de
|
||||
l'utilisateur *root* ! Dans le cas de `ping`, on se retrouverait alors à
|
||||
pouvoir lire l'intégralité de la mémoire, alors que l'on avait juste besoin
|
||||
d'écrire sur une interface réseau.
|
||||
|
||||
|
||||
### Et ainsi les privilèges furent séparés
|
||||
|
||||
Depuis Linux 2.2 (en 1998), les différentes actions réclamant des privilèges
|
||||
sont regroupés en catégories que l'on appelle *capabilities*. Chacune donne
|
||||
accès à un certain nombre d'actions, on trouve notamment :
|
||||
|
||||
* `CAP_CHOWN` : permet de modifier le propriétaire d'un fichier de manière
|
||||
arbitraire ;
|
||||
|
@ -29,93 +79,105 @@ On trouve par exemple :
|
|||
* et beaucoup d'autres, il y en a environ 41 en tout (ça dépend de la
|
||||
version du noyau) !
|
||||
|
||||
::::: {.more}
|
||||
|
||||
### `ping`
|
||||
Petit point historique, Linux n'est pas à l'origine du concept de
|
||||
*capabilities*, il s'agit au départ de la norme POSIX 1003.1e, mais celle-ci
|
||||
s'est éteinte auprès le 17e *draft*. Il devait y être standardisé de nombreux
|
||||
composants améliorant la sécurité des systèmes POSIX, notamment les
|
||||
*capabilities* POSIX, les *ACL POSIX*, ...
|
||||
|
||||
Pour émettre un ping, il est nécessaire d'envoyer des paquets ICMP. À la
|
||||
différence des datagrammes UDP ou des segments TCP, il n'est pas forcément
|
||||
simple d'envoyer des paquets ICMP lorsque l'on est simple utilisateur, car
|
||||
l'usage du protocole ICMP dans une soket est restreint : il faut soit être
|
||||
super-utilisateur, soit que le noyau ait été configuré pour autoriser certains
|
||||
utilisateurs à envoyer des `ECHO_REQUEST`.
|
||||
:::::
|
||||
|
||||
Pour permettre à tous les utilisateurs de pouvoir envoyer des ping, le
|
||||
programme est donc généralement *Setuid root*. Cela permet à n'importe quel
|
||||
utilisateur de prendre les droits du super-utilisateur, le temps de l'exécution
|
||||
du programme.
|
||||
Ainsi, `ping` pourrait se contenter de `CAP_NET_RAW`, une *capability* qui
|
||||
permet notamment d'envoyer des données brutes sur n'importe quelle *socket*,
|
||||
sans passer par les types de *socket* plus restreints, mais accessibles aux
|
||||
utilisateurs.
|
||||
|
||||
Les problèmes surviennent lorsque l'on découvre des vulnérabilités dans les
|
||||
programmes *Setuid root*. En effet, s'il devient possible pour un utilisateur
|
||||
d'exécuter du code arbitraire, ce code sera exécuté avec les privilèges de
|
||||
l'utilisateur *root* ! Dans le cas de `ping`, on se retrouverait alors à
|
||||
pouvoir lire l'intégralité de la mémoire, alors que l'on avait juste besoin
|
||||
d'écrire sur une interface réseau.
|
||||
C'est peut-être encore beaucoup, mais au moins, une vulnérabilité dans `ping`
|
||||
ne permettrait plus d'accéder à tous les fichiers ou à toute la mémoire.
|
||||
|
||||
C'est donc à ce moment que les *capabilities* entrent en jeu : un processus (ou
|
||||
même un thread) privilégié peut décider, généralement à son lancement, de
|
||||
réduire ses *capabilities*, pour ne garder que celles dont il a réellement
|
||||
besoin. Ainsi, `ping` pourrait se contenter de `CAP_NET_RAW`.
|
||||
::::: {.question}
|
||||
|
||||
#### Peut-on faire mieux pour `ping` ? {-}
|
||||
|
||||
Un paramètre existe bien [depuis 2011 dans le
|
||||
noyau](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=c319b4d76b9e583a5d88d6bf190e079c4e43213d) :
|
||||
`net.ipv4.ping_group_range`. Mais ce n'est que [depuis
|
||||
2020](https://github.com/systemd/systemd/pull/13141) que les distributions
|
||||
comme Fedora et Ubuntu se mettent à fournir par défaut une configuration qui
|
||||
permette de se passer de *capabilities* pour lancer `ping`.
|
||||
|
||||
Cette option prend un intervalle d'identifiant numérique de groupes autorisés à
|
||||
créer de `ECHO_REQUEST`. Par défaut la valeur invalide de `1 0` est définie, ce
|
||||
qui signifie qu'aucun groupe n'est autorisé à créer des `ECHO_REQUEST` à moins
|
||||
d'être privilégié.
|
||||
|
||||
:::::
|
||||
|
||||
::::: {.warning}
|
||||
Bien que ce paramètre existe [depuis
|
||||
2011](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=c319b4d76b9e583a5d88d6bf190e079c4e43213d),
|
||||
ce n'est que [depuis 2020](https://github.com/systemd/systemd/pull/13141) que les distributions comme Fedora et Ubuntu
|
||||
se mettent à fournir par défaut une configuration qui permette de se passer de
|
||||
*capabilities* pour lancer `ping`.\
|
||||
|
||||
Si vous vous rendez compte que votre binaire `ping` est dans ce cas, testez
|
||||
depuis un conteneur, par exemple :
|
||||
Nous allons par la suite travailler avec le binaire `ping` pour appréhender les
|
||||
*capabilities*. Si vous vous rendez compte que votre binaire `ping` est dans le
|
||||
cas figure décrit juste avant (avec une distribution ayant mis en œuvre
|
||||
l'option `net.ipv4.ping_group_range`), vous pouvez retrouver le comportement
|
||||
historique en désactivant l'option :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ docker run -it --rm alpine
|
||||
|
||||
(ctnr)# apk add --no-cache acl iputils
|
||||
(1/4) Installing libacl (2.2.53-r0)
|
||||
(2/4) Installing acl (2.2.53-r0)
|
||||
(3/4) Installing libcap (2.50-r0)
|
||||
(4/4) Installing iputils (20210202-r0)
|
||||
|
||||
(ctnr)# su -s/bin/ash daemon
|
||||
|
||||
(ctnr)$ _
|
||||
42sh# sysctl net.ipv4.ping_group_range="1 0"
|
||||
```
|
||||
</div>
|
||||
|
||||
Dans le conteneur le binaire `ping` est *setuid root*, vous pouvez faire des
|
||||
tests en retirant le *setuid* :
|
||||
Pas d'inquiétudes, le paramètre est changé de manière temporaire seulement, au
|
||||
prochain redémarrage il reprendra sa valeur définie par la distribution.
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
(ctnr)# chmod u-s /bin/ping
|
||||
|
||||
(ctnr)$ ping epita.fr
|
||||
ping: socket: Operation not permitted
|
||||
```
|
||||
</div>
|
||||
|
||||
Puis en ajoutant la *capability* :
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
(ctnr)# setcap cap_net_raw+p /bin/ping
|
||||
|
||||
(ctnr)$ ping epita.fr
|
||||
PING epita.fr (172.67.156.141) 56(84) bytes of data.
|
||||
```
|
||||
</div>
|
||||
|
||||
Vous vous retrouverez dans le scénario attendu, tout en pouvant agir sur le
|
||||
binaire `ping` sans avoir peur de casser votre distribution.
|
||||
:::::
|
||||
|
||||
|
||||
### Les ensembles de *capabilities*
|
||||
|
||||
Tout d'abord, il faut noter que chaque *thread* dispose de 5 ensembles de
|
||||
*capabilities*. Au cours de son exécution, il peut changer ses ensembles de
|
||||
*capabilities*. Ceux-ci sont utilisés de la façon suivante :
|
||||
|
||||
- ***bounding*** (B) : c'est l'ensemble limitant des *capabilities* qui pourront
|
||||
faire l'objet d'un calcul lors des prochaines exécutions. Quelques soit les
|
||||
*capabilities* que peut nous faire gagner une exécution, si la *capability*
|
||||
ne se trouve pas dans le *bounding set*, elle ne sera pas considérée et il
|
||||
sera impossible de l'obtenir. L'option `--cap-drop` de Docker modifie cet
|
||||
ensemble pour restreindre les *capabilities* disponibles dans un conteneur ;
|
||||
|
||||
- ***effective*** (E) : il s'agit des *capabilities* actuellement actives, qui
|
||||
seront vérifiées lors des tests de privilèges ;
|
||||
|
||||
- ***permitted*** (P) : ce sont les *capabilities* que la tâche peut placer dans
|
||||
l'ensemble *effective* via `capset(2)`. L'ensemble *effective* ne peut pas
|
||||
avoir de *capabilities* qui ne sont pas dans l'ensemble *permitted* ;
|
||||
|
||||
- ***inheritable*** (I) : est utilisé au moment de la résolution des *capabilities*
|
||||
lors de l'exécution d'un nouveau processus. Il s'agit des *capabilities* qui
|
||||
seront transmises au processus fil. À moins d'avoir la *capability*
|
||||
`CAP_SETPCAP`, cet ensemble ne peut pas avoir plus de *capability* que celles
|
||||
présentent dans l'ensemble *permitted* ;
|
||||
|
||||
- ***ambient*** (A) : existe depuis Linux 4.3 pour permettre aux utilisateurs
|
||||
non-`root` de conserver des *capabilities* d'une exécution à l'autre (sans
|
||||
que l'ensemble des binaires soient marqués avec toutes les *capabilities*
|
||||
dans leur ensemble **inheritable**)[^AMBIENT_PB]. L'ensemble évolue
|
||||
automatiquement à la baisse si une *capability* n'est plus *permitted*.
|
||||
|
||||
[^AMBIENT_PB]: Le problème ayant donné naissance aux *ambient capabilities* est
|
||||
décrit dans cet échange : <https://lwn.net/Articles/636533/>
|
||||
|
||||
|
||||
### Les attributs de fichier étendus
|
||||
|
||||
Une grosse majorité des systèmes de fichiers (ext[234], XFS, btrfs, ...)
|
||||
permet d'enregistrer, pour chaque fichier, des attributs (dits attributs
|
||||
*étendus*, par opposition aux attributs *réguliers* qui sont réservés à l'usage
|
||||
du système de fichiers).
|
||||
du système de fichiers, tels que le propriétaire, le groupe, les modes du
|
||||
fichier, ...).
|
||||
|
||||
Sous Linux, les attributs sont regroupés dans des espaces de noms :
|
||||
|
||||
|
@ -140,9 +202,14 @@ user.foo="bar"
|
|||
```
|
||||
</div>
|
||||
|
||||
En tant que simple utilisateur, vous ne pouvez pas modifier des attributs en
|
||||
dehors de l'espace *user*. Par contre, en *root*, vous pouvez définir et
|
||||
changer les ACL POSIX :
|
||||
Lorsque l'on est propriétaire du fichier, on peut modifier les attributs des
|
||||
espaces *security*, *system* et *user*. Évidemment, *root* peut aussi
|
||||
intervenir et par exemple les ACL POSIX[^ACLPOSIX] (espace *system*) :
|
||||
|
||||
[^ACLPOSIX]: Les ACL POSIX sont des permissions supplémentaires qui viennent
|
||||
s'ajouter aux modes standards du fichier (propriétaire, groupe, reste du
|
||||
monde). Avec les ACL POSIX, on peut doonner des droits à un ou plusieurs
|
||||
utilisateurs ou groupe, de manière spécifique.
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
|
@ -155,8 +222,8 @@ Hello World!
|
|||
```
|
||||
</div>
|
||||
|
||||
Bien que les droits UNIX traditionnels ne vous donnent pas accès au fichier,
|
||||
les ACL POSIX vous autorisent à le lire.
|
||||
Dans cet exemple, bien que les droits UNIX traditionnels ne vous donnent pas
|
||||
accès au fichier, les ACL POSIX vous autorisent à le lire.
|
||||
|
||||
Vous pouvez voir ces attributs bruts avec la commande :
|
||||
|
||||
|
@ -168,11 +235,50 @@ system.posix_acl_access=0sgAAEAD/////AgAEOgDAEAA/////xAABAD////8=
|
|||
```
|
||||
</div>
|
||||
|
||||
Il s'agit d'une représentation d'une structure du noyau, pas forcément très
|
||||
lisible en l'état. On utilisera `getfacl` pour la version lisible.
|
||||
Il s'agit d'une structure du noyau encodée en base64 (la valeur débute par
|
||||
`0s`, de la même manière que l'on a l'habitude de reconnaître l'hexadécimal
|
||||
avec `0x`, voir `getfattr(1)`), pas forcément très lisible en l'état. On
|
||||
utilisera plutôt `getfacl` pour la version lisible.
|
||||
|
||||
::::: {.code}
|
||||
|
||||
Les structures utilisées pour stocker les ACL POSIX dans les attributs étendus
|
||||
sont définies dans `linux/posix_acl_xattr.h` :\
|
||||
|
||||
D'abord un en-tête, composé de la version avec laquelle la suite des octets a
|
||||
été enregistrée (actuellement 2) :
|
||||
|
||||
<div lang="en-US">
|
||||
```c
|
||||
struct posix_acl_xattr_header {
|
||||
uint32_t a_version;
|
||||
};
|
||||
```
|
||||
</div>
|
||||
|
||||
Puis on trouve directement la liste d'entrée(s) :
|
||||
|
||||
<div lang="en-US">
|
||||
```c
|
||||
struct posix_acl_xattr_entry {
|
||||
uint16_t e_tag;
|
||||
uint16_t e_perm;
|
||||
uint32_t e_id;
|
||||
};
|
||||
```
|
||||
</div>
|
||||
|
||||
Le *tag* identifie de quel type d'entrée il s'agit (propriétaire, utilisateur,
|
||||
groupe, masque, reste du monde, ...). Les *perm*issions sont un champ de bits
|
||||
pour la lecture, écriture et exécution. Enfin l'*id*entifiant renseigne sur le
|
||||
numéro d'utilisateur ou de groupe à qui s'applique l'entrée.\
|
||||
|
||||
Les constantes utilisées sont définies dans `linux/posix_acl.h`.
|
||||
|
||||
:::::
|
||||
|
||||
|
||||
#### `ping`
|
||||
### *Capabilities* et attributs étendus pour `ping`
|
||||
|
||||
De la même manière que l'on peut définir de façon plus fine les droits d'accès
|
||||
par utilisateur, un attribut de l'espace de nom *security* peut être défini
|
||||
|
@ -190,8 +296,12 @@ security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=
|
|||
```
|
||||
</div>
|
||||
|
||||
Suivant votre distribution, d'autres programmes sont susceptibles de profiter
|
||||
des attributs étendus pour se passer du *setuid root*, par exemple : `chvt`,
|
||||
`beep`, `iftop`, `mii-tool`, `mtr`, `nethogs`, ...
|
||||
|
||||
Comme pour les ACL POSIX, une structure du noyau est enregistrée comme attribut
|
||||
du fichier ; et on peut l'afficher dans sa version plus lisible :
|
||||
du fichier ; et on peut l'afficher dans sa version plus lisible avec `getcap` :
|
||||
|
||||
<div lang="en-US">
|
||||
```bash
|
||||
|
@ -200,9 +310,265 @@ du fichier ; et on peut l'afficher dans sa version plus lisible :
|
|||
```
|
||||
</div>
|
||||
|
||||
::::: {.code}
|
||||
|
||||
La structure utilisée pour stocker les *capabilities* dans les attributs étendus
|
||||
est définie dans `linux/capability.h` :
|
||||
|
||||
<div lang="en-US">
|
||||
```c
|
||||
struct vfs_cap_data {
|
||||
uint32_t magic_etc;
|
||||
struct {
|
||||
uint32_t permitted;
|
||||
uint32_t inheritable;
|
||||
} data[VFS_CAP_U32];
|
||||
};
|
||||
```
|
||||
</div>
|
||||
|
||||
La valeur `magic` contient la version sur 1 octet, puis 3 octets sont réservés
|
||||
pour des flags. Actuellement un seul flag existe, il s'agit de
|
||||
`VFS_CAP_FLAGS_EFFECTIVE` qui détermine si la liste effective de *capabilities*
|
||||
du programme doit être remplie avec les *capabilities* *permitted* si elle doit
|
||||
rester vide (auquel cas ce sera au programme de s'ajouter les *capabilities* au
|
||||
cours de l'exécution).\
|
||||
|
||||
Deux entiers de 32 bits représentent ensuite les *capabilities* de 0 à 31 sous
|
||||
forme de champ de bits, un entier pour la liste des *capabilities* permises, un
|
||||
autre pour les *capabilities* héritables. Comme il y a une quarantaine de
|
||||
*capabilities*, celles de 32 à 40 se retrouvent à la suite, dans deux nouveaux
|
||||
entiers de 32 bits. C'est pour cela que `data` est un tableau, avec
|
||||
`VFS_CAP_U32` valant 2, car on a deux fois nos 2 entiers de 32 bits à la
|
||||
suite.\
|
||||
|
||||
Il s'agit de la version 2 de la structure. La version 1 était utilisée
|
||||
lorsqu'il n'y avait encore que moins de 33 *capabilities* : il n'y avait alors
|
||||
pas de tableau, seulement les deux entiers de 32 bits. On remarque que les deux
|
||||
versions sont facilement compatibles entre-elles, la seconde version étendant
|
||||
simplement la première.\
|
||||
|
||||
Une version 3 existe, la structure obtient un champ supplémentaire `rootid` :
|
||||
|
||||
<div lang="en-US">
|
||||
```c
|
||||
struct vfs_ns_cap_data {
|
||||
uint32_t magic_etc;
|
||||
struct {
|
||||
uint32_t permitted;
|
||||
uint32_t inheritable;
|
||||
} data[VFS_CAP_U32];
|
||||
uint32_t rootid;
|
||||
};
|
||||
```
|
||||
</div>
|
||||
|
||||
Ce nouveau champ `rootid` est utilisé pour stocker un identifiant d'utilisateur
|
||||
`root` au sein d'un *namespace* User. C'est utile pour pouvoir jouer avec les
|
||||
*capabilities* au sein d'un conteneur non-privilégié. S'il vaut autre chose que
|
||||
0 et que l'on ne se trouve pas dans un *namespace* User, les *capabilities* ne
|
||||
seront pas appliquées.
|
||||
|
||||
:::::
|
||||
|
||||
|
||||
### Gagner des *capabilities*
|
||||
|
||||
C'est à l'exécution (`execve(2)`) que sont calculés les nouveaux ensembles de
|
||||
*capabilities* : c'est donc uniquement à ce moment que l'on peut en gagner de
|
||||
nouvelles. Voici comment les différents ensembles du nouveau processus sont
|
||||
calculés :
|
||||
|
||||
$$p′_A = (\textsf{file caps or setuid or setgid}\: ?\: ∅\: :\: p_A)$$
|
||||
$$p′_P = (p_I\: \&\: f_I)\ |\ (f_P\: \&\: p_B)\ |\ p′_A$$
|
||||
$$p′_E = f_E\: ?\: p_P′\: :\: p′_A$$
|
||||
$$p′_I = p_I$$
|
||||
$$p′_B = p_B$$
|
||||
|
||||
Avec $p$ le processus avant l'exécution, $f$ le fichier exécutable donnant
|
||||
naissance au nouveau processus et $p′$ le nouveau processus.\
|
||||
|
||||
On remarque que sans les *ambients capabilities*, on perd systématiquement les
|
||||
*capabilities* dont on disposait avant l'`execve(2)`, car $f_I$ n'est que très
|
||||
rarement défini sur un exécutable. Dans le cas général, on récupère donc soit
|
||||
$f_P$, soit $p_A$ (les deux étant exclusif : si $f_P$ est défini, $p′_A$ est
|
||||
vide).
|
||||
|
||||
Bien entendu, lorsque l'on se trouve dans le contexte d'exécution de `root` (ou
|
||||
que l'on exécute un binaire *setuid root*), ces calculs sont biaisés, car le
|
||||
super-utilisateur a normalement toutes les *capabilities* (mais toujours
|
||||
limitées par l'ensemble *bounding*) : $f_P$ et $f_I$ contiennent alors toutes
|
||||
les *capabilities*, indifféremment du fichier considéré. Les calculs peuvent
|
||||
alors être simplifiés en :
|
||||
|
||||
$$p′_P = (p_I\: \&\: p_B)$$
|
||||
$$p′_E = p′_P$$
|
||||
|
||||
Il y a cependant une exception, lorsque l'utilisateur réel n'est pas `root`
|
||||
(comme par exemple face à un binaire *setuid root*, seul l'utilisateur effectif
|
||||
change) : dans ce cas, si le fichier expose des *capabilities*, seulement
|
||||
celles-ci sont gagnées.
|
||||
|
||||
|
||||
::::: {.question}
|
||||
|
||||
#### Peut-on placer des *capabilities* sur un script ? {-}
|
||||
|
||||
De la même manière qu'il n'est pas possible d'avoir de script *setuid root*
|
||||
sous Linux[^NO_SETUID_SCRIPT], ajouter des *capabilities* à un script ne
|
||||
permettra pas d'en gagner. Le calcul s'effectue en effet sur les *capabilities*
|
||||
de l'exécutable de l'interpréteur et non sur celles du script.
|
||||
|
||||
[^NO_SETUID_SCRIPT]: <https://unix.stackexchange.com/a/2910>
|
||||
|
||||
:::::
|
||||
|
||||
|
||||
### Gérer ses *capabilities*
|
||||
|
||||
Un *thread* peut agir comme il le souhaite sur les ensembles *effective*,
|
||||
*permitted* et *inheritable*. À condition bien sûr de ne jamais dépasser les
|
||||
*capabilities* déjà contenues dans l'ensemble *permitted*, sauf si l'on dispose
|
||||
de la *capability* `CAP_SETPCAP` : dans ce cas, on se retrouve limité seulement
|
||||
par l'ensemble *bounding*.
|
||||
|
||||
::::: {.code}
|
||||
|
||||
On utilise les appels système `capget(2)` et `capset(2)` pour respectivement
|
||||
connaître les *capabilities* actuelles de notre fil d'exécution et pour les
|
||||
écraser. Ces appels systèmes utilisent une structure d'en-tête et une structure
|
||||
de données, qui sont définies dans `linux/capability.h` :
|
||||
|
||||
<div lang="en-US">
|
||||
```c
|
||||
#define _LINUX_CAPABILITY_VERSION_1 0x19980330
|
||||
#define _LINUX_CAPABILITY_U32S_1 1
|
||||
|
||||
/* V2 added in Linux 2.6.25; deprecated */
|
||||
#define _LINUX_CAPABILITY_VERSION_2 0x20071026
|
||||
#define _LINUX_CAPABILITY_U32S_2 2
|
||||
|
||||
/* V3 added in Linux 2.6.26 */
|
||||
#define _LINUX_CAPABILITY_VERSION_3 0x20080522
|
||||
#define _LINUX_CAPABILITY_U32S_3 2
|
||||
|
||||
typedef struct __user_cap_header_struct {
|
||||
uint32_t version;
|
||||
int pid;
|
||||
} *cap_user_header_t;
|
||||
|
||||
typedef struct __user_cap_data_struct {
|
||||
uint32_t effective;
|
||||
uint32_t permitted;
|
||||
uint32_t inheritable;
|
||||
} *cap_user_data_t;
|
||||
```
|
||||
</div>
|
||||
|
||||
La structure d'en-tête permet de renseigner sur la version de la structure de
|
||||
données que l'on souhaite utiliser ou recevoir. Comme pour les *capabilities*
|
||||
dans les attributs étendus, la première version était utilisée lorsqu'il y
|
||||
avait moins de 33 *capabilities*, ce qui permettait de tout stocker sur un seul
|
||||
entier de 32 bits non signé. Les versions 2 et 3 sont identiques et permettent
|
||||
de récupérer les *capabilities* au moyen d'un tableau de 2 structures.\
|
||||
|
||||
::::: {.warning}
|
||||
|
||||
Les versions 2 et 3 ici ne sont pas comparables aux versions 2 et 3 de nos
|
||||
structures `vfs_cap_data` et `vfs_ns_cap_data` : il n'y a pas de notion de
|
||||
*namespace* ici.
|
||||
|
||||
Il y a eu un couac dans les en-têtes distribués avec Linux 2.6.25, causant des
|
||||
*buffers overflow* dans les applications qui n'avaient pas prévues de gérer les
|
||||
versions. Cela a été corrigé dans la version 2.6.26 : un avertissement est
|
||||
consigné dans les journaux systèmes en cas d'utilisation malheureuse de la
|
||||
version 2.
|
||||
|
||||
:::::
|
||||
|
||||
Dans la pratique, on préférera utiliser `cap_get_proc(3)` et `cap_set_proc(3)`
|
||||
fournis par la `libcap`.
|
||||
|
||||
:::::
|
||||
|
||||
Pour agir sur l'ensemble *bounding*, il faut disposer de la *capability*
|
||||
`CAP_SETPCAP`[^WHY_BOUNDING_SETPCAP]. Lorsque l'on retire une *capability* de
|
||||
cet ensemble, elle n'est pas retirée des autres ensembles et on peut donc
|
||||
continuer de bénéficier des privilèges qu'elle accorde.
|
||||
|
||||
Il faut bien penser, lorsque l'on retire une *capability* de l'ensemble
|
||||
*bounding*, à également la retirer de l'ensemble *inheritable*, sans quoi si le
|
||||
programme exécuté a la *capability* en question dans ses attributs, celle-ci
|
||||
sera préservée (revoir la formule pour $p′_P$, l'ensemble *bounding* n'est pas
|
||||
considéré avec l'ensemble *inheritable*).
|
||||
|
||||
[^WHY_BOUNDING_SETPCAP]: En effet, avant Linux 2.6.25, cet ensemble était
|
||||
utilisé par tout le système, pas seulement pas le *thread* courant et ses
|
||||
fils.
|
||||
|
||||
::::: {.code}
|
||||
|
||||
La consultation et la modification de l'ensemble *bounding* se fait au moyen de
|
||||
`prctl(2)`, en utilisant les paramètres `PR_CAPBSET_READ` ou `PR_CAPBSET_DROP`.
|
||||
|
||||
Le second paramètre attendu est l'une des constantes représentant une
|
||||
*capability*.\
|
||||
|
||||
Avec `PR_CAPBSET_READ`, `prctl(2)` retournera 0 si la *capability* ne fait pas
|
||||
parti de l'ensemble *bounding*, ou 1 si elle est bien présente.
|
||||
|
||||
Avec `PR_CAPBSET_DROP`, `prctl(2)` retirera la *capability* passée en argument
|
||||
de l'ensemble *bounding*. Une fois cette action effectuée, il est impossible de
|
||||
revenir en arrière.\
|
||||
|
||||
Dans la pratique, on préférera utiliser `cap_get_bound(3)` et
|
||||
`cap_drop_bound(3)` fournis par la `libcap`.
|
||||
|
||||
:::::
|
||||
|
||||
Enfin, le *thread* peut aussi modifier à sa guise l'ensemble *ambient*, à
|
||||
condition que les *capabilities* ajoutées soient dans les ensemble *permitted*
|
||||
et *inheritable*.
|
||||
|
||||
::::: {.code}
|
||||
|
||||
On consulte et modifie l'ensemble *ambient* avec `prctl(2)` à qui l'on passe
|
||||
comme premier paramètre `PR_CAP_AMBIENT`. Le second paramètre permet de
|
||||
préciser l'action que l'on veut réaliser :\
|
||||
|
||||
- `PR_CAP_AMBIENT_RAISE` : ajoute la *capability* précisée comme troisième
|
||||
paramètre ;
|
||||
- `PR_CAP_AMBIENT_LOWER` : retire la *capability* précisée comme troisième
|
||||
paramètre ;
|
||||
- `PR_CAP_AMBIENT_IS_SET` : retourne 1 si la *capability* précisée comme
|
||||
troisième paramètre fait parti de l'ensemble *ambient*, sinon retourne 0 ;
|
||||
- `PR_CAP_AMBIENT_CLEAR_ALL` : vide l'ensemble *ambient*.
|
||||
|
||||
:::::
|
||||
|
||||
|
||||
### Explorer les *capabilities* avec un shell
|
||||
|
||||
La `libcap`, au travers des paquets `libcap2-bin` (Debian et ses dérivées) ou
|
||||
`libcap` (Alpine, Archlinux, Gentoo et leurs dérivées), apporte le programme
|
||||
`capsh(1)`, très utile pour appréhender les *capabilities*.
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh# capsh --drop=cap_net_raw -- -c "/bin/ping nemunai.re"
|
||||
/bin/bash: line 1: /bin/ping: Operation not permitted
|
||||
```
|
||||
</div>
|
||||
|
||||
Une autre commande intéressante est `pscap(1)`, de la `libcap-ng`. Parmi tous
|
||||
les programmes en cours d'exécution, cet utilitaire va afficher tous les
|
||||
programmes s'exécutant actuellement avec des *capabilities*, en indiquant pour
|
||||
chacun lesquelles sont actives et disponibles.
|
||||
|
||||
|
||||
::::: {.exercice}
|
||||
|
||||
### Visualisateur de capabilities d'un processus {-}
|
||||
### Visualisateur de *capabilities* d'un processus {-}
|
||||
|
||||
Écrivons maintenant un programme permettant de voir les *capabilities*
|
||||
d'un processus :
|
||||
|
@ -237,10 +603,78 @@ inheritable: 0x0
|
|||
```
|
||||
</div>
|
||||
|
||||
Appelé sans argument, `view_caps` affichera les capabilities du processus
|
||||
courant.
|
||||
Appelé sans argument, `view_caps` affichera les *capabilities* du processus
|
||||
courant, avec en plus les informations sur son ensemble *ambient* et
|
||||
*bounding* :
|
||||
|
||||
Astuces : `capget(2)`, X-macros, ...
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh# ./view_caps
|
||||
cap_user_header_t
|
||||
-----------------
|
||||
Version: 20080522
|
||||
PID: 42
|
||||
|
||||
cap_user_data_t
|
||||
---------------
|
||||
effective: 0x3fffffffff
|
||||
CAP_AUDIT_CONTROL
|
||||
CAP_AUDIT_READ
|
||||
[...]
|
||||
CAP_SYSLOG
|
||||
CAP_WAKE_ALARM
|
||||
permitted: 0x3fffffffff
|
||||
CAP_AUDIT_CONTROL
|
||||
CAP_AUDIT_READ
|
||||
[...]
|
||||
CAP_SYSLOG
|
||||
CAP_WAKE_ALARM
|
||||
inheritable: 0x0
|
||||
|
||||
ambient:
|
||||
CAP_AUDIT_CONTROL
|
||||
CAP_AUDIT_READ
|
||||
[...]
|
||||
CAP_SYSLOG
|
||||
CAP_WAKE_ALARM
|
||||
|
||||
bounding:
|
||||
CAP_AUDIT_CONTROL
|
||||
CAP_AUDIT_READ
|
||||
[...]
|
||||
CAP_SYSLOG
|
||||
CAP_WAKE_ALARM
|
||||
```
|
||||
</div>
|
||||
|
||||
<div lang="en-US">
|
||||
```
|
||||
42sh$ ./view_caps
|
||||
cap_user_header_t
|
||||
-----------------
|
||||
Version: 20080522
|
||||
PID: 42
|
||||
|
||||
cap_user_data_t
|
||||
---------------
|
||||
effective: 0x0
|
||||
permitted: 0x0
|
||||
inheritable: 0x0
|
||||
|
||||
ambient:
|
||||
|
||||
bounding:
|
||||
CAP_AUDIT_CONTROL
|
||||
CAP_AUDIT_READ
|
||||
[...]
|
||||
CAP_SYSLOG
|
||||
CAP_WAKE_ALARM
|
||||
```
|
||||
</div>
|
||||
|
||||
Il peut être intéressant de lancer `view_caps` au sein d'un conteneur Docker
|
||||
pour voir évoluer l'ensemble *bounding*, ou bien d'utiliser `capsh(1)` en
|
||||
amont.
|
||||
|
||||
:::::
|
||||
|
||||
|
@ -263,7 +697,5 @@ Et de ces quelques articles :
|
|||
<https://forums.grsecurity.net/viewtopic.php?f=7&t=2522#p10271>
|
||||
* [Linux Capabilities on HackTricks](https://book.hacktricks.xyz/linux-unix/privilege-escalation/linux-capabilities) :\
|
||||
<https://book.hacktricks.xyz/linux-unix/privilege-escalation/linux-capabilities>
|
||||
|
||||
Pour revenir à Docker, un certain nombre de *capabilities* sont désactivées par
|
||||
défaut ; vous pouvez en ajouter et en retirer via les arguments `--cap-add` et
|
||||
`--cap-drop` du `docker container run`.
|
||||
- [POSIZ Access Control Lists on Linux](https://www.usenix.org/legacy/publications/library/proceedings/usenix03/tech/freenix03/full_papers/gruenbacher/gruenbacher_html/main.html) :\
|
||||
<https://www.usenix.org/legacy/publications/library/proceedings/usenix03/tech/freenix03/full_papers/gruenbacher/gruenbacher_html/main.html>
|
||||
|
|
Loading…
Reference in New Issue