偶尔走失,从未离开

昨天和队友一块打了2017 X-NUCA,遇到一道关于变量覆盖的代码审计题,记录下来。

2017 X-NUCA 某代码审计题

文章目录:

0x01 黑盒分析

通过git泄露得到源码,开始分析

在分析之前,先以黑盒的形式观察网站的主要功能点,这样后面的审计更有针对性。

z.png

这是一个博客,可以查看文章,注册,登录,注册登录之后可以查看个人信息

首先是文章功能,阅读post.php,index.php得知,其实都是写死的静态页面,不存在有漏洞的可能性,只剩下注册和登录。

0x02 白盒审计

首先查看全局文件,common.php

<?php
define("DB_NAME", "www");
define("DB_HOST", "localhost");
define("DB_USER", "xxx");
define("DB_PASSWD", "xxx");
define("DSN", "mysql:host=".DB_HOST.";dbname=".DB_NAME);
ini_set("display_errors", "On");
error_reporting(0);
foreach (array('_COOKIE','_POST','_GET') as $_request)  
{
    foreach ($$_request as $_key=>$_value)  
    {
        $$_key=  $_value;
    }
}
session_start();
?>

它将我们传入的变量进行了处理,这里存在很经典的变量覆盖漏洞。

在do_changepass.php中发现了flag有关的处理

<?php
include_once("common.php");
if(!isset($_SESSION["userinfo"])) {
    header("Location: login.php");
    die();
}
$userinfo = $_SESSION["userinfo"];
if($old_pass = $userinfo['password']) {
    if($userinfo["id"] == 1) {
        echo "flag{xxx}";
        die();
    }
    $dbh = new PDO(DSN, DB_USER, DB_PASSWD);
    $sql = "update user set password = :password where id=:id";
    $sth = $dbh->prepare($sql);
    $res = $sth->execute((array(':password'=>$new_pass, ':id'=>$userinfo['id'])));
    if($res === false) {
        header("Location: error.php?msg=changepass%20error!");
        die();
    }
    $userinfo["password"] = $new_pass;
    $_SESSION['userinfo'] = $userinfo;
    header("Location: index.php");
} else {
    header("Location: error.php?msg=invalid%20old%20pass!");
    die();
}
?>

userinfo["id"]等于1时返回flag,而userinfo来自_SESSION["userinfo"]

我们向前溯回,寻找_SESSION["userinfo"]的来源

do_login.php

<?php
include_once("common.php");
$dbh = new PDO(DSN, DB_USER, DB_PASSWD);
$sql = "select * from user where username = :username and password = :password";
$sth = $dbh->prepare($sql);
$sth->execute(array(':username'=>$username, ':password'=>$password));
$res = $sth->fetch(PDO::FETCH_ASSOC);
if($res === false) {
    header("Location: error.php?msg=invalid%20password!");
    die();
}
$userinfo["id"] = $res["id"];
$userinfo["username"] = $username;
$userinfo["password"] = $password;
$userinfo["role"] = $res["role"];
$_SESSION["userinfo"] = $userinfo;
header("Location: index.php");
?>

首先包含了全局文件,接着用PDO预处理绑定了参数,就不再存在sql注入了。

根据提交的username,password查询出了id,role,并赋值给了userinfo数组

整理下,可以清晰地看到变量id传递的过程,它之所以这样传递是为了让变量可以在不同页面使用。

$res["id"] --> $userinfo["id"] --> $_SESSION["userinfo"]["id"] -->$userinfo["id"]

当验证最终的$userinfo["id"]值为1时,则打印出flag

前面我们说到,所有的文件都包含了common.php,也就是都存在变量覆盖漏洞,我们可以在任意页面覆盖变量。

前两个传递中,即在login.php页面的传递过程中,我们是无法成功覆盖变量id的,因为后面对其进行了初始化,会导致我们的覆盖失效。而在do_changepass.php中,直接使用了$_SESSION["userinfo"],没有进行初始化,所以我们可以从这里入手。

在继续分析之前,我们需要澄清一些概念。

0x03 php的一些特性

当我们将一个字符串赋值给一个数组时,会发生强制转换,目标数组原有值会丢失,数组第一个元素的值为字符串的值,其它元素均为空。

<?php
$v = "cat";
$arr = array("rat", "love");
$arr = $v;
echo $arr[0];  //得到cat,而不是rat
echo $arr[1];  //输出空,而不是love
echo $arr[2];  //输出空,而不是cat
?>

而将一个字符串当做数组使用时,即将不同的值赋值给字符串的不同"元素",其字符串的性质不会发生变化,如:

<?php
$v = "cat";
$arr = array("rat", "love");
$v["a"] = "123";
$v["b"] = "456";
echo $v // 输出 4at
?>

根据输出结果我们可以猜测,字符串是被以字符串数组的形式来处理的,将"a"与$v中的键名(字符串数组中键名都是数字)比较,发生了强制转换,被转为0,然后将"123"赋值给字符串的第一个字符,但是只能赋值进去一个字符"1",赋值过程如下:

cat --> 1at --> 4at

理清了这些概念,我们回到审计。

0x04 继续审计

根据前面的思路,由于userinfo没有进行初始化,我们可以将其赋为任意字符串,让其变成字符串类型,然后在

do_login.php中

$userinfo["id"] = $res["id"];
$userinfo["username"] = $username;
$userinfo["password"] = $password;
$userinfo["role"] = $res["role"];

这几个赋值,相当于是对userinfo["0"]进行赋值操作,且只能赋值一个字符,最终起到效果的是最后一个赋值,即res["role"]的值,

在do_changepass.php中,使用userinfo["id"]进行比较时,事实上被强制转换为$userinfo["0"]与1比较,即与我们的res["role"]的值相比较,阅读源码可知,该数值默认为1,符合打印出flag的要求。

至此,我们已经有了拿到flag的完整思路

说点题外话,事实上,在user.php中,也有flag相关的处理。

<?php
include_once("header.php");
if(!isset($_SESSION['userinfo'])) {
    header("Location: login.php");
    die();
}
$userinfo = $_SESSION["userinfo"];
$id = isset($id) ? $id : $userinfo['id'];

?>
<div class="container">
<?php
$dbh = new PDO(DSN, DB_USER, DB_PASSWD);
$sql = "select * from user where id = :id";
$sth = $dbh->prepare($sql);
$sth->execute(array(':id'=>$id));
$res = $sth->fetch(PDO::FETCH_ASSOC);
if($res === false) {
    echo "<h3>wooops, 404 found!</h3>";
} else {
    echo "<h3>" . $res['username'] . ", role: " . ($res['role'] ? "user" : "admin") . "</h3>";
}
if($userinfo["username"] === 'admin') {
    echo "<h3>flag{xxxxxx}</flag>";
}
?>
</div>
<?php include_once("footer.php"); ?>

当userinfo["username"]的值为admin时,打印出flag,不过我们前面的思路只能让其值等于某一个字符,并不能让其等于"admin",所以实际上这条路是走不通的。

0x05 拿flag

我们注册一个任意用户,pig pig

然后访问do_login.php,POST数据

username=pig&password=pig&userinfo=xxx

id值已经为1,我们已经被判断为管理员了,接着访问changepass.php,随便修改下密码

15038237621074.jpg

返回了flag

事实上,不需要修改密码,直接访问do_changepass.php即可返回flag,原因如下:

do_changepass.php 第8行

if($old_pass = $userinfo['password']) {
    if($userinfo["id"] == 1) {
        echo "flag{xxx}";
        die();
    }

作者误写为赋值语句,而不是判断语句,所以这个if是永真的。

这个题其实还有一种覆盖的方法

0x06 拿flag法2

在do_register.php中,第28行

$userinfo["id"] = $res["id"];
$userinfo["username"] = $username;
$userinfo["password"] = $password;
$_SESSION["userinfo"] = $userinfo;
$userinfo["role"] = $res["role"];
header("Location: index.php");

它这个语句中,对_SESSION语句的赋值没有写在最后,也就是说最终生效的不是res["role"]的值,而是password的值,

我们注册一个用户,用户名任意,密码为1,注册时向do_register.php POST数据

username=xxx&password=1&userinfo=xxx

然后直接访问do_changepass.php即可得到flag

 标签: 代码审计, 变量覆盖

作者  :  watcher


添加新评论