- It's a known secret that each faction speaks different languages, however few can speak all of them. KORP has long wanted to send a spy in the factions to keep an eye on them. Through their extensive network, they have found different talented factionless to test. The first to show their multidigilingual skills will get a place in them, and be their secret agent amongst the factions. Can you show them your worth?
- The objective is to consturct a polyglot of 6 different programming languages
hard
HTB{7he_ComMOn_5yM8OL_Of_l4n9U49E5_C4n_LE4d_7O_m4ny_PolY9lO7_WoNdeR5}
- no downlodables provided
Since the challenge provides no files and just the instance, the only thing we can do is play with the server. After starting a connection, we are given an interesting message
****************************************
* How many languages can you talk? *
* Pass a program that's *
* all of the below that just reads *
* the file `flag.txt` to win *
* and pass the test. *
* *
* Languages: *
* * Python3 *
* * Perl *
* * Ruby *
* * PHP8 *
* * C *
* * C++ *
* *
* Succeed in this and you will be *
* rewarded! *
****************************************
Enter the program of many languages (input in base64):
It clearly showcases what we need to do. We need to create a singular program, that's valid python3, perl, ruby, php8, c, and c++ code. This is what's known as a polyglot, essentially one thing that has many valid interpretations. It seems that for every language, we just need it to read the flag in the server. So we need to study how to write a polyglot of all of the above languages.
One of the key principles when writing polyglots, is we need to use as many common characters as possible. The more common syntax and keywords languages have, the easier a polyglot will be to write. For example, a double quote is shared between all these languages. Also, the hashtag is another character shared by many of these languages, as well as some logical operator keywords. Python, Perl, and Ruby all use the and or not
keywords for logical conditional operations. On the other hand, C and C++ use && || !
. This means we can't use them, since python will produce a syntax error if it sees a charactet it doesn't like before starting to run the program. At this point we should also mention that C/C++ can be treated as the same, since we can just write C code, and it will count as C++ code as well.
Since we care about code execution, keywords that enable us to execute code are also important. If we think about it, since python, ruby, and perl are all interpreted, they have some common functions, like eval
and exec
. PHP also has these keywords, with exec
being capable of code execution. On the other hand, C/C++ can use the system
function.
A problem with languages like C/C++ and PHP is that they need keywords to be declared, so the interpreter or the compiler can now where to start compiling or running code. This can cause a problem with the other languages, since they don't recognize them, which can cause syntex errors. To achieve no problems, we need to study how to achieve conditional running and compilation.
A powerful primitive we can use is the C preprocessor. With this, we can define code blocks that, when the compiler is called, will not be considered for compilation. We can do this like so
#if 0
...
#endif
However this can cause problems with the interpreted languages as well. The syntax for code execution/file reading for Python, Ruby, and Perl differs. To work around this, we will leverage the differences in how they evaluate logical expressions. Take for example the below expression
0 and 1 or 2
How is this evaluated? Python and Ruby give different answers
>>> 0 and 1 or 2
2
irb(main):001:0> 0 and 1 or 2
=> 1
So with this, we have also achieved conditional code execution between python and ruby. How do we add perl into this mix? For this, we can take advantage of the perl's numeric context. Essentially, numeric operations can be performed only in a numeric context, and undefined values are treated as zero. For example, "b" + "0"
evaluates to 0
, since "b"
is undefined in a numerical context. On the other hand, "5" + "7"
evaluates to 12
. So for perl, ``"b" + "0" == 0` evaluates to True, whereas in python and ruby
>>> "b" + "0" == 0
False
irb(main):001:0> "b" + "0" == 0
=> false
So we've found a unique behaviour. Now to construct the singular line, we add an or
between the perl part, and the python ruby part, and use the above expression to make sure the perl line is executed
("b" + "0" == 0 and 3) or (0 and 1 or 2)
Just replace the 3, 1, and 2 with perl, ruby, and python code respectively
print((("b" + "0" == 0 and exec("cat flag.txt")) or (0 and exec("cat flag.txt") or eval('__import__("sys").stdout.write(open("flag.txt").read())'))));
We need the print
to print the results, and the ;
, or else perl complains.
As for PHP, the helpful thing with it is that the <?php...?>
tag is needed, so we can define the code we need inside that tag. And to prevent other languages from catching onto it, we can use their respective comment symbols (#
and //
) hide it. Note that we need to place #
first, because otherwise python will complain. To make sure no problem arise after the system
command, we can use the __halt_compiler
function.
#//<?php system('cat flag.txt;'); __halt_compiler();?>
print((("b" + "0" == 0 and exec("cat flag.txt")) or (0 and exec("cat flag.txt") or eval('__import__("sys").stdout.write(open("flag.txt").read())'))));
So far, we have satisfied the logic for all languages, but C/C++. As we said, we will use the preprocessor to remove all the other languages. However, the c code will exist for the other languages, which will be a problem when we go and write the main
declaration. So how do we bypass that? A smart approach is to use the inline assembly feature C offers to write our main. As for how to read the file, we will just write a simple open read write syscalls shellcode. As for the flag name, we can push it beforehand to the stack. The end result looks like this (Inline assembly for C needs to be in AT&T syntax)
__asm__(".section .text\n.globl main\nmain:\nmov $0x0000000000000000, %rax\npush %rax\nmov $0x7478742e67616c66, %rax\npush %rax\nmov %rsp, %rdi\nxor %rsi, %rsi\nmov $2, %rax\nsyscall\nmov %rax, %rdi\nmov %rsp, %rsi\nmov $0x100, %rdx\nxor %rax, %rax\nsyscall\nmov $1, %rdi\nmov %rsp, %rsi\nmov %rax, %rdx\nmov $1, %rax\nsyscall\nxor %rdi, %rdi\nmov $60, %rax\nsyscall\n");
Note the null value being pushed before the little-endian hex representation of flag.txt
for a null byte. Now we just wrap all the other languages in the C preprocessor.
#if 0
#<?php system('cat flag.txt;'); __halt_compiler();?>
print((("b" + "0" == 0 and exec("cat flag.txt")) or (0 and exec("cat flag.txt") or eval('__import__("sys").stdout.write(open("flag.txt").read())'))));
#endif
__asm__(".section .text\n.globl main\nmain:\nmov $0x0000000000000000, %rax\npush %rax\nmov $0x7478742e67616c66, %rax\npush %rax\nmov %rsp, %rdi\nxor %rsi, %rsi\nmov $2, %rax\nsyscall\nmov %rax, %rdi\nmov %rsp, %rsi\nmov $0x100, %rdx\nxor %rax, %rax\nsyscall\nmov $1, %rdi\nmov %rsp, %rsi\nmov %rax, %rdx\nmov $1, %rax\nsyscall\nxor %rdi, %rdi\nmov $60, %rax\nsyscall\n");
With this we can also remove the C comment from the php line. All we have to do now is turn this into base64, and send it to the server to get the flag (below is the payload encoded in base64)
I2lmIDAKIzw/cGhwIHN5c3RlbSgnY2F0IGZsYWcudHh0OycpOyBfX2hhbHRfY29tcGlsZXIoKTs/PgpwcmludCgoKCJiIiArICIwIiA9PSAwIGFuZCBleGVjKCJjYXQgZmxhZy50eHQiKSkgb3IgKDAgYW5kIGV4ZWMoImNhdCBmbGFnLnR4dCIpIG9yIGV2YWwoJ19faW1wb3J0X18oInN5cyIpLnN0ZG91dC53cml0ZShvcGVuKCJmbGFnLnR4dCIpLnJlYWQoKSknKSkpKTsKI2VuZGlmCl9fYXNtX18oIi5zZWN0aW9uIC50ZXh0XG4uZ2xvYmwgbWFpblxubWFpbjpcbm1vdiAkMHgwMDAwMDAwMDAwMDAwMDAwLCAlcmF4XG5wdXNoICVyYXhcbm1vdiAkMHg3NDc4NzQyZTY3NjE2YzY2LCAlcmF4XG5wdXNoICVyYXhcbm1vdiAlcnNwLCAlcmRpXG54b3IgJXJzaSwgJXJzaVxubW92ICQyLCAlcmF4XG5zeXNjYWxsXG5tb3YgJXJheCwgJXJkaVxubW92ICVyc3AsICVyc2lcbm1vdiAkMHgxMDAsICVyZHhcbnhvciAlcmF4LCAlcmF4XG5zeXNjYWxsXG5tb3YgJDEsICVyZGlcbm1vdiAlcnNwLCAlcnNpXG5tb3YgJXJheCwgJXJkeFxubW92ICQxLCAlcmF4XG5zeXNjYWxsXG54b3IgJXJkaSwgJXJkaVxubW92ICQ2MCwgJXJheFxuc3lzY2FsbFxuIik7
The code of 'server.py` is presented below:
from base64 import b64decode
import subprocess
import os
def banner():
print("****************************************")
print("* How many languages can you talk? *")
print("* Pass a program that's *")
print("* all of the below that just reads *")
print("* the file `flag.txt` to win *")
print("* and pass the test. *")
print("* *")
print("* Languages: *")
print("* * Python3 *")
print("* * Perl *")
print("* * Ruby *")
print("* * PHP8 *")
print("* * C *")
print("* * C++ *")
print("* *")
print("* Succeed in this and you will be *")
print("* rewarded! *")
print("****************************************")
print()
def read_flag():
with open('flag.txt') as f:
flag = f.read().rstrip()
return flag
def get_polyglot():
return b64decode(input('Enter the program of many languages: ')).decode()
def save_to_files(poly):
lines = poly.count('\n')
for _p, ext, _m in langs:
with open(f'{poly_code_dir}/poly.{ext}', 'w') as f:
f.write(poly)
def check_poly_code(flag):
for prog, ext, name in langs:
print(f'\n[*] Executing {name} using command {prog}')
if ext == 'c' or ext == 'cpp':
subprocess.run([prog, f'{poly_code_dir}/poly.{ext}', '-o', f'{poly_code_dir}/poly_{ext}'])
result = subprocess.run([f'{poly_code_dir}/poly_{ext}'], capture_output=True, text=True)
out = result.stdout
print(f' [+] Completed. Checking output')
if flag not in out:
print(' [-] Failed to pass test. You are not worthy enough...')
return
else:
result = subprocess.run([prog, f'{poly_code_dir}/poly.{ext}'], capture_output=True, text=True)
out = result.stdout
print(f' [+] Completed. Checking output')
if flag not in out:
print(' [-] Failed to pass test. You are not worthy enough...')
return
print(' [+] Passed the check')
print()
print('You seem to know your way around code. We will be looking at you with great interest...', flag)
def main():
banner()
flag = read_flag()
save_to_files(get_polyglot())
check_poly_code(flag)
if __name__ == '__main__':
poly_code_dir = 'poly_code'
if not os.path.exists(poly_code_dir):
os.makedirs(poly_code_dir)
langs = [('python', 'py', 'Python3'), ('perl', 'pl', 'Perl'), ('ruby', 'rb', 'Ruby'), ('php', 'php', 'PHP8'), ('gcc', 'c', 'C'), ('g++', 'cpp', 'C++')]
main()
from pwn import *
def send_payload():
io.recvuntil(b': ')
io.sendline(payload)
def get_flag():
io.recvuntil(b'HTB{')
flag = io.recvuntil(b'}')
return b'HTB{' + flag
def pwn():
send_payload()
flag = get_flag()
print(flag)
if __name__ == '__main__':
ip = '127.0.0.1'
port = 1337
io = remote(ip, port)
#io = process(['python', 'server.py'])
payload = b'I2lmIDAKIzw/cGhwIHN5c3RlbSgnY2F0IGZsYWcudHh0OycpOyBfX2hhbHRfY29tcGlsZXIoKTs/PgpwcmludCgoKCJiIiArICIwIiA9PSAwIGFuZCBleGVjKCJjYXQgZmxhZy50eHQiKSkgb3IgKDAgYW5kIGV4ZWMoImNhdCBmbGFnLnR4dCIpIG9yIGV2YWwoJ19faW1wb3J0X18oInN5cyIpLnN0ZG91dC53cml0ZShvcGVuKCJmbGFnLnR4dCIpLnJlYWQoKSknKSkpKTsKI2VuZGlmCl9fYXNtX18oIi5zZWN0aW9uIC50ZXh0XG4uZ2xvYmwgbWFpblxubWFpbjpcbm1vdiAkMHgwMDAwMDAwMDAwMDAwMDAwLCAlcmF4XG5wdXNoICVyYXhcbm1vdiAkMHg3NDc4NzQyZTY3NjE2YzY2LCAlcmF4XG5wdXNoICVyYXhcbm1vdiAlcnNwLCAlcmRpXG54b3IgJXJzaSwgJXJzaVxubW92ICQyLCAlcmF4XG5zeXNjYWxsXG5tb3YgJXJheCwgJXJkaVxubW92ICVyc3AsICVyc2lcbm1vdiAkMHgxMDAsICVyZHhcbnhvciAlcmF4LCAlcmF4XG5zeXNjYWxsXG5tb3YgJDEsICVyZGlcbm1vdiAlcnNwLCAlcnNpXG5tb3YgJXJheCwgJXJkeFxubW92ICQxLCAlcmF4XG5zeXNjYWxsXG54b3IgJXJkaSwgJXJkaVxubW92ICQ2MCwgJXJheFxuc3lzY2FsbFxuIik7'
pwn()
'''
exploit code
#if 0
#<?php system('cat flag.txt;'); __halt_compiler();?>
print((("b" + "0" == 0 and exec("cat flag.txt")) or (0 and exec("cat flag.txt") or eval('__import__("sys").stdout.write(open("flag.txt").read())'))));
#endif
__asm__(".section .text\n.globl main\nmain:\nmov $0x0000000000000000, %rax\npush %rax\nmov $0x7478742e67616c66, %rax\npush %rax\nmov %rsp, %rdi\nxor %rsi, %rsi\nmov $2, %rax\nsyscall\nmov %rax, %rdi\nmov %rsp, %rsi\nmov $0x100, %rdx\nxor %rax, %rax\nsyscall\nmov $1, %rdi\nmov %rsp, %rsi\nmov %rax, %rdx\nmov $1, %rax\nsyscall\nxor %rdi, %rdi\nmov $60, %rax\nsyscall\n");
'''