Skip to content

Instantly share code, notes, and snippets.

@ky28059
Last active May 13, 2025 01:05
Show Gist options
  • Save ky28059/5ffffe2d75240def2e2d77c7e8243229 to your computer and use it in GitHub Desktop.
Save ky28059/5ffffe2d75240def2e2d77c7e8243229 to your computer and use it in GitHub Desktop.

SDCTF 2025 — triglot

You think you know programming? You think you know languages? heh... as if 🙄

Come back to me when you can write a program that runs in the 3 deadly P's: Perl, Python, and (P)Javascript

Connect with nc -q 2 -N 52.8.15.62 8001

Flag is located at ./flag.txt

We're given a script that looks like this:

#!/usr/bin/env bash
set -eo pipefail

wrong() {
  printf "EXTREMELY LOUD INCORRECT BUZZER!!!\n"
}
trap wrong ERR

code=$(cat)

printf '%s' "$code" | perl -c
printf '%s' "$code" | python3 -c 'import sys,ast; ast.parse(sys.stdin.read())'
printf '%s' "$code" | node -e "const fs=require('fs'), src=fs.readFileSync(0,'utf8'); require('vm').createScript(src)"

perl_out=$(printf '%s' "$code" | perl -)
py_out=$(printf '%s' "$code" | python3 -)
js_out=$(printf '%s' "$code" | node -)

if [[ "$perl_out" == "$py_out" && "$perl_out" == "$js_out" ]]; then
  printf "Your triglot compiles!! Here's your output:\n"
  printf '%s\n' "$perl_out"
else
  exit 1
fi

It seems like we just need to make a script that runs in Perl, Python, and JS, and prints the flag in each one.

To start, a Python / JS polyglot seems pretty straightforward: we can use the fact that // is floor division in Python and a line comment in JS to create areas of our script only executed in Python:

3 // 2; [... code only executed in python]
3 // 2; [... code only executed in python]

With this, we can use Python multi-line strings to "comment out" chunks of code to be only executed in JS:

3 // 2; '''
[... code only executed in JS]
//'''
3 // 2; '''
[... code only executed in JS]
//'''

Thus, our Python / JS polyglot that cats the flag is as simple as

3 // 2; print(open('./flag.txt').read()); '''
const { readFileSync } = require('fs'); console.log(readFileSync('./flag.txt').toString());
//'''
3 // 2; print(open('./flag.txt').read()); '''
const { readFileSync } = require('fs'); console.log(readFileSync('./flag.txt').toString());
//'''

Now for the challenging part: adding Perl. We can try to use Perl multiline comments to embed the above Python / JS payload in a surrounding Perl script, but the syntax is too strange: the =hello "start comment" deliminator

=hello
... perl comment
=cut

would give syntax errors in JS and Python, and trying to disguise it as a variable assignment e.g.

hello = 5;
a
=hello
... perl comment
=cut

would also lead to it being parsed as an assignment in Perl (with many errors following).

Instead, we can use the convenient Perl __END__ special token to cause the Perl interpreter to ignore all subsequent text in the payload, avoiding having to manage Perl syntax errors in the process!

Thus, the main idea becomes doing something like:

open(FH, "<", "./flag.txt"); print <FH>;

__END__=5;
3 // 2; print(open('./flag.txt').read()); '''
const { readFileSync } = require('fs'); console.log(readFileSync('./flag.txt').toString());
//'''
open(FH, "<", "./flag.txt"); print <FH>;

__END__=5;
3 // 2; print(open('./flag.txt').read()); '''
const { readFileSync } = require('fs'); console.log(readFileSync('./flag.txt').toString());
//'''
open(FH, "<", "./flag.txt"); print <FH>;

__END__=5;
3 // 2; print(open('./flag.txt').read()); '''
const { readFileSync } = require('fs'); console.log(readFileSync('./flag.txt').toString());
//'''

where we paste our JS / Python polyglot after the __END__ token to avoid having to deal with Perl syntax there.

(note: we need to do __END__=5 and wrap the __END__ token in an assignment to avoid a JS ReferenceError or Python NameError.)

Finally, we just need to figure out a way to get the first

open(FH, "<", "./flag.txt"); print <FH>;

Perl snippet to not raise syntax errors in Python or JS.

This part is a bit tricky: we need to use the // trick from before (which is luckily valid Perl for short-circuited "definedness OR") to comment out Python- / Perl-only areas.

Since single-line comments are started by # in both Perl and Python, we need to use multiline strings for Python comments instead. Note that if we're inside a string, # won't start a single-line comment; thus, we can try to layer strings such that we are inside a string in Python and not Perl, and vice versa to comment out different sections of code in each.

But there's one last trick, since simply having a python triple-quoted string in Perl will raise syntax errors: Perl "quote-like operators" (which I learned about from this C, Ruby Perl, Python polyglot on GitHub).

We can use the token q=""" to start a multi-line string in Python and a q-string in Perl, where:

  • """ ends the Python string, and
  • = ends the Perl string,

letting us construct something like

q="""
[... this is commented out in python and perl]
=;
[... this is commented out in python only]
#"""
q="""
[... this is commented out in python and perl]
=;
[... this is commented out in python only]
#"""

Weaving these all together, we can construct

3 // 2; q="""
/*=;
open(FH, "<", "./flag.txt"); print <FH>;
#*/
3 // 2;#"""
3 // 2; q="""
/*=;
open(FH, "<", "./flag.txt"); print <FH>;
#*/
3 // 2;#"""
3 // 2; q="""
/*=;
open(FH, "<", "./flag.txt"); print <FH>;
#*/
3 // 2;#"""

for a final payload of

3 // 2; q="""
/*=;
open(FH, "<", "./flag.txt"); print <FH>;
#*/
3 // 2;#"""

__END__=5;
3 // 2; print(open('./flag.txt').read()); '''
const { readFileSync } = require('fs'); console.log(readFileSync('./flag.txt').toString());
//'''
3 // 2; q="""
/*=;
open(FH, "<", "./flag.txt"); print <FH>;
#*/
3 // 2;#"""

__END__=5;
3 // 2; print(open('./flag.txt').read()); '''
const { readFileSync } = require('fs'); console.log(readFileSync('./flag.txt').toString());
//'''
3 // 2; q="""
/*=;
open(FH, "<", "./flag.txt"); print <FH>;
#*/
3 // 2;#"""

__END__=5;
3 // 2; print(open('./flag.txt').read()); '''
const { readFileSync } = require('fs'); console.log(readFileSync('./flag.txt').toString());
//'''

Submitting this, we get our flag:

kevin@ky28059:/mnt/c/users/kevin/Downloads$ cat payload | nc -q 4 -N 52.8.15.62 8001
Your triglot compiles!! Here's your output:
sdctf{y0u_know_s0_m4ny_langu4g3s!}

Final notes

As I was making this writeup, I realized that the quote-wrapping tricks used for the initial Perl part of the payload was sufficient for the entire payload, as it had code areas that ran exclusively in Perl, JS, and Python.

Then, our entire payload can just be simplified to

3 // 2; q="""
console.log(require('fs').readFileSync('./flag.txt').toString()); /*=;
open(FH, "<", "./flag.txt"); print <FH>;
#*/
3 // 2;#"""; print(open('./flag.txt').read())
3 // 2; q="""
console.log(require('fs').readFileSync('./flag.txt').toString()); /*=;
open(FH, "<", "./flag.txt"); print <FH>;
#*/
3 // 2;#"""; print(open('./flag.txt').read())
3 // 2; q="""
console.log(require('fs').readFileSync('./flag.txt').toString()); /*=;
open(FH, "<", "./flag.txt"); print <FH>;
#*/
3 // 2;#"""; print(open('./flag.txt').read())

(we need to be careful about avoiding =s in the JS section, lest we end the Perl string prematurely.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment