PICOCTF2019] Java Script - Kiddie

노션으로 옮김·2020년 5월 6일
1

wargame

목록 보기
47/59
post-thumbnail

문제

문제페이지에 접속해보면

문자열을 입력받고 제출할 경우 img 태그가 삽입된다.


풀이

전체 html 소스를 확인하자.

<html>
	<head>    
		<script src="jquery-3.3.1.min.js"></script>
		<script>
			var bytes = [];
			$.get("bytes", function(resp) {
				bytes = Array.from(resp.split(" "), x => Number(x));
			});

			function assemble_png(u_in){
				var LEN = 16;
				var key = "0000000000000000";
				var shifter;
				if(u_in.length == LEN){
					key = u_in;
				}
				var result = [];
				for(var i = 0; i < LEN; i++){
					shifter = key.charCodeAt(i) - 48;
					for(var j = 0; j < (bytes.length / LEN); j ++){
						result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
					}
				}
				while(result[result.length-1] == 0){
					result = result.slice(0,result.length-1);
				}
				document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
				return false;
			}
		</script>
	</head>
	<body>

		<center>
			<form action="#" onsubmit="assemble_png(document.getElementById('user_in').value)">
				<input type="text" id="user_in">
				<input type="submit" value="Submit">
			</form>
			<img id="Area" src=""/>
		</center>

	</body>
</html>

소스 분석

하나씩 확인해보자.

페이지가 로드되면 ajax 쿼리로 get 요청을 한 후, 그 결과를 bytes에 저장한다.

var bytes = [];
$.get("bytes", function(resp) {
	bytes = Array.from(resp.split(" "), x => Number(x));
});         

그 후 폼이 입력되면 assemble_png 함수가 호출된다.
이 함수에서는 가장 먼저 입력값의 길이가 LEN과 일치할 경우, 입력값을 key로 설정한다.

function assemble_png(u_in){
    var LEN = 16;
    var key = "0000000000000000";
    var shifter;
    if(u_in.length == LEN){
        key = u_in;
    }

그리고 루프를 도는데 어떤 연산을 통해 오프셋을 설정한 뒤, 앞서 받아온 bytes 안에서 오프셋에 해당하는 값을 가져와서 result에 저장한다.

var result = [];
for(var i = 0; i < LEN; i++){
    shifter = key.charCodeAt(i) - 48;
    for(var j = 0; j < (bytes.length / LEN); j ++){
        result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
    }
}

여기서 중요한 것은 오프셋이 설정되는 과정이다.
key를 하나씩 가져와서, '0'(48)을 기준으로 차이나는 오프셋 크기를 shifter로 설정한다.
그리고 그 shifter를 이용해 bytes에서 값을 가져오는 것이다.

마지막으로 위에서 만든 result값을 png이미지로 로드시킨다.

document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));

key 구하기

'0'을 기준으로 오프셋을 설정하므로 key는 문자가 아닌 숫자로 구성된다는 것을 유추할 수 있다.

또한 만들어진 result가 나타내는 png 파일은 첫 16바이트가 고정된다.
이것은 key 길이와 동일하다.

따라서, 10진수 범위 내에서 png 헤더의 첫 16바이트를 갖게하는 shifter 값을 찾는 방법으로 key 값을 구할 수 있게 된다.

res = requests.get('http://2019shell1.picoctf.com:57738/bytes')
data = res.text.split(' ')

expected = '89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52'.split(' ')

len_round = 16
key = []

def getKey():
    for i in range(len_round):
        k = []
        for shifter in range(10):
            result = data[((shifter * len_round) % len(data)) + i]
            if int(result, 10) == int(expected[i], 16):
                print('Key found : ' + result)
                k.append(shifter)
        key.append(k)

png 파일 생성

png헤더의 첫 16바이트를 만들어내는 key는 두 개였다.

이제 이 key를 이용해 이미지 파일을 실제로 생성해서, 올바른 이미지를 만드는 키 값을 찾아야 한다.

result = [0 for x in range(len(data))]
def decPNG(key, idx):
    for i in  range(len_round):
        shifter = key[i]
        for j in  range(len(data) // len_round):
            result[(j * len_round) + i] = int(data[(((j + shifter) * len_round) % len(data)) + i], 10)
    n = 'flag{}.png'.format(idx)
    try:
        img = Image.open(io.BytesIO(bytes(result)))
        img.save(n)
        print ("Create png successfully [{}]".format(n))
    except IOError:
        print ("Failed to create png [{}]".format(n))

플래그 확인

두 개중 하나만 제대로 된 이미지 파일을 생성했다.

oot@kali:/work/ctf/PICO2019/js_kiddie# python3 ex.py
Create png successfully [flag0.png]
Failed to create png [flag1.png]

생성된 이미지 파일은 qr코드였고, 해당 qr코드를 조회하면 플래그를 확인할 수 있다.

picoCTF{905765bf9ae368ad98261c10914d894e}


Full Exploit Code

import io
import requests
import itertools

res = requests.get('http://2019shell1.picoctf.com:57738/bytes')
data = res.text.split(' ')

expected = '89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52'.split(' ')

len_round = 16
key = []

result = [0 for x in range(len(data))]
def decPNG(key, idx):
    for i in  range(len_round):
        shifter = key[i]
        for j in  range(len(data) // len_round):
            result[(j * len_round) + i] = int(data[(((j + shifter) * len_round) % len(data)) + i], 10)
    n = 'flag{}.png'.format(idx)
    try:
        img = Image.open(io.BytesIO(bytes(result)))
        img.save(n)
        print ("Create png successfully [{}]".format(n))
    except IOError:
        print ("Failed to create png [{}]".format(n))


def getKey():
    for i in range(len_round):
        k = []
        for shifter in range(10):
            result = data[((shifter * len_round) % len(data)) + i]
            if int(result, 10) == int(expected[i], 16):
                k.append(shifter)
        key.append(k)


getKey()

i = 0
for p in itertools.product(*key):
    decPNG(p, i)
    i += 1

참조

https://ryanking13.github.io/2018/03/24/png-structure.html

0개의 댓글