高専セキュリティコンテストに参加しました。insecureというチームで、thrust2799、kyumina、mi_24vと一緒に参加して、3位でした。楽しいコンテストでした。
Slack
参加者を集めるSlackが開きました。これが今回の最大の成功だと思っていて、Slackでは他高専の高専生や運営と楽しく盛り上がることができました。
Slackではemojiやreactionの追加や自由な書き込みが許可されて、 自己紹介や雑談に花が咲きました。ここで、カジュアルに書き込みができる雰囲気が作られたのはとても良いと思いました。
Slackではもちろん運営からの連絡もあって、競技環境の事前公開(本番環境と近い部分がある)が公開されて接続テストをしたり、問題の予告がでたりしました。
当日まで
接続テストがありました。オンライン参加のチームにはとても重要なことでした。接続は事前に申告した高専のネットワークからしかできず、テスト前とかあってぎりぎりになりましたが、ちゃんと接続が確認できました。
さらに、その環境には問題の予告が置いてありました。今回の問題はIoT関連ということで、IoTカー、というものに焦点があたっていました。サーバにはiotcarというシェルスクリプト)と、iotcar.pyというpythonスクリプトが置いてあり、どちらも所有者がrootになっており、読み書き実行の権限は基本的にありませんでしたが、iotcarは、実行可能フラグとスティッキービットsetuid ビットが立っていました(実行時に、所有者の権限で実行される)。
iotcar.pyはIoTカーを模倣したスクリプトで、UDPで適当なパケットを送りつけると、それにあわせた動作をする、かわりに /etc/iotcar 以下に結果を吐き出します。事前環境では、「アクセルの踏み込み量」のみを操作可能でした。
./iotcar でパケットを待ち受けた状態で、 0xf0, 0x40, 0x01, 0x10, 0x04, 0xff, 0xff, 0x01, 0x02, 0xf7 のようなパケットを送りつけると /etc/iotcar/motor に 0xffffが書き込まれることを確認しました(パケット中の0xff, 0xffを書き換えるとmotorに書き込まれる値も変わった)。
これと、IoTカーの仕様のようなものが公開されていたので、こんなスクリプトを組んで準備していました。
import socket import array import argparse import sys command_help = """ command list sportsmode sports mode 0-255 center is 128 handle handle degree 0-255 center is 128 brake brake 0-255 accel accel 0-255 charge battery charge 0: charge mode, 1: run mode hybrid energy mode 0: hybrid, 1: electron, 2: gasoline gear gear 0|N: neutral, 1-5: n speed 254|R: back, 255|P: parking engine power on off 0|off: power off, 1|on: power on window 0|close: close, 1|open: open windowlock 0|off: free, 1|on: locked temp aircon temperature 0-45 airmode aircon run mode 0|off: off, 1: cooler, 2: heater, 3: wind only loof ???? 0|off: off, 1|on: on kumoridome 0|off: off, 1|on: on wiper 0|off: off, 1|on: on doorlock 0|off: free, 1|on: locked door open/close 0|close: close, 1|open: open hazard hazard lamp 0|off: off, 1|on: on light small light 0|off: off, 1|on: on navi 0|off: off, 1|on: on audio 0|off: off, 1|on: on highbeam highbeam light 0|off: off, 1|on: on craction 1sec craction 0: dummy value ABS smooth brake aid 0|off: off, 1|on: on 4WD 0|off: off, 1: auto, 2|on: on """ commandlist = { 'sportsmode': [0x10, 1], 'handle': [0x10, 2], 'brake': [0x10, 3], 'accel': [0x10, 4], 'charge': [0x10, 5], 'hybrid': [0x10, 6], 'gear': [0x10, 7], 'engine': [0x10, 8], 'window': [0x20, 1], 'windowlock': [0x20, 2], 'temp': [0x20, 3], 'airmode': [0x20, 4], 'loof': [0x20, 5], 'kumoridome': [0x20, 6], 'wiper': [0x20, 7], 'doorlock': [0x20, 8], 'door': [0x20, 9], 'hazard': [0x20, 10], 'light': [0x20, 11], 'navi': [0x20, 12], 'audio': [0x20, 13], 'highbeam': [0x20, 14], 'headlamp': [0x20, 15], 'cracshon': [0x20, 16], 'ABS': [0x20, 17], '4WD': [0x20, 18], } def send(address, port, command, value, key, v2=0): server_address = (address, port) max_size = 10 key1 = (key & 0xff00) >> 8 key2 = key & 0x00ff subid, cmdid = commandlist[command] if value == "close" or value == "off": value = 0 elif value == "open" or value == "on": value = 1 if command == '4WD': value = 2 else: value = int(value) print("{} with ({}, {})".format(command, value, v2)) client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) client.sendto(array.array("B", [0xf0, 0x40, 0x01, subid, cmdid, value, v2, key1, key2, 0xf7]), server_address) client.close() if __name__ == '__main__': parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('-a', '--addr', default='localhost', type=str) parser.add_argument('-p', '--port', default=52782, type=int) parser.add_argument('command', type=str, help=command_help) parser.add_argument('value') parser.add_argument('-v2', default=0) parser.add_argument('key', type=str, help="0xkey1key2") args = vars(parser.parse_args()) if not args['command'] in commandlist: print(helpmsg) sys.exit() try: args['key'] = int(args['key'], 16) except: print("key is not hex value") sys.exit() send(args['addr'], args['port'], args['command'], args['value'], args['key'], args['v2'])
それから、前日には競技説明がありました。これの告知が無くて(サイトには確かにあるって書いてあったので私達がわるいんだけど)、うちのチームはみんなして競技説明をすっぽかしました。競技説明があとからでも見られて助かりました。
当日
テスト前というのに学校に出ていって、開会式を待ちました。開会式では、ちょっとヒントっぽい話(実際に問題を解くときに活躍することはなかった)があったり、問題が予定より大幅に減った(ジャンルが6 -> 3になって、「環境設定」や「暗号通信」がなくなった)ことを告知されました。
さらに、今回の問題形式が 問題文なし であることや、他チーム、スコアサーバへの攻撃の禁止を言い渡されました。
競技形式
各チームは、自らに割り当てられたサーバの「脆弱性を修正」します。5分に1度チェックスクリプトが走って(こういうの何ていうんだっけ)、脆弱性の修正が確認されたら得点が入ります。得点は脆弱性ごとに100, 200, 300の点数がつけられており、点数を得られるのは一度だけです(継続していたら点数が増えるとかはない)。
脆弱性にはジャンルが設けられており、 「安全でないWebインタフェース」「不十分な認証/認可/バックドア」「危険なネットワークサービス」のそれぞれに100, 200, 300点問題が1つずつありました。合計1800点です。
競技
とりあえずssh接続をしました。ホームディレクトリにはscuserが所有者となった iotcar.py だけが置かれていました。
iotcar.pyの修正
iotcarを操作するpacketが運営から飛んできそうだなーとおもっててそれを捕まえないことには……と思ってたので、iotcarを実行しようとしました。 python iotcar.py
な感じです。するとpythonがシンタックスエラーを吐いたので修正をはじめました。このときに、 debug option
とかいうヤバそうな文字列を見かけたので、関連箇所を全部コメントアウトしておきました。
iotcar.pyが修正される
参加者から質問が飛んで、修正版 iotcar.py が配布されました(ここで参加者が質問できる雰囲気良い)。私の努力は無に帰して、 pyhon iotcar.py
が正常に起動することを確認した後は、 nohup python iotcar.py &
しておきました。
そして、このまましばらく待つことにしました。ちょっと暇だったので netstat -antu
とかしました。ここで telnet や ftp が生きてることがわかりました。 thrustがこのあたりの処理を買って出てくれたので任せました。
backdoorユーザを発見する
kyuminaの画面を見たのか何だったか、 backdoor
とかいうユーザが存在することを知りました。 /home/backdoor
を訪問すると、READMEと、 PASSWORD_is_backdoor みたいなファイルがありました。
とりあえず README を読むと 「このユーザは開発用です」 みたいなことが書いてありました。とりあえずこのユーザを無効化することにしました。 telnet localhost
して backdoorにbackdoorで入ると入れたので、 passwd
して パスワードを _backdoor_
にかえておきました。これで何かしらの点数が入ったはずです。
Webインタフェースを見る
thrustがapacheが動いてるらしいことを教えてくれて、みたら「デフォルトのページ」「ファイラでphpinfo吐いてて、/etc/passwd吐いてるファイルであるところのindex_old.php」「adminディレクトリ(認証がかかってる)」などがありました。これらは全部www-dataの持ち物で、エディット出来ませんでした。
とりあえず認証が突破できる気がすると思ったので .htpasswd を落としてきて、ローカルの環境で john .htpasswd
したら一瞬で adminのパスワードが password であることがわかりました。
そういうことでadminとして入ると、「IoTカーのWebコントローラ」みたいなのがありました。「status」「update」「date」「manual」くらいの項目があり、statusは/etc/iotcar以下のファイルを読んで、IoTカーの状態を表示してくれる部分のようでしたが、/etc/iotcar以下には未だ何もなかったので(kyuminaがiotcar.pyまわりをやってくれてたけどなにもわからなかった)、意味なし太郎でした。
updateはなんかヤバそうで、ファイルのuploadフォームがありました。まずこれのソースコードを読むと(読む権限はあった)、「ファイルがアップロードされたらfilesディレクトリに移して、unzipして、出てきたディレクトリに移動し、sh command.sh
する」ようなphpがありました。
「あっこれ使える」と思い、 /var/www/html/.htpasswdを .htpasswd_に書き換え、新しい.htpasswdをつくるようなシェルスクリプトを書いて、command.shとして保存し、適当なディレクトリに埋めてzip圧縮したものをuploadしました。
すると、サーバがInternal ServerErrorを吐きました。みると、 /var/www/htmlに.htpasswdMがありました。Windows環境でつくったシェルスクリプトだったので、改行コードの\rがファイル名として認識されたようです。
.htpasswdMをなんとかしようとしたのですが、頼みの綱のWebインタフェースは500だし、.htpasswd自体への書き込み権限はないしで、積んだと思い、「環境のリセット」をお願いしました。
環境のリセットは運営が用意した救済策で「どうしても積んだと思ったら申告してください。15分程度で環境をリセットします」ときいていました。
ここから大変な時間が始まります……
2時間待つ
15分まつ、とのことでしたが、30分待っても環境は復活せず、ちょっとはやいお昼を食べたり、Twitterをしたりしていました。その間にも、他のチームが続々とリセット申告を行いました。運営が「ちょっと、Azureのフォームが重たいので、時間かかってます」ということには、私たちはガルパン鑑賞会を始めていました。
結局、2時間まちました。
再開
競技時間がのこり2時間を切った頃に、やっと復旧して、競技を再開しました。他のチームも結構待たされてて、運営も「root権限でどうにかなりそうな事態ならリセットではなくその旨を申告してみてください」というアナウンスをしていました。
私はもっかいおんなじことをしてInternal Server Errorを吐かせ、「/var/www/html以下をリセットしてください!」とお願いしました。
今度は改行コードをちゃんと修正して、adminのパスワードをpasswordからadminpasswordに変えました。多分これでも点が入ったと思います。
date.php
date.phpは「システムの時刻を設定できます」とのことでしたがよくわからなかったのでソースコードを覗きました。すると「POSTにshというパラメータがあったら、 system('date +s "' . $_POST['sh'] . '"')する」という雰囲気のコードが有り、やばそうでした。
とりあえず、これを用いて、 index_old.phpをindex_hoge.phpにしたり(入力に "; mv /var/www/html/index_old.php /var/www/html/index_hoge.php; echo "OK
などをあたえる)、date.phpをhogedate.phpにしたりしていましたが、このあたりでは点が入らないようでした。
thrustのお手伝い
thrustくんが、「ftpでログインしたさきに、you_must_delete_thisみたいなファイルがある」ということを教えてくれて、それがdeleteされていなかったので、どうしたもんかなと思っていましたが、そういえばと思い ftp localhost
して anonymous
ユーザでパスワードなしでログインすると案の定入れたので、 delete それ
しました。多分これでも点数がはいったはず。
その後行き詰まって
admin
ディレクトリを hoge
に変えたり、やばいphpを軒並み名前変更したりしていましたが、得点が入りませんでした。
index.htmlがデフォルトのままでいろんなパスとかバレてるのが良くないのかな、とか、Options Indexesが良くないのか、とおもい修正もしてみましたが一向に点は入らず……。
おわり
時間いっぱいおしまいになりました。insecureは900点で 3位でした。1位は 1700点のSIGSEGVで、 2位は 1200点の Harekazeでした。序盤に600点くらいとったあと、300点しか取れなくて、サイバー甲子園でもこんな感じだったぞと思いました。
終わって
SIGSEGVがなんであんなに点取れてん、という話になって「開始30分でrootがとれたので」と言われて「!?!?!?!?!」となりました。さらに話を聞くと、「$HOMEに root所有者のsticky bitsetuid bitありのiotcarが存在して、こいつは、 0777 root所有の iotcar.py を実行してるだけだから iotcar.pyに/etc/sudoersを編集するコード書けばok」とのことでした。
このへんから話の雲行きが怪しくなって、というのも「あーなるほど」というチームと「私らのチームそんなiotcarとか言うファイル無いんですけど」というチームが現れました(私らは後者です)。
→ 12/5 に連絡があり、やっぱり環境がちがうケースが存在しました。どうやら二種類の環境が存在していたようです。それぞれで別の部門としてランク分けされました。