11

I have figured out that < wasn't encoded as &lt;. However it is removed if followed by anything but a blank space. For example, <a will get removed but not < a> (note the space), which isn't useful.

Except from < character, there is no restriction and no encoded characters.

Here is the code where it gets reflected:

<span>USER INPUT</span>

Here is the content type of the page:

text/html; charset=utf-8

And the content type for the input request:

application/x-www-form-urlencoded; charset=UTF-8 

Others specific informations :

Server: nginx
X-Powered-By: PHP/5.5.18-1~dotdeb.1

Is this method vulnerable to XSS?

10
  • 2
    What about <<a?
    – Ángel
    May 9, 2016 at 22:06
  • Same, it renders nothing. Putting any character after a < will cause the string to be not displayed.
    – Xavier59
    May 9, 2016 at 22:08
  • 2
    What is the content type of the page? May 9, 2016 at 22:50
  • 2
    A very, very, very good read for you : owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet and owasp.org/index.php/… . You should be covered then.
    – Mxsky
    May 10, 2016 at 6:40
  • 2
    Are you sure the content-type of the response is application/x-www-form-urlencoded? This looks like the content-type of the request at first glance - strange to have it in a response. My other thought is to try the null byte character... e.g. <\x00script May 10, 2016 at 9:34

4 Answers 4

8
+50

No, in a HTML context you cannot inject new tags without allowing letters after the opening bracket. Still, this filtering technique is unnecessarily risky.

The HTML parser of your web browser parses code as a state machine. To understand what your options are, have a look at the HTML syntax specification and the possible state transitions.

Your injection point happens to be in the data state (which is the "default" state, outside of any tags):

8.2.4.1 Data state

Consume the next input character:

U+0026 AMPERSAND (&)
    Switch to the character reference in data state.
"<" (U+003C)
    Switch to the tag open state.
U+0000 NULL
    Parse error. Emit the current input character as a character token.
EOF
    Emit an end-of-file token.
Anything else
    Emit the current input character as a character token.

For XSS, the only interesting continuation is to open a tag with < and switch to the tag open state:

8.2.4.8 Tag open state

Consume the next input character:

"!" (U+0021)
    Switch to the markup declaration open state.
"/" (U+002F)
    Switch to the end tag open state.
Uppercase ASCII letter
    Create a new start tag token, set its tag name to the lowercase version of the current input character (add 0x0020 to the character's code point), then switch to the tag name state. (Don't emit the token yet; further details will be filled in before it is emitted.)
Lowercase ASCII letter
    Create a new start tag token, set its tag name to the current input character, then switch to the tag name state. (Don't emit the token yet; further details will be filled in before it is emitted.)
"?" (U+003F)
    Parse error. Switch to the bogus comment state.
Anything else
    Parse error. Switch to the data state. Emit a U+003C LESS-THAN SIGN character token. Reconsume the current input character. 

Here your options are a-z, A-Z, !,/ and ?. You have not been clear about whether special characters are blacklisted, too. But even if they are not, you're out of luck:

  • From <! you only get to a comment (<!--), a doctype declaration (<!DOCTYPE) or a CDATA section (<![CDATA). These are not real DOM nodes and hence they are useless for XSS. (For instance, you wouldn't be able to attach an event handler to a comment.)
  • <? might be interesting in XML, but is treated as a comment in HTML.
  • </ will only let you close tags.

You may have noticed that the specification also doesn't tolerate any padding characters, such as spaces, tabs, newlines or control characters.

If you want to dig a little deeper and verify the implementation, you can always look into the source code. For example, this extract is part of the HTML5 tokenizer used in Mozilla Firefox. As you can see, the tag open state adheres closely to the specification:

case NS_HTML5TOKENIZER_TAG_OPEN: {
  for (; ; ) {
    if (++pos == endPos) {
      NS_HTML5_BREAK(stateloop);
    }
    c = checkChar(buf, pos);
    if (c >= 'A' && c <= 'Z') {
      endTag = false;
      clearStrBufAndAppend((char16_t) (c + 0x20));
      state = P::transition(mViewSource, NS_HTML5TOKENIZER_TAG_NAME, reconsume, pos);
      NS_HTML5_BREAK(tagopenloop);
    } else if (c >= 'a' && c <= 'z') {
      endTag = false;
      clearStrBufAndAppend(c);
      state = P::transition(mViewSource, NS_HTML5TOKENIZER_TAG_NAME, reconsume, pos);
      NS_HTML5_BREAK(tagopenloop);
    }
    switch(c) {
      case '!': {
        state = P::transition(mViewSource, NS_HTML5TOKENIZER_MARKUP_DECLARATION_OPEN, reconsume, pos);
        NS_HTML5_CONTINUE(stateloop);
      }
      case '/': {
        state = P::transition(mViewSource, NS_HTML5TOKENIZER_CLOSE_TAG_OPEN, reconsume, pos);
        NS_HTML5_CONTINUE(stateloop);
      }
      case '\?': {
        if (viewingXmlSource) {
          state = P::transition(mViewSource, NS_HTML5TOKENIZER_PROCESSING_INSTRUCTION, reconsume, pos);
          NS_HTML5_CONTINUE(stateloop);
        }
        if (P::reportErrors) {
          errProcessingInstruction();
        }
        clearStrBufAndAppend(c);
        state = P::transition(mViewSource, NS_HTML5TOKENIZER_BOGUS_COMMENT, reconsume, pos);
        NS_HTML5_CONTINUE(stateloop);
      }
      case '>': {
        if (P::reportErrors) {
          errLtGt();
        }
        tokenHandler->characters(nsHtml5Tokenizer::LT_GT, 0, 2);
        cstart = pos + 1;
        state = P::transition(mViewSource, NS_HTML5TOKENIZER_DATA, reconsume, pos);
        NS_HTML5_CONTINUE(stateloop);
      }
      default: {
        if (P::reportErrors) {
          errBadCharAfterLt(c);
        }
        tokenHandler->characters(nsHtml5Tokenizer::LT_GT, 0, 1);
        cstart = pos;
        reconsume = true;
        state = P::transition(mViewSource, NS_HTML5TOKENIZER_DATA, reconsume, pos);
        NS_HTML5_CONTINUE(stateloop);
      }
    }
  }
  tagopenloop_end: ;
}

So, the XSS filter you describe seems to be safe per the HTML specification. It's very thin ice, tough. You never know, if some vendor comes up with a quirky implementation that could still enable an exploit. (Microsoft, I'm looking at you!)

The correct XSS protection is therefore to simply escape the brackets.

0
4

Very unlikely. The only vector I can see is a character encoding bug whereby the backend language (e.g. PHP) is configured to use a different character encoding than UTF-8, and it isn't encoding-aware when outputting the string.

That said, I'd say it's almost certainly not possible to get XSS in this instance. You could, however, get XSS in any location where you're already inside a tag (e.g. echo into an attribute).

1

In this particular case, you are injecting your input as HTML code (since it is between the span tags and content type is text/html). So, the only way I can think of for injection JavaScript code here is using HTML tags. Given how the server is responding back, it is very unlikely you can inject JavaScript in this particular case.

You have 2 options: First, you can try to find if you can find another place where you are already inside an HTML tag (as @Polynomial mentioned). Second, you can find a place on the page, where this input is being injected as JavaScript (for e.g. as a value to event handlers, within tags, etc.), as opposed to HTML.

If you are able to find a Second condition (inject input as JS) and you are in a similar situation where server is trying to blacklist certain characters, you can try to use something like JSF##k to bypass a particular filer/blacklist. JSF##k is an esoteric and educational programming style based on the atomic parts of JavaScript. It uses only six different characters to write and execute code. Take a look at this post for more info on it.

1

First and foremost XSS is entirely dependent on context in which the output is displayed. Forcing all input though the same filter will never work all of the time - and will require the developers and penetration testers to hunt for all of the injections. The content security policy will help prevent all types of XSS, even when the application suffers from broken output sanitation.

There are a number of ways XSS is possible given these arbitrary restrictions. What comes to mind first is DOM Based XSS and event handler injection. Reflective XSS will also work in the following conditions:

<script>
var a="$xss";
</script>

Proof of Concept:"+alert(document.cookie)+"

Or if it is a location:

<a href="$xss">click me</a>
<iframe src="$xss"/>

Proof of Concept:javascript:alert(1)

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .