Une méthode d'échappement de conteneur non documentée
Auteur : Nitro @360GearTeam
arrière-plan
Récemment, une méthode d'échappement de conteneur non divulguée a été découverte. Lorsqu'un conteneur partage l'espace de noms PID hôte et s'exécute avec l'uid 0 (l'espace de noms utilisateur n'est pas activé et aucune fonctionnalité supplémentaire n'est ajoutée), cela peut être réalisé en utilisant les liens symboliques de certains processus. /proc/[pid]/root
Le conteneur s'échappe.
analyser
/proc/[pid]/rootIntroduction
Selon proc(5)
le manuel, via /proc/[pid]/root
des liens symboliques, les rootfs de n'importe quel processus sont accessibles, que le processus actuel et le processus cible appartiennent ou non au même espace de noms de montage. Ensuite, j'ai trouvé dans le manuel une description des /proc/[pid]/root
problèmes d'autorisation lors de l'accès aux liens symboliques :
Permission to dereference or read (readlink(2)) this
symbolic link is governed by a ptrace access mode
PTRACE_MODE_READ_FSCREDS check; see ptrace(2).
En d’autres termes, pour accéder à ce lien symbolique, vous devez passer par ptrace(2)
les contrôles d’autorisation appropriés. En fait, cette description est assez vague : pourquoi accéder à un lien symbolique nécessite-t-il de vérifier les permissions liées à l'appel système ptrace ?
En regardant ptrace(2)
le manuel et en trouvant PTRACE_MODE_READ_FSCREDS
la section relative aux drapeaux :
Ptrace access mode checking
Various parts of the kernel-user-space API (not just ptrace()
operations), require so-called "ptrace access mode" checks, whose
outcome determines whether an operation is permitted (or, in a
few cases, causes a "read" operation to return sanitized data).
These checks are performed in cases where one process can inspect
sensitive information about, or in some cases modify the state
of, another process. The checks are based on factors such as the
credentials and capabilities of the two processes, whether or not
the "target" process is dumpable, and the results of checks
performed by any enabled Linux Security Module (LSM)—for example,
SELinux, Yama, or Smack—and by the commoncap LSM (which is always
invoked).
Prior to Linux 2.6.27, all access checks were of a single type.
Since Linux 2.6.27, two access mode levels are distinguished:
PTRACE_MODE_READ
For "read" operations or other operations that are less
dangerous, such as: get_robust_list(2); kcmp(2); reading
/proc/[pid]/auxv, /proc/[pid]/environ, or
/proc/[pid]/stat; or readlink(2) of a /proc/[pid]/ns/*
file.
PTRACE_MODE_ATTACH
For "write" operations, or other operations that are more
dangerous, such as: ptrace attaching (PTRACE_ATTACH) to
another process or calling process_vm_writev(2).
(PTRACE_MODE_ATTACH was effectively the default before
Linux 2.6.27.)
Since Linux 4.5, the above access mode checks are combined (ORed)
with one of the following modifiers:
PTRACE_MODE_FSCREDS
Use the caller's filesystem UID and GID (see
credentials(7)) or effective capabilities for LSM checks.
PTRACE_MODE_REALCREDS
Use the caller's real UID and GID or permitted
capabilities for LSM checks. This was effectively the
default before Linux 4.5.
Because combining one of the credential modifiers with one of the
aforementioned access modes is typical, some macros are defined
in the kernel sources for the combinations:
PTRACE_MODE_READ_FSCREDS
Defined as PTRACE_MODE_READ | PTRACE_MODE_FSCREDS.
PTRACE_MODE_READ_REALCREDS
Defined as PTRACE_MODE_READ | PTRACE_MODE_REALCREDS.
PTRACE_MODE_ATTACH_FSCREDS
Defined as PTRACE_MODE_ATTACH | PTRACE_MODE_FSCREDS.
PTRACE_MODE_ATTACH_REALCREDS
Defined as PTRACE_MODE_ATTACH | PTRACE_MODE_REALCREDS.
Après avoir vu cette description, je comprends pourquoi l'accès aux /proc/[pid]/root
liens symboliques nécessite ptrace(2)
des contrôles d'autorisation pertinents, car /proc/[pid]/root
l'opération d'accès aux rootfs du processus cible via des liens symboliques est similaire au suivi d'un processus via l'appel système ptrace, dans la mesure où un processus accède à un autre processus. données.
PTRACE_MODE_READ_FSCREDS
Le bit d'indicateur est une combinaison des bits d'indicateur PTRACE_MODE_READ
et PTRACE_MODE_FSCREDS
, de sorte que le processus appelant doit disposer d'autorisations ou de capacités suffisantes sur le système de fichiers pour /proc/[pid]/root
accéder aux rootfs du processus cible via le lien symbolique.
**Comment le noyau effectue-t-il exactement les vérifications d'autorisation ? **Pour répondre à cette question, vous devez analyser le code source du noyau concerné.
Le diagramme de relation d'appel de fonction pertinent est le suivant :
L'implémentation de la plupart des fichiers dans le système de fichiers proc se fait dans /fs/proc/base.c
des fichiers. .get_link
Lors de l'accès à un lien symbolique, le noyau obtiendra le chemin réel correspondant via la méthode dans l'inode du fichier de lien symbolique . Pour /proc/[pid]/root
, la méthode utilisée .get_link
est proc_pid_get_link()
la fonction.
proc_pid_get_link()
La fonction appellera la fonction dans le même fichier proc_fd_access_allowed()
pour vérifier si le processus appelant dispose des autorisations suffisantes.
proc_fd_access_allowed()
La fonction obtient l'instance du processus cible via l'inode du lien symbolique task_struct
, puis appelle ptrace_may_access()
la fonction pour vérifier les autorisations. La valeur du deuxième paramètre lors de l'appel de cette fonction est PTRACE_MODE_READ_FSCREDS
.
ptrace_may_access()
Les fonctions sont définies dans /kernel/ptrace.c
des fichiers et le travail réel est délégué aux __ptrace_may_access()
fonctions :
/* Returns 0 on success, -errno on denial. */
static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
{
const struct cred *cred = current_cred(), *tcred;
struct mm_struct *mm;
kuid_t caller_uid;
kgid_t caller_gid;
if (!(mode & PTRACE_MODE_FSCREDS) == !(mode & PTRACE_MODE_REALCREDS)) {
WARN(1, "denying ptrace access check without PTRACE_MODE_*CREDS\n");
return -EPERM;
}
/* May we inspect the given task?
* This check is used both for attaching with ptrace
* and for allowing access to sensitive information in /proc.
*
* ptrace_attach denies several cases that /proc allows
* because setting up the necessary parent/child relationship
* or halting the specified task is impossible.
*/
/* Don't let security modules deny introspection */
if (same_thread_group(task, current))
return 0;
rcu_read_lock();
if (mode & PTRACE_MODE_FSCREDS) {
caller_uid = cred->fsuid;
caller_gid = cred->fsgid;
} else {
/*
* Using the euid would make more sense here, but something
* in userland might rely on the old behavior, and this
* shouldn't be a security problem since
* PTRACE_MODE_REALCREDS implies that the caller explicitly
* used a syscall that requests access to another process
* (and not a filesystem syscall to procfs).
*/
caller_uid = cred->uid;
caller_gid = cred->gid;
}
tcred = __task_cred(task);
if (uid_eq(caller_uid, tcred->euid) &&
uid_eq(caller_uid, tcred->suid) &&
uid_eq(caller_uid, tcred->uid) &&
gid_eq(caller_gid, tcred->egid) &&
gid_eq(caller_gid, tcred->sgid) &&
gid_eq(caller_gid, tcred->gid))
goto ok;
if (ptrace_has_cap(tcred->user_ns, mode))
goto ok;
rcu_read_unlock();
return -EPERM;
ok:
rcu_read_unlock();
/*
* If a task drops privileges and becomes nondumpable (through a syscall
* like setresuid()) while we are trying to access it, we must ensure
* that the dumpability is read after the credentials; otherwise,
* we may be able to attach to a task that we shouldn't be able to
* attach to (as if the task had dropped privileges without becoming
* nondumpable).
* Pairs with a write barrier in commit_creds().
*/
smp_rmb();
mm = task->mm;
if (mm &&
((get_dumpable(mm) != SUID_DUMP_USER) &&
!ptrace_has_cap(mm->user_ns, mode)))
return -EPERM;
return security_ptrace_access_check(task, mode);
}
Vous pouvez voir que si le processus actuel et le processus cible se trouvent dans le même groupe de threads, ils disposent de toutes les autorisations.
Ensuite, comme la valeur de mode contient PTRACE_MODE_FSCREDS
le bit d'indicateur, il est d'abord vérifié si la somme du processus appelant fsuid
est cohérente avec la somme fsgid
du processus cible . S'ils sont incohérents, la fonction est appelée pour vérifier si le processus appelant dispose d'autorisations dans l'espace de noms utilisateur du processus cible . Dans le cas contraire, l'accès est refusé.fsuid
fsgid
ptrace_has_cap()
CAP_SYS_PTRACE
CAP_SYS_PTRACE
Ensuite, lorsque le processus cible est défini sur nondumpable et que le processus appelant ne dispose pas d'autorisations dans l'espace de noms utilisateur du processus cible , l'accès est refusé.
Appelez enfin security_ptrace_access_check()
la fonction pour effectuer la vérification finale. Cette fonction est liée au LSM. Ici, nous nous concentrons uniquement sur commcap
l'implémentation de et non sur d'autres implémentations telles que Yama, AppArmor, etc.
Pour commcap
, security_ptrace_access_check()
l'appel final est la fonction /security/commcap.c
dans le fichiercap_ptrace_access_check()
:
/**
* cap_ptrace_access_check - Determine whether the current process may access
* another
* @child: The process to be accessed
* @mode: The mode of attachment.
*
* If we are in the same or an ancestor user_ns and have all the target
* task's capabilities, then ptrace access is allowed.
* If we have the ptrace capability to the target user_ns, then ptrace
* access is allowed.
* Else denied.
*
* Determine whether a process may access another, returning 0 if permission
* granted, -ve if denied.
*/
int cap_ptrace_access_check(struct task_struct *child, unsigned int mode)
{
int ret = 0;
const struct cred *cred, *child_cred;
const kernel_cap_t *caller_caps;
rcu_read_lock();
cred = current_cred();
child_cred = __task_cred(child);
if (mode & PTRACE_MODE_FSCREDS)
caller_caps = &cred->cap_effective;
else
caller_caps = &cred->cap_permitted;
if (cred->user_ns == child_cred->user_ns &&
cap_issubset(child_cred->cap_permitted, *caller_caps))
goto out;
if (ns_capable(child_cred->user_ns, CAP_SYS_PTRACE))
goto out;
ret = -EPERM;
out:
rcu_read_unlock();
return ret;
}
Tout d'abord, il est PTRACE_MODE_FSCREDS
décidé d'utiliser l'ensemble de capacités effectives (ensemble de capacités effectives) ou l'ensemble de capacités autorisées (ensemble de capacités autorisées) pour effectuer une vérification d'autorisation selon que l'indicateur de mode est activé ou non. Ensuite, si le processus appelant et le processus cible appartiennent au même espace de noms utilisateur et que l'ensemble de capacités d'autorisation du processus cible est un sous-ensemble de l'ensemble de capacités du processus appelant, alors le processus appelant réussit la vérification des autorisations. Sinon, il vérifie ensuite si le processus appelant a des capacités dans l'espace de noms utilisateur où se trouve le processus cible CAP_SYS_PTRACE
. Si tel est le cas, la vérification des autorisations est réussie, et sinon, l'accès est refusé.
À ce stade, nous pouvons répondre à la question soulevée au début de l'article, c'est-à-dire que dans la configuration par défaut, tous les processus de conteneur ont le même ensemble de capacités, de sorte que ce conteneur qui partage l'espace de noms PID hôte peut accéder aux rootfs d'autres processus de conteneur. . Les rootfs d'un processus exécuté en tant qu'utilisateur non root sur l'hôte ne sont pas accessibles car le fsuid et le fsgid du processus appelant ne correspondent pas respectivement à l'euid, au suid, à l'uid, à l'egid, au sgid et au gid du processus cible. Le rootfs d'un processus exécuté en tant qu'utilisateur root sur la machine hôte n'est pas accessible car le processus exécuté en tant qu'utilisateur root sur la machine hôte possède toutes les capacités et son ensemble de capacités n'est pas un sous-ensemble de l'ensemble de capacités du processus appelant. .
Le résumé final est le suivant :
- L'accès est autorisé si le processus appelant et le processus cible appartiennent au même groupe de processus.
- Si l'indicateur de mode d'accès est spécifié
PTRACE_MODE_FSCREDS
, l'UID du système de fichiers (fsuid) et le GID du système de fichiers (fsgid) du processus appelant seront utilisés dans les vérifications ultérieures des autorisations du système de fichiers. Si l'indicateur de mode d'accès est spécifiéPTRACE_MODE_REALCREDS
, le véritable UID (uid) et le vrai GID (gid) du processus appelant seront utilisés dans les vérifications ultérieures des autorisations du système de fichiers. - Si l’une des conditions suivantes ne peut être remplie, l’accès est refusé :
- Le fsuid et le fsgid du processus appelant correspondent respectivement à l'euid, au suid, à l'uid, à l'egid, au sgid et au gid du processus cible.
- Le processus appelant a des capacités dans l'espace de noms utilisateur du processus cible
CAP_SYS_PTRACE
.
- Si le processus cible est défini sur non dumpable et que le processus appelant n'a aucune capacité dans l'espace de noms utilisateur du processus cible
CAP_SYS_PTRACE
, l'accès est refusé. - Le module commcap LSM du noyau refuse l'accès si l'une des conditions suivantes n'est pas remplie :
- Le processus appelant et le processus cible appartiennent au même espace de noms utilisateur, et l'ensemble de capacités du processus appelant est un surensemble de l'ensemble de capacités autorisé du processus cible.
- Le processus appelant a des capacités dans le même espace de noms utilisateur que le processus cible
CAP_SYS_PTRACE
.
Utiliser des idées
À partir des recherches ci-dessus, nous pouvons résumer une nouvelle idée pour l’évasion des conteneurs.
D'après les recherches ci-dessus, la raison pour laquelle le conteneur ne peut pas accéder au rootfs du processus exécuté en tant qu'utilisateur non root sur l'hôte dans ce cas est que le fsuid et le fsgid du processus conteneur sont respectivement les mêmes que l'euid, le suid. , uid et egid, sgid et gid du processus cible. Alors, comment pouvons-nous les faire correspondre ? En fait, c'est très simple. Après avoir trouvé le processus exécuté en tant qu'utilisateur non root sur l'hôte, nous créons un utilisateur dans le conteneur avec les mêmes UID et GID que le processus cible, puis utilisons la commande su pour passer à l'utilisateur et avoir l'autorisation d'accéder à la cible. Le processus est /proc/[pid]/root
terminé. Bien entendu, il convient de noter que le processus cible doit être dumpable.
Dans l'exemple suivant, un pod est d'abord créé qui partage l'espace de noms PID de l'hôte, puis une commande est exécutée sur l'hôte en tant qu'utilisateur ordinaire sleep 36000
. Enfin, le système de fichiers de l'hôte est accessible via ce processus dans le pod :
Dans le même temps, les droits d'accès peuvent être étendus dans le conteneur grâce au concept de création et d'adhésion à des groupes auxiliaires . L'exemple suivant ajoute l'utilisateur actuel au groupe Docker pour accéder au moteur Docker de l'hôte dans le conteneur, réalisant ainsi l'échappement du conteneur.
Dans l'exemple, un conteneur partageant l'espace de noms PID de l'hôte est d'abord démarré via Docker, puis l'uid et le gid de l'utilisateur ordinaire de l'hôte sont trouvés via la commande ps, et l'utilisateur correspondant est créé dans le conteneur. /proc/[pid]/root
Accédez ensuite au répertoire hôte via n'importe quel processus ordinaire /run
et constatez que le fichier socket utilisé pour communiquer avec le moteur Docker /run/docker.sock
est autorisé à être accessible aux utilisateurs avec le gid 969. Créez ensuite un groupe avec le gid 969 dans le conteneur, et ajoutez l'utilisateur précédemment créé à ce groupe. Enfin, installez le client Docker pour accéder au moteur Docker de la machine hôte.
REMARQUE : lorsque le conteneur partage l'espace de noms PID de l'hôte, il dispose également de la capacité CAP_SYS_PTRACE, /proc/[pid]/root
ce qui facilite l'échappement via des liens symboliques.
Dans l'exemple suivant, un pod avec l'espace de noms PID d'hôte partagé et la fonctionnalité CAP_SYS_PTRACE est ajouté au cluster.
Ensuite, dans le Pod, /proc/1/root
vous pouvez accéder au système de fichiers hôte via .
Défense et détection
Du point de vue du renforcement de la sécurité, afin d'éviter les problèmes ci-dessus, les opérations suivantes doivent être effectuées dans le cluster Kubernetes :
- Les conteneurs doivent être exécutés en tant qu'utilisateur non root.
- Il est interdit de partager à volonté l'espace de noms PID de l'hôte.
- Il est interdit d'accorder des autorisations arbitraires aux conteneurs
CAP_SYS_PTRACE
.
/proc/<pid>/root
En ce qui concerne la méthode de détection, vous pouvez envisager de surveiller les opérations d'accès aux fichiers dans le conteneur avec comme préfixe de chemin dans le système de détection des menaces en temps réel d'exécution .
Les références
- https://man7.org/linux/man-pages/man2/ptrace.2.html
- https://man7.org/linux/man-pages/man5/proc.5.html
Ce qu'il faut faire dans le cluster rnetes :
- Les conteneurs doivent être exécutés en tant qu'utilisateur non root.
- Il est interdit de partager à volonté l'espace de noms PID de l'hôte.
- Il est interdit d'accorder des autorisations arbitraires aux conteneurs
CAP_SYS_PTRACE
.
/proc/<pid>/root
En ce qui concerne la méthode de détection, vous pouvez envisager de surveiller les opérations d'accès aux fichiers dans le conteneur avec comme préfixe de chemin dans le système de détection des menaces en temps réel d'exécution .
Les références
- https://man7.org/linux/man-pages/man2/ptrace.2.html
- https://man7.org/linux/man-pages/man5/proc.5.html
Lien d'origine : https://www.anquanke.com/post/id/290540