センスがないブログ

趣味の話とかいろいろ

ISCCTF2020のwriteup的サムシン

 

前置き

2020.iscctf.site

2020/10/24 10:00~23:00(JST)で開催されたISCCTF2020に参加しました。

f:id:NonSense:20201024233912p:plain

最終的に6位でした

Pwnは何も知らないので一番簡単そうなBOF問だけ解いてそれ以外は全部ダメでした

formatStringAttack*1とかROPとか何するかは知ってるけどやり方を知らないので結局意味ないですね

でもそれ以外の点が出る問題は全部解けたのでいい感じです。

Miscの競プロ問も面白そうだし点だしても良かったんじゃね?とか思いました。自分は競技プロできないので死ぬほど時間かかりそうです。

f:id:NonSense:20201024234156p:plain

とりあえずPwn以外でWriteupを書いていこうと思います。

 

[Rev 100] strings

Stringsコマンド叩けば出てきます

f:id:NonSense:20201024234935p:plain

[Rev 468] bookshop

本屋さんアプリ的なもののバイナリがもらえます

f:id:NonSense:20201024235806p:plain

とりあえずお買い物してみます

What will you do?
buy(b)/status(s)/read(r)
b
now stock is...
shop status is ...
item                |price  |stock
-----------------------------------
Kirara               380     3
MAX                  380     3
Carat                490     1
Forward              590     2
Miracle              366     3
flag                 65535   1
-----------------------------------
what do you want?
> Kirara
buying Kirara ...
Thank you for your purchase!!
What will you do?
buy(b)/status(s)/read(r)
r
you having...
Kirara
Which one do you read?
>Kirara
Sorry can't read Kirara
What will you do?
buy(b)/status(s)/read(r)
s
shop status is ...
item                |price  |stock
-----------------------------------
Kirara               380     2
MAX                  380     3
Carat                490     1
Forward              590     2
Miracle              366     3
flag                 65535   1
-----------------------------------
your status is ...
--------------------
having...
Kirara
current money is ... 1620
-------------------

 買ったものは読めるようになるっぽいので、flagを買って中身を読んでみたいです。

What will you do?
buy(b)/status(s)/read(r)
b
now stock is...
shop status is ...
item                |price  |stock
-----------------------------------
Kirara               380     2
MAX                  380     3
Carat                490     1
Forward              590     2
Miracle              366     3
flag                 65535   1
-----------------------------------
what do you want?
> flag
Oops!!
You don't have enough money to buy it
Your current money is 1620

 お金が足りないようです。

ここでズルをする方法として、

1. 頑張ってバイナリを読んでflagの中身を取り出す

2. 所持金をめっちゃ増やす

3. flagの値段を買える値段にする

とかがあると思います

自分は、なんか楽そうだったので3の値段安くする方法でやりました

 

値段を設定しているところを探すためにとりあえずバイナリの中身を見てみます

f:id:NonSense:20201025001717p:plain

 IDAはなんかこういい感じにしてくれるのでgdb-pedaで脳死pdisass mainができなくてめんどくさいときにぶん投げると導いてくれます(たぶん)

 なんとなく17Chとか1EAhとか24Ehとかが10進数に直すと380,490,590と、値段で見たことある感じの値があります。そのなかに0FFFFhとflagの値段の65535っぽいのがあるので、これを適当な値に書き換えてみます。

 命令のアドレスで場所を特定して、FF FF 00 00を6E 01 00 00と書き換えます。(10進で366)

f:id:NonSense:20201025003022p:plain

 これで実行すると

Welcome to BookStore!!
What will you do?
buy(b)/status(s)/read(r)
s
shop status is ...
item                |price  |stock
-----------------------------------
Kirara               380     3
MAX                  380     3
Carat                490     1
Forward              590     2
Miracle              366     3
flag                 366     1
-----------------------------------
your status is ...
--------------------
current money is ... 2000
-------------------

ちゃんと書き換わっています

いい感じになったので、flagを買って読んでみます

What will you do?
buy(b)/status(s)/read(r)
b
now stock is...
shop status is ...
item                |price  |stock
-----------------------------------
Kirara               380     3
MAX                  380     3
Carat                490     1
Forward              590     2
Miracle              366     3
flag                 366     1
-----------------------------------
what do you want?
> flag
buying flag ...
Thank you for your purchase!!
What will you do?
buy(b)/status(s)/read(r)
r
you having...
flag
Which one do you read?
>flag
ISCCTF{y0u_c4n_p4tch_b1n4r13s_w1th_xxd_4nd_vim}

 

[Rev 495] The Full Bug

この問題はあんまり良くない解き方をしてしまいました

 

もらえるバイナリは、たぶんコマンドライン引数にフラグ文字列を入れてそれが正しいかそうでないかとか判定するやつだと思います(中身をよく見ていない)

そして、動的解析をしようとするとめっちゃ怒られます

f:id:NonSense:20201025004411p:plain

なんかよくわからんしめんどくさそうなのでIDAさんに突っ込みます。

f:id:NonSense:20201025004951p:plain

関数一覧の一部ですが、C++かRustの人間の温かみをあまり感じられないものが大量に出てきます。

mainから呼び出しを辿っていくと、The_Full_Bug::mainなる関数がでてきて、これが処理の中心な感じがします。

f:id:NonSense:20201025005651p:plain

ダイヤグラムを見る感じ、なんかいろいろなことをしてそうです。

もう少し詳しく見てみます。

右側中央の大きなブロックの中に配列にデータを突っ込んでそうなコードを見つけました。

f:id:NonSense:20201025010228p:plain

 16進部分だけ持ってくると

46 43 5B 51 44 24 47 52 70 26 57 25 76 78 53 7F 4F 43 2C 7F 5B 52 20 73 5B 52 6D 6F 58 53 2C 7C 58 24 2C 24 5B 41 5B 7E 4D 26 44 62 4D 27 44 6F 71 51 5B 7F 71 53 2C 7E 58 27 5F 24 4F 27 76 6D 77 78 71 2C

 こんな感じのデータが取れました。

その後の処理を見ていくと、

_$LT$alloc..vec..Vec$LT$T$GT$$u20$as$u20$core..ops..deref..Deref$GT$::deref::h095f8fa7760680c1
core::slice::_$LT$impl$u20$$u5b$T$u5d$$GT$::iter::hbf4a62277f4c02b7
core::iter::traits::iterator::Iterator::map::h4ec00d9c43bda0fd
core::iter::traits::iterator::Iterator::collect::h5b2f0cfab82d6013
base64::decode::decode::h75202e9d7508ae03
core::result::Result$LT$T$C$E$GT$::unwrap::hef1d52418495604f
alloc::string::String::from_utf8::h9956b5d28caa9b81
core::result::Result$LT$T$C$E$GT$::unwrap::hf0324079e5d371f6
core::fmt::ArgumentV1::new::h593d68c1ab5846f7
core::fmt::Arguments::new_v1::h09c81c25c4a3b7d7
std::io::stdio::_print::h20eb70976f14aeea

のように関数呼び出しが続きます。

わざわざ1byteごとにメモリに入れてたのも踏まえつつ、雰囲気で見ると、mapで1byteづつ取り出して各々なんかしらの処理を加えたものをbase64でデコードしたものをプリントしているように見えます。

だいたいの場合1byteごとに処理するのはXORか加減算くらいなので、とりあえずCyberChefにさっきのバイト列を突っ込んでXOR Brute Forceをしてみます

f:id:NonSense:20201025011559p:plain

なんとなくKey = 0x15のときだけBase64っぽいので、Base64デコードまでしてみます。

f:id:NonSense:20201025011755p:plain

フラグでました

たぶん本当はデバッガ検知を無効にしたりAngrで殴ったり色々しないといけないような気がします。

 

[Web 100] Greetinjs

 JSのソースを読むとあります

f:id:NonSense:20201025012525p:plain

 

[Web 285] Yonezer

 アクセスするとこんな画面です

f:id:NonSense:20201025013213p:plain

Lets' Watchを押すとこんな感じです

f:id:NonSense:20201025013301p:plain

 Sourceを押すとソースコードを見せてくれます

<html>
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="style.css">
</head>
<body>

<?php

function html($string) {
    return htmlspecialchars($string);
}

$flag = file_get_contents("../flag.txt");

class secret{

    public function data(){
        global $flag;
        echo($flag);
    }

}



class share_video{
    public $text="Hello Everone";

    public function data(){
        echo("<h1>" . html($this->text) . "</h1><br>");
        echo("<MARQUEE><h1>Do you like this video 👀?</h1></MARQUEE>\n");
        $urls = ["https://www.youtube.com/embed/s582L3gujnw","https://www.youtube.com/embed/gJX2iy6nhHc", "https://www.youtube.com/embed/SX_ViT4Ra7k","https://www.youtube.com/embed/Zw_FKq10S8M"];
        $num = rand(0,3);
        $url = $urls[$num];
        echo ("<div id=\"all\"><iframe width=\"1000\" height=\"600\" src=\"". $url . "\"></iframe></div>");

        }
    }


$serialized = @$_GET["data"];
$hoge = @unserialize($serialized);
if($hoge){
    $hoge->data();

}
?>
</body>
</html>

 画面表示をするために、オブジェクトのdataメソッドを呼んでいますが、オブジェクトの取得元は、GETパラメータで受け取った文字列をunserialize関数でphp変数に戻したものです。

www.php.net

 マニュアルにあるように、unserialize関数はユーザが直接いじれる値に使ってはいけないようです。

 問題のソースコードを読む感じでは、flag変数にflag.txtの内容があり、secretクラスのdataメソッドを呼ぶと表示してくれそうなので、そういう感じにしたいです。

ここで、URLをみてみると、

http://[server_addr]/share.php?data=O:11:"share_video":1:{s:4:"text";s:13:"Hello%20Everone";}

 と良さげな感じのデータが乗っています。これはシリアライズされたクラスのデータのハズです。

www.phpinternalsbook-ja.com

 中身はよくわかんないですが、とりあえず今はsecretのdataを呼び出せればいいので、クラス名っぽいところと文字数だけ変えてアクセスしてみます。

http://[server_addr]/share.php?data=O:6:"secret":1:{s:4:"text";s:13:"Hello%20Everone";}

f:id:NonSense:20201025022202p:plain

[Web 475] mark damn it

 Markdownを入力してConvertを押すとHTMLに変換してくれます。

f:id:NonSense:20201025022905p:plain

 問題添付ファイルのうち、Gemfileを見てみます。

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "kramdown", "2.2.1"
gem "sinatra"
gem "puma"

 MarkdownからHTMLへの変換はkramdownを使っていそうです。

わざわざバージョンを指定している感じがとても怪しいので少し調べてみます。

nvd.nist.gov

 2020年の7月に発表されたての脆弱性:CVE-2020-14001が出てきました。

kramdownの2.3.0未満のバージョンには任意ファイルの読み込みと任意コードの実行ができる脆弱性があるようです。

今回の問題では2.2.1が使われているので、この脆弱性を使えそうです。

 

www.feneshi.co

 このページ*2のように、テンプレートオプションの中にうまい具合にしてコマンドを入れ込んで、サーバのディレクトリ内を調べていきます。

f:id:NonSense:20201025024942p:plain

 フラグファイルっぽいものがあったので中身を覗いてみます

f:id:NonSense:20201025025056p:plain

 

[Web 491] crackjwt

f:id:NonSense:20201025025747p:plain

 かわいい*3

 Get Flagを押してみます

f:id:NonSense:20201025025947p:plain

 adminじゃないとフラグは読ませてくれないようです。

問題文がjwtをcrackしろと言っているのでjwtについて調べてみます

techblog.yahoo.co.jp

 なんかしらの情報とハッシュをくっつけて認証とかに使うモノのようです。

 

ここで問題ページのソースコードを見てみます。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>crackjwt</title>
    <style>
        p {
            text-align: center;
        }

        .flgBtn {
            display: inline-block;
            padding: 0.3em 1em;
            text-decoration: none;
            color: #1DA1F2;
            border: solid 2px #1DA1F2;
            border-radius: 3px;
            transition: .5s;
        }

        .flgBtn:hover {
            background: #1DA1F2;
            color: white;
        }
    </style>
</head>

<body>
    <p>
        <img src="static/welcome.gif" alt="welcome"><br>
        <a href="flag.php" class="flgBtn">Get Flag</a>
    </p>
    <!-- <a href="?source">debug</a> -->
</body>

</html>

 コメントに消しそびれみたいなものがあるのでその記述にそって、?sourceをURLにつけてアクセスしてみると、そのページのphpソースコードが表示されました。

indexページとflag.phpのソースがそれぞれ見れたので、関係ありそうなところだけ見ていきます。

 index

$file = fopen('/var/www/app/private/secret.txt', 'r');
$secret = fgets($file);
fclose($file);
if (!isset($_COOKIE['token'])) {
    setcookie('token', generate($secret));
}

function generate($secret)
{
    $header = json_encode(array(
        'alg' => 'sha256',
        'typ' => 'JWT'
    ));

    $payload = json_encode(array(
        'isAdmin' => '0'
    ));
    $signature = hash('sha256', $header . $payload . $secret);
    return trim(base64_encode($header), '=') . '.' .
        trim(base64_encode($payload), '=') . '.' .
        trim(base64_encode($signature), '=');
}

 flag.php

$file = fopen('/var/www/app/private/secret.txt', 'r');
$secret = fgets($file);
fclose($file);

if (!isset($_COOKIE['token'])) {
    $flg = 1;
}


$parted = explode('.', $_COOKIE['token']);
$signature = $parted[2];

if (isset($flg) || hash('sha256', base64_decode($parted[0]) . base64_decode($parted[1]) . $secret) != base64_decode($signature)) {
    die('<script>alert("Invalid token!!");document.location="/"</script>');
}

$payload = json_decode(base64_decode($parted[1]), true);
$isAdmin = $payload["isAdmin"];
if ($isAdmin == 0) {
    echo '<img src="static/nyoronyoro.gif" alt="nyoronyoro"><p>You don\'t have the authority to read flag.<br>Please come back as an administrator.</p>';
} else {
    require('/var/www/app/private/flag.php');
    echo '<img src="./static/congrats.gif" alt="congrats"><p>Congrats!!<br><strong>' . $flag . '</strong></p>';
}

 Cookieの中のtokenがJWTのようです。

Cookieの中身を確認してみると、

token=eyJhbGciOiJzaGEyNTYiLCJ0eXAiOiJKV1QifQ.eyJpc0FkbWluIjoiMCJ9.Yzc5Y2Y2MzlkYjkyNjdjZDkwNzJhNDkyODU0ZTE0ZWYwOTI2NDI3NTlkN2M0YmViN2Y1NDBjOTU4NWYzNzFjYg

のようになっています。

jwtは「.」区切りでbase64エンコードされているので各々デコードすると、

{"alg":"sha256","typ":"JWT"}
{"isAdmin":"0"}
c79cf639db9267cd9072a492854e14ef092642759d7c4beb7f540c9585f371cb

このように3つのデータを取り出せます。

上から、ヘッダ, ペイロード, シグネチャです。

isAdminを1にすればadminとして認めてくれそうな気がします。

このページにおいて、認証処理がどのようになっているかを確認するためにflag.phpのコードを見てみます。

 

flag.phpのコードから、認証の処理は

$parted = explode('.', $_COOKIE['token']);
$signature = $parted[2];
if (isset($flg) || hash('sha256', base64_decode($parted[0]) . base64_decode($parted[1]) . $secret) != base64_decode($signature)) {
    die('<script>alert("Invalid token!!");document.location="/"</script>');
}

 のように、tokenのヘッダとペイロードとsecretをつなげたモノのsha256ハッシュが、シグネチャと一致していないと弾かれるようになっています。

 

なんとかして、でっち上げたJWTを本物と認識させたいです。

moneyforward.com

 このサイトにあるnone-attackは、今回の問題では3シグネチャが必ず必要になるため使うことができません。

また、brute-force secretでもどの辞書を使っても特定することができませんでした。脆弱なsecretは使われていないようです。

 

そうなると、別の方法でsecretを手に入れなければなりません。

flag.phpのコードを見ると、secretは

 /var/www/app/private/secret.txt

 にあるようです。 まともな設定なら見られなさそうな場所です。

問題の添付ファイルにnginx.confがあるので中身を見てみます。

server {
    index index.php index.html;
    server_name localhost;
    root /var/www/html;
    
    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location /static {
        alias /var/www/app/static/;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass crackjwt:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

qiita.com

 staticのエイリアスパストラバーサルが起こせそうです。

 secret.txtを読んでみたいので

http://[server_addr]/static../private/secret.txt

にアクセスしてみます。

f:id:NonSense:20201025041035p:plain

 secret.txtの中身のようなものが手に入りました。

 JWTの形式は先ほどの通りなので、

{"alg":"sha256","typ":"JWT"}{"isAdmin":"1"}48a939f9d0ef3778ee4fbbca6ffdd933

 をsha256ハッシュしてBase64エンコードしたものがシグネチャ部分になります。

f:id:NonSense:20201025042117p:plain

残りのヘッダとペイロードBase64エンコードして「.」でつなげます。

eyJhbGciOiJzaGEyNTYiLCJ0eXAiOiJKV1QifQ.eyJpc0FkbWluIjoiMSJ9.ZmFlNjNhNjFkMTExYTRiYmY2MmU5YzAzZmI1N2U5MTNmM2FmYjE5OTdiNDVjMzA4MDNjZDljYjgxYmY4ODc3YQ

 これをCookieにセットして、flag.phpにアクセスします。

f:id:NonSense:20201025042609p:plain

 

[Misc 468] Shell Ain't Bad Place to Be

// maybe-later: 

www.jx-zhang.xyz

 

[Forenssics 387] Last Logon

// maybe-later:

./rip.exe -r SAM -a

github.com

*1:この問題に関してはオフセットは7なのに、ずっと8だと思いこんでいた。凡ミス

*2:これは後で気づいたことですが、たぶん作問者様のページっぽい

*3:かわいい