CTF 문제 풀다 해킹당한 이야기

tl;dr

  1. CTF 문제 풀기 위해 redis bind를 0.0.0.0으로 설정했는데 풀고 난 후 까먹고 돌려놓지 않음.
  2. module upload하는 exploit으로 서버 털림
  3. 리얼 월드  Incident Response/Investigation을 내 서버에 해봄 ㅋㅋ

발단

연휴에 아마존 형님으로 부터 한 통의 메일을 받았다. 대략 내용은 네가 갖고 있는 서버가 다른 서버를 공격했다, 네가 한게 아니라면 해킹 당했을 확률이 높으니 확인해봐라.. CTF 문제 Exploit들이나 PoC 테스트할 때 AWS 인스턴스에서 보낸적이 많으니 대수롭지 않게 넘어갔다.

asdf

하지만,, 집착의 아이콘 베조스 형님 답게 ** SECOND NOTIFICATION ** 알림과 24시간 내에 답장할 것을 요구하고 있다.

전개

인스턴스의 Public IP를 자세히 보니 내 개인 서버인 app.imjuno.com 주소인 것을 확인했다. 해당 서버는 ssh public-key 인증을 사용하고 별다른 서비스를 열어둔게 없어 오탐이겠지 생각하고 대충 티켓을 마무리 지으려고 했다.

대충 공격 벡터는 ssh login 밖에 없을 것이라 생각해 private key가 털린 것 같다고 중요한 파일들은 모두 백업하고 다른 key-pair를 이용해 인스턴스를 만들겠다고 답장을 보냈다.

그랬더니 앞으로의 다짐을 메일로 써서 보내라고 한다. ㅠㅜ

3줄 이상 써야할 것 같아서 열심히 private-key 보안을 철저히 하고, ubuntu 계정으로 sudo 커맨드를 사용할 때 password를 사용하게 변경할 것을 약속했다.

근데 여기서 궁금증이 생긴게 어떻게 내 서버에서 공격 패킷이 나갔냐는 것이다. 우선 어떤 공격이 진행 됐는지 확인하기 위해 메일 하단에 첨부된 로그를 살펴 보았다.

payload_class를 보면 exploit:gen/docker_unauth_rce 이런 내용이었다. payload_data를 base64 디코딩 해보면, docker remote API의 접근제어가 걸려있지 않은 서버를 대상으로 특정 entrypoint를 실행시키는 exploit인 것을 확인할 수 있다.

d.sh 를 받고 실행 하는데 어떤 행위를 하는 쉘 스크립트일까? (https://gist.github.com/junorouse/453435135782e4d1c4a4f83b95cadc03)

kinsing이라는 바이너리를 특정 서버 or bitbucket(해당 repo는 지금 삭제됨) 에서 다운로드 후 실행을 하는 스크립트다.

사실 이때까지만 해도 AWS가 오탐했다고 생각했다. 내 서버는 절대 털릴일이 없을 것이라는 무언의 자신감 때문이었을까? (ㅎ.ㅎ)

위기

해당 바이너리를 분석하기 시작했다. golang으로 만들어진 바이너리 였고 스트립 되어있었다. golang 특성상 심볼을 모두 복구할 수 있기 때문에 대충 어떤 프로그램인지 알아낼 수 있었다. 그러던 중 C&C 서버로 보이는 URL을 찾아냈다. 혹시 몰라 해당 URL을 구글 검색하니 매우 귀중한 자료를 찾을 수 있었는데,

C&C 서버로 보이는 URL

https://www.alibabacloud.com/blog/new-outbreak-of-h2miner-worms-exploiting-redis-rce-detected_595743 // h2miner worm의 C&C고 redis exploit을 사용한다고 나와있다. redis exploit 뿐만 아니라 다른 공격 방법도 사용한다고 분석 됐는데 그 중 Docker Remote API Unauthorized RCE 라는 공격이 AWS가 내게 리포트 해준 exploit이다.

흠… 설마 설마 하고 redis 로그를 확인 했다.. 해당 공격 방법은 다음 블로그 참고. (https://blog.wooeong.kr/2020/05/ssrf-to-redis.html)

절정

이모티콘] 네이버 라인 이모티콘 모음 - 문페이스편 : 네이버 블로그
11980:S 23 Mar 04:38:19.877 * Module ‘system’ loaded from /tmp/exp_lin.so
11980:S 22 Mar 14:19:02.050 * Module ‘system’ loaded from /tmp/exp_lin.so
11980:S 22 Mar 02:32:03.184 * Module ‘system’ loaded from /tmp/exp_lin.so
11980:S 20 Mar 08:09:21.899 * Module ‘system’ loaded from /tmp/exp_lin.so
11980:S 18 Mar 12:53:46.233 * Module ‘system’ loaded from /tmp/exp_lin.so
11980:S 16 Mar 22:55:49.485 * Module ‘system’ loaded from /tmp/exp_lin.so
11980:S 16 Mar 01:30:13.683 * Module ‘system’ loaded from /tmp/exp_lin.so
11980:S 14 Mar 20:26:21.484 * Module ‘system’ loaded from /tmp/exp_lin.so
11980:S 13 Mar 11:11:22.432 * Module ‘system’ loaded from ./red2.so
11980:S 13 Mar 02:06:53.651 * Module ‘system’ loaded from /tmp/exp_lin.so
좀 많이 털림..

ㅋㅋㅋㅋㅋㅋㅋ A&D CTF 마냥 42번이나 털린 것을 확인했다. redis가 0.0.0.0으로 bind 되어있어야 해당 공격을 수행할 수 있는데 진짜 0.0.0.0으로 바인드 되었나 의심스러워 내 서버를 포렌식 하기 시작했다.

03월 07일 22:44 (KST)에 변경되었다!

자, 파일 내용을 확인해보면 … bind 0.0.0.0설정이 적용된 말도 안되는 상황을 맞닥뜨렸다. 띠용 띠용 띠용 상태라 왜 내가 저 설정으로 해놨는지 생각하기 시작했다.


추가로 아래에 있는 protection mode는 0.0.0.0 바인딩 되었을 때 뭔가를 보호해주는 것이 아니라 아무런 bind 설정을 하지 않으면 *:6379로 바인딩 되는데 해당 상태 때만 리모트 인터페이스에서 접근하지 못하게 하는 친구다.

$ ps aux | grep redis-server
redis 16416 0.0 0.3 51660 3756 ? Ssl 07:52 0:00 /usr/bin/redis-server *:6379
——
$ nc 13.124.191.129 6379 -v
Connection to 13.124.191.129 port 6379 [tcp/*] succeeded!
-DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command ‘CONFIG SET protected-mode no’ from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to ‘no’, and then restarting the server. 3) If you started the server manually just for testing, restart it with the ‘–protected-mode no’ option. 4) Setup a bind address or an authentication password. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside.

해당 시점은 dreamhack 강의의 실습 문제를 만들고 있었을 때랑 상당히 겹친다. 03월 05일 쯤 Redis에 Protection Mode라는게 생겼다고 신기해 하고 있는 모습이다. (ㅋㅋ)

다만 실습 문제를 다 만들고 binding을 다시 해제 하지 않을 만큼 멍청한 나 자신이 아니기 때문에 믿지 않았다. 문득 그러다 CTF 문제를 풀기 위해 0.0.0.0으로 바인딩 해놨을 것 같아,, 해당 시점에 열렸던 CTF 목록을 찾아보기로 했다.

그렇다. zer0pts를 참여했는데 redis 관련 문제가 나왔던 것을 기억해 대회 중 대화했던 메시지 로그를 찾아보았다. app.imjuno.com 6379에 migrate를 해 flag를 뽑아낸 exploit 코드를 확인할 수 있다…..

이모티콘] 네이버 라인 이모티콘 모음 - 문페이스편 : 네이버 블로그

털린건 확실하고,,, 그럼 이제 어떤 자료들이 털렸나 확인을 해봐야 한다. 우선 홈 폴더인 /home/ubuntu는 755 퍼미션으로 설정되어 있고 몇 개의 중요 자료들이 들어있다. (exploits / poc / private keys 등)

다행히 systemd service들은 namespace를 이용한 sandboxing을 지원한다. redis service 파일을 확인해 보면 여러가지 Protect* directive가 true로 설정되어 있는데,

systemd 사랑해요

실제로 mountinfo를 확인해보면 /home/ 은 접근할 수 없는 폴더로 마운트 되어있다.

이모티콘 중 가장 불신의 이모티콘.jpg - 스퀘어 카테고리

휴휴, 그럼 이제 /var/www/html 웹 폴더인데 아쉽게도 systemd는 해당 폴더를 분리해놓지 않았다. mountinfo로 확인해볼 수 있지만 github에 있는 익스플로잇 코드를 통해 실제 interactive shell을 획득해 ls -al 명령어를 입력해보았다. 예상대로 /home 은 접근할 수 없지만 /var/www/는 목록 뿐만 아니라 내부의 파일 또한 대부분 755로 설정되어 있어 모두 열람이 가능했다. ㅠㅜㅠㅠㅜㅠㅜㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ

해당 폴더에는 몇 개의 발표 자료와 CTF 문제 풀이를 위한 exploit들이 들어 있다. ㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅡㅡㅜㅜㅜㅜㅜㅜㅜㅜㅠㅜ

systemd가 걸어둔 restrictions 때문에 LPE 벡터도 매우 적고 WORM 바이러스라 내부에 존재하는 파일들을 빼갈 것 같지는 않지만 혹시 모르니 서버에 존재하는 모든 private key(대부분 git repo의 release용 read-only key)를 유출되었다고 가정하고 revoke 과정을 거쳤다. 문제는 해당 서버에 집에 있는 PC를 키기 위한 WOL 스크립트가 있는데 집 공유기의 password가 평문으로 저장되어 있다. revoke 시키던가 펌웨어 wipe하고 재설치 하는 과정이 필요할 것 같다.

결말

실제 redis log파일은 https://drive.google.com/file/d/1ryiVDOwSSQWqyn2H5UIO1kYXd9ro3efX/view?usp=sharing 에 올려두었다. 아래 스크립트를 통해 로그 파일은 분석해보면 65번의 공격이 있었고 47번 성공했고 파일 경로를 잘못 올리는 등의 문제로 18번은 실패했다. 공격이 들어온 IP를 분석해보면 대부분 클라우드를 사용했고 DNS Resolve가 되는 IP들이 있는걸 보아하니 2차 공격을 행한 서버도 있는 것 같다.

교훈

  1. CTF 문제 풀 때는 새로운 인스턴스를 생성하고 하자.
  2. AWS 메일은 열심히 읽어보자.
  3. 꺼진불도 다시보자…
  4. https://dreamhack.io/learn/1/15#54
네이버 라인 스티커 (천송이 이모티콘) 다운 : 네이버 블로그
files = [
    'redis-server.log',
    'redis-server.log.1',
    'redis-server.log.2',
    'redis-server.log.3',
    'redis-server.log.4',
    'redis-server.log.5',
    'redis-server.log.6',
    'redis-server.log.7',
][::-1]

attacker_ips = []

def analyze(f):
    data = ''
    with open(f) as _:
        d = _.read()

    flag = False
    ips = []

    for x in d.strip().split('\n'):
        if 'SLAVE OF' in x:
            try:
                z = (x.split('SLAVE OF ')[1].split(':')[0], x.split('addr=')[1].split(':')[0])
            except:
                z = None

            if z is None:
                continue

            ips.append(z)

            for k in z:
                attacker_ips.append(k)

            flag = True

        if 'Module' in x and ('failed to load' in x or 'loaded from' in x or 'not loaded' in x):
            assert(flag == True)
            ips = set(ips)
            print ips
            print x
            print '--->',
            print 'Success' if 'loaded from' in x else 'Fail'
            flag = False
            # assert(len(ips) == 1)
            ips = []
            print '---------------------------------------\n'




if __name__ == '__main__':
    for f in files:
        print '************* %s *************' % f
        analyze(f)
    
    attacker_ips = set(attacker_ips)

    from pprint import pprint

    pprint(attacker_ips)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.