TT Lab
블로그 시작하기
배우기 러닝패스 코스

지운 토큰이 기록에서 문을 연다

정산 서비스의 비밀번호를 자격 증명으로

TT Lab 에서 이어서 보기

이 실습은 VM 에서 돕니다

우분투 24.04 VM 한 대입니다. root 이고 systemd 와 커널 기능이 그대로 있습니다. 실습 파드에는 capability 가 없어 이 실습이 다루는 설정을 바꿀 수 없기 때문에 VM 으로 돕니다. 처음 뜨는 데 몇 분이 걸립니다. 세션이 끝나면 VM 은 통째로 사라지므로 남기고 싶은 것은 화면에서 복사해 두세요.

목표

같은 서버의 일반 계정이 서비스의 DB 비밀번호를 어디서 읽을 수 있는지 확인하고, 그 값을 systemd 자격 증명(LoadCredential=)으로 옮깁니다. 노출됐던 값은 회전하고, 누가 열어 보려 했는지 감사로 남기고, 남은 사본을 치웁니다.

왜 중요한가

명령행 인자와 환경 변수는 비밀값을 넘기는 가장 쉬운 방법이라 가장 흔합니다. 그런데 리눅스에서 다른 프로세스의 명령행은 누구나 볼 수 있고, systemd 유닛의 Environment= 는 systemctl show 로 일반 계정에게도 보입니다. 서버에 계정이 하나라도 더 있거나 웹 서비스 하나가 뚫리면, 공격자는 권한 상승 없이 이 값을 줍습니다.

systemd 자격 증명은 이 문제를 구조로 풉니다. 비밀값은 root 만 읽는 파일에 두고, systemd 가 서비스가 뜰 때 그 서비스만 읽을 수 있는 자리(/run/credentials/<유닛>/)에 놓아 줍니다. 명령행에도 환경에도 값이 나타나지 않습니다.

단계

  1. intern 으로 프로세스 목록·systemctl show·유닛 파일을 보고 /root/creds/exposure.txt 에 저장합니다.
  2. /etc/credstore(700)와 /etc/credstore/billing-db(600)를 만듭니다.
  3. 유닛을 고쳐 LoadCredential= 과 --password-file 로 바꾸고 systemd 가 다시 읽게 합니다.
  4. 서비스를 다시 띄우고, 프로세스·권한·상태를 확인합니다.
  5. 새 비밀번호로 회전합니다(DB, 자격 증명 파일, 서비스).
  6. /etc/audit/rules.d/credstore.rules 로 읽기 시도를 감사하고, intern 의 시도가 남는지 봅니다.
  7. intern 이 읽을 수 있는 자리에 남은 비밀번호 사본을 치웁니다.

참고

일반 계정의 눈으로 본다

intern 계정으로(su - intern -c '<명령>') 프로세스 목록, systemctl show billing -p Environment, 유닛 파일을 차례로 보고 그 출력을 /root/creds/exposure.txt 에 저장하세요. 비밀번호가 보이는 줄이 들어 있어야 합니다.

root 로 보면 원래 다 보이니 의미가 없습니다. 권한 없는 계정이 무엇을 보는지가 피해의 크기입니다. ps -eo args 는 모든 프로세스의 명령행을 보여 줍니다.

비밀번호를 root 만 읽는 파일로

디렉터리 /etc/credstore(권한 700)를 만들고 지금 DB 비밀번호를 /etc/credstore/billing-db(권한 600, 소유 root)에 넣으세요. 줄바꿈 없이 값만 넣습니다.

지금 비밀번호는 유닛 파일이나 /var/lib/billing-db/password 에서 알 수 있습니다. 파일을 만든 다음 권한을 바꾸면 그 사이에 잠깐 열려 있으니, 처음부터 좁은 권한으로 만드는 습관을 들이세요.

유닛이 파일로 받게 한다

/etc/systemd/system/billing.service 를 고쳐 환경 변수와 --db-password 인자를 없애고, LoadCredential= 로 /etc/credstore/billing-db 를 받아 --password-file 로 넘기세요. systemd 가 새 정의를 읽어야 하고, 유닛 파일에 비밀번호가 남아 있으면 안 됩니다.

LoadCredential=<이름>:<경로> 로 받은 파일은 서비스 안에서 $CREDENTIALS_DIRECTORY/<이름> 에 있습니다. ExecStart 에는 %d/<이름> 으로 적을 수 있습니다. 유닛 파일을 고친 뒤 systemd 에 다시 읽으라고 알려야 합니다.

돌고 있는 프로세스에서 사라졌는가

billing 을 다시 띄우세요. 돌고 있는 프로세스의 명령행과 환경에 비밀번호가 없어야 하고, intern 은 자격 증명 디렉터리를 읽을 수 없어야 하며, http://127.0.0.1:8095/ 는 200 이어야 합니다.

정의를 바꾼 것과 돌고 있는 프로세스가 바뀐 것은 다릅니다. 프로세스의 명령행과 환경은 /proc/<pid>/cmdline·/proc/<pid>/environ 에 있고, 메인 PID 는 systemctl show -p MainPID 로 봅니다.

새 비밀번호로 회전한다

노출됐던 비밀번호를 버립니다. 새 비밀번호를 만들어 billing-db-passwd <새 값> 으로 DB 에 적용하고, /etc/credstore/billing-db 를 같은 값으로 바꾼 뒤 서비스가 새 값으로 돌게 하세요.

세 곳이 같은 값이어야 합니다 — DB, 자격 증명 파일, 그리고 돌고 있는 서비스의 메모리. 서비스는 기동할 때만 파일을 읽습니다.

누가 열어 보려 했는지 남긴다

/etc/credstore/billing-db 를 읽으려는 시도를 키 cred_access 로 감사하는 규칙을 /etc/audit/rules.d/credstore.rules 에 적고 커널에 올리세요. 그다음 intern 으로 그 파일을 읽으려 해 보고(거절됩니다), 그 시도가 감사 기록에 남은 것을 확인하세요.

파일 감시 규칙은 -w <경로> -p <권한> -k <키> 꼴이고 읽기는 r 입니다. 파일을 감시하면 상위 디렉터리에서 막힌 시도는 그 파일까지 닿지 않으니, 무엇을 감시해야 시도가 남는지 생각해 보세요. ausearch -k <키> -i 로 확인합니다.

남은 사본을 치운다

옛 비밀번호든 새 비밀번호든 intern 이 읽을 수 있는 파일에 남아 있으면 안 됩니다. /etc·/opt·/var/log·/tmp·/home 에서 그런 사본을 찾아 없애세요. 로그를 지울 수 없다면 비밀번호가 든 줄만 걷어 내세요.

intern 의 눈으로 찾아야 합니다. grep -rlsF -- <값> <디렉터리> 를 su - intern -c 안에서 돌리면 그 계정이 실제로 읽을 수 있는 파일만 나옵니다. 장애 때 켜 둔 디버그 기록이 흔한 자리입니다.