本文备份于我的博客 大家好,我是Web安全板块的新版主crownless。最近我将以CTF赛题讲解的形式介绍一系列的Web基础知识,讲解的顺序将是循序渐进,因此不需要读者有任何基础知识。希望能吸引更多人关注看雪的Web安全板块,为板块增加人气和活力。 如果你还没有阅读第一篇教程 、第二篇教程 、第三篇教程 和第四篇教程 ,建议你先阅读之后再阅读本文。 在这篇文章中,你将学到:
让我们首先打开赛题链接:http://111.231.88.117/babyphp/ 你会看到如下php代码:
源码量还是不小的,让我们耐心地分析。 首先,程序执行了require_once('flag.php');
。require_once 函数的定义是“require_once语句和require语句完全相同,唯一区别是PHP会检查该文件是否已经被包含过,如果是则不会再次包含。”。require 语句的作用是“require和include几乎完全一样,除了处理失败的方式不同之外。require在出错时产生E_COMPILE_ERROR级别的错误。换句话说将导致脚本中止而include只产生警告(E_WARNING),脚本会继续运行。”。include 语句“包含并运行指定文件”。 那么什么是“包含并运行”呢?你可以理解为把要“包含”的文件原封不动地替换到require_once
、require
、include
函数所在的位置,并执行其中的php语句 。以require_once('flag.php');
为例,flag.php
这个文件中可能存在$flag='xxx';
,也就是我们想夺取的flag的内容。require_once
函数的作用是在php后端执行到require_once
时立即将flag.php
的内容替换require_once('flag.php');
,并在我们当前审计的php后端中执行flag.php
中的所有内容。在php开发中,适当使用require_once
、require
、include
可以提高代码复用程度,减少代码冗余。而在Web类型的CTF比赛中,将flag单独写在flag.php
中并通过require_once
、require
、include
包含进来,可以避免在主程序中暴露flag的内容。 接着,程序执行了error_reporting(0);
。error_reporting 可以“设置应该报告何种PHP错误”。而error_reporting(0);
可以关闭所有PHP错误报告。这句话并不妨碍我们做题。 接着运行了如下代码段:
highlight_file 可以“语法高亮一个文件”,也就是将参数所指的文件语法高亮后发送到前端。这里用到的的参数是__FILE__
,这是一个魔术常量 ,代表了本文件的完整路径和文件名。我们以前已经学过isset函数了。所以我们要做的是在URL中传入msg
参数,这样才会不执行到die();
。 继续往下看,运行了如下代码段:
如果你阅读过第四篇教程 ,应该知道需要给msg
参数传入data://text/plain,Hello Challenge!
,才能避免执行到die('Wow so rude!!!!1');
。但是当你在地址栏输入http://111.231.88.117/babyphp/?msg=data://text/plain,Hello Challenge!
并按回车时,地址栏中的地址居然变成了http://111.231.88.117/babyphp/?msg=data://text/plain,Hello%20Challenge!
,也就是Hello和Challenge之间的空格变成了%20
,这是为什么呢?其实,这是浏览器把空格符号进行了URL编码(URL Encode)。如果不这么做,将违反HTTP协议,导致后端返回400 Bad Request错误,如下图所示: 使用URL编码还有一个好处。以第三篇教程 为例,如果你想给a
参数传入123&456=
,而不进行URL编码的话,后端接收到的HTTP请求将是
就会理解为传入了两个参数:a
和456
,这明显与我们原来的意图不符。所以,浏览器会自动对发送的表单的数据进行URL编码。 好了,现在我们访问http://111.231.88.117/babyphp/?msg=data://text/plain,Hello%20Challenge! ,将会显示“Hello Hacker! Have a look around. lol no”,意味着echo "Hello Hacker! Have a look around.\n";
已经成功执行。不过不要急,这里我还要教你一种让file_get_contents
函数返回"Hello Challenge!"的方法,那就是使用data://text/plain;base64,
。base64是另一种比较常用的编码,这个网站 可以转换明文和base64编码。我们可以将Hello Challenge!
输入进去,将会转化成SGVsbG8gQ2hhbGxlbmdlIQ==
,我们接着将其与http://111.231.88.117/babyphp/?msg=data://text/plain;base64,
拼接,得到http://111.231.88.117/babyphp/?msg=data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ== 。当后端执行到file_get_contents("data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==")时,将会首先从data://text/plain;base64,
判定这是base64编码过的数据,然后就会把SGVsbG8gQ2hhbGxlbmdlIQ==
解码为Hello Challenge!
,从而返回Hello Challenge!
。 让我们继续往下看:
程序接着将key1
参数赋值给k1
,将key2
参数赋值给k2
。接着设置cc
为1337,bb
为42。接下来,if语句中有die("lol no\n");
,所以我们的目标是让if返回FALSE
。而根据||
的运算规则,必须让intval($k1) !== $cc
和$k1 === $cc
都返回FALSE
才行。intval 可以获取变量k1
的整数值。我们的目标是让!==
这个比较返回FALSE
,那么就要做到intval($k1) === $cc
,而cc
为1337,那么我们只要传入key1=1337即可。由于||
运算符的特性,当intval($k1) !== $cc
返回FALSE
时还会执行到$k1 === $cc
。===
被称为全等运算符,效果是当$k1
等于$cc
并且它们的类型也相同时,才会返回TRUE。那么,因为$k1
是字符串类型,而$cc
是整数类型,所以$k1 === $cc
会返回FALSE
,正是我们想要的。所以,我们当前的payload是:
http://111.231.88.117/babyphp/?msg=data://text/plain;base64,SGVsbG8gQ2hhbGxlbmdlIQ==&key1=1337
访问以上网址,显示“Hello Hacker! Have a look around. lel no”,注意这里显示的是“lel no”而不是“lol no”,说明我们顺利通过了这一关。
继续往下看:
strlen 可以计算字符串的长度。从这段代码中我们得知k2
的长度必须为42,且is_numeric($k2)
必须返回FALSE
,preg_match('/^\d+$/', $k2)
必须返回TRUE
,$k2 == $cc
必须返回TRUE
,同时满足这几个条件,程序才会执行$cc = $_GET['cc'];
。 很显然,我们必须控制cc
变量,所以我们要尝试满足以上提到的所有条件。preg_match 函数可以“执行匹配正则表达式”。可能你没有学过正则表达式,不要紧,我们这里只学一小点。 首先,preg_match
的第一个参数是“要搜索的模式”。什么是“模式”呢?你可以理解为其“代表了一种规则”。模式需要由分隔符(delimiter)闭合包裹。那么这里的分隔符显然是/
,因为最开始和最后两个/
包裹了“模式”。preg_match
的第二个参数是“源字符串”。 那么什么是“匹配正则表达式”呢?意思就是“源字符串符合模式所指的规则”。 关于正则表达式,这篇文章 是很好的入门材料,有兴趣的读者可以参考。 回到我们的赛题。那么^\d+$
代表什么规则呢?首先,^
匹配源字符串的开始位置。\d
匹配一个数字字符。+
匹配前面的子表达式一次或多次。$
匹配输入字符串的结束位置。合在一起,规则可以表达为:源字符串的内容必须为一个或多个数字。 为了测试一个源字符串是否符合模式,我们可以访问这个网站 。在大输入框中输入源字符串,比如1111111111
,将^\d+$
复制到“正则表达式”输入框中,点击“测试匹配”,结果居然发现“没有匹配”。 出了什么问题呢?如果你足够细心,应该能发现$
符号和$
符号是不一样的!$
匹配输入字符串的结束位置,是一个“元字符”,而$
仅仅是一个普通的字符。所以我们理解错了规则。正确的规则是“源字符串必须以一个或多个数字作为开始,然后跟着$
符号”。让我们在大输入框中输入1111111111$
,就能成功找到一处匹配。 现在我们已经能让preg_match('/^\d+$/', $k2)
返回TRUE
,但必须还要满足其他条件。那么is_numeric($k2)
是否会返回FALSE
呢?答案是肯定的!is_numeric 检测变量是否为数字或数字字符串,因为$
符号不是数字,所以is_numeric($k2)
会返回FALSE
。 未完待续。
<?php
require_once('flag.php');
error_reporting(0);
if(!isset($_GET['msg'])){
highlight_file(__FILE__);
die();
}
$msg = $_GET['msg'];
if(file_get_contents($msg)!=="Hello Challenge!"){
die('Wow so rude!!!!1');
}
echo "Hello Hacker! Have a look around.\n";
$k1=$_GET['key1'];
$k2=$_GET['key2'];
$cc = 1337;$bb = 42;
if(intval($k1) !== $cc || $k1 === $cc){
die("lol no\n");
}
if(strlen($k2) == $bb){
if(preg_match('/^\d+$/', $k2) && !is_numeric($k2)){
if($k2 == $cc){
$cc = $_GET['cc'];
}
}
}
list($k1,$k2) = [$k2, $k1];
if(substr($cc, $bb) === sha1($cc)){
foreach ($_GET as $lel => $hack){
$$lel = $hack;
}
}
$b="2";$a="b";
if($$a !== $k1){
die("lel no\n");
}
// plz die now
assert_options(ASSERT_ACTIVE, 1);
assert_options(ASSERT_BAIL, 1);
eval("$bb=$cc");
assert("$bb=$cc");
echo "Good Job ;)\n";
//TODO
//echo $flag;
?>
if(!isset($_GET['msg'])){
highlight_file(__FILE__);
die();
}
$msg = $_GET['msg'];
if(file_get_contents($msg)!=="Hello Challenge!"){
die('Wow so rude!!!!1');
}
echo "Hello Hacker! Have a look around.\n";
GET /dmsj/level0/?a=123&456= HTTP/1.1
$k1=$_GET['key1'];
$k2=$_GET['key2'];
$cc = 1337;$bb = 42;
if(intval($k1) !== $cc || $k1 === $cc){
die("lol no\n");
}
if(strlen($k2) == $bb){
if(preg_match('/^\d+$/', $k2) && !is_numeric($k2)){
if($k2 == $cc){
$cc = $_GET['cc'];
}
}
}
php require_once函数
php require函数
php include函数
php error_reporting函数
php highlight_file函数
URL编码
php file_get_contents函数进阶——data://text/plain;base64,
php intval函数
php ===
(全等)运算符
php ||
(逻辑或)运算符
php strlen函数
php preg_match函数
正则表达式入门
php is_numeric函数
php list函数
php substr函数
php sha1函数
php foreach语法结构
php可变变量
php assert_options函数
php eval函数
php assert函数
php require_once函数
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-2-1 16:50
被crownless编辑
,原因: