1. 模式卡片(最重要)

每学一个漏洞类型,记一张”模式卡”:

1
2
3
4
5
6
7
8
9
【SQL 注入 - JDBC 拼接】

特征:看到字符串拼接 SQL
String sql = "select ... where id = '" + id + "'";
stmt.executeQuery(sql);

数据流:参数 → 字符串拼接 → executeQuery
结论:有漏洞
修复:用 PreparedStatement + ? 占位

2. 危险函数清单

记一张表,不断往里加:

1
2
3
4
5
6
| 漏洞类型 | 危险函数(Sink)          | 项目中哪个文件出现过 |
|---------|------------------------|------------------|
| SQL 注入 | executeQuery() | SQLI/JDBC.java |
| SQL 注入 | queryForMap() | SQLI/JDBC.java |
| 命令执行 | Runtime.exec() | RCE/RuntimeVul.java |
| ... | ... | ... |

3. 审计 checklist

审计一个新文件时,养成固定习惯:

1
2
3
4
5
□ 1. 这个文件有几个 @GetMapping / @PostMapping?(有多少接口入口)
□ 2. 每个接口的参数从哪来?(Source 是什么)
□ 3. 参数最终流向了哪里?(Sink 是什么)
□ 4. 中间有没有校验/过滤?(Sanitizer 是什么)
□ 5. 结论:安全 / 有漏洞 / 不确定

基础语法

A.文件骨架

项目数据流:

浏览器发起请求
↓
Controller(接收请求,比如 IDOR1.java)
↓
Mapper(去数据库查数据,比如 UserMapper.java)
↓
数据库

1、package:

1
package com.best.hello.controller.IDOR;

Java 要求:文件的物理路径必须和 package 声明一致。

审计用途:拿到一个项目后,看到 package 就能快速定位文件在哪。controller 包 = 接收请求的地方。

2、import

1
import com.alibaba.fastjson.JSON;     

引用说明

审计用途:import 里如果有 com.alibaba.fastjson,要警觉——这个库历史上有反序列化漏洞。

3、注释

注释 // 和 /* */ 语法和c语言差不多。

审计用途:

  1. 废弃代码泄露:程序员把以前老的、有漏洞的登录接口注释掉了,但代码还在。
  2. 敏感信息泄露:程序员写着 // TODO: 这里测试用,默认密码是 admin123,或者 // 暂时把鉴权关掉,明天再开。在源码里全局搜索 TODO、test、password 等注释内容,往往有奇效。

4、class 类声明与文件名的死规矩

1
public class IDOR1 { ... }

审计用途:

如果类前面加了 public,那么这个类的名字,必须和这个文件的物理名字一模一样!

  • 类叫 public class IDOR1,文件就必须叫 IDOR1.java。
  • 审计用途:当在路由表或者其他配置文件里看到报错或者跳转指向了 IDOR1 类,在源码目录里直接搜索 IDOR1.java 这个文件就行,绝对不会叫别的名字。这让找文件极快。

5、{} 花括号 与 () 圆括号

语法:

  • {}:圈定地盘。类有多大、方法有多长、if 逻辑管几行代码,全看花括号。
  • ():装参数的

审计用途:

在查**逻辑漏洞(特别是越权、权限绕过)**时,{} 就是命门
很多水平差的程序员,写 if (isAdmin) 的时候,花括号 {} 圈错了位置,导致安全校验只管了上面一句话,下面真正干活的危险代码漏在花括号外面了,任何人都可以直接执行。

审计的时候可用鼠标点击以下{},看落在哪里。

B. 变量与数据

1. 基本数据类型 与 类型转换

  • 语法点:

    • Java 是强类型语言,数字就是 int,文本就是 String。
    • Web 请求传进来的参数,永远都是 String(文本)!
    • 类型转换:把文本变成真正的数字,需要用到强转语法,最常见的是 Integer.parseInt(文本)。
  • long(长整型数字):

    • 是什么:就是超大号的 int。int 最多只能存 21 亿左右,如果是个时间戳,或者全球用户的唯一 ID,int 装不下,就会用 long。
    • 审计视点:和 int 一样,只要强转成了 long,它自己就是安全的
      • ⚠️ 注意边界:它保护的只是它自己。同一个 SQL 里如果还拼了别的没强转的参数,那些参数照样能注入。
  • double(双精度浮点数 / 小数):

    • 是什么:用来存带小数点的数字(比如 3.14)。
    • 审计视点(业务逻辑漏洞):在审“商城支付、充值”代码时,如果看到 double price,要极度警惕精度丢失或者负数金额导致的逻辑漏洞(比如黑客买东西传入价格为 -10.0,不但没扣钱,余额反而增加了)。
  • boolean(布尔型 / 真假):

    • 是什么:全世界最简单的类型,它的值只有两个:true(真) 或 false(假)。
    • 审计视点(越权漏洞核心):在看权限校验时,经常看到 boolean isAdmin = …。黑客的目标就是想尽一切办法(比如修改网页传参),让这个变量变成 true。
  • char(字符):

    • 是什么:只能存单个字符,并且用单引号圈起来(比如 ‘A’、’1’)。它和 String(双引号 “ABC”)不一样,String 是一长串,char 只有一个字。审计中了解即可。

审计用途:

危险写法:

1
2
3
4
// 黑客输入 1 OR 1=1,id 变成了文本 "1 OR 1=1"
String id = request.getParameter("id");
// 直接把文本拼接进 SQL 语句,导致 SQL 注入!
String sql = "SELECT * FROM users WHERE id = " + id;

安全写法:

1
2
3
4
5
String id = request.getParameter("id");
// 把文本强制转换成纯数字!
int safeId = Integer.parseInt(id);
// 拼接入 SQL
String sql = "SELECT * FROM users WHERE id = " + safeId;

这种写法这里,如果输入 1 OR 1=1,到了 Integer.parseInt 这一步,Java 发现这根本不是纯数字,会当场抛出异常,这个请求直接失败,根本走不到下面执行 SQL 的地方。

(补一句:抛异常 ≠ 程序崩溃。程序本身不会死,死的只是这一次请求。但如果外面用 try-catch 把异常信息回显到了页面上,反而会变成信息泄露。)

看到参数被强转成了 int、long,说明这个变量自己是安全的,可以直接划掉看下一个。

但要记住边界:被保护的只是「这一个变量」。如果同一个 SQL 里还拼了别的没强转的参数,那些参数照样能注入 —— 这类「只净化了一半」的代码,比完全没做校验的更难发现,因为审计的人看到一行强转就放心了。

2. null 与空判断

语法:null 意思是这个变量是个空壳,里面什么都没有。

审计用途:不写空判断会引发崩溃,或者被用来绕过逻辑。

如果后端没写if对null的处理逻辑,直接就调用user.getName()然后抛nullPointerException(空指针异常), 结合try-catch,如果系统因此崩溃,说不定就会把底层路径和错误信息吐在网页上(信息泄露)。

3. 数组 []

语法:String[] ips = {“192.168.1.1”, “10.0.0.1”};

和 List 一样,是一长串数据的集合

4. static 和 final

语法:

  • static(静态的):属于类本身的。调用时不需要 new,直接 类名.方法名()。
  • final(最终的):被它修饰的变量,一旦赋值,这辈子都不能再改。

因为 static 是全局共享的,所以调用它的时候,不需要去 new 具体的实体,直接用类名去调用。

审计用途:

在代码里看到:public static final String SECRET_KEY = “abc123xxx”;
马上注意。这种叫**硬编码凭证(Hardcoded Credentials)**漏洞。密钥被写死在代码里了,以后只要源码泄露,所有用户的加密数据全废。

C. 方法与调用

1、构造方法

伴随 new 关键字一起执行的“出厂设置”代码。

语法:

  1. 方法名必须和类名一模一样!
  2. 绝不能写返回值(连 void 都不能写)

【有点类似c++】

示例:

1
2
3
4
5
6
7
8
9
10
11
public class User {
String name;

// 这就是构造方法(没有 void,名字跟类名完全一样)
public User(String initName) {
this.name = initName; // 出厂自带初始名字
}
}

// 调用的时候:
User u = new User("管理员"); // new 的时候,其实就是在调构造方法

审计用途:

在 JSON 库反序列化(Fastjson、Jackson)里,黑客会利用目标类的构造方法或 setter 里的危险操作(比如一 new 出来就去连接恶意服务器)来搞事情。

(⚠️ 这里别记混:原生 Java 反序列化走的是 readObject(),它恰恰不调用构造方法 —— 这正是它反直觉的地方,也是它危险的原因之一。构造方法会被触发的是 JSON 库那条路线。阶段 5 会展开。)

2、方法重载 Overload

语法:

和override名称类似但不同。

  • 重写 (Override):子类觉得父类的方法不好,同名同参数,推翻重做。(多态的基础)
  • 重载 (Overload):同一个类里面,可以有名字一模一样,但参数不同的好几个方法。(方便程序员调用)【类似c++】

示例:

1
2
3
4
5
6
7
8
public class Tool {
// 方法 1:切一个土豆
public void cut(String vegetable) { ... }

// 方法 2:切一个土豆,顺便指定切块还是切丝?
// 名字一样,但括号里的参数变了,这就叫重载
public void cut(String vegetable, String style) { ... }
}

D. 流程控制

这里很多之前在c++里学过,快速过一下。

1、三元运算符

语法:

条件?正确执行:错误执行

1
2
3
4
String role;
if (uid == 1) { role = "admin"; } else { role = "guest"; }

String role = (uid == 1) ? "admin" : "guest";

2、switch 与 break/continue

1
2
3
4
5
6
7
8
9
10
11
switch(roleId) {
case 1:
System.out.println("给超级管理员权限");
break; // 必须写 break,写了才会跑出去
case 2:
System.out.println("给普通用户权限");
// 如果程序员忘了写 break; 会接着执行后面的语句,直到结束或遇到break。
case 3:
System.out.println("给访客权限");
break;
}

3、while循环

语法略。

审计用途:如果在代码里看到 while,一定要去检查它有没有可能永远停不下来。如果黑客能构造一个永远停不下来的条件(死循环),服务器的 CPU 瞬间就会飙到 100%,导致网站瘫痪( ReDoS 或逻辑拒绝服务攻击)。

4、throw 和 throws

catch 是抓住异常,而 throw 是程序**主动“扔出(制造)”**异常。

1
2
3
4
5
6
7
// 方法声明上加 throws,告诉别人:“我这方法可能会扔炸弹,调用我的人自己小心点”
public void checkUser(String name) throws Exception {
if (name == null) {
// throw:当场制造一个炸弹扔出去!程序立刻终止。
throw new Exception("用户名不能为空!");
}
}

5、instanceof

判断对象是不是某个特定类型的类

1
2
3
4
Object obj = 接收黑客传来的数据();
if (obj instanceof String) {
System.out.println("传来的是个字符串");
}

6、List和try-catch

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 列表 (List)
List<String> payloads = new ArrayList<>();
payloads.add("admin' OR 1=1--");
payloads.add("1/0");

// 2. 循环 (for) 挨个处理
for (String data : payloads) {

// 3. 异常捕获 (try-catch)
try {
System.out.println("正在处理: " + data);
// 假设这里有一段会崩溃的底层代码
} catch (Exception e) {
System.out.println("发生致命错误:" + e.getMessage());
}

}

payloads.add("admin' OR 1=1--"); payloads.add("1/0");

解读:调用对象 payloads 里的 add 方法添加 admin’ OR 1=1– 和 1/0

for (String data : payloads)

解读:每轮取出payloads里面的数据,命名为data用了循环

try-catch

**try { … }**尝试,把可能出问题、会崩溃的代码关在这个大括号里。Java 会小心翼翼地运行它

**catch (Exception e) { … }**抓住

  • Exception:异常(所有的报错、崩溃在 Java 里都叫 Exception)。
  • e:给抓住的这个报错信息起个代号

如果 try 里面的代码真崩溃了,程序不会直接闪退,而是会立刻跳进 catch 的大括号里,让程序员有个机会去处理报错

【很多开发人员在写 catch 的时候,为了调试方便,会直接把底层的报错信息(e.getMessage() 或者更可怕的 e.printStackTrace())直接通过网页返回给用户】

练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 这个方法接收用户传来的文件名清单
public String deleteFiles(List<String> fileNames) {

// 循环遍历用户传来的每一个文件名
for (String name : fileNames) {

try {
// 调用底层的删除接口执行删除
fileService.delete(name);
} catch (Exception e) {
// 如果删除失败(比如文件不存在,或者没权限)
// 注意:这个 return 会让整个方法立刻结束,后面还没处理的文件名全部被跳过
// 这正好是「循环里写 return」的典型坑
return "删除失败,系统底层错误信息:" + e.toString();
}

}

return "全部删除成功!";
}

为了试探这台服务器系统内部路径,可以在fileName里面放一个假文件名,等待错误信息抛出。比如,”../../../non_existent_file.txt”

报错可能为:

1
java.io.FileNotFoundException: /usr/local/tomcat/webapps/ROOT/../../../non_existent_file.txt (No such file or directory)

7、finally(绝对执行)

跟在try和catch后面,无论代码是否报错/崩溃,finally里面的代码都会执行。

示例:

1
2
3
4
5
6
7
8
9
10
try {
// 1. 连接数据库
// 2. 查询数据(如果这里报错崩溃了...)
} catch (Exception e) {
// 3. 记录报错信息
} finally {
// 4. 无论上面成败,最后一定一定一定会走到这里!
// 通常用来断开数据库连接、释放内存。
dbConnection.close();
}

审计:

先记住一个前提:关闭资源有三种写法,只有一种是危险的。 别看错方向。

写法 长什么样 安全吗
try-with-resources try (InputStream in = ...) { } ✅ 安全,圆括号里的东西会自动关闭
手动 close 写在 try 末尾 rs.close(); conn.close(); ❌ 危险
close 写在 finally 里 finally { conn.close(); } ✅ 安全

危险的是第二种:close 写在 try 块的最后几行。因为只要它上面任何一行抛异常,后面的 close 就一行都执行不到。

靶场里的 JDBC.java 就是第二种,真实行号:

1
2
3
4
5
6
66:  ResultSet rs = stmt.executeQuery(sql);   // 报错注入在这里抛异常
...
74: rs.close(); // 这三行在 try 里面
75: stmt.close(); // 一旦上面抛异常
76: conn.close(); // 一行都执行不到
78: } catch (Exception e) { return e.toString(); }

所以攻击方式是:疯狂发送会让第 66 行报错的请求(报错注入最合适)。每次报错都跳过了 close,连接只借不还,服务器的连接数很快就被耗尽,网站瘫痪(DoS)。

而 Traversal.java:40 是第一种写法,没有 finally,但完全安全:

1
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)))) {

【所以审计时:看到打开文件 / 连接数据库的对象,先看它有没有写在 try ( ... ) 的圆括号里(自动关,安全);如果没有,再看 close() 是写在 try 末尾(← 危险)还是 finally 里(← 安全)】

E. 面向对象

1、接口(Interface)与实现类(Implements)

定义接口:

1
2
3
public interface UserService {
void deleteUser(int userId);
}

解读:只要实现这个接口的实现类的都要有void deleteUser(int userId)功能

实现类:

1
2
3
4
5
6
7
8
public class UserServiceImpl implements UserService {

@Override
public void deleteUser(int userId) {
// 这里才是真正的删除逻辑,真正的危险代码会出现在这里!
System.out.println("正在从数据库删除用户ID: " + userId);
}
}

public class UserServiceImpl 普通的类,通常名为接口名+Impl

implements 关键字,实现、履行

UserService接口的名字

@Override 注解,以 @ 开头的叫注解(给编译器看的提示标签),覆盖、重写。告诉 Java:“我正在把菜单里规定的 deleteUser 方法,写出具体的逻辑来覆盖掉空壳”。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ==================== 角色1:前台窗口 (WebController 类) ====================
public class WebController {

// 前台手里安排了一位干活的后厨师傅 (UserServiceImpl)
UserService userService = new UserServiceImpl();

// 【第 1 棒:接客】
public void handleDelete(int id) {
// 此时,黑客传进来的 999 进到了这里的变量 id 里面!此时 id = 999

// 【第 2 棒:转交任务】
// 前台自己不会删数据库,他把装有 999 的 id 变量,递给了 userService 师傅
userService.deleteUser(id);
}
}


// ==================== 角色2:后厨师傅 (UserServiceImpl 类) ====================
public class UserServiceImpl implements UserService {

// 【第 3 棒:真正干活】
@Override
public void deleteUser(int id) {
// 师傅接到了前台递过来的 999,填入到底层的 SQL 语句中!
System.out.println("底层正在执行: DELETE FROM users WHERE id = " + id);
// 最终屏幕上打印出:底层正在执行: DELETE FROM users WHERE id = 999
}
}

解读: UserService userService = new UserServiceImpl();

UserService接口名,

userService变量名,对应UserServiceImpl();这个实现类

这里顺便给这个写法起个正式名字:多态。

UserService userService = new UserServiceImpl(); 这一行,左边是接口类型,右边是具体实现类 —— 这就是多态:接口类型的变量,装的是实现类的对象,调方法时实际跑的是实现类的那一版。

审计意义:看到 XxxService xxxService = ... 时,光看左边不知道真正干活的是谁,必须看右边 new 的是哪个类。如果它是 @Autowired 注入的(没有 new),那就得手动去找谁实现了这个接口。

2、继承 extends(父与子)

示例:

1
2
3
4
5
6
7
8
9
10
11
// 1. 父类(干爹):把通用的功能写好
public class Animal {
public void eat() {
System.out.println("正在吃东西...");
}
}

// 2. 子类(儿子):继承父类
public class Dog extends Animal {
// 儿子啥都不用写,天生就拥有了干爹的 eat() 功能!
}

类似C++的继承

权限:

  • public(公开的):谁都能用,儿子当然也能用。
  • private(私有的):父类的私房钱,就算是亲儿子(子类)也绝对不准碰,儿子甚至都不知道有这个东西。
  • protected(受保护的/家族内部的):专为继承设计的权限外人不能碰,但是亲儿子(子类)可以随便用、随便改

但java不支持多继承,支持多重继承。【不能一个子拥有两个父,但是可以儿子有爸爸,爸爸有爷爷】

image-20260916144502187
  • 子类拥有父类非 private 的属性、方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。

但是 implements 可以让 java 变相拥有多继承特性 —— 一个类可以实现多个接口。

1
2
3
4
5
6
7
8
9
10
11
public interface A {
public void eat();
public void sleep();
}

public interface B {
public void show();
}

public class C implements A,B {
}

super 和 this 的引用:

  • this:指”当前这个对象自己”
  • super:指”父类那一部分”,用来调用父类的方法或字段

final 的两层含义(只写”final 无法被继承”容易混,因为 final 也能修饰变量和方法):

  • final class X → 这个类不能被继承
  • final void m() → 这个方法不能被重写
  • final int a = 1 → 这个变量不能再被赋值

审计视点:记住这个句型 —— 父类里调用了一个方法,而这个方法被子类重写了,那实际跑的就是子类的那一版。

反序列化的 gadget 链,本质就是一串这样的”父类喊、子类答”接起来的。阶段 5 会展开。

3、abstract 抽象类

“半成品图纸”,绝对不能直接被 new 出来,只能留给儿子去继承(extends)并补全

1
2
3
4
5
public abstract class Animal {
// 抽象方法:没有花括号,只有分号。逼着儿子必须重写它!
public abstract void makeSound();
}
// Animal a = new Animal(); // ❌ 绝对报错!半成品不能直接造出来。

**审计:**看到 abstract class 知道它是半成品,审计时必须直接去找它的子类(找谁 extends 了它)

4、匿名内部类

不想麻烦去专门写一个正规的类文件,直接就地捏一个没名字的一次性类。

示例:

1
2
3
4
5
6
7
// 正常你要 new 一个接口的实现类,要先写个文件,这里不用:
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("这是一个一次性的匿名类");
}
};

审计:

在 JNDI 注入(Log4j2 漏洞)或者反序列化的 Payload 里,经常用这种奇怪的语法,把恶意代码就地封装在一个没有名字的类里绕过检查。看到这种 new 接口名() { … } 知道是在原地造类。

5、enum 枚举

就像网页上的下拉选择框,只能从里面挑,不能乱填

1
2
3
4
5
public enum Role {
ADMIN, USER, GUEST
}
// 使用:
Role myRole = Role.ADMIN;

审计:

看到 enum,知道这是一个限定死的固定选项,黑客传别的东西进不去。

F. 集合

List在前面和trycatch一起写了,此处略。

1、键值对(Key-Value)

1
2
3
4
5
6
7
8
9
// 1. 准备一本字典
Map<String, String> userParams = new HashMap<>();

// 2. 往字典里存入数据:存入 账号 和 密码
userParams.put("username", "admin");
userParams.put("password", "123456");

// 3. 从字典里查数据
String currentPass = userParams.get("password");

Map<String, String> userParams = new HashMap<>();

Map也是接口,逻辑类似上文。只不过多了一个<String, String>

第一个 String 是 Key(键名),第二个 String 是 Value(键值)

userParams.put("username", "admin");

在userParams中新增目录为username的记录,对应内容:admin

String currentPass = userParams.get("password");

把目录名(Key)是 password 的具体内容查出来拿走。

练习:

1
2
3
4
5
6
7
8
public void doLogin(Map<String, String> requestData) {
String inputUser = requestData.get("user");
String inputRole = requestData.get("role");

if (inputRole.equals("admin")) {
System.out.println("【高危】赋予管理员权限给用户: " + inputUser);
}
}

当这个role等于admin的时候,会被触发这个高危语句

.equals(“admin”):在 Java 中,判断两段文本内容是否一样,不能直接用两个等号 ==,而是用 .equals()(意思是等于)

2、Set(去重清单)

也是清单,前面有List(单列清单)和 Map(双列字典)

这个清单也是单列清单,但是:绝对不允许有重复的东西

示例:

1
2
3
4
5
6
Set<String> blacklistedIPs = new HashSet<>();
blacklistedIPs.add("192.168.1.1");
blacklistedIPs.add("192.168.1.1"); // 试图加一个一模一样的进去

System.out.println(blacklistedIPs.size()); // 打印清单里有几个东西
// 结果是 1!因为第二个被直接无视了。

【开发人员喜欢用set存放黑名单ip或者用户的权限角色,审计时看到就知道不用另外去找有没有去重逻辑】

3、stream() (流式处理)

Java 8 之后程序员特别喜欢用的一种炫技版 for 循环

老写法:

1
2
3
4
5
6
7
8
9
10
List<String> userList = Arrays.asList("guest", "admin_bob", "user1", "admin_alice");

// 1. 遍历清单
for (String name : userList) {
// 2. if 过滤
if (name.contains("admin")) {
// 3. 处理结果
System.out.println("找到管理员: " + name);
}
}

新写法:

1
2
3
4
5
6
List<String> userList = Arrays.asList("guest", "admin_bob", "user1", "admin_alice");

// 开启流水线作业
userList.stream()
.filter(name -> name.contains("admin")) // 保安:只放行包含 admin 的名字
.forEach(name -> System.out.println("找到管理员: " + name)); // 终点:挨个打印
  • .stream():把这个清单放到履带流水线上,准备开动。
  • .filter(…):流水线上的安检机器(相当于 if)。
    • 括号里的 name -> name.contains(“admin”) 叫做 Lambda 表达式(箭头语法)。
    • 意思非常直白:“拿到一个 name ➡️ 判断它是否包含 admin,包含就放行,不包含就扔掉”。
  • .forEach(…):流水线的终点打包员(相当于 for 循环体里面的代码,前面有filter时相当于循环内if里的语句,没有时就单纯循环体代码。)。
    • 意思:“拿到顺利通过安检的 name ➡️ 把它打印出来”。

如果循环里不仅有 if,在 if 的前面或后面还有其他代码(比如先打个日志,再做判断),那流水线该怎么写?

放弃 .filter ,直接把整个包含** if 的复杂逻辑,原封不动地塞进 .forEach 的花括号 {} 里

1
2
3
4
5
6
7
8
9
10
11
12
for (String name : userList) {

// if 前面的代码
System.out.println("准备开始安检...");

if (name.contains("admin")) {
System.out.println("抓到一个管理员: " + name);
}

// if 后面的代码
System.out.println("当前人员安检完毕!");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
userList.stream().forEach(name -> {

// if 前面的代码
System.out.println("准备开始安检...");

if (name.contains("admin")) {
System.out.println("抓到一个管理员: " + name);
}

// if 后面的代码
System.out.println("当前人员安检完毕!");

});

练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.best.hello.controller;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/sqli")
public class SQLInjectController {

@GetMapping("/jdbc/vuln")
public String jdbcVuln(@RequestParam("username") String name) {
//这里有个小坑,Spring 框架去网页上抓数据时,只认 @RequestParam 括号里的那个名字,即username。
//"username":这是对外的窗口名字(URL 里的参数必须叫这个)
//name:这是对内的 Java 变量代号(拿到数据后,在代码里面用这个代号)
// 底层存在漏洞的 SQL 拼接
String sql = "SELECT * FROM users WHERE user = '" + name + "'";
System.out.println("执行的SQL是: " + sql);

// 假设这里去数据库执行了
return "查询完成";
}
}
  1. 假设这个靶场网站的域名是 http://127.0.0.1:8080。作为黑客,如果你想触发这个带有漏洞的 jdbcVuln 方法,你应该在浏览器地址栏里输入的**完整 URL(包含传参)**是什么?
  2. 假设你想利用单引号 ‘ 闭合产生 SQL 注入。请把你构造的包含恶意测试内容(比如 admin’)的完整 URL 写出来,并说出程序控制台最终打印出来的 SQL 语句是什么样子的?

http://127.0.0.1:8080/sqli/jdbc/vuln?username=admin' or 1=1 -- -

SELECT * FROM users WHERE user = 'admin' or 1=1 -- -'

两个提醒:

  1. URL 里的空格和单引号严格来说要编码成 %20 和 %27。浏览器会自动帮你转,但以后用 Burp 或 curl 手工发请求时得自己写。
  2. -- - 这种注释写法是 MySQL 专用的(-- 后面必须跟一个空格)。换到别的数据库就不灵了,别记死。

基础知识二:

反射:是 Java 提供的一个“上帝模式”。它允许程序在运行的时候,仅仅凭借一段普通的文本字符串(比如 “java.lang.Runtime”),就能把内存里的某个类抓出来,哪怕方法是私有的(private),也能强行扒开调用

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ================= 真正能跑通的反射弹计算器代码 =================

// 第 1 步:直接拿带有真实名字的文本,去系统里把图纸找出来
Class<?> clazz = Class.forName("java.lang.Runtime");

// 第 2 步:拿着刚刚找到的图纸(clazz),从里面翻出名字叫 "exec" 的那个动作
Method method = clazz.getMethod("exec", String.class);

// 第 3 步:准备一个真正的活人实体。
// 因为 Runtime 是 Java 系统的“大内总管”,Java 不允许我们随便 new 它,
// 必须用 Runtime.getRuntime() 去请出这位正在值班的总管大人。
Runtime realObject = Runtime.getRuntime();

// 第 4 步:扣动扳机!
// 强令总管大人(realObject),去执行那个动作(method),原材料是 "calc"(计算器)
method.invoke(realObject, "calc");

Class<?> clazz = Class.forName(hackerClassName);

**<?>**意思是java的占位符,未知。【因为 Class.forName(hackerClassName) 是通过传来的字符串去动态找类的。在代码运行之前,Java 根本不知道具体是什么类型】

forName(…),根据字符串的名字,把类加载进内存里面

注意:看到要警惕,如果括号内的东西是黑客可控的,那他就可以加载任意危险的类。

getMethod(…):根据字符串名字,从刚刚的类里面找出具体的方法/动作。

  • ⚠️ 注意:getMethod() 只能拿到 public 方法。私有方法要用 getDeclaredMethod()。
  • 想调用私有方法,还得再加一句 method.setAccessible(true); —— 这才是前面说的”强行扒开”。这个动作在反射漏洞里非常关键。

.invoke(…):调用/激活,上面找了半天类和方法,到了 invoke 这里,代码才真正被执行

method.invoke(Runtime.getRuntime(), hackerCommand);

invoke(参数1:具体由哪个对象来干活?, 参数2:干活需要的原材料是什么?)

练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void testAPI(Map<String, String> userInput) {
try {
// 从用户传入的字典里取出类名和方法名
String className = userInput.get("clazz");
String methodName = userInput.get("method");

// 执行反射
Class<?> targetClass = Class.forName(className);
targetClass.getMethod(methodName).invoke(targetClass.newInstance());

} catch (Exception e) {
System.out.println("运行出错");
}
}

前面两步是获取具体的类模板名和方法名。

重点是这句的链式调用:==整行从左往右看,但遇到括号先算括号里面的==

targetClass.getMethod(methodName).invoke(targetClass.newInstance());

这句话的理解顺序:从左边开始,`targetClass类模板,调用getMethod()获取methodName这个方法,然后调用invoke执行这个方法,但在invoke这里的时候,先执行括号内的targetClass.newInstance()造出一个用来执行前面methodName的实体。类模板里的方法,需要对应的类里的实体才能执行。

这里有一个需要注意的点:

.invoke(…) 的第一个参数永远永远都是干活的实体,有两个参数的情况是,第一个参数为实体,第二个实体为干活需要的原材

【如果执行的是 static 静态方法,连工人都不需要,第一个参数直接填 null,这在高级漏洞利用里会经常出现】