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)

Github Security Virtual Meetup Note

깃헙에서 화상으로 시큐리티 밋업을 진행했다. 역시 신청해두고 자느라 라이브로 보지 못했는데 트위치 스트림에 다시보기 기능이 있어 볼 수 있었다. 재밌는 내용을 공유하면 좋을 것 같아 정리해 보았다.

행사 정보: https://securitylab.github.com/events/2020-04-23-github-security-meetup
라이브 스트림: https://www.twitch.tv/videos/600717225

InQL

GraphQL의 자세한 설명은 공식 홈페이지를 참고하자. A Query Language for your API. (https://graphql.org/) Burp Suite Extension과 Standard-alone Python(+Jython) 형태로 제공된다. GraphQL 취약점의 종류는 4가지로 분류 된다.

  • Missing Authentication/Authorization
  • Resource Exhaustion
  • Information Exposure
  • IDOR

GraphQL을 사용하는 API의 보안 취약점 점검을 위해 해당 API가 어떤 Query Set을 지원하는지 알아야 한다. InQL은 해당 API가 Introspection 통해 알아낸 쿼리 Set 정보들을(Mutation / Query / Subscription) Burp Suite Repeater에 옮기거나 직접 Fuzz Testing할 수 있도록 가공해준다. 다만 한가지 아쉬운 점은 Introspection 기능이 꺼져있을 때 wordlist 기반으로 사용할 수 있는 Query가 뭔지 찾아내는 기는은 아직 없다. (매번 손으로 찾거나 스크립트를 만들어 썻는데 누가 범용적으로 사용할 수 있는거 안 만들어 주나? ㅎㅁㅎ) 해당 프로젝트는 https://github.com/doyensec/inql 에서 확인할 수 있다.


Go-ing for an evening stroll

Trail Of bits(ToB) 선생님들의 Talk다. ToB는 티오리와 마찬가지로 golang 기반의 많은 프로젝트(Block Chain/K8S등)를 audit 해왔다. 해당 과정에서 발견된 Common Vulnerabilities 4가지를 공유한다.

  • Integers / Numerics
  • Standard Library Issues
  • Error Checking / type assertions
  • Defer Semantics

Integers/Numerics

golang의 int는 해당 프로그램이 실행되고 있는 아키텍처에 따라 사이즈가 정해진다. 32bit 시스템은 32bits로 처리하고 64bit 시스템은 64bits로 처리한다.

v, err := strconv.Atoi("4294967377") // 0x100000051
g := int32(v)
// 32bits, v and g both are 81
// 64bits, v is 4294967377, g is 81

(WordPress가 golang highlighting기능을 지원하지 않는다는 것을 알았다 ;;) strconv.Atoi, ParseInt/ParseUint의 xref를 찾아 손쉽게 찾을 수 있다. CodeQL Query를 작성했다고 하는데 https://github.com/github/codeql-go 찾아보니깐 없다. 발견하는 로직이 어렵지 않아 금방 작성할 수 있을 것 같다. 해당 취약점은 쿠버네티스나 블록체인 노드처럼 범용적으로 사용되는 프로젝트에는 문제가 크게 발생할 수 있다. (32bit / 64bit 에서 구동할지 모른다.) 하지만 Commercial 하게 회사 혼자 사용하려고 작성한 소프트웨어에서는 보통 64bit 시스템에서 구동을 하니 크게 문제되진 않지만 좋은 Practice는 아니니깐 개선하는 방향이 좋다.

Standard Library Issues

누구나 사용하는 Standard Library에 모두가 Assume하는 방식대로 작동하지 않는다면??? 대게 메뉴얼을 제대로 읽지 않아 발생한다. (대게가 大가 아니라 竹인걸 알고 계셨나요???)

os.MkdirAll("/some/path/i/want/to/make", 0600);
ioutil.WriteFile("/some/file/i/want/to/make", 0600);

위 코드의 동작은 간단하다. 폴더와 파일을 퍼미션과 함께 생성한다. 그럼 이렇게 생각해보면 어떨까?

  1. 만약 해당 폴더와 파일이 이미 존재한다면?
  2. 에러와 경고를 발생시키지도 않고 퍼미션을 Overwrite하지도 않는다.
  3. 0600 퍼미션은 해당 코드를 실행하고 있는 사용자만 접근 가능한 퍼미션이다. 보통 Sensitive한 폴더/파일을 작성하기 위해 해당 퍼미션을 부여하는데 공격자는 해당 폴더와 파일을 0777 퍼미션으로 미리 만들어 놓고 Sensitive한 파일에 접근할 수 있다. (Pre-population attack)
  4. RTFM

Error Checking / type assertions

Error와 Assertion은 다른 언어와 달리 golang이 갖고 있는 재밌는 친구다. 몇 가지 취약점 케이스를 살펴보자.

v, err := SomeFunc(..)
g, err := SomeFunc(..)

h := someval.(int)

err == nil || err != nil

첫번째는 err변수를 중복해서 사용하는 것이다. (v를 assign할 때 error가 발생하였어도 g를 assign 할 때 nil을 리턴한다면 에러 체크가 우회된다.)

두 번째는 type assertion 기능을 ok check없이 사용하는 것이다. https://tour.golang.org/methods/15 를 확인해보면 ok check없이 사용할 경우 panic을 발생시킨다. 블록체인 노드처럼 네트워킹 서비스에서 panic이 발생해 프로그램이 꺼진다면 (DoS) 매우 큰 문제가 된다.

세 번째는 err != nil을 사용하지 않고 err == nil을 사용해 Data Flow 이해에 어려움을 느끼게 되는 안티 패턴이다.

missed error checks와 type assertion panic도 손쉽게 찾아낼 수 있다. (errcheck -asserts / ineffassign / errcheck -assert -blank 사용)

Defer Semantics

defer은 에러 핸들링을 하거나 동작을 끝마칠 때 사용된다. (panic 발생시 recover || resource.Close(), resource.Finish())

func main() {
	ret := hello(3)
	fmt.Println("return from hello:", ret)
}

func hello(x int8) string {
	defer func() string {
		if r:= recover(); r != nil {
			fmt.Println("let's start error handling", r)
		} else {
			fmt.Println("we don't need to recover.")
		}
		return "hi from deferred function!"
	}()

	switch x {
	case 1:
		panic("not hello")
	case 2:
		panic(nil)
	case 3:
	}

	return "hi from hello!"
}

case 1:

let's start error handling not hello
return from hello: 

defer안에서 반환한 값들은 error여도 모두 무시되기 때문에 file.Close()에서 에러가 발생해도 프로그램 실행은 계속된다.

case 2:

we don't need to recover.
return from hello: 

panicnil을 넘기면 recover의 반환 값도 nil이다. 따라서 에러 핸들링 코드로 가지 않는다.

case 3: 정상적인 케이스

defer를 사용할 때 항상 주의해야 한다. 보다 자세한 내용은 https://github.com/lojikil/kyoto-go-nihilism/blob/master/go-nihilism.md 를 참고하면 된다.


그 뒤에 devsecops와 csp/samesite 관련 talk가 진행되었다. devsecops는 좋은 이야기지만 내가 작성하는 것 보다 더 나은 자료들이 많을 것 같고, csp/samesite 이야기는 technical한 이야기 보다 RoR에 어떻게 효율적으로 적용하는지 설명해준다. (SameSite는 재미난 버그 케이스를 발견해 조만간 포스팅하지 않을까 생각된다.) golang의 경우 인터널을 잘 알고있는 사람이 많은 것 같지 않아 정리해보았다. 메뉴얼을 제대로 읽지 않거나 설명해주는 내용이 부실해 개발자들이 자신만의 추측대로 라이브러리나 기능을 사용해 발생하는 edge-case들을 찾는건 언제나 재미있는 것 같다. CTF 문제로도 나오면 재미있을 것 같고.. 다만 문제로 낸다면 방대한 양의 코드나 메뉴얼을 전부다 살펴보지 않게 범위를 줄여주거나 방향을 제시해야 할 것 같다.

ToB에서 진행한 버추얼 밋업도 자느라 못봤다 ㅜ.ㅜ 이번 코로나 사태로 외국에서 밋업들이 활발하게 진행된다는 것을 알게되었고 대부분 on-site 로 진행되어 해당 지역에 사는 사람들만 참가할 수 있었는데 (NY: tob, bay area: fuzzing meetup 등) 화상으로 변경되어 외국에서도 참가할 수 있어 좋은 것 같다. 우리나라도 정기적인 밋업을 통해 정보 공유가 활발하게 일어나면 좋겠는데 역시나 Sponsoring이 없다면 힘들 것 같기도 ㅋ..

Browser based CSRF Mitigation? Yoooh!!!!!! Same-Site-Cookie!

Chrome 76 부터 Same-Site-Cookie 설정이 default로 enabled되었다는 것을 보았다. 사용자의 Privacy와 CSRF를 막기위해 개발되었다. Privacy는 쿠키를 통한 사용자 트래킹 (?)을 막을 수 있게 되었다는데 이 글에선 CSRF를 중점적으로 다룰 것이다.

우선 CSRF가 무엇인지 이해하고 넘어가야 한다. 하나의 예시를 보여주도록 하겠다.

악의적인 공격자의 웹페이지에 아래와 같은 코드가 있다고 생각해보자.

<html>
<body onload=x.submit()>
<form id=x method=post action=http://vic.tim/delete/3>
</form>
</body>
</html>

vic.tim 사이트에 CSRF를 방어할 수 있는 로직이 없다면 피해자는 자신도 모르게 3번 게시글을 삭제할 것이다. 이를 방어하기 위해 지금껏 시큐리티 미들웨어나 프레임워크들이 했던 방식은 크게 2가지이다.

  1. CSRF Token 검증
  2. Referer 헤더 검증

위 방법은 CSRF 공격을 막는데 효과적이지만 모든 입력받는 컨트롤러에서 확인을 해줘야 하기 때문에 효율적이지는 못하다.

이제 세션쿠키에 헤더를 추가하는 것만으로도 CSRF를 방어할 수 있는 시대가 왔다. 간단한 예제를 통해 알아보자.

Cookie를 설정할 때 CookieName=foobar; Domain=..; Expires=…; 쿠키 key=value 말고 추가적으로 Domain, Expires와 같은 설정을 추가할 수 있다. 이곳에 Same-Site 라는 설정 값이 추가된 것이다.

SameSite의 값은 Strict, Lax, None을 지원한다. 각각의 기능을 설명해보면,

Strict – 사용자가 직접 주소창에 입력하는 top-level navigation을 제외한 모든 요청에 Cookie가 포함되지 않는다.

<?php
header("Set-Cookie: foo=bar; SameSite=Strict;");
header("Set-Cookie : bob=alice; SameSite=Lax;");
header("Set-Cookie  : juno=im;");
echo '<pre>';
var_dump($_SERVER['HTTP_COOKIE']);
echo '</pre>';
<a href="http://app.imjuno.com/q/">go</a>                                   
<br>
<form method="post" action="http://app.imjuno.com/q/">
    <button type=submit>go</button>
</form>
a tag를 이용한 접속

Lax와 Normal 쿠키만 전송되는 것을 확인할 수 있다.

post action을 통한 접속

FORM을 이용한 POST 요청은 Normal 쿠키만 전송된다. 아래는 요청 타입별로 어떤 Same-Site 쿠키가 전송되는지에 대한 정리표이다. 개발할 때 참고하면 좋을 것 같다.

MDN에는 아직 Experimental 기능이라고 나오지만 현재 모든 브라우저에서 지원하고 있다. Chrome이 가장 먼저 적용한줄 알았는데 이미 다른 브라우저들은 적용했더라 -_-

https://caniuse.com/#search=SameSite

구글에서 이미 서버사이드 프로그래밍에 자주 쓰이는 언어들에서 SameSite 쿠키를 사용하는 방법을 깃헙에 올려두었다.

사용자 세션에 SameSite=Strict를 사용할 경우 CSRF를 완벽하게 막을 순 있겠지만 a href를 통한 접속에 쿠키가 같이 보내지지 않기 때문에 유저 편의성을 해칠 수 있다. 예를 들면 이메일에서 링크를 눌러 접속한다던지….

마지막으로 어떠한 미티게이션을 도입하더라도 Silver Bullet은 없기 때문에 (XSS가 발생하면 무의미 해진다 ㅋㅋ) 서버의 코드를 잘 작성하는게 제일 중요하다는 것을 말하구 싶다~!~!

metatron-discovery security review speed run

서론

7월 26일 슬랙에 특성 회사 Github 저장소에 스타를 누를경우 선착순으로 경품을 준다는 메시지가 올라왔다. “처음엔 뭔 이런 이벤트가 다있지?” 하는 마음에 넘어갔지만 며칠 뒤 아래 게시글을 통해 사건의 발단되었다.

ㅋㅋㅋㅋ 그냥 웃고 넘어가려고 했는데 회사 슬랙에 @wooeong이 메시지를 보냈다.

링크를 눌러보니 이 프로젝트의 코드 퀄리티를 대충이나마 짐작할 수 있었다.

와!!!!!!!!!! (Command Injection?)

몇 개월동안 블로그를 방치해두기도 했고 빠르게 작성할 수 있을 것 같아 저녁시간을 이용해서 @wooeong과 함께 speed-run을 진행하고 그 결과를 쉐어해기로 했다. speed-run은 매년 Power Of Community라는 infosec conference에서 우리 회사가 진행하는 해킹 이벤트다. 주어진 짧은 시간(보통 10분~15분)에 해킹 문제를 풀면된다. 해킹이 생소하신 개발자분들은 해커톤 느낌으로다가 생각해주시면 될 것 같다.

우선 저장소에 나와있는 순서대로 빌드부터 해보기로 했다.

빌드가 되긴 한다…

국산 오픈소스 프로젝트의 국룰을 어기고 빌드가 성공적으로 진행된다. 다만 아래 사진처럼 철지난 npm 패키지들을 무수히 사용해 48개의 취약점이 발견되었다. 확인 해보지는 않았지만 regex DoS라던지 XSS 던지 하는 것들이 있을 것 같다.

실행하고 싶어요..

conf에 대한 예제가 있는데 이걸 어떻게 인자로 넣어야 하는지에 대한 설명은 readme 어느 곳에도 없었다. 깔끔하게 손절하려고 하던 순간, https://metatron.app/download/guide_vm/ vm image로 제공해준다.

[metatron@localhost ~]$ cat how_to_get_latest_binaries 
wget https://sktmetatronkrsouthshared.blob.core.windows.net/metatron-public/discovery-dist/latest/druid-0.9.1-latest-hadoop-2.7.3-bin.tar.gz

wget https://sktmetatronkrsouthshared.blob.core.windows.net/metatron-public/discovery-dist/latest/metatron-discovery-latest-bin.tar.gz
[metatron@localhost ~]$ 

홈 디렉토리에 저런 파일이 있다. get하는 방법만 알려주고 run하는 방법은 어디에도 나와있지 않기 때문에 실제 데모페이지가 돌아가고 있는 3.2.6 버전으로 audit을 하기로 했다. 블랙/화이트 박스 테스팅을 모두 진행하였고 전체의 아키텍쳐를 이해하지 않은 상태에서 간단한 스트링서치로만 audit을 진행했다. 한 버그클래스에 여러 버그 케이스가 존재하는 경우는 대표적인 것 하나만 작성했다. 아래는 speed-run을 통해 발견된 취약점들이다.

취약점

IDOR (Insecure Direct Object Reference)

대부분의 엔드포인트에서 권한 분리가 제대로 적용되어있지 않다. 펜딩 멤버와 같이 어드민만 볼 수 있는 API를 호출하는 것이 가능하다. 어드민 덧글을 게스트 유저가 수정해보겠다. 참고로 게스트는 다른 덧글을 수정할 수 있는 권한이 없다. (많은 엔드포인트에서 비슷한 취약점이 발생한다.)

Guest Token
Admin이 작성한 덧글 수정 요청
수정 완료

Arbitrary File Read + Path traversal

확장자 체크없이 인풋 받은 path를 그대로 읽어 출력해준다. 이쯤되면 “기능인가?” 의심해볼 필요가 있다.

SSRF

Host / Port 그리고 GET Parameter 조작이 가능하다.

Default username/password

metatron에서 호스팅해주는 데모 서버에 VM이미지를 받았을 때 존재하는 계정 정보로 로그인이 가능하다. (원래는 pending state를 거쳐야 함) — https://discovery.metatron.app/(polaris / polaris)

결론

전체적으로 코드가 많이 난잡하다. (지금 졸려서 내 글도 난잡한 것 같다.) 잠재적인 취약점이 많이 보이는데 회사일도 아니고 시간 도 없어서 말을 줄이겠다. 빅데이터 관련된 지식에 문외한이라 뭘 하는 프로그램인지는 모르겠지만 뭔가 좋은 것 같다. 이것저것 눌러보니 비주얼라이징이 이쁘게 된다. (한국) 기업치고 자사 프로덕트를 오픈소스하기 까지 쉽지 않았을텐데 코드 퀄리티를 떠나서 이 점은 칭찬하고 싶다. 다만 Github Star를 돈으로 산 행위는 이해할 수 없다. ㅋ.ㅋ

https://github.com/metatron-app/metatron-discovery/blob/f019b72a69e1713e38b5e958daf2b0919b7a5d77/discovery-server/src/main/java/app/metatron/discovery/domain/dataprep/PrepDatasetStagingDbService.java#L248-L249

오픈소스를 마음 먹었으면 이런 주석정도는 지울 수 있지 않나..

업데이트

[13:35 30-Jul-19] Github issue를 통해 오픈소스 메인테이너에게 리포트 ( https://github.com/metatron-app/metatron-discovery/issues/2406 )